View Javadoc

1   package org.apache.velocity.tools.struts;
2   
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  import java.util.ArrayList;
23  import java.util.Collections;
24  import java.util.Comparator;
25  import java.util.Iterator;
26  import java.util.List;
27  import java.util.Locale;
28  import java.util.Map;
29  import javax.servlet.ServletContext;
30  import javax.servlet.http.HttpServletRequest;
31  import javax.servlet.http.HttpSession;
32  import org.apache.commons.validator.Field;
33  import org.apache.commons.validator.Form;
34  import org.apache.commons.validator.ValidatorAction;
35  import org.apache.commons.validator.ValidatorResources;
36  import org.apache.commons.validator.Var;
37  import org.apache.struts.Globals;
38  import org.apache.struts.config.ActionConfig;
39  import org.apache.struts.config.ModuleConfig;
40  import org.apache.struts.util.MessageResources;
41  import org.apache.struts.util.ModuleUtils;
42  import org.apache.struts.validator.Resources;
43  import org.apache.struts.validator.ValidatorPlugIn;
44  import org.apache.velocity.tools.Scope;
45  import org.apache.velocity.tools.config.DefaultKey;
46  import org.apache.velocity.tools.config.ValidScope;
47  import org.apache.velocity.tools.view.ViewContext;
48  import org.apache.velocity.tools.view.ViewToolContext;
49  
50  /**
51   * <p>View tool that works with Struts Validator to
52   *    produce client side javascript validation for your forms.</p>
53   * <p>Usage:
54   * <pre>
55   * Template example:
56   *
57   * $validator.getJavascript("nameOfYourForm")
58   *
59   * Toolbox configuration:
60   * &lt;tools&gt;
61   *   &lt;toolbox scope="request"&gt;
62   *     &lt;tool class="org.apache.velocity.tools.struts.ValidatorTool"/&gt;
63   *   &lt;/toolbox&gt;
64   * &lt;/tools&gt;
65   * </pre>
66   * </p>
67   * <p>This is an adaptation of the JavascriptValidatorTag
68   * from the Struts 1.1 validator library.</p>
69   *
70   * @author David Winterfeldt
71   * @author David Graham
72   * @author <a href="mailto:marinoj@centrum.is">Marino A. Jonsson</a>
73   * @author Nathan Bubna
74   * @since VelocityTools 1.1
75   * @version $Revision: 595822 $ $Date: 2007-11-16 13:07:51 -0800 (Fri, 16 Nov 2007) $
76   */
77  @DefaultKey("validator")
78  @ValidScope(Scope.REQUEST)
79  public class ValidatorTool
80  {
81  
82      /** A reference to the ViewContext */
83      protected ViewContext context;
84  
85      /** A reference to the ServletContext */
86      protected ServletContext app;
87  
88      /** A reference to the HttpServletRequest. */
89      protected HttpServletRequest request;
90  
91      /** A reference to the HttpSession. */
92      protected HttpSession session;
93  
94      /** A reference to the ValidatorResources. */
95      protected ValidatorResources resources;
96  
97  
98      private static final String HTML_BEGIN_COMMENT = "\n<!-- Begin \n";
99      private static final String HTML_END_COMMENT = "//End --> \n";
100 
101     private boolean xhtml = false;
102 
103     private boolean htmlComment = true;
104     private boolean cdata = true;
105     private String formName = null;
106     private String methodName = null;
107     private String src = null;
108     private int page = 0;
109     /**
110      * formName is used for both Javascript and non-javascript validations.
111      * For the javascript validations, there is the possibility that we will
112      * be rewriting the formName (if it is a ValidatorActionForm instead of just
113      * a ValidatorForm) so we need another variable to hold the formName just for
114      * javascript usage.
115      */
116     protected String jsFormName = null;
117 
118 
119 
120 
121     /**
122      * A Comparator to use when sorting ValidatorAction objects.
123      */
124     private static final Comparator actionComparator = new Comparator() {
125         public int compare(Object o1, Object o2) {
126 
127             ValidatorAction va1 = (ValidatorAction) o1;
128             ValidatorAction va2 = (ValidatorAction) o2;
129 
130             if ((va1.getDepends() == null || va1.getDepends().length() == 0)
131                 && (va2.getDepends() == null || va2.getDepends().length() == 0)) {
132                 return 0;
133 
134             } else if (
135                 (va1.getDepends() != null && va1.getDepends().length() > 0)
136                     && (va2.getDepends() == null || va2.getDepends().length() == 0)) {
137                 return 1;
138 
139             } else if (
140                 (va1.getDepends() == null || va1.getDepends().length() == 0)
141                     && (va2.getDepends() != null && va2.getDepends().length() > 0)) {
142                 return -1;
143 
144             } else {
145                 return va1.getDependencyList().size() - va2.getDependencyList().size();
146             }
147         }
148     };
149 
150 
151     @Deprecated
152     public void init(Object obj)
153     {
154         if (obj instanceof ViewContext)
155         {
156             this.context = (ViewContext)obj;
157             this.request = context.getRequest();
158             this.session = request.getSession(false);
159             this.app = context.getServletContext();
160         }
161     }
162 
163     /**
164      * Initializes this tool.
165      *
166      * @param params the Map of configuration parameters
167      * @throws IllegalArgumentException if the param is not a ViewContext
168      */
169     public void configure(Map params)
170     {
171         this.context = (ViewContext)params.get(ViewToolContext.CONTEXT_KEY);
172         this.request = (HttpServletRequest)params.get(ViewContext.REQUEST);
173         this.session = request.getSession(false);
174         this.app = (ServletContext)params.get(ViewContext.SERVLET_CONTEXT_KEY);
175 
176         Boolean b = (Boolean)params.get("XHTML");
177         if (b != null)
178         {
179             this.xhtml = b.booleanValue();
180         }
181 
182         /* Is there a mapping associated with this request? */
183         ActionConfig config =
184                 (ActionConfig)request.getAttribute(Globals.MAPPING_KEY);
185         if (config != null)
186         {
187             /* Is there a form bean associated with this mapping? */
188             this.formName = config.getAttribute();
189         }
190 
191         ModuleConfig mconfig = ModuleUtils.getInstance().getModuleConfig(request, app);
192         this.resources = (ValidatorResources)app.getAttribute(ValidatorPlugIn.
193                 VALIDATOR_KEY +
194                 mconfig.getPrefix());
195 
196     }
197 
198     /****************** get/set accessors ***************/
199 
200     /**
201      * Gets the current page number of a multi-part form.
202      * Only field validations with a matching page number
203      * will be generated that match the current page number.
204      * Only valid when the formName attribute is set.
205      *
206      * @return the current page number of a multi-part form
207      */
208     public int getPage()
209     {
210         return page;
211     }
212 
213     /**
214      * Sets the current page number of a multi-part form.
215      * Only field validations with a matching page number
216      * will be generated that match the current page number.
217      *
218      * @param page the current page number of a multi-part form
219      */
220     public void setPage(int page)
221     {
222         this.page = page;
223     }
224 
225     /**
226      * Gets the method name that will be used for the Javascript
227      * validation method name if it has a value.  This overrides
228      * the auto-generated method name based on the key (form name)
229      * passed in.
230      *
231      * @return the method name that will be used for the Javascript validation method
232      */
233     public String getMethod()
234     {
235         return methodName;
236     }
237 
238     /**
239      * Sets the method name that will be used for the Javascript
240      * validation method name if it has a value.  This overrides
241      * the auto-generated method name based on the key (form name)
242      * passed in.
243      *
244      * @param methodName the method name that will be used for the Javascript validation method name
245      */
246     public void setMethod(String methodName)
247     {
248         this.methodName = methodName;
249     }
250 
251     /**
252      * Gets whether or not to delimit the
253      * JavaScript with html comments.  If this is set to 'true', which
254      * is the default, html comments will surround the JavaScript.
255      *
256      * @return true if the JavaScript should be delimited with html comments
257      */
258     public boolean getHtmlComment()
259     {
260         return this.htmlComment;
261     }
262 
263     /**
264      * Sets whether or not to delimit the
265      * JavaScript with html comments.  If this is set to 'true', which
266      * is the default, html comments will surround the JavaScript.
267      *
268      * @param htmlComment whether or not to delimit the JavaScript with html comments
269      */
270     public void setHtmlComment(boolean htmlComment)
271     {
272         this.htmlComment = htmlComment;
273     }
274 
275     /**
276      * Gets the src attribute's value when defining
277      * the html script element.
278      *
279      * @return the src attribute's value
280      */
281     public String getSrc()
282     {
283         return src;
284     }
285 
286     /**
287      * Sets the src attribute's value (used to include
288      * an external script resource) when defining
289      * the html script element. The src attribute is only recognized
290      * when the formName attribute is specified.
291      *
292      * @param src the src attribute's value
293      */
294     public void setSrc(String src)
295     {
296         this.src = src;
297     }
298 
299     /**
300      * Returns the cdata setting "true" or "false".
301      *
302      * @return boolean - "true" if JavaScript will be hidden in a CDATA section
303      */
304     public boolean getCdata()
305     {
306         return cdata;
307     }
308 
309     /**
310      * Sets the cdata status.
311      * @param cdata The cdata to set
312      */
313     public void setCdata(boolean cdata)
314     {
315         this.cdata = cdata;
316     }
317 
318 
319     /****************** methods that aren't just accessors ***************/
320 
321     /**
322      * Render both dynamic and static JavaScript to perform
323      * validations based on the form name attribute of the action
324      * mapping associated with the current request (if such exists).
325      *
326      * @return the javascript for the current form
327      * @throws Exception
328      */
329     public String getJavascript() throws Exception
330     {
331         return getJavascript(this.formName);
332     }
333 
334     /**
335      * Render both dynamic and static JavaScript to perform
336      * validations based on the supplied form name.
337      *
338      * @param formName the key (form name)
339      * @return the Javascript for the specified form
340      * @throws Exception
341      */
342     public String getJavascript(String formName) throws Exception
343     {
344         this.formName = formName;
345         return getJavascript(formName, true);
346     }
347 
348     /**
349      * Render just the dynamic JavaScript to perform validations based
350      * on the form name attribute of the action mapping associated
351      * with the current request (if such exists). Useful i.e. if the static
352      * parts are located in a seperate .js file.
353      *
354      * @return the javascript for the current form
355      * @throws Exception
356      */
357     public String getDynamicJavascript() throws Exception
358     {
359         return getDynamicJavascript(this.formName);
360     }
361 
362 
363     /**
364      * Render just the static JavaScript methods. Useful i.e. if the static
365      * parts should be located in a seperate .js file.
366      *
367      * @return all static Javascript methods
368      * @throws Exception
369      */
370     public String getStaticJavascript() throws Exception
371     {
372         StringBuilder results = new StringBuilder();
373 
374         results.append(getStartElement());
375         if (this.htmlComment)
376         {
377             results.append(HTML_BEGIN_COMMENT);
378         }
379         results.append(getJavascriptStaticMethods(resources));
380         results.append(getJavascriptEnd());
381 
382         return results.toString();
383     }
384 
385 
386     /**
387      * Render just the dynamic JavaScript to perform validations based
388      * on the supplied form name. Useful i.e. if the static
389      * parts are located in a seperate .js file.
390      *
391      * @param formName the key (form name)
392      * @return the dynamic Javascript for the specified form
393      * @throws Exception
394      */
395     public String getDynamicJavascript(String formName) throws Exception
396     {
397         this.formName = formName;
398         return getJavascript(formName, false);
399     }
400 
401     /**
402      * Render both dynamic and static JavaScript to perform
403      * validations based on the supplied form name.
404      *
405      * @param formName the key (form name)
406      * @param getStatic indicates if the static methods should be rendered
407      * @return the Javascript for the specified form
408      * @throws Exception
409      */
410     protected String getJavascript(String formName, boolean getStatic) throws Exception
411     {
412         StringBuilder results = new StringBuilder();
413 
414         Locale locale = StrutsUtils.getLocale(request, session);
415 
416         Form form = resources.getForm(locale, formName);
417         if (form != null)
418         {
419             results.append(getDynamicJavascript(resources, locale, form));
420         }
421 
422         if(getStatic)
423         {
424             results.append(getJavascriptStaticMethods(resources));
425         }
426 
427         if (form != null)
428         {
429             results.append(getJavascriptEnd());
430         }
431 
432         return results.toString();
433     }
434 
435 
436 
437     /**
438      * Generates the dynamic JavaScript for the form.
439      *
440      * @param resources the validator resources
441      * @param locale the locale for the current request
442      * @param form the form to generate javascript for
443      * @return the dynamic javascript
444      */
445     protected String getDynamicJavascript(ValidatorResources resources,
446                                           Locale locale,
447                                           Form form)
448     {
449         StringBuilder results = new StringBuilder();
450 
451         MessageResources messages =
452             StrutsUtils.getMessageResources(request, app);
453 
454         List actions = createActionList(resources, form);
455 
456         final String methods = createMethods(actions);
457 
458         String formName = form.getName();
459 
460         jsFormName = formName;
461         if(jsFormName.charAt(0) == '/') {
462             String mappingName = StrutsUtils.getActionMappingName(jsFormName);
463             ModuleConfig mconfig = ModuleUtils.getInstance().getModuleConfig(request, app);
464 
465             ActionConfig mapping = (ActionConfig) mconfig.findActionConfig(mappingName);
466             if (mapping == null) {
467                 throw new NullPointerException("Cannot retrieve mapping for action " + mappingName);
468             }
469             jsFormName = mapping.getAttribute();
470         }
471 
472         results.append(getJavascriptBegin(methods));
473 
474         for (Iterator i = actions.iterator(); i.hasNext();)
475         {
476             ValidatorAction va = (ValidatorAction)i.next();
477             int jscriptVar = 0;
478             String functionName = null;
479 
480             if (va.getJsFunctionName() != null && va.getJsFunctionName().length() > 0)
481             {
482                 functionName = va.getJsFunctionName();
483             }
484             else
485             {
486                 functionName = va.getName();
487             }
488 
489             results.append("    function ");
490             results.append(jsFormName);
491             results.append("_");
492             results.append(functionName);
493             results.append(" () { \n");
494 
495             for (Iterator x = form.getFields().iterator(); x.hasNext();)
496             {
497                 Field field = (Field)x.next();
498 
499                 // Skip indexed fields for now until there is
500                 // a good way to handle error messages (and the length
501                 // of the list (could retrieve from scope?))
502                 if (field.isIndexed()
503                     || field.getPage() != page
504                     || !field.isDependency(va.getName()))
505                 {
506                     continue;
507                 }
508 
509                 String message = Resources.getMessage(app, request, messages,
510                                                       locale, va, field);
511 
512                 message = (message != null) ? message : "";
513 
514                 //jscriptVar = this.getNextVar(jscriptVar);
515 
516                 results.append("     this.a");
517                 results.append(jscriptVar++);
518                 results.append(" = new Array(\"");
519                 results.append(field.getKey()); // TODO: escape?
520                 results.append("\", \"");
521                 results.append(escapeJavascript(message));
522                 results.append("\", new Function (\"varName\", \"");
523 
524                 Map<String,Var> vars = (Map<String,Var>)field.getVars();
525                 // Loop through the field's variables.
526                 for (Map.Entry<String,Var> entry : vars.entrySet())
527                 {
528                     String varName = entry.getKey(); // TODO: escape?
529                     Var var = entry.getValue();
530                     String varValue =
531                         Resources.getVarValue(var, app, request, false);
532                     String jsType = var.getJsType();
533 
534                     // skip requiredif variables field, fieldIndexed, fieldTest, fieldValue
535                     if (varName.startsWith("field"))
536                     {
537                         continue;
538                     }
539 
540                     // these are appended no matter what jsType is
541                     results.append("this.");
542                     results.append(varName);
543 
544                     String escapedVarValue = escapeJavascript(varValue);
545 
546                     if (Var.JSTYPE_INT.equalsIgnoreCase(jsType))
547                     {
548                         results.append("=");
549                         results.append(escapedVarValue);
550                         results.append("; ");
551                     }
552                     else if (Var.JSTYPE_REGEXP.equalsIgnoreCase(jsType))
553                     {
554                         results.append("=/");
555                         results.append(escapedVarValue);
556                         results.append("/; ");
557                     }
558                     else if (Var.JSTYPE_STRING.equalsIgnoreCase(jsType))
559                     {
560                         results.append("='");
561                         results.append(escapedVarValue);
562                         results.append("'; ");
563                     }
564                     // So everyone using the latest format
565                     // doesn't need to change their xml files immediately.
566                     else if ("mask".equalsIgnoreCase(varName))
567                     {
568                         results.append("=/");
569                         results.append(escapedVarValue);
570                         results.append("/; ");
571                     }
572                     else
573                     {
574                         results.append("='");
575                         results.append(escapedVarValue);
576                         results.append("'; ");
577                     }
578                 }
579                 results.append(" return this[varName];\"));\n");
580             }
581             results.append("    } \n\n");
582         }
583         return results.toString();
584     }
585 
586 
587     /**
588      * <p>Backslash-escapes the following characters from the input string:
589      * &quot;, &apos;, \, \r, \n.</p>
590      *
591      * <p>This method escapes characters that will result in an invalid
592      * Javascript statement within the validator Javascript.</p>
593      *
594      * @param str The string to escape.
595      * @return The string <code>s</code> with each instance of a double quote,
596      *         single quote, backslash, carriage-return, or line feed escaped
597      *         with a leading backslash.
598      * @since VelocityTools 1.2
599      */
600     protected String escapeJavascript(String str)
601     {
602         if (str == null)
603         {
604             return null;
605         }
606         int length = str.length();
607         if (length == 0)
608         {
609             return str;
610         }
611 
612         // guess at how many chars we'll be adding...
613         StringBuilder out = new StringBuilder(length + 4);
614         // run through the string escaping sensitive chars
615         for (int i=0; i < length; i++)
616         {
617             char c = str.charAt(i);
618             if (c == '"'  ||
619                 c == '\'' ||
620                 c == '\\' ||
621                 c == '\n' ||
622                 c == '\r')
623             {
624                 out.append('\\');
625             }
626             out.append(c);
627         }
628         return out.toString();
629     }
630 
631 
632     /**
633      * Creates the JavaScript methods list from the given actions.
634      * @param actions A List of ValidatorAction objects.
635      * @return JavaScript methods.
636      */
637     protected String createMethods(List actions)
638     {
639         String methodOperator = " && ";
640 
641         StringBuilder methods = null;
642         for (Iterator i = actions.iterator(); i.hasNext();)
643         {
644             ValidatorAction va = (ValidatorAction)i.next();
645             if (methods == null)
646             {
647                 methods = new StringBuilder(va.getMethod());
648             }
649             else
650             {
651                 methods.append(methodOperator);
652                 methods.append(va.getMethod());
653             }
654             methods.append("(form)");
655         }
656         return methods.toString();
657     }
658 
659 
660     /**
661      * Get List of actions for the given Form.
662      *
663      * @param resources the validator resources
664      * @param form the form for which the actions are requested
665      * @return A sorted List of ValidatorAction objects.
666      */
667     protected List createActionList(ValidatorResources resources, Form form)
668     {
669         List actionMethods = new ArrayList();
670         // Get List of actions for this Form
671         for (Iterator i = form.getFields().iterator(); i.hasNext();)
672         {
673             Field field = (Field)i.next();
674             for (Iterator x = field.getDependencyList().iterator(); x.hasNext();)
675             {
676                 Object o = x.next();
677                 if (o != null && !actionMethods.contains(o))
678                 {
679                     actionMethods.add(o);
680                 }
681             }
682         }
683 
684         List actions = new ArrayList();
685 
686         // Create list of ValidatorActions based on actionMethods
687         for (Iterator i = actionMethods.iterator(); i.hasNext();)
688         {
689             String depends = (String) i.next();
690             ValidatorAction va = resources.getValidatorAction(depends);
691 
692             // throw nicer NPE for easier debugging
693             if (va == null)
694             {
695                 throw new NullPointerException(
696                     "Depends string \"" + depends +
697                     "\" was not found in validator-rules.xml.");
698             }
699 
700             String javascript = va.getJavascript();
701             if (javascript != null && javascript.length() > 0)
702             {
703                 actions.add(va);
704             }
705             else
706             {
707                 i.remove();
708             }
709         }
710 
711         Collections.sort(actions, actionComparator);
712         return actions;
713     }
714 
715 
716     /**
717      * Returns the opening script element and some initial javascript.
718      *
719      * @param methods javascript validation methods
720      * @return  the opening script element and some initial javascript
721      */
722     protected String getJavascriptBegin(String methods)
723     {
724         StringBuilder sb = new StringBuilder();
725         String name = jsFormName.replace('/', '_'); // remove any '/' characters
726         name = jsFormName.substring(0, 1).toUpperCase() +
727                       jsFormName.substring(1, jsFormName.length());
728 
729         sb.append(this.getStartElement());
730 
731         if (this.xhtml && this.cdata)
732         {
733             sb.append("<![CDATA[\r\n");
734         }
735 
736         if (!this.xhtml && this.htmlComment)
737         {
738             sb.append(HTML_BEGIN_COMMENT);
739         }
740         sb.append("\n     var bCancel = false; \n\n");
741 
742         if (methodName == null || methodName.length() == 0)
743         {
744             sb.append("    function validate");
745             sb.append(name);
746         }
747         else
748         {
749             sb.append("    function ");
750             sb.append(methodName);
751         }
752         sb.append("(form) {\n");
753         sb.append("      if (bCancel) \n");
754         sb.append("          return true; \n");
755         sb.append("      else \n");
756 
757         // Always return true if there aren't any Javascript validation methods
758         if (methods == null || methods.length() == 0)
759         {
760             sb.append("       return true; \n");
761         }
762         else
763         {
764             //Making Sure that Bitwise operator works:
765             sb.append(" var formValidationResult;\n");
766             sb.append("       formValidationResult = " + methods + "; \n");
767             sb.append("     return (formValidationResult == 1);\n");
768 
769         }
770         sb.append("   } \n\n");
771 
772         return sb.toString();
773     }
774 
775     /**
776      *
777      * @param resources the validation resources
778      * @return the static javascript methods
779      */
780     protected String getJavascriptStaticMethods(ValidatorResources resources)
781     {
782         StringBuilder sb = new StringBuilder("\n\n");
783 
784         Iterator actions = resources.getValidatorActions().values().iterator();
785         while (actions.hasNext())
786         {
787             ValidatorAction va = (ValidatorAction) actions.next();
788             if (va != null)
789             {
790                 String javascript = va.getJavascript();
791                 if (javascript != null && javascript.length() > 0)
792                 {
793                     sb.append(javascript);
794                     sb.append("\n");
795                 }
796             }
797         }
798         return sb.toString();
799     }
800 
801 
802     /**
803      * Returns the closing script element.
804      *
805      * @return the closing script element
806      */
807     protected String getJavascriptEnd()
808     {
809         StringBuilder sb = new StringBuilder();
810         sb.append("\n");
811 
812         if (!this.xhtml && this.htmlComment)
813         {
814             sb.append(HTML_END_COMMENT);
815         }
816 
817         if (this.xhtml && this.cdata)
818         {
819             sb.append("]]>\r\n");
820         }
821         sb.append("</script>\n\n");
822 
823         return sb.toString();
824     }
825 
826 
827     /**
828      * Constructs the beginning <script> element depending on xhtml status.
829      *
830      * @return the beginning <script> element depending on xhtml status
831      */
832     private String getStartElement()
833     {
834         StringBuilder start = new StringBuilder("<script type=\"text/javascript\"");
835 
836         // there is no language attribute in xhtml
837         if (!this.xhtml)
838         {
839             start.append(" language=\"Javascript1.1\"");
840         }
841 
842         if (this.src != null)
843         {
844             start.append(" src=\"" + src + "\"");
845         }
846 
847         start.append("> \n");
848         return start.toString();
849     }
850 
851 }