Announcement Announcement Module
Collapse
No announcement yet.
Spring's form taglib, select and custom property editors Page Title Module
Move Remove Collapse
X
Conversation Detail Module
Collapse
  • Filter
  • Time
  • Show
Clear All
new posts

  • Spring's form taglib, select and custom property editors

    Up to now I saw repeatedly questions about non-working <form:select> (ie. using Spring's form taglib) when using custom property editors. Such questions mostly stayed unanswered. Now I also have such a case - and it neither works for me. But one after another ...

    I have the following select in my JSP:

    Code:
    <form:select path="company">
      <form:option value="" label="all"/>
      <form:options items="${companies}" itemValue="id" itemLabel="name"/>
    </form:select>
    And I have also registered successfully my custom property editor for converting Company objects into string and back. I see this one coming into play when doing remote debugging.

    Now the problem: If I select a company and submit the form, the company gets selected, but on redisplay of the form it is no longer selected. Remote debugging shows that Spring behaves correct up to the point where the tags are written out. The final method not working as expected seems to be org.springframework.web.servlet.tags.form. SelectedValueComparator#exhaustiveCompare:

    Ignoring LabeledEnum the first comparison made is
    Code:
    ObjectUtils.getDisplayString(boundValue).equals(candidateDisplayString)
    where boundValue is a Company object and candidateDisplayString is
    Code:
    ObjectUtils.getDisplayString(candidate)
    candidate is actually the @itemValue from the code in the JSP using the taglib, in this case the id property of Company is of type Integer.
    Code:
    ObjectUtils.getDisplayString()
    uses toString() implementation of the class, so at the end Company.toString() (providing no implementation, so defaulting to Object.toString()) is compared to Integer.toString(), which fails expectedly.

    The last comparison is only done when candidate is of type String
    Code:
    if (propertyEditor != null && candidate instanceof String)
    and here I wonder if that condition is correct. From what I understand the comparison between the bound value (ie. the selected Company object) and the candidate value (ie. one of the Company objects from the collection) must be made on the same type of the objects, so either:
    - Company objects directly, not Company on the one side, its itemValue on the other side
    - their itemValue properties on both sides
    - the string representations of the Company objects on both sides
    - or the string representations of their itemValues on both sides.
    The latter two would be tried if there would not be that strange condition mentioned above (candidate being of type String).

    So has anybody <form:select> with Spring's taglib and custom property editor working and can prove my investigations from above being wrong? If not, can anybody reproduce my investigations and confirm it's somehow a bug?

    Thanks in advance
    Jörg

  • #2
    Ping.

    No Spring guru that can comment on this one?

    Jörg

    Comment


    • #3
      We use the Spring form tag library in a lot of screens, even the select with CustomPropertyEditors without any problem.

      In our jsp.
      Code:
      <form:select path="userType">
      	<c:if test="${fn:length(allUserTypes) gt 1}">
      		<form:option value="">
      	        <spring:message code="autorisatie.prompt.select.usertype"/>
              </form:option>																
      	</c:if>													            
      	<form:options items="${allUserTypes}" itemValue="id" itemLabel="description"/>
      </form:select>
      We have created a custom property editor to convert id's in to its respective classes.

      Code:
      public class CustomEntityPropertyEditor extends PropertyEditorSupport {    
          
          private Class entityType = null;
          private HibernateTemplate template;
          private boolean allowEmpty = false;
          
          /**
           * Package protected constructor so that instances can only be created by 
           * the FactoryBean.
           * 
           * @param sessionFactory
           */
          CustomEntityPropertyEditor(final HibernateTemplate template, final Class entityType) {
              super();
              this.template = template;
              this.entityType=entityType;
          }
          
          CustomEntityPropertyEditor(final HibernateTemplate template, final Class entityType, final boolean allowEmpty) {
              this(template, entityType);
              this.allowEmpty=allowEmpty;
          }
              
          @Override
          public String getAsText() {
              Object value = getValue();
              if (value instanceof IEntity) {
                  return String.valueOf(((IEntity) value).getId());
              } else {
                  return super.getAsText();
              }
          }
          
          @Override
          public void setAsText(final String text) throws IllegalArgumentException {
              if (StringUtils.hasText(text) ) {
                  Long id = Long.valueOf(text);
                  Object object = template.load(entityType, id); 
                  setValue(object);
              } else if (allowEmpty) {
                  setValue(null);
              } else {
                  throw new IllegalArgumentException("Cannot convert text '"+text+"', into an instance of type '"+entityType.getClass().getName()+"'");
              }
          }
      }
      This works like a charm.

      Edit: Although I now start to doubt, because the default toString implementation of our Entity is to display the id. Hmm lets create a simple test.

      Comment


      • #4
        Originally posted by mdeinum View Post
        Although I now start to doubt, because the default toString implementation of our Entity is to display the id.
        As written toString() implementation would make it working if entity.getId().toString() and entity.toString() were equal. But that's not what I want actually. I'd like to see a more clean approach of comparing the same types of objects (as written either the entities, their ids or their string representations).

        Regards
        Jörg

        Comment


        • #5
          Also I guess your logic is flawed somewhere.

          From the OptionWriter.doRenderFromCollection and the OptionWriter.renderOption

          Code:
          private void doRenderFromCollection(Collection optionCollection, TagWriter tagWriter) throws JspException {
          	for (Iterator iterator = optionCollection.iterator(); iterator.hasNext();) {
          		Object item = iterator.next();
          		BeanWrapper wrapper = new BeanWrapperImpl(item);
          
          		Object value = (this.valueProperty == null ? item :
          						wrapper.getPropertyValue(this.valueProperty));
          		Object label = (this.labelProperty == null ? item :
          						wrapper.getPropertyValue(this.labelProperty));
          
          		renderOption(tagWriter, item, value, label);
          	}
          }
          
          
          private void renderOption(TagWriter tagWriter, Object item, Object value, Object label) throws JspException {
          	tagWriter.startTag("option");
          
          	String valueDisplayString = getDisplayString(value);
          	String labelDisplayString = getDisplayString(label);
          
          	// allows render values to handle some strange browser compat issues.
          	tagWriter.writeAttribute("value", valueDisplayString);
          
          	if (isSelected(value) || isSelected(item)) {
          		tagWriter.writeAttribute("selected", "selected");
          	}
          	tagWriter.appendValue(labelDisplayString);
          	tagWriter.endTag();
          }
          The doRenderFromCollection method calls the renderOption with the real item from the collection and the resolved value and label.

          The renderOption method first check if the real item is selected next if the value is selected.

          The isSelected method in turn calls the isSelected method on the SelectedValueComparator. Which is printed below.

          Code:
          public static boolean isSelected(BindStatus bindStatus, Object candidateValue) {
          	Object boundValue = getBoundValue(bindStatus);
          
          	if (boundValue == null) {
          		return (candidateValue == null);
          	}
          
          	boolean selected = false;
          
          	if (boundValue.getClass().isArray()) {
          		selected = collectionCompare(CollectionUtils.arrayToList(boundValue), candidateValue, bindStatus);
          	}
          	else if (boundValue instanceof Collection) {
          		selected = collectionCompare((Collection) boundValue, candidateValue, bindStatus);
          	}
          	else if (boundValue instanceof Map) {
          		selected = mapCompare((Map) boundValue, candidateValue, bindStatus);
          	}
          
          	if (!selected) {
          		if (ObjectUtils.nullSafeEquals(boundValue, candidateValue)) {
          			selected = true;
          		}
          		else {
          			selected = exhaustiveCompare(boundValue, candidateValue, bindStatus.getEditor());
          		}
          	}
          
          	return selected;
          }

          Assuming you have a normal object, the getBoundValue will retrieve the current selected object from the BindStatus object, actually from the editor from the bindstatus.

          Now you would have 2 company objects. The real item resolved from the collection and the current value bound to the command object. Now if you have implemented a correct equals method it would return true. (I have seen cases without an equals method go wrong, due to objects retrieved in different hibernate session!)

          Comment


          • #6
            Ah, thanks Marten, I missed the double call to isSelected(..) with value and item and debugged through it only once. That's why I got the impression only entity id is compared with the entity. Indeed the second comparison is done with the candidate item and so of course a proper implementation of equals(..) fixes the problem.

            Thanks and regards,
            Jörg

            Comment


            • #7
              Well-researched post - thanks to you both for the useful info.

              Comment


              • #8
                I have a somewhat related issue.

                How do you register your property editors?

                Comment

                Working...
                X