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