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