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.messages.ComponentMessagesSource; 028 029import java.util.Map; 030 031/** 032 * @since 5.3 033 */ 034@Marker(Core.class) 035public class AssetsModule 036{ 037 public static void bind(ServiceBinder binder) 038 { 039 binder.bind(AssetFactory.class, ClasspathAssetFactory.class).withSimpleId(); 040 binder.bind(AssetPathConverter.class, IdentityAssetPathConverter.class); 041 binder.bind(AssetPathConstructor.class, AssetPathConstructorImpl.class); 042 binder.bind(ClasspathAssetAliasManager.class, ClasspathAssetAliasManagerImpl.class); 043 binder.bind(AssetSource.class, AssetSourceImpl.class); 044 binder.bind(StreamableResourceSource.class, StreamableResourceSourceImpl.class); 045 binder.bind(CompressionAnalyzer.class, CompressionAnalyzerImpl.class); 046 binder.bind(ContentTypeAnalyzer.class, ContentTypeAnalyzerImpl.class); 047 binder.bind(ResourceChangeTracker.class, ResourceChangeTrackerImpl.class); 048 binder.bind(ResourceMinimizer.class, MasterResourceMinimizer.class); 049 binder.bind(AssetChecksumGenerator.class, AssetChecksumGeneratorImpl.class); 050 binder.bind(JavaScriptStackAssembler.class, JavaScriptStackAssemblerImpl.class); 051 } 052 053 @Contribute(AssetSource.class) 054 public void configureStandardAssetFactories(MappedConfiguration<String, AssetFactory> configuration, 055 @ContextProvider 056 AssetFactory contextAssetFactory, 057 058 @ClasspathProvider 059 AssetFactory classpathAssetFactory) 060 { 061 configuration.add(AssetConstants.CONTEXT, contextAssetFactory); 062 configuration.add(AssetConstants.CLASSPATH, classpathAssetFactory); 063 } 064 065 066 @Contribute(SymbolProvider.class) 067 @FactoryDefaults 068 public static void setupSymbols(MappedConfiguration<String, Object> configuration) 069 { 070 // Minification may be enabled in production mode, but unless a minimizer is provided, nothing 071 // will change. 072 configuration.add(SymbolConstants.MINIFICATION_ENABLED, SymbolConstants.PRODUCTION_MODE_VALUE); 073 configuration.add(SymbolConstants.GZIP_COMPRESSION_ENABLED, true); 074 configuration.add(SymbolConstants.COMBINE_SCRIPTS, SymbolConstants.PRODUCTION_MODE_VALUE); 075 configuration.add(SymbolConstants.ASSET_URL_FULL_QUALIFIED, false); 076 077 configuration.add(SymbolConstants.ASSET_PATH_PREFIX, "assets"); 078 079 configuration.add(SymbolConstants.BOOTSTRAP_ROOT, "${tapestry.asset.root}/bootstrap"); 080 081 configuration.add("tapestry.asset.root", "classpath:META-INF/assets/tapestry5"); 082 } 083 084 // The use of decorators is to allow third-parties to get their own extensions 085 // into the pipeline. 086 087 @Decorate(id = "GZipCompression", serviceInterface = StreamableResourceSource.class) 088 public StreamableResourceSource enableCompression(StreamableResourceSource delegate, 089 @Symbol(SymbolConstants.GZIP_COMPRESSION_ENABLED) 090 boolean gzipEnabled, @Symbol(SymbolConstants.MIN_GZIP_SIZE) 091 int compressionCutoff, 092 AssetChecksumGenerator checksumGenerator) 093 { 094 return gzipEnabled 095 ? new SRSCompressingInterceptor(delegate, compressionCutoff, checksumGenerator) 096 : null; 097 } 098 099 @Decorate(id = "CacheCompressed", serviceInterface = StreamableResourceSource.class) 100 @Order("before:GZIpCompression") 101 public StreamableResourceSource enableCompressedCaching(StreamableResourceSource delegate, 102 @Symbol(SymbolConstants.GZIP_COMPRESSION_ENABLED) 103 boolean gzipEnabled, ResourceChangeTracker tracker) 104 { 105 return gzipEnabled 106 ? new SRSCompressedCachingInterceptor(delegate, tracker) 107 : null; 108 } 109 110 @Decorate(id = "Cache", serviceInterface = StreamableResourceSource.class) 111 @Order("after:GZipCompression") 112 public StreamableResourceSource enableUncompressedCaching(StreamableResourceSource delegate, 113 ResourceChangeTracker tracker) 114 { 115 return new SRSCachingInterceptor(delegate, tracker); 116 } 117 118 // Goes after cache, to ensure that what we are caching is the minified version. 119 @Decorate(id = "Minification", serviceInterface = StreamableResourceSource.class) 120 @Order("after:Cache") 121 public StreamableResourceSource enableMinification(StreamableResourceSource delegate, ResourceMinimizer minimizer, 122 @Symbol(SymbolConstants.MINIFICATION_ENABLED) 123 boolean enabled) 124 { 125 return enabled 126 ? new SRSMinimizingInterceptor(delegate, minimizer) 127 : null; 128 } 129 130 // Ordering this after minification means that the URL replacement happens first; 131 // then the minification, then the uncompressed caching, then compression, then compressed 132 // cache. 133 @Decorate(id = "CSSURLRewrite", serviceInterface = StreamableResourceSource.class) 134 @Order("after:Minification") 135 public StreamableResourceSource enableCSSURLRewriting(StreamableResourceSource delegate, 136 OperationTracker tracker, 137 AssetSource assetSource, 138 AssetChecksumGenerator checksumGenerator) 139 { 140 return new CSSURLRewriter(delegate, tracker, assetSource, checksumGenerator); 141 } 142 143 /** 144 * Adds content types: 145 * <dl> 146 * <dt>css</dt> 147 * <dd>text/css</dd> 148 * <dt>js</dt> 149 * <dd>text/javascript</dd> 150 * <dt>jpg, jpeg</dt> 151 * <dd>image/jpeg</dd> 152 * <dt>gif</dt> 153 * <dd>image/gif</dd> 154 * <dt>png</dt> 155 * <dd>image/png</dd> 156 * <dt>svg</dt> 157 * <dd>image/svg+xml</dd> 158 * <dt>swf</dt> 159 * <dd>application/x-shockwave-flash</dd> 160 * <dt>woff</dt> 161 * <dd>application/font-woff</dd> 162 * <dt>tff</dt> <dd>application/x-font-ttf</dd> 163 * <dt>eot</dt> <dd>application/vnd.ms-fontobject</dd> 164 * </dl> 165 */ 166 @Contribute(ContentTypeAnalyzer.class) 167 public void setupDefaultContentTypeMappings(MappedConfiguration<String, String> configuration) 168 { 169 configuration.add("css", "text/css"); 170 configuration.add("js", "text/javascript"); 171 configuration.add("gif", "image/gif"); 172 configuration.add("jpg", "image/jpeg"); 173 configuration.add("jpeg", "image/jpeg"); 174 configuration.add("png", "image/png"); 175 configuration.add("swf", "application/x-shockwave-flash"); 176 configuration.add("svg", "image/svg+xml"); 177 configuration.add("woff", "application/font-woff"); 178 configuration.add("ttf", "application/x-font-ttf"); 179 configuration.add("eot", "application/vnd.ms-fontobject"); 180 } 181 182 /** 183 * Disables compression for the following content types: 184 * <ul> 185 * <li>image/jpeg</li> 186 * <li>image/gif</li> 187 * <li>image/png</li> 188 * <li>application/x-shockwave-flash</li> 189 * <li>application/font-woff</li> 190 * <li>application/x-font-ttf</li> 191 * <li>application/vnd.ms-fontobject</li> 192 * </ul> 193 */ 194 @Contribute(CompressionAnalyzer.class) 195 public void disableCompressionForImageTypes(MappedConfiguration<String, Boolean> configuration) 196 { 197 configuration.add("image/*", false); 198 configuration.add("application/x-shockwave-flash", false); 199 configuration.add("application/font-woff", false); 200 configuration.add("application/x-font-ttf", false); 201 configuration.add("application/vnd.ms-fontobject", false); 202 } 203 204 @Marker(ContextProvider.class) 205 public static AssetFactory buildContextAssetFactory(ApplicationGlobals globals, 206 AssetPathConstructor assetPathConstructor, 207 ResponseCompressionAnalyzer compressionAnalyzer, 208 ResourceChangeTracker resourceChangeTracker, 209 StreamableResourceSource streamableResourceSource) 210 { 211 return new ContextAssetFactory(compressionAnalyzer, resourceChangeTracker, streamableResourceSource, assetPathConstructor, globals.getContext()); 212 } 213 214 @Contribute(ClasspathAssetAliasManager.class) 215 public static void addApplicationAndTapestryMappings(MappedConfiguration<String, String> configuration, 216 217 @Symbol(InternalConstants.TAPESTRY_APP_PACKAGE_PARAM) 218 String appPackage) 219 { 220 configuration.add("tapestry", "org/apache/tapestry5"); 221 222 configuration.add("app", toPackagePath(appPackage)); 223 } 224 225 /** 226 * Contributes an handler for each mapped classpath alias, as well handlers for context assets 227 * and stack assets (combined {@link org.apache.tapestry5.services.javascript.JavaScriptStack} files). 228 */ 229 @Contribute(Dispatcher.class) 230 @AssetRequestDispatcher 231 public static void provideBuiltinAssetDispatchers(MappedConfiguration<String, AssetRequestHandler> configuration, 232 233 @ContextProvider 234 AssetFactory contextAssetFactory, 235 236 @Autobuild 237 StackAssetRequestHandler stackAssetRequestHandler, 238 239 ClasspathAssetAliasManager classpathAssetAliasManager, 240 ResourceStreamer streamer, 241 AssetSource assetSource) 242 { 243 Map<String, String> mappings = classpathAssetAliasManager.getMappings(); 244 245 for (String folder : mappings.keySet()) 246 { 247 String path = mappings.get(folder); 248 249 configuration.add(folder, new ClasspathAssetRequestHandler(streamer, assetSource, path)); 250 } 251 252 configuration.add(RequestConstants.CONTEXT_FOLDER, 253 new ContextAssetRequestHandler(streamer, contextAssetFactory.getRootResource())); 254 255 configuration.add(RequestConstants.STACK_FOLDER, stackAssetRequestHandler); 256 257 } 258 259 @Contribute(ClasspathAssetAliasManager.class) 260 public static void addMappingsForLibraryVirtualFolders(MappedConfiguration<String, String> configuration, 261 ComponentClassResolver resolver) 262 { 263 // Each library gets a mapping or its folder automatically 264 265 Map<String, String> folderToPackageMapping = resolver.getFolderToPackageMapping(); 266 267 for (String folder : folderToPackageMapping.keySet()) 268 { 269 // This is the 5.3 version, which is still supported: 270 configuration.add(folder, toPackagePath(folderToPackageMapping.get(folder))); 271 272 // This is the 5.4 version; once 5.3 support is dropped, this can be simplified, and the 273 // "meta/" prefix stripped out. 274 String folderSuffix = folder.equals("") ? folder : "/" + folder; 275 276 configuration.add("meta" + folderSuffix, "META-INF/assets" + folderSuffix); 277 } 278 } 279 280 private static String toPackagePath(String packageName) 281 { 282 return packageName.replace('.', '/'); 283 } 284 285 /** 286 * Contributes: 287 * <dl> 288 * <dt>ClientLocalization</dt> 289 * <dd>A virtual resource of formatting symbols for decimal numbers</dd> 290 * <dt>Core</dt> 291 * <dd>Built in messages used by Tapestry's default validators and components</dd> 292 * <dt>AppCatalog</dt> 293 * <dd>The Resource defined by {@link SymbolConstants#APPLICATION_CATALOG}</dd> 294 * <dt> 295 * 296 * @since 5.2.0 297 */ 298 @Contribute(ComponentMessagesSource.class) 299 public static void setupGlobalMessageCatalog(AssetSource assetSource, 300 @Symbol(SymbolConstants.APPLICATION_CATALOG) 301 Resource applicationCatalog, OrderedConfiguration<Resource> configuration) 302 { 303 configuration.add("ClientLocalization", new ClientLocalizationMessageResource()); 304 configuration.add("Core", assetSource.resourceForPath("org/apache/tapestry5/core.properties")); 305 configuration.add("AppCatalog", applicationCatalog); 306 } 307 308 @Contribute(Dispatcher.class) 309 @Primary 310 public static void setupAssetDispatch(OrderedConfiguration<Dispatcher> configuration, 311 @AssetRequestDispatcher 312 Dispatcher assetDispatcher) 313 { 314 315 // This goes first because an asset to be streamed may have an file 316 // extension, such as 317 // ".html", that will confuse the later dispatchers. 318 319 configuration.add("Asset", assetDispatcher, "before:ComponentEvent"); 320 } 321}