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.commons.MappedConfiguration;
020import org.apache.tapestry5.commons.OrderedConfiguration;
021import org.apache.tapestry5.commons.Resource;
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.AssetChecksumGeneratorImpl;
039import org.apache.tapestry5.internal.services.assets.AssetPathConstructorImpl;
040import org.apache.tapestry5.internal.services.assets.CSSURLRewriter;
041import org.apache.tapestry5.internal.services.assets.ClasspathAssetRequestHandler;
042import org.apache.tapestry5.internal.services.assets.CompressionAnalyzerImpl;
043import org.apache.tapestry5.internal.services.assets.ContentTypeAnalyzerImpl;
044import org.apache.tapestry5.internal.services.assets.ContextAssetRequestHandler;
045import org.apache.tapestry5.internal.services.assets.JavaScriptStackAssembler;
046import org.apache.tapestry5.internal.services.assets.JavaScriptStackAssemblerImpl;
047import org.apache.tapestry5.internal.services.assets.JavaScriptStackMinimizeDisabler;
048import org.apache.tapestry5.internal.services.assets.MasterResourceMinimizer;
049import org.apache.tapestry5.internal.services.assets.ResourceChangeTracker;
050import org.apache.tapestry5.internal.services.assets.ResourceChangeTrackerImpl;
051import org.apache.tapestry5.internal.services.assets.SRSCachingInterceptor;
052import org.apache.tapestry5.internal.services.assets.SRSCompressedCachingInterceptor;
053import org.apache.tapestry5.internal.services.assets.SRSCompressingInterceptor;
054import org.apache.tapestry5.internal.services.assets.SRSMinimizingInterceptor;
055import org.apache.tapestry5.internal.services.assets.StackAssetRequestHandler;
056import org.apache.tapestry5.internal.services.assets.StreamableResourceSourceImpl;
057import org.apache.tapestry5.internal.services.assets.UTF8ForTextAssets;
058import org.apache.tapestry5.internal.services.messages.ClientLocalizationMessageResource;
059import org.apache.tapestry5.ioc.OperationTracker;
060import org.apache.tapestry5.ioc.ServiceBinder;
061import org.apache.tapestry5.ioc.annotations.Autobuild;
062import org.apache.tapestry5.ioc.annotations.Contribute;
063import org.apache.tapestry5.ioc.annotations.Decorate;
064import org.apache.tapestry5.ioc.annotations.Marker;
065import org.apache.tapestry5.ioc.annotations.Order;
066import org.apache.tapestry5.ioc.annotations.Primary;
067import org.apache.tapestry5.ioc.annotations.Symbol;
068import org.apache.tapestry5.ioc.services.ChainBuilder;
069import org.apache.tapestry5.ioc.services.FactoryDefaults;
070import org.apache.tapestry5.ioc.services.SymbolProvider;
071import org.apache.tapestry5.services.AssetFactory;
072import org.apache.tapestry5.services.AssetPathConverter;
073import org.apache.tapestry5.services.AssetRequestDispatcher;
074import org.apache.tapestry5.services.AssetSource;
075import org.apache.tapestry5.services.ClasspathAssetAliasManager;
076import org.apache.tapestry5.services.ClasspathAssetProtectionRule;
077import org.apache.tapestry5.services.ClasspathProvider;
078import org.apache.tapestry5.services.ComponentClassResolver;
079import org.apache.tapestry5.services.ContextProvider;
080import org.apache.tapestry5.services.Core;
081import org.apache.tapestry5.services.assets.AssetChecksumGenerator;
082import org.apache.tapestry5.services.assets.AssetPathConstructor;
083import org.apache.tapestry5.services.assets.AssetRequestHandler;
084import org.apache.tapestry5.services.assets.ContentTypeAnalyzer;
085import org.apache.tapestry5.services.assets.ResourceMinimizer;
086import org.apache.tapestry5.services.assets.StreamableResourceSource;
087import org.apache.tapestry5.services.javascript.JavaScriptStackSource;
088import org.apache.tapestry5.services.messages.ComponentMessagesSource;
089
090/**
091 * @since 5.3
092 */
093@Marker(Core.class)
094public class AssetsModule
095{
096    public static void bind(ServiceBinder binder)
097    {
098        binder.bind(AssetFactory.class, ClasspathAssetFactory.class).withSimpleId();
099        binder.bind(AssetPathConverter.class, IdentityAssetPathConverter.class);
100        binder.bind(AssetPathConstructor.class, AssetPathConstructorImpl.class);
101        binder.bind(ClasspathAssetAliasManager.class, ClasspathAssetAliasManagerImpl.class);
102        binder.bind(AssetSource.class, AssetSourceImpl.class);
103        binder.bind(StreamableResourceSource.class, StreamableResourceSourceImpl.class);
104        binder.bind(CompressionAnalyzer.class, CompressionAnalyzerImpl.class);
105        binder.bind(ContentTypeAnalyzer.class, ContentTypeAnalyzerImpl.class);
106        binder.bind(ResourceChangeTracker.class, ResourceChangeTrackerImpl.class);
107        binder.bind(ResourceMinimizer.class, MasterResourceMinimizer.class);
108        binder.bind(AssetChecksumGenerator.class, AssetChecksumGeneratorImpl.class);
109        binder.bind(JavaScriptStackAssembler.class, JavaScriptStackAssemblerImpl.class);
110    }
111
112    @Contribute(AssetSource.class)
113    public void configureStandardAssetFactories(MappedConfiguration<String, AssetFactory> configuration,
114                                                @ContextProvider
115                                                AssetFactory contextAssetFactory,
116
117                                                @ClasspathProvider
118                                                AssetFactory classpathAssetFactory)
119    {
120        configuration.add(AssetConstants.CONTEXT, contextAssetFactory);
121        configuration.add(AssetConstants.CLASSPATH, classpathAssetFactory);
122        configuration.add(AssetConstants.HTTP, new ExternalUrlAssetFactory(AssetConstants.HTTP));
123        configuration.add(AssetConstants.HTTPS, new ExternalUrlAssetFactory(AssetConstants.HTTPS));
124        configuration.add(AssetConstants.FTP, new ExternalUrlAssetFactory(AssetConstants.FTP));
125        configuration.add(AssetConstants.PROTOCOL_RELATIVE, new ExternalUrlAssetFactory(AssetConstants.PROTOCOL_RELATIVE));
126    }
127
128
129    @Contribute(SymbolProvider.class)
130    @FactoryDefaults
131    public static void setupSymbols(MappedConfiguration<String, Object> configuration)
132    {
133        // Minification may be enabled in production mode, but unless a minimizer is provided, nothing
134        // will change.
135        configuration.add(SymbolConstants.MINIFICATION_ENABLED, SymbolConstants.PRODUCTION_MODE_VALUE);
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(TapestryHttpSymbolConstants.GZIP_COMPRESSION_ENABLED)
154                                                      boolean gzipEnabled, @Symbol(TapestryHttpSymbolConstants.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(TapestryHttpSymbolConstants.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(TapestryHttpInternalConstants.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}