View Javadoc

1   /*
2    *  Licensed to the Apache Software Foundation (ASF) under one
3    *  or more contributor license agreements.  See the NOTICE file
4    *  distributed with this work for additional information
5    *  regarding copyright ownership.  The ASF licenses this file
6    *  to you under the Apache License, Version 2.0 (the
7    *  "License"); you may not use this file except in compliance
8    *  with the License.  You may obtain a copy of the License at
9    * 
10   *  http://www.apache.org/licenses/LICENSE-2.0
11   * 
12   *  Unless required by applicable law or agreed to in writing,
13   *  software distributed under the License is distributed on an
14   *  "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
15   *  KIND, either express or implied.  See the License for the
16   *  specific language governing permissions and limitations
17   *  under the License.
18   */
19  package org.apache.myfaces.trinidad.webapp;
20  
21  import java.io.BufferedReader;
22  import java.io.File;
23  import java.io.IOException;
24  import java.io.InputStream;
25  import java.io.InputStreamReader;
26  import java.io.InterruptedIOException;
27  import java.io.OutputStream;
28  import java.io.Reader;
29  import java.lang.reflect.Constructor;
30  import java.lang.reflect.InvocationTargetException;
31  import java.net.SocketException;
32  import java.net.UnknownServiceException;
33  import java.net.URL;
34  import java.net.URLConnection;
35  import java.util.HashMap;
36  import java.util.Map;
37  
38  import javax.faces.FacesException;
39  import javax.faces.FactoryFinder;
40  import javax.faces.context.FacesContext;
41  import javax.faces.context.FacesContextFactory;
42  import javax.faces.event.PhaseListener;
43  import javax.faces.lifecycle.Lifecycle;
44  import javax.servlet.ServletConfig;
45  import javax.servlet.ServletContext;
46  import javax.servlet.ServletException;
47  import javax.servlet.ServletRequest;
48  import javax.servlet.ServletResponse;
49  import javax.servlet.http.HttpServlet;
50  import javax.servlet.http.HttpServletRequest;
51  import javax.servlet.http.HttpServletResponse;
52  
53  import org.apache.myfaces.trinidad.config.Configurator;
54  import org.apache.myfaces.trinidad.logging.TrinidadLogger;
55  import org.apache.myfaces.trinidad.resource.CachingResourceLoader;
56  import org.apache.myfaces.trinidad.resource.DirectoryResourceLoader;
57  import org.apache.myfaces.trinidad.resource.ResourceLoader;
58  import org.apache.myfaces.trinidad.resource.ServletContextResourceLoader;
59  
60  /**
61   * A Servlet which serves up web application resources (images, style sheets,
62   * JavaScript libraries) by delegating to a ResourceLoader.
63   *
64   * The servlet path at which this servlet is registered is used to lookup the
65   * class name of the resource loader implementation.
66   * For example, if this servlet is registered with name "resources" and
67   * URL pattern "/images/*", then its servlet path is "/images".  This is used
68   * to construct the class loader lookup for the text file
69   * "/META-INF/servlets/resources/images.resources" which contains a single line entry
70   * with the class name of the resource loader to use.  This technique is very
71   * similar to "/META-INF/services" lookup that allows the implementation object
72   * to implement an interface in the public API and be used by the public API
73   * but reside in a private implementation JAR.
74   */
75  // TODO use ClassLoader.getResources() and make hierarchical
76  // TODO verify request headers and (cached) response headers
77  // TODO set "private" cache headers in debug mode?
78  public class ResourceServlet extends HttpServlet
79  {
80    /**
81     * 
82     */
83    private static final long serialVersionUID = 4547362994406585148L;
84    
85    /**
86     * Override of Servlet.destroy();
87     */
88    @Override
89    public void destroy()
90    {
91      _loaders = null;
92      _facesContextFactory = null;
93      _lifecycle = null;
94  
95      super.destroy();
96    }
97    
98    /**
99     * Override of Servlet.init();
100    */
101   @Override
102   public void init(
103     ServletConfig config
104     ) throws ServletException
105   {
106     super.init(config);
107 
108     // Acquire our FacesContextFactory instance
109     try
110     {
111       _facesContextFactory = (FacesContextFactory)
112                 FactoryFinder.getFactory
113                 (FactoryFinder.FACES_CONTEXT_FACTORY);
114     }
115     catch (FacesException e)
116     {
117       Throwable rootCause = e.getCause();
118       if (rootCause == null)
119       {
120         throw e;
121       }
122       else
123       {
124         throw new ServletException(e.getMessage(), rootCause);
125       }
126     }
127 
128     // Acquire our Lifecycle instance
129     _lifecycle = new _ResourceLifecycle();
130     _initDebug(config);
131     _loaders = new HashMap<String, ResourceLoader>();
132   }
133 
134   @Override
135   public void service(
136     ServletRequest  request,
137     ServletResponse response
138     ) throws ServletException, IOException
139   {
140     boolean hasFacesContext = false;
141     FacesContext context = FacesContext.getCurrentInstance();
142     // If we happen to invoke the ResourceServlet *via* the
143     // FacesServlet, you get a lot of fun from the recursive
144     // attempt to create a FacesContext.  Developers should not
145     // do this, but it's easy to check
146     if (context != null)
147     {
148       hasFacesContext = true;
149     }
150     else
151     {
152       Configurator.disableConfiguratorServices(request);
153     
154       //=-= Scott O'Bryan =-=
155       // Be careful.  This can be wrapped by other things even though it's meant to be a
156       // Trinidad only resource call.
157       context = _facesContextFactory.getFacesContext(getServletContext(), request, response, _lifecycle);
158     }
159 
160     try
161     {
162       super.service(request, response);
163     }
164     catch (ServletException e)
165     {
166       _LOG.severe(e);
167       throw e;
168     }
169     catch (IOException e)
170     {
171       if (!_canIgnore(e))
172         _LOG.severe(e);
173       throw e;
174     }
175     finally
176     {
177       if (!hasFacesContext)
178         context.release();
179     }
180   }
181 
182   /**
183    * Override of HttpServlet.doGet()
184    */
185   @Override
186   protected void doGet(
187     HttpServletRequest request,
188     HttpServletResponse response
189     ) throws ServletException, IOException
190   {
191     ResourceLoader loader = _getResourceLoader(request);
192     String resourcePath = getResourcePath(request);
193     URL url = loader.getResource(resourcePath);
194 
195     // Make sure the resource is available
196     if (url == null)
197     {
198       response.sendError(HttpServletResponse.SC_NOT_FOUND);
199       return;
200     }
201 
202     // Stream the resource contents to the servlet response
203     URLConnection connection = url.openConnection();
204     connection.setDoInput(true);
205     connection.setDoOutput(false);
206 
207     _setHeaders(connection, response);
208 
209     InputStream in = connection.getInputStream();
210     OutputStream out = response.getOutputStream();
211     byte[] buffer = new byte[_BUFFER_SIZE];
212 
213     try
214     {
215       _pipeBytes(in, out, buffer);
216     }
217     finally
218     {
219       try
220       {
221         in.close();
222       }
223       finally
224       {
225         out.close();
226       }
227     }
228   }
229 
230   /**
231    * Override of HttpServlet.getLastModified()
232    */
233   @Override
234   protected long getLastModified(
235     HttpServletRequest request)
236   {
237     try
238     {
239       ResourceLoader loader = _getResourceLoader(request);
240       String resourcePath = getResourcePath(request);
241       URL url = loader.getResource(resourcePath);
242 
243       if (url == null)
244         return super.getLastModified(request);
245 
246       URLConnection connection = url.openConnection();
247       connection.setDoInput(false);
248       connection.setDoOutput(false);
249 
250       long lastModified = connection.getLastModified();
251       // Make sure the connection is closed
252       try
253       {
254         InputStream is = connection.getInputStream();
255         if (is != null)
256           is.close();
257       }
258       catch (UnknownServiceException use)
259       {
260       }
261 
262       return lastModified;
263     }
264     catch (IOException e)
265     {
266       // Note: API problem with HttpServlet.getLastModified()
267       //       should throw ServletException, IOException
268       return super.getLastModified(request);
269     }
270   }
271 
272   /**
273    * Returns the resource path from the http servlet request.
274    *
275    * @param request  the http servlet request
276    *
277    * @return the resource path
278    */
279   protected String getResourcePath(
280     HttpServletRequest request)
281   {
282     return request.getServletPath() + request.getPathInfo();
283   }
284 
285   /**
286    * Returns the resource loader for the requested servlet path.
287    */
288   private ResourceLoader _getResourceLoader(
289     HttpServletRequest request)
290   {
291     final String servletPath = request.getServletPath();
292     ResourceLoader loader = _loaders.get(servletPath);
293 
294     if (loader == null)
295     {
296       try
297       {
298         String key = "META-INF/servlets/resources" +
299                     servletPath +
300                     ".resources";
301         ClassLoader cl = Thread.currentThread().getContextClassLoader();
302         URL url = cl.getResource(key);
303 
304         if (url != null)
305         {
306           Reader r = new InputStreamReader(url.openStream());
307           BufferedReader br = new BufferedReader(r);
308           try
309           {
310             String className = br.readLine();
311             if (className != null)
312             {
313               className = className.trim();
314               Class<?> clazz = cl.loadClass(className);
315               try
316               {
317                 Constructor<?> decorator = clazz.getConstructor(_DECORATOR_SIGNATURE);
318                 ServletContext context = getServletContext();
319                 File tempdir = (File)
320                 context.getAttribute("javax.servlet.context.tempdir");
321                 ResourceLoader delegate = new DirectoryResourceLoader(tempdir);
322                 loader = (ResourceLoader)
323                 decorator.newInstance(new Object[]{delegate});
324               }
325               catch (InvocationTargetException e)
326               {
327                 // by default, create new instance with no-args constructor
328                 loader = (ResourceLoader) clazz.newInstance();
329               }
330               catch (NoSuchMethodException e)
331               {
332                 // by default, create new instance with no-args constructor
333                 loader = (ResourceLoader) clazz.newInstance();
334               }
335             }
336           }
337           finally
338           {
339             br.close();
340           }
341         }
342         else
343         {
344           // default to serving resources from the servlet context
345           _LOG.warning("Unable to find ResourceLoader for ResourceServlet" +
346                        " at servlet path:{0}" +
347                        "\nCause: Could not find resource:{1}",
348                        new Object[] {servletPath, key});
349           loader = new ServletContextResourceLoader(getServletContext())
350                    {
351                      @Override
352                      public URL getResource(
353                        String path) throws IOException
354                      {
355                        return super.getResource(path);
356                      }
357                    };
358         }
359 
360         // Enable resource caching, but only if we aren't debugging
361         if (!_debug)
362           loader = new CachingResourceLoader(loader);
363       }
364       catch (IllegalAccessException e)
365       {
366         loader = ResourceLoader.getNullResourceLoader();
367       }
368       catch (InstantiationException e)
369       {
370         loader = ResourceLoader.getNullResourceLoader();
371       }
372       catch (ClassNotFoundException e)
373       {
374         loader = ResourceLoader.getNullResourceLoader();
375       }
376       catch (IOException e)
377       {
378         loader = ResourceLoader.getNullResourceLoader();
379       }
380 
381       _loaders.put(servletPath, loader);
382     }
383 
384     return loader;
385   }
386 
387   /**
388    * Reads the specified input stream into the provided byte array storage and
389    * writes it to the output stream.
390    */
391   private static void _pipeBytes(
392     InputStream in,
393     OutputStream out,
394     byte[] buffer
395     ) throws IOException
396   {
397     int length;
398 
399     while ((length = (in.read(buffer))) >= 0)
400     {
401       out.write(buffer, 0, length);
402     }
403   }
404 
405   /**
406    * Initialize whether resource debug mode is enabled.
407    */
408   private void _initDebug(
409     ServletConfig config
410     )
411   {
412     String debug = config.getInitParameter(DEBUG_INIT_PARAM);
413     if (debug == null)
414     {
415       // Check for a context init parameter if servlet init
416       // parameter isn't set
417       debug = config.getServletContext().getInitParameter(DEBUG_INIT_PARAM);
418     }
419 
420     _debug = "true".equalsIgnoreCase(debug);
421     if (_debug)
422     {
423       _LOG.info("RESOURCESERVLET_IN_DEBUG_MODE",DEBUG_INIT_PARAM);
424     }
425   }
426 
427   /**
428    * Sets HTTP headers on the response which tell
429    * the browser to cache the resource indefinitely.
430    */
431   private void _setHeaders(
432     URLConnection       connection,
433     HttpServletResponse response)
434   {
435     String contentType = connection.getContentType();
436     if (contentType == null || "content/unknown".equals(contentType))
437     {
438       URL url = connection.getURL();
439       String resourcePath = url.getPath();
440       if(resourcePath.endsWith(".css"))
441         contentType = "text/css";
442       else
443         contentType = getServletContext().getMimeType(resourcePath);
444     }
445     response.setContentType(contentType);
446 
447     int contentLength = connection.getContentLength();
448     if (contentLength >= 0)
449       response.setContentLength(contentLength);
450 
451     long lastModified = connection.getLastModified();
452     if (lastModified >= 0)
453       response.setDateHeader("Last-Modified", lastModified);
454 
455     // If we're not in debug mode, set cache headers
456     if (!_debug)
457     {
458       // We set two headers: Cache-Control and Expires.
459       // This combination lets browsers know that it is
460       // okay to cache the resource indefinitely.
461 
462       // Set Cache-Control to "Public".
463       response.setHeader("Cache-Control", "Public");
464 
465       // Set Expires to current time + one year.
466       long currentTime = System.currentTimeMillis();
467 
468       response.setDateHeader("Expires", currentTime + ONE_YEAR_MILLIS);
469     }
470   }
471 
472   private static boolean _canIgnore(Throwable t)
473   {
474     if (t instanceof InterruptedIOException)
475     {
476       // All "interrupted" IO is not notable
477       return true;
478     }
479     else if (t instanceof SocketException)
480     {
481       // And any sort of SocketException should also be
482       // ignored (Internet Explorer is a prime source of these,
483       // as it doesn't try to close down sockets properly
484       // when a user cancels)
485       return true;
486     }
487     else if (t instanceof IOException)
488     {
489       String message = t.getMessage();
490       // Check for "Broken pipe" and "connection was aborted"/
491       // "connection abort" messages
492       if ((message != null) &&
493           ((message.indexOf("Broken pipe") >= 0) ||
494            (message.indexOf("abort") >= 0)))
495         return true;
496     }
497     return false;
498   }
499 
500   static private class _ResourceLifecycle extends Lifecycle
501   {
502     @Override
503     public void execute(FacesContext p0) throws FacesException
504     {
505     }
506 
507     @Override
508     public PhaseListener[] getPhaseListeners()
509     {
510       return null;
511     }
512 
513     @Override
514     public void removePhaseListener(PhaseListener p0)
515     {
516     }
517 
518     @Override
519     public void render(FacesContext p0) throws FacesException
520     {
521     }
522 
523     @Override
524     public void addPhaseListener(PhaseListener p0)
525     {
526     }
527   }
528 
529   /**
530    * Context parameter for activating debug mode, which will disable
531    * caching.
532    */
533   public static final String DEBUG_INIT_PARAM =
534     "org.apache.myfaces.trinidad.resource.DEBUG";
535 
536   // One year in milliseconds.  (Actually, just short of on year, since
537   // RFC 2616 says Expires should not be more than one year out, so
538   // cutting back just to be safe.)
539   public static final long ONE_YEAR_MILLIS = 31363200000L;
540 
541   
542   private static final Class[] _DECORATOR_SIGNATURE =
543                                   new Class[]{ResourceLoader.class};
544 
545   private static final TrinidadLogger _LOG = TrinidadLogger.createTrinidadLogger(ResourceServlet.class);
546 
547   // Size of buffer used to read in resource contents
548   private static final int _BUFFER_SIZE = 2048;
549 
550   private boolean _debug;
551   private Map<String, ResourceLoader> _loaders;
552   private FacesContextFactory _facesContextFactory;
553   private Lifecycle _lifecycle;
554 }