Monday, April 7, 2008

CampDepict: JRuby, CDK, and Camping

JRuby has a bright future, especially in areas where the immediacy of Ruby and the established APIs of Java meet. Rich Apodaca recently created a smiles to 2D structure tool using ruby, Rails, the CDK Java cheminformatics library, and a java-to-ruby bridge (part 1 and part 2). Here is the same project, created using JRuby, one of Rich's Java libraries, and Camping , a lightweight web framework.

1. Install the JDK

2. Install JRuby - I'm using 1.1 here. Did you remember to set JRUBY_HOME? Is jruby in your PATH?

$ jruby -v
ruby 1.8.6 (2008-03-28 rev 6360) [x86-jruby1.1]

3. Install Camping and dependencies into jruby.
$ jruby $JRUBY_HOME/bin/gem install camping --source http://code.whytheluckystiff.net/

Note: if you don't have c ruby installed, the following will work:
$ gem installed camping --source http://code.whytheluckystiff.net/

4. Download structure-cdk-0.1.2 from sourceforge, and save the two jars (cdk-20060714.jar and structure-cdk-0.1.2.jar) from the lib directory to $JRUBY_HOME/lib.

5. Create a depict directory, and a depict/static directory.

6. Create two 200x200 pngs called invalid.png, and blank.png. Store them in the static directory.

7. Save campdepict.rb to the depict directory (text at the end of this entry).

8. And run! (You can stop the server with a Ctrl-C).
$ jruby $JRUBY_HOME/bin/camping campdepict.rb

(sorry, had some image upload issues with Blogger ... no pretty picture).

'Splainin'

Camping deployments usually start as one file containing model, view and controller, but can be separated as needed (check that wiki). After some smiles validation by Index, the view passes the smiles string back to the Image_for controller, which calls the helper where the cdk routines get called.

The smiles-to-png code in the image_for helper metho is a hybrid of Rich's rcdk code and depict code. But instead of extracting the Java calls to CDK in a separate Java class, the calls are embedded directly into the jruby class. JRuby lets you store Java objects directly as ruby objects. Although the ruby smiles string is transparently translated to Java ( ...parseSmiles( smiles ) ), the translation of the Java ByteArrayOutputStream to a ruby String requires a thunk routine (String.from_java_bytes out.toByteArray).

Final Notes

The begin/rescue/end block is dependent on the cdk-20060714.jar packaged with structure-cdk-0.1.2. More recent versions have changes to both this code and the structure-cdk-0.1.2 package.

There are some bugs, which I haven't tracked down yet. The code will not render salts, metals, or accurately depict chiral centers with wedge bonds.

As per the jruby wiki, invoking jruby with a few command line switches will help the performance quite a bit:
jruby -J-server -J-Djruby.thread.pooling=true $JRUBY_HOME/bin/camping campdepict.rb

Finally, it works just as well under windows as Linux.

campdepict.rb

include Java

Camping.goes :Campdepict

module Campdepict::Controllers

class Index < '/'
def get
if input.smiles then
@smiles = input.smiles
else
@smiles = ''
end
@escapedsmiles = CGI::escape(@smiles)
render :smiles_picture
end
end

class Image_for < R '/image_for'
def get
render_smiles(input.smiles)
end
end

end

module Campdepict::Views
def layout
html do
head do
title { "SMILES Depictor" }
end
body { self << yield }
end
end

def smiles_picture
h1 "Depict a SMILES String"
img :src=> '/image_for/?smiles=#{@escapedSmiles}"
br
form :action => R(Index), :method => 'get' do
label 'Smiles', :for => 'smiles'
input :name => 'smiles',
:value => @smiles,
:size => '50',
:type => 'text
end
br
end

end

module Campdepict::Helpers
EDGE_SIZE = 200 # image size
MIME_TYPES = {'.css' => 'text/css', '.js' => 'text/javascript',
'.jpg' => 'image/jpeg', '.png' => 'image/png'}
PATH = Dir.pwd

def render_smiles(smiles)
if ! smiles (smiles.eql? '') then
return static_get('blank.png')
end

# cdk-20060714 dependent code
begin
smiles_parser = org.openscience.cdk.smiles.SmilesParser.new
sdg = org.openscience.cdk.layout.StructureDiagramGenerator.new
sdg.setMolecule( smiles_parser.parseSmiles( smiles ) )
sdg.generateCoordinates
image = Java::net.sf.structure.cdk.util.ImageKit.createRenderedImage(sdg.getMolecule(), EDGE_SIZE, EDGE_SIZE )
rescue
return static_get('invalid.png')
end

out = java.io.ByteArrayOutputStream.new
javax.imageio.ImageIO.write image, "png", out

String.from_java_bytes out.toByteArray
end

# static_get is straight outa the Camping wiki
def static_get(path)
@headers['Content-Type'] = MIME_TYPES[path[/\.\w+$/, 0]] "text/plain"
unless path.include? ".." # prevent directory traversal attacks
@headers['X-Sendfile'] = "#{PATH}/static/#{path}"
else
@status = "403"
"403 - Invalid path"
end
end
end