Announcement Announcement Module
Collapse
No announcement yet.
Binding String[] to java.util.Set Page Title Module
Move Remove Collapse
X
Conversation Detail Module
Collapse
  • Filter
  • Time
  • Show
Clear All
new posts

  • Binding String[] to java.util.Set

    My domain class contains a Set property:

    Code:
    class User
    {
    	// Contains com.foobar.Role instances...
    	private Set roles = new TreeSet();
    	
    	public Set getRoles()
    	{
    		// ...
    	}
    	
    	public void setRoles(Set roles)
    	{
    		// authorization check ommited
    		this.roles = roles;
    	}
    }
    I'd like to bind a form to this command class.
    One of the fields on the form is a multi-select box, indicating the roles assigned to the user instance being edited.

    So I create and register a custom PropertyEditor for the field.
    (Since the BeanWrapper/DataBinder allow only one custom property editor for a given property path, I created an editor for java.util.Set instead of com.foobar.Role. Someone has to create a new HashSet/TreeSet somewhere, right?)

    Code:
    binder.registerCustomEditor(Set.class, "roles", new RoleSetPropertyEditor());
    The custom property editor does get called as expected.
    If I select roles 1,4 and 5 in the HTML multi-select, my setAsText(String text) is called with the String "1,4,5".
    Currently I just split on the comma, and continue with RolePropertyEditor instances from that point.

    Now, some of the PropertyEditors in use in the system produce Strings representations of the form "key1,key2" (for a single entity).
    I forsee problems writing "SomeEntitySetPropertyEditors" for these domain objects, since some of these will be presented in similar multiple-select elements in the HTML pages. At that point parsing becomes less clean, since a comma could be part of an array element, or just the delimiter...

    My questions are:

    1) Where exactly is the String[] converted to a comma-separated String?
    2) Is this a JavaBeans convention, or a Spring-specific implementation?
    3) Is there a way to utilize a more granular binding process? E.g.: is there a way to receive the String[] value as a String[] in my PropertyEditor instead of a concatenation?

  • #2
    I've been playing with this binding problem a bit more.

    All the controllers that process forms containing Set properties use a PropertyEditor utility class:
    Code:
    // Register an editor for a regular property
    binder.registerCustomEditor(FooBar.class, new FooBarPropertyEditor(someDependency));
    // Register an editor for some Set-based property
    binder.registerCustomEditor(Set.class, "roles", new SetPropertyEditor(new RolePropertyEditor()));
    SetPropertyEditor being the mentioned utility class:

    Code:
    public class SetPropertyEditor extends PropertyEditorSupport
    {
        private PropertyEditor itemEditor;
    
        public SetPropertyEditor(PropertyEditor itemEditor)
        {
            this.itemEditor = itemEditor;
        }
    
        /**
         * Dummy implementation (for completeness)
         * 
         * Spring will call this during binding but it's of no use, since there's no easy way to bind
         * a "select multiple" HTML form element to the comma-separated String returned by the binder. 
         * 
         * @return
         */
        public String getAsText()
        {
            Set values = (Set)getValue();
            StringBuffer textBuffer = new StringBuffer();
            for(Iterator i = values.iterator(); i.hasNext();)
            {
                Object item = (Object)i.next();
                itemEditor.setValue(item);
                textBuffer.append(itemEditor.getAsText());
                if(i.hasNext()) textBuffer.append(',');
            }
            return textBuffer.toString();
        }
    
        protected Set createSet(int size)
        {
            return new HashSet(size);
        }
    
        public void setAsText(String text) throws IllegalArgumentException
        {
            Set values = StringUtils.commaDelimitedListToSet(text);
            Set items = createSet(values.size());
            for(Iterator i = values.iterator(); i.hasNext();)
            {
                String value = (String)i.next();
                itemEditor.setAsText(value);
                Object item = itemEditor.getValue();
                items.add(item);
            }
            setValue(items);
        }
    }
    The form in the JSP view is similar to:
    Code:
    <spring&#58;bind path="myForm.roles">
    	<select name="$&#123;status.expression&#125;" multiple>
    	<c&#58;forEach var="role" items="$&#123;roles&#125;">
    		<option $&#123;vf&#58;inSet&#40;role, myForm.roles&#41; ? 'selected' &#58; ''&#125; value="$&#123;role.id&#125;">
    			<c&#58;out value="$&#123;role.name&#125;"/>
    		</option>
    	</c&#58;forEach>
    	</select>
    </spring&#58;bind>
    vf:inSet in above fragment is a JSP 2.0 equivalent for java.util.Set#contains(Object)
    It seems that support for collections, and in particular Sets, still sucks in JSTL 1.1/JSP 2.0, since the default index operator doens't work for sets.

    Is there an easier way to bind Set properties in Spring?

    Comment


    • #3
      1.1.4 Example?

      Now that 1.1.4 has been updated, per the JIRA request http://opensource.atlassian.com/proj...browse/SPR-552 , could someone demonstrate the proper way to implement this example's custom PropertyEditor?

      Thanks!

      Comment


      • #4
        Re: 1.1.4 Example?

        Originally posted by code_kungfu
        Now that 1.1.4 has been updated, per the JIRA request http://opensource.atlassian.com/proj...browse/SPR-552 , could someone demonstrate the proper way to implement this example's custom PropertyEditor?
        I think the easiest way is as follows:

        1) Implement or override setValue(Object)
        2) Implement or override setAsText(String)

        Make this implementation delegate to setValue()

        3) Inside the setValue() implementation, provide the following logic:

        a) If the passed-in object is of type String, convert it to a Set (or other collection) of a single element

        b) If the passed-in object is op type String[], convert all array elements to collection elements.

        You could hardcode the actual conversion from String/String[] to your element type, or provide a configurable nested PropertyEditor. (But watch out for thread-safety in clients!)
        Apart from that you could also make the collection type configurable.

        I'll post an example to this thread later.

        Comment


        • #5
          Example

          Here is the code the binds a Set<Long>, which I am using for the type of my object identifiers. This implementation could easily be changed to a generic approach, provided the generic type could be instantiated with a single String argument constructor and you passed the instantiated type Class in the PropertyEditor constructor. If I have the need to support any other homogeneous Set type besides Long, I'll make a generic implementation and post it here...

          Code:
          public class IdSetEditor extends PropertyEditorSupport &#123;
              
          	public String getAsText&#40;&#41; &#123;
                  Set<Long> values = &#40;Set<Long>&#41;getValue&#40;&#41;;
                  StringBuffer textBuffer = new StringBuffer&#40;&#41;;
                  
                  for &#40;Long entity &#58; values&#41; &#123;
                  	textBuffer.append&#40;entity.toString&#40;&#41;&#41;;
                  	textBuffer.append&#40;','&#41;;
                  &#125;
                  
                  // Drop the last comma if necessary
                  if &#40;textBuffer.length&#40;&#41; > 0&#41;
                  	textBuffer.deleteCharAt&#40;textBuffer.length&#40;&#41;-1&#41;;
                  
                  return textBuffer.toString&#40;&#41;;
              &#125;
          
          	/**
          	 * Set the set value using either a String&#91;&#93;, CSV, or Set
          	 * 
          	 * @param value The new value.
          	 */
          	public void setValue&#40;Object value&#41; &#123;
          		if &#40;value == null || value instanceof Set&#41; &#123;
          			super.setValue&#40;value&#41;;
          			return;
          		&#125;
          		
          		// Convert the String&#91;&#93; into a Set
          		if &#40;value instanceof String&#91;&#93;&#41; &#123;
          			String&#91;&#93; ids = &#40;String&#91;&#93;&#41;value;
          			Set<Long> values = createSet&#40;ids.length&#41;;
          			
          			for &#40;String id &#58; ids&#41; &#123;
          				values.add&#40;Long.valueOf&#40;id&#41;&#41;;
          			&#125;
          			
          			super.setValue&#40;values&#41;;
          			return;
          		&#125;
          		
          		// Convert the CSV into a Set &#40;fallback for < Spring 1.1.4&#41;
          		if &#40;value instanceof String&#41; &#123;
          	        Set<String> ids = StringUtils.commaDelimitedListToSet&#40;&#40;String&#41;value&#41;;
          	        Set<Long> values = createSet&#40;ids.size&#40;&#41;&#41;;
          	        for &#40;String id &#58; ids&#41; &#123;
          	        	values.add&#40;Long.valueOf&#40;id&#41;&#41;;
          	        &#125;
          
          	        setValue&#40;values&#41;;
          	        return;
          		&#125;
          		
          		throw new IllegalArgumentException&#40;"Value must be a Set, String&#91;&#93;, or CSV"&#41;;
          	&#125;	
          
              public void setAsText&#40;String text&#41; throws IllegalArgumentException &#123;
              	setValue&#40;text&#41;;
              &#125;
              
              protected Set<Long> createSet&#40;int size&#41; &#123;
                  return new HashSet<Long>&#40;size&#41;;
              &#125;

          Comment


          • #6
            Binding a Collection of non-Strings: My Solution...

            [Environment: Spring 1.1.5 and Java 1.4]

            I think I've run into the same thing, basically...

            My form has a multi-selected list box, and on submit an array of String values is placed on the request as a parameter. On my domain object (command), I have a Set of Integers (not Strings).

            The CustomerCollectionEditor (BeanWrapperImpl) converts the string array to a Set containing Strings on my command. I really need the Set to contain Integers, not String representations of Integers.

            This isn't the cleanest code (it works though)...

            Code:
            public class CollectionEditor extends PropertyEditorSupport &#123;
                public CollectionEditor&#40;Class collectionType, Transformer transformer&#41; &#123;
                    if &#40;collectionType == null&#41; &#123;
                        throw new IllegalArgumentException&#40;"Collection type is required"&#41;;
                    &#125;
                    if &#40;!collectionType.isInterface&#40;&#41;&#41; &#123;
                        throw new IllegalArgumentException&#40;"Collection type has to be an interface"&#41;;
                    &#125;
                    if &#40;!Collection.class.isAssignableFrom&#40;collectionType&#41;&#41; &#123;
                        throw new IllegalArgumentException&#40;
                                "Collection type &#91;" + collectionType.getName&#40;&#41; + "&#93; does not implement &#91;java.util.Collection&#93;"&#41;;
                    &#125;
                    this.collectionType = collectionType;
                    this.transformer = transformer;
                &#125;
            
                public void setAsText&#40;String text&#41; throws IllegalArgumentException &#123;
                    setValue&#40;text&#41;;
                &#125;
            
                public void setValue&#40;Object value&#41; &#123;
                    if &#40;this.collectionType.isInstance&#40;value&#41;&#41; &#123;
                        // Use the source value as-is, as it matches the target type.
                        super.setValue&#40;value&#41;;
                    &#125;
                    else if &#40;value instanceof Collection&#41; &#123;
                        // Convert Collection elements to array elements.
                        Collection source = &#40;Collection&#41; value;
                        Collection target = createCollection&#40;this.collectionType, source.size&#40;&#41;&#41;;
                        for &#40;Iterator i = source.iterator&#40;&#41;; i.hasNext&#40;&#41;; &#41;
                            target.add&#40;transformer.transform&#40;i.next&#40;&#41;&#41;&#41;;
                        super.setValue&#40;target&#41;;
                    &#125;
                    else if &#40;value != null && value.getClass&#40;&#41;.isArray&#40;&#41;&#41; &#123;
                        // Convert array elements to Collection elements.
                        int length = Array.getLength&#40;value&#41;;
                        Collection target = createCollection&#40;this.collectionType, length&#41;;
                        for &#40;int i = 0; i < length; i++&#41; &#123;
                            target.add&#40;transformer.transform&#40;Array.get&#40;value, i&#41;&#41;&#41;;
                        &#125;
                        super.setValue&#40;target&#41;;
                    &#125;
                    else &#123;
                        // A plain value&#58; convert it to a Collection with a single element.
                        Collection target = createCollection&#40;this.collectionType, 1&#41;;
                        target.add&#40;transformer.transform&#40;value&#41;&#41;;
                        super.setValue&#40;target&#41;;
                    &#125;
                &#125;
            
                protected Collection createCollection&#40;Class collectionType, int initialCapacity&#41; &#123;
                    if &#40;List.class.equals&#40;collectionType&#41;&#41; &#123;
                        return new ArrayList&#40;initialCapacity&#41;;
                    &#125;
                    else if &#40;SortedSet.class.equals&#40;collectionType&#41;&#41; &#123;
                        return new TreeSet&#40;&#41;;
                    &#125;
                    else &#123;
                        return CollectionFactory.createLinkedSetIfPossible&#40;initialCapacity&#41;;
                    &#125;
                &#125;
            
                private final Class collectionType;
                private final Transformer transformer;
                
            &#125;
            I copied the code from CustomCollectionEditor and hacked it... like I said, not clean... I'll refactor it later.

            Transformer comes from org.apache.commons.collections.Transformer.

            Here's an example of how I use this in a SimpleFormController's initBinder(...):

            Code:
            binder.registerCustomEditor&#40;Set.class, new CollectionEditor&#40;Set.class, new Transformer&#40;&#41; &#123;
                public Object transform&#40;Object arg&#41; &#123;
                    return &#40;arg == null ? null &#58; new Integer&#40;arg.toString&#40;&#41;&#41;&#41;;
                &#125;
            &#125;&#41;&#41;;
            The transformer just turns the string into the desired type. The nice thing is that this works for all collections types (like the code I ripped) and it will handle any transformation since you provide that code.

            I think I'd solved this problem differently using Java 1.5. I also don't know how well this would handle an exception being thrown from an invalid string entry (e.g. new Integer("booty")).

            Anybody else have any other improvements on this?
            Christian

            Comment


            • #7
              Link to Jira issue

              I've used your code with a very small change. See http://opensource.atlassian.com/proj...browse/SPR-761

              I am also using the following reflection based transformer to demonstrate reflection to students:

              public class StringConstructorTransformer implements Transformer {
              private final Constructor constructor;

              public StringConstructorTransformer(Class clazz) {
              Class[] parameters = new Class[] {String.class};
              try {
              constructor = clazz.getConstructor(parameters);
              } catch (Exception e) {
              throw new RuntimeException(e);
              }
              }

              public Object transform(Object o) {
              try {
              return o == null ? null : constructor.newInstance(new Object[] {o.toString()});
              } catch (Exception e) {
              throw new RuntimeException(e);
              }
              }
              }


              Transformer intTransformer =
              new StringConstructorTransformer(Integer.class);

              to which one could add a String constructor

              public StringConstructorTransformer(String className) {...}

              For ease of configuration.

              Comment


              • #8
                Related issue

                You use the following code:

                Code:
                <spring:bind path="myForm.roles">
                   <select name="${status.expression}" multiple>
                   <c:forEach var="role" items="${roles}">
                      <option ${vf:inSet(role, myForm.roles) ? 'selected' : ''} value="${role.id}">
                         <c:out value="${role.name}"/>
                      </option>
                   </c:forEach>
                   </select>
                </spring:bind>
                but I believe we should be able to do

                Code:
                <spring:bind path="myForm.roles">
                   <select name="${status.expression}" multiple>
                   <c:forEach var="role" items="${roles}">
                      <option ${vf:inSet(role, status.value) ? 'selected' : ''} value="${role.id}">
                         <c:out value="${role.name}"/>
                      </option>
                   </c:forEach>
                   </select>
                </spring:bind>
                instead. I.e. reuse the status.value of the binded path. Apparently there's an issue in Spring that doesn't let us do that as status.value will return a String instead of the original value.

                See http://forum.springframework.org/showthread.php?t=13630
                Last edited by robyn; May 14th, 2006, 12:47 PM.

                Comment


                • #9
                  Custom Collection of Numbers Editor

                  Hello All,

                  I had struggled with a similar problem, and I ended up writing this propertyEditor that takes in a comma-separated list of values and generates a Collection of Numbers.

                  You can use any instantiatable subclass of Collection (that's things like HashSet, TreeSet, ArrayList, Vector, etc) for the Collection class,

                  And you can use any Number that extends the java Number class for the collection elements.

                  I found it to be extremely useful, you may feel the same.

                  Code:
                  import org.springframework.util.StringUtils;
                  import org.springframework.util.NumberUtils;
                  
                  import java.beans.PropertyEditorSupport;
                  import java.util.*;
                  import java.text.NumberFormat;
                  
                  /**
                   * Created by IntelliJ IDEA.
                   * User&#58; afleming
                   * Date&#58; Oct 9, 2005
                   * Time&#58; 6&#58;07&#58;53 PM
                   * Copyright Adam Fleming 2005
                   *
                   * Based on Juergen Hoeller's mojo-rific CustomNumberEditor
                   * This class is a property editor that converts between comma-separated list of numbers and a java collection
                   * of said numbers.
                   *
                   * This is exceedingly useful for dealing with Multiple-Select Form Elements
                   *
                   * It allows for a user-defined numberFormat, much like Jurgen's CustomNumberEditor
                   *
                   * @see org.springframework.beans.propertyeditors.CustomNumberEditor
                   * http&#58;//jsourcery.com/output/sourceforge/spring/1.2.1/org/springframework/beans/propertyeditors/CustomNumberEditor.source.html
                   */
                  public class CollectionOfNumbersEditor extends PropertyEditorSupport &#123;
                     private final Class numberClass;
                     private final Collection collectionInstance;
                     private final NumberFormat numberFormat;
                     public static final String separator = ",";
                  
                     public CollectionOfNumbersEditor&#40; Class collectionClass, Class numberClass&#41; &#123;
                        super&#40;&#41;;
                        collectionInstance = checkClasses&#40;numberClass, collectionClass&#41;;
                        this.numberClass = numberClass;
                        this.numberFormat = null;
                     &#125;
                  
                     public CollectionOfNumbersEditor&#40;Class collectionClass, Class numberClass, NumberFormat numberFormat&#41; &#123;
                        super&#40;&#41;;
                        collectionInstance = checkClasses&#40;numberClass, collectionClass&#41;;
                        this.numberClass = numberClass;
                        this.numberFormat = numberFormat;
                     &#125;
                  
                     private Collection checkClasses&#40;Class numberClass, Class collectionClass&#41; throws IllegalArgumentException&#123;
                        Collection collectionInstance;
                        // Ensure we have a subclass of Number
                        if &#40;numberClass == null || !Number.class.isAssignableFrom&#40;numberClass&#41;&#41; &#123;
                           throw new IllegalArgumentException&#40;"Number class must be a subclass of "+Number.class.getName&#40;&#41;&#41;;
                        &#125;
                        // Ensure we have a subclass of Collection
                        if &#40;collectionClass == null || !Collection.class.isAssignableFrom&#40;collectionClass&#41;&#41; &#123;
                           throw new IllegalArgumentException&#40;"Collection class must be a subclass of "+Collection.class.getName&#40;&#41;&#41;;
                        &#125;
                        // Ensure we have a instantiatable subclass of Collection
                        try &#123;
                           collectionInstance = &#40;Collection&#41; collectionClass.newInstance&#40;&#41;;
                        &#125; catch &#40;InstantiationException e&#41; &#123;
                           throw new IllegalArgumentException&#40;"Could no instantiate "+collectionClass.getName&#40;&#41;+".\n"+
                                 "The Collection class must be instantiatable."&#41;;
                        &#125; catch &#40;IllegalAccessException e&#41; &#123;
                           throw new IllegalArgumentException&#40;"Could no instantiate "+collectionClass.getName&#40;&#41;+".\n"+
                                 "The Collection class must be instantiatable."&#41;;
                        &#125;
                        return collectionInstance;
                     &#125;
                  
                     /**
                      * This method takes a CSV list of values as input and creates a collection of the specified collection type,
                      * containing elements of the specified numberType
                      */
                     public void setAsText&#40;String string&#41; throws IllegalArgumentException &#123;
                        // Set the Value.  We start off with an empty collection, and
                        // can continue working with the collection instance.
                        setValue&#40;collectionInstance&#41;;
                        if&#40;!StringUtils.hasText&#40;string&#41;&#41;return;
                        for &#40;StringTokenizer tk = new StringTokenizer&#40;string, separator&#41;; tk.hasMoreTokens&#40;&#41;;&#41; &#123;
                           Number n = parseNumber&#40;tk.nextToken&#40;&#41;&#41;;
                           if&#40;n == null&#41;continue;
                           collectionInstance.add&#40; n &#41;;
                        &#125;
                     &#125;
                  
                     /**
                      * This method creates a CSV of all the values in the underlying collection
                      */
                     public String getAsText&#40;&#41; &#123;
                        if&#40;getValue&#40;&#41;==null&#41; return "";
                        StringBuilder sb = new StringBuilder&#40;&#41;;
                        for &#40;Iterator iterator = &#40;&#40;Collection&#41;getValue&#40;&#41;&#41;.iterator&#40;&#41;; iterator.hasNext&#40;&#41;;&#41; &#123;
                           if&#40;numberFormat==null&#41;
                              sb.append&#40;iterator.next&#40;&#41;&#41;;
                           else
                              sb.append&#40;numberFormat.format&#40;iterator.next&#40;&#41;&#41;&#41;;
                           if&#40;iterator.hasNext&#40;&#41;&#41; sb.append&#40;separator&#41;;
                        &#125;
                        return sb.toString&#40;&#41;;
                     &#125;
                  
                     private Number parseNumber&#40;String text&#41;&#123;
                        if&#40;!StringUtils.hasText&#40;text&#41;&#41; return null;
                        if&#40;numberFormat==null&#41;
                           return NumberUtils.parseNumber&#40;text, this.numberClass&#41;;
                        return NumberUtils.parseNumber&#40;text, this.numberClass, numberFormat&#41;;
                     &#125;
                  &#125;

                  Comment


                  • #10
                    Creating a comma-separated string from collection of objects.

                    There may be scenarios where we require to create a comma-separated string from collection of objects. The type of collection often varies: it may be a DataTable from which we need a certain column, or a List of a class.
                    One of the common approaches followed for achieving this is looping through the datatable or array of objects.
                    Now LINQ has provided a way to concatenate the values in a single statement.

                    Convert.ToString(dtTable.Select("Column1 = value", "").Select(dr => dr["Column2"]).Aggregate((i, j) => Convert.ToString(i) + ", " + Convert.ToString(j)));

                    Eliza

                    Comment

                    Working...
                    X