Announcement Announcement Module
Collapse
No announcement yet.
Binding composite properties with DataBinder Page Title Module
Move Remove Collapse
X
Conversation Detail Module
Collapse
  • Filter
  • Time
  • Show
Clear All
new posts

  • Binding composite properties with DataBinder

    I have frequently come across the problem of binding multiple form fields to a single property. The whole binding infrastructure is geared to mapping a single input field to a single property, which is fine for most cases. It really falls over for things like dates - it seems a lot of people are using plain text input fields and demanding dates in a particular format. A far more common approach is to use multiple select boxes - one for day, month and year. The problem then is how to combine those into a single property value.

    This is a problem I've solved, and seen solved a number of times, and never been entirely happy with the result. I've recently come up with a mechanism that works quite well for me, and I'll post the code up if anyone's interested.

    The basic sequence is as follows:

    Submit a form with a composite property on it. Form fields should follow the convention propertyName_partName. Eg, dateOfBirth might have three fields, dateOfBirth_day, dateOfBirth_month and dateOfBirth_year.

    The controller uses a subclass of ServletRequestDataBinder that manipulates the array of PropertyValues to convert multiple parts of a composite property into a single property, the value of which gives all parts and values in the standard Properties format. Eg.

    Code:
       dateOfBirth_day 13
       dateOfBirth_month 2
       dateOfBirth_year 2005
    Would become:

    Code:
    dateOfBirth day=13\nmonth=2\nyear=2005
    I have an AbstractCompositePropertyEditor that converts the string into a Properties object. Then, you just have to extend it to convert the values in the Properties instance into whatever is expected by the underlying property, and register that PropertyEditor against your field.

    I'm a lot happier with this approach than other ones I've used anyway.

  • #2
    Dave

    I would be interested in seeing your code for this solution.

    Thanks.

    Comment


    • #3
      Well at least someone other than me is interested in this

      The first part is the composite property databinder. This extracts all request parameters that contain a '_' character and interpret them as parts of a property named by the portion of the parameter preceded by the '_' character.

      Code:
      package org.mooli.web.bind;
      
      import java.util.HashMap;
      import java.util.Map;
      
      import org.springframework.beans.MutablePropertyValues;
      import org.springframework.beans.PropertyValue;
      import org.springframework.beans.PropertyValues;
      import org.springframework.web.bind.ServletRequestDataBinder;
      
      /**
       * {@link org.springframework.validation.DataBinder} for converting composite properties
       * described by multiple property names into a single named property whose value is a
       * {@link java.util.Properties} instance representing the complex state of this property value. This
       * can then be used by subclasses of {@link org.mooli.web.bind.AbstractCompositePropertyEditor}
       * to convert {@link java.util.Properties} instances into complex objects.
       * @author dhewitt
       */
      public class CompositePropertyDataBinder extends ServletRequestDataBinder {
      
          /** The separator for complex property names. */
          public static final String PROPERTY_SEPARATOR = "_";
      
      
          /**
           * @param target the target to bind to
           * @param objectName the name of the object
           */
          public CompositePropertyDataBinder(final Object target, final String objectName) {
              super(target, objectName);
          }
      
          /**
           * Converts complex {@link PropertyValue}s into single instances with values. In
           * {@link java.util.Properties} string format. eg:
           *
           * <code>
           * name="birthDate_year"   value="1960"
           * name="birthDate_month"  value="10"
           * name="birthDate_date"   value="11"
           * </code>
           *
           * <code>
           * name="birthDate" value="year=1960\nmonth=10\ndate=11"
           * </code>
           * @see org.springframework.validation.DataBinder#bind&#40;org.springframework.beans.PropertyValues&#41;
           */
          public final void bind&#40;final PropertyValues pvs&#41; &#123;
              MutablePropertyValues mpvs = new MutablePropertyValues&#40;pvs&#41;;
              PropertyValue&#91;&#93; pvArray = mpvs.getPropertyValues&#40;&#41;;
              Map boundProperties = new HashMap&#40;&#41;;
              for &#40;int i = 0; i < pvArray.length; i++&#41; &#123;
                  if &#40;pvArray&#91;i&#93;.getName&#40;&#41;.indexOf&#40;PROPERTY_SEPARATOR&#41; > 0&#41; &#123;
                      bindProperty&#40;pvArray&#91;i&#93;, boundProperties&#41;;
                  &#125;
              &#125;
              mpvs.addPropertyValues&#40;boundProperties&#41;;
              super.bind&#40;mpvs&#41;;
          &#125;
      
          /**
           * Converts complex &#123;@link PropertyValue&#125;s into single instances with values. In
           * &#123;@link java.util.Properties&#125; string format. eg&#58;
           *
           * <code>
           * name="birthDate_year"   value="1960"
           * name="birthDate_month"  value="10"
           * name="birthDate_date"   value="11"
           * </code>
           *
           * <code>
           * name="birthDate" value="year=1960\nmonth=10\ndate=11"
           * </code>
           *
           * @param propertyValue the value to convert
           * @param boundProperties the map to add the converted property to
           */
          private void bindProperty&#40;final PropertyValue propertyValue, final Map boundProperties&#41; &#123;
              String pvName = propertyValue.getName&#40;&#41;;
              String value = pvName.substring&#40;pvName.indexOf&#40;PROPERTY_SEPARATOR&#41; + PROPERTY_SEPARATOR.length&#40;&#41;&#41;;
              value += "=" + propertyValue.getValue&#40;&#41;;
              String property = pvName.substring&#40;0, pvName.indexOf&#40;PROPERTY_SEPARATOR&#41;&#41;;
              if &#40;boundProperties.containsKey&#40;property&#41;&#41; &#123;
                  value = boundProperties.get&#40;property&#41; + "\n" + value;
              &#125;
              boundProperties.put&#40;property, value&#41;;
          &#125;
      &#125;

      The second part is the abstract composite property editor that converts a string representation of a properties object into a properties instance for translation into a single object, and vice versa:
      Code:
      package org.mooli.web.bind;
      
      import java.beans.PropertyEditorSupport;
      import java.io.ByteArrayOutputStream;
      import java.io.IOException;
      import java.util.Properties;
      
      import org.apache.commons.logging.Log;
      import org.apache.commons.logging.LogFactory;
      import org.springframework.beans.propertyeditors.PropertiesEditor;
      
      /**
       * &#123;@link java.beans.PropertyEditor&#125; for converting a string representation of a
       * &#123;@link java.util.Properties&#125; object into an arbitrary instance. Eg. could be used
       * to convert several fields corresponding to a single date into a single instance
       * of &#123;@link Date&#125;.
       * @author dhewitt
       */
      public abstract class AbstractCompositePropertyEditor extends PropertyEditorSupport &#123;
      
          /** Logging support. */
          private final Log logger = LogFactory.getLog&#40;getClass&#40;&#41;&#41;;
      
          /** Whether to ignor errors when binding a composite property. */
          private final boolean ignoreError;
      
          /**
           * @param ignoreError
           */
          public AbstractCompositePropertyEditor&#40;&#41; &#123;
              this&#40;false&#41;;
          &#125;
      
          /**
           * @param ignore if true, errors &#40;such as missing or invalid composite properties&#41;
           *  should be ignored.
           */
          public AbstractCompositePropertyEditor&#40;final boolean ignore&#41; &#123;
              super&#40;&#41;;
              this.ignoreError = ignore;
          &#125;
      
          /**
           * @see java.beans.PropertyEditor#setAsText&#40;java.lang.String&#41;
           */
          public final void setAsText&#40;final String text&#41; throws IllegalArgumentException &#123;
              super.setValue&#40;null&#41;;
              try &#123;
                  PropertiesEditor propertiesEditor = new PropertiesEditor&#40;&#41;;
                  propertiesEditor.setAsText&#40;text&#41;;
                  Properties properties = &#40;Properties&#41;propertiesEditor.getValue&#40;&#41;;
                  Object value = convertProperties&#40;properties&#41;;
                  super.setValue&#40;value&#41;;
              &#125; catch &#40;IllegalArgumentException e&#41; &#123;
                  if &#40;!ignoreError&#41; &#123;
                      throw e;
                  &#125; else &#123;
                      logger.debug&#40;"Unable to convert value '" + text
                              + "' but ignoreError is true - ignoring error", e&#41;;
                  &#125;
              &#125;
          &#125;
      
          /**
           * Convert an instance of &#123;@link Properties&#125; into an arbitrary object.
           * @param properties the Properties to convert
           * @return the object being edited
           * @throws IllegalArgumentException if the object could not be materialised
           *  from the given properties
           */
          protected abstract Object convertProperties&#40;final Properties properties&#41; throws IllegalArgumentException;
      
          /**
           * Helper method for safely accessing properties, throwing an &#123;@link IllegalArgumentException&#125;
           * if the property does not exist.
           * @param part the property to get
           * @param properties the properties to access
           * @return the value of the given property
           * @throws IllegalArgumentException if the property did not exist
           */
          protected final String getPropertyPart&#40;final String part, final Properties properties&#41; throws IllegalArgumentException &#123;
              if &#40;properties.containsKey&#40;part&#41;
                      && properties.getProperty&#40;part&#41; != null&#41; &#123;
                  return properties.getProperty&#40;part&#41;;
              &#125;
              if &#40;ignoreError&#41; &#123;
                  logger.debug&#40;"Part '" + part + "' not present, ignoring."&#41;;
                  return null;
              &#125; else &#123;
                  throw new IllegalArgumentException&#40;"Required part '" + part + "' not present."&#41;;
              &#125;
          &#125;
      
          /**
           * Helper method for safely accessing int properties, throwing an &#123;@link IllegalArgumentException&#125;
           * if the property does not exist or is unparseable.
           * @param part the property to get
           * @param properties the properties to access
           * @return the value of the given property
           * @throws IllegalArgumentException if the property did not exist or is not an int
           */
          protected final int getPropertyPartAsInt&#40;final String part, final Properties properties&#41; throws IllegalArgumentException &#123;
              try &#123;
                  String propertyPart = getPropertyPart&#40;part, properties&#41;;
                  return propertyPart == null ? 0 &#58; Integer.parseInt&#40;propertyPart&#41;;
              &#125; catch &#40;NumberFormatException e&#41; &#123;
                  throw new IllegalArgumentException&#40;"Unparseable part '"
                          + part + "' &#58; '" + properties.getProperty&#40;part&#41; + "'"&#41;;
              &#125;
          &#125;
      
          /**
           * Converts a &#123;@link Properties&#125; instance to a string representation.
           * @param properties the properties to convert
           * @return the string representation
           */
          private String convertStringToProperties&#40;final Properties properties&#41; &#123;
              ByteArrayOutputStream out = new ByteArrayOutputStream&#40;&#41;;
              try &#123;
                  properties.store&#40;out, null&#41;;
                  return new String&#40;out.toByteArray&#40;&#41;&#41;;
              &#125; catch &#40;IOException e&#41; &#123;
                  return e.getMessage&#40;&#41;;
              &#125;
          &#125;
      &#125;
      The final part is a concrete implementation of the property editor for handling dates. This has a configurable strategy for handling several different combinations of date/time part parsing, but the default behaviour is to just parse dates:

      Code:
      package org.mooli.web.bind;
      
      import java.util.Calendar;
      import java.util.Date;
      import java.util.Properties;
      
      /**
       * Editor that converts instances of &#123;@link Properties&#125; to &#123;@link Date&#125; objects.
       * @author dhewitt
       */
      public final class DateTimePropertyEditor extends AbstractCompositePropertyEditor &#123;
      
      
          /** Editor for setting the date portion. */
          public static final DatePartEditor DATE_EDITOR = new DatePartEditor&#40;
                  new String&#91;&#93; &#123;"year", "month", "date"&#125;,
                  new int &#91;&#93; &#123;Calendar.YEAR, Calendar.MONTH, Calendar.DATE&#125;&#41; &#123;
              /**
               * @see org.mooli.web.bind.DateTimePropertyEditor.DatePartEditor
               * #populateCalendar&#40;java.util.Calendar, java.util.Properties, org.mooli.web.bind.DateTimePropertyEditor&#41;
               */
              public void populateCalendar&#40;final Calendar cal, final Properties props, final DateTimePropertyEditor editor&#41; &#123;
                  String&#91;&#93; names = getNames&#40;&#41;;
                  int&#91;&#93; fields = getFields&#40;&#41;;
                  int year = editor.getPropertyPartAsInt&#40;names&#91;0&#93;, props&#41;;
                  if &#40;year > 0&#41; &#123;
                      int month = editor.getPropertyPartAsInt&#40;names&#91;1&#93;, props&#41;;
                      int date = editor.getPropertyPartAsInt&#40;names&#91;2&#93;, props&#41;;
      
                      cal.set&#40;year, month, 1&#41;;
                      int actualMin = cal.getActualMinimum&#40;fields&#91;2&#93;&#41;;
                      int actualMax = cal.getActualMaximum&#40;fields&#91;2&#93;&#41;;
                      int fixedDate = Math.max&#40;actualMin, Math.min&#40;date, actualMax&#41;&#41;;
                      cal.set&#40;fields&#91;2&#93;, fixedDate&#41;;
                  &#125;
              &#125;
          &#125;;
      
          /** Editor for setting the hour. */
          public static final DatePartEditor HOUR_EDITOR = new DatePartEditor&#40;"hour", Calendar.HOUR_OF_DAY&#41;;
      
          /** Editor for setting the minute. */
          public static final DatePartEditor MINUTE_EDITOR = new DatePartEditor&#40;"minute", Calendar.MINUTE&#41;;
      
          /** Editor for setting the second. */
          public static final DatePartEditor SECOND_EDITOR = new DatePartEditor&#40;"second", Calendar.SECOND&#41;;
      
          /** The default editors used to convert &#123;@link Properties&#125; to &#123;@link Date&#125;s - only
           * converts the date portion.
           */
          public static final DatePartEditor&#91;&#93; DEFAULT_EDITORS = new DatePartEditor&#91;&#93; &#123;
                  DATE_EDITOR&#125;;
      
          /** The editors to use for binding date and time, excluding seconds. */
          public static final DatePartEditor&#91;&#93; DATETIME_EDITORS = new DatePartEditor&#91;&#93; &#123;
                  DATE_EDITOR, HOUR_EDITOR, MINUTE_EDITOR&#125;;
      
          /** The editors to use for binding time, excluding seconds. */
          public static final DatePartEditor&#91;&#93; TIME_EDITORS = new DatePartEditor&#91;&#93; &#123;
                  HOUR_EDITOR, MINUTE_EDITOR&#125;;
      
          /** The editors to use for binding date and time, including seconds. */
          public static final DatePartEditor&#91;&#93; ALL_EDITORS = new DatePartEditor&#91;&#93; &#123;
                  DATE_EDITOR, HOUR_EDITOR, MINUTE_EDITOR, SECOND_EDITOR&#125;;
      
          /** The editors to use for extracting parts of a date. */
          private DatePartEditor&#91;&#93; editors;
      
      
          /**
           * Create a property editor using the default date part editors &#40;date only&#41;.
           */
          public DateTimePropertyEditor&#40;&#41; &#123;
              this&#40;DEFAULT_EDITORS&#41;;
          &#125;
      
          /**
           * Create a property editor using the specified date part editors.
           * @param newEditors the editors to use
           */
          public DateTimePropertyEditor&#40;final DatePartEditor&#91;&#93; newEditors&#41; &#123;
              super&#40;&#41;;
              this.editors = newEditors;
          &#125;
      
          /**
           * @param ignoreEmpty whether to ignore empty properties
           */
          public DateTimePropertyEditor&#40;final boolean ignoreEmpty&#41; &#123;
              this&#40;DEFAULT_EDITORS, ignoreEmpty&#41;;
          &#125;
      
          /**
           *
           * @param newEditors the editors to use
           * @param ignore whether to ignore empty properties
           */
          public DateTimePropertyEditor&#40;final DatePartEditor&#91;&#93; newEditors, final boolean ignore&#41; &#123;
              super&#40;ignore&#41;;
              this.editors = newEditors;
          &#125;
      
      
          /**
           * @see org.mooli.web.bind.AbstractCompositePropertyEditor#getAsProperties&#40;&#41;
           */
          protected Properties getAsProperties&#40;&#41; &#123;
              Calendar cal = Calendar.getInstance&#40;&#41;;
              cal.setTime&#40;&#40;Date&#41;getValue&#40;&#41;&#41;;
              Properties props = new Properties&#40;&#41;;
              for &#40;int i = 0; i < editors.length; i++&#41; &#123;
                  editors&#91;i&#93;.populateProperties&#40;cal, props&#41;;
              &#125;
              return props;
          &#125;
      
          /**
           * @see org.mooli.web.bind.AbstractCompositePropertyEditor#convertProperties&#40;java.util.Properties&#41;
           */
          protected Object convertProperties&#40;final Properties properties&#41; &#123;
              Calendar cal = Calendar.getInstance&#40;&#41;;
              cal.clear&#40;&#41;;
              for &#40;int i = 0; i < editors.length; i++&#41; &#123;
                  editors&#91;i&#93;.populateCalendar&#40;cal, properties, this&#41;;
              &#125;
              return cal.getTime&#40;&#41;;
          &#125;
      
          /**
           * Class for populating part of a date  from values specified in a
           * &#123;@link Properties&#125; object.
           */
          private static class DatePartEditor &#123;
      
              /** The field names to use when getting keys from the &#123;@link Properties&#125; instance. */
              private final String&#91;&#93; names;
      
              /** The &#123;@link Calendar&#125; fields to populate. */
              private final int&#91;&#93; fields;
      
              /**
               * @param name the name to use
               * @param field the field to set
               */
              public DatePartEditor&#40;final String name, final int field&#41; &#123;
                  this&#40;new String&#91;&#93; &#123;name&#125;, new int&#91;&#93; &#123;field&#125;&#41;;
              &#125;
      
              /**
               * @param newNames the names to use
               * @param newFields the corresponding fields to set
               */
              public DatePartEditor&#40;final String&#91;&#93; newNames, final int&#91;&#93; newFields&#41; &#123;
                  super&#40;&#41;;
                  this.names = newNames;
                  this.fields = newFields;
                  //assert names.length == fields.length;
              &#125;
      
              /**
               * Populate a &#123;@link Calendar&#125; based on an instance of &#123;@link Properties&#125;.
               * @param cal the calendar to populate
               * @param props the properties to use
               * @param editor the enclosing editor
               */
              public void populateCalendar&#40;final Calendar cal, final Properties props, final DateTimePropertyEditor editor&#41; &#123;
                  for &#40;int i = 0; i < names.length && i < fields.length; i++&#41; &#123;
                      cal.set&#40;fields&#91;i&#93;, editor.getPropertyPartAsInt&#40;names&#91;i&#93;, props&#41;&#41;;
                  &#125;
              &#125;
      
              /**
               * Populate a &#123;@link Properties&#125; based on an instance of &#123;@link Calendar&#125;.
               * @param cal the calendar to use
               * @param props the properties to populate
               */
              public final void populateProperties&#40;final Calendar cal, final Properties props&#41; &#123;
                  for &#40;int i = 0; i < names.length && i < fields.length; i++&#41; &#123;
                      props.setProperty&#40;names&#91;i&#93;, String.valueOf&#40;cal.get&#40;fields&#91;i&#93;&#41;&#41;&#41;;
                  &#125;
              &#125;
      
              /**
               * @return Returns the fields.
               */
              public final int&#91;&#93; getFields&#40;&#41; &#123;
                  return fields;
              &#125;
              /**
               * @return Returns the names.
               */
              public final String&#91;&#93; getNames&#40;&#41; &#123;
                  return names;
              &#125;
      
          &#125;
      
      &#125;
      This could all be tidied up somewhat, but the basic mechanism works well for me, and I have used this approach several times for other complex objects.

      Comment


      • #4
        Minor comment

        Dave,

        I would be careful about using "_" as it seems to have a magic meaning within Spring.

        Other than that, looks good

        Comment


        • #5
          Is all this necessary?

          I'm not sure I see the advantages of your implementation over using nested properties, supported in Spring without requiring a custom binder.

          Code:
          <spring&#58;bind path="myDate.year">
          <select name="$&#123;status.expression&#125;"/>">
          	<option value="0">Year</option>
          	<c&#58;forEach begin="$&#123;begin&#125;" end="$&#123;end&#125;" varStatus="loop">
          		<option value="$&#123;loop.index&#125;"
          			<c&#58;if test="$&#123;current == loop.index&#125;"> selected="selected"</c&#58;if>><c&#58;out
          			value="$&#123;loop.index&#125;" /></option>
          	</c&#58;forEach>
          </select>
          </spring&#58;bind>
          
          // Similar code repeated for month and day
          myDate is a wrapper object I made, a simple bean with month, year and day properties. On submitting this form Spring will call myDate.setYear(year), without a custom binder or property editor.

          What advantages do you think your approach has over using something like the above, which fits within the available Spring structure? In other words, what am I missing?

          Comment


          • #6
            simple approach using javacript

            A less generic but simple approach is to use javacript. On form submit, combine the parts into a hidden field. To Spring, that would look as if you were using a plain text input field. IMO, this qualifies as presentation logic and can therefore be handled at the view level.

            Cheers, Dan

            Comment


            • #7
              Complexity and Focus

              Wow, that's one hell of a complex solution! I like that it allows you to use the same mechanisms in Spring that other fields use, but it's an awful lot of code for such a simple requirement.

              Someone else mentioned using an intermediate object with three fields (I assume they were Strings, but it's not important). Does that mean that instead of Date objects in your domain you instead of this FauxDate (or whatever you call it) object? That's not good... now you're hacking your domain just to get around an issue....

              So on page 469 of Java Development with the Spring Framework, it has the proposed solutions to this issue.

              The first is to use Javascript -- as someone already suggested -- to merge the three fields into a single field and then use the CustomDateEditor.

              The second -- which is the way I decided to go on this issue -- is to simply override onBind() to accomplish the task.

              In my JSP, I have three fields -- dateOfBirthMonth, dateOfBirthDay, and dateOfBirthYear. I needed to put them into my command object, which is a Player object containing a java.util.Date for date of birth. Here's how I handled this in onBind():

              Code:
                  protected void onBind( HttpServletRequest request, Object command ) throws Exception
                  {
                      Player p = (Player)command;
                      Date dateOfBirth = CommonRequestUtils.getCompositeDate( request, "dateOfBirthMonth", "dateOfBirthDay", "dateOfBirthYear" );        
                      p.setBirthDate( dateOfBirth );        
                  }
              Nice and simple. Here's the getCompositeDate method which automatically handles invalid or missing date components. I use a custom exception, InvalidCompositeDateBindingException if the date is invalid -- it contains the offending field and value that failed so that I can tailor a validation message if need be.

              Code:
                  public static final Date getCompositeDate( 
                          HttpServletRequest request,
                          String monthFieldName,
                          String dayFieldName,
                          String yearFieldName )
                      throws ServletRequestBindingException
                  {
                      // Create an empty Calendar object for setting; we don't want any
                      // time left over from the getInstance() call.
                      Calendar cal = Calendar.getInstance();
                      cal.setLenient( false );
                      cal.clear();
                      
                      int year = RequestUtils.getRequiredIntParameter( request, yearFieldName );
                      int month = RequestUtils.getRequiredIntParameter( request, monthFieldName ) - 1;
                      int day = RequestUtils.getRequiredIntParameter( request, dayFieldName );
                      
                      cal.set( Calendar.YEAR, year );
                      cal.set( Calendar.MONTH,  month );
                      cal.set( Calendar.DAY_OF_MONTH, day );       
                     
                      // If any of the field setters failed, an InvalidCompositeDateBindingException will be thrown
                      // containing the offending field.
                     try
                      {
                          return cal.getTime();
                      }
                      catch ( IllegalArgumentException iiae )
                      {            
                          int fieldCode = -1;
                          int fieldValue = -1;
                          try
                          {
                              fieldCode = Calendar.class.getField( iiae.getMessage() ).getInt( cal );
                              switch( fieldCode )
                              {
                                  case Calendar.YEAR:
                                      fieldValue = year;
                                      break;
                                  case Calendar.MONTH:
                                      fieldValue = month;
                                      break;
                                  case Calendar.DAY_OF_MONTH:
                                      fieldValue = day;
                                      break;
                              }
                          }
                          catch ( NoSuchFieldException nsfe )
                          {                
                              // ignore
                          }
                          catch ( IllegalAccessException iae )
                          {            
                              // ignore
                          }
                          
                          throw new InvalidCompositeDateBindingException( fieldCode, fieldValue );  
                      }
                  }
              }
              Hope this helps.

              Dan
              Last edited by dantelope; Jan 6th, 2006, 10:11 PM.

              Comment


              • #8
                You should post a feature request for this exact thing on the JIRA, and then attach your proposed solution. The Spring developers are very good at listening to the community's needs when it comes to stuff like this. (Though, don't be offended if Juergen totally rewrites it. That just means he thought it was a good idea

                FWIW, I've much wanted this functionality, so I hope it gets included into the main Spring.

                Comment


                • #9
                  On second thought...

                  After having actually tried to use the method I posted above, I've decided that way sucks...

                  I was trying to convert the form into a domain object, but I think that's not a smart way to. Instead, I've created a form object which contains a domain object and a helper bean consisting of the month, day, and year and a date-conversion method that either returns a date or, if the date is invalid, null.

                  Then I do standard empty checks in my validator to ensure the date fields are filled in; then I check the date return for null to see if the date is invalid.

                  This is significantly easier to deal with and makes much more sense from a development standpoint. It's easier to deal with these objects than to try to shoehorn Spring into doing something tricky like converting an entire form into a very complex domain object.

                  Oh well, live and learn.

                  Dan

                  Comment


                  • #10
                    is it possible to make this bind withour never talking about day, month, year? to work only with date?
                    this code is not working, but i think that there is some way to fix it... am i right?
                    Code:
                    <Spring:bind path="fxRate.fr_rpd_rp_date">
                     <input type="text" name="fr_rpd_rp_date" size="10" value="${status.value}" />
                    </Spring:bind>
                    the date is shown, like yyyy-mm-dd... without any problem, but when this field is modificated, the onSubmid method is not called because, i think, of some validation of spring, that cannot "translate" this new yyyy-mm-dd in Date again... can u plz help me with that? i dont know, may be that is the best way that dhewitt showed us, but i found it a little bit big... :o
                    thks a lot

                    Comment


                    • #11
                      Binding several input fields straight into a java.util.Date

                      Dear All,

                      I want to share with you my findings on the topic of binding data originating from different HTML inputs straight into a java.util.Date.

                      It does not require any javascript, any new PropertyEditor, anything... Hopefully you will also like it!!!!

                      Let's consider an example. Your HTML view has:
                      • an input for the day where you expect the user to enter DD/MM/YYYY
                      • a drop down for selecting hours
                      • a drop down for selecting minutes

                      Your form object has a java.util.Date attribute called travelDate.

                      The binding
                      1. By naming all the HTML elements after the attribute name: travelDate
                      2. Register a CustomDateEditor in your Controller (binder.registerCustomEditor) with a comma separated list of patterns that match in order the appearance of your HTML input fields. This is the trick!!

                      Spring will invoke the setAsText of your CustomDateEditor by passing the user input in the form of comma separated string. For instance '07/02/2007,14,37'.

                      This will in turn be parsed as a date.

                      Comment


                      • #12
                        Originally posted by dantelope View Post
                        Instead, I've created a form object which contains a domain object and a helper bean consisting of the month, day, and year and a date-conversion method that either returns a date or, if the date is invalid, null.
                        Yeah, that's similar to what we did in our latest project.
                        We've created a holder object, binded its properties, e.g. dateField.day, dateField.month, dateField.year and placed date conversion/validation code into
                        the holder. Note that even if all three fields are valid, the resulting object may not represent a valid date.

                        It does not require you to construct any custom editors. Custom editors also do not have access to Errors, so validators are better here.

                        Comment


                        • #13
                          Adelinor,

                          your solution seems like a nice, straightforward approach... if there wouldn't be bind / validation errors.

                          Let's go with the initial example of having three input fields; day, month, year. These should be bound to a java.util.Date using your technique. How do you manage to refill the form fields in case of bind or validation errors? The getAsText always returns all values in a comma seperated string (e.g. 09,02,1941). Your form fields end up showing the following values:
                          day = 09
                          month = 09
                          year = 09,0

                          Am I missing something here? Is there a way to solve this issue without writing custom code for it and simply using the CustomeDateEditor as you suggested?

                          Comment


                          • #14
                            Display of bind errors

                            Indeed this where you need to write some unelelgant code in the JSP...

                            Using the spring:bind custom tag, you get back an array of input strings. Using JSTL, you can go through this array and set variable for each individual member of the date.

                            Considering the example of three inputs for day, month and year, you would get the individual values like that:

                            Code:
                            <spring:bind path="form.birthDate">
                                <c:forEarch items="${status.value}" var="item" varStatus="loopStatus">
                                    <c:choose>
                                        <c:when test="${loopStatus == 0}">
                                            <c:set var="day" value="${item}"/>
                                        </c:when>
                                        <c:when test="${loopStatus == 1}">
                                            <c:set var="month" value="${item}"/>
                                        </c:when>
                                        <c:when test="${loopStatus == 2}">
                                            <c:set var="year" value="${item}"/>
                                        </c:when>
                                    </c:choose>
                                </c:forEach>
                            </spring:bind>
                            After the previous code (that could be included in a JSP include or tile), the individual date field values are available for display:

                            Code:
                            <input name="birthDate" value="<c:out value="${day}"/>">
                            <input name="birthDate" value="<c:out value="${month}"/>">
                            ...
                            -- adelino

                            Comment


                            • #15
                              Thanks for the feedback. This, however, doesn't look as straightforward anymore Especially not if you have a lot of pages where you need dates like this. Don't really like the bulky code in the JSPs for such a simple task.

                              Besides that it might be kinda tricky to implement it when you want to use Spring 2.0 form tags, won't it?

                              Although I haven't tried it for this scenario I think I prefer the approach with the holder object mentioned above.

                              Comment

                              Working...
                              X