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