View Javadoc
1   package org.andromda.cartridges.jsf.component;
2   
3   import java.io.IOException;
4   import java.util.ArrayList;
5   import java.util.Collection;
6   import java.util.Iterator;
7   import java.util.LinkedHashMap;
8   import java.util.List;
9   import java.util.Locale;
10  import java.util.Map;
11  import javax.faces.component.EditableValueHolder;
12  import javax.faces.component.UIComponent;
13  import javax.faces.component.UIComponentBase;
14  import javax.faces.context.FacesContext;
15  import javax.faces.context.ResponseWriter;
16  import javax.faces.validator.Validator;
17  import org.andromda.cartridges.jsf.utils.ComponentUtils;
18  import org.andromda.cartridges.jsf.validator.JSFValidator;
19  import org.andromda.cartridges.jsf.validator.JSFValidatorException;
20  import org.andromda.cartridges.jsf.validator.ValidatorMessages;
21  import org.andromda.utils.StringUtilsHelper;
22  import org.apache.commons.lang.StringUtils;
23  import org.apache.commons.logging.Log;
24  import org.apache.commons.logging.LogFactory;
25  import org.apache.commons.validator.Arg;
26  import org.apache.commons.validator.Field;
27  import org.apache.commons.validator.Form;
28  import org.apache.commons.validator.ValidatorAction;
29  import org.apache.commons.validator.ValidatorResources;
30  
31  /**
32   * A JSF component that enabled the commons-validator server side validation, as well
33   * as encodes JavaScript for all client-side validations
34   * specified in the same JSP page (with <code>jsf:validator</code>.
35   */
36  public class JSFValidatorComponent
37      extends UIComponentBase
38  {
39      private static final Log logger = LogFactory.getLog(JSFValidatorComponent.class);
40  
41      /**
42       *
43       */
44      public JSFValidatorComponent()
45      {
46          // - default constructor for faces-config.xml
47      }
48  
49      /**
50       * A map of validators, representing all of the Commons Validators attached
51       * to components in the current component hierarchy. The keys of the map are
52       * validator type names. The values are maps from IDs to JSFValidator
53       * objects.
54       */
55      private Map validators = new LinkedHashMap();
56  
57      /**
58       * The component renders itself; therefore, this method returns null.
59       * @return null
60       */
61      public String getRendererType()
62      {
63          return null;
64      }
65  
66      /**
67       * Returns the component's family. In this case, the component is not
68       * associated with a family, so this method returns null.
69       * @return null
70       */
71      public String getFamily()
72      {
73          return null;
74      }
75  
76      /**
77       * Registers a validator according to type and id.
78       *
79       * @param type The type of the validator
80       * @param id The validator's identifier
81       * @param validator The JSF validator associated with the id and type
82       */
83      private void addValidator(
84          final String type,
85          final String id,
86          final JSFValidator validator)
87      {
88          Map map = (Map)this.validators.get(type);
89          if (map == null)
90          {
91              map = new LinkedHashMap();
92              this.validators.put(
93                  type,
94                  map);
95          }
96          if (id != null)
97          {
98              map.put(
99                  id,
100                 validator);
101         }
102     }
103 
104     private Object getContextAttribute(final String attributeName)
105     {
106         return ComponentUtils.getAttribute(FacesContext.getCurrentInstance()
107             .getExternalContext().getContext(), attributeName);
108     }
109 
110     private void setContextAttribute(final String attributeName, final String attributeValue)
111     {
112         ComponentUtils.setAttribute(FacesContext.getCurrentInstance()
113             .getExternalContext().getContext(), attributeName, attributeValue);
114     }
115 
116     /**
117      * <p>
118      * Recursively finds all Commons validators for the all of the components in
119      * a component hierarchy and adds them to a map.
120      * </p>
121      * If a validator's type is required, this method sets the associated
122      * component's required property to true. This is necessary because JSF does
123      * not validate empty fields unless a component's required property is true.
124      *
125      * @param component The component at the root of the component tree
126      * @param context The FacesContext for this request
127      * @param form the id of the form.
128      */
129     private void findValidators(
130         final UIComponent component,
131         final FacesContext context,
132         final UIComponent form)
133     {
134         if (component instanceof EditableValueHolder && this.canValidate(component))
135         {
136             final EditableValueHolder valueHolder = (EditableValueHolder)component;
137             if (form != null)
138             {
139                 final String formId = form.getId();
140                 final String componentId = component.getId();
141                 final ValidatorResources resources = JSFValidator.getValidatorResources();
142                 if (resources != null)
143                 {
144                     final Form validatorForm = resources.getForm(
145                             Locale.getDefault(),
146                             formId);
147                     if (validatorForm != null)
148                     {
149                         final List validatorFields = validatorForm.getFields();
150                         for (final Iterator iterator = validatorFields.iterator(); iterator.hasNext();)
151                         {
152                             final Field field = (Field)iterator.next();
153 
154                             // we need to make it match the name of the id on the jsf components (if its nested).
155                             final String fieldProperty = StringUtilsHelper.lowerCamelCaseName(field.getProperty());
156                             if (componentId.equals(fieldProperty))
157                             {
158                                 for (final Iterator dependencyIterator = field.getDependencyList().iterator();
159                                     dependencyIterator.hasNext();)
160                                 {
161                                     final String dependency = (String)dependencyIterator.next();
162                                     final ValidatorAction action = JSFValidator.getValidatorAction(dependency);
163                                     if (action != null)
164                                     {
165                                         final JSFValidator validator = new JSFValidator(formId, action);
166                                         final Arg[] args = field.getArgs(dependency);
167                                         if (args != null)
168                                         {
169                                             for (final Iterator varIterator = field.getVars().keySet().iterator(); varIterator.hasNext();)
170                                             {
171                                                 final String name = (String)varIterator.next();
172                                                 validator.addParameter(
173                                                     name,
174                                                     field.getVarValue(name));
175                                             }
176                                             validator.setArgs(ValidatorMessages.getArgs(
177                                                     dependency,
178                                                     field));
179                                             this.addValidator(
180                                                 dependency,
181                                                 component.getClientId(context),
182                                                 validator);
183                                             if (!this.validatorPresent(valueHolder, validator))
184                                             {
185                                                 valueHolder.addValidator(validator);
186                                             }
187                                         }
188                                     }
189                                     else
190                                     {
191                                         logger.error(
192                                             "No validator action with name '" + dependency +
193                                             "' registered in rules files '" + JSFValidator.RULES_LOCATION + '\'');
194                                     }
195                                 }
196                             }
197                         }
198                     }
199                 }
200             }
201         }
202         for (final Iterator iterator = component.getFacetsAndChildren(); iterator.hasNext();)
203         {
204             final UIComponent childComponent = (UIComponent)iterator.next();
205             this.findValidators(
206                 childComponent,
207                 context,
208                 form);
209         }
210     }
211 
212     /**
213      * Indicates whether or not this component can be validated.
214      *
215      * @param component the component to check.
216      * @return canValidate true/false
217      */
218     private boolean canValidate(final UIComponent component)
219     {
220         boolean canValidate = true;
221         if (component != null)
222         {
223             canValidate = component.isRendered();
224             if (canValidate)
225             {
226                 final UIComponent parent = component.getParent();
227                 if (parent != null)
228                 {
229                     canValidate = canValidate(parent);
230                 }
231             }
232         }
233         return canValidate;
234     }
235 
236     /**
237      * Indicates whether or not the JSFValidator instance is present and if so returns true.
238      *
239      * @param valueHolder the value holder on which to check if its present.
240      * @param validator
241      * @return true/false
242      */
243     private boolean validatorPresent(EditableValueHolder valueHolder, final Validator validator)
244     {
245         boolean present = false;
246         if (validator != null)
247         {
248             final Validator[] validators = valueHolder.getValidators();
249             if (validators != null)
250             {
251                 for (int ctr = 0; ctr < validators.length; ctr++)
252                 {
253                     final Validator test = validators[ctr];
254                     if (test instanceof JSFValidator)
255                     {
256                         present = test.toString().equals(validator.toString());
257                         if (present)
258                         {
259                             break;
260                         }
261                     }
262                 }
263             }
264         }
265         return present;
266     }
267 
268     private static final String JAVASCRIPT_UTILITIES = "javascriptUtilities";
269 
270     /**
271      * Write the start of the script for client-side validation.
272      *
273      * @param writer A response writer
274      */
275     private final void writeScriptStart(final FacesContext context, UIComponent component)
276         throws IOException
277     {
278         final ResponseWriter writer = context.getResponseWriter();
279         String id = component.getClientId(context);
280         writer.startElement(
281             "script",
282             component);
283         writer.writeAttribute(
284             "type",
285             "text/javascript",
286             null);
287         writer.writeAttribute("id", id + ":validation-code", null);
288     }
289 
290     /**
291      * Write the end of the script for client-side validation.
292      *
293      * @param writer A response writer
294      */
295     private void writeScriptEnd(ResponseWriter writer)
296         throws IOException
297     {
298         writer.endElement("script");
299     }
300 
301     /**
302      * Returns the name of the JavaScript function, specified in the JSP page that validates this JSP page's form.
303      *
304      * @param action the validation action from which to retrieve the function name.
305      */
306     private String getJavaScriptFunctionName(final ValidatorAction action)
307     {
308         String functionName = null;
309         final String javascript = action.getJavascript();
310         if (StringUtils.isNotBlank(javascript))
311         {
312             final String function = "function ";
313             int functionIndex = javascript.indexOf(function);
314             functionName = javascript.substring(functionIndex + 9);
315             functionName = functionName.substring(
316                     0,
317                     functionName.indexOf('(')).replaceAll(
318                     "[\\s]+",
319                     " ");
320         }
321         return functionName;
322     }
323 
324     /**
325      * The attribute storing whether or not client-side validation
326      * shall performed.
327      */
328     public static final String CLIENT = "client";
329 
330     /**
331      * Sets whether or not client-side validation shall be performed.
332      *
333      * @param functionName String
334      */
335     public void setClient(final String functionName)
336     {
337         this.getAttributes().put(CLIENT, functionName);
338     }
339 
340     /**
341      * Gets whether or not client side validation shall be performed.
342      *
343      * @return true/false
344      */
345     private boolean isClient()
346     {
347         String client = (String)this.getAttributes().get(CLIENT);
348         return StringUtils.isBlank(client) ? true : Boolean.valueOf(client).booleanValue();
349     }
350 
351     /**
352      * writes the javascript functions to the response.
353      *
354      * @param form UIComponent
355      * @param writer A response writer
356      * @param context The FacesContext for this request
357      * @throws IOException
358      */
359     private final void writeValidationFunctions(
360         final UIComponent form,
361         final ResponseWriter writer,
362         final FacesContext context)
363         throws IOException
364     {
365         writer.write("var bCancel = false;\n");
366         writer.write("self.validate" + StringUtils.capitalize(form.getId()) + " = ");
367         writer.write("function(form) { return bCancel || true\n");
368 
369         // - for each validator type, write "&& fun(form);
370         final Collection validatorTypes = new ArrayList(this.validators.keySet());
371 
372         // - remove any validators that don't have javascript functions defined.
373         for (final Iterator iterator = validatorTypes.iterator(); iterator.hasNext();)
374         {
375             final String type = (String)iterator.next();
376             final ValidatorAction action = JSFValidator.getValidatorAction(type);
377             final String functionName = this.getJavaScriptFunctionName(action);
378             if (StringUtils.isBlank(functionName))
379             {
380                 iterator.remove();
381             }
382         }
383 
384         for (final Iterator iterator = validatorTypes.iterator(); iterator.hasNext();)
385         {
386             final String type = (String)iterator.next();
387             final ValidatorAction action = JSFValidator.getValidatorAction(type);
388             if (!JAVASCRIPT_UTILITIES.equals(type))
389             {
390                 writer.write("&& ");
391                 writer.write(this.getJavaScriptFunctionName(action));
392                 writer.write("(form)\n");
393             }
394         }
395         writer.write(";}\n");
396 
397         // - for each validator type, write callback
398         for (final Iterator iterator = validatorTypes.iterator(); iterator.hasNext();)
399         {
400             final String type = (String)iterator.next();
401             final ValidatorAction action = JSFValidator.getValidatorAction(type);
402             String callback = action.getJsFunctionName();
403             if (StringUtils.isBlank(callback))
404             {
405                 callback = type;
406             }
407             writer.write("function ");
408             writer.write(form.getId() + '_' + callback);
409             writer.write("() { \n");
410 
411             // for each field validated by this type, add configuration object
412             final Map map = (Map)this.validators.get(type);
413             int ctr = 0;
414             for (final Iterator idIterator = map.keySet().iterator(); idIterator.hasNext(); ctr++)
415             {
416                 final String id = (String)idIterator.next();
417                 final JSFValidator validator = (JSFValidator)map.get(id);
418                 writer.write("this[" + ctr + "] = ");
419                 this.writeJavaScriptParams(
420                     writer,
421                     context,
422                     id,
423                     validator);
424                 writer.write(";\n");
425             }
426             writer.write("}\n");
427         }
428 
429         // - for each validator type, write code
430         for (final Iterator iterator = validatorTypes.iterator(); iterator.hasNext();)
431         {
432             final String type = (String)iterator.next();
433             final ValidatorAction action = JSFValidator.getValidatorAction(type);
434             writer.write(action.getJavascript());
435             writer.write("\n");
436         }
437     }
438 
439     /**
440      * Writes the JavaScript parameters for the client-side validation code.
441      *
442      * @param writer A response writer
443      * @param context The FacesContext for this request
444      * @param id String
445      * @param validator The Commons validator
446      */
447     private void writeJavaScriptParams(
448         final ResponseWriter writer,
449         final FacesContext context,
450         final String id,
451         final JSFValidator validator)
452         throws IOException
453     {
454         writer.write("new Array(\"");
455         writer.write(id);
456         writer.write("\", \"");
457         writer.write(validator.getErrorMessage(context));
458         writer.write("\", new Function(\"x\", \"return {");
459         final Map parameters = validator.getParameters();
460         for (final Iterator iterator = parameters.keySet().iterator(); iterator.hasNext();)
461         {
462             final String name = (String)iterator.next();
463             writer.write(name);
464             writer.write(":");
465             boolean mask = "mask".equals(name);
466 
467             // - mask validator does not construct regular expression
468             if (mask)
469             {
470                 writer.write("/");
471             }
472             else
473             {
474                 writer.write("'");
475             }
476             final Object parameter = parameters.get(name);
477             writer.write(parameter.toString());
478             if (mask)
479             {
480                 writer.write("/");
481             }
482             else
483             {
484                 writer.write("'");
485             }
486             if (iterator.hasNext())
487             {
488                 writer.write(",");
489             }
490         }
491         writer.write("}[x];\"))");
492     }
493 
494     /**
495      * Stores all forms found within this view.
496      */
497     private final Collection forms = new ArrayList();
498 
499     private UIComponent findForm(final String id)
500     {
501         UIComponent form = null;
502         UIComponent validator = null;
503         try
504         {
505             validator = this.findComponent(id);
506         }
507         catch (NullPointerException exception)
508         {
509             // ignore - means we couldn't find the component
510         }
511         if (validator instanceof JSFValidatorComponent)
512         {
513             final UIComponent parent = validator.getParent();
514             // When would parent ever NOT be an instance of UIComponent?
515             if (parent instanceof UIComponent)
516             {
517                 form = parent;
518             }
519         }
520         return form;
521     }
522 
523     /**
524      * Used to keep track of whether or not the validation rules are present or not.
525      */
526     private static final String RULES_NOT_PRESENT = "validationRulesNotPresent";
527 
528     /**
529      * Begin encoding for this component. This method finds all Commons
530      * validators attached to components in the current component hierarchy and
531      * writes out JavaScript code to invoke those validators, in turn.
532      *
533      * @param context The FacesContext for this request
534      * @throws IOException
535      */
536     public void encodeBegin(final FacesContext context)
537         throws IOException
538     {
539         boolean validationResourcesPresent = this.getContextAttribute(RULES_NOT_PRESENT) == null;
540         if (validationResourcesPresent && JSFValidator.getValidatorResources() == null)
541         {
542             this.setContextAttribute(
543                 RULES_NOT_PRESENT,
544                 "true");
545             validationResourcesPresent = false;
546         }
547         if (validationResourcesPresent)
548         {
549             try
550             {
551                 this.validators.clear();
552                 this.forms.clear();
553                 // - add the javascript utilities each time
554                 this.addValidator(
555                     JAVASCRIPT_UTILITIES,
556                     null,
557                     null);
558                 final UIComponent form = this.findForm(this.getId());
559                 if (form != null)
560                 {
561                     this.findValidators(
562                         form,
563                         context,
564                         form);
565                     if (this.isClient())
566                     {
567                         final ResponseWriter writer = context.getResponseWriter();
568                         this.writeScriptStart(context, form);
569                         this.writeValidationFunctions(
570                             form,
571                             writer,
572                             context);
573                         this.writeScriptEnd(writer);
574                     }
575                 }
576             }
577             catch (final JSFValidatorException exception)
578             {
579                 logger.error(exception);
580             }
581         }
582     }
583 }