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