    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}