Binding Into Domain Models

Many web frameworks strongly encourage developers to bind form values to an intermediate value holder and then validate against this value holder.  This leads to several lines of code that need to be developed for each form-processing action that I'd just as soon not have to write.  For a "shallow" form-to-model binding this is a bit annoying if not exactly a major barrier of productivity.  For a "deep" form-to-model binding, where it is desired to edit a collection of top-level Entities as well as edit the contents of related child Entities, the intermedate value-holder approach starts taking more of your time as you develop an anemic value-holding parallel model and the means of copying this parallel value-holding graph to your real Entity graph.  A strength of Stripes that really contributed to my attraction to the framework is Stripes' ability to bind form values directly into your model objects.  Stripes supports binding form values to as deep an Entity graph as you care to--all without breaking, or even significantly increasing the complexity of, the Stripes Validation facility. 

The below approach uses the Hibernate Infrastructure that is described in another User Suggestion, as well as the Generic Data Access Objects approach that is described at the Hibernate Wiki

The biggest differences between binding directly and binding thru an intermediate value-holder are:

  • Bind Directly requires that your entity already be hydrated and in place in the action, so that Stripes has a real object to bind into
  • There are cases in Stripes (really in any presentation framework) where you would prefer to ignore the results of form binding.  Two example cases of this are ActionBeans that have cancel- or skip-style event handlers that don't want to process any values submitted from the form, and cases where form binding has taken place before validation fails.
  • For Stripes to know that a request parameter is actually an id value that should be used to hydrate an Entity to be hooked into the graph, you should make "symmetrical" custom coverters and formatters for each Entity, where the Converter knows how to map an ID to an Entity, and the formatter knows how to map an Entity to the ID that the converter uses.

To help hook in my custom ActionBeanContext subclass, I created a Layer Supertype that handles getContext() and setContext() for all my ActionBeans. 

 
package com.techlunch.newb.action; 


import net.sourceforge.stripes.action.*; 


public abstract class AbstractAction implements ActionBean 
{ 
private StripesNewbActionBeanContext	context	= null; 


public StripesNewbActionBeanContext getContext() 
{ 
return context; 
} 


public void setContext( ActionBeanContext context ) 
{ 
this.context	= (StripesNewbActionBeanContext)context; 

preBind(); 
} 


/* 
* Default implementation does nothing. Override this method if you 
* need to execute any additional logic that would normally take place in 
* the setContext() method, if this layer supertype wasn't "hogging" it. 
*/ 
protected void preBind() 
{ 
} 
} 

 

This layer supertype makes setContext() a template method, where subclasses can hook in any logic they want executed at setContext()-time via overriding the preBind() method.  Even actions that won't be binding directly to an Entity may still want to take advantage of our custom ActionBeanContext subclass, so that is why more work to support binding directly to your entities isn't being done with this abstract class.
ActionBeans that intend to bind directly to entities can pre-load their entities in any number of ways, so centralizing this logic is only of limited value.  One thing that does need to happen universally is the evicting of bound entities from the current identity map and unit of work (the Session in Hibernate) for the reasons outlined above.  The following layer supertype helps centralize this logic for your Action Beans.

 
package com.techlunch.newb.action; 


import com.techlunch.newb.model.*; 

import java.util.*; 

import net.sourceforge.stripes.action.*; 
import net.sourceforge.stripes.validation.*; 


public abstract class BoundEntityAction extends AbstractAction
{ 
/* 
* This tells the AbstractAction which object instances comprise the graph 
* that any form submission will be bound into. 
*/ 
public abstract List<Object> getBoundObjects(); 


/* 
* Call this if validation fails, or from an event handler that wants 
* Hibernate to to ignore any form binding that may have taken place (for 
* example, the Cancel or Skip cases). 
*/ 
public void evictBoundObjects() 
{ 
HbnSessionUtil.evict( getBoundObjects() ); 
} 


public Resolution handleValidationErrors( ValidationErrors errors ) 
{ 
evictBoundObjects(); 

return null; 
} 
} 

 

The getBoundObjects() method is responsible for returning all the Entity instances that have form values bound into them.  No default implementation is given for the getBoundObjects() method, so that each ActionBean knows that it is his responsibility to let the supertype know what Entities are affected.  The evictBoundObjects() method is responsible for evicting each element returned by getBoundObjects() from the Hibernate Session.  The handleValidationErrors() method is a way of telling Stripes that your ActionBean has logic that it wants executed in the event of failed validation.  In this case, we want to evict any Entities that have been bound to if validation fails. 
An ActionBean that facilitates a "shallow" Entity bind would then look like:

 
package com.techlunch.newb.action; 


import com.techlunch.newb.model.*; 
import com.techlunch.newb.model.dao.*; 

import java.util.*; 

import net.sourceforge.stripes.action.*; 
import net.sourceforge.stripes.validation.*; 


public class ManufacturerAction extends BoundEntityAction 
{ 
@ValidateNestedProperties( { 
@Validate( field = "name", required = true ), 
@Validate( field = "desc", required = true )} ) 
private Manufacturer	man	= null; 
private ManufacturerDAO	manDao	= DAOFactory.DEFAULT.getManufacturerDAO(); 


public void preBind() 
{ 
String	idStr	= getContext().getReqParam( "man.id" ); 

if (idStr == null) 
this.man	= new Manufacturer(); 
else 
this.man	= manDao.findById( Long.parseLong( idStr ) ); 
} 


public Manufacturer getMan() 
{ 
return man; 
} 


public void setMan( Manufacturer man ) 
{ 
this.man = man; 
} 


public List<Object> getBoundObjects() 
{ 
List<Object>	lst	= new ArrayList<Object>(); 

lst.add( getMan() ); 

return lst; 
} 


@DontValidate 
@DefaultHandler 
public Resolution view() 
{ 
return new ForwardResolution( "/pages/viewMan.jsp" ); 
} 


public Resolution save() 
{ 
manDao.makePersistent( man ); 

getContext().addMsg( Messages.saved( "Manufacturer" ) ); 

return new RedirectResolution( "/Manufacturer.action?man=" 
+ man.getId() ); 
} 


@DontValidate 
public Resolution cancel() 
{ 
evictBoundObjects(); 

getContext().addMsg( Messages.cancelled( "Manufacturer" ) ); 

return new RedirectResolution( "/pages/sampleHome.jsp" ); 
} 
} 

 

There's alot going on in there, so let's take it step-by-step.

 
@ValidateNestedProperties( { 
@Validate( field = "name", required = true ), 
@Validate( field = "desc", required = true )} ) 
private Manufacturer	man	= null; 
private ManufacturerDAO	manDao	= DAOFactory.DEFAULT.getManufacturerDAO(); 

 

In this example, the Entity is named Manufacturer.  The @ValidateNestedProperties annotation tells Stripes that the properties to be validated are actually properties under the Manufacturer instance, not properties of the ActionBean.  ManufacturerDAO is the class that encapsulates data access logic for the Manufacturer Entity.

 
public void preBind() 
{ 
String	idStr	= getContext().getReqParam( "man.id" ); 

if (idStr == null) 
this.man	= new Manufacturer(); 
else 
this.man	= manDao.findById( Long.parseLong( idStr ) ); 
} 

 

The Layer Supertype that our ActionBean extends defines the preBind() method, which is executed before Stripes performs any form binding.  The method is overriden in this case so that any form binding has a real Entity to bind values into.  To do this, inside the preBind() method, we first pull the request parameter that we know will contain the id of the Entity to hydrate out of ActionContext().  If the id is null, we can assume that any form binding that occurs will occur on a new Entity instance that is not yet persisted.  If the id is not null, we can use the DAO to hydrate an instance of the Entity.

 
public Manufacturer getMan() 
{ 
return man; 
} 

public void setMan( Manufacturer man ) 
{ 
this.man = man; 
} 

 

At first these two setters appear to be self-explanatory.  But what could be calling a setter on the ActionBean with an already-hydrated instance of our Entity?  We can use Stripes' Converter facility to handle the hydration of our Manufacturer instance for us in some circumstances.  The above get/set pair defines a property of the ActionBean called "man".  Assuming we have a converter for Manufacturers, then we can set this property in an <s:link> with the id property of Manufacturer, and Stripes will inject an already-hydrated instance of the Manufacturer for us.  Unfortunately, this conversion takes place as part of form binding, so it only helps us prepare the ActionBean for the view() event handler.  Here is an example JSP snippet using the man property, as well as the Converter that was created to convert from a string to a Manufacturer instance:

 
<s:link xhref="/Manufacturer.action"> 
<s:link-param name="man" value="${man.id}" /> 
${man.name} 
</s:link> 
 
package com.techlunch.newb.converter; 


import com.techlunch.newb.model.*; 
import com.techlunch.newb.model.dao.*; 

import java.util.*; 

import net.sourceforge.stripes.validation.*; 


public class ManufacturerConverter implements TypeConverter<Manufacturer> 
{ 
public void setLocale( Locale locale ) 
{ 
// Do nothing. This doesn't use locales. 
} 


public Manufacturer convert( String formVal, Class targetClass, 
Collection<ValidationError> errors ) 
{ 
return DAOFactory.DEFAULT.getManufacturerDAO().findById( Long.parseLong( formVal ) ); 
} 
} 

 

Getting back to the next section of the ManufacturerAction bean:

 
public List<Object> getBoundObjects() 
{ 
List<Object>	lst	= new ArrayList<Object>(); 

lst.add( getMan() ); 

return lst; 
} 

 

This method just tells the BoundEntityAction that the only Entity that is being bound into is the "man" property alone.  If we need to evict bound entities from the session, this is the only object that will need to be evicted.

Now on to our event handlers:

 
@DontValidate 
@DefaultHandler 
public Resolution view() 
{ 
return new ForwardResolution( "/pages/viewMan.jsp" ); 
} 

 

There isn't much left to be done for the view() event handler.  The Manufacturer instance to be viewed is already sitting in the ActionBean because: the above <s:link> uses the man property, Stripes sees that it is trying to inject a String into a Manufacturer variable, looks for a Converter to make this happen, and find our ManufacturerConverter, which knows to hydrate our Manufacturer instance.  The only thing really left for the event handler to do is to specify our JSP view.

 
public Resolution save() 
{ 
manDao.makePersistent( man ); 

getContext().addMsg( Messages.saved( "Manufacturer" ) ); 

return new RedirectResolution( "/Manufacturer.action?man=" 
+ man.getId() ); 
} 

 

This save() event handler uses the DAO to persist the man property, which is only explicitly needed in the case of adding a new instance, but works for both add-new and save-existing, and so allows us to unify those execution paths.  The event handler also makes use of Stripes' ridiculously convenient message facility for communicating feedback messages from the ActionBean to the JSP.  Finally, the save() event handler returns a RedirectResolution (facilitating the redirect-after-post usage pattern).

 
@DontValidate 
public Resolution cancel() 
{ 
evictBoundObjects(); 

getContext().addMsg( Messages.cancelled( "Manufacturer" ) ); 

return new RedirectResolution( "/pages/sampleHome.jsp" ); 
} 

 

This cancel() event handler wants to ignore validation, but it also wants any form changes to be explicitly ignored.  We accomplish this just like we do in the case of failed validation, by calling the evictBoundObjects() method.  It adds a feedback message stating that no changes took place, and redirects to the home page. 

That's all there is to it.  Stripes and Hibernate make binding a web form to your Entities, validating that form binding, persisting the changes, and giving feedback to the user a breeze, with very little code required to make it all happen.