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