Announcement Announcement Module
Collapse
No announcement yet.
Failed binding from SWF doesn't use default messages Page Title Module
Move Remove Collapse
X
Conversation Detail Module
Collapse
  • Filter
  • Time
  • Show
Clear All
new posts

  • Failed binding from SWF doesn't use default messages

    Context
    I have some default binding error messages configured for scenarios where a form values can't be bound. For example, when a string in the form can't be bound to a Long value. A subset of my messages.properties looks like:

    Code:
    typeMismatch.java.lang.Integer=Must specify an integer value.
    typeMismatch.java.lang.Long=Must specify an integer value.
    typeMismatch.java.lang.Float=Must specify a decimal value.
    typeMismatch.java.lang.Double=Must specify a decimal value.
    I'm making these messages known by declaring this in my application-context.xml:

    Code:
    <bean id="messageSource" class="org.springframework.context.support.ResourceBundleMessageSource">
      <property name="basenames">
        <list>
          <value>messages</value>
        </list>
      </property>
    </bean>
    I've set up my app so that these messages are used for binding errors. This works great for all of my forms based on SimpleFormController. That is, the errorMessage availble within my <spring:bind... > would be "Must specify an integer value." instead of the beastly default which looks like "Failed to convert property value of type [java.lang.String] for property entries[FOOBAR]; nested exception is java.lang.NumberFormatException: For input string: "A""

    Problem
    I'm trying to get Spring Web Flow's FormAction to use the same 'default' messages that SimpleFormController. It seems to be ignoring the default messages I've set, and it's returning the beastly message above instead.

    A lilttle investigation unearthed the problem, but I don't know how to fix it. The error codes generated from the binder aren't the same. There's one difference between what SWF and SimpleFormController produce. Let's take a look:

    When the invalid value "A" is submitted via SWF's FormAction, these error codes are used:
    • typeMismatch.mapping.entries[FOOBAR]
      typeMismatch.mapping.entries
      typeMismatch.entries[FOOBAR]
      typeMismatch.entries
      typeMismatch
    The error codes used when the same value is submitted against a SimpleFormController are:
    • typeMismatch.mapping.entries[FOOBAR]
      typeMismatch.mapping.entries
      typeMismatch.entries[FOOBAR]
      typeMismatch.entries
      typeMismatch.java.lang.Long
      typeMismatch
    Almost the same, except for the typeMismatch.java.lang.Long entry.

    Any suggestions as to why that one error code would be missing from SWF's list?

    Christian

    Platform: Spring 1.2.3 and Spring Web Flow PR5 against Java 1.5.0_04 on Tomcat 5.5.9

    The segment of my flow definition in play here looks like:

    Code:
    <view-state id="bidAssignment" view="bidAssignmentPage">
      <entry>
        <action bean="bidAssignmentAction" method="setupForm" />
      </entry>
      <transition on="submit" to="bidAssignmentSubmit">
        <action  bean="bidAssignmentAction" method="bindAndValidate" />
      </transition>
      <transition on="cancel" to="finish" />
    </view-state>
    
    <action-state id="bidAssignmentSubmit">
      <action bean="bidAssignmentAction" method="submit" />
      <transition on="success" to="unassignedBidCheck" />
    </action-state>

  • #2
    Very strange,

    The only difference I can see is that the SpringMVC controllers use the ServletRequestDataBinder and the SWF FormAction uses WebDataBinder, which is the superclass of ServletRequestDataBinder.
    However, both use a BindException to store the data binding errors, and as a result both use the DefaultMessageCodesResolver strategy.

    I'm going to try to reproduce this in a unit test.

    Erwin

    Comment


    • #3
      I can't reproduce this. Here is my unit test:

      Code:
      public class FormActionBindingTests extends TestCase &#123;
      	
      	public static class TestBean &#123;
      		
      		private Long prop;
      		
      		public Long getProp&#40;&#41; &#123;
      			return prop;
      		&#125;
      		
      		public void setProp&#40;Long prop&#41; &#123;
      			this.prop = prop;
      		&#125;
      	&#125;
      	
      	public void testMessageCodesOnBindFailure&#40;&#41; throws Exception &#123;
      		MockHttpServletRequest request = new MockHttpServletRequest&#40;&#41;;
      		request.setMethod&#40;"POST"&#41;;
      		request.addParameter&#40;"prop", "A"&#41;;
      		MockHttpServletResponse response = new MockHttpServletResponse&#40;&#41;;
      		MockRequestContext context = new MockRequestContext&#40;&#41;;
      		context.setLastEvent&#40;new ServletEvent&#40;request, response&#41;&#41;;
      		context.setProperty&#40;"method", "bindAndValidate"&#41;;
      		
      		// use a for FormAction to do the binding
      		FormAction formAction = new FormAction&#40;&#41;;
      		formAction.setFormObjectClass&#40;TestBean.class&#41;;
      		formAction.execute&#40;context&#41;;
      		Errors formActionErrors = &#40;Errors&#41;context.getRequestScope&#40;&#41;.get&#40;BindException.ERROR_KEY_PREFIX + "formObject"&#41;;
      		assertNotNull&#40;formActionErrors&#41;;
      		assertTrue&#40;formActionErrors.hasErrors&#40;&#41;&#41;;
      		
      		// use a SimpleFormController to do the binding
      		SimpleFormController simpleFormController = new SimpleFormController&#40;&#41;;
      		simpleFormController.setCommandClass&#40;TestBean.class&#41;;
      		simpleFormController.setCommandName&#40;"formObject"&#41;;
      		ModelAndView modelAndView = simpleFormController.handleRequest&#40;request, response&#41;;
      		Errors simpleFormControllerErrors = &#40;Errors&#41;modelAndView.getModel&#40;&#41;.get&#40;BindException.ERROR_KEY_PREFIX + "formObject"&#41;;
      		assertNotNull&#40;simpleFormControllerErrors&#41;;
      		assertTrue&#40;simpleFormControllerErrors.hasErrors&#40;&#41;&#41;;
      		
      		assertNotSame&#40;formActionErrors, simpleFormControllerErrors&#41;;
      		assertEquals&#40;formActionErrors.getErrorCount&#40;&#41;, simpleFormControllerErrors.getErrorCount&#40;&#41;&#41;;
      		assertEquals&#40;formActionErrors.getGlobalErrorCount&#40;&#41;, simpleFormControllerErrors.getGlobalErrorCount&#40;&#41;&#41;;
      		assertEquals&#40;formActionErrors.getFieldErrorCount&#40;"prop"&#41;, simpleFormControllerErrors.getFieldErrorCount&#40;"prop"&#41;&#41;;
      		assertEquals&#40;1, formActionErrors.getFieldErrorCount&#40;"prop"&#41;&#41;;
      		assertEquals&#40;formActionErrors.getFieldError&#40;"prop"&#41;.getCodes&#40;&#41;.length, simpleFormControllerErrors.getFieldError&#40;"prop"&#41;.getCodes&#40;&#41;.length&#41;;
      		//System.out.println&#40;formActionErrors.getFieldError&#40;"prop"&#41;&#41;;
      		//System.out.println&#40;simpleFormControllerErrors.getFieldError&#40;"prop"&#41;&#41;;
      	&#125;
      &#125;
      The prints at the end print the following:

      Field error in object 'formObject' on field 'prop': rejected value [A]; codes [typeMismatch.formObject.prop,typeMismatch.prop,typ eMismatch.java.lang.Long,typeMismatch]; arguments [MessageSourceResolvable: codes [formObject.prop,prop]; arguments []; default message [prop]]; default message [Failed to convert property value of type [java.lang.String] to required type [java.lang.Long] for property 'prop'; nested exception is java.lang.NumberFormatException: For input string: "A"]
      Field error in object 'formObject' on field 'prop': rejected value [A]; codes [typeMismatch.formObject.prop,typeMismatch.prop,typ eMismatch.java.lang.Long,typeMismatch]; arguments [MessageSourceResolvable: codes [formObject.prop,prop]; arguments []; default message [prop]]; default message [Failed to convert property value of type [java.lang.String] to required type [java.lang.Long] for property 'prop'; nested exception is java.lang.NumberFormatException: For input string: "A"]
      So it seems to be exactly the same, as expected. Also note that both "formActionErrors" and "simpleFormControllerErrors" contain the code "typeMismatch.java.lang.Long".

      Erwin

      Comment


      • #4
        Erwin,

        It's a little more complicated than that. I didn't think it was important, but let me show you what my command (form) object looks like:

        Code:
        public class MemberMapping &#123;
        
            public Integer getId&#40;&#41; &#123; return id; &#125;
        
            public void setId&#40;Integer id&#41; &#123; this.id = id; &#125;
        
            public String getNotes&#40;&#41; &#123; return notes; &#125;
        
            public void setNotes&#40;String notes&#41; &#123; this.notes = notes; &#125;
        
            public Map<String, Long> getEntries&#40;&#41; &#123; return entries; &#125;
        
            public void setEntries&#40;Map<String, Long> entries&#41; &#123; this.entries = entries; &#125;
        
            public void merge&#40;MemberMapping mapping&#41; &#123;
                entries.putAll&#40;mapping.getEntries&#40;&#41;&#41;;
            &#125;
        
            private Integer id;
            private String notes;
            private Map<String, Long> entries = new HashMap<String, Long>&#40;&#41;;
        
        &#125;
        So, I'm setting Longs as the values on a map, which must the be the difference.

        Both my SWF FormAction and SimpleFormController have this binder defined in initBinder().

        Code:
        binder.registerCustomEditor&#40;Long.class, "entries", new CustomNumberEditor&#40;Long.class, false&#41;&#41;;
        So my guess is that this is the cause.

        Christian

        Comment


        • #5
          Could you try to adapt the unit test to reproduce the problem. So far I don't see how this can be happening since the code that generates the error message codes is exactly the same in the case of SWF than for SimpleFormController.

          Erwin

          Comment


          • #6
            Wow, I finally discovered the problem. To your relief, it's not a problem with Spring Web Flow. The problem is that my Map has null values in one scenario (when bound by SWF), and real Long values when bound by SimpleFormController.

            When binding to a Map type (and possibly other Collections as well), if the entry you are trying to bind to doesn't already have a value, then the type specific message code isn't added to the error code list. Let me walk you through it...

            Basically these lines of code aren't executed (DefaultMessageCodesResolver.java: 104-106):
            Code:
            if &#40;fieldType != null&#41; &#123;
                codeList.add&#40;errorCode + CODE_SEPARATOR + fieldType..getName&#40;&#41;&#41;;
            &#125;
            Because these lines return null (BeanWrapperImpl.java: 1146-1155):
            Code:
            Object value = getPropertyValue&#40;propertyName&#41;;
            if &#40;value != null&#41; &#123;
                return value.getClass&#40;&#41;;
            &#125;
            
            ...
            
            return null;
            You can run the test case below and set break points on these lines to get a good idea of what's going on:
            • 1. BindException [line: 221] - resolveMessageCodes(String, String)
              2. BeanWrapperImpl [line: 1146] - getPropertyType(String)
              3. DefaultMessageCodesResolver [line: 104] - resolveMessageCodes(String, String, String, Class)
            What to look for when stepping:
            1. "fieldType" is set to null when the map entry value (entries[FOOBAR]) is null, or returns the type of the current (pre-binding) entry value.
            2. This is the code that determine the field type in #1.
            3. This is the code that addes the code typeMismatch.java.lang.Long is the fieldType isn't null.

            This seems to be a problem with BeanWrapperImpl's getPropertyType method. I think it's a valid use case to have a collection you want to bind values to, that does not yet have any values. You should get the same error codes regardless. It's not the existing values that count, it's the values you're trying to bind. Maybe if the fieldType is returned as null, then a secondary check should be performed to see what the value should be based on any custom editors for the named property?

            Should I go ahead and create a JIRA ticket for this?

            Here's the updated test case. It passes as expected, but shows that the message code (typeMismatch.java.lang.Long) is missing. Note the constructor to TestBean.
            Code:
            public class FormActionBindingTests extends TestCase &#123;
            
            public static class TestBean &#123;
            
            		private Map<String, Long> entries = new HashMap<String, Long>&#40;&#41;;
            
            		public TestBean&#40;&#41; &#123;
            			// The first triggers the typeMismatch.java.lang.Long code, the second does not.
            			//entries.put&#40;"FOOBAR", new Long&#40;0&#41;&#41;;
            			entries.put&#40;"FOOBAR", null&#41;;
            		&#125;
            
            		public Map<String, Long> getEntries&#40;&#41; &#123;
            			return entries;
            		&#125;
            
            		public void setEntries&#40;Map<String, Long> entries&#41; &#123;
            			this.entries = entries;
            		&#125;
            
            	&#125;
            
            	public void testMessageCodesOnBindFailure&#40;&#41; throws Exception &#123;
            		MockHttpServletRequest request = new MockHttpServletRequest&#40;&#41;;
            		request.setMethod&#40;"POST"&#41;;
            		request.addParameter&#40;"entries&#91;FOOBAR&#93;", "A"&#41;;
            		MockHttpServletResponse response = new MockHttpServletResponse&#40;&#41;;
            		MockRequestContext context = new MockRequestContext&#40;&#41;;
            		context.setLastEvent&#40;new ServletEvent&#40;request, response&#41;&#41;;
            		context.setProperty&#40;"method", "bindAndValidate"&#41;;
            
            		// use a for FormAction to do the binding
            		FormAction formAction = new FormAction&#40;&#41; &#123;
            			protected void initBinder&#40;org.springframework.webflow.RequestContext context,
            					org.springframework.validation.DataBinder binder&#41; &#123;
            				binder.registerCustomEditor&#40;Long.class, "entries", new CustomNumberEditor&#40;Long.class, false&#41;&#41;;
            			&#125;;
            		&#125;;
            		formAction.setFormObjectClass&#40;TestBean.class&#41;;
            		formAction.execute&#40;context&#41;;
            		Errors formActionErrors = &#40;Errors&#41; context.getRequestScope&#40;&#41;.get&#40;BindException.ERROR_KEY_PREFIX + "formObject"&#41;;
            		assertNotNull&#40;formActionErrors&#41;;
            		assertTrue&#40;formActionErrors.hasErrors&#40;&#41;&#41;;
            
            		// use a SimpleFormController to do the binding
            		SimpleFormController simpleFormController = new SimpleFormController&#40;&#41; &#123;
            			protected void initBinder&#40;javax.servlet.http.HttpServletRequest request,
            					org.springframework.web.bind.ServletRequestDataBinder binder&#41; throws Exception &#123;
            				binder.registerCustomEditor&#40;Long.class, "entries", new CustomNumberEditor&#40;Long.class, false&#41;&#41;;
            			&#125;;
            		&#125;;
            		simpleFormController.setCommandClass&#40;TestBean.class&#41;;
            		simpleFormController.setCommandName&#40;"formObject"&#41;;
            		ModelAndView modelAndView = simpleFormController.handleRequest&#40;request, response&#41;;
            		Errors simpleFormControllerErrors = &#40;Errors&#41; modelAndView.getModel&#40;&#41;.get&#40;BindException.ERROR_KEY_PREFIX + "formObject"&#41;;
            		assertNotNull&#40;simpleFormControllerErrors&#41;;
            		assertTrue&#40;simpleFormControllerErrors.hasErrors&#40;&#41;&#41;;
            
            		assertNotSame&#40;formActionErrors, simpleFormControllerErrors&#41;;
            		assertEquals&#40;formActionErrors.getErrorCount&#40;&#41;, simpleFormControllerErrors.getErrorCount&#40;&#41;&#41;;
            		assertEquals&#40;formActionErrors.getGlobalErrorCount&#40;&#41;, simpleFormControllerErrors.getGlobalErrorCount&#40;&#41;&#41;;
            		assertEquals&#40;formActionErrors.getFieldErrorCount&#40;"entries&#91;FOOBAR&#93;"&#41;, simpleFormControllerErrors
            				.getFieldErrorCount&#40;"entries&#91;FOOBAR&#93;"&#41;&#41;;
            		assertEquals&#40;1, formActionErrors.getFieldErrorCount&#40;"entries&#91;FOOBAR&#93;"&#41;&#41;;
            		assertEquals&#40;formActionErrors.getFieldError&#40;"entries&#91;FOOBAR&#93;"&#41;.getCodes&#40;&#41;.length, simpleFormControllerErrors.getFieldError&#40;
            				"entries&#91;FOOBAR&#93;"&#41;.getCodes&#40;&#41;.length&#41;;
            		System.out.println&#40;formActionErrors.getFieldError&#40;"entries&#91;FOOBAR&#93;"&#41;&#41;;
            		System.out.println&#40;simpleFormControllerErrors.getFieldError&#40;"entries&#91;FOOBAR&#93;"&#41;&#41;;
            	&#125;
            &#125;

            Comment


            • #7
              JIRA Issue Created

              JIRA Issue Created: http://opensource.atlassian.com/proj...rowse/SPR-1193

              Thanks for your help figuring this one out!

              Comment


              • #8
                Excellent analysis! I'll ask Juergen to take a look at this.

                Erwin

                Comment

                Working...
                X