Wednesday, October 24, 2012

Clementine gem - ClojureScript on Rails Asset Pipeline

After about a year of silence, Clementine gem, https://github.com/yokolet/clementine, has been updated. The remarkable feature of this gem is to make ClojureScript available on Rails asset pipeline. Like CoffeeScript, ClojureScript code is compiled dynamically with this gem. Besides, the gem works on both CRuby and JRuby. I've blogged before, but those blog posts got really old. So, I'm going to update for the latest version.

For those of you don't know ClojureScript, I recommand the book, http://shop.oreilly.com/product/0636920025139.do, which is very good to understand what it is. Also, you'd better to visit https://github.com/clojure/clojurescript and http://clojurescriptone.com/ to get familiar with ClojureScript.

The first thing to be done is to create a Rails app.
rails new blog
Then, add Clementine in your Gemfile.
Well, since I use the same Rails app for both JRuby and CRuby, my Gemfile looks like this:
source 'https://rubygems.org'

gem 'rails', '3.2.8'

gem 'sqlite3', :platforms => :mri_19
gem 'activerecord-jdbcsqlite3-adapter', :platforms => :jruby
gem 'jruby-openssl', :platforms => :jruby

# Gems used only for assets and not required
# in production environments by default.
group :assets do
  gem 'sass-rails',   '~> 3.2.3'
  gem 'coffee-rails', '~> 3.2.1'

  # See https://github.com/sstephenson/execjs#readme for more supported runtimes
  gem 'therubyracer', :platforms => :mri_19
  gem 'therubyrhino', :platforms => :jruby

  gem 'uglifier', '>= 1.0.3'
end

gem 'jquery-rails'

gem 'clementine', '0.0.3'
After adding Clementine in the Gemfile, run the commend:
bundle install
While installing Clementine, you'll see confusing message, "Installing clementine (version string) with native extensions." Clementine doesn't rely on C library at all. Clementine is just bootstrapping ClojureScript while installation is going on. Clementine has a whole ClojureScript code in it, but doesn't have Clojure and Google closure compiler/library that ClojureScript needs. Clementine runs ClojureScript's bootstrap command to grab those from each site during the installation. When the gem installation is completed, Clementine has an appropriate version of clojure.jar, compiler.jar, goog.jar and js.jar in its lib directory.

Next, create controller to see how ClojureScript works on Rails asset pipeine.
rails g controller Greetings index
As in "Using ClojureScript on a Web Page" sample at https://github.com/clojure/clojurescript/wiki/Quick-Start, create the file, app/assets/javascripts/hello.js.cljs and write:
(ns hello)
(defn ^:export greet [name]
  (str "Hellooooooooo " name))
Add three lines to app/views/greetings/index.html.erb, which looks like:
<script>
    alert(hello.greet("ClojureScript"));
</script>
<h1>Greetings#index</h1>
<p>Find me in app/views/greetings/index.html.erb</p>
That's it. Start Rails
rails s
and request http://localhost:3000/greetings/index from your browser. You'll see this javascript alert dialog
. If you tried on CRuby, you should have waited long time to see this dialog. This is because JVM got started to compile ClojureScript. Starting JVM is a huge job. I recommend JRuby to cut down the compilation time. JVM is already there.

The default setting is advanced optimization mode. So, you can see how it is compiled using browser's inspection feature:
If you want to see readable JavaScript code for debugging, you should specify options of ClojureScript. To give compiler options, create the file, config/initializers/clementine.rb and write Clementine options as in below:
Clementine.options[:optimizations] = :whitespace
Clementine.options[:pretty_print] = true
Don't forget to edit app/assets/javascripts/hello.js.cljs to see the effect of given options. If you don't change anything in hello.js.cljs, Rails won't call compiler. It is the asset pipeline.

Restart Rails and request the same URL. You'll see something like this on browser's inspection window:


That's all. Try ClojureScript on Rails.

Thursday, November 10, 2011

ClojureScript on Rails Asset Pipeline

This post is the second part subsequent to Tilt Template for ClojureScript. Now, Tilt template for ClojureScript has worked, so the template should work with Rails asset pipeline. Though it is brief, Ruby on Rails Guides: Asset Pipeline mentions "Registering gems on Tilt enabling Sprockets to find them." So, I tried that..


To make it work, I added just one simple class, which is clementine_rails.rb below:

module Clementine
class ClementineRails < Rails::Engine
initializer :register_clojurescript do |app|
app.assets.register_engine '.cljs', ClojureScriptTemplate
app.assets.register_engine '.clj', ClojureScriptTemplate
end
end
end

That's it. Then Sprockets finds ClojureScriptTemplate.


Let's try ClojureScript on asset pipeline.


I need Rails app in any case, so I created it. Undoubtedly, my Ruby is JRuby, and is bundler/rails gems installed.
rails new rouge

Then, I added "clementine" gem (a name of ClojureScript template gem) to Gemfile. Since the gem is under development, I specified the directory of the gem.

gem 'clementine', :path => "/Users/yoko/Projects/clementine"

Then, typed bundle install so that clementine gem is recognized. I need at least one controller, so, next, I created "Greetings" controller.

rails g controller Greetings index



Well, finally, Clojure stuff comes in. I put the file below in the directory, app/assets/javascripts. The filename is hello.js.clj . This is because Rails asset pipeline strips extensions off. The file, hello.js.clj, will be used as hello.js. Perhaps, the name, "hello" , works, but it doesn't look like javascript.

(ns hello)
(defn ^:export greet [name]
(str "Hellooo " name))

Finishing touch is to add a javascript snippet to use the function defined by ClojureScript. I added three lines in app/views/greetings/index.html.erb:

<script>
alert(hello.greet("ClojureScript"));
</script>
<h1>Greetings#index</h1>
<p>Find me in app/views/greetings/index.html.erb</p>



When I started Rails and requested http://localhost:3000/greetings/index, I saw the alert box successfully!


Tentatively, I set ClojureScript's option, {:optimizations :advanced}, as default. When I looked the contents of hello.js, I confirmed it was the one ClojureScript compiled.


Although the clementine gem should be improved in various ways, ClojureScript is on Rails asset pipeline. If you are interested, watch https://github.com/yokolet/clementine.

Wednesday, November 09, 2011

Tilt Template for ClojureScript

As a JVM language lover, I've written code mixed with more than one language. Ruby gems from Clojure, Clojure from Ruby, or other combinations. This blog is about using ClojureScript from JRuby. ClojureScript (https://github.com/clojure/clojurescript/) is "a new compiler for Clojure that targets JavaScript." ClojureScript uses Google Closure ( don't be confused! ) for optimization. So, instead of JavaScript, we can use Clojure's succinct syntax to write a JavaScript part.


As you know, Clojure is a JVM language. Clojure from JRuby is not so complicated. Many people know what's like that. My attempt here is using ClojureScript from Tilt (https://github.com/rtomayko/tilt). Tilt is a well known generic wrapper for Ruby template engines. It covers more than 20 template engines such as ERB, Haml, or CoffeeScript. Not only those, Tilt allows us to write other template wrappers. Thus, Tilt template for ClojureScript is possible.


Here's what I did. Whole code is on github, https://github.com/yokolet/clementine.

Firstly, I copied jars and Clojure code for ClojureScript under vendor/assets directory. The directory can be lib or other. An important point is to set Java's classpath to those jars and directories. See, clementine.rb below:

require 'rubygems'
require "clementine/version"
if defined?(RUBY_ENGINE) && RUBY_ENGINE == "jruby"
require "java"

CLOJURESCRIPT_HOME = File.dirname(__FILE__) + "/../vendor/assets"
$: << CLOJURESCRIPT_HOME + "/lib"
require 'clojure'

%w{compiler.jar goog.jar js.jar}.each {|name| $CLASSPATH << CLOJURESCRIPT_HOME + "/lib/" + name}
%w{clj cljs}.each {|path| $CLASSPATH << CLOJURESCRIPT_HOME + "/src/" + path}

require "clementine/clojurescript_engine"
require "clementine/clojurescript_template"
end

Line 6 sets CLOJURESCRIPT_HOME directory relative to clementine.rb. CLOJURESCRIPT_HOME + "/lib" directory has jars, so I set this directory to $LOAD_PATH so that JRuby can load clojure.jar. Line 8, "require 'clojure'" loads clojure.jar. You can also write "require 'clojure.jar'" instead, but extension doesn't matter for JRuby. Then, I added paths to $CLASSPATH. We can use $CLASSPATH global variable after "require 'java'". $CLASSPATH is Ruby Array and equivalent to Java's classpath. Line 10 and 11 set all paths to run ClojureScript.


The code below is a ClojureScriptEngine class, clojurescript_engine.rb:

require 'java'
%w{RT Keyword PersistentHashMap}.each do |name|
java_import "clojure.lang.#{name}"
end

module Clementine
class ClojureScriptEngine
def initialize(file, options)
@file = file
@options = options
end

def compile
cl_opts = PersistentHashMap.create(convert_options())
RT.loadResourceScript("cljs/closure.clj")
builder = RT.var("cljs.closure", "build")
builder.invoke(@file, cl_opts)
end

def convert_options()
opts = {}
@options.each do |k, v|
cl_key = Keyword.intern(k.to_s)
case
when (v.kind_of? Symbol)
cl_value = Keyword.intern(v.to_s)
else
cl_value = v
end
opts[cl_key] = cl_value
end
opts
end
end
end

This class basically performs ClojureScript REPL, (cljsc/build "hello.cljs" {:optimizations :advanced :output-to "hello.js"}). The function, cljsc/build, is defined in cljs/closure.clj file, so the code loads cljs/closure.clj by clojure.lang.RT.oadResourceScript("cljs/closure.clj"). Line 16 gets a reference to the method, then, line 17 invokes the method with arguments.
This gist https://gist.github.com/1350398 might be more understandable since it is straightforward.


Lastly, Tilt templete code, clojurescript_template.rb, became as in below:

require 'tilt/template'

module Clementine
class ClojureScriptTemplate < Tilt::Template
self.default_mime_type = 'application/javascript'

def self.engine_initialized?
true
end

def initialize_engine; end

def prepare
@engine = ClojureScriptEngine.new(@file, options)
end

def evaluate(scope, locals, &block)
@output ||= @engine.compile
end
end
end

Probably, you'd better to go to Tilt sites rather than to read my how-to about this code. So, I won't comment anything about this code.


Let's try ClojureScript Tilt template.

$ cd clementine
$ jruby -Ilib -S irb
jruby-1.6.5 :001 > require 'clementine'
=> true
jruby-1.6.5 :002 > require 'tilt'
=> true
jruby-1.6.5 :003 > Tilt.register(Clementine::ClojureScriptTemplate, 'cljs', 'clj')
=> ["cljs", "clj"]
jruby-1.6.5 :004 > template = Tilt.new('/Users/yoko/Works/tmp/clojurescript/hello.clj', 1, {:optimizations => :advanced})
=> #<Clementine::ClojureScriptTemplate:0x5513dd59 @compiled_method={}, @reader=#<Proc:0x517667bd@/usr/local/rvm/gems/jruby-1.6.5@jror/gems/tilt-1.3.3/lib/tilt/template.rb:67>, @engine=#<Clementine::ClojureScriptEngine:0x3494d313 @file="/Users/yoko/Works/tmp/clojurescript/hello.clj", @options={:optimizations=>:advanced}>, @options={:optimizations=>:advanced}, @line=1, @file="/Users/yoko/Works/tmp/clojurescript/hello.clj", @default_encoding=nil, @data="(ns hello)\n(defn ^:export greet []\n (str \"Hello \" n))">
jruby-1.6.5 :005 > template.render
=> "function b(a){throw a;}var f=true,h=null,j=false;function aa(){return function(a){return a}}function k(a){return function(){return this[a]}}function l(a){return function(){return
(snip)



It worked!


This is a preliminary implementation and far from a release at this moment. But, I could confirm ClojureScript is available from Ruby. ClojureScript will be added to Ruby's template engines. My next goal will be ClojureScript from Ruby web application frameworks. It'll be more practical.

Tuesday, September 27, 2011

RedBridge Presentaion Slides

I'll put the links to my presentation slides together here. Since I've written slides using Rails and jQuery instead of PowerPoint or OpenOffice, my slides are not on slideshare.

  • StrangeLoop 2011 - "Embedding Ruby and RubyGems Over RedBridge", http://redbridge-at-strangeloop2011.herokuapp.com/slideshow

  • When you put the cursor on the page, forward/backward arrows will appear on both sides of a main area. If you click the right arrow, you can see next page. Or, you can click bullets on the bottom.
    I used MobilySlider jQuery plugin.


  • RubyConf 2010 - "RubyGems To ALL JVM Languages", http://servletgarden-point.appspot.com/slideshow

  • When you click right and left arrows on the both side of a main area, you can go forward and backward. Also, you can click page numbers.
    I used Sudo Slider jQuery plugin.


  • JRubyKaigi 2010 - "Want To Use Ruby From Java? RedBridge Makes It Pretty Easy!", http://latest.1.servletgarden-point.appspot.com/slideshow

  • Click menus on the left side, then right side of contents will be changed. The right side is an accordion. If you click bottom/head tabs, other pages will show up. Basically, the slide is in English. But, a little amount of Japanese are in.

Sinatra on Scala

I rewrote the Servlet in my former post, Sinatra on RedBridge using Scala. Below is the code:

package chestnut

import java.io.{File, IOException}
import java.util.{ArrayList, List}

import javax.servlet.{ServletConfig, ServletException}
import javax.servlet.annotation.WebServlet
import javax.servlet.http.{HttpServlet, HttpServletRequest => HSReq, HttpServletResponse => HSResp}

import org.jruby.embed.{LocalContextScope, ScriptingContainer => Container}

import scala.collection.JavaConversions._
import scala.collection.mutable.ListBuffer


class HelloSinatra extends HttpServlet() {
private val gemPaths = new ListBuffer[String]
private var config: ServletConfig = _
private val container = new Container(LocalContextScope.THREADSAFE)
private var config_ru_path: String = _

def addGemPaths(gem_path: String) {
val gem_dir = new File(gem_path)
val gems = gem_dir.listFiles
for (gem <- gems) {
val path = gem + "/lib"
gemPaths += path
}
}

override def init(c: ServletConfig) {
config = c
val path = config.getServletContext().getRealPath("/WEB-INF/lib/vendor/jruby/1.8/gems")
addGemPaths(path)
gemPaths += config.getServletContext().getRealPath("/WEB-INF/lib/app")
config_ru_path = config.getServletContext().getRealPath("/WEB-INF/lib/app/config.ru")
}

override def doGet(request: HSReq, response: HSResp) = processHttpRequest(request, response)

override def doPost(request: HSReq, response: HSResp) = processHttpRequest(request, response)

private def processHttpRequest(request: HSReq, response: HSResp) {
container.setLoadPaths(gemPaths)
container.runScriptlet("require 'rubygems'")
container.put("config_ru_path", config_ru_path);
container.put("request", request);
container.put("config", config);
val script =
"""require 'rack'
rack_app, options = Rack::Builder.parse_file config_ru_path
require 'jruby-rack'
require 'rack/handler/servlet'
handler = Rack::Handler::Servlet.new rack_app
request.instance_variable_set(:@context, config.getServletContext)
class << request
def to_io
self.getInputStream.to_io
end
def getScriptName
self.getPathTranslated
end
def context
@context
end
end
handler.call(request)"""
var rack_response = container.runScriptlet(script);
var body : String = container.callMethod(rack_response, "getBody").asInstanceOf[String]
body += "\n\n\nand Hello from Scala Servlet!!"
response.getWriter().print(body);
container.clear();
}

}

Also, you can see the code, https://gist.github.com/1245405.


In the above Scala version, I added a small message to the Scala servlet. When you look at the result on your browser, you'll see two lines from Sinatra and Scala. The first line is from Sinatra, and the second line is from Scala servlet.

Saturday, September 24, 2011

Clojure's PersistentHashMap on JRuby

Clojure is an impressive language. Not just succinct syntax, Clojure has immutable data types for concurrency. Such Clojure's persistent data types might be useful in some cases in other languages. A question is whether we can use Clojure data types from JRuby. The answer is yes. Below is what I tried on irb:

$ rvm jruby-1.6.4
$ irb
jruby-1.6.4 :001 > require 'java'
=> true
jruby-1.6.4 :002 > $CLASSPATH << "/Users/yoko/Tools/clojure-1.3.0"
=> ["file:/Users/yoko/Tools/clojure-1.3.0/"]
jruby-1.6.4 :003 > require 'clojure-1.3.0-slim'
=> true
jruby-1.6.4 :004 > h = {"a" => 100, "b" => 300, "c" => 200}
=> {"a"=>100, "b"=>300, "c"=>200}
jruby-1.6.4 :005 > pmap = Java::clojure.lang.PersistentHashMap.create(h)
=> {"a"=>100, "b"=>300, "c"=>200}
jruby-1.6.4 :006 > pmap.class
=> Java::ClojureLang::PersistentHashMap

OK. I could successfully create Clojure's PersistentHashMap object. PersistentHashMap implements java.util.Map interface, which means the object has all of Ruby's Hash methods.

jruby-1.6.4 :007 > pmap.each {|k, v| puts "#{k} is #{v}"}
a is 100
b is 300
c is 200
=> {"a"=>100, "b"=>300, "c"=>200}
jruby-1.6.4 :008 > pmap.select {|k, v| k > "a"}
=> [["b", 300], ["c", 200]]
jruby-1.6.4 :009 > pmap.has_value?(400)
=> false

PersistenHasMap is immutable. So, when the method tries to change its data, Clojure raises exception:

jruby-1.6.4 :012 > pmap.delete_if {|k, v| k >= "b"}
Java::JavaLang::UnsupportedOperationException:
from clojure.lang.APersistentMap.remove(APersistentMap.java:273)
from org.jruby.java.proxies.MapJavaProxy$RubyHashMap.internalDelete(MapJavaProxy.java:157)
from org.jruby.RubyHash.delete(RubyHash.java:1407)
(snip)

This way, we can use Clojure's immutable data types on JRuby.

Thursday, September 08, 2011

Haml on Clojure Web App

I've wrote a couple of blog posts about making RubyGems work on JVM languages over RedBridge. Clojure is among them. So far, I could successfully make simple examples with DataMapper and UUID RubyGems. This time, I tackled a Clojure web app. The Rubygems to mix in was Haml.


To create a Clojure web app, I used Leiningen (https://github.com/technomancy/leiningen) and Ring (https://github.com/mmcgrana/ring) like I did when I wrote the blog post, JRuby on Heroku via Clojure.


I installed Leiningen, then followed the instruction:
$ lein new helloworld
$ cd helloworld
$ vi project.clj


My project.clj became below in the end:
(defproject hello-world "0.0.1"
:dependencies
[[org.clojure/clojure "1.2.1"]
[org.clojure/clojure-contrib "1.2.0"]
[org.jruby/jruby-complete "1.6.4"]
[ring/ring-jetty-adapter "0.3.9"]])

There were the choices such that [ring "0.3.9"] or others, but this worked well enough. Then, I ran:
$ lein deps

This downloaded all jar archives with dependencies and put them in lib directory. Leiningen deletes all entries in lib directory and downloaded jar archives when project.clj is updated. So, I kept in mind to stay away from lib directory.


Next, I wrote the Clojure web app. I needed to think about was how to load Haml gem. There were two choices, setting a load path and putting a gem to some directory that was listed in a classpath. The former is easier but needs to be careful not to see L/Ruby GEM_HOME that might be already there. So, I chose the latter. For just in case, I also wrote the former way commented out.


I used clojure.contrib.clsaspath to know what directories are in the classpath. As far as I checked, I could use classes and src directories. I chose src, but perhaps, there's no difference between them. I copied haml-3.1.2 and whole stuff under that direcrory from I installed the gem in Ruby way. The very important thing is the name, "haml-3.1.2/lib" should be renamed to something else. Leiningen or some other tool deletes all files under "lib" directory while deploying the app on Heroku. (This never happened on my local env) So, I changed the name from "haml-3.1.2/lib" to "haml-3.1.2/gem" . I really struggled to figure out this fact.


Usually, we write "require 'rubygems'; require 'haml'" , however, I wrote "require 'rubygems'; require 'haml-3.1.2/gem/haml'" in this case. JRuby loads Ruby code from classpath. On the classpath list, I have "src", and 'haml-3.1.2/gem/haml' is a relative path to "src" . The code is below (https://gist.github.com/1205198):

1 (ns demo.gemstoclojure
2 (:use ring.adapter.jetty)
3 (:use clojure.contrib.io)
4 (:use clojure.contrib.classpath))
5
6 (import '(org.jruby.embed ScriptingContainer LocalContextScope))
7 (def c (ScriptingContainer. LocalContextScope/THREADSAFE))
8
9 (println (classpath))
10 (println (pwd))
11
12 ;; Using $LOAD_PATH to load haml gem
13 ;(def gempath [(str (pwd) "/src/haml-3.1.2/gem")])
14 ;(. c setLoadPaths gempath)
15 ;(. c runScriptlet "require 'rubygems'; require 'haml'")
16
17 ;; Using classpath to load haml gem
18 (. c runScriptlet "require 'rubygems'; require 'haml-3.1.2/gem/haml'")
19
20 (def engineclass (. c runScriptlet "Haml::Engine"))
21 (def template
22 "%html
23 %head
24 %title
25 Hello Clojure!
26 %body
27 %h2
28 Hello Clojure from Haml!")
29 (def engine (. c callMethod engineclass "new" template Object))
30
31 (defn app [req]
32 {:status 200
33 :headers {"Content-Type" "text/html"}
34 :body (. c callMethod engine "render" String)})
35
36 (defn -main []
37 (let [port (Integer/parseInt (System/getenv "PORT"))]
38 (run-jetty app {:port port})))

The code above:

  • instantiates ScriptingContainer (line 6-7) (RedBridge!)

  • loads haml gem (line 18)

  • gets Haml::Engine class (line 20)

  • writes haml template (line 21-28)

  • instantiates Haml::Engine with template (line 29)

  • renders haml template (line 34)




Then, I wrote Procfile:

web: lein run -m demo.gemstoclojure



At last, I deployed this app on Heroku, and saw the result:

$ curl http://freezing-autumn-54.herokuapp.com
<html>
<head>
<title>
Hello Clojure!
</title>
</head>
<body>
<h2>
Hello Clojure from Haml!
</h2>
</body>
</html>



Yikes! RubyGems worked on the Clojure web app.

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.