XPages series #6: Tracking data changes
Karsten Lehmann 22 July 2009 17:52:10
We will change and add a lot of code in our XPages/JSF sample implementation in this blog posting, because our current Movie Actor application does note handle data changes very well.Remember part 4 of this series, when we introduced the Actor and ActorList implementation:
Our ActorList Java bean is the so called backing bean behind the ActorList XPage, which means that it is used to provide the data and application logic needed by that XPage.
Moving all the code from the XPage to the backing bean helps us to separate the UI from the business logic. It improves code reuse and testing/debugging.
In part 4 I described how to show your own Java objects in a data table control and in a repeat control. We used this technology to display a list of Actor objects in read-only mode.
As I said in the article, you can replace the computed fields in the data table by edit boxes to make the Actor object list editable. JSF then calls the setter methods of the Actor objects with the new values.
That works, but the architecture isn't really smart that way.
In this article, I would like to add a data store to our little scenario, that we will use to store the data changes to a persistent data storage.
That way we can
- check whether the current user is allowed to change the data
- write the changes and the previous values to a log file (e.g. for auditing purpose)
- do data transformation, like storing the information of one data object in two places (e.g. two Notes documents or Notes and SQL)
- trigger additional data changes, like inheriting field values to/from other documents
In short: We get complete control over the data that is written to disk.
Our today's result will look like this:
(we concentrate on the functionality first, this is not a web design series ;-) )
It's the data table of part 4, but with edit boxes and a button "Save changes". That button triggers the save operation to store the modified Actor table row entries into our own data store.
The logging field at the bottom contains a list of data comparison results (I had changed the comment property of the first Actor in the list and pressed the save button).
Our data store
Let's start with a simple Java interface definition of our data store:
package com.acme.actors.model;
import java.util.List;
/**
* Interface for a CRUD-based Data Store
*/
public interface DataStore {
/**
* The method creates a new Actor instance
*
* @return new Actor
*/
public Actor createActor();
/**
* The method searches for an Actor by its ID in the data store
*
* @param id Actor ID
* @return Actor or null
*/
public Actor findActorById(String id);
/**
* Writes changes made to the specified Actor instance to the data store
* to store them permanently.
*
* @param changedActor Changed Actor instance
*/
public void updateActor(Actor changedActor);
/**
* The method delete an Actor from the data store
*
* @param id Actor ID
*/
public void deleteActor(String id);
/**
* The method returns a list of Actor objects, ordered by the lastname property.
* This list cannot be modified. Use the DataStore methods instead.
*
* @return List
*/
public List<Actor> getActorsByLastname();
}
That's a data store interface for our Actor objects following the CRUD pattern. CRUD stands for Create, Read, Update and Delete.
It's not a concrete implementation. A Java interface acts like a blueprint for the implementations: they have to implement all the methods of the interface.
Our implementation, using an in-memory list of objects can be found here:
- MyDataStore.java - com.acme.actors.model.mydb.MyDataStore
(I inserted a link to the file to make this article more readable)
package com.acme.actors.model;
import javax.faces.FacesException;
import com.acme.tools.JSFUtil;
/**
* The DataStoreManager manages the {@link DataStore} instance that is
* used for the data storage of the application.
*/
public class DataStoreManager {
private String m_storageClass;
private DataStore m_store;
public DataStoreManager() {}
/**
* This method is used by JSF to set the class name of the
* {@link DataStore} to be used.
*
* @param className class name
*/
@SuppressWarnings("unchecked")
public void setStorageClass(String className) {
m_storageClass=className;
try {
//try to initialize the data store using Java reflection
Class<? extends DataStore> storageClazz=(Class<? extends DataStore>) getClass().forName(className);
m_store=(DataStore) storageClazz.newInstance();
} catch (ClassNotFoundException e) {
throw new FacesException(e);
} catch (IllegalAccessException e) {
throw new FacesException(e);
} catch (InstantiationException e) {
throw new FacesException(e);
}
}
/**
* Returns the class name of the {@link DataStore} to be used.
*
* @return class name
*/
public String getStorageClass() {
return m_storageClass;
}
/**
* Returns the data store instance for the data access.
*
* @return data store
*/
public DataStore getStore() {
return m_store;
}
/**
* Convenience method to access the instance of the DataStoreManager for
* the current user from the session scope.
*
* @return DataStoreManager instance
*/
public static DataStoreManager getManager() {
return (DataStoreManager) JSFUtil.getBindingValue("#{DataStoreManager}");
}
}
Take a look at two methods in particular: setStorageClass(String) and the static method getManager().
We use setStorageClass(String) to tell the DataStoreManager bean which implementation of the DataStore interface it should use. It then tries to instantiate the concrete data store, for our test implementation the class is "com.acme.actors.model.mydb.MyDataStore".
The static method getManager() uses one of the helper functions of series part 5 to return the session scope instance of the DataStoreManager.
So in our Java code, we can quickly do data store operations like this
DataStore ds=DataStoreManager.getManager().getStore();
ds.deleteActor("12345");
Actor newActor=ds.createActor();
newActor.setFirstname("Humphrey");
newActor.setLastname("Bogart");
newActor setComment("Casablanca");
ds.updateActor(newActor);
JSF will take care that the DataStoreManager bean is created when we call getManager(). We can even call getManager() before the bean is used anywhere in an XPage.
Please note that the code above does not contain any Lotus Notes specific code like bindings to Notes documents and views, just our own simple Java classes and interfaces.
Data handling is done in our code.
Setting default bean properties
How do we know when to tell the DataStoreManager the class name of our data store?
That's easy: We let JSF do it automatically. By adding a small piece of code in the faces-config.xml, we can define default values that should be set when a bean is created by JSF.
<?xml version="1.0" encoding="UTF-8"?>
<faces-config>
<managed-bean>
<managed-bean-name>DataStoreManager</managed-bean-name>
<managed-bean-class>com.acme.actors.model.DataStoreManager</managed-bean-class>
<managed-bean-scope>session</managed-bean-scope>
<managed-property>
<property-name>storageClass</property-name>
<value>com.acme.actors.model.mydb.MyDataStore</value>
</managed-property>
</managed-bean>
<managed-bean>
<managed-bean-name>ActorList</managed-bean-name>
<managed-bean-class>com.acme.actors.controller.ActorList</managed-bean-class>
<managed-bean-scope>session</managed-bean-scope>
</managed-bean>
<managed-bean>
<managed-bean-name>Log</managed-bean-name>
<managed-bean-class>com.acme.logging.Log</managed-bean-class>
<managed-bean-scope>session</managed-bean-scope>
</managed-bean>
</faces-config>
You can set more than just string properties. For example, you can fill a Map with key/value pairs or predefine the values of a Java List. Please refer to the JSF documentation I mentioned in part 2 of this series for the exact syntax.
Detecting object property changes
There are certainly many ways to track property changes for the data objects. For this example, I chose to store a list of ObjectPropertyChangeEvents in my data objects. Such a change event basically is a container for the name of the property (like "Firstname", "Lastname", "Comment"), its old and its new value. We will generate such events in the setter methods of our new Actor class implementation like this:
public void setFirstname(String newFirstName) {
if (!StringUtil.isEqual(m_firstName, newFirstName)) {
//first name has changed. Store old and new value in a change event
ObjectPropertyChangeEvent evt=new ObjectPropertyChangeEvent(FLD_ACTOR_FIRSTNAME, m_firstName, newFirstName);
m_firstName=newFirstName;
addChangeEvent(evt);
}
}
Here is the base implementation of a data object. It contains all the property change management methods. Our Actor class will now be a subclass of DataObject to leverage those methods.
package com.acme.actors.model;
import java.util.Hashtable;
/**
* Abstract base object for a data store object that provides an internal list
* of changed object properties. The purpose is to quickly get information
* in a data source what property changes have to be transferred to
* persistent storage.
*/
public abstract class DataObject {
private boolean m_initialized=false;
private Hashtable<String, ObjectPropertyChangeEvent> m_changeEvents;
public DataObject() {}
protected void init() {
if (!m_initialized) {
//lazy creation of member variables
m_changeEvents=new Hashtable<String, ObjectPropertyChangeEvent>();
m_initialized=true;
}
}
/**
* Method to check whether properties in this object have been changed.
*
* @return <code>true</code> if there are changed properties
*/
public boolean hasChanges() {
init();
return !m_changeEvents.isEmpty();
}
/**
* Returns the property change events for this object
*
* @return change events or an empty list
*/
public ObjectPropertyChangeEvent[] getChanges() {
init();
return m_changeEvents.values().toArray(new ObjectPropertyChangeEvent[m_changeEvents.size()]);
}
/**
* Adds a change event to the internal list. If the property has been changed
* before, the old and new change event will be merged into one.
*
* @param evt change event
*/
protected void addChangeEvent(ObjectPropertyChangeEvent evt) {
init();
String fieldId=evt.getFieldId();
ObjectPropertyChangeEvent oldEvt=m_changeEvents.get(fieldId);
if (oldEvt==null) {
m_changeEvents.put(fieldId, evt);
}
else {
//merge old and new event
Object oldValue=oldEvt.getOldValue();
m_changeEvents.put(fieldId, new ObjectPropertyChangeEvent(fieldId, oldValue, evt.getNewValue()));
}
}
/**
* Method to clear the change event list
*/
public void resetChanges() {
m_initialized=false;
init();
}
}
Here is the new Actor implementation and the source code of the ObjectPropertyChangeEvent:
Actor.java - com.acme.actors.model.Actor
ObjectPropertyChangeEvent.java - com.acme.actors.model.ObjectPropertyChangeEvent
Our data store is now ready to use and our Actor data objects are prepared to write a change event log when somebody calls the setter methods.
Adding the "save changes" operation
At first, we now have to produce code in our ActorList backing bean that handles the save operation: It grabs the changed Actor objects from the data table and sends them to the persistent data store.
We use another of our helper function for this purpose: JSFUtil.findComponent(String). It traverses the JSF component tree of the current XPage to give us access to the data table (like the getComponent() method does in XPages JavaScript).
This is our new ActorList backing bean:
package com.acme.actors.controller;
import java.util.List;
import javax.faces.component.UIComponent;
import javax.faces.component.UIData;
import com.acme.actors.model.Actor;
import com.acme.actors.model.DataStoreManager;
import com.acme.tools.JSFUtil;
public class ActorList {
public ActorList() {}
public List<Actor> getActorsByLastname() {
//delegate call to the new data store
return DataStoreManager.getManager().getStore().getActorsByLastname();
}
public void save() {
//grab the data table content and send it to the data store
UIComponent table=JSFUtil.findComponent("actorTable");
if (table instanceof UIData) {
//the table is a subclass of UIData, which gives us access to the row values of the table
UIData tableData=(UIData) table;
int rows=tableData.getRowCount();
int oldRowIndex=tableData.getRowIndex();
for (int i=0; i tableData.setRowIndex(i);
Object currObj=tableData.getRowData();
if (currObj instanceof Actor) {
Actor currActor=(Actor)currObj;
//send the Actor object to the data store to write changes
//to the persistent storage
DataStoreManager.getManager().getStore().updateActor(currActor);
}
}
tableData.setRowIndex(oldRowIndex);
}
else {
Log.getInstance().println("Could not find table with id=actorTable");
}
}
}
Finally, we need to add the "Save changes" button to the XPage UI with the following JavaScript onClick code:
var actorList=com.acme.tools.JSFUtil.getBindingValue("#{ActorList}");
actorList.save();
Here is a list of remaining files that have been used or changed in this article:
- Full source of the ActorList XPage
ActorList.xsp.txt - Helper class for logging (bean implementation)
Log.java - com.acme.logging.Log - Helper class for string comparison
StringUtil.java - com.acme.tools.StringUtil
Ok, guys... the worst part of this series is over ;-). It has become a really long article, even longer than part 4.
But it's easier than it looks like. Believe me. :o)
- Comments [6]