This document will describe various XSnapshot use cases within an application, as illustrated in the example application. We assume that you have already read the Quick Start to set up XSnapshot within your project's build process.
You create snapshot objects by adding XSnapshot tags to your POJO data model. The @xsnapshot.snapshot-class
tag at the class level directs XSnapshot to produce a snapshot object for a POJO, and the @xsnapshot.property
tag tells XSnapshot what properties to include in a particular snapshot. You can declare multiple snapshots for a single POJO.
This is a simple example to show basic XSnapshot definitions. Note that
this is similar to @ejb.value-object
/** * @xsnapshot.snapshot name="simple" class="SimpleFoo" * @xsnapshot.snapshot name="full" class="FullFoo" */ public class Foo { // ... private fields /** @xsnapshot.property match="*" */ public String getName() { ... } public void setName(String s) { ... } /** @xsnapshot.property match="full" */ public String getDescription() { ... } public void setDescription(String s) { ... } }
This example will generate two snapshot objects called SimpleFoo
and FullFoo
. The match="*"
will direct XSnapshot
to include the name
property in all snapshots. The match="full"
on getDescription
will tell XSnapshot to only include description
in the snapshot that matches the "full" snapshot, i.e., FullFoo
.
The XSnapshotUtils
object can be used to create snapshot
object instances from POJOs and copy the values from snapshots back to
the POJO model. This is the common usage pattern:
Foo foo = ...; // convert to snapshot with specified name // the name is the string argument that matches the name as specified in // @xsnapshot.snapshot-class tag SimpleFoo simple = xsnapshotUtils.createSnapshot(foo, "simple"); FullFoo full = xsnapshotUtils.createSnapshot(foo, "full");
// create model Foo newFoo = (Foo)xsnapshotUtils.createModel(full); // copy into existing model xsnapshotUtils.copyIntoModel(full, foo); // note that createModel is the equivalent of // copyIntoModel(full, obj = new Foo()), return obj;
Please see the quick start guide for info on how to set up the xsnapshotUtils object using Spring wiring. This object is not a singleton; it depends on an XSnapshot registry at runtime.
XSnapshot is powerful because, unlike BeanUtils.copyProperties and @ejb.value-object, it can handle recursing into components nested within objects, converting deeper parts of the POJO object graph into snapshots. This is a variant of the value objects/data transfer objects (DTO) pattern, and is incredibly useful when you want to serialize pieces of POJO models over a remoting framework (RMI, Hessian, AJAX/DWR, SOAP, etc.)
Here's an example adapted from the sample application:
/** * @xsnapshot.snapshot-class name="details" class="EmployeeDetailsSnapshot" */ public class Employee { /** * @xsnapshot.property match="*" */ public String getLastName() { ... } /** * @xsnapshot.property match="*" name="locationId" snapshot="simple" */ public Location getLocation() { ... } /** * @xsnapshot.array match="*" name="projectIds" * model-element-type="Project" * element-snapshot="simple" */ public Set getProjects() { ... } }
Assuming Project and Location both have "simple" snapshots, the above example will generate a class called EmployeeDetailsSnapshot
public class EmployeeDetailsSnapshot { public String getLastName() { ... } public SimpleLocation getLocation() { ... } public SimpleProject[] getProjects() { ... } }
The resulting EmployeeDetailsSnapshot can be freely serialized, unlike the Employee object. This is because the Project and Location objects may be part of bidirectional relationships; if you try to serialize an Employee directly, you end up also grabbing all of the associated projects and locations, which in turn may grab every employee employee in the database!
Also note that the @xsnapshot.array tag converts a collection into an array, which can sometimes be more easily handled over certain remoting frameworks like Axis. Earlier versions of the JAX-RPC spec did not permit java.util collections to bind to XML types.
ActionForms are really just a special kind of DTO/snapshot, and can be easily defined with XSnapshot doclet tags. This is an example taken from the sample application:
/** * @xsnapshot.snapshot-class name="form" * class="net.sf.xsnapshot.example.presentation.web.struts.forms.EmployeeForm" * extends="org.apache.struts.validator.ValidatorActionForm" */ public class Employee { // setters and other primitive properties omitted for brevity /** @xsnapshot.property match="*" */ public String getLastName() { ... } /** * @xsnapshot.property match="form" * type="net.sf.xsnapshot.example.presentation.web.struts.forms.DateInputModel" * init-value="new net.sf.xsnapshot.example.presentation.web.struts.forms.DateInputModel()" * snapshot-transformer="to-date-input-model" * model-transformer="from-date-input-model" */ public Date getDateOfBirth() { ... } /** * @xsnapshot.property match="form" * name="locationId" type="java.lang.Long" * nested-property="id" copy-to-model="true" */ public Location getLocation() { ... } /** * @xsnapshot.array match="form" name="projectIds" * model-element-type="net.sf.xsnapshot.example.data.model.Project" * snapshot-element-type="java.lang.Long" element-nested-property="id" */ public Set getProjects() { ... } }
The resulting generated form will look something like this:
public class EmployeeForm extends ValidatorActionForm { public String getLastName() { ... } public DateInputModel getDateOfBirth() { ... } public Long getLocationId() { ... } public Long[] getProjectIds() { ... } }
There are several salient points that can be made from this example:
ValidatorActionForm
it
can be used directly as a Struts form bean. DateInputModel
, a class with three String properties for year, month
and day. This allows us to use three separate <html:select> widgets on the JSP. The
property-level conversion is handled by custom type converters, which are registered with
the XSnapshotRegistry via the Spring configuration.There are two basic patterns to using XSnapshot in basic CRUD Struts actions. First, populating a form for editing:
Employee emp = employeeBean.getEmployee(id); EmployeeForm form = xsnapshotUtils.createSnapshot(emp); request.setAttribute("EmployeeForm", form);
... and next, save a new object or updating an existing object from a form:
EmployeeForm empForm = (EmployeeForm)form; Employee emp = null; if (empForm.getId() == null) { emp = new Employee(); } else { emp = employeeBean.getEmployee(empForm.getId()); } employeeBean.saveOrUpdateEmployee(emp);
That's it! All of the complexity is in the POJO model and in the Spring configuration, where it belongs.
The really exciting thing about this is, whenever you add a new property to your model, you don't have to touch your action. Just your JSP. XSnapshot even will convert IDs from forms back to the related entities with callbacks to persistence. BeanUtils can't do that.
XSnapshot needs to be configured with persistence callbacks to convert IDs in forms back to
the related objects behind the scenes, to keep Actions lean and mean as above. This is described partially in the quick start guide. All you have to do to make
the above examples work is make sure that there is a transformer registered with XSnapshot
under the key "inverse-nested-property". Whenever an XSnapshot mapping specifies
a property with nested-property="id" (or equivalent in collection/map forms), this transformer
is implicitly called as part of the copyIntoModel
process to convert the
property back into the full value.
By imbuing this transformer with access to persistence, it is trivial to "convert" an ID to an entity: it's just a load. Effectively, this transformer is like a special DAO, which can be configured in Spring like all your other DAOs. Currently, XSnapshot provides an id-to-entity converter for both Hibernate 2.x and 3.x.