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