Classes in this File | Line Coverage | Branch Coverage | Complexity | ||||
AbstractResourceProcessor |
|
| 3.1;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 | } |