View Javadoc

1   /*
2    * $Id: JavascriptValidatorTag.java 471754 2006-11-06 14:55:09Z husted $
3    *
4    * Licensed to the Apache Software Foundation (ASF) under one
5    * or more contributor license agreements.  See the NOTICE file
6    * distributed with this work for additional information
7    * regarding copyright ownership.  The ASF licenses this file
8    * to you under the Apache License, Version 2.0 (the
9    * "License"); you may not use this file except in compliance
10   * with the License.  You may obtain a copy of the License at
11   *
12   *  http://www.apache.org/licenses/LICENSE-2.0
13   *
14   * Unless required by applicable law or agreed to in writing,
15   * software distributed under the License is distributed on an
16   * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
17   * KIND, either express or implied.  See the License for the
18   * specific language governing permissions and limitations
19   * under the License.
20   */
21  
22  package org.apache.struts.faces.taglib;
23  
24  import java.io.IOException;
25  import java.util.ArrayList;
26  import java.util.Collections;
27  import java.util.Comparator;
28  import java.util.Iterator;
29  import java.util.List;
30  import java.util.Locale;
31  import java.util.Map;
32  
33  import javax.faces.component.UIComponent;
34  import javax.faces.context.FacesContext;
35  import javax.faces.webapp.UIComponentTag;
36  import javax.servlet.http.HttpServletRequest;
37  import javax.servlet.jsp.JspException;
38  import javax.servlet.jsp.JspWriter;
39  import javax.servlet.jsp.PageContext;
40  import javax.servlet.jsp.tagext.BodyTagSupport;
41  import javax.servlet.jsp.tagext.Tag;
42  import javax.servlet.ServletContext;
43  
44  import org.apache.commons.validator.Field;
45  import org.apache.commons.validator.Form;
46  import org.apache.commons.validator.ValidatorAction;
47  import org.apache.commons.validator.ValidatorResources;
48  import org.apache.commons.validator.Var;
49  import org.apache.commons.validator.util.ValidatorUtils;
50  import org.apache.struts.Globals;
51  import org.apache.struts.config.ModuleConfig;
52  import org.apache.struts.faces.component.FormComponent;
53  import org.apache.struts.taglib.TagUtils;
54  import org.apache.struts.util.MessageResources;
55  import org.apache.struts.util.ModuleUtils;
56  import org.apache.struts.validator.Resources;
57  import org.apache.struts.validator.ValidatorPlugIn;
58  
59  /**
60   * Custom tag that generates JavaScript for client side validation based
61   * on the validation rules loaded by the <code>ValidatorPlugIn</code>
62   * defined in the struts-config.xml file.  This is based on the code in
63   * the corresponding class of the Struts HTML tag library, modified as needed
64   * to reflect differences in the way JavaServer Faces renders field
65   * identifiers.
66   *
67   * @version $Rev: 471754 $ $Date: 2006-11-06 08:55:09 -0600 (Mon, 06 Nov 2006) $
68   */
69  public class JavascriptValidatorTag extends BodyTagSupport {
70  
71      // ----------------------------------------------------------- Properties
72  
73      /**
74       * The servlet context attribute key for our resources.
75       */
76      protected String bundle = Globals.MESSAGES_KEY;
77  
78      /**
79       * The default locale on our server.
80       * @deprecated This variable is no longer used.
81       */
82      protected static Locale defaultLocale = Locale.getDefault();
83  
84      /**
85       * The name of the form that corresponds with the action name
86       * in struts-config.xml. Specifying a form name places a
87       * &lt;script&gt; &lt;/script&gt; around the javascript.
88       */
89      protected String formName = null;
90  
91      /**
92       * The line ending string.
93       */
94      protected static String lineEnd = System.getProperty("line.separator");
95  
96      /**
97       * The current page number of a multi-part form.
98       * Only valid when the formName attribute is set.
99       */
100     protected int page = 0;
101 
102     /**
103      * This will be used as is for the JavaScript validation method name if it
104      * has a value.  This is the method name of the main JavaScript method that
105      * the form calls to perform validations.
106      */
107     protected String methodName = null;
108 
109     /**
110      * The static JavaScript methods will only be printed if this is set to
111      * "true".
112      */
113     protected String staticJavascript = "true";
114 
115     /**
116      * The dynamic JavaScript objects will only be generated if this is set to
117      * "true".
118      */
119     protected String dynamicJavascript = "true";
120 
121     /**
122      * The src attribute for html script element (used to include an external
123      * script resource). The src attribute is only recognized
124      * when the formName attribute is specified.
125      */
126     protected String src = null;
127 
128     /**
129      * The JavaScript methods will enclosed with html comments if this is set to
130      * "true".
131      */
132     protected String htmlComment = "true";
133 
134     /**
135      * Hide JavaScript methods in a CDATA section for XHTML when "true".
136      */
137     protected String cdata = "true";
138 
139     private String htmlBeginComment = "\n<!-- Begin \n";
140 
141     private String htmlEndComment = "//End --> \n";
142 
143     /**
144      * Gets the key (form name) that will be used
145      * to retrieve a set of validation rules to be
146      * performed on the bean passed in for validation.
147      */
148     public String getFormName() {
149         return formName;
150     }
151 
152     /**
153      * Sets the key (form name) that will be used
154      * to retrieve a set of validation rules to be
155      * performed on the bean passed in for validation.
156      * Specifying a form name places a
157      * &lt;script&gt; &lt;/script&gt; tag around the javascript.
158      */
159     public void setFormName(String formName) {
160         this.formName = formName;
161     }
162 
163     /**
164      * Gets the current page number of a multi-part form.
165      * Only field validations with a matching page numer
166      * will be generated that match the current page number.
167      * Only valid when the formName attribute is set.
168      */
169     public int getPage() {
170         return page;
171     }
172 
173     /**
174      * Sets the current page number of a multi-part form.
175      * Only field validations with a matching page numer
176      * will be generated that match the current page number.
177      * Only valid when the formName attribute is set.
178      */
179     public void setPage(int page) {
180         this.page = page;
181     }
182 
183     /**
184      * Gets the method name that will be used for the Javascript
185      * validation method name if it has a value.  This overrides
186      * the auto-generated method name based on the key (form name)
187      * passed in.
188      */
189     public String getMethod() {
190         return methodName;
191     }
192 
193     /**
194      * Sets the method name that will be used for the Javascript
195      * validation method name if it has a value.  This overrides
196      * the auto-generated method name based on the key (form name)
197      * passed in.
198      */
199     public void setMethod(String methodName) {
200         this.methodName = methodName;
201     }
202 
203     /**
204      * Gets whether or not to generate the static
205      * JavaScript.  If this is set to 'true', which
206      * is the default, the static JavaScript will be generated.
207      */
208     public String getStaticJavascript() {
209         return staticJavascript;
210     }
211 
212     /**
213      * Sets whether or not to generate the static
214      * JavaScript.  If this is set to 'true', which
215      * is the default, the static JavaScript will be generated.
216      */
217     public void setStaticJavascript(String staticJavascript) {
218         this.staticJavascript = staticJavascript;
219     }
220 
221     /**
222      * Gets whether or not to generate the dynamic
223      * JavaScript.  If this is set to 'true', which
224      * is the default, the dynamic JavaScript will be generated.
225      */
226     public String getDynamicJavascript() {
227         return dynamicJavascript;
228     }
229 
230     /**
231      * Sets whether or not to generate the dynamic
232      * JavaScript.  If this is set to 'true', which
233      * is the default, the dynamic JavaScript will be generated.
234      */
235     public void setDynamicJavascript(String dynamicJavascript) {
236         this.dynamicJavascript = dynamicJavascript;
237     }
238 
239     /**
240       * Gets whether or not to delimit the
241       * JavaScript with html comments.  If this is set to 'true', which
242       * is the default, the htmlComment will be surround the JavaScript.
243       */
244     public String getHtmlComment() {
245         return htmlComment;
246     }
247 
248     /**
249      * Sets whether or not to delimit the
250      * JavaScript with html comments.  If this is set to 'true', which
251      * is the default, the htmlComment will be surround the JavaScript.
252      */
253     public void setHtmlComment(String htmlComment) {
254         this.htmlComment = htmlComment;
255     }
256 
257     /**
258      * Gets the src attribute's value when defining
259      * the html script element.
260      */
261     public String getSrc() {
262         return src;
263     }
264 
265     /**
266      * Sets the src attribute's value when defining
267      * the html script element. The src attribute is only recognized
268      * when the formName attribute is specified.
269      */
270     public void setSrc(String src) {
271         this.src = src;
272     }
273 
274     /**
275      * Render the JavaScript for to perform validations based on the form name.
276      *
277      * @exception JspException if a JSP exception has occurred
278      */
279     public int doStartTag() throws JspException {
280         StringBuffer results = new StringBuffer();
281 
282         HttpServletRequest request =
283           (HttpServletRequest)pageContext.getRequest();
284         ServletContext servletContext = pageContext.getServletContext();
285         ModuleConfig config =
286           ModuleUtils.getInstance().getModuleConfig(request, servletContext);
287 
288         ValidatorResources resources =
289             (ValidatorResources) pageContext.getAttribute(
290                 ValidatorPlugIn.VALIDATOR_KEY + config.getPrefix(),
291                 PageContext.APPLICATION_SCOPE);
292 
293         Locale locale = TagUtils.getInstance().getUserLocale(pageContext, null);
294 
295         Form form = resources.getForm(locale, formName);
296         if (form != null) {
297             if ("true".equalsIgnoreCase(dynamicJavascript)) {
298                 MessageResources messages =
299                     (MessageResources) pageContext.getAttribute(
300                         bundle + config.getPrefix(),
301                         PageContext.APPLICATION_SCOPE);
302 
303                 List lActions = new ArrayList();
304                 List lActionMethods = new ArrayList();
305 
306                 // Get List of actions for this Form
307                 for (Iterator i = form.getFields().iterator(); i.hasNext();) {
308                     Field field = (Field) i.next();
309 
310                     for (Iterator x = field.getDependencyList().iterator();
311                         x.hasNext();) {
312                         Object o = x.next();
313 
314                         if (o != null && !lActionMethods.contains(o)) {
315                             lActionMethods.add(o);
316                         }
317                     }
318 
319                 }
320 
321                 // Create list of ValidatorActions based on lActionMethods
322                 for (Iterator i = lActionMethods.iterator(); i.hasNext();) {
323                     String depends = (String) i.next();
324                     ValidatorAction va = resources.getValidatorAction(depends);
325 
326                     // throw nicer NPE for easier debugging
327                     if (va == null) {
328                         throw new NullPointerException(
329                             "Depends string \""
330                                 + depends
331                                 + "\" was not found in validator-rules.xml.");
332                     }
333 
334                     String javascript = va.getJavascript();
335                     if (javascript != null && javascript.length() > 0) {
336                         lActions.add(va);
337                     } else {
338                         i.remove();
339                     }
340                 }
341 
342                 Collections.sort(lActions, new Comparator() {
343                     public int compare(Object o1, Object o2) {
344                         ValidatorAction va1 = (ValidatorAction) o1;
345                         ValidatorAction va2 = (ValidatorAction) o2;
346 
347                         if ((va1.getDepends() == null
348                             || va1.getDepends().length() == 0)
349                             && (va2.getDepends() == null
350                             || va2.getDepends().length() == 0)) {
351                             return 0;
352                         } else if (
353                             (va1.getDepends() != null
354                             && va1.getDepends().length() > 0)
355                             && (va2.getDepends() == null
356                             || va2.getDepends().length() == 0)) {
357                             return 1;
358                         } else if (
359                             (va1.getDepends() == null
360                             || va1.getDepends().length() == 0)
361                             && (va2.getDepends() != null
362                             && va2.getDepends().length() > 0)) {
363                             return -1;
364                         } else {
365                             return va1.getDependencyList().size() -
366                               va2.getDependencyList().size();
367                         }
368                     }
369                 });
370 
371                 String methods = null;
372                 for (Iterator i = lActions.iterator(); i.hasNext();) {
373                     ValidatorAction va = (ValidatorAction) i.next();
374 
375                     if (methods == null) {
376                         methods = va.getMethod() + "(form)";
377                     } else {
378                         methods += " && " + va.getMethod() + "(form)";
379                     }
380                 }
381 
382                 results.append(getJavascriptBegin(methods));
383 
384                 for (Iterator i = lActions.iterator(); i.hasNext();) {
385                     ValidatorAction va = (ValidatorAction) i.next();
386                     String jscriptVar = null;
387                     String functionName = null;
388 
389                     if (va.getJsFunctionName() != null
390                         && va.getJsFunctionName().length() > 0) {
391                         functionName = va.getJsFunctionName();
392                     } else {
393                         functionName = va.getName();
394                     }
395 
396                     if (isStruts11()) {
397                         results.append("    function " +
398                           functionName + " () { \n");
399                     } else {
400                         results.append("    function " +
401                           formName + "_" + functionName +
402                           " () { \n");
403                     }
404                     for (Iterator x = form.getFields().iterator();
405                         x.hasNext();) {
406                         Field field = (Field) x.next();
407 
408                         // Skip indexed fields for now until there is a good
409                         // way to handle error messages (and the length of the
410                         // list (could retrieve from scope?))
411                         if (field.isIndexed()
412                             || field.getPage() != page
413                             || !field.isDependency(va.getName())) {
414 
415                             continue;
416                         }
417 
418                         String message =
419                             Resources.getMessage(messages, locale, va, field);
420 
421                         message = (message != null) ? message : "";
422 
423                         jscriptVar = this.getNextVar(jscriptVar);
424 
425                         results.append(
426                             "     this."
427                                 + jscriptVar
428                                 + " = new Array(\""
429                                 + getFormClientId()
430                                 + ":"
431                                 + field.getKey()
432                                 + "\", \""
433                                 + message
434                                 + "\", ");
435 
436                         results.append("new Function (\"varName\", \"");
437 
438                         Map vars = field.getVars();
439                         // Loop through the field's variables.
440                         Iterator varsIterator = vars.keySet().iterator();
441                         while (varsIterator.hasNext()) {
442                             String varName = (String) varsIterator.next();
443                             Var var = (Var) vars.get(varName);
444                             String varValue = var.getValue();
445                             String jsType = var.getJsType();
446 
447                             // skip requiredif variables field, fieldIndexed,
448                             // fieldTest, fieldValue
449                             if (varName.startsWith("field")) {
450                                 continue;
451                             }
452 
453                             if (Var.JSTYPE_INT.equalsIgnoreCase(jsType)) {
454                                 results.append(
455                                     "this."
456                                         + varName
457                                         + "="
458                                         + ValidatorUtils.replace(
459                                             varValue,
460                                             "\\",
461                                             "\\\\")
462                                         + "; ");
463                             } else if (Var.JSTYPE_REGEXP.equalsIgnoreCase(
464                                 jsType)) {
465                                 results.append(
466                                     "this."
467                                         + varName
468                                         + "=/"
469                                         + ValidatorUtils.replace(
470                                             varValue,
471                                             "\\",
472                                             "\\\\")
473                                         + "/; ");
474                             } else if (Var.JSTYPE_STRING.equalsIgnoreCase(
475                                 jsType)) {
476                                 results.append(
477                                     "this."
478                                         + varName
479                                         + "='"
480                                         + ValidatorUtils.replace(
481                                             varValue,
482                                             "\\",
483                                             "\\\\")
484                                         + "'; ");
485                                 // So everyone using the latest format doesn't
486                                 // need to change their xml files immediately.
487                             } else if ("mask".equalsIgnoreCase(varName)) {
488                                 results.append(
489                                     "this."
490                                         + varName
491                                         + "=/"
492                                         + ValidatorUtils.replace(
493                                             varValue,
494                                             "\\",
495                                             "\\\\")
496                                         + "/; ");
497                             } else {
498                                 results.append(
499                                     "this."
500                                         + varName
501                                         + "='"
502                                         + ValidatorUtils.replace(
503                                             varValue,
504                                             "\\",
505                                             "\\\\")
506                                         + "'; ");
507                             }
508                         }
509 
510                         results.append(" return this[varName];\"));\n");
511                     }
512                     results.append("    } \n\n");
513                 }
514             } else if ("true".equalsIgnoreCase(staticJavascript)) {
515                 results.append(this.getStartElement());
516                 if ("true".equalsIgnoreCase(htmlComment)) {
517                     results.append(htmlBeginComment);
518                 }
519             }
520         }
521 
522         if ("true".equalsIgnoreCase(staticJavascript)) {
523             results.append(getJavascriptStaticMethods(resources));
524         }
525 
526         if (form != null
527             && ("true".equalsIgnoreCase(dynamicJavascript)
528                 || "true".equalsIgnoreCase(staticJavascript))) {
529 
530             results.append(getJavascriptEnd());
531         }
532 
533 
534         JspWriter writer = pageContext.getOut();
535         try {
536             writer.print(results.toString());
537         } catch (IOException e) {
538             throw new JspException(e.getMessage());
539         }
540 
541         return (EVAL_BODY_TAG);
542 
543     }
544 
545     /**
546      * Release any acquired resources.
547      */
548     public void release() {
549         super.release();
550         bundle = Globals.MESSAGES_KEY;
551         formName = null;
552         page = 0;
553         methodName = null;
554         staticJavascript = "true";
555         dynamicJavascript = "true";
556         htmlComment = "true";
557         cdata = "true";
558         src = null;
559         formClientId = null;
560     }
561 
562     /**
563      * Returns the opening script element and some initial javascript.
564      */
565     protected String getJavascriptBegin(String methods) {
566         StringBuffer sb = new StringBuffer();
567         String name =
568             formName.substring(0, 1).toUpperCase()
569                 + formName.substring(1, formName.length());
570 
571         sb.append(this.getStartElement());
572 
573         if (this.isXhtml() && "true".equalsIgnoreCase(this.cdata)) {
574             sb.append("<![CDATA[\r\n");
575         }
576 
577         if (!this.isXhtml() && "true".equals(htmlComment)) {
578             sb.append(htmlBeginComment);
579         }
580         sb.append("\n     var bCancel = false; \n\n");
581 
582         if (methodName == null || methodName.length() == 0) {
583             sb.append(
584                 "    function validate"
585                     + name
586                     + "(form) {                                          "
587                     + "                         \n");
588         } else {
589             sb.append(
590                 "    function "
591                     + methodName
592                     + "(form) {                                          "
593                     + "                         \n");
594         }
595         sb.append("        if (bCancel) \n");
596         sb.append("      return true; \n");
597         sb.append("        else \n");
598 
599         // Always return true if there aren't any Javascript validation methods
600         if (methods == null || methods.length() == 0) {
601             sb.append("       return true; \n");
602         } else {
603             sb.append("       return " + methods + "; \n");
604         }
605 
606         sb.append("   } \n\n");
607 
608         return sb.toString();
609     }
610 
611     protected String getJavascriptStaticMethods(ValidatorResources resources) {
612         StringBuffer sb = new StringBuffer();
613 
614         sb.append("\n\n");
615 
616         Iterator actions = resources.getValidatorActions().values().iterator();
617         while (actions.hasNext()) {
618             ValidatorAction va = (ValidatorAction) actions.next();
619             if (va != null) {
620                 String javascript = va.getJavascript();
621                 if (javascript != null && javascript.length() > 0) {
622                     sb.append(javascript + "\n");
623                 }
624             }
625         }
626 
627         return sb.toString();
628     }
629 
630     /**
631      * Returns the closing script element.
632      */
633     protected String getJavascriptEnd() {
634         StringBuffer sb = new StringBuffer();
635 
636         sb.append("\n");
637         if (!this.isXhtml() && "true".equals(htmlComment)){
638             sb.append(htmlEndComment);
639         }
640 
641         if (this.isXhtml() && "true".equalsIgnoreCase(this.cdata)) {
642             sb.append("]]>\r\n");
643         }
644 
645         sb.append("</script>\n\n");
646 
647         return sb.toString();
648     }
649 
650     /**
651      * The value <code>null</code> will be returned at the end of the sequence.
652      * &nbsp;&nbsp;&nbsp; ex: "zz" will return <code>null</code>
653      */
654     private String getNextVar(String input) {
655         if (input == null) {
656             return "aa";
657         }
658 
659         input = input.toLowerCase();
660 
661         for (int i = input.length(); i > 0; i--) {
662             int pos = i - 1;
663 
664             char c = input.charAt(pos);
665             c++;
666 
667             if (c <= 'z') {
668                 if (i == 0) {
669                     return c + input.substring(pos, input.length());
670                 } else if (i == input.length()) {
671                     return input.substring(0, pos) + c;
672                 } else {
673                     return input.substring(0, pos) + c + input.substring(pos,
674                       input.length() - 1);
675                 }
676             } else {
677                 input = replaceChar(input, pos, 'a');
678             }
679 
680         }
681 
682         return null;
683 
684     }
685 
686     /**
687      * Replaces a single character in a <code>String</code>
688      */
689     private String replaceChar(String input, int pos, char c) {
690         if (pos == 0) {
691             return c + input.substring(pos, input.length());
692         } else if (pos == input.length()) {
693             return input.substring(0, pos) + c;
694         } else {
695             return input.substring(0, pos) + c + input.substring(pos,
696               input.length() - 1);
697         }
698     }
699 
700     /**
701      * Constructs the beginning &lt;script&gt; element depending on
702      * xhtml status.
703      */
704     private String getStartElement() {
705         StringBuffer start =
706           new StringBuffer("<script type=\"text/javascript\"");
707 
708         // there is no language attribute in xhtml
709         if (!this.isXhtml()) {
710             start.append(" language=\"Javascript1.1\"");
711         }
712 
713         if (this.src != null) {
714             start.append(" src=\"" + src + "\"");
715         }
716 
717         start.append("> \n");
718         return start.toString();
719     }
720 
721     /**
722      * Returns true if this is an xhtml page.
723      */
724     private boolean isXhtml() {
725         return TagUtils.getInstance().isXhtml(this.pageContext);
726     }
727 
728     /**
729      * Returns the cdata setting "true" or "false".
730      * @return String - "true" if JavaScript will be hidden in a CDATA section
731      */
732     public String getCdata() {
733         return cdata;
734     }
735 
736     /**
737      * Sets the cdata status.
738      * @param cdata The cdata to set
739      */
740     public void setCdata(String cdata) {
741         this.cdata = cdata;
742     }
743 
744 
745     private String formClientId = null;
746 
747     /**
748      * <p>Return the <code>clientId</code> of the form component for which
749      * we are rendering validation Javascript.</p>
750      *
751      * @exception IllegalStateException if we are not nested inside a
752      *  UIComponentTag with a child FormComponent matching our form name
753      */
754     private String getFormClientId(){
755 
756         // Return any cached value
757         if (formClientId != null) {
758             return (formClientId);
759         }
760 
761         // Locate our parent tag that is a component tag
762         Tag parent = getParent();
763         while (parent != null) {
764             if (parent instanceof UIComponentTag) {
765                 break;
766             }
767             parent = parent.getParent();
768         }
769         if (parent == null) {
770             throw new IllegalArgumentException
771                 ("Not nested inside a UIComponentTag");
772         }
773 
774         // Are we nested inside our corresponding form tag?
775         UIComponent parentComponent =
776             ((UIComponentTag) parent).getComponentInstance();
777         if (parentComponent instanceof FormComponent) {
778             if (formName.equals(
779                 parentComponent.getAttributes().get("beanName"))) {
780                 formClientId = parentComponent.getClientId
781                     (FacesContext.getCurrentInstance());
782                 return (formClientId);
783             }
784         }
785 
786         // Scan the children of this tag's component
787         Iterator kids = ((UIComponentTag) parent).
788             getComponentInstance().getChildren().iterator();
789         while (kids.hasNext()) {
790             UIComponent kid = (UIComponent) kids.next();
791             if (!(kid instanceof FormComponent)) {
792                 continue;
793             }
794             if (formName.equals(kid.getAttributes().get("beanName"))) {
795                 formClientId =
796                     kid.getClientId(FacesContext.getCurrentInstance());
797                 return (formClientId);
798             }
799         }
800         throw new IllegalArgumentException
801             ("Cannot find child FormComponent for form '" + formName + "'");
802 
803     }
804 
805 
806     // ---------------------------------------------------------- Static Methods
807 
808 
809     private static boolean struts11;
810 
811     static {
812         try {
813             JavascriptValidatorTag.class.getClassLoader().loadClass
814                 ("org.apache.struts.taglib.TagUtils");
815             struts11 = false;
816         } catch (Exception e) {
817             struts11 = true;
818         }
819     }
820 
821 
822     /**
823      * <p>Return <code>true</code> if we are running on top of Struts 1.1</p>
824      */
825     private static boolean isStruts11() {
826         return struts11;
827     }
828 
829 
830 }