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/An improved approach would be to add the appropriate directives to grails-app/conf/BuildConfig.groovy. But for now, this will work.
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
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;My grails-app/conf/Config.groovy file has the following entries:
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}";
}
jcr.repo.home = "/var/lib/ilocker"So the line
jcr.repo.config = "/etc/ilocker/repository.xml"
_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) {Obviously, completely ignoring JackRabbit level security. To read content in my controllers, I write code like this for example:
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();
}
}
class AdminController {And then in ContentService.groovy I have:
def contentService;
def index = {
String content = contentService.get(controllerName, actionName);
[ chtml: content ]
}
public String get(String controller, String action) {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
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;
}
_repository = new TransientRepository("/etc/ilocker/repository.xml", "/var/lib/ilocker/");. The full script can be found here.
Session session = _repository.login(
new SimpleCredentials("username", "password".toCharArray()));
try {
File input = new File(args[0]);
input.eachLine
{ line ->
Listwords = 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();
}
}
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">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.
<param name="path" value="${rep.home}/repository/index"/>
<param name="supportHighlighting" value="true"/>
</SearchIndex>
This work was done for OpenArc, a Pittsburgh-based open source consulting firm with clients in Pittsburgh, Chicago, and D.C.