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}