Binding nested objects, complex properties with Spring’s form:select tag.

While creating a form for an Entity (say Movie), it so happens that due to some business scenarios linking this entity with some other Entity (say Actor) becomes inevitable.  Let’s see the following code snippets of the above two entities.
Movie.java

public class Movie {
 . . .
      private int movieId;
      private String movieName;
      private Actor actor;
 . . .
}

Actor.java

public class Actor {
 . . .
       private int actorId;
       private String actorName;
 . . .
}

And then during the creation of Movie you need to link it with an Actor.

How do you do that?

Using <form:select>. Yes, but there’s  little more than that 😉

So we go ahead and prepare a view for Movie entity, which will have a text field for Movie’s name and a drop down (select tag) to link it with some Actor.

In the JSP page we’ll have the following code to take care of it.

<!-- Input field for Movie's name -->
<form:input path="movieName"/>

<!-- Select Tag, for giving the actors as a Dropdown -->
<!-- From Controller we'll set a list of Actors in Model and send it for this JSP -->
<form:select path="actor">
<form:options items="${actorList}" itemValue="actorId" itemLabel="actorName"/>
</form:select>

This all looks good, but once you submit you get to see a fat, ugly error.

This happens because Spring tries to bind the Movie object with Actor. And instead of getting an object all it gets is the Id field for actor, as that’s the value going to the Controller if you select an option.

<form:options items="${actorList}" itemValue="actorId" itemLabel="actorName"/>

So for any option selected itemValue attribute will contain the Id of the Actor above and Spring will try to set this itemValue in actor property of class Movie, but here the property actor isn’t a primitive one, rather it’s complex as it’s an object in itself.

So, to overcome this we need to convert the Id property of Actor into a full fledged Actor object. Prior to Spring 3, PropertyEditors were employed, but gone are those days!

We’ll use Formatters, the new kid on the block!

To do that, first we need to write a class implementing the Formatter<T> interface.

package org.springframework.format;
public interface Formatter<T> extends Printer<T>, Parser<T> {
}

So, we’ll create a class ActorFormatter.java

/* ActorFormatter.java */

//Removed the imports for brevity

@Component
public class ActorFormatter implements Formatter<Actor> {
     @Autowired
     private ActorService actorService;
     //Some service class which can give the Actor after
     //fetching from Database

     @Override
     public String print(Actor actor, Locale arg1) {
           return actor.getActorName().toString();
     }

     @Override
      public Actor parse(String actorId, Locale arg1) throws ParseException {
           return actorService.getActor(actorId);
           //Else you can just return a new object by setting some values
           //which you deem fit.
      }
}

Now, we’ve to register this formatter with Spring. To accomplish that, write the following in your WebApplicationContext file, or the config xml file.


<mvc:annotation-driven/>

<bean id="conversionService" class="org.springframework.format.support.FormattingConversionServiceFactoryBean">
      <property name="formatters">
           <set>
                 <ref bean="actorFormatter"/>
           </set>
      </property>
</bean>

Now, if you’re using Spring MVC then you don’t need to do anything, except putting an @Valid annotation in front of @ModelAttribute in your @RequestMapping.

@RequestMapping("/movie")
public String createMovieForm(@ModelAttribute("movie") @Valid Movie movie, BindingResult result, Model model) {
. . .
}

But if you’re using Spring MVC Portlet, then this @Valid won’t trigger the conversion strategies, and in which case you’ll need an @InitBinder.
So just include the below code snippet in your Controller.

@Autowired
private ConversionService conversionService;
//Autowiring the ConversionService we declared in the context file above.

@InitBinder
public void registerConversionServices(WebDataBinder dataBinder) {
       dataBinder.setConversionService(conversionService);
}

So, that’s all for this one. 🙂

Advertisements

17 thoughts on “Binding nested objects, complex properties with Spring’s form:select tag.”

  1. Hi.

    is possible to get values from a form using @ModelAttribute for a Composite key class. The examples like yours is little bit differents

    For example:
    @Embeddable
    public class CarPK implements Serializable {

    @Column
    private int serial;
    @Column
    private String brand;

    public CarPK() {

    }

    public CarPK(int serial, String brand) {
    this.serial = serial;
    this.brand = brand;
    }

    //cut
    }

    @Entity
    @Table(name=”Car”)
    public class Car {

    @EmbeddedId
    private CarPK id;

    @Column(name = “name”)
    private String name;

    //cut
    }

    For a simple example in controller:
    @RequestMapping(value = “/addcar”)
    public String addCar(@ModelAttribute Car car) {

    //Data in car are no correctly populated

    carSvc.add(car);
    return “redirect:/cars”;
    }

    and In service:
    @Transactional
    public void add(Car c) {

    em.persist(c);

    }

    Any idea?

    Thanks

    Like

  2. Dude, your code is not working. The formatter never gets triggered when you receive the form data in the RequestMapping method.

    Things I’ve tried:
    1) Adding
    2) Parsing with either a database call or setting new object fields
    3) Putting the @Valid annotation before or in front of @ModelAttribute
    4) I even tried putting the @InitBinder, but received an error that ConversionService was already set, so that wasn’t the issue.

    I am using Spring MVC 4.1.6, is it possible they’ve changed something since Spring 3?

    Whatever I do, the form only binds the itemValue to the path=”someField”, and no parsing happens at all.

    Would be glad if you could look up at this issue.

    Kind regards, Stefan.

    Like

    1. I am very very very sorry OP. In context to your code I was making the Formatter’s type a Movie, instead of an Actor.

      Please disregard my previous post, it is working fine now.

      Many thanks for the great article.
      Cheers, Stefan.

      Like

    2. Hi guys, i got this error, can some help me ?

      “Error creating bean with name ‘conversionService’ defined in URL [file:/C:/My%20Program%20Files/OSAC-Project/.met
      adata/.plugins/org.eclipse.wst.server.core/tmp0/wtpwebapps/OSAC/WEB-INF/classes/applicationContext.xml]: Cannot create inner bean ‘com.orange.OSAC.tools.applicationFormat
      ter#691db49d’ of type [com.orange.OSAC.tools.applicationFormatter] while setting bean property ‘formatters’ with key [0]; “

      Like

  3. Thank you very much for this article.

    I was going crazy and was not able to figure out why my objects were not loaded, nor validated. Because the same applies for the validator.

    So if you have a hibernate validator:

    You have to do the same for it:

    @InitBinder(“person”)
    public void initBinder(WebDataBinder binder) {
    binder.setConversionService(conversionService);
    binder.setValidator(validator);
    }

    Like

    1. XML is not posting in replays 🙂
      I mean, add conversion-service attrigute to the mvc:annotation-driven xml tag

      mvc:annotation-driven conversion-service=”conversionService”

      Like

  4. HI Ankeet,
    i have followed your mentioned steps for Spring mvc implemnetation
    while i am running application in tomcate the following error message shown in consol,

    can you please help me to solve this issue

    2013-03-21 07:27:50,968 ERROR Context initialization failed
    org.springframework.beans.factory.BeanCreationException: Error creating bean with name ‘conversionService’ defined in ServletContext resource [/WEB-INF/spring/mvc-config.xml]: Invocation of init method failed; nested exception is java.lang.IllegalArgumentException: Custom formatters must be implementations of Formatter or AnnotationFormatterFactory
    at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.initializeBean(AbstractAutowireCapableBeanFactory.java:1488)
    at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.doCreateBean(AbstractAutowireCapableBeanFactory.java:524)
    at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.createBean(AbstractAutowireCapableBeanFactory.java:461)
    at org.springframework.beans.factory.support.AbstractBeanFactory$1.getObject(AbstractBeanFactory.java:295)
    at org.springframework.beans.factory.support.DefaultSingletonBeanRegistry.getSingleton(DefaultSingletonBeanRegistry.java:223)
    at org.springframework.beans.factory.support.AbstractBeanFactory.doGetBean(AbstractBeanFactory.java:292)
    at org.springframework.beans.factory.support.AbstractBeanFactory.getBean(AbstractBeanFactory.java:198)
    at org.springframework.context.support.AbstractApplicationContext.finishBeanFactoryInitialization(AbstractApplicationContext.java:915)
    at org.springframework.context.support.AbstractApplicationContext.refresh(AbstractApplicationContext.java:479)
    at org.springframework.web.servlet.FrameworkServlet.configureAndRefreshWebApplicationContext(FrameworkServlet.java:651)
    at org.springframework.web.servlet.FrameworkServlet.createWebApplicationContext(FrameworkServlet.java:599)
    at org.springframework.web.servlet.FrameworkServlet.createWebApplicationContext(FrameworkServlet.java:665)
    at org.springframework.web.servlet.FrameworkServlet.initWebApplicationContext(FrameworkServlet.java:518)
    at org.springframework.web.servlet.FrameworkServlet.initServletBean(FrameworkServlet.java:459)
    at org.springframework.web.servlet.HttpServletBean.init(HttpServletBean.java:136)
    at javax.servlet.GenericServlet.init(GenericServlet.java:160)
    at org.apache.catalina.core.StandardWrapper.initServlet(StandardWrapper.java:1266)
    at org.apache.catalina.core.StandardWrapper.loadServlet(StandardWrapper.java:1185)
    at org.apache.catalina.core.StandardWrapper.load(StandardWrapper.java:1080)
    at org.apache.catalina.core.StandardContext.loadOnStartup(StandardContext.java:5015)
    at org.apache.catalina.core.StandardContext.startInternal(StandardContext.java:5302)
    at org.apache.catalina.util.LifecycleBase.start(LifecycleBase.java:150)
    at org.apache.catalina.core.ContainerBase$StartChild.call(ContainerBase.java:1568)
    at org.apache.catalina.core.ContainerBase$StartChild.call(ContainerBase.java:1558)
    at java.util.concurrent.FutureTask$Sync.innerRun(Unknown Source)
    at java.util.concurrent.FutureTask.run(Unknown Source)
    at java.util.concurrent.ThreadPoolExecutor.runWorker(Unknown Source)
    at java.util.concurrent.ThreadPoolExecutor$Worker.run(Unknown Source)
    at java.lang.Thread.run(Unknown Source)
    Caused by: java.lang.IllegalArgumentException: Custom formatters must be implementations of Formatter or AnnotationFormatterFactory
    at org.springframework.format.support.FormattingConversionServiceFactoryBean.registerFormatters(FormattingConversionServiceFactoryBean.java:148)
    at org.springframework.format.support.FormattingConversionServiceFactoryBean.afterPropertiesSet(FormattingConversionServiceFactoryBean.java:135)
    at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.invokeInitMethods(AbstractAutowireCapableBeanFactory.java:1547)
    at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.initializeBean(AbstractAutowireCapableBeanFactory.java:1485)
    … 28 more

    Like

    1. Hi Kasim,

      This error might be because of formatters going as null. Put @Component on your formatter class, and define in bean appropriately, like if the formatter class is ABC, give the reference in bean as “aBC” as per the bean instantiation rules.

      Also, check the version of Spring you’re using.

      Let me know in any case! Hope this helps. I can’t give you much without seeing the config file and the formatter implementation.

      Like

      1. Hi Ankeet,
        i deleted the old file and once again i tried your steps and come up with new exception,please correct me,if i am wrong and help me work correctly.
        below are exception and my files
        Spring version : 3.0.5
        web server : tomcat 7

        Exception :
        2013-03-24 05:29:17,859 ERROR Context initialization failed
        org.springframework.beans.factory.BeanCreationException: Error creating bean with name ‘conversionService’ defined in ServletContext resource [/WEB-INF/spring/mvc-config.xml]: Error setting property values; nested exception is org.springframework.beans.NotWritablePropertyException: Invalid property ‘formatters’ of bean class [org.springframework.format.support.FormattingConversionServiceFactoryBean]: Bean property ‘formatters’ is not writable or has an invalid setter method. Does the parameter type of the setter match the return type of the getter?
        at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.applyPropertyValues(AbstractAutowireCapableBeanFactory.java:1361)
        at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.populateBean(AbstractAutowireCapableBeanFactory.java:1086)
        … more
        Caused by: org.springframework.beans.NotWritablePropertyException: Invalid property ‘formatters’ of bean class [org.springframework.format.support.FormattingConversionServiceFactoryBean]: Bean property ‘formatters’ is not writable or has an invalid setter method. Does the parameter type of the setter match the return type of the getter?
        at org.springframework.beans.BeanWrapperImpl.setPropertyValue(BeanWrapperImpl.java:1024)
        at org.springframework.beans.BeanWrapperImpl.setPropertyValue(BeanWrapperImpl.java:900)
        at org.springframework.beans.AbstractPropertyAccessor.setPropertyValues(AbstractPropertyAccessor.java:76)
        at org.springframework.beans.AbstractPropertyAccessor.setPropertyValues(AbstractPropertyAccessor.java:58)
        at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.applyPropertyValues(AbstractAutowireCapableBeanFactory.java:1358)
        … 28 more

        configuration file :

        …//hibernate configuratios

        mvc-config.xml

        User.java
        @Entity
        @Table(name=”user”)
        public class User //implements Serializable
        {
        ……
        @ManyToOne(optional=false)
        @JoinColumn(name=”role_id”)

        private Role role;

        @Column(name=”name”)
        private String name;
        ……..setter and getter
        Role.java

        @Entity
        @Table(name=”roles”)
        public class Role //implements Serializable
        {

        @Id
        @Column(name=”id”)
        @GeneratedValue
        private int id;
        @Column(name=”role”)
        private String role;
        @Column(name=”status”)
        private Boolean status;
        ……..setter and getter

        Formatter.java
        public interface Formatter extends Printer, Parser{
        }

        RoleFormatter.java
        @Component
        public class RoleFormatter implements Formatter {

        @Autowired
        private RoleService roleService;

        @Override
        public String print(Role role, Locale arg1) {
        // TODO Auto-generated method stub
        return role.getRole().toString();
        }

        @Override
        public Role parse(String id, Locale arg1) throws ParseException {
        return roleService.getRole(Integer.parseInt(id));
        }

        In JSP:

        Controller
        RegistrationController.java
        import javax.validation.Valid;

        @Autowired
        private RoleService roleService;

        @RequestMapping(method = RequestMethod.GET)
        public String showForm(ModelMap model) {
        User user = new User();
        List roles = roleService.getAllRoles();
        model.addAttribute(“roleList”, roles);
        model.addAttribute(“USER”, user);
        return “registration”;
        }

        @RequestMapping(method = RequestMethod.POST)
        public String processForm(@ModelAttribute(value =”USER”) User user,
        @Valid User user,BindingResult result,Model model) {
        if (result.hasErrors()) {
        return “errorMessage”;
        } else {
        String result1 = userService.addUser(user);
        if (result1.equalsIgnoreCase(“SUCCESS”)) {

        if (user != null) {
        Role roleObj1 = user.getRole();
        if (roleObj1 != null && roleObj1.getId() == 3) {
        return “CAdminDashboard”;
        } else if (roleObj1 != null && roleObj1.getId() == 4) {
        return “CManagerDashboard”;
        } else if (roleObj1 != null && roleObj1.getId() == 2) {
        return “HTCDashboard”;
        } else if (roleObj1 != null && roleObj1.getId() == 1) {
        return “onDemandDashboard”;
        } else {
        return “errorMessage”;
        }
        } else {
        return “errorMessage”;
        }
        } else {
        return “errorMessage”;
        }
        }
        }
        …………….

        Like

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s