Friday, September 26, 2008

Adding JCR support to your existing web application

In addition to the two recent technology posts (1, 2) that contained a product and technology overview about JCR, this post is guiding you through the practical steps in order to add JCR support to your web applications.

A new alternative to manage your application data is to store it in a content repository. The approach brings some advantages if compared to the most widespread persistence mechanism in use currently, the relational databases. The main advantage is the ability to have unstructured data. You can store your data first, and then define how it’s going to be organized and how the pieces of persistent information relate to each other. Besides the fact that not enforcing data integrity constraints, when they are not necessary, certainly helps in your application performance. This feature is especially useful when it’s necessary to enhance the data model. Relational databases are known for not having a very flexible structure. On the other hand, the JCR API was designed to target this need.
Suppose that you got a request to have a modification date added to some of your entities. In the relational model you’ll have to find out which tables should get the new field, and maybe provide an initial value for the column. Using JCR, you can simply have this modification date available only in the newly created entities. There is no need to concern about the existing data, because with JCR you can add and remove attributes on-the-fly. There is also no need to change any structure information, because it’s not required to declare any structure at all. If you would like to add a JCR repository to your existing web application, here is a walkthrough covering all the required steps.
The presented scenario assumes a Maven 2 web application, to be deployed in a Tomcat 5.5 server.
If you don’t have a web application to start with, or if you prefer to use a test project first, I suggest you to use Apache Maven to create a simple web project. To do so, run the following command in an empty directory:


mvn archetype:create -DarchetypeGroupId=org.apache.maven.archetypes \
-DarchetypeArtifactId=maven-archetype-webapp \
-DgroupId=com.ctp.jcr.sample -DartifactId=webapp -Dversion=0.1 \
-Dpackage=com.ctp.jcr.sample.webapp


The “archetype:create” Maven goal will create an empty web project called “webapp”. Let’s import this project into Eclipse, so it will be easier to change it. Change to the project directory and run the following command to generate the Eclipse specific project configuration files:


mvn eclipse:eclipse -Dwtpversion=1.5


The project is ready to be imported by Eclipse (File > Import > Existing projects into workspace; no Maven for Eclipse plugin is required, just an environment variable needs to be configurated).
Another approach to create this project is to use a Maven plug-in for Eclipse called m2eclipse. After installing it, you can create a Maven project by opening the menu “File > New > Other…” and choosing “Maven Project”. In the “New Maven Project” wizard, click “Next”, Choose the “Internal” catalog and in the archetypes list, choose “maven-archetype-webapp”. Click “Next” and provide the group ID “com.ctp.jcr.sample” and the artifact ID “webapp”. To complete the project creation, click “Finish”.
The next step is to add the proper dependencies to the project’s POM. So change it to match the following dependencies:


<dependency>
<groupId>javax.jcr</groupId>
<artifactId>jcr</artifactId>
<version>1.0</version>
</dependency>
<dependency>
<groupId>org.apache.jackrabbit</groupId>
<artifactId>jackrabbit-core</artifactId>
<version>1.4.5</version>
<scope></scope>
</dependency>
<dependency>
<groupId>org.apache.jackrabbit</groupId>
<artifactId>jackrabbit-jcr-rmi</artifactId>
<version>1.4</version>
</dependency>
<dependency>
<groupId>org.apache.jackrabbit</groupId>
<artifactId>jackrabbit-jcr-server</artifactId>
<version>1.4</version>
</dependency>
<dependency>
<groupId>org.apache.jackrabbit</groupId>
<artifactId>jackrabbit-webapp</artifactId>
<version>1.4</version>
</dependency>
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-api</artifactId>
<version>1.3.0</version>
</dependency>
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-log4j12</artifactId>
<version>1.3.0</version>
</dependency>
<dependency>
<groupId>commons-collections</groupId>
<artifactId>commons-collections</artifactId>
<version>3.2.1</version>
</dependency>
<dependency>
<groupId>xerces</groupId>
<artifactId>xercesImpl</artifactId>
<version>2.8.1</version>
</dependency>
<dependency>
<groupId>javax.servlet</groupId>
<artifactId>servlet-api</artifactId>
<version>2.4</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>javax.servlet</groupId>
<artifactId>jsp-api</artifactId>
<version>2.0</version>
<scope>provided</scope>
</dependency>


After changing the POM file, run the “eclipse:eclipse” goal again, and refresh your Eclipse project. As the JCR API has a “provided” scope, the JAR file which can be found in your local Maven repository (~/.m2/repository/javax/jcr/jcr/1.0/jcr-1.0.jar) should be copied to the shared/lib Tomcat folder.
Using a context descriptor file (create the file webapp/src/main/webapp/META-INF/context.xml) a shared resource, the JCR repository, will be defined. This resource receives essentially two important parameters: “configFilePath”, which is the absolute path to the repository descriptor (the “repository.xml” file, to be explained in the next steps) and “repHomeDir”, the absolute path to the directory which will contain the repository data files.


<Context>
<Resource name="jcr/repository"
auth="Container"
type="javax.jcr.Repository"
factory="org.apache.jackrabbit.core.jndi.BindableRepositoryFactory"
configFilePath="c:/TEMP/webapp/repository/repository.xml"
repHomeDir="c:/TEMP/webapp/repository" />
</Context>


This shared resource needs to be declared in the web.xml file:


<web-app xmlns="http://java.sun.com/xml/ns/j2ee"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://java.sun.com/xml/ns/j2ee http://java.sun.com/xml/ns/j2ee/web-app_2_4.xsd"
version="2.4">
<display-name>Archetype Created Web Application</display-name>
<resource-ref>
<description>JCR Repository</description>
<res-ref-name>jcr/repository</res-ref-name>
<res-type>javax.jcr.Repository</res-type>
<res-auth>Container</res-auth>
</resource-ref>
</web-app>


Don’t forget to create the repository descriptor file, in the same path as specified in the context descriptor. If you don’t have your own descriptor, or if you don’t want to concern about it right now, simply copy and paste this one:


<?xml version="1.0"?>
<!DOCTYPE Repository PUBLIC "-//The Apache Software Foundation//DTD Jackrabbit 1.4//EN"
"http://jackrabbit.apache.org/dtd/repository-1.4.dtd">
<Repository>
<FileSystem class="org.apache.jackrabbit.core.fs.local.LocalFileSystem">
<param name="path" value="${rep.home}/repository" />
</FileSystem>
<Security appName="Jackrabbit">
<AccessManager class="org.apache.jackrabbit.core.security.SimpleAccessManager" />
<LoginModule class="org.apache.jackrabbit.core.security.SimpleLoginModule" />
</Security>
<Workspaces rootPath="${rep.home}/workspaces" defaultWorkspace="default" />
<Workspace name="${wsp.name}">
<FileSystem class="org.apache.jackrabbit.core.fs.local.LocalFileSystem">
<param name="path" value="${wsp.home}" />
</FileSystem>
<PersistenceManager
class="org.apache.jackrabbit.core.state.xml.XMLPersistenceManager" />
<SearchIndex class="org.apache.jackrabbit.core.query.lucene.SearchIndex">
<param name="path" value="${wsp.home}/index" />
</SearchIndex>
</Workspace>
<Versioning rootPath="${rep.home}/versions">
<FileSystem class="org.apache.jackrabbit.core.fs.local.LocalFileSystem">
<param name="path" value="${rep.home}/versions" />
</FileSystem>
<PersistenceManager
class="org.apache.jackrabbit.core.state.xml.XMLPersistenceManager" />
</Versioning>
</Repository>


This simplified repository descriptor doesn’t depend on any kind of specific storage system. You can enhance it to store your data in a relational database, for example, but in our example it will store everything as XML files under the repository home directory.
Now let’s create a session factory class. This class will locate a repository instance and authenticate to it, to get a javax.jcr.Session instance. Create the directory ./webapp/src/main/java, and create the following class on it:


package com.ctp.jcr.sample.webapp;

import javax.jcr.Repository;
import javax.jcr.RepositoryException;
import javax.jcr.Session;
import javax.jcr.SimpleCredentials;
import javax.naming.Context;
import javax.naming.InitialContext;
import javax.naming.NamingException;

public class SessionFactory {

public static Session getSession() throws RepositoryException, NamingException {
InitialContext ctx = new InitialContext();
Context env = (Context) ctx.lookup("java:comp/env");
Repository repo = (Repository) env.lookup("jcr/repository");
return repo.login(new SimpleCredentials("admin", "".toCharArray()));
}

}


With this setup, the JCR repository can be accessed through your web application. The following JSP shows how to use the service factory to list the repository contents:


<%@page import="javax.jcr.Node"%>
<%@page import="javax.jcr.Session"%>
<%@page import="com.ctp.jcr.sample.webapp.SessionFactory"%>
<%@page import="javax.jcr.NodeIterator"%>
<html>
<body>
<%!
private void printContents(Node n, JspWriter out, String padding) throws Exception {
out.println(padding + n.getPath() + "(" + n.getPrimaryNodeType().getName() + ")");
NodeIterator it = n.getNodes();
while (it.hasNext()) {
printContents(it.nextNode(), out, padding + " ");
}
}
%>
<%
Session jcrSession = SessionFactory.getSession();
Node root = jcrSession.getRootNode();
%>
<pre>
<%
printContents(root, out, "");
%>
</pre>
</body>
</html>


This is the result displayed by the page. Observe that even for an empty repository, the infrastructure nodes are displayed as well.

Optional step: exposing the repository through WebDAV



To access your repository through WebDAV, it’s necessary to declare a servlet, through which the repository will be exposed. Add the following servlet declarations to your web.xml file:


<servlet>
<description> This servlet provides other servlets and jsps a common way to
access the repository. The repository can be accessed via JNDI, RMI or
Webdav. </description>
<servlet-name>Repository</servlet-name>
<servlet-class>org.apache.jackrabbit.j2ee.RepositoryAccessServlet</servlet-class>
<init-param>
<description> Property file that hold the same initialization properties
than the init-params below. If a parameter is specified in both places
the one in the bootstrap-config wins. </description>
<param-name>bootstrap-config</param-name>
<param-value>WEB-INF/repository/bootstrap.properties</param-value>
</init-param>
<load-on-startup>3</load-on-startup>
</servlet>
<servlet>
<description> The webdav servlet that connects HTTP request to the
repository. </description>
<servlet-name>Webdav</servlet-name>
<servlet-class>org.apache.jackrabbit.j2ee.SimpleWebdavServlet</servlet-class>
<init-param>
<description> defines the prefix for spooling resources out of the
repository. </description>
<param-name>resource-path-prefix</param-name>
<param-value>/repository</param-value>
</init-param>
<init-param>
<description> Defines various dav-resource configuration parameters.
</description>
<param-name>resource-config</param-name>
<param-value>/WEB-INF/repository/config.xml</param-value>
</init-param>
<load-on-startup>4</load-on-startup>
</servlet>
<servlet>
<description> The webdav servlet that connects HTTP request to the
repository. </description>
<servlet-name>JCRWebdavServer</servlet-name>
<servlet-class>org.apache.jackrabbit.j2ee.JCRWebdavServerServlet</servlet-class>
<init-param>
<description> defines the prefix for spooling resources out of the
repository. </description>
<param-name>resource-path-prefix</param-name>
<param-value>/server</param-value>
</init-param>
<load-on-startup>5</load-on-startup>
</servlet>
<servlet>
<servlet-name>RMI</servlet-name>
<servlet-class>org.apache.jackrabbit.servlet.remote.RemoteBindingServlet</servlet-class>
</servlet>
<servlet>
<description>Downloads binary data from repository</description>
<display-name>Repository Download Servlet</display-name>
<servlet-name>RepositoryDownloadServlet</servlet-name>
<servlet-class>br.com.hapkidocontato.site.presentation.RepositoryDownloadServlet</servlet-class>
</servlet>
<servlet-mapping>
<servlet-name>Webdav</servlet-name>
<url-pattern>/repository/*</url-pattern>
</servlet-mapping>
<servlet-mapping>
<servlet-name>JCRWebdavServer</servlet-name>
<url-pattern>/server/*</url-pattern>
</servlet-mapping>
<servlet-mapping>
<servlet-name>RMI</servlet-name>
<url-pattern>/rmi</url-pattern>
</servlet-mapping>
<servlet-mapping>
<servlet-name>RepositoryDownloadServlet</servlet-name>
<url-pattern>/content/*</url-pattern>
</servlet-mapping>


Other two configurations file are referenced by those servlets. Create the directory webapp/src/main/webapp/WEB-INF/repository/ and create the file bootstrap.properties with the following values:


repository.name=java:comp/env/jcr/repository
jndi.enabled=true
jndi.name=jcr/repository


In the same directory, create the file config.xml:


<?xml version="1.0" encoding="UTF-8"?>
<config>
<iomanager>
<class name="org.apache.jackrabbit.server.io.IOManagerImpl" />
<iohandler>
<class name="org.apache.jackrabbit.server.io.VersionHandler" />
</iohandler>
<iohandler>
<class name="org.apache.jackrabbit.server.io.VersionHistoryHandler" />
</iohandler>
<iohandler>
<class name="org.apache.jackrabbit.server.io.ZipHandler" />
</iohandler>
<iohandler>
<class name="org.apache.jackrabbit.server.io.XmlHandler" />
</iohandler>
<iohandler>
<class name="org.apache.jackrabbit.server.io.DirListingExportHandler" />
</iohandler>
<iohandler>
<class name="org.apache.jackrabbit.server.io.DefaultHandler" />
</iohandler>
</iomanager>
<propertymanager>
<class name="org.apache.jackrabbit.server.io.PropertyManagerImpl" />
<propertyhandler>
<class name="org.apache.jackrabbit.server.io.VersionHandler" />
</propertyhandler>
<propertyhandler>
<class name="org.apache.jackrabbit.server.io.VersionHistoryHandler" />
</propertyhandler>
<propertyhandler>
<class name="org.apache.jackrabbit.server.io.ZipHandler" />
</propertyhandler>
<propertyhandler>
<class name="org.apache.jackrabbit.server.io.XmlHandler" />
</propertyhandler>
<propertyhandler>
<class name="org.apache.jackrabbit.server.io.DirListingExportHandler" />
</propertyhandler>
<propertyhandler>
<class name="org.apache.jackrabbit.server.io.DefaultHandler" />
</propertyhandler>
</propertymanager>
<noncollection>
<nodetypes>
<nodetype>nt:file</nodetype>
<nodetype>nt:resource</nodetype>
</nodetypes>
</noncollection>
<filter>
<class name="org.apache.jackrabbit.webdav.simple.DefaultItemFilter" />
<namespaces>
<prefix>rep</prefix>
<prefix>jcr</prefix>
</namespaces>
</filter>
</config>


And that’s all: the default workspace can be accessed through WebDAV in the following URL: http://localhost:8080/webapp/repository/default. On Windows, you can go to “My Network places” and “Add network place” to map the WebDAV location as a folder. When prompted by a username and password, just provide anything; the repository is configured to grant access to any user.
To find more about JCR, Jackrabbit and its related technologies, visit the dev.day.com weblog.

Thanks Douglas for this post! - [Tom & Balz]

1 comment:

Karthikaiselvan said...

Hey, can we provide node level authentication instead of repository level authentication?

Thanks in advance.