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}