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  package org.apache.shale.validator.faces;
19  
20  import java.io.IOException;
21  import java.util.ArrayList;
22  import java.util.Iterator;
23  import java.util.LinkedHashMap;
24  import java.util.List;
25  import java.util.Map;
26  import java.util.Set;
27  import javax.faces.component.EditableValueHolder;
28  import javax.faces.component.UIComponent;
29  import javax.faces.component.UIComponentBase;
30  import javax.faces.component.UIForm;
31  import javax.faces.component.UINamingContainer;
32  import javax.faces.context.FacesContext;
33  import javax.faces.context.ResponseWriter;
34  import javax.faces.el.ValueBinding;
35  import org.apache.commons.validator.ValidatorAction;
36  import org.apache.commons.validator.Var;
37  import org.apache.shale.util.Tags;
38  import org.apache.shale.validator.CommonsValidator;
39  
40  /***
41   * <p>A JSF component that encodes JavaScript for
42   *    all client-side validations specified in the same
43   *    JSP page (with <code>s:commonsValidator</code>.
44   *
45   * $Id: ValidatorScript.java 464373 2006-10-16 04:21:54Z rahul $
46   */
47  public class ValidatorScript extends UIComponentBase {
48  
49  
50      // -------------------------------------------------------- Private Variables
51  
52  
53      /***
54       * <p>The value of the <code>functionName</code> property.</p>
55       */
56      private String functionName;
57  
58  
59      /***
60       * <p>A map of validators, representing all of the Commons Validators
61       *    attached to components in the current component hierarchy.
62       *    The keys of the map are validator type names. The values are
63       *    maps from IDs to CommonsValidator objects.</p>
64       */
65      private Map validators = new LinkedHashMap();
66  
67  
68      /***
69       * <p>Holds vars loaded with EL that are evaluated at render time.
70       * The key into the map is the component's client id.</p>
71       */
72      private Map validatorVars = new LinkedHashMap();
73  
74  
75      /***
76       * <p>The component renders itself; therefore, this
77       *    method returns null.</p>
78       *
79       * @return <code>null</code> component handles renderering
80       */
81      public String getRendererType() {
82          return null;
83      }
84  
85  
86      /***
87       * <p>Returns the component's family. In this case,
88       *    the component is not associated with a family,
89       *    so this method returns null.</p>
90       *
91       * @return component's family
92       */
93      public String getFamily() {
94          return null;
95      }
96  
97  
98      /***
99       * <p>Return the value of the <code>functionName</code> property.</p>
100      *
101      * @return callback function from the form's onsubmit.
102      */
103     public String getFunctionName() {
104 
105         if (this.functionName != null) {
106             return functionName;
107         }
108         ValueBinding _vb = getValueBinding("functionName");
109         if (_vb != null) {
110             return (String) _vb.getValue(getFacesContext());
111         } else {
112             return null;
113         }
114 
115     }
116 
117 
118     /***
119      * <p>Set the value of the <code>functionName</code> property.</p>
120      *
121      * @param functionName The new function name
122      */
123     public void setFunctionName(String functionName) {
124         this.functionName = functionName;
125     }
126 
127 
128     /***
129      * <p>Restore the state of this component.</p>
130      *
131      * @param context FacesContext for the current request
132      * @param state State to be restored
133      */
134     public void restoreState(FacesContext context, Object state) {
135 
136         Object values[] = (Object[]) state;
137         super.restoreState(context, values[0]);
138         this.functionName = (String) values[1];
139 
140     }
141 
142 
143     /***
144      * <p>Save the state of this component.</p>
145      *
146      * @param context FacesContext for the current request
147      * @return component's state
148      */
149     public Object saveState(FacesContext context) {
150 
151         Object values[] = new Object[2];
152         values[0] = super.saveState(context);
153         values[1] = this.functionName;
154         return values;
155 
156     }
157 
158 
159     /***
160      * <p>Restructures the validator hierarchy grouping by
161      * form name, type and id.</p>
162      *
163      * @return validators grouped by form, type and clientId
164      */
165     private Map getValidatorsGroupByFormName() {
166 
167         Map formValidators = new LinkedHashMap();
168 
169         Iterator vi = validators.entrySet().iterator();
170         while (vi.hasNext()) {
171 
172             Map.Entry typeEntry = (Map.Entry) vi.next();
173             Map typeMap = (Map) typeEntry.getValue();
174 
175             String type = (String) typeEntry.getKey();
176 
177             Iterator ti = typeMap.entrySet().iterator();
178             while (ti.hasNext()) {
179 
180                 Map.Entry idEntry = (Map.Entry) ti.next();
181                 String id = (String) idEntry.getKey();
182                 CommonsValidator v = (CommonsValidator) idEntry.getValue();
183                 String formName = v.getFormName();
184 
185                 Map formTypeMap = (Map) formValidators.get(formName);
186                 if (formTypeMap == null) {
187                     formTypeMap = new LinkedHashMap();
188                     formValidators.put(formName, formTypeMap);
189                 }
190 
191                 Map formTypeIdMap = (Map) formTypeMap.get(type);
192                 if (formTypeIdMap == null) {
193                     formTypeIdMap = new LinkedHashMap();
194                     formTypeMap.put(type, formTypeIdMap);
195                 }
196 
197                 formTypeIdMap.put(id, v);
198             }
199 
200         }
201 
202         return formValidators;
203 
204     }
205 
206 
207     /***
208      * <p>Registers a validator according to type and id.</p>
209      *
210      * @param type The type of the validator
211      * @param id The validator's identifier
212      * @param v The Commons validator associated with the id and type
213      */
214     private void addValidator(String type, String id, CommonsValidator v) {
215         // look for validators organized by type
216         Map map = (Map) validators.get(type);
217         if (map == null) {
218             map = new LinkedHashMap();
219             validators.put(type, map);
220         }
221         if (id != null) {
222              map.put(id, v);
223         }
224    }
225 
226 
227     /***
228      * <p>Recursively finds all Commons validators for the all of the
229      *    components in a component hierarchy and adds them to a map.</p>
230      * <p>If a validator's type is required, this method sets the
231      *    associated component's required property to true. This is
232      *    necessary because JSF does not validate empty fields unless
233      *    a component's required property is true.</p>
234      *
235      * @param c The component at the root of the component tree
236      * @param context The FacesContext for this request
237      */
238    private void findCommonsValidators(UIComponent c, FacesContext context) {
239       if (c instanceof EditableValueHolder && c.isRendered()) {
240          EditableValueHolder h = (EditableValueHolder) c;
241          javax.faces.validator.Validator[] vs = h.getValidators();
242          for (int i = 0; i < vs.length; i++) {
243             if (vs[i] instanceof CommonsValidator) {
244                CommonsValidator v = (CommonsValidator) vs[i];
245                v.setFormName(findForm(context, c));
246                if (Boolean.TRUE.equals(v.getClient())) {
247 
248                    //look for the clientId set
249                    Map clientIds = (Map) c.getAttributes().get(ValidatorInputRenderer.VALIDATOR_CLIENTIDS_ATTR);
250                    if (clientIds != null) {
251 
252                       validatorVars.putAll(clientIds);
253                       Iterator ci = clientIds.entrySet().iterator();
254                       while (ci.hasNext()) {
255                          Map.Entry e = (Map.Entry) ci.next();
256 
257                          String id = (String) e.getKey();
258                          addValidator(v.getType(), id, v);
259 
260                          ValidatorAction action = v.getValidatorAction();
261                          List list = action.getDependencyList();
262                          Iterator iter = list.iterator();
263                          while (iter.hasNext()) {
264                              String type = (String) iter.next();
265                              addValidator(type, id, v);
266                          }
267 
268                       }
269 
270                    } else {
271                        //otherwise just try using the client id
272                        String id = c.getClientId(context);
273                        addValidator(v.getType(), id, v);
274 
275                        ValidatorAction action = v.getValidatorAction();
276                        List list = action.getDependencyList();
277                        Iterator iter = list.iterator();
278                        while (iter.hasNext()) {
279                            String type = (String) iter.next();
280                            addValidator(type, id, v);
281                        }
282                    }
283 
284                }
285                if (Boolean.TRUE.equals(v.getServer())) {
286                   // Fields with empty values are not validated, so
287                   // we force the issue here by setting the component's
288                   // required attribute to true.
289 
290                   if ("required".equals(v.getType())) {
291                      h.setRequired(true);
292                   }
293                }
294             }
295          }
296       }
297 
298       Iterator childrenIterator = c.getFacetsAndChildren();
299       while (childrenIterator.hasNext()) {
300          UIComponent child = (UIComponent) childrenIterator.next();
301          findCommonsValidators(child, context);
302       }
303       childrenIterator = null;
304    }
305 
306 
307     /***
308      * <p>Write the start of the script for client-side validation.</p>
309      *
310      * @param writer A response writer
311      *
312      * @exception IOException if an input/output error occurs
313      */
314    private void writeScriptStart(ResponseWriter writer) throws IOException {
315       writer.startElement("script", this);
316       writer.writeAttribute("type", "text/javascript", null);
317       writer.writeAttribute("language", "Javascript1.1", null);
318       writer.write("\n");
319     }
320 
321 
322     /***
323      * <p>Write the end of the script for client-side validation.</p>
324      *
325      * @param writer A response writer
326      *
327      * @exception IOException if an input/output error occurs
328      */
329    private void writeScriptEnd(ResponseWriter writer) throws IOException {
330       writer.write("\n");
331       writer.endElement("script");
332    }
333 
334 
335     /***
336      * <p>Returns the name of the JavaScript function, specified in
337      *    the JSP page (presumably), that validates this JSP page's form.</p>
338      *
339      * @param writer A response writer
340      * @param context The FacesContext for this request
341      *
342      * @exception IOException if an input/output error occurs
343      */
344    private void writeValidationFunctions(ResponseWriter writer,
345       FacesContext context) throws IOException {
346 
347       StringBuffer buff = new StringBuffer();
348       buff.append("var bCancel = false;\n")
349           .append("function ")
350           .append(getAttributes().get("functionName").toString()).append("(form) {\n")
351           .append("\tvar bValid = true;\n")
352           .append("\tvar sFormName = jcv_retrieveFormName(form);\n");
353 
354       Map formValidators = getValidatorsGroupByFormName();
355       Iterator formIter = formValidators.entrySet().iterator();
356       while (formIter.hasNext()) {
357           Map.Entry typeEntry = (Map.Entry) formIter.next();
358           String formName = (String) typeEntry.getKey();
359           Map formTypeValidators = (Map) typeEntry.getValue();
360 
361           // for each validator type, write callback
362 
363           buff.append("\tif ((bValid && !bCancel && (\"")
364               .append(formName)
365               .append("\" == sFormName))) {\n")
366               .append("\t\tbValid = (");
367 
368 
369           Iterator iter = getTypesOrderedByDependencies(formTypeValidators.keySet()).iterator();
370           boolean first = true;
371           while (iter.hasNext()) {
372               String type = (String) iter.next();
373               ValidatorAction a = CommonsValidator.getValidatorAction(type);
374 
375               buff.append((!first ? " && " : ""))
376                   .append(a.getJsFunctionName())
377                   .append("(form)");
378               first = false;
379 
380               writer.write("function ");
381               StringBuffer callback = new StringBuffer();
382 
383               // most of the type the callback function is based on the form name and
384               // type but for some rules require special names
385               String fnameMnemonic = CommonsValidator.getJsCallbackMnemonic(type);
386 
387               callback.append(formName).append('_').append(fnameMnemonic);
388               writer.write(callback.toString());
389               writer.write("() { \n");
390               // for each field validated by this type, add configuration object
391               Map map = (Map) formTypeValidators.get(type);
392               Iterator iter2 = map.keySet().iterator();
393               int k = 0;
394               while (iter2.hasNext()) {
395                   String id = (String) iter2.next();
396                   CommonsValidator v = (CommonsValidator) map.get(id);
397                   writer.write("this[" + k + "] = ");
398                   k++;
399                   writeJavaScriptParams(writer, context, id, v);
400                   writer.write(";\n");
401               }
402               writer.write("\t}\n");
403           }
404 
405           buff.append(");\n\t\n}");
406 
407       }
408       formValidators.clear();
409 
410       // write out the form function
411       buff.append("\n\treturn bValid;\n")
412           .append("}\n");
413 
414       writer.write(buff.toString());
415 
416       // resolve dependencies for a complete types list
417 
418       List types = new ArrayList(validators.keySet());
419       types.add("includeJavaScriptUtilities");
420 
421       Iterator iter = types.iterator();
422       while (iter.hasNext()) {
423          String type = (String) iter.next();
424          ValidatorAction a = CommonsValidator.getValidatorAction(type);
425          writer.write(a.getJavascript());
426          writer.write("\n");
427       }
428 
429       types.clear();
430 
431    }
432 
433 
434    /***
435     * <p>Backslash-escapes the following characters from the input string:
436     * &quot;, &apos;, \, \r, \n.</p>
437     *
438     * <p>This method escapes characters that will result in an invalid
439     * Javascript statement within the validator Javascript.</p>
440     *
441     * @param str The string to escape.
442     * @return The string <code>s</code> with each instance of a double quote,
443     *         single quote, backslash, carriage-return, or line feed escaped
444     *         with a leading backslash.
445     */
446    private String escapeJavascript(String str) {
447        if (str == null) {
448            return null;
449        }
450 
451        int length = str.length();
452 
453        if (length == 0) {
454            return str;
455        }
456 
457        // guess at how many chars we'll be adding...
458        StringBuffer out = new StringBuffer(length + 4);
459 
460        // run through the string escaping sensitive chars
461        for (int i = 0; i < length; i++) {
462            char c = str.charAt(i);
463 
464            if ((c == '"') || (c == '\'') || (c == '//') || (c == '\n')
465                    || (c == '\r')) {
466                out.append('//');
467            }
468 
469            out.append(c);
470        }
471 
472        return out.toString();
473    }
474 
475 
476 
477    /***
478     * <p>Returns an array of validator types organized by dependencies.</p>
479     *
480     * @param typeSet The type set to be processed
481     * @return array of validator types ordered by dependencies
482     */
483    private List getTypesOrderedByDependencies(Set typeSet) {
484 
485        List tmpList = new ArrayList(typeSet);
486 
487        ordered: for (int i = 0; i < tmpList.size(); i++) {
488            boolean swap = false;
489            for (int j = 0; j < tmpList.size(); j++) {
490                String type = (String) tmpList.get(j);
491                ValidatorAction a = CommonsValidator.getValidatorAction(type);
492 
493                List dependencies  = a.getDependencyList();
494                if (dependencies != null && dependencies.size() > 0) {
495                    int max = -1;
496                    for (int n = 0; n < dependencies.size(); n++) {
497                        max = Math.max(max, tmpList.indexOf(dependencies.get(n)));
498                    }
499                    if (max > j) {
500                        String tmp = (String) tmpList.get(j);
501                        tmpList.remove(j);
502                        tmpList.add(max, tmp);
503                        swap = true;
504                        j = max;
505                    }
506                }
507 
508            }
509            if (!swap) {
510              break ordered;
511            }
512        }
513 
514        return tmpList;
515 
516    }
517 
518     /***
519      * <p>Writes the JavaScript parameters for the client-side
520      *    validation code.</p>
521      *
522      * @param writer A response writer
523      * @param context The FacesContext for this request
524      * @param id The clientId of the owning component
525      * @param v The Commons validator
526      *
527      * @exception IOException if an input/output error occurs
528      */
529    public void writeJavaScriptParams(ResponseWriter writer,
530       FacesContext context, String id, CommonsValidator v) throws IOException {
531 
532       Map localVars = null;
533       // look for var's evaluated at render time.  This is for client
534       // side JavaScript validation
535       if (validatorVars != null && validatorVars.containsKey(id)) {
536           Map typeVars = (Map) validatorVars.get(id);
537           if (typeVars != null && typeVars.containsKey(v.getType())) {
538               localVars = (Map) typeVars.get(v.getType());
539           }
540       }
541 
542       Tags tagUtils = new Tags();
543       ValidatorAction validatorAction = v.getValidatorAction();
544       writer.write("new Array(\"");
545       writer.write(id);
546       writer.write("\", \"");
547       writer.write(v.getErrorMessage(context, validatorAction, localVars));
548       writer.write("\", new Function(\"x\", \"return {");
549 
550       Iterator vi = v.getVars().entrySet().iterator();
551 
552       boolean first = true;
553 
554       // vars captured at render time and are the result of
555       // EL.
556       Map idVars = (Map) validatorVars.get(id);
557 
558       next: while (vi.hasNext()) {
559          Map.Entry e = (Map.Entry) vi.next();
560 
561          Object value = e.getValue();
562 
563          // look for a render override by clientId/type
564          if (idVars != null && idVars.containsKey(v.getType())) {
565 
566              Map typeVars = (Map) idVars.get(v.getType());
567              // look for a render override by clientId/type/name
568              if (typeVars != null && typeVars.containsKey(e.getKey())) {
569                 value = typeVars.get(e.getKey());
570              }
571          } else {
572             if (value != null && value instanceof String
573                 && isValueReference((String) e.getValue())) {
574 
575                 value = tagUtils.eval((String) e.getValue());
576             }
577          }
578 
579          if (value == null) {
580             continue next;
581          }
582          String name = (String) e.getKey();
583          if (!first) {
584              writer.write(",");
585          } else {
586              first = false;
587          }
588          writer.write(name);
589          writer.write(":");
590 
591          String jsType = v.getVarType(name);
592          // Ugh...mask validator doesn't construct RegExp
593          if (jsType.equals(Var.JSTYPE_REGEXP)) {
594              writer.write("/");
595          } else {
596              writer.write("'");
597          }
598 
599          writer.write(escapeJavascript(value.toString()));
600 
601          if (jsType.equals(Var.JSTYPE_REGEXP)) {
602              writer.write("/");
603          } else {
604              writer.write("'");
605          }
606       }
607       writer.write("}[x];\"))");
608    }
609 
610 
611    /***
612     * <p>Traverses up the tree looking for the owning form.
613     * Returns the parent <code>UIForm</code> or empty string
614     * if one is not found.</p>
615     *
616     * @param context FacesContext for the current request
617     * @param component <code>UIForm</code> parent of the component.
618     * @return client id of the parent form component
619     */
620    public String findForm(FacesContext context, UIComponent component) {
621 
622        UIComponent parent = component.getParent();
623        if (parent != null) {
624           if (parent instanceof UIForm) {
625              return parent.getClientId(context).replace(UINamingContainer.SEPARATOR_CHAR, '_');
626           } else {
627              return findForm(context, parent);
628           }
629        }
630 
631        return "";
632    }
633 
634 
635     /***
636      * <p>Begin encoding for this component. This method
637      *    finds all Commons validators attached to components
638      *    in the current component hierarchy and writes out
639      *    JavaScript code to invoke those validators, in turn.</p>
640      *
641      * @param context The FacesContext for this request
642      *
643      * @exception IOException if an input/output error occurs
644      */
645    public void encodeBegin(FacesContext context) throws IOException {
646       ResponseWriter writer = context.getResponseWriter();
647 
648       validators.clear();
649       findCommonsValidators(context.getViewRoot(), context);
650 
651       writeScriptStart(writer);
652       writeValidationFunctions(writer, context);
653       writeScriptEnd(writer);
654    }
655 
656 
657     /***
658      * <p>Return true if the specified string contains an EL expression.</p>
659      * 
660      * <p>This is taken almost verbatim from {@link javax.faces.webapp.UIComponentTag}
661      * in order to remove JSP dependencies from the renderers.</p>
662      *
663      * @param value String to be checked for being an expression
664      */
665     private boolean isValueReference(String value) {
666 
667         if (value == null) {
668             return false;
669         }
670 
671         int start = value.indexOf("#{");
672         if (start < 0) {
673             return false;
674         }
675 
676         int end = value.lastIndexOf('}');
677         return (end >= 0) && (start < end);
678     }
679 
680 
681 }