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