Spring MVC 2.5, binding collection elements and a bottle of Bud in hand

Okay, I finally got to write this — it's really been a while since I actually faced this problem, a couple of months or so. And it's also been a while since me posting any furious rant in the blog, so here you go .

WARNING 1: This column describes a probably uncommon pitfall when binding collection elements in Spring MVC 2.5, but a rather nasty one, so you are better off reading it, trust me.
WARNING 2: A whole bunch of code examples follows!
WARNING 3: This might be a bit too much for an unprepared reader, so go get your sandwich first

I hope everyone who has worked with Spring MVC has succeeded in binding collection elements at one point. Let's jump straight to an example — suppose, you have the following class which you use to construct your command object for a form controller:

public final class Thing extends AbstractEntity {

  private static final long serialVersionUID = 1L;

  @SuppressWarnings("unchecked")
  public Thing() {
    // let's pretend we have an implementation of the ElementFactory interface
    // which constructs Stuff objects and sets the Thing reference to this
    stuff = new AutoPopulatingList(new StuffFactory(this));
  }
  
  private List<Stuff> stuff;

  public List<Stuff> getStuff() {
    return stuff;
  }

  public void setStuff(final List<Stuff> stuff) {
    this.stuff = stuff;
  }

  // some extra valuable fields and methods reside here

}

And now for the Stuff class:

public final class Stuff extends AbstractEntity {

  private static final long serialVersionUID = 1L;

  private Thing thing;

  private String value;

  public Thing getThing() {
    return thing;
  }

  public void setThing(final Thing thing) {
    this.thing = thing;
  }

  public String getValue() {
    return value;
  }

  public void setValue(final String value) {
    this.value = value;
  }

}

Damn, why is there no code auto-completion in Confluence?

Let's say, we have your plain vanilla ManageThingsController which you use to both add and edit Thing objects.

A part of the JSP view for this controller could look something like this:

<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %>
<%@ taglib prefix="form" uri="http://www.springframework.org/tags/form" %>

<div class="form">
  <form:form commandName="thing" method="post">
    <div>
      <span class="label">Stuff #1</span>
      <span class="field">
        <form:input path="stuff[0].value"/>
      </span>
    </div>
    <div>
      <span class="label">Stuff #2</span>
      <span class="field">
        <form:input path="stuff[1].value"/>
      </span>
    </div>
    <div>
      <span class="label">Stuff #3</span>
      <span class="field">
        <form:input path="stuff[2].value"/>
      </span>
    </div>
  </form:form>
</div>

<c:if test="${not empty things}">
  <c:forEach var="t" items="${things}">
    <div>
      <td><a href="?id=${t.id}">Edit thing with id #${t.id}</a></td><!-- Do not try this at home -->
    </div>
  </c:forEach>
</c:if>

Phew, I'm tired of writing code here. Anyway, so far so good. Now let's have a look at our controller implementation:

@Controller
public final class ManageThingsController {

  private static final String COMMAND_NAME = "thing";

  // for simplicity's sake I'll just define views here manually,
  // normally you wouldn't do that

  private final String formView = "manageThings";

  private final String successView = "redirect:manageThings";

  private final ThingService thingService;

  @Autowired
  public ManageThingsController(final ThingService thingService) {
    this.thingService = thingService;
  }

  @SuppressWarnings("unused")
  @RequestMapping(method = RequestMethod.GET)
  public String setupForm(@ModelAttribute(COMMAND_NAME) final Thing thing) {
    return formView;
  }

  @RequestMapping(method = RequestMethod.POST)
  public String processSubmit(@ModelAttribute(COMMAND_NAME) final Thing thing) {
    thingService.save(thing);
    return successView;
  }

  @ModelAttribute(COMMAND_NAME)
  public Thing createCommand(@RequestParam(value = "id", required = false) final Integer id) {
    return (id == null) ? new Thing() : thingService.find(id); // normally you wouldn't do this either, but rather 
delegate this to a facade or smth, but "normally" doesn't apply here, sorry
  }

  // one more method will be here shortly

}

So far so good. The only thing remaining is the method to find all Thing objects (so we could click the "edit" link on page). How could you do that? I guess, you would first load all Thing instances from the data storage. Then I would normally just check in the view for the "id" request parameter and not render the element being edited in the table which contains all elements. But for this example we'll do it (intentionally!) differently: we'll remove the Thing which is currently being edited (if indeed there is such an object, i.e. it's not a new instance being added) in the controller. Let's try it this way:

// let's conveniently pass our command object there as a parameter

@ModelAttribute("things")
public List<Thing> getThings(@ModelAttribute(COMMAND_NAME) final Thing thing) {
  List<Thing> things = thingService.findAll();

  if (!thing.isNew) {
    things.remove(thing); // this might even work ;D
  }

  return things;
}

Looks real enough. But is just won't work properly in this particular case, uh-uh, i.e. when we are to bind some collection elements like that. And you will wind up with a weird ArrayIndexOutOfBoundsException on submitting the form, that's right: when the form gets loaded, the collection elements will be present there, in the command object! But when submitting, they will just disappear (I don't remember the details of my debugging it, but they were not pleasant) and you will be screwed. This is really not the situation you would want to get yourself into, so just let me remain the only moron I know to have faced this problem and struggled with it for hours and just do it the right way from the beginning:

@ModelAttribute("things")
public List<Thing> getThings(@RequestParam(value = "id", required = false) final Integer id) {
  List<Thing> things = thingService.findAll();

  if (id != null) {
    for (Iterator<Thing> i = things.iterator(); i.hasNext();) {
      if (id.equals(i.next().getId())) {
        i.remove();
        break;
      }
    }
  }

  return things;
}

And now you are yet another one happy developer. The sun is shining and you can relax and ponder on something that matters, instead of debugging this <censored> crap.

By the way, if anyone can provide any insight on this, e.g. maybe my trying of using a @ModelAttribute inside another @ModelAttribute declaration was wrong from the beginning. The funny thing was that it works fine for any binding other than collection elements. Either my or Spring MVC bug? Don't know, but you might find out someday .

Labels

spring spring Delete
mvc mvc Delete
binding binding Delete
Enter labels to add to this page:
Please wait 
Looking for a label? Just start typing.
  1. Jan 26, 2009

    Anonymous says:

    Look at @InitBinder annotation and PropertyEditorSupport from java.beans package...

    Look at @InitBinder annotation and PropertyEditorSupport from java.beans package and it's usage. It is nicely described in Spring Reference documentation.

  2. Jan 18

    Anonymous says:

    Hi: This is a great example!!! Could you please kindly provide a link to downlo...

    Hi:

    This is a great example!!!
    Could you please kindly provide a link to download all of the source code so people like me and others can try it out, please!!!!

    Yours,

    Thank Full

    1. Jan 18

      Timur Strekalov says:

      Hey, Thanks for reading all this junk Sadly, I don't think I have the source c...

      Hey,

      Thanks for reading all this junk Sadly, I don't think I have the source code for this thing, since it's been, like, 1.5 years. Moreover, I don't even know if this whole thing is still valid — Spring has improved even more since then, so I'm assuming this could've been fixed by now — provided this was a Spring bug in the first place, of course, I just didn't have too much time to figure it out. But if you have any other questions, I'd be glad to help.

      Regards,
      Timur

Add Comment