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: 1486734 $ $Date: 2013-05-28 04:22:14 +0000 (Tue, 28 May 2013) $
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         // If the context path is root, it is not necessary to append it, otherwise
202         // and extra '/' will be set.
203         if (contextPath != null && !(contextPath.length() == 1 && contextPath.charAt(0) == '/') )
204         {
205             builder.append(contextPath);
206         }
207         if (mapping != null)
208         {
209             if (mapping.isExtensionMapping())
210             {
211                 //See JSF 2.0 section 7.5.2 
212                 String[] contextSuffixes = _initialized ? _contextSuffixes : getContextSuffix(context); 
213                 boolean founded = false;
214                 for (String contextSuffix : contextSuffixes)
215                 {
216                     if (viewId.endsWith(contextSuffix))
217                     {
218                         builder.append(viewId.substring(0, viewId.indexOf(contextSuffix)));
219                         builder.append(mapping.getExtension());
220                         founded = true;
221                         break;
222                     }
223                 }
224                 if (!founded)
225                 {   
226                     //See JSF 2.0 section 7.5.2
227                     // - If the argument viewId has an extension, and this extension is mapping, 
228                     // the result is contextPath + viewId
229                     //
230                     // -= Leonardo Uribe =- It is evident that when the page is generated, the derived 
231                     // viewId will end with the 
232                     // right contextSuffix, and a navigation entry on faces-config.xml should use such id,
233                     // this is just a workaroud
234                     // for usability. There is a potential risk that change the mapping in a webapp make 
235                     // the same application fail,
236                     // so use viewIds ending with mapping extensions is not a good practice.
237                     if (viewId.endsWith(mapping.getExtension()))
238                     {
239                         builder.append(viewId);
240                     }
241                     else if(viewId.lastIndexOf(".") != -1 )
242                     {
243                         builder.append(viewId.substring(0,viewId.lastIndexOf(".")));
244                         builder.append(contextSuffixes[0]);
245                     }
246                     else
247                     {
248                         builder.append(viewId);
249                         builder.append(contextSuffixes[0]);
250                     }
251                 }
252             }
253             else
254             {
255                 builder.append(mapping.getPrefix());
256                 builder.append(viewId);
257             }
258         }
259         else
260         {
261             builder.append(viewId);
262         }
263         String calculatedActionURL = builder.toString();
264         if (log.isLoggable(Level.FINEST))
265         {
266             log.finest("Calculated actionURL: '" + calculatedActionURL + "' for viewId: '" + viewId + "'");
267         }
268         return calculatedActionURL;
269     }
270 
271     /**
272      * Read the web.xml file that is in the classpath and parse its internals to
273      * figure out how the FacesServlet is mapped for the current webapp.
274      */
275     protected FacesServletMapping getFacesServletMapping(FacesContext context)
276     {
277         Map<Object, Object> attributes = context.getAttributes();
278 
279         // Has the mapping already been determined during this request?
280         FacesServletMapping mapping = (FacesServletMapping) attributes.get(CACHED_SERVLET_MAPPING);
281         if (mapping == null)
282         {
283             ExternalContext externalContext = context.getExternalContext();
284             mapping = calculateFacesServletMapping(externalContext.getRequestServletPath(),
285                     externalContext.getRequestPathInfo());
286 
287             attributes.put(CACHED_SERVLET_MAPPING, mapping);
288         }
289         return mapping;
290     }
291 
292     /**
293      * Determines the mapping of the FacesServlet in the web.xml configuration
294      * file. However, there is no need to actually parse this configuration file
295      * as runtime information is sufficient.
296      *
297      * @param servletPath The servletPath of the current request
298      * @param pathInfo    The pathInfo of the current request
299      * @return the mapping of the FacesServlet in the web.xml configuration file
300      */
301     protected static FacesServletMapping calculateFacesServletMapping(
302         String servletPath, String pathInfo)
303     {
304         if (pathInfo != null)
305         {
306             // If there is a "extra path", it's definitely no extension mapping.
307             // Now we just have to determine the path which has been specified
308             // in the url-pattern, but that's easy as it's the same as the
309             // current servletPath. It doesn't even matter if "/*" has been used
310             // as in this case the servletPath is just an empty string according
311             // to the Servlet Specification (SRV 4.4).
312             return FacesServletMapping.createPrefixMapping(servletPath);
313         }
314         else
315         {
316             // In the case of extension mapping, no "extra path" is available.
317             // Still it's possible that prefix-based mapping has been used.
318             // Actually, if there was an exact match no "extra path"
319             // is available (e.g. if the url-pattern is "/faces/*"
320             // and the request-uri is "/context/faces").
321             int slashPos = servletPath.lastIndexOf('/');
322             int extensionPos = servletPath.lastIndexOf('.');
323             if (extensionPos > -1 && extensionPos > slashPos)
324             {
325                 String extension = servletPath.substring(extensionPos);
326                 return FacesServletMapping.createExtensionMapping(extension);
327             }
328             else
329             {
330                 // There is no extension in the given servletPath and therefore
331                 // we assume that it's an exact match using prefix-based mapping.
332                 return FacesServletMapping.createPrefixMapping(servletPath);
333             }
334         }
335     }
336 
337     protected String[] getContextSuffix(FacesContext context)
338     {
339         String defaultSuffix = context.getExternalContext().getInitParameter(ViewHandler.DEFAULT_SUFFIX_PARAM_NAME);
340         if (defaultSuffix == null)
341         {
342             defaultSuffix = ViewHandler.DEFAULT_SUFFIX;
343         }
344         return StringUtils.splitShortString(defaultSuffix, ' ');
345     }
346     
347     protected String getFaceletsContextSuffix(FacesContext context)
348     {
349         String defaultSuffix = context.getExternalContext().getInitParameter(ViewHandler.FACELETS_SUFFIX_PARAM_NAME);
350         if (defaultSuffix == null)
351         {
352             defaultSuffix = ViewHandler.DEFAULT_FACELETS_SUFFIX;
353         }
354         return defaultSuffix;
355     }
356     
357     
358     
359     protected String[] getFaceletsViewMappings(FacesContext context)
360     {
361         String faceletsViewMappings= context.getExternalContext().getInitParameter(
362                 ViewHandler.FACELETS_VIEW_MAPPINGS_PARAM_NAME);
363         if(faceletsViewMappings == null)    //consider alias facelets.VIEW_MAPPINGS
364         {
365             faceletsViewMappings= context.getExternalContext().getInitParameter("facelets.VIEW_MAPPINGS");
366         }
367         
368         return faceletsViewMappings == null ? null : StringUtils.splitShortString(faceletsViewMappings, ';');
369     }
370 
371     /**
372      * Return the normalized viewId according to the algorithm specified in 7.5.2 
373      * by stripping off any number of occurrences of the prefix mapping from the viewId.
374      * <p/>
375      * For example, both /faces/view.xhtml and /faces/faces/faces/view.xhtml would both return view.xhtml
376      * F 
377      */
378     protected String handlePrefixMapping(String viewId, String prefix)
379     {
380         // If prefix mapping (such as "/faces/*") is used for FacesServlet, 
381         // normalize the viewId according to the following
382         // algorithm, or its semantic equivalent, and return it.
383                
384         // Remove any number of occurrences of the prefix mapping from the viewId. 
385         // For example, if the incoming value was /faces/faces/faces/view.xhtml 
386         // the result would be simply view.xhtml.
387         
388         if ("".equals(prefix))
389         {
390             // if prefix is an empty string (Spring environment), we let it be "//"
391             // in order to prevent an infinite loop in uri.startsWith(-emptyString-).
392             // Furthermore a prefix of "//" is just another double slash prevention.
393             prefix = "//";
394         }
395         else
396         {
397             // need to make sure its really /faces/* and not /facesPage.xhtml
398             prefix = prefix + '/'; 
399         }
400         
401         String uri = viewId;
402         while (uri.startsWith(prefix) || uri.startsWith("//")) 
403         {
404             if (uri.startsWith(prefix))
405             {
406                 // cut off only /faces, leave the trailing '/' char for the next iteration
407                 uri = uri.substring(prefix.length() - 1);
408             }
409             else
410             {
411                 // uri starts with '//' --> cut off the leading slash, leaving
412                 // the second slash to compare for the next iteration
413                 uri = uri.substring(1);
414             }
415         }
416         
417         //now delete any remaining leading '/'
418         // TODO: CJH: I don't think this is correct, considering that getActionURL() expects everything to
419         // start with '/', and in the suffix case we only mess with the suffix and leave leading
420         // slashes alone.  Please review...
421         /*if(uri.startsWith("/"))
422         {
423             uri = uri.substring(1);
424         }*/
425         
426         return uri;
427     }
428     
429     /**
430      * Return the viewId with any non-standard suffix stripped off and replaced with
431      * the default suffix configured for the specified context.
432      * <p/>
433      * For example, an input parameter of "/foo.jsf" may return "/foo.jsp".
434      */
435     protected String handleSuffixMapping(FacesContext context, String requestViewId)
436     {
437         String[] faceletsViewMappings = _initialized ? _faceletsViewMappings : getFaceletsViewMappings(context);
438         String[] jspDefaultSuffixes = _initialized ? _contextSuffixes : getContextSuffix(context);
439         
440         int slashPos = requestViewId.lastIndexOf('/');
441         int extensionPos = requestViewId.lastIndexOf('.');
442         
443         StringBuilder builder = SharedStringBuilder.get(context, VIEW_HANDLER_SUPPORT_SB);
444         
445         //Try to locate any resource that match with the expected id
446         for (String defaultSuffix : jspDefaultSuffixes)
447         {
448             //StringBuilder builder = new StringBuilder(requestViewId);
449             builder.setLength(0);
450             builder.append(requestViewId);
451            
452             if (extensionPos > -1 && extensionPos > slashPos)
453             {
454                 builder.replace(extensionPos, requestViewId.length(), defaultSuffix);
455             }
456             else
457             {
458                 builder.append(defaultSuffix);
459             }
460             String candidateViewId = builder.toString();
461             
462             if( faceletsViewMappings != null && faceletsViewMappings.length > 0 )
463             {
464                 for (String mapping : faceletsViewMappings)
465                 {
466                     if(mapping.startsWith("/"))
467                     {
468                         continue;   //skip this entry, its a prefix mapping
469                     }
470                     if(mapping.equals(candidateViewId))
471                     {
472                         return candidateViewId;
473                     }
474                     if(mapping.startsWith(".")) //this is a wildcard entry
475                     {
476                         builder.setLength(0); //reset/reuse the builder object 
477                         builder.append(candidateViewId); 
478                         builder.replace(candidateViewId.lastIndexOf('.'), candidateViewId.length(), mapping);
479                         String tempViewId = builder.toString();
480                         if(checkResourceExists(context,tempViewId))
481                         {
482                             return tempViewId;
483                         }
484                     }
485                 }
486             }
487 
488             // forced facelets mappings did not match or there were no entries in faceletsViewMappings array
489             if(checkResourceExists(context,candidateViewId))
490             {
491                 return candidateViewId;
492             }
493         
494         }
495         
496         //jsp suffixes didn't match, try facelets suffix
497         String faceletsDefaultSuffix = _initialized ? _faceletsContextSufix : this.getFaceletsContextSuffix(context);
498         if (faceletsDefaultSuffix != null)
499         {
500             for (String defaultSuffix : jspDefaultSuffixes)
501             {
502                 if (faceletsDefaultSuffix.equals(defaultSuffix))
503                 {
504                     faceletsDefaultSuffix = null;
505                     break;
506                 }
507             }
508         }
509         if (faceletsDefaultSuffix != null)
510         {
511             //StringBuilder builder = new StringBuilder(requestViewId);
512             builder.setLength(0);
513             builder.append(requestViewId);
514             
515             if (extensionPos > -1 && extensionPos > slashPos)
516             {
517                 builder.replace(extensionPos, requestViewId.length(), faceletsDefaultSuffix);
518             }
519             else
520             {
521                 builder.append(faceletsDefaultSuffix);
522             }
523             
524             String candidateViewId = builder.toString();
525             if(checkResourceExists(context,candidateViewId))
526             {
527                 return candidateViewId;
528             }
529         }
530 
531         // Otherwise, if a physical resource exists with the name requestViewId let that value be viewId.
532         if(checkResourceExists(context,requestViewId))
533         {
534             return requestViewId;
535         }
536         
537         //Otherwise return null.
538         return null;
539     }
540     
541     protected boolean checkResourceExists(FacesContext context, String viewId)
542     {
543         try
544         {
545             if (isCheckedViewIdCachingEnabled(context))
546             {
547                 Boolean resourceExists = getCheckedViewIDMap(context).get(
548                         viewId);
549                 if (resourceExists == null)
550                 {
551                     ViewDeclarationLanguage vdl = context.getApplication().getViewHandler()
552                             .getViewDeclarationLanguage(context, viewId);
553                     if (vdl != null)
554                     {
555                         resourceExists = vdl.viewExists(context, viewId);
556                     }
557                     else
558                     {
559                         // Fallback to default strategy
560                         resourceExists = context.getExternalContext().getResource(
561                                 viewId) != null;
562                     }
563                     getCheckedViewIDMap(context).put(viewId, resourceExists);
564                 }
565                 return resourceExists;
566             }
567             else
568             {
569                 ViewDeclarationLanguage vdl = context.getApplication().getViewHandler()
570                             .getViewDeclarationLanguage(context, viewId);
571                 if (vdl != null)
572                 {
573                     if (vdl.viewExists(context, viewId))
574                     {
575                         return true;
576                     }
577                 }
578                 else
579                 {
580                     // Fallback to default strategy
581                     if (context.getExternalContext().getResource(viewId) != null)
582                     {
583                         return true;
584                     }
585                 }
586             }
587         }
588         catch(MalformedURLException e)
589         {
590             //ignore and move on
591         }     
592         return false;
593     }
594 
595     private ConcurrentLRUCache<String, Boolean> getCheckedViewIDMap(FacesContext context)
596     {
597         if (_checkedViewIdMap == null)
598         {
599             int maxSize = getViewIDCacheMaxSize(context);
600             _checkedViewIdMap = new ConcurrentLRUCache<String, Boolean>((maxSize * 4 + 3) / 3, maxSize);
601         }
602         return _checkedViewIdMap;
603     }
604 
605     private boolean isCheckedViewIdCachingEnabled(FacesContext context)
606     {
607         if (_checkedViewIdCacheEnabled == null)
608         {
609             // first, check if the ProjectStage is development and skip caching in this case
610             if (context.isProjectStage(ProjectStage.Development))
611             {
612                 _checkedViewIdCacheEnabled = Boolean.FALSE;
613             }
614             else
615             {
616                 // in all ohter cases, make sure that the cache is not explicitly disabled via context param
617                 _checkedViewIdCacheEnabled = WebConfigParamUtils.getBooleanInitParameter(context.getExternalContext(),
618                         CHECKED_VIEWID_CACHE_ENABLED_ATTRIBUTE,
619                         CHECKED_VIEWID_CACHE_ENABLED_DEFAULT);
620             }
621 
622             if (log.isLoggable(Level.FINE))
623             {
624                 log.log(Level.FINE, "MyFaces ViewID Caching Enabled="
625                         + _checkedViewIdCacheEnabled);
626             }
627         }
628         return _checkedViewIdCacheEnabled;
629     }
630 
631     private int getViewIDCacheMaxSize(FacesContext context)
632     {
633         ExternalContext externalContext = context.getExternalContext();
634 
635         return WebConfigParamUtils.getIntegerInitParameter(externalContext,
636                 CHECKED_VIEWID_CACHE_SIZE_ATTRIBUTE, CHECKED_VIEWID_CACHE_DEFAULT_SIZE);
637     }
638 }