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}