    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.12.0.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}