Commits

unkyaku  committed ecfca59

Allow field-validators to short-circuit. Solution for XW-185.

git-svn-id: http://svn.opensymphony.com/svn/xwork/trunk@241e221344d-f017-0410-9bd5-d282ab1896d7

  • Participants
  • Parent commits 0130589

Comments (0)

Files changed (11)

File src/etc/xwork-validator-1.0.dtd

 <!ELEMENT field-validator (param*, message)>
 <!ATTLIST field-validator
 	type CDATA #REQUIRED
+    short-circuit (true|false) "false"
 >
 
 <!ELEMENT validator (param*, message)>

File src/java/com/opensymphony/xwork/validator/ActionValidatorManager.java

 import java.io.InputStream;
 
 import java.util.ArrayList;
+import java.util.Collection;
 import java.util.Collections;
 import java.util.HashMap;
 import java.util.Iterator;
 import java.util.List;
 import java.util.Map;
+import java.util.Set;
+import java.util.TreeSet;
 
 
 /**
     public static void validate(Object object, String context, ValidatorContext validatorContext) throws ValidationException {
         List validators = getValidators(object.getClass(), context);
 
+        Set shortcircuitedFields = null;
+
         for (Iterator iterator = validators.iterator(); iterator.hasNext();) {
             Validator validator = (Validator) iterator.next();
             validator.setValidatorContext(validatorContext);
                 LOG.debug("Running validator: " + validator + " for object " + object);
             }
 
+            if (validator instanceof FieldValidator) {
+                FieldValidator fValidator = (FieldValidator) validator;
+
+                if ((shortcircuitedFields != null) && shortcircuitedFields.contains(fValidator.getFieldName())) {
+                    continue;
+                }
+
+                if (validator instanceof ShortCircuitingFieldValidator && ((ShortCircuitingFieldValidator) validator).isShortCircuit()) {
+                    int errs = 0;
+
+                    if (validatorContext.hasFieldErrors()) {
+                        Collection fieldErrors = (Collection) validatorContext.getFieldErrors().get(fValidator.getFieldName());
+
+                        if (fieldErrors != null) {
+                            errs = fieldErrors.size();
+                        }
+                    }
+
+                    validator.validate(object);
+
+                    if (validatorContext.hasFieldErrors()) {
+                        Collection fieldErrors = (Collection) validatorContext.getFieldErrors().get(fValidator.getFieldName());
+
+                        if ((fieldErrors != null) && (fieldErrors.size() > errs)) {
+                            if (LOG.isDebugEnabled()) {
+                                LOG.debug("Short-circuiting");
+                            }
+
+                            if (shortcircuitedFields == null) {
+                                shortcircuitedFields = new TreeSet();
+                            }
+
+                            shortcircuitedFields.add(fValidator.getFieldName());
+                        }
+                    }
+
+                    continue;
+                }
+            }
+
             validator.validate(object);
         }
     }

File src/java/com/opensymphony/xwork/validator/ShortCircuitingFieldValidator.java

+/*
+ * Copyright (c) 2002-2003 by OpenSymphony
+ * All rights reserved.
+ */
+package com.opensymphony.xwork.validator;
+
+
+/**
+ * The ShortCircuitingFieldValidator interface defines the methods to be
+ * implemented by FieldValidators that can short-circuit the validator queue
+ * that it is in.
+ *
+ * @author Mark Woon
+ */
+public interface ShortCircuitingFieldValidator extends FieldValidator {
+    //~ Methods ////////////////////////////////////////////////////////////////
+
+    /**
+     * Sets whether this field validator should short circuit the validator queue
+     * it's in if validation fails.
+     *
+     * @param shortcircuit true if this field validator should short circuit on
+     * failure, false otherwise
+     */
+    public void setShortCircuit(boolean shortcircuit);
+
+    /**
+     * Gets whether this field validator should short circuit the validator queue
+     * it's in if validation fails.
+     *
+     * @return true if this field validator should short circuit on failure,
+     * false otherwise
+     */
+    public boolean isShortCircuit();
+}

File src/java/com/opensymphony/xwork/validator/ValidatorFileParser.java

 package com.opensymphony.xwork.validator;
 
 import com.opensymphony.xwork.ObjectFactory;
+
 import org.apache.commons.logging.Log;
 import org.apache.commons.logging.LogFactory;
+
 import org.w3c.dom.Document;
 import org.w3c.dom.Element;
 import org.w3c.dom.Node;
 import org.w3c.dom.NodeList;
+
 import org.xml.sax.*;
 
-import javax.xml.parsers.DocumentBuilder;
-import javax.xml.parsers.DocumentBuilderFactory;
 import java.io.IOException;
 import java.io.InputStream;
+
 import java.util.ArrayList;
 import java.util.HashMap;
 import java.util.List;
 import java.util.Map;
 
+import javax.xml.parsers.DocumentBuilder;
+import javax.xml.parsers.DocumentBuilderFactory;
+
 
 /**
  * ValidatorFileParser
                 Element validatorElement = (Element) nodes.item(i);
                 String name = validatorElement.getAttribute("name");
                 String className = validatorElement.getAttribute("class");
-                Class clazz = null;
 
                 try {
                     // catch any problems here
             }
 
             Validator validator = ValidatorFactory.getValidator(validatorType, params);
+
+            if (validator instanceof ShortCircuitingFieldValidator) {
+                ((ShortCircuitingFieldValidator) validator).setShortCircuit(Boolean.valueOf(validatorElement.getAttribute("short-circuit")).booleanValue());
+            }
+
             NodeList messageNodes = validatorElement.getElementsByTagName("message");
             Element messageElement = (Element) messageNodes.item(0);
             String key = messageElement.getAttribute("key");

File src/java/com/opensymphony/xwork/validator/validators/FieldValidatorSupport.java

  */
 package com.opensymphony.xwork.validator.validators;
 
-import com.opensymphony.xwork.validator.FieldValidator;
+import com.opensymphony.xwork.validator.ShortCircuitingFieldValidator;
 
 
 /**
  *
  * @author Jason Carreira
  */
-public abstract class FieldValidatorSupport extends ValidatorSupport implements FieldValidator {
+public abstract class FieldValidatorSupport extends ValidatorSupport implements ShortCircuitingFieldValidator {
     //~ Instance fields ////////////////////////////////////////////////////////
 
     private String fieldName = null;
+    private boolean m_shortCircuit;
 
     //~ Methods ////////////////////////////////////////////////////////////////
 
     public String getFieldName() {
         return fieldName;
     }
+
+    public void setShortCircuit(boolean shortcircuit) {
+        m_shortCircuit = shortcircuit;
+    }
+
+    public boolean isShortCircuit() {
+        return m_shortCircuit;
+    }
 }

File src/test/com/opensymphony/xwork/test/User-validation.xml

+<!DOCTYPE validators PUBLIC "-//OpenSymphony Group//XWork Validator 1.0//EN" "http://www.opensymphony.com/xwork/xwork-validator-1.0.dtd">
+<validators>
+    <field name="name">
+        <field-validator type="required">
+            <message key="name.key">You must enter a value for name.</message>
+        </field-validator>
+    </field>
+    <field name="email">
+        <field-validator type="required" short-circuit="true">
+            <message>You must enter a value for email.</message>
+        </field-validator>
+        <field-validator type="email" short-circuit="true">
+            <message>Not a valid e-mail.</message>
+        </field-validator>
+        <field-validator type="fieldexpression">
+            <param name="expression">email.endsWith('mycompany.com')</param>
+            <message>Email not from the right company.</message>
+        </field-validator>
+    </field>
+    <field name="email2">
+        <field-validator type="required">
+            <message>You must enter a value for email2.</message>
+        </field-validator>
+        <field-validator type="email">
+            <message>Not a valid e-mail2.</message>
+        </field-validator>
+        <field-validator type="fieldexpression">
+            <param name="expression">email.endsWith('mycompany.com')</param>
+            <message>Email2 not from the right company.</message>
+        </field-validator>
+    </field>
+</validators>

File src/test/com/opensymphony/xwork/test/User.java

+/*
+ * Copyright (c) 2002-2003 by OpenSymphony
+ * All rights reserved.
+ */
+/*
+ * This file is copyright (c) 2001-2004, Board of Trustees of Stanford
+ * University.  Created in the laboratory of Professor Russ B. Altman
+ * (russ.altman@stanford.edu), Stanford University, Department of
+ * Medicine, as part of the NIH PharmGKB knowledge base development
+ * effort.  This work is supported by NIH U01GM61374.  Contact
+ * help@pharmgkb.org for assistance, questions or suggestions.
+ */
+package com.opensymphony.xwork.test;
+
+
+/**
+ * Test bean.
+ *
+ * @author Mark Woon
+ */
+public class User {
+    //~ Instance fields ////////////////////////////////////////////////////////
+
+    private String m_email;
+    private String m_email2;
+    private String m_name;
+
+    //~ Methods ////////////////////////////////////////////////////////////////
+
+    public void setEmail(String email) {
+        m_email = email;
+    }
+
+    public String getEmail() {
+        return m_email;
+    }
+
+    public void setEmail2(String email) {
+        m_email2 = email;
+    }
+
+    public String getEmail2() {
+        return m_email2;
+    }
+
+    public void setName(String name) {
+        m_name = name;
+    }
+
+    public String getName() {
+        return m_name;
+    }
+}

File src/test/com/opensymphony/xwork/validator/ActionValidatorManagerTest.java

 import com.opensymphony.xwork.SimpleAction;
 import com.opensymphony.xwork.test.SimpleAction2;
 import com.opensymphony.xwork.test.SimpleAction3;
+import com.opensymphony.xwork.test.User;
 import com.opensymphony.xwork.validator.validators.DateRangeFieldValidator;
 import com.opensymphony.xwork.validator.validators.ExpressionValidator;
 import com.opensymphony.xwork.validator.validators.IntRangeFieldValidator;
         List validatorList2 = ActionValidatorManager.getValidators(SimpleAction2.class, alias);
         assertFalse(validatorList.size() == validatorList2.size());
     }
+
+    public void testShortCircuit() {
+        // get validators
+        List validatorList = ActionValidatorManager.getValidators(User.class, null);
+        assertEquals(7, validatorList.size());
+
+        try {
+            User user = new User();
+            user.setName("Mark");
+            user.setEmail("mark@mycompany.com");
+            user.setEmail2("mark2@mycompany.com");
+
+            // this should work
+            ValidatorContext context = new GenericValidatorContext(user);
+            ActionValidatorManager.validate(user, null, context);
+            assertFalse(context.hasErrors());
+
+            // this should short-circuit at the second validator for email
+            user.setEmail("bad_email");
+            user.setEmail2("bad_email");
+            context = new GenericValidatorContext(user);
+            ActionValidatorManager.validate(user, null, context);
+            assertTrue(context.hasFieldErrors());
+
+            List l = (List) context.getFieldErrors().get("email");
+            assertNotNull(l);
+            assertEquals(1, l.size());
+            assertEquals("Not a valid e-mail.", l.get(0));
+            l = (List) context.getFieldErrors().get("email2");
+            assertNotNull(l);
+            assertEquals(2, l.size());
+            assertEquals("Not a valid e-mail2.", l.get(0));
+            assertEquals("Email2 not from the right company.", l.get(1));
+        } catch (ValidationException ex) {
+            ex.printStackTrace();
+            fail("Validation error: " + ex.getMessage());
+        }
+    }
 }

File src/test/com/opensymphony/xwork/validator/GenericValidatorContext.java

+/*
+ * Copyright (c) 2002-2003 by OpenSymphony
+ * All rights reserved.
+ */
+/*
+ * This file is copyright (c) 2001-2004, Board of Trustees of Stanford
+ * University.  Created in the laboratory of Professor Russ B. Altman
+ * (russ.altman@stanford.edu), Stanford University, Department of
+ * Medicine, as part of the NIH PharmGKB knowledge base development
+ * effort.  This work is supported by NIH U01GM61374.  Contact
+ * help@pharmgkb.org for assistance, questions or suggestions.
+ */
+package com.opensymphony.xwork.validator;
+
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+
+/**
+ * Dummy validator context to use to capture error messages.
+ *
+ * @author Mark Woon
+ * @author Matthew Payne
+ */
+public class GenericValidatorContext extends DelegatingValidatorContext {
+    //~ Instance fields ////////////////////////////////////////////////////////
+
+    private Collection m_actionErrors;
+    private Collection m_actionMessages;
+    private Map m_fieldErrors;
+
+    //~ Constructors ///////////////////////////////////////////////////////////
+
+    public GenericValidatorContext(Object object) {
+        super(object);
+    }
+
+    //~ Methods ////////////////////////////////////////////////////////////////
+
+    public synchronized void setActionErrors(Collection errorMessages) {
+        this.m_actionErrors = errorMessages;
+    }
+
+    public synchronized Collection getActionErrors() {
+        return new ArrayList(internalGetActionErrors());
+    }
+
+    public synchronized void setActionMessages(Collection messages) {
+        this.m_actionMessages = messages;
+    }
+
+    public synchronized Collection getActionMessages() {
+        return new ArrayList(internalGetActionMessages());
+    }
+
+    public synchronized void setFieldErrors(Map errorMap) {
+        this.m_fieldErrors = errorMap;
+    }
+
+    /**
+     * Get the field specific errors.
+     *
+     * @return an unmodifiable Map with errors mapped from fieldname (String) to Collection of String error messages
+     */
+    public synchronized Map getFieldErrors() {
+        return new HashMap(internalGetFieldErrors());
+    }
+
+    public synchronized void addActionError(String anErrorMessage) {
+        internalGetActionErrors().add(anErrorMessage);
+    }
+
+    /**
+     * Add an Action level message to this Action
+     */
+    public void addActionMessage(String aMessage) {
+        internalGetActionMessages().add(aMessage);
+    }
+
+    public synchronized void addFieldError(String fieldName, String errorMessage) {
+        final Map errors = internalGetFieldErrors();
+        List thisFieldErrors = (List) errors.get(fieldName);
+
+        if (thisFieldErrors == null) {
+            thisFieldErrors = new ArrayList();
+            errors.put(fieldName, thisFieldErrors);
+        }
+
+        thisFieldErrors.add(errorMessage);
+    }
+
+    public synchronized boolean hasActionErrors() {
+        return (m_actionErrors != null) && !m_actionErrors.isEmpty();
+    }
+
+    /**
+     * Note that this does not have the same meaning as in WW 1.x
+     *
+     * @return (hasActionErrors() || hasFieldErrors())
+     */
+    public synchronized boolean hasErrors() {
+        return (hasActionErrors() || hasFieldErrors());
+    }
+
+    public synchronized boolean hasFieldErrors() {
+        return (m_fieldErrors != null) && !m_fieldErrors.isEmpty();
+    }
+
+    private Collection internalGetActionErrors() {
+        if (m_actionErrors == null) {
+            m_actionErrors = new ArrayList();
+        }
+
+        return m_actionErrors;
+    }
+
+    private Collection internalGetActionMessages() {
+        if (m_actionMessages == null) {
+            m_actionMessages = new ArrayList();
+        }
+
+        return m_actionMessages;
+    }
+
+    private Map internalGetFieldErrors() {
+        if (m_fieldErrors == null) {
+            m_fieldErrors = new HashMap();
+        }
+
+        return m_fieldErrors;
+    }
+}

File src/test/com/opensymphony/xwork/validator/ValidatorFileParserTest.java

 
 import com.opensymphony.xwork.config.ConfigurationManager;
 import com.opensymphony.xwork.config.providers.MockConfigurationProvider;
+import com.opensymphony.xwork.validator.validators.IntRangeFieldValidator;
 import com.opensymphony.xwork.validator.validators.RequiredFieldValidator;
 
 import junit.framework.TestCase;
 
         List configs = ValidatorFileParser.parseActionValidators(is);
         assertNotNull(configs);
-        assertEquals(configs.size(), 1);
+        assertEquals(3, configs.size());
 
         Validator validator = (Validator) configs.get(0);
         assertTrue(validator instanceof RequiredFieldValidator);
         FieldValidator fieldValidator = (FieldValidator) validator;
         assertEquals("foo", fieldValidator.getFieldName());
         assertEquals("You must enter a value for foo.", fieldValidator.getDefaultMessage());
+
+        validator = (Validator) configs.get(1);
+        assertTrue(validator instanceof ShortCircuitingFieldValidator);
+        assertTrue(((ShortCircuitingFieldValidator) validator).isShortCircuit());
+
+        validator = (Validator) configs.get(2);
+        assertTrue(validator instanceof IntRangeFieldValidator);
+        assertFalse(((ShortCircuitingFieldValidator) validator).isShortCircuit());
     }
 
     protected void setUp() throws Exception {

File src/test/com/opensymphony/xwork/validator/validator-parser-test.xml

             <message key="test.key">You must enter a value for foo.</message>
         </field-validator>
     </field>
-</validators>
-
+    <field name="bar">
+        <field-validator type="required" short-circuit="true">
+            <message key="bar.key">You must enter a value for bar.</message>
+        </field-validator>
+        <field-validator type="int">
+            <param name="min">6</param>
+            <param name="max">10</param>
+            <message>bar must be between ${min} and ${max}, current value is ${bar}.</message>
+        </field-validator>
+    </field>
+</validators>