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