001// Copyright 2006-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;
016
017import org.apache.tapestry5.SymbolConstants;
018import org.apache.tapestry5.internal.InternalConstants;
019import org.apache.tapestry5.internal.services.assets.ResourceChangeTracker;
020import org.apache.tapestry5.ioc.IOOperation;
021import org.apache.tapestry5.ioc.OperationTracker;
022import org.apache.tapestry5.ioc.Resource;
023import org.apache.tapestry5.ioc.annotations.Symbol;
024import org.apache.tapestry5.services.Request;
025import org.apache.tapestry5.services.Response;
026import org.apache.tapestry5.services.assets.CompressionStatus;
027import org.apache.tapestry5.services.assets.StreamableResource;
028import org.apache.tapestry5.services.assets.StreamableResourceProcessing;
029import org.apache.tapestry5.services.assets.StreamableResourceSource;
030
031import javax.servlet.http.HttpServletResponse;
032import java.io.IOException;
033import java.io.OutputStream;
034import java.util.Set;
035
036public class ResourceStreamerImpl implements ResourceStreamer
037{
038    static final String IF_MODIFIED_SINCE_HEADER = "If-Modified-Since";
039
040    private static final String QUOTE = "\"";
041
042    private final Request request;
043
044    private final Response response;
045
046    private final StreamableResourceSource streamableResourceSource;
047
048    private final boolean productionMode;
049
050    private final OperationTracker tracker;
051
052    private final ResourceChangeTracker resourceChangeTracker;
053
054    public ResourceStreamerImpl(Request request,
055
056                                Response response,
057
058                                StreamableResourceSource streamableResourceSource,
059
060                                OperationTracker tracker,
061
062                                @Symbol(SymbolConstants.PRODUCTION_MODE)
063                                boolean productionMode,
064
065                                ResourceChangeTracker resourceChangeTracker)
066    {
067        this.request = request;
068        this.response = response;
069        this.streamableResourceSource = streamableResourceSource;
070
071        this.tracker = tracker;
072        this.productionMode = productionMode;
073        this.resourceChangeTracker = resourceChangeTracker;
074    }
075
076    public boolean streamResource(final Resource resource, final String providedChecksum, final Set<Options> options) throws IOException
077    {
078        if (!resource.exists())
079        {
080            // TODO: Or should we just return false here and not send back a specific error with the (eventual) 404?
081
082            response.sendError(HttpServletResponse.SC_NOT_FOUND, String.format("Unable to locate asset '%s' (the file does not exist).", resource));
083
084            return true;
085        }
086
087        final boolean compress = providedChecksum.startsWith("z");
088
089        return tracker.perform(String.format("Streaming %s%s", resource, compress ? " (compressed)" : ""), new IOOperation<Boolean>()
090        {
091            public Boolean perform() throws IOException
092            {
093                StreamableResourceProcessing processing = compress
094                        ? StreamableResourceProcessing.COMPRESSION_ENABLED
095                        : StreamableResourceProcessing.COMPRESSION_DISABLED;
096
097                StreamableResource streamable = streamableResourceSource.getStreamableResource(resource, processing, resourceChangeTracker);
098
099                return streamResource(streamable, compress ? providedChecksum.substring(1) : providedChecksum, options);
100            }
101        });
102    }
103
104    public boolean streamResource(StreamableResource streamable, String providedChecksum, Set<Options> options) throws IOException
105    {
106        assert streamable != null;
107        assert providedChecksum != null;
108        assert options != null;
109
110        String actualChecksum = streamable.getChecksum();
111
112        if (providedChecksum.length() > 0 && !providedChecksum.equals(actualChecksum))
113        {
114            return false;
115        }
116
117        long lastModified = streamable.getLastModified();
118
119        long ifModifiedSince;
120
121        try
122        {
123            ifModifiedSince = request.getDateHeader(IF_MODIFIED_SINCE_HEADER);
124        } catch (IllegalArgumentException ex)
125        {
126            // Simulate the header being missing if it is poorly formatted.
127
128            ifModifiedSince = -1;
129        }
130
131        if (ifModifiedSince > 0 && ifModifiedSince >= lastModified)
132        {
133            response.setStatus(HttpServletResponse.SC_NOT_MODIFIED);
134            return true;
135        }
136
137        // ETag should be surrounded with quotes.
138        String token = QUOTE + actualChecksum + QUOTE;
139
140        // Even when sending a 304, we want the ETag associated with the request.
141        // In most cases (except JavaScript modules), the checksum is also embedded into the URL.
142        // However, E-Tags are also useful for enabling caching inside intermediate servers, CDNs, etc.
143        response.setHeader("ETag", token);
144
145        // If the client can send the correct ETag token, then its cache already contains the correct
146        // content.
147        String providedToken = request.getHeader("If-None-Match");
148
149        if (token.equals(providedToken))
150        {
151            response.setStatus(HttpServletResponse.SC_NOT_MODIFIED);
152            return true;
153        }
154
155        // Prevent the upstream code from compressing when we don't want to.
156
157        response.disableCompression();
158
159        response.setDateHeader("Last-Modified", lastModified);
160
161
162        if (productionMode && !options.contains(Options.OMIT_EXPIRATION))
163        {
164            // Starting in 5.4, this is a lot less necessary; any change to a Resource will result
165            // in a new asset URL with the changed checksum incorporated into the URL.
166            response.setDateHeader("Expires", lastModified + InternalConstants.TEN_YEARS);
167        }
168
169        // This is really for modules, which can not have a content hash code in the URL; therefore, we want
170        // the browser to re-validate the resources on each new page render; because of the ETags, that will
171        // mostly result in quick SC_NOT_MODIFIED responses.
172        if (options.contains(Options.OMIT_EXPIRATION))
173        {
174            response.setHeader("Cache-Control", "max-age=0, must-revalidate");
175        }
176
177        response.setContentLength(streamable.getSize());
178
179        if (streamable.getCompression() == CompressionStatus.COMPRESSED)
180        {
181            response.setHeader(InternalConstants.CONTENT_ENCODING_HEADER, InternalConstants.GZIP_CONTENT_ENCODING);
182        }
183
184        OutputStream os = response.getOutputStream(streamable.getContentType());
185
186        streamable.streamTo(os);
187
188        os.close();
189
190        return true;
191    }
192
193}