Enterprise Java

Type Conversion in Spring

Here are few straight cases where we need type conversion:
Case #1. To help simplifying bean configurations, Spring supports conversion of property values to and from text values. Each property editor is designed for a property of certain type only. And to put them in use, we have to register them with Spring container.
Case #2. Also when using Spring MVC, controllers binds the form field values to properties of an object. Suppose the object is composed with another object, then the MVC controller cannot automatically assign values to the internal custom type object as all the values in the form are inputted as text values. Spring container will take of conversion of text values to primitive types but not to custom type objects. For this to be taken care, we have to initialize custom editors in the MVC flow.

This article will discuss the various ways of implementing the converters for custom type objects. To elaborate more on these, let us consider the following use case. In the example, I would like to simulate play ground reservation system. So here are my domain objects:

public class Reservation {

	public String playGround;
	public Date dateToReserve;
	public int hour;
	public Player captain;
	public SportType sportType;

	public Reservation(String playGround, Date date, int hour, Player captain, SportType sportType) {
		this.playGround = playGround;
		this.dateToReserve = date;
		this.hour = hour;
		this.captain = captain;
		this.sportType = sportType;
	}

	/**
	 * Getters and Setters
	 */
}

public class Player {

	private String name;
	private String phone;
	/**
	 * Getters and Setters
	 */

}

public class SportType {

	public static final SportType TENNIS = new SportType(1, "Tennis");
	public static final SportType SOCCER = new SportType(2, "Soccer");

	private int id;
	private String name;

	public SportType(int id, String name) {
		this.id = id;
		this.name = name;
	}

	public static Iterable<SportType> list(){
		return Arrays.asList(TENNIS, SOCCER);
	}

	public static SportType getSport(int id){
		for(SportType sportType : list()){
			if(sportType.getId() == id){
				return sportType;
			}
		}
		return null;
	}

	/**
	 * Getters and Setters
	 */
}

Here is an example of Case #1: Suppose we want to define a reservation bean, here is how we do it:

<bean id="dummyReservation" class="com.pramati.model.Reservation">
	<property name="playGround" value="Soccer Court #1"/>
	<property name="dateToReserve" value="11-11-2011"/>
	<property name="hour" value="15"/>
	<property name="captain">
		<bean class="com.pramati.model.Player">
			<property name="name" value="Prasanth"/>
			<property name="phone" value="92131233124"/>
		</bean>
	</property>
	<property name="sportType">
		<property name="id" value="1"/>
		<property name="name" value="TENNIS"/>
	</property>
</bean>

This bean definition is pretty verbose. It could have been more presentable if the definition looks somewhat like this:

<bean id="dummyReservation" class="com.pramati.model.Reservation">
	<property name="playGround" value="Soccer Court #1"/>
	<property name="dateToReserve" value="11-11-2011"/>
	<property name="hour" value="15"/>
	<property name="captain" value="Prasanth,92131233124"/>
	<property name="sportType" value="1,TENNIS"/>
</bean>

For this to work, we should tell Spring to use the custom converters in the process of defining a bean.

Here is an example of Case #2: Suppose I have a JSP in my application which allows user to reserve the playground for a particular time of a day.

<%@ page language="java" contentType="text/html; charset=ISO-8859-1" pageEncoding="ISO-8859-1"%>
<%@ taglib prefix="form" uri="http://www.springframework.org/tags/form"%>
<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd">
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=ISO-8859-1">
<title>Reservation Form</title>
<style type="text/css">
.error {
	color: #ff0000;
	font-weight: bold;
}
</style>
</head>
<body>
	<form:form method="post" modelAttribute="reservation">
		<table>
			<tr>
				<th>Court Name</th>
				<td><form:input path="courtName" /></td>
			</tr>
			<tr>
				<th>Reservation Date</th>
				<td><form:input path="date" /></td>
			</tr>
			<tr>
				<th>Hour</th>
				<td><form:input path="hour" /></td>
			</tr>
			<tr>
				<th>Player Name</th>
				<td><form:input path="player.name" /></td>
			</tr>
			<tr>
				<th>Player Contact Number</th>
				<td><form:input path="player.phone" /></td>
			</tr>
			<tr>
				<th>Sport Type</th>
				<td><form:select path="sportType" items="${sportTypes}"
						itemLabel="name" itemValue="id" /></td>
			</tr>
			<tr>
				<td colspan="3"><input type="submit" name="Submit" /></td>
			</tr>
		</table>
	</form:form>
</body>
</html>

And here is the corresponding MVC controller:

@Controller
@RequestMapping
@SessionAttributes("reservation")
public class ReservationFormController {

	@Autowired
	private ReservationService reservationService;

	@ModelAttribute("sportTypes")
	public Iterable<SportType> getSportTypes(){
		return SportType.list();
	}

	@RequestMapping(value="/reservationForm/{captainName}", method=RequestMethod.GET)
	public String initForm(Model model, @PathVariable String captainName){
		Reservation reservation = new Reservation();
		reservation.setPlayer(new Player(captainName, null));
		reservation.setSportType(SportType.TENNIS);
		model.addAttribute("reservation", reservation);
		return "reservationForm";
	}

	@RequestMapping(value="/reservationForm/{captainName}",method=RequestMethod.POST)
	public String reserve(@Valid Reservation reservation, BindingResult bindingResult, SessionStatus sessionStatus){
		validator.validate(reservation, bindingResult);
		if(bindingResult.hasErrors()){
			return "/reservationForm";
		} else{
			reservationService.make(reservation);
			sessionStatus.setComplete();
			return "redirect:../reservationSuccess";
		}
	}
}

Now as you see, in the JSP we are tying the form fields to a Reservation object(modelAttribute=”reservation”). This object is kept in the model by the controller(in initForm() method) which gets passed to the view. Now when we submit the form, Spring throws a validation message saying that the field values can’t be converted to types, Player and SportType. For this to work, we have to define the custom converters and inject them into the Spring MVC flow.

Now the question is how to define the custom converters? Spring provides two ways of supporting these custom converters:

  • Solution #1: Use PropertyEditors
  • Solution #2: Use Converters

Using PropertyEditor:

PropertyEditorSupport, implements PropertyEditor interface, is a support class to help build PropertyEditors.

public class SportTypeEditorSupport extends PropertyEditorSupport {

	/**
     * Sets the property value by parsing a given String.  May raise
     * java.lang.IllegalArgumentException if either the String is
     * badly formatted or if this kind of property can't be expressed
     * as text.
     *
     * @param text  The string to be parsed.
     */
	@Override
	public void setAsText(String text) throws IllegalArgumentException {
		try{
			SportType sportType = SportType.getSport(Integer.parseInt(text));
			setValue(sportType);// setValue stores the custom type Object into a instance variable in PropertyEditorSupport.
		}
		catch(NumberFormatException nfe){
			throw new RuntimeException(nfe.getMessage());
		}
	}

	 /**
     * Gets the property value as a string suitable for presentation
     * to a human to edit.
     *
     * @return The property value as a string suitable for presentation
     *       to a human to edit.
     * <p>   Returns "null" is the value can't be expressed as a string.
     * <p>   If a non-null value is returned, then the PropertyEditor should
     *	     be prepared to parse that string back in setAsText().
     */
	@Override
	public String getAsText() {
		SportType sportType = (SportType)getValue();
		return Integer.toString(sportType.getId());
	}
}

Now register the custom editor in PropertyEditorRegistry. PropertyEditorRegistrar registers custom editors in PropertyEditorRegistry. Here is how you do it:

import java.text.SimpleDateFormat;
import java.util.Date;

import org.springframework.beans.PropertyEditorRegistrar;
import org.springframework.beans.PropertyEditorRegistry;
import org.springframework.beans.propertyeditors.CustomDateEditor;

import com.pramati.model.SportType;

public class CustomPropertyEditorRegistrar implements PropertyEditorRegistrar {
	@Override
	public void registerCustomEditors(PropertyEditorRegistry registry) {
		registry.registerCustomEditor(Date.class, new CustomDateEditor(
				new SimpleDateFormat("dd-MM-yyyy"), true));
		registry.registerCustomEditor(SportType.class, new SportTypeEditorSupport());
	}
}

The CustomEditorConfigurer is implemented as a bean factory post processor for you to register your custom property editors before any of the beans get instantiated. To do this, we associate PropertyEditorRegistry with CustomEditorConfigurer.

<bean id="customPropertyEditorRegistrar" class="com.pramati.spring.mvc.CustomPropertyEditorRegistrar"/>

<bean class="org.springframework.beans.factory.config.CustomEditorConfigurer">
	<property name="propertyEditorRegistrars">
		<list>
			<ref bean="customPropertyEditorRegistrar"/>
		</list>
	</property>
</bean>

Now when Spring container sees this:

<property name="captain" value="Prasanth,92131233124"/>

Spring automatically converts the value specified to Player object. But this configuration is not enough for Spring MVC flow. Controllers would still complain about incompatible types as it expects a Player object where as it gets a String. For the form field value to be interpreted as custom type object, we have to make few MVC configuration changes.

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.core.convert.ConversionService;
import org.springframework.validation.Validator;
import org.springframework.web.bind.WebDataBinder;
import org.springframework.web.bind.support.WebBindingInitializer;
import org.springframework.web.context.request.WebRequest;

public class CustomWebBindingInitializer implements WebBindingInitializer {
	@Autowired
	private CustomPropertyEditorRegistrar customPropertyEditorRegistrar;

	@Override
	public void initBinder(WebDataBinder binder, WebRequest request) {
		customPropertyEditorRegistrar.registerCustomEditors(binder);
	}
}

Now remove and define the necessary beans manually as we need to inject WebBindingInitializer into RequestMappingHandlerAdapter

<bean class="org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerMapping"/>

<bean class="org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter">
	<property name="webBindingInitializer">
		<bean class="com.pramati.spring.mvc.CustomWebBindingInitializer"/>
	</property>
</bean>

Now controller automatically converts the String to the necessary custom type object. Note that we have to make separate configuration changes for simplifying bean configuration and type conversion of form fields in Spring MVC. Also it is worth pointing that when extending PropertyEditorSupport, we store the custom type object into the instance variable and hence using PropertyEditors is not thread safe. To overcome these problems, Spring 3.0 has introduced the concept of Converters and Formatters.

Using Converters:

Converter components are used for converting one type to another type and also to provide a cleaner separation by forcing to place all such conversion related code in one single place. Spring already supports built-in converters for the commonly used types and the framework is extensible enough for writing custom converters as well. Spring Formatters come into picture to format the data according to the display where it is rendered. It’s always worthwhile to see the exhaustive list of pre-built converters before even thinking of writing a custom converter that suits for a particular business need. For seeing the list of prebuilt converters, see the package org.springframework.core.convert.support

Coming back to our use case, let us implement String to SportType converter:

import org.springframework.core.convert.ConversionFailedException;
import org.springframework.core.convert.TypeDescriptor;
import org.springframework.core.convert.converter.Converter;
import com.pramati.model.SportType;

public class StringToSportTypeConverter implements Converter<String, SportType> {

	@Override
	public SportType convert(String sportIdStr) {
		int sportId = -1;
		try{
			sportId = Integer.parseInt(sportIdStr);
		} catch (NumberFormatException e) {
			throw new ConversionFailedException(TypeDescriptor.valueOf(String.class), TypeDescriptor.valueOf(SportType.class), sportIdStr, null);
		}

		SportType sportType = SportType.getSport(sportId);
		return sportType;
	}

}

Now register this with ConversionService and link it with SpringMVC flow:

<mvc:annotation-driven conversion-service="conversionService"/>

<bean id="conversionService" class="org.springframework.context.support.ConversionServiceFactoryBean" >
	<property name="converters">
		<set>
			<bean class="com.pramati.type.converters.StringToSportTypeConverter"/>
			<bean class="com.pramati.type.converters.StringToDateConverter"/>
			<bean class="com.pramati.type.converters.StringToPlayerConverter"/>
		</set>
	</property>
</bean>

If you are using custom bean declarations instead of ‹mvc:annotation-driven/›, here is the way to do it:

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.core.convert.ConversionService;
import org.springframework.validation.Validator;
import org.springframework.web.bind.WebDataBinder;
import org.springframework.web.bind.support.WebBindingInitializer;
import org.springframework.web.context.request.WebRequest;

public class CustomWebBindingInitializer implements WebBindingInitializer {

	@Autowired
	private ConversionService conversionService;

	@Override
	public void initBinder(WebDataBinder binder, WebRequest request) {
		binder.setConversionService(conversionService);
	}

}

Now inject WebBindingInitializer into RequestMappingHandlerAdapter.

<bean class="org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerMapping"/>
<bean class="org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter">
	<property name="webBindingInitializer">
		<bean class="com.pramati.spring.mvc.CustomWebBindingInitializer"/>
	</property>
</bean>

Registering ConversionService alone will take care of simplifying bean configuration(Case #1). For Case #2 to work, we have to register ConversionService with the Spring MVC flow. And note that this way of doing type conversion is also thread safe.

Also instead of making Converters/PropertEditors available to all the controllers in the application, we can enable them on a per-controller-basis. Here is how you do it. Remove the above specified generic configuration and introduce @InitBinder in the controller class like this:

@Controller
@RequestMapping
@SessionAttributes("reservation")
public class ReservationFormController {

	private ReservationService reservationService;
	private ReservationValidator validator;

	@Autowired
	public ReservationFormController(ReservationService reservationService, ReservationValidator validator){
		this.reservationService = reservationService;
		this.validator = validator;
	}

	@Autowired
	private ConversionService conversionService;
	@InitBinder
	protected void initBinder(ServletRequestDataBinder binder) {
		binder.setConversionService(conversionService);
	}

	/*@InitBinder
	protected void initBinder(ServletRequestDataBinder binder) {
		binder.registerCustomEditor(Date.class, new CustomDateEditor(new SimpleDateFormat("dd-MM-yyyy"), true));
		binder.registerCustomEditor(SportType.class, new SportTypeEditorSupport(reservationService));
	}*/

	/*@Autowired
	private PropertyEditorRegistrar propertyEditorRegistrar;
	@InitBinder
	protected void initBinder(ServletRequestDataBinder binder) {
		propertyEditorRegistrar.registerCustomEditors(binder);
	}*/

	@ModelAttribute("sportTypes")
	public Iterable<SportType> getSportTypes(){
		return SportType.list();
	}

	@RequestMapping(value="/reservationForm/{userName}", method=RequestMethod.GET)
	public String initForm(Model model, @PathVariable String userName){
		Reservation reservation = new Reservation();
		reservation.setPlayer(new Player(userName, null));
		reservation.setSportType(SportType.TENNIS);
		model.addAttribute("reservation", reservation);
		return "reservationForm";
	}

	@RequestMapping(value="/reservationForm/{userName}",method=RequestMethod.POST)
	public String reserve(@Valid Reservation reservation, BindingResult bindingResult, SessionStatus sessionStatus){
		validator.validate(reservation, bindingResult);
		if(bindingResult.hasErrors()){
			return "/reservationForm";
		} else{
			reservationService.make(reservation);
			sessionStatus.setComplete();
			return "redirect:../reservationSuccess";
		}
	}

	@RequestMapping("/reservationSuccess")
	public void success(){

	}
}

So if you see the above code, you would have noticed the commented code where we have used PropertyEditors instead of converters. And hence this feature of enabling type converters on a controller basis is available in both the implementations.
 

Reference: Type Conversion in Spring from our JCG partner Prasanth Gullapalli at the prasanthnath blog.

Prasanth Gullapalli

Prasanth is passionated about technology and specializes in application development in distributed environments. He has always been fascinated by the new technologies and emerging trends in software development.
Subscribe
Notify of
guest

This site uses Akismet to reduce spam. Learn how your comment data is processed.

2 Comments
Oldest
Newest Most Voted
Inline Feedbacks
View all comments
logan
logan
10 years ago

Thanks for sharing. But I think the code is in a mess. So it is difficult to do test myself.Can you please share the demo code as well?

yihaomen
10 years ago

from this article, I created a project and I share the code here: http://www.yihaomen.com/article/java/429.htm
at the end of the article ,you can download the code.

Back to top button