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