001// Copyright 2010-2013 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
015package org.apache.tapestry5.internal.services.ajax;
016
017import org.apache.tapestry5.Asset;
018import org.apache.tapestry5.ComponentResources;
019import org.apache.tapestry5.FieldFocusPriority;
020import org.apache.tapestry5.func.F;
021import org.apache.tapestry5.func.Worker;
022import org.apache.tapestry5.internal.InternalConstants;
023import org.apache.tapestry5.internal.services.DocumentLinker;
024import org.apache.tapestry5.internal.services.javascript.JavaScriptStackPathConstructor;
025import org.apache.tapestry5.ioc.internal.util.CollectionFactory;
026import org.apache.tapestry5.ioc.internal.util.InternalUtils;
027import org.apache.tapestry5.ioc.util.IdAllocator;
028import org.apache.tapestry5.json.JSONArray;
029import org.apache.tapestry5.json.JSONObject;
030import org.apache.tapestry5.services.javascript.*;
031
032import java.util.ArrayList;
033import java.util.Comparator;
034import java.util.List;
035import java.util.Map;
036import java.util.Set;
037
038public class JavaScriptSupportImpl implements JavaScriptSupport
039{
040    private final IdAllocator idAllocator;
041
042    private final DocumentLinker linker;
043
044    // Using a Map as a case-insensitive set of stack names.
045
046    private final Map<String, Boolean> addedStacks = CollectionFactory.newCaseInsensitiveMap();
047
048    private final Set<String> otherLibraries = CollectionFactory.newSet();
049
050    private final Set<String> importedStylesheetURLs = CollectionFactory.newSet();
051
052    private final List<StylesheetLink> stylesheetLinks = CollectionFactory.newList();
053
054    private final List<InitializationImpl> inits = CollectionFactory.newList();
055
056    private final JavaScriptStackSource javascriptStackSource;
057
058    private final JavaScriptStackPathConstructor stackPathConstructor;
059
060    private final boolean partialMode;
061
062    private FieldFocusPriority focusPriority;
063
064    private String focusFieldId;
065
066    private Map<String, String> libraryURLToStackName, moduleNameToStackName;
067
068    class InitializationImpl implements Initialization
069    {
070        InitializationPriority priority = InitializationPriority.NORMAL;
071
072        final String moduleName;
073
074        String functionName;
075
076        JSONArray arguments;
077
078        InitializationImpl(String moduleName)
079        {
080            this.moduleName = moduleName;
081        }
082
083        public Initialization invoke(String functionName)
084        {
085            assert InternalUtils.isNonBlank(functionName);
086
087            this.functionName = functionName;
088
089            return this;
090        }
091
092        public Initialization priority(InitializationPriority priority)
093        {
094            assert priority != null;
095
096            this.priority = priority;
097
098            return this;
099        }
100
101        public void with(Object... arguments)
102        {
103            assert arguments != null;
104
105            this.arguments = new JSONArray(arguments);
106        }
107    }
108
109    public JavaScriptSupportImpl(DocumentLinker linker, JavaScriptStackSource javascriptStackSource,
110                                 JavaScriptStackPathConstructor stackPathConstructor)
111    {
112        this(linker, javascriptStackSource, stackPathConstructor, new IdAllocator(), false);
113    }
114
115    /**
116     * @param linker
117     *         responsible for assembling all the information gathered by JavaScriptSupport and
118     *         attaching it to the Document (for a full page render) or to the JSON response (in a partial render)
119     * @param javascriptStackSource
120     *         source of information about {@link JavaScriptStack}s, used when handling the import
121     *         of libraries and stacks (often, to handle transitive dependencies)
122     * @param stackPathConstructor
123     *         encapsulates the knowledge of how to represent a stack (which may be converted
124     *         from a series of JavaScript libraries into a single virtual JavaScript library)
125     * @param idAllocator
126     *         used when allocating unique ids (it is usually pre-initialized in an Ajax request to ensure
127     *         that newly allocated ids do not conflict with previous renders and partial updates)
128     * @param partialMode
129     *         if true, then the JSS configures itself for a partial page render (part of an Ajax request)
130     *         which automatically assumes the "core" library has been added (to the original page render)
131     *         and makes other minor changes to behavior.
132     */
133    public JavaScriptSupportImpl(DocumentLinker linker, JavaScriptStackSource javascriptStackSource,
134                                 JavaScriptStackPathConstructor stackPathConstructor, IdAllocator idAllocator, boolean partialMode)
135    {
136        this.linker = linker;
137        this.idAllocator = idAllocator;
138        this.javascriptStackSource = javascriptStackSource;
139        this.stackPathConstructor = stackPathConstructor;
140        this.partialMode = partialMode;
141
142        // In partial mode, assume that the infrastructure stack is already present
143        // (from the original page render).
144
145        if (partialMode)
146        {
147            addedStacks.put(InternalConstants.CORE_STACK_NAME, true);
148        }
149    }
150
151    public void commit()
152    {
153        if (focusFieldId != null)
154        {
155            require("t5/core/pageinit").invoke("focus").with(focusFieldId);
156        }
157
158        F.flow(stylesheetLinks).each(new Worker<StylesheetLink>()
159        {
160            public void work(StylesheetLink value)
161            {
162                linker.addStylesheetLink(value);
163            }
164        });
165
166        F.flow(inits).sort(new Comparator<InitializationImpl>()
167        {
168            public int compare(InitializationImpl o1, InitializationImpl o2)
169            {
170                return o1.priority.compareTo(o2.priority);
171            }
172        }).each(new Worker<InitializationImpl>()
173        {
174            public void work(InitializationImpl element)
175            {
176                linker.addInitialization(element.priority, element.moduleName, element.functionName, element.arguments);
177            }
178        });
179    }
180
181    public void addInitializerCall(InitializationPriority priority, String functionName, JSONObject parameter)
182    {
183        createInitializer(priority).with(functionName, parameter);
184    }
185
186    public void addInitializerCall(String functionName, JSONArray parameter)
187    {
188        addInitializerCall(InitializationPriority.NORMAL, functionName, parameter);
189    }
190
191    public void addInitializerCall(InitializationPriority priority, String functionName,
192            JSONArray parameter)
193    {
194        // TAP5-2300: In 5.3, a JSONArray implied an array of method arguments, so unwrap and add
195        // functionName to the arguments
196
197        List parameterList = new ArrayList(parameter.length() + 1);
198        parameterList.add(functionName);
199        parameterList.addAll(parameter.toList());
200        createInitializer(priority).with(parameterList.toArray());
201    }
202
203    private Initialization createInitializer(InitializationPriority priority)
204    {
205        assert priority != null;
206
207        importCoreStack();
208
209        return require("t5/core/init").priority(priority);
210    }
211
212    public void addInitializerCall(String functionName, JSONObject parameter)
213    {
214        addInitializerCall(InitializationPriority.NORMAL, functionName, parameter);
215    }
216
217    public void addInitializerCall(InitializationPriority priority, String functionName, String parameter)
218    {
219        createInitializer(priority).with(functionName, parameter);
220    }
221
222    public void addInitializerCall(String functionName, String parameter)
223    {
224        addInitializerCall(InitializationPriority.NORMAL, functionName, parameter);
225    }
226
227    public void addScript(InitializationPriority priority, String format, Object... arguments)
228    {
229        assert priority != null;
230        assert InternalUtils.isNonBlank(format);
231
232        importCoreStack();
233
234        String newScript = arguments.length == 0 ? format : String.format(format, arguments);
235
236        if (partialMode)
237        {
238            require("t5/core/pageinit").invoke("evalJavaScript").with(newScript);
239        } else
240        {
241            linker.addScript(priority, newScript);
242        }
243    }
244
245    public void addScript(String format, Object... arguments)
246    {
247        addScript(InitializationPriority.NORMAL, format, arguments);
248    }
249
250    public void addModuleConfigurationCallback(ModuleConfigurationCallback callback)
251    {
252        linker.addModuleConfigurationCallback(callback);
253    }
254
255    public String allocateClientId(ComponentResources resources)
256    {
257        return allocateClientId(resources.getId());
258    }
259
260    public String allocateClientId(String id)
261    {
262        return idAllocator.allocateId(id);
263    }
264
265    public JavaScriptSupport importJavaScriptLibrary(Asset asset)
266    {
267        assert asset != null;
268
269        return importJavaScriptLibrary(asset.toClientURL());
270    }
271
272    public JavaScriptSupport importJavaScriptLibrary(String libraryURL)
273    {
274        importCoreStack();
275
276        String stackName = findStackForLibrary(libraryURL);
277
278        if (stackName != null)
279        {
280            return importStack(stackName);
281        }
282
283        if (!otherLibraries.contains(libraryURL))
284        {
285            linker.addLibrary(libraryURL);
286
287            otherLibraries.add(libraryURL);
288        }
289
290        return this;
291    }
292
293    private void importCoreStack()
294    {
295        addAssetsFromStack(InternalConstants.CORE_STACK_NAME);
296    }
297
298    /**
299     * Locates the name of the stack that includes the library URL. Returns the stack,
300     * or null if the library is free-standing.
301     */
302    private String findStackForLibrary(String libraryURL)
303    {
304        return getLibraryURLToStackName().get(libraryURL);
305    }
306
307
308    private Map<String, String> getLibraryURLToStackName()
309    {
310        if (libraryURLToStackName == null)
311        {
312            libraryURLToStackName = CollectionFactory.newMap();
313
314            for (String stackName : javascriptStackSource.getStackNames())
315            {
316                for (Asset library : javascriptStackSource.getStack(stackName).getJavaScriptLibraries())
317                {
318                    libraryURLToStackName.put(library.toClientURL(), stackName);
319                }
320            }
321        }
322
323        return libraryURLToStackName;
324    }
325
326    private String findStackForModule(String moduleName)
327    {
328        return getModuleNameToStackName().get(moduleName);
329    }
330
331    private Map<String, String> getModuleNameToStackName()
332    {
333
334        if (moduleNameToStackName == null)
335        {
336            moduleNameToStackName = CollectionFactory.newMap();
337
338            for (String stackName : javascriptStackSource.getStackNames())
339            {
340                for (String moduleName : javascriptStackSource.getStack(stackName).getModules())
341                {
342                    moduleNameToStackName.put(moduleName, stackName);
343                }
344            }
345        }
346
347        return moduleNameToStackName;
348    }
349
350
351    private void addAssetsFromStack(String stackName)
352    {
353        if (addedStacks.containsKey(stackName))
354        {
355            return;
356        }
357
358        JavaScriptStack stack = javascriptStackSource.getStack(stackName);
359
360        for (String dependentStackname : stack.getStacks())
361        {
362            addAssetsFromStack(dependentStackname);
363        }
364
365        addedStacks.put(stackName, true);
366
367        boolean addAsCoreLibrary = stackName.equals(InternalConstants.CORE_STACK_NAME);
368
369        List<String> libraryURLs = stackPathConstructor.constructPathsForJavaScriptStack(stackName);
370
371        for (String libraryURL : libraryURLs)
372        {
373            if (addAsCoreLibrary)
374            {
375                linker.addCoreLibrary(libraryURL);
376            } else
377            {
378                linker.addLibrary(libraryURL);
379            }
380        }
381
382        stylesheetLinks.addAll(stack.getStylesheets());
383
384        String initialization = stack.getInitialization();
385
386        if (initialization != null)
387        {
388            addScript(InitializationPriority.IMMEDIATE, initialization);
389        }
390    }
391
392    public JavaScriptSupport importStylesheet(Asset stylesheet)
393    {
394        assert stylesheet != null;
395
396        return importStylesheet(new StylesheetLink(stylesheet));
397    }
398
399    public JavaScriptSupport importStylesheet(StylesheetLink stylesheetLink)
400    {
401        assert stylesheetLink != null;
402
403        importCoreStack();
404
405        String stylesheetURL = stylesheetLink.getURL();
406
407        if (!importedStylesheetURLs.contains(stylesheetURL))
408        {
409            importedStylesheetURLs.add(stylesheetURL);
410
411            stylesheetLinks.add(stylesheetLink);
412        }
413
414        return this;
415    }
416
417    public JavaScriptSupport importStack(String stackName)
418    {
419        assert InternalUtils.isNonBlank(stackName);
420
421        importCoreStack();
422
423        addAssetsFromStack(stackName);
424
425        return this;
426    }
427
428    public JavaScriptSupport autofocus(FieldFocusPriority priority, String fieldId)
429    {
430        assert priority != null;
431        assert InternalUtils.isNonBlank(fieldId);
432
433        if (focusFieldId == null || priority.compareTo(focusPriority) > 0)
434        {
435            this.focusPriority = priority;
436            focusFieldId = fieldId;
437        }
438
439        return this;
440    }
441
442    public Initialization require(String moduleName)
443    {
444        assert InternalUtils.isNonBlank(moduleName);
445
446        importCoreStack();
447
448        String stackName = findStackForModule(moduleName);
449
450        if (stackName != null)
451        {
452            importStack(stackName);
453        }
454
455        InitializationImpl init = new InitializationImpl(moduleName);
456
457        inits.add(init);
458
459        return init;
460    }
461
462}