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.shared.application;
20  
21  import java.net.MalformedURLException;
22  import java.util.Map;
23  import java.util.logging.Level;
24  import java.util.logging.Logger;
25  
26  import javax.faces.application.ProjectStage;
27  import javax.faces.application.ViewHandler;
28  import javax.faces.context.ExternalContext;
29  import javax.faces.context.FacesContext;
30  
31  import org.apache.myfaces.buildtools.maven2.plugin.builder.annotation.JSFWebConfigParam;
32  import org.apache.myfaces.shared.renderkit.html.util.SharedStringBuilder;
33  import org.apache.myfaces.shared.util.ConcurrentLRUCache;
34  import org.apache.myfaces.shared.util.ExternalContextUtils;
35  import org.apache.myfaces.shared.util.StringUtils;
36  import org.apache.myfaces.shared.util.WebConfigParamUtils;
37  
38  /**
39   * A ViewHandlerSupport implementation for use with standard Java Servlet engines,
40   * ie an engine that supports javax.servlet, and uses a standard web.xml file.
41   *
42   * @author Mathias Broekelmann (latest modification by $Author: lu4242 $)
43   * @version $Revision: 1296657 $ $Date: 2012-03-03 11:27:42 -0500 (Sat, 03 Mar 2012) $
44   */
45  public class DefaultViewHandlerSupport implements ViewHandlerSupport
46  {
47      /**
48       * Identifies the FacesServlet mapping in the current request map.
49       */
50      private static final String CACHED_SERVLET_MAPPING =
51          DefaultViewHandlerSupport.class.getName() + ".CACHED_SERVLET_MAPPING";
52  
53      //private static final Log log = LogFactory.getLog(DefaultViewHandlerSupport.class);
54      private static final Logger log = Logger.getLogger(DefaultViewHandlerSupport.class.getName());
55  
56      /**
57       * Controls the size of the cache used to "remember" if a view exists or not.
58       */
59      @JSFWebConfigParam(defaultValue = "500", since = "2.0.2", group="viewhandler", tags="performance", 
60              classType="java.lang.Integer",
61              desc="Controls the size of the cache used to 'remember' if a view exists or not.")
62      private static final String CHECKED_VIEWID_CACHE_SIZE_ATTRIBUTE = "org.apache.myfaces.CHECKED_VIEWID_CACHE_SIZE";
63      private static final int CHECKED_VIEWID_CACHE_DEFAULT_SIZE = 500;
64  
65      /**
66       * Enable or disable a cache used to "remember" if a view exists or not and reduce the impact of
67       * sucesive calls to ExternalContext.getResource().
68       */
69      @JSFWebConfigParam(defaultValue = "true", since = "2.0.2", expectedValues="true, false", group="viewhandler", 
70              tags="performance",
71              desc="Enable or disable a cache used to 'remember' if a view exists or not and reduce the impact " +
72                   "of sucesive calls to ExternalContext.getResource().")
73      private static final String CHECKED_VIEWID_CACHE_ENABLED_ATTRIBUTE = 
74          "org.apache.myfaces.CHECKED_VIEWID_CACHE_ENABLED";
75      private static final boolean CHECKED_VIEWID_CACHE_ENABLED_DEFAULT = true;
76      
77      private static final String VIEW_HANDLER_SUPPORT_SB = "oam.viewhandler.SUPPORT_SB";
78  
79      private volatile ConcurrentLRUCache<String, Boolean> _checkedViewIdMap = null;
80      private Boolean _checkedViewIdCacheEnabled = null;
81  
82      public String calculateViewId(FacesContext context, String viewId)
83      {
84          //If no viewId found, don't try to derive it, just continue.
85          if (viewId == null)
86          {
87              return null;
88          }
89          FacesServletMapping mapping = getFacesServletMapping(context);
90          if (mapping == null || mapping.isExtensionMapping())
91          {
92              viewId = handleSuffixMapping(context, viewId);
93          }
94          else if(mapping.isPrefixMapping())
95          {
96              viewId = handlePrefixMapping(viewId,mapping.getPrefix());
97              
98              // A viewId that is equals to the prefix mapping on servlet mode is
99              // considered invalid, because jsp vdl will use RequestDispatcher and cause
100             // a loop that ends in a exception. Note in portlet mode the view
101             // could be encoded as a query param, so the viewId could be valid.
102             if (viewId != null && viewId.equals(mapping.getPrefix()) &&
103                 !ExternalContextUtils.isPortlet(context.getExternalContext()))
104             {
105                 throw new InvalidViewIdException();
106             }
107         }
108         else if (viewId != null && mapping.getUrlPattern().startsWith(viewId))
109         {
110             throw new InvalidViewIdException(viewId);
111         }
112 
113         //if(viewId != null)
114         //{
115         //    return (checkResourceExists(context,viewId) ? viewId : null);
116         //}
117 
118         return viewId;    // return null if no physical resource exists
119     }
120     
121     public String calculateAndCheckViewId(FacesContext context, String viewId)
122     {
123         //If no viewId found, don't try to derive it, just continue.
124         if (viewId == null)
125         {
126             return null;
127         }
128         FacesServletMapping mapping = getFacesServletMapping(context);
129         if (mapping == null || mapping.isExtensionMapping())
130         {
131             viewId = handleSuffixMapping(context, viewId);
132         }
133         else if(mapping.isPrefixMapping())
134         {
135             viewId = handlePrefixMapping(viewId,mapping.getPrefix());
136 
137             if(viewId != null)
138             {
139                 // A viewId that is equals to the prefix mapping on servlet mode is
140                 // considered invalid, because jsp vdl will use RequestDispatcher and cause
141                 // a loop that ends in a exception. Note in portlet mode the view
142                 // could be encoded as a query param, so the viewId could be valid.
143                 if (viewId != null && viewId.equals(mapping.getPrefix()) &&
144                     !ExternalContextUtils.isPortlet(context.getExternalContext()))
145                 {
146                     throw new InvalidViewIdException();
147                 }
148 
149                 return (checkResourceExists(context,viewId) ? viewId : null);
150             }
151         }
152         else if (viewId != null && mapping.getUrlPattern().startsWith(viewId))
153         {
154             throw new InvalidViewIdException(viewId);
155         }
156         else
157         {
158             if(viewId != null)
159             {
160                 return (checkResourceExists(context,viewId) ? viewId : null);
161             }
162         }
163 
164         return viewId;    // return null if no physical resource exists
165     }
166 
167     public String calculateActionURL(FacesContext context, String viewId)
168     {
169         if (viewId == null || !viewId.startsWith("/"))
170         {
171             throw new IllegalArgumentException("ViewId must start with a '/': " + viewId);
172         }
173 
174         FacesServletMapping mapping = getFacesServletMapping(context);
175         ExternalContext externalContext = context.getExternalContext();
176         String contextPath = externalContext.getRequestContextPath();
177         //StringBuilder builder = new StringBuilder(contextPath);
178         StringBuilder builder = SharedStringBuilder.get(context, VIEW_HANDLER_SUPPORT_SB);
179         builder.append(contextPath);
180         if (mapping != null)
181         {
182             if (mapping.isExtensionMapping())
183             {
184                 //See JSF 2.0 section 7.5.2 
185                 String[] contextSuffixes = getContextSuffix(context); 
186                 boolean founded = false;
187                 for (String contextSuffix : contextSuffixes)
188                 {
189                     if (viewId.endsWith(contextSuffix))
190                     {
191                         builder.append(viewId.substring(0, viewId.indexOf(contextSuffix)));
192                         builder.append(mapping.getExtension());
193                         founded = true;
194                         break;
195                     }
196                 }
197                 if (!founded)
198                 {   
199                     //See JSF 2.0 section 7.5.2
200                     // - If the argument viewId has an extension, and this extension is mapping, 
201                     // the result is contextPath + viewId
202                     //
203                     // -= Leonardo Uribe =- It is evident that when the page is generated, the derived 
204                     // viewId will end with the 
205                     // right contextSuffix, and a navigation entry on faces-config.xml should use such id,
206                     // this is just a workaroud
207                     // for usability. There is a potential risk that change the mapping in a webapp make 
208                     // the same application fail,
209                     // so use viewIds ending with mapping extensions is not a good practice.
210                     if (viewId.endsWith(mapping.getExtension()))
211                     {
212                         builder.append(viewId);
213                     }
214                     else if(viewId.lastIndexOf(".") != -1 )
215                     {
216                         builder.append(viewId.substring(0,viewId.lastIndexOf(".")));
217                         builder.append(contextSuffixes[0]);
218                     }
219                     else
220                     {
221                         builder.append(viewId);
222                         builder.append(contextSuffixes[0]);
223                     }
224                 }
225             }
226             else
227             {
228                 builder.append(mapping.getPrefix());
229                 builder.append(viewId);
230             }
231         }
232         else
233         {
234             builder.append(viewId);
235         }
236         String calculatedActionURL = builder.toString();
237         if (log.isLoggable(Level.FINEST))
238         {
239             log.finest("Calculated actionURL: '" + calculatedActionURL + "' for viewId: '" + viewId + "'");
240         }
241         return calculatedActionURL;
242     }
243 
244     /**
245      * Read the web.xml file that is in the classpath and parse its internals to
246      * figure out how the FacesServlet is mapped for the current webapp.
247      */
248     protected FacesServletMapping getFacesServletMapping(FacesContext context)
249     {
250         Map<Object, Object> attributes = context.getAttributes();
251 
252         // Has the mapping already been determined during this request?
253         FacesServletMapping mapping = (FacesServletMapping) attributes.get(CACHED_SERVLET_MAPPING);
254         if (mapping == null)
255         {
256             ExternalContext externalContext = context.getExternalContext();
257             mapping = calculateFacesServletMapping(externalContext.getRequestServletPath(),
258                     externalContext.getRequestPathInfo());
259 
260             attributes.put(CACHED_SERVLET_MAPPING, mapping);
261         }
262         return mapping;
263     }
264 
265     /**
266      * Determines the mapping of the FacesServlet in the web.xml configuration
267      * file. However, there is no need to actually parse this configuration file
268      * as runtime information is sufficient.
269      *
270      * @param servletPath The servletPath of the current request
271      * @param pathInfo    The pathInfo of the current request
272      * @return the mapping of the FacesServlet in the web.xml configuration file
273      */
274     protected static FacesServletMapping calculateFacesServletMapping(
275         String servletPath, String pathInfo)
276     {
277         if (pathInfo != null)
278         {
279             // If there is a "extra path", it's definitely no extension mapping.
280             // Now we just have to determine the path which has been specified
281             // in the url-pattern, but that's easy as it's the same as the
282             // current servletPath. It doesn't even matter if "/*" has been used
283             // as in this case the servletPath is just an empty string according
284             // to the Servlet Specification (SRV 4.4).
285             return FacesServletMapping.createPrefixMapping(servletPath);
286         }
287         else
288         {
289             // In the case of extension mapping, no "extra path" is available.
290             // Still it's possible that prefix-based mapping has been used.
291             // Actually, if there was an exact match no "extra path"
292             // is available (e.g. if the url-pattern is "/faces/*"
293             // and the request-uri is "/context/faces").
294             int slashPos = servletPath.lastIndexOf('/');
295             int extensionPos = servletPath.lastIndexOf('.');
296             if (extensionPos > -1 && extensionPos > slashPos)
297             {
298                 String extension = servletPath.substring(extensionPos);
299                 return FacesServletMapping.createExtensionMapping(extension);
300             }
301             else
302             {
303                 // There is no extension in the given servletPath and therefore
304                 // we assume that it's an exact match using prefix-based mapping.
305                 return FacesServletMapping.createPrefixMapping(servletPath);
306             }
307         }
308     }
309 
310     protected String[] getContextSuffix(FacesContext context)
311     {
312         String defaultSuffix = context.getExternalContext().getInitParameter(ViewHandler.DEFAULT_SUFFIX_PARAM_NAME);
313         if (defaultSuffix == null)
314         {
315             defaultSuffix = ViewHandler.DEFAULT_SUFFIX;
316         }
317         return StringUtils.splitShortString(defaultSuffix, ' ');
318     }
319     
320     protected String getFaceletsContextSuffix(FacesContext context)
321     {
322         String defaultSuffix = context.getExternalContext().getInitParameter(ViewHandler.FACELETS_SUFFIX_PARAM_NAME);
323         if (defaultSuffix == null)
324         {
325             defaultSuffix = ViewHandler.DEFAULT_FACELETS_SUFFIX;
326         }
327         return defaultSuffix;
328     }
329     
330     
331     
332     protected String[] getFaceletsViewMappings(FacesContext context)
333     {
334         String faceletsViewMappings= context.getExternalContext().getInitParameter(
335                 ViewHandler.FACELETS_VIEW_MAPPINGS_PARAM_NAME);
336         if(faceletsViewMappings == null)    //consider alias facelets.VIEW_MAPPINGS
337         {
338             faceletsViewMappings= context.getExternalContext().getInitParameter("facelets.VIEW_MAPPINGS");
339         }
340         
341         return faceletsViewMappings == null ? null : StringUtils.splitShortString(faceletsViewMappings, ';');
342     }
343 
344     /**
345      * Return the normalized viewId according to the algorithm specified in 7.5.2 
346      * by stripping off any number of occurrences of the prefix mapping from the viewId.
347      * <p/>
348      * For example, both /faces/view.xhtml and /faces/faces/faces/view.xhtml would both return view.xhtml
349      * F 
350      */
351     protected String handlePrefixMapping(String viewId, String prefix)
352     {
353         // If prefix mapping (such as "/faces/*") is used for FacesServlet, 
354         // normalize the viewId according to the following
355         // algorithm, or its semantic equivalent, and return it.
356                
357         // Remove any number of occurrences of the prefix mapping from the viewId. 
358         // For example, if the incoming value was /faces/faces/faces/view.xhtml 
359         // the result would be simply view.xhtml.
360         
361         if ("".equals(prefix))
362         {
363             // if prefix is an empty string (Spring environment), we let it be "//"
364             // in order to prevent an infinite loop in uri.startsWith(-emptyString-).
365             // Furthermore a prefix of "//" is just another double slash prevention.
366             prefix = "//";
367         }
368         else
369         {
370             // need to make sure its really /faces/* and not /facesPage.xhtml
371             prefix = prefix + '/'; 
372         }
373         
374         String uri = viewId;
375         while (uri.startsWith(prefix) || uri.startsWith("//")) 
376         {
377             if (uri.startsWith(prefix))
378             {
379                 // cut off only /faces, leave the trailing '/' char for the next iteration
380                 uri = uri.substring(prefix.length() - 1);
381             }
382             else
383             {
384                 // uri starts with '//' --> cut off the leading slash, leaving
385                 // the second slash to compare for the next iteration
386                 uri = uri.substring(1);
387             }
388         }
389         
390         //now delete any remaining leading '/'
391         // TODO: CJH: I don't think this is correct, considering that getActionURL() expects everything to
392         // start with '/', and in the suffix case we only mess with the suffix and leave leading
393         // slashes alone.  Please review...
394         /*if(uri.startsWith("/"))
395         {
396             uri = uri.substring(1);
397         }*/
398         
399         return uri;
400     }
401     
402     /**
403      * Return the viewId with any non-standard suffix stripped off and replaced with
404      * the default suffix configured for the specified context.
405      * <p/>
406      * For example, an input parameter of "/foo.jsf" may return "/foo.jsp".
407      */
408     protected String handleSuffixMapping(FacesContext context, String requestViewId)
409     {
410         String[] faceletsViewMappings = getFaceletsViewMappings(context);
411         String[] jspDefaultSuffixes = getContextSuffix(context);
412         
413         int slashPos = requestViewId.lastIndexOf('/');
414         int extensionPos = requestViewId.lastIndexOf('.');
415         
416         StringBuilder builder = SharedStringBuilder.get(context, VIEW_HANDLER_SUPPORT_SB);
417         
418         //Try to locate any resource that match with the expected id
419         for (String defaultSuffix : jspDefaultSuffixes)
420         {
421             //StringBuilder builder = new StringBuilder(requestViewId);
422             builder.setLength(0);
423             builder.append(requestViewId);
424            
425             if (extensionPos > -1 && extensionPos > slashPos)
426             {
427                 builder.replace(extensionPos, requestViewId.length(), defaultSuffix);
428             }
429             else
430             {
431                 builder.append(defaultSuffix);
432             }
433             String candidateViewId = builder.toString();
434             
435             if( faceletsViewMappings != null && faceletsViewMappings.length > 0 )
436             {
437                 for (String mapping : faceletsViewMappings)
438                 {
439                     if(mapping.startsWith("/"))
440                     {
441                         continue;   //skip this entry, its a prefix mapping
442                     }
443                     if(mapping.equals(candidateViewId))
444                     {
445                         return candidateViewId;
446                     }
447                     if(mapping.startsWith(".")) //this is a wildcard entry
448                     {
449                         builder.setLength(0); //reset/reuse the builder object 
450                         builder.append(candidateViewId); 
451                         builder.replace(candidateViewId.lastIndexOf('.'), candidateViewId.length(), mapping);
452                         String tempViewId = builder.toString();
453                         if(checkResourceExists(context,tempViewId))
454                         {
455                             return tempViewId;
456                         }
457                     }
458                 }
459             }
460 
461             // forced facelets mappings did not match or there were no entries in faceletsViewMappings array
462             if(checkResourceExists(context,candidateViewId))
463             {
464                 return candidateViewId;
465             }
466         
467         }
468         
469         //jsp suffixes didn't match, try facelets suffix
470         String faceletsDefaultSuffix = this.getFaceletsContextSuffix(context);
471         if (faceletsDefaultSuffix != null)
472         {
473             for (String defaultSuffix : jspDefaultSuffixes)
474             {
475                 if (faceletsDefaultSuffix.equals(defaultSuffix))
476                 {
477                     faceletsDefaultSuffix = null;
478                     break;
479                 }
480             }
481         }
482         if (faceletsDefaultSuffix != null)
483         {
484             //StringBuilder builder = new StringBuilder(requestViewId);
485             builder.setLength(0);
486             builder.append(requestViewId);
487             
488             if (extensionPos > -1 && extensionPos > slashPos)
489             {
490                 builder.replace(extensionPos, requestViewId.length(), faceletsDefaultSuffix);
491             }
492             else
493             {
494                 builder.append(faceletsDefaultSuffix);
495             }
496             
497             String candidateViewId = builder.toString();
498             if(checkResourceExists(context,candidateViewId))
499             {
500                 return candidateViewId;
501             }
502         }
503 
504         // Otherwise, if a physical resource exists with the name requestViewId let that value be viewId.
505         if(checkResourceExists(context,requestViewId))
506         {
507             return requestViewId;
508         }
509         
510         //Otherwise return null.
511         return null;
512     }
513     
514     protected boolean checkResourceExists(FacesContext context, String viewId)
515     {
516         try
517         {
518             if (isCheckedViewIdCachingEnabled(context))
519             {
520                 Boolean resourceExists = getCheckedViewIDMap(context).get(
521                         viewId);
522                 if (resourceExists == null)
523                 {
524                     resourceExists = context.getExternalContext().getResource(
525                             viewId) != null;
526                     getCheckedViewIDMap(context).put(viewId, resourceExists);
527                 }
528                 return resourceExists;
529             }
530 
531             if (context.getExternalContext().getResource(viewId) != null)
532             {
533                 return true;
534             }
535         }
536         catch(MalformedURLException e)
537         {
538             //ignore and move on
539         }     
540         return false;
541     }
542 
543     private ConcurrentLRUCache<String, Boolean> getCheckedViewIDMap(FacesContext context)
544     {
545         if (_checkedViewIdMap == null)
546         {
547             int maxSize = getViewIDCacheMaxSize(context);
548             _checkedViewIdMap = new ConcurrentLRUCache<String, Boolean>((maxSize * 4 + 3) / 3, maxSize);
549         }
550         return _checkedViewIdMap;
551     }
552 
553     private boolean isCheckedViewIdCachingEnabled(FacesContext context)
554     {
555         if (_checkedViewIdCacheEnabled == null)
556         {
557             // first, check if the ProjectStage is development and skip caching in this case
558             if (context.isProjectStage(ProjectStage.Development))
559             {
560                 _checkedViewIdCacheEnabled = Boolean.FALSE;
561             }
562             else
563             {
564                 // in all ohter cases, make sure that the cache is not explicitly disabled via context param
565                 _checkedViewIdCacheEnabled = WebConfigParamUtils.getBooleanInitParameter(context.getExternalContext(),
566                         CHECKED_VIEWID_CACHE_ENABLED_ATTRIBUTE,
567                         CHECKED_VIEWID_CACHE_ENABLED_DEFAULT);
568             }
569 
570             if (log.isLoggable(Level.FINE))
571             {
572                 log.log(Level.FINE, "MyFaces ViewID Caching Enabled="
573                         + _checkedViewIdCacheEnabled);
574             }
575         }
576         return _checkedViewIdCacheEnabled;
577     }
578 
579     private int getViewIDCacheMaxSize(FacesContext context)
580     {
581         ExternalContext externalContext = context.getExternalContext();
582 
583         return WebConfigParamUtils.getIntegerInitParameter(externalContext,
584                 CHECKED_VIEWID_CACHE_SIZE_ATTRIBUTE, CHECKED_VIEWID_CACHE_DEFAULT_SIZE);
585     }
586 }