Showing posts with label springmodules-jcr. Show all posts
Showing posts with label springmodules-jcr. Show all posts

Saturday, September 15, 2007

Jackrabbit Event Handling

In his article, Advanced Java Content Repository API, Sunil Patil says that two of the most popular advanced features of a JCR compliant content repository (one of which is Jackrabbit) are Versioning and Observation. Since I was already looking at Jackrabbit, I decided to check out these APIs a bit to see if I could find some use for them.

I can see the Versioning API being useful for organizations who actually generate their own content, and would need to track any changes made to documents. This is particularly true in industries with strong compliance requirements, such as Finance, Healthcare, etc. Although we do generate some amount of internal content, typically they don't need to be maintained and revised, they just expire after a period of time, so we don't really have a need for version history. So I read about it in Sunil Patil's article, but didn't make any effort to actually try it in my own use case.

The Observation API looked interesting. It allows you to register Listeners on various predefined events such as a Node being removed or added, and Properties being added, removed or changed. I got interested in it because I thought that perhaps we could use these events to trigger legacy code that did not depend on the repository. As before, I decided to use the JCR module from the Spring Modules Project to make integration with Spring easier.

As an experiment, I decided to use the Observation API to trap a content update event, which would then trigger off a Lucene index update. The content update consists of dropping the content node for the content, creating a new one, and re-inserting the properties back in. The code for ContentUpdater.java is shown below:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
import java.io.File;
import java.io.IOException;

import javax.jcr.Node;
import javax.jcr.NodeIterator;
import javax.jcr.RepositoryException;
import javax.jcr.Session;
import javax.jcr.query.Query;
import javax.jcr.query.QueryManager;
import javax.jcr.query.QueryResult;

import org.apache.log4j.Logger;
import org.springframework.beans.factory.annotation.Required;
import org.springmodules.jcr.JcrCallback;
import org.springmodules.jcr.JcrTemplate;

public class ContentUpdater {

  private static final Logger LOGGER = Logger.getLogger(ContentUpdater.class);
  
  private String contentSource;
  private JcrTemplate jcrTemplate;
  private IParser parser;

  @Required
  public void setContentSource(String contentSource) {
    this.contentSource = contentSource;
  }

  @Required
  public void setJcrTemplate(JcrTemplate jcrTemplate) {
    this.jcrTemplate = jcrTemplate;
  }

  @Required
  public void setParser(IParser parser) {
    this.parser = parser;
  }

  public void update(final File file) {
    jcrTemplate.execute(new JcrCallback() {
      public Object doInJcr(Session session) throws IOException, RepositoryException {
        try {
          DataHolder dataHolder = parser.parse(file);
          String contentId = dataHolder.getProperty("contentId");
          Node contentSourceNode = getContentNode(session, contentSource, null);
          Node contentNode = getContentNode(session, contentSource, contentId);
          if (contentNode != null) {
            contentNode.remove();
          }
          contentNode = contentSourceNode.addNode("content");
          for (String propertyKey : dataHolder.getPropertyKeys()) {
            String value = dataHolder.getProperty(propertyKey);
            contentNode.setProperty(propertyKey, value);
          }
          session.save();
        } catch (Exception e) {
          throw new IOException("Parse error", e);
        }
        return null;
      }
    }); 
  }
  
  public Node getContentNode(final Session session, final String contentSource, 
      final String contentId) throws Exception {
    if (contentId == null) {
      return session.getRootNode().getNode(contentSource);
    }
    QueryManager queryManager = session.getWorkspace().getQueryManager();
    Query query = queryManager.createQuery("//" + contentSource + 
      "/content[@contentId='" + contentId + "']", Query.XPATH);
    QueryResult result = query.execute();
    NodeIterator ni = result.getNodes();
    if (ni.hasNext()) {
      Node contentNode = ni.nextNode();
      return contentNode;
    } else {
      return null;
    }
  }
}

When the session.save() happens, a bunch of events are thrown out by Jackrabbit to be picked up by any interested EventListener objects. We define one such EventListener which listens to one specific event generated by the ContentUpdater.java class, and handles it. The code for the ContentUpdatedEventListener.java is shown below:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
import java.io.IOException;
import java.util.List;

import javax.jcr.Node;
import javax.jcr.NodeIterator;
import javax.jcr.Property;
import javax.jcr.PropertyIterator;
import javax.jcr.RepositoryException;
import javax.jcr.Session;
import javax.jcr.observation.Event;
import javax.jcr.observation.EventIterator;
import javax.jcr.observation.EventListener;
import javax.jcr.query.Query;
import javax.jcr.query.QueryManager;
import javax.jcr.query.QueryResult;

import org.apache.log4j.Logger;
import org.springframework.beans.factory.annotation.Required;
import org.springmodules.jcr.JcrCallback;
import org.springmodules.jcr.JcrTemplate;

/**
 * Event listener that gets called whenever a source File node changes.
 */
public class ContentUpdatedEventListener implements EventListener {

  private static final Logger LOGGER = Logger.getLogger(ContentUpdatedEventListener.class);
  
  private JcrTemplate jcrTemplate;
  private List<IEventHandler> eventHandlers;

  @Required
  public void setJcrTemplate(JcrTemplate jcrTemplate) {
    this.jcrTemplate = jcrTemplate;
  }

  @Required
  public void setEventHandlers(List<IEventHandler> eventHandlers) {
    this.eventHandlers = eventHandlers;
  }

  public void onEvent(final EventIterator eventIterator) {
    jcrTemplate.execute(new JcrCallback() {
      public Object doInJcr(Session session) throws IOException, RepositoryException {
        while (eventIterator.hasNext()) {
          Event event = eventIterator.nextEvent();
          if (event.getType() == Event.NODE_ADDED) {
            QueryManager queryManager = session.getWorkspace().getQueryManager();
            Query query = queryManager.createQuery("/" + event.getPath(), Query.XPATH);
            QueryResult result = query.execute();
            NodeIterator nodes = result.getNodes();
            if (nodes.hasNext()) {
              Node contentNode = nodes.nextNode();
              PropertyIterator properties = contentNode.getProperties();
              DataHolder dataHolder = new DataHolder();
              while (properties.hasNext()) {
                Property property = properties.nextProperty();
                dataHolder.setProperty(property.getName(), property.getValue().getString());
              }
              LOGGER.debug("Did I get here?");
              for (IEventHandler eventHandler : eventHandlers) {
                try {
                  eventHandler.handle(dataHolder);
                } catch (Exception e) {
                  LOGGER.info("Failed to handle event:" + event.getPath() +  
                      " of type:" + event.getType() + 
                      " by " + eventHandler.getClass().getName(), e);
                }
              }
            }
          }
        }
        return null;
      }
    });
  }
}

To make the design more modular and cleaner, the EventListener can be injected with a List of IEventHandler objects, whose handle() method gets called in a for loop, so multiple actions can happen when an event is trapped by the Listener. The IEventHandler.java code is shown below:

1
2
3
public interface IEventHandler {
  public void handle(DataHolder holder) throws Exception;
}

A dummy implementation that does nothing but prints that it is updating a Lucene index is shown below, for illustration:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
import org.apache.log4j.Logger;
import org.springframework.beans.factory.annotation.Required;

/**
 * A dummy class to demonstrate event handling.
 */
public class LuceneIndexUpdateEventHandler implements IEventHandler {

  private static final Logger LOGGER = Logger.getLogger(LuceneIndexUpdateEventHandler.class);
  private String indexPath;

  @Required
  public void setIndexPath(String indexPath) {
    this.indexPath = indexPath;
  }

  public void handle(DataHolder holder) throws Exception {
    LOGGER.info("Updated Lucene index at:" + indexPath);
  }
}

Finally, we tie it all together with Spring configuration. Here is the applicationContext.xml file. Refer to my last post for the complete applicationContext.xml file, I just show the diffs here to highlight the changes and explain them:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
<beans ...>
  ...
  <bean id="jcrSessionFactory" class="org.springmodules.jcr.JcrSessionFactory">
    ...
    <property name="eventListeners">
      <list>
        <ref bean="contentUpdatedEventListenerDefinition"/>
      </list>
    </property>
  </bean>

  <!-- The updater -->
  <bean id="myRandomContentUpdater" class="com.mycompany.myapp.ContentUpdater">
    <property name="contentSource" value="myRandomContentSource"/>
    <property name="jcrTemplate" ref="jcrTemplate"/>
    <property name="parser" ref="someRandomDocumentParser"/>
  </bean>

  <!-- Linked to the EventListener via this bean -->
  <bean id="contentUpdatedEventListenerDefinition" class="org.springmodules.jcr.EventListenerDefinition">
    <property name="absPath" value="/"/>
    <property name="eventTypes" value="1"/><!-- Event.NODE_ADDED -->
    <property name="listener" ref="contentUpdatedEventListener"/>
  </bean>
  
  <!-- The EventListener -->
  <bean id="contentUpdatedEventListener" class="com.mycompany.myapp.ContentUpdatedEventListener">
    <property name="jcrTemplate" ref="jcrTemplate"/>
    <property name="eventHandlers">
      <list>
        <ref bean="luceneIndexUpdateEventHandler"/>
      </list>
    </property>
  </bean>

  <!-- The EventHandler -->
  <bean id="luceneIndexUpdateEventHandler" class="com.mycompany.myapp.LuceneIndexUpdateEventHandler">
    <property name="indexPath" value="/tmp/lucene"/>
  </bean>
  
</beans>

The first change is to register one or more EventListenerDefinition beans to the JcrSessionFactory. This is shown in the first block above. The second block is simply the configuration for the ContentUpdater. The third block is the EventListenerDefinition which says that the EventListener it defines listens to all events starting from root and fiters on event type 1 (Event.NODE_ADDED), and the actual reference to the EventListener bean. The fourth block is the definition and configuration for the ContentUpdatedEventListener EventListener implementation, which also takes in a List of IEventHandler objects. In our case the list contains only the reference to the dummy LuceneIndexUpdaterEventHandler class. The final block is the bean definition for the IEventHandler.

To run this code, I have a very simple JUnit harness that calls the ContentUpdater.update() method with a File reference. The node corresponding to the File is updated and an event sent, and we get to see a log message like the following in our logs. Notice that this log is usually emitted after JUnit's messages, signifying that this is called asynchronously.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
Tests run: 1, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 4.246 sec

Results :

Tests run: 1, Failures: 0, Errors: 0, Skipped: 0

13 Sep 2007 09:33:29,509 INFO  com.healthline.jrtest.LuceneIndexUpdateEventHandler 
com.healthline.jrtest.LuceneIndexUpdateEventHandler.handle(LuceneIndexUpdateEventHandler.java:25)
(ObservationManager, ): Updated Lucene index at:/tmp/lucene
[INFO] ------------------------------------------------------------------------
[INFO] BUILD SUCCESSFUL
[INFO] ------------------------------------------------------------------------
[INFO] Total time: 8 seconds
[INFO] Finished at: Thu Sep 13 09:33:29 PDT 2007
[INFO] Final Memory: 11M/86M
[INFO] ------------------------------------------------------------------------

The Observation API reminds me of a middleware application that I maintained for a while at a previous job, which was a bridge between our various home-grown content management systems and our actual publishing system. Events were sent as HTTP requests, and were converted into actual publishing requests by the application and sent to the publishing system. Jackrabbit's Observation API would be a perfect fit in this situation, and it would be so much more elegant.

As I was exploring the Versioning and Observation APIs, I had an epiphany. I realized the reason I have this whole love-hate thing (love the features, can't find enough reason to implement it) with Jackrabbit is because its targeted to a business model different from mine. Jackrabbit (and I am guessing any CMS in general) are targeted to businesses which tend to manage their content in individual pieces, such as news stories in a news company or product spec sheets for manufacturing companies, for example. Unlike them, we manage our content in bulk, regenerating all content from a content provider in batch mode. That may change in the future, and perhaps it would then be time to reconsider.

Saturday, September 08, 2007

Spring loaded Jackrabbit

So far I haven't been very enthusiastic about Jackrabbit, and yet I keep writing about it. My lack of enthusiasm stems from the fact that it would quite an effort to move our existing content to any content repository, which is stored as a combination of flat files, database tables and Lucene indexes, as well as keep up with the steady flow of new content we are licensing. We also have tools and gadgets which require more granular access than that provided through Jackrabbit's standard query API.

However, of late, almost everything I do seems to be driven by whether I can apply it readily, which, in retrospect, seems to be a bit short-sighted. This was driven home to me recently when I was asked to implement an idea I had suggested (and developed a proof of concept for my own understanding) about a year ago. So what seems to be impractical today may not be so a year from now, so it may be worth spending time on some technology today in the hope that maybe the knowledge would be useful down the line. In fact, that's one reason I started with this blog in the first place. And there is no doubt that Jackrabbit is cool technology, and while there are still warts, I expect it to mature enough to justify production-quality use by the time I am ready to use it.

That said, one of the things which make a particular software attractive to me is its ability to be integrated with the Spring Framework, only because I find Spring's IoC/dependency injection useful and hence tend to use it everywhere, from web applications to standalone Java projects. The Spring Modules project has built code to integrate with various other popular software, and one of them is JCR. Within the springmodules-jcr project, there is support for Jackrabbit and Jeceira, another open source CMS based on the JCR specifications.

Based upon an InfoQ article "Integrating Java Content Repository and Spring", written by Costin Leau, one of the developers on the Spring Modules project, I decided to rewrite my Content Loader and Retriever implementations that I described in my blog post two weeks ago, to use JcrTemplate and JcrCallback provided by springmodules-jcr, as well as let Spring build up my Repository and other objects using dependency injection.

First, the applicationContext.xml so you know how its all set up:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xmlns:util="http://www.springframework.org/schema/util"
       xsi:schemaLocation="
       http://www.springframework.org/schema/beans 
       http://www.springframework.org/schema/beans/spring-beans-2.0.xsd
       http://www.springframework.org/schema/util 
       http://www.springframework.org/schema/util/spring-util-2.0.xsd">

  <bean id="repository" class="org.springmodules.jcr.jackrabbit.RepositoryFactoryBean">
    <property name="configuration" value="classpath:repository.xml"/>
    <property name="homeDir" value="file:/tmp/repository"/>
  </bean>
  
  <bean id="jcrSessionFactory" class="org.springmodules.jcr.JcrSessionFactory">
    <property name="repository" ref="repository"/>
    <property name="credentials">
      <bean class="javax.jcr.SimpleCredentials">
        <constructor-arg index="0" value="user"/>
        <constructor-arg index="1">
          <bean factory-bean="password" factory-method="toCharArray"/>
        </constructor-arg>
      </bean>
    </property>
  </bean>
  
  <bean id="password" class="java.lang.String">
    <constructor-arg index="0" value="password"/>
  </bean>
  
  <bean id="jcrTemplate" class="org.springmodules.jcr.JcrTemplate">
    <property name="sessionFactory" ref="jcrSessionFactory"/>
    <property name="allowCreate" value="true"/>
  </bean>

  <bean id="fileFinder" class="com.mycompany.myapp.FileFinder">
    <property name="filter" value=".xml"/>
  </bean>
  
  <bean id="someRandomDocumentParser" 
      class="com.mycompany.myapp.SomeRandomDocumentParser"/>
  
  <bean id="myRandomContentLoader" class="com.mycompany.myapp.ContentLoader2">
    <property name="fileFinder" ref="fileFinder"/>
    <property name="jcrTemplate" ref="jcrTemplate"/>
    <property name="contentSource" value="myRandomContentSource"/>
    <property name="parser" ref="someRandomDocumentParser"/>
    <property name="sourceDirectory" value="/path/to/my/random/content"/>
  </bean>
  
  <bean id="myRandomContentRetriever" class="com.mycompany.myapp.ContentRetriever2">
    <property name="jcrTemplate" ref="jcrTemplate"/>
  </bean>
    
</beans>    

The ContentLoader2.java is a version of ContentLoader.java which uses the springmodules-jcr API to work with Jackrabbit:

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
package com.mycompany.myapp;

import java.io.File;
import java.io.IOException;
import java.util.List;

import javax.jcr.Node;
import javax.jcr.PathNotFoundException;
import javax.jcr.RepositoryException;
import javax.jcr.Session;

import org.apache.log4j.Logger;
import org.springframework.beans.factory.annotation.Required;
import org.springmodules.jcr.JcrCallback;
import org.springmodules.jcr.JcrTemplate;

public class ContentLoader2 {

  private static final Logger LOGGER = Logger.getLogger(ContentLoader2.class);
  
  private FileFinder fileFinder;
  private String sourceDirectory;
  private String contentSource;
  private IParser parser;
  private JcrTemplate jcrTemplate;
  
  @Required
  public void setFileFinder(FileFinder fileFinder) {
    this.fileFinder = fileFinder;
  }

  @Required
  public void setJcrTemplate(JcrTemplate jcrTemplate) {
    this.jcrTemplate = jcrTemplate;
  }

  @Required
  public void setContentSource(String contentSource) {
    this.contentSource = contentSource;
  }

  @Required
  public void setParser(IParser parser) {
    this.parser = parser;
  }

  @Required
  public void setSourceDirectory(String sourceDirectory) {
    this.sourceDirectory = sourceDirectory;
  }

  public void load() throws Exception {
    jcrTemplate.execute(new JcrCallback() {
      public Object doInJcr(Session session) throws IOException, RepositoryException {
        try {
          Node contentSourceNode = getFreshContentSourceNode(session, contentSource);
          List<File> filesFound = fileFinder.find(sourceDirectory);
          for (File fileFound : filesFound) {
            DataHolder dataHolder = parser.parse(fileFound);
            if (dataHolder == null) {
              continue;
            }
            LOGGER.info("Parsing file:" + fileFound);
            Node contentNode = contentSourceNode.addNode("content");
            for (String propertyKey : dataHolder.getPropertyKeys()) {
              String value = dataHolder.getProperty(propertyKey);
              LOGGER.debug("Setting property " + propertyKey + "=" + value);
              contentNode.setProperty(propertyKey, value);
            }
            session.save();
          }
        } catch (Exception e) {
          throw new IOException("Exception parsing and storing file", e);
        }
      }
    });
  }

  /**
   * Our policy is to do a fresh load each time, so we want to remove the contentSource
   * node from our repository first, then create a new one.
   * @param session the Repository Session.
   * @param contentSourceName the name of the content source.
   * @return a content source node. This is a top level element of the repository,
   * right under the repository root node.
   * @throws Exception if one is thrown.
   */
  private Node getFreshContentSourceNode(Session session, String contentSourceName) throws Exception {
    Node root = session.getRootNode();
    Node contentSourceNode = null;
    try {
      contentSourceNode = root.getNode(contentSourceName);
      if (contentSourceNode != null) {
        contentSourceNode.remove();
      }
    } catch (PathNotFoundException e) {
      LOGGER.info("Path for content source: " + contentSourceName + " not found, creating");
    }
    contentSourceNode = root.addNode(contentSourceName);
    return contentSourceNode;
  }
}

The ContentRetriever2.java, like the ContentLoader2.java, is a version of the original ContentRetriever.java file that works with Jackrabbit using the springmodules-jcr API:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
package com.mycompany.myapp;

import java.io.IOException;
import java.util.ArrayList;
import java.util.List;

import javax.jcr.Node;
import javax.jcr.NodeIterator;
import javax.jcr.Property;
import javax.jcr.PropertyIterator;
import javax.jcr.RepositoryException;
import javax.jcr.Session;
import javax.jcr.query.Query;
import javax.jcr.query.QueryManager;
import javax.jcr.query.QueryResult;

import org.springmodules.jcr.JcrCallback;
import org.springmodules.jcr.JcrTemplate;

public class ContentRetriever2 {

  private JcrTemplate jcrTemplate;

  public void setJcrTemplate(JcrTemplate jcrTemplate) {
    this.jcrTemplate = jcrTemplate;
  }

  @SuppressWarnings("unchecked")
  public List<DataHolder> findAllByContentSource(final String contentSource) {
    return (List<DataHolder>) jcrTemplate.execute(new JcrCallback() {
      public Object doInJcr(Session session) throws IOException, RepositoryException {
        List<DataHolder> contents = new ArrayList<DataHolder>();
        Node contentSourceNode = session.getRootNode().getNode(contentSource);
        NodeIterator ni = contentSourceNode.getNodes();
        while (ni.hasNext()) {
          Node contentNode = ni.nextNode();
          String contentId = contentNode.getProperty("contentId").getValue().getString();
          contents.add(getContent(contentSource, contentId));
        }
        return contents;
      }
    });
  }
  
  public DataHolder getContent(final String contentSource, final String contentId) {
    return (DataHolder) jcrTemplate.execute(new JcrCallback() {
      public Object doInJcr(Session session) throws IOException, RepositoryException {
        DataHolder dataHolder = new DataHolder();
        QueryManager queryManager = session.getWorkspace().getQueryManager();
        Query query = queryManager.createQuery("//" + contentSource + 
          "/content[@contentId='" + contentId + "']", Query.XPATH);
        QueryResult result = query.execute();
        NodeIterator ni = result.getNodes();
        while (ni.hasNext()) {
          Node contentNode = ni.nextNode();
          PropertyIterator pi = contentNode.getProperties();
          dataHolder.setProperty("contentSource", contentSource);
          while (pi.hasNext()) {
            Property prop = pi.nextProperty();
            dataHolder.setProperty(prop.getName(), prop.getValue().getString());  
          }
          break;
        }
        return dataHolder;
      }
    });
  }
  
  public DataHolder getContentByUrl(final String contentSource, final String url) {
    return (DataHolder) jcrTemplate.execute(new JcrCallback() {
      public Object doInJcr(Session session) throws IOException, RepositoryException {
        DataHolder dataHolder = null;
        QueryManager queryManager = session.getWorkspace().getQueryManager();
        Query query = queryManager.createQuery("//" + contentSource + 
          "/content[@cfUrl='" + url + "']", Query.XPATH);
        QueryResult result = query.execute();
        NodeIterator ni = result.getNodes();
        while (ni.hasNext()) {
          Node contentNode = ni.nextNode();
          String contentId = contentNode.getProperty("contentId").getValue().getString();
          dataHolder = getContent(contentSource, contentId);
          break;
        }
        return dataHolder;
      }
    });
  }
}

If you compared the code above to my older post, there is not much difference. The old code is now encapsulated inside of a JcrCallback anonymous inner class implementation, which is called from a JcrTemplate.execute() method. The other thing that has changed is that I no longer build my JCR Repository and Session objects in my code anymore. Also there is no Repository.login() calls in my code, because Spring already logged me in. However, one of the most important differences is the absence of checked Exceptions being thrown from the code. JcrTemplate converts the checked RepositoryException and IOException raised from the calls to JCR code into unchecked ones.

There is obviously a lot about Jackrabbit, JCR and springmodules-jcr that I don't know yet. From my limited knowledge, it looks like a product with lots of promise, even though I don't think its useful to me right now. I plan to keep looking some more, and over the next few weeks, write about the features I think will be useful to me if I ever end up setting up one in a real environment.