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    }