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

For more information, please explore the Attic.

View Javadoc

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  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      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          if (resourceId == null) {
80              throw new NullPointerException();
81          }
82          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          if (context.getResponseComplete()) {
89              return;
90          }
91  
92          // Filter based on our includes and excludes patterns
93          if (!accept(resourceId)) {
94              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             sendNotFound(context, resourceId);
102             context.responseComplete();
103             return;
104         }
105 
106         // Acquire a URL to the specified resource, if it exists
107         // If not, send an HTTP "not found" response
108         URL url = getResourceURL(context, resourceId);
109         if (log().isDebugEnabled()) {
110             log().debug("Translated resource id '" + resourceId + "' to URL '"
111                         + url + "'");
112         }
113         if (url == null) {
114             if (log().isTraceEnabled()) {
115                 log().trace("Resource '" + resourceId + "' not found, returning 404");
116             }
117             sendNotFound(context, resourceId);
118             context.responseComplete();
119             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         long ifModifiedSince = ifModifiedSince(context);
126         if ((ifModifiedSince >= 0)
127             && ((ifModifiedSince + 1000L) >= getLastModified())) {
128             if (log().isTraceEnabled()) {
129                 log().trace("Resource '" + resourceId + "' not modified, returning 304");
130             }
131             sendNotModified(context, resourceId);
132             context.responseComplete();
133             return;
134         }
135 
136         // Set up the response headers
137         sendLastModified(context, getLastModifiedString());
138 
139         // Copy the resource contents to the response output stream
140         InputStream inputStream = null;
141         OutputStream outputStream = null;
142         try {
143             inputStream = inputStream(context, url);
144             String contentType = mimeType(context, resourceId);
145             outputStream = outputStream(context, contentType);
146             copyStream(context, inputStream, outputStream);
147         } finally {
148             if (outputStream != null) {
149                 try { outputStream.close(); } catch (Exception e) { ; }
150             }
151             if (inputStream != null) {
152                 try { inputStream.close(); } catch (Exception e) { ; }
153             }
154         }
155 
156         // Finish up by indicating that this response is already complete
157         context.responseComplete();
158 
159     }
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     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     private long lastModified = 0;
177 
178 
179     /***
180      * <p>The string version of the <code>lastModified</code> value.</p>
181      */
182     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     protected Map mimeTypes = new HashMap();
192     {
193         mimeTypes.put(".css", "text/css");
194         mimeTypes.put(".gif", "image/gif");
195         mimeTypes.put(".ico", "image/vnd.microsoft.icon");
196         mimeTypes.put(".jpeg", "image/jpeg");
197         mimeTypes.put(".jpg", "image/jpeg");
198         mimeTypes.put(".js", "text/javascript");
199         mimeTypes.put(".png", "image/png");
200     }
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     private static SimpleDateFormat format =
211             new SimpleDateFormat("EEE, dd MMM yyyy HH:mm:ss zzz",
212                                  Locale.US);
213     static {
214         format.setTimeZone(TimeZone.getTimeZone("GMT"));
215     }
216 
217 
218     // -------------------------------------------------------------- Properties
219 
220 
221     /***
222      * <p>Return the buffer size when copying.</p>
223      */
224     public int getBufferSize() {
225         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        if (lastModified == 0) {
248             setLastModified((new Date()).getTime());
249         }
250         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         this.lastModified = lastModified;
263         this.lastModifiedString = httpTimestamp(lastModified);
264     }
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         if (lastModified == 0) {
275             setLastModified((new Date()).getTime());
276         }
277         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         byte[] buffer = new byte[getBufferSize()];
311         while (true) {
312             int len = inputStream.read(buffer);
313             if (len <= 0) {
314                 break;
315             }
316             outputStream.write(buffer, 0, len);
317         }
318 
319     }
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         synchronized (format) {
337             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         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         Object request = context.getExternalContext().getRequest();
373         if (request instanceof HttpServletRequest) {
374             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         URLConnection conn = url.openConnection();
393         conn.setUseCaches(false);
394         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         Object ctxt = context.getExternalContext().getContext();
412         Class clazz = ctxt.getClass();
413         Method method = null;
414         try {
415             method = clazz.getMethod("getMimeType", new Class[] { String.class });
416             // Return the container calculated type, if any
417             String result = (String) method.invoke(ctxt,
418                                                    new Object[] { resourceId });
419             if (result != null) {
420                 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         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         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         Object response = context.getExternalContext().getResponse();
504         if (response instanceof HttpServletResponse) {
505             ((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     }
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         if (servletRequest(context)) {
528             HttpServletResponse response = (HttpServletResponse)
529               context.getExternalContext().getResponse();
530             response.sendError(HttpServletResponse.SC_NOT_FOUND, resourceId);
531         } else {
532             throw new IllegalArgumentException(resourceId);
533         }
534 
535     }
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         if (servletRequest(context)) {
551             HttpServletResponse response = (HttpServletResponse)
552               context.getExternalContext().getResponse();
553             response.sendError(HttpServletResponse.SC_NOT_MODIFIED, resourceId);
554         } else {
555             throw new IllegalArgumentException(resourceId);
556         }
557 
558     }
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         if (this.log == null) {
570             log = LogFactory.getLog(AbstractResourceProcessor.class);
571         }
572         return log;
573 
574     }
575 
576 
577 }