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]

Reply via email to