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