Permalink
Switch branches/tags
Nothing to show
Find file Copy path
Fetching contributors…
Cannot retrieve contributors at this time
197 lines (167 sloc) 7.48 KB
#
# Wraps a Rack application in a Java servlet.
#
# Relevant documentation:
#
# http://rack.rubyforge.org/doc/SPEC.html
# http://java.sun.com/j2ee/sdk_1.3/techdocs/api/javax
# /servlet/http/HttpServlet.html
#
module Trinidad
module Rack
java_import 'javax.servlet.http.HttpServlet'
class RackServlet < HttpServlet
java_import 'java.io.FileInputStream'
#
# Sets the Rack application that handles requests sent to this
# servlet container.
#
def rackup(app)
@app = app
end
#
# Takes an incoming request (as a Java Servlet) and dispatches it to
# the rack application setup via [rackup]. All this really involves
# is translating the various bits of the Servlet API into the Rack
# API on the way in, and translating the response back on the way
# out.
#
# Also, we implement a common extension to the Rack api for
# asynchronous request processing. We supply an 'async.callback'
# parameter in env to the Rack application. If we catch an
# :async symbol thrown by the app, we initiate a Jetty continuation.
#
# When 'async.callback' gets a response with empty headers and an
# empty body, we declare the async response finished.
#
def service(request, response)
# Turn the ServletRequest into a Rack env hash
env = servlet_to_rack(request)
# Add our own special bits to the rack environment so that
# Rack middleware can have access to the Java internals.
env['rack.java.servlet'] = true
env['rack.java.servlet.request'] = request
env['rack.java.servlet.response'] = response
rack_response = @app.call(env)
# For apps that don't throw :async.
unless(rack_response[0] == -1)
# Nope, nothing asynchronous here.
rack_to_servlet(rack_response, response)
return
end
end
private
#
# Turns a Servlet request into a Rack request hash.
#
def servlet_to_rack(request)
# The Rack request that we will pass on.
env = Hash.new
# Map Servlet bits to Rack bits.
env['REQUEST_METHOD'] = request.getMethod
env['QUERY_STRING'] = request.getQueryString.to_s
env['SERVER_NAME'] = request.getServerName
env['SERVER_PORT'] = request.getServerPort.to_s
env['rack.version'] = ::Rack::VERSION
env['rack.url_scheme'] = request.getScheme
env['HTTP_VERSION'] = request.getProtocol
env["SERVER_PROTOCOL"] = request.getProtocol
env['REMOTE_ADDR'] = request.getRemoteAddr
env['REMOTE_HOST'] = request.getRemoteHost
# request.getPathInfo seems to be blank, so we're using the URI.
env['REQUEST_PATH'] = request.getRequestURI
env['PATH_INFO'] = request.getRequestURI
env['SCRIPT_NAME'] = ""
# Rack says URI, but it hands off a URL.
env['REQUEST_URI'] = request.getRequestURL.toString
# Java chops off the query string, but a Rack application will
# expect it, so we'll add it back if present
env['REQUEST_URI'] << "?#{env['QUERY_STRING']}" \
if env['QUERY_STRING']
# JRuby is like the matrix, only there's no spoon or fork().
env['rack.multiprocess'] = false
env['rack.multithread'] = true
env['rack.run_once'] = false
# Populate the HTTP headers.
request.getHeaderNames.each do |header_name|
header = header_name.upcase.tr('-', '_')
env["HTTP_#{header}"] = request.getHeader(header_name)
end
# Rack Weirdness: HTTP_CONTENT_TYPE and HTTP_CONTENT_LENGTH
# both need to have the HTTP_ part dropped.
env["CONTENT_TYPE"] = env.delete("HTTP_CONTENT_TYPE") \
if env["HTTP_CONTENT_TYPE"]
env["CONTENT_LENGTH"] = env.delete("HTTP_CONTENT_LENGTH") \
if env["HTTP_CONTENT_LENGTH"]
# The input stream is a wrapper around the Java InputStream.
env['rack.input'] = request.getInputStream.to_io
# The output stream defaults to stderr.
env['rack.errors'] ||= $stderr
# All done, hand back the Rack request.
return(env)
end
#
# Turns a Rack response into a Servlet response; can be called
# multiple times. Returns true if this is the full request (either
# a synchronous request or the last part of an async request),
# false otherwise.
#
# Note that keep-alive *only* happens if we get either a pathname
# (because we can find the length ourselves), or if we get a
# Content-Length header as part of the response. While we can
# readily buffer the response object to figure out how long it is,
# we have no guarantee that we aren't going to be buffering
# something *huge*.
#
# http://docstore.mik.ua/orelly/java-ent/servlet/ch05_03.htm
#
def rack_to_servlet(rack_response, response)
# Split apart the Rack response.
status, headers, body = rack_response
# We assume the request is finished if we got empty headers,
# an empty body, and we have a committed response.
finished = (headers.empty? and \
body.respond_to?(:empty?) and body.empty?)
return(true) if (finished and response.isCommitted)
# No need to send headers again if we've already shipped
# data out on an async request.
unless(response.isCommitted)
# Set the HTTP status code.
response.setStatus(status)
# Did we get a Content-Length header?
content_length = headers.delete('Content-Length')
response.setContentLength(content_length.to_i) \
if content_length
# Add all the result headers.
headers.each { |h, v| response.addHeader(h, v) }
end
# How else would we write output?
output = response.getOutputStream
# Turn the body into something nice and Java-y.
if(body.respond_to?(:to_path))
# We've been told to serve a file; use FileInputStream to
# stream the file directly to the servlet, because this
# is a lot faster than doing it with Ruby.
file = java.io.File.new(body.to_path)
# We set the content-length so we can use Keep-Alive,
# unless this is an async request.
response.setContentLength(file.length) \
unless content_length
# Stream the file directly.
buffer = Java::byte[4096].new
input_stream = FileInputStream.new(file)
while((count = input_stream.read(buffer)) != -1)
output.write(buffer, 0, count)
end
input_stream.close
else
body.each { |l| output.write(l.to_java_bytes) }
end
# Close the body if we're supposed to.
body.close if body.respond_to?(:close)
# All done.
output.flush
end
end
end
end