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}