001 // Copyright 2010, 2011 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 015 package org.apache.tapestry5.internal.services.assets; 016 017 import org.apache.tapestry5.Asset; 018 import org.apache.tapestry5.SymbolConstants; 019 import org.apache.tapestry5.internal.IOOperation; 020 import org.apache.tapestry5.internal.TapestryInternalUtils; 021 import org.apache.tapestry5.internal.services.ResourceStreamer; 022 import org.apache.tapestry5.ioc.OperationTracker; 023 import org.apache.tapestry5.ioc.Resource; 024 import org.apache.tapestry5.ioc.annotations.PostInjection; 025 import org.apache.tapestry5.ioc.annotations.Symbol; 026 import org.apache.tapestry5.ioc.internal.util.CollectionFactory; 027 import org.apache.tapestry5.json.JSONArray; 028 import org.apache.tapestry5.services.*; 029 import org.apache.tapestry5.services.assets.*; 030 import org.apache.tapestry5.services.javascript.JavaScriptStack; 031 import org.apache.tapestry5.services.javascript.JavaScriptStackSource; 032 033 import java.io.*; 034 import java.util.List; 035 import java.util.Map; 036 import java.util.regex.Matcher; 037 import java.util.regex.Pattern; 038 import java.util.zip.GZIPOutputStream; 039 040 public class StackAssetRequestHandler implements AssetRequestHandler, InvalidationListener 041 { 042 private static final String JAVASCRIPT_CONTENT_TYPE = "text/javascript"; 043 044 private final StreamableResourceSource streamableResourceSource; 045 046 private final JavaScriptStackSource javascriptStackSource; 047 048 private final LocalizationSetter localizationSetter; 049 050 private final ResponseCompressionAnalyzer compressionAnalyzer; 051 052 private final ResourceStreamer resourceStreamer; 053 054 private final Pattern pathPattern = Pattern.compile("^(.+)/(.+)\\.js$"); 055 056 // Two caches, keyed on extra path. Both are accessed only from synchronized blocks. 057 private final Map<String, StreamableResource> uncompressedCache = CollectionFactory.newCaseInsensitiveMap(); 058 059 private final Map<String, StreamableResource> compressedCache = CollectionFactory.newCaseInsensitiveMap(); 060 061 private final ResourceMinimizer resourceMinimizer; 062 063 private final OperationTracker tracker; 064 065 private final boolean minificationEnabled; 066 067 private final ResourceChangeTracker resourceChangeTracker; 068 069 public StackAssetRequestHandler(StreamableResourceSource streamableResourceSource, 070 JavaScriptStackSource javascriptStackSource, LocalizationSetter localizationSetter, 071 ResponseCompressionAnalyzer compressionAnalyzer, ResourceStreamer resourceStreamer, 072 ResourceMinimizer resourceMinimizer, OperationTracker tracker, 073 074 @Symbol(SymbolConstants.MINIFICATION_ENABLED) 075 boolean minificationEnabled, ResourceChangeTracker resourceChangeTracker) 076 { 077 this.streamableResourceSource = streamableResourceSource; 078 this.javascriptStackSource = javascriptStackSource; 079 this.localizationSetter = localizationSetter; 080 this.compressionAnalyzer = compressionAnalyzer; 081 this.resourceStreamer = resourceStreamer; 082 this.resourceMinimizer = resourceMinimizer; 083 this.tracker = tracker; 084 this.minificationEnabled = minificationEnabled; 085 this.resourceChangeTracker = resourceChangeTracker; 086 } 087 088 @PostInjection 089 public void listenToInvalidations(ResourceChangeTracker resourceChangeTracker) 090 { 091 resourceChangeTracker.addInvalidationListener(this); 092 } 093 094 public boolean handleAssetRequest(Request request, Response response, final String extraPath) throws IOException 095 { 096 TapestryInternalUtils.performIO(tracker, String.format("Streaming asset stack %s", extraPath), 097 new IOOperation() 098 { 099 public void perform() throws IOException 100 { 101 boolean compress = compressionAnalyzer.isGZipSupported(); 102 103 StreamableResource resource = getResource(extraPath, compress); 104 105 resourceStreamer.streamResource(resource); 106 } 107 }); 108 109 return true; 110 } 111 112 /** 113 * Notified by the {@link ResourceChangeTracker} when (any) resource files change; the internal caches are cleared. 114 */ 115 public synchronized void objectWasInvalidated() 116 { 117 uncompressedCache.clear(); 118 compressedCache.clear(); 119 } 120 121 private StreamableResource getResource(String extraPath, boolean compressed) throws IOException 122 { 123 return compressed ? getCompressedResource(extraPath) : getUncompressedResource(extraPath); 124 } 125 126 private synchronized StreamableResource getCompressedResource(String extraPath) throws IOException 127 { 128 StreamableResource result = compressedCache.get(extraPath); 129 130 if (result == null) 131 { 132 StreamableResource uncompressed = getUncompressedResource(extraPath); 133 result = compressStream(uncompressed); 134 compressedCache.put(extraPath, result); 135 } 136 137 return result; 138 } 139 140 private synchronized StreamableResource getUncompressedResource(String extraPath) throws IOException 141 { 142 StreamableResource result = uncompressedCache.get(extraPath); 143 144 if (result == null) 145 { 146 result = assembleStackContent(extraPath); 147 uncompressedCache.put(extraPath, result); 148 } 149 150 return result; 151 } 152 153 private StreamableResource assembleStackContent(String extraPath) throws IOException 154 { 155 Matcher matcher = pathPattern.matcher(extraPath); 156 157 if (!matcher.matches()) 158 throw new RuntimeException("Invalid path for a stack asset request."); 159 160 String localeName = matcher.group(1); 161 String stackName = matcher.group(2); 162 163 return assembleStackContent(localeName, stackName); 164 } 165 166 private StreamableResource assembleStackContent(String localeName, String stackName) throws IOException 167 { 168 localizationSetter.setNonPeristentLocaleFromLocaleName(localeName); 169 170 JavaScriptStack stack = javascriptStackSource.getStack(stackName); 171 List<Asset> libraries = stack.getJavaScriptLibraries(); 172 173 StreamableResource stackContent = assembleStackContent(localeName, stackName, libraries); 174 175 return minificationEnabled ? resourceMinimizer.minimize(stackContent) : stackContent; 176 } 177 178 private StreamableResource assembleStackContent(String localeName, String stackName, List<Asset> libraries) throws IOException 179 { 180 ByteArrayOutputStream stream = new ByteArrayOutputStream(); 181 OutputStreamWriter osw = new OutputStreamWriter(stream, "UTF-8"); 182 PrintWriter writer = new PrintWriter(osw, true); 183 long lastModified = 0; 184 185 StringBuilder description = new StringBuilder(String.format("'%s' JavaScript stack, for locale %s, resources=", stackName, localeName)); 186 String sep = ""; 187 188 JSONArray paths = new JSONArray(); 189 190 for (Asset library : libraries) 191 { 192 String path = library.toClientURL(); 193 194 paths.put(path); 195 196 writer.format("\n/* %s */;\n", path); 197 198 Resource resource = library.getResource(); 199 200 description.append(sep).append(resource.toString()); 201 sep = ", "; 202 203 StreamableResource streamable = streamableResourceSource.getStreamableResource(resource, 204 StreamableResourceProcessing.FOR_AGGREGATION, resourceChangeTracker); 205 206 streamable.streamTo(stream); 207 208 lastModified = Math.max(lastModified, streamable.getLastModified()); 209 } 210 211 writer.close(); 212 213 return new StreamableResourceImpl( 214 description.toString(), 215 JAVASCRIPT_CONTENT_TYPE, CompressionStatus.COMPRESSABLE, lastModified, 216 new BytestreamCache(stream)); 217 } 218 219 private StreamableResource compressStream(StreamableResource uncompressed) throws IOException 220 { 221 ByteArrayOutputStream compressed = new ByteArrayOutputStream(); 222 OutputStream compressor = new BufferedOutputStream(new GZIPOutputStream(compressed)); 223 224 uncompressed.streamTo(compressor); 225 226 compressor.close(); 227 228 BytestreamCache cache = new BytestreamCache(compressed); 229 230 return new StreamableResourceImpl(uncompressed.getDescription(), JAVASCRIPT_CONTENT_TYPE, CompressionStatus.COMPRESSED, 231 uncompressed.getLastModified(), cache); 232 } 233 }