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
015package org.apache.tapestry5.internal.services.ajax;
016
017import java.util.List;
018import java.util.Map;
019import java.util.Set;
020
021import org.apache.tapestry5.Asset;
022import org.apache.tapestry5.ComponentResources;
023import org.apache.tapestry5.FieldFocusPriority;
024import org.apache.tapestry5.func.F;
025import org.apache.tapestry5.func.Worker;
026import org.apache.tapestry5.internal.InternalConstants;
027import org.apache.tapestry5.internal.services.DocumentLinker;
028import org.apache.tapestry5.internal.services.javascript.JavaScriptStackPathConstructor;
029import org.apache.tapestry5.ioc.internal.util.CollectionFactory;
030import org.apache.tapestry5.ioc.internal.util.InternalUtils;
031import org.apache.tapestry5.ioc.util.IdAllocator;
032import org.apache.tapestry5.json.JSONArray;
033import org.apache.tapestry5.json.JSONObject;
034import org.apache.tapestry5.services.javascript.InitializationPriority;
035import org.apache.tapestry5.services.javascript.JavaScriptStack;
036import org.apache.tapestry5.services.javascript.JavaScriptStackSource;
037import org.apache.tapestry5.services.javascript.JavaScriptSupport;
038import org.apache.tapestry5.services.javascript.StylesheetLink;
039
040public 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}