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.URL;
33  import java.net.URLConnection;
34  import java.util.HashMap;
35  import java.util.Map;
36  
37  import javax.faces.FacesException;
38  import javax.faces.FactoryFinder;
39  import javax.faces.application.ProjectStage;
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.naming.Context;
45  import javax.naming.InitialContext;
46  import javax.naming.NamingException;
47  import javax.servlet.ServletConfig;
48  import javax.servlet.ServletContext;
49  import javax.servlet.ServletException;
50  import javax.servlet.ServletRequest;
51  import javax.servlet.ServletResponse;
52  import javax.servlet.http.HttpServlet;
53  import javax.servlet.http.HttpServletRequest;
54  import javax.servlet.http.HttpServletResponse;
55  
56  import org.apache.myfaces.trinidad.config.Configurator;
57  import org.apache.myfaces.trinidad.logging.TrinidadLogger;
58  import org.apache.myfaces.trinidad.resource.CachingResourceLoader;
59  import org.apache.myfaces.trinidad.resource.DirectoryResourceLoader;
60  import org.apache.myfaces.trinidad.resource.ResourceLoader;
61  import org.apache.myfaces.trinidad.resource.ServletContextResourceLoader;
62  import org.apache.myfaces.trinidad.util.URLUtils;
63  
64  /**
65   * A Servlet which serves up web application resources (images, style sheets,
66   * JavaScript libraries) by delegating to a ResourceLoader.
67   *
68   * The servlet path at which this servlet is registered is used to lookup the
69   * class name of the resource loader implementation.
70   * For example, if this servlet is registered with name "resources" and
71   * URL pattern "/images/*", then its servlet path is "/images".  This is used
72   * to construct the class loader lookup for the text file
73   * "/META-INF/servlets/resources/images.resources" which contains a single line entry
74   * with the class name of the resource loader to use.  This technique is very
75   * similar to "/META-INF/services" lookup that allows the implementation object
76   * to implement an interface in the public API and be used by the public API
77   * but reside in a private implementation JAR.
78   */
79  // TODO use ClassLoader.getResources() and make hierarchical
80  // TODO verify request headers and (cached) response headers
81  // TODO set "private" cache headers in debug mode?
82  public class ResourceServlet extends HttpServlet
83  {
84    /**
85     * 
86     */
87    private static final long serialVersionUID = 4547362994406585148L;
88    
89    /**
90     * Override of Servlet.destroy();
91     */
92    @Override
93    public void destroy()
94    {
95      _loaders = null;
96      _facesContextFactory = null;
97      _lifecycle = null;
98  
99      super.destroy();
100   }
101   
102   /**
103    * Override of Servlet.init();
104    */
105   @Override
106   public void init(
107     ServletConfig config
108     ) throws ServletException
109   {
110     super.init(config);
111 
112     // Acquire our FacesContextFactory instance
113     try
114     {
115       _facesContextFactory = (FacesContextFactory)
116                 FactoryFinder.getFactory
117                 (FactoryFinder.FACES_CONTEXT_FACTORY);
118     }
119     catch (FacesException e)
120     {
121       Throwable rootCause = e.getCause();
122       if (rootCause == null)
123       {
124         throw e;
125       }
126       else
127       {
128         throw new ServletException(e.getMessage(), rootCause);
129       }
130     }
131 
132     // Acquire our Lifecycle instance
133     _lifecycle = new _ResourceLifecycle();
134     _initDebug(config);
135     _loaders = new HashMap<String, ResourceLoader>();
136   }
137 
138   @Override
139   public void service(
140     ServletRequest  request,
141     ServletResponse response
142     ) throws ServletException, IOException
143   {
144     boolean hasFacesContext = false;
145     FacesContext context = FacesContext.getCurrentInstance();
146     // If we happen to invoke the ResourceServlet *via* the
147     // FacesServlet, you get a lot of fun from the recursive
148     // attempt to create a FacesContext.  Developers should not
149     // do this, but it's easy to check
150     if (context != null)
151     {
152       hasFacesContext = true;
153     }
154     else
155     {
156       Configurator.disableConfiguratorServices(request);
157     
158       //=-= Scott O'Bryan =-=
159       // Be careful.  This can be wrapped by other things even though it's meant to be a
160       // Trinidad only resource call.
161       context = _facesContextFactory.getFacesContext(getServletContext(), request, response, _lifecycle);
162     }
163 
164     try
165     {
166       super.service(request, response);
167     }
168     catch (ServletException e)
169     {
170       _LOG.severe(e);
171       throw e;
172     }
173     catch (IOException e)
174     {
175       if (!_canIgnore(e))
176         _LOG.severe(e);
177       throw e;
178     }
179     finally
180     {
181       if (!hasFacesContext)
182         context.release();
183     }
184   }
185 
186   /**
187    * Override of HttpServlet.doGet()
188    */
189   @Override
190   protected void doGet(
191     HttpServletRequest request,
192     HttpServletResponse response
193     ) throws ServletException, IOException
194   {
195     ResourceLoader loader = _getResourceLoader(request);
196     String resourcePath = getResourcePath(request);
197     URL url = loader.getResource(resourcePath);
198 
199     // Make sure the resource is available
200     if (url == null)
201     {
202       // log some details on the failure
203       _LOG.warning("URL for resource not found.\n  resourcePath: {0}\n  loader class name: {1}\n  request.pathTranslated: {2}\n  request.requestURL: {3}",
204                    new Object[] { resourcePath, 
205                                   loader, 
206                                   request.getPathTranslated(), 
207                                   request.getRequestURL() });
208       response.sendError(HttpServletResponse.SC_NOT_FOUND);
209       return;
210     }
211 
212     // Stream the resource contents to the servlet response
213     URLConnection connection = url.openConnection();
214     connection.setDoInput(true);
215     connection.setDoOutput(false);
216 
217     _setHeaders(connection, response, loader);
218 
219     InputStream in = connection.getInputStream();
220     OutputStream out = response.getOutputStream();
221     byte[] buffer = new byte[_BUFFER_SIZE];
222 
223     try
224     {
225       _pipeBytes(in, out, buffer);
226     }
227     finally
228     {
229       try
230       {
231         in.close();
232       }
233       finally
234       {
235         out.close();
236       }
237     }
238   }
239 
240   /**
241    * Override of HttpServlet.getLastModified()
242    */
243   @Override
244   protected long getLastModified(
245     HttpServletRequest request)
246   {
247     try
248     {
249       ResourceLoader loader = _getResourceLoader(request);
250       String resourcePath = getResourcePath(request);
251       URL url = loader.getResource(resourcePath);
252 
253       if (url == null)
254         return super.getLastModified(request);
255 
256       return URLUtils.getLastModified(url);
257     }
258     catch (IOException e)
259     {
260       // Note: API problem with HttpServlet.getLastModified()
261       //       should throw ServletException, IOException
262       return super.getLastModified(request);
263     }
264   }
265 
266   /**
267    * Returns the resource path from the http servlet request.
268    *
269    * @param request  the http servlet request
270    *
271    * @return the resource path
272    */
273   protected String getResourcePath(
274     HttpServletRequest request)
275   {
276     return request.getServletPath() + request.getPathInfo();
277   }
278 
279   /**
280    * Returns the resource loader for the requested servlet path.
281    */
282   private ResourceLoader _getResourceLoader(
283     HttpServletRequest request)
284   {
285     final String servletPath = request.getServletPath();
286     ResourceLoader loader = _loaders.get(servletPath);
287 
288     if (loader == null)
289     {
290       try
291       {
292         String key = "META-INF/servlets/resources" +
293                     servletPath +
294                     ".resources";
295         ClassLoader cl = Thread.currentThread().getContextClassLoader();
296         URL url = cl.getResource(key);
297 
298         if (url != null)
299         {
300           Reader r = new InputStreamReader(url.openStream());
301           BufferedReader br = new BufferedReader(r);
302           try
303           {
304             String className = br.readLine();
305             if (className != null)
306             {
307               className = className.trim();
308               Class<?> clazz = cl.loadClass(className);
309               try
310               {
311                 Constructor<?> decorator = clazz.getConstructor(_DECORATOR_SIGNATURE);
312                 ServletContext context = getServletContext();
313                 File tempdir = (File)
314                 context.getAttribute("javax.servlet.context.tempdir");
315                 ResourceLoader delegate = new DirectoryResourceLoader(tempdir);
316                 loader = (ResourceLoader)
317                 decorator.newInstance(new Object[]{delegate});
318               }
319               catch (InvocationTargetException e)
320               {
321                 // by default, create new instance with no-args constructor
322                 loader = (ResourceLoader) clazz.newInstance();
323               }
324               catch (NoSuchMethodException e)
325               {
326                 // by default, create new instance with no-args constructor
327                 loader = (ResourceLoader) clazz.newInstance();
328               }
329             }
330           }
331           finally
332           {
333             br.close();
334           }
335         }
336         else
337         {
338           // default to serving resources from the servlet context
339           _LOG.warning("Unable to find ResourceLoader for ResourceServlet" +
340                        " at servlet path:{0}" +
341                        "\nCause: Could not find resource:{1}",
342                        new Object[] {servletPath, key});
343           loader = new ServletContextResourceLoader(getServletContext())
344                    {
345                      @Override
346                      public URL getResource(
347                        String path) throws IOException
348                      {
349                        return super.getResource(path);
350                      }
351                    };
352         }
353 
354         // Enable resource caching, but only if we aren't debugging
355         if (!_debug && loader.isCachable())
356         {
357           loader = new CachingResourceLoader(loader);
358         }
359       }
360       catch (IllegalAccessException e)
361       {
362         loader = ResourceLoader.getNullResourceLoader();
363       }
364       catch (InstantiationException e)
365       {
366         loader = ResourceLoader.getNullResourceLoader();
367       }
368       catch (ClassNotFoundException e)
369       {
370         loader = ResourceLoader.getNullResourceLoader();
371       }
372       catch (IOException e)
373       {
374         loader = ResourceLoader.getNullResourceLoader();
375       }
376 
377       _loaders.put(servletPath, loader);
378     }
379 
380     return loader;
381   }
382 
383   /**
384    * Reads the specified input stream into the provided byte array storage and
385    * writes it to the output stream.
386    */
387   private static void _pipeBytes(
388     InputStream in,
389     OutputStream out,
390     byte[] buffer
391     ) throws IOException
392   {
393     int length;
394 
395     while ((length = (in.read(buffer))) >= 0)
396     {
397       out.write(buffer, 0, length);
398     }
399   }
400 
401   /**
402    * Initialize whether resource debug mode is enabled.
403    */
404   private void _initDebug(
405     ServletConfig config
406     )
407   {
408     String debug = config.getInitParameter(DEBUG_INIT_PARAM);
409     if (debug == null)
410     {
411       // Check for a context init parameter if servlet init
412       // parameter isn't set
413       debug = config.getServletContext().getInitParameter(DEBUG_INIT_PARAM);
414     }
415 
416     // private call to get the used JSF 2.0 ProjectStage as we don't have
417     // access to the FacesContext object here...
418     ProjectStage currentStage = _getFacesProjectStage(config.getServletContext());
419 
420     if (debug != null)
421     {
422       _debug = "true".equalsIgnoreCase(debug);  
423     }
424     else
425     {
426       // if the DDEBUG_INIT_PARAM parameter has NOT been specified, let us
427       // apply the DEFAULT values for the certain Project Stages:
428       // -PRODUCTION we want this value to be FALSE;
429       // -other stages we use TRUE
430       _debug = !(ProjectStage.Production.equals(currentStage));
431     }
432 
433     if (_debug)
434     {
435       // If DEBUG_INIT_PARAM is TRUE on Production-Stage, we
436       // generate a WARNING msg
437       if (ProjectStage.Production.equals(currentStage))
438       {
439         _LOG.warning("RESOURCESERVLET_IN_DEBUG_MODE",DEBUG_INIT_PARAM);
440       }
441       else
442       {
443         _LOG.info("RESOURCESERVLET_IN_DEBUG_MODE",DEBUG_INIT_PARAM); 
444       }
445     }
446   }
447 
448   /**
449    * private version of the <code>Application.getProjectStage()</code>. See the 
450    * original JavaDoc for a description of the underlying algorithm.
451    * 
452    * It is written as we do not have access to the FacesContext object at the point
453    * of executing this method. 
454    * 
455    * This code comes from the <b>Apache MyFaces 2.0</b> implementation.
456    */
457   private ProjectStage _getFacesProjectStage(ServletContext servletContext)
458   {
459     if (_projectStage == null)
460     {
461       String stageName = null;
462       // Look for a JNDI environment entry under the key given by the
463       // value of ProjectStage.PROJECT_STAGE_JNDI_NAME (a String)
464       try
465       {
466         Context ctx = new InitialContext();
467         Object temp = ctx.lookup(ProjectStage.PROJECT_STAGE_JNDI_NAME);
468         if (temp != null)
469         {
470           if (temp instanceof String)
471           {
472             stageName = (String) temp;
473           }
474           else
475           {
476             if (_LOG.isSevere())
477             {
478               _LOG.severe("Invalid JNDI lookup for key " + ProjectStage.PROJECT_STAGE_JNDI_NAME);
479             }
480           }
481         }
482       }
483       catch (NamingException e)
484       {
485         // no-op we need to ignore this...
486       }
487 
488       /*
489        * If found, continue with the algorithm below, otherwise, look for an entry in the initParamMap of the
490        * ExternalContext from the current FacesContext with the key ProjectStage.PROJECT_STAGE_PARAM_NAME
491        */
492       if (stageName == null)
493       {
494         stageName = servletContext.getInitParameter(ProjectStage.PROJECT_STAGE_PARAM_NAME);
495       }
496       
497       // If a value is found found
498       if (stageName != null)
499       {
500         /*
501          * see if an enum constant can be obtained by calling ProjectStage.valueOf(), passing the value from the
502          * initParamMap. If this succeeds without exception, save the value and return it.
503          */
504         try
505         {
506           _projectStage = ProjectStage.valueOf(stageName);
507           return _projectStage;
508         }
509         catch (IllegalArgumentException e)
510         {
511           _LOG.severe("Couldn't discover the current project stage", e);
512         }
513       }
514       else
515       {
516         if (_LOG.isInfo())
517         {
518           _LOG.info("Couldn't discover the current project stage, using " + ProjectStage.Production);
519         }
520       }
521       /*
522        * If not found, or any of the previous attempts to discover the enum constant value have failed, log a
523        * descriptive error message, assign the value as ProjectStage.Production and return it.
524        */
525 
526       _projectStage = ProjectStage.Production;      
527     }
528 
529     return _projectStage;
530   }
531 
532   /**
533    * Sets HTTP headers on the response which tell
534    * the browser to cache the resource indefinitely.
535    */
536   private void _setHeaders(
537     URLConnection       connection,
538     HttpServletResponse response,
539     ResourceLoader      loader)
540   {
541     String resourcePath;
542     URL    url;
543     String contentType  = ResourceLoader.getContentType(loader, connection);
544 
545     if (contentType == null || "content/unknown".equals(contentType))
546     {
547       url = connection.getURL();
548       resourcePath = url.getPath();
549 
550       // 'Case' statement for unknown content types
551       if (resourcePath.endsWith(".css"))
552         contentType = "text/css";
553       else if (resourcePath.endsWith(".js"))
554         contentType = "application/x-javascript";
555       else if (resourcePath.endsWith(".cur") || resourcePath.endsWith(".ico"))
556         contentType = "image/vnd.microsoft.icon";
557       else
558         contentType = getServletContext().getMimeType(resourcePath);
559 
560       // The resource has an file extension we have not
561       // included in the case statement above
562       if (contentType == null)
563       {
564         _LOG.warning("ResourceServlet._setHeaders(): " +
565                      "Content type for {0} is NULL!\n" +
566                      "Cause: Unknown file extension",
567                      resourcePath);
568       }
569     }
570 
571     if (contentType != null)
572     {
573       response.setContentType(contentType);
574       int contentLength = connection.getContentLength();
575 
576       if (contentLength >= 0)
577         response.setContentLength(contentLength);
578     }
579 
580     long lastModified;
581     try
582     {
583       lastModified = URLUtils.getLastModified(connection);
584     }
585     catch (IOException exception)
586     {
587       lastModified = -1;
588     }
589 
590     if (lastModified >= 0)
591       response.setDateHeader("Last-Modified", lastModified);
592 
593     // If we're not in debug mode, set cache headers
594     if (!_debug)
595     {
596       // We set two headers: Cache-Control and Expires.
597       // This combination lets browsers know that it is
598       // okay to cache the resource indefinitely.
599 
600       // Set Cache-Control to "Public".
601       response.setHeader("Cache-Control", "Public");
602 
603       // Set Expires to current time + one year.
604       long currentTime = System.currentTimeMillis();
605 
606       response.setDateHeader("Expires", currentTime + ONE_YEAR_MILLIS);
607     }
608   }
609 
610   private static boolean _canIgnore(Throwable t)
611   {
612     if (t instanceof InterruptedIOException)
613     {
614       // All "interrupted" IO is not notable
615       return true;
616     }
617     else if (t instanceof SocketException)
618     {
619       // And any sort of SocketException should also be
620       // ignored (Internet Explorer is a prime source of these,
621       // as it doesn't try to close down sockets properly
622       // when a user cancels)
623       return true;
624     }
625     else if (t instanceof IOException)
626     {
627       String message = t.getMessage();
628       // Check for "Broken pipe" and "connection was aborted"/
629       // "connection abort" messages
630       if ((message != null) &&
631           ((message.indexOf("Broken pipe") >= 0) ||
632            (message.indexOf("abort") >= 0)))
633         return true;
634     }
635     return false;
636   }
637 
638   static private class _ResourceLifecycle extends Lifecycle
639   {
640     @Override
641     public void execute(FacesContext p0) throws FacesException
642     {
643     }
644 
645     @Override
646     public PhaseListener[] getPhaseListeners()
647     {
648       return null;
649     }
650 
651     @Override
652     public void removePhaseListener(PhaseListener p0)
653     {
654     }
655 
656     @Override
657     public void render(FacesContext p0) throws FacesException
658     {
659     }
660 
661     @Override
662     public void addPhaseListener(PhaseListener p0)
663     {
664     }
665   }
666 
667   /**
668    * Context parameter for activating debug mode, which will disable
669    * caching.
670    */
671   public static final String DEBUG_INIT_PARAM =
672     "org.apache.myfaces.trinidad.resource.DEBUG";
673 
674   // One year in milliseconds.  (Actually, just short of on year, since
675   // RFC 2616 says Expires should not be more than one year out, so
676   // cutting back just to be safe.)
677   public static final long ONE_YEAR_MILLIS = 31363200000L;
678 
679   private static final Class[] _DECORATOR_SIGNATURE =
680                                   new Class[]{ResourceLoader.class};
681 
682   private static final TrinidadLogger _LOG = TrinidadLogger.createTrinidadLogger(ResourceServlet.class);
683 
684   // Size of buffer used to read in resource contents
685   private static final int _BUFFER_SIZE = 2048;
686 
687   private boolean _debug;
688   private Map<String, ResourceLoader> _loaders;
689   private FacesContextFactory _facesContextFactory;
690   private Lifecycle _lifecycle;
691   private ProjectStage _projectStage;
692 }