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}