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
015package org.apache.tapestry5.internal.services.assets;
016
017import org.apache.tapestry5.Asset;
018import org.apache.tapestry5.SymbolConstants;
019import org.apache.tapestry5.internal.IOOperation;
020import org.apache.tapestry5.internal.TapestryInternalUtils;
021import org.apache.tapestry5.internal.services.ResourceStreamer;
022import org.apache.tapestry5.ioc.OperationTracker;
023import org.apache.tapestry5.ioc.Resource;
024import org.apache.tapestry5.ioc.annotations.PostInjection;
025import org.apache.tapestry5.ioc.annotations.Symbol;
026import org.apache.tapestry5.ioc.internal.util.CollectionFactory;
027import org.apache.tapestry5.json.JSONArray;
028import org.apache.tapestry5.services.*;
029import org.apache.tapestry5.services.assets.*;
030import org.apache.tapestry5.services.javascript.JavaScriptStack;
031import org.apache.tapestry5.services.javascript.JavaScriptStackSource;
032
033import java.io.*;
034import java.util.List;
035import java.util.Map;
036import java.util.regex.Matcher;
037import java.util.regex.Pattern;
038import java.util.zip.GZIPOutputStream;
039
040public 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}