Spring in Practice
By By Willie Wheeler, John Wheeler, and Joshua White
Among the tasks a content management system (CMS) must support are the authoring, editing and deployment of content by non-technical users. Examples include articles (news, reviews), announcements, press releases, product descriptions and course materials. In this article, based on chapter 12 of Spring in Practice, the authors build an article repository using Jackrabbit, JCR and Spring Modules JCR.
Prerequisites
None. Previous experience with JCR and Jackrabbit would be helpful.
Key technologies
JCR 2.0 (JSR 283), Jackrabbit 2.x, Spring Modules JCR
Background
Our first order of business is to establish a place to store our content, so let’s start with that. In subsequent recipes we’ll build on top of this early foundation.
Problem
Build an article repository supporting article import and retrieval. Future plans are to support more advanced capabilities such as article authoring, versioning, and workflows involving fine-grained access control.
Solution
While it’s often fine to use files or databases for content storage, sometimes you must support advanced content- related operations such as fine-grained access control, author-based versioning, content observation (for example, “watches”), advanced querying, and locking. A content repository builds upon a persistent store by adding direct support for such operations.
We’ll use a JSR 283 content repository to store and deliver our articles. JSR 283, better known as the Java Content Repository (JCR) 2.0 specification1, defines a standard architecture and API for accessing content repositories. We’ll use the open source Apache Jackrabbit2.x JCR reference implementation at http://jackrabbit.apache.org/.
Do we really need JCR just to import and retrieve articles?
No. If all we need is the ability to import and deliver articles, JCR is overkill. We’re assuming for the sake of discussion, however, that you’re treating the minimal delivery capability we establish here as a basis upon which to build more advanced features. Given that assumption, it makes sense to build JCR in from the beginning as it’s
not especially difficult to do.
If you know that you don’t need anything advanced, you might consider using a traditional relational database backend or even a NoSQL document repository such as CouchDB or MongoDB. Either of those options is probably more straightforward than JCR.
For more information on JCR, please see the Jackrabbit website above or check out the JSR 283 home page at http://jcp.org/en/jsr/detail?id=283.
Java Content Repository basics
The JCR specification aims to provide a standard API for accessing content repositories. According to the JSR 283 home page:
A content repository is a high-level information management system that is a superset of traditional data repositories. A content repository implements content services such as: author based versioning, full textual searching, fine grained access control, content categorization and content event monitoring. It is these content services that differentiate a content repository from a Data Repository.
Architecturally, so-called content applications (such as a content authoring system, a CMS, and so on) involve the three layers shown figure 1.
Figure 1 JCR application architecture. Content apps make calls against the standardized JCR API, and repository vendors provide compliant implementations.
The uppermost layer contains the content applications themselves. These might be CMS apps that content developers use to create and manage content, or they might be content delivery apps that content consumers use.
This app layer interacts with the content repository2 (for example, Jackrabbit) through the JCR API, which offers some key benefits:
- The API specifies capabilities that repository vendors either must or should provide.
- It allows content apps to insulate themselves from implementation specifics by coding against a standard
JCR API instead of a proprietary repository-specific API.
Apps can, of course, take advantage of vendor-specific features, but, to the extent that apps limit such excursions, it will be easier to avoid vendor lock-in.
The content repository itself is organized as a tree of nodes. Each node can have any number of associated properties. We can represent individual articles and pages as nodes, for instance, and article and page metadata as properties.
That’s a quick JCR overview, but it describes the basic idea. Let’s do a quick overview of our article repository, and after that we’ll start on the code.
Article repository overview
At the highest level, we can distinguish article development (for example, authoring, version control, editing, packaging) from article delivery. Our focus in this recipe is article delivery and, specifically, the ability to import an “article package” (assets plus metadata) into a runtime repository and deliver it to readers. Obviously, there has to be a way to do the development too, but here we’ll assume that the author uses his favorite text editor, version control system, and ZIP tool.4 In other words, development is outside the scope of this writing.
See figure 2 for an overview of this simple article management architecture.
Figure 2 An article CMS architecture with the bare essentials. Our development environment has authoring, version control and a packager. Our runtime environment supports importing article packages (e.g., article content, assets and metadata) and delivering it to end users. In this recipe, JCR is our runtime article repository.
That’s our repository overview. Now it’s time for some specifics. As a first step, we’ll set up a Jackrabbit repository to serve as the foundation for our article delivery engine.
Set up the Jackrabbit content repository
If you’re already knowledgeable about Jackrabbit, feel free to configure it as you wish. Otherwise, Spring in Practice’s code download has a sample repository.xml Jackrabbit configuration file.
(It’s in the sample_conf folder.) Just create a fresh directory somewhere on your filesystem and drop the repository.xml configuration file right in there. You shouldn’t need to change anything in the configuration if you’re just trying to get something quick and dirty to work.
There isn’t anything we need to start up. Eventually we will point the app at the directory you just created. Our app, on startup, will create an embedded Jackrabbit instance against your directory.
To model our articles we’re going to need a couple of domain objects: articles and pages. That’s the topic of our next discussion.
Build the domain objects
Our articles include metadata and pages. The listing below shows an abbreviated version of our basic article domain object covering the key parts; please see the code download for the full class.
Listing 1 Article.java, a simple domain object for articles
package com.springinpractice.ch12.model; import java.util.ArrayList; import java.util.Date; import java.util.List; public class Article { private String id; private String title; private String author; private Date publishDate; private String description; private String keywords; private List pages = new ArrayList(); public String getId() { return id; } public void setId(String id) { this.id = id; } public String getTitle() { return title; } public void setTitle(String title) { this.title = title; } ... other getters and setters ...
There shouldn’t be anything too surprising about the article above. We don’t need any annotations for right now. It’s just a pure POJO.
We’re going to need a page domain object as well. It’s even simpler as we see in the listing below
Listing 2 Page.java, a page domain object
package com.springinpractice.ch12.model; public class Page { private String content; public String getContent() { return content; } public void setContent(String content) { this.content = content; } }
It would probably be a nice to add a title to our page domain object, but this is good enough for our current purpose.
Next, we want to look at the data access layer, which provides a domain-friendly API into the repository.
Build the data access layer
Even though we’re using Jackrabbit instead of using the Hibernate backend from other chapters, we can still use the Dao abstraction we’ve been using. Figure 3 is a class diagram for our DAO interfaces and class.
Our Hibernate DAOs had an AbstractHbnDao to factor some of the code common to all Hibernate-backed DAOs. In the current case, we haven’t created the analogous AbstractJcrDao because we have only a single JCR DAO. If we had more, however, it would make sense to do the same thing.
We’re going to want a couple of extra operations on our ArticleDao, as the listing below shows.
Listing 3 ArticleDao.java, a data access object interface for articles
package com.springinpractice.ch12.dao; import com.springinpractice.ch12.model.Article; import com.springinpractice.dao.Dao; public interface ArticleDao extends Dao{ void createOrUpdate(Article article); #1 Article getPage(String articleId, int pageNumber); #2 }
#1 Saves using a known ID
#2 Gets article with page hydrated
Our articles have preset IDs (as opposed to being autogenerated following a save(, so our createOrUpdate() method (#1) makes it convenient to save an article using a known article ID. The getPage() method (#2) supports displaying a single page (1-indexed). It returns an article with the page in question eagerly loaded so we can display it. The other pages have placeholder objects just to ensure that the page count is correct.
The following listing provides our JCR-based implementation of the ArticleDao.
Listing 4 JcrArticleDao.java, a JCR-based DAO implementation
package com.springinpractice.ch12.dao.jcr; import static org.springframework.util.Assert.notNull; import java.io.IOException; import java.io.Serializable; import java.util.ArrayList; import java.util.List; import javax.inject.Inject; import javax.jcr.Node; import javax.jcr.NodeIterator; import javax.jcr.PathNotFoundException; import javax.jcr.RepositoryException; import javax.jcr.Session; import org.springframework.dao.DataIntegrityViolationException; import org.springframework.dao.DataRetrievalFailureException; import org.springframework.stereotype.Repository; import org.springframework.transaction.annotation.Transactional; import org.springmodules.jcr.JcrCallback; import org.springmodules.jcr.SessionFactory; import org.springmodules.jcr.support.JcrDaoSupport; import com.springinpractice.ch12.dao.ArticleDao; import com.springinpractice.ch12.model.Article; import com.springinpractice.ch12.model.Page; @Repository @Transactional(readOnly = true) public class JcrArticleDao extends JcrDaoSupport implements ArticleDao { #1 @Inject private ArticleMapper articleMapper; #2 @Inject public void setSessionFactory(SessionFactory sessionFactory) { #3 super.setSessionFactory(sessionFactory); } @Transactional(readOnly = false) public void create(final Article article) { #4 notNull(article); getTemplate().execute(new JcrCallback() { #5 public Object doInJcr(Session session) throws IOException, RepositoryException { if (exists(article.getId())) { throw new DataIntegrityViolationException( “Article already exists”); #6 } articleMapper.addArticleNode(article, getArticlesNode(session)); session.save(); return null; } }, true); } ... various other DAO methods ... private String getArticlesNodeName() { return “articles”; } private String getArticlesPath() { return “/” + getArticlesNodeName(); } private String getArticlePath(String articleId) { return getArticlesPath() + “/” + articleId; } private Node getArticlesNode(Session session) throws RepositoryException { try { return session.getNode(getArticlesPath()); } catch (PathNotFoundException e) { return session.getRootNode().addNode(getArticlesNodeName()); } } }
#1 Class definition
#2 Map between articles and nodes
#3 Creates JCR sessions
#4 Writes method
#5 Using JcrTemplate
#6 Throws DataAccessException
The JcrArticleDao class illustrates some ways in which we can use Spring to augment JCR. The first part is our high-level class definition (#1). We implement the ArticleDao interface from listing 3, and also extend JcrDaoSupport, which is part of Spring Modules JCR. JcrDaoSupport gives us access to JCR Sessions, a JcrTemplate, and a convertJcrAccessException(RepositoryException) method that converts JCR RepositoryExceptions to exceptions in the Spring DataAccessException hierarchy. We also declare the @Repository annotation to support component scanning and the @Transactional annotation to support transactions.
Transactions on the DAO?
It might surprise you that we’re annotating a DAO with @Transactional. After all, we usually define transactions on service beans since any given service method might make multiple DAO calls that need to happen within the scope of a single atomic transaction.
However, we’re not going to have service beans—we’re going to wire our ArticleDao right into the controller itself. The reason is that our service methods would simply pass-through to ArticleDao and, in that sort of situation, there’s really no benefit to going through the ceremony of defining an explicit service layer. If we were to extend our simple app to something with real service methods (as opposed to data access methods), we’d build a transactional service layer.
At #2, we inject an ArticleMapper, which is a custom class that converts back and forth between Articles and JCR Nodes. We’ll see that in listing 5 below.
We override JcrDaoSupport.setSessionFactory() at (#3). We do this just to make the property injectable through the component scanning mechanism, since JcrDaoSupport doesn’t itself support that.
Our create() method (#4) is one of our CRUD methods. We’ve suppressed the other ones since we’re more interested in covering Spring than the details of using JCR, but the code download has the other methods. We’ve annotated it with @Transactional(readOnly = false) to override the class-level @Transactional(readOnly = true) annotation. See the code download for the rest of the methods.
We’ve chosen to implement our DAO methods using the template method pattern common throughout Spring (JpaTemplate, HibernateTemplate, JdbcTemplate, RestTemplate, and so on). In this case, we’re using the Spring Modules JCR JcrTemplate (via JcrDaoSupport.getTemplate()) and its corresponding JcrCallback interface (#5). This template is helpful because it automatically handles concerns such as opening and closing JCR sessions, managing the relationship between sessions and transactions, and translating RepositoryExceptions and IOExceptions into the Spring DataAccessException hierarchy.
Finally, to maintain consistency with JcrDaoSupport’s exception translation mechanism, we throw a DataIntegrityViolationException (#6) (part of the aforementioned DataAccessException hierarchy) in the event of a duplicate article.
We’ve mentioned Spring Modules JCR a few times here. Let’s talk about that briefly.
A word about Spring Modules JCR
Spring Modules is a now-defunct project that includes several useful Spring-style libraries for integrating with various not-quite-core APIs and codebases, including Ehcache, OScache, Lucene, and JCR (among several others). Unfortunately, some promising attempts to revive Spring Modules, either in whole or in part, appear to have stalled.
It’s unclear whether Spring will ever directly support JCR, but there’s a lot of good Spring/JCR code in the Spring Modules project, and I wanted to use it for this writing. To that end I (Willie) forked an existing Spring Modules JCR effort on GitHub to serve as a stable-ish basis for Spring in Practice’s code. I’ve made some minor enhancements (mostly around cleaning up the POM and elaborating support for namespace-based configuration) to make Spring/JCR integration easier. Note, however, that I don’t have any plans around building this fork out beyond our present needs.
The reality is that integrating Spring and JCR currently requires a bit of extra effort because there isn’t an established project for doing that.
In our discussion of the JcrArticleDao, we mentioned an ArticleMapper component to convert between articles and JCR nodes. The listing below presents the ArticleMapper.
Listing 5 ArticleMapper.java converts between articles and JCR nodes
package com.springinpractice.ch12.dao.jcr; import java.util.Calendar; import java.util.Date; import javax.jcr.Node; import javax.jcr.RepositoryException; import org.springframework.stereotype.Component; import com.springinpractice.ch12.model.Article; import com.springinpractice.ch12.model.Page; @Component public class ArticleMapper { public Article toArticle(Node node) throws RepositoryException { #1 Article article = new Article(); article.setId(node.getName()); article.setTitle(node.getProperty(“title”).getString()); article.setAuthor(node.getProperty(“author”).getString()); if (node.hasProperty(“publishDate”)) { article.setPublishDate( node.getProperty(“publishDate”).getDate().getTime()); } if (node.hasProperty(“description”)) { article.setDescription(node.getProperty(“description”).getString()); } if (node.hasProperty(“keywords”)) { article.setKeywords(node.getProperty(“keywords”).getString()); } return article; } public Node addArticleNode(Article article, Node parent) #2 throws RepositoryException { Node node = parent.addNode(article.getId()); node.setProperty(“title”, article.getTitle()); node.setProperty(“author”, article.getAuthor()); Date publishDate = article.getPublishDate(); if (publishDate != null) { Calendar cal = Calendar.getInstance(); cal.setTime(publishDate); node.setProperty(“publishDate”, cal); } String description = article.getDescription(); if (description != null) { node.setProperty(“description”, description); } String keywords = article.getKeywords(); if (keywords != null) { node.setProperty(“keywords”, keywords); } Node pagesNode = node.addNode(“pages”, “nt:folder”); int numPages = article.getPages().size(); for (int i = 0; i < numPages; i++) { Page page = article.getPages().get(i); addPageNode(pagesNode, page, i + 1); } return node; } private void addPageNode(Node pagesNode, Page page, int pageNumber) #3 throws RepositoryException { Node pageNode = pagesNode.addNode(String.valueOf(pageNumber), “nt:file”); Node contentNode = pageNode.addNode(Node.JCR_CONTENT, “nt:resource”); contentNode.setProperty(“jcr:data”, page.getContent()); } }
#1 Maps Node to Article #2 Maps Article to Node #3 Maps Page to Node
Listing 5 is more concerned with mapping code rather than Spring techniques, but we’re including it here to give you a sense for what coding against JCR looks like just in case you’re unfamiliar with it. We use toArticle() (#1) to map a JCR Node to an Article. Then we have addArticleNode() (#2) and addPageNode() (#3) to convert Articles and Pages to Nodes, respectively. In the listing below, we bring everything together with our Spring configuration.
Listing 6 beans-jcr.xml, the Spring beans configuration for the JCR repo
xmlns:context=“http://www.springframework.org/schema/context” xmlns:jcr=“http://springmodules.dev.java.net/schema/jcr” #1 xmlns:jackrabbit=“http://springmodules.dev.java.net/schema/jcr/jackrabbit” #2 xmlns:p=“http://www.springframework.org/schema/p” xmlns:tx=“http://www.springframework.org/schema/tx” xmlns:xsi=“http://www.w3.org/2001/XMLSchema-instance” xsi:schemaLocation=“ http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-3.0.xsd http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context-3.0.xsd http://www.springframework.org/schema/tx http://www.springframework.org/schema/tx/spring-tx-3.0.xsd http://springmodules.dev.java.net/schema/jcr http://springmodules.dev.java.net/schema/jcr/springmodules-jcr.xsd http://springmodules.dev.java.net/schema/jcr/jackrabbit http://springmodules.dev.java.net/schema/jcr/springmodules-jackrabbit.xsd”> homeDir=“${repository.dir}” configuration=“${repository.conf}” /> #4 #5 repository=“repository” credentials=“credentials” /> #6 #7 #8 #9
#1 JCR namespace #2 Jackrabbit namespace #3 Repository configuration properties #4 Creates Jackrabbit repository #5 Repository credentials #6 JCR session factory #7 Scans for DAOs #8 Jackrabbit transaction manager #9 Activates transactions
As always, we begin by declaring the relevant namespaces and schema locations. In this case we need to declare (among others) the Spring Modules jcr (#1) and jackrabbit (#2) namespaces so we can use the custom namespace configuration they provide.
We need to pull in a couple of externalized properties so we can configure a Jackrabbit repository without resorting to hardcoding. We do that at #3. Our environment.properties file has only two properties:
repository.conf=file:/path/to/repository.xml repository.dir=file:/path/to/repository
(Of course, you’ll need to adjust the values according to your own environment.) Note that the property values here are Spring resources, and they don’t necessarily have to be file resources. We can, for instance, create classpath resources, network resources and so forth.
Now, we can create the Jackrabbit repository itself. We use the element to do that (#4), along with the repository.conf and repository.dir properties we grabbed from environment.properties. Behind the scenes, this reads the Jackrabbit repository.xml configuration file we mentioned earlier, and then builds a repository at whatever home directory we specify.
Our DAOs will need a way to get JCR sessions, and, for that, we need a Spring Modules SessionFactory. The SessionFactory gets sessions from the repository using credentials. We define the credentials at #5. For our repository.xml configuration, it’s actually fine to use dummy credentials, so that’s what we do. If you want to use real credentials just update repository.xml and beans-jcr.xml appropriately. Anyway, we pass the repository and credentials into the SessionFactory (#6), and then we component scan the ArticleDao (#7), which automatically injects the SessionFactory.
For transactions we define a Jackrabbit LocalTransactionManager (courtesy of Spring Modules) (#8) and use (#9) to activate declarative transaction management.
Figure 4 shows the bean dependency diagram for the configuration we just reviewed.
We now have a JCR-backed DAO for our article delivery engine.
Discussion
This recipe showed how to use Spring Modules JCR to integrate Spring and Jackrabbit, the JCR reference implementation. We followed the practice of defining a DAO interface and then implementing the DAO using a specific persistence technology.
Here are some other Manning titles you might be interested in:
Spring in Action, Third Edition
Craig Walls
Spring Batch in Action
Thierry Templier, Arnaud Cogoluegnes, Gary Gregory, and Olivier Bazoud
Spring Integration in Action
Mark Fisher, Jonas Partner, Marius Bogoevici, and Iwein Fuld