View Javadoc

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