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.services.assets; 014 015import org.apache.tapestry5.Asset; 016import org.apache.tapestry5.SymbolConstants; 017import org.apache.tapestry5.commons.Resource; 018import org.apache.tapestry5.commons.util.CollectionFactory; 019import org.apache.tapestry5.http.ContentType; 020import org.apache.tapestry5.ioc.annotations.Symbol; 021import org.apache.tapestry5.ioc.services.ThreadLocale; 022import org.apache.tapestry5.services.assets.*; 023import org.apache.tapestry5.services.javascript.JavaScriptStack; 024import org.apache.tapestry5.services.javascript.JavaScriptStackSource; 025import org.apache.tapestry5.services.javascript.JavaScriptAggregationStrategy; 026import org.apache.tapestry5.services.javascript.ModuleManager; 027 028import java.io.*; 029import java.util.Collections; 030import java.util.List; 031import java.util.Locale; 032import java.util.Map; 033import java.util.regex.Pattern; 034 035public class JavaScriptStackAssemblerImpl implements JavaScriptStackAssembler 036{ 037 private static final ContentType JAVASCRIPT_CONTENT_TYPE = new ContentType("text/javascript;charset=utf-8"); 038 039 private final ThreadLocale threadLocale; 040 041 private final ResourceChangeTracker resourceChangeTracker; 042 043 private final StreamableResourceSource streamableResourceSource; 044 045 private final JavaScriptStackSource stackSource; 046 047 private final AssetChecksumGenerator checksumGenerator; 048 049 private final ModuleManager moduleManager; 050 051 private final ResourceMinimizer resourceMinimizer; 052 053 private final boolean minificationEnabled; 054 055 private final Map<String, StreamableResource> cache = Collections.synchronizedMap(CollectionFactory.<StreamableResource>newCaseInsensitiveMap()); 056 057 private class Parameters 058 { 059 final Locale locale; 060 061 final String stackName; 062 063 final boolean compress; 064 065 final JavaScriptAggregationStrategy javascriptAggregationStrategy; 066 067 private Parameters(Locale locale, String stackName, boolean compress, JavaScriptAggregationStrategy javascriptAggregationStrategy) 068 { 069 this.locale = locale; 070 this.stackName = stackName; 071 this.compress = compress; 072 this.javascriptAggregationStrategy = javascriptAggregationStrategy; 073 } 074 075 Parameters disableCompress() 076 { 077 return new Parameters(locale, stackName, false, javascriptAggregationStrategy); 078 } 079 } 080 081 // TODO: Support for aggregated CSS as well as aggregated JavaScript 082 083 public JavaScriptStackAssemblerImpl(ThreadLocale threadLocale, ResourceChangeTracker resourceChangeTracker, StreamableResourceSource streamableResourceSource, 084 JavaScriptStackSource stackSource, AssetChecksumGenerator checksumGenerator, ModuleManager moduleManager, 085 ResourceMinimizer resourceMinimizer, 086 @Symbol(SymbolConstants.MINIFICATION_ENABLED) 087 boolean minificationEnabled) 088 { 089 this.threadLocale = threadLocale; 090 this.resourceChangeTracker = resourceChangeTracker; 091 this.streamableResourceSource = streamableResourceSource; 092 this.stackSource = stackSource; 093 this.checksumGenerator = checksumGenerator; 094 this.moduleManager = moduleManager; 095 this.resourceMinimizer = resourceMinimizer; 096 this.minificationEnabled = minificationEnabled; 097 098 resourceChangeTracker.clearOnInvalidation(cache); 099 } 100 101 public StreamableResource assembleJavaScriptResourceForStack(String stackName, boolean compress, JavaScriptAggregationStrategy javascriptAggregationStrategy) throws IOException 102 { 103 Locale locale = threadLocale.getLocale(); 104 105 return assembleJavascriptResourceForStack(new Parameters(locale, stackName, compress, javascriptAggregationStrategy)); 106 } 107 108 private StreamableResource assembleJavascriptResourceForStack(Parameters parameters) throws IOException 109 { 110 String key = 111 String.format("%s[%s] %s", 112 parameters.stackName, 113 parameters.compress ? "COMPRESS" : "UNCOMPRESSED", 114 parameters.locale.toString()); 115 116 StreamableResource result = cache.get(key); 117 118 if (result == null) 119 { 120 result = assemble(parameters); 121 cache.put(key, result); 122 } 123 124 return result; 125 } 126 127 private StreamableResource assemble(Parameters parameters) throws IOException 128 { 129 if (parameters.compress) 130 { 131 StreamableResource uncompressed = assembleJavascriptResourceForStack(parameters.disableCompress()); 132 133 return new CompressedStreamableResource(uncompressed, checksumGenerator); 134 } 135 136 JavaScriptStack stack = stackSource.getStack(parameters.stackName); 137 138 return assembleStreamableForStack(parameters.locale.toString(), parameters, stack.getJavaScriptLibraries(), stack.getModules()); 139 } 140 141 interface StreamableReader 142 { 143 /** 144 * Reads the content of a StreamableResource as a UTF-8 string, and optionally transforms it in some way. 145 */ 146 String read(StreamableResource resource) throws IOException; 147 } 148 149 static String getContent(StreamableResource resource) throws IOException 150 { 151 final ByteArrayOutputStream bos = new ByteArrayOutputStream(resource.getSize()); 152 resource.streamTo(bos); 153 154 return new String(bos.toByteArray(), "UTF-8"); 155 } 156 157 158 final StreamableReader libraryReader = new StreamableReader() 159 { 160 public String read(StreamableResource resource) throws IOException 161 { 162 return getContent(resource); 163 } 164 }; 165 166 private final static Pattern DEFINE = Pattern.compile("\\bdefine\\s*\\((?!\\s*['\"])"); 167 168 private static class ModuleReader implements StreamableReader 169 { 170 final String moduleName; 171 172 private ModuleReader(String moduleName) 173 { 174 this.moduleName = moduleName; 175 } 176 177 public String read(StreamableResource resource) throws IOException 178 { 179 String content = getContent(resource); 180 181 return transform(content); 182 } 183 184 public String transform(String moduleContent) 185 { 186 return DEFINE.matcher(moduleContent).replaceFirst("define(\"" + moduleName + "\","); 187 } 188 } 189 190 191 private class Assembly 192 { 193 final ByteArrayOutputStream outputStream = new ByteArrayOutputStream(2000); 194 final PrintWriter writer; 195 long lastModified = 0; 196 final StringBuilder description; 197 private String sep = ""; 198 199 private Assembly(String description) throws UnsupportedEncodingException 200 { 201 writer = new PrintWriter(new OutputStreamWriter(outputStream, "UTF-8")); 202 203 this.description = new StringBuilder(description); 204 } 205 206 void add(Resource resource, StreamableReader reader) throws IOException 207 { 208 writer.format("\n/* %s */;\n", resource.toString()); 209 210 description.append(sep).append(resource.toString()); 211 sep = ", "; 212 213 StreamableResource streamable = streamableResourceSource.getStreamableResource(resource, 214 StreamableResourceProcessing.FOR_AGGREGATION, resourceChangeTracker); 215 216 writer.print(reader.read(streamable)); 217 218 lastModified = Math.max(lastModified, streamable.getLastModified()); 219 } 220 221 StreamableResource finish() 222 { 223 writer.close(); 224 225 return new StreamableResourceImpl( 226 description.toString(), 227 JAVASCRIPT_CONTENT_TYPE, CompressionStatus.COMPRESSABLE, lastModified, 228 new BytestreamCache(outputStream), checksumGenerator, null); 229 } 230 } 231 232 private StreamableResource assembleStreamableForStack(String localeName, Parameters parameters, 233 List<Asset> libraries, List<String> moduleNames) throws IOException 234 { 235 Assembly assembly = new Assembly(String.format("'%s' JavaScript stack, for locale %s, resources=", parameters.stackName, localeName)); 236 237 for (Asset library : libraries) 238 { 239 Resource resource = library.getResource(); 240 241 assembly.add(resource, libraryReader); 242 } 243 244 for (String moduleName : moduleNames) 245 { 246 Resource resource = moduleManager.findResourceForModule(moduleName); 247 248 if (resource == null) 249 { 250 throw new IllegalArgumentException(String.format("Could not identify a resource for module name '%s'.", moduleName)); 251 } 252 253 assembly.add(resource, new ModuleReader(moduleName)); 254 } 255 256 StreamableResource streamable = assembly.finish(); 257 258 if (minificationEnabled && parameters.javascriptAggregationStrategy.enablesMinimize()) 259 { 260 return resourceMinimizer.minimize(streamable); 261 } 262 263 return streamable; 264 } 265}