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