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 * <p> 189 * TBD: Security issues. Hackers can download .class files. 190 */ 191 192 public void service(IRequestCycle cycle) throws IOException 193 { 194 // If they were vended an asset in the past then it must be up-to date. 195 // Asset URIs change if the underlying file is modified. 196 197 if (_request.getHeader("If-Modified-Since") != null) 198 { 199 _response.setStatus(HttpServletResponse.SC_NOT_MODIFIED); 200 return; 201 } 202 203 String path = cycle.getParameter(PATH); 204 String md5 = cycle.getParameter(DIGEST); 205 206 try 207 { 208 if (!_digestSource.getDigestForResource(path).equals(md5)) 209 throw new ApplicationRuntimeException(AssetMessages.md5Mismatch(path)); 210 211 URL resourceURL = _classResolver.getResource(path); 212 213 if (resourceURL == null) 214 throw new ApplicationRuntimeException(AssetMessages.noSuchResource(path)); 215 216 URLConnection resourceConnection = resourceURL.openConnection(); 217 218 writeAssetContent(cycle, path, resourceConnection); 219 } 220 catch (Throwable ex) 221 { 222 _exceptionReporter.reportRequestException(AssetMessages.exceptionReportTitle(path), ex); 223 } 224 225 } 226 227 /** @since 2.2 */ 228 229 private void writeAssetContent(IRequestCycle cycle, String resourcePath, 230 URLConnection resourceConnection) throws IOException 231 { 232 InputStream input = null; 233 234 try 235 { 236 // Getting the content type and length is very dependant 237 // on support from the application server (represented 238 // here by the servletContext). 239 240 String contentType = getMimeType(resourcePath); 241 int contentLength = resourceConnection.getContentLength(); 242 243 if (contentLength > 0) 244 _response.setContentLength(contentLength); 245 246 _response.setDateHeader("Last-Modified", _startupTime); 247 _response.setDateHeader("Expires", _expireTime); 248 249 // Set the content type. If the servlet container doesn't 250 // provide it, try and guess it by the extension. 251 252 if (contentType == null || contentType.length() == 0) 253 contentType = getMimeType(resourcePath); 254 255 OutputStream output = _response.getOutputStream(new ContentType(contentType)); 256 257 input = new BufferedInputStream(resourceConnection.getInputStream()); 258 259 byte[] buffer = new byte[BUFFER_SIZE]; 260 261 while (true) 262 { 263 int bytesRead = input.read(buffer); 264 265 if (bytesRead < 0) 266 break; 267 268 output.write(buffer, 0, bytesRead); 269 } 270 271 input.close(); 272 input = null; 273 } 274 finally 275 { 276 IOUtils.close(input); 277 } 278 } 279 280 /** @since 4.0 */ 281 282 public void setExceptionReporter(RequestExceptionReporter exceptionReporter) 283 { 284 _exceptionReporter = exceptionReporter; 285 } 286 287 /** @since 4.0 */ 288 public void setLinkFactory(LinkFactory linkFactory) 289 { 290 _linkFactory = linkFactory; 291 } 292 293 /** @since 4.0 */ 294 public void setClassResolver(ClassResolver classResolver) 295 { 296 _classResolver = classResolver; 297 } 298 299 /** @since 4.0 */ 300 public void setContext(WebContext context) 301 { 302 _context = context; 303 } 304 305 /** @since 4.0 */ 306 public void setResponse(WebResponse response) 307 { 308 _response = response; 309 } 310 311 /** @since 4.0 */ 312 public void setDigestSource(ResourceDigestSource md5Source) 313 { 314 _digestSource = md5Source; 315 } 316 317 /** @since 4.0 */ 318 public void setRequest(WebRequest request) 319 { 320 _request = request; 321 } 322 }