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*\\("); 166 167 private 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 DEFINE.matcher(content).replaceFirst("define(\"" + moduleName + "\","); 181 } 182 } 183 184 185 private class Assembly 186 { 187 final ByteArrayOutputStream outputStream = new ByteArrayOutputStream(2000); 188 final PrintWriter writer; 189 long lastModified = 0; 190 final StringBuilder description; 191 private String sep = ""; 192 193 private Assembly(String description) throws UnsupportedEncodingException 194 { 195 writer = new PrintWriter(new OutputStreamWriter(outputStream, "UTF-8")); 196 197 this.description = new StringBuilder(description); 198 } 199 200 void add(Resource resource, StreamableReader reader) throws IOException 201 { 202 writer.format("\n/* %s */;\n", resource.toString()); 203 204 description.append(sep).append(resource.toString()); 205 sep = ", "; 206 207 StreamableResource streamable = streamableResourceSource.getStreamableResource(resource, 208 StreamableResourceProcessing.FOR_AGGREGATION, resourceChangeTracker); 209 210 writer.print(reader.read(streamable)); 211 212 lastModified = Math.max(lastModified, streamable.getLastModified()); 213 } 214 215 StreamableResource finish() 216 { 217 writer.close(); 218 219 return new StreamableResourceImpl( 220 description.toString(), 221 JAVASCRIPT_CONTENT_TYPE, CompressionStatus.COMPRESSABLE, lastModified, 222 new BytestreamCache(outputStream), checksumGenerator, null); 223 } 224 } 225 226 private StreamableResource assembleStreamableForStack(String localeName, Parameters parameters, 227 List<Asset> libraries, List<String> moduleNames) throws IOException 228 { 229 Assembly assembly = new Assembly(String.format("'%s' JavaScript stack, for locale %s, resources=", parameters.stackName, localeName)); 230 231 for (Asset library : libraries) 232 { 233 Resource resource = library.getResource(); 234 235 assembly.add(resource, libraryReader); 236 } 237 238 for (String moduleName : moduleNames) 239 { 240 Resource resource = moduleManager.findResourceForModule(moduleName); 241 242 if (resource == null) 243 { 244 throw new IllegalArgumentException(String.format("Could not identify a resource for module name '%s'.", moduleName)); 245 } 246 247 assembly.add(resource, new ModuleReader(moduleName)); 248 } 249 250 StreamableResource streamable = assembly.finish(); 251 252 if (minificationEnabled && parameters.javascriptAggregationStrategy.enablesMinimize()) 253 { 254 return resourceMinimizer.minimize(streamable); 255 } 256 257 return streamable; 258 } 259}