public this repo is viewable by everyone
Description: A java servlet filter that implements the rack interface for calling ruby web applications
Clone URL: git://github.com/booleanman/rackinterfacefilter.git
initial import
Fred McCann (author)
2 months ago
commit  9869f46c9d9e16ab35ee240c10a6f02fd16a86ba
tree    890f3c7e961a8ff3742d80ae442126806ecf4721
parent  fc891bf61ad679c7c8d1117092cf4400086ae18a
...
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
...
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
0
@@ -0,0 +1,77 @@
0
+Rack Interface Filter is a simple filter that can be deployed to a servlet container. On startup, the filter
0
+instantiates a JRuby instance, bootstraps a ruby web framework, then instantiates a rack adaptor for that framework.
0
+When an request comes in, the filter translates the request to a RubyHash and passes it to the rack adaptor's call method.
0
+
0
+This is very much a work in progress, and the only framework with which it has been used is Merb. There is currently no
0
+support for bundling up a ruby web application as a war file, though it shouldn't be hard to add.
0
+
0
+To test this with a merb application, add a WEB-INF directory to the public directory:
0
+
0
+/MerbApp
0
+ /app
0
+ /config
0
+ /public
0
+ /WEB-INF
0
+ /classes
0
+ /lib
0
+ bsf.jar
0
+ commons-logging-1.x.jar
0
+ jruby.jar
0
+ RackInterface-0.x.jar
0
+ web.xml
0
+ Rakefile
0
+ /spec
0
+
0
+
0
+Sample web.xml:
0
+
0
+<?xml version="1.0" encoding="UTF-8"?>
0
+<!DOCTYPE web-app PUBLIC "-//Sun Microsystems, Inc.//DTD Web Application 2.3//EN" "http://java.sun.com/dtd/web-app_2_3.dtd">
0
+<web-app>
0
+ <display-name>Rack Test</display-name>
0
+
0
+ <distributable/>
0
+
0
+ <filter>
0
+   <filter-name>RackInterface</filter-name>
0
+   <filter-class>com.zumisoft.rack.RackFilter</filter-class>
0
+ <init-param>
0
+ <param-name>framework</param-name>
0
+ <param-value>merb</param-value>
0
+ </init-param>
0
+   </filter>
0
+
0
+   <filter-mapping>
0
+   <filter-name>RackInterface</filter-name>
0
+   <url-pattern>/*</url-pattern>
0
+   </filter-mapping>
0
+
0
+ <session-config>
0
+ <session-timeout>30</session-timeout>
0
+ </session-config>
0
+</web-app>
0
+
0
+
0
+To test this with tomcat, create the file
0
+
0
+$CATALINA_HOME/conf/Catalina/localhost/ROOT.xml:
0
+
0
+<?xml version="1.0" encoding="UTF-8"?>
0
+<Context docBase="/path/to/merb/app/public" path="" />
0
+
0
+
0
+From here, you should have the merb app deployed to the root of the server (though it should be possible to map it to
0
+some other url).
0
+
0
+Start up tomcat, and try to hit the app.
0
+
0
+The placement of WEB-INF, the general assumptions about directory layouts, and war packaging are all fluctuating at the moment. This layout seemed like the quickest way to get a test with Merb running.
0
+
0
+There are a few issues already. For one, sometimes the rack adaptor is returning a <File> object as the third element
0
+in the response tuple. I'm not sure what to do about that yet. If strings are returned, everything works. As a workaround
0
+for that, you can specify ignore paths ("/images", "/stylesheets", "/javascript", "/favicon.ico" by default). If a reuest
0
+is made for a url that starts with an ignore path, it's passed to the servlet container's default servlet. Right now, this is just passing through requests for static content. My guess is that the default servlet is better at handling static content anyway,
0
+but this behavior might have to change. You could also use the ignore path to allow requests to flow through to other servlets or
0
+filters to stack ruby web apps or regular java servlets.
0
+
0
+As a disclaimer, I clearly have no idea what I'm doing. Any help or suggestions would be great.
...
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
0
...
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
0
@@ -0,0 +1,71 @@
0
+<project name="RackInterface" default="compile" basedir=".">
0
+ <property file="build.properties"/>
0
+ <property file="${user.home}/build.properties"/>
0
+ <property name="app.name" value="RackInterface"/>
0
+ <property name="app.version" value="0.1"/>
0
+ <property name="build.home" value="${basedir}/build"/>
0
+ <property name="dist.home" value="${basedir}/dist"/>
0
+ <property name="src.home" value="${basedir}/src"/>
0
+ <property name="lib.dir" value="${basedir}/lib"/>
0
+ <property name="build-lib.dir" value="${basedir}/build-lib"/>
0
+ <property name="compile.debug" value="true"/>
0
+ <property name="compile.deprecation" value="false"/>
0
+ <property name="compile.optimize" value="true"/>
0
+
0
+ <path id="compile.classpath">
0
+ <fileset dir="${lib.dir}">
0
+ <include name="*.jar"/>
0
+ </fileset>
0
+ <fileset dir="${build-lib.dir}">
0
+ <include name="*.jar"/>
0
+ </fileset>
0
+ </path>
0
+
0
+ <target name="all" depends="clean,compile" description="Clean build and dist directories, then compile"/>
0
+
0
+ <target name="clean" description="Delete old build and dist directories">
0
+ <delete dir="${build.home}"/>
0
+ <delete dir="${dist.home}"/>
0
+ </target>
0
+
0
+ <target name="compile" depends="prepare" description="Compile Java sources">
0
+
0
+ <!-- Compile Java classes as necessary -->
0
+ <javac srcdir="${src.home}"
0
+ destdir="${build.home}"
0
+ debug="${compile.debug}"
0
+ deprecation="${compile.deprecation}"
0
+ optimize="${compile.optimize}">
0
+ <classpath refid="compile.classpath"/>
0
+ </javac>
0
+
0
+ <!-- Copy application resources -->
0
+ <copy todir="${build.home}">
0
+ <fileset dir="${src.home}" excludes="**/*.java"/>
0
+ </copy>
0
+ </target>
0
+
0
+ <target name="dist" depends="compile,javadoc" description="Create binary distribution">
0
+ <!-- Create application JAR file -->
0
+ <jar jarfile="${dist.home}/${app.name}-${app.version}.jar" basedir="${build.home}"/>
0
+ </target>
0
+
0
+
0
+ <target name="javadoc" depends="compile" description="Create Javadoc API documentation">
0
+
0
+ <mkdir dir="${dist.home}/docs/api"/>
0
+ <javadoc sourcepath="${src.home}"
0
+ destdir="${dist.home}/docs/api"
0
+ packagenames="*">
0
+ <classpath refid="compile.classpath"/>
0
+ </javadoc>
0
+
0
+ </target>
0
+
0
+ <target name="prepare">
0
+ <!-- Create build directories as needed -->
0
+ <mkdir dir="${build.home}"/>
0
+ <mkdir dir="${dist.home}"/>
0
+ </target>
0
+  
0
+</project>
0
\ No newline at end of file
...
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
...
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
0
@@ -0,0 +1,42 @@
0
+package com.zumisoft.rack;
0
+
0
+import java.io.BufferedInputStream;
0
+import org.apache.commons.logging.Log;
0
+import org.apache.commons.logging.LogFactory;
0
+import org.jruby.Ruby;
0
+import org.jruby.runtime.builtin.IRubyObject;
0
+
0
+/**
0
+ * Creates a rack adapter for a specific ruby web framework
0
+ *
0
+ * @author Fred McCann
0
+ */
0
+public class RackAdapterBuilder {
0
+ private static Log log = LogFactory.getLog(RackAdapterBuilder.class);
0
+
0
+  /**
0
+   * Runs bootstrap code for specified framework and returns a live rack adaptor
0
+   *
0
+   * @param runtime The ruby runtime
0
+   * @param name The name of the web framework (case insensitive)
0
+   * @return The ruby object that acts as the rack interface to the running webapp
0
+   */
0
+  public static IRubyObject getAdaptorForFramework(Ruby runtime, String name) {
0
+    log.debug("Find bootstrap for framework: "+name);
0
+  
0
+    String bootstrap = null;
0
+    try {
0
+     bootstrap = RackInterfaceUtils.readInputStream(new BufferedInputStream(
0
+        RackAdapterBuilder.class.getClassLoader().getResourceAsStream(
0
+          "com/zumisoft/rack/bootstrap/"+name.toLowerCase()+".rb")));
0
+    }    
0
+    catch (Exception e) {
0
+      final String message = "Can't find adaptor for framework: " + name;
0
+      log.fatal(message, e);
0
+      throw new RuntimeException(message, e);
0
+    }
0
+    
0
+    log.debug("Bootstrap:\n"+bootstrap.toString());
0
+    return runtime.evalScriptlet(bootstrap.toString());
0
+  }
0
+}
...
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
...
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
0
@@ -0,0 +1,143 @@
0
+package com.zumisoft.rack;
0
+
0
+import java.io.BufferedInputStream;
0
+import java.io.IOException;
0
+import java.util.Enumeration;
0
+import javax.servlet.http.HttpServletRequest;
0
+import org.apache.commons.logging.Log;
0
+import org.apache.commons.logging.LogFactory;
0
+import org.jruby.RubyHash;
0
+import org.jruby.Ruby;
0
+import org.jruby.RubyArray;
0
+import org.jruby.RubyIO;
0
+import org.jruby.RubyStringIO;
0
+import org.jruby.runtime.Block;
0
+import org.jruby.runtime.builtin.IRubyObject;
0
+import org.jruby.util.io.STDIO;
0
+
0
+/**
0
+ * Creates a rack compatible environment hash from request
0
+ *
0
+ * @author Fred McCann
0
+ */
0
+@SuppressWarnings("unchecked")
0
+public class RackEnvironmentBuilder {
0
+ private static Log log = LogFactory.getLog(RackEnvironmentBuilder.class);
0
+ protected static final String HTTP_HEAD_PREPEPEND = "HTTP_";
0
+ protected static final Integer[] SUPPORTED_RACK_VERSION = {new Integer(0), new Integer(1)};
0
+
0
+ // Rack environment keys
0
+ protected static final String REQUEST_METHOD = "REQUEST_METHOD";
0
+ protected static final String REQUEST_URI = "REQUEST_URI";
0
+ protected static final String SCRIPT_NAME = "SCRIPT_NAME";
0
+ protected static final String PATH_INFO = "PATH_INFO";
0
+ protected static final String QUERY_STRING = "QUERY_STRING";
0
+ protected static final String SERVER_NAME = "SERVER_NAME";
0
+ protected static final String SERVER_PORT = "SERVER_PORT";
0
+ protected static final String RACK_VERSION = "rack.version";
0
+ protected static final String RACK_URL_SCHEME = "rack.url_scheme";
0
+ protected static final String RACK_INPUT = "rack.input";
0
+ protected static final String RACK_ERRORS = "rack.errors";
0
+ protected static final String RACK_MULTITHREAD = "rack.multithread";
0
+ protected static final String RACK_MULTIPROCESS = "rack.multiprocess";
0
+ protected static final String RACK_RUN_ONCE = "rack.run_once";
0
+ protected static final String CONTENT_TYPE = "CONTENT_TYPE";
0
+
0
+ /**
0
+ * This is used to construct keys for header names
0
+ *
0
+ * @param header
0
+ * @return the key under which to store the header in the rack env
0
+ */
0
+ private static String httpHeaderKey(final String header) {
0
+ return new StringBuilder(HTTP_HEAD_PREPEPEND).append(header.toUpperCase().replaceAll("-", "_")).toString();
0
+ }
0
+
0
+ /**
0
+ * Translates an HttpServletRequest into a rack compatible hash
0
+ *
0
+ * @param runtime The ruby runtime
0
+ * @param req an HttpServletRequest
0
+ * @return A Ruby hash according to the rack spec
0
+ * @throws IOException
0
+ */
0
+ public static RubyHash buildEnvironment(Ruby runtime, HttpServletRequest req) throws IOException {
0
+ RubyHash env = new RubyHash(runtime);
0
+ log.debug("Creating Rack environment");
0
+
0
+ env.put(runtime.newString(REQUEST_METHOD), runtime.newString(req.getMethod()));
0
+
0
+ // I can't see where this is required in the spec, but Merb wants it
0
+ env.put(runtime.newString(REQUEST_URI), runtime.newString(req.getServletPath()));
0
+
0
+ if (req.getServletPath().equals("/"))
0
+   env.put(runtime.newString(SCRIPT_NAME), runtime.newString(""));
0
+ else
0
+   env.put(runtime.newString(SCRIPT_NAME), runtime.newString(req.getServletPath()));
0
+   
0
+ if (req.getPathInfo() == null)
0
+ env.put(runtime.newString(PATH_INFO), runtime.newString(""));
0
+ else
0
+ env.put(runtime.newString(PATH_INFO), runtime.newString(req.getPathInfo()));
0
+
0
+ if (req.getQueryString() == null)
0
+ env.put(runtime.newString(QUERY_STRING), runtime.newString(""));
0
+ else
0
+ env.put(runtime.newString(QUERY_STRING), runtime.newString(req.getQueryString()));
0
+
0
+ env.put(runtime.newString(SERVER_NAME), runtime.newString(req.getServerName()));
0
+
0
+ String port = Integer.toString(req.getServerPort());
0
+ env.put(runtime.newString(SERVER_PORT), runtime.newString(port));
0
+
0
+ Enumeration<String> headerNames = req.getHeaderNames();
0
+ while (headerNames.hasMoreElements()) {
0
+ String name = headerNames.nextElement();
0
+ String value = req.getHeader(name);
0
+
0
+ if (value != null) {
0
+ final String key = httpHeaderKey(name);
0
+ env.put(runtime.newString(key), runtime.newString(value));
0
+ }
0
+ }
0
+
0
+ RubyArray version = RubyArray.newArray(runtime);
0
+ version.add(SUPPORTED_RACK_VERSION[0]);
0
+ version.add(SUPPORTED_RACK_VERSION[1]);
0
+ env.put(runtime.newString(RACK_VERSION), version);
0
+
0
+ env.put(runtime.newString(RACK_URL_SCHEME), runtime.newString(req.getScheme()));
0
+    
0
+ // Read request's input stream and package it up as a StringIO object for consumption
0
+    String body = null;
0
+    try {
0
+     body = RackInterfaceUtils.readInputStream(new BufferedInputStream(req.getInputStream()));
0
+    
0
+     if (log.isDebugEnabled())
0
+       log.debug("Request body: "+body);
0
+    
0
+     RubyStringIO requestBody = (RubyStringIO)RubyStringIO.createStringIOClass(runtime)
0
+       .newInstance(new IRubyObject[]{runtime.newString(body.toString())}, Block.NULL_BLOCK);
0
+      env.put(runtime.newString(RACK_INPUT), requestBody);  
0
+      log.debug("SIZE = "+requestBody.size());
0
+    }
0
+    catch (Exception e) {
0
+      final String message = "Error reading request: "+e.getMessage();
0
+      log.error(message, e);
0
+      throw new RuntimeException(message, e);
0
+    }
0
+
0
+ env.put(runtime.newString(RACK_ERRORS), new RubyIO(runtime, STDIO.ERR));
0
+ env.put(runtime.newString(RACK_MULTITHREAD), runtime.newBoolean(true)); // I think this is kosher. Maybe should be a setting?
0
+ env.put(runtime.newString(RACK_MULTIPROCESS), runtime.newBoolean(false)); // I think this is kosher. Maybe should be a setting?
0
+ env.put(runtime.newString(RACK_RUN_ONCE), runtime.newBoolean(false));
0
+
0
+ // I don't see this in the Rack spec, but Merb certainly wants it
0
+ env.put(runtime.newString(CONTENT_TYPE), req.getContentType());
0
+
0
+ if (log.isDebugEnabled())
0
+   log.debug("env = " + env.inspect());
0
+
0
+ return env;
0
+ }
0
+}
...
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
...
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
0
@@ -0,0 +1,52 @@
0
+package com.zumisoft.rack;
0
+
0
+import java.io.IOException;
0
+import javax.servlet.Filter;
0
+import javax.servlet.FilterChain;
0
+import javax.servlet.FilterConfig;
0
+import javax.servlet.ServletException;
0
+import javax.servlet.ServletRequest;
0
+import javax.servlet.ServletResponse;
0
+import javax.servlet.http.HttpServletRequest;
0
+import javax.servlet.http.HttpServletResponse;
0
+import org.apache.commons.logging.Log;
0
+import org.apache.commons.logging.LogFactory;
0
+
0
+/**
0
+ * A filter that intercepts all requests and sends them to a ruby web framework
0
+ * via the rack interface
0
+ *
0
+ * @author Fred McCann
0
+ */
0
+public class RackFilter implements Filter {
0
+  private static Log log = LogFactory.getLog(RackFilter.class);  
0
+ private RackRequestHandler handler = null;
0
+
0
+ /**
0
+ * Currently sets up a single instance of a RackRequestHandler and initializes it.
0
+ * This could be changed to work with a pool of handlers if needed
0
+ */
0
+  public void init(FilterConfig config) throws ServletException {
0
+    log.debug("Initializing RackFilter");
0
+    handler = new RackRequestHandler(config);
0
+ }
0
+
0
+  /**
0
+   * Examines incoming requests. If the request matches an ignore path,
0
+   * it's passed on to other filters / servlets
0
+   */
0
+  public void doFilter(ServletRequest req, ServletResponse resp,
0
+      FilterChain chain) throws IOException, ServletException {
0
+    log.debug("Call RackFilter");     
0
+    if (!handler.call((HttpServletRequest)req, (HttpServletResponse)resp))
0
+      chain.doFilter (req, resp);      
0
+  }
0
+
0
+  /**
0
+   * Shuts down the existing RackRequestHandler
0
+   */
0
+  public void destroy() {
0
+    log.debug("Destroying RackFilter");
0
+    handler.shutdown();
0
+  }
0
+}
...
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
...
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
0
@@ -0,0 +1,34 @@
0
+package com.zumisoft.rack;
0
+
0
+import java.io.IOException;
0
+import java.io.InputStream;
0
+
0
+/**
0
+ * Collection of utility methods
0
+ *
0
+ * @author Fred McCann
0
+ */
0
+public class RackInterfaceUtils {
0
+  private static final int BUFFER_SIZE = 16384;
0
+
0
+  /**
0
+   * Reads some input stream and returns a String. Closes stream when finished.
0
+   */
0
+  public static String readInputStream(InputStream stream) throws IOException {
0
+    StringBuffer str = new StringBuffer();
0
+    try {
0
+     byte[] buffer = new byte[BUFFER_SIZE];      
0
+     int len = 0;
0
+     while ( (len = stream.read(buffer)) > 0 )
0
+       str.append(new String(buffer, 0, len));      
0
+    }
0
+    catch (IOException ioe) {
0
+      throw ioe;
0
+    }
0
+    finally {
0
+      stream.close();
0
+    }
0
+    
0
+    return str.toString();
0
+  }
0
+}
...
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
...
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
0
@@ -0,0 +1,195 @@
0
+package com.zumisoft.rack;
0
+
0
+import java.io.File;
0
+import java.io.IOException;
0
+import java.io.PrintWriter;
0
+import java.util.ArrayList;
0
+import java.util.Iterator;
0
+import java.util.List;
0
+import javax.servlet.FilterConfig;
0
+import javax.servlet.http.HttpServletRequest;
0
+import javax.servlet.http.HttpServletResponse;
0
+import org.apache.commons.logging.Log;
0
+import org.apache.commons.logging.LogFactory;
0
+import org.jruby.Ruby;
0
+import org.jruby.RubyArray;
0
+import org.jruby.RubyHash;
0
+import org.jruby.javasupport.JavaEmbedUtils;
0
+import org.jruby.runtime.builtin.IRubyObject;
0
+import static org.jruby.util.KCode.UTF8;
0
+
0
+/**
0
+ * Contains an embedded ruby interpreter with a ruby web framework loaded.
0
+ * Requests are translated into Rack compatible hashes and are passes to the framework
0
+ * for processing.
0
+ *
0
+ * @author Fred McCann
0
+ */
0
+@SuppressWarnings("unchecked")
0
+public class RackRequestHandler {
0
+ private static Log log = LogFactory.getLog(RackRequestHandler.class);
0
+ private static final String RACK_METHOD = "call";
0
+ private static final String PS = File.separator;
0
+ private static final String RUBY_VERSION = "1.8";
0
+ private Ruby runtime = null;
0
+ private IRubyObject rackAdapter = null;
0
+ private final String APP_HOME;
0
+ private final String JRUBY_HOME;
0
+ private String[] ignorePaths = new String[0];
0
+
0
+ /**
0
+ * Create a new request handler
0
+ *
0
+ * <p>Responds to the mandatory filter init-params:</p>
0
+ * <ul>
0
+ * <li>framework: The name of the ruby framework to use (only support Merb at the moment, though ashould be able to add others I hope)/li>
0
+ *  </ul>
0
+ * <p>Responds to the optional filter init-params:</p>
0
+ * <ul>
0
+ * <li>APP_HOME: The home directory of the reby web app</li>
0
+ * <li>JRUBY_HOME: Home directory of JRuby installtion</li>
0
+ * <li>ignorePaths: Paths from which serve as static files or to pass processing to other servlets. Defaults to /stylesheets, /images, /favicon.ico, and /javascript</li>
0
+ * </ul>
0
+ */
0
+ public RackRequestHandler(FilterConfig config) {
0
+ log.info("Creating new RubyRequestHandler instance");
0
+
0
+ // Configure ignore paths
0
+ String pathCfg = config.getInitParameter("ignorePaths");
0
+ if (pathCfg != null) {
0
+ this.ignorePaths = pathCfg.split("\\s+");
0
+ }
0
+ else {
0
+   ignorePaths = new String[]{"/images", "/stylesheets", "/javascript", "/favicon.ico"};
0
+ }
0
+
0
+ if (log.isInfoEnabled())
0
+   for (int x=0; x<ignorePaths.length; x++)
0
+     log.info(ignorePaths[x] + " is configured as an ignore path.");
0
+
0
+ // If jruby home is not sepcified for us, try to find it in the environment
0
+ if (config.getInitParameter("JRUBY_HOME") != null)
0
+ JRUBY_HOME = config.getInitParameter("JRUBY_HOME");
0
+ else
0
+ JRUBY_HOME = System.getenv("JRUBY_HOME");
0
+
0
+ if (JRUBY_HOME == null) {
0
+ final String message = "JRUBY_HOME is not set. This must be " +
0
+ "provided by the envoironment or set as as " +
0
+ "init-parameter in web.inf";
0
+ log.fatal(message);
0
+ throw new RuntimeException(message);
0
+ }
0
+
0
+ log.info("Using JRUBY_HOME = "+JRUBY_HOME);
0
+
0
+ // Try to find the root directory of the ruby web app. If not specified, assume that it
0
+ // is the parent directory of the public root
0
+ if (config.getInitParameter("APP_HOME") != null)
0
+   APP_HOME = config.getInitParameter("APP_HOME");
0
+ else {
0
+   File publicRoot = new File(config.getServletContext().getRealPath(PS));
0
+   APP_HOME = publicRoot.getParent();
0
+ }
0
+ log.info("Using APP_HOME = "+APP_HOME);
0
+
0
+ // JRuby Paths
0
+ List<String> loadPath = new ArrayList<String>();
0
+ loadPath.add(JRUBY_HOME+PS+"lib"+PS+"ruby"+PS+"site_ruby"+PS+RUBY_VERSION);
0
+ loadPath.add(JRUBY_HOME+PS+"lib"+PS+"ruby"+PS+"site_ruby");
0
+ loadPath.add(JRUBY_HOME+PS+"lib"+PS+"ruby"+PS+RUBY_VERSION);
0
+ loadPath.add(JRUBY_HOME+PS+"lib"+PS+"ruby"+PS+RUBY_VERSION+PS+"java");
0
+
0
+ // Set up Ruby Runtime
0
+ this.runtime = JavaEmbedUtils.initialize(loadPath);
0
+ this.runtime.setKCode(UTF8);
0
+ this.runtime.setJRubyHome(JRUBY_HOME);
0
+ this.runtime.setCurrentDirectory(APP_HOME);
0
+
0
+ // Instantiate rack adapter
0
+ try {
0
+   final String framework = config.getInitParameter("framework");
0
+   rackAdapter = RackAdapterBuilder.getAdaptorForFramework(runtime, framework);
0
+ log.debug("Loaded "+framework+" rack adapter: " + rackAdapter.inspect());
0
+ }
0
+ catch (Exception e) {
0
+ String message = "Can't load rack adapter.";
0
+ log.error(message, e);
0
+ throw new RuntimeException(message, e);
0
+ }
0
+ }
0
+
0
+ /**
0
+ * Handle a request
0
+ *
0
+ * @param req
0
+ * @param resp
0
+ * @throws java.io.IOException
0
+ * @return boolean false if url is matched by an ignore path
0
+ */
0
+ public boolean call(HttpServletRequest req, HttpServletResponse resp) throws IOException {
0
+ // see if this request is to a static path
0
+ String url = req.getServletPath();
0
+ log.debug("URL = "+url);
0
+ for (int x=0; x<ignorePaths.length; x++) {
0
+ if (url.startsWith(ignorePaths[x])) {
0
+ log.debug(url + " matches ignore path " + ignorePaths[x]);
0
+ return false;
0
+ }
0
+ }
0
+
0
+ // Create Rack env parameter
0
+ RubyHash env = RackEnvironmentBuilder.buildEnvironment(runtime, req);
0
+
0
+ // Call Rack adapter
0
+ RubyArray responseArray = null;
0
+ try {
0
+ responseArray = (RubyArray)JavaEmbedUtils.invokeMethod(
0
+ runtime, rackAdapter, RACK_METHOD, new IRubyObject[]{env}, Object.class);
0
+ log.debug("Response from adapter: " + responseArray.inspect());
0
+ }
0
+ catch (Exception e) {
0
+ String message = "There was an error calling the rack adapter.";
0
+ log.error(message, e);
0
+ throw new RuntimeException(message, e);
0
+ }
0
+
0
+ // Write Response
0
+ Integer responseStatus = Integer.valueOf(responseArray.get(0).toString());
0
+ RubyHash responseHeaders = (RubyHash) responseArray.get(1);
0
+ Object responseBody = (Object) responseArray.get(2);
0
+
0
+ resp.setStatus(responseStatus);
0
+
0
+ Iterator<String> i = responseHeaders.keys().iterator();
0
+ while (i.hasNext()) {
0
+ String header = i.next();
0
+ String value = (String) responseHeaders.get(header);
0
+ resp.setHeader(header, value);
0
+ }
0
+
0
+ PrintWriter bodyWriter = resp.getWriter();
0
+ log.debug("Response is of class "+responseBody.getClass().getCanonicalName());
0
+
0
+ // Right now this plays well with strings and not much else. I've seen some <File> objects come along.
0
+ // They will likely need to be dealt with
0
+ if (responseBody instanceof Object[]) {
0
+ Object[] arr = (Object[])responseBody;
0
+ for (int x = 0; x < arr.length; x++)
0
+ bodyWriter.write(arr[x].toString());
0
+ }
0
+ else {
0
+ bodyWriter.write(responseBody.toString());
0
+ }
0
+
0
+ return true;
0
+ }
0
+
0
+ /**
0
+ * Call this before disposing of the handler to properly shutdown the ruby runtime
0
+ */
0
+ public void shutdown() {
0
+ log.info("Shutting down request handler.");
0
+ runtime.tearDown();
0
+ }
0
+}
...
 
 
 
 
0
...
1
2
3
4
5
0
@@ -0,0 +1,4 @@
0
+require 'rubygems'
0
+require 'merb-core'
0
+Merb.start
0
+Merb::Rack::Application.new
0
\ No newline at end of file

Comments

    No one has commented yet.