Skip to content
Browse files

Moved to trunk directory.

git-svn-id: http://hessian.rubyforge.org/svn/trunk@18 ad7ab41a-a90a-0410-8376-d282d5f142c6
  • Loading branch information...
1 parent 50ec7bb commit ca0933815d6864d6dc25c852cb3fda68fe637c36 sillen committed
Showing with 488 additions and 0 deletions.
  1. +27 −0 LICENSE
  2. +142 −0 README
  3. +42 −0 Rakefile
  4. +206 −0 lib/hessian.rb
  5. +17 −0 test/servlet_invoker.rb
  6. +54 −0 test/test_hessian_parser.rb
View
27 LICENSE
@@ -0,0 +1,27 @@
+Copyright (c) 2006, Christer Sandberg (chrsan@gmail.com)
+All rights reserved.
+
+Redistribution and use in source and binary forms, with or without
+modification, are permitted provided that the following conditions are met:
+
+ * Redistributions of source code must retain the above copyright notice,
+ this list of conditions and the following disclaimer.
+
+ * Redistributions in binary form must reproduce the above copyright
+ notice, this list of conditions and the following disclaimer in the
+ documentation and/or other materials provided with the distribution.
+
+ * The names of its contributors may be used to endorse or promote
+ products derived from this software without specific prior written
+ permission.
+
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE
+FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
+OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
View
142 README
@@ -0,0 +1,142 @@
+= Ruby Hessian Client
+
+This is a Ruby implementation of the Hessian Binary Web Service Protocol.
+
+== What is Hessian?
+
+Here's the description from the Hessian site at
+{http://www.caucho.com/hessian}[http://www.caucho.com/hessian].
+
+<em>The Hessian binary web service protocol makes web services usable without
+requiring a large framework, and without learning yet another alphabet soup of
+protocols. Because it is a binary protocol, it is well-suited to sending binary
+data without any need to extend the protocol with attachments.</em>
+
+Please consult the Hessian[http://www.caucho.com/hessian] home page or the
+{Hessian 1.0.2}[http://www.caucho.com/resin-3.0/protocols/hessian-1.0-spec.xtp]
+specification for more information.
+
+== Usage
+
+The Hessian Ruby Client acts as a proxy for remote calls to Hessian services.
+
+Hessian services are identified by URI's and the Ruby Hessian Client supports
+the http and https schemes. A call to a Hessian service is made like a regular
+method invocation in Ruby, and the Ruby Hessian Client will transparently
+serialize and deserialize to and from Ruby types.
+
+Hessian's object serialization has 13 types, and the following mappings are
+used in the Ruby Hessian Client.
+
+The 9 primitive types:
+
+binary:: Hessian::Binary
+boolean:: TrueClass or FalseClass
+date:: Time
+double:: Float
+int:: Integer
+long:: Integer
+remote:: Not implemented!
+string:: String
+xml:: String
+
+The 2 combining types:
+
+list:: Array
+map:: Hash
+
+Finally, the 2 special constructs:
+
+null:: NilClass
+ref:: The type referenced
+
+The combining Hessian types can be given an explicit type understood by the
+service. This can be used to specify that a list only contain strings for
+example, or more importantly - this is how to serialize custom types.
+A <i>custom type</i> in this context is all types not conforming to the 13
+types above, such as your own objects etc.
+
+Custom types are serialized using the +map+ with an explicit type
+specified and the underlying Ruby type must be a Hash. To specifiy an explicit
+type wrap the object in a Hessian::TypeWrapper or create the method
++hessian_type+ on that object. See the examples below.
+
+The Ruby Hessian Client will automatically convert instances of the Struct
+class to a Hash but for all other custom types you must create the conversion.
+
+When a service reply (i.e. the return type of the method called) is a custom
+type, the Ruby Hessian Client will always return a Hash since the type returned
+might be unknown in Ruby. You may then convert that Hash to suite your needs.
+
+Errors in a call to a Hessian service will result in a fault that is wrapped as
+a Hessian::HessianException.
+
+== Examples
+
+To create a Hessian::HessianClient instance:
+ client = Hessian::HessianClient.new('http://localhost:8080/echo')
+
+To specify a user and a password for basic authentication:
+ client.user, client.password = 'foo', 'bar'
+
+To create a Hessian::HessianClient using a proxy:
+ proxy = { :host => 'proxy.foo.bar', :port => '80' }
+ client = Hessian::HessianClient.new('http://localhost:8080/echo, proxy)
+
+The valid symbol keys for the proxy hash are:
+[:host] Proxy host
+[:port] Proxy port
+[:user] Proxy user
+[:password] Proxy password
+
+Let's say that the service bound to <tt>/echo</tt> is the following:
+ class Echo
+ def echo(msg)
+ "Echo reply: #{msg}"
+ end
+ end
+
+Then invoking the service call +echo+:
+ client.echo('hessian') => "Echo reply: hessian"
+
+=== Explicit types
+
+To specify an <i>explicitly typed</i> list as a Java string array
+for example (where +foo+ is a method on the Java service used):
+ list = %w(one two three)
+ client.foo(Hessian::TypeWrapper.new('[string', list))
+or
+ list = %w(one two three)
+ def list.hessian_type
+ '[string'
+ end
+ client.foo(list)
+
+Let's pretend that we want to call a Java service that has the following
+interface:
+ public interface PersonService implements Serializable {
+ public Person addPerson(Person person);
+ }
+and where the Person is defined as (could be a JavaBean as well):
+ public interface Person implements Serializable {
+ public int age;
+ public String name;
+ }
+
++Person+ is a custom type so we need to send a Hash with an explicit type
+specified:
+ person = { 'age' => 32, 'name' => 'Christer Sandberg' }
+ reply = client.addPerson(Hessian::TypeWrapper.new('Person', person))
+
+The Ruby Hessian Client will automatically convert a Struct to a Hash so the
+following will also work:
+ person = Struct.new(:age, :name)[32, 'Christer Sandberg']
+ class << person
+ def hessian_type
+ 'Person'
+ end
+ end
+ reply = client.addPerson(person)
+
+The result from these invocations would be a Hash:
+ reply => { 'age' => 32, 'name' => 'Christer Sandberg' }
View
42 Rakefile
@@ -0,0 +1,42 @@
+require 'rubygems'
+require 'rake/gempackagetask'
+require 'rake/testtask'
+require 'rake/rdoctask'
+
+task :default => [:test]
+
+Rake::TestTask.new do |t|
+ t.verbose = true
+ t.warning = true
+end
+
+PKG_VERSION = "0.5.3"
+PKG_FILES = FileList[
+ 'LICENSE',
+ 'Rakefile',
+ 'lib/**/*.rb',
+ 'test/**/test_*.rb'
+]
+
+spec = Gem::Specification.new do |s|
+ s.name = "hessian"
+ s.version = PKG_VERSION
+ s.author = "Christer Sandberg"
+ s.email = "chrsan@gmail.com"
+ s.homepage = "http://www.baanii.se/"
+ s.platform = Gem::Platform::RUBY
+ s.summary = "A Ruby Hessian client."
+ s.files = PKG_FILES.to_a
+ s.require_path = "lib"
+end
+
+Rake::RDocTask.new do |rdoc|
+ rdoc.main = "README"
+ rdoc.rdoc_files.include("README", "lib/**/*.rb")
+ rdoc.options << "-S"
+end
+
+package_task = Rake::GemPackageTask.new(spec) do |pkg|
+ pkg.need_zip = true
+ pkg.need_tar_gz = true
+end
View
206 lib/hessian.rb
@@ -0,0 +1,206 @@
+require 'uri'
+require 'net/http'
+require 'net/https'
+
+module Hessian
+ class TypeWrapper
+ attr_accessor :hessian_type, :object
+ def initialize(hessian_type, object)
+ @hessian_type, @object = hessian_type, object
+ end
+ end
+
+ class Binary
+ attr :data
+ def initialize(data)
+ @data = data.to_s
+ end
+ end
+
+ class HessianException < RuntimeError
+ attr_reader :code
+ def initialize(code)
+ @code = code
+ end
+ end
+
+ class HessianClient
+ attr_accessor :user, :password
+ attr_reader :scheme, :host, :port, :path, :proxy
+ def initialize(url, proxy = {})
+ uri = URI.parse(url)
+ @scheme, @host, @port, @path = uri.scheme, uri.host, uri.port, uri.path
+ raise "Unsupported Hessian protocol: #@scheme" unless @scheme == 'http' || @scheme == 'https'
+ @proxy = proxy
+ end
+
+ def method_missing(id, *args)
+ return invoke(id.id2name, args)
+ end
+
+ private
+ def invoke(method, args)
+ call = HessianWriter.new.write_call method, args
+ header = { 'Content-Type' => 'application/binary' }
+ req = Net::HTTP::Post.new(@path, header)
+ req.basic_auth @user, @password if @user
+ conn = Net::HTTP.new(@host, @port, *@proxy.values_at(:host, :port, :user, :password))
+ conn.use_ssl = true and conn.verify_mode = OpenSSL::SSL::VERIFY_NONE if @scheme == 'https'
+ conn.start do |http|
+ res = http.request(req, call)
+ HessianParser.new.parse_response res.body
+ end
+ end
+
+ class HessianWriter
+ def write_call(method, args)
+ @refs = {}
+ out = [ 'c', '0', '1', 'm', method.length ].pack('ahhan') << method
+ args.each { |arg| out << write_object(arg) }
+ out << 'z'
+ end
+
+ private
+ def write_object(val, hessian_type = nil)
+ return 'N' if val.nil?
+ case val
+ when TypeWrapper: write_object(val.object, val.hessian_type)
+ when Struct: write_object(val.members.inject({}) { |map, m| map[m] = val[m]; map })
+ when Binary: [ 'B', val.data.length ].pack('an') << val.data
+ when String: [ 'S', val.length ].pack('an') << val.unpack('C*').pack('U*')
+ when Integer
+ # Max and min values for integers in Java.
+ if val >= -0x80000000 && val <= 0x7fffffff
+ [ 'I', val ].pack('aN')
+ else
+ "L%s" % to_long(val)
+ end
+ when Float: [ 'D', val ].pack('aG')
+ when Time: "d%s" % to_long(val.to_i * 1000)
+ when TrueClass: 'T'
+ when FalseClass: 'F'
+ when Array
+ ref = write_ref val; return ref if ref
+ t = hessian_type_string(hessian_type, val)
+ str = 'Vt' << t << 'l' << [ val.length ].pack('N')
+ val.each { |v| str << write_object(v) }
+ str << 'z'
+ when Hash
+ ref = write_ref val; return ref if ref
+ str = 'Mt' << hessian_type_string(hessian_type, val)
+ val.each { |k, v| str << write_object(k); str << write_object(v) }
+ str << 'z'
+ else
+ raise "Not implemented for #{val.class}"
+ end
+ end
+
+ def hessian_type_string(hessian_type, object)
+ if hessian_type.nil? && object.respond_to?(:hessian_type)
+ hessian_type = object.hessian_type
+ end
+ hessian_type ? [ hessian_type.length, hessian_type ].pack('na*') : "\000\000"
+ end
+
+ def to_long(val)
+ str, pos = " " * 8, 0
+ 56.step(0, -8) { |o| str[pos] = val >> o & 0x00000000000000ff; pos += 1 }
+ str
+ end
+
+ def write_ref(val)
+ id = @refs[val.object_id]
+ if id
+ [ 'R', id ].pack('aN')
+ else
+ @refs[val.object_id] = @refs.length
+ nil
+ end
+ end
+ end
+
+ class HessianParser
+ def parse_response(res)
+ raise "Invalid response, expected 'r', received '#{res[0,1]}'" unless res[0,1] == 'r'
+ @chunks = []
+ @refs = []
+ @data = res[3..-1]
+ parse_object
+ end
+
+ private
+ def parse_object
+ t = @data.slice!(0, 1)
+ case t
+ when 'f': raise_exception
+ when 's', 'S', 'x', 'X'
+ v = from_utf8(@data.slice!(0, 2).unpack('n')[0])
+ @data.slice!(0, v[1])
+ @chunks << v[0]
+ if 'sx'.include? t
+ parse_object
+ else
+ str = @chunks.join; @chunks.clear; str
+ end
+ when 'b', 'B'
+ v = @data.slice!(0, @data.slice!(0, 2).unpack('n')[0])
+ @chunks << v
+ if t == 'b'
+ parse_object
+ else
+ bytes = @chunks.join; @chunks.clear; Binary.new bytes
+ end
+ when 'I': @data.slice!(0, 4).unpack('N')[0]
+ when 'L': parse_long
+ when 'd': l = parse_long; Time.at(l / 1000, l % 1000 * 1000)
+ when 'D': @data.slice!(0, 8).unpack('G')[0]
+ when 'T': true
+ when 'F': false
+ when 'N': nil
+ when 'R': @refs[@data.slice!(0, 4).unpack('N')[0]]
+ when 'V'
+ # Skip type + type length (2 bytes) if specified.
+ @data.slice!(0, 3 + @data.unpack('an')[1]) if @data[0,1] == 't'
+ # Skip the list length if specified.
+ @data.slice!(0, 5) if @data[0,1] == 'l'
+ @refs << (list = [])
+ list << parse_object while @data[0,1] != 'z'
+ # Get rid of the 'z'.
+ @data.slice!(0, 1)
+ list
+ when 'M'
+ # Skip type + type length (2 bytes) if specified.
+ @data.slice!(0, 3 + @data.unpack('an')[1]) if @data[0,1] == 't'
+ @refs << (map = {})
+ map[parse_object()] = parse_object while @data[0,1] != 'z'
+ # Get rid of the 'z'.
+ @data.slice!(0, 1)
+ map
+ else
+ raise "Invalid type: '#{t}'"
+ end
+ end
+
+ def from_utf8(len = '*')
+ s = @data.unpack("U#{len}").pack('C*')
+ [ s, s.unpack('C*').pack('U*').length ]
+ end
+
+ def parse_long
+ val, o = 0, 56
+ @data.slice!(0, 8).each_byte { |b| val += (b & 0xff) << o; o -= 8 }
+ val
+ end
+
+ def raise_exception
+ # Skip code description.
+ parse_object
+ code = parse_object
+ # Skip message description
+ parse_object
+ msg = parse_object
+ raise HessianException.new(code), msg
+ end
+ end
+ end
+end
View
17 test/servlet_invoker.rb
@@ -0,0 +1,17 @@
+require 'net/http'
+
+HEADER = { 'Content-Type' => 'application/binary' }
+
+call = %w(c 0 1 m).pack('ahha')
+methods = %w(
+ getInt getLong getDouble getFalse getTrue getString getNull
+ getDate getIntArray getObjectArray getArrayInList getMap
+)
+
+methods.each do |m|
+ Net::HTTP.start('localhost', 8080) do |http|
+ res = http.send_request('POST', '/test',
+ call + [ m.length, m ].pack('na*') + 'z', HEADER)
+ p res.body
+ end
+end
View
54 test/test_hessian_parser.rb
@@ -0,0 +1,54 @@
+require 'hessian'
+require 'test/unit'
+
+class HessianParserTest < Test::Unit::TestCase
+ def parse res
+ Hessian::HessianClient::HessianParser.new.parse_response res
+ end
+
+ def test_integer
+ assert_equal 4711, parse("r\001\000I\000\000\022gz")
+ end
+ def test_long
+ assert_equal 2, parse("r\001\000L\000\000\000\000\000\000\000\002z")
+ end
+ def test_double
+ assert_equal 3.4, parse("r\001\000D@\v333333z")
+ end
+ def test_false
+ assert_equal false, parse("r\001\000Fz")
+ end
+ def test_true
+ assert_equal true, parse("r\001\000Tz")
+ end
+ def test_string
+ assert_equal "string", parse("r\001\000S\000\006stringz")
+ end
+ def test_null
+ assert_equal nil, parse("r\001\000Nz")
+ end
+ def test_date
+ time = parse("r\001\000d\000\000\001\010\344\036\332\360z")
+ assert_instance_of Time, time
+ assert_equal '2006-01-19 20:23:13', time.strftime("%Y-%m-%d %H:%M:%S")
+ end
+ def test_integer_array
+ assert_equal [ 1, 2, 3 ], parse([ "r\001\000Vt\000\004[intl\000\000\000\003",
+ "I\000\000\000\001I\000\000\000\002I\000\000\000\003zz" ].join)
+ end
+ def test_array
+ assert_equal [ 'sillen', 32 ], parse([ "r\001\000Vt\000\a[objectl\000\000\000\002",
+ "S\000\006sillenI\000\000\000 zz" ].join)
+ end
+ def test_array_in_array
+ assert_equal [ 'A list', [ 9, 3 ] ], parse([ "r\001\000Vl\000\000\000\002S\000\006",
+ "A listVt\000\022[java.lang.Integerl\000\000\000\002I\000\000\000\t",
+ "I\000\000\000\003zzz" ].join)
+ end
+ def test_map
+ map = { 'sillen' => 32, 'numbers' => [ 1.1, 1.2, 1.3 ] }
+ assert_equal map, parse([ "r\001\000Mt\000\000S\000\anumbersVt\000\a[double",
+ "l\000\000\000\003D?\361\231\231\231\231\231\232D?\363333333D?",
+ "\364\314\314\314\314\314\315zS\000\006sillenI\000\000\000 zz" ].join)
+ end
+end

0 comments on commit ca09338

Please sign in to comment.
Something went wrong with that request. Please try again.