2009/05/20 - Apache Shale has been retired.

For more information, please explore the Attic.

Coverage Report - org.apache.shale.remoting.impl.AbstractResourceProcessor
 
Classes in this File Line Coverage Branch Coverage Complexity
AbstractResourceProcessor
100%
103/103
N/A
3.1
 
 1  
 /*
 2  
  * Licensed to the Apache Software Foundation (ASF) under one or more
 3  
  * contributor license agreements.  See the NOTICE file distributed with
 4  
  * this work for additional information regarding copyright ownership.
 5  
  * The ASF licenses this file to you under the Apache License, Version 2.0
 6  
  * (the "License"); you may not use this file except in compliance with
 7  
  * the License.  You may obtain a copy of the License at
 8  
  *
 9  
  *      http://www.apache.org/licenses/LICENSE-2.0
 10  
  *
 11  
  * Unless required by applicable law or agreed to in writing, software
 12  
  * distributed under the License is distributed on an "AS IS" BASIS,
 13  
  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 14  
  * See the License for the specific language governing permissions and
 15  
  * limitations under the License.
 16  
  */
 17  
 
 18  
 package org.apache.shale.remoting.impl;
 19  
 
 20  
 import java.io.IOException;
 21  
 import java.io.InputStream;
 22  
 import java.io.OutputStream;
 23  
 import java.lang.reflect.Method;
 24  
 import java.net.URL;
 25  
 import java.net.URLConnection;
 26  
 import java.text.SimpleDateFormat;
 27  
 import java.util.Date;
 28  
 import java.util.HashMap;
 29  
 import java.util.Iterator;
 30  
 import java.util.Locale;
 31  
 import java.util.Map;
 32  
 import java.util.TimeZone;
 33  
 
 34  
 import javax.faces.context.FacesContext;
 35  
 import javax.servlet.ServletContext;
 36  
 import javax.servlet.http.HttpServletRequest;
 37  
 import javax.servlet.http.HttpServletResponse;
 38  
 
 39  
 import org.apache.commons.logging.Log;
 40  
 import org.apache.commons.logging.LogFactory;
 41  
 import org.apache.shale.remoting.Processor;
 42  
 import org.apache.shale.remoting.faces.ResponseFactory;
 43  
 
 44  
 /**
 45  
  * <p>Convenience abstract base class for {@link Processor} implementations
 46  
  * that serve up static resources.</p>
 47  
  */
 48  15
 public abstract class AbstractResourceProcessor extends FilteringProcessor {
 49  
 
 50  
 
 51  
     // ------------------------------------------------------ Instance Variables
 52  
 
 53  
 
 54  
     /**
 55  
      * <p>The <code>Log</code> instance for this class.</p>
 56  
      */
 57  15
     private transient Log log = null;
 58  
 
 59  
 
 60  
     // ------------------------------------------------------- Processor Methods
 61  
 
 62  
 
 63  
     /**
 64  
      * <p>Check if the specified resource actually exists.  If it does not,
 65  
      * return an HTTP 404 status (servlet) or throw an IllegalArgumentException
 66  
      * (portlet).</p>
 67  
      *
 68  
      * @param context <code>FacesContext</code> for the current request
 69  
      * @param resourceId Resource identifier of the resource to be served
 70  
      *
 71  
      * @exception IllegalArgumentException if the specified resource does
 72  
      *  not exist in a portlet environment (because we cannot return an
 73  
      *  HTTP status 404)
 74  
      * @exception IOException if an input/output error occurs
 75  
      */
 76  
     public void process(FacesContext context, String resourceId) throws IOException {
 77  
 
 78  
         // Validate our input parameters
 79  8
         if (resourceId == null) {
 80  
             throw new NullPointerException();
 81  
         }
 82  8
         if (!resourceId.startsWith("/")) {
 83  
             throw new IllegalArgumentException(resourceId);
 84  
         }
 85  
 
 86  
         // If someone else has completed the response, we do not have
 87  
         // anything to do
 88  8
         if (context.getResponseComplete()) {
 89  
             return;
 90  
         }
 91  
 
 92  
         // Filter based on our includes and excludes patterns
 93  8
         if (!accept(resourceId)) {
 94  3
             if (log().isTraceEnabled()) {
 95  
                 log().trace("Resource id '" + resourceId
 96  
                             + "' rejected by include/exclude rules");
 97  
             }
 98  
             // Send an HTTP "not found" response to avoid giving the client
 99  
             // any information about a resource that exists and was refused,
 100  
             // versus a resource that does not exist
 101  3
             sendNotFound(context, resourceId);
 102  3
             context.responseComplete();
 103  3
             return;
 104  
         }
 105  
 
 106  
         // Acquire a URL to the specified resource, if it exists
 107  
         // If not, send an HTTP "not found" response
 108  5
         URL url = getResourceURL(context, resourceId);
 109  5
         if (log().isDebugEnabled()) {
 110  
             log().debug("Translated resource id '" + resourceId + "' to URL '"
 111  
                         + url + "'");
 112  
         }
 113  5
         if (url == null) {
 114  1
             if (log().isTraceEnabled()) {
 115  
                 log().trace("Resource '" + resourceId + "' not found, returning 404");
 116  
             }
 117  1
             sendNotFound(context, resourceId);
 118  1
             context.responseComplete();
 119  1
             return;
 120  
         }
 121  
 
 122  
         // If this request includes "If-Modified-Since" header, return
 123  
         // an HTTP "not modified" response if the specified timestamp is
 124  
         // equal to or later than our application resource timestamp
 125  4
         long ifModifiedSince = ifModifiedSince(context);
 126  4
         if ((ifModifiedSince >= 0)
 127  
             && ((ifModifiedSince + 1000L) >= getLastModified())) {
 128  1
             if (log().isTraceEnabled()) {
 129  
                 log().trace("Resource '" + resourceId + "' not modified, returning 304");
 130  
             }
 131  1
             sendNotModified(context, resourceId);
 132  1
             context.responseComplete();
 133  1
             return;
 134  
         }
 135  
 
 136  
         // Set up the response headers
 137  3
         sendLastModified(context, getLastModifiedString());
 138  
 
 139  
         // Copy the resource contents to the response output stream
 140  3
         InputStream inputStream = null;
 141  3
         OutputStream outputStream = null;
 142  
         try {
 143  3
             inputStream = inputStream(context, url);
 144  3
             String contentType = mimeType(context, resourceId);
 145  3
             outputStream = outputStream(context, contentType);
 146  3
             copyStream(context, inputStream, outputStream);
 147  
         } finally {
 148  3
             if (outputStream != null) {
 149  3
                 try { outputStream.close(); } catch (Exception e) { ; }
 150  
             }
 151  3
             if (inputStream != null) {
 152  3
                 try { inputStream.close(); } catch (Exception e) { ; }
 153  
             }
 154  
         }
 155  
 
 156  
         // Finish up by indicating that this response is already complete
 157  3
         context.responseComplete();
 158  
 
 159  3
     }
 160  
 
 161  
 
 162  
 
 163  
     // ------------------------------------------------------ Instance Variables
 164  
 
 165  
 
 166  
     /**
 167  
      * <p>The buffer size when copying the input stream to the output stream.</p>
 168  
      */
 169  15
     private int bufferSize = 1024;
 170  
 
 171  
 
 172  
     /**
 173  
      * <p>The date/time (in milliseconds since the epoch) value to generate on the
 174  
      * <code>Last-Modified</code> header included with each served resource.</p>
 175  
      */
 176  15
     private long lastModified = 0;
 177  
 
 178  
 
 179  
     /**
 180  
      * <p>The string version of the <code>lastModified</code> value.</p>
 181  
      */
 182  15
     private String lastModifiedString = null;
 183  
 
 184  
 
 185  
     /**
 186  
      * <p><code>Map</code> of MIME types, keyed by file extension.  This is
 187  
      * used as a fallback if the <code>ServletContext</code> or
 188  
      * <code>PortletContext</code> call to <code>mimeType()</code> does not
 189  
      * return any result.</p>
 190  
      */
 191  15
     protected Map mimeTypes = new HashMap();
 192  
     {
 193  15
         mimeTypes.put(".css", "text/css");
 194  15
         mimeTypes.put(".gif", "image/gif");
 195  15
         mimeTypes.put(".ico", "image/vnd.microsoft.icon");
 196  15
         mimeTypes.put(".jpeg", "image/jpeg");
 197  15
         mimeTypes.put(".jpg", "image/jpeg");
 198  15
         mimeTypes.put(".js", "text/javascript");
 199  15
         mimeTypes.put(".png", "image/png");
 200  15
     }
 201  
 
 202  
 
 203  
     // -------------------------------------------------------- Static Variables
 204  
 
 205  
 
 206  
     /**
 207  
      * <p>The date formatting helper we will use in <code>httpTimestamp()</code>.
 208  
      * Note that usage of this helper must be synchronized.</p>
 209  
      */
 210  1
     private static SimpleDateFormat format =
 211  
             new SimpleDateFormat("EEE, dd MMM yyyy HH:mm:ss zzz",
 212  
                                  Locale.US);
 213  
     static {
 214  1
         format.setTimeZone(TimeZone.getTimeZone("GMT"));
 215  1
     }
 216  
 
 217  
 
 218  
     // -------------------------------------------------------------- Properties
 219  
 
 220  
 
 221  
     /**
 222  
      * <p>Return the buffer size when copying.</p>
 223  
      */
 224  
     public int getBufferSize() {
 225  3
         return this.bufferSize;
 226  
     }
 227  
 
 228  
 
 229  
     /**
 230  
      * <p>Set the buffer size when copying.</p>
 231  
      *
 232  
      * @param bufferSize The new buffer size
 233  
      */
 234  
     public void setBufferSize(int bufferSize) {
 235  
         this.bufferSize = bufferSize;
 236  
     }
 237  
 
 238  
 
 239  
     /**
 240  
      * <p>Return the date/time (expressed as the number of milliseconds since
 241  
      * the epoch) that will be generated on the <code>Last-Modified</code>
 242  
      * header of all resources served by this processor.  If this value has
 243  
      * not been set upon first call to this method, it will be set to the
 244  
      * current date and time.</p>
 245  
      */
 246  
     public long getLastModified() {
 247  2
        if (lastModified == 0) {
 248  1
             setLastModified((new Date()).getTime());
 249  
         }
 250  2
         return lastModified;
 251  
     }
 252  
 
 253  
 
 254  
     /**
 255  
      * <p>Set the date/time (expressed as the number of milliseconds since
 256  
      * the epoch) that wll be generated on the <code>Last-Modified</code>
 257  
      * header of all resources served by this processor.</p>
 258  
      *
 259  
      * @param lastModified The new last modified value
 260  
      */
 261  
     public void setLastModified(long lastModified) {
 262  4
         this.lastModified = lastModified;
 263  4
         this.lastModifiedString = httpTimestamp(lastModified);
 264  4
     }
 265  
 
 266  
 
 267  
     /**
 268  
      * <p>Return a String version of the last modified date/time, formatted
 269  
      * as required by Section 3.3.1 of the HTTP/1.1 Specification.  If the
 270  
      * <code>lastModified</code> property has not been set upon first call to
 271  
      * this method, it will be set to the current date and time.</p>
 272  
      */
 273  
     public String getLastModifiedString() {
 274  3
         if (lastModified == 0) {
 275  3
             setLastModified((new Date()).getTime());
 276  
         }
 277  3
         return lastModifiedString;
 278  
     }
 279  
 
 280  
 
 281  
     // -------------------------------------------------------- Abstract Methods
 282  
 
 283  
 
 284  
     /**
 285  
      * <p>Convert the specified resource identifier into a URL, if the resource
 286  
      * actually exists.  Otherwise, return <code>null</code>.</p>
 287  
      *
 288  
      * @param context <code>FacesContext</code> for the current request
 289  
      * @param resourceId Resource identifier to translate
 290  
      */
 291  
     protected abstract URL getResourceURL(FacesContext context, String resourceId);
 292  
 
 293  
 
 294  
     // ------------------------------------------------------- Protected Methods
 295  
 
 296  
 
 297  
     /**
 298  
      * <p>Copy the contents of the specified input stream to the specified
 299  
      * output stream.</p>
 300  
      *
 301  
      * @param context <code>FacesContext</code> for the current request
 302  
      * @param inputStream <code>InputStream</code> to be copied from
 303  
      * @param outputStream <code>OutputStream</code> to be copied to
 304  
      *
 305  
      * @exception IOException if an input/output error occurs
 306  
      */
 307  
     protected void copyStream(FacesContext context, InputStream inputStream,
 308  
                               OutputStream outputStream) throws IOException {
 309  
 
 310  3
         byte[] buffer = new byte[getBufferSize()];
 311  
         while (true) {
 312  6
             int len = inputStream.read(buffer);
 313  6
             if (len <= 0) {
 314  3
                 break;
 315  
             }
 316  3
             outputStream.write(buffer, 0, len);
 317  3
         }
 318  
 
 319  3
     }
 320  
 
 321  
 
 322  
     /**
 323  
      * <p>Return a textual representation of the specified date/time stamp
 324  
      * (expressed as a <code>java.util.Date</code> object)
 325  
      * in the format required by the HTTP/1.1 Specification (RFC 2616),
 326  
      * Section 3.3.1.  An example of this format is:
 327  
      * <blockquote>
 328  
      *   Sun, 06 Nov 1994 08:49:37 GMT
 329  
      * </blockquote></p>
 330  
      *
 331  
      * @param timestamp The date/time to be formatted, expressed as
 332  
      *  a <code>java.util.Date</code>
 333  
      */
 334  
     protected String httpTimestamp(Date timestamp) {
 335  
 
 336  4
         synchronized (format) {
 337  4
             return format.format(timestamp);
 338  
         }
 339  
 
 340  
     }
 341  
 
 342  
 
 343  
     /**
 344  
      * <p>Return a textual representation of the specified date/time stamp
 345  
      * (expressed in milliseconds since the epoch, and assumed to be GMT)
 346  
      * in the format required by the HTTP/1.1 Specification (RFC 2616),
 347  
      * Section 3.3.1.  An example of this format is:
 348  
      * <blockquote>
 349  
      *   Sun, 06 Nov 1994 08:49:37 GMT
 350  
      * </blockquote></p>
 351  
      *
 352  
      * @param timestamp The date/time to be formatted, expressed as the number
 353  
      *  of milliseconds since the epoch
 354  
      */
 355  
     protected String httpTimestamp(long timestamp) {
 356  
 
 357  4
         return httpTimestamp(new Date(timestamp));
 358  
 
 359  
     }
 360  
 
 361  
 
 362  
     /**
 363  
      * <p>Return the value of the <code>If-Modified-Since</code> header
 364  
      * included on this request, as a number of milliseconds since the
 365  
      * epoch.  If this header was not included (or we cannot tell if it
 366  
      * was included), return -1 instead.</p>
 367  
      *
 368  
      * @param context <code>FacesContext</code> for the current request
 369  
      */
 370  
     protected long ifModifiedSince(FacesContext context) {
 371  
 
 372  4
         Object request = context.getExternalContext().getRequest();
 373  4
         if (request instanceof HttpServletRequest) {
 374  4
             return ((HttpServletRequest) request).getDateHeader("If-Modified-Since");
 375  
         }
 376  
         return -1;
 377  
 
 378  
     }
 379  
 
 380  
 
 381  
     /**
 382  
      * <p>Return an <code>InputStream</code> derived from the specified URL,
 383  
      * which will point to the static resource to be served.</p>
 384  
      *
 385  
      * @param context <code>FacesContext</code> for the current request
 386  
      * @param url <code>URL</code> from which to derive an input stream
 387  
      *
 388  
      * @exception IOException if an input/output error occurs
 389  
      */
 390  
     protected InputStream inputStream(FacesContext context, URL url) throws IOException {
 391  
 
 392  3
         URLConnection conn = url.openConnection();
 393  3
         conn.setUseCaches(false);
 394  3
         return conn.getInputStream();
 395  
 
 396  
     }
 397  
 
 398  
 
 399  
     /**
 400  
      * <p>Return the appropriate MIME type (if known) for the specified resource
 401  
      * path.  This method is portable across servlet and portlet environments.
 402  
      * If no MIME type is known, fall back to a configured list, based on the
 403  
      * extension of the requested resource.  If no result can be found in the
 404  
      * fallback list, return <code>null</code>.</p>
 405  
      *
 406  
      * @param context <code>FacesContext</code> for the current request
 407  
      * @param resourceId Resource identifier of the resource to categorize
 408  
      */
 409  
     protected String mimeType(FacesContext context, String resourceId) {
 410  
 
 411  3
         Object ctxt = context.getExternalContext().getContext();
 412  3
         Class clazz = ctxt.getClass();
 413  3
         Method method = null;
 414  
         try {
 415  5
             method = clazz.getMethod("getMimeType", new Class[] { String.class });
 416  
             // Return the container calculated type, if any
 417  3
             String result = (String) method.invoke(ctxt,
 418  
                                                    new Object[] { resourceId });
 419  3
             if (result != null) {
 420  3
                 return result;
 421  
             }
 422  
             // Check our fallback list
 423  
             Iterator entries = mimeTypes.entrySet().iterator();
 424  
             while (entries.hasNext()) {
 425  
                 Map.Entry entry = (Map.Entry) entries.next();
 426  
                 if (resourceId.endsWith((String) entry.getKey())) {
 427  
                     return (String) entry.getValue();
 428  
                 }
 429  
             }
 430  
             // We have no clue what MIME type should be used for this resource
 431  
             return null;
 432  
         } catch (Exception e) {
 433  
             if (log.isErrorEnabled()) {
 434  
                 log.error("mimeType.exception", e);
 435  
             }
 436  
             return null;
 437  
         }
 438  
 
 439  
     }
 440  
 
 441  
 
 442  
     /**
 443  
      * <p>Return an <code>OutputStream</code> to which our static
 444  
      * resource is to be served.</p>
 445  
      *
 446  
      * @param context <code>FacesContext</code> for the current request
 447  
      * @param contentType Content type for this response
 448  
      *
 449  
      * @exception IOException if an input/output error occurs
 450  
      */
 451  
     protected OutputStream outputStream(FacesContext context, String contentType)
 452  
       throws IOException {
 453  
 
 454  3
         return (new ResponseFactory()).getResponseStream(context, contentType);
 455  
 
 456  
     }
 457  
 
 458  
 
 459  
     /**
 460  
      * <p>Return <code>true</code> if we are processing a servlet request (as
 461  
      * opposed to a portlet request).</p>
 462  
      *
 463  
      * @param context <code>FacesContext</code> for the current request
 464  
      */
 465  
     protected boolean servletRequest(FacesContext context) {
 466  
 
 467  5
         return context.getExternalContext().getContext() instanceof ServletContext;
 468  
 
 469  
     }
 470  
 
 471  
 
 472  
     /**
 473  
      * <p>Set the content type on the servlet or portlet response object.</p>
 474  
      *
 475  
      * @param context <code>FacesContext</code> for the current request
 476  
      * @param contentType The content type to be set
 477  
      */
 478  
     protected void sendContentType(FacesContext context, String contentType) {
 479  
 
 480  
         Object response = context.getExternalContext().getResponse();
 481  
         try {
 482  
             Method method =
 483  
               response.getClass().getMethod("setResponseType",
 484  
                                              new Class[] { String.class });
 485  
             method.invoke(response, new Object[] { contentType });
 486  
         } catch (Exception e) {
 487  
             if (log.isErrorEnabled()) {
 488  
                 log.error("contentType.exception", e);
 489  
             }
 490  
         }
 491  
 
 492  
     }
 493  
 
 494  
 
 495  
     /**
 496  
      * <p>Set the <code>Last-Modified</code> header to the specified timestamp.</p>
 497  
      *
 498  
      * @param context <code>FacesContext</code> for this request
 499  
      * @param timestamp String version of the last modified timestamp
 500  
      */
 501  
     protected void sendLastModified(FacesContext context, String timestamp) {
 502  
 
 503  3
         Object response = context.getExternalContext().getResponse();
 504  3
         if (response instanceof HttpServletResponse) {
 505  3
             ((HttpServletResponse) response).setHeader("Last-Modified", timestamp);
 506  
         /* else it is a portlet response with mechanism to support this
 507  
         } else {
 508  
             ;
 509  
         */
 510  
         }
 511  
 
 512  3
     }
 513  
 
 514  
 
 515  
     /**
 516  
      * <p>Send a "not found" HTTP response, if possible.  Otherwise, throw an
 517  
      * <code>IllegalArgumentException</code> that will ripple out.</p>
 518  
      *
 519  
      * @param context <code>FacesContext</code> for the current request
 520  
      * @param resourceId Resource identifier of the resource that was not found
 521  
      *
 522  
      * @exception IllegalArgumentException if we cannot send an HTTP response
 523  
      * @exception IOException if an input/output error occurs
 524  
      */
 525  
     protected void sendNotFound(FacesContext context, String resourceId) throws IOException {
 526  
 
 527  4
         if (servletRequest(context)) {
 528  4
             HttpServletResponse response = (HttpServletResponse)
 529  
               context.getExternalContext().getResponse();
 530  4
             response.sendError(HttpServletResponse.SC_NOT_FOUND, resourceId);
 531  4
         } else {
 532  
             throw new IllegalArgumentException(resourceId);
 533  
         }
 534  
 
 535  4
     }
 536  
 
 537  
 
 538  
     /**
 539  
      * <p>Send a "not modified" HTTP response, if possible.  Otherwise, throw an
 540  
      * <code>IllegalArgumentException</code> that will ripple out.</p>
 541  
      *
 542  
      * @param context <code>FacesContext</code> for the current request
 543  
      * @param resourceId Resource identifier of the resource that was not modified
 544  
      *
 545  
      * @exception IllegalArgumentException if we cannot send an HTTP response
 546  
      * @exception IOException if an input/output error occurs
 547  
      */
 548  
     protected void sendNotModified(FacesContext context, String resourceId) throws IOException {
 549  
 
 550  1
         if (servletRequest(context)) {
 551  1
             HttpServletResponse response = (HttpServletResponse)
 552  
               context.getExternalContext().getResponse();
 553  1
             response.sendError(HttpServletResponse.SC_NOT_MODIFIED, resourceId);
 554  1
         } else {
 555  
             throw new IllegalArgumentException(resourceId);
 556  
         }
 557  
 
 558  1
     }
 559  
 
 560  
 
 561  
     // --------------------------------------------------------- Private Methods
 562  
 
 563  
 
 564  
     /**
 565  
      * <p>Return the <code>Log</code> instance to use, creating one if needed.</p>
 566  
      */
 567  
     private Log log() {
 568  
 
 569  10
         if (this.log == null) {
 570  8
             log = LogFactory.getLog(AbstractResourceProcessor.class);
 571  
         }
 572  10
         return log;
 573  
 
 574  
     }
 575  
 
 576  
 
 577  
 }