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