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

  • dhewitt
    started a topic Binding composite properties with DataBinder

    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.

  • fuzziman
    replied
    do the following at your own peril

    As I stated above, this is not the best way to do it, but If you are binding composite String fields (e.g. phone number field, seperated into area code and phone number), then this method could be usable.

    Once again, applies to spring: bind tags only.

    java code:
    Code:
    /*	 custom editor that is able to join 2 composite phone number fields with a "-" */ 
    class CustomPhoneNumberEditor extends PropertyEditorSupport {
    		public CustomPhoneNumberEditor() {}
    		public void setAsText(String text) {
    			if (text != null) {
    				if (",".equals(text))
    					setValue(null);    // both fields were empty
    				else
    					setValue(text.replace(',', '-'));
    			}
    		}
    		public String getAsText() {
    			//so that textbox wont display "null"
    			if (getValue()==null)
    			    return null; 
    			
    			String result = (String)getValue();
    			
    			// fn:split doesn't support empty tokens
    			// this allows values to display properly when nothing was entered
    			if (result.startsWith("-")) 
    				result = " " + result;  
    			if (result.endsWith("-")) 
    				result = result + " ";
    			return result;
    		}
    	};
    	
    }
    public void initBinder(HttpServletRequest request, ServletRequestDataBinder binder) throws Exception {
       binder.registerCustomEditor(String.class, "phoneNumber", new CustomPhoneNumberEditor());		
    }
    JSP code:

    Code:
    <spring:bind path="command.phoneNumber">
    	<input type="text" value="${fn:trim(fn:split(status.value, '-')[0])}" name="${status.expression }" />
    	<input type="text" value="${fn:trim(fn:split(status.value, '-')[1])}" name="${status.expression }" />
    </spring:bind>
    Following on from my previous post, this code solves the problem when you dont enter an area code, and you enter a phone number. This scenario will post ",91234567" which binds to "-91234567". When it returns to the JSP and you do a split, you won't have 2 elements in the list (since split doesn't support null tokens).

    The magic of the hack lies in manually detecting and changing your null tokens to a space " " , then using fn:trim to get rid of it again.

    So in conclusion, this was quite elegent in theory, but ended up looking like a big hack. I'd go with the wrapping object...

    Hope that may have been helpful to someone.

    Leave a comment:


  • fuzziman
    replied
    as an experiment out of interest

    after some investigation, i would also conclude as with others that the wrapping object would be the most straightforward implementation, especially if you are using spring 2 form tags.

    However, there is something so deceptively attractive about adelinor's method to take advantage of spring automatic comma seperated binding, and there is a way to implement the view in a single line, using the function tag library. (only applies to spring bind tags)

    The key is this: fn: split(status.value, '/')[0]

    When getAsText() of your CustomDateEditor is called, it will return the formatted string, which you can split out.

    Example code:
    Code:
    <spring:bind path="command.dob">
    	<select name="${status.expression }" id="day">
    		<c:forEach items="${dayMap}" var="elem">
    			<option value="${elem.key}" <c:if test="${fn:split(status.value, '/')[0] eq elem.key}" >selected="selected"</c:if>>${elem.value }</option>
    		</c:forEach>		
    	</select>
    	<select name="${status.expression }" id="month">
    		<c:forEach items="${monthMap}" var="elem">
    			<option value="${elem.key}" <c:if test="${fn:split(status.value, '/')[1] eq elem.key}" >selected="selected"</c:if>>${elem.value }</option>
    		</c:forEach>				
    	</select>
    	<select name="${status.expression }" id="year">
    		<c:forEach items="${yearMap}" var="elem">
    			<option value="${elem.key}" <c:if test="${fn:split(status.value, '/')[2] eq elem.key}" >selected="selected"</c:if>>${elem.value }</option>
    		</c:forEach>
    	</select>
    </spring:bind>
    For completeness, here is the java code:

    Code:
    class CompositeCustomDateEditor extends CustomDateEditor{
        public CompositeCustomDateEditor(DateFormat dateFormat, boolean allowEmpty) {
            super(dateFormat, allowEmpty);
        }
        public void setAsText(String text) {
            if (text != null) {
              super.setAsText(text.replace(',', '/'));
            }
        }
    }
    
    public void initBinder(HttpServletRequest request, ServletRequestDataBinder binder) throws Exception {
    
        SimpleDateFormat dateFormat = new SimpleDateFormat("dd/MM/yyyy");
        binder.registerCustomEditor( Date.class, "dob", new CompositeCustomDateEditor(dateFormat, true));		
    }
    You will notice that these dropdowns do not have the ability to be blank. This is because it would lead to a bug where if day was not selected, and month and year were selected, after submit, the custom editor cannot set a valid date, hence you would lose the values you selected for month and year. I don't think there is a way around this. As nekoval pointed out, there is no binding error support in custom editors.

    If you are binding composite string fields though, you can hack around this, see my next post below.

    Leave a comment:


  • haasip satang
    replied
    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.

    Leave a comment:


  • adelinor
    replied
    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

    Leave a comment:


  • haasip satang
    replied
    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?

    Leave a comment:


  • nekoval
    replied
    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.

    Leave a comment:


  • adelinor
    replied
    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.

    Leave a comment:


  • orusso
    replied
    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

    Leave a comment:


  • dantelope
    replied
    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

    Leave a comment:


  • sethladd
    replied
    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.

    Leave a comment:


  • dantelope
    replied
    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.

    Leave a comment:


  • dan.baumann
    replied
    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

    Leave a comment:


  • debradley@gmail.com
    replied
    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?

    Leave a comment:


  • Colin Yates
    replied
    Minor comment

    Dave,

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

    Other than that, looks good

    Leave a comment:

Working...
X