Friday, October 08, 2010

Grails and JCifs

JCIFS is:
...an Open Source client library that implements the CIFS/SMB networking protocol in 100% Java. CIFS is the standard file sharing protocol on the Microsoft Windows platform.
As part of a project to provide schools and businesses with an open source solution to access their "My Documents" folder anytime/anywhere over the web, I recently had the pleasure of integrating JCIFS into my Grails application.

The obligatory screenshot:


I dropped the latest JCIFS jar file into my $GRAILS-APP/lib folder, and began implementing the "My Documents" feature against a samba server for starters. When I moved to a Windows 2008 server everything fell apart, with all operations started timing out. After some digging around in the rather extensive set of config options, I realized I need the following in my grails config file:
System.setProperty("jcifs.smb.client.dfs.disabled", "true");
Your environment may differ but make sure you take a good look at the JCIFS configuration options at least.

Ok, so here's a simple example of removing a file:
  void removeFile(WorkspacePath p)
{
def ntlm = new NtlmPasswordAuthentication("", p.username, p.password);
SmbFile file = new SmbFile(absoluteFilePath(p.url, p.path), ntlm);
file.delete();
}
Note: I pass "" as the first argument to NtlmPasswordAuthentication as the domain is part of p.username (e.g. joel@example.com).

One thing you need to make sure of is always ending directory paths with a "/", otherwise you will get errors. Here's a more complicated example of a "eachFile" method that takes a closure as it's final argument:
  public void eachFile(WorkspacePath p, Closure c)
{
println "eachFile ${p.url} - ${p.path}";
def path = absoluteDirPath(p.url, p.path);
def ntlm = new NtlmPasswordAuthentication("", p.username, p.password);
SmbFile file = new SmbFile(path, ntlm);

// are we dealing with a directory path or just a single file?
if (!file.isDirectory()) {
c.call([name: file.name, file: file, path: file.canonicalPath,
inputStream: { return new SmbFileInputStream(file); },
outputStream: { return new SmbFileOutputStream(file); }
]);
return;
}

file.listFiles().each {
f-> if (f.isDirectory()) return;
if (f.isHidden()) return;

c.call([name: f.name, file: f, path: f.canonicalPath,
inputStream: { return new SmbFileInputStream(f); },
outputStream: { return new SmbFileOutputStream(f); }
]);
}
}
We've been quite pleased with JCIFS and it well its been working in our grails application. We are currently using 1.3.14 with the patches noted here. I just noticed that 1.3.15 is out so I'm interested in trying that as soon as possible!

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.