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