Hi Struts developers, By now, Struts doesn't generate any Javascript validation function for indexed properties. Since I wasn�t able to find a solution and I implemented my own solution, I would like to share my thoughts with you.
I tried to find a clue about this issue on the Internet and on following Struts books: Manning - XDoclet in Action Wiley Publishing - Mastering Jakarta Struts Oreilly - Programming Jakarta Struts Wrox Press - Professional Jakarta Struts Morgan Kaufmann 2003 - The Struts Framework I found a single explication related to this sourcecode comment in the org.apache.struts.taglib.html.JavascriptValidatorTag class: // Skip indexed fields for now until there is a good way to handle // error messages (and the length of the list (could retrieve from scope?)) Is there any reason why this feature is not implemented yet? Please contact me if there is one... I am curious :o) Here's the solution that I implemented: To add this feature (when using no page attribute in validation.xml), I extend the <html:javascript /> tag to <myhtml:javascript /> tag. It is using the same TLD definition. The implementation class of <myhtml:javascript /> tag extends (of course :o)) the org.apache.struts.taglib.html.JavascriptValidatorTag class. I'm using the 1.44 version of this class, but my implementation should be working with the 1.51 version. http://cvs.apache.org/viewcvs.cgi/jakarta-struts/src/share/org/apache/struts/taglib/html/JavascriptValidatorTag.java?rev=1.44&view=markup Thus, the problem is the size of the indexed property that we want to validate (if no page defined). I choose this following implementation logic: - we can define a bean in the page scope called "indexedListProperty"_size where the indexedListProperty is the value of this attribute in the validation.xml file. This bean has the size of the indexed list property. Then, indexedListProperty attribute is mandatory in the validation.xml file. - Another way to find the size of the list is to get the size from the indexed list property of the beanform defined in the request scope. Of course, this indexed list property should be instantiated (in the action normally). Here is an example of my implementation (example is worth a thousand words :o): Java beanForm class: public class MyObjectForm extends ValidatorForm implements Serializable { List myIndexedProperty = new ArrayList(); // list of MyLineObjectForm object public MyLineObjectForm getMyLineObjectForm(int i) { // This a patch for Apache Commons BeanUtils to prevent java.lang.ArrayIndexOutOfBoundsException // when sending information from the client side to the server side when the list has no size defining // (cant using reset method of the action) // Best way? while (myIndexedProperty.size() < i + 1) { myIndexedProperty.add(new MyLineObjectForm()); } return (MyLineObjectForm)myIndexedProperty.get(i); } public void setMyLineObjectForm(int i, MyLineObjectForm form) { sampleForms_.add(i, form); } // Using to get and set the indexed property list in the action // Not mandatory in the case of static indexedProperty public List getMyLineObjectForms() { return myIndexedProperty; } public void setMyLineObjectForms(List forms) { myIndexedProperty = forms; } } public class MyLineObjectForm extends ValidatorForm implements Serializable { Float value = null; // Prefer using Float type that String for my example public Float getValue() { return value; } public void setValue(Float value) { value = value; } } In the validation.xml, we have: <form name="myObjectForm"> <field indexedListProperty="myLineObjectForms" <!-- Mandatory if using <bean:size /> tag in the page scope --> indexedProperty="myLineObjectForm" <!-- NOT Mandatory --> property="value" <!-- Mandatory --> depends="float"> <msg name="bounds" key="errors.float2args"/> <arg0 key="javascript.myLineObjectForm.value.text"/> <arg1 name="floatRange" key="${var:min}" resource="false"/> <arg2 name="floatRange" key="${var:max}" resource="false"/> <var> <var-name>min</var-name> <var-value>0.0</var-value> </var> <var> <var-name>max</var-name> <var-value>100000.0</var-value> </var> </field> </form> In the JSP file, we can call the validator in two ways: <html:form method="POST" action="myObjectForm" onsubmit="return validateMyObjectForm(this);"> First way: Supposed that "listing" bean is already defined in the page scope and its type is a java.util.List. <bean:size id="myLineObjectForms_size" collection="listing" /> OR <bean:size id="myLineObjectForms_size" name="listing" /> <myhtml:javascript formName="myObjectForm" cdata="false" dynamicJavascript="true" staticJavascript="false" /> <script type="text/javascript" src="<html:rewrite page='/js/validator.jsp'/>" > </script> Other way: <myhtml:javascript formName="myObjectForm" cdata="false" dynamicJavascript="true" staticJavascript="false" /> <script type="text/javascript" src="<html:rewrite page='/js/validator.jsp'/>" > </script> </html:form> The validator.jsp definition is: <%@ page language="java" contentType="javascript/x-javascript" %> <%@ taglib uri="/WEB-INF/tld/struts-html.tld" prefix="html" %> <html:javascript dynamicJavascript="false" staticJavascript="true"/> So, if the bean myLineObjectForms_size is defined in the page scope or if the property myObjectForm.myLineObjectForms is instantiated in the action class, then myLineObjectForms.size exist. In the generated JSP, we should have this Javascript definition: function FloatValidations () { this.a0 = new Array("myLineObjectForm[0].value", "'Value' must be a float.", new Function ("varName", "this.min='0.0'; this.max='100000.0'; return this[varName];")); this.a1 = new Array("myLineObjectForm[1].value", "'Value' must be a float.", new Function ("varName", "this.min='0.0'; this.max='100000.0'; return this[varName];")); this.a2 = new Array("myLineObjectForm[2].value", "'Value' must be a float.", new Function ("varName", "this.min='0.0'; this.max='100000.0'; return this[varName];")); # and so on } That�s all folks! Feel free to e-mail me with any questions or comments you have about my implementation. Vincent Siveton [EMAIL PROTECTED] ================================================== JavascriptValidatorTag class ================================================== import java.io.IOException; import java.lang.reflect.InvocationTargetException; import java.util.ArrayList; import java.util.Collections; import java.util.Comparator; import java.util.Iterator; import java.util.List; import java.util.Locale; import java.util.Map; import javax.servlet.jsp.JspException; import javax.servlet.jsp.JspWriter; import javax.servlet.jsp.PageContext; import org.apache.commons.beanutils.BeanUtils; import org.apache.commons.validator.Field; import org.apache.commons.validator.Form; import org.apache.commons.validator.ValidatorAction; import org.apache.commons.validator.ValidatorResources; import org.apache.commons.validator.Var; import org.apache.commons.validator.util.ValidatorUtils; import org.apache.struts.Globals; import org.apache.struts.action.ActionForm; import org.apache.struts.action.ActionMapping; import org.apache.struts.config.ModuleConfig; import org.apache.struts.taglib.TagUtils; import org.apache.struts.util.MessageResources; import org.apache.struts.validator.Resources; import org.apache.struts.validator.ValidatorPlugIn; import ca.mcgill.genome.util.StringUtil; /** * Custom tag that generates JavaScript for client side validation based on the validation * rules loaded by the <code>ValidatorPlugIn</code> defined in the struts-config.xml file. * * It's an extension of the <code>org.apache.struts.taglib.html.JavascriptValidatorTag</code> * * @see http://cvs.apache.org/viewcvs.cgi/jakarta-struts/src/share/org/apache/struts/taglib/html/JavascriptValidatorTag.java v 1.44 * * @author vsiveton * @version 1.0 * * Date Author Changes */ public class JavascriptValidatorTag extends org.apache.struts.taglib.html.JavascriptValidatorTag { private static org.apache.log4j.Logger log = org.apache.log4j.Logger.getLogger(JavascriptValidatorTag.class); /** * A Comparator to use when sorting ValidatorAction objects. */ private static final Comparator actionComparator = new Comparator() { public int compare(Object o1, Object o2) { ValidatorAction va1 = (ValidatorAction) o1; ValidatorAction va2 = (ValidatorAction) o2; if ((va1.getDepends() == null || va1.getDepends().length() == 0) && (va2.getDepends() == null || va2.getDepends().length() == 0)) { return 0; } else if ((va1.getDepends() != null && va1.getDepends().length() > 0) && (va2.getDepends() == null || va2.getDepends().length() == 0)) { return 1; } else if ((va1.getDepends() == null || va1.getDepends().length() == 0) && (va2.getDepends() != null && va2.getDepends().length() > 0)) { return -1; } else { return va1.getDependencyList().size() - va2.getDependencyList().size(); } } }; /** * Render the JavaScript for to perform validations based on the form name. * * @exception JspException if a JSP exception has occurred */ public int doStartTag() throws JspException { JspWriter writer = pageContext.getOut(); try { writer.print(this.newRenderJavascript()); } catch (IOException e) { throw new JspException(e.getMessage()); } return EVAL_BODY_TAG; } // TODO Rename me! /** * Returns fully rendered JavaScript. * * Should be rename to renderJavascript() * The renderJavascript() method in our Struts release doesnt throws JspException * * @since Struts 1.2 * @throws JspException */ protected String newRenderJavascript() throws JspException { StringBuffer results = new StringBuffer(); ModuleConfig config = TagUtils.getInstance().getModuleConfig(pageContext); ValidatorResources resources = (ValidatorResources) pageContext.getAttribute(ValidatorPlugIn.VALIDATOR_KEY + config.getPrefix(), PageContext.APPLICATION_SCOPE); Locale locale = TagUtils.getInstance().getUserLocale(this.pageContext, null); Form form = resources.getForm(locale, formName); if (form != null) { if ("true".equalsIgnoreCase(dynamicJavascript)) { results.append(this.createDynamicJavascript(config, resources, locale, form)); } else if ("true".equalsIgnoreCase(staticJavascript)) { results.append(this.renderStartElement()); if ("true".equalsIgnoreCase(htmlComment)) { results.append(HTML_BEGIN_COMMENT); } } } if ("true".equalsIgnoreCase(staticJavascript)) { results.append(getJavascriptStaticMethods(resources)); } if (form != null && ("true".equalsIgnoreCase(dynamicJavascript) || "true".equalsIgnoreCase(staticJavascript))) { results.append(getJavascriptEnd()); } return results.toString(); } /** * Generates the dynamic JavaScript for the form. * @see org.apache.struts.taglib.html.JavascriptValidatorTag.createDynamicJavascript() * * In the case of indexed property, the size of each indexed property is evaluate in first from * a page bean in the page scope called : * "myIndexedProperty_size" where myIndexedProperty is an attribute defined in the validation.xml * * @param config * @param resources * @param locale * @param form */ private String createDynamicJavascript(ModuleConfig config, ValidatorResources resources, Locale locale, Form form) throws JspException { StringBuffer results = new StringBuffer(); if (form == null) { throw new JspException("No Form defined"); } MessageResources messages = (MessageResources) pageContext.getAttribute(bundle + config.getPrefix(), PageContext.APPLICATION_SCOPE); List actions = this.createActionList(resources, form); final String methods = this.createMethods(actions, this.stopOnError(config)); results.append(this.getJavascriptBegin(methods)); for (Iterator i = actions.iterator(); i.hasNext();) { ValidatorAction va = (ValidatorAction) i.next(); int jscriptVar = 0; String functionName = null; if (va.getJsFunctionName() != null && va.getJsFunctionName().length() > 0) { functionName = va.getJsFunctionName(); } else { functionName = va.getName(); } results.append(" function " + functionName + " () { \n"); for (Iterator x = form.getFields().iterator(); x.hasNext();) { Field field = (Field) x.next(); // Skipped if (field.getPage() != page || !field.isDependency(va.getName())) { continue; } String message = Resources.getMessage(messages, locale, va, field); message = (message != null) ? message : ""; // Field attributes definition from the validation.xml String fieldProperty = field.getProperty(); // Mandatory if (isEmptyString(fieldProperty)) { throw new JspException( "No definition in validation.xml for attribute key 'property' in the form attribute [" + form.getName() + "] and the field attribute [" + field + "]"); } // Update org.apache.struts.taglib.html.JavascriptValidatorTag.createDynamicJavascript() if (!field.isIndexed()) { // Current beanForm from request scope ActionForm actionForm = (ActionForm) pageContext.getRequest().getAttribute(form.getName()); if (actionForm == null) { throw new JspException("No bean found for attribute key [" + form.getName() + "] in request scope"); } // Only used to check if the the fieldProperty exists in the beanForm. // Use to prevent JS error try { BeanUtils.getProperty(actionForm, fieldProperty); } catch (IllegalAccessException e) { throw new JspException( "IllegalAccessException for the getter method of the property [" + fieldProperty + "] message=[" + e.getMessage() + "]"); } catch (InvocationTargetException e) { throw new JspException( "InvocationTargetException for the getter method of the property [" + fieldProperty + "] message=[" + e.getMessage() + "]"); } catch (NoSuchMethodException e) { throw new JspException("No such getter method found for the property [" + fieldProperty + "] message=[" + e.getMessage() + "]"); } // prefix variable with 'a' to make it a legal identifier results.append(" this.a" + jscriptVar++ +" = new Array(\"" + fieldProperty + "\", \"" + message + "\", "); results.append(createDynamicJavascriptFunction(field) + ");\n"); } else { // Customized for the indexed properties // Field attributes definition from the validation.xml String fieldIndexedListProperty = field.getIndexedListProperty(); // Mandatory String fieldIndexedProperty = field.getIndexedProperty(); if (isEmptyString(fieldIndexedListProperty)) { throw new JspException( "No definition in validation.xml for attribute key 'indexedProperty' in the form attribute [" + form.getName() + "] and the field attribute [" + field + "]"); } // Try to get the current beanForm from request scope ActionMapping actionMapping = (ActionMapping)pageContext.getRequest().getAttribute(Globals.MAPPING_KEY); if (actionMapping == null) { throw new JspException("No bean found for attribute key [" + actionMapping + "] in request scope"); } ActionForm actionForm = (ActionForm) pageContext.getRequest().getAttribute(actionMapping.getName()); // Size bean from page scope Integer indexedPropertySize = (Integer) pageContext.getAttribute(fieldIndexedListProperty + "_size", PageContext.PAGE_SCOPE); if ((actionForm == null) && (indexedPropertySize == null)) { if (actionForm == null) { throw new JspException("No bean found for attribute key [" + form.getName() + "] in request scope"); } if (indexedPropertySize == null) { throw new JspException("No bean found for attribute key [" + fieldIndexedListProperty + "_size] in page scope"); } } int propertySize = 0; // Try to get the size from the size bean defined in the pageScope if (indexedPropertySize != null) { propertySize = indexedPropertySize.intValue(); } else { // Try to get the size of the indexedListProperty field from the beanForm if (actionForm == null) { throw new JspException("No bean found for attribute key [" + form.getName() + "] in request scope"); } String[] arrayElts = null; try { arrayElts = BeanUtils.getArrayProperty(actionForm, fieldIndexedListProperty); if (arrayElts == null) { throw new JspException("BeanUtils.getArrayProperty() can not return a null value. Check the Apache Commons BeanUtils version."); } } catch (IllegalAccessException e) { throw new JspException( "IllegalAccessException for the getter method of the property [" + fieldIndexedListProperty + "] message=[" + e.getMessage() + "]"); } catch (InvocationTargetException e) { throw new JspException( "InvocationTargetException for the getter method of the property [" + fieldIndexedListProperty + "] message=[" + e.getMessage() + "]"); } catch (NoSuchMethodException e) { throw new JspException( "No such getter method found for the property [" + fieldIndexedListProperty + "] message=[" + e.getMessage() + "]"); } propertySize = arrayElts.length; if (propertySize == 0) { if (log.isInfoEnabled()) { log.info( "The property [" + fieldIndexedListProperty + "] has no element. Please initialized it in the beanForm [" + form.getName() + "] or in the associated Action class if you want to generate the Dynamic JavaScript function." + "An other way is to defined a bean called [" + fieldIndexedListProperty + "_size] in the page scope with the size wanted for the indexedProperty [" + fieldIndexedProperty + "]"); } } } for (int j = 0; j < propertySize; j++) { String jsIndexedFieldProperty = fieldIndexedProperty + "[" + j + "]." + fieldProperty; results.append(" this.a" + jscriptVar++ +" = new Array(\"" + jsIndexedFieldProperty + "\", \"" + message + "\", "); results.append(createDynamicJavascriptFunction(field) + ");\n"); } } } results.append(" } \n\n"); } return results.toString(); } /** * Create dynamicly the Function JS from a given field * * @param field * @return */ private String createDynamicJavascriptFunction(Field field) { StringBuffer results = new StringBuffer(); results.append("new Function (\"varName\", \""); Map vars = field.getVars(); // Loop through the field's variables. Iterator varsIterator = vars.keySet().iterator(); while (varsIterator.hasNext()) { String varName = (String) varsIterator.next(); Var var = (Var) vars.get(varName); String varValue = var.getValue(); String jsType = var.getJsType(); // skip requiredif variables field, fieldIndexed, fieldTest, fieldValue if (varName.startsWith("field")) { continue; } if (Var.JSTYPE_INT.equalsIgnoreCase(jsType)) { results.append("this." + varName + "=" + ValidatorUtils.replace(varValue, "\\", "\\\\") + "; "); } else if (Var.JSTYPE_REGEXP.equalsIgnoreCase(jsType)) { results.append("this." + varName + "=/" + ValidatorUtils.replace(varValue, "\\", "\\\\") + "/; "); } else if (Var.JSTYPE_STRING.equalsIgnoreCase(jsType)) { results.append("this." + varName + "='" + ValidatorUtils.replace(varValue, "\\", "\\\\") + "'; "); // So everyone using the latest format doesn't need to change their xml files immediately. } else if ("mask".equalsIgnoreCase(varName)) { results.append("this." + varName + "=/" + ValidatorUtils.replace(varValue, "\\", "\\\\") + "/; "); } else { results.append("this." + varName + "='" + ValidatorUtils.replace(varValue, "\\", "\\\\") + "'; "); } } results.append(" return this[varName];\")"); return results.toString(); } /** * Determines if validations should stop on an error. * @param config The <code>ModuleConfig</code> used to lookup the * stopOnError setting. * @return <code>true</code> if validations should stop on errors. */ private boolean stopOnError(ModuleConfig config) { Object stopOnErrorObj = pageContext.getAttribute(ValidatorPlugIn.STOP_ON_ERROR_KEY + '.' + config.getPrefix(), PageContext.APPLICATION_SCOPE); boolean stopOnError = true; if (stopOnErrorObj instanceof Boolean) { stopOnError = ((Boolean) stopOnErrorObj).booleanValue(); } return stopOnError; } /** * Creates the JavaScript methods list from the given actions. * @param actions A List of ValidatorAction objects. * @param stopOnError If true, behaves like released version of struts 1.1 * and stops after first error. If false, evaluates all validations. * @return JavaScript methods. */ private String createMethods(List actions, boolean stopOnError) { String methods = null; final String methodOperator = stopOnError ? " && " : " & "; Iterator iter = actions.iterator(); while (iter.hasNext()) { ValidatorAction va = (ValidatorAction) iter.next(); if (methods == null) { methods = va.getMethod() + "(form)"; } else { methods += methodOperator + va.getMethod() + "(form)"; } } return methods; } /** * Get List of actions for the given Form. * @param resources * @param form * @return A sorted List of ValidatorAction objects. */ private List createActionList(ValidatorResources resources, Form form) { List actionMethods = new ArrayList(); Iterator iterator = form.getFields().iterator(); while (iterator.hasNext()) { Field field = (Field) iterator.next(); for (Iterator x = field.getDependencyList().iterator(); x.hasNext();) { Object o = x.next(); if (o != null && !actionMethods.contains(o)) { actionMethods.add(o); } } } List actions = new ArrayList(); // Create list of ValidatorActions based on actionMethods iterator = actionMethods.iterator(); while (iterator.hasNext()) { String depends = (String) iterator.next(); ValidatorAction va = resources.getValidatorAction(depends); // throw nicer NPE for easier debugging if (va == null) { throw new NullPointerException("Depends string \"" + depends + "\" was not found in validator-rules.xml."); } if (va.getJavascript() != null && va.getJavascript().length() > 0) { actions.add(va); } else { iterator.remove(); } } Collections.sort(actions, actionComparator); return actions; } /** * Utility method to verify if a string is empty or not * * @param s * @return */ private static boolean isEmptyString(String s) { if ((s != null) && (!s.equals(""))) { return false; } else { return true; } } } --------------------------------------------------------------------- To unsubscribe, e-mail: [EMAIL PROTECTED] For additional commands, e-mail: [EMAIL PROTECTED]
