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}