The most difficult part was gem bundling. Since Rails on RedBridge is built on Java web application, all gems should be in a Java based web application structure. Besides, the gems should be looked up by a Rails app on a servlet. After I tried some, I concluded Rails 3 was the best choice because of its built-in feature of gem bundler. So, I created a Java web application project with Apache Tomcat on Eclipse and Rails 3 app within.
First, I built jruby-complete.jar from JRuby 1.5.0 source archive. At this moment, JRuby 1.5.0.RC3 is the latest.
$ tar zxfv /Users/yoko/Downloads/jruby-src-1.5.0.RC3.tar.gz
$ cd jruby-1.5.0.RC3
$ ant jar-complete
$ export JRUBY_HOME=`pwd`
$ PATH=$JRUBY_HOME/bin:$PATH
Then, I installed rails 3 and jdbc adapter gems. In my case, I used sqlite3, so I typed a gem name for jdbc sqlite3 adapter.
$ jruby -S gem install rails --pre --no-rdoc --no-ri
$ jruby -S gem install activerecord-jdbcsqlite3-adapter --no-rdoc --no-ri
Next, I created a Java web application project, Hemlock, on Eclipse. Once the project was built, I had the tree structure below:
Hemlock -+- WebContent -+- META-INF --- MANIFEST.MF
| +- WEB-INF -+- lib
| +- web.xml
+- build -+- classes
+- src
Then, I created a Rails app under WebContent/WEB-INF/lib. This is because a web application adds the path, WEB-INF/lib, into the classpath automatically. WebContent/WEB-INF/classes would have been among the choices for the same reason.
My Rails app is based on a classic tutorial, "Four Days on Rails." This original document got outdated, but Japanese version was updated up to Rails 2.3.2. You can download the PDF file from Ita-san's blog: Four Days on Rails 2.0. Online translation service might help you to read the PDF.
$ cd [path-to-workspace]/Hemlock/WebContent/WEB-INF/lib
$ jruby -S rails todo
$ cd todo
Then, I edited Gemfile and config/database.yml to replace adapter gem and setting from sqlite3 to jdbcsqlite3.
Gemfile
#gem 'sqlite3-ruby', :require => 'sqlite3'
gem 'activerecord-jdbcsqlite3-adapter'
config/database.yml
#adapter: sqlite3
adapter: jdbcsqlite3
Before proceeding further, I did scaffolding to see database connection was set up right.
$ jruby -S rails g scaffold Category category:string
$ jruby -S rake db:migrate
$ jruby -S rails server
Requesting http://localhost:3000/categories on my browser, I added three categories.
I did one more setup on Rails --- Rails metal. Rails metal was originally invented to improve perfomance. But, metal exposes Rack's bare interface, so metal makes it easy to handle Rails app in the servlet.
$ jruby -S rails g metal poller
OK, let's setup gems for a servlet.
$ jruby -S bundle install vendor --disable-shared-gems
This command installed all necessary gems in WebContent/WEB-INF/lib/todo/vendor/gems directory.
Then, I moved on to the web application setting and the servlet. I copied jruby-compelete.jar to WEB-INF/lib, first. Next, after refreshing the Hemlock Eclipse project, I added jruby-complete.jar by Properties -> Java Build Path -> Add External JARS -> [on a JAR Selection window, I chose Hemlock/WebContent/WEB-INF/lib/jruby-complete.jar] -> OK. My servlet became as in below:
package com.servletgarden.hemlock;
import java.io.IOException;
import java.util.Arrays;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.jruby.embed.LocalContextScope;
import org.jruby.embed.ScriptingContainer;
public class SimpleMetalServlet extends HttpServlet {
private static final long serialVersionUID = 1L;
private String basepath;
private ScriptingContainer container;
private ListloadPaths;
public SimpleMetalServlet() {
super();
}
@Override
public void init() {
basepath = getServletContext().getRealPath("/WEB-INF");
String[] paths = {
basepath + "/lib/todo/vendor/gems/bundler-0.9.25/lib/",
basepath + "/lib/todo"
};
loadPaths = Arrays.asList(paths);
container = new ScriptingContainer(LocalContextScope.THREADSAFE);
}
@Override
protected void service(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
container.setLoadPaths(loadPaths);
container.runScriptlet("ENV['BUNDLE_GEMFILE'] = \"" + basepath + "/lib/todo/Gemfile\"");
container.runScriptlet("require 'config/environment'");
Mapenv = getEnvMap(request);
container.put("env", env);
List result = (List) container.runScriptlet("Poller.call env");
returnResult(response, result);
}
private MapgetEnvMap(HttpServletRequest request) {
Mapmap = new HashMap ();
map.put("PATH_INFO", "/poller");
return map;
}
private void returnResult(HttpServletResponse response, List result) throws IOException {
response.setStatus(((Long)result.get(0)).intValue());
Mapheaders = (Map )result.get(1);
Setkeys = headers.keySet();
for (String key : keys) {
if ("Content-Type".equals(key)) {
response.setContentType((String)headers.get(key));
}
}
Listcontents = (List )result.get(2);
for (String content : contents) {
response.getWriter().write(content);
}
}
}
When I right-clicked on the servlet source and chose Run As -> Run on Server, my Eclipse showed that simple "Hello, World!"
OK, let's look at what I did in SimpleMetalServlet. The first one is a load path setting.
Hemlock tree
Hemlock -+- WebContent -+- META-INF --- MANIFEST.MF
| +- WEB-INF -+- lib -+- todo -+- Gemfile
| | | +- app -+- controllers --- ...
| | | | +- metal --- poller.rb
| | | | +- ...
| | | +- config -+- environment.rb
| | | +- vendor -+- gems -+- bundler-0.9.25 -+- lib
| | | -+- ...
| | | +- ...
| | +- jruby-complete.jar
| +- web.xml
+- build -+- classes
+- src -+- com -+- servletgarden -+- hemlock -+- SimpleMetalServlet.java
in init() method:
basepath = getServletContext().getRealPath("/WEB-INF");
String[] paths = {
basepath + "/lib/todo/vendor/gems/bundler-0.9.25/lib/",
basepath + "/lib/todo"
};
loadPaths = Arrays.asList(paths);
in service() method:
container.setLoadPaths(loadPaths);
The path to bundler is to use bundler gem to load bundled gems. Bundler gem is installed in the todo/vendor/gems directory, but before loading bundled gem, SimpleMetalServlet can't load bundler itself. So, the servlet is telling JRuby runtime where the bundler gem is. The path to todo, Rails app top directory, is to tell JRuby where Rails app top directory is located.
The second one is the path to Gemfile.
container.runScriptlet("ENV['BUNDLE_GEMFILE'] = \"" + basepath + "/lib/todo/Gemfile\"");
Gemfile matters to Rails. To make Rails up and running, SimpleMetalServlet needs to give info about Gemfile. In this case, SimpleMetalServlet set the real path to Gemfile tied to the key "BUNDLE_GEMFILE" in ENV hash.
Then,
container.runScriptlet("require 'config/environment'");
Todo Rails app should get started by this.
Everything should be setup now, so SimpleMetalServlet is able to kick the metal method. Before talking about how the servlet kicked it, let's see what code Rails tailored.
poller.rb
# Allow the metal piece to run in isolation
require File.expand_path('../../../config/environment', __FILE__) unless defined?(Rails)
class Poller
def self.call(env)
if env["PATH_INFO"] =~ /^\/poller/
[200, {"Content-Type" => "text/html"}, ["Hello, World!"]]
else
[404, {"Content-Type" => "text/html", "X-Cascade" => "pass"}, ["Not Found"]]
end
end
end
Poller class has a class method, call, which needs an argument, env. The value, env, is a hash, in which PATH_INFO key and the value tied to it is expected. So, SimpleMetalServlet creates java.util.Map type object, puts to ScriptingContainer and evaluates Poller.call method with the argument, env.
Mapenv = getEnvMap(request);
container.put("env", env);
List result = (List) container.runScriptlet("Poller.call env");
private MapgetEnvMap(HttpServletRequest request) {
Mapmap = new HashMap ();
map.put("PATH_INFO", "/poller");
return map;
}
At last, response handling comes in.
returnResult(response, result);
private void returnResult(HttpServletResponse response, List result) throws IOException {
response.setStatus(((Long)result.get(0)).intValue());
Mapheaders = (Map )result.get(1);
Setkeys = headers.keySet();
for (String key : keys) {
if ("Content-Type".equals(key)) {
response.setContentType((String)headers.get(key));
}
}
Listcontents = (List )result.get(2);
for (String content : contents) {
response.getWriter().write(content);
}
}
Rack based application returns the response as an array of a status code, hash of http response headers, and array of body contents. SimpleMetalServlet parses the response and sends back to a browser. Since Poller class returns so simple reponse, SimpleMetalServlet doesn't do much in this case.
I tweaked poller.rb a bit so that registered categories are showed up.
poller.rb
#[200, {"Content-Type" => "text/html"}, ["Hello, World!"]]
[200, {"Content-Type" => "application/xhtml+xml"}, [Category.all.to_xml]]
Refreshing Hemlock project on Eclipse, restarting Tomcat also on Eclipse, then requesting http://localhost:8080/Hemlock/SimpleMetalServlet gave me the XML below.
It seems Rails on RedBridge worked.