Latest Posts

Archives [+]

Using JCR in Rails applications

This post was orginally published at Ngoc's blog in Vietnamese


Using ActiveRecord is convenient due to its ease of use and richness of plugins. However, instead of a relational DB it can also be used with JCR. Let's try that for a simple blog application based on Rails.

Calling JCR API everywhere is a bit painful. Let's extend ActiveRecord::Base to write a simple object-content mapper. If you already have a Rails application and you want to replace the relational DB with JCR and you have been following the "fat model, thin controller, stupid view" best practice, doing this minimizes code changes to only some parts of the model. The only disavantage is that you can no longer use any ActiveRecord plugin which talks to relational DB.

Structure

Jackrabbit is used as the transient JCR server (actually any JCR server will do), and Rails connects to it through RMI. Generally there are two ways to combine Java and Ruby: bring Ruby to Java or bring Java to Ruby. There's already an article about using "JRuby". Let's use "Rjb" for a change.

We do not need a relational DB. To prevent Rails from connecting to a DB, edit config/environment.rb to remove ActiveRecord from config.frameworks (we still require ActiveRecord in the object-node mapper).

Our models will extend class JCR defined in jcr.rb, instead of ActiveRecord::Base.

/blog
 |
 +- app
 |  |
 |  +- controllers
 |  |  |
 |  |  +- articles_controller.rb
 |  |
 |  +- models
 |     |
 |     +- article.rb [<- extends JCR]
 |
 +- lib
 |  |
 |  +- java
 |  |  |
 |  |  +- [Jackrabbit JAR files]
 |  |
 |  +- jcr.rb [<- class JCR, the object-content mapper]
 |
 +- script
 |  |
 |  +- jcr [<- starts Jackrabbit]
 |
 +- [other things]

script/jcr

This script starts the Jackrabbit server on port 1100.

require 'rubygems'
require 'rjb'

LIB_DIR = File.dirname(__FILE__) + '/../lib'
CLASS_PATH = Dir.glob("#{LIB_DIR}/*.jar").join(File::PATH_SEPARATOR)

Rjb::load(CLASS_PATH, [])

JString              = Rjb::import('java.lang.String')
LocateRegistry       = Rjb::import('java.rmi.registry.LocateRegistry')

Repository           = Rjb::import('javax.jcr.Repository')
SimpleCredentials    = Rjb::import('javax.jcr.SimpleCredentials')
TransientRepository  = Rjb::import('org.apache.jackrabbit.core.TransientRepository')
ServerAdapterFactory = Rjb::import('org.apache.jackrabbit.rmi.server.ServerAdapterFactory')

repository = TransientRepository.new
factory = ServerAdapterFactory.new
remote = factory.getRemoteRepository(repository)
reg = LocateRegistry.createRegistry(1100)
reg.rebind('jackrabbit', remote)

puts 'Jackrabbit started at rmi://localhost:1100/jackrabbit'

trap('INT') do
  puts 'Shutting down...'
  repository.shutdown
end
sleep

jcr.rb

class ActiveRecord::BaseWithoutTable < ActiveRecord::Base
...
end

class JCR < ActiveRecord::BaseWithoutTable
  JCR_WORKSPACE = 'default'

  JString                  = Rjb::import('java.lang.String')
  JRepository              = Rjb::import('javax.jcr.Repository')
  JSimpleCredentials       = Rjb::import('javax.jcr.SimpleCredentials')
  JClientRepositoryFactory = Rjb::import('org.apache.jackrabbit.rmi.client.ClientRepositoryFactory')

  factory = JClientRepositoryFactory.new
  repository = factory.getRepository('rmi://localhost:1100/jackrabbit')
  begin
    @@session = repository.login(JSimpleCredentials.new('admin', JString.new('admin').toCharArray), JCR_WORKSPACE)
  rescue
    puts 'Could not connect to JCR server'
    exit(-1)
  end

  def self.session
    @@session
  end

  def session
    @@session
  end

  #-----------------------------------------------------------------------------

  # Avoid error ActiveRecord::ConnectionNotEstablished.
  def self.table_exists?
    false
  end

  def self.all
    node = session.getRootNode.getNode(table_name)
    it = node.getNodes

    ret = []
    while it.hasNext do ret << node_to_object(it.next) end
    ret
  end

  def self.find(attributes_values)
    work_space = session.getWorkspace
    query_manager = work_space.getQueryManager

    query_str = "//#{table_name}/node["
    attributes_values.each do |key, value|
      query_str << "@#{key.to_s} = '#{value}'"
    end
    query_str << ']'

    query = query_manager.createQuery(query_str, 'xpath')
    result = query.execute
    it = result.getNodes

    ret = []
    while it.hasNext do ret << node_to_object(it.next) end
    ret
  end

  def self.first(attributes_values)
    find(attributes_values).first
  end

  #-----------------------------------------------------------------------------

  def self.node_to_object(node)
    ret = new
    columns.each do |c|
      property = node.getProperty(c.name)
      value = property.send("get#{c.sql_type.to_s.capitalize}")
      ret.send("#{c.name}=", value)
    end
    ret
  end

  def save
    if valid?
      node = session.getRootNode.getNode(self.class.table_name)
      node = node.addNode('node')
      self.class.columns.each do |c|
        value = send(c.name)
        node.setProperty(c.name, value)
      end
      session.save
      true
    else
      false
    end
  end

  def update_attributes(attributes_values)
    attributes_values.each { |key, value| send("#{key.to_s}=", value) }
    save
  end
end

This file defines class Jcr which extends "BaseWithoutTable". If you want to get the session to the Jackrabbit server, call Jcr.session.

article.rb

class Article < Jcr
  column :user_name, :string
  column :title,     :string
  column :body,      :string

  validates_presence_of :user_name, :title, :body

  def to_param
    title
  end
end

With the above simple definition, we can have functionalities like finding, saving. For more complex functionality, the mapper must be improved, or we can use the JCR session directly.

Hope you have an idea how to use JCR in your Rails applications.

Source code

 

COMMENTS

  • By Alexander Klimetschek - 12:20 PM on Oct 27, 2008   Reply
    Very interesting! The next step would probably be an intelligent way to map object to nodes, so that you don't have the mapping table=node with all rows being subnodes in a flat hierarchy. (Rule #2 in http://wiki.apache.org/jackrabbit/DavidsModel)

    ADD A COMMENT