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  
30  import java.lang.reflect.Constructor;
31  import java.lang.reflect.InvocationTargetException;
32  
33  import java.net.SocketException;
34  import java.net.URL;
35  import java.net.URLConnection;
36  
37  import java.util.Arrays;
38  import java.util.List;
39  import java.util.Map;
40  import java.util.concurrent.ConcurrentHashMap;
41  import java.util.concurrent.ConcurrentMap;
42  
43  import javax.faces.FacesException;
44  import javax.faces.FactoryFinder;
45  import javax.faces.application.ProjectStage;
46  import javax.faces.context.ExternalContext;
47  import javax.faces.context.FacesContext;
48  import javax.faces.context.FacesContextFactory;
49  import javax.faces.event.PhaseListener;
50  import javax.faces.lifecycle.Lifecycle;
51  
52  import javax.naming.Context;
53  import javax.naming.InitialContext;
54  import javax.naming.NamingException;
55  
56  import javax.servlet.ServletConfig;
57  import javax.servlet.ServletContext;
58  import javax.servlet.ServletException;
59  import javax.servlet.ServletRequest;
60  import javax.servlet.ServletResponse;
61  import javax.servlet.http.HttpServlet;
62  import javax.servlet.http.HttpServletRequest;
63  import javax.servlet.http.HttpServletResponse;
64  
65  import org.apache.myfaces.trinidad.config.Configurator;
66  import org.apache.myfaces.trinidad.logging.TrinidadLogger;
67  import org.apache.myfaces.trinidad.resource.CachingResourceLoader;
68  import org.apache.myfaces.trinidad.resource.DirectoryResourceLoader;
69  import org.apache.myfaces.trinidad.resource.ResourceLoader;
70  import org.apache.myfaces.trinidad.resource.ServletContextResourceLoader;
71  import org.apache.myfaces.trinidad.util.URLUtils;
72  
73  /**
74   * A Servlet which serves up web application resources (images, style sheets,
75   * JavaScript libraries) by delegating to a ResourceLoader.
76   *
77   * The servlet path at which this servlet is registered is used to lookup the
78   * class name of the resource loader implementation.
79   * For example, if this servlet is registered with name "resources" and
80   * URL pattern "/images/*", then its servlet path is "/images".  This is used
81   * to construct the class loader lookup for the text file
82   * "/META-INF/servlets/resources/images.resources" which contains a single line entry
83   * with the class name of the resource loader to use.  This technique is very
84   * similar to "/META-INF/services" lookup that allows the implementation object
85   * to implement an interface in the public API and be used by the public API
86   * but reside in a private implementation JAR.
87   */
88  // TODO use ClassLoader.getResources() and make hierarchical
89  // TODO verify request headers and (cached) response headers
90  // TODO set "private" cache headers in debug mode?
91  public class ResourceServlet extends HttpServlet
92  {
93    /**
94       *
95       */
96      @SuppressWarnings("compatibility:8282627001212629976")
97      private static final long serialVersionUID = 4547362994406585148L;
98    
99    /**
100    * Override of Servlet.destroy();
101    */
102   @Override
103   public void destroy()
104   {
105     _loaders = null;
106     _loaderErrors = null;
107     _facesContextFactory = null;
108     _lifecycle = null;
109 
110     super.destroy();
111   }
112   
113   /**
114    * Override of Servlet.init();
115    */
116   @Override
117   public void init(
118     ServletConfig config
119     ) throws ServletException
120   {
121     super.init(config);
122 
123     // Acquire our FacesContextFactory instance
124     try
125     {
126       _facesContextFactory = (FacesContextFactory)
127                 FactoryFinder.getFactory
128                 (FactoryFinder.FACES_CONTEXT_FACTORY);
129     }
130     catch (FacesException e)
131     {
132       Throwable rootCause = e.getCause();
133       if (rootCause == null)
134       {
135         throw e;
136       }
137       else
138       {
139         throw new ServletException(e.getMessage(), rootCause);
140       }
141     }
142 
143     // Acquire our Lifecycle instance
144     _lifecycle = new _ResourceLifecycle();
145     _initDebug(config);
146     _loaders = new ConcurrentHashMap<String, ResourceLoader>();
147     _loaderErrors = new ConcurrentHashMap<String, Class<?>>();
148   }
149 
150   @Override
151   public void service(
152     ServletRequest  request,
153     ServletResponse response
154     ) throws ServletException, IOException
155   {
156     boolean hasFacesContext = false;
157     FacesContext context = FacesContext.getCurrentInstance();
158 
159     // If we happen to invoke the ResourceServlet *via* the
160     // FacesServlet, you get a lot of fun from the recursive
161     // attempt to create a FacesContext.  Developers should not
162     // do this, but it's easy to check
163     if (context != null)
164     {
165       // Additional _isContextValid check on the context to workaround Mojarra issues like this one...
166       // https://java.net/jira/browse/JAVASERVERFACES-2533
167       if (_isContextValid(context))
168       {
169         hasFacesContext = true;
170       }
171       else
172       {
173         // clean up the invalid context before we try to create one
174         context.release();
175       }
176     }
177     
178     if (!hasFacesContext)
179     {
180       Configurator.disableConfiguratorServices(request);
181     
182       //=-= Scott O'Bryan =-=
183       // Be careful.  This can be wrapped by other things even though it's meant to be a
184       // Trinidad only resource call.
185       context = _facesContextFactory.getFacesContext(getServletContext(), request, response, _lifecycle);
186     }
187 
188     try
189     {
190       super.service(request, response);
191     }
192     catch (ServletException e)
193     {
194       _logServiceException(e, request);
195       throw e;
196     }
197     catch (IOException e)
198     {
199       if (!_canIgnore(e))
200         _logServiceException(e, request);
201       throw e;
202     }
203     catch (RuntimeException e)
204     {
205       _logServiceException(e, request);
206       throw e;
207     }
208     catch (Error e)
209     {
210       _logServiceException(e, request);
211       throw e;
212     }
213     finally
214     {
215       // cleanup the context we created for serving this request
216       if (!hasFacesContext)
217         context.release();
218     }
219   }
220   
221   /**
222    * Override of HttpServlet.doGet()
223    */
224   @Override
225   protected void doGet(
226     HttpServletRequest request,
227     HttpServletResponse response
228     ) throws ServletException, IOException
229   {
230     ResourceLoader loader = _getResourceLoader(request);
231     String resourcePath = getResourcePath(request);
232     URL url = loader.getResource(resourcePath);
233 
234     // Make sure the resource is available
235     if (url == null)
236     {
237       _logURLNotFound(request, loader, resourcePath);
238       response.sendError(HttpServletResponse.SC_NOT_FOUND);
239       return;
240     }
241 
242     // Stream the resource contents to the servlet response
243     URLConnection connection = url.openConnection();
244     connection.setDoInput(true);
245     connection.setDoOutput(false);
246 
247     //We need to do a connect here.  Some connections, like file connections and
248     //whatnot may have header information right away.  Other connections, like
249     //to external resources, will not have been able to connect until this is called.
250     //The reason this worked before is the getInputStream implicitly called the
251     //connect, but the header information which was returned would before the stream
252     //was obtained would not be avialble.
253     connection.connect();    
254  
255     _setHeaders(connection, response, loader);
256 
257     InputStream in = connection.getInputStream();
258     OutputStream out = response.getOutputStream();
259     byte[] buffer = new byte[_BUFFER_SIZE];
260 
261     try
262     {
263       _pipeBytes(in, out, buffer);
264     }
265     finally
266     {
267       try
268       {
269         in.close();
270       }
271       finally
272       {
273         out.close();
274       }
275     }
276   }
277 
278   /**
279    * Override of HttpServlet.getLastModified()
280    */
281   @Override
282   protected long getLastModified(
283     HttpServletRequest request)
284   {
285     try
286     {
287       ResourceLoader loader = _getResourceLoader(request);
288       String resourcePath = getResourcePath(request);
289       URL url = loader.getResource(resourcePath);
290 
291       if (url == null)
292         return super.getLastModified(request);
293 
294       return URLUtils.getLastModified(url);
295     }
296     catch (IOException e)
297     {
298       // Note: API problem with HttpServlet.getLastModified()
299       //       should throw ServletException, IOException
300       return super.getLastModified(request);
301     }
302   }
303 
304   /**
305    * Returns the resource path from the http servlet request.
306    *
307    * @param request  the http servlet request
308    *
309    * @return the resource path
310    */
311   protected String getResourcePath(
312     HttpServletRequest request)
313   {
314     return request.getServletPath() + request.getPathInfo();
315   }
316 
317   /**
318    * Checks if the faces context that we have is valid for this request
319    */
320   private boolean _isContextValid(FacesContext context)
321   {
322     ExternalContext ec = context.getExternalContext();
323     
324     // a bogus context would not have a request object
325     return ((ec != null) && (ec.getRequest() != null));
326   }
327 
328   /**
329    * Returns the resource loader for the requested servlet path. 
330    */
331   private ResourceLoader _getResourceLoader(
332     String servletPath)
333   {
334     ResourceLoader loader = null;
335     
336     try
337     {
338       String key = "META-INF/servlets/resources" +
339                   servletPath +
340                   ".resources";
341       ClassLoader cl = Thread.currentThread().getContextClassLoader();
342       URL url = cl.getResource(key);
343 
344       if (url != null)
345       {
346         Reader r = new InputStreamReader(url.openStream());
347         BufferedReader br = new BufferedReader(r);
348         try
349         {
350           String className = br.readLine();
351           if (className != null)
352           {
353             className = className.trim();
354             Class<?> clazz = cl.loadClass(className);
355             try
356             {
357               // check if this is a decorator first, and if not, fall back to the no-arg constructor. 
358               Constructor<?> decorator = clazz.getConstructor(_DECORATOR_SIGNATURE);
359               ServletContext context = getServletContext();
360               File tempdir = (File)
361               context.getAttribute("javax.servlet.context.tempdir");
362               ResourceLoader delegate = new DirectoryResourceLoader(tempdir);
363               loader = (ResourceLoader) decorator.newInstance(new Object[]{delegate});
364             }
365             catch (InvocationTargetException e)
366             {
367               // by default, create new instance with no-args constructor
368               _logLoaderException(e, servletPath);
369               loader = (ResourceLoader) clazz.newInstance();
370             }
371             catch (NoSuchMethodException e)
372             {
373               // by default, create new instance with no-args constructor
374               loader = (ResourceLoader) clazz.newInstance();
375             }
376           }
377         }
378         finally
379         {
380           br.close();
381         }
382       }
383       else
384       {
385         // default to serving resources from the servlet context
386         if (_LOG.isWarning())
387         {
388           _LOG.warning("Unable to find ResourceLoader for ResourceServlet" +
389                        " at servlet path:{0}" +
390                        "\nCause: Could not find resource:{1}",
391                        new Object[] {servletPath, key});
392         }
393         
394         loader = new ServletContextResourceLoader(getServletContext())
395                  {
396                    @Override
397                    public URL getResource(
398                      String path) throws IOException
399                    {
400                      return super.getResource(path);
401                    }
402                  };
403       }
404 
405       // Enable resource caching, but only if we aren't debugging
406       if (!_debug && loader.isCachable())
407       {
408         loader = new CachingResourceLoader(loader);
409       }
410     }
411     catch (IllegalAccessException e)
412     {
413       loader = logExceptionAndReturnFailureLoader(e, servletPath);
414     }
415     catch (InstantiationException e)
416     {
417       loader = logExceptionAndReturnFailureLoader(e, servletPath);
418     }
419     catch (ClassNotFoundException e)
420     {
421       loader = logExceptionAndReturnFailureLoader(e, servletPath);
422     }
423     catch (IOException e)
424     {
425       loader = logExceptionAndReturnFailureLoader(e, servletPath);
426     }
427 
428     return loader;    
429   }
430   
431   /**
432    * Get the servlet path from the request and see if we've already cached the resource loader for this path.
433    * If not, call _getResourceLoader(String servletPath) to find it.
434    *
435    * @param request
436    * @return
437    */
438   private ResourceLoader _getResourceLoader(
439     HttpServletRequest request)
440   {
441     final String servletPath = request.getServletPath();
442     ResourceLoader loader = _loaders.get(servletPath);
443 
444     if (loader == null)
445     {
446       loader = _getResourceLoader(servletPath);
447 
448       // add the new loader to the loader cache
449       _registerLoader(servletPath, loader);
450     }
451 
452     return loader;
453   }
454 
455   /**
456    * Add the loader to the loader map
457    * @param servletPath
458    * @param loader
459    */
460   private void _registerLoader(String servletPath, ResourceLoader loader)
461   {
462     _loaders.put(servletPath, loader);
463   }
464   
465   /*
466    * exception logger for _getResourceLoader() that will only log one error for multiple failures of the same servlet
467    */
468   private void _logLoaderException(Exception e, String servletPath)
469   {
470     Class<?> previousExceptionClass = _loaderErrors.get(servletPath);
471     
472     // if we haven't logged an error for this servletPath, or the exception class differs, log an error
473     if (previousExceptionClass == null || previousExceptionClass != e.getClass())
474     {
475       _LOG.severe(e);
476       
477       if (e.getCause() != null)
478         _LOG.severe("Caused by ", e.getCause());
479             
480       _loaderErrors.put(servletPath, e.getClass());
481     }
482   }
483 
484   /**
485    * Method to log a warning message with helpful details when we fail to find a URL.
486    * 
487    * @param request
488    * @param loader
489    * @param resourcePath
490    */
491   private void _logURLNotFound(HttpServletRequest request,
492                                ResourceLoader loader,
493                                String resourcePath)
494   {
495     // log some details on the failure
496     if (_LOG.isWarning())
497     {
498       FacesContext context = FacesContext.getCurrentInstance();
499       Object servletContext = _getServletContextFromFacesContext(context);
500       
501       _LOG.warning("URL for resource not found.\n"+
502                    "  resourcePath: {0}\n"+
503                    "  loader class name: {1}\n"+
504                    "  request.pathTranslated: {2}\n"+
505                    "  request.requestURL: {3}\n"+
506                    "  FacesContext: {4}\n" +
507                    "  ServletContext: {5}\n",
508                    new Object[] { resourcePath, 
509                                   loader, 
510                                   request.getPathTranslated(), 
511                                   request.getRequestURL(),
512                                   context,
513                                   servletContext});
514     }
515   }
516   
517   /**
518    * Method to log a severe error when we get an exception in the .service() method.
519    * 
520    * @param e
521    * @param request
522    */
523   private void _logServiceException(Throwable e, ServletRequest  request)
524   {
525     // log some details on the service exception
526     FacesContext context = FacesContext.getCurrentInstance();
527     Object servletContext = _getServletContextFromFacesContext(context);
528     HttpServletRequest sr = (HttpServletRequest) request;
529    
530     _LOG.severe("An Exception occured in ResourceServlet.service().\n"+
531                  "  request.pathTranslated:" + sr.getPathTranslated() + "\n" +
532                  "  request.requestURI:" + sr.getRequestURI() + "\n" +
533                  "  FacesContext: " + context + "\n" +
534                  "  ServletContext: " + servletContext + "\n",
535                  e);
536   }
537 
538   /**
539    *Returns the servlet context object from the FacesContext
540    * 
541    * @param context
542    * @return
543    */
544   private Object _getServletContextFromFacesContext(FacesContext context)
545   {
546     ExternalContext ec = null;
547     Object sc = null;
548     
549     if (context != null)
550     {
551       ec = context.getExternalContext();
552       
553       if (ec != null)
554       {
555         sc = ec.getContext();
556       }
557     }
558     
559     return sc;
560   }
561   
562   /**
563    * Reads the specified input stream into the provided byte array storage and
564    * writes it to the output stream.
565    */
566   private static void _pipeBytes(
567     InputStream in,
568     OutputStream out,
569     byte[] buffer
570     ) throws IOException
571   {
572     int length;
573 
574     while ((length = (in.read(buffer))) >= 0)
575     {
576       out.write(buffer, 0, length);
577     }
578   }
579 
580   /**
581    * Initialize whether resource debug mode is enabled.
582    */
583   private void _initDebug(
584     ServletConfig config
585     )
586   {
587     String debug = config.getInitParameter(DEBUG_INIT_PARAM);
588     if (debug == null)
589     {
590       // Check for a context init parameter if servlet init
591       // parameter isn't set
592       debug = config.getServletContext().getInitParameter(DEBUG_INIT_PARAM);
593     }
594 
595     // private call to get the used JSF 2.0 ProjectStage as we don't have
596     // access to the FacesContext object here...
597     ProjectStage currentStage = _getFacesProjectStage(config.getServletContext());
598 
599     if (debug != null)
600     {
601       _debug = "true".equalsIgnoreCase(debug);  
602     }
603     else
604     {
605       // if the DDEBUG_INIT_PARAM parameter has NOT been specified, let us
606       // apply the DEFAULT values for the certain Project Stages:
607       // -PRODUCTION we want this value to be FALSE;
608       // -other stages we use TRUE
609       _debug = !(ProjectStage.Production.equals(currentStage));
610     }
611 
612     if (_debug)
613     {
614       // If DEBUG_INIT_PARAM is TRUE on Production-Stage, we
615       // generate a WARNING msg
616       if (ProjectStage.Production.equals(currentStage))
617       {
618         _LOG.warning("RESOURCESERVLET_IN_DEBUG_MODE",DEBUG_INIT_PARAM);
619       }
620       else
621       {
622         _LOG.info("RESOURCESERVLET_IN_DEBUG_MODE",DEBUG_INIT_PARAM); 
623       }
624     }
625   }
626 
627   /**
628    * private version of the <code>Application.getProjectStage()</code>. See the 
629    * original JavaDoc for a description of the underlying algorithm.
630    * 
631    * It is written as we do not have access to the FacesContext object at the point
632    * of executing this method. 
633    * 
634    * This code comes from the <b>Apache MyFaces 2.0</b> implementation.
635    */
636   private ProjectStage _getFacesProjectStage(ServletContext servletContext)
637   {
638     if (_projectStage == null)
639     {
640       String stageName = null;
641       // Look for a JNDI environment entry under the key given by the
642       // value of ProjectStage.PROJECT_STAGE_JNDI_NAME (a String)
643       try
644       {
645         Context ctx = new InitialContext();
646         Object temp = ctx.lookup(ProjectStage.PROJECT_STAGE_JNDI_NAME);
647         if (temp != null)
648         {
649           if (temp instanceof String)
650           {
651             stageName = (String) temp;
652           }
653           else
654           {
655             if (_LOG.isSevere())
656             {
657               _LOG.severe("Invalid JNDI lookup for key " + ProjectStage.PROJECT_STAGE_JNDI_NAME);
658             }
659           }
660         }
661       }
662       catch (NamingException e)
663       {
664         // no-op we need to ignore this...
665       }
666 
667       /*
668        * If found, continue with the algorithm below, otherwise, look for an entry in the initParamMap of the
669        * ExternalContext from the current FacesContext with the key ProjectStage.PROJECT_STAGE_PARAM_NAME
670        */
671       if (stageName == null)
672       {
673         stageName = servletContext.getInitParameter(ProjectStage.PROJECT_STAGE_PARAM_NAME);
674       }
675       
676       // If a value is found found
677       if (stageName != null)
678       {
679         /*
680          * see if an enum constant can be obtained by calling ProjectStage.valueOf(), passing the value from the
681          * initParamMap. If this succeeds without exception, save the value and return it.
682          */
683         try
684         {
685           _projectStage = ProjectStage.valueOf(stageName);
686           return _projectStage;
687         }
688         catch (IllegalArgumentException e)
689         {
690           _LOG.severe("Couldn't discover the current project stage", e);
691         }
692       }
693       else
694       {
695         if (_LOG.isInfo())
696         {
697           _LOG.info("Couldn't discover the current project stage, using " + ProjectStage.Production);
698         }
699       }
700       /*
701        * If not found, or any of the previous attempts to discover the enum constant value have failed, log a
702        * descriptive error message, assign the value as ProjectStage.Production and return it.
703        */
704 
705       _projectStage = ProjectStage.Production;      
706     }
707 
708     return _projectStage;
709   }
710 
711   /**
712    * Sets HTTP headers on the response which tell
713    * the browser to cache the resource indefinitely.
714    */
715   private void _setHeaders(
716     URLConnection       connection,
717     HttpServletResponse response,
718     ResourceLoader      loader)
719   {
720     String resourcePath;
721     URL    url;
722     String contentType  = ResourceLoader.getContentType(loader, connection);
723 
724     if (contentType == null || "content/unknown".equals(contentType))
725     {
726       url = connection.getURL();
727       resourcePath = url.getPath();
728 
729       // 'Case' statement for unknown content types
730       if (resourcePath.endsWith(".css"))
731         contentType = "text/css";
732       else if (resourcePath.endsWith(".js"))
733         contentType = "application/x-javascript";
734       else if (resourcePath.endsWith(".cur") || resourcePath.endsWith(".ico"))
735         contentType = "image/vnd.microsoft.icon";
736       else
737         contentType = getServletContext().getMimeType(resourcePath);
738 
739       // The resource has an file extension we have not
740       // included in the case statement above
741       if (contentType == null)
742       {
743         _LOG.warning("ResourceServlet._setHeaders(): " +
744                      "Content type for {0} is NULL!\n" +
745                      "Cause: Unknown file extension",
746                      resourcePath);
747       }
748     }
749 
750     if (contentType != null)
751     {
752       response.setContentType(contentType);
753       int contentLength = connection.getContentLength();
754 
755       if (contentLength >= 0)
756         response.setContentLength(contentLength);
757     }
758 
759     long lastModified;
760     try
761     {
762       lastModified = URLUtils.getLastModified(connection);
763     }
764     catch (IOException exception)
765     {
766       lastModified = 0;
767     }
768 
769     if (lastModified == 0)
770       response.setDateHeader("Last-Modified", lastModified);
771 
772     //Handle cache controls.  So this is a bit complex and confusing.  If
773     //either of the cache headers are provide AND we are not in debug mode
774     //then we should set the cache to a default value.  This is to maintain
775     //backward compatibility.  If, however, the expiration or cache control
776     //headers are provided, we should just use those.
777     String cacheControl =  connection.getHeaderField("cache-control");
778     Long expires = connection.getExpiration();
779     
780     if (!_debug && null == cacheControl && 0 == expires)
781     {
782       // We set two headers: Cache-Control and Expires.
783       // This combination lets browsers know that it is
784       // okay to cache the resource indefinitely.
785 
786       // Set Cache-Control to "Public".
787       cacheControl = "Public";
788       expires = System.currentTimeMillis() + ONE_YEAR_MILLIS;
789     }
790     
791     if(null != cacheControl)
792     {
793       response.setHeader("Cache-Control", cacheControl);
794     }
795     
796     if(0 != expires)
797     {
798       response.setDateHeader("Expires", expires);
799     }
800     
801     Map<String, List<String>> headerMap = connection.getHeaderFields();
802     if(null != headerMap) //There are some wonkey impl's out there, best to guard against null
803     {
804       //put additional headers
805       for(Map.Entry<String, List<String>> entry: connection.getHeaderFields().entrySet())
806       {
807         //We handled a number of headers above and there are others we might want
808         //to exclude.  We test for those headers here.
809         String key = entry.getKey();
810         if(Arrays.binarySearch(_INCLUDED_HEADERS, key.toLowerCase()) >= 0 && null != entry.getValue())
811         {
812           //Header is not excluded, add all the entries
813           for(String value:entry.getValue())
814           {
815             response.addHeader(key, value);
816           }
817         }
818       }
819     }
820   }
821 
822   private static boolean _canIgnore(Throwable t)
823   {
824     if (t instanceof InterruptedIOException)
825     {
826       // All "interrupted" IO is not notable
827       return true;
828     }
829     else if (t instanceof SocketException)
830     {
831       // And any sort of SocketException should also be
832       // ignored (Internet Explorer is a prime source of these,
833       // as it doesn't try to close down sockets properly
834       // when a user cancels)
835       return true;
836     }
837     else if (t instanceof IOException)
838     {
839       String message = t.getMessage();
840       // Check for "Broken pipe" and "connection was aborted"/
841       // "connection abort" messages
842       if ((message != null) &&
843           ((message.indexOf("Broken pipe") >= 0) ||
844            (message.indexOf("abort") >= 0)))
845         return true;
846     }
847     return false;
848   }
849 
850   /**
851    * Instead of calling ResourceLoader.getNullResourceLoader(), we want to return our own version which will
852    * periodically attempt to get the real resource loader and start using that instead.
853    */
854   private ResourceLoader logExceptionAndReturnFailureLoader(Exception e, String servletPath)
855   {
856     _logLoaderException(e, servletPath);
857     return new _ExponentialBackoffResourceLoader(servletPath);
858   }
859 
860   /**
861    * The _ExponentialBackOffResourceLoader is a private class to be used in place of the resource loader returned by
862    * ResourceLoader.getNullResourceLoader().  We want to initially start out with a wait time of 10 miliseconds before
863    * we attempt to get the correct resource loader.  We'll double the wait time for every attempt, until the wait time
864    * is longer than a minute.  At that point, we will still keep trying but no longer double the wait time.
865    */
866   private final class _ExponentialBackoffResourceLoader extends ResourceLoader
867   {
868     /**
869      * Constructs a new repairing resource loader.
870      */
871     protected _ExponentialBackoffResourceLoader(String servletPath)
872     {
873       super(null);
874       
875       // Set the timestamp for this request
876       _initialTimeStamp = System.currentTimeMillis();
877       _nextBackoffTime = _initialTimeStamp + _INITIAL_WAIT_TIME;
878       _servletPath = servletPath;
879     }
880 
881     
882     @Override
883     protected URL findResource(
884       String name
885       ) throws IOException
886     {
887       long currentTime = System.currentTimeMillis();
888 
889       // If we have waited the current wait time, we need to try again.
890       if (currentTime > _nextBackoffTime)
891       {
892         synchronized(this)
893         {
894           // We need to check this again, just in case a different thread already updated the value
895           // If another thread updated the value, we don't need try to get the resource loader or
896           // update the backoff time
897           if (currentTime > _nextBackoffTime)
898           {
899             ResourceLoader newLoader = _getResourceLoader(_servletPath);
900             
901             if (!(newLoader instanceof _ExponentialBackoffResourceLoader))
902             {
903               // We got a real resource loader, lets repair ourself by updating the loader cache.
904               _registerLoader(_servletPath, newLoader);
905               
906               //Log a message
907               _LOG.warning("Fixed resource loader for servlet path: {0}", _servletPath);
908               
909               return newLoader.getResource(name);
910             }
911 
912             // No new loader found, update the next backoff time.
913             // this doesn't produce an exact doubling of the _INITIAL_WAIT_TIME, but we really don't need to
914             // be exact here, as long as we reach the max wait time
915             _nextBackoffTime += Math.min((currentTime - _initialTimeStamp) * 2, _MAX_WAIT_TIME);
916           }
917         }
918       }
919       
920       // We haven't waited long enough, just return null.
921       return null;
922     }
923 
924     private volatile long _nextBackoffTime; // The number of miliseconds to wait before trying again.
925     private final long _initialTimeStamp; // The timestamp of the last failed attempt to get the resource loader
926     private final String _servletPath;
927   }
928 
929   static private class _ResourceLifecycle extends Lifecycle
930   {
931     @Override
932     public void execute(FacesContext p0) throws FacesException
933     {
934     }
935 
936     @Override
937     public PhaseListener[] getPhaseListeners()
938     {
939       return null;
940     }
941 
942     @Override
943     public void removePhaseListener(PhaseListener p0)
944     {
945     }
946 
947     @Override
948     public void render(FacesContext p0) throws FacesException
949     {
950     }
951 
952     @Override
953     public void addPhaseListener(PhaseListener p0)
954     {
955     }
956   }
957 
958   /**
959    * Context parameter for activating debug mode, which will disable
960    * caching.
961    */
962   public static final String DEBUG_INIT_PARAM =
963     "org.apache.myfaces.trinidad.resource.DEBUG";
964 
965   // One year in milliseconds.  (Actually, just short of on year, since
966   // RFC 2616 says Expires should not be more than one year out, so
967   // cutting back just to be safe.)
968   public static final long ONE_YEAR_MILLIS = 31363200000L;
969   private static final long _INITIAL_WAIT_TIME = 10L; // 10 milliseconds  
970   private static final long _MAX_WAIT_TIME = 60000L; // 60 * 1000 milliseconds
971 
972   private static final Class[] _DECORATOR_SIGNATURE =
973                                   new Class[]{ResourceLoader.class};
974 
975   private static final TrinidadLogger _LOG = TrinidadLogger.createTrinidadLogger(ResourceServlet.class);
976 
977   // Size of buffer used to read in resource contents
978   private static final int _BUFFER_SIZE = 2048;
979   
980   private volatile boolean _debug;
981   private volatile ConcurrentMap<String, ResourceLoader> _loaders;
982   private volatile ConcurrentMap<String, Class<?>> _loaderErrors;
983   private volatile FacesContextFactory _facesContextFactory;
984   private volatile Lifecycle _lifecycle;
985   private volatile ProjectStage _projectStage;
986 
987   //There are only a handful of headers that this servlet supports being propigated
988   //to the server.  Headers must be included in this white-list in order to be sent
989   //over.  The reason we provide a white-list instead of a black-list is primarily 
990   //for security.
991   private static final String[] _INCLUDED_HEADERS;
992 
993   static 
994   {
995     //Here are some notable headers that are missing from the list:
996     // "Content-Type"      - Set directly on the response
997     //  "Last-Modified"    - Sets directly on the response
998     //  "Cache-Control"    - This is added because the servlet sets its own
999     //  "Expires"          - This is addded because the servlet sets its own
1000     //  "Server"           - This should reflect THIS server
1001     //  "Set-Cookie"       - Cookies, right now, are not preserved in the client.  
1002     //                       We are a proxy, we need better cookie handling.
1003     //  "Status"           - Returned by the servlet container
1004     //  "Via"              - Intentionally mask any proxying behind the firwall
1005     //  "WWW-Authenticate" - This should be handled by this servlet
1006     //  "Allow"            - Returned by this servlet    
1007 
1008     String[] tempArray = new String[]
1009     {
1010       "age",
1011       "content-language",
1012       "content-location",
1013       "content-md5",
1014       "content-disposition",
1015       "content-range",
1016       "date",
1017       "link",
1018       "p3p",
1019       "refresh",
1020       "retry-after",
1021       "strict-transport-security",
1022       "trailer",
1023 //      "transfer-encoding",
1024 //      "vary",
1025       "warning"
1026     };
1027           
1028     //Ensure the items are in natural order so they can be binary searched.
1029     Arrays.sort(tempArray);
1030     
1031     _INCLUDED_HEADERS = tempArray;
1032   }
1033 }