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