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