Monday, February 14, 2011

The second step to Rails on RedBridge

This is the second attempt to make Rails work on RedBrdige (JRuby embedding API). I believe Rails on RedBridge got closer to a real application. The first attempt is also in this blog, A small step to Rails on RedBridge. The blog post had some impact on a few people who want to control Rails (or Sinatra) from Java. The example there successfully showed how we could wake Rails up from Java Servlet, but was almost a hello world example. Meanwhile, I demonstrated simple Rails app from Groovy (Groovy Servlet, precisely) in my session at RubyConf 2010, "RubyGems to All JVM Languages." Rails on RedBridge way is an approach from Java API on Servlet, so all JVM languages are possibly available to use Rails *gem* from them over JRuby's Java API. That demonstration at RubyConf 2010 impressed the audiences there enough to have a clap, but I knew it was still a hello world example. Recently, I tried to develop Rails on RedBridge more so that Rails app got work like other rackup style frameworks. RailsCrossing is the outcome. It is the Java API to use Rails.


OK. I'm going to write how to setup and write code using RailsCrossing.


1. Creating Java Web Application Project

The first job is to create a Java web application project. I used NetBeans for that. You can use whatever IDE you like that has the feature to create a Java Servlet based web application project. My web app project has a name "Spruce" and the server to run it is Tomcat 7.0.4.

Initially, a directory tree of the Spruce project looks like below (I didn't write NetBeans specific files/directories):

Spruce -+- src
+- web -+- META-INF -+- context.xml
+- WEB-INF
+- index.jsp


The directory tree is slightly different when other IDEs are used. That's not the matter. Just look at WEB-INF directory. The WEB-INF directory is special for Java Servlet web app. It is the place to put libraries and is protected from accesses originated from web clients.


2. Prepare for Rails APP

RailsCrossing assumes Rails app is located under WEB-INF directory. It could not be necessarily there. However, I made that constraint since Java Servlet web application should be portable. Everything needed to the web app work should be in the web app directory tree. It's the policy.

Let's prepare the app.

First, get jruby-complete.jar and put it in WEB-INF/lib directory. (Get the latest (master) branch of JRuby. Or, you can use jruby-complete.jar of RailsCrossing project)

Spruce -+- src
+- web -+- META-INF -+- context.xml
+- WEB-INF -+- lib -+- jruby-complete.jar
+- index.jsp


Next, install bundler gem. Make sure bundler will be installed under WEB-INF directory. So, let's go to lib directory and type as in below (you can use jruby command instead):

$ java -jar jruby-complete.jar -S gem install bundler --no-ri --no-rdoc -i jruby/1.8
Fetching: bundler-1.0.10.gem (100%)
Successfully installed bundler-1.0.10
1 gem installed

Don't forget -i option to install bundler gem under lib directory. The command above installs bundler gem in WEB-INF/lib/jruby/1.8. So, you can find bundle command in the path, WEB-INF/lib/jruby/1.8/bin/bundle.

Spruce -+- src
+- web -+- META-INF -+- context.xml
+- WEB-INF -+- lib -+- jruby-complete.jar
| +- jruby -+- 1.8 -+- bin -+- bundle
| +- cache -+- ...
| +- doc
| +- gem -+- bundler-1.0.10 -+- ..
| +- specifications -+- ...
|
+- index.jsp


Let's set GEM_PATH to use bundler to install other gems.
$ export GEM_PATH=[path to lib directory]/jruby/1.8


Write or create Gemfile. If you want to use bundle command rather than open a new file by editor, type as in below:
java -jar jruby-complete.jar -S jruby/1.8/bin/bundle init

Gemfile should be something like this:

# A sample Gemfile
source "http://rubygems.org"

gem "rails"

I just deleted the comment sign (#) on the rails line after bundle init command. The last preparation is to install rails gem.
java -Xmx500m -jar jruby-complete.jar -S jruby/1.8/bin/bundle install --path .

Don't forget "--path ." to install gems under the same path as bundler has been installed in. When bundle install command successfully finished, bunch of gems are in jruby/1.8.

Spruce -+- src
+- web -+- META-INF -+- context.xml
+- WEB-INF -+- lib -+- Gemfile
| +- Gemfile.lock
| +- jruby-complete.jar
| +- jruby -+- 1.8 -+- bin -+- bundle
| | +- erubis
| | +- ....
| +- cache -+- ...
| +- doc
| +- gem -+- abstract-1.0.0 -+- ..
| +- gem -+- .... -+- ..
| +- gem -+- bundler-1.0.10 -+- ..
| +- gem -+- .... -+- ..
| +- specifications -+- ...
|
+- index.jsp



3. Create Rails App

Probably, I don't need to say anything. I typed below:
java -jar jruby-complete.jar -S jruby/1.8/bin/rails new blog --template http://jruby.org

Now, blog Rails app is created under WEB-INF/lib/blog.

Spruce -+- src
+- web -+- META-INF -+- context.xml
+- WEB-INF -+- lib -+- Gemfile
| +- Gemfile.lock
| +- blog -+- Gemfile
| | +- README
| | +- Rakefile
| | +- ...
| +- jruby-complete.jar
| +- jruby -+- 1.8 -+- bin -+- bundle
| | +- erubis
| | +- ....
| +- cache -+- ...
| +- doc
| +- gem -+- abstract-1.0.0 -+- ..
| +- gem -+- .... -+- ..
| +- gem -+- bundler-1.0.10 -+- ..
| +- gem -+- .... -+- ..
| +- specifications -+- ...
|
+- index.jsp


In general, database setup follows. But, to make it understandable to know how it works, let's create a controller.

$ cd blog (Now, I'm in Spruce/web/WEB-INF/lib/blog directory)
$ java -Xmx500m -jar ../jruby-complete.jar ../jruby/1.8/bin/bundle install --path ../. (Don't forget --path ../. option!)
$ java -jar ../jruby-complete.jar script/rails g controller home index

So far so good. These steps to creating Rails app should be familiar with.


4. Simple Sample Servlet to run Rails' controller

Here comes RailsCrossing API. You can write Servlet using methods in CrossingHelpers or subclassing CrossingServlet. The CrossingServlet does everything to run Rails' controller. For example, SimpleSample below would be the simplest example.

package com.servletgarden.spruce;

import com.servletgarden.railsxing.CrossingServlet;
import java.io.IOException;
import javax.servlet.Servlet;
import javax.servlet.ServletConfig;
import javax.servlet.ServletException;
import javax.servlet.annotation.WebServlet;
import javax.servlet.annotation.WebInitParam;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

/**
*
* @author Yoko Harada
*/
@WebServlet(name="SimpleSample",
urlPatterns={"/*"},
initParams={@WebInitParam(name="rails_path", value="/lib/blog"), @WebInitParam(name="gem_path", value="/lib/jruby/1.8")})
public class SimpleSample extends CrossingServlet {

/**
* @see Servlet#init(ServletConfig)
*/
@Override
public void init(ServletConfig config) throws ServletException {
super.init(config);
}

/**
* Processes requests for both HTTP GET and POST methods.
* @param request servlet request
* @param response servlet response
* @throws ServletException if a servlet-specific error occurs
* @throws IOException if an I/O error occurs
*/
protected void processRequest(HttpServletRequest request, HttpServletResponse response)
throws ServletException, IOException {

dispatch(request, response);
}

/**
* Handles the HTTP GET method.
* @param request servlet request
* @param response servlet response
* @throws ServletException if a servlet-specific error occurs
* @throws IOException if an I/O error occurs
*/
@Override
protected void doGet(HttpServletRequest request, HttpServletResponse response)
throws ServletException, IOException {
processRequest(request, response);
}

/**
* Handles the HTTP POST method.
* @param request servlet request
* @param response servlet response
* @throws ServletException if a servlet-specific error occurs
* @throws IOException if an I/O error occurs
*/
@Override
protected void doPost(HttpServletRequest request, HttpServletResponse response)
throws ServletException, IOException {
processRequest(request, response);
}

/**
* Returns a short description of the servlet.
* @return a String containing servlet description
*/
@Override
public String getServletInfo() {
return "the simplest example used RailsCrossing";
}

}


Let's look at what's going on in detail.

In the SimpleSample init method, it calls init method of a super class, CrossingServlet. In CrossingServlet init method, initialization of ScriptingContainer (JRuby embedding API a.k.a. RedBridge) and Rails are done. Then, Rails routes mappings are parsed.

public void init(ServletConfig config) throws ServletException {
container = CrossingHelpers.initialize(config);
routes = CrossingHelpers.parseRoutes(container);
}

While initialization is going on CrossingServlet uses "rails_path" and "gem_path" initParams relative to WEB-INF directory. These are necessary to fire up Rails.


Well, how Rails routes mapping can be parsed? Probably, many of you have used rails console. RailsCrossing does the same thing on a Servlet as people do on rails console. For example,

$ java -jar ../jruby-complete.jar script/rails c
irb(main):007:0> route_ary = ActionDispatch::Routing::Routes.routes
=> [#<ActionDispatch::Routing::Route:0x555669ae @name="home_index"...(snip)
irb(main):004:0> route_ary[0].to_a[1]
=> {:path_info=>/\A\/home\/index(?:\.([^\/.?]+))?\Z/, :request_method=>/^GET$/}
irb(main):005:0> route_ary[0].to_a[2]
=> {:controller=>"home", :action=>"index"}
irb(main):006:0> route_ary[0].to_a[3]
=> "home_index"

CrossingHelpers.parseRoutes method exactly evaluates these commands to draw mapping info out from Rails.


Let get back to SimpleSample Servlet. Look at the processRequest method. In this method, CrossingServlet's dispatch method is called. Just that. What's this dispatch?
Here it is:

protected void dispatch(HttpServletRequest request, HttpServletResponse response) throws IOException {
CrossingRoute route = findMatchedRoute(request.getContextPath(), request.getPathInfo(), request.getMethod());
if (route == null) return;
Map env = getEnvMap(request);
CrossingResponse crossingResponse = CrossingHelpers.dispatch(container, route, env);
response.setStatus(crossingResponse.getStatus());
Set keys = crossingResponse.getResponseHeader().keySet();
for (String key : keys) {
String value = crossingResponse.getResponseHeader().get(key);
response.setHeader(key, value);
}
PrintWriter writer = response.getWriter();
writer.write(crossingResponse.getBody());
}

Based on the parsed routes, CrossingServlet finds what controller should be used from path_info in HttpServletRequest. If matched controller is found, it creates a map of each HTTP request params including HTTP request header and query string. Then, dispatching. Here, RailsCrossing does rails console commands on Servlet again.

irb(main):006:0> env = {"rack.input" => "", "REQUEST_METHOD" => "GET"}
=> {"rack.input"=>"", "REQUEST_METHOD"=>"GET"}
irb(main):008:0> HomeController.action('index').call(env)[0]
=> 200
irb(main):009:0> HomeController.action('index').call(env)[1]
=> {"Content-Type"=>"text/html; charset=utf-8", "ETag"=>"\"a5e60d2fa2208dc316b0f09cef107bba\"", "Cache-Control"=>"max-age=0, private, must-revalidate"}
irb(main):010:0> HomeController.action('index').call(env)[2].body
=> "<!DOCTYPE html>\n<html>\n<head>\n <title>Blog</title>\n...

Once CrossingServlet gets the response, it sets HTTP response status code and header params, then writes html part out to Servlet's writer.

These are what RaisCrossing does.


5. One More Setup

The difference of a context path lies between Java web app and Rails app. In general, Java web application is referenced by http://servername:port/context_name/servlet_name/path_info, while rails is done by http://servername:port/path_info. We need to fix these to make them coincide.

Let's change Rails first editing config/routes.rb:

Blog::Application.routes.draw do
scope "/blog" do
get "home/index"
...
end
end

I added scope in config/routes.rb. Now, blog app is referenced by http://servername:port/blog/path_info. Next is a tomcat setting. I edited META_INF/context.xml:

<?xml version="1.0" encoding="UTF-8"?>
<Context antiJARLocking="true" path="/blog"/>

Originally, it was path="/Spruce" since I named the web app Spruce. After Spruce has been changed to "blog," every HTTP request to http://servername:port/blog/* has been directed to SimpleSample Servlet.


6. Run Servlet

Since I created web app on NetBeans, I can get SimpleSample Servlet started from NetBeans menu with the path, "/home/index" If not, start tomcat up and request "http://localhost:8080/blog/home/index" You'll see familiar controller default output on a browser.


7. Thoughts

RailsCrossing uses Rails metal introduced in version 3. This rack style invocation was easy to get it run. However, it is mostly undocumented area. I attempted many commands on rails console and tried to find the best way; however, there might be still better way.

A good side of RailsCrossing is ... it is a Java API. This means, JVM languages such as Scala, Groovy, or other can use RailsCrossing over java integration feature. I'll try Rails from Scala next.

No comments: