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.application;
20  
21  import org.apache.myfaces.buildtools.maven2.plugin.builder.annotation.JSFWebConfigParam;
22  import org.apache.myfaces.shared.resource.ResourceHandlerCache;
23  import org.apache.myfaces.shared.resource.ResourceHandlerCache.ResourceValue;
24  import org.apache.myfaces.shared.resource.ResourceHandlerSupport;
25  import org.apache.myfaces.shared.resource.ResourceImpl;
26  import org.apache.myfaces.shared.resource.ResourceLoader;
27  import org.apache.myfaces.shared.resource.ResourceMeta;
28  import org.apache.myfaces.shared.resource.ResourceValidationUtils;
29  import org.apache.myfaces.shared.util.ClassUtils;
30  import org.apache.myfaces.shared.util.ExternalContextUtils;
31  import org.apache.myfaces.shared.util.StringUtils;
32  import org.apache.myfaces.shared.util.WebConfigParamUtils;
33  
34  import javax.faces.application.Resource;
35  import javax.faces.application.ResourceHandler;
36  import javax.faces.application.ResourceWrapper;
37  import javax.faces.context.ExternalContext;
38  import javax.faces.context.FacesContext;
39  import javax.servlet.http.HttpServletResponse;
40  import java.io.IOException;
41  import java.io.InputStream;
42  import java.io.OutputStream;
43  import java.net.URL;
44  import java.util.Locale;
45  import java.util.Map;
46  import java.util.MissingResourceException;
47  import java.util.ResourceBundle;
48  import java.util.logging.Level;
49  import java.util.logging.Logger;
50  
51  /**
52   * DOCUMENT ME!
53   *
54   * @author Simon Lessard (latest modification by $Author: lu4242 $)
55   * 
56   * @version $Revision: 1533290 $ $Date: 2013-10-17 18:35:35 -0500 (Thu, 17 Oct 2013) $
57   */
58  public class ResourceHandlerImpl extends ResourceHandler
59  {
60  
61      private static final String IS_RESOURCE_REQUEST = "org.apache.myfaces.IS_RESOURCE_REQUEST";
62  
63      private ResourceHandlerSupport _resourceHandlerSupport;
64  
65      private ResourceHandlerCache _resourceHandlerCache;
66  
67      //private static final Log log = LogFactory.getLog(ResourceHandlerImpl.class);
68      private static final Logger log = Logger.getLogger(ResourceHandlerImpl.class.getName());
69  
70      /**
71       * Allow slash in the library name of a Resource. 
72       */
73      @JSFWebConfigParam(since="2.1.6, 2.0.12", defaultValue="false", 
74              expectedValues="true, false", group="resources")
75      public static final String INIT_PARAM_STRICT_JSF_2_ALLOW_SLASH_LIBRARY_NAME = 
76              "org.apache.myfaces.STRICT_JSF_2_ALLOW_SLASH_LIBRARY_NAME";
77      public static final boolean INIT_PARAM_STRICT_JSF_2_ALLOW_SLASH_LIBRARY_NAME_DEFAULT = false;
78      
79      /**
80       * Define the default buffer size that is used between Resource.getInputStream() and 
81       * httpServletResponse.getOutputStream() when rendering resources using the default
82       * ResourceHandler.
83       */
84      @JSFWebConfigParam(since="2.1.10, 2.0.16", defaultValue="2048", group="resources")
85      public static final String INIT_PARAM_RESOURCE_BUFFER_SIZE = "org.apache.myfaces.RESOURCE_BUFFER_SIZE";
86      public static final int INIT_PARAM_RESOURCE_BUFFER_SIZE_DEFAULT = 2048;
87      
88      private Boolean _allowSlashLibraryName;
89      private int _resourceBufferSize = -1;
90      
91      private String[] _excludedResourceExtensions;
92  
93      @Override
94      public Resource createResource(String resourceName)
95      {
96          return createResource(resourceName, null);
97      }
98  
99      @Override
100     public Resource createResource(String resourceName, String libraryName)
101     {
102         return createResource(resourceName, libraryName, null);
103     }
104 
105     @Override
106     public Resource createResource(String resourceName, String libraryName,
107             String contentType)
108     {
109         Resource resource = null;
110         
111         if (!ResourceValidationUtils.isValidResourceName(resourceName))
112         {
113             return null;
114         }
115         if (libraryName != null && !ResourceValidationUtils.isValidLibraryName(
116                 libraryName, isAllowSlashesLibraryName()))
117         {
118             return null;
119         }
120         
121         if (contentType == null)
122         {
123             //Resolve contentType using ExternalContext.getMimeType
124             contentType = FacesContext.getCurrentInstance().getExternalContext().getMimeType(resourceName);
125         }
126 
127         final String localePrefix = getLocalePrefixForLocateResource();
128 
129         // check cache
130         if(getResourceLoaderCache().containsResource(resourceName, libraryName, contentType, localePrefix))
131         {
132             ResourceValue resourceValue = getResourceLoaderCache().getResource(
133                     resourceName, libraryName, contentType, localePrefix);
134             
135             resource = new ResourceImpl(resourceValue.getResourceMeta(), resourceValue.getResourceLoader(),
136                     getResourceHandlerSupport(), contentType);
137         }
138         else
139         {
140             for (ResourceLoader loader : getResourceHandlerSupport().getResourceLoaders())
141             {
142                 ResourceMeta resourceMeta = deriveResourceMeta(loader, resourceName, libraryName, localePrefix);
143     
144                 if (resourceMeta != null)
145                 {
146                     resource = new ResourceImpl(resourceMeta, loader, getResourceHandlerSupport(), contentType);
147 
148                     // cache it
149                     getResourceLoaderCache().putResource(resourceName, libraryName, contentType,
150                             localePrefix, resourceMeta, loader);
151                     break;
152                 }
153             }
154         }
155         
156         return resource;
157     }
158 
159     /**
160      * This method try to create a ResourceMeta for a specific resource
161      * loader. If no library, or resource is found, just return null,
162      * so the algorithm in createResource can continue checking with the 
163      * next registered ResourceLoader. 
164      */
165     protected ResourceMeta deriveResourceMeta(ResourceLoader resourceLoader,
166             String resourceName, String libraryName, String localePrefix)
167     {
168         String resourceVersion = null;
169         String libraryVersion = null;
170         ResourceMeta resourceId = null;
171         
172         //1. Try to locate resource in a localized path
173         if (localePrefix != null)
174         {
175             if (null != libraryName)
176             {
177                 String pathToLib = localePrefix + '/' + libraryName;
178                 libraryVersion = resourceLoader.getLibraryVersion(pathToLib);
179 
180                 if (null != libraryVersion)
181                 {
182                     String pathToResource = localePrefix + '/'
183                             + libraryName + '/' + libraryVersion + '/'
184                             + resourceName;
185                     resourceVersion = resourceLoader
186                             .getResourceVersion(pathToResource);
187                 }
188                 else
189                 {
190                     String pathToResource = localePrefix + '/'
191                             + libraryName + '/' + resourceName;
192                     resourceVersion = resourceLoader
193                             .getResourceVersion(pathToResource);
194                 }
195 
196                 if (!(resourceVersion != null && ResourceLoader.VERSION_INVALID.equals(resourceVersion)))
197                 {
198                     resourceId = resourceLoader.createResourceMeta(localePrefix, libraryName,
199                             libraryVersion, resourceName, resourceVersion);
200                 }
201             }
202             else
203             {
204                 resourceVersion = resourceLoader
205                         .getResourceVersion(localePrefix + '/'+ resourceName);
206                 if (!(resourceVersion != null && ResourceLoader.VERSION_INVALID.equals(resourceVersion)))
207                 {               
208                     resourceId = resourceLoader.createResourceMeta(localePrefix, null, null,
209                             resourceName, resourceVersion);
210                 }
211             }
212 
213             if (resourceId != null)
214             {
215                 URL url = resourceLoader.getResourceURL(resourceId);
216                 if (url == null)
217                 {
218                     resourceId = null;
219                 }
220             }            
221         }
222         
223         //2. Try to localize resource in a non localized path
224         if (resourceId == null)
225         {
226             if (null != libraryName)
227             {
228                 libraryVersion = resourceLoader.getLibraryVersion(libraryName);
229 
230                 if (null != libraryVersion)
231                 {
232                     String pathToResource = (libraryName + '/' + libraryVersion
233                             + '/' + resourceName);
234                     resourceVersion = resourceLoader
235                             .getResourceVersion(pathToResource);
236                 }
237                 else
238                 {
239                     String pathToResource = (libraryName + '/'
240                             + resourceName);
241                     resourceVersion = resourceLoader
242                             .getResourceVersion(pathToResource);
243                 }
244 
245                 if (!(resourceVersion != null && ResourceLoader.VERSION_INVALID.equals(resourceVersion)))
246                 {               
247                     resourceId = resourceLoader.createResourceMeta(null, libraryName,
248                             libraryVersion, resourceName, resourceVersion);
249                 }
250             }
251             else
252             {
253                 resourceVersion = resourceLoader
254                         .getResourceVersion(resourceName);
255                 
256                 if (!(resourceVersion != null && ResourceLoader.VERSION_INVALID.equals(resourceVersion)))
257                 {               
258                     resourceId = resourceLoader.createResourceMeta(null, null, null,
259                             resourceName, resourceVersion);
260                 }
261             }
262 
263             if (resourceId != null)
264             {
265                 URL url = resourceLoader.getResourceURL(resourceId);
266                 if (url == null)
267                 {
268                     resourceId = null;
269                 }
270             }            
271         }
272         
273         return resourceId;
274     }
275 
276     @Override
277     public String getRendererTypeForResourceName(String resourceName)
278     {
279         if (resourceName.endsWith(".js"))
280         {
281             return "javax.faces.resource.Script";
282         }
283         else if (resourceName.endsWith(".css"))
284         {
285             return "javax.faces.resource.Stylesheet";
286         }
287         return null;
288     }
289 
290     /**
291      *  Handle the resource request, writing in the output. 
292      *  
293      *  This method implements an algorithm semantically identical to 
294      *  the one described on the javadoc of ResourceHandler.handleResourceRequest 
295      */
296     @Override
297     public void handleResourceRequest(FacesContext facesContext) throws IOException
298     {
299         //try
300         //{
301             String resourceBasePath = getResourceHandlerSupport()
302                     .calculateResourceBasePath(facesContext);
303     
304             if (resourceBasePath == null)
305             {
306                 // No base name could be calculated, so no further
307                 //advance could be done here. HttpServletResponse.SC_NOT_FOUND
308                 //cannot be returned since we cannot extract the 
309                 //resource base name
310                 return;
311             }
312     
313             // We neet to get an instance of HttpServletResponse, but sometimes
314             // the response object is wrapped by several instances of 
315             // ServletResponseWrapper (like ResponseSwitch).
316             // Since we are handling a resource, we can expect to get an 
317             // HttpServletResponse.
318             ExternalContext extContext = facesContext.getExternalContext();
319             Object response = extContext.getResponse();
320             HttpServletResponse httpServletResponse = ExternalContextUtils.getHttpServletResponse(response);
321             if (httpServletResponse == null)
322             {
323                 throw new IllegalStateException("Could not obtain an instance of HttpServletResponse.");
324             }
325     
326             if (isResourceIdentifierExcluded(facesContext, resourceBasePath))
327             {
328                 httpServletResponse.setStatus(HttpServletResponse.SC_NOT_FOUND);
329                 return;
330             }
331     
332             String resourceName = null;
333             if (resourceBasePath.startsWith(ResourceHandler.RESOURCE_IDENTIFIER))
334             {
335                 resourceName = resourceBasePath
336                         .substring(ResourceHandler.RESOURCE_IDENTIFIER.length() + 1);
337                 
338                 if (resourceBasePath != null && !ResourceValidationUtils.isValidResourceName(resourceName))
339                 {
340                     httpServletResponse.setStatus(HttpServletResponse.SC_NOT_FOUND);
341                     return;
342                 }
343             }
344             else
345             {
346                 //Does not have the conditions for be a resource call
347                 httpServletResponse.setStatus(HttpServletResponse.SC_NOT_FOUND);
348                 return;
349             }
350     
351             String libraryName = facesContext.getExternalContext()
352                     .getRequestParameterMap().get("ln");
353     
354             if (libraryName != null && !ResourceValidationUtils.isValidLibraryName(
355                     libraryName, isAllowSlashesLibraryName()))
356             {
357                 httpServletResponse.setStatus(HttpServletResponse.SC_NOT_FOUND);
358                 return;
359             }
360             
361             Resource resource = null;
362             if (libraryName != null)
363             {
364                 //log.info("libraryName=" + libraryName);
365                 resource = facesContext.getApplication().getResourceHandler().createResource(resourceName, libraryName);
366             }
367             else
368             {
369                 resource = facesContext.getApplication().getResourceHandler().createResource(resourceName);
370             }
371     
372             if (resource == null)
373             {
374                 httpServletResponse.setStatus(HttpServletResponse.SC_NOT_FOUND);
375                 return;
376             }
377     
378             if (!resource.userAgentNeedsUpdate(facesContext))
379             {
380                 httpServletResponse.setStatus(HttpServletResponse.SC_NOT_MODIFIED);
381                 return;
382             }
383     
384             httpServletResponse.setContentType(_getContentType(resource, facesContext.getExternalContext()));
385     
386             Map<String, String> headers = resource.getResponseHeaders();
387     
388             for (Map.Entry<String, String> entry : headers.entrySet())
389             {
390                 httpServletResponse.setHeader(entry.getKey(), entry.getValue());
391             }
392     
393             // Sets the preferred buffer size for the body of the response
394             extContext.setResponseBufferSize(this.getResourceBufferSize());
395             
396             //serve up the bytes (taken from trinidad ResourceServlet)
397             try
398             {
399                 InputStream in = resource.getInputStream();
400                 OutputStream out = httpServletResponse.getOutputStream();
401                 //byte[] buffer = new byte[_BUFFER_SIZE];
402                 byte[] buffer = new byte[this.getResourceBufferSize()];
403     
404                 try
405                 {
406                     int count = pipeBytes(in, out, buffer);
407                     //set the content lenght
408                     if (!httpServletResponse.isCommitted())
409                     {
410                         httpServletResponse.setContentLength(count);
411                     }
412                 }
413                 finally
414                 {
415                     try
416                     {
417                         in.close();
418                     }
419                     finally
420                     {
421                         out.close();
422                     }
423                 }
424             }
425             catch (IOException e)
426             {
427                 //TODO: Log using a localized message (which one?)
428                 if (log.isLoggable(Level.SEVERE))
429                 {
430                     log.log(Level.SEVERE,"Error trying to load resource " + resourceName
431                             + " with library " + libraryName + " :"
432                             + e.getMessage(), e);
433                 }
434                 httpServletResponse.setStatus(HttpServletResponse.SC_NOT_FOUND);
435             }
436         //}
437         //catch (Throwable ex)
438         //{
439             // handle the Throwable accordingly. Maybe generate an error page.
440             // FIXME we are creating a html error page for a non html request here
441             // shouln't we do something better? -=Jakob Korherr=-
442             //ErrorPageWriter.handleThrowable(facesContext, ex);
443         //}
444     }
445 
446     /**
447      * Reads the specified input stream into the provided byte array storage and
448      * writes it to the output stream.
449      */
450     private static int pipeBytes(InputStream in, OutputStream out, byte[] buffer)
451             throws IOException
452     {
453         int count = 0;
454         int length;
455 
456         while ((length = (in.read(buffer))) >= 0)
457         {
458             out.write(buffer, 0, length);
459             count += length;
460         }
461         return count;
462     }
463 
464     @Override
465     public boolean isResourceRequest(FacesContext facesContext)
466     {
467         // Since this method could be called many times we save it
468         // on request map so the first time is calculated it remains
469         // alive until the end of the request
470         Boolean value = (Boolean) facesContext.getAttributes().get(IS_RESOURCE_REQUEST);
471 
472         if (value == null)
473         {
474             String resourceBasePath = getResourceHandlerSupport()
475                     .calculateResourceBasePath(facesContext);
476 
477             value = resourceBasePath != null
478                     && resourceBasePath.startsWith(ResourceHandler.RESOURCE_IDENTIFIER);
479             facesContext.getAttributes().put(IS_RESOURCE_REQUEST, value);
480         }
481         return value;
482     }
483 
484     protected String getLocalePrefixForLocateResource()
485     {
486         String localePrefix = null;
487         FacesContext context = FacesContext.getCurrentInstance();
488         boolean isResourceRequest = context.getApplication().getResourceHandler().isResourceRequest(context);
489 
490         if (isResourceRequest)
491         {
492             localePrefix = context.getExternalContext().getRequestParameterMap().get("loc");
493             
494             if (localePrefix != null)
495             {
496                 if (!ResourceValidationUtils.isValidLocalePrefix(localePrefix))
497                 {
498                     return null;
499                 }
500                 return localePrefix;
501             }
502         }
503         
504         String bundleName = context.getApplication().getMessageBundle();
505 
506         if (null != bundleName)
507         {
508             Locale locale = null;
509             
510             if (isResourceRequest || context.getViewRoot() == null)
511             {
512                 locale = context.getApplication().getViewHandler()
513                                 .calculateLocale(context);
514             }
515             else
516             {
517                 locale = context.getViewRoot().getLocale();
518             }
519 
520             try
521             {
522                 ResourceBundle bundle = ResourceBundle
523                         .getBundle(bundleName, locale, ClassUtils.getContextClassLoader());
524 
525                 if (bundle != null)
526                 {
527                     localePrefix = bundle.getString(ResourceHandler.LOCALE_PREFIX);
528                 }
529             }
530             catch (MissingResourceException e)
531             {
532                 // Ignore it and return null
533             }
534         }
535         return localePrefix;
536     }
537 
538     protected boolean isResourceIdentifierExcluded(FacesContext context, String resourceIdentifier)
539     {
540         if (_excludedResourceExtensions == null)
541         {
542             String value = WebConfigParamUtils.getStringInitParameter(context.getExternalContext(),
543                             RESOURCE_EXCLUDES_PARAM_NAME,
544                             RESOURCE_EXCLUDES_DEFAULT_VALUE);
545             
546             _excludedResourceExtensions = StringUtils.splitShortString(value, ' ');
547         }
548         
549         for (int i = 0; i < _excludedResourceExtensions.length; i++)
550         {
551             if (resourceIdentifier.endsWith(_excludedResourceExtensions[i]))
552             {
553                 return true;
554             }
555         }
556         return false;
557     }
558 
559     /**
560      * Check if a library exists or not. This is done delegating
561      * to each ResourceLoader used, because each one has a different
562      * prefix and way to load resources.
563      * 
564      */
565     @Override
566     public boolean libraryExists(String libraryName)
567     {
568         String localePrefix = getLocalePrefixForLocateResource();
569 
570         String pathToLib = null;
571         
572         if (libraryName != null && !ResourceValidationUtils.isValidLibraryName(
573                 libraryName, isAllowSlashesLibraryName()))
574         {
575             return false;
576         }
577         
578         if (localePrefix != null)
579         {
580             //Check with locale
581             pathToLib = localePrefix + '/' + libraryName;
582             
583             for (ResourceLoader loader : getResourceHandlerSupport()
584                     .getResourceLoaders())
585             {
586                 if (loader.libraryExists(pathToLib))
587                 {
588                     return true;
589                 }
590             }            
591         }
592 
593         //Check without locale
594         for (ResourceLoader loader : getResourceHandlerSupport()
595                 .getResourceLoaders())
596         {
597             if (loader.libraryExists(libraryName))
598             {
599                 return true;
600             }
601         }
602 
603         return false;
604     }
605 
606     /**
607      * @param resourceHandlerSupport
608      *            the resourceHandlerSupport to set
609      */
610     public void setResourceHandlerSupport(
611             ResourceHandlerSupport resourceHandlerSupport)
612     {
613         _resourceHandlerSupport = resourceHandlerSupport;
614     }
615 
616     /**
617      * @return the resourceHandlerSupport
618      */
619     protected ResourceHandlerSupport getResourceHandlerSupport()
620     {
621         if (_resourceHandlerSupport == null)
622         {
623             _resourceHandlerSupport = new DefaultResourceHandlerSupport();
624         }
625         return _resourceHandlerSupport;
626     }
627 
628     private ResourceHandlerCache getResourceLoaderCache()
629     {
630         if (_resourceHandlerCache == null)
631         {
632             _resourceHandlerCache = new ResourceHandlerCache();
633         }
634         return _resourceHandlerCache;
635     }
636 
637     private String _getContentType(Resource resource, ExternalContext externalContext)
638     {
639         String contentType = resource.getContentType();
640 
641         // the resource does not provide a content-type --> determine it via mime-type
642         if (contentType == null || contentType.length() == 0)
643         {
644             String resourceName = getWrappedResourceName(resource);
645 
646             if (resourceName != null)
647             {
648                 contentType = externalContext.getMimeType(resourceName);
649             }
650         }
651 
652         return contentType;
653     }
654 
655     /**
656      * Recursively unwarp the resource until we find the real resourceName
657      * This is needed because the JSF2 specced ResourceWrapper doesn't override
658      * the getResourceName() method :(
659      * @param resource
660      * @return the first non-null resourceName or <code>null</code> if none set
661      */
662     private String getWrappedResourceName(Resource resource)
663     {
664         String resourceName = resource.getResourceName();
665         if (resourceName != null)
666         {
667             return resourceName;
668         }
669 
670         if (resource instanceof ResourceWrapper)
671         {
672             return getWrappedResourceName(((ResourceWrapper) resource).getWrapped());
673         }
674 
675         return null;
676     }
677     
678     protected boolean isAllowSlashesLibraryName()
679     {
680         if (_allowSlashLibraryName == null)
681         {
682             _allowSlashLibraryName = WebConfigParamUtils.getBooleanInitParameter(
683                     FacesContext.getCurrentInstance().getExternalContext(), 
684                     INIT_PARAM_STRICT_JSF_2_ALLOW_SLASH_LIBRARY_NAME,
685                     INIT_PARAM_STRICT_JSF_2_ALLOW_SLASH_LIBRARY_NAME_DEFAULT);
686         }
687         return _allowSlashLibraryName;
688     }
689 
690     protected int getResourceBufferSize()
691     {
692         if (_resourceBufferSize == -1)
693         {
694             _resourceBufferSize = WebConfigParamUtils.getIntegerInitParameter(
695                 FacesContext.getCurrentInstance().getExternalContext(),
696                 INIT_PARAM_RESOURCE_BUFFER_SIZE,
697                 INIT_PARAM_RESOURCE_BUFFER_SIZE_DEFAULT);
698         }
699         return _resourceBufferSize;
700     }
701 
702 }