Friday, October 01, 2010

Grails and JackRabbit

Here's a brief overview of I plugged JackRabbit, a fully conforming implementation of the Java Content Repository specifications, into several of the Grails based projects I've been working on recently.

Currently, I'm using JackRabbit for user editable page content. Perhaps overkill, but I have plans to leverage additional JackRabbit features down the road.

First off, there is a Grails JackRabbit plugin, but it looked rather old and un-maintained and had no real documentation, so I just rolled my own solution.

Ok, so first, drop the jackrabbit jars into your $PROJ/lib/ folder.
(~/src/ilocker) ls -1 lib/
jackrabbit-api-2.1.1.jar
jackrabbit-core-2.1.1.jar
jackrabbit-jcr-commons-2.1.1.jar
jackrabbit-jcr-server-2.1.1.jar
jackrabbit-spi-2.1.1.jar
jackrabbit-spi-commons-2.1.1.jar
jcr-2.0.jar
An improved approach would be to add the appropriate directives to grails-app/conf/BuildConfig.groovy. But for now, this will work.

Next you'll need an appropriately configured JackRabbit repository.xml file. I configured JackRabbit with a Postgresql DbDataStore. A sample of my configuration can be found here.

So how to get started? I created a grails-app/service/ContentService.groovy, that starts out like this:
import org.springframework.beans.factory.InitializingBean;
import javax.jcr.Repository;
import javax.jcr.Session;
import javax.jcr.SimpleCredentials;
import javax.jcr.Node;
import org.apache.jackrabbit.core.TransientRepository;

class ContentService implements InitializingBean
{
static scope = "singleton";
def grailsApplication;
Repository _repository;

public void afterPropertiesSet() {
def jcr = grailsApplication.config.jcr;
_repository = new TransientRepository(jcr.repo.config, jcr.repo.home);
log.info "Configuring Content Service ... config=${jcr.repo.config}, home=${jcr.repo.home}";
}
My grails-app/conf/Config.groovy file has the following entries:
jcr.repo.home = "/var/lib/ilocker"
jcr.repo.config = "/etc/ilocker/repository.xml"
So the line
_repository = new TransientRepository(jcr.repo.config, jcr.repo.home);
above wires everything up to use /etc/ilocker/repository.xml and to set ${rep.home} = /var/lib/ilocker. Make sure the tomcat user has appropriate access to /var/lib/ilocker when you put the site into production!

Getting JackRabbit to work first time around can be a little dicey, because JackRabbit will copy the repository.xml to ${rep.home}/workspaces. If anything is misconfigured, it's easiest to just change repository.xml, delete ${rep.home}/workspaces, and try again. If you don't delete ${rep.home}/workspaces, your changes to repository.xml will have no effect (unless you create a new workspace). Take note!

Now to write content to our ContentService, I'm using:
  public void put(String controller, String action, String data) {
Session session = _repository.login(new SimpleCredentials("username", "password".toCharArray()));

log.info "ContentService.put ${controller} ${action}";

try {
Node controllerNode = getControllerNode(session, controller);
Node node = getActionNode(controllerNode, action);
Calendar lastModified = Calendar.getInstance();

node.setProperty("jcr:lastModified", lastModified);
node.setProperty("jcr:mimeType", "text/html");
node.setProperty("jcr:encoding", "utf-8");
node.setProperty("jcr:data", data);

session.save();
}
finally {
session.logout();
}
}
Obviously, completely ignoring JackRabbit level security. To read content in my controllers, I write code like this for example:
class AdminController {

def contentService;

def index = {
String content = contentService.get(controllerName, actionName);
[ chtml: content ]
}
And then in ContentService.groovy I have:
  public String get(String controller, String action) {
Session session = _repository.login(new SimpleCredentials("username", "password".toCharArray()));
String value;

log.info "ContentService.get ${controller} ${action}";

try {
Node controllerNode = getControllerNode(session, controller);
Node actionNode = getActionNode(controllerNode, action);
if (actionNode.hasProperty("jcr:data")) {
value = actionNode.getProperty("jcr:data").getString();
}
}
finally {
session.logout();
}

return value;
}

private Node getControllerNode(Session session, String controller) {
Node root = session.getRootNode();
if (root.hasNode(controller))
return root.getNode(controller);

Node node = root.addNode(controller, "nt:folder");
return node;
}

private Node getActionNode(Node parent, String action) {

if (parent.hasNode(action)) {
Node actionNode = parent.getNode(action)
return actionNode.getNode("jcr:content");
}

Node actionNode = parent.addNode(action, "nt:file");
Node content = actionNode.addNode("jcr:content", "nt:resource");
return content;
}
Again punting on JackRabbit level security. To preload my sites with default content, I wrote a simple groovy program to load the repository. I put jackrabbit-standalone-2.1.1.jar into $HOME/.groovy/lib/ then wrote a simple script, the heart of which is
    _repository = new TransientRepository("/etc/ilocker/repository.xml", "/var/lib/ilocker/");

Session session = _repository.login(
new SimpleCredentials("username", "password".toCharArray()));

try {

File input = new File(args[0]);
input.eachLine
{ line ->
List words = line.tokenize('\t');
println "Processing " + words[0] + "." + words[1];

Node home = getHomeNode(session, words[0]);
Node content = getContentNode(home, words[1]);

// store std. attributes
Calendar lastModified = Calendar.getInstance();
content.setProperty("jcr:lastModified", lastModified);
content.setProperty("jcr:mimeType", "text/html");
content.setProperty("jcr:encoding", "utf-8");

// store extended attributes
content.addMixin("mix:title");
content.setProperty("jcr:title", words[3]);

// store content
File data;
if (words[2].startsWith("/")) data = new File(words[2]);
else data = new File(scriptPath, words[2]);

String jcrData = data.getText();
content.setProperty("jcr:data", jcrData);

session.save();
}
}
finally {
session.logout();
}
}
. The full script can be found here.

Well I hope you found this a useful overview of integrating JackRabbit into a Grails application. The only trouble I've had in production with the above setup is when I had:
    <SearchIndex class="org.apache.jackrabbit.core.query.lucene.SearchIndex">
<param name="path" value="${rep.home}/repository/index"/>
<param name="supportHighlighting" value="true"/>
</SearchIndex>
In my repository.xml. Then I would get periodic repository locking errors when Lucene indexing kicked in. Since I'm not doing any JCR searching, I just deleted all Lucene search index nodes from my repository.xml.

This work was done for OpenArc, a Pittsburgh-based open source consulting firm with clients in Pittsburgh, Chicago, and D.C.

3 comments:

Codezilla said...

Dude, you *so* rock

Anonymous said...

i used mysql as DBMS but couldn't access JR Repo from grails using the tutorial, its throwing following exception:


2011-05-19 09:44:39,415 [http-8082-1] ERROR bundle.BundleFsPersistenceManager - failed to read bundle: deadbeef-face-babe-cafe-babecafebabe: java.lang.IllegalArgumentException: Invalid namespace index: 6086317
2011-05-19 09:44:39,415 [http-8082-1] ERROR core.RepositoryImpl - failed to start Repository: org.apache.jackrabbit.core.state.ItemStateException: failed to read bundle: deadbeef-face-babe-cafe-babecafebabe: java.lang.IllegalArgumentException: Invalid namespace index: 6086317
javax.jcr.RepositoryException: org.apache.jackrabbit.core.state.ItemStateException: failed to read bundle: deadbeef-face-babe-cafe-babecafebabe: java.lang.IllegalArgumentException: Invalid namespace index: 6086317
at org.apache.jackrabbit.core.version.InternalVersionManagerImpl.(InternalVersionManagerImpl.java:258)
at org.apache.jackrabbit.core.RepositoryImpl.createVersionManager(RepositoryImpl.java:512)
at org.apache.jackrabbit.core.RepositoryImpl.(RepositoryImpl.java:355)
at org.apache.jackrabbit.core.RepositoryImpl.create(RepositoryImpl.java:673)
at org.apache.jackrabbit.core.TransientRepository$2.getRepository(TransientRepository.java:231)
at org.apache.jackrabbit.core.TransientRepository.startRepository(TransientRepository.java:279)
at org.apache.jackrabbit.core.TransientRepository.login(TransientRepository.java:375)
at org.apache.jackrabbit.commons.AbstractRepository.login(AbstractRepository.java:123)
at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:39)
at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:25)
at java.lang.reflect.Method.invoke(Method.java:597)

Jeltok said...

Thanks for the info