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