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 * </dl> 330 * 331 * @since 5.2.0 332 */ 333 @Contribute(ComponentMessagesSource.class) 334 public static void setupGlobalMessageCatalog(AssetSource assetSource, 335 @Symbol(SymbolConstants.APPLICATION_CATALOG) 336 Resource applicationCatalog, OrderedConfiguration<Resource> configuration) 337 { 338 configuration.add("ClientLocalization", new ClientLocalizationMessageResource()); 339 configuration.add("Core", assetSource.resourceForPath("org/apache/tapestry5/core.properties")); 340 configuration.add("AppCatalog", applicationCatalog); 341 } 342 343 @Contribute(Dispatcher.class) 344 @Primary 345 public static void setupAssetDispatch(OrderedConfiguration<Dispatcher> configuration, 346 @AssetRequestDispatcher 347 Dispatcher assetDispatcher) 348 { 349 350 // This goes first because an asset to be streamed may have an file 351 // extension, such as 352 // ".html", that will confuse the later dispatchers. 353 354 configuration.add("Asset", assetDispatcher, "before:ComponentEvent"); 355 } 356}