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