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