Tuesday, September 06, 2011

Sinatra on RedBridge

Trinidad (http://thinkincode.net/trinidad/), Kirk (https://github.com/strobecorp/kirk), TorqueBox (http://torquebox.org/), mizuno (https://github.com/matadon/mizuno ) and perhaps some more are out there. As you know, those hook up Rails and/or Sinatra app on Java web servers. They are all easy to use tools even for people don't have Java background. On the other hand, there are people who want to write Java Servlet and control Rails/Sinatra apps on a Servlet, like me. :)


In the past, to make Rails apps controllable on Servlet, I've attempted Rails on RedBridge a couple of times. For example, The second step to Rails on RedBridge and Rails on RedBridge, Scaffolded App to Work are. In those attempts, I tried to invoke rack middleware's method, call, directly using RedBrdige. It was possible because Rails' controllers have a "call(env)" method, the one of a rack middleware. The idea was simple; however, it revealed that creating "env" argument was not so simple. In my past attempts, I somehow created "env" argument on Servlet and succeeded to make just a simple app to work. But, for more complicated database models or complicated HTTP requests, that was not enough. Thus, in this attempt, I used JRuby-Rack to create "env", of course, on the Servlet. A web framework is now Sinatra. Since Sinatra is much simpler than Rails, it is easy to try my idea out.


My attempt has done in three steps. The first step is really simple and just checks whether it works. I created a Java Web Application project on Eclipse whose name is Walnut. Java web server is Tomcat 7.0.4. Nothing is special to create the project so far. After I created the project, I put jruby-complete.jar in WEB-INF/lib under the web app directory tree. On Eclipse, the directory is located in WebContent/WEB-INF/lib, which varies on IDEs. Then, set the build path on Eclipse so that jruby-complete.jar is used in both compiling and executing. Before writing a Servlet, I installed gems in the Web application directory tree.

cd WebContent/WEB-INF/lib
mkdir -p jruby/1.8
export GEM_HOME=`pwd`/jruby/1.8
java -jar jruby-complete.jar -S gem install bundler --no-ri --no-rdoc -i jruby/1.8
java -jar jruby-complete.jar -S jruby/1.8/bin/bundle init
vi Gemfile
java -jar jruby-complete.jar -S jruby/1.8/bin/bundle install --path=.

Gemfile is below:

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

gem "sinatra"
gem "jruby-rack"


The first Servlet I wrote was below, which can be seen at HelloJRuby.java:

1 package walnut;
2
3 import java.io.File;
4 import java.io.IOException;
5 import java.util.ArrayList;
6 import java.util.List;
7 import java.util.Map;
8 import java.util.concurrent.ConcurrentHashMap;
9
10 import javax.servlet.ServletConfig;
11 import javax.servlet.ServletException;
12 import javax.servlet.annotation.WebServlet;
13 import javax.servlet.http.HttpServlet;
14 import javax.servlet.http.HttpServletRequest;
15 import javax.servlet.http.HttpServletResponse;
16
17 import org.jruby.embed.LocalContextScope;
18 import org.jruby.embed.ScriptingContainer;
19
20 /**
21 * Servlet implementation class HelloJRuby
22 */
23 @WebServlet("/HelloJRuby")
24 public class HelloJRuby extends HttpServlet {
25 private static final long serialVersionUID = 1L;
26 private List<String> gemPaths;
27 private ServletConfig config;
28 private ScriptingContainer container;
29
30 /**
31 * @see HttpServlet#HttpServlet()
32 */
33 public HelloJRuby() {
34 super();
35 gemPaths = new ArrayList<String>();
36 container = new ScriptingContainer(LocalContextScope.THREADSAFE);
37 }
38
39 private void addGemPaths(String gem_path) {
40 File gem_dir = new File(gem_path);
41 File[] gems = gem_dir.listFiles();
42 for (File gem : gems) {
43 String path = gem + "/lib";
44 gemPaths.add(path);
45 }
46 }
47
48 public void init(ServletConfig config) {
49 this.config = config;
50 String path = config.getServletContext().getRealPath("/WEB-INF/lib/jruby/1.8/gems");
51 addGemPaths(path);
52 }
53
54 /**
55 * @see HttpServlet#doGet(HttpServletRequest request, HttpServletResponse response)
56 */
57 protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
58 processHttpRequest(request, response);
59 }
60
61 /**
62 * @see HttpServlet#doPost(HttpServletRequest request, HttpServletResponse response)
63 */
64 protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
65 processHttpRequest(request, response);
66 }
67
68 private void processHttpRequest(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
69 container.setLoadPaths(gemPaths);
70 String class_def =
71 "require 'rubygems'\n" +
72 "require 'sinatra/base'\n" +
73 "class MyApp < Sinatra::Base\n" +
74 " get '/' do\n" +
75 " 'Hello from Sinatra'\n" +
76 " end\n" +
77 "end\n" +
78 "MyApp.new";
79 Object myApp = container.runScriptlet(class_def);
80 Map<String, String> minimal_env = new ConcurrentHashMap<String, String>();
81 minimal_env.put("PATH_INFO", "/");
82 minimal_env.put("REQUEST_METHOD", "GET");
83 minimal_env.put("rack.input", "");
84 List rack_response = (List)container.callMethod(myApp, "call", minimal_env, List.class);
85 response.getWriter().print(rack_response.get(2));
86 container.clear();
87 }
88 }

The Sinatra app in HelloJRuby Servlet would be the simplest one with a minimal env argument. What I wanted to test in this Servlet was whether gems were correctly loaded or not. Since a web application must be portable, every path must be relative to a Servlet context. The line 50 gets the path to gems, which is relative to the context, then the paths are saved in a List, "gemPaths" . This gemPaths is set to the ScriptingContainer in line 69. I ran this Servlet on Eclipse and got the string "Hello from Sinatra" on a browser.


The second Servlet uses JRuby-Rack instead of creating rack request directly. This needs a little trick to make JRuby-Rack work. I didn't want to use whole lot of JRuby-Rack but just a part of it to create a rack request on a Servlet. The second Servlet is below, which is also HelloRack.java on Github:

1 package walnut;
2
3 import java.io.File;
4 import java.io.IOException;
5 import java.util.ArrayList;
6 import java.util.List;
7
8 import javax.servlet.ServletConfig;
9 import javax.servlet.ServletException;
10 import javax.servlet.annotation.WebServlet;
11 import javax.servlet.http.HttpServlet;
12 import javax.servlet.http.HttpServletRequest;
13 import javax.servlet.http.HttpServletResponse;
14
15 import org.jruby.embed.LocalContextScope;
16 import org.jruby.embed.ScriptingContainer;
17
18 /**
19 * Servlet implementation class HelloRack
20 */
21 @WebServlet("/HelloRack")
22 public class HelloRack extends HttpServlet {
23 private static final long serialVersionUID = 1L;
24 private List<String> gemPaths;
25 private ServletConfig config;
26 private ScriptingContainer container;
27
28 /**
29 * @see HttpServlet#HttpServlet()
30 */
31 public HelloRack() {
32 super();
33 gemPaths = new ArrayList<String>();
34 container = new ScriptingContainer(LocalContextScope.THREADSAFE);
35 }
36
37 private void addGemPaths(String gem_path) {
38 File gem_dir = new File(gem_path);
39 File[] gems = gem_dir.listFiles();
40 for (File gem : gems) {
41 String path = gem + "/lib";
42 gemPaths.add(path);
43 }
44 }
45
46 public void init(ServletConfig config) {
47 this.config = config;
48 String path = config.getServletContext().getRealPath("/WEB-INF/lib/jruby/1.8/gems");
49 addGemPaths(path);
50 }
51
52 /**
53 * @see HttpServlet#doGet(HttpServletRequest request, HttpServletResponse response)
54 */
55 protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
56 processHttpRequest(request, response);
57 }
58
59 /**
60 * @see HttpServlet#doPost(HttpServletRequest request, HttpServletResponse response)
61 */
62 protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
63 processHttpRequest(request, response);
64 }
65
66 private void processHttpRequest(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
67 container.setLoadPaths(gemPaths);
68 String class_def =
69 "require 'rubygems'\n" +
70 "require 'sinatra/base'\n" +
71 "class MyApp < Sinatra::Base\n" +
72 " get '/' do\n" +
73 " 'Hello from Sinatra over JRuby-Rack'\n" +
74 " end\n" +
75 "end\n" +
76 "MyApp.new";
77 Object myApp = container.runScriptlet(class_def);
78 container.put("rack_app", myApp);
79 String creates_handler =
80 "require 'jruby-rack'\n" +
81 "require 'rack/handler/servlet'\n" +
82 "Rack::Handler::Servlet.new rack_app";
83 Object handler = container.runScriptlet(creates_handler);
84 container.put("handler", handler);
85 container.put("request", request);
86 container.put("config", config);
87 String calls_app =
88 "request.instance_variable_set(:@context, config.getServletContext)\n" +
89 "class << request\n" +
90 " def to_io\n" +
91 " self.getInputStream.to_io\n" +
92 " end\n" +
93 " def getScriptName\n"+
94 " self.getPathTranslated\n" +
95 " end\n" +
96 " def context\n" +
97 " @context\n" +
98 " end\n" +
99 "end\n" +
100 "handler.call(request)";
101 Object rack_response = container.runScriptlet(calls_app);
102 String body = (String)container.callMethod(rack_response, "getBody", String.class);
103 response.getWriter().print(body);
104 container.clear();
105 }
106 }

Line 79-83 creates Rack Servlet handler. This is a part of JRuby-Rack and nothing special. Following part of line 84-101 is a little hack. Basically, JRuby-Rack uses an instance of HttpServletRequest as an argument of "call" method. However, some methods are lacked. The snippet here adds those methods in Ruby way, dynamically to the instance. The return value at line 101 is an instance of JRuby::Rack::Response type (response.rb). So, I called "getBody" method of the returned object. We can call other methods of JRuby::Rack::Response in the same way. When I ran this Servlet on Eclipse, I got the expected result. JRuby-Rack worked.


The third step is the one leads to a real Sinatra app. In HelloRack Servlet, I wrote whole Sinatra app in the Servlet as a string. Nobody does this to create a real app. So, the third Servlet, HelloSinatraApp, uses config.ru file, below:

require 'my_app'
run MyApp

"my_app.rb" is:

require 'sinatra/base'

class MyApp < Sinatra::Base
get '/' do
'Hello from Sinatra App over JRuby-Rack!'
end
end

I put both config.ru and my_app.rb files in WEB-INF/lib/app directory. This means I need to add WEB-INF/lib/app to a load path. The HelloSinatraApp Servlet is below, or HelloSinatraApp.java on Github:

1 package walnut;
2
3 import java.io.File;
4 import java.io.IOException;
5 import java.util.ArrayList;
6 import java.util.List;
7
8 import javax.servlet.ServletConfig;
9 import javax.servlet.ServletException;
10 import javax.servlet.annotation.WebServlet;
11 import javax.servlet.http.HttpServlet;
12 import javax.servlet.http.HttpServletRequest;
13 import javax.servlet.http.HttpServletResponse;
14
15 import org.jruby.embed.LocalContextScope;
16 import org.jruby.embed.ScriptingContainer;
17
18 /**
19 * Servlet implementation class HelloSinatraApp
20 */
21 @WebServlet("/HelloSinatraApp")
22 public class HelloSinatraApp extends HttpServlet {
23 private static final long serialVersionUID = 1L;
24 private List<String> gemPaths;
25 private ServletConfig config;
26 private ScriptingContainer container;
27 private String config_ru_path;
28
29 /**
30 * @see HttpServlet#HttpServlet()
31 */
32 public HelloSinatraApp() {
33 super();
34 gemPaths = new ArrayList<String>();
35 container = new ScriptingContainer(LocalContextScope.THREADSAFE);
36 }
37
38 private void addGemPaths(String gem_path) {
39 File gem_dir = new File(gem_path);
40 File[] gems = gem_dir.listFiles();
41 for (File gem : gems) {
42 String path = gem + "/lib";
43 gemPaths.add(path);
44 }
45 }
46
47 public void init(ServletConfig config) {
48 this.config = config;
49 String path = config.getServletContext().getRealPath("/WEB-INF/lib/jruby/1.8/gems");
50 addGemPaths(path);
51 gemPaths.add(config.getServletContext().getRealPath("/WEB-INF/lib/app"));
52 config_ru_path = config.getServletContext().getRealPath("/WEB-INF/lib/app/config.ru");
53 }
54
55 /**
56 * @see HttpServlet#doGet(HttpServletRequest request, HttpServletResponse response)
57 */
58 protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
59 processHttpRequest(request, response);
60 }
61
62 /**
63 * @see HttpServlet#doPost(HttpServletRequest request, HttpServletResponse response)
64 */
65 protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
66 processHttpRequest(request, response);
67 }
68
69 private void processHttpRequest(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
70 container.setLoadPaths(gemPaths);
71 container.runScriptlet("require 'rubygems'");
72 container.put("config_ru_path", config_ru_path);
73 String creates_handler =
74 "require 'rack'\n" +
75 "rack_app, options = Rack::Builder.parse_file config_ru_path\n" +
76 "require 'jruby-rack'\n" +
77 "require 'rack/handler/servlet'\n" +
78 "Rack::Handler::Servlet.new rack_app";
79 Object handler = container.runScriptlet(creates_handler);
80 container.put("handler", handler);
81 container.put("request", request);
82 container.put("config", config);
83 String calls_app =
84 "request.instance_variable_set(:@context, config.getServletContext)\n" +
85 "class << request\n" +
86 " def to_io\n" +
87 " self.getInputStream.to_io\n" +
88 " end\n" +
89 " def getScriptName\n"+
90 " self.getPathTranslated\n" +
91 " end\n" +
92 " def context\n" +
93 " @context\n" +
94 " end\n" +
95 "end\n" +
96 "handler.call(request)";
97 Object rack_response = container.runScriptlet(calls_app);
98 String body = (String)container.callMethod(rack_response, "getBody", String.class);
99 response.getWriter().print(body);
100 container.clear();
101 }
102
103 }

Let's see. The path to WEB-INF/lib/app is added in line 51. The path to config.ru is set in line 52. Line 73-79 creates Sinatra app instance and Rack Servlet handler. Other part is the same as the second Servlet, HelloRack. Again, I got the result I expected from this Servlet. I may write paths to gems and apps in web.xml to make this Servlet configurable.


Like in the above, Sinatra on RadBridge worked! This needs a knowledge of Servlet and Java web application. But, the good side is the app won't choose web servers. The app is an ordinary Java web application and works on any Servlet based web application. If you want to write a Servlet for some reasons, like me, this would be the choice.

No comments: