Announcement Announcement Module
Collapse
No announcement yet.
"required" behavior not adapted well to submit Page Title Module
Move Remove Collapse
X
Conversation Detail Module
Collapse
  • Filter
  • Time
  • Show
Clear All
new posts

  • "required" behavior not adapted well to submit

    I wrote some code to read the "not-null" property names from a Hibernate configuration file, converted the names to a "required" String array, and assigned the "required" array in an initBinder() override in my form class. The purpose is to eliminate the need to write separate validator routines simply to check whether a form input is null or empty. Ultimately, I wanted to add other "automatic" validations based upon length and type specified in the Hibernate configuration, but I wanted to try a test case first.

    I implemented the test code in the petclinic application in class AbstractClinicForm as follows:

    Code:
    	protected void initBinder(HttpServletRequest request, ServletRequestDataBinder binder)
    			throws Exception {
    		SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd");
    		dateFormat.setLenient(false);
    		binder.registerCustomEditor(Date.class, null, new CustomDateEditor(dateFormat, false));
          // end pre-existing code
    		
          // begin added code:
    
    		// Assign required fields as defined in the schema to the data binder:
    		Object currentDataObject = binder.getTarget();
          // getSchematicInfo() returns a link to the Hibernate schema information:
    		String requiredPropertyNames[] = getSchemaInfo().requiredFields(currentDataObject.getClass());
    		binder.setRequiredFields(requiredPropertyNames);
    	}
    Hence, for the Owner forms, this would set up a required String array consisting of the following names:
    • firstName
    • lastName
    • address
    • city
    • telephone
    Now, when I run petclinic with the above modification, EditOwnerForm works fine, but FindOwnersForm silently fails; it simply doesn't recognize the input. Sleuthing reveals that Spring's required code fails validation because the other fields that are not on the FindOwnersForm are null and thereby fail.

    I can fix the above code to work by checking to see if the request parameter being "required" validated is even present and, if it isn't, drop its name from the String array of "required" names. The following code does this exactly:
    Code:
    	protected void initBinder(HttpServletRequest request, ServletRequestDataBinder binder)
    			throws Exception {
    		SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd");
    		dateFormat.setLenient(false);
    		binder.registerCustomEditor(Date.class, null, new CustomDateEditor(dateFormat, false));
    		
    		// Assign required fields as defined in the schema to the data binder:
    		Object currentDataObject = binder.getTarget();
    		String requiredPropertyNames[] = getSchemaInfo().requiredFields(currentDataObject.getClass());
    		List importantPropertyNames = new ArrayList();
    		for &#40;int i = 0 ; i < requiredPropertyNames.length ; i++&#41;
    		&#123;
    		    String thisName = requiredPropertyNames&#91;i&#93;;
    		    if &#40;request.getParameter&#40;thisName&#41; != null&#41;
    		    &#123;
    		        importantPropertyNames.add&#40;thisName&#41;;
    		    &#125;
    		&#125;
    		requiredPropertyNames = new String&#91;importantPropertyNames.size&#40;&#41;&#93;;
    		for &#40;int i = 0 ; i < importantPropertyNames.size&#40;&#41; ; i++&#41;
    		&#123;
    		    requiredPropertyNames&#91;i&#93; = &#40;String&#41; importantPropertyNames.get&#40;i&#41;;
    		&#125;
    		binder.setRequiredFields&#40;requiredPropertyNames&#41;;
    	&#125;
    Here's the problem: this is clunky. Cannot the "schema-oriented" validations like required have a property flag added to not validate if the corresponding request parameter is null? Consider the code in DataBinder#bind(PropertyValues) that implements the "required" functionality:
    Code:
    ...
    		// check for missing fields
    		if &#40;this.requiredFields != null&#41; &#123;
    			for &#40;int i = 0; i < this.requiredFields.length; i++&#41; &#123;
    				PropertyValue pv = pvs.getPropertyValue&#40;this.requiredFields&#91;i&#93;&#41;;
    				if &#40;pv == null || "".equals&#40;pv.getValue&#40;&#41;&#41; || pv.getValue&#40;&#41; == null&#41; &#123;
    					// create field error with code "required"
    					String field = this.requiredFields&#91;i&#93;;
    					this.errors.addError&#40;
    							new FieldError&#40;this.errors.getObjectName&#40;&#41;, field, "", true,
    														 this.errors.resolveMessageCodes&#40;MISSING_FIELD_ERROR_CODE, field&#41;,
    														 getArgumentsForBindingError&#40;field&#41;, "Field '" + field + "' is required"&#41;&#41;;
    				&#125;
    			&#125;
    		&#125;
    ...
    Consider changing the second if statement to read:
    Code:
    				if &#40;pv == null || "".equals&#40;pv.getValue&#40;&#41;&#41; || &#40;pv.getValue&#40;&#41; == null && !canBeNull&#41;&#41; &#123;
    where you can see that the getValue() is only tested for null if canBeNull is false. canBeNull is a property that can be configured in the Spring configuration file and is false by default.

    Now the required validation can work both ways, but we've added the power to optionally automate the schema-oriented validations for HTML input: not-null, length, and type.

    What do you think?

    Thanks for following the long read (that is, if you got this far :roll: )

  • #2
    I see what you want to do but isn't this a bit like implementing a contradiction in terminus: canBeNull <> required? Wouldn't you say "canBeNull==!required"?

    Erwin

    Comment


    • #3
      I think I got the logic sense right

      I think the sense of the logic in the proposed "if" statement is correct. I haven't tried it yet.

      Comment


      • #4
        New &quot;Required&quot; Binding Framework

        :idea: I've updated petclinic to switch between two meanings of required:
        • :arrow: validate as though request parameter was required regardless of whether the request parameter existed.
        • :arrow: validate required only if request parameter exists.

        This functionality is implemented as follows:
        • :arrow: AbstractClinicForm overrides createBinder() to create SchemaDataBinder.
        • :arrow: SchemaDataBinder which subclasses ServletRequestDataBinder to implement bind(PropertyValues) as described above

        Listing of SchemaDataBinder
        Particularly note the highlighted (********) section:
        Code:
        package org.springframework.validation;
        
        import java.util.Arrays;
        import java.util.List;
        
        import org.springframework.beans.MutablePropertyValues;
        import org.springframework.beans.PropertyAccessException;
        import org.springframework.beans.PropertyAccessExceptionsException;
        import org.springframework.beans.PropertyValue;
        import org.springframework.beans.PropertyValues;
        import org.springframework.web.bind.ServletRequestDataBinder;
        
        /**
         * This data binder ignores the "required" validation
         * if the value to be validated is not returned in the
         * request parameters.
         * 
         * @author Blotto &#91;[email protected]&#93;
         * @version $Id$
         */
        public class SchemaDataBinder extends ServletRequestDataBinder
        &#123;
            /**
             * If true, then if a required field isn't represented
             * in the request parameters, then don't apply the required validation.
             * If false, then apply the required validation in all cases.
             */
            private boolean validateExisting;
            
            /**
             * The constructor.
             * 
             * @param target target object to bind onto
             * @param objectName objectName of the target object
             * @param validateExisting whether to test only existing
             * 			request parameters
             */
            public SchemaDataBinder&#40;Object target, String objectName, boolean validateExisting&#41;
            &#123;
                super&#40;target, objectName&#41;;
                this.validateExisting = validateExisting;
            &#125;
        
        	/**
        	 * Like the super method except that this
        	 * one doesn't create a required validation error if the
        	 * value to be validated isn't in the request parameters.
        	 * 
        	 * @param pvs property values to bind.
        	 * @see DataBinder#bind&#40;PropertyValues&#41;
        	 */
        	public void bind&#40;PropertyValues pvs&#41; &#123;
        		// check for fields to bind
        	    String&#91;&#93; allowedFields = getAllowedFields&#40;&#41;;
        		List allowedFieldsList = &#40;allowedFields != null&#41; ? Arrays.asList&#40;allowedFields&#41; &#58; null;
        		MutablePropertyValues mpvs = &#40;pvs instanceof MutablePropertyValues&#41; ?
        		    &#40;MutablePropertyValues&#41; pvs &#58; new MutablePropertyValues&#40;pvs&#41;;
        		PropertyValue&#91;&#93; pvArray = pvs.getPropertyValues&#40;&#41;;
        		for &#40;int i = 0; i < pvArray.length; i++&#41; &#123;
        			String field = pvArray&#91;i&#93;.getName&#40;&#41;;
        			if &#40;!&#40;&#40;allowedFieldsList != null && allowedFieldsList.contains&#40;field&#41;&#41; || isAllowed&#40;field&#41;&#41;&#41; &#123;
        				mpvs.removePropertyValue&#40;pvArray&#91;i&#93;&#41;;
        			&#125;
        		&#125;
        		pvs = mpvs;
        
        		// check for missing fields
        		String&#91;&#93; requiredFields = getRequiredFields&#40;&#41;;
        		if &#40;requiredFields != null&#41; &#123;
        			for &#40;int i = 0; i < requiredFields.length; i++&#41; &#123;
        				PropertyValue pv = pvs.getPropertyValue&#40;requiredFields&#91;i&#93;&#41;;
        				// *************************************************
        				// Here is where the modification is performed&#58;
        				boolean requiredFailed = &#40;validateExisting&#41;
        					? &#40;pv != null && "".equals&#40;pv.getValue&#40;&#41;&#41;&#41;
        					&#58; &#40;pv == null || "".equals&#40;pv.getValue&#40;&#41;&#41; || pv.getValue&#40;&#41; == null&#41;;
        				if &#40;requiredFailed&#41; &#123;
        				// End of modification
        				// *************************************************
        					// create field error with code "required"
        					String field = requiredFields&#91;i&#93;;
        					BindException errors = getErrors&#40;&#41;;
        					errors.addError&#40;
        							new FieldError&#40;errors.getObjectName&#40;&#41;, field, "", true,
        														 errors.resolveMessageCodes&#40;MISSING_FIELD_ERROR_CODE, field&#41;,
        														 getArgumentsForBindingError&#40;field&#41;, "Field '" + field + "' is required"&#41;&#41;;
        				&#125;
        			&#125;
        		&#125;
        
        		try &#123;
        			// bind request parameters onto params, ignoring unknown properties
        			getErrors&#40;&#41;.getBeanWrapper&#40;&#41;.setPropertyValues&#40;pvs, true&#41;;
        		&#125;
        		catch &#40;PropertyAccessExceptionsException ex&#41; &#123;
        			PropertyAccessException&#91;&#93; exs = ex.getPropertyAccessExceptions&#40;&#41;;
        			for &#40;int i = 0; i < exs.length; i++&#41; &#123;
        				// create field with the exceptions's code, e.g. "typeMismatch"
        				String field = exs&#91;i&#93;.getPropertyChangeEvent&#40;&#41;.getPropertyName&#40;&#41;;
        				BindException errors = getErrors&#40;&#41;;
        				errors.addError&#40;
        						new FieldError&#40;errors.getObjectName&#40;&#41;, field, exs&#91;i&#93;.getPropertyChangeEvent&#40;&#41;.getNewValue&#40;&#41;, true,
        													 errors.resolveMessageCodes&#40;exs&#91;i&#93;.getErrorCode&#40;&#41;, field&#41;,
        													 getArgumentsForBindingError&#40;field&#41;, exs&#91;i&#93;.getLocalizedMessage&#40;&#41;&#41;&#41;;
        			&#125;
        		&#125;
        	&#125;
        
        &#125;
        Here are the modified methods in AbstractClinicForm.

        First, the property which defines which
        required behavior is in effect:
        Code:
            private boolean requiredValidateExisting;
            
            public boolean isRequiredValidateExisting&#40;&#41;
            &#123;
                return requiredValidateExisting;
            &#125;
            /**
             * sets whether or not to suppress <b>required</b> validation
             * when the value to be validated isn't in the HTTP request
             * parameters.
             * 
             * @param requiredValidateExisting
             */
            public void setRequiredValidateExisting&#40;boolean requiredValidateExisting&#41;
            &#123;
                this.requiredValidateExisting = requiredValidateExisting;
            &#125;
        Here's the override to utilize a different binder object:
        Code:
        	/**
        	 * Create a new binder instance for the given command and request.
        	 * Called by bindAndValidate. This overrides the custom
        	 * ServletRequestDataBinder with a SchemaDataBinder.
        	 * @param request current HTTP request
        	 * @param command the command to bind onto
        	 * @return the new binder instance
        	 * @throws Exception in case of invalid state or arguments
        	 * @see #bindAndValidate
        	 * @see #initBinder
        	 * @see #setMessageCodesResolver
        	 */
        	protected ServletRequestDataBinder createBinder&#40;HttpServletRequest request, Object command&#41;
        	    throws Exception &#123;
        	    SchemaDataBinder binder = new SchemaDataBinder&#40;command, getCommandName&#40;&#41;, requiredValidateExisting&#41;;
        		MessageCodesResolver messageCodesResolver = getMessageCodesResolver&#40;&#41;;
        		if &#40;messageCodesResolver != null&#41; &#123;
        			binder.setMessageCodesResolver&#40;messageCodesResolver&#41;;
        		&#125;
        		initBinder&#40;request, binder&#41;;
        		return binder;
        	&#125;
        Here is initBinder() which adds the required fields from the schema:

        Code:
        	/**
        	 * Set up a custom property editor for the application's date format.
        	 * Also, retrieve the "required" fields from the corresponding not-null
        	 * columns in the database schema.  
        	 */
        	protected void initBinder&#40;HttpServletRequest request, ServletRequestDataBinder binder&#41;
        			throws Exception &#123;
        	    super.initBinder&#40;request, binder&#41;;
        	    
        	    registerDateFormat&#40;binder&#41;;
        		registerRequired&#40;request, binder&#41;;
        	&#125;
        
        	/**
        	 * Retrieves the "required" fields from the corresponding not-null
        	 * columns in the database schema.
        	 * 
             * @param binder what binder to assign the "required" fields to.
             */
            private void registerRequired&#40;HttpServletRequest request, ServletRequestDataBinder binder&#41;
            		throws Exception
            &#123;
        		Object currentDataObject = binder.getTarget&#40;&#41;;
        		String requiredPropertyNames&#91;&#93; = getSchemaInfo&#40;&#41;.requiredFields&#40;currentDataObject.getClass&#40;&#41;&#41;;
        		binder.setRequiredFields&#40;requiredPropertyNames&#41;;
            &#125;
        
            /**
        	 * registers a date property editor
        	 * 
             * @param binder the binder to register to.
             */
            protected void registerDateFormat&#40;ServletRequestDataBinder binder&#41;
            &#123;
        		SimpleDateFormat dateFormat = new SimpleDateFormat&#40;"yyyy-MM-dd"&#41;;
        		dateFormat.setLenient&#40;false&#41;;
        		binder.registerCustomEditor&#40;Date.class, null, new CustomDateEditor&#40;dateFormat, false&#41;&#41;;
            &#125;
        Finally, the section of petclinic-servlet.xml that configures AbstractClinicForm:
        Code:
        	
        	<bean id="abstractClinicForm" abstract="true"
        			class="org.springframework.samples.petclinic.web.AbstractClinicForm">
        		<!-- The class that implements the SchemaInfo interface to -->
        		<!-- answer the required fields for this data object&#58; -->
        		<property name="schemaInfo"><ref bean="hibernateSchemaInfo"/></property>
        		<!-- Whether or not to ignore "required" validation of values -->
        		<!-- that are not in the request parameters -->
        		<property name="requiredValidateExisting"><value>true</value></property>
        	</bean>
        This code has been tested and works for HTML input forms. It has not yet been checked w/ checkboxes and radio buttons.

        Comment


        • #5
          Above post should be integrated into DataBinder

          I propose integrating the ideas in the above post into DataBinder and BaseCommandController directly. The default functionality would match the existing functionality, but setting the property requiredValidateExisting in BaseCommandController would cause the new behavior to take place. Of course, radio button and checkbox operation should be verified first.

          Comment

          Working...
          X