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 015package org.apache.tapestry.asset; 016 017import java.io.BufferedInputStream; 018import java.io.IOException; 019import java.io.InputStream; 020import java.io.OutputStream; 021import java.net.URL; 022import java.net.URLConnection; 023import java.util.HashMap; 024import java.util.Map; 025 026import javax.servlet.http.HttpServletResponse; 027 028import org.apache.hivemind.ApplicationRuntimeException; 029import org.apache.hivemind.ClassResolver; 030import org.apache.hivemind.util.Defense; 031import org.apache.hivemind.util.IOUtils; 032import org.apache.tapestry.IRequestCycle; 033import org.apache.tapestry.Tapestry; 034import org.apache.tapestry.engine.IEngineService; 035import org.apache.tapestry.engine.ILink; 036import org.apache.tapestry.error.RequestExceptionReporter; 037import org.apache.tapestry.services.LinkFactory; 038import org.apache.tapestry.services.ServiceConstants; 039import org.apache.tapestry.util.ContentType; 040import org.apache.tapestry.web.WebContext; 041import org.apache.tapestry.web.WebRequest; 042import 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 058public 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(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(this, 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 if (dotx > -1) { 175 String key = path.substring(dotx + 1).toLowerCase(); 176 result = (String) _mimeTypes.get(key); 177 } 178 179 if (result == null) 180 result = "text/plain"; 181 } 182 183 return result; 184 } 185 186 /** 187 * Retrieves a resource from the classpath and returns it to the client in a binary output 188 * stream. 189 */ 190 191 public void service(IRequestCycle cycle) throws IOException 192 { 193 String path = cycle.getParameter(PATH); 194 String md5Digest = cycle.getParameter(DIGEST); 195 196 try 197 { 198 if (!_digestSource.getDigestForResource(path).equals(md5Digest)) 199 { 200 _response.sendError(HttpServletResponse.SC_FORBIDDEN, AssetMessages 201 .md5Mismatch(path)); 202 return; 203 } 204 205 // If they were vended an asset in the past then it must be up-to date. 206 // Asset URIs change if the underlying file is modified. 207 208 if (_request.getHeader("If-Modified-Since") != null) 209 { 210 _response.setStatus(HttpServletResponse.SC_NOT_MODIFIED); 211 return; 212 } 213 214 URL resourceURL = _classResolver.getResource(path); 215 216 if (resourceURL == null) 217 throw new ApplicationRuntimeException(AssetMessages.noSuchResource(path)); 218 219 URLConnection resourceConnection = resourceURL.openConnection(); 220 221 writeAssetContent(cycle, path, resourceConnection); 222 } 223 catch (Throwable ex) 224 { 225 _exceptionReporter.reportRequestException(AssetMessages.exceptionReportTitle(path), ex); 226 } 227 228 } 229 230 /** @since 2.2 */ 231 232 private void writeAssetContent(IRequestCycle cycle, String resourcePath, 233 URLConnection resourceConnection) throws IOException 234 { 235 InputStream input = null; 236 237 try 238 { 239 // Getting the content type and length is very dependant 240 // on support from the application server (represented 241 // here by the servletContext). 242 243 String contentType = getMimeType(resourcePath); 244 int contentLength = resourceConnection.getContentLength(); 245 246 if (contentLength > 0) 247 _response.setContentLength(contentLength); 248 249 _response.setDateHeader("Last-Modified", _startupTime); 250 _response.setDateHeader("Expires", _expireTime); 251 252 // Set the content type. If the servlet container doesn't 253 // provide it, try and guess it by the extension. 254 255 if (contentType == null || contentType.length() == 0) 256 contentType = getMimeType(resourcePath); 257 258 OutputStream output = _response.getOutputStream(new ContentType(contentType)); 259 260 input = new BufferedInputStream(resourceConnection.getInputStream()); 261 262 byte[] buffer = new byte[BUFFER_SIZE]; 263 264 while (true) 265 { 266 int bytesRead = input.read(buffer); 267 268 if (bytesRead < 0) 269 break; 270 271 output.write(buffer, 0, bytesRead); 272 } 273 274 input.close(); 275 input = null; 276 } 277 finally 278 { 279 IOUtils.close(input); 280 } 281 } 282 283 /** @since 4.0 */ 284 285 public void setExceptionReporter(RequestExceptionReporter exceptionReporter) 286 { 287 _exceptionReporter = exceptionReporter; 288 } 289 290 /** @since 4.0 */ 291 public void setLinkFactory(LinkFactory linkFactory) 292 { 293 _linkFactory = linkFactory; 294 } 295 296 /** @since 4.0 */ 297 public void setClassResolver(ClassResolver classResolver) 298 { 299 _classResolver = classResolver; 300 } 301 302 /** @since 4.0 */ 303 public void setContext(WebContext context) 304 { 305 _context = context; 306 } 307 308 /** @since 4.0 */ 309 public void setResponse(WebResponse response) 310 { 311 _response = response; 312 } 313 314 /** @since 4.0 */ 315 public void setDigestSource(ResourceDigestSource md5Source) 316 { 317 _digestSource = md5Source; 318 } 319 320 /** @since 4.0 */ 321 public void setRequest(WebRequest request) 322 { 323 _request = request; 324 } 325}