001    // Copyright 2010, 2011 The Apache Software Foundation
002    //
003    // Licensed under the Apache License, Version 2.0 (the "License");
004    // you may not use this file except in compliance with the License.
005    // You may obtain a copy of the License at
006    //
007    // http://www.apache.org/licenses/LICENSE-2.0
008    //
009    // Unless required by applicable law or agreed to in writing, software
010    // distributed under the License is distributed on an "AS IS" BASIS,
011    // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
012    // See the License for the specific language governing permissions and
013    // limitations under the License.
014    
015    package org.apache.tapestry5.internal.services.ajax;
016    
017    import java.util.List;
018    import java.util.Map;
019    import java.util.Set;
020    
021    import org.apache.tapestry5.Asset;
022    import org.apache.tapestry5.ComponentResources;
023    import org.apache.tapestry5.FieldFocusPriority;
024    import org.apache.tapestry5.func.F;
025    import org.apache.tapestry5.func.Worker;
026    import org.apache.tapestry5.internal.InternalConstants;
027    import org.apache.tapestry5.internal.services.DocumentLinker;
028    import org.apache.tapestry5.internal.services.javascript.JavaScriptStackPathConstructor;
029    import org.apache.tapestry5.ioc.internal.util.CollectionFactory;
030    import org.apache.tapestry5.ioc.internal.util.InternalUtils;
031    import org.apache.tapestry5.ioc.util.IdAllocator;
032    import org.apache.tapestry5.json.JSONArray;
033    import org.apache.tapestry5.json.JSONObject;
034    import org.apache.tapestry5.services.javascript.InitializationPriority;
035    import org.apache.tapestry5.services.javascript.JavaScriptStack;
036    import org.apache.tapestry5.services.javascript.JavaScriptStackSource;
037    import org.apache.tapestry5.services.javascript.JavaScriptSupport;
038    import org.apache.tapestry5.services.javascript.StylesheetLink;
039    
040    public class JavaScriptSupportImpl implements JavaScriptSupport
041    {
042        private final IdAllocator idAllocator;
043    
044        private final DocumentLinker linker;
045    
046        // Using a Map as a case-insensitive set of stack names.
047    
048        private final Map<String, Boolean> addedStacks = CollectionFactory.newCaseInsensitiveMap();
049    
050        private final List<String> stackLibraries = CollectionFactory.newList();
051    
052        private final List<String> otherLibraries = CollectionFactory.newList();
053    
054        private final Set<String> importedStylesheetURLs = CollectionFactory.newSet();
055    
056        private final List<StylesheetLink> stylesheetLinks = CollectionFactory.newList();
057    
058        private final Map<InitializationPriority, JSONObject> inits = CollectionFactory.newMap();
059    
060        private final JavaScriptStackSource javascriptStackSource;
061    
062        private final JavaScriptStackPathConstructor stackPathConstructor;
063    
064        private final boolean partialMode;
065    
066        private FieldFocusPriority focusPriority;
067    
068        private String focusFieldId;
069    
070        public JavaScriptSupportImpl(DocumentLinker linker, JavaScriptStackSource javascriptStackSource,
071                JavaScriptStackPathConstructor stackPathConstructor)
072        {
073            this(linker, javascriptStackSource, stackPathConstructor, new IdAllocator(), false);
074        }
075    
076        /**
077         * @param linker
078         *            responsible for assembling all the information gathered by JavaScriptSupport and
079         *            attaching it to the Document (for a full page render) or to the JSON response (in a partial render)
080         * @param javascriptStackSource
081         *            source of information about {@link JavaScriptStack}s, used when handling the import
082         *            of libraries and stacks (often, to handle transitive dependencies)
083         * @param stackPathConstructor
084         *            encapsulates the knowledge of how to represent a stack (which may be converted
085         *            from a series of JavaScript libraries into a single virtual JavaScript library)
086         * @param idAllocator
087         *            used when allocating unique ids (it is usually pre-initialized in an Ajax request to ensure
088         *            that newly allocated ids do not conflict with previous renders and partial updates)
089         * @param partialMode
090         *            if true, then the JSS configures itself for a partial page render (part of an Ajax request)
091         *            which automatically assumes the "core" library has been added (to the original page render)
092         *            and makes other minor changes to behavior.
093         */
094        public JavaScriptSupportImpl(DocumentLinker linker, JavaScriptStackSource javascriptStackSource,
095                JavaScriptStackPathConstructor stackPathConstructor, IdAllocator idAllocator, boolean partialMode)
096        {
097            this.linker = linker;
098            this.idAllocator = idAllocator;
099            this.javascriptStackSource = javascriptStackSource;
100            this.stackPathConstructor = stackPathConstructor;
101            this.partialMode = partialMode;
102    
103            // In partial mode, assume that the infrastructure stack is already present
104            // (from the original page render).
105    
106            if (partialMode)
107                addedStacks.put(InternalConstants.CORE_STACK_NAME, true);
108        }
109    
110        public void commit()
111        {
112            if (focusFieldId != null)
113                addInitializerCall("activate", focusFieldId);
114    
115            F.flow(stylesheetLinks).each(new Worker<StylesheetLink>()
116            {
117                public void work(StylesheetLink value)
118                {
119                    linker.addStylesheetLink(value);
120                }
121            });
122    
123            Worker<String> linkLibrary = new Worker<String>()
124            {
125                public void work(String value)
126                {
127                    linker.addScriptLink(value);
128                }
129            };
130    
131            F.flow(stackLibraries).each(linkLibrary);
132            F.flow(otherLibraries).each(linkLibrary);
133    
134            for (InitializationPriority p : InitializationPriority.values())
135            {
136                JSONObject init = inits.get(p);
137    
138                if (init != null)
139                    linker.setInitialization(p, init);
140            }
141        }
142    
143        public void addInitializerCall(InitializationPriority priority, String functionName, JSONObject parameter)
144        {
145            storeInitializerCall(priority, functionName, parameter);
146        }
147    
148        public void addInitializerCall(String functionName, JSONArray parameter)
149        {
150            storeInitializerCall(InitializationPriority.NORMAL, functionName, parameter);
151        }
152    
153        public void addInitializerCall(InitializationPriority priority, String functionName, JSONArray parameter)
154        {
155            storeInitializerCall(priority, functionName, parameter);
156        }
157    
158        private void storeInitializerCall(InitializationPriority priority, String functionName, Object parameter)
159        {
160            assert priority != null;
161            assert parameter != null;
162            assert InternalUtils.isNonBlank(functionName);
163            addCoreStackIfNeeded();
164    
165            JSONObject init = inits.get(priority);
166    
167            if (init == null)
168            {
169                init = new JSONObject();
170                inits.put(priority, init);
171            }
172    
173            JSONArray invocations = init.has(functionName) ? init.getJSONArray(functionName) : null;
174    
175            if (invocations == null)
176            {
177                invocations = new JSONArray();
178                init.put(functionName, invocations);
179            }
180    
181            invocations.put(parameter);
182        }
183    
184        public void addInitializerCall(String functionName, JSONObject parameter)
185        {
186            addInitializerCall(InitializationPriority.NORMAL, functionName, parameter);
187        }
188    
189        public void addInitializerCall(InitializationPriority priority, String functionName, String parameter)
190        {
191            storeInitializerCall(priority, functionName, parameter);
192        }
193    
194        public void addInitializerCall(String functionName, String parameter)
195        {
196            addInitializerCall(InitializationPriority.NORMAL, functionName, parameter);
197        }
198    
199        public void addScript(InitializationPriority priority, String format, Object... arguments)
200        {
201            assert priority != null;
202            assert InternalUtils.isNonBlank(format);
203    
204            addCoreStackIfNeeded();
205    
206            String newScript = arguments.length == 0 ? format : String.format(format, arguments);
207    
208            if (partialMode)
209            {
210                addInitializerCall(priority, "evalScript", newScript);
211            }
212            else
213            {
214                linker.addScript(priority, newScript);
215            }
216        }
217    
218        public void addScript(String format, Object... arguments)
219        {
220            addScript(InitializationPriority.NORMAL, format, arguments);
221        }
222    
223        public String allocateClientId(ComponentResources resources)
224        {
225            return allocateClientId(resources.getId());
226        }
227    
228        public String allocateClientId(String id)
229        {
230            return idAllocator.allocateId(id);
231        }
232    
233        public void importJavaScriptLibrary(Asset asset)
234        {
235            assert asset != null;
236    
237            importJavaScriptLibrary(asset.toClientURL());
238        }
239    
240        public void importJavaScriptLibrary(String libraryURL)
241        {
242            addCoreStackIfNeeded();
243    
244            String stackName = findStackForLibrary(libraryURL);
245    
246            if (stackName != null)
247            {
248                importStack(stackName);
249                return;
250            }
251    
252            if (otherLibraries.contains(libraryURL))
253                return;
254    
255            otherLibraries.add(libraryURL);
256        }
257    
258        private Map<String, String> libraryURLToStackName;
259    
260        /**
261         * Locates the name of the stack that includes the library URL. Returns the stack,
262         * or null if the library is free-standing.
263         */
264        private String findStackForLibrary(String libraryURL)
265        {
266            return getLibraryURLToStackName().get(libraryURL);
267        }
268    
269        private Map<String, String> getLibraryURLToStackName()
270        {
271            if (libraryURLToStackName == null)
272            {
273                libraryURLToStackName = CollectionFactory.newMap();
274    
275                for (String stackName : javascriptStackSource.getStackNames())
276                {
277                    for (Asset library : javascriptStackSource.getStack(stackName).getJavaScriptLibraries())
278                    {
279                        libraryURLToStackName.put(library.toClientURL(), stackName);
280                    }
281                }
282            }
283    
284            return libraryURLToStackName;
285        }
286    
287        private void addCoreStackIfNeeded()
288        {
289            addAssetsFromStack(InternalConstants.CORE_STACK_NAME);
290        }
291    
292        private void addAssetsFromStack(String stackName)
293        {
294            if (addedStacks.containsKey(stackName))
295                return;
296    
297            JavaScriptStack stack = javascriptStackSource.getStack(stackName);
298    
299            for (String dependentStackname : stack.getStacks())
300            {
301                addAssetsFromStack(dependentStackname);
302            }
303    
304            stackLibraries.addAll(stackPathConstructor.constructPathsForJavaScriptStack(stackName));
305    
306            stylesheetLinks.addAll(stack.getStylesheets());
307    
308            addedStacks.put(stackName, true);
309    
310            String initialization = stack.getInitialization();
311    
312            if (initialization != null)
313                addScript(InitializationPriority.IMMEDIATE, initialization);
314        }
315    
316        public void importStylesheet(Asset stylesheet)
317        {
318            assert stylesheet != null;
319            importStylesheet(new StylesheetLink(stylesheet));
320        }
321    
322        public void importStylesheet(StylesheetLink stylesheetLink)
323        {
324            assert stylesheetLink != null;
325            String stylesheetURL = stylesheetLink.getURL();
326    
327            if (importedStylesheetURLs.contains(stylesheetURL))
328                return;
329    
330            importedStylesheetURLs.add(stylesheetURL);
331    
332            stylesheetLinks.add(stylesheetLink);
333        }
334    
335        public void importStack(String stackName)
336        {
337            assert InternalUtils.isNonBlank(stackName);
338            addCoreStackIfNeeded();
339    
340            addAssetsFromStack(stackName);
341        }
342    
343        public void autofocus(FieldFocusPriority priority, String fieldId)
344        {
345            assert priority != null;
346            assert InternalUtils.isNonBlank(fieldId);
347    
348            if (focusFieldId == null || priority.compareTo(focusPriority) > 0)
349            {
350                this.focusPriority = priority;
351                focusFieldId = fieldId;
352            }
353        }
354    
355    }