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.application.jsp;
20  
21  import org.apache.commons.logging.Log;
22  import org.apache.commons.logging.LogFactory;
23  import org.apache.myfaces.portlet.MyFacesGenericPortlet;
24  import org.apache.myfaces.portlet.PortletUtil;
25  import org.apache.myfaces.util.DebugUtils;
26  import org.apache.myfaces.shared_impl.webapp.webxml.ServletMapping;
27  import org.apache.myfaces.shared_impl.webapp.webxml.WebXml;
28  
29  import javax.faces.FacesException;
30  import javax.faces.application.Application;
31  import javax.faces.application.ViewHandler;
32  import javax.faces.component.UIViewRoot;
33  import javax.faces.context.ExternalContext;
34  import javax.faces.context.FacesContext;
35  import javax.faces.render.RenderKitFactory;
36  import javax.portlet.PortletURL;
37  import javax.portlet.RenderResponse;
38  import javax.servlet.ServletResponse;
39  import javax.servlet.http.HttpServletRequest;
40  import javax.servlet.http.HttpServletResponse;
41  import javax.servlet.http.HttpSession;
42  import java.io.IOException;
43  import java.util.Iterator;
44  import java.util.List;
45  import java.util.Locale;
46  
47  /**
48   * Implementation of the ViewHandler interface that knows how to use JSP pages
49   * as the view templating mechanism, and either javax.servlet or javax.portlet API
50   * as the request/response framework.
51   * <p>
52   * This implementation works tightly together with the various JSP TagHandler classes
53   * to implement the behaviour mandated by the ViewHandler specification. 
54   * <p>
55   * Rendering of a view is done simply by invoking the servlet generated from the jsp
56   * file that is named by the viewId of the view being rendered. Such servlets generate
57   * all their output in one pass, writing text and invoking JSP taghandlers in the order
58   * they are present in the jsp file.
59   * <p>
60   * On the first visit to a view, components are created when their taghandler is invoked
61   * They must then be rendered immediately in order for their output to be generated in the
62   * correct location relative to non-JSF text (and other non-jsf tags) within the same jsp
63   * file. This means that components which are defined via tags later in the page do not
64   * yet exist. This is particularly problematic for:
65   * <ul>
66   * <li>label components that want to refer to another component via a "for" attribute
67   * <li>components that "render their children", ie which render themselves differently
68   * depending upon the number and type of child nodes, or which want to wrap the output
69   * of each child node in specific text.
70   * </ul>
71   * <p>
72   * On later visits to the same view, the component tree already exists (has been restored).
73   * Components can therefore reference others later in the page. When the JSP-generated
74   * servlet invokes a taghandler, it "locates" the appropriate existing component in the
75   * tree rather than creating a new one, and then invokes the appropriate rendering
76   * methods on it.
77   * <p>
78   * @author Thomas Spiegl (latest modification by $Author: skitching $)
79   * @version $Revision: 608647 $ $Date: 2008-01-03 16:57:10 -0500 (Thu, 03 Jan 2008) $
80   */
81  public class JspViewHandlerImpl
82      extends ViewHandler {
83      private static final Log log = LogFactory.getLog(JspViewHandlerImpl.class);
84      public static final String FORM_STATE_MARKER = "<!--@@JSF_FORM_STATE_MARKER@@-->";
85      public static final int FORM_STATE_MARKER_LEN = FORM_STATE_MARKER.length();
86  
87  
88      public JspViewHandlerImpl() {
89          if (log.isTraceEnabled()) log.trace("New ViewHandler instance created");
90      }
91  
92      /**
93       * Get the locales specified as acceptable by the original request, compare them to the
94       * locales supported by this Application and return the best match.
95       */
96      public Locale calculateLocale(FacesContext facesContext) {
97          Iterator locales = facesContext.getExternalContext().getRequestLocales();
98          while (locales.hasNext()) {
99              Locale locale = (Locale) locales.next();
100             for (Iterator it = facesContext.getApplication().getSupportedLocales(); it.hasNext();) {
101                 Locale supportLocale = (Locale) it.next();
102                 // higher priority to a language match over an exact match
103                 // that occures further down (see Jstl Reference 1.0 8.3.1)
104                 if (locale.getLanguage().equals(supportLocale.getLanguage()) &&
105                     (supportLocale.getCountry() == null ||
106                         supportLocale.getCountry().length() == 0)) {
107                     return supportLocale;
108                 }
109                 else if (supportLocale.equals(locale)) {
110                     return supportLocale;
111                 }
112             }
113         }
114 
115         Locale defaultLocale = facesContext.getApplication().getDefaultLocale();
116         return defaultLocale != null ? defaultLocale : Locale.getDefault();
117     }
118 
119     public String calculateRenderKitId(FacesContext facesContext) {
120         String renderKitId = facesContext.getApplication().getDefaultRenderKitId();
121         return (renderKitId != null) ? renderKitId : RenderKitFactory.HTML_BASIC_RENDER_KIT;
122         //TODO: how to calculate from client?
123     }
124 
125     /**
126      * Create a UIViewRoot object and return it; the returned object has no children.
127      * <p>
128      * As required by the spec, the returned object inherits locale and renderkit settings from
129      * the viewRoot currently configured for the facesContext (if any). This means that on navigation
130      * from one view to another these settings are "inherited".
131      * <p>
132      */
133     public UIViewRoot createView(FacesContext facesContext, String viewId) {
134         Application application = facesContext.getApplication();
135         ViewHandler applicationViewHandler = application.getViewHandler();
136 
137         Locale currentLocale = null;
138         String currentRenderKitId = null;
139         UIViewRoot uiViewRoot = facesContext.getViewRoot();
140         if (uiViewRoot != null) {
141             //Remember current locale and renderKitId
142             currentLocale = uiViewRoot.getLocale();
143             currentRenderKitId = uiViewRoot.getRenderKitId();
144         }
145 
146         uiViewRoot = (UIViewRoot) application.createComponent(UIViewRoot.COMPONENT_TYPE);
147 //      as of JSF spec page 7-16:
148 //      "It is the callers responsibility to ensure that setViewId() is called
149 //      on the returned view, passing the same viewId value."
150 //      so we do not set the viewId here
151 
152 //      ok, but the RI does so, so let's do it, too.
153         uiViewRoot.setViewId(viewId);
154 
155         if (currentLocale != null) {
156             //set old locale
157             uiViewRoot.setLocale(currentLocale);
158         }
159         else {
160             //calculate locale
161             uiViewRoot.setLocale(applicationViewHandler.calculateLocale(facesContext));
162         }
163 
164         if (currentRenderKitId != null) {
165             //set old renderKit
166             uiViewRoot.setRenderKitId(currentRenderKitId);
167         }
168         else {
169             //calculate renderKit
170             uiViewRoot.setRenderKitId(applicationViewHandler.calculateRenderKitId(facesContext));
171         }
172 
173         if (log.isTraceEnabled()) log.trace("Created view " + viewId);
174         return uiViewRoot;
175     }
176 
177     public String getActionURL(FacesContext facesContext, String viewId) {
178         if (PortletUtil.isRenderResponse(facesContext)) {
179             RenderResponse response = (RenderResponse) facesContext.getExternalContext().getResponse();
180             PortletURL url = response.createActionURL();
181             url.setParameter(MyFacesGenericPortlet.VIEW_ID, viewId);
182             return url.toString();
183         }
184 
185         String path = getViewIdPath(facesContext, viewId);
186         if (path.length() > 0 && path.charAt(0) == '/') {
187             return facesContext.getExternalContext().getRequestContextPath() + path;
188         }
189         else {
190             return path;
191         }
192     }
193 
194     public String getResourceURL(FacesContext facesContext, String path) {
195         if (path.length() > 0 && path.charAt(0) == '/') {
196             return facesContext.getExternalContext().getRequestContextPath() + path;
197         }
198         else {
199             return path;
200         }
201     }
202 
203     /**
204      * Simply invoke the appropriate jsp-generated servlet.
205      */
206     public void renderView(FacesContext facesContext, UIViewRoot viewToRender)
207         throws IOException, FacesException {
208         if (viewToRender == null) {
209             log.fatal("viewToRender must not be null");
210             throw new NullPointerException("viewToRender must not be null");
211         }
212 
213         ExternalContext externalContext = facesContext.getExternalContext();
214 
215         String viewId = facesContext.getViewRoot().getViewId();
216 
217         if (PortletUtil.isPortletRequest(facesContext)) {
218             externalContext.dispatch(viewId);
219             return;
220         }
221 
222         ServletMapping servletMapping = getServletMapping(externalContext);
223 
224         if (servletMapping != null && servletMapping.isExtensionMapping()) {
225             String defaultSuffix = externalContext.getInitParameter(ViewHandler.DEFAULT_SUFFIX_PARAM_NAME);
226             String suffix = defaultSuffix != null ? defaultSuffix : ViewHandler.DEFAULT_SUFFIX;
227             DebugUtils.assertError(suffix.charAt(0) == '.',
228                                    log, "Default suffix must start with a dot!");
229             if (!viewId.endsWith(suffix)) {
230                 int slashPos = viewId.lastIndexOf('/');
231                 int extensionPos = viewId.lastIndexOf('.');
232                 if (extensionPos == -1 || extensionPos <= slashPos) {
233                     if (log.isTraceEnabled())
234                         log.trace("Current viewId has no extension, appending default suffix " + suffix);
235                     viewId = viewId + suffix;
236                 }
237                 else {
238                     if (log.isTraceEnabled()) log.trace("Replacing extension of current viewId by suffix " + suffix);
239                     viewId = viewId.substring(0, extensionPos) + suffix;
240                 }
241                 facesContext.getViewRoot().setViewId(viewId);
242             }
243         }
244 
245         if (log.isTraceEnabled()) log.trace("Dispatching to " + viewId);
246 
247         // handle character encoding as of section 2.5.2.2 of JSF 1.1
248         if (externalContext.getResponse() instanceof ServletResponse) {
249             ServletResponse response = (ServletResponse) externalContext.getResponse();
250             response.setLocale(viewToRender.getLocale());
251         }
252 
253         // TODO: 2.5.2.2 for Portlet?  What do I do?
254 
255         externalContext.dispatch(viewId);
256 
257         // handle character encoding as of section 2.5.2.2 of JSF 1.1
258         if (externalContext.getRequest() instanceof HttpServletRequest) {
259             HttpServletResponse response = (HttpServletResponse) externalContext.getResponse();
260             HttpServletRequest request = (HttpServletRequest) externalContext.getRequest();
261             HttpSession session = request.getSession(false);
262 
263             if (session != null) {
264                 session.setAttribute(ViewHandler.CHARACTER_ENCODING_KEY, response.getCharacterEncoding());
265             }
266         }
267 
268     }
269 
270 
271     /**
272      * Just invoke StateManager.restoreView.
273      */
274     public UIViewRoot restoreView(FacesContext facesContext, String viewId) {
275         Application application = facesContext.getApplication();
276         ViewHandler applicationViewHandler = application.getViewHandler();
277         String renderKitId = applicationViewHandler.calculateRenderKitId(facesContext);
278         UIViewRoot viewRoot = application.getStateManager().restoreView(facesContext,
279                                                                         viewId,
280                                                                         renderKitId);
281         return viewRoot;
282     }
283 
284     /**
285      * Writes a state marker that is replaced later by one or more hidden form
286      * inputs.
287      * <p>
288      * The problem with html is that the only place to encode client-side state is
289      * in a hidden html input field. However when a form is submitted, only the fields
290      * within a particular form are sent; fields in other forms are not sent. Therefore
291      * the view tree state must be written into every form in the page. This method
292      * is therefore invoked at the end of every form.
293      * <p>
294      * And the problem with JSP2.0 as a templating system is that as described at the
295      * top of this file the component tree is not built before rendering starts. It
296      * is created only as jsf tags are encountered while the page is being rendered.
297      * <p>
298      * But when the end of a form (h:form tag) is encountered, not all of the component
299      * tree exists yet, so it is impossible to write the tree state out at that time.
300      * <p>
301      * The solution used here is to buffer the entire page being generated, and simply
302      * output a "marker" string. After the rendering pass is complete the component
303      * tree state can be computed. The generated page is then post-processed to
304      * replace all the "marker" strings with the complete tree state.
305      * <p> 
306      * Note that this is only a problem with client-side state saving. For server-side
307      * state it should be possible to write the appropriate "key" out even though the
308      * data will not be stored under that key until the end of the rendering. This is
309      * not yet implemented however. TODO: optimise server-side state saving by not
310      * writing markers.
311      * <p>
312      * See {@link org.apache.myfaces.taglib.core.ViewTag#doAfterBody()} for the call
313      * that triggers the replacement of marker strings.
314      *  
315      * @param facesContext
316      * @throws IOException
317      */
318     public void writeState(FacesContext facesContext) throws IOException {
319         facesContext.getResponseWriter().write(FORM_STATE_MARKER);
320     }
321 
322 
323     protected String getViewIdPath(FacesContext facescontext, String viewId) {
324         if (viewId == null) {
325             log.error("ViewId must not be null");
326             throw new NullPointerException("ViewId must not be null");
327         }
328         if (!viewId.startsWith("/")) {
329             log.error("ViewId must start with '/' (viewId = " + viewId + ")");
330             throw new IllegalArgumentException("ViewId must start with '/' (viewId = " + viewId + ")");
331         }
332 
333         if (PortletUtil.isPortletRequest(facescontext)) {
334             return viewId;
335         }
336 
337         ServletMapping servletMapping = getServletMapping(facescontext.getExternalContext());
338 
339         if (servletMapping != null) {
340             if (servletMapping.isExtensionMapping()) {
341                 // extension mapping
342                 String urlpattern = servletMapping.getUrlPattern();
343                 if (urlpattern.startsWith("*")) {
344                     urlpattern = urlpattern.substring(1, urlpattern.length());
345                 }
346                 if (viewId.endsWith(urlpattern)) {
347                     return viewId;
348                 }
349                 else {
350                     int slashPos = viewId.lastIndexOf('/');
351                     int extensionPos = viewId.lastIndexOf('.');
352                     
353                     if (extensionPos == -1 || extensionPos <= slashPos) {
354                         return viewId + urlpattern;
355                     }
356                     else {
357                         return viewId.substring(0, extensionPos) + urlpattern;
358                     }
359 
360                 }
361             }
362             else {
363                 // prefix mapping
364                 String urlpattern = servletMapping.getUrlPattern();
365                 if (urlpattern.endsWith("/*")) {
366                     urlpattern = urlpattern.substring(0, urlpattern.length() - 2);
367                 }
368                 return urlpattern + viewId;
369             }
370         }
371         else {
372             return viewId;
373         }
374     }
375 
376     private static ServletMapping getServletMapping(ExternalContext externalContext) {
377         String servletPath = externalContext.getRequestServletPath();
378         String requestPathInfo = externalContext.getRequestPathInfo();
379 
380         WebXml webxml = WebXml.getWebXml(externalContext);
381         List mappings = webxml.getFacesServletMappings();
382 
383 
384         if (requestPathInfo == null) {
385             // might be extension mapping
386             for (int i = 0, size = mappings.size(); i < size; i++) {
387                 ServletMapping servletMapping = (ServletMapping) mappings.get(i);
388                 String urlpattern = servletMapping.getUrlPattern();
389                 String extension = urlpattern.substring(1, urlpattern.length());
390                 if (servletPath.endsWith(extension)) {
391                     return servletMapping;
392                 }
393                 else if (servletPath.equals(urlpattern)) {
394                     // path mapping with no pathInfo for the current request
395                     return servletMapping;
396                 }
397             }
398         }
399         else {
400             // path mapping
401             for (int i = 0, size = mappings.size(); i < size; i++) {
402 
403                 ServletMapping servletMapping = (ServletMapping) mappings.get(i);
404                 String urlpattern = servletMapping.getUrlPattern();
405                 urlpattern = urlpattern.substring(0, urlpattern.length() - 2);
406                 // servletPath starts with "/" except in the case where the
407                 // request is matched with the "/*" pattern, in which case
408                 // it is the empty string (see Servlet Sepc 2.3 SRV4.4)
409                 if (servletPath.equals(urlpattern)) {
410                     return servletMapping;
411                 }
412             }
413         }
414 
415         // handle cases as best possible where servletPath is not a faces servlet,
416         // such as when coming through struts-faces
417         if (mappings.size() > 0) {
418             return (ServletMapping) mappings.get(0);
419         }
420         else {
421             log.warn("no faces servlet mappings found");
422             return null;
423         }
424     }
425 
426 
427 }