    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.List;
    016import java.util.Map;
    017
    018import org.apache.tapestry5.SymbolConstants;
    019import org.apache.tapestry5.internal.AssetConstants;
    020import org.apache.tapestry5.internal.InternalConstants;
    021import org.apache.tapestry5.internal.services.AssetSourceImpl;
    022import org.apache.tapestry5.internal.services.ClasspathAssetAliasManagerImpl;
    023import org.apache.tapestry5.internal.services.ClasspathAssetFactory;
    024import org.apache.tapestry5.internal.services.ContextAssetFactory;
    025import org.apache.tapestry5.internal.services.ExternalUrlAssetFactory;
    026import org.apache.tapestry5.internal.services.IdentityAssetPathConverter;
    027import org.apache.tapestry5.internal.services.RequestConstants;
    028import org.apache.tapestry5.internal.services.ResourceStreamer;
    029import org.apache.tapestry5.internal.services.assets.AssetChecksumGeneratorImpl;
    030import org.apache.tapestry5.internal.services.assets.AssetPathConstructorImpl;
    031import org.apache.tapestry5.internal.services.assets.CSSURLRewriter;
    032import org.apache.tapestry5.internal.services.assets.ClasspathAssetRequestHandler;
    033import org.apache.tapestry5.internal.services.assets.CompressionAnalyzerImpl;
    034import org.apache.tapestry5.internal.services.assets.ContentTypeAnalyzerImpl;
    035import org.apache.tapestry5.internal.services.assets.ContextAssetRequestHandler;
    036import org.apache.tapestry5.internal.services.assets.JavaScriptStackAssembler;
    037import org.apache.tapestry5.internal.services.assets.JavaScriptStackAssemblerImpl;
    038import org.apache.tapestry5.internal.services.assets.JavaScriptStackMinimizeDisabler;
    039import org.apache.tapestry5.internal.services.assets.MasterResourceMinimizer;
    040import org.apache.tapestry5.internal.services.assets.ResourceChangeTracker;
    041import org.apache.tapestry5.internal.services.assets.ResourceChangeTrackerImpl;
    042import org.apache.tapestry5.internal.services.assets.SRSCachingInterceptor;
    043import org.apache.tapestry5.internal.services.assets.SRSCompressedCachingInterceptor;
    044import org.apache.tapestry5.internal.services.assets.SRSCompressingInterceptor;
    045import org.apache.tapestry5.internal.services.assets.SRSMinimizingInterceptor;
    046import org.apache.tapestry5.internal.services.assets.StackAssetRequestHandler;
    047import org.apache.tapestry5.internal.services.assets.StreamableResourceSourceImpl;
    048import org.apache.tapestry5.internal.services.assets.UTF8ForTextAssets;
    049import org.apache.tapestry5.internal.services.messages.ClientLocalizationMessageResource;
    050import org.apache.tapestry5.ioc.MappedConfiguration;
    051import org.apache.tapestry5.ioc.OperationTracker;
    052import org.apache.tapestry5.ioc.OrderedConfiguration;
    053import org.apache.tapestry5.ioc.Resource;
    054import org.apache.tapestry5.ioc.ServiceBinder;
    055import org.apache.tapestry5.ioc.annotations.Autobuild;
    056import org.apache.tapestry5.ioc.annotations.Contribute;
    057import org.apache.tapestry5.ioc.annotations.Decorate;
    058import org.apache.tapestry5.ioc.annotations.Marker;
    059import org.apache.tapestry5.ioc.annotations.Order;
    060import org.apache.tapestry5.ioc.annotations.Primary;
    061import org.apache.tapestry5.ioc.annotations.Symbol;
    062import org.apache.tapestry5.ioc.services.ChainBuilder;
    063import org.apache.tapestry5.ioc.services.FactoryDefaults;
    064import org.apache.tapestry5.ioc.services.SymbolProvider;
    065import org.apache.tapestry5.services.ApplicationGlobals;
    066import org.apache.tapestry5.services.AssetFactory;
    067import org.apache.tapestry5.services.AssetPathConverter;
    068import org.apache.tapestry5.services.AssetRequestDispatcher;
    069import org.apache.tapestry5.services.AssetSource;
    070import org.apache.tapestry5.services.ClasspathAssetAliasManager;
    071import org.apache.tapestry5.services.ClasspathAssetProtectionRule;
    072import org.apache.tapestry5.services.ClasspathProvider;
    073import org.apache.tapestry5.services.ComponentClassResolver;
    074import org.apache.tapestry5.services.ContextProvider;
    075import org.apache.tapestry5.services.Core;
    076import org.apache.tapestry5.services.Dispatcher;
    077import org.apache.tapestry5.services.Request;
    078import org.apache.tapestry5.services.ResponseCompressionAnalyzer;
    079import org.apache.tapestry5.services.assets.AssetChecksumGenerator;
    080import org.apache.tapestry5.services.assets.AssetPathConstructor;
    081import org.apache.tapestry5.services.assets.AssetRequestHandler;
    082import org.apache.tapestry5.services.assets.CompressionAnalyzer;
    083import org.apache.tapestry5.services.assets.ContentTypeAnalyzer;
    084import org.apache.tapestry5.services.assets.ResourceMinimizer;
    085import org.apache.tapestry5.services.assets.StreamableResourceSource;
    086import org.apache.tapestry5.services.javascript.JavaScriptStackSource;
    087import org.apache.tapestry5.services.messages.ComponentMessagesSource;
    088
    089/**
    090 * @since 5.3
    091 */
    092@Marker(Core.class)
    093public class AssetsModule
    094{
    095    public static void bind(ServiceBinder binder)
    096    {
    097        binder.bind(AssetFactory.class, ClasspathAssetFactory.class).withSimpleId();
    098        binder.bind(AssetPathConverter.class, IdentityAssetPathConverter.class);
    099        binder.bind(AssetPathConstructor.class, AssetPathConstructorImpl.class);
    100        binder.bind(ClasspathAssetAliasManager.class, ClasspathAssetAliasManagerImpl.class);
    101        binder.bind(AssetSource.class, AssetSourceImpl.class);
    102        binder.bind(StreamableResourceSource.class, StreamableResourceSourceImpl.class);
    103        binder.bind(CompressionAnalyzer.class, CompressionAnalyzerImpl.class);
    104        binder.bind(ContentTypeAnalyzer.class, ContentTypeAnalyzerImpl.class);
    105        binder.bind(ResourceChangeTracker.class, ResourceChangeTrackerImpl.class);
    106        binder.bind(ResourceMinimizer.class, MasterResourceMinimizer.class);
    107        binder.bind(AssetChecksumGenerator.class, AssetChecksumGeneratorImpl.class);
    108        binder.bind(JavaScriptStackAssembler.class, JavaScriptStackAssemblerImpl.class);
    109    }
    110
    111    @Contribute(AssetSource.class)
    112    public void configureStandardAssetFactories(MappedConfiguration<String, AssetFactory> configuration,
    113                                                @ContextProvider
    114                                                AssetFactory contextAssetFactory,
    115
    116                                                @ClasspathProvider
    117                                                AssetFactory classpathAssetFactory)
    118    {
    119        configuration.add(AssetConstants.CONTEXT, contextAssetFactory);
    120        configuration.add(AssetConstants.CLASSPATH, classpathAssetFactory);
    121        configuration.add(AssetConstants.HTTP, new ExternalUrlAssetFactory(AssetConstants.HTTP));
    122        configuration.add(AssetConstants.HTTPS, new ExternalUrlAssetFactory(AssetConstants.HTTPS));
    123        configuration.add(AssetConstants.FTP, new ExternalUrlAssetFactory(AssetConstants.FTP));
    124        configuration.add(AssetConstants.PROTOCOL_RELATIVE, new ExternalUrlAssetFactory(AssetConstants.PROTOCOL_RELATIVE));
    125    }
    126
    127
    128    @Contribute(SymbolProvider.class)
    129    @FactoryDefaults
    130    public static void setupSymbols(MappedConfiguration<String, Object> configuration)
    131    {
    132        // Minification may be enabled in production mode, but unless a minimizer is provided, nothing
    133        // will change.
    134        configuration.add(SymbolConstants.MINIFICATION_ENABLED, SymbolConstants.PRODUCTION_MODE_VALUE);
    135        configuration.add(SymbolConstants.GZIP_COMPRESSION_ENABLED, true);
    136        configuration.add(SymbolConstants.COMBINE_SCRIPTS, SymbolConstants.PRODUCTION_MODE_VALUE);
    137        configuration.add(SymbolConstants.ASSET_URL_FULL_QUALIFIED, false);
    138
    139        configuration.add(SymbolConstants.ASSET_PATH_PREFIX, "assets");
    140
    141        configuration.add(SymbolConstants.BOOTSTRAP_ROOT, "${tapestry.asset.root}/bootstrap");
    142        configuration.add(SymbolConstants.FONT_AWESOME_ROOT, "${tapestry.asset.root}/font_awesome");
    143
    144        configuration.add("tapestry.asset.root", "classpath:META-INF/assets/tapestry5");
    145        configuration.add(SymbolConstants.OMIT_EXPIRATION_CACHE_CONTROL_HEADER, "max-age=60,must-revalidate");
    146    }
    147
    148    // The use of decorators is to allow third-parties to get their own extensions
    149    // into the pipeline.
    150
    151    @Decorate(id = "GZipCompression", serviceInterface = StreamableResourceSource.class)
    152    public StreamableResourceSource enableCompression(StreamableResourceSource delegate,
    153                                                      @Symbol(SymbolConstants.GZIP_COMPRESSION_ENABLED)
    154                                                      boolean gzipEnabled, @Symbol(SymbolConstants.MIN_GZIP_SIZE)
    155                                                      int compressionCutoff,
    156                                                      AssetChecksumGenerator checksumGenerator)
    157    {
    158        return gzipEnabled
    159                ? new SRSCompressingInterceptor(delegate, compressionCutoff, checksumGenerator)
    160                : null;
    161    }
    162
    163    @Decorate(id = "CacheCompressed", serviceInterface = StreamableResourceSource.class)
    164    @Order("before:GZIpCompression")
    165    public StreamableResourceSource enableCompressedCaching(StreamableResourceSource delegate,
    166                                                            @Symbol(SymbolConstants.GZIP_COMPRESSION_ENABLED)
    167                                                            boolean gzipEnabled, ResourceChangeTracker tracker)
    168    {
    169        return gzipEnabled
    170                ? new SRSCompressedCachingInterceptor(delegate, tracker)
    171                : null;
    172    }
    173
    174    @Decorate(id = "Cache", serviceInterface = StreamableResourceSource.class)
    175    @Order("after:GZipCompression")
    176    public StreamableResourceSource enableUncompressedCaching(StreamableResourceSource delegate,
    177                                                              ResourceChangeTracker tracker)
    178    {
    179        return new SRSCachingInterceptor(delegate, tracker);
    180    }
    181
    182    // Goes after cache, to ensure that what we are caching is the minified version.
    183    @Decorate(id = "Minification", serviceInterface = StreamableResourceSource.class)
    184    @Order("after:Cache,TextUTF8")
    185    public StreamableResourceSource enableMinification(StreamableResourceSource delegate, ResourceMinimizer minimizer,
    186                                                       @Symbol(SymbolConstants.MINIFICATION_ENABLED)
    187                                                       boolean enabled)
    188    {
    189        return enabled
    190                ? new SRSMinimizingInterceptor(delegate, minimizer)
    191                : null;
    192    }
    193
    194    // Ordering this after minification means that the URL replacement happens first;
    195    // then the minification, then the uncompressed caching, then compression, then compressed
    196    // cache.
    197    @Decorate(id = "CSSURLRewrite", serviceInterface = StreamableResourceSource.class)
    198    @Order("after:Minification")
    199    public StreamableResourceSource enableCSSURLRewriting(StreamableResourceSource delegate,
    200                                                          OperationTracker tracker,
    201                                                          AssetSource assetSource,
    202                                                          AssetChecksumGenerator checksumGenerator,
    203                                                          @Symbol(SymbolConstants.STRICT_CSS_URL_REWRITING) boolean strictCssUrlRewriting)
    204    {
    205        return new CSSURLRewriter(delegate, tracker, assetSource, checksumGenerator, strictCssUrlRewriting);
    206    }
    207
    208    @Decorate(id = "DisableMinificationForStacks", serviceInterface = StreamableResourceSource.class)
    209    @Order("before:Minification")
    210    public StreamableResourceSource setupDisableMinificationByJavaScriptStack(StreamableResourceSource delegate,
    211                                                                              @Symbol(SymbolConstants.MINIFICATION_ENABLED)
    212                                                                              boolean enabled,
    213                                                                              JavaScriptStackSource javaScriptStackSource,
    214                                                                              Request request)
    215    {
    216        return enabled
    217                ? new JavaScriptStackMinimizeDisabler(delegate, javaScriptStackSource, request)
    218                : null;
    219    }
    220
    221    /**
    222     * Ensures that all "text/*" assets are given the UTF-8 charset.
    223     *
    224     * @since 5.4
    225     */
    226    @Decorate(id = "TextUTF8", serviceInterface = StreamableResourceSource.class)
    227    @Order("after:Cache")
    228    public StreamableResourceSource setupTextAssetsAsUTF8(StreamableResourceSource delegate)
    229    {
    230        return new UTF8ForTextAssets(delegate);
    231    }
    232
    233    /**
    234     * Adds content types:
    235     * <dl>
    236     * <dt>css</dt>
    237     * <dd>text/css</dd>
    238     * <dt>js</dt>
    239     * <dd>text/javascript</dd>
    240     * <dt>jpg, jpeg</dt>
    241     * <dd>image/jpeg</dd>
    242     * <dt>gif</dt>
    243     * <dd>image/gif</dd>
    244     * <dt>png</dt>
    245     * <dd>image/png</dd>
    246     * <dt>svg</dt>
    247     * <dd>image/svg+xml</dd>
    248     * <dt>swf</dt>
    249     * <dd>application/x-shockwave-flash</dd>
    250     * <dt>woff</dt>
    251     * <dd>application/font-woff</dd>
    252     * <dt>tff</dt> <dd>application/x-font-ttf</dd>
    253     * <dt>eot</dt> <dd>application/vnd.ms-fontobject</dd>
    254     * </dl>
    255     */
    256    @Contribute(ContentTypeAnalyzer.class)
    257    public void setupDefaultContentTypeMappings(MappedConfiguration<String, String> configuration)
    258    {
    259        configuration.add("css", "text/css");
    260        configuration.add("js", "text/javascript");
    261        configuration.add("gif", "image/gif");
    262        configuration.add("jpg", "image/jpeg");
    263        configuration.add("jpeg", "image/jpeg");
    264        configuration.add("png", "image/png");
    265        configuration.add("swf", "application/x-shockwave-flash");
    266        configuration.add("svg", "image/svg+xml");
    267        configuration.add("woff", "application/font-woff");
    268        configuration.add("ttf", "application/x-font-ttf");
    269        configuration.add("eot", "application/vnd.ms-fontobject");
    270    }
    271
    272    /**
    273     * Disables compression for the following content types:
    274     * <ul>
    275     * <li>image/jpeg</li>
    276     * <li>image/gif</li>
    277     * <li>image/png</li>
    278     * <li>image/svg+xml</li>
    279     * <li>application/x-shockwave-flash</li>
    280     * <li>application/font-woff</li>
    281     * <li>application/x-font-ttf</li>
    282     * <li>application/vnd.ms-fontobject</li>
    283     * </ul>
    284     */
    285    @Contribute(CompressionAnalyzer.class)
    286    public void disableCompressionForImageTypes(MappedConfiguration<String, Boolean> configuration)
    287    {
    288        configuration.add("image/*", false);
    289        configuration.add("image/svg+xml", true);
    290        configuration.add("application/x-shockwave-flash", false);
    291        configuration.add("application/font-woff", false);
    292        configuration.add("application/x-font-ttf", false);
    293        configuration.add("application/vnd.ms-fontobject", false);
    294    }
    295
    296    @Marker(ContextProvider.class)
    297    public static AssetFactory buildContextAssetFactory(ApplicationGlobals globals,
    298                                                        AssetPathConstructor assetPathConstructor,
    299                                                        ResponseCompressionAnalyzer compressionAnalyzer,
    300                                                        ResourceChangeTracker resourceChangeTracker,
    301                                                        StreamableResourceSource streamableResourceSource)
    302    {
    303        return new ContextAssetFactory(compressionAnalyzer, resourceChangeTracker, streamableResourceSource, assetPathConstructor, globals.getContext());
    304    }
    305
    306    @Contribute(ClasspathAssetAliasManager.class)
    307    public static void addApplicationAndTapestryMappings(MappedConfiguration<String, String> configuration,
    308
    309                                                         @Symbol(InternalConstants.TAPESTRY_APP_PACKAGE_PARAM)
    310                                                         String appPackage)
    311    {
    312        configuration.add("tapestry", "org/apache/tapestry5");
    313
    314        configuration.add("app", toPackagePath(appPackage));
    315    }
    316
    317    /**
    318     * Contributes an handler for each mapped classpath alias, as well handlers for context assets
    319     * and stack assets (combined {@link org.apache.tapestry5.services.javascript.JavaScriptStack} files).
    320     */
    321    @Contribute(Dispatcher.class)
    322    @AssetRequestDispatcher
    323    public static void provideBuiltinAssetDispatchers(MappedConfiguration<String, AssetRequestHandler> configuration,
    324
    325                                                      @ContextProvider
    326                                                      AssetFactory contextAssetFactory,
    327
    328                                                      @Autobuild
    329                                                      StackAssetRequestHandler stackAssetRequestHandler,
    330
    331                                                      ClasspathAssetAliasManager classpathAssetAliasManager,
    332                                                      ResourceStreamer streamer,
    333                                                      AssetSource assetSource,
    334                                                      ClasspathAssetProtectionRule classpathAssetProtectionRule)
    335    {
    336        Map<String, String> mappings = classpathAssetAliasManager.getMappings();
    337
    338        for (String folder : mappings.keySet())
    339        {
    340            String path = mappings.get(folder);
    341
    342            configuration.add(folder, new ClasspathAssetRequestHandler(streamer, assetSource, path, classpathAssetProtectionRule));
    343        }
    344
    345        configuration.add(RequestConstants.CONTEXT_FOLDER,
    346                new ContextAssetRequestHandler(streamer, contextAssetFactory.getRootResource()));
    347
    348        configuration.add(RequestConstants.STACK_FOLDER, stackAssetRequestHandler);
    349
    350    }
    351
    352    @Contribute(ClasspathAssetAliasManager.class)
    353    public static void addMappingsForLibraryVirtualFolders(MappedConfiguration<String, String> configuration,
    354                                                           ComponentClassResolver resolver)
    355    {
    356        // Each library gets a mapping or its folder automatically
    357
    358        Map<String, String> folderToPackageMapping = resolver.getFolderToPackageMapping();
    359
    360        for (String folder : folderToPackageMapping.keySet())
    361        {
    362            // This is the 5.3 version, which is still supported:
    363            configuration.add(folder, toPackagePath(folderToPackageMapping.get(folder)));
    364
    365            // This is the 5.4 version; once 5.3 support is dropped, this can be simplified, and the
    366            // "meta/" prefix stripped out.
    367            String folderSuffix = folder.equals("") ? folder : "/" + folder;
    368
    369            configuration.add("meta" + folderSuffix, "META-INF/assets" + folderSuffix);
    370        }
    371    }
    372
    373    private static String toPackagePath(String packageName)
    374    {
    375        return packageName.replace('.', '/');
    376    }
    377
    378    /**
    379     * Contributes:
    380     * <dl>
    381     * <dt>ClientLocalization</dt>
    382     * <dd>A virtual resource of formatting symbols for decimal numbers</dd>
    383     * <dt>Core</dt>
    384     * <dd>Built in messages used by Tapestry's default validators and components</dd>
    385     * <dt>AppCatalog</dt>
    386     * <dd>The Resource defined by {@link SymbolConstants#APPLICATION_CATALOG}</dd>
    387     * <dt>
    388     *     </dl>
    389     *
    390     * @since 5.2.0
    391     */
    392    @Contribute(ComponentMessagesSource.class)
    393    public static void setupGlobalMessageCatalog(AssetSource assetSource,
    394                                                 @Symbol(SymbolConstants.APPLICATION_CATALOG)
    395                                                 Resource applicationCatalog, OrderedConfiguration<Resource> configuration)
    396    {
    397        configuration.add("ClientLocalization", new ClientLocalizationMessageResource());
    398        configuration.add("Core", assetSource.resourceForPath("org/apache/tapestry5/core.properties"));
    399        configuration.add("AppCatalog", applicationCatalog);
    400    }
    401
    402    @Contribute(Dispatcher.class)
    403    @Primary
    404    public static void setupAssetDispatch(OrderedConfiguration<Dispatcher> configuration,
    405                                          @AssetRequestDispatcher
    406                                          Dispatcher assetDispatcher)
    407    {
    408
    409        // This goes first because an asset to be streamed may have an file
    410        // extension, such as
    411        // ".html", that will confuse the later dispatchers.
    412
    413        configuration.add("Asset", assetDispatcher, "before:ComponentEvent");
    414    }
    415
    416    @Primary
    417    public static ClasspathAssetProtectionRule buildClasspathAssetProtectionRule(
    418            List<ClasspathAssetProtectionRule> rules, ChainBuilder chainBuilder)
    419    {
    420        return chainBuilder.build(ClasspathAssetProtectionRule.class, rules);
    421    }
    422
    423    public static void contributeClasspathAssetProtectionRule(
    424            OrderedConfiguration<ClasspathAssetProtectionRule> configuration)
    425    {
    426        ClasspathAssetProtectionRule classFileRule = (s) -> s.toLowerCase().endsWith(".class");
    427        configuration.add("ClassFile", classFileRule);
    428        ClasspathAssetProtectionRule propertiesFileRule = (s) -> s.toLowerCase().endsWith(".properties");
    429        configuration.add("PropertiesFile", propertiesFileRule);
    430        ClasspathAssetProtectionRule xmlFileRule = (s) -> s.toLowerCase().endsWith(".xml");
    431        configuration.add("XMLFile", xmlFileRule);
    432        ClasspathAssetProtectionRule folderRule = (s) -> isFolderToBlock(s);
    433        configuration.add("Folder", folderRule);
    434    }
    435
    436    final private static boolean isFolderToBlock(String path)
    437    {
    438        path = path.replace('\\', '/');
    439        final int lastIndex = path.lastIndexOf('/');
    440        if (lastIndex >= 0)
    441        {
    442            path = path.substring(lastIndex);
    443        }
    444        return !path.contains(".");
    445    }
    446
    447}