    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    }