2009/05/20 - Apache Shale has been retired.

For more information, please explore the Attic.

View Javadoc

1   /*
2    * Licensed to the Apache Software Foundation (ASF) under one or more
3    * contributor license agreements.  See the NOTICE file distributed with
4    * this work for additional information regarding copyright ownership.
5    * The ASF licenses this file to you under the Apache License, Version 2.0
6    * (the "License"); you may not use this file except in compliance with
7    * the License.  You may obtain a copy of the License at
8    *
9    *      http://www.apache.org/licenses/LICENSE-2.0
10   *
11   * Unless required by applicable law or agreed to in writing, software
12   * distributed under the License is distributed on an "AS IS" BASIS,
13   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14   * See the License for the specific language governing permissions and
15   * limitations under the License.
16   */
17  
18  /*
19   * $Id: ClayViewHandler.java 488118 2006-12-18 04:26:47Z gvanmatre $
20   */
21  package org.apache.shale.clay.faces;
22  
23  import java.io.IOException;
24  import java.io.StringWriter;
25  import java.util.Iterator;
26  import java.util.Locale;
27  
28  import javax.faces.application.StateManager;
29  import javax.faces.application.ViewHandler;
30  import javax.faces.component.UIComponent;
31  import javax.faces.component.UIViewRoot;
32  import javax.faces.context.FacesContext;
33  import javax.faces.context.ResponseWriter;
34  import javax.faces.el.MethodBinding;
35  import javax.servlet.http.HttpServletResponse;
36  
37  import org.apache.commons.logging.Log;
38  import org.apache.commons.logging.LogFactory;
39  import org.apache.shale.clay.component.Clay;
40  import org.apache.shale.clay.config.Globals;
41  import org.apache.shale.clay.config.beans.PageNotFoundException;
42  import org.apache.shale.clay.utils.JSFRuntimeTracker;
43  
44  /***
45   * <p>This <code>ViewHandler</code> will handle full HTML template views using the
46   * {@link Clay} component as the single subtree under the view root.  Views will
47   * be intercepted having a suffix matching the registered clay template suffix
48   * in the web deployment descriptor.  The default suffixes are ".html" and "*.xml".
49   * All other view render requests that don't match the suffixes will be delegated to the
50   * original decorated view handler.</p>
51   */
52  public class ClayViewHandler extends ViewHandler {
53  
54      /***
55       * <p>
56       * Commons logger utility class instance.
57       * </p>
58       */
59      private static Log log;
60      static {
61          log = LogFactory.getLog(ClayViewHandler.class);
62      }
63  
64      /***
65       * <p>The decorated view handler.</p>
66       */
67      private ViewHandler original = null;
68  
69      /***
70       * <p>The {@link Clay} component <code>id</code> property value.</p>
71       */
72      private static final String CLAY_VIEW_ID = "clayView";
73  
74      /***
75       * <p>{@link Clay} <code>componentType</code> that is used to create a instance using
76       * the faces Application object.</p>
77       */
78      private static final String CLAY_COMPONENT_TYPE = "org.apache.shale.clay.component.Clay";
79  
80      /***
81       * <p>Holds the suffixes used to identify a Clay HTML or XML full view template.</p>
82       */
83      private String[] suffixes = null;
84  
85      /***
86       * <p>This is an overloaded constructor passing the <code>original</code>
87       * view handler.</p>
88       *
89       * @param original view handler
90       */
91      public ClayViewHandler(ViewHandler original) {
92          this.original = original;
93  
94          if (log.isInfoEnabled()) {
95             log.info("Loading Clay View Handler");
96          }
97      }
98  
99      /***
100      * @param context faces context
101      * @return locale calculated from the original handler
102      */
103     public Locale calculateLocale(FacesContext context) {
104         return original.calculateLocale(context);
105     }
106 
107     /***
108      * <p>Application scope attribute under which the
109      * <code>ViewControllerMapper</code> for translating view identifiers
110      * to class names of the corresponding <code>ViewController</code>
111      * is stored.</p>
112      */
113     public static final String VIEW_MAPPER =
114       "org$apache$shale$view$VIEW_MAPPER";
115 
116 
117     /***
118      * @param context faces context
119      * @return render kit id calculated from the original handler
120      */
121     public String calculateRenderKitId(FacesContext context) {
122         return original.calculateRenderKitId(context);
123     }
124 
125     /***
126      * <p>This method is overridden to check to see if the target view
127      * is a clay html or xml template.  If it is, the view id is normalized
128      * before the original implementation is invoked.
129      * </p>
130      *
131      * @param context faces context
132      * @param viewId name of the page
133      * @return root of the component tree for the view id
134      */
135     public UIViewRoot createView(FacesContext context, String viewId) {
136         String id = viewId;
137         int index = indexOfClayTemplateSuffix(context, id);
138 
139         if (index != -1) {
140             id = normalizeViewId(id, index);
141         }
142 
143         return original.createView(context, id);
144     }
145 
146     /***
147      * <p>Changes the suffix of the <code>viewId</code> to that of the
148      * suffix of a clay template defined by the <code>index</code> into
149      * the <code>suffixes</code> array.
150      *
151      * @param viewId name of the page
152      * @param index into the <code>suffixes</code> array
153      * @return viewId with a suffix matching the suffixes[index]
154      */
155     private String normalizeViewId(String viewId, int index) {
156 
157         StringBuffer buff = new StringBuffer(viewId);
158         int i = buff.lastIndexOf(".");
159         if (i > -1) {
160             buff.setLength(i);
161             buff.append(suffixes[index]);
162         }
163 
164         return buff.toString();
165 
166     }
167 
168     /***
169      * <p>
170      * If the <code>viewId</code> is suffixed with the Clay template suffix,
171      * rewrite the returned actionUrl with a clay suffix. The super
172      * implementation will assume ".jsp" or whatever the
173      * <code>javax.faces.DEFAULT_SUFFIX</code> is set to in the web deployment
174      * descriptor.
175      * </p>
176      *
177      * @param context faces context
178      * @param viewId name of the page
179      * @return action attribute of the UIForm component
180      */
181     public String getActionURL(FacesContext context, String viewId) {
182         String actionURL = original.getActionURL(context, viewId);
183         int index = indexOfClayTemplateSuffix(context, viewId);
184         if (index != -1) {
185            actionURL = normalizeViewId(actionURL, index);
186         } else if (context.getExternalContext().getRequestMap().get(Globals.CLAY_FULL_VIEW_RESTORE_IND) != null) {
187            //This stuff is for myfaces.  The action url is calculated from the
188            //servlet mappings. When switching from a .html or .xml clay view to a .jsp, myfaces changes the
189            //target viewId suffix to match the previous one.  This seems to handle the problem. I'm not
190            // sure how common it will be to have a mix of .jsp, .html and .xml within the same application.
191            int i = actionURL.lastIndexOf("/");
192            if (i > -1) {
193               StringBuffer tmp = new StringBuffer(actionURL);
194               tmp.setLength(i);
195               tmp.append(viewId);
196               actionURL = tmp.toString();
197            }
198 
199         }
200 
201         return actionURL;
202     }
203 
204     /***
205      * @param context faces context
206      * @param path context root relative path
207      * @return full path to the resource
208      */
209     public String getResourceURL(FacesContext context, String path) {
210         return original.getResourceURL(context, path);
211     }
212 
213     /***
214      * <p>This method looks to see if the target view identified by the viewId is a
215      * {@link org.apache.shale.clay.component.Clay} HTML or XML full view.
216      * This is determined by a value cached in request scope by the
217      * {@link ClayViewHandlerCommand} or the suffix of the viewId.  If a match
218      * is found, the index position into the suffixes array is returned;
219      * Otherwise a -1 is returned.</p>
220      *
221      * @param context faces context
222      * @param viewId name of the page
223      * @return index into the suffixes array or -1 if not found
224      */
225     protected int indexOfClayTemplateSuffix(FacesContext context, String viewId) {
226 
227         if (suffixes == null) {
228             suffixes = new String[2];
229 
230             suffixes[0] = context.getExternalContext().getInitParameter(
231                     Globals.CLAY_HTML_TEMPLATE_SUFFIX);
232             if (suffixes[0] == null) {
233                 suffixes[0] = Globals.CLAY_DEFAULT_HTML_TEMPLATE_SUFFIX;
234             }
235 
236             suffixes[1] = context.getExternalContext().getInitParameter(
237                     Globals.CLAY_XML_TEMPLATE_SUFFIX);
238             if (suffixes[1] == null) {
239                 suffixes[1] = Globals.CLAY_DEFAULT_XML_TEMPLATE_SUFFIX;
240             }
241 
242         }
243 
244         //looked for the original first set by the ClayViewHandlerCommand
245         String originalSuffix = (String) context.getExternalContext()
246                  .getRequestMap().get(Globals.CLAY_FULL_VIEW_SUFFIX);
247         if (originalSuffix != null) {
248             for (int i = 0; i < suffixes.length; i++) {
249                 if (originalSuffix.equals(suffixes[i])) {
250                     return i;
251                 }
252             }
253         }
254 
255         //look at the path
256         for (int i = 0; i < suffixes.length; i++) {
257             if (viewId.endsWith(suffixes[i])) {
258                 return i;
259             }
260         }
261 
262         return -1;
263     }
264 
265     /***
266      * <p>The default view handler implementation will try to
267      * make the viewId end with ".jsp".  If the viewId ends
268      * in the clay template suffix, use the state manager
269      * to restore the view.</p>
270      *
271      * @param context faces context
272      * @param viewId name of the page
273      * @return root of the page
274      */
275     public UIViewRoot restoreView(FacesContext context, String viewId) {
276         UIViewRoot view = null;
277 
278         int index = indexOfClayTemplateSuffix(context, viewId);
279         if (index != -1) {
280 
281             if (log.isDebugEnabled()) {
282                 log.debug("Clay template restoreView for " + viewId);
283             }
284 
285             StateManager stateManager = context.getApplication().getStateManager();
286             view = stateManager.restoreView(context, normalizeViewId(viewId, index), calculateRenderKitId(context));
287             //if a clay template was restored, clear the request param on the forward for
288             //clean navigation to non clay templates
289             if (view != null) {
290                 context.getExternalContext().getRequestMap().put(Globals.CLAY_FULL_VIEW_SUFFIX, null);
291                 context.getExternalContext().getRequestMap().put(Globals.CLAY_FULL_VIEW_RESTORE_IND, Boolean.TRUE);
292             }
293 
294         } else {
295             view = original.restoreView(context, viewId);
296         }
297 
298         return view;
299     }
300 
301     /***
302      * <p>Invokes the original view handler's writeState.</p>
303      *
304      * @param context faces context
305      * @exception IOException serializing tree
306      */
307     public void writeState(FacesContext context) throws IOException {
308         original.writeState(context);
309     }
310 
311     /***
312      * <p>HTML form markers for client side state saving for MyFaces and Sun RI
313      * implementations.</p>
314      */
315     protected static final String[] FORM_MARKERS = {
316         "com.sun.faces.saveStateFieldMarker",    // RI 1.1
317         "<!--@@JSF_FORM_STATE_MARKER@@-->",      // myfaces 1.1.x
318         "~com.sun.faces.saveStateFieldMarker~"}; // RI 1.2
319 
320     /***
321      * <p>Returns an index into the <code>FORM_MAKKERS</code> array.  The index will be used to
322      * get the form marker matching the JSF runtime. Only the myfaces and Sun RI are supported.
323      * The form marker is determined by trying to load the myfaces view handler.  Next,
324      * the Sun RI 1.2 JSPVersionTracker is attempted to be loaded.  The default
325      * is the marker for the Sun RI.</p>
326      *
327      * @return index into the FORM_MARKERS array
328      */
329     protected int indexOfFormMarker() {
330         int i = 0;
331         if (JSFRuntimeTracker.getJsfRuntime() == JSFRuntimeTracker.MYFACES_1_1) {
332           i = 1;
333         } else if (JSFRuntimeTracker.getJsfRuntime() == JSFRuntimeTracker.RI_1_2) {
334           i = 2;
335         }
336         return i;
337     }
338 
339     /***
340      * <p>The <code>viewId</code> is check to see if it ends with the
341      * same suffix as the full HTML or XML views.  This match might be performed
342      * by the {@link ClayViewHandlerCommand} when using the myfaces jsf implementation.
343      * If a match is not found, control is passed to the decorated view handler.
344      * Otherwise, a {@link org.apache.shale.clay.component.Clay} component is
345      * instantiated as a single subtree under the
346      * view root. The component's <code>id</code> property is set with a constant,
347      * <code>CLAY_VIEW_ID</code>.  The <code>jsfid</code>
348      * property is set to the <code>viewId</code>.  The <code>managedBeanName</code> property
349      * is set with the Shale <code>ViewControllerMapper</code>.  A <code>ResponseWriter</code>
350      * is created and rendering is invoked on the component.  This differs from the base implementation.
351      * The base implementation would dispatch to a JSP that would assemble the component tree
352      * and invoke rendering to the response writer.</p>
353      *
354      * @param context faces context
355      * @param view root of the component tree
356      * @exception IOException response writer
357      */
358     public void renderView(FacesContext context, UIViewRoot view)
359             throws IOException {
360 
361         int index = indexOfClayTemplateSuffix(context, view.getViewId());
362         //is this view a clay html template view
363         if (index != -1) {
364 
365             if (log.isDebugEnabled()) {
366                 log.debug("Clay template renderView for " + view.getViewId());
367             }
368 
369             //look to see if it already exists
370             UIComponent component = view.findComponent(CLAY_VIEW_ID);
371             if (component == null) {
372 
373             String viewId = normalizeViewId(view.getViewId(), index);
374             view.setViewId(viewId);
375               component = context.getApplication().createComponent(CLAY_COMPONENT_TYPE);
376               ((Clay) component).setId(CLAY_VIEW_ID);
377               ((Clay) component).setJsfid(viewId);
378               ((Clay) component).setManagedBeanName(getManagedBeanName(context, viewId));
379               view.getChildren().add(view.getChildren().size(), component);
380             }
381 
382             //get the response
383             HttpServletResponse response = (HttpServletResponse) context.getExternalContext().getResponse();
384             // TODO create a configurable method to set the content type
385             if (response.getContentType() == null) {
386                 response.setContentType("text/html");
387             }
388             //set the locale
389             response.setLocale(context.getViewRoot().getLocale());
390 
391             //builds a buffer to write the page to
392             StringWriter writer = new StringWriter();
393             //create a buffered response writer
394             ResponseWriter buffResponsewriter = context.getRenderKit()
395                   .createResponseWriter(writer, null, response.getCharacterEncoding());
396             //push buffered writer to the faces context
397             context.setResponseWriter(buffResponsewriter);
398             //start a document
399             buffResponsewriter.startDocument();
400 
401             try {
402                recursiveRender(view, context);
403             } catch (PageNotFoundException e) {
404                 //look to see if the page not found is a top level page
405                 if (e.getResource().equals(view.getViewId())) {
406                    response.sendError(HttpServletResponse.SC_NOT_FOUND, e.getResource());
407                    context.responseComplete();
408                    return;
409                 }
410 
411                 throw e;
412             }
413             //end the document
414             buffResponsewriter.endDocument();
415 
416             //save the view
417             StateManager stateManager = context.getApplication().getStateManager();
418             StateManager.SerializedView serializedview = stateManager.saveSerializedView(context);
419 
420             ResponseWriter responsewriter = context.getRenderKit()
421                     .createResponseWriter(response.getWriter(), null, response.getCharacterEncoding());
422             //push response writer to the faces context
423             context.setResponseWriter(responsewriter);
424 
425             StringBuffer buff = writer.getBuffer();
426             if (stateManager.isSavingStateInClient(context)) {
427                 int curPos = 0;   // current position
428                 int fndPos = 0;   //start of a marker
429                 int frmMkrIdx = indexOfFormMarker();
430 
431                 //might be multiple forms in the document
432                 do {
433                     fndPos = buff.indexOf(FORM_MARKERS[frmMkrIdx], curPos);
434                     if (fndPos > -1) {
435                         responsewriter.write(buff.substring(curPos, fndPos));
436                         stateManager.writeState(context, serializedview);
437                         curPos = fndPos + FORM_MARKERS[frmMkrIdx].length();
438                     } else {
439                         responsewriter.write(buff.substring(curPos));
440                     }
441                 } while(curPos < buff.length() && fndPos > -1);
442 
443             } else {
444                //using server side state no need to look for form markers
445                responsewriter.write(buff.toString());
446             }
447 
448         } else {
449            //dispatch (forward) to the jsp
450            original.renderView(context, view);
451         }
452     }
453 
454     /***
455      * <p>
456      * Recursively invokes the rendering of the sub component tree.
457      * </p>
458      *
459      * @param child component to invoke renderering on
460      * @param context faces context
461      * @exception IOException writing markup
462      */
463     protected void recursiveRender(UIComponent child,
464             FacesContext context) throws IOException {
465 
466         if (!child.getRendersChildren()) {
467             child.encodeBegin(context);
468             Iterator ci = child.getChildren().iterator();
469             while (ci.hasNext()) {
470                 UIComponent c = (UIComponent) ci.next();
471                 c.encodeBegin(context);
472 
473                 if (!c.getRendersChildren()) {
474                     recursiveRender(c, context);
475                 } else {
476                     c.encodeChildren(context);
477                 }
478 
479                 c.encodeEnd(context);
480                 c = null;
481             }
482             child.encodeEnd(context);
483         } else {
484             // let the component handle iterating over the children
485             child.encodeBegin(context);
486             child.encodeChildren(context);
487             child.encodeEnd(context);
488         }
489     }
490 
491     /***
492      * <p>Returns the "@managed-bean-name" the view controller is registered under.  The
493      * assumed mapping will be the same as in core Shale.</p>
494      *
495      * @param context faces context
496      * @param viewId name of the page
497      * @return default managed bean name associated with the view
498      */
499     protected String getManagedBeanName(FacesContext context, String viewId) {
500         String managedBeanName = null;
501 
502         Object mapper = context.getApplication().getVariableResolver()
503                                  .resolveVariable(context, VIEW_MAPPER);
504         // is there a view controller mapper
505         if (mapper != null) {
506             StringBuffer el = new StringBuffer();
507             el.append("#{").append(VIEW_MAPPER).append(".mapViewId").append("}");
508             MethodBinding mb = context.getApplication()
509                     .createMethodBinding(el.toString(), new Class[] {String.class});
510             managedBeanName = (String) mb.invoke(context, new Object[] {viewId});
511         }
512 
513         return managedBeanName;
514     }
515 
516 }