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.internal.webresources; 014 015import org.apache.tapestry5.SymbolConstants; 016import org.apache.tapestry5.internal.TapestryInternalUtils; 017import org.apache.tapestry5.internal.services.assets.BytestreamCache; 018import org.apache.tapestry5.ioc.IOOperation; 019import org.apache.tapestry5.ioc.OperationTracker; 020import org.apache.tapestry5.ioc.Resource; 021import org.apache.tapestry5.ioc.annotations.PostInjection; 022import org.apache.tapestry5.ioc.annotations.Symbol; 023import org.apache.tapestry5.ioc.internal.util.CollectionFactory; 024import org.apache.tapestry5.services.assets.ResourceDependencies; 025import org.apache.tapestry5.services.assets.ResourceTransformer; 026import org.apache.tapestry5.webresources.WebResourcesSymbols; 027import org.slf4j.Logger; 028 029import java.io.*; 030import java.util.Map; 031 032public class ResourceTransformerFactoryImpl implements ResourceTransformerFactory 033{ 034 private final Logger logger; 035 036 private final OperationTracker tracker; 037 038 private final boolean productionMode; 039 040 private final File cacheDir; 041 042 public ResourceTransformerFactoryImpl(Logger logger, OperationTracker tracker, 043 @Symbol(SymbolConstants.PRODUCTION_MODE) 044 boolean productionMode, 045 @Symbol(WebResourcesSymbols.CACHE_DIR) 046 String cacheDir) 047 { 048 this.logger = logger; 049 this.tracker = tracker; 050 this.productionMode = productionMode; 051 052 this.cacheDir = new File(cacheDir); 053 054 if (!productionMode) 055 { 056 logger.info(String.format("Using %s to store compiled assets (development mode only).", cacheDir)); 057 } 058 } 059 060 @PostInjection 061 public void createCacheDir(@Symbol(SymbolConstants.RESTRICTIVE_ENVIRONMENT) boolean restrictive) 062 { 063 if (!restrictive) 064 { 065 cacheDir.mkdirs(); 066 } 067 } 068 069 static class Compiled extends ContentChangeTracker 070 { 071 private BytestreamCache bytestreamCache; 072 073 Compiled(Resource root) 074 { 075 addDependency(root); 076 } 077 078 void store(InputStream stream) throws IOException 079 { 080 ByteArrayOutputStream bos = new ByteArrayOutputStream(); 081 082 TapestryInternalUtils.copy(stream, bos); 083 084 stream.close(); 085 bos.close(); 086 087 this.bytestreamCache = new BytestreamCache(bos); 088 } 089 090 InputStream openStream() 091 { 092 return bytestreamCache.openStream(); 093 } 094 } 095 096 097 @Override 098 public ResourceTransformer createCompiler(String contentType, String sourceName, String targetName, ResourceTransformer transformer, CacheMode cacheMode) 099 { 100 ResourceTransformer trackingCompiler = wrapWithTracking(sourceName, targetName, transformer); 101 102 if (productionMode) 103 { 104 return trackingCompiler; 105 } 106 107 ResourceTransformer timingCompiler = wrapWithTiming(targetName, trackingCompiler); 108 109 switch (cacheMode) 110 { 111 case NONE: 112 113 return timingCompiler; 114 115 case SINGLE_FILE: 116 117 return wrapWithFileSystemCaching(timingCompiler, targetName); 118 119 case MULTIPLE_FILE: 120 121 return wrapWithInMemoryCaching(timingCompiler, targetName); 122 123 default: 124 125 throw new IllegalStateException(); 126 } 127 } 128 129 private ResourceTransformer wrapWithTracking(final String sourceName, final String targetName, ResourceTransformer core) 130 { 131 return new DelegatingResourceTransformer(core) 132 { 133 @Override 134 public InputStream transform(final Resource source, final ResourceDependencies dependencies) throws IOException 135 { 136 final String description = String.format("Compiling %s from %s to %s", source, sourceName, targetName); 137 138 return tracker.perform(description, new IOOperation<InputStream>() 139 { 140 @Override 141 public InputStream perform() throws IOException 142 { 143 return delegate.transform(source, dependencies); 144 } 145 }); 146 } 147 }; 148 } 149 150 private ResourceTransformer wrapWithTiming(final String targetName, ResourceTransformer coreCompiler) 151 { 152 return new DelegatingResourceTransformer(coreCompiler) 153 { 154 @Override 155 public InputStream transform(final Resource source, final ResourceDependencies dependencies) throws IOException 156 { 157 final long startTime = System.nanoTime(); 158 159 InputStream result = delegate.transform(source, dependencies); 160 161 final long elapsedTime = System.nanoTime() - startTime; 162 163 logger.info(String.format("Compiled %s to %s in %.2f ms", 164 source, targetName, 165 ResourceTransformUtils.nanosToMillis(elapsedTime))); 166 167 return result; 168 } 169 }; 170 } 171 172 /** 173 * Caching is not needed in production, because caching of streamable resources occurs at a higher level 174 * (possibly after sources have been aggregated and minimized and gzipped). However, in development, it is 175 * very important to avoid costly CoffeeScript compilation (or similar operations); Tapestry's caching is 176 * somewhat primitive: a change to *any* resource in a given domain results in the cache of all of those resources 177 * being discarded. 178 */ 179 private ResourceTransformer wrapWithInMemoryCaching( ResourceTransformer core, final String targetName) 180 { 181 return new DelegatingResourceTransformer(core) 182 { 183 final Map<Resource, Compiled> cache = CollectionFactory.newConcurrentMap(); 184 185 @Override 186 public InputStream transform(Resource source, ResourceDependencies dependencies) throws IOException 187 { 188 Compiled compiled = cache.get(source); 189 190 if (compiled != null && !compiled.dirty()) 191 { 192 logger.info(String.format("Resource %s and dependencies are unchanged; serving compiled %s content from in-memory cache", 193 source, targetName)); 194 195 return compiled.openStream(); 196 } 197 198 compiled = new Compiled(source); 199 200 InputStream is = delegate.transform(source, new ResourceDependenciesSplitter(dependencies, compiled)); 201 202 compiled.store(is); 203 204 is.close(); 205 206 cache.put(source, compiled); 207 208 return compiled.openStream(); 209 } 210 }; 211 } 212 213 private ResourceTransformer wrapWithFileSystemCaching( ResourceTransformer core, final String targetName) 214 { 215 return new DelegatingResourceTransformer(core) 216 { 217 @Override 218 public InputStream transform(Resource source, ResourceDependencies dependencies) throws IOException 219 { 220 long checksum = ResourceTransformUtils.toChecksum(source); 221 222 String fileName = Long.toHexString(checksum) + "-" + source.getFile(); 223 224 File cacheFile = new File(cacheDir, fileName); 225 226 if (cacheFile.exists()) 227 { 228 logger.debug(String.format("Serving up compiled %s content for %s from file system cache", targetName, source)); 229 230 return new BufferedInputStream(new FileInputStream(cacheFile)); 231 } 232 233 InputStream compiled = delegate.transform(source, dependencies); 234 235 // We need the InputStream twice; once to return, and once to write out to the cache file for later. 236 237 ByteArrayOutputStream bos = new ByteArrayOutputStream(); 238 239 TapestryInternalUtils.copy(compiled, bos); 240 241 compiled.close(); 242 243 BytestreamCache cache = new BytestreamCache(bos); 244 245 writeToCacheFile(cacheFile, cache.openStream()); 246 247 return cache.openStream(); 248 } 249 }; 250 } 251 252 private void writeToCacheFile(File file, InputStream stream) throws IOException 253 { 254 OutputStream outputStream = new BufferedOutputStream(new FileOutputStream(file)); 255 256 TapestryInternalUtils.copy(stream, outputStream); 257 258 outputStream.close(); 259 } 260}