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.modules;
014
015import org.apache.tapestry5.BooleanHook;
016import org.apache.tapestry5.MarkupWriter;
017import org.apache.tapestry5.SymbolConstants;
018import org.apache.tapestry5.annotations.Path;
019import org.apache.tapestry5.internal.InternalConstants;
020import org.apache.tapestry5.internal.services.DocumentLinker;
021import org.apache.tapestry5.internal.services.ResourceStreamer;
022import org.apache.tapestry5.internal.services.ajax.JavaScriptSupportImpl;
023import org.apache.tapestry5.internal.services.assets.ResourceChangeTracker;
024import org.apache.tapestry5.internal.services.javascript.*;
025import org.apache.tapestry5.internal.util.MessageCatalogResource;
026import org.apache.tapestry5.ioc.*;
027import org.apache.tapestry5.ioc.annotations.Contribute;
028import org.apache.tapestry5.ioc.annotations.Primary;
029import org.apache.tapestry5.ioc.annotations.Symbol;
030import org.apache.tapestry5.ioc.services.FactoryDefaults;
031import org.apache.tapestry5.ioc.services.SymbolProvider;
032import org.apache.tapestry5.ioc.util.IdAllocator;
033import org.apache.tapestry5.json.JSONObject;
034import org.apache.tapestry5.services.*;
035import org.apache.tapestry5.services.compatibility.Compatibility;
036import org.apache.tapestry5.services.compatibility.Trait;
037import org.apache.tapestry5.services.javascript.*;
038import org.apache.tapestry5.services.messages.ComponentMessagesSource;
039
040import java.util.Locale;
041
042/**
043 * Defines the services related to JavaScript and {@link org.apache.tapestry5.services.javascript.JavaScriptStack}s.
044 *
045 * @since 5.4
046 */
047public class JavaScriptModule
048{
049    private final static String ROOT = "${tapestry.asset.root}";
050
051    private final Environment environment;
052
053    private final EnvironmentalShadowBuilder environmentalBuilder;
054
055    public JavaScriptModule(Environment environment, EnvironmentalShadowBuilder environmentalBuilder)
056    {
057        this.environment = environment;
058        this.environmentalBuilder = environmentalBuilder;
059    }
060
061    public static void bind(ServiceBinder binder)
062    {
063        binder.bind(ModuleManager.class, ModuleManagerImpl.class);
064        binder.bind(JavaScriptStackSource.class, JavaScriptStackSourceImpl.class);
065        binder.bind(JavaScriptStack.class, ExtensibleJavaScriptStack.class).withMarker(Core.class).withId("CoreJavaScriptStack");
066        binder.bind(JavaScriptStack.class, ExtensibleJavaScriptStack.class).withMarker(Internal.class).withId("InternalJavaScriptStack");
067    }
068
069    /**
070     * Contributes the "core" and "internal" {@link JavaScriptStack}s
071     *
072     * @since 5.2.0
073     */
074    @Contribute(JavaScriptStackSource.class)
075    public static void provideBuiltinJavaScriptStacks(MappedConfiguration<String, JavaScriptStack> configuration,
076                                                      @Core JavaScriptStack coreStack,
077                                                      @Internal JavaScriptStack internalStack)
078    {
079        configuration.add(InternalConstants.CORE_STACK_NAME, coreStack);
080        configuration.add("internal", internalStack);
081    }
082
083    // These are automatically bundles with the core JavaScript stack; some applications may want to add a few
084    // additional ones, such as t5/core/zone.
085    private static final String[] bundledModules = new String[]{
086            "alert", "ajax", "bootstrap", "console", "dom", "events", "exception-frame", "fields", "forms",
087            "pageinit", "messages", "utils", "validation"
088    };
089
090    /**
091     * The core JavaScriptStack has a number of entries:
092     * <dl>
093     * <dt>requirejs</dt> <dd>The RequireJS AMD JavaScript library</dd>
094     * <dt>scriptaculous.js, effects.js</dt> <dd>Optional JavaScript libraries in compatibility mode (see {@link Trait#SCRIPTACULOUS})</dd>
095     * <dt>t53-compatibility.js</dt> <dd>Optional JavaScript library (see {@link Trait#INITIALIZERS})</dd>
096     * <dt>underscore-library, underscore-module</dt>
097     * <dt>The Underscore JavaScript library, and the shim that allows underscore to be injected</dt>
098     * <dt>t5/core/init</dt> <dd>Optional module related to t53-compatibility.js</dd>
099     * <dt>jquery-library</dt> <dd>The jQuery library</dd>
100     * <dt>jquery-noconflict</dt> <dd>Switches jQuery to no-conflict mode (only present when the infrastructure is "prototype").</dd>
101     * <dt>jquery</dt> <dd>A module shim that allows jQuery to be injected (and also switches jQuery to no-conflict mode)</dd>
102     * <dt>bootstrap.css, tapestry.css, exception-frame.css, tapestry-console.css, tree.css</dt>
103     * <dd>CSS files</dd>
104     * <dt>t5/core/[...]</dt>
105     * <dd>Additional JavaScript modules</dd>
106     * <dt>jquery</dt>
107     * <dd>Added if the infrastructure provider is "jquery".</dd>
108     * </dl>
109     *
110     * User modules may replace or extend this list.
111     */
112    @Contribute(JavaScriptStack.class)
113    @Core
114    public static void setupCoreJavaScriptStack(OrderedConfiguration<StackExtension> configuration,
115                                                Compatibility compatibility,
116                                                @Symbol(SymbolConstants.JAVASCRIPT_INFRASTRUCTURE_PROVIDER)
117                                                String provider)
118    {
119        configuration.add("requirejs", StackExtension.library(ROOT + "/require.js"));
120        configuration.add("underscore-library", StackExtension.library(ROOT + "/underscore-1.8.3.js"));
121
122        if (provider.equals("prototype"))
123        {
124            final String SCRIPTY = "${tapestry.scriptaculous}";
125
126            add(configuration, StackExtensionType.LIBRARY, SCRIPTY + "/prototype.js");
127
128            if (compatibility.enabled(Trait.SCRIPTACULOUS))
129            {
130                add(configuration, StackExtensionType.LIBRARY,
131                        SCRIPTY + "/scriptaculous.js",
132                        SCRIPTY + "/effects.js");
133            }
134        }
135
136        if (compatibility.enabled(Trait.INITIALIZERS))
137        {
138            add(configuration, StackExtensionType.LIBRARY, ROOT + "/t53-compatibility.js");
139            configuration.add("t5/core/init", new StackExtension(StackExtensionType.MODULE, "t5/core/init"));
140        }
141
142        configuration.add("jquery-library", StackExtension.library(ROOT + "/jquery.js"));
143
144        if (provider.equals("prototype"))
145        {
146            configuration.add("jquery-noconflict", StackExtension.library(ROOT + "/jquery-noconflict.js"));
147        }
148
149        add(configuration, StackExtensionType.MODULE, "jquery");
150
151        addCoreStylesheets(configuration, "${" + SymbolConstants.BOOTSTRAP_ROOT + "}/css/bootstrap.css");
152
153        for (String name : bundledModules)
154        {
155            String full = "t5/core/" + name;
156            configuration.add(full, StackExtension.module(full));
157        }
158
159        configuration.add("underscore-module", StackExtension.module("underscore"));
160    }
161
162    @Contribute(JavaScriptStack.class)
163    @Internal
164    public static void setupInternalJavaScriptStack(OrderedConfiguration<StackExtension> configuration)
165    {
166
167        // For the internal stack, ignore the configuration and just use the Bootstrap CSS shipped with the
168        // framework. This is part of a hack to make internal pages (such as ExceptionReport and T5Dashboard)
169        // render correctly even when the Bootstrap CSS has been replaced by the application.
170
171        addCoreStylesheets(configuration, ROOT + "/bootstrap/css/bootstrap.css");
172    }
173
174    private static void addCoreStylesheets(OrderedConfiguration<StackExtension> configuration, String bootstrapPath)
175    {
176        add(configuration, StackExtensionType.STYLESHEET,
177                bootstrapPath,
178
179                ROOT + "/tapestry.css",
180
181                ROOT + "/exception-frame.css",
182
183                ROOT + "/tapestry-console.css",
184
185                ROOT + "/tree.css");
186    }
187
188    private static void add(OrderedConfiguration<StackExtension> configuration, StackExtensionType type, String... paths)
189    {
190        for (String path : paths)
191        {
192            int slashx = path.lastIndexOf('/');
193            String id = path.substring(slashx + 1);
194
195            configuration.add(id, new StackExtension(type, path));
196        }
197    }
198
199
200    /**
201     * Builds a proxy to the current {@link JavaScriptSupport} inside this thread's {@link org.apache.tapestry5.services.Environment}.
202     *
203     * @since 5.2.0
204     */
205    public JavaScriptSupport buildJavaScriptSupport()
206    {
207        return environmentalBuilder.build(JavaScriptSupport.class);
208    }
209
210    @Contribute(Dispatcher.class)
211    @Primary
212    public static void setupModuleDispatchers(OrderedConfiguration<Dispatcher> configuration,
213                                              ModuleManager moduleManager,
214                                              OperationTracker tracker,
215                                              ResourceStreamer resourceStreamer,
216                                              PathConstructor pathConstructor,
217                                              JavaScriptStackSource javaScriptStackSource,
218                                              JavaScriptStackPathConstructor javaScriptStackPathConstructor,
219                                              LocalizationSetter localizationSetter,
220                                              @Symbol(SymbolConstants.MODULE_PATH_PREFIX)
221                                              String modulePathPrefix,
222                                              @Symbol(SymbolConstants.ASSET_PATH_PREFIX)
223                                              String assetPathPrefix)
224    {
225        configuration.add("Modules",
226                new ModuleDispatcher(moduleManager, resourceStreamer, tracker, pathConstructor,
227                    javaScriptStackSource, javaScriptStackPathConstructor, localizationSetter, modulePathPrefix,
228                    assetPathPrefix, false),
229                "after:Asset", "before:ComponentEvent");
230
231        configuration.add("ComnpressedModules",
232                new ModuleDispatcher(moduleManager, resourceStreamer, tracker, pathConstructor,
233                    javaScriptStackSource, javaScriptStackPathConstructor, localizationSetter, modulePathPrefix,
234                    assetPathPrefix, true),
235                "after:Modules", "before:ComponentEvent");
236    }
237
238    /**
239     * Adds page render filters, each of which provides an {@link org.apache.tapestry5.annotations.Environmental}
240     * service. Filters
241     * often provide {@link org.apache.tapestry5.annotations.Environmental} services needed by
242     * components as they render.
243     * <dl>
244     * <dt>JavascriptSupport</dt>
245     * <dd>Provides {@link JavaScriptSupport}</dd>
246     * </dl>
247     */
248    @Contribute(MarkupRenderer.class)
249    public void exposeJavaScriptSupportForFullPageRenders(OrderedConfiguration<MarkupRendererFilter> configuration,
250                                                          final JavaScriptStackSource javascriptStackSource,
251                                                          final JavaScriptStackPathConstructor javascriptStackPathConstructor,
252                                                          final Request request)
253    {
254
255        final BooleanHook suppressCoreStylesheetsHook = createSuppressCoreStylesheetHook(request);
256
257        MarkupRendererFilter javaScriptSupport = new MarkupRendererFilter()
258        {
259            public void renderMarkup(MarkupWriter writer, MarkupRenderer renderer)
260            {
261                DocumentLinker linker = environment.peekRequired(DocumentLinker.class);
262
263                JavaScriptSupportImpl support = new JavaScriptSupportImpl(linker, javascriptStackSource,
264                        javascriptStackPathConstructor, suppressCoreStylesheetsHook);
265
266                environment.push(JavaScriptSupport.class, support);
267
268                renderer.renderMarkup(writer);
269
270                environment.pop(JavaScriptSupport.class);
271
272                support.commit();
273            }
274        };
275
276        configuration.add("JavaScriptSupport", javaScriptSupport, "after:DocumentLinker");
277    }
278
279    /**
280     * Contributes {@link PartialMarkupRendererFilter}s used when rendering a
281     * partial Ajax response.
282     * <dl>
283     * <dt>JavaScriptSupport
284     * <dd>Provides {@link JavaScriptSupport}</dd>
285     * </dl>
286     */
287    @Contribute(PartialMarkupRenderer.class)
288    public void exposeJavaScriptSupportForPartialPageRender(OrderedConfiguration<PartialMarkupRendererFilter> configuration,
289                                                            final JavaScriptStackSource javascriptStackSource,
290
291                                                            final JavaScriptStackPathConstructor javascriptStackPathConstructor,
292
293                                                            final Request request)
294    {
295        final BooleanHook suppressCoreStylesheetsHook = createSuppressCoreStylesheetHook(request);
296
297        PartialMarkupRendererFilter javascriptSupport = new PartialMarkupRendererFilter()
298        {
299            public void renderMarkup(MarkupWriter writer, JSONObject reply, PartialMarkupRenderer renderer)
300            {
301                IdAllocator idAllocator;
302
303                if (request.getParameter(InternalConstants.SUPPRESS_NAMESPACED_IDS) == null)
304                {
305                    String uid = Long.toHexString(System.nanoTime());
306
307                    String namespace = "_" + uid;
308
309                    idAllocator = new IdAllocator(namespace);
310                } else
311                {
312                    // When suppressed, work just like normal rendering.
313                    idAllocator = new IdAllocator();
314                }
315
316                DocumentLinker linker = environment.peekRequired(DocumentLinker.class);
317
318                JavaScriptSupportImpl support = new JavaScriptSupportImpl(linker, javascriptStackSource,
319                        javascriptStackPathConstructor, idAllocator, true, suppressCoreStylesheetsHook);
320
321                environment.push(JavaScriptSupport.class, support);
322
323                renderer.renderMarkup(writer, reply);
324
325                environment.pop(JavaScriptSupport.class);
326
327                support.commit();
328            }
329        };
330
331        configuration.add("JavaScriptSupport", javascriptSupport, "after:DocumentLinker");
332    }
333
334    private BooleanHook createSuppressCoreStylesheetHook(final Request request)
335    {
336        return new BooleanHook()
337        {
338            @Override
339            public boolean checkHook()
340            {
341                return request.getAttribute(InternalConstants.SUPPRESS_CORE_STYLESHEETS) != null;
342            }
343        };
344    }
345
346
347    @Contribute(ModuleManager.class)
348    public static void setupBaseModules(MappedConfiguration<String, Object> configuration,
349                                        @Path("${tapestry.asset.root}/underscore-shim.js")
350                                        Resource underscoreShim,
351
352                                        @Path("${tapestry.asset.root}/jquery-shim.js")
353                                        Resource jqueryShim,
354
355                                        @Path("${tapestry.asset.root}/typeahead.js")
356                                        Resource typeahead,
357
358                                        @Path("${tapestry.asset.root}/moment-2.10.6.js")
359                                        Resource moment,
360
361                                        @Path("${" + SymbolConstants.BOOTSTRAP_ROOT + "}/js/transition.js")
362                                        Resource transition)
363    {
364        // The underscore shim module allows Underscore to be injected
365        configuration.add("underscore", new JavaScriptModuleConfiguration(underscoreShim));
366        configuration.add("jquery", new JavaScriptModuleConfiguration(jqueryShim));
367
368        configuration.add("bootstrap/transition", new AMDWrapper(transition).require("jquery", "$").asJavaScriptModuleConfiguration());
369
370        for (String name : new String[]{"affix", "alert", "button", "carousel", "collapse", "dropdown", "modal",
371                "scrollspy", "tab", "tooltip"})
372        {
373            Resource lib = transition.forFile(name + ".js");
374
375            configuration.add("bootstrap/" + name, new AMDWrapper(lib).require("bootstrap/transition").asJavaScriptModuleConfiguration());
376        }
377
378        Resource popover = transition.forFile("popover.js");
379
380        configuration.add("bootstrap/popover", new AMDWrapper(popover).require("bootstrap/tooltip").asJavaScriptModuleConfiguration());
381
382        configuration.add("t5/core/typeahead", new JavaScriptModuleConfiguration(typeahead).dependsOn("jquery"));
383
384        configuration.add("moment", new JavaScriptModuleConfiguration(moment));
385
386    }
387
388    @Contribute(SymbolProvider.class)
389    @FactoryDefaults
390    public static void setupFactoryDefaults(MappedConfiguration<String, Object> configuration)
391    {
392        configuration.add(SymbolConstants.JAVASCRIPT_INFRASTRUCTURE_PROVIDER, "prototype");
393        configuration.add(SymbolConstants.MODULE_PATH_PREFIX, "modules");
394    }
395
396    @Contribute(ModuleManager.class)
397    public static void setupFoundationFramework(MappedConfiguration<String, Object> configuration,
398                                                @Symbol(SymbolConstants.JAVASCRIPT_INFRASTRUCTURE_PROVIDER)
399                                                String provider,
400                                                @Path("classpath:org/apache/tapestry5/t5-core-dom-prototype.js")
401                                                Resource domPrototype,
402                                                @Path("classpath:org/apache/tapestry5/t5-core-dom-jquery.js")
403                                                Resource domJQuery)
404    {
405        if (provider.equals("prototype"))
406        {
407            configuration.add("t5/core/dom", new JavaScriptModuleConfiguration(domPrototype));
408        }
409
410        if (provider.equals("jquery"))
411        {
412            configuration.add("t5/core/dom", new JavaScriptModuleConfiguration(domJQuery));
413        }
414
415        // If someone wants to support a different infrastructure, they should set the provider symbol to some other value
416        // and contribute their own version of the t5/core/dom module.
417    }
418
419    @Contribute(ModuleManager.class)
420    public static void setupApplicationCatalogModules(MappedConfiguration<String, Object> configuration,
421                                                      LocalizationSetter localizationSetter,
422                                                      ComponentMessagesSource messagesSource,
423                                                      ResourceChangeTracker resourceChangeTracker,
424                                                      @Symbol(SymbolConstants.COMPACT_JSON) boolean compactJSON)
425    {
426        for (Locale locale : localizationSetter.getSupportedLocales())
427        {
428            MessageCatalogResource resource = new MessageCatalogResource(locale, messagesSource, resourceChangeTracker, compactJSON);
429
430            configuration.add("t5/core/messages/" + locale.toString(), new JavaScriptModuleConfiguration(resource));
431        }
432    }
433
434    /**
435     * Contributes 'ConfigureHTMLElement', which writes the attributes into the HTML tag to describe locale, etc.
436     * Contributes 'AddBrowserCompatibilityStyles', which writes {@code <style/>} elements into the {@code <head/>}
437     * element that modifies the page loading mask to work on IE 8 and IE 9.
438     */
439    @Contribute(MarkupRenderer.class)
440    public static void prepareHTMLPageOnRender(OrderedConfiguration<MarkupRendererFilter> configuration)
441    {
442        configuration.addInstance("ConfigureHTMLElement", ConfigureHTMLElementFilter.class);
443        configuration.add("AddBrowserCompatibilityStyles", new AddBrowserCompatibilityStyles());
444    }
445
446}