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 }