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