Coverage Report - org.apache.tapestry.asset.AssetService
 
Classes in this File Line Coverage Branch Coverage Complexity
AssetService
69% 
83% 
2.824
 
 1  
 // Copyright 2004, 2005 The Apache Software Foundation
 2  
 //
 3  
 // Licensed under the Apache License, Version 2.0 (the "License");
 4  
 // you may not use this file except in compliance with the License.
 5  
 // You may obtain a copy of the License at
 6  
 //
 7  
 //     http://www.apache.org/licenses/LICENSE-2.0
 8  
 //
 9  
 // Unless required by applicable law or agreed to in writing, software
 10  
 // distributed under the License is distributed on an "AS IS" BASIS,
 11  
 // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 12  
 // See the License for the specific language governing permissions and
 13  
 // limitations under the License.
 14  
 
 15  
 package org.apache.tapestry.asset;
 16  
 
 17  
 import org.apache.commons.io.FilenameUtils;
 18  
 import org.apache.commons.io.IOUtils;
 19  
 import org.apache.commons.logging.Log;
 20  
 import org.apache.hivemind.ClassResolver;
 21  
 import org.apache.hivemind.util.Defense;
 22  
 import org.apache.tapestry.IRequestCycle;
 23  
 import org.apache.tapestry.Tapestry;
 24  
 import org.apache.tapestry.engine.IEngineService;
 25  
 import org.apache.tapestry.engine.ILink;
 26  
 import org.apache.tapestry.error.RequestExceptionReporter;
 27  
 import org.apache.tapestry.services.LinkFactory;
 28  
 import org.apache.tapestry.services.ServiceConstants;
 29  
 import org.apache.tapestry.util.ContentType;
 30  
 import org.apache.tapestry.util.io.GzipUtil;
 31  
 import org.apache.tapestry.web.WebContext;
 32  
 import org.apache.tapestry.web.WebRequest;
 33  
 import org.apache.tapestry.web.WebResponse;
 34  
 
 35  
 import javax.servlet.http.HttpServletResponse;
 36  
 import java.io.ByteArrayOutputStream;
 37  
 import java.io.IOException;
 38  
 import java.io.InputStream;
 39  
 import java.io.OutputStream;
 40  
 import java.net.URL;
 41  
 import java.net.URLConnection;
 42  
 import java.util.HashMap;
 43  
 import java.util.Map;
 44  
 import java.util.TreeMap;
 45  
 import java.util.zip.GZIPOutputStream;
 46  
 
 47  
 /**
 48  
  * A service for building URLs to and accessing {@link org.apache.tapestry.IAsset}s. Most of the
 49  
  * work is deferred to the {@link org.apache.tapestry.IAsset}instance.
 50  
  * <p>
 51  
  * The retrieval part is directly linked to {@link PrivateAsset}. The service responds to a URL
 52  
  * that encodes the path of a resource within the classpath. The {@link #service(IRequestCycle)}
 53  
  * method reads the resource and streams it out.
 54  
  * <p>
 55  
  * TBD: Security issues. Should only be able to retrieve a resource that was previously registerred
 56  
  * in some way ... otherwise, hackers will be able to suck out the .class files of the application!
 57  
  * 
 58  
  * @author Howard Lewis Ship
 59  
  */
 60  
 
 61  6
 public class AssetService implements IEngineService
 62  
 {
 63  
     /**
 64  
      * Query parameter that stores the path to the resource (with a leading slash).
 65  
      * 
 66  
      * @since 4.0
 67  
      */
 68  
 
 69  
     public static final String PATH = "path";
 70  
 
 71  
     /**
 72  
      * Query parameter that stores the digest for the file; this is used to authenticate that the
 73  
      * client is allowed to access the file.
 74  
      * 
 75  
      * @since 4.0
 76  
      */
 77  
 
 78  
     public static final String DIGEST = "digest";
 79  
 
 80  
     /**
 81  
      * Defaults MIME types, by extension, used when the servlet container doesn't provide MIME
 82  
      * types. ServletExec Debugger, for example, fails to provide these.
 83  
      */
 84  
 
 85  
     private static final Map _mimeTypes;
 86  
 
 87  
     static
 88  
     {
 89  1
         _mimeTypes = new HashMap(17);
 90  1
         _mimeTypes.put("css", "text/css");
 91  1
         _mimeTypes.put("gif", "image/gif");
 92  1
         _mimeTypes.put("jpg", "image/jpeg");
 93  1
         _mimeTypes.put("jpeg", "image/jpeg");
 94  1
         _mimeTypes.put("png", "image/png");
 95  1
         _mimeTypes.put("htm", "text/html");
 96  1
         _mimeTypes.put("html", "text/html");
 97  1
     }
 98  
     
 99  
     /** Represents a month of time in seconds. */
 100  
     static final long MONTH_SECONDS = 60 * 60 * 24 * 30;
 101  
     
 102  
     private Log _log;
 103  
     
 104  
     /** @since 4.0 */
 105  
     private ClassResolver _classResolver;
 106  
 
 107  
     /** @since 4.0 */
 108  
     private LinkFactory _linkFactory;
 109  
 
 110  
     /** @since 4.0 */
 111  
     private WebContext _context;
 112  
 
 113  
     /** @since 4.0 */
 114  
 
 115  
     private WebRequest _request;
 116  
 
 117  
     /** @since 4.0 */
 118  
     private WebResponse _response;
 119  
 
 120  
     /** @since 4.0 */
 121  
     private ResourceDigestSource _digestSource;
 122  
 
 123  
     /** @since 4.1 */
 124  
     private ResourceMatcher _unprotectedMatcher;
 125  
     
 126  
     /**
 127  
      * Startup time for this service; used to set the Last-Modified response header.
 128  
      * 
 129  
      * @since 4.0
 130  
      */
 131  
 
 132  6
     private final long _startupTime = System.currentTimeMillis();
 133  
 
 134  
     /**
 135  
      * Time vended assets expire. Since a change in asset content is a change in asset URI, we want
 136  
      * them to not expire ... but a year will do.
 137  
      */
 138  
 
 139  6
     final long _expireTime = _startupTime + 365 * 24 * 60 * 60 * 1000L;
 140  
 
 141  
     /** @since 4.0 */
 142  
 
 143  
     private RequestExceptionReporter _exceptionReporter;
 144  
 
 145  
     /**
 146  
      * Cache of static content resources.
 147  
      */
 148  6
     private final Map _cache = new HashMap();
 149  
     
 150  
     /**
 151  
      * Builds a {@link ILink}for a {@link PrivateAsset}.
 152  
      * <p>
 153  
      * A single parameter is expected, the resource path of the asset (which is expected to start
 154  
      * with a leading slash).
 155  
      */
 156  
 
 157  
     public ILink getLink(boolean post, Object parameter)
 158  
     {
 159  2
         Defense.isAssignable(parameter, String.class, "parameter");
 160  
 
 161  1
         String path = (String) parameter;
 162  1
         String digest = null;
 163  
         
 164  1
         if(!_unprotectedMatcher.containsResource(path))
 165  0
             digest = _digestSource.getDigestForResource(path);
 166  
         
 167  1
         Map parameters = new TreeMap(new AssetComparator());
 168  
         
 169  1
         parameters.put(ServiceConstants.SERVICE, getName());
 170  1
         parameters.put(PATH, path);
 171  
         
 172  1
         if (digest != null)
 173  0
             parameters.put(DIGEST, digest);
 174  
         
 175  
         // Service is stateless, which is the exception to the rule.
 176  
         
 177  1
         return _linkFactory.constructLink(this, post, parameters, false);
 178  
     }
 179  
 
 180  
     public String getName()
 181  
     {
 182  1
         return Tapestry.ASSET_SERVICE;
 183  
     }
 184  
 
 185  
     private String getMimeType(String path)
 186  
     {
 187  1
         String result = _context.getMimeType(path);
 188  
         
 189  1
         if (result == null)
 190  
         {
 191  0
             int dotx = path.lastIndexOf('.');
 192  0
             if (dotx > -1)
 193  
             {
 194  0
                 String key = path.substring(dotx + 1).toLowerCase();
 195  0
                 result = (String) _mimeTypes.get(key);
 196  
             }
 197  
             
 198  0
             if (result == null)
 199  0
                 result = "text/plain";
 200  
         }
 201  
 
 202  1
         return result;
 203  
     }
 204  
     
 205  
     /**
 206  
      * Retrieves a resource from the classpath and returns it to the client in a binary output
 207  
      * stream.
 208  
      */
 209  
 
 210  
     public void service(IRequestCycle cycle)
 211  
             throws IOException
 212  
     {
 213  1
         String path = cycle.getParameter(PATH);
 214  1
         String md5Digest = cycle.getParameter(DIGEST);
 215  1
         boolean checkDigest = !_unprotectedMatcher.containsResource(path);
 216  
         
 217  
         URLConnection resourceConnection;
 218  
         
 219  
         try
 220  
         {
 221  1
             if (checkDigest && !_digestSource.getDigestForResource(path).equals(md5Digest))
 222  
             {
 223  0
                 _response.sendError(HttpServletResponse.SC_FORBIDDEN, AssetMessages.md5Mismatch(path));
 224  0
                 return;
 225  
             }
 226  
             
 227  
             // If they were vended an asset in the past then it must be up-to date.
 228  
             // Asset URIs change if the underlying file is modified. (unless unprotected)
 229  
             
 230  1
             if (checkDigest && _request.getHeader("If-Modified-Since") != null)
 231  
             {
 232  0
                 _response.setStatus(HttpServletResponse.SC_NOT_MODIFIED);
 233  0
                 return;
 234  
             }
 235  
             
 236  1
             URL resourceURL = _classResolver.getResource(translatePath(path));
 237  
             
 238  1
             if (resourceURL == null)
 239  
             {
 240  0
                 _response.setStatus(HttpServletResponse.SC_NOT_FOUND);
 241  0
                 _log.warn(AssetMessages.noSuchResource(path));
 242  0
                 return;
 243  
             }
 244  
             
 245  1
             resourceConnection = resourceURL.openConnection();
 246  
             
 247  
             // check caching for unprotected resources
 248  
             
 249  1
             if (!checkDigest && cachedResource(resourceConnection))
 250  0
                 return;
 251  
             
 252  1
             writeAssetContent(cycle, path, resourceConnection);
 253  
         }
 254  0
         catch (IOException eof)
 255  
         {
 256  
             // ignored / expected exceptions happen when browser prematurely abandons connections - IE does this a lot
 257  
         }
 258  0
         catch (Throwable ex)
 259  
         {
 260  0
             _exceptionReporter.reportRequestException(AssetMessages.exceptionReportTitle(path), ex);
 261  1
         }
 262  1
     }
 263  
 
 264  
     /**
 265  
      * Utility that helps to resolve css file relative resources included
 266  
      * in a css temlpate via "url('../images/foo.gif')" or fix paths containing 
 267  
      * relative resource ".." style notation.
 268  
      * 
 269  
      * @param path The incoming path to check for relativity.
 270  
      * @return The path unchanged if not containing a css relative path, otherwise
 271  
      *          returns the path without the css filename in it so the resource is resolvable
 272  
      *          directly from the path.
 273  
      */
 274  
     String translatePath(String path)
 275  
     {
 276  11
         if (path == null) 
 277  0
             return null;
 278  
         
 279  11
         String ret = FilenameUtils.normalize(path);
 280  11
         ret = FilenameUtils.separatorsToUnix(ret);
 281  
         
 282  11
         return ret;
 283  
     }
 284  
     
 285  
     /**
 286  
      * Checks if the resource contained within the specified URL 
 287  
      * has a modified time greater than the request header value
 288  
      * of <code>If-Modified-Since</code>. If it doesn't then the 
 289  
      * response status is set to {@link HttpServletResponse#SC_NOT_MODIFIED}.
 290  
      * 
 291  
      * @param resourceURL Resource being checked
 292  
      * @return True if resource should be cached and response header was set.
 293  
      * @since 4.1
 294  
      */
 295  
     
 296  
     boolean cachedResource(URLConnection resourceURL)
 297  
     {
 298  
         // even if it doesn't exist in header the value will be -1, 
 299  
         // which means we need to write out the contents of the resource
 300  
 
 301  4
         long modifiedSince = _request.getDateHeader("If-Modified-Since");
 302  
 
 303  4
         if (modifiedSince <= 0)
 304  2
             return false;
 305  
         
 306  2
         if (_log.isDebugEnabled())
 307  0
             _log.debug("cachedResource(" + resourceURL.getURL() + ") modified-since header is: " + modifiedSince);
 308  
         
 309  2
         if (resourceURL.getLastModified() > modifiedSince)
 310  1
             return false;
 311  
         
 312  1
         _response.setStatus(HttpServletResponse.SC_NOT_MODIFIED);
 313  
         
 314  1
         return true;
 315  
     }
 316  
 
 317  
     /**
 318  
      * Writes the asset specified by <code>resourceConnection</code> out to the response stream.
 319  
      *
 320  
      * @param cycle
 321  
      *          The current request.
 322  
      * @param resourcePath
 323  
      *          The path of the resource.
 324  
      * @param resourceConnection
 325  
      *          A connection for the resource.
 326  
      * @throws IOException On error.
 327  
      */
 328  
 
 329  
     private void writeAssetContent(IRequestCycle cycle, String resourcePath, URLConnection resourceConnection)
 330  
             throws IOException
 331  
     {
 332  
         // Getting the content type and length is very dependant
 333  
         // on support from the application server (represented
 334  
         // here by the servletContext).
 335  
         
 336  1
         String contentType = getMimeType(resourcePath);
 337  
 
 338  1
         long lastModified = resourceConnection.getLastModified();
 339  1
         if (lastModified <= 0)
 340  0
             lastModified = _startupTime;
 341  
         
 342  1
         _response.setDateHeader("Last-Modified", lastModified);
 343  
         
 344  
         // write out expiration/cache info
 345  
 
 346  1
         _response.setDateHeader("Expires", _expireTime);
 347  1
         _response.setHeader("Cache-Control", "public, max-age=" + (MONTH_SECONDS * 3));
 348  
         
 349  
         // Set the content type. If the servlet container doesn't
 350  
         // provide it, try and guess it by the extension.
 351  
         
 352  1
         if (contentType == null || contentType.length() == 0)
 353  0
             contentType = getMimeType(resourcePath);
 354  
         
 355  1
         byte[] data = getAssetData(cycle, resourcePath, resourceConnection, contentType);
 356  
 
 357  
         // See ETag definition  - http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.19
 358  
         
 359  1
         _response.setHeader("ETag", "W/\"" + data.length + "-" + lastModified + "\"");
 360  
 
 361  
         // force image(or other) caching when detected, esp helps with ie related things
 362  
         // see http://mir.aculo.us/2005/08/28/internet-explorer-and-ajax-image-caching-woes
 363  
         
 364  1
         _response.setContentLength(data.length);
 365  
         
 366  1
         OutputStream output = _response.getOutputStream(new ContentType(contentType));
 367  
         
 368  1
         output.write(data);
 369  1
     }
 370  
     
 371  
     byte[] getAssetData(IRequestCycle cycle, String resourcePath,
 372  
             URLConnection resourceConnection, String contentType) 
 373  
     throws IOException
 374  
     {
 375  1
         InputStream input = null;
 376  
 
 377  
         try {
 378  
             
 379  
             CachedAsset cache;
 380  1
             byte[] data = null;
 381  
             
 382  
             // check cache first
 383  
             
 384  1
             if (_cache.get(resourcePath) != null)
 385  
             {    
 386  0
                 cache = (CachedAsset)_cache.get(resourcePath);
 387  
                 
 388  0
                 if (cache.getLastModified() < resourceConnection.getLastModified())
 389  0
                     cache.clear(resourceConnection.getLastModified());
 390  
                 
 391  0
                 data = cache.getData();
 392  
             } else
 393  
             {    
 394  1
                 cache = new CachedAsset(resourcePath, resourceConnection.getLastModified(), null, null);
 395  
                 
 396  1
                 _cache.put(resourcePath, cache);
 397  
             }
 398  
             
 399  1
             if (data == null)
 400  
             {
 401  1
                 input = resourceConnection.getInputStream();
 402  1
                 data = IOUtils.toByteArray(input);
 403  
 
 404  1
                 cache.setData(data);
 405  
             }
 406  
             
 407  
             // compress javascript responses when possible
 408  
             
 409  1
             if (GzipUtil.shouldCompressContentType(contentType) && GzipUtil.isGzipCapable(_request))
 410  
             {    
 411  0
                 if (cache.getGzipData() == null)
 412  
                 {    
 413  0
                     ByteArrayOutputStream bo = new ByteArrayOutputStream();
 414  0
                     GZIPOutputStream gzip = new GZIPOutputStream(bo);
 415  
                     
 416  0
                     gzip.write(data);
 417  0
                     gzip.close();
 418  
                     
 419  0
                     data = bo.toByteArray();
 420  0
                     cache.setGzipData(data);
 421  0
                 } else
 422  0
                     data = cache.getGzipData();
 423  
                 
 424  0
                 _response.setHeader("Content-Encoding", "gzip");
 425  
             }
 426  
             
 427  1
             return data;
 428  
             
 429  
         } finally {
 430  
             
 431  1
             if (input != null) {
 432  1
                 IOUtils.closeQuietly(input);
 433  
             }
 434  
         }
 435  
     }
 436  
     
 437  
     /** @since 4.0 */
 438  
 
 439  
     public void setExceptionReporter(RequestExceptionReporter exceptionReporter)
 440  
     {
 441  0
         _exceptionReporter = exceptionReporter;
 442  0
     }
 443  
 
 444  
     /** @since 4.0 */
 445  
     public void setLinkFactory(LinkFactory linkFactory)
 446  
     {
 447  1
         _linkFactory = linkFactory;
 448  1
     }
 449  
 
 450  
     /** @since 4.0 */
 451  
     public void setClassResolver(ClassResolver classResolver)
 452  
     {
 453  1
         _classResolver = classResolver;
 454  1
     }
 455  
 
 456  
     /** @since 4.0 */
 457  
     public void setContext(WebContext context)
 458  
     {
 459  1
         _context = context;
 460  1
     }
 461  
 
 462  
     /** @since 4.0 */
 463  
     public void setResponse(WebResponse response)
 464  
     {
 465  2
         _response = response;
 466  2
     }
 467  
 
 468  
     /** @since 4.0 */
 469  
     public void setDigestSource(ResourceDigestSource md5Source)
 470  
     {
 471  0
         _digestSource = md5Source;
 472  0
     }
 473  
 
 474  
     /** @since 4.0 */
 475  
     public void setRequest(WebRequest request)
 476  
     {
 477  4
         _request = request;
 478  4
     }
 479  
     
 480  
     /** @since 4.1 */
 481  
     public void setUnprotectedMatcher(ResourceMatcher matcher)
 482  
     {
 483  2
         _unprotectedMatcher = matcher;
 484  2
     }
 485  
     
 486  
     public void setLog(Log log)
 487  
     {
 488  4
         _log = log;
 489  4
     }
 490  
 }