Coverage Report - org.apache.tapestry.asset.AssetService
 
Classes in this File Line Coverage Branch Coverage Complexity
AssetService
0%
0/133
0%
0/56
3.118
 
 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  0
 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  0
         _mimeTypes = new HashMap(17);
 90  0
         _mimeTypes.put("css", "text/css");
 91  0
         _mimeTypes.put("gif", "image/gif");
 92  0
         _mimeTypes.put("jpg", "image/jpeg");
 93  0
         _mimeTypes.put("jpeg", "image/jpeg");
 94  0
         _mimeTypes.put("png", "image/png");
 95  0
         _mimeTypes.put("htm", "text/html");
 96  0
         _mimeTypes.put("html", "text/html");
 97  0
     }
 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  0
     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  0
     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  0
     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  0
         Defense.isAssignable(parameter, String.class, "parameter");
 160  
 
 161  0
         String path = (String) parameter;
 162  0
         String digest = null;
 163  
         
 164  0
         if(!_unprotectedMatcher.containsResource(path))
 165  0
             digest = _digestSource.getDigestForResource(path);
 166  
         
 167  0
         Map parameters = new TreeMap(new AssetComparator());
 168  
         
 169  0
         parameters.put(ServiceConstants.SERVICE, getName());
 170  0
         parameters.put(PATH, path);
 171  
         
 172  0
         if (digest != null)
 173  0
             parameters.put(DIGEST, digest);
 174  
         
 175  
         // Service is stateless, which is the exception to the rule.
 176  
         
 177  0
         return _linkFactory.constructLink(this, post, parameters, false);
 178  
     }
 179  
 
 180  
     public String getName()
 181  
     {
 182  0
         return Tapestry.ASSET_SERVICE;
 183  
     }
 184  
 
 185  
     private String getMimeType(String path)
 186  
     {
 187  0
         String result = _context.getMimeType(path);
 188  
         
 189  0
         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  0
         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  0
         String path = translatePath(cycle.getParameter(PATH));
 214  0
         String md5Digest = cycle.getParameter(DIGEST);
 215  0
         boolean checkDigest = !_unprotectedMatcher.containsResource(path);
 216  
         
 217  
         URLConnection resourceConnection;
 218  
         
 219  
         try
 220  
         {
 221  0
             URL resourceURL = _classResolver.getResource(path);
 222  
 
 223  0
             if (resourceURL == null)
 224  
             {
 225  0
                 _response.setStatus(HttpServletResponse.SC_NOT_FOUND);
 226  0
                 _log.info(AssetMessages.noSuchResource(path));
 227  0
                 return;
 228  
             } 
 229  
             
 230  0
             if (checkDigest && !_digestSource.getDigestForResource(path).equals(md5Digest))
 231  
             {
 232  0
                 _response.setStatus(HttpServletResponse.SC_NOT_FOUND);
 233  0
                 _log.info(AssetMessages.md5Mismatch(path));
 234  0
                 return;
 235  
             }
 236  
             
 237  
             // If they were vended an asset in the past then it must be up-to date.
 238  
             // Asset URIs change if the underlying file is modified. (unless unprotected)
 239  
             
 240  0
             if (checkDigest && _request.getHeader("If-Modified-Since") != null)
 241  
             {
 242  0
                 _response.setStatus(HttpServletResponse.SC_NOT_MODIFIED);
 243  0
                 return;
 244  
             }
 245  
             
 246  0
             resourceConnection = resourceURL.openConnection();
 247  
             
 248  
             // check caching for unprotected resources
 249  
             
 250  0
             if (!checkDigest && cachedResource(resourceConnection))
 251  0
                 return;
 252  
             
 253  0
             writeAssetContent(cycle, path, resourceConnection);
 254  
         }
 255  0
         catch (IOException eof)
 256  
         {
 257  
             // ignored / expected exceptions happen when browser prematurely abandons connections - IE does this a lot
 258  
         }
 259  0
         catch (Throwable ex)
 260  
         {
 261  0
             _response.setStatus(HttpServletResponse.SC_NOT_FOUND);
 262  0
             _log.warn(AssetMessages.exceptionReportTitle(path), ex);
 263  
             //_exceptionReporter.reportRequestException(AssetMessages.exceptionReportTitle(path), ex);
 264  0
         }
 265  0
     }
 266  
 
 267  
     /**
 268  
      * Utility that helps to resolve css file relative resources included
 269  
      * in a css temlpate via "url('../images/foo.gif')" or fix paths containing 
 270  
      * relative resource ".." style notation.
 271  
      * 
 272  
      * @param path The incoming path to check for relativity.
 273  
      * @return The path unchanged if not containing a css relative path, otherwise
 274  
      *          returns the path without the css filename in it so the resource is resolvable
 275  
      *          directly from the path.
 276  
      */
 277  
     String translatePath(String path)
 278  
     {
 279  0
         if (path == null) 
 280  0
             return null;
 281  
         
 282  0
         String ret = FilenameUtils.normalize(path);
 283  0
         ret = FilenameUtils.separatorsToUnix(ret);
 284  
         
 285  0
         return ret;
 286  
     }
 287  
     
 288  
     /**
 289  
      * Checks if the resource contained within the specified URL 
 290  
      * has a modified time greater than the request header value
 291  
      * of <code>If-Modified-Since</code>. If it doesn't then the 
 292  
      * response status is set to {@link HttpServletResponse#SC_NOT_MODIFIED}.
 293  
      * 
 294  
      * @param resourceURL Resource being checked
 295  
      * @return True if resource should be cached and response header was set.
 296  
      * @since 4.1
 297  
      */
 298  
     
 299  
     boolean cachedResource(URLConnection resourceURL)
 300  
     {
 301  
         // even if it doesn't exist in header the value will be -1, 
 302  
         // which means we need to write out the contents of the resource
 303  
 
 304  0
         long modifiedSince = _request.getDateHeader("If-Modified-Since");
 305  
 
 306  0
         if (modifiedSince <= 0)
 307  0
             return false;
 308  
         
 309  0
         if (_log.isDebugEnabled())
 310  0
             _log.debug("cachedResource(" + resourceURL.getURL() + ") modified-since header is: " + modifiedSince);
 311  
         
 312  0
         if (resourceURL.getLastModified() > modifiedSince)
 313  0
             return false;
 314  
         
 315  0
         _response.setStatus(HttpServletResponse.SC_NOT_MODIFIED);
 316  
         
 317  0
         return true;
 318  
     }
 319  
 
 320  
     /**
 321  
      * Writes the asset specified by <code>resourceConnection</code> out to the response stream.
 322  
      *
 323  
      * @param cycle
 324  
      *          The current request.
 325  
      * @param resourcePath
 326  
      *          The path of the resource.
 327  
      * @param resourceConnection
 328  
      *          A connection for the resource.
 329  
      * @throws IOException On error.
 330  
      */
 331  
 
 332  
     private void writeAssetContent(IRequestCycle cycle, String resourcePath, URLConnection resourceConnection)
 333  
             throws IOException
 334  
     {
 335  
         // Getting the content type and length is very dependant
 336  
         // on support from the application server (represented
 337  
         // here by the servletContext).
 338  
         
 339  0
         String contentType = getMimeType(resourcePath);
 340  
 
 341  0
         long lastModified = resourceConnection.getLastModified();
 342  0
         if (lastModified <= 0)
 343  0
             lastModified = _startupTime;
 344  
         
 345  0
         _response.setDateHeader("Last-Modified", lastModified);
 346  
         
 347  
         // write out expiration/cache info
 348  
 
 349  0
         _response.setDateHeader("Expires", _expireTime);
 350  0
         _response.setHeader("Cache-Control", "public, max-age=" + (MONTH_SECONDS * 3));
 351  
         
 352  
         // Set the content type. If the servlet container doesn't
 353  
         // provide it, try and guess it by the extension.
 354  
         
 355  0
         if (contentType == null || contentType.length() == 0)
 356  0
             contentType = getMimeType(resourcePath);
 357  
         
 358  0
         byte[] data = getAssetData(cycle, resourcePath, resourceConnection, contentType);
 359  
 
 360  
         // See ETag definition  - http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.19
 361  
         
 362  0
         _response.setHeader("ETag", "W/\"" + data.length + "-" + lastModified + "\"");
 363  
 
 364  
         // force image(or other) caching when detected, esp helps with ie related things
 365  
         // see http://mir.aculo.us/2005/08/28/internet-explorer-and-ajax-image-caching-woes
 366  
         
 367  0
         _response.setContentLength(data.length);
 368  
         
 369  0
         OutputStream output = _response.getOutputStream(new ContentType(contentType));
 370  
         
 371  0
         output.write(data);
 372  0
     }
 373  
     
 374  
     byte[] getAssetData(IRequestCycle cycle, String resourcePath,
 375  
             URLConnection resourceConnection, String contentType) 
 376  
     throws IOException
 377  
     {
 378  0
         InputStream input = null;
 379  
 
 380  
         try {
 381  
             
 382  
             CachedAsset cache;
 383  0
             byte[] data = null;
 384  
             
 385  
             // check cache first
 386  
             
 387  0
             if (_cache.get(resourcePath) != null)
 388  
             {    
 389  0
                 cache = (CachedAsset)_cache.get(resourcePath);
 390  
                 
 391  0
                 if (cache.getLastModified() < resourceConnection.getLastModified())
 392  0
                     cache.clear(resourceConnection.getLastModified());
 393  
                 
 394  0
                 data = cache.getData();
 395  
             } else
 396  
             {    
 397  0
                 cache = new CachedAsset(resourcePath, resourceConnection.getLastModified(), null, null);
 398  
                 
 399  0
                 _cache.put(resourcePath, cache);
 400  
             }
 401  
             
 402  0
             if (data == null)
 403  
             {
 404  0
                 input = resourceConnection.getInputStream();
 405  0
                 data = IOUtils.toByteArray(input);
 406  
 
 407  0
                 cache.setData(data);
 408  
             }
 409  
             
 410  
             // compress javascript responses when possible
 411  
             
 412  0
             if (GzipUtil.shouldCompressContentType(contentType) && GzipUtil.isGzipCapable(_request))
 413  
             {    
 414  0
                 if (cache.getGzipData() == null)
 415  
                 {    
 416  0
                     ByteArrayOutputStream bo = new ByteArrayOutputStream();
 417  0
                     GZIPOutputStream gzip = new GZIPOutputStream(bo);
 418  
                     
 419  0
                     gzip.write(data);
 420  0
                     gzip.close();
 421  
                     
 422  0
                     data = bo.toByteArray();
 423  0
                     cache.setGzipData(data);
 424  0
                 } else
 425  0
                     data = cache.getGzipData();
 426  
                 
 427  0
                 _response.setHeader("Content-Encoding", "gzip");
 428  
             }
 429  
             
 430  0
             return data;
 431  
             
 432  
         } finally {
 433  
             
 434  0
             if (input != null) {
 435  0
                 IOUtils.closeQuietly(input);
 436  
             }
 437  
         }
 438  
     }
 439  
     
 440  
     /** @since 4.0 */
 441  
 
 442  
     public void setExceptionReporter(RequestExceptionReporter exceptionReporter)
 443  
     {
 444  0
         _exceptionReporter = exceptionReporter;
 445  0
     }
 446  
 
 447  
     /** @since 4.0 */
 448  
     public void setLinkFactory(LinkFactory linkFactory)
 449  
     {
 450  0
         _linkFactory = linkFactory;
 451  0
     }
 452  
 
 453  
     /** @since 4.0 */
 454  
     public void setClassResolver(ClassResolver classResolver)
 455  
     {
 456  0
         _classResolver = classResolver;
 457  0
     }
 458  
 
 459  
     /** @since 4.0 */
 460  
     public void setContext(WebContext context)
 461  
     {
 462  0
         _context = context;
 463  0
     }
 464  
 
 465  
     /** @since 4.0 */
 466  
     public void setResponse(WebResponse response)
 467  
     {
 468  0
         _response = response;
 469  0
     }
 470  
 
 471  
     /** @since 4.0 */
 472  
     public void setDigestSource(ResourceDigestSource md5Source)
 473  
     {
 474  0
         _digestSource = md5Source;
 475  0
     }
 476  
 
 477  
     /** @since 4.0 */
 478  
     public void setRequest(WebRequest request)
 479  
     {
 480  0
         _request = request;
 481  0
     }
 482  
     
 483  
     /** @since 4.1 */
 484  
     public void setUnprotectedMatcher(ResourceMatcher matcher)
 485  
     {
 486  0
         _unprotectedMatcher = matcher;
 487  0
     }
 488  
     
 489  
     public void setLog(Log log)
 490  
     {
 491  0
         _log = log;
 492  0
     }
 493  
 }