Saturday, August 29, 2009

Yaml doesn't work on Google App Engine

While I was testing JRuby Embed API on Google App Engine, I encountered this awkward problem. Yaml never worked on GAE. Exactly the same Servlet successfully worked on GlassFish. Servlet and Ruby codes were:

# yaml_snippet.rb

require 'yaml'

content = YAML::load @text

def format element
case element
when String: print "<p>#{element}</p>"
when Array:
print "<ul>"
element.each do |child|
print "<li>"
format child
print "</li>"
end
puts "</ul>"
when Hash:
element.each do |key, value|
print "<ul><li>#{key}"
format value
print "</li></ul>"
end
end
end

content.each do |heading, paragraph|
puts "<h4>#{heading}</h4>"
paragraph.each do |element|
format element
end
end

package olive.jruby.example;

import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.io.PrintWriter;
import java.util.Arrays;
import java.util.List;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.jruby.embed.ScriptingContainer;
import org.jruby.javasupport.JavaEmbedUtils.EvalUnit;

public class YamlSampleServlet extends HttpServlet {
private ScriptingContainer container;
private EvalUnit yaml_unit;
private String text =
"Trees:\n" +
"- This is a small example to general HTML.\n" +
"- - Quince\n" +
" - flower: Red\n" +
"- - Apple\n" +
" - fruit: Red\n" +
"- - Maple\n" +
" - leaf: Red";

@Override
public void init() {
String classpath = getServletContext().getRealPath("/WEB-INF/classes");
List<String> loadPaths = Arrays.asList(classpath.split(File.pathSeparator));
container = new ScriptingContainer();
container.getProvider().setLoadPaths(loadPaths);
String filename = "ruby/yaml_snippet.rb";
InputStream istream = container.getRuntime().getJRubyClassLoader().getResourceAsStream(filename);
yaml_unit = container.parse(istream, filename);
}

protected void processRequest(HttpServletRequest request, HttpServletResponse response)
throws ServletException, IOException {
response.setContentType("text/html;charset=UTF-8");
PrintWriter out = response.getWriter();
container.setWriter(out);
try {
out.println("<html>");
out.println("<head>");
out.println("<title>Servlet YamlSampleServlet</title>");
out.println("</head>");
out.println("<body>");
out.println("<h3>Servlet YamlSampleServlet at " + request.getContextPath() + "</h3>");
container.put("@text", text);
yaml_unit.run();
out.println("</pre></body>");
out.println("</html>");
} finally {
out.close();
}
}

@Override
protected void doGet(HttpServletRequest request, HttpServletResponse response)
throws ServletException, IOException {
processRequest(request, response);
}

@Override
protected void doPost(HttpServletRequest request, HttpServletResponse response)
throws ServletException, IOException {
processRequest(request, response);
}

@Override
public String getServletInfo() {
return "Yaml Sample";
}
}

And the programs produced:

<html>
<head>
<title>Servlet YamlSampleServlet</title>
</head>
<body>
<h3>Servlet YamlSampleServlet at /Olive</h3>
<h4>Trees</h4>
<p>This is a small example to general HTML.</p><ul><li><p>Quince</p></li><li><ul><li>flower<p>Red</p></li></ul></li></ul>
<ul><li><p>Apple</p></li><li><ul><li>fruit<p>Red</p></li></ul></li></ul>
<ul><li><p>Maple</p></li><li><ul><li>leaf<p>Red</p></li></ul></li></ul>
</pre></body>
</html>

However, this test servlet never worked on GAE because of the exception:

[java] yaml/constants:15:in `const_missing': uninitialized constant YAML::Yecht::Resolver (NameError)
[java] from yaml:88
[java] from yaml:2:in `require'
[java] from ruby/yaml_snippet.rb:2
[java] ...internal jruby stack elided...
[java] from Module.const_missing(yaml:88)
[java] from (unknown).(unknown)(yaml:2)
[java] from (unknown).(unknown)(yaml:2)
[java] from Kernel.require(ruby/yaml_snippet.rb:2)
[java] from (unknown).(unknown)(:1)

JRuby has yaml library under the "builtin" directory in its source tree, so all of necessary scripts here are in jruby.jar as well as jruby-complete.jar. But, yaml library needs jruby.home system property to be set correctly, so jruby-complete.jar is the only choice on a web application. In case of an application, I could set jruby.home explicitly as in below and got the result as I expected:

package brick;

import java.io.InputStream;
import org.jruby.embed.ScriptingContainer;
import org.jruby.javasupport.JavaEmbedUtils.EvalUnit;

public class YamlSample {

private String filename = "ruby/yaml_snippet.rb";
private String text =
"Trees:\n" +
"- This is a small example to general HTML.\n" +
"- - Quince\n" +
" - flower: Red\n" +
"- - Apple\n" +
" - fruit: Red\n" +
"- - Maple\n" +
" - leaf: Red";

private YamlSample() {
System.setProperty("jruby.home", "/Users/yoko/Works/080909-jruby/jruby~main");
ScriptingContainer container = new ScriptingContainer();
InputStream istream = container.getRuntime().getJRubyClassLoader().getResourceAsStream(filename);
EvalUnit yaml_unit = container.parse(istream, filename);
container.put("@text", text);
yaml_unit.run();
}

public static void main(String[] args) {
new YamlSample();
}
}

I tried to copy all yaml library under the source tree of my web application, and clean & build, redeploy, and request again... with no luck. Sigh... I tried to set the path to the yaml library in jar archive,

String classpath = getServletContext().getRealPath("/WEB-INF/lib/jruby-complete.jar!/builtin");
List loadPaths = Arrays.asList(classpath.split(File.pathSeparator));
container = new ScriptingContainer();
container.getProvider().setLoadPaths(loadPaths);

... no luck. The same exception, as ever.

Since the program worked on GlassFish, I guess the difference in class loading mechanism between GlassFish and GAE might have caused the exception on GAE. But, I haven't figured the culprit out so far. Any idea?

No comments: