001 // Copyright 2004, 2005 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 015 package org.apache.tapestry.asset; 016 017 import java.io.BufferedInputStream; 018 import java.io.IOException; 019 import java.io.InputStream; 020 import java.io.OutputStream; 021 import java.net.URL; 022 import java.net.URLConnection; 023 import java.util.HashMap; 024 import java.util.Map; 025 026 import javax.servlet.http.HttpServletResponse; 027 028 import org.apache.hivemind.ApplicationRuntimeException; 029 import org.apache.hivemind.ClassResolver; 030 import org.apache.hivemind.util.Defense; 031 import org.apache.hivemind.util.IOUtils; 032 import org.apache.tapestry.IRequestCycle; 033 import org.apache.tapestry.Tapestry; 034 import org.apache.tapestry.engine.IEngineService; 035 import org.apache.tapestry.engine.ILink; 036 import org.apache.tapestry.error.RequestExceptionReporter; 037 import org.apache.tapestry.services.LinkFactory; 038 import org.apache.tapestry.services.ServiceConstants; 039 import org.apache.tapestry.util.ContentType; 040 import org.apache.tapestry.web.WebContext; 041 import org.apache.tapestry.web.WebRequest; 042 import org.apache.tapestry.web.WebResponse; 043 044 /** 045 * A service for building URLs to and accessing {@link org.apache.tapestry.IAsset}s. Most of the 046 * work is deferred to the {@link org.apache.tapestry.IAsset}instance. 047 * <p> 048 * The retrieval part is directly linked to {@link PrivateAsset}. The service responds to a URL 049 * that encodes the path of a resource within the classpath. The {@link #service(IRequestCycle)} 050 * method reads the resource and streams it out. 051 * <p> 052 * TBD: Security issues. Should only be able to retrieve a resource that was previously registerred 053 * in some way ... otherwise, hackers will be able to suck out the .class files of the application! 054 * 055 * @author Howard Lewis Ship 056 */ 057 058 public class AssetService implements IEngineService 059 { 060 061 /** @since 4.0 */ 062 private ClassResolver _classResolver; 063 064 /** @since 4.0 */ 065 private LinkFactory _linkFactory; 066 067 /** @since 4.0 */ 068 private WebContext _context; 069 070 /** @since 4.0 */ 071 072 private WebRequest _request; 073 074 /** @since 4.0 */ 075 private WebResponse _response; 076 077 /** @since 4.0 */ 078 private ResourceDigestSource _digestSource; 079 080 /** 081 * Defaults MIME types, by extension, used when the servlet container doesn't provide MIME 082 * types. ServletExec Debugger, for example, fails to provide these. 083 */ 084 085 private final static Map _mimeTypes; 086 087 static 088 { 089 _mimeTypes = new HashMap(17); 090 _mimeTypes.put("css", "text/css"); 091 _mimeTypes.put("gif", "image/gif"); 092 _mimeTypes.put("jpg", "image/jpeg"); 093 _mimeTypes.put("jpeg", "image/jpeg"); 094 _mimeTypes.put("htm", "text/html"); 095 _mimeTypes.put("html", "text/html"); 096 } 097 098 private static final int BUFFER_SIZE = 10240; 099 100 /** 101 * Startup time for this service; used to set the Last-Modified response header. 102 * 103 * @since 4.0 104 */ 105 106 private final long _startupTime = System.currentTimeMillis(); 107 108 /** 109 * Time vended assets expire. Since a change in asset content is a change in asset URI, we want 110 * them to not expire ... but a year will do. 111 */ 112 113 private final long _expireTime = _startupTime + 365 * 24 * 60 * 60 * 1000; 114 115 /** @since 4.0 */ 116 117 private RequestExceptionReporter _exceptionReporter; 118 119 /** 120 * Query parameter that stores the path to the resource (with a leading slash). 121 * 122 * @since 4.0 123 */ 124 125 public static final String PATH = "path"; 126 127 /** 128 * Query parameter that stores the digest for the file; this is used to authenticate that the 129 * client is allowed to access the file. 130 * 131 * @since 4.0 132 */ 133 134 public static final String DIGEST = "digest"; 135 136 /** 137 * Builds a {@link ILink}for a {@link PrivateAsset}. 138 * <p> 139 * A single parameter is expected, the resource path of the asset (which is expected to start 140 * with a leading slash). 141 */ 142 143 public ILink getLink(IRequestCycle cycle, boolean post, Object parameter) 144 { 145 Defense.isAssignable(parameter, String.class, "parameter"); 146 147 String path = (String) parameter; 148 149 String digest = _digestSource.getDigestForResource(path); 150 151 Map parameters = new HashMap(); 152 153 parameters.put(ServiceConstants.SERVICE, getName()); 154 parameters.put(PATH, path); 155 parameters.put(DIGEST, digest); 156 157 // Service is stateless, which is the exception to the rule. 158 159 return _linkFactory.constructLink(cycle, post, parameters, false); 160 } 161 162 public String getName() 163 { 164 return Tapestry.ASSET_SERVICE; 165 } 166 167 private String getMimeType(String path) 168 { 169 String result = _context.getMimeType(path); 170 171 if (result == null) 172 { 173 int dotx = path.lastIndexOf('.'); 174 String key = path.substring(dotx + 1).toLowerCase(); 175 176 result = (String) _mimeTypes.get(key); 177 178 if (result == null) 179 result = "text/plain"; 180 } 181 182 return result; 183 } 184 185 /** 186 * Retrieves a resource from the classpath and returns it to the client in a binary output 187 * stream. 188 */ 189 190 public void service(IRequestCycle cycle) throws IOException 191 { 192 String path = cycle.getParameter(PATH); 193 String md5Digest = cycle.getParameter(DIGEST); 194 195 try 196 { 197 if (!_digestSource.getDigestForResource(path).equals(md5Digest)) 198 { 199 _response.sendError(HttpServletResponse.SC_FORBIDDEN, AssetMessages 200 .md5Mismatch(path)); 201 return; 202 } 203 204 // If they were vended an asset in the past then it must be up-to date. 205 // Asset URIs change if the underlying file is modified. 206 207 if (_request.getHeader("If-Modified-Since") != null) 208 { 209 _response.setStatus(HttpServletResponse.SC_NOT_MODIFIED); 210 return; 211 } 212 213 URL resourceURL = _classResolver.getResource(path); 214 215 if (resourceURL == null) 216 throw new ApplicationRuntimeException(AssetMessages.noSuchResource(path)); 217 218 URLConnection resourceConnection = resourceURL.openConnection(); 219 220 writeAssetContent(cycle, path, resourceConnection); 221 } 222 catch (Throwable ex) 223 { 224 _exceptionReporter.reportRequestException(AssetMessages.exceptionReportTitle(path), ex); 225 } 226 227 } 228 229 /** @since 2.2 */ 230 231 private void writeAssetContent(IRequestCycle cycle, String resourcePath, 232 URLConnection resourceConnection) throws IOException 233 { 234 InputStream input = null; 235 236 try 237 { 238 // Getting the content type and length is very dependant 239 // on support from the application server (represented 240 // here by the servletContext). 241 242 String contentType = getMimeType(resourcePath); 243 int contentLength = resourceConnection.getContentLength(); 244 245 if (contentLength > 0) 246 _response.setContentLength(contentLength); 247 248 _response.setDateHeader("Last-Modified", _startupTime); 249 _response.setDateHeader("Expires", _expireTime); 250 251 // Set the content type. If the servlet container doesn't 252 // provide it, try and guess it by the extension. 253 254 if (contentType == null || contentType.length() == 0) 255 contentType = getMimeType(resourcePath); 256 257 OutputStream output = _response.getOutputStream(new ContentType(contentType)); 258 259 input = new BufferedInputStream(resourceConnection.getInputStream()); 260 261 byte[] buffer = new byte[BUFFER_SIZE]; 262 263 while (true) 264 { 265 int bytesRead = input.read(buffer); 266 267 if (bytesRead < 0) 268 break; 269 270 output.write(buffer, 0, bytesRead); 271 } 272 273 input.close(); 274 input = null; 275 } 276 finally 277 { 278 IOUtils.close(input); 279 } 280 } 281 282 /** @since 4.0 */ 283 284 public void setExceptionReporter(RequestExceptionReporter exceptionReporter) 285 { 286 _exceptionReporter = exceptionReporter; 287 } 288 289 /** @since 4.0 */ 290 public void setLinkFactory(LinkFactory linkFactory) 291 { 292 _linkFactory = linkFactory; 293 } 294 295 /** @since 4.0 */ 296 public void setClassResolver(ClassResolver classResolver) 297 { 298 _classResolver = classResolver; 299 } 300 301 /** @since 4.0 */ 302 public void setContext(WebContext context) 303 { 304 _context = context; 305 } 306 307 /** @since 4.0 */ 308 public void setResponse(WebResponse response) 309 { 310 _response = response; 311 } 312 313 /** @since 4.0 */ 314 public void setDigestSource(ResourceDigestSource md5Source) 315 { 316 _digestSource = md5Source; 317 } 318 319 /** @since 4.0 */ 320 public void setRequest(WebRequest request) 321 { 322 _request = request; 323 } 324 }