2009/05/20 - Apache Shale has been retired.
For more information, please explore the Attic.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
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
52
53
54 /***
55 * <p>The <code>Log</code> instance for this class.</p>
56 */
57 private transient Log log = null;
58
59
60
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
79 if (resourceId == null) {
80 throw new NullPointerException();
81 }
82 if (!resourceId.startsWith("/")) {
83 throw new IllegalArgumentException(resourceId);
84 }
85
86
87
88 if (context.getResponseComplete()) {
89 return;
90 }
91
92
93 if (!accept(resourceId)) {
94 if (log().isTraceEnabled()) {
95 log().trace("Resource id '" + resourceId
96 + "' rejected by include/exclude rules");
97 }
98
99
100
101 sendNotFound(context, resourceId);
102 context.responseComplete();
103 return;
104 }
105
106
107
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
123
124
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
137 sendLastModified(context, getLastModifiedString());
138
139
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
157 context.responseComplete();
158
159 }
160
161
162
163
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
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
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
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
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
417 String result = (String) method.invoke(ctxt,
418 new Object[] { resourceId });
419 if (result != null) {
420 return result;
421 }
422
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
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
507
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
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 }