Friday, November 16, 2007

Stripes TypeConverters: Populating Domain Objects

Most web frameworks today have type converters. Type converters are chunks of code that takes query parameters as Strings and convert them into their appropriate data type. So numbers become Integers, dates become Dates, etc. Most frameworks also allow you to create custom type converters. For example, you might want to convert a currency form field into a Money object. That's all well and good. But we can take it a bit further with Stripes.

A common scenario in the web world is requesting data from the server and displaying it on the page. I mean, really, all web applications do is send data and get data and display the data they got. Nothing too complex. How we populate domain objects based on query parameters is where everyone's code can differ. Most common is for an action to process the query parameters and we get the one(s) we need and use them to lookup information in a database, shove those into some domain objects or properties of some sort, throw them in the request and forward to our view. Again, nothing too complex.

There are many times when we need to populate the same domain object(s) based on the same parameter on many different pages. I'll set up a scenario that we can use throughout the rest of this article. You created a User management screen. Displaying a list of users you want to edit one. The query parameter you pass in is the user.id. So we might have some code in our Stripes action bean that looks something like this:


private User user;
private UserDao userDao;

public User getUser()
{
return this.user;
}

public void setUser(User user)
{
this.user = user;
}

@SpringBean("userDao")
public void setUserDao(UserDao userDao)
{
this.userDao = userDao;
}

public Resolution edit()
{
user = UserDao.getUser(user.getId());
return new ForwardResolution(EDIT_USER_PAGE);
}


We could of course also discuss the pros and cons of doing the same thing in a method annotated with @After/@Before but for the sake of simplicity we'll say that's possible as well and move on.

So the client comes back and decides that they need an additional page where the administrator can click on a product and see all the users that purchased this product. And further more he can click on a user and view some details about that user. We begin to build the new action and view and notice that we've got to duplicate some code we just wrote. We need the UserDao again and we need to do a lookup so we copy and paste, test it, it works. Yea! And then we need another page, and another page, and we copy and paste and copy and paste. At this point we've got the same code written 10 times. That's bad. So we decide hey, we are using Stripes, let's use a TypeConverter to do this.

Stripes has a TypeConverter interface that requires two methods:

convert(String input, Class targetType, Collection errors)
setLocale(Locale locale)

We are only going to worry about convert right now. So we begin our custom type converter:


public class UserTypeConverter implements TypeConverter
{
private UserDao userDAo;

@SpringBean("userDao")
public void setUserDao(UserDao userDao)
{
this.userDao = userDao;
}

public User convert(String id, Class user, Collection error)
{
if (id != null)
{
User user = userDao.getUser(id);
return user;
}
return null;
}
}


This is pretty straightforward. We are just looking the user up from our UserDao using an id. But how does that id get there? Pretty simple as well. Let's look at our action again, only this time using the converter.


User user;

public void setUser(User user)
{
this.user = user;
}

public User getUser()
{
return this.user;
}

public Resolution edit()
{
return new ForwardResolution(EDIT_USER_PAGE);
}


And the URL might looking something like this:

http://localhost:8080/app/User.action?edit=&user=1234

Anytime the id is not null, user gets populated. If it is null, for example when adding a new user, user will only be populated from the normal form to object binding that Stripes normally does. But how does our action know that User is supposed to use the UserTypeConverter? Well, if we weren't needing the Spring Bean injection magic would could have simply done this in our action.

@Validate(converter=UserTypeConverter.class)
User user;

But since we need the Spring Bean magic we need to create our own TypeConverterFactory. Again, another interface. Stripes uses a default to init all the built in type converters. Here is what one might look like:


public class CustomTypeConverterFactory extends DefaultTypeConverterFactory
{
public TypeConverter getInstance(Class clazz, Locale locale) throws Exception
{
TypeConverter tc = super.getInstance(clazz, locale);
ServletContext sc = getConfiguration().getServletContext();

SpringHelper.injectBeans(tc, sc);

return tc;
}

@Override
public void init(Configuration configuration) {
super.init(configuration);
add(User.class, UserTypeConverter.class);
}
}


And then we need to tell Stripes to use our CustomTypeConverterFactory. In the web.xml in the StripesFilter init-params add the following param-name:

TypeConverterFactory.Class

and param-value:

com.app.package.foo.CustomTypeConverterFactory

And we're done. Comments welcome.

2 comments:

Jeppe Cramon said...

We use the TypeConverter features together with JPA and the EntityManager to provide this feature for all our persistent entities. I works very well, around 50 lines of code in all in all 3 classes (Converter/Formatter and Factory).
With this change we don't even have to specify any particular in the ActionBean'.
A perfect example of Stripes easy and elegant extension mechanism.

/Jeppe

Jasper Fontaine said...

Jeppe, any particular reason why you're not using the Stripernate add-on? I use it in combination with JPA and it works like a charm. Is it because you need to use the EntityManager instead of straight Hibernate sessions?
If that's the case, is there any chance you'll be publishing your solution as a Stripes add-on, possibly coordinating with Aaron? (i'm asking for a lot, i know ;) )