View Javadoc

1   /**
2    * Copyright (C) 2005-2009 Alfresco Software Limited.
3    *
4    * This file is part of the Spring Surf Extension project.
5    *
6    * Licensed under the Apache License, Version 2.0 (the "License");
7    * you may not use this file except in compliance with the License.
8    * 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, software
13   * distributed under the License is distributed on an "AS IS" BASIS,
14   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15   * See the License for the specific language governing permissions and
16   * limitations under the License.
17   */
18  
19  package org.springframework.extensions.webscripts.ui.common;
20  
21  import java.io.IOException;
22  import java.util.Iterator;
23  import java.util.Map;
24  
25  import javax.faces.component.NamingContainer;
26  import javax.faces.component.UIComponent;
27  import javax.faces.component.UIForm;
28  import javax.faces.context.FacesContext;
29  import javax.faces.context.ResponseWriter;
30  import javax.faces.el.EvaluationException;
31  import javax.faces.el.MethodBinding;
32  import javax.faces.event.AbortProcessingException;
33  import javax.faces.event.ActionEvent;
34  
35  import org.apache.myfaces.shared_impl.renderkit.html.HtmlFormRendererBase;
36  
37  /**
38   * Class containing misc helper methods for managing JSF Components.
39   * 
40   * NOTE: Extracted from org.alfresco.web.ui.common.Utils;
41   * 
42   * @author Kevin Roast
43   */
44  public final class JSFUtils
45  {
46     
47     /**
48      * Private constructor
49      */
50     private JSFUtils()
51     {
52     }
53     
54     /**
55      * Helper to output an attribute to the output stream
56      * 
57      * @param out        ResponseWriter
58      * @param attr       attribute value object (cannot be null)
59      * @param mapping    mapping to output as e.g. style="..."
60      * 
61      * @throws IOException
62      */
63     public static void outputAttribute(ResponseWriter out, Object attr, String mapping)
64        throws IOException
65     {
66        if (attr != null)
67        {
68           out.write(' ');
69           out.write(mapping);
70           out.write("=\"");
71           out.write(attr.toString());
72           out.write('"');
73        }
74     }
75     
76     /**
77      * Get the hidden field name for any action component.
78      * 
79      * All components that wish to simply encode a form value with their client ID can reuse the same
80      * hidden field within the parent form. NOTE: components which use this method must only encode
81      * their client ID as the value and nothing else!
82      * 
83      * Build a shared field name from the parent form name and the string "act".
84      * 
85      * @return hidden field name shared by all action components within the Form.
86      */
87     public static String getActionHiddenFieldName(FacesContext context, UIComponent component)
88     {
89        return JSFUtils.getParentForm(context, component).getClientId(context) + NamingContainer.SEPARATOR_CHAR + "act";
90     }
91     
92     /**
93      * Helper to recursively render a component and it's child components
94      * 
95      * @param context    FacesContext
96      * @param component  UIComponent
97      * 
98      * @throws IOException
99      */
100    public static void encodeRecursive(FacesContext context, UIComponent component)
101       throws IOException
102    {
103       if (component.isRendered() == true)
104       {
105          component.encodeBegin(context);
106          
107          // follow the spec for components that render their children
108          if (component.getRendersChildren() == true)
109          {
110             component.encodeChildren(context);
111          }
112          else
113          {
114             if (component.getChildCount() != 0)
115             {
116                for (Iterator i=component.getChildren().iterator(); i.hasNext(); /**/)
117                {
118                   encodeRecursive(context, (UIComponent)i.next());
119                }
120             }
121          }
122          
123          component.encodeEnd(context);
124       }
125    }
126    
127    /**
128     * Generate the JavaScript to submit set the specified hidden Form field to the
129     * supplied value and submit the parent Form.
130     * 
131     * NOTE: the supplied hidden field name is added to the Form Renderer map for output.
132     * 
133     * @param context       FacesContext
134     * @param component     UIComponent to generate JavaScript for
135     * @param fieldId       Hidden field id to set value for
136     * @param fieldValue    Hidden field value to set hidden field too on submit
137     * 
138     * @return JavaScript event code
139     */
140    public static String generateFormSubmit(FacesContext context, UIComponent component, String fieldId, String fieldValue)
141    {
142       return generateFormSubmit(context, component, fieldId, fieldValue, false, null);
143    }
144    
145    /**
146     * Generate the JavaScript to submit set the specified hidden Form field to the
147     * supplied value and submit the parent Form.
148     * 
149     * NOTE: the supplied hidden field name is added to the Form Renderer map for output.
150     * 
151     * @param context       FacesContext
152     * @param component     UIComponent to generate JavaScript for
153     * @param fieldId       Hidden field id to set value for
154     * @param fieldValue    Hidden field value to set hidden field too on submit
155     * @param params        Optional map of param name/values to output
156     * 
157     * @return JavaScript event code
158     */
159    public static String generateFormSubmit(FacesContext context, UIComponent component, String fieldId, 
160          String fieldValue, Map<String, String> params)
161    {
162       return generateFormSubmit(context, component, fieldId, fieldValue, false, params);
163    }
164       
165    /**
166     * Generate the JavaScript to submit set the specified hidden Form field to the
167     * supplied value and submit the parent Form.
168     * 
169     * NOTE: the supplied hidden field name is added to the Form Renderer map for output.
170     * 
171     * @param context       FacesContext
172     * @param component     UIComponent to generate JavaScript for
173     * @param fieldId       Hidden field id to set value for
174     * @param fieldValue    Hidden field value to set hidden field too on submit
175     * @param valueIsParam  Determines whether the fieldValue parameter should be treated
176     *                      as a parameter in the generated JavaScript, false will treat
177     *                      the value i.e. surround it with single quotes
178     * @param params        Optional map of param name/values to output
179     * 
180     * @return JavaScript event code
181     */
182    public static String generateFormSubmit(FacesContext context, UIComponent component, String fieldId, 
183          String fieldValue, boolean valueIsParam, Map<String, String> params)
184    {
185       UIForm form = JSFUtils.getParentForm(context, component);
186       if (form == null)
187       {
188          throw new IllegalStateException("Must nest components inside UIForm to generate form submit!");
189       }
190       
191       String formClientId = form.getClientId(context);
192       
193       StringBuilder buf = new StringBuilder(200);
194       buf.append("document.forms['");
195       buf.append(formClientId);
196       buf.append("']['");
197       buf.append(fieldId);
198       buf.append("'].value=");
199       if (valueIsParam == false)
200       {
201          buf.append("'");
202       }
203       buf.append(fieldValue);
204       if (valueIsParam == false)
205       {
206          buf.append("'");
207       }
208       buf.append(";");
209       
210       if (params != null)
211       {
212          for (String name : params.keySet())
213          {
214             buf.append("document.forms['");
215             buf.append(formClientId);
216             buf.append("']['");
217             buf.append(name);
218             buf.append("'].value='");
219             buf.append(StringUtils.replace(params.get(name), "'", "\\'"));
220             buf.append("';");
221             
222             // weak, but this seems to be the way Sun RI do it...
223             //FormRenderer.addNeededHiddenField(context, name);
224             HtmlFormRendererBase.addHiddenCommandParameter(context, form, name);
225          }
226       }
227       
228       buf.append("document.forms['");
229       buf.append(formClientId);
230       buf.append("'].submit();");
231       
232       if (valueIsParam == false)
233       {
234          buf.append("return false;");
235       }
236       
237       // weak, but this seems to be the way Sun RI do it...
238       //FormRenderer.addNeededHiddenField(context, fieldId);
239       HtmlFormRendererBase.addHiddenCommandParameter(context, form, fieldId);
240       
241       return buf.toString();
242    }
243    
244    /**
245     * Generate the JavaScript to submit the parent Form.
246     * 
247     * @param context       FacesContext
248     * @param component     UIComponent to generate JavaScript for
249     * 
250     * @return JavaScript event code
251     */
252    public static String generateFormSubmit(FacesContext context, UIComponent component)
253    {
254       UIForm form = JSFUtils.getParentForm(context, component);
255       if (form == null)
256       {
257          throw new IllegalStateException("Must nest components inside UIForm to generate form submit!");
258       }
259       
260       String formClientId = form.getClientId(context);
261       
262       StringBuilder buf = new StringBuilder(48);
263       
264       buf.append("document.forms['");
265       buf.append(formClientId);
266       buf.append("'].submit()");
267       
268       buf.append(";return false;");
269       
270       return buf.toString();
271    }
272    
273    /**
274     * Build a context path safe image tag for the supplied image path.
275     * Image path should be supplied with a leading slash '/'.
276     * 
277     * @param context       FacesContext
278     * @param image         The local image path from the web folder with leading slash '/'
279     * @param width         Width in pixels
280     * @param height        Height in pixels
281     * @param alt           Optional alt/title text
282     * @param onclick       JavaScript onclick event handler code
283     * 
284     * @return Populated <code>img</code> tag
285     */
286    public static String buildImageTag(FacesContext context, String image, int width, int height,
287          String alt, String onclick)
288    {
289       return buildImageTag(context, image, width, height, alt, onclick, null);
290    }
291    
292    /**
293     * Build a context path safe image tag for the supplied image path.
294     * Image path should be supplied with a leading slash '/'.
295     * 
296     * @param context       FacesContext
297     * @param image         The local image path from the web folder with leading slash '/'
298     * @param width         Width in pixels
299     * @param height        Height in pixels
300     * @param alt           Optional alt/title text
301     * @param onclick       JavaScript onclick event handler code
302     * @param verticalAlign         Optional HTML alignment value
303     * 
304     * @return Populated <code>img</code> tag
305     */
306    public static String buildImageTag(FacesContext context, String image, int width, int height,
307                                       String alt, String onclick, String verticalAlign)
308    {
309       StringBuilder buf = new StringBuilder(200);
310       
311       String style = "border-width:0px;";
312       buf.append("<img src='")
313          .append(context.getExternalContext().getRequestContextPath())
314          .append(image)
315          .append("' width='")
316          .append(width)
317          .append("' height='")
318          .append(height)
319          .append("'");
320       
321       if (alt != null)
322       {
323          alt = StringUtils.encode(alt);
324          buf.append(" alt='")
325             .append(alt)
326             .append("' title='")
327             .append(alt)
328             .append("'");
329       }
330       else
331       {
332          buf.append(" alt=''");
333       }
334 
335       if (verticalAlign != null)
336       {
337          StringBuilder styleBuf = new StringBuilder(40);
338          styleBuf.append(style).append("vertical-align:").append(verticalAlign).append(";");
339          style = styleBuf.toString();
340       }
341       
342       if (onclick != null)
343       {
344          buf.append(" onclick=\"").append(onclick).append('"');
345          StringBuilder styleBuf = new StringBuilder(style.length() + 16);
346          styleBuf.append(style).append("cursor:pointer;");
347          style = styleBuf.toString();
348       }
349       buf.append(" style='").append(style).append("'/>");
350       
351       return buf.toString();
352    }
353    
354    /**
355     * Build a context path safe image tag for the supplied image path.
356     * Image path should be supplied with a leading slash '/'.
357     * 
358     * @param context       FacesContext
359     * @param image         The local image path from the web folder with leading slash '/'
360     * @param width         Width in pixels
361     * @param height        Height in pixels
362     * @param alt           Optional alt/title text
363     * 
364     * @return Populated <code>img</code> tag
365     */
366    public static String buildImageTag(FacesContext context, String image, int width, int height, String alt)
367    {
368       return buildImageTag(context, image, width, height, alt, null);
369    }
370    
371    /**
372     * Build a context path safe image tag for the supplied image path.
373     * Image path should be supplied with a leading slash '/'.
374     * 
375     * @param context       FacesContext
376     * @param image         The local image path from the web folder with leading slash '/'
377     * @param alt           Optional alt/title text
378     * 
379     * @return Populated <code>img</code> tag
380     */
381    public static String buildImageTag(FacesContext context, String image, String alt)
382    {
383       return buildImageTag(context, image, alt, null);
384    }
385    
386    /**
387     * Build a context path safe image tag for the supplied image path.
388     * Image path should be supplied with a leading slash '/'.
389     * 
390     * @param context       FacesContext
391     * @param image         The local image path from the web folder with leading slash '/'
392     * @param alt           Optional alt/title text
393     * @param verticalAlign         Optional HTML alignment value
394     * 
395     * @return Populated <code>img</code> tag
396     */
397    public static String buildImageTag(FacesContext context, String image, String alt, String verticalAlign)
398    {
399       StringBuilder buf = new StringBuilder(128);
400       buf.append("<img src='")
401          .append(context.getExternalContext().getRequestContextPath())
402          .append(image)
403          .append("' ");
404 
405       String style = "border-width:0px;";
406       if (alt != null)
407       {
408          alt = StringUtils.encode(alt);
409          buf.append(" alt='")
410             .append(alt)
411             .append("' title='")
412             .append(alt)
413             .append("'");
414       }
415       else
416       {
417          buf.append(" alt=''");
418       }
419 
420       if (verticalAlign != null)
421       {
422          StringBuilder styleBuf = new StringBuilder(40);
423          styleBuf.append(style).append("vertical-align:").append(verticalAlign).append(";");
424          style = styleBuf.toString();
425       }
426       
427       buf.append(" style='").append(style).append("'/>");
428       
429       return buf.toString();
430    }
431    
432    /**
433     * Return the parent UIForm component for the specified UIComponent
434     * 
435     * @param context       FaceContext
436     * @param component     The UIComponent to find parent Form for
437     * 
438     * @return UIForm parent or null if none found in hiearachy
439     */
440    public static UIForm getParentForm(FacesContext context, UIComponent component)
441    {
442       UIComponent parent = component.getParent();
443       while (parent != null)
444       {
445          if (parent instanceof UIForm)
446          {
447             break;
448          }
449          parent = parent.getParent();
450       }
451       return (UIForm)parent;
452    }
453    
454    /**
455     * Return the parent UIComponent implementing the NamingContainer interface for
456     * the specified UIComponent.
457     * 
458     * @param context       FaceContext
459     * @param component     The UIComponent to find parent Form for
460     * 
461     * @return NamingContainer parent or null if none found in hiearachy
462     */
463    public static UIComponent getParentNamingContainer(FacesContext context, UIComponent component)
464    {
465       UIComponent parent = component.getParent();
466       while (parent != null)
467       {
468          if (parent instanceof NamingContainer)
469          {
470             break;
471          }
472          parent = parent.getParent();
473       }
474       return (UIComponent)parent;
475    }
476    
477    /**
478     * Determines whether the given component is disabled or readonly
479     * 
480     * @param component The component to test
481     * @return true if the component is either disabled or set to readonly
482     */
483    public static boolean isComponentDisabledOrReadOnly(UIComponent component)
484    {
485       boolean disabled = false;
486       boolean readOnly = false;
487       
488       Object disabledAttr = component.getAttributes().get("disabled");
489       if (disabledAttr != null)
490       {
491          disabled = disabledAttr.equals(Boolean.TRUE);
492       }
493       
494       if (disabled == false)
495       {
496          Object readOnlyAttr = component.getAttributes().get("readonly");
497          if (readOnlyAttr != null)
498          {
499             readOnly = readOnlyAttr.equals(Boolean.TRUE);
500          }
501       }
502 
503       return disabled || readOnly;
504    }
505    
506    /**
507     * Invoke the method encapsulated by the supplied MethodBinding
508     * 
509     * @param context    FacesContext
510     * @param method     MethodBinding to invoke
511     * @param event      ActionEvent to pass to the method of signature:
512     *                   public void myMethodName(ActionEvent event)
513     */
514    public static void processActionMethod(FacesContext context, MethodBinding method, ActionEvent event)
515    {
516       try
517       {
518          method.invoke(context, new Object[] {event});
519       }
520       catch (EvaluationException e)
521       {
522          Throwable cause = e.getCause();
523          if (cause instanceof AbortProcessingException)
524          {
525             throw (AbortProcessingException)cause;
526          }
527          else
528          {
529             throw e;
530          }
531       }   
532    }
533    
534 }