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