001// Licensed under the Apache License, Version 2.0 (the "License");
002// you may not use this file except in compliance with the License.
003// You may obtain a copy of the License at
004//
005// http://www.apache.org/licenses/LICENSE-2.0
006//
007// Unless required by applicable law or agreed to in writing, software
008// distributed under the License is distributed on an "AS IS" BASIS,
009// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
010// See the License for the specific language governing permissions and
011// limitations under the License.
012
013package org.apache.tapestry5.internal.services.ajax;
014
015import org.apache.tapestry5.Asset;
016import org.apache.tapestry5.BooleanHook;
017import org.apache.tapestry5.ComponentResources;
018import org.apache.tapestry5.FieldFocusPriority;
019import org.apache.tapestry5.commons.util.CollectionFactory;
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.InternalUtils;
026import org.apache.tapestry5.ioc.util.IdAllocator;
027import org.apache.tapestry5.json.JSONArray;
028import org.apache.tapestry5.json.JSONObject;
029import org.apache.tapestry5.services.javascript.*;
030
031import java.util.*;
032
033public class JavaScriptSupportImpl implements JavaScriptSupport
034{
035    private final IdAllocator idAllocator;
036
037    private final DocumentLinker linker;
038
039    // Using a Map as a case-insensitive set of stack names.
040
041    private final Map<String, Boolean> addedStacks = CollectionFactory.newCaseInsensitiveMap();
042
043    private final Set<String> otherLibraries = CollectionFactory.newSet();
044
045    private final Set<String> importedStylesheetURLs = CollectionFactory.newSet();
046
047    private final List<StylesheetLink> stylesheetLinks = CollectionFactory.newList();
048
049    private final List<InitializationImpl> inits = CollectionFactory.newList();
050
051    private final JavaScriptStackSource javascriptStackSource;
052
053    private final JavaScriptStackPathConstructor stackPathConstructor;
054
055    private final boolean partialMode;
056
057    private final BooleanHook suppressCoreStylesheetsHook;
058
059    private FieldFocusPriority focusPriority;
060
061    private String focusFieldId;
062
063    private Map<String, String> libraryURLToStackName, moduleNameToStackName;
064
065    class InitializationImpl implements Initialization
066    {
067        InitializationPriority priority = InitializationPriority.NORMAL;
068
069        final String moduleName;
070
071        String functionName;
072
073        JSONArray arguments;
074
075        InitializationImpl(String moduleName)
076        {
077            this.moduleName = moduleName;
078        }
079
080        public Initialization invoke(String functionName)
081        {
082            assert InternalUtils.isNonBlank(functionName);
083
084            this.functionName = functionName;
085
086            return this;
087        }
088
089        public Initialization priority(InitializationPriority priority)
090        {
091            assert priority != null;
092
093            this.priority = priority;
094
095            return this;
096        }
097
098        public void with(Object... arguments)
099        {
100            assert arguments != null;
101
102            this.arguments = new JSONArray(arguments);
103        }
104    }
105
106    public JavaScriptSupportImpl(DocumentLinker linker, JavaScriptStackSource javascriptStackSource,
107                                 JavaScriptStackPathConstructor stackPathConstructor, BooleanHook suppressCoreStylesheetsHook)
108    {
109        this(linker, javascriptStackSource, stackPathConstructor, new IdAllocator(), false, suppressCoreStylesheetsHook);
110    }
111
112    /**
113     * @param linker
114     *         responsible for assembling all the information gathered by JavaScriptSupport and
115     *         attaching it to the Document (for a full page render) or to the JSON response (in a partial render)
116     * @param javascriptStackSource
117     *         source of information about {@link org.apache.tapestry5.services.javascript.JavaScriptStack}s, used when handling the import
118     *         of libraries and stacks (often, to handle transitive dependencies)
119     * @param stackPathConstructor
120     *         encapsulates the knowledge of how to represent a stack (which may be converted
121     *         from a series of JavaScript libraries into a single virtual JavaScript library)
122     * @param idAllocator
123     *         used when allocating unique ids (it is usually pre-initialized in an Ajax request to ensure
124     *         that newly allocated ids do not conflict with previous renders and partial updates)
125     * @param partialMode
126     *         if true, then the JSS configures itself for a partial page render (part of an Ajax request)
127     *         which automatically assumes the "core" library has been added (to the original page render)
128     * @param suppressCoreStylesheetsHook
129     *         a hook that enables ignoring CSS files on the core stack
130     */
131    public JavaScriptSupportImpl(DocumentLinker linker, JavaScriptStackSource javascriptStackSource,
132                                 JavaScriptStackPathConstructor stackPathConstructor, IdAllocator idAllocator, boolean partialMode,
133                                 BooleanHook suppressCoreStylesheetsHook)
134    {
135        this.linker = linker;
136        this.idAllocator = idAllocator;
137        this.javascriptStackSource = javascriptStackSource;
138        this.stackPathConstructor = stackPathConstructor;
139        this.partialMode = partialMode;
140        this.suppressCoreStylesheetsHook = suppressCoreStylesheetsHook;
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        if (!(addAsCoreLibrary && suppressCoreStylesheetsHook.checkHook()))
383        {
384            stylesheetLinks.addAll(stack.getStylesheets());
385        }
386
387        String initialization = stack.getInitialization();
388
389        if (initialization != null)
390        {
391            addScript(InitializationPriority.IMMEDIATE, initialization);
392        }
393    }
394
395    public JavaScriptSupport importStylesheet(Asset stylesheet)
396    {
397        assert stylesheet != null;
398
399        return importStylesheet(new StylesheetLink(stylesheet));
400    }
401
402    public JavaScriptSupport importStylesheet(StylesheetLink stylesheetLink)
403    {
404        assert stylesheetLink != null;
405
406        importCoreStack();
407
408        String stylesheetURL = stylesheetLink.getURL();
409
410        if (!importedStylesheetURLs.contains(stylesheetURL))
411        {
412            importedStylesheetURLs.add(stylesheetURL);
413
414            stylesheetLinks.add(stylesheetLink);
415        }
416
417        return this;
418    }
419
420    public JavaScriptSupport importStack(String stackName)
421    {
422        assert InternalUtils.isNonBlank(stackName);
423
424        importCoreStack();
425
426        addAssetsFromStack(stackName);
427
428        return this;
429    }
430
431    public JavaScriptSupport autofocus(FieldFocusPriority priority, String fieldId)
432    {
433        assert priority != null;
434        assert InternalUtils.isNonBlank(fieldId);
435
436        if (focusFieldId == null || priority.compareTo(focusPriority) > 0)
437        {
438            this.focusPriority = priority;
439            focusFieldId = fieldId;
440        }
441
442        return this;
443    }
444
445    public Initialization require(String moduleName)
446    {
447        assert InternalUtils.isNonBlank(moduleName);
448
449        importCoreStack();
450
451        String stackName = findStackForModule(moduleName);
452
453        if (stackName != null)
454        {
455            importStack(stackName);
456        }
457
458        InitializationImpl init = new InitializationImpl(moduleName);
459
460        inits.add(init);
461
462        return init;
463    }
464
465}