rubylet 0.1.0-java
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- data/MIT-LICENSE +20 -0
- data/VERSION +1 -0
- data/lib/rubylet.rb +2 -0
- data/lib/rubylet/environment.rb +138 -0
- data/lib/rubylet/errors.rb +23 -0
- data/lib/rubylet/input.rb +31 -0
- data/lib/rubylet/logger.rb +38 -0
- data/lib/rubylet/mem_file_io.rb +80 -0
- data/lib/rubylet/rewindable_io.rb +66 -0
- data/lib/rubylet/servlet.rb +149 -0
- data/lib/rubylet/session.rb +33 -0
- data/lib/rubylet/tee_io.rb +43 -0
- data/spec/rubylet/mem_file_io_spec.rb +68 -0
- data/spec/rubylet/rewindable_io_spec.rb +39 -0
- data/spec/rubylet/tee_io_spec.rb +75 -0
- data/spec/spec_helper.rb +3 -0
- metadata +123 -0
data/MIT-LICENSE
ADDED
@@ -0,0 +1,20 @@
|
|
1
|
+
Copyright (c) 2012 Common Ground Publishing
|
2
|
+
|
3
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
4
|
+
a copy of this software and associated documentation files (the
|
5
|
+
"Software"), to deal in the Software without restriction, including
|
6
|
+
without limitation the rights to use, copy, modify, merge, publish,
|
7
|
+
distribute, sublicense, and/or sell copies of the Software, and to
|
8
|
+
permit persons to whom the Software is furnished to do so, subject to
|
9
|
+
the following conditions:
|
10
|
+
|
11
|
+
The above copyright notice and this permission notice shall be
|
12
|
+
included in all copies or substantial portions of the Software.
|
13
|
+
|
14
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
15
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
16
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
17
|
+
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
18
|
+
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
19
|
+
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
20
|
+
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/VERSION
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
0.1.0
|
data/lib/rubylet.rb
ADDED
@@ -0,0 +1,138 @@
|
|
1
|
+
require 'monitor'
|
2
|
+
|
3
|
+
require 'rubylet/errors'
|
4
|
+
require 'rubylet/logger'
|
5
|
+
require 'rubylet/session'
|
6
|
+
|
7
|
+
class Rubylet::Environment < Hash
|
8
|
+
include MonitorMixin
|
9
|
+
|
10
|
+
def initialize(req, servlet)
|
11
|
+
super() # required to initialize MonitorMixin
|
12
|
+
|
13
|
+
if req.respond_to?(:isAsyncSupported) && req.isAsyncSupported
|
14
|
+
@async_callback_condition = new_cond
|
15
|
+
@async_callback = nil
|
16
|
+
self['async.callback'] = method(:forward_to_async_callback)
|
17
|
+
end
|
18
|
+
|
19
|
+
load_headers(req)
|
20
|
+
load(req, servlet)
|
21
|
+
end
|
22
|
+
|
23
|
+
def normalize(raw)
|
24
|
+
if raw =~ /^Content-(Type|Length)$/i
|
25
|
+
raw
|
26
|
+
else
|
27
|
+
'HTTP_' + raw
|
28
|
+
end.upcase.gsub(/-/, '_')
|
29
|
+
end
|
30
|
+
private :normalize
|
31
|
+
|
32
|
+
# Copied from JRuby-Rack rack/handler/servlet.rb
|
33
|
+
def load_headers(req)
|
34
|
+
req.getHeaderNames.each do |h|
|
35
|
+
self[normalize(h)] = req.getHeader(h)
|
36
|
+
end
|
37
|
+
end
|
38
|
+
private :load_headers
|
39
|
+
|
40
|
+
# Strip a lone slash and also reduce duplicate '/' to a single
|
41
|
+
# slash.
|
42
|
+
def clean_slashes(str)
|
43
|
+
no_dups = str.gsub(%r{/+}, '/')
|
44
|
+
no_dups == '/' ? '' : no_dups
|
45
|
+
end
|
46
|
+
private :clean_slashes
|
47
|
+
|
48
|
+
# Register an async callback. This block will be called
|
49
|
+
# when a Rack application calls the 'async.callback' proc.
|
50
|
+
#
|
51
|
+
# Calls to 'async.callback' will block until another
|
52
|
+
# thread registers a handler by calling this method.
|
53
|
+
def on_async_callback(&block)
|
54
|
+
synchronize do
|
55
|
+
@async_callback = block
|
56
|
+
@async_callback_condition.broadcast
|
57
|
+
end
|
58
|
+
end
|
59
|
+
|
60
|
+
def forward_to_async_callback(resp)
|
61
|
+
synchronize do
|
62
|
+
# wait until someone sets @async_callback
|
63
|
+
@async_callback_condition.wait_until { @async_callback }
|
64
|
+
# pass resp on to the callback
|
65
|
+
@async_callback.call(resp)
|
66
|
+
end
|
67
|
+
end
|
68
|
+
private :forward_to_async_callback
|
69
|
+
|
70
|
+
# Load the Rack environment from a servlet request and context.
|
71
|
+
#
|
72
|
+
# @param [javax.servlet.http.HttpServletRequest] req
|
73
|
+
# @param [javax.servlet.ServletContext context
|
74
|
+
def load(req, servlet)
|
75
|
+
context = servlet.context
|
76
|
+
|
77
|
+
self['REQUEST_METHOD'] = req.getMethod
|
78
|
+
|
79
|
+
# context path joined with servlet_path, but not nil and empty
|
80
|
+
# string rather than '/'
|
81
|
+
self['SCRIPT_NAME'] =
|
82
|
+
begin
|
83
|
+
context_path = req.context_path || '/'
|
84
|
+
servlet_path = req.servlet_path || ''
|
85
|
+
clean_slashes(context_path + servlet_path)
|
86
|
+
end
|
87
|
+
|
88
|
+
self['PATH_INFO'] = clean_slashes(req.path_info || '')
|
89
|
+
self['QUERY_STRING'] = req.getQueryString || ''
|
90
|
+
self['SERVER_NAME'] = req.getServerName
|
91
|
+
self['SERVER_PORT'] = req.getServerPort.to_s
|
92
|
+
|
93
|
+
self['rack.version'] = ::Rack::VERSION
|
94
|
+
self['rack.url_scheme'] = req.getScheme
|
95
|
+
|
96
|
+
# TODO: this is not rewindable in violation of the Rack
|
97
|
+
# spec. Requiring rewind ability on every request seems
|
98
|
+
# a bit much, particularly since it would be trivial to
|
99
|
+
# wrap the io in a helper buffer as-needed, but should
|
100
|
+
# probably implement this eventually.
|
101
|
+
#
|
102
|
+
# @see http://rack.rubyforge.org/doc/SPEC.html
|
103
|
+
self['rack.input'] = req.getInputStream.to_io
|
104
|
+
|
105
|
+
self['rack.errors'] = Rubylet::Errors.new(context)
|
106
|
+
self['rack.multithread'] = true
|
107
|
+
self['rack.multiprocess'] = false
|
108
|
+
self['rack.run_once'] = false
|
109
|
+
|
110
|
+
if servlet.param('useSessions')
|
111
|
+
self['rack.session'] = Rubylet::Session.new(req)
|
112
|
+
end
|
113
|
+
|
114
|
+
if servlet.param('userLogger')
|
115
|
+
self['rack.logger'] = Rubylet::Logger.new(context)
|
116
|
+
end
|
117
|
+
|
118
|
+
self['REMOTE_ADDR'] = req.getRemoteAddr
|
119
|
+
self['REMOTE_HOST'] = req.getRemoteHost
|
120
|
+
self['REMOTE_USER'] = req.getRemoteUser
|
121
|
+
self['REMOTE_PORT'] = req.getRemotePort.to_s
|
122
|
+
self['REQUEST_PATH'] = req.getPathInfo
|
123
|
+
# note, ruby side is URI, java side is URL
|
124
|
+
self['REQUEST_URI'] =
|
125
|
+
begin
|
126
|
+
q = req.getQueryString
|
127
|
+
req.getRequestURL.to_s + (q ? ('?' + q) : '')
|
128
|
+
end
|
129
|
+
|
130
|
+
self['SERVER_PROTOCOL'] = req.getProtocol
|
131
|
+
self['SERVER_SOFTWARE'] = context.getServerInfo
|
132
|
+
|
133
|
+
self['java.context_path'] = req.getContextPath
|
134
|
+
self['java.path_info'] = req.getPathInfo
|
135
|
+
self['java.servlet_path'] = req.getServletPath
|
136
|
+
end
|
137
|
+
private :load
|
138
|
+
end
|
@@ -0,0 +1,23 @@
|
|
1
|
+
# Implements the rack.errors interface, logging everything via
|
2
|
+
# ServletContext#log
|
3
|
+
module Rubylet
|
4
|
+
class Errors
|
5
|
+
def initialize(context)
|
6
|
+
@context = context
|
7
|
+
end
|
8
|
+
|
9
|
+
def puts(obj)
|
10
|
+
write(obj.to_s)
|
11
|
+
end
|
12
|
+
|
13
|
+
def write(str)
|
14
|
+
@context.log(str)
|
15
|
+
end
|
16
|
+
|
17
|
+
# A no-op for ServletContext#log
|
18
|
+
def flush; end
|
19
|
+
|
20
|
+
# Spec says this must never be called
|
21
|
+
def close; end
|
22
|
+
end
|
23
|
+
end
|
@@ -0,0 +1,31 @@
|
|
1
|
+
require 'java'
|
2
|
+
require 'forwardable'
|
3
|
+
|
4
|
+
# Wraps servlet InputStream with a buffer to support Rack required #rewind.
|
5
|
+
class Rubylet::Input
|
6
|
+
extend Forwardable
|
7
|
+
|
8
|
+
MAX_BUFFER = 500 * 1024 # 500 kiB
|
9
|
+
|
10
|
+
def_delegators :@io, :gets, :read, :each, :close
|
11
|
+
|
12
|
+
def initialize(req)
|
13
|
+
@stream = java.io.BufferedInputStream.new(req.getInputStream,
|
14
|
+
req.getContentLength)
|
15
|
+
if req.getContentLength > 0 && req.getContentLength < MAX_BUFFER
|
16
|
+
@stream.mark(req.getContentLength)
|
17
|
+
else
|
18
|
+
# TODO: buffer to a file or something; use jruby-rack's RewindableInputStream
|
19
|
+
req.getServletContext.log(
|
20
|
+
"WARN: input stream of size #{req.getContentLength}; " +
|
21
|
+
"rack #rewind only supported to first #{MAX_BUFFER/1024} kiB")
|
22
|
+
@stream.mark(MAX_BUFFER)
|
23
|
+
end
|
24
|
+
|
25
|
+
@io = stream.to_io
|
26
|
+
end
|
27
|
+
|
28
|
+
def rewind
|
29
|
+
@stream.reset
|
30
|
+
end
|
31
|
+
end
|
@@ -0,0 +1,38 @@
|
|
1
|
+
require 'rubylet/errors'
|
2
|
+
|
3
|
+
# TODO: should this wrap slf4j or something? Or is it typically overridden
|
4
|
+
# by the rack application?
|
5
|
+
module Rubylet
|
6
|
+
class Logger < Rubylet::Errors
|
7
|
+
|
8
|
+
def info(message = nil, &block)
|
9
|
+
log(:info, message, &block)
|
10
|
+
end
|
11
|
+
|
12
|
+
def debug(message = nil, &block)
|
13
|
+
log(:debug, message, &block)
|
14
|
+
end
|
15
|
+
|
16
|
+
def warn(message = nil, &block)
|
17
|
+
log(:warn, message, &block)
|
18
|
+
end
|
19
|
+
|
20
|
+
def error(message = nil, &block)
|
21
|
+
log(:error, message, &block)
|
22
|
+
end
|
23
|
+
|
24
|
+
def fatal(message = nil, &block)
|
25
|
+
log(:fatal, message, &block)
|
26
|
+
end
|
27
|
+
|
28
|
+
def log(level, message = nil, &block)
|
29
|
+
msg = if message
|
30
|
+
message
|
31
|
+
elsif block_given?
|
32
|
+
yield
|
33
|
+
end
|
34
|
+
|
35
|
+
write("#{level.to_s.upcase}: #{msg}")
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|
@@ -0,0 +1,80 @@
|
|
1
|
+
require 'tempfile'
|
2
|
+
|
3
|
+
# A memory/file IO buffer. Data will be written to a memory buffer up
|
4
|
+
# to a limit, after which a file buffer is used. Supports Rack input
|
5
|
+
# stream operations. Not threadsafe.
|
6
|
+
module Rubylet
|
7
|
+
class MemFileIO
|
8
|
+
DEFAULT_LIMIT = 4096 * 4
|
9
|
+
|
10
|
+
# @param [Integer] limit the max size of the first (memory) buffer
|
11
|
+
def initialize(limit = DEFAULT_LIMIT)
|
12
|
+
@limit = limit
|
13
|
+
@mem = StringIO.new
|
14
|
+
@file = nil
|
15
|
+
end
|
16
|
+
|
17
|
+
def puts(str)
|
18
|
+
for_write(str.size + $/.size).puts(str)
|
19
|
+
end
|
20
|
+
|
21
|
+
def write(str)
|
22
|
+
for_write(str.size).write(str)
|
23
|
+
end
|
24
|
+
|
25
|
+
def gets
|
26
|
+
for_read.gets
|
27
|
+
end
|
28
|
+
|
29
|
+
def read(length = nil, buffer = nil)
|
30
|
+
if buffer
|
31
|
+
for_read.read(length, buffer)
|
32
|
+
else
|
33
|
+
for_read.read(length)
|
34
|
+
end
|
35
|
+
end
|
36
|
+
|
37
|
+
def each(&block)
|
38
|
+
for_read.each(&block)
|
39
|
+
end
|
40
|
+
|
41
|
+
def rewind
|
42
|
+
for_read.rewind
|
43
|
+
end
|
44
|
+
|
45
|
+
def close
|
46
|
+
for_read.close
|
47
|
+
end
|
48
|
+
|
49
|
+
private
|
50
|
+
|
51
|
+
def for_read
|
52
|
+
@mem || @file
|
53
|
+
end
|
54
|
+
|
55
|
+
def switch_to_file
|
56
|
+
mem = @mem
|
57
|
+
@mem = nil
|
58
|
+
mem.rewind
|
59
|
+
@file = make_file(mem.read)
|
60
|
+
end
|
61
|
+
|
62
|
+
def for_write(size)
|
63
|
+
if @file
|
64
|
+
@file
|
65
|
+
elsif (@mem.size + size) <= @limit
|
66
|
+
@mem
|
67
|
+
else
|
68
|
+
switch_to_file
|
69
|
+
@file
|
70
|
+
end
|
71
|
+
end
|
72
|
+
|
73
|
+
def make_file(data)
|
74
|
+
file = Tempfile.new('MemFileBacking')
|
75
|
+
file.unlink
|
76
|
+
file.write(data)
|
77
|
+
file
|
78
|
+
end
|
79
|
+
end
|
80
|
+
end
|
@@ -0,0 +1,66 @@
|
|
1
|
+
require 'rubylet/tee_io'
|
2
|
+
require 'rubylet/mem_file_io'
|
3
|
+
require 'stringio'
|
4
|
+
|
5
|
+
# Wraps an IO with a memory and file backed buffer to support Rack's
|
6
|
+
# required #rewind.
|
7
|
+
#
|
8
|
+
# Only some IO methods are supported.
|
9
|
+
module Rubylet
|
10
|
+
class RewindableIO
|
11
|
+
def initialize(io, limit = MemFileIO::DEFAULT_LIMIT)
|
12
|
+
@buf = MemFileIO.new(limit)
|
13
|
+
@io = TeeIO.new(io, @buf)
|
14
|
+
end
|
15
|
+
|
16
|
+
def gets
|
17
|
+
str = @buf.gets
|
18
|
+
if str.nil? # end of buffer
|
19
|
+
@io.gets
|
20
|
+
elsif str.end_with?($/) # good, we got a whole line
|
21
|
+
str
|
22
|
+
else # partial line
|
23
|
+
str + (@io.gets || '')
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
def read(length = nil, buffer = nil)
|
28
|
+
if length.nil? && buffer.nil?
|
29
|
+
@buf.read + @io.read
|
30
|
+
elsif length.nil?
|
31
|
+
@buf.read(nil, buffer)
|
32
|
+
@io.read(nil, buffer)
|
33
|
+
elsif buffer.nil?
|
34
|
+
str = ''
|
35
|
+
@buf.read(length, str)
|
36
|
+
if (str.size == length)
|
37
|
+
@io.read(length - str.size, str)
|
38
|
+
end
|
39
|
+
str
|
40
|
+
else # both non-nil
|
41
|
+
size0 = buffer.size
|
42
|
+
@buf.read(length, buffer)
|
43
|
+
from_buf = buffer.size - size0
|
44
|
+
if (from_buf < length)
|
45
|
+
@io.read(length - from_buf, buffer)
|
46
|
+
end
|
47
|
+
buffer
|
48
|
+
end
|
49
|
+
end
|
50
|
+
|
51
|
+
def each
|
52
|
+
while s = gets
|
53
|
+
yield s
|
54
|
+
end
|
55
|
+
end
|
56
|
+
|
57
|
+
def rewind
|
58
|
+
@buf.rewind
|
59
|
+
end
|
60
|
+
|
61
|
+
def close
|
62
|
+
@buf.close
|
63
|
+
@io.close
|
64
|
+
end
|
65
|
+
end
|
66
|
+
end
|
@@ -0,0 +1,149 @@
|
|
1
|
+
require 'java'
|
2
|
+
require 'rack'
|
3
|
+
|
4
|
+
module Rubylet
|
5
|
+
# Implements the Servlet API in Ruby as a Rack server.
|
6
|
+
class Servlet
|
7
|
+
include Java::JavaxServlet::Servlet
|
8
|
+
|
9
|
+
attr_reader :servlet_config, :app, :context
|
10
|
+
|
11
|
+
# @param [javax.servlet.ServletConfig] servletConfig
|
12
|
+
def init(servletConfig)
|
13
|
+
@servlet_config = servletConfig
|
14
|
+
@context = servletConfig.getServletContext
|
15
|
+
|
16
|
+
rackup_file = param('rackupFile') || 'config.ru'
|
17
|
+
@app, _opts = Rack::Builder.parse_file(rackup_file)
|
18
|
+
|
19
|
+
if defined?(Rails) && defined?(Rails::Application) && (@app < Rails::Application)
|
20
|
+
servlet_path = @servlet_config.getInitParameter('servletPath')
|
21
|
+
relative_root = if servlet_path
|
22
|
+
File.join(@context.context_path, servlet_path)
|
23
|
+
else
|
24
|
+
@context.context_path
|
25
|
+
end
|
26
|
+
app.config.action_controller.relative_url_root = relative_root
|
27
|
+
ActionController::Base.config.relative_url_root = relative_root
|
28
|
+
end
|
29
|
+
end
|
30
|
+
|
31
|
+
# def param(config, name)
|
32
|
+
# config.getInitParameter(name) ||
|
33
|
+
# config.getServletContext.getInitParameter(name)
|
34
|
+
# end
|
35
|
+
|
36
|
+
def param(name)
|
37
|
+
@servlet_config.getInitParameter(name) ||
|
38
|
+
@servlet_config.getServletContext.getInitParameter(name)
|
39
|
+
end
|
40
|
+
|
41
|
+
def destroy
|
42
|
+
# no-op
|
43
|
+
end
|
44
|
+
|
45
|
+
def to_s
|
46
|
+
"#<#{self.class} @app=#{@app}>"
|
47
|
+
end
|
48
|
+
|
49
|
+
alias :servlet_info :to_s
|
50
|
+
|
51
|
+
# @param [javax.servlet.ServletRequest] req a Java servlet request,
|
52
|
+
# assumed to be an HttpServletRequest
|
53
|
+
#
|
54
|
+
# @param [javax.servlet.ServletResponse] resp a Java servlet response
|
55
|
+
# assumed to be an HttpServletResponse
|
56
|
+
def service(req, resp)
|
57
|
+
env = Environment.new(req, self)
|
58
|
+
|
59
|
+
catch(:async) do
|
60
|
+
status, headers, body = app.call(env) # may throw async
|
61
|
+
throw :async if status == -1 # alternate means of indicating async
|
62
|
+
respond(resp, env, status, headers, body)
|
63
|
+
return # :async not thrown
|
64
|
+
end
|
65
|
+
|
66
|
+
# :async was thrown; this only works since Servlet 3.0
|
67
|
+
# web.xml must also have <async-supported>true<async-supported> in
|
68
|
+
# all servlets and filters in the chain.
|
69
|
+
#
|
70
|
+
# TODO: this works in proof-of-concept! anything more todo?
|
71
|
+
#
|
72
|
+
# TODO: Rack async doesn't seem to be standardized yet... In particular,
|
73
|
+
# Thin provides an 'async.close' that (I think) can be used to
|
74
|
+
# close the response connection after streaming in data asynchronously.
|
75
|
+
# For now, this code only allows a one-time deferal of the body (i.e.
|
76
|
+
# when 'async.callback' is called, the body is sent out and the
|
77
|
+
# connection closed immediately.)
|
78
|
+
#
|
79
|
+
# TODO: because of the above, there isn't a way to quickly send headers
|
80
|
+
# but then delay the body.
|
81
|
+
#
|
82
|
+
# Example Rack application:
|
83
|
+
#
|
84
|
+
# require 'thread'
|
85
|
+
#
|
86
|
+
# class AsyncExample
|
87
|
+
# def call(env)
|
88
|
+
# cb = env['async.callback']
|
89
|
+
#
|
90
|
+
# Thread.new do
|
91
|
+
# sleep 5 # long task, wait for message, etc.
|
92
|
+
# body = ['Hello, World!']
|
93
|
+
# cb.call [200, {'Content-Type' => 'text/plain'}, body]
|
94
|
+
# end
|
95
|
+
#
|
96
|
+
# throw :async
|
97
|
+
# end
|
98
|
+
# end
|
99
|
+
async_context = req.startAsync
|
100
|
+
env.on_async_callback do |(status, headers, body)|
|
101
|
+
resp = async_context.getResponse
|
102
|
+
respond(resp, env, status, headers, body)
|
103
|
+
async_context.complete
|
104
|
+
end
|
105
|
+
end
|
106
|
+
|
107
|
+
private
|
108
|
+
|
109
|
+
def respond(resp, env, status, headers, body)
|
110
|
+
resp.setStatus(status)
|
111
|
+
headers.each do |k, v|
|
112
|
+
resp.setHeader k, v
|
113
|
+
end
|
114
|
+
# commit the response and send the headers to the client
|
115
|
+
resp.flushBuffer
|
116
|
+
|
117
|
+
if body.respond_to? :to_path
|
118
|
+
#env['rack.logger'].warn {
|
119
|
+
# "serving static file with ruby: #{body.to_path}"
|
120
|
+
#}
|
121
|
+
|
122
|
+
# TODO: faster to user pure java implementation? Probably better to
|
123
|
+
# either not have ruby serve static files or to put some cache out
|
124
|
+
# front.
|
125
|
+
write_body(body, resp.getOutputStream) { |part| part.to_java_bytes }
|
126
|
+
else
|
127
|
+
write_body(body, resp.getWriter)
|
128
|
+
end
|
129
|
+
end
|
130
|
+
|
131
|
+
# Write each part of body with writer. Optionally transform each
|
132
|
+
# part with the given block. Flush the writer after each
|
133
|
+
# part. Ensure body is closed if it responds to :close. Close the
|
134
|
+
# writer.
|
135
|
+
def write_body(body, writer)
|
136
|
+
begin
|
137
|
+
body.each do |part|
|
138
|
+
writer.write(block_given? ? yield(part) : part)
|
139
|
+
writer.flush
|
140
|
+
end
|
141
|
+
ensure
|
142
|
+
body.close if body.respond_to?(:close) rescue nil
|
143
|
+
writer.close
|
144
|
+
end
|
145
|
+
end
|
146
|
+
end
|
147
|
+
end
|
148
|
+
|
149
|
+
require 'rubylet/environment'
|
@@ -0,0 +1,33 @@
|
|
1
|
+
class Rubylet::Session
|
2
|
+
|
3
|
+
def initialize(req)
|
4
|
+
@javaSession = req.getSession
|
5
|
+
end
|
6
|
+
|
7
|
+
def store(key, value)
|
8
|
+
@javaSession.setAttribute(key, value)
|
9
|
+
end
|
10
|
+
|
11
|
+
alias_method :[]=, :store
|
12
|
+
|
13
|
+
def fetch(key, default = nil)
|
14
|
+
@javaSession.getAttribute(key) || default
|
15
|
+
end
|
16
|
+
|
17
|
+
alias_method :[], :fetch
|
18
|
+
|
19
|
+
def delete(key)
|
20
|
+
@javaSession.removeAttribute(key)
|
21
|
+
end
|
22
|
+
|
23
|
+
def clear
|
24
|
+
@javaSession.getAttributeNames.each {|key| delete(key) }
|
25
|
+
end
|
26
|
+
|
27
|
+
def to_hash
|
28
|
+
hash = {}
|
29
|
+
@javaSession.getAttributeNames.each {|key| hash[key] = @javaSession.getAttribute(key) }
|
30
|
+
hash
|
31
|
+
end
|
32
|
+
|
33
|
+
end
|
@@ -0,0 +1,43 @@
|
|
1
|
+
# All reads on +io+ are copied to +out+. Only supports IO methods
|
2
|
+
# specified by Rack Input Stream spec.
|
3
|
+
module Rubylet
|
4
|
+
class TeeIO
|
5
|
+
def initialize(io, out)
|
6
|
+
@io = io
|
7
|
+
@out = out
|
8
|
+
end
|
9
|
+
|
10
|
+
# Does not support custom separator per Rack spec
|
11
|
+
def gets
|
12
|
+
str = @io.gets
|
13
|
+
@out.write(str) if str
|
14
|
+
str
|
15
|
+
end
|
16
|
+
|
17
|
+
def read(length = nil, buffer = nil)
|
18
|
+
str = ''
|
19
|
+
ret = @io.read(length, str)
|
20
|
+
@out.write(str)
|
21
|
+
if buffer
|
22
|
+
buffer << str
|
23
|
+
if ret.object_id == str.object_id
|
24
|
+
buffer
|
25
|
+
else
|
26
|
+
ret
|
27
|
+
end
|
28
|
+
else
|
29
|
+
ret
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
def each
|
34
|
+
while s = gets
|
35
|
+
yield s
|
36
|
+
end
|
37
|
+
end
|
38
|
+
|
39
|
+
def close
|
40
|
+
@io.close
|
41
|
+
end
|
42
|
+
end
|
43
|
+
end
|
@@ -0,0 +1,68 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
require 'rubylet/mem_file_io'
|
3
|
+
|
4
|
+
module Rubylet
|
5
|
+
|
6
|
+
describe MemFileIO do
|
7
|
+
|
8
|
+
it 'writes data with puts' do
|
9
|
+
@io = MemFileIO.new
|
10
|
+
@io.puts('hello')
|
11
|
+
@io.rewind
|
12
|
+
@io.read.must_equal "hello#{$/}"
|
13
|
+
end
|
14
|
+
|
15
|
+
it 'writes data with write' do
|
16
|
+
@io = MemFileIO.new
|
17
|
+
@io.write('hello')
|
18
|
+
@io.rewind
|
19
|
+
@io.read.must_equal 'hello'
|
20
|
+
end
|
21
|
+
|
22
|
+
it 'allows multiple rewind' do
|
23
|
+
@io = MemFileIO.new
|
24
|
+
|
25
|
+
@io.write('hello')
|
26
|
+
@io.rewind
|
27
|
+
@io.read.must_equal 'hello'
|
28
|
+
|
29
|
+
@io.write('hello')
|
30
|
+
@io.rewind
|
31
|
+
@io.read.must_equal 'hellohello'
|
32
|
+
end
|
33
|
+
|
34
|
+
it 'overwrites? (FIXME: is this correct?)' do
|
35
|
+
@io = MemFileIO.new
|
36
|
+
|
37
|
+
@io.write('hello')
|
38
|
+
@io.rewind
|
39
|
+
@io.read.must_equal 'hello'
|
40
|
+
|
41
|
+
@io.rewind
|
42
|
+
@io.write('***')
|
43
|
+
@io.rewind
|
44
|
+
@io.read.must_equal '***lo'
|
45
|
+
end
|
46
|
+
|
47
|
+
it 'switches to backing file' do
|
48
|
+
@io = MemFileIO.new(4)
|
49
|
+
@io.write('1234')
|
50
|
+
@io.write('5678')
|
51
|
+
@io.rewind
|
52
|
+
@io.read.must_equal '12345678'
|
53
|
+
end
|
54
|
+
|
55
|
+
it 'returns nil on EOF' do
|
56
|
+
@io = MemFileIO.new
|
57
|
+
@io.read
|
58
|
+
@io.read(1).must_equal nil
|
59
|
+
end
|
60
|
+
|
61
|
+
it 'returns empty string on EOF' do
|
62
|
+
@io = MemFileIO.new
|
63
|
+
@io.read
|
64
|
+
@io.read.must_equal ''
|
65
|
+
end
|
66
|
+
end
|
67
|
+
|
68
|
+
end
|
@@ -0,0 +1,39 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
require 'rubylet/rewindable_io'
|
3
|
+
require 'stringio'
|
4
|
+
|
5
|
+
module Rubylet
|
6
|
+
|
7
|
+
describe RewindableIO do
|
8
|
+
|
9
|
+
data = "some data\nwith newlines\nanother line"
|
10
|
+
|
11
|
+
before :each do
|
12
|
+
@io = StringIO.new
|
13
|
+
@io.write(data)
|
14
|
+
@io.rewind
|
15
|
+
|
16
|
+
@reio = RewindableIO.new(@io)
|
17
|
+
end
|
18
|
+
|
19
|
+
it 'rewinds' do
|
20
|
+
@reio.read.must_equal data
|
21
|
+
@reio.rewind
|
22
|
+
@reio.read.must_equal data
|
23
|
+
end
|
24
|
+
|
25
|
+
it 'rewinds with file spill over' do
|
26
|
+
expected = ''
|
27
|
+
10.times do
|
28
|
+
@io.write(data)
|
29
|
+
expected << data
|
30
|
+
end
|
31
|
+
@io.rewind
|
32
|
+
|
33
|
+
@reio = RewindableIO.new(@io, 32)
|
34
|
+
@reio.read.must_equal expected
|
35
|
+
@reio.rewind
|
36
|
+
@reio.read.must_equal expected
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|
@@ -0,0 +1,75 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
require 'rubylet/tee_io'
|
3
|
+
require 'stringio'
|
4
|
+
|
5
|
+
module Rubylet
|
6
|
+
|
7
|
+
describe TeeIO do
|
8
|
+
|
9
|
+
DATA = "some data\nwith newlines\nanother line"
|
10
|
+
|
11
|
+
before :each do
|
12
|
+
@io = StringIO.new
|
13
|
+
@io.write(DATA)
|
14
|
+
@io.rewind
|
15
|
+
|
16
|
+
@out = StringIO.new
|
17
|
+
|
18
|
+
@tee = TeeIO.new(@io, @out)
|
19
|
+
end
|
20
|
+
|
21
|
+
it 'copies data on read' do
|
22
|
+
@tee.read
|
23
|
+
@out.rewind
|
24
|
+
@out.read.must_equal DATA
|
25
|
+
end
|
26
|
+
|
27
|
+
describe '#gets' do
|
28
|
+
it 'copies data on gets' do
|
29
|
+
@tee.gets
|
30
|
+
@out.rewind
|
31
|
+
@out.read.must_equal "some data\n"
|
32
|
+
end
|
33
|
+
|
34
|
+
it 'returns nil on EOF' do
|
35
|
+
@tee.read
|
36
|
+
@tee.gets.must_equal nil
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
40
|
+
it 'copies data on each' do
|
41
|
+
@tee.each {}
|
42
|
+
@out.rewind
|
43
|
+
@out.read.must_equal DATA
|
44
|
+
end
|
45
|
+
|
46
|
+
it 'copies data byte by byte' do
|
47
|
+
@tee.read(1)
|
48
|
+
@tee.read(1)
|
49
|
+
@out.rewind
|
50
|
+
@out.read.must_equal 'so'
|
51
|
+
end
|
52
|
+
|
53
|
+
it 'reads data into a given buffer' do
|
54
|
+
buf = String.new
|
55
|
+
ret = @tee.read nil, buf
|
56
|
+
|
57
|
+
ret.object_id.must_equal buf.object_id
|
58
|
+
|
59
|
+
buf.must_equal DATA
|
60
|
+
@out.rewind
|
61
|
+
@out.read.must_equal DATA
|
62
|
+
end
|
63
|
+
|
64
|
+
it 'returns nil on EOF' do
|
65
|
+
@tee.read
|
66
|
+
@tee.read(1).must_equal nil
|
67
|
+
end
|
68
|
+
|
69
|
+
it 'returns empty string on EOF' do
|
70
|
+
@tee.read
|
71
|
+
@tee.read.must_equal ''
|
72
|
+
end
|
73
|
+
end
|
74
|
+
|
75
|
+
end
|
data/spec/spec_helper.rb
ADDED
metadata
ADDED
@@ -0,0 +1,123 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: rubylet
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
prerelease:
|
5
|
+
version: 0.1.0
|
6
|
+
platform: java
|
7
|
+
authors:
|
8
|
+
- CG Labs
|
9
|
+
autorequire:
|
10
|
+
bindir: bin
|
11
|
+
cert_chain: []
|
12
|
+
date: 2012-12-13 00:00:00.000000000 Z
|
13
|
+
dependencies:
|
14
|
+
- !ruby/object:Gem::Dependency
|
15
|
+
name: minitest-matchers
|
16
|
+
version_requirements: !ruby/object:Gem::Requirement
|
17
|
+
requirements:
|
18
|
+
- - ! '>='
|
19
|
+
- !ruby/object:Gem::Version
|
20
|
+
version: !binary |-
|
21
|
+
MA==
|
22
|
+
none: false
|
23
|
+
requirement: !ruby/object:Gem::Requirement
|
24
|
+
requirements:
|
25
|
+
- - ! '>='
|
26
|
+
- !ruby/object:Gem::Version
|
27
|
+
version: !binary |-
|
28
|
+
MA==
|
29
|
+
none: false
|
30
|
+
prerelease: false
|
31
|
+
type: :development
|
32
|
+
- !ruby/object:Gem::Dependency
|
33
|
+
name: rake
|
34
|
+
version_requirements: !ruby/object:Gem::Requirement
|
35
|
+
requirements:
|
36
|
+
- - ! '>='
|
37
|
+
- !ruby/object:Gem::Version
|
38
|
+
version: !binary |-
|
39
|
+
MA==
|
40
|
+
none: false
|
41
|
+
requirement: !ruby/object:Gem::Requirement
|
42
|
+
requirements:
|
43
|
+
- - ! '>='
|
44
|
+
- !ruby/object:Gem::Version
|
45
|
+
version: !binary |-
|
46
|
+
MA==
|
47
|
+
none: false
|
48
|
+
prerelease: false
|
49
|
+
type: :development
|
50
|
+
- !ruby/object:Gem::Dependency
|
51
|
+
name: version
|
52
|
+
version_requirements: !ruby/object:Gem::Requirement
|
53
|
+
requirements:
|
54
|
+
- - ~>
|
55
|
+
- !ruby/object:Gem::Version
|
56
|
+
version: !binary |-
|
57
|
+
MQ==
|
58
|
+
none: false
|
59
|
+
requirement: !ruby/object:Gem::Requirement
|
60
|
+
requirements:
|
61
|
+
- - ~>
|
62
|
+
- !ruby/object:Gem::Version
|
63
|
+
version: !binary |-
|
64
|
+
MQ==
|
65
|
+
none: false
|
66
|
+
prerelease: false
|
67
|
+
type: :development
|
68
|
+
description: Java Servlet implementation that forwards to Rack application
|
69
|
+
email:
|
70
|
+
- eng@commongroundpublishing.com
|
71
|
+
executables: []
|
72
|
+
extensions: []
|
73
|
+
extra_rdoc_files: []
|
74
|
+
files:
|
75
|
+
- VERSION
|
76
|
+
- MIT-LICENSE
|
77
|
+
- lib/rubylet.rb
|
78
|
+
- lib/rubylet/mem_file_io.rb
|
79
|
+
- lib/rubylet/environment.rb
|
80
|
+
- lib/rubylet/rewindable_io.rb
|
81
|
+
- lib/rubylet/input.rb
|
82
|
+
- lib/rubylet/tee_io.rb
|
83
|
+
- lib/rubylet/errors.rb
|
84
|
+
- lib/rubylet/session.rb
|
85
|
+
- lib/rubylet/logger.rb
|
86
|
+
- lib/rubylet/servlet.rb
|
87
|
+
- spec/spec_helper.rb
|
88
|
+
- spec/rubylet/rewindable_io_spec.rb
|
89
|
+
- spec/rubylet/mem_file_io_spec.rb
|
90
|
+
- spec/rubylet/tee_io_spec.rb
|
91
|
+
homepage:
|
92
|
+
licenses: []
|
93
|
+
post_install_message:
|
94
|
+
rdoc_options: []
|
95
|
+
require_paths:
|
96
|
+
- lib
|
97
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
98
|
+
requirements:
|
99
|
+
- - ! '>='
|
100
|
+
- !ruby/object:Gem::Version
|
101
|
+
version: !binary |-
|
102
|
+
MA==
|
103
|
+
segments:
|
104
|
+
- 0
|
105
|
+
hash: 2
|
106
|
+
none: false
|
107
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
108
|
+
requirements:
|
109
|
+
- - ! '>='
|
110
|
+
- !ruby/object:Gem::Version
|
111
|
+
version: !binary |-
|
112
|
+
MA==
|
113
|
+
segments:
|
114
|
+
- 0
|
115
|
+
hash: 2
|
116
|
+
none: false
|
117
|
+
requirements: []
|
118
|
+
rubyforge_project:
|
119
|
+
rubygems_version: 1.8.24
|
120
|
+
signing_key:
|
121
|
+
specification_version: 3
|
122
|
+
summary: Java Servlet implementation that forwards to Rack application
|
123
|
+
test_files: []
|