Index: /home/aporter/workspace/Stripes Trunk/stripes/src/net/sourceforge/stripes/config/Configuration.java
===================================================================
--- /home/aporter/workspace/Stripes Trunk/stripes/src/net/sourceforge/stripes/config/Configuration.java	(revision 861)
+++ /home/aporter/workspace/Stripes Trunk/stripes/src/net/sourceforge/stripes/config/Configuration.java	(working copy)
@@ -21,6 +21,7 @@
 import net.sourceforge.stripes.localization.LocalePicker;
 import net.sourceforge.stripes.validation.TypeConverterFactory;
 import net.sourceforge.stripes.validation.ValidationMetadataProvider;
+import net.sourceforge.stripes.validation.ValidationProvider;
 import net.sourceforge.stripes.tag.TagErrorRendererFactory;
 import net.sourceforge.stripes.tag.PopulationStrategy;
 import net.sourceforge.stripes.format.FormatterFactory;
@@ -201,4 +202,12 @@
      * @return an instance of {@link ValidationMetadataProvider}
      */
     ValidationMetadataProvider getValidationMetadataProvider();
+    
+    /**
+     * Returns an instance of {@link ValidationProvider} that can be used by Stripes to
+     * perform validations during {@link LifecycleStage#BindingAndValidation}.
+     * 
+     * @return an instance of {@link ValidationProvider}
+     */
+    ValidationProvider getValidationProvider();
 }
Index: /home/aporter/workspace/Stripes Trunk/stripes/src/net/sourceforge/stripes/config/DefaultConfiguration.java
===================================================================
--- /home/aporter/workspace/Stripes Trunk/stripes/src/net/sourceforge/stripes/config/DefaultConfiguration.java	(revision 861)
+++ /home/aporter/workspace/Stripes Trunk/stripes/src/net/sourceforge/stripes/config/DefaultConfiguration.java	(working copy)
@@ -53,8 +53,10 @@
 import net.sourceforge.stripes.util.Log;
 import net.sourceforge.stripes.validation.DefaultTypeConverterFactory;
 import net.sourceforge.stripes.validation.DefaultValidationMetadataProvider;
+import net.sourceforge.stripes.validation.DefaultValidationProvider;
 import net.sourceforge.stripes.validation.TypeConverterFactory;
 import net.sourceforge.stripes.validation.ValidationMetadataProvider;
+import net.sourceforge.stripes.validation.ValidationProvider;
 
 /**
  * <p>Centralized location for defaults for all Configuration properties.  This implementation does
@@ -95,6 +97,7 @@
     private ExceptionHandler exceptionHandler;
     private MultipartWrapperFactory multipartWrapperFactory;
     private ValidationMetadataProvider validationMetadataProvider;
+    private ValidationProvider validationProvider;
 
     /** Gratefully accepts the BootstrapPropertyResolver handed to the Configuration. */
     public void setBootstrapPropertyResolver(BootstrapPropertyResolver resolver) {
@@ -186,6 +189,14 @@
                 this.validationMetadataProvider = new DefaultValidationMetadataProvider();
                 this.validationMetadataProvider.init(this);
             }
+            
+            this.validationProvider = initValidationProvider();
+            {
+                if (this.validationProvider == null) {
+                    this.validationProvider = new DefaultValidationProvider();
+                    this.validationProvider.init(this);
+                }
+            }
 
             this.interceptors = new HashMap<LifecycleStage, Collection<Interceptor>>();
             Map<LifecycleStage, Collection<Interceptor>> map = initCoreInterceptors();
@@ -386,9 +397,22 @@
     public ValidationMetadataProvider getValidationMetadataProvider() {
         return this.validationMetadataProvider;
     }
+    
+    /**
+     * Returns an instance of {@link ValidationProvider} that can be used by Stripes to
+     * perform validation during {@link LifecycleStage#BindingAndValidation}.
+     * 
+     * @return an instance of {@link ValidationMetadataProvider}
+     */
+    public ValidationProvider getValidationProvider() {
+        return this.validationProvider;
+    }
 
     /** Allows subclasses to initialize a non-default {@link ValidationMetadataProvider}. */
     protected ValidationMetadataProvider initValidationMetadataProvider() { return null; }
+    
+    /** Allows subclasses to initialize a non-default {@link ValidationProvider}. */
+    protected ValidationProvider initValidationProvider() { return null; }
 
     /**
      * Returns a list of interceptors that should be executed around the lifecycle stage
Index: /home/aporter/workspace/Stripes Trunk/stripes/src/net/sourceforge/stripes/config/RuntimeConfiguration.java
===================================================================
--- /home/aporter/workspace/Stripes Trunk/stripes/src/net/sourceforge/stripes/config/RuntimeConfiguration.java	(revision 861)
+++ /home/aporter/workspace/Stripes Trunk/stripes/src/net/sourceforge/stripes/config/RuntimeConfiguration.java	(working copy)
@@ -39,6 +39,7 @@
 import net.sourceforge.stripes.validation.TypeConverter;
 import net.sourceforge.stripes.validation.TypeConverterFactory;
 import net.sourceforge.stripes.validation.ValidationMetadataProvider;
+import net.sourceforge.stripes.validation.ValidationProvider;
 
 /**
  * <p>Configuration class that uses the BootstrapPropertyResolver to look for configuration values,
@@ -97,6 +98,9 @@
 
     /** The Configuration Key for looking up the name of the ValidationMetadataProvider class */
     public static final String VALIDATION_METADATA_PROVIDER = "ValidationMetadataProvider.Class";
+    
+    /** The Configuration Key for looking up the name of the ValidationProvider class */
+    public static final String VALIDATION_PROVIDER = "ValidationProvider.Class";
 
     /** The Configuration Key for looking up the comma separated list of core interceptor classes. */
     public static final String CORE_INTERCEPTOR_LIST = "CoreInterceptor.Classes";
@@ -174,6 +178,11 @@
     @Override protected ValidationMetadataProvider initValidationMetadataProvider() {
         return initializeComponent(ValidationMetadataProvider.class, VALIDATION_METADATA_PROVIDER);
     }
+    
+    /** Looks for a class name in config and uses that to create the component. */
+    @Override protected ValidationProvider initValidationProvider() {
+        return initializeComponent(ValidationProvider.class, VALIDATION_PROVIDER);
+    }
 
     /**
      * Looks for a list of class names separated by commas under the configuration key
Index: /home/aporter/workspace/Stripes Trunk/stripes/src/net/sourceforge/stripes/controller/DefaultActionBeanPropertyBinder.java
===================================================================
--- /home/aporter/workspace/Stripes Trunk/stripes/src/net/sourceforge/stripes/controller/DefaultActionBeanPropertyBinder.java	(revision 861)
+++ /home/aporter/workspace/Stripes Trunk/stripes/src/net/sourceforge/stripes/controller/DefaultActionBeanPropertyBinder.java	(working copy)
@@ -23,7 +23,6 @@
 import net.sourceforge.stripes.util.*;
 import net.sourceforge.stripes.util.bean.*;
 import net.sourceforge.stripes.validation.*;
-import net.sourceforge.stripes.validation.expression.ExpressionValidator;
 
 import javax.servlet.http.HttpServletRequest;
 import java.lang.reflect.*;
@@ -148,8 +147,8 @@
                         continue;
                     }
 
-                    if (validate && validationInfo != null) {
-                        doPreConversionValidations(name, values, validationInfo, errors);
+                    if (validate) {
+                        configuration.getValidationProvider().doPreConversionValidations(bean, eval, name, values, validationInfo, errors);
                     }
 
                     // Only do type conversion if there aren't errors already
@@ -202,11 +201,12 @@
             }
         }
 
-        // Run post-conversion validation after absolutely everything has been bound
-        // and validated so that the expression validation can have access to the full
-        // state of the bean
         if (validate) {
-            doPostConversionValidations(bean, allConvertedFields, fieldErrors);
+            // Run post-conversion validation after absolutely everything has been bound
+            // and validated so that the expression validation can have access to the full
+            // state of the bean
+            configuration.getValidationProvider().doPostConversionValidations(bean,
+                    allConvertedFields, fieldErrors);
         }
 
         return fieldErrors;
@@ -559,124 +559,6 @@
     }
 
     /**
-     * Performs several basic validations on the String value supplied in the HttpServletRequest,
-     * based on information provided in annotations on the ActionBean.
-     * 
-     * @param propertyName the name of the property being validated (used for constructing errors)
-     * @param values the String[] of values from the request being validated
-     * @param validationInfo the ValidationMetadata for the property being validated
-     * @param errors a collection of errors to be populated with any validation errors discovered
-     */
-    protected void doPreConversionValidations(ParameterName propertyName, String[] values,
-            ValidationMetadata validationInfo, List<ValidationError> errors) {
-
-        for (String value : values) {
-            // Only run validations when there are non-empty values
-            if (value != null && value.length() > 0) {
-                if (validationInfo.minlength() != null
-                        && value.length() < validationInfo.minlength()) {
-                    ValidationError error = new ScopedLocalizableError("validation.minlength",
-                            "valueTooShort", validationInfo.minlength());
-
-                    error.setFieldValue(value);
-                    errors.add(error);
-                }
-
-                if (validationInfo.maxlength() != null
-                        && value.length() > validationInfo.maxlength()) {
-                    ValidationError error = new ScopedLocalizableError("validation.maxlength",
-                            "valueTooLong", validationInfo.maxlength());
-                    error.setFieldValue(value);
-                    errors.add(error);
-                }
-
-                if (validationInfo.mask() != null
-                        && !validationInfo.mask().matcher(value).matches()) {
-
-                    ValidationError error = new ScopedLocalizableError("validation.mask",
-                            "valueDoesNotMatch");
-
-                    error.setFieldValue(value);
-                    errors.add(error);
-                }
-            }
-        }
-    }
-
-    /**
-     * Performs basic post-conversion validations on the properties of the ActionBean after they
-     * have been converted to their rich type by the type conversion system. Validates single
-     * properties in isolation from other properties.
-     * 
-     * @param bean the ActionBean that is undergoing validation and binding
-     * @param convertedValues a map of ParameterName to all converted values for each field
-     * @param errors the validation errors object to put errors in to
-     */
-    protected void doPostConversionValidations(ActionBean bean,
-            Map<ParameterName, List<Object>> convertedValues, ValidationErrors errors) {
-
-        Map<String, ValidationMetadata> validationInfos = this.configuration
-                .getValidationMetadataProvider().getValidationMetadata(bean.getClass());
-        for (Map.Entry<ParameterName, List<Object>> entry : convertedValues.entrySet()) {
-            // Sort out what we need to validate this field
-            ParameterName name = entry.getKey();
-            List<Object> values = entry.getValue();
-            ValidationMetadata validationInfo = validationInfos.get(name.getStrippedName());
-
-            if (values.size() == 0 || validationInfo == null) {
-                continue;
-            }
-
-            for (Object value : values) {
-                // If the value is a number then we should check to see if there are range
-                // boundaries
-                // established, and check them.
-                if (value instanceof Number) {
-                    Number number = (Number) value;
-
-                    if (validationInfo.minvalue() != null
-                            && number.doubleValue() < validationInfo.minvalue()) {
-                        ValidationError error = new ScopedLocalizableError("validation.minvalue",
-                                "valueBelowMinimum", validationInfo.minvalue());
-                        error.setFieldValue(String.valueOf(value));
-                        errors.add(name.getName(), error);
-                    }
-
-                    if (validationInfo.maxvalue() != null
-                            && number.doubleValue() > validationInfo.maxvalue()) {
-                        ValidationError error = new ScopedLocalizableError("validation.maxvalue",
-                                "valueAboveMaximum", validationInfo.maxvalue());
-                        error.setFieldValue(String.valueOf(value));
-                        errors.add(name.getName(), error);
-                    }
-                }
-            }
-
-            // And then do any expression validation
-            doExpressionValidation(bean, name, values, validationInfo, errors);
-        }
-    }
-
-    /**
-     * Performs validation of attribute values using a JSP EL expression if one is defined in the
-     * {@literal @}Validate annotation. The expression is evaluated once for each value converted.
-     * See {@link net.sourceforge.stripes.validation.expression.ExpressionValidator} for details
-     * on how this is implemented.
-     * 
-     * @param bean the ActionBean who's property is being validated
-     * @param name the name of the property being validated
-     * @param values the non-null post-conversion values for the property
-     * @param validationInfo the validation metadata for the property
-     * @param errors the validation errors object to add errors to
-     */
-    protected void doExpressionValidation(ActionBean bean, ParameterName name, List<Object> values,
-            ValidationMetadata validationInfo, ValidationErrors errors) {
-
-        if (validationInfo.expression() != null)
-            ExpressionValidator.evaluate(bean, name, values, validationInfo, errors);
-    }
-
-    /**
      * <p>
      * Converts the String[] of values for a given parameter in the HttpServletRequest into the
      * desired type of Object. If a converter is declared using an annotation for the property (or
Index: /home/aporter/workspace/Stripes Trunk/stripes/src/net/sourceforge/stripes/validation/DefaultValidationProvider.java
===================================================================
--- /home/aporter/workspace/Stripes Trunk/stripes/src/net/sourceforge/stripes/validation/DefaultValidationProvider.java	(revision 0)
+++ /home/aporter/workspace/Stripes Trunk/stripes/src/net/sourceforge/stripes/validation/DefaultValidationProvider.java	(revision 0)
@@ -0,0 +1,60 @@
+package net.sourceforge.stripes.validation;
+
+import java.util.List;
+import java.util.Map;
+import java.util.Vector;
+
+import net.sourceforge.stripes.action.ActionBean;
+import net.sourceforge.stripes.config.Configuration;
+import net.sourceforge.stripes.controller.ParameterName;
+import net.sourceforge.stripes.util.Log;
+import net.sourceforge.stripes.util.bean.PropertyExpressionEvaluation;
+
+public class DefaultValidationProvider implements ValidationProvider {
+
+    private static final Log log = Log.getInstance(DefaultValidationProvider.class);
+
+    private List<ValidationHandler> validationHandlers;
+
+    public void init(Configuration configuration) throws Exception {
+        validationHandlers = new Vector<ValidationHandler>();
+        
+        addValidationHandler(StripesValidationHandler.class, configuration);
+
+        for (Class<? extends ValidationHandler> clazz : configuration
+                .getBootstrapPropertyResolver().getClassPropertyList(ValidationHandler.class)) {
+            addValidationHandler(clazz, configuration);
+        }
+    }
+    
+    protected void addValidationHandler(Class<? extends ValidationHandler> clazz, Configuration configuration)
+    {
+        try {
+            ValidationHandler validationHandler = clazz.newInstance();
+            validationHandler.init(configuration);
+            validationHandlers.add(validationHandler);
+            log.debug("Added ValidationHandler ", clazz.getName());
+        }
+        catch (Exception e) {
+            log.error(
+                        e,
+                        "Encountered exception while trying to create and initialize auto-discovered ValidationHandler ",
+                        clazz.getName());
+        }
+    }
+
+    public void doPreConversionValidations(ActionBean bean, PropertyExpressionEvaluation eval, ParameterName propertyName, String[] values,
+            ValidationMetadata validationInfo, List<ValidationError> errors) {
+        for (ValidationHandler validationHandler : validationHandlers) {
+            validationHandler.doPreConversionValidations(bean, eval, propertyName, values, validationInfo,
+                    errors);
+        }
+    }
+
+    public void doPostConversionValidations(ActionBean bean,
+            Map<ParameterName, List<Object>> convertedValues, ValidationErrors errors) {
+        for (ValidationHandler validationHandler : validationHandlers) {
+            validationHandler.doPostConversionValidations(bean, convertedValues, errors);
+        }
+    }
+}
Index: /home/aporter/workspace/Stripes Trunk/stripes/src/net/sourceforge/stripes/validation/StripesValidationHandler.java
===================================================================
--- /home/aporter/workspace/Stripes Trunk/stripes/src/net/sourceforge/stripes/validation/StripesValidationHandler.java	(revision 0)
+++ /home/aporter/workspace/Stripes Trunk/stripes/src/net/sourceforge/stripes/validation/StripesValidationHandler.java	(revision 0)
@@ -0,0 +1,141 @@
+package net.sourceforge.stripes.validation;
+
+import java.util.List;
+import java.util.Map;
+
+import net.sourceforge.stripes.action.ActionBean;
+import net.sourceforge.stripes.config.Configuration;
+import net.sourceforge.stripes.controller.ParameterName;
+import net.sourceforge.stripes.util.bean.PropertyExpressionEvaluation;
+import net.sourceforge.stripes.validation.expression.ExpressionValidator;
+
+public class StripesValidationHandler implements ValidationHandler {
+    
+    private Configuration configuration;
+    
+    public void init(Configuration configuration) {
+System.out.println("saving configuration: " + configuration);
+        this.configuration = configuration;
+    }
+    
+    /**
+     * Performs several basic validations on the String value supplied in the HttpServletRequest,
+     * based on information provided in annotations on the ActionBean.
+     * 
+     * @param propertyName the name of the property being validated (used for constructing errors)
+     * @param values the String[] of values from the request being validated
+     * @param validationInfo the ValidationMetadata for the property being validated
+     * @param errors a collection of errors to be populated with any validation errors discovered
+     */
+    public void doPreConversionValidations(ActionBean bean, PropertyExpressionEvaluation eval, ParameterName propertyName,
+            String[] values, ValidationMetadata validationInfo, List<ValidationError> errors) {
+        // Can't do anything if we don't have enough info
+        if (validationInfo == null)
+            return;
+        
+        for (String value : values) {
+            // Only run validations when there are non-empty values
+            if (value != null && value.length() > 0) {
+                if (validationInfo.minlength() != null
+                        && value.length() < validationInfo.minlength()) {
+                    ValidationError error = new ScopedLocalizableError("validation.minlength",
+                            "valueTooShort", validationInfo.minlength());
+
+                    error.setFieldValue(value);
+                    errors.add(error);
+                }
+
+                if (validationInfo.maxlength() != null
+                        && value.length() > validationInfo.maxlength()) {
+                    ValidationError error = new ScopedLocalizableError("validation.maxlength",
+                            "valueTooLong", validationInfo.maxlength());
+                    error.setFieldValue(value);
+                    errors.add(error);
+                }
+
+                if (validationInfo.mask() != null
+                        && !validationInfo.mask().matcher(value).matches()) {
+
+                    ValidationError error = new ScopedLocalizableError("validation.mask",
+                            "valueDoesNotMatch");
+
+                    error.setFieldValue(value);
+                    errors.add(error);
+                }
+            }
+        }
+    }
+
+    /**
+     * Performs basic post-conversion validations on the properties of the ActionBean after they
+     * have been converted to their rich type by the type conversion system. Validates single
+     * properties in isolation from other properties.
+     * 
+     * @param bean the ActionBean that is undergoing validation and binding
+     * @param convertedValues a map of ParameterName to all converted values for each field
+     * @param errors the validation errors object to put errors in to
+     */
+    public void doPostConversionValidations(ActionBean bean,
+            Map<ParameterName, List<Object>> convertedValues, ValidationErrors errors) {
+
+        Map<String, ValidationMetadata> validationInfos = configuration
+                .getValidationMetadataProvider().getValidationMetadata(bean.getClass());
+        for (Map.Entry<ParameterName, List<Object>> entry : convertedValues.entrySet()) {
+            // Sort out what we need to validate this field
+            ParameterName name = entry.getKey();
+            List<Object> values = entry.getValue();
+            ValidationMetadata validationInfo = validationInfos.get(name.getStrippedName());
+
+            if (values.size() == 0 || validationInfo == null) {
+                continue;
+            }
+
+            for (Object value : values) {
+                // If the value is a number then we should check to see if there are range
+                // boundaries
+                // established, and check them.
+                if (value instanceof Number) {
+                    Number number = (Number) value;
+
+                    if (validationInfo.minvalue() != null
+                            && number.doubleValue() < validationInfo.minvalue()) {
+                        ValidationError error = new ScopedLocalizableError("validation.minvalue",
+                                "valueBelowMinimum", validationInfo.minvalue());
+                        error.setFieldValue(String.valueOf(value));
+                        errors.add(name.getName(), error);
+                    }
+
+                    if (validationInfo.maxvalue() != null
+                            && number.doubleValue() > validationInfo.maxvalue()) {
+                        ValidationError error = new ScopedLocalizableError("validation.maxvalue",
+                                "valueAboveMaximum", validationInfo.maxvalue());
+                        error.setFieldValue(String.valueOf(value));
+                        errors.add(name.getName(), error);
+                    }
+                }
+            }
+
+            // And then do any expression validation
+            doExpressionValidation(bean, name, values, validationInfo, errors);
+        }
+    }
+
+    /**
+     * Performs validation of attribute values using a JSP EL expression if one is defined in the
+     * {@literal @}Validate annotation. The expression is evaluated once for each value converted.
+     * See {@link net.sourceforge.stripes.validation.expression.ExpressionValidator} for details
+     * on how this is implemented.
+     * 
+     * @param bean the ActionBean who's property is being validated
+     * @param name the name of the property being validated
+     * @param values the non-null post-conversion values for the property
+     * @param validationInfo the validation metadata for the property
+     * @param errors the validation errors object to add errors to
+     */
+    protected void doExpressionValidation(ActionBean bean, ParameterName name, List<Object> values,
+            ValidationMetadata validationInfo, ValidationErrors errors) {
+
+        if (validationInfo.expression() != null)
+            ExpressionValidator.evaluate(bean, name, values, validationInfo, errors);
+    }
+}
Index: /home/aporter/workspace/Stripes Trunk/stripes/src/net/sourceforge/stripes/validation/ValidationHandler.java
===================================================================
--- /home/aporter/workspace/Stripes Trunk/stripes/src/net/sourceforge/stripes/validation/ValidationHandler.java	(revision 0)
+++ /home/aporter/workspace/Stripes Trunk/stripes/src/net/sourceforge/stripes/validation/ValidationHandler.java	(revision 0)
@@ -0,0 +1,37 @@
+/* Copyright 2008 Aaron Porter
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package net.sourceforge.stripes.validation;
+
+import java.util.List;
+import java.util.Map;
+
+import net.sourceforge.stripes.action.ActionBean;
+import net.sourceforge.stripes.config.Configuration;
+import net.sourceforge.stripes.controller.ParameterName;
+import net.sourceforge.stripes.util.bean.PropertyExpressionEvaluation;
+
+/**
+ * @author Aaron Porter
+ *
+ */
+public interface ValidationHandler {
+    public void init(Configuration configuration);
+    
+    public void doPreConversionValidations(ActionBean bean, PropertyExpressionEvaluation eval, ParameterName propertyName,
+            String[] values, ValidationMetadata validationInfo, List<ValidationError> errors);
+
+    public void doPostConversionValidations(ActionBean bean,
+            Map<ParameterName, List<Object>> convertedValues, ValidationErrors errors);
+}
Index: /home/aporter/workspace/Stripes Trunk/stripes/src/net/sourceforge/stripes/validation/ValidationProvider.java
===================================================================
--- /home/aporter/workspace/Stripes Trunk/stripes/src/net/sourceforge/stripes/validation/ValidationProvider.java	(revision 0)
+++ /home/aporter/workspace/Stripes Trunk/stripes/src/net/sourceforge/stripes/validation/ValidationProvider.java	(revision 0)
@@ -0,0 +1,17 @@
+package net.sourceforge.stripes.validation;
+
+import java.util.List;
+import java.util.Map;
+
+import net.sourceforge.stripes.action.ActionBean;
+import net.sourceforge.stripes.config.ConfigurableComponent;
+import net.sourceforge.stripes.controller.ParameterName;
+import net.sourceforge.stripes.util.bean.PropertyExpressionEvaluation;
+
+public interface ValidationProvider extends ConfigurableComponent {
+    public void doPreConversionValidations(ActionBean bean, PropertyExpressionEvaluation eval, ParameterName propertyName,
+            String[] values, ValidationMetadata validationInfo, List<ValidationError> errors);
+
+    public void doPostConversionValidations(ActionBean bean,
+            Map<ParameterName, List<Object>> convertedValues, ValidationErrors errors);
+}

