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