Sunday, February 27, 2011

Rails on RedBridge, Scaffolded App to Work

In my blog post, "The second step to Rails on RedBridge", I used Rails simple controller and showed how it could get run on Servlet. That was easy since there was no interaction with a Web browser. That controller just returned a result to the browser. That's it. So, Rails didn't need much information to work. However, a controller-only application isn't common. People start using Rails from scaffolding. Every Rails book talks about scaffolding first. Thus, I tried to make scaffolded Rails app to work on the Servlet. So far so good. Though it was a newbie app, scaffolding has been covered by Rails on RedBridge. The scaffolded app worked via JRuby's embedding API on the Servlet. The code is on github, https://github.com/yokolet/RailsCrossing, and the sample app by Scala is also on github, https://github.com/yokolet/RailsCrossingSamples.

The way to make it work was a bit hard. Rails API doc was not so helpful in terms of hooking it up on the Servlet. To figure out how it should have worked, I used rails console a lot, read sources, watched what were supposed to be in HTTP requests/responses over FireBug, wrote snippets to see what was missing. So, I'm going to write down how RailsCrossing (Rails on RedBridge) made scaffolded app to work.

The idea of RailsCrossing is to hit "SomeController.action('some_action').call(env)" method. Thus,

  1. Find the controller class
  2. Find the action
  3. Stuff all necessary information into "env" hash

are all to make it work.


Finding controller class is not so complicated. For example, we can confirm routes mapping on rails console:

irb(main):001:0> ActionDispatch::Routing::Routes.routes.to_s
=> "GET /Acacia/home/index(.:format) {:controller=>\"Acacia/home\", :action=>\"index\"}GET /Acacia/users(.:format) {:action=>\"index\", :controller=>\"users\"}POST /Acacia/users(.:format) {:action=>\"create\", :controller=>\"users\"}GET /Acacia/users/new(.:format) {:action=>\"new\", :controller=>\"users\"}GET /Acacia/users/:id/edit(.:format) {:action=>\"edit\", :controller=>\"users\"}GET /Acacia/users/:id(.:format) {:action=>\"show\", :controller=>\"users\"}PUT /Acacia/users/:id(.:format) {:action=>\"update\", :controller=>\"users\"}DELETE /Acacia/users/:id(.:format) {:action=>\"destroy\", :controller=>\"users\"}ANY /rails/info/properties(.:format) {:controller=>\"rails/info\", :action=>\"properties\"}"

Odd thing was the scope name "Acacia" appeared in a controller name when I created controller by "generate controller" but not by "generate scaffold." Although I surrounded whole stuff by a scope "/Acaia," routes.rb, it happened. I'm not sure this is intended or a bug, but, anyways, I took the scope name out from to find controller class if it is included.


Finding an action needed closer look at a Rails behavior. As you may know, Rails RESTful app uses GET, POST, PUT, and DELETE HTTP requests for index/show/new/edit, create, update, and destroy actions respectively. Even if the given path is the same, say /Acacia/users/1, it is for show when HTTP method is GET while update when PUT. Rails uses "_method" hidden HTML form field to know the difference. So, I added lines to get that parameter from HTTP request, then put in env hash with a "rack.methodoverride.original_method" key. Also the _method value was used to find out matched action and controller combination.
(See getEnv() and findMatchedRoute() methods in CrossingHelpers.java)


Stuffing all necessary information in the "env" hash was rather baffling. JRuby's Java proxy didn't work well since Rails expected hash keys could be referenced as a symbol. For example, a hash is created by h = {"name" => "foo"}, it should be referenced by h[:name]. JRuby's Java String proxy seemed not to be effective for this usage. To avoid this problem, I directly used JRuby's RubyHash class.

Next problem was keys to assign input parameters. "rack.input" is mandatory, but, as far as sending HTML form parameters, "rack.input" isn't used for that purpose. It is used to compare the value of "rack.request.form_input," and not used afterward as well as "rack.request.form_input." (If input is multipart/form-data (file upload), "rack.input" is used to read binary data, though.) Instead, "rack.request.form_hash" key is used to send input values. There's one more key-value pair involved in sending form parameters. It is "action_dispatch.request.path_parameters." The id, like user id, seems to be expected to be there. I put the hash got by Rails.application.routes.recognize_path("some path") as a value of ""action_dispatch.request.path_parameters." The example of a hash value related to sending form parameters became as in below:

"action_dispatch.request.path_parameters" => {:action=>"show", :controller=>"users", :id=>"1"}
"rack.input" => ""
"rack.request.form_input" => ""
"rack.request.form_hash" => {"utf8"=>"✓", authenticity_token"=>"fi/oOym8i+mZTwM7+h+rZBQL/s9hv62+
mnNRv6mNQnw=", "user"=>{"name"=>"foo", "email"=>"foo@bar"}, "commit"=>"Update User"}


Moreover, there are "flash" messages, such as green "User was successfully updated." Those also should be in "env" hash. This sort of messages are in a HTTP response returned as a result of form input processing. When the response is redirected and back to Rails app, the messages show up to browser. To keep the messages over a sequence of HTTP request/response has been completed, we need cookie like mechanism. Rails uses cookie to save such messages. On Servlet, we can use Servlet API's session, which is available to save an object. So, I took the hash for flash massages out from the response, then put it in Servlet's session without any modification.

String script =
"response = " + route.getName() + ".action('" + route.getAction() + "').call(env)\n" +
"return response[0], response[1], response[2].body, response[2].request.flash";
RubyArray responseArray = (RubyArray)container.runScriptlet(script);
CrossingResponse response = new CrossingResponse();
response.context_path = context_path;
response.status = ((Long)responseArray.get(0)).intValue(); //status code; Fixnum
response.responseHeader = (Map)responseArray.get(1); // response header; Hash
response.body = (String)responseArray.get(2); // response body; HTML
response.flash = (Map) responseArray.get(3); // flash messages; Hash

....

request.getSession().setAttribute("action_dispatch.request.flash_hash", crossingResponse.getFlash()); //save falsh messages to Servlet's session

....

// before sending a request to Rails app
Map map = (Map) request.getSession().getAttribute("action_dispatch.request.flash_hash");
if (map != null) env.put("action_dispatch.request.flash_hash", map);



Serving static assets, such as stylesheets and javascripts, needed to have another way. Even though I added the scope in routes.rb, Rails didn't add the scope name to static assets not like paths to displayed in HTML body part. There should have the Rails way to add some path to static assets always, but I didn't search that. Because I wanted to keep Rails app as much as it is. Instead, RailsCrossing rewrites the URI so that the Servlet context path (the same as the Rails scope name) will be in the URI just before the response is sent back to the browser. RailsCrossing has its own default Servlet (CrossingDefaultServlet.java) to serve such static files. This is necessary so that Servlet Container dispatches the HTTP request to Acacia web app. Unless, the request is dispatched to a Servlet Container's default Servlet that doesn't know where the files are.


Above are what I tried to get scaffolded Rails app to work. Probably, there are the right ways to get this done, but I couldn't find by googling. (I want to know where I should go.)


RailsCrossing is still on the long way to serve Rails app satisfactory, but getting closer and closer. As I wrote, simple scaffolded Rails app has been covered. Next would be Ajax request handling, file upload, complicated database schema, etc. ... maybe, I need to work more.

No comments: