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.webresources;
016
017import org.apache.tapestry5.SymbolConstants;
018import org.apache.tapestry5.internal.TapestryInternalUtils;
019import org.apache.tapestry5.internal.services.assets.BytestreamCache;
020import org.apache.tapestry5.ioc.IOOperation;
021import org.apache.tapestry5.ioc.OperationTracker;
022import org.apache.tapestry5.ioc.Resource;
023import org.apache.tapestry5.ioc.annotations.PostInjection;
024import org.apache.tapestry5.ioc.annotations.Symbol;
025import org.apache.tapestry5.ioc.internal.util.CollectionFactory;
026import org.apache.tapestry5.services.assets.ResourceDependencies;
027import org.apache.tapestry5.services.assets.ResourceTransformer;
028import org.apache.tapestry5.webresources.WebResourcesSymbols;
029import org.slf4j.Logger;
030
031import java.io.*;
032import java.util.Map;
033
034public class ResourceTransformerFactoryImpl implements ResourceTransformerFactory
035{
036    private final Logger logger;
037
038    private final OperationTracker tracker;
039
040    private final boolean productionMode;
041
042    private final File cacheDir;
043
044    public ResourceTransformerFactoryImpl(Logger logger, OperationTracker tracker,
045                                          @Symbol(SymbolConstants.PRODUCTION_MODE)
046                                          boolean productionMode,
047                                          @Symbol(WebResourcesSymbols.CACHE_DIR)
048                                          String cacheDir)
049    {
050        this.logger = logger;
051        this.tracker = tracker;
052        this.productionMode = productionMode;
053
054        this.cacheDir = new File(cacheDir);
055
056        if (!productionMode)
057        {
058            logger.info(String.format("Using %s to store compiled assets (development mode only).", cacheDir));
059        }
060    }
061
062    @PostInjection
063    public void createCacheDir()
064    {
065        cacheDir.mkdirs();
066    }
067
068    static class Compiled extends ContentChangeTracker
069    {
070        private BytestreamCache bytestreamCache;
071
072        Compiled(Resource root)
073        {
074            addDependency(root);
075        }
076
077        void store(InputStream stream) throws IOException
078        {
079            ByteArrayOutputStream bos = new ByteArrayOutputStream();
080
081            TapestryInternalUtils.copy(stream, bos);
082
083            stream.close();
084            bos.close();
085
086            this.bytestreamCache = new BytestreamCache(bos);
087        }
088
089        InputStream openStream()
090        {
091            return bytestreamCache.openStream();
092        }
093    }
094
095
096    public ResourceTransformer createCompiler(String contentType, String sourceName, String targetName, ResourceTransformer transformer, CacheMode cacheMode)
097    {
098        ResourceTransformer trackingCompiler = wrapWithTracking(sourceName, targetName, transformer);
099
100        if (productionMode)
101        {
102            return trackingCompiler;
103        }
104
105        ResourceTransformer timingCompiler = wrapWithTiming(targetName, trackingCompiler);
106
107        switch (cacheMode)
108        {
109            case NONE:
110
111                return timingCompiler;
112
113            case SINGLE_FILE:
114
115                return wrapWithFileSystemCaching(timingCompiler, targetName);
116
117            case MULTIPLE_FILE:
118
119                return wrapWithInMemoryCaching(timingCompiler, targetName);
120
121            default:
122
123                throw new IllegalStateException();
124        }
125    }
126
127    private ResourceTransformer wrapWithTracking(final String sourceName, final String targetName, final ResourceTransformer core)
128    {
129        return new ResourceTransformer()
130        {
131            public String getTransformedContentType()
132            {
133                return core.getTransformedContentType();
134            }
135
136            public InputStream transform(final Resource source, final ResourceDependencies dependencies) throws IOException
137            {
138                final String description = String.format("Compiling %s from %s to %s", source, sourceName, targetName);
139
140                return tracker.perform(description, new IOOperation<InputStream>()
141                {
142                    public InputStream perform() throws IOException
143                    {
144                        return core.transform(source, dependencies);
145                    }
146                });
147            }
148        };
149    }
150
151    private ResourceTransformer wrapWithTiming(final String targetName, final ResourceTransformer coreCompiler)
152    {
153        return new ResourceTransformer()
154        {
155            public String getTransformedContentType()
156            {
157                return coreCompiler.getTransformedContentType();
158            }
159
160            public InputStream transform(final Resource source, final ResourceDependencies dependencies) throws IOException
161            {
162                final long startTime = System.nanoTime();
163
164                InputStream result = coreCompiler.transform(source, dependencies);
165
166                final long elapsedTime = System.nanoTime() - startTime;
167
168                logger.info(String.format("Compiled %s to %s in %.2f ms",
169                        source, targetName,
170                        ResourceTransformUtils.nanosToMillis(elapsedTime)));
171
172                return result;
173            }
174        };
175    }
176
177    /**
178     * Caching is not needed in production, because caching of streamable resources occurs at a higher level
179     * (possibly after sources have been aggregated and minimized and gzipped). However, in development, it is
180     * very important to avoid costly CoffeeScript compilation (or similar operations); Tapestry's caching is
181     * somewhat primitive: a change to *any* resource in a given domain results in the cache of all of those resources
182     * being discarded.
183     */
184    private ResourceTransformer wrapWithInMemoryCaching(final ResourceTransformer core, final String targetName)
185    {
186        return new ResourceTransformer()
187        {
188            final Map<Resource, Compiled> cache = CollectionFactory.newConcurrentMap();
189
190            public String getTransformedContentType()
191            {
192                return core.getTransformedContentType();
193            }
194
195            public InputStream transform(Resource source, ResourceDependencies dependencies) throws IOException
196            {
197                Compiled compiled = cache.get(source);
198
199                if (compiled != null && !compiled.dirty())
200                {
201                    logger.info(String.format("Resource %s and dependencies are unchanged; serving compiled %s content from in-memory cache",
202                            source, targetName));
203
204                    return compiled.openStream();
205                }
206
207                compiled = new Compiled(source);
208
209                InputStream is = core.transform(source, new ResourceDependenciesSplitter(dependencies, compiled));
210
211                compiled.store(is);
212
213                is.close();
214
215                cache.put(source, compiled);
216
217                return compiled.openStream();
218            }
219        };
220    }
221
222    private ResourceTransformer wrapWithFileSystemCaching(final ResourceTransformer core, final String targetName)
223    {
224        return new ResourceTransformer()
225        {
226            public String getTransformedContentType()
227            {
228                return core.getTransformedContentType();
229            }
230
231            public InputStream transform(Resource source, ResourceDependencies dependencies) throws IOException
232            {
233                long checksum = ResourceTransformUtils.toChecksum(source);
234
235                String fileName = Long.toHexString(checksum) + "-" + source.getFile();
236
237                File cacheFile = new File(cacheDir, fileName);
238
239                if (cacheFile.exists())
240                {
241                    logger.debug(String.format("Serving up compiled %s content for %s from file system cache", targetName, source));
242
243                    return new BufferedInputStream(new FileInputStream(cacheFile));
244                }
245
246                InputStream compiled = core.transform(source, dependencies);
247
248                // We need the InputStream twice; once to return, and once to write out to the cache file for later.
249
250                ByteArrayOutputStream bos = new ByteArrayOutputStream();
251
252                TapestryInternalUtils.copy(compiled, bos);
253
254                compiled.close();
255
256                BytestreamCache cache = new BytestreamCache(bos);
257
258                writeToCacheFile(cacheFile, cache.openStream());
259
260                return cache.openStream();
261            }
262        };
263    }
264
265    private void writeToCacheFile(File file, InputStream stream) throws IOException
266    {
267        OutputStream outputStream = new BufferedOutputStream(new FileOutputStream(file));
268
269        TapestryInternalUtils.copy(stream, outputStream);
270
271        outputStream.close();
272    }
273}