View Javadoc

1   /*
2    * Licensed to the Apache Software Foundation (ASF) under one
3    * or more contributor license agreements.  See the NOTICE file
4    * distributed with this work for additional information
5    * regarding copyright ownership.  The ASF licenses this file
6    * to you under the Apache License, Version 2.0 (the
7    * "License"); you may not use this file except in compliance
8    * with the License.  You may obtain a copy of the License at
9    *
10   *   http://www.apache.org/licenses/LICENSE-2.0
11   *
12   * Unless required by applicable law or agreed to in writing,
13   * software distributed under the License is distributed on an
14   * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
15   * KIND, either express or implied.  See the License for the
16   * specific language governing permissions and limitations
17   * under the License.
18   */
19  package org.apache.myfaces.shared.application;
20  
21  import java.net.MalformedURLException;
22  import java.util.Map;
23  import java.util.logging.Level;
24  import java.util.logging.Logger;
25  
26  import javax.faces.application.ViewHandler;
27  import javax.faces.context.ExternalContext;
28  import javax.faces.context.FacesContext;
29  import javax.faces.render.ResponseStateManager;
30  import javax.faces.view.ViewDeclarationLanguage;
31  
32  import org.apache.myfaces.shared.renderkit.html.util.SharedStringBuilder;
33  import org.apache.myfaces.shared.util.ExternalContextUtils;
34  import org.apache.myfaces.shared.util.StringUtils;
35  import org.apache.myfaces.shared.util.ViewProtectionUtils;
36  
37  /**
38   * A ViewHandlerSupport implementation for use with standard Java Servlet engines,
39   * ie an engine that supports javax.servlet, and uses a standard web.xml file.
40   */
41  public class DefaultViewHandlerSupport implements ViewHandlerSupport
42  {
43      /**
44       * Identifies the FacesServlet mapping in the current request map.
45       */
46      private static final String CACHED_SERVLET_MAPPING =
47          DefaultViewHandlerSupport.class.getName() + ".CACHED_SERVLET_MAPPING";
48  
49      //private static final Log log = LogFactory.getLog(DefaultViewHandlerSupport.class);
50      private static final Logger log = Logger.getLogger(DefaultViewHandlerSupport.class.getName());
51      
52      private static final String VIEW_HANDLER_SUPPORT_SB = "oam.viewhandler.SUPPORT_SB";
53      
54      private final String[] _faceletsViewMappings;
55      private final String[] _contextSuffixes;
56      private final String _faceletsContextSufix;
57      private final boolean _initialized;
58      private CheckedViewIdsCache checkedViewIdsCache = null;
59      
60      public DefaultViewHandlerSupport()
61      {
62          _faceletsViewMappings = null;
63          _contextSuffixes = null;
64          _faceletsContextSufix = null;
65          _initialized = false;
66      }
67      
68      public DefaultViewHandlerSupport(FacesContext facesContext)
69      {
70          _faceletsViewMappings = getFaceletsViewMappings(facesContext);
71          _contextSuffixes = getContextSuffix(facesContext);
72          _faceletsContextSufix = getFaceletsContextSuffix(facesContext);
73          _initialized = true;
74      }
75  
76      public String calculateViewId(FacesContext context, String viewId)
77      {
78          //If no viewId found, don't try to derive it, just continue.
79          if (viewId == null)
80          {
81              return null;
82          }
83          FacesServletMapping mapping = getFacesServletMapping(context);
84          if (mapping == null || mapping.isExtensionMapping())
85          {
86              viewId = handleSuffixMapping(context, viewId);
87          }
88          else if (mapping.isExactMapping())
89          {
90              // if the current request is a exact mapping and the viewId equals the exact viewId
91              if (viewId.equals(mapping.getExact()))
92              {
93                  viewId = handleSuffixMapping(context, viewId + ".jsf");
94              }
95          }
96          else if(mapping.isPrefixMapping())
97          {
98              viewId = handlePrefixMapping(viewId,mapping.getPrefix());
99              
100             // A viewId that is equals to the prefix mapping on servlet mode is
101             // considered invalid, because jsp vdl will use RequestDispatcher and cause
102             // a loop that ends in a exception. Note in portlet mode the view
103             // could be encoded as a query param, so the viewId could be valid.
104             if (viewId != null && viewId.equals(mapping.getPrefix()) &&
105                 !ExternalContextUtils.isPortlet(context.getExternalContext()))
106             {
107                 throw new InvalidViewIdException();
108             }
109             
110             // In JSF 2.3 some changes were done in the VDL to avoid the jsp vdl
111             // RequestDispatcher redirection (only accept viewIds with jsp extension).
112             // If we have this case
113             if (viewId != null && viewId.equals(mapping.getPrefix()))
114             {
115                 viewId = handleSuffixMapping(context, viewId+".jsf");
116             }
117         }
118         else if (mapping.getUrlPattern().startsWith(viewId))
119         {
120             throw new InvalidViewIdException(viewId);
121         }
122 
123         //if(viewId != null)
124         //{
125         //    return (checkResourceExists(context,viewId) ? viewId : null);
126         //}
127 
128         return viewId;    // return null if no physical resource exists
129     }
130     
131     public String calculateAndCheckViewId(FacesContext context, String viewId)
132     {
133         //If no viewId found, don't try to derive it, just continue.
134         if (viewId == null)
135         {
136             return null;
137         }
138         FacesServletMapping mapping = getFacesServletMapping(context);
139         if (mapping == null || mapping.isExtensionMapping())
140         {
141             viewId = handleSuffixMapping(context, viewId);
142         }
143         else if (mapping.isExactMapping())
144         {
145             // if the current request is a exact mapping and the viewId equals the exact viewId
146             if (viewId.equals(mapping.getExact()))
147             {
148                 viewId = handleSuffixMapping(context, viewId + ".jsf");
149             }
150         }
151         else if(mapping.isPrefixMapping())
152         {
153             viewId = handlePrefixMapping(viewId,mapping.getPrefix());
154 
155             if(viewId != null)
156             {
157                 // A viewId that is equals to the prefix mapping on servlet mode is
158                 // considered invalid, because jsp vdl will use RequestDispatcher and cause
159                 // a loop that ends in a exception. Note in portlet mode the view
160                 // could be encoded as a query param, so the viewId could be valid.
161                 //if (viewId.equals(mapping.getPrefix()) &&
162                 //    !ExternalContextUtils.isPortlet(context.getExternalContext()))
163                 //{
164                 //    throw new InvalidViewIdException();
165                 //}
166             
167                 // In JSF 2.3 some changes were done in the VDL to avoid the jsp vdl
168                 // RequestDispatcher redirection (only accept viewIds with jsp extension).
169                 // If we have this case
170                 if (viewId != null && viewId.equals(mapping.getPrefix()))
171                 {
172                     viewId = handleSuffixMapping(context, viewId+".jsf");
173                 }
174 
175                 return (checkResourceExists(context,viewId) ? viewId : null);
176             }
177         }
178         else if (mapping.getUrlPattern().startsWith(viewId))
179         {
180             throw new InvalidViewIdException(viewId);
181         }
182         else
183         {
184             if(viewId != null)
185             {
186                 return (checkResourceExists(context,viewId) ? viewId : null);
187             }
188         }
189 
190         return viewId;    // return null if no physical resource exists
191     }
192 
193     public String calculateActionURL(FacesContext context, String viewId)
194     {
195         if (viewId == null || !viewId.startsWith("/"))
196         {
197             throw new IllegalArgumentException("ViewId must start with a '/': " + viewId);
198         }
199 
200         FacesServletMapping mapping = getFacesServletMapping(context);
201         ExternalContext externalContext = context.getExternalContext();
202         String contextPath = externalContext.getRequestContextPath();
203         //StringBuilder builder = new StringBuilder(contextPath);
204         StringBuilder builder = SharedStringBuilder.get(context, VIEW_HANDLER_SUPPORT_SB);
205         // If the context path is root, it is not necessary to append it, otherwise
206         // and extra '/' will be set.
207         if (contextPath != null && !(contextPath.length() == 1 && contextPath.charAt(0) == '/') )
208         {
209             builder.append(contextPath);
210         }
211         
212         // In JSF 2.3 we could have cases where the viewId can be bound to an url-pattern that is not
213         // prefix or suffix, but exact mapping. In this part we need to take the viewId and check if
214         // the viewId is bound or not with a mapping.
215         if (mapping != null && mapping.isExactMapping())
216         {
217             String exactMappingViewId = calculatePrefixedExactMapping(context, viewId);
218             if (exactMappingViewId != null && !exactMappingViewId.isEmpty())
219             {
220                 // if the current exactMapping already matches the requested viewId -> same view, skip....
221                 if (!mapping.getExact().equals(exactMappingViewId))
222                 {
223                     // different viewId -> lets try to lookup a exact mapping
224                     mapping = FacesServletMappingUtils.getExactMapping(context, exactMappingViewId);
225 
226                     // no exactMapping for the requested viewId available BUT the current view is a exactMapping
227                     // we need a to search for a prefix or extension mapping
228                     if (mapping == null)
229                     {
230                         mapping = FacesServletMappingUtils.getGenericPrefixOrSuffixMapping(context);
231                         if (mapping == null)
232                         {
233                             throw new IllegalStateException(
234                                     "No generic (either prefix or suffix) servlet-mapping found for FacesServlet."
235                                     + "This is required serve views, that are not exact mapped.");
236                         }
237                     }
238                 }
239             }
240         }
241         
242         if (mapping != null)
243         {
244             if (mapping.isExactMapping())
245             {
246                 builder.append(mapping.getExact());
247             }
248             else if (mapping.isExtensionMapping())
249             {
250                 //See JSF 2.0 section 7.5.2 
251                 String[] contextSuffixes = _initialized ? _contextSuffixes : getContextSuffix(context); 
252                 boolean founded = false;
253                 for (String contextSuffix : contextSuffixes)
254                 {
255                     if (viewId.endsWith(contextSuffix))
256                     {
257                         builder.append(viewId.substring(0, viewId.indexOf(contextSuffix)));
258                         builder.append(mapping.getExtension());
259                         founded = true;
260                         break;
261                     }
262                 }
263                 if (!founded)
264                 {   
265                     //See JSF 2.0 section 7.5.2
266                     // - If the argument viewId has an extension, and this extension is mapping, 
267                     // the result is contextPath + viewId
268                     //
269                     // -= Leonardo Uribe =- It is evident that when the page is generated, the derived 
270                     // viewId will end with the 
271                     // right contextSuffix, and a navigation entry on faces-config.xml should use such id,
272                     // this is just a workaroud
273                     // for usability. There is a potential risk that change the mapping in a webapp make 
274                     // the same application fail,
275                     // so use viewIds ending with mapping extensions is not a good practice.
276                     if (viewId.endsWith(mapping.getExtension()))
277                     {
278                         builder.append(viewId);
279                     }
280                     else if(viewId.lastIndexOf('.') != -1 )
281                     {
282                         builder.append(viewId.substring(0, viewId.lastIndexOf('.')));
283                         builder.append(contextSuffixes[0]);
284                     }
285                     else
286                     {
287                         builder.append(viewId);
288                         builder.append(contextSuffixes[0]);
289                     }
290                 }
291             }
292             else if (mapping.isPrefixMapping())
293             {
294                 builder.append(mapping.getPrefix());
295                 builder.append(viewId);
296             }
297         }
298         else
299         {
300             builder.append(viewId);
301         }
302         
303         //JSF 2.2 check view protection.
304         if (ViewProtectionUtils.isViewProtected(context, viewId))
305         {
306             int index = builder.indexOf("?");
307             if (index >= 0)
308             {
309                 builder.append("&");
310             }
311             else
312             {
313                 builder.append("?");
314             }
315             builder.append(ResponseStateManager.NON_POSTBACK_VIEW_TOKEN_PARAM);
316             builder.append("=");
317             ResponseStateManager rsm = context.getRenderKit().getResponseStateManager();
318             builder.append(rsm.getCryptographicallyStrongTokenFromSession(context));
319         }
320         
321         String calculatedActionURL = builder.toString();
322         if (log.isLoggable(Level.FINEST))
323         {
324             log.finest("Calculated actionURL: '" + calculatedActionURL + "' for viewId: '" + viewId + "'");
325         }
326         return calculatedActionURL;
327     }
328     
329     private String calculatePrefixedExactMapping(FacesContext context, String viewId)
330     {
331         String[] contextSuffixes = _initialized ? _contextSuffixes : getContextSuffix(context); 
332         String prefixedExactMapping = null;
333         for (String contextSuffix : contextSuffixes)
334         {
335             if (viewId.endsWith(contextSuffix))
336             {
337                 prefixedExactMapping = viewId.substring(0, viewId.length() - contextSuffix.length());
338                 break;
339             }
340         }
341         return prefixedExactMapping == null ? viewId : prefixedExactMapping;
342     }
343             
344 
345     /**
346      * Read the web.xml file that is in the classpath and parse its internals to
347      * figure out how the FacesServlet is mapped for the current webapp.
348      */
349     protected FacesServletMapping getFacesServletMapping(FacesContext context)
350     {
351         Map<Object, Object> attributes = context.getAttributes();
352 
353         // Has the mapping already been determined during this request?
354         FacesServletMapping mapping = (FacesServletMapping) attributes.get(CACHED_SERVLET_MAPPING);
355         if (mapping == null)
356         {
357             ExternalContext externalContext = context.getExternalContext();
358             mapping = FacesServletMappingUtils.calculateFacesServletMapping(
359                     context, externalContext.getRequestServletPath(),
360                     externalContext.getRequestPathInfo(),
361                     true);
362 
363             attributes.put(CACHED_SERVLET_MAPPING, mapping);
364         }
365         return mapping;
366     }
367 
368     protected String[] getContextSuffix(FacesContext context)
369     {
370         String defaultSuffix = context.getExternalContext().getInitParameter(ViewHandler.DEFAULT_SUFFIX_PARAM_NAME);
371         if (defaultSuffix == null)
372         {
373             defaultSuffix = ViewHandler.DEFAULT_SUFFIX;
374         }
375         return StringUtils.splitShortString(defaultSuffix, ' ');
376     }
377     
378     protected String getFaceletsContextSuffix(FacesContext context)
379     {
380         String defaultSuffix = context.getExternalContext().getInitParameter(ViewHandler.FACELETS_SUFFIX_PARAM_NAME);
381         if (defaultSuffix == null)
382         {
383             defaultSuffix = ViewHandler.DEFAULT_FACELETS_SUFFIX;
384         }
385         return defaultSuffix;
386     }
387     
388     
389     
390     protected String[] getFaceletsViewMappings(FacesContext context)
391     {
392         String faceletsViewMappings= context.getExternalContext().getInitParameter(
393                 ViewHandler.FACELETS_VIEW_MAPPINGS_PARAM_NAME);
394         if(faceletsViewMappings == null)    //consider alias facelets.VIEW_MAPPINGS
395         {
396             faceletsViewMappings= context.getExternalContext().getInitParameter("facelets.VIEW_MAPPINGS");
397         }
398         
399         return faceletsViewMappings == null ? null : StringUtils.splitShortString(faceletsViewMappings, ';');
400     }
401 
402     /**
403      * Return the normalized viewId according to the algorithm specified in 7.5.2 
404      * by stripping off any number of occurrences of the prefix mapping from the viewId.
405      * <p>
406      * For example, both /faces/view.xhtml and /faces/faces/faces/view.xhtml would both return view.xhtml
407      * </p>
408      */
409     protected String handlePrefixMapping(String viewId, String prefix)
410     {
411         // If prefix mapping (such as "/faces/*") is used for FacesServlet, 
412         // normalize the viewId according to the following
413         // algorithm, or its semantic equivalent, and return it.
414                
415         // Remove any number of occurrences of the prefix mapping from the viewId. 
416         // For example, if the incoming value was /faces/faces/faces/view.xhtml 
417         // the result would be simply view.xhtml.
418         
419         if ("".equals(prefix))
420         {
421             // if prefix is an empty string (Spring environment), we let it be "//"
422             // in order to prevent an infinite loop in uri.startsWith(-emptyString-).
423             // Furthermore a prefix of "//" is just another double slash prevention.
424             prefix = "//";
425         }
426         else
427         {
428             // need to make sure its really /faces/* and not /facesPage.xhtml
429             prefix = prefix + '/'; 
430         }
431         
432         String uri = viewId;
433         while (uri.startsWith(prefix) || uri.startsWith("//")) 
434         {
435             if (uri.startsWith(prefix))
436             {
437                 // cut off only /faces, leave the trailing '/' char for the next iteration
438                 uri = uri.substring(prefix.length() - 1);
439             }
440             else
441             {
442                 // uri starts with '//' --> cut off the leading slash, leaving
443                 // the second slash to compare for the next iteration
444                 uri = uri.substring(1);
445             }
446         }
447         
448         //now delete any remaining leading '/'
449         // TODO: CJH: I don't think this is correct, considering that getActionURL() expects everything to
450         // start with '/', and in the suffix case we only mess with the suffix and leave leading
451         // slashes alone.  Please review...
452         /*if(uri.startsWith("/"))
453         {
454             uri = uri.substring(1);
455         }*/
456         
457         return uri;
458     }
459     
460     /**
461      * Return the viewId with any non-standard suffix stripped off and replaced with
462      * the default suffix configured for the specified context.
463      * <p>
464      * For example, an input parameter of "/foo.jsf" may return "/foo.jsp".
465      * </p>
466      */
467     protected String handleSuffixMapping(FacesContext context, String requestViewId)
468     {
469         String[] faceletsViewMappings = _initialized ? _faceletsViewMappings : getFaceletsViewMappings(context);
470         String[] jspDefaultSuffixes = _initialized ? _contextSuffixes : getContextSuffix(context);
471         
472         int slashPos = requestViewId.lastIndexOf('/');
473         int extensionPos = requestViewId.lastIndexOf('.');
474         
475         StringBuilder builder = SharedStringBuilder.get(context, VIEW_HANDLER_SUPPORT_SB);
476         
477         //Try to locate any resource that match with the expected id
478         for (String defaultSuffix : jspDefaultSuffixes)
479         {
480             //StringBuilder builder = new StringBuilder(requestViewId);
481             builder.setLength(0);
482             builder.append(requestViewId);
483            
484             if (extensionPos > -1 && extensionPos > slashPos)
485             {
486                 builder.replace(extensionPos, requestViewId.length(), defaultSuffix);
487             }
488             else
489             {
490                 builder.append(defaultSuffix);
491             }
492             String candidateViewId = builder.toString();
493             
494             if( faceletsViewMappings != null && faceletsViewMappings.length > 0 )
495             {
496                 for (String mapping : faceletsViewMappings)
497                 {
498                     if(mapping.startsWith("/"))
499                     {
500                         continue;   //skip this entry, its a prefix mapping
501                     }
502                     if(mapping.equals(candidateViewId))
503                     {
504                         return candidateViewId;
505                     }
506                     if(mapping.startsWith(".")) //this is a wildcard entry
507                     {
508                         builder.setLength(0); //reset/reuse the builder object 
509                         builder.append(candidateViewId); 
510                         builder.replace(candidateViewId.lastIndexOf('.'), candidateViewId.length(), mapping);
511                         String tempViewId = builder.toString();
512                         if(checkResourceExists(context,tempViewId))
513                         {
514                             return tempViewId;
515                         }
516                     }
517                 }
518             }
519 
520             // forced facelets mappings did not match or there were no entries in faceletsViewMappings array
521             if(checkResourceExists(context,candidateViewId))
522             {
523                 return candidateViewId;
524             }
525         
526         }
527         
528         //jsp suffixes didn't match, try facelets suffix
529         String faceletsDefaultSuffix = _initialized ? _faceletsContextSufix : this.getFaceletsContextSuffix(context);
530         if (faceletsDefaultSuffix != null)
531         {
532             for (String defaultSuffix : jspDefaultSuffixes)
533             {
534                 if (faceletsDefaultSuffix.equals(defaultSuffix))
535                 {
536                     faceletsDefaultSuffix = null;
537                     break;
538                 }
539             }
540         }
541         if (faceletsDefaultSuffix != null)
542         {
543             //StringBuilder builder = new StringBuilder(requestViewId);
544             builder.setLength(0);
545             builder.append(requestViewId);
546             
547             if (extensionPos > -1 && extensionPos > slashPos)
548             {
549                 builder.replace(extensionPos, requestViewId.length(), faceletsDefaultSuffix);
550             }
551             else
552             {
553                 builder.append(faceletsDefaultSuffix);
554             }
555             
556             String candidateViewId = builder.toString();
557             if(checkResourceExists(context,candidateViewId))
558             {
559                 return candidateViewId;
560             }
561         }
562 
563         // Otherwise, if a physical resource exists with the name requestViewId let that value be viewId.
564         if(checkResourceExists(context,requestViewId))
565         {
566             return requestViewId;
567         }
568         
569         //Otherwise return null.
570         return null;
571     }
572     
573     protected boolean checkResourceExists(FacesContext facesContext, String viewId)
574     {
575         if (checkedViewIdsCache == null)
576         {
577             checkedViewIdsCache = CheckedViewIdsCache.getInstance(facesContext);
578         }
579 
580         try
581         {
582             Boolean resourceExists = null;
583             if (checkedViewIdsCache.isEnabled())
584             {
585                 resourceExists = checkedViewIdsCache.getCache().get(viewId);
586             }
587 
588             if (resourceExists == null)
589             {
590                 ViewDeclarationLanguage vdl = facesContext.getApplication().getViewHandler()
591                         .getViewDeclarationLanguage(facesContext, viewId);
592                 if (vdl != null)
593                 {
594                     resourceExists = vdl.viewExists(facesContext, viewId);
595                 }
596                 else
597                 {
598                     // Fallback to default strategy
599                     resourceExists = facesContext.getExternalContext().getResource(viewId) != null;
600                 }
601 
602                 if (checkedViewIdsCache.isEnabled())
603                 {
604                     checkedViewIdsCache.getCache().put(viewId, resourceExists);
605                 }
606             }
607 
608             return resourceExists;
609         }
610         catch (MalformedURLException e)
611         {
612             //ignore and move on
613         }     
614         return false;
615     }
616 
617 }