    001// Licensed under the Apache License, Version 2.0 (the "License");
    002// you may not use this file except in compliance with the License.
    003// You may obtain a copy of the License at
    004//
    005// http://www.apache.org/licenses/LICENSE-2.0
    006//
    007// Unless required by applicable law or agreed to in writing, software
    008// distributed under the License is distributed on an "AS IS" BASIS,
    009// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    010// See the License for the specific language governing permissions and
    011// limitations under the License.
    012
    013package org.apache.tapestry5.internal.services.assets;
    014
    015import org.apache.tapestry5.Asset;
    016import org.apache.tapestry5.SymbolConstants;
    017import org.apache.tapestry5.commons.Resource;
    018import org.apache.tapestry5.commons.util.CollectionFactory;
    019import org.apache.tapestry5.http.ContentType;
    020import org.apache.tapestry5.ioc.annotations.Symbol;
    021import org.apache.tapestry5.ioc.services.ThreadLocale;
    022import org.apache.tapestry5.services.assets.*;
    023import org.apache.tapestry5.services.javascript.JavaScriptStack;
    024import org.apache.tapestry5.services.javascript.JavaScriptStackSource;
    025import org.apache.tapestry5.services.javascript.JavaScriptAggregationStrategy;
    026import org.apache.tapestry5.services.javascript.ModuleManager;
    027
    028import java.io.*;
    029import java.util.Collections;
    030import java.util.List;
    031import java.util.Locale;
    032import java.util.Map;
    033import java.util.regex.Pattern;
    034
    035public class JavaScriptStackAssemblerImpl implements JavaScriptStackAssembler
    036{
    037    private static final ContentType JAVASCRIPT_CONTENT_TYPE = new ContentType("text/javascript;charset=utf-8");
    038
    039    private final ThreadLocale threadLocale;
    040
    041    private final ResourceChangeTracker resourceChangeTracker;
    042
    043    private final StreamableResourceSource streamableResourceSource;
    044
    045    private final JavaScriptStackSource stackSource;
    046
    047    private final AssetChecksumGenerator checksumGenerator;
    048
    049    private final ModuleManager moduleManager;
    050
    051    private final ResourceMinimizer resourceMinimizer;
    052
    053    private final boolean minificationEnabled;
    054
    055    private final Map<String, StreamableResource> cache = Collections.synchronizedMap(CollectionFactory.<StreamableResource>newCaseInsensitiveMap());
    056
    057    private class Parameters
    058    {
    059        final Locale locale;
    060
    061        final String stackName;
    062
    063        final boolean compress;
    064
    065        final JavaScriptAggregationStrategy javascriptAggregationStrategy;
    066
    067        private Parameters(Locale locale, String stackName, boolean compress, JavaScriptAggregationStrategy javascriptAggregationStrategy)
    068        {
    069            this.locale = locale;
    070            this.stackName = stackName;
    071            this.compress = compress;
    072            this.javascriptAggregationStrategy = javascriptAggregationStrategy;
    073        }
    074
    075        Parameters disableCompress()
    076        {
    077            return new Parameters(locale, stackName, false, javascriptAggregationStrategy);
    078        }
    079    }
    080
    081    // TODO: Support for aggregated CSS as well as aggregated JavaScript
    082
    083    public JavaScriptStackAssemblerImpl(ThreadLocale threadLocale, ResourceChangeTracker resourceChangeTracker, StreamableResourceSource streamableResourceSource,
    084                                        JavaScriptStackSource stackSource, AssetChecksumGenerator checksumGenerator, ModuleManager moduleManager,
    085                                        ResourceMinimizer resourceMinimizer,
    086                                        @Symbol(SymbolConstants.MINIFICATION_ENABLED)
    087                                        boolean minificationEnabled)
    088    {
    089        this.threadLocale = threadLocale;
    090        this.resourceChangeTracker = resourceChangeTracker;
    091        this.streamableResourceSource = streamableResourceSource;
    092        this.stackSource = stackSource;
    093        this.checksumGenerator = checksumGenerator;
    094        this.moduleManager = moduleManager;
    095        this.resourceMinimizer = resourceMinimizer;
    096        this.minificationEnabled = minificationEnabled;
    097
    098        resourceChangeTracker.clearOnInvalidation(cache);
    099    }
    100
    101    public StreamableResource assembleJavaScriptResourceForStack(String stackName, boolean compress, JavaScriptAggregationStrategy javascriptAggregationStrategy) throws IOException
    102    {
    103        Locale locale = threadLocale.getLocale();
    104
    105        return assembleJavascriptResourceForStack(new Parameters(locale, stackName, compress, javascriptAggregationStrategy));
    106    }
    107
    108    private StreamableResource assembleJavascriptResourceForStack(Parameters parameters) throws IOException
    109    {
    110        String key =
    111                String.format("%s[%s] %s",
    112                        parameters.stackName,
    113                        parameters.compress ? "COMPRESS" : "UNCOMPRESSED",
    114                        parameters.locale.toString());
    115
    116        StreamableResource result = cache.get(key);
    117
    118        if (result == null)
    119        {
    120            result = assemble(parameters);
    121            cache.put(key, result);
    122        }
    123
    124        return result;
    125    }
    126
    127    private StreamableResource assemble(Parameters parameters) throws IOException
    128    {
    129        if (parameters.compress)
    130        {
    131            StreamableResource uncompressed = assembleJavascriptResourceForStack(parameters.disableCompress());
    132
    133            return new CompressedStreamableResource(uncompressed, checksumGenerator);
    134        }
    135
    136        JavaScriptStack stack = stackSource.getStack(parameters.stackName);
    137
    138        return assembleStreamableForStack(parameters.locale.toString(), parameters, stack.getJavaScriptLibraries(), stack.getModules());
    139    }
    140
    141    interface StreamableReader
    142    {
    143        /**
    144         * Reads the content of a StreamableResource as a UTF-8 string, and optionally transforms it in some way.
    145         */
    146        String read(StreamableResource resource) throws IOException;
    147    }
    148
    149    static String getContent(StreamableResource resource) throws IOException
    150    {
    151        final ByteArrayOutputStream bos = new ByteArrayOutputStream(resource.getSize());
    152        resource.streamTo(bos);
    153
    154        return new String(bos.toByteArray(), "UTF-8");
    155    }
    156
    157
    158    final StreamableReader libraryReader = new StreamableReader()
    159    {
    160        public String read(StreamableResource resource) throws IOException
    161        {
    162            return getContent(resource);
    163        }
    164    };
    165
    166    private final static Pattern DEFINE = Pattern.compile("\\bdefine\\s*\\((?!\\s*['\"])");
    167
    168    private static class ModuleReader implements StreamableReader
    169    {
    170        final String moduleName;
    171
    172        private ModuleReader(String moduleName)
    173        {
    174            this.moduleName = moduleName;
    175        }
    176
    177        public String read(StreamableResource resource) throws IOException
    178        {
    179            String content = getContent(resource);
    180
    181            return transform(content);
    182        }
    183
    184        public String transform(String moduleContent)
    185        {
    186            return DEFINE.matcher(moduleContent).replaceFirst("define(\"" + moduleName + "\",");
    187        }
    188    }
    189
    190
    191    private class Assembly
    192    {
    193        final ByteArrayOutputStream outputStream = new ByteArrayOutputStream(2000);
    194        final PrintWriter writer;
    195        long lastModified = 0;
    196        final StringBuilder description;
    197        private String sep = "";
    198
    199        private Assembly(String description) throws UnsupportedEncodingException
    200        {
    201            writer = new PrintWriter(new OutputStreamWriter(outputStream, "UTF-8"));
    202
    203            this.description = new StringBuilder(description);
    204        }
    205
    206        void add(Resource resource, StreamableReader reader) throws IOException
    207        {
    208            writer.format("\n/* %s */;\n", resource.toString());
    209
    210            description.append(sep).append(resource.toString());
    211            sep = ", ";
    212
    213            StreamableResource streamable = streamableResourceSource.getStreamableResource(resource,
    214                    StreamableResourceProcessing.FOR_AGGREGATION, resourceChangeTracker);
    215
    216            writer.print(reader.read(streamable));
    217
    218            lastModified = Math.max(lastModified, streamable.getLastModified());
    219        }
    220
    221        StreamableResource finish()
    222        {
    223            writer.close();
    224
    225            return new StreamableResourceImpl(
    226                    description.toString(),
    227                    JAVASCRIPT_CONTENT_TYPE, CompressionStatus.COMPRESSABLE, lastModified,
    228                    new BytestreamCache(outputStream), checksumGenerator, null);
    229        }
    230    }
    231
    232    private StreamableResource assembleStreamableForStack(String localeName, Parameters parameters,
    233                                                          List<Asset> libraries, List<String> moduleNames) throws IOException
    234    {
    235        Assembly assembly = new Assembly(String.format("'%s' JavaScript stack, for locale %s, resources=", parameters.stackName, localeName));
    236
    237        for (Asset library : libraries)
    238        {
    239            Resource resource = library.getResource();
    240
    241            assembly.add(resource, libraryReader);
    242        }
    243
    244        for (String moduleName : moduleNames)
    245        {
    246            Resource resource = moduleManager.findResourceForModule(moduleName);
    247
    248            if (resource == null)
    249            {
    250                throw new IllegalArgumentException(String.format("Could not identify a resource for module name '%s'.", moduleName));
    251            }
    252
    253            assembly.add(resource, new ModuleReader(moduleName));
    254        }
    255
    256        StreamableResource streamable = assembly.finish();
    257
    258        if (minificationEnabled && parameters.javascriptAggregationStrategy.enablesMinimize())
    259        {
    260            return resourceMinimizer.minimize(streamable);
    261        }
    262
    263        return streamable;
    264    }
    265}