ONJava.com    
 Published on ONJava.com (http://www.onjava.com/)
 See this if you're having trouble printing code examples


Advanced Java Content Repository API

by Sunil Patil
11/08/2006

Java Content Repository (JCR) API, specified as JSR-170, is an attempt to standardize an API used for accessing content repositories. In this article, we'll talk about the advanced and optional features defined in the JCR API. We assume that you're already familiar with basic features of JCR--such as how to add a new node or property, how to configure Apache Jackrabbit, etc. If you are not familiar with these topics or just want to refresh your memory, please check out the What is Java Content Repository article first.

In this article, we'll use Apache Jackrabbit, which is the reference implementation of JSR-170 and an open source project hosted on Apache, but you can use any other JSR-170-compliant content repository of your choice.

Content management systems from different vendors have been available for quite some time. All of these vendors ship their own version of a content repository with their CMS systems, usually providing a proprietary API for accessing their content repository. One of the primary goals of JSR-170 was to make it easy for these CMS vendors to adopt JSR-170. To do that, JSR-170 features are divided into three parts. First are the Level 1 features, which provide a read-only view of the repository. Level 2 defines the basic features required for read and write operations. In addition to these two levels, JSR-170 defines five optional features--versioning, observation, locking, SQL Search, and transactions--that can be implemented by a content repository. That means a vendor who has a Level 1-compliant repository can decide to implement the SQL search feature but none of the other advanced features. A second vendor might implement a different feature set--for example, a Level 2-compliant repository with versioning and observation.

This article is a step-by-step guide on how to develop your application using two of the most popular optional features defined in JSR-170. We will start our discussion with versioning, and then follow with observation, which lets you execute some business logic when a particular persistent change is made in the repository.

Sample Application

In this article, we'll use Apache Jackrabbit to change the sample blogging application developed in the "What is Java Content Repository" API to demonstrate the advanced features defined in JSR-170. Please note that Jackrabbit is not only Level 1- and Level 2-compliant, but it also implements all the optional features of JSR-170.

This sample blogging application is a web application that uses Struts 1.2 as its MVC framework. It allows you to post new blog entries, edit or delete existing blog entries, and attach image files to blog entries. The sample blogging application consists of two parts: a UI part, which handles taking inputs from users and displaying results, and the second part, which actually interacts with the content repository. We have created the BlogEntryDAO interface, which serves as the contract between these two parts. To save some time, we won't talk about how to build UI part; instead, we will concentrate only on data. Please download the sample code and copy the UI code from it directly.

The first thing to do in our sample application is to change the BlogEntryDAO interface as follows to define methods for versioning feature:

public interface BlogEntryDAO {
    //Basic methods implemented in first part
    public ArrayList getVersionList(String blogTitle)
        throws BlogApplicationException;
    public void restoreVersion(String blogTitle,String versionName)
        throws BlogApplicationException;
}

The first method, getVersionList(), takes the title of the blogList and returns a list of all the versions for that blogEntry.The restoreVersion() method is used for restoring a version of a blog entry using the specified version name.

Versioning

The first thing to discuss about versioning is what the term means in the context of a content repository. JSR-170 defines versioning as saving the state of the node to be recorded in such a way that it can later be restored. Since versioning is an optional feature, you should check first whether your repository supports it or not by calling the repository.getDescriptor("OPTION_VERSIONING_SUPPORTED").

In a versioning repository, a workspace can have both a versionable and non-versionable node. You can mark a particular node as versionable by adding mix:versionable as a mixin type to that node. Every versionable node is marked as read-only, so every time you want to make change in it, you need to call node.checkout() first to remove the read-only attribute from that node. Make whatever changes you want and then call node.save() to persist your changes. After that, you should call node.checkin(), which will put the node back in a read-only state and create a new version with a system-generated node.

If your repository is versionable, it will have a special version storage area in addition to one or more workspaces. This area is used for storing version history. The best part about JCR API is that it uses the same structure of nodes and properties for storing version data. It relates one node of nt:versionHistory type with every versionable node. This node contains a history of versions for that particular node and will always have one root node containing the basic version (the state of node at the time of creation). After that, whenever you check in your change, one new version node is created and is related to a previous version of the node by a successor relationship. Every version itself is a node of the nt:version type and will store the state of the node at the time the version was created in the jcr:frozenNode property.

Until now our discussion has focused mostly on theoretical aspects of versioning. Now let's try to take this discussion to the next level by changing our sample blogging application to use versioning. We want to change our sample blogging application so that every blog entry should display a "display version list" link. Each version listed by this link would have a "restore" link to restore the blogEntry to that particular version. Follow these steps to change the sample blogging application:

  1. As usual, you can copy the UI part for this from the sample application code.

  2. Change the insertBlogEntry() method of JackrabbitBlogEntryDAO class like this:

    public void insertBlogEntry(BlogEntryDTO blogEntryDTO) throws BlogApplicationException {
        Session session = JackrabbitPlugin.getSession();
                    Node rootNode = session.getRootNode();
        Node blogEntry = rootNode.addNode("blogEntry");
        blogEntry.addMixin("mix:versionable");
        blogEntry.setProperty(PROP_TITLE, blogEntryDTO.getTitle());
        blogEntry.setProperty(PROP_BLOGCONTENT, blogEntryDTO.getBlogContent());
        blogEntry.setProperty(PROP_CREATIONTIME, blogEntryDTO.getCreationTime());
        blogEntry.setProperty(PROP_BLOGAUTHOR, blogEntryDTO.getUserName());
        session.save();
    }

    First, we want to add a mixin type called mix:versionable to blogEntry. After this, the content repository will put the blogEntry in read-only state after saving it. However, this change would only make new entries versionable, leaving you with a mix of versioned and unversioned entries. You can solve this problem by deleting the existing content repository and adding new blog entries to it, but Apache Jackrabbit does not provide any built-in way to delete a content repository. The only way to do it is to delete the contents of c:/temp/blogging.

  3. Next, change updateBlogEntry() method like this:

    public void updateBlogEntry(BlogEntryDTO blogEntryDTO)
                throws BlogApplicationException {
        Session session = JackrabbitPlugin.getSession();
        Node blogEntryNode = getBlogEntryNode(blogEntryDTO.getTitle(),
                session);
        blogEntryNode.checkout();
        blogEntryNode.setProperty(PROP_BLOGAUTHOR, blogEntryDTO.getUserName());
        blogEntryNode.setProperty(PROP_BLOGCONTENT, blogEntryDTO
                .getBlogContent());
    
        blogEntryNode.setProperty(PROP_CREATIONTIME, blogEntryDTO
                .getCreationTime());
        session.save();
        blogEntryNode.checkin();
    }

    As mentioned in the previous discussion, once you mark a node as versionable, the repository will put it in read-only state, and as a result, you won't be able to make any changes in it. So when updating a blog entry, the first thing that you should do is call checkout(), which will make it writeable. After that, you can change the state of the node by calling setProperty() methods, and then save your changes with session.save(). Finally, call blogEntry.checkin().

  4. Make similar changes in other methods of JackrabbitBlogEntryDAO.

  5. Now, implement the getVersionList() method, which takes the title of a blog entry as a parameter and returns a list of versions for that particular blog entry.

    public ArrayList getVersionList(String blogTitle) throws BlogApplicationException {
        ArrayList versionList = new ArrayList();
        Node blogEntryNode = getBlogEntryNode(blogTitle);
        VersionHistory blogEntryVersionHistory = blogEntryNode.getVersionHistory();
        VersionIterator blogEntryVersionIt = blogEntryVersionHistory.getAllVersions();
    
        blogEntryVersionIt.skip(1);
        while(blogEntryVersionIt.hasNext()){
            Version version = blogEntryVersionIt.nextVersion();
            
            NodeIterator nodeIterator = version.getNodes();
            while(nodeIterator.hasNext()){
                Node node = nodeIterator.nextNode();
                String title  = node.getProperty(PROP_TITLE).getString();
                String blogContent = node.getProperty(PROP_BLOGCONTENT).getString();
                String blogAuthor = node.getProperty(PROP_BLOGAUTHOR).getString();
                String versionName = version.getName();
                Calendar creationTime = node.getProperty(PROP_CREATIONTIME).getDate();
                VersionEntryDTO blogEntryDTO = new 
                    VersionEntryDTO (blogAuthor, title, blogContent,
                                    creationTime, versionName);
                versionList.add(blogEntryDTO);
            }
        }
        return versionList;
    }

    The first step is to call getVersionHistory() to get access to the nt:versionHistory node attached to the blog entry node. A VersionHistory object wraps a nt:versionHistory node. Next, calling getAllVersions() on the VersionHistory object will give you an iterator. A version history will always have at least one version: the root version. You can call the getNodes() method on the version object to access child nodes of that version, which are Version objects (wrapping nt:version nodes). Each child node contains the state of the blogEntry at the time the version was created, and from that state we can create a BlogEntryDTO.

  6. The last step is to implement restoreVersion(), which takes two parameters: a blogTitle to uniquely identify blogEntry, and a versionName.

    public void restoreVersion(String blogTitle, String versionName) 
        throws BlogApplicationException {
        Node blogEntryNode = getBlogEntryNode(blogTitle);
        blogEntryNode.restore(versionName,true);
    }

    Once you have the blogEntry node, you can call restore() with the versionName to be restored. The second parameter to this method is a flag that represents what happens if the UUID of the node you're trying to restore already exists outside the subtree of this node. If so, and if the flag is true, then the incoming node takes precedence.

Now build your source code and deploy this application on the server. First, add a new blogEntry and make a few changes in it by editing that node two or three times. Now click on the "Display Version History" link. This will take you to a page containing a list of versions for that blog entry, and each version listed contains a restore link; when you click on the link for particular version, the blogEntry will be restored to the state stored in that particular version.

Observation

Observation allows you to monitor your content repository and to execute some business logic as soon as some persistent change is made. For example, let's suppose that this sample blogging application is used by your company for corporate blogging, and every employee gets a blogging account. Now imagine the legal department of your company wants an email sent to them as soon as each new blog entry is posted, just to check for any legal implications. Observation enables this feature.

You can find out if your repository implements observation by querying the repository descriptor table using repository.getDescriptor("OPTION_OBSERVATION_SUPPORTED"); this method call returns true if observation is supported, false otherwise.

Follow these steps to change your sample blogging application so that the event listener is notified as soon as a new blog entry is added; this version prints the new blog to the console rather than sending email.

  1. First, change JackrabbitPlugin.init() like this:

    public void init(ActionServlet actionServlet, ModuleConfig moduleConfig)
        throws ServletException {
        System.setProperty("org.apache.jackrabbit.repository.home",
                            "c:/temp/Blogging");
        Repository repository = new TransientRepository();
        session = repository.login(new SimpleCredentials("username",
                "password".toCharArray()));
        NodeEventListener nodeEventListener = new NodeEventListener();
        if (repository.getDescriptor(
            Repository.OPTION_OBSERVATION_SUPPORTED).equals("true")){
            ObservationManager observationManager =
                session.getWorkspace().getObservationManager();
        observationManager.addEventListener(nodeEventListener,
            Event.NODE_ADDED,"/",true,null,null,false);
        }
    }

    After getting a connection to the repository, we check whether this repository supports observation. If it does, get an ObservationManager by calling getObservationManager() on the workspace. Notice that observers operate at the workspace level, not the repository level.

    The ObservationManager class defines methods for registering and deregistering listeners. You can call ObservationManager.addEventListener() to register a new event listener. The first argument is an EventListener implementation, whose onEvent() will be called by the repository to notify it about events. The next argument is the event that you want to listen for; in our case, we are interested in a node added event. The third parameter is the path of the node that you're interested in. The fourth parameter is a flag that, if true, says that you're interested regardless of where the node is added in the child node hierarchy. The next two parameters are used for filtering events; the first parameter takes an array of UUIDs to indicate that you're only interested in events whose associated parent node has one of the UUIDs in this list. The next parameter takes an array of nodeTypes; if the value of this parameter is not null, then you'll be notified only if the associated parent node has one of the node types. The last parameter is a flag indicating whether you're interested in events that occur in the same node. If true, you won't be notified (not only of your own adds, but also when someone else adds a node in the same workspace).

  2. Now it's time to implement NodeEventListener:

    public class NodeEventListener implements EventListener {
     public void onEvent(EventIterator eventIterator) {
        while (eventIterator.hasNext()) {
            Event event = eventIterator.nextEvent();
            String eventPath = event.getPath();
            int eventType = event.getType();
            if (eventType == Event.NODE_ADDED) {
                String nodePath = eventPath.substring(1, eventPath.length());
                Session session = JackrabbitPlugin.getSession();
                Node blogEntryNode = session.getRootNode().getNode(nodePath);
                if(blogEntryNode.getName().equals("blogEntry")){
                    _logger.debug("New blog entry is added by"
                        + blogEntryNode.getProperty(
                                JackrabbitBlogEntryDAO.PROP_BLOGAUTHOR)
                                .getString()
                        + ", titled "
                        + blogEntryNode.getProperty(
                                JackrabbitBlogEntryDAO.PROP_TITLE)
                                .getString());
                }
            }
        }
    }

    When a persistent change occurs, the repository calls the onEvent() method of each registered listener that is entitled to receive notification, and passes it as an EventIterator object. The EventIterator contains the bundle of events (again, filtered for a particular listener) that describe the persistent changes made to the workspace. In our case, we are interested only when a new node is added, so EventIterator will have only one node added event. Once we have that event, we can get the path of the newly added node by calling the event.getPath() method. Use that path to search for the newly added node and print details in the logger.

Once you've built and deployed your code, try adding a new blogEntry: you will see that as soon as the Session.save() (or Item.save()) method is called, the onEvent() method of NodeEventListener will be called in a different thread and will dump details of the newly created node in the logfile.

Summary

In this article, we discussed two of the most useful advanced features defined in JSR-170. We talked about how to implement versioning and observation using JSR-170. Please take a look at the Apache Jackrabbit online documentation as well as the sample code for examples on how to develop your application using the locking and SQL Search optional features.

One of the most important factors for the success of any standardization effort is how much industry support exists for the effort. The best thing about JSR-170 is that it goes that extra distance to make this specification more adaptable. Due to its strategy of dividing core features in Level 1 and Level 2 compliance, it's easier for content repository vendors to start supporting it at whichever level makes the most sense to them. The second risk with specification is that if you don't define advanced features, then every vendor will start implementing it in its own way. JSR-170 does a fine job by defining these advanced features as optional, so if you decide to implement, you do so in a standardized way.

Take a look at JSR-170's expert group list, and you will find that it has already gained support from most of the CMS vendors such as Day Software, EMC Corporation, Filenet, Vignette, IBM, BEA, Oracle, and Sun Microsystems. Work has already begun on the next version of Java Content Repository API specification, version 2.0, under JSR 283. This specification will try to address a few more features that were not covered in JSR-170.

Resources

Sunil Patil has worked on J2EE technologies for more than five years. His areas of interest include object relational mapping tools, UI frameworks, and portals.


Return to ONJava.com.

Copyright © 2009 O'Reilly Media, Inc.