001: package org.apache.velocity.tools.struts;
002:
003: /*
004: * Licensed to the Apache Software Foundation (ASF) under one
005: * or more contributor license agreements. See the NOTICE file
006: * distributed with this work for additional information
007: * regarding copyright ownership. The ASF licenses this file
008: * to you under the Apache License, Version 2.0 (the
009: * "License"); you may not use this file except in compliance
010: * with the License. You may obtain a copy of the License at
011: *
012: * http://www.apache.org/licenses/LICENSE-2.0
013: *
014: * Unless required by applicable law or agreed to in writing,
015: * software distributed under the License is distributed on an
016: * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
017: * KIND, either express or implied. See the License for the
018: * specific language governing permissions and limitations
019: * under the License.
020: */
021:
022: import java.util.ArrayList;
023: import java.util.Collections;
024: import java.util.Comparator;
025: import java.util.Iterator;
026: import java.util.List;
027: import java.util.Locale;
028: import java.util.Map;
029:
030: import javax.servlet.ServletContext;
031: import javax.servlet.http.HttpServletRequest;
032: import javax.servlet.http.HttpSession;
033:
034: import org.apache.commons.validator.Field;
035: import org.apache.commons.validator.Form;
036: import org.apache.commons.validator.ValidatorAction;
037: import org.apache.commons.validator.ValidatorResources;
038: import org.apache.commons.validator.Var;
039: import org.apache.struts.Globals;
040: import org.apache.struts.config.ActionConfig;
041: import org.apache.struts.config.ModuleConfig;
042: import org.apache.struts.util.MessageResources;
043: import org.apache.struts.util.ModuleUtils;
044: import org.apache.struts.validator.Resources;
045: import org.apache.struts.validator.ValidatorPlugIn;
046: import org.apache.velocity.tools.view.context.ViewContext;
047:
048: /**
049: * <p>View tool that works with Struts Validator to
050: * produce client side javascript validation for your forms.</p>
051: * <p>Usage:
052: * <pre>
053: * Template example:
054: *
055: * $validator.getJavascript("nameOfYourForm")
056: *
057: * Toolbox configuration:
058: * <tool>
059: * <key>validator</key>
060: * <scope>request</scope>
061: * <class>org.apache.velocity.tools.struts.ValidatorTool</class>
062: * </tool>
063: * </pre>
064: * </p>
065: * <p>This is an adaptation of the JavascriptValidatorTag
066: * from the Struts 1.1 validator library.</p>
067: *
068: * @author David Winterfeldt
069: * @author David Graham
070: * @author <a href="mailto:marinoj@centrum.is">Marino A. Jonsson</a>
071: * @author Nathan Bubna
072: * @since VelocityTools 1.1
073: * @version $Revision: 497643 $ $Date: 2007-01-18 15:54:14 -0800 (Thu, 18 Jan 2007) $
074: */
075: public class ValidatorTool {
076:
077: /** A reference to the ViewContext */
078: protected ViewContext context;
079:
080: /** A reference to the ServletContext */
081: protected ServletContext app;
082:
083: /** A reference to the HttpServletRequest. */
084: protected HttpServletRequest request;
085:
086: /** A reference to the HttpSession. */
087: protected HttpSession session;
088:
089: /** A reference to the ValidatorResources. */
090: protected ValidatorResources resources;
091:
092: private static final String HTML_BEGIN_COMMENT = "\n<!-- Begin \n";
093: private static final String HTML_END_COMMENT = "//End --> \n";
094:
095: private boolean xhtml = false;
096:
097: private boolean htmlComment = true;
098: private boolean cdata = true;
099: private String formName = null;
100: private String methodName = null;
101: private String src = null;
102: private int page = 0;
103: /**
104: * formName is used for both Javascript and non-javascript validations.
105: * For the javascript validations, there is the possibility that we will
106: * be rewriting the formName (if it is a ValidatorActionForm instead of just
107: * a ValidatorForm) so we need another variable to hold the formName just for
108: * javascript usage.
109: */
110: protected String jsFormName = null;
111:
112: /**
113: * A Comparator to use when sorting ValidatorAction objects.
114: */
115: private static final Comparator actionComparator = new Comparator() {
116: public int compare(Object o1, Object o2) {
117:
118: ValidatorAction va1 = (ValidatorAction) o1;
119: ValidatorAction va2 = (ValidatorAction) o2;
120:
121: if ((va1.getDepends() == null || va1.getDepends().length() == 0)
122: && (va2.getDepends() == null || va2.getDepends()
123: .length() == 0)) {
124: return 0;
125:
126: } else if ((va1.getDepends() != null && va1.getDepends()
127: .length() > 0)
128: && (va2.getDepends() == null || va2.getDepends()
129: .length() == 0)) {
130: return 1;
131:
132: } else if ((va1.getDepends() == null || va1.getDepends()
133: .length() == 0)
134: && (va2.getDepends() != null && va2.getDepends()
135: .length() > 0)) {
136: return -1;
137:
138: } else {
139: return va1.getDependencyList().size()
140: - va2.getDependencyList().size();
141: }
142: }
143: };
144:
145: /**
146: * Default constructor. Tool must be initialized before use.
147: */
148: public ValidatorTool() {
149: }
150:
151: /**
152: * Initializes this tool.
153: *
154: * @param obj the current ViewContext
155: * @throws IllegalArgumentException if the param is not a ViewContext
156: */
157: public void init(Object obj) {
158: if (!(obj instanceof ViewContext)) {
159: throw new IllegalArgumentException(
160: "Tool can only be initialized with a ViewContext");
161: }
162:
163: this .context = (ViewContext) obj;
164: this .request = context.getRequest();
165: this .session = request.getSession(false);
166: this .app = context.getServletContext();
167:
168: Boolean b = (Boolean) context.getAttribute(ViewContext.XHTML);
169: if (b != null) {
170: this .xhtml = b.booleanValue();
171: }
172:
173: /* Is there a mapping associated with this request? */
174: ActionConfig config = (ActionConfig) request
175: .getAttribute(Globals.MAPPING_KEY);
176: if (config != null) {
177: /* Is there a form bean associated with this mapping? */
178: this .formName = config.getAttribute();
179: }
180:
181: ModuleConfig mconfig = ModuleUtils.getInstance()
182: .getModuleConfig(request, app);
183: this .resources = (ValidatorResources) app
184: .getAttribute(ValidatorPlugIn.VALIDATOR_KEY
185: + mconfig.getPrefix());
186:
187: }
188:
189: /****************** get/set accessors ***************/
190:
191: /**
192: * Gets the current page number of a multi-part form.
193: * Only field validations with a matching page number
194: * will be generated that match the current page number.
195: * Only valid when the formName attribute is set.
196: *
197: * @return the current page number of a multi-part form
198: */
199: public int getPage() {
200: return page;
201: }
202:
203: /**
204: * Sets the current page number of a multi-part form.
205: * Only field validations with a matching page number
206: * will be generated that match the current page number.
207: *
208: * @param page the current page number of a multi-part form
209: */
210: public void setPage(int page) {
211: this .page = page;
212: }
213:
214: /**
215: * Gets the method name that will be used for the Javascript
216: * validation method name if it has a value. This overrides
217: * the auto-generated method name based on the key (form name)
218: * passed in.
219: *
220: * @return the method name that will be used for the Javascript validation method
221: */
222: public String getMethod() {
223: return methodName;
224: }
225:
226: /**
227: * Sets the method name that will be used for the Javascript
228: * validation method name if it has a value. This overrides
229: * the auto-generated method name based on the key (form name)
230: * passed in.
231: *
232: * @param methodName the method name that will be used for the Javascript validation method name
233: */
234: public void setMethod(String methodName) {
235: this .methodName = methodName;
236: }
237:
238: /**
239: * Gets whether or not to delimit the
240: * JavaScript with html comments. If this is set to 'true', which
241: * is the default, html comments will surround the JavaScript.
242: *
243: * @return true if the JavaScript should be delimited with html comments
244: */
245: public boolean getHtmlComment() {
246: return this .htmlComment;
247: }
248:
249: /**
250: * Sets whether or not to delimit the
251: * JavaScript with html comments. If this is set to 'true', which
252: * is the default, html comments will surround the JavaScript.
253: *
254: * @param htmlComment whether or not to delimit the JavaScript with html comments
255: */
256: public void setHtmlComment(boolean htmlComment) {
257: this .htmlComment = htmlComment;
258: }
259:
260: /**
261: * Gets the src attribute's value when defining
262: * the html script element.
263: *
264: * @return the src attribute's value
265: */
266: public String getSrc() {
267: return src;
268: }
269:
270: /**
271: * Sets the src attribute's value (used to include
272: * an external script resource) when defining
273: * the html script element. The src attribute is only recognized
274: * when the formName attribute is specified.
275: *
276: * @param src the src attribute's value
277: */
278: public void setSrc(String src) {
279: this .src = src;
280: }
281:
282: /**
283: * Returns the cdata setting "true" or "false".
284: *
285: * @return boolean - "true" if JavaScript will be hidden in a CDATA section
286: */
287: public boolean getCdata() {
288: return cdata;
289: }
290:
291: /**
292: * Sets the cdata status.
293: * @param cdata The cdata to set
294: */
295: public void setCdata(boolean cdata) {
296: this .cdata = cdata;
297: }
298:
299: /****************** methods that aren't just accessors ***************/
300:
301: /**
302: * Render both dynamic and static JavaScript to perform
303: * validations based on the form name attribute of the action
304: * mapping associated with the current request (if such exists).
305: *
306: * @return the javascript for the current form
307: * @throws Exception
308: */
309: public String getJavascript() throws Exception {
310: return getJavascript(this .formName);
311: }
312:
313: /**
314: * Render both dynamic and static JavaScript to perform
315: * validations based on the supplied form name.
316: *
317: * @param formName the key (form name)
318: * @return the Javascript for the specified form
319: * @throws Exception
320: */
321: public String getJavascript(String formName) throws Exception {
322: this .formName = formName;
323: return getJavascript(formName, true);
324: }
325:
326: /**
327: * Render just the dynamic JavaScript to perform validations based
328: * on the form name attribute of the action mapping associated
329: * with the current request (if such exists). Useful i.e. if the static
330: * parts are located in a seperate .js file.
331: *
332: * @return the javascript for the current form
333: * @throws Exception
334: */
335: public String getDynamicJavascript() throws Exception {
336: return getDynamicJavascript(this .formName);
337: }
338:
339: /**
340: * Render just the static JavaScript methods. Useful i.e. if the static
341: * parts should be located in a seperate .js file.
342: *
343: * @return all static Javascript methods
344: * @throws Exception
345: */
346: public String getStaticJavascript() throws Exception {
347: StringBuffer results = new StringBuffer();
348:
349: results.append(getStartElement());
350: if (this .htmlComment) {
351: results.append(HTML_BEGIN_COMMENT);
352: }
353: results.append(getJavascriptStaticMethods(resources));
354: results.append(getJavascriptEnd());
355:
356: return results.toString();
357: }
358:
359: /**
360: * Render just the dynamic JavaScript to perform validations based
361: * on the supplied form name. Useful i.e. if the static
362: * parts are located in a seperate .js file.
363: *
364: * @param formName the key (form name)
365: * @return the dynamic Javascript for the specified form
366: * @throws Exception
367: */
368: public String getDynamicJavascript(String formName)
369: throws Exception {
370: this .formName = formName;
371: return getJavascript(formName, false);
372: }
373:
374: /**
375: * Render both dynamic and static JavaScript to perform
376: * validations based on the supplied form name.
377: *
378: * @param formName the key (form name)
379: * @param getStatic indicates if the static methods should be rendered
380: * @return the Javascript for the specified form
381: * @throws Exception
382: */
383: protected String getJavascript(String formName, boolean getStatic)
384: throws Exception {
385: StringBuffer results = new StringBuffer();
386:
387: Locale locale = StrutsUtils.getLocale(request, session);
388:
389: Form form = resources.getForm(locale, formName);
390: if (form != null) {
391: results
392: .append(getDynamicJavascript(resources, locale,
393: form));
394: }
395:
396: if (getStatic) {
397: results.append(getJavascriptStaticMethods(resources));
398: }
399:
400: if (form != null) {
401: results.append(getJavascriptEnd());
402: }
403:
404: return results.toString();
405: }
406:
407: /**
408: * Generates the dynamic JavaScript for the form.
409: *
410: * @param resources the validator resources
411: * @param locale the locale for the current request
412: * @param form the form to generate javascript for
413: * @return the dynamic javascript
414: */
415: protected String getDynamicJavascript(ValidatorResources resources,
416: Locale locale, Form form) {
417: StringBuffer results = new StringBuffer();
418:
419: MessageResources messages = StrutsUtils.getMessageResources(
420: request, app);
421:
422: List actions = createActionList(resources, form);
423:
424: final String methods = createMethods(actions);
425:
426: String formName = form.getName();
427:
428: jsFormName = formName;
429: if (jsFormName.charAt(0) == '/') {
430: String mappingName = StrutsUtils
431: .getActionMappingName(jsFormName);
432: ModuleConfig mconfig = ModuleUtils.getInstance()
433: .getModuleConfig(request, app);
434:
435: ActionConfig mapping = (ActionConfig) mconfig
436: .findActionConfig(mappingName);
437: if (mapping == null) {
438: throw new NullPointerException(
439: "Cannot retrieve mapping for action "
440: + mappingName);
441: }
442: jsFormName = mapping.getAttribute();
443: }
444:
445: results.append(getJavascriptBegin(methods));
446:
447: for (Iterator i = actions.iterator(); i.hasNext();) {
448: ValidatorAction va = (ValidatorAction) i.next();
449: int jscriptVar = 0;
450: String functionName = null;
451:
452: if (va.getJsFunctionName() != null
453: && va.getJsFunctionName().length() > 0) {
454: functionName = va.getJsFunctionName();
455: } else {
456: functionName = va.getName();
457: }
458:
459: results.append(" function ");
460: results.append(jsFormName);
461: results.append("_");
462: results.append(functionName);
463: results.append(" () { \n");
464:
465: for (Iterator x = form.getFields().iterator(); x.hasNext();) {
466: Field field = (Field) x.next();
467:
468: // Skip indexed fields for now until there is
469: // a good way to handle error messages (and the length
470: // of the list (could retrieve from scope?))
471: if (field.isIndexed() || field.getPage() != page
472: || !field.isDependency(va.getName())) {
473: continue;
474: }
475:
476: String message = Resources.getMessage(app, request,
477: messages, locale, va, field);
478:
479: message = (message != null) ? message : "";
480:
481: //jscriptVar = this.getNextVar(jscriptVar);
482:
483: results.append(" this.a");
484: results.append(jscriptVar++);
485: results.append(" = new Array(\"");
486: results.append(field.getKey()); // TODO: escape?
487: results.append("\", \"");
488: results.append(escapeJavascript(message));
489: results.append("\", ");
490: results.append("new Function (\"varName\", \"");
491:
492: Map vars = field.getVars();
493: // Loop through the field's variables.
494: Iterator varsIterator = vars.keySet().iterator();
495: while (varsIterator.hasNext()) {
496: String varName = (String) varsIterator.next(); // TODO: escape?
497: Var var = (Var) vars.get(varName);
498: String varValue = Resources.getVarValue(var, app,
499: request, false);
500: String jsType = var.getJsType();
501:
502: // skip requiredif variables field, fieldIndexed, fieldTest, fieldValue
503: if (varName.startsWith("field")) {
504: continue;
505: }
506:
507: // these are appended no matter what jsType is
508: results.append("this.");
509: results.append(varName);
510:
511: String escapedVarValue = escapeJavascript(varValue);
512:
513: if (Var.JSTYPE_INT.equalsIgnoreCase(jsType)) {
514: results.append("=");
515: results.append(escapedVarValue);
516: results.append("; ");
517: } else if (Var.JSTYPE_REGEXP
518: .equalsIgnoreCase(jsType)) {
519: results.append("=/");
520: results.append(escapedVarValue);
521: results.append("/; ");
522: } else if (Var.JSTYPE_STRING
523: .equalsIgnoreCase(jsType)) {
524: results.append("='");
525: results.append(escapedVarValue);
526: results.append("'; ");
527: }
528: // So everyone using the latest format
529: // doesn't need to change their xml files immediately.
530: else if ("mask".equalsIgnoreCase(varName)) {
531: results.append("=/");
532: results.append(escapedVarValue);
533: results.append("/; ");
534: } else {
535: results.append("='");
536: results.append(escapedVarValue);
537: results.append("'; ");
538: }
539: }
540: results.append(" return this[varName];\"));\n");
541: }
542: results.append(" } \n\n");
543: }
544: return results.toString();
545: }
546:
547: /**
548: * <p>Backslash-escapes the following characters from the input string:
549: * ", ', \, \r, \n.</p>
550: *
551: * <p>This method escapes characters that will result in an invalid
552: * Javascript statement within the validator Javascript.</p>
553: *
554: * @param str The string to escape.
555: * @return The string <code>s</code> with each instance of a double quote,
556: * single quote, backslash, carriage-return, or line feed escaped
557: * with a leading backslash.
558: * @since VelocityTools 1.2
559: */
560: protected String escapeJavascript(String str) {
561: if (str == null) {
562: return null;
563: }
564: int length = str.length();
565: if (length == 0) {
566: return str;
567: }
568:
569: // guess at how many chars we'll be adding...
570: StringBuffer out = new StringBuffer(length + 4);
571: // run through the string escaping sensitive chars
572: for (int i = 0; i < length; i++) {
573: char c = str.charAt(i);
574: if (c == '"' || c == '\'' || c == '\\' || c == '\n'
575: || c == '\r') {
576: out.append('\\');
577: }
578: out.append(c);
579: }
580: return out.toString();
581: }
582:
583: /**
584: * Creates the JavaScript methods list from the given actions.
585: * @param actions A List of ValidatorAction objects.
586: * @return JavaScript methods.
587: */
588: protected String createMethods(List actions) {
589: String methodOperator = " && ";
590:
591: StringBuffer methods = null;
592: for (Iterator i = actions.iterator(); i.hasNext();) {
593: ValidatorAction va = (ValidatorAction) i.next();
594: if (methods == null) {
595: methods = new StringBuffer(va.getMethod());
596: } else {
597: methods.append(methodOperator);
598: methods.append(va.getMethod());
599: }
600: methods.append("(form)");
601: }
602: return methods.toString();
603: }
604:
605: /**
606: * Get List of actions for the given Form.
607: *
608: * @param resources the validator resources
609: * @param form the form for which the actions are requested
610: * @return A sorted List of ValidatorAction objects.
611: */
612: protected List createActionList(ValidatorResources resources,
613: Form form) {
614: List actionMethods = new ArrayList();
615: // Get List of actions for this Form
616: for (Iterator i = form.getFields().iterator(); i.hasNext();) {
617: Field field = (Field) i.next();
618: for (Iterator x = field.getDependencyList().iterator(); x
619: .hasNext();) {
620: Object o = x.next();
621: if (o != null && !actionMethods.contains(o)) {
622: actionMethods.add(o);
623: }
624: }
625: }
626:
627: List actions = new ArrayList();
628:
629: // Create list of ValidatorActions based on actionMethods
630: for (Iterator i = actionMethods.iterator(); i.hasNext();) {
631: String depends = (String) i.next();
632: ValidatorAction va = resources.getValidatorAction(depends);
633:
634: // throw nicer NPE for easier debugging
635: if (va == null) {
636: throw new NullPointerException("Depends string \""
637: + depends
638: + "\" was not found in validator-rules.xml.");
639: }
640:
641: String javascript = va.getJavascript();
642: if (javascript != null && javascript.length() > 0) {
643: actions.add(va);
644: } else {
645: i.remove();
646: }
647: }
648:
649: Collections.sort(actions, actionComparator);
650: return actions;
651: }
652:
653: /**
654: * Returns the opening script element and some initial javascript.
655: *
656: * @param methods javascript validation methods
657: * @return the opening script element and some initial javascript
658: */
659: protected String getJavascriptBegin(String methods) {
660: StringBuffer sb = new StringBuffer();
661: String name = jsFormName.replace('/', '_'); // remove any '/' characters
662: name = jsFormName.substring(0, 1).toUpperCase()
663: + jsFormName.substring(1, jsFormName.length());
664:
665: sb.append(this .getStartElement());
666:
667: if (this .xhtml && this .cdata) {
668: sb.append("<![CDATA[\r\n");
669: }
670:
671: if (!this .xhtml && this .htmlComment) {
672: sb.append(HTML_BEGIN_COMMENT);
673: }
674: sb.append("\n var bCancel = false; \n\n");
675:
676: if (methodName == null || methodName.length() == 0) {
677: sb.append(" function validate");
678: sb.append(name);
679: } else {
680: sb.append(" function ");
681: sb.append(methodName);
682: }
683: sb.append("(form) {");
684: //FIXME? anyone know why all these spaces need to be here?
685: sb
686: .append(" \n");
687: sb.append(" if (bCancel) \n");
688: sb.append(" return true; \n");
689: sb.append(" else \n");
690:
691: // Always return true if there aren't any Javascript validation methods
692: if (methods == null || methods.length() == 0) {
693: sb.append(" return true; \n");
694: } else {
695: //Making Sure that Bitwise operator works:
696: sb.append(" var formValidationResult;\n");
697: sb.append(" formValidationResult = " + methods
698: + "; \n");
699: sb.append(" return (formValidationResult == 1);\n");
700:
701: }
702: sb.append(" } \n\n");
703:
704: return sb.toString();
705: }
706:
707: /**
708: *
709: * @param resources the validation resources
710: * @return the static javascript methods
711: */
712: protected String getJavascriptStaticMethods(
713: ValidatorResources resources) {
714: StringBuffer sb = new StringBuffer("\n\n");
715:
716: Iterator actions = resources.getValidatorActions().values()
717: .iterator();
718: while (actions.hasNext()) {
719: ValidatorAction va = (ValidatorAction) actions.next();
720: if (va != null) {
721: String javascript = va.getJavascript();
722: if (javascript != null && javascript.length() > 0) {
723: sb.append(javascript + "\n");
724: }
725: }
726: }
727: return sb.toString();
728: }
729:
730: /**
731: * Returns the closing script element.
732: *
733: * @return the closing script element
734: */
735: protected String getJavascriptEnd() {
736: StringBuffer sb = new StringBuffer();
737: sb.append("\n");
738:
739: if (!this .xhtml && this .htmlComment) {
740: sb.append(HTML_END_COMMENT);
741: }
742:
743: if (this .xhtml && this .cdata) {
744: sb.append("]]>\r\n");
745: }
746: sb.append("</script>\n\n");
747:
748: return sb.toString();
749: }
750:
751: /**
752: * Constructs the beginning <script> element depending on xhtml status.
753: *
754: * @return the beginning <script> element depending on xhtml status
755: */
756: private String getStartElement() {
757: StringBuffer start = new StringBuffer(
758: "<script type=\"text/javascript\"");
759:
760: // there is no language attribute in xhtml
761: if (!this .xhtml) {
762: start.append(" language=\"Javascript1.1\"");
763: }
764:
765: if (this .src != null) {
766: start.append(" src=\"" + src + "\"");
767: }
768:
769: start.append("> \n");
770: return start.toString();
771: }
772:
773: }
|