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}