Mindoo Blog - Cutting edge technologies - About Java, Lotus Notes and iPhone

  • 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:

    Image:XPages series #6: Tracking data changes

    (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: The first interesting thing is how we tell JSF about this concrete data store implementation. We use a new managed bean for that, called DataStoreManager:

    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:
    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

    1Adrian Stefanescu  03.09.2009 14:25:35  XPages series #6: Tracking data changes

    Hallo,

    vielen Dank für diese Serie von Tutorials (nach meiner Meinung eine der interessantesten Publikationen zum Thema Xpages).

    Endlich wurden einige Fragen, die sich mir bei der Betrachtung der Xpages gestellt haben, beantwortet. Wie kann man eine Trennung zwischen Präsentation und Logik realisieren? Wie kann man Unit Tests implementieren?

    Meine einzige Anregung wäre, dass noch mehr Informationen zu den technischen Hitergründen der Xpages Technologie erwähnt würden.

    Ich bin schon gespannt auf die nächsten Folgen.

    Weiter so!!

    Grüße aus Berlin

    Adrian Stefanescu

    2Ivailo Iliev  29.09.2009 9:34:50  XPages series #6: Tracking data changes

    Hi, this is amazing, thanks for all the articles.

    can you put somewhere here the nsf file so we can have the source- it will be really helpful.

    I have notices something else- sometimes in xPages it's changing automatically the values of EL expressions, and sometimes on 8.5 it's not re-compiling(or re-interpreting) the java changes. I've seen in 8.5.1 it's working more stable, but there is still the problem with re-making the EL strings- sometimes it's deleting them, sometimes it's just changing them. Do you have some experience with this- maybe there is an useful solution how to prevent it :)

    Anyhow- I think this way of development can make Nodes development into much better one and will un-tie our hands :)

    3Karsten Lehmann  29.09.2009 10:09:04  XPages series #6: Tracking data changes

    Hi Ivan,

    I had to delay my further XPage development in the last weeks to work on other projects, but I'm currently coming back to it to design and build a quite large Java architecture for an XPages application (about 10 Java projects to split the architecture up into independent and reusable parts, that don't all require the XPages or JSF runtime in the classpath).

    Regarding the EL expression issue:

    I also saw that in 8.5 and (maybe only partly) fixed in the 8.5.1 code drops.

    John Mackey may have a fix for this in 8.5. He talks about it in his article "XPages - How to pass a Document Data Source to a Custom Control" in the XPages blog at { Link }

    I haven't tried yet if it helps.

    For providing the NSF:

    I think I published all the source files in the blog articles. At the moment, I would consider the sample scenario to be "work in progress", until I decide that the XPages series about leveraging JSF features has ended.

    I think there is still something left to write about, so it will take some time until I can publish a final NSF file. I hope that I will have some new inspiration for articles, now that I'm working again with the technology.

    4Sushant  05.03.2011 12:24:55  XPages series #6: Tracking data changes

    Hi, i am new to Managed Beans and JSF in Xpages. How can we store the data using a managed bean into a Notes Document ? or I want to have the model object ready when the session is going on but how do I commit that to a notes datastore or a Notes Document at the end of session or when I save and submit the Xpage?

    5Reynald Reyes  10.03.2011 2:58:21  XPages series #6: Tracking data changes

    Hi Sushant,

    You can follow the steps used by jeremy hodge in { Link } .. a very detail explanation about committing into the database

    6Karsten Lehmann  10.03.2011 11:52:29  XPages series #6: Tracking data changes

    Hi Sushant,

    I'm on vacation this week. I will post an example how to load/store Lotus Notes data with beans and log data changes next week. This is the third and final example of our Lotusphere session BP212 (slides in this blog).

    Karsten