    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;
    014
    015import org.apache.tapestry5.Asset;
    016import org.apache.tapestry5.SymbolConstants;
    017import org.apache.tapestry5.internal.InternalConstants;
    018import org.apache.tapestry5.internal.services.assets.ResourceChangeTracker;
    019import org.apache.tapestry5.ioc.IOOperation;
    020import org.apache.tapestry5.ioc.OperationTracker;
    021import org.apache.tapestry5.ioc.Resource;
    022import org.apache.tapestry5.ioc.annotations.InjectService;
    023import org.apache.tapestry5.ioc.annotations.Symbol;
    024import org.apache.tapestry5.services.AssetFactory;
    025import org.apache.tapestry5.services.Request;
    026import org.apache.tapestry5.services.Response;
    027import org.apache.tapestry5.services.assets.*;
    028
    029import javax.servlet.http.HttpServletResponse;
    030import java.io.IOException;
    031import java.io.OutputStream;
    032import java.util.Set;
    033
    034public class ResourceStreamerImpl implements ResourceStreamer
    035{
    036    static final String IF_MODIFIED_SINCE_HEADER = "If-Modified-Since";
    037
    038    private static final String QUOTE = "\"";
    039
    040    private final Request request;
    041
    042    private final Response response;
    043
    044    private final StreamableResourceSource streamableResourceSource;
    045
    046    private final boolean productionMode;
    047
    048    private final OperationTracker tracker;
    049
    050    private final ResourceChangeTracker resourceChangeTracker;
    051
    052    private final String omitExpirationCacheControlHeader;
    053
    054    private final AssetFactory classpathAssetFactory;
    055
    056    private final AssetFactory contextAssetFactory;
    057
    058    public ResourceStreamerImpl(Request request,
    059
    060                                Response response,
    061
    062                                StreamableResourceSource streamableResourceSource,
    063
    064                                OperationTracker tracker,
    065
    066                                @Symbol(SymbolConstants.PRODUCTION_MODE)
    067                                boolean productionMode,
    068
    069                                ResourceChangeTracker resourceChangeTracker,
    070
    071                                @Symbol(SymbolConstants.OMIT_EXPIRATION_CACHE_CONTROL_HEADER)
    072                                String omitExpirationCacheControlHeader,
    073
    074                                @InjectService("ClasspathAssetFactory")
    075                                AssetFactory classpathAssetFactory,
    076
    077                                @InjectService("ContextAssetFactory")
    078                                AssetFactory contextAssetFactory)
    079    {
    080        this.request = request;
    081        this.response = response;
    082        this.streamableResourceSource = streamableResourceSource;
    083
    084        this.tracker = tracker;
    085        this.productionMode = productionMode;
    086        this.resourceChangeTracker = resourceChangeTracker;
    087        this.omitExpirationCacheControlHeader = omitExpirationCacheControlHeader;
    088
    089        this.classpathAssetFactory = classpathAssetFactory;
    090        this.contextAssetFactory = contextAssetFactory;
    091    }
    092
    093    public boolean streamResource(final Resource resource, final String providedChecksum, final Set<Options> options) throws IOException
    094    {
    095        if (!resource.exists())
    096        {
    097            // TODO: Or should we just return false here and not send back a specific error with the (eventual) 404?
    098
    099            response.sendError(HttpServletResponse.SC_NOT_FOUND, String.format("Unable to locate asset '%s' (the file does not exist).", resource));
    100
    101            return true;
    102        }
    103
    104        final boolean compress = providedChecksum.startsWith("z");
    105
    106        return tracker.perform("Streaming " + resource + (compress ? " (compressed)" : ""), new IOOperation<Boolean>()
    107        {
    108            public Boolean perform() throws IOException
    109            {
    110                StreamableResourceProcessing processing = compress
    111                        ? StreamableResourceProcessing.COMPRESSION_ENABLED
    112                        : StreamableResourceProcessing.COMPRESSION_DISABLED;
    113
    114                StreamableResource streamable = streamableResourceSource.getStreamableResource(resource, processing, resourceChangeTracker);
    115
    116                return streamResource(resource, streamable, compress ? providedChecksum.substring(1) : providedChecksum, options);
    117            }
    118        });
    119    }
    120
    121    public boolean streamResource(StreamableResource streamable, String providedChecksum, Set<Options> options) throws IOException
    122    {
    123        return streamResource(null, streamable, providedChecksum, options);
    124    }
    125
    126    public boolean streamResource(Resource resource, StreamableResource streamable, String providedChecksum, Set<Options> options) throws IOException
    127    {
    128        assert streamable != null;
    129        assert providedChecksum != null;
    130        assert options != null;
    131
    132        String actualChecksum = streamable.getChecksum();
    133
    134        if (providedChecksum.length() > 0 && !providedChecksum.equals(actualChecksum))
    135        {
    136
    137            // TAP5-2185: Trying to find the wrongly-checksummed resource in the classpath and context,
    138            // so we can create an Asset with the correct checksum and redirect to it.
    139            Asset asset = null;
    140            if (resource != null)
    141            {
    142                asset = findAssetInsideWebapp(resource);
    143            }
    144            if (asset != null)
    145            {
    146                response.sendRedirect(asset.toClientURL());
    147                return true;
    148            }
    149            return false;
    150        }
    151
    152
    153        // ETag should be surrounded with quotes.
    154        String token = QUOTE + actualChecksum + QUOTE;
    155
    156        // Even when sending a 304, we want the ETag associated with the request.
    157        // In most cases (except JavaScript modules), the checksum is also embedded into the URL.
    158        // However, E-Tags are also useful for enabling caching inside intermediate servers, CDNs, etc.
    159        response.setHeader("ETag", token);
    160
    161        // If the client can send the correct ETag token, then its cache already contains the correct
    162        // content.
    163        String providedToken = request.getHeader("If-None-Match");
    164
    165        if (token.equals(providedToken))
    166        {
    167            response.setStatus(HttpServletResponse.SC_NOT_MODIFIED);
    168            return true;
    169        }
    170
    171        long lastModified = streamable.getLastModified();
    172
    173        long ifModifiedSince;
    174
    175        try
    176        {
    177            ifModifiedSince = request.getDateHeader(IF_MODIFIED_SINCE_HEADER);
    178        } catch (IllegalArgumentException ex)
    179        {
    180            // Simulate the header being missing if it is poorly formatted.
    181
    182            ifModifiedSince = -1;
    183        }
    184
    185        if (ifModifiedSince > 0 && ifModifiedSince >= lastModified)
    186        {
    187            response.setStatus(HttpServletResponse.SC_NOT_MODIFIED);
    188            return true;
    189        }
    190
    191        // Prevent the upstream code from compressing when we don't want to.
    192
    193        response.disableCompression();
    194
    195        response.setDateHeader("Last-Modified", lastModified);
    196
    197
    198        if (productionMode && !options.contains(Options.OMIT_EXPIRATION))
    199        {
    200            // Starting in 5.4, this is a lot less necessary; any change to a Resource will result
    201            // in a new asset URL with the changed checksum incorporated into the URL.
    202            response.setDateHeader("Expires", lastModified + InternalConstants.TEN_YEARS);
    203        }
    204
    205        // This is really for modules, which can not have a content hash code in the URL; therefore, we want
    206        // the browser to re-validate the resources on each new page render; because of the ETags, that will
    207        // mostly result in quick SC_NOT_MODIFIED responses.
    208        if (options.contains(Options.OMIT_EXPIRATION))
    209        {
    210            response.setHeader("Cache-Control", omitExpirationCacheControlHeader);
    211        }
    212
    213        response.setContentLength(streamable.getSize());
    214
    215        if (streamable.getCompression() == CompressionStatus.COMPRESSED)
    216        {
    217            response.setHeader(InternalConstants.CONTENT_ENCODING_HEADER, InternalConstants.GZIP_CONTENT_ENCODING);
    218        }
    219
    220        ResponseCustomizer responseCustomizer = streamable.getResponseCustomizer();
    221
    222        if (responseCustomizer != null)
    223        {
    224            responseCustomizer.customizeResponse(streamable, response);
    225        }
    226
    227        OutputStream os = response.getOutputStream(streamable.getContentType().toString());
    228
    229        streamable.streamTo(os);
    230
    231        os.close();
    232
    233        return true;
    234    }
    235
    236    private Asset findAssetInsideWebapp(Resource resource)
    237    {
    238        Asset asset;
    239        asset = findAssetFromClasspath(resource);
    240        if (asset == null)
    241        {
    242            asset = findAssetFromContext(resource);
    243        }
    244        return asset;
    245    }
    246
    247    private Asset findAssetFromContext(Resource resource)
    248    {
    249        Asset asset = null;
    250        try
    251        {
    252            asset = contextAssetFactory.createAsset(resource);
    253        }
    254        catch (RuntimeException e)
    255        {
    256            // not an existing context asset. go ahead.
    257        }
    258        return asset;
    259    }
    260
    261    private Asset findAssetFromClasspath(Resource resource)
    262    {
    263        Asset asset = null;
    264        try
    265        {
    266            asset = classpathAssetFactory.createAsset(resource);
    267        }
    268        catch (RuntimeException e)
    269        {
    270            // not an existing classpath asset. go ahead.
    271        }
    272        return asset;
    273    }
    274
    275}