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}