diff options
Diffstat (limited to 'lib/soap')
29 files changed, 6537 insertions, 0 deletions
diff --git a/lib/soap/baseData.rb b/lib/soap/baseData.rb new file mode 100644 index 000000000..30f3bce76 --- /dev/null +++ b/lib/soap/baseData.rb @@ -0,0 +1,782 @@ +=begin +SOAP4R - Base type library +Copyright (C) 2000, 2001, 2003 NAKAMURA, Hiroshi. + +This program is free software; you can redistribute it and/or modify it under +the terms of the GNU General Public License as published by the Free Software +Foundation; either version 2 of the License, or (at your option) any later +version. + +This program is distributed in the hope that it will be useful, but WITHOUT ANY +WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A +PRATICULAR PURPOSE. See the GNU General Public License for more details. + +You should have received a copy of the GNU General Public License along with +this program; if not, write to the Free Software Foundation, Inc., 675 Mass +Ave, Cambridge, MA 02139, USA. +=end + + +require 'xsd/datatypes' +require 'soap/soap' + + +module SOAP + + +### +## Mix-in module for SOAP base type classes. +# +module SOAPModuleUtils + include SOAP + +public + + def decode(elename) + d = self.new + d.elename = elename + d + end +end + + +### +## Mix-in module for SOAP base type instances. +# +module SOAPBasetype + include SOAP + + attr_accessor :encodingstyle + + attr_accessor :elename + attr_accessor :id + attr_reader :precedents + attr_accessor :root + attr_accessor :parent + attr_accessor :position + attr_reader :extraattr + +public + + def initialize(*vars) + super(*vars) + @encodingstyle = nil + @elename = XSD::QName.new + @id = nil + @precedents = [] + @parent = nil + @position = nil + @extraattr = {} + end +end + + +### +## Mix-in module for SOAP compound type instances. +# +module SOAPCompoundtype + include SOAP + + attr_accessor :encodingstyle + + attr_accessor :elename + attr_accessor :id + attr_reader :precedents + attr_accessor :root + attr_accessor :parent + attr_accessor :position + attr_reader :extraattr + + attr_accessor :definedtype + +public + + def initialize(type) + super() + @type = type + @encodingstyle = nil + @elename = XSD::QName.new + @id = nil + @precedents = [] + @root = false + @parent = nil + @position = nil + @definedtype = nil + @extraattr = {} + end +end + + +### +## Convenience datatypes. +# +class SOAPReference < XSD::NSDBase + include SOAPBasetype + extend SOAPModuleUtils + +public + + attr_accessor :refid + attr_accessor :elename + + # Override the definition in SOAPBasetype. + def initialize(refid = nil) + super() + @type = XSD::QName.new + @encodingstyle = nil + @elename = XSD::QName.new + @id = nil + @precedents = [] + @root = false + @parent = nil + @refid = refid + @obj = nil + end + + def __getobj__ + @obj + end + + def __setobj__(obj) + @obj = obj + @refid = SOAPReference.create_refid(@obj) + @obj.id = @refid unless @obj.id + @obj.precedents << self + # Copies NSDBase information + @obj.type = @type unless @obj.type + end + + # Why don't I use delegate.rb? + # -> delegate requires target object type at initialize time. + # Why don't I use forwardable.rb? + # -> forwardable requires a list of forwarding methods. + # + # ToDo: Maybe I should use forwardable.rb and give it a methods list like + # delegate.rb... + # + def method_missing(msg_id, *params) + if @obj + @obj.send(msg_id, *params) + else + nil + end + end + + def self.decode(elename, refid) + d = super(elename) + d.refid = refid + d + end + + def SOAPReference.create_refid(obj) + 'id' << obj.__id__.to_s + end +end + +class SOAPNil < XSD::XSDNil + include SOAPBasetype + extend SOAPModuleUtils +end + +# SOAPRawString is for sending raw string. In contrast to SOAPString, +# SOAP4R does not do XML encoding and does not convert its CES. The string it +# holds is embedded to XML instance directly as a 'xsd:string'. +class SOAPRawString < XSD::XSDString + include SOAPBasetype + extend SOAPModuleUtils +end + + +### +## Basic datatypes. +# +class SOAPAnySimpleType < XSD::XSDAnySimpleType + include SOAPBasetype + extend SOAPModuleUtils +end + +class SOAPString < XSD::XSDString + include SOAPBasetype + extend SOAPModuleUtils +end + +class SOAPBoolean < XSD::XSDBoolean + include SOAPBasetype + extend SOAPModuleUtils +end + +class SOAPDecimal < XSD::XSDDecimal + include SOAPBasetype + extend SOAPModuleUtils +end + +class SOAPFloat < XSD::XSDFloat + include SOAPBasetype + extend SOAPModuleUtils +end + +class SOAPDouble < XSD::XSDDouble + include SOAPBasetype + extend SOAPModuleUtils +end + +class SOAPDuration < XSD::XSDDuration + include SOAPBasetype + extend SOAPModuleUtils +end + +class SOAPDateTime < XSD::XSDDateTime + include SOAPBasetype + extend SOAPModuleUtils +end + +class SOAPTime < XSD::XSDTime + include SOAPBasetype + extend SOAPModuleUtils +end + +class SOAPDate < XSD::XSDDate + include SOAPBasetype + extend SOAPModuleUtils +end + +class SOAPGYearMonth < XSD::XSDGYearMonth + include SOAPBasetype + extend SOAPModuleUtils +end + +class SOAPGYear < XSD::XSDGYear + include SOAPBasetype + extend SOAPModuleUtils +end + +class SOAPGMonthDay < XSD::XSDGMonthDay + include SOAPBasetype + extend SOAPModuleUtils +end + +class SOAPGDay < XSD::XSDGDay + include SOAPBasetype + extend SOAPModuleUtils +end + +class SOAPGMonth < XSD::XSDGMonth + include SOAPBasetype + extend SOAPModuleUtils +end + +class SOAPHexBinary < XSD::XSDHexBinary + include SOAPBasetype + extend SOAPModuleUtils +end + +class SOAPBase64 < XSD::XSDBase64Binary + include SOAPBasetype + extend SOAPModuleUtils + Type = QName.new(EncodingNamespace, Base64Literal) + +public + # Override the definition in SOAPBasetype. + def initialize(value = nil) + super(value) + @type = Type + end + + def as_xsd + @type = XSD::XSDBase64Binary::Type + end +end + +class SOAPAnyURI < XSD::XSDAnyURI + include SOAPBasetype + extend SOAPModuleUtils +end + +class SOAPQName < XSD::XSDQName + include SOAPBasetype + extend SOAPModuleUtils +end + + +class SOAPInteger < XSD::XSDInteger + include SOAPBasetype + extend SOAPModuleUtils +end + +class SOAPLong < XSD::XSDLong + include SOAPBasetype + extend SOAPModuleUtils +end + +class SOAPInt < XSD::XSDInt + include SOAPBasetype + extend SOAPModuleUtils +end + +class SOAPShort < XSD::XSDShort + include SOAPBasetype + extend SOAPModuleUtils +end + + +### +## Compound datatypes. +# +class SOAPStruct < XSD::NSDBase + include SOAPCompoundtype + include Enumerable + +public + + def initialize(type = nil) + super(type || XSD::QName.new) + @array = [] + @data = [] + end + + def to_s() + str = '' + self.each do |key, data| + str << "#{ key }: #{ data }\n" + end + str + end + + def add(name, value) + add_member(name, value) + end + + def [](idx) + if idx.is_a?(Range) + @data[idx] + elsif idx.is_a?(Integer) + if (idx > @array.size) + raise ArrayIndexOutOfBoundsError.new('In ' << @type.name) + end + @data[idx] + else + if @array.include?(idx) + @data[@array.index(idx)] + else + nil + end + end + end + + def []=(idx, data) + if @array.include?(idx) + @data[@array.index(idx)] = data + else + add(idx, data) + end + end + + def key?(name) + @array.include?(name) + end + + def members + @array + end + + def each + for i in 0..(@array.length - 1) + yield(@array[i], @data[i]) + end + end + + def replace + members.each do |member| + self[member] = yield(self[member]) + end + end + + def self.decode(elename, type) + s = SOAPStruct.new(type) + s.elename = elename + s + end + +private + + def add_member(name, value = nil) + value = SOAPNil.new() unless value + @array.push(name) + value.elename = value.elename.dup_name(name) + @data.push(value) + end +end + + +# SOAPElement is not typed so it does not derive NSDBase. +class SOAPElement + include Enumerable + + attr_accessor :encodingstyle + attr_accessor :extraattr + attr_reader :precedents + + attr_accessor :qualified + attr_accessor :elename + + def initialize(elename, text = nil) + if !elename.is_a?(XSD::QName) + elename = XSD::QName.new(nil, elename) + end + @encodingstyle = LiteralNamespace + @extraattr = {} + @precedents = [] + + @qualified = false + @elename = elename + + @array = [] + @data = [] + @text = text + end + + # Text interface. + attr_accessor :text + + # Element interfaces. + def add(value) + add_member(value.elename.name, value) + end + + def [](idx) + if @array.include?(idx) + @data[@array.index(idx)] + else + nil + end + end + + def []=(idx, data) + if @array.include?(idx) + @data[@array.index(idx)] = data + else + add(data) + end + end + + def key?(name) + @array.include?(name) + end + + def members + @array + end + + def to_obj + if members.empty? + @text + else + hash = {} + each do |k, v| + hash[k] = v.to_obj + end + hash + end + end + + def each + for i in 0..(@array.length - 1) + yield(@array[i], @data[i]) + end + end + + def self.decode(elename) + o = SOAPElement.new + o.elename = elename + o + end + + def self.from_obj(hash_or_string) + o = SOAPElement.new(nil) + if hash_or_string.is_a?(Hash) + hash_or_string.each do |k, v| + child = self.from_obj(v) + child.elename = XSD::QName.new(nil, k) + o.add(child) + end + else + o.text = hash_or_string + end + o + end + +private + + def add_member(name, value) + add_accessor(name) + @array.push(name) + @data.push(value) + end + + def add_accessor(name) + methodname = name + if self.respond_to?(methodname) + methodname = safe_accessor_name(methodname) + end + begin + instance_eval <<-EOS + def #{ methodname }() + @data[@array.index('#{ name }')] + end + + def #{ methodname }=(value) + @data[@array.index('#{ name }')] = value + end + EOS + rescue SyntaxError + methodname = safe_accessor_name(methodname) + retry + end + end + + def safe_accessor_name(name) + "var_" << name.gsub(/[^a-zA-Z0-9_]/, '') + end +end + + +class SOAPArray < XSD::NSDBase + include SOAPCompoundtype + include Enumerable + +public + + attr_accessor :sparse + + attr_reader :offset, :rank + attr_accessor :size, :size_fixed + attr_reader :arytype + + def initialize(type = nil, rank = 1, arytype = nil) + super(type || XSD::QName.new) + @rank = rank + @data = Array.new + @sparse = false + @offset = Array.new(rank, 0) + @size = Array.new(rank, 0) + @size_fixed = false + @position = nil + @arytype = arytype + end + + def offset=(var) + @offset = var + @sparse = true + end + + def add(value) + self[*(@offset)] = value + end + + def [](*idxary) + if idxary.size != @rank + raise ArgumentError.new("Given #{ idxary.size } params does not match rank: #{ @rank }") + end + + retrieve(idxary) + end + + def []=(*idxary) + value = idxary.slice!(-1) + + if idxary.size != @rank + raise ArgumentError.new("Given #{ idxary.size } params(#{ idxary }) does not match rank: #{ @rank }") + end + + for i in 0..(idxary.size - 1) + if idxary[i] + 1 > @size[i] + @size[i] = idxary[i] + 1 + end + end + + data = retrieve(idxary[0, idxary.size - 1]) + data[idxary.last] = value + + if value.is_a?(SOAPBasetype) || value.is_a?(SOAPCompoundtype) + value.elename = value.elename.dup_name('item') + + # Sync type + unless @type.name + @type = XSD::QName.new(value.type.namespace, + SOAPArray.create_arytype(value.type.name, @rank)) + end + + unless value.type + value.type = @type + end + end + + @offset = idxary + offsetnext + end + + def each + @data.each do |data| + yield(data) + end + end + + def to_a + @data.dup + end + + def replace + @data = deep_map(@data) do |ele| + yield(ele) + end + end + + def deep_map(ary, &block) + ary.collect do |ele| + if ele.is_a?(Array) + deep_map(ele, &block) + else + new_obj = block.call(ele) + new_obj.elename = new_obj.elename.dup_name('item') + new_obj + end + end + end + + def include?(var) + traverse_data(@data) do |v, *rank| + if v.is_a?(SOAPBasetype) && v.data == var + return true + end + end + false + end + + def traverse + traverse_data(@data) do |v, *rank| + unless @sparse + yield(v) + else + yield(v, *rank) if v && !v.is_a?(SOAPNil) + end + end + end + + def soap2array(ary) + traverse_data(@data) do |v, *position| + iteary = ary + for rank in 1..(position.size - 1) + idx = position[rank - 1] + if iteary[idx].nil? + iteary = iteary[idx] = Array.new + else + iteary = iteary[idx] + end + end + if block_given? + iteary[position.last] = yield(v) + else + iteary[position.last] = v + end + end + end + + def position + @position + end + +private + + def retrieve(idxary) + data = @data + for rank in 1..(idxary.size) + idx = idxary[rank - 1] + if data[idx].nil? + data = data[idx] = Array.new + else + data = data[idx] + end + end + data + end + + def traverse_data(data, rank = 1) + for idx in 0..(ranksize(rank) - 1) + if rank < @rank + traverse_data(data[idx], rank + 1) do |*v| + v[1, 0] = idx + yield(*v) + end + else + yield(data[idx], idx) + end + end + end + + def ranksize(rank) + @size[rank - 1] + end + + def offsetnext + move = false + idx = @offset.size - 1 + while !move && idx >= 0 + @offset[idx] += 1 + if @size_fixed + if @offset[idx] < @size[idx] + move = true + else + @offset[idx] = 0 + idx -= 1 + end + else + move = true + end + end + end + + # Module function + +public + + def self.decode(elename, type, arytype) + typestr, nofary = parse_type(arytype.name) + rank = nofary.count(',') + 1 + plain_arytype = XSD::QName.new(arytype.namespace, typestr) + o = SOAPArray.new(type, rank, plain_arytype) + size = [] + nofary.split(',').each do |s| + if s.empty? + size.clear + break + else + size << s.to_i + end + end + unless size.empty? + o.size = size + o.size_fixed = true + end + o.elename = elename + o + end + +private + + def self.create_arytype(typename, rank) + "#{ typename }[" << ',' * (rank - 1) << ']' + end + + TypeParseRegexp = Regexp.new('^(.+)\[([\d,]*)\]$') + + def self.parse_type(string) + TypeParseRegexp =~ string + return $1, $2 + end +end + + +require 'soap/mapping/typeMap' + + +end diff --git a/lib/soap/element.rb b/lib/soap/element.rb new file mode 100644 index 000000000..640eafb0e --- /dev/null +++ b/lib/soap/element.rb @@ -0,0 +1,232 @@ +=begin +SOAP4R - SOAP elements library +Copyright (C) 2000, 2001, 2003 NAKAMURA, Hiroshi. + +This program is free software; you can redistribute it and/or modify it under +the terms of the GNU General Public License as published by the Free Software +Foundation; either version 2 of the License, or (at your option) any later +version. + +This program is distributed in the hope that it will be useful, but WITHOUT ANY +WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A +PRATICULAR PURPOSE. See the GNU General Public License for more details. + +You should have received a copy of the GNU General Public License along with +this program; if not, write to the Free Software Foundation, Inc., 675 Mass +Ave, Cambridge, MA 02139, USA. +=end + + +require 'xsd/qname' +require 'soap/baseData' + + +module SOAP + + +### +## SOAP elements +# +module SOAPEnvelopeElement; end + +class SOAPFault < SOAPStruct + include SOAPEnvelopeElement + include SOAPCompoundtype + +public + + def faultcode + self['faultcode'] + end + + def faultstring + self['faultstring'] + end + + def faultactor + self['faultactor'] + end + + def detail + self['detail'] + end + + def faultcode=(rhs) + self['faultcode'] = rhs + end + + def faultstring=(rhs) + self['faultstring'] = rhs + end + + def faultactor=(rhs) + self['faultactor'] = rhs + end + + def detail=(rhs) + self['detail'] = rhs + end + + def initialize(faultcode = nil, faultstring = nil, faultactor = nil, detail = nil) + super(EleFaultName) + @elename = EleFaultName + @encodingstyle = EncodingNamespace + + if faultcode + self.faultcode = faultcode + self.faultstring = faultstring + self.faultactor = faultactor + self.detail = detail + self.faultcode.elename = EleFaultCodeName if self.faultcode + self.faultstring.elename = EleFaultStringName if self.faultstring + self.faultactor.elename = EleFaultActorName if self.faultactor + self.detail.elename = EleFaultDetailName if self.detail + end + end + + def encode(buf, ns, attrs = {}, indent = '') + SOAPGenerator.assign_ns(attrs, ns, EnvelopeNamespace) + SOAPGenerator.assign_ns(attrs, ns, EncodingNamespace) + attrs[ns.name(AttrEncodingStyleName)] = EncodingNamespace + name = ns.name(@elename) + SOAPGenerator.encode_tag(buf, name, attrs, indent) + yield(self.faultcode, false) + yield(self.faultstring, false) + yield(self.faultactor, false) + yield(self.detail, false) if self.detail + SOAPGenerator.encode_tag_end(buf, name, indent, true) + end +end + + +class SOAPBody < SOAPStruct + include SOAPEnvelopeElement + +public + + def initialize(data = nil, is_fault = false) + super(nil) + @elename = EleBodyName + @encodingstyle = nil + add(data.elename.name, data) if data + @is_fault = is_fault + end + + def encode(buf, ns, attrs = {}, indent = '') + name = ns.name(@elename) + SOAPGenerator.encode_tag(buf, name, attrs, indent) + if @is_fault + yield(@data, true) + else + @data.each do |data| + yield(data, true) + end + end + SOAPGenerator.encode_tag_end(buf, name, indent, true) + end + + def root_node + @data.each do |node| + if node.root == 1 + return node + end + end + # No specified root... + @data.each do |node| + if node.root != 0 + return node + end + end + + raise SOAPParser::FormatDecodeError.new('No root element.') + end +end + + +class SOAPHeaderItem < XSD::NSDBase + include SOAPEnvelopeElement + include SOAPCompoundtype + +public + + attr_accessor :content + attr_accessor :mustunderstand + attr_accessor :encodingstyle + + def initialize(content, mustunderstand = true, encodingstyle = nil) + super(nil) + @content = content + @mustunderstand = mustunderstand + @encodingstyle = encodingstyle || LiteralNamespace + end + + def encode(buf, ns, attrs = {}, indent = '') + attrs.each do |key, value| + @content.attr[key] = value + end + @content.attr[ns.name(EnvelopeNamespace, AttrMustUnderstand)] = + (@mustunderstand ? '1' : '0') + if @encodingstyle + @content.attr[ns.name(EnvelopeNamespace, AttrEncodingStyle)] = + @encodingstyle + end + @content.encodingstyle = @encodingstyle if !@content.encodingstyle + yield(@content, true) + end +end + + +class SOAPHeader < SOAPArray + include SOAPEnvelopeElement + + def initialize() + super(nil, 1) # rank == 1 + @elename = EleHeaderName + @encodingstyle = nil + end + + def encode(buf, ns, attrs = {}, indent = '') + name = ns.name(@elename) + SOAPGenerator.encode_tag(buf, name, attrs, indent) + @data.each do |data| + yield(data, true) + end + SOAPGenerator.encode_tag_end(buf, name, indent, true) + end + + def length + @data.length + end +end + + +class SOAPEnvelope < XSD::NSDBase + include SOAPEnvelopeElement + include SOAPCompoundtype + + attr_accessor :header + attr_accessor :body + + def initialize(header = nil, body = nil) + super(nil) + @elename = EleEnvelopeName + @encodingstyle = nil + @header = header + @body = body + end + + def encode(buf, ns, attrs = {}, indent = '') + SOAPGenerator.assign_ns(attrs, ns, EnvelopeNamespace, + SOAPNamespaceTag) + name = ns.name(@elename) + SOAPGenerator.encode_tag(buf, name, attrs, indent) + + yield(@header, true) if @header and @header.length > 0 + yield(@body, true) + + SOAPGenerator.encode_tag_end(buf, name, indent, true) + end +end + + +end diff --git a/lib/soap/encodingstyle/aspDotNetHandler.rb b/lib/soap/encodingstyle/aspDotNetHandler.rb new file mode 100644 index 000000000..fdce42a48 --- /dev/null +++ b/lib/soap/encodingstyle/aspDotNetHandler.rb @@ -0,0 +1,232 @@ +=begin +SOAP4R - ASP.NET EncodingStyle handler library +Copyright (C) 2001, 2003 NAKAMURA, Hiroshi. + +This program is free software; you can redistribute it and/or modify it under +the terms of the GNU General Public License as published by the Free Software +Foundation; either version 2 of the License, or (at your option) any later +version. + +This program is distributed in the hope that it will be useful, but WITHOUT ANY +WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A +PRATICULAR PURPOSE. See the GNU General Public License for more details. + +You should have received a copy of the GNU General Public License along with +this program; if not, write to the Free Software Foundation, Inc., 675 Mass +Ave, Cambridge, MA 02139, USA. +=end + + +require 'soap/encodingstyle/handler' + + +module SOAP +module EncodingStyle + + +class ASPDotNetHandler < Handler + Namespace = 'http://tempuri.org/ASP.NET' + add_handler + + def initialize(charset = nil) + super(charset) + @textbuf = '' + @decode_typemap = nil + end + + + ### + ## encode interface. + # + def encode_data(buf, ns, qualified, data, parent, indent = '') + attrs = {} + name = if qualified and data.elename.namespace + SOAPGenerator.assign_ns(attrs, ns, data.elename.namespace) + ns.name(data.elename) + else + data.elename.name + end + + case data + when SOAPRawString + SOAPGenerator.encode_tag(buf, name, attrs, indent) + buf << data.to_s + when XSD::XSDString + SOAPGenerator.encode_tag(buf, name, attrs, indent) + buf << SOAPGenerator.encode_str(@charset ? + XSD::Charset.encoding_to_xml(data.to_s, @charset) : data.to_s) + when XSD::XSDAnySimpleType + SOAPGenerator.encode_tag(buf, name, attrs, indent) + buf << SOAPGenerator.encode_str(data.to_s) + when SOAPStruct + SOAPGenerator.encode_tag(buf, name, attrs, indent) + data.each do |key, value| + if !value.elename.namespace + value.elename.namespace = data.elename.namespace + end + yield(value, true) + end + when SOAPArray + SOAPGenerator.encode_tag(buf, name, attrs, indent) + data.traverse do |child, *rank| + data.position = nil + yield(child, true) + end + else + raise EncodingStyleError.new("Unknown object:#{ data } in this encodingSt +yle.") + end + end + + def encode_data_end(buf, ns, qualified, data, parent, indent = "") + name = if qualified and data.elename.namespace + ns.name(data.elename) + else + data.elename.name + end + cr = data.is_a?(SOAPCompoundtype) + SOAPGenerator.encode_tag_end(buf, name, indent, cr) + end + + + ### + ## decode interface. + # + class SOAPTemporalObject + attr_accessor :parent + + def initialize + @parent = nil + end + end + + class SOAPUnknown < SOAPTemporalObject + def initialize(handler, elename) + super() + @handler = handler + @elename = elename + end + + def as_struct + o = SOAPStruct.decode(@elename, XSD::AnyTypeName) + o.parent = @parent + o.type.name = @name + @handler.decode_parent(@parent, o) + o + end + + def as_string + o = SOAPString.decode(@elename) + o.parent = @parent + @handler.decode_parent(@parent, o) + o + end + + def as_nil + o = SOAPNil.decode(@elename) + o.parent = @parent + @handler.decode_parent(@parent, o) + o + end + end + + def decode_tag(ns, elename, attrs, parent) + # ToDo: check if @textbuf is empty... + @textbuf = '' + o = SOAPUnknown.new(self, elename) + o.parent = parent + o + end + + def decode_tag_end(ns, node) + o = node.node + if o.is_a?(SOAPUnknown) + newnode = o.as_string +# if /\A\s*\z/ =~ @textbuf +# o.as_struct +# else +# o.as_string +# end + node.replace_node(newnode) + o = node.node + end + + decode_textbuf(o) + @textbuf = '' + end + + def decode_text(ns, text) + # @textbuf is set at decode_tag_end. + @textbuf << text + end + + def decode_prologue + end + + def decode_epilogue + end + + def decode_parent(parent, node) + case parent.node + when SOAPUnknown + newparent = parent.node.as_struct + node.parent = newparent + parent.replace_node(newparent) + decode_parent(parent, node) + + when SOAPStruct + data = parent.node[node.elename.name] + case data + when nil + parent.node.add(node.elename.name, node) + when SOAPArray + name, type_ns = node.elename.name, node.type.namespace + data.add(node) + node.elename, node.type.namespace = name, type_ns + else + parent.node[node.elename.name] = SOAPArray.new + name, type_ns = data.elename.name, data.type.namespace + parent.node[node.elename.name].add(data) + data.elename.name, data.type.namespace = name, type_ns + name, type_ns = node.elename.name, node.type.namespace + parent.node[node.elename.name].add(node) + node.elename.name, node.type.namespace = name, type_ns + end + + when SOAPArray + if node.position + parent.node[*(decode_arypos(node.position))] = node + parent.node.sparse = true + else + parent.node.add(node) + end + + when SOAPBasetype + raise EncodingStyleError.new("SOAP base type must not have a child.") + + else + # SOAPUnknown does not have parent. + # raise EncodingStyleError.new("Illegal parent: #{ parent }.") + end + end + +private + + def decode_textbuf(node) + if node.is_a?(XSD::XSDString) + if @charset + node.set(XSD::Charset.encoding_from_xml(@textbuf, @charset)) + else + node.set(@textbuf) + end + else + # Nothing to do... + end + end +end + +ASPDotNetHandler.new + + +end +end diff --git a/lib/soap/encodingstyle/handler.rb b/lib/soap/encodingstyle/handler.rb new file mode 100644 index 000000000..1ab9d86ec --- /dev/null +++ b/lib/soap/encodingstyle/handler.rb @@ -0,0 +1,111 @@ +=begin +SOAP4R - EncodingStyle handler library +Copyright (C) 2001, 2003 NAKAMURA, Hiroshi. + +This program is free software; you can redistribute it and/or modify it under +the terms of the GNU General Public License as published by the Free Software +Foundation; either version 2 of the License, or (at your option) any later +version. + +This program is distributed in the hope that it will be useful, but WITHOUT ANY +WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A +PRATICULAR PURPOSE. See the GNU General Public License for more details. + +You should have received a copy of the GNU General Public License along with +this program; if not, write to the Free Software Foundation, Inc., 675 Mass +Ave, Cambridge, MA 02139, USA. +=end + + +require 'soap/soap' +require 'soap/baseData' +require 'soap/element' + + +module SOAP +module EncodingStyle + + +class Handler + @@handlers = {} + + class EncodingStyleError < Error; end + + class << self + def uri + self::Namespace + end + + def handler(uri) + @@handlers[uri] + end + + def each + @@handlers.each do |key, value| + yield(value) + end + end + + private + + def add_handler + @@handlers[self.uri] = self + end + end + + attr_reader :charset + attr_accessor :generate_explicit_type + def decode_typemap=(complextypes) + @decode_typemap = complextypes + end + + def initialize(charset) + @charset = charset + @generate_explicit_type = true + @decode_typemap = nil + end + + ### + ## encode interface. + # + # Returns a XML instance as a string. + def encode_data(buf, ns, qualified, data, parent, indent) + raise NotImplementError.new('Method encode_data must be defined in derived class.') + end + + def encode_data_end(buf, ns, qualified, data, parent, indent) + raise NotImplementError.new('Method encode_data must be defined in derived class.') + end + + def encode_prologue + end + + def encode_epilogue + end + + ### + ## decode interface. + # + # Returns SOAP/OM data. + def decode_tag(ns, name, attrs, parent) + raise NotImplementError.new('Method decode_tag must be defined in derived class.') + end + + def decode_tag_end(ns, name) + raise NotImplementError.new('Method decode_tag_end must be defined in derived class.') + end + + def decode_text(ns, text) + raise NotImplementError.new('Method decode_text must be defined in derived class.') + end + + def decode_prologue + end + + def decode_epilogue + end +end + + +end +end diff --git a/lib/soap/encodingstyle/literalHandler.rb b/lib/soap/encodingstyle/literalHandler.rb new file mode 100644 index 000000000..b5d0d464d --- /dev/null +++ b/lib/soap/encodingstyle/literalHandler.rb @@ -0,0 +1,218 @@ +=begin +SOAP4R - XML Literal EncodingStyle handler library +Copyright (C) 2001, 2003 NAKAMURA, Hiroshi. + +This program is free software; you can redistribute it and/or modify it under +the terms of the GNU General Public License as published by the Free Software +Foundation; either version 2 of the License, or (at your option) any later +version. + +This program is distributed in the hope that it will be useful, but WITHOUT ANY +WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A +PRATICULAR PURPOSE. See the GNU General Public License for more details. + +You should have received a copy of the GNU General Public License along with +this program; if not, write to the Free Software Foundation, Inc., 675 Mass +Ave, Cambridge, MA 02139, USA. +=end + + +require 'soap/encodingstyle/handler' + + +module SOAP +module EncodingStyle + + +class LiteralHandler < Handler + Namespace = SOAP::LiteralNamespace + add_handler + + def initialize(charset = nil) + super(charset) + @textbuf = '' + end + + + ### + ## encode interface. + # + def encode_data(buf, ns, qualified, data, parent, indent = '') + attrs = {} + name = if qualified and data.elename.namespace + SOAPGenerator.assign_ns(attrs, ns, data.elename.namespace) + ns.name(data.elename) + else + data.elename.name + end + + case data + when SOAPRawString + SOAPGenerator.encode_tag(buf, name, attrs, indent) + buf << data.to_s + when XSD::XSDString + SOAPGenerator.encode_tag(buf, name, attrs, indent) + buf << SOAPGenerator.encode_str(@charset ? + XSD::Charset.encoding_to_xml(data.to_s, @charset) : data.to_s) + when XSD::XSDAnySimpleType + SOAPGenerator.encode_tag(buf, name, attrs, indent) + buf << SOAPGenerator.encode_str(data.to_s) + when SOAPStruct + SOAPGenerator.encode_tag(buf, name, attrs, indent) + data.each do |key, value| + value.elename.namespace = data.elename.namespace if !value.elename.namespace + yield(value, true) + end + when SOAPArray + SOAPGenerator.encode_tag(buf, name, attrs, indent) + data.traverse do |child, *rank| + data.position = nil + yield(child, true) + end + when SOAPElement + SOAPGenerator.encode_tag(buf, name, attrs.update(data.extraattr), + indent) + buf << data.text if data.text + data.each do |key, value| + value.elename.namespace = data.elename.namespace if !value.elename.namespace + #yield(value, data.qualified) + yield(value, qualified) + end + else + raise EncodingStyleError.new("Unknown object:#{ data } in this encodingStyle.") + end + end + + def encode_data_end(buf, ns, qualified, data, parent, indent) + name = if qualified and data.elename.namespace + ns.name(data.elename) + else + data.elename.name + end + SOAPGenerator.encode_tag_end(buf, name, indent) + end + + + ### + ## decode interface. + # + class SOAPTemporalObject + attr_accessor :parent + + def initialize + @parent = nil + end + end + + class SOAPUnknown < SOAPTemporalObject + def initialize(handler, elename) + super() + @handler = handler + @elename = elename + end + + def as_struct + o = SOAPStruct.decode(@elename, XSD::AnyTypeName) + o.parent = @parent + @handler.decode_parent(@parent, o) + o + end + + def as_string + o = SOAPString.decode(@elename) + o.parent = @parent + @handler.decode_parent(@parent, o) + o + end + + def as_nil + o = SOAPNil.decode(@elename) + o.parent = @parent + @handler.decode_parent(@parent, o) + o + end + end + + def decode_tag(ns, elename, attrs, parent) + # ToDo: check if @textbuf is empty... + @textbuf = '' + o = SOAPUnknown.new(self, elename) + o.parent = parent + o + end + + def decode_tag_end(ns, node) + o = node.node + if o.is_a?(SOAPUnknown) + newnode = if /\A\s*\z/ =~ @textbuf + o.as_struct + else + o.as_string + end + node.replace_node(newnode) + o = node.node + end + + decode_textbuf(o) + @textbuf = '' + end + + def decode_text(ns, text) + # @textbuf is set at decode_tag_end. + @textbuf << text + end + + def decode_prologue + end + + def decode_epilogue + end + + def decode_parent(parent, node) + case parent.node + when SOAPUnknown + newparent = parent.node.as_struct + node.parent = newparent + parent.replace_node(newparent) + decode_parent(parent, node) + + when SOAPStruct + parent.node.add(node.name, node) + + when SOAPArray + if node.position + parent.node[*(decode_arypos(node.position))] = node + parent.node.sparse = true + else + parent.node.add(node) + end + + when SOAPBasetype + raise EncodingStyleError.new("SOAP base type must not have a child.") + + else + # SOAPUnknown does not have parent. + # raise EncodingStyleError.new("Illegal parent: #{ parent }.") + end + end + +private + + def decode_textbuf(node) + if node.is_a?(XSD::XSDString) + if @charset + node.set(XSD::Charset.encoding_from_xml(@textbuf, @charset)) + else + node.set(@textbuf) + end + else + # Nothing to do... + end + end +end + +LiteralHandler.new + + +end +end diff --git a/lib/soap/encodingstyle/soapHandler.rb b/lib/soap/encodingstyle/soapHandler.rb new file mode 100644 index 000000000..b1b5072e4 --- /dev/null +++ b/lib/soap/encodingstyle/soapHandler.rb @@ -0,0 +1,548 @@ +=begin +SOAP4R - SOAP EncodingStyle handler library +Copyright (C) 2001, 2003 NAKAMURA, Hiroshi. + +This program is free software; you can redistribute it and/or modify it under +the terms of the GNU General Public License as published by the Free Software +Foundation; either version 2 of the License, or (at your option) any later +version. + +This program is distributed in the hope that it will be useful, but WITHOUT ANY +WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A +PRATICULAR PURPOSE. See the GNU General Public License for more details. + +You should have received a copy of the GNU General Public License along with +this program; if not, write to the Free Software Foundation, Inc., 675 Mass +Ave, Cambridge, MA 02139, USA. +=end + + +require 'soap/encodingstyle/handler' + + +module SOAP +module EncodingStyle + + +class SOAPHandler < Handler + Namespace = SOAP::EncodingNamespace + add_handler + + def initialize(charset = nil) + super(charset) + @refpool = [] + @idpool = [] + @textbuf = '' + @is_first_top_ele = true + end + + + ### + ## encode interface. + # + def encode_data(buf, ns, qualified, data, parent, indent = '') + attrs = encode_attrs(ns, qualified, data, parent) + + if parent && parent.is_a?(SOAPArray) && parent.position + attrs[ns.name(AttrPositionName)] = '[' << parent.position.join(',') << ']' + end + + name = nil + if qualified and data.elename.namespace + SOAPGenerator.assign_ns(attrs, ns, data.elename.namespace) + name = ns.name(data.elename) + else + name = data.elename.name + end + + if data.respond_to?(:encode) + SOAPGenerator.encode_tag(buf, name, attrs, indent) + return data.encode(buf, ns, attrs, indent) + end + + case data + when SOAPReference + attrs['href'] = '#' << data.refid + SOAPGenerator.encode_tag(buf, name, attrs, indent) + when SOAPRawString + SOAPGenerator.encode_tag(buf, name, attrs, indent) + buf << data.to_s + when XSD::XSDString + SOAPGenerator.encode_tag(buf, name, attrs, indent) + buf << SOAPGenerator.encode_str(@charset ? + XSD::Charset.encoding_to_xml(data.to_s, @charset) : data.to_s) + when XSD::XSDAnySimpleType + SOAPGenerator.encode_tag(buf, name, attrs, indent) + buf << SOAPGenerator.encode_str(data.to_s) + when SOAPStruct + SOAPGenerator.encode_tag(buf, name, attrs, indent) + data.each do |key, value| + yield(value, false) + end + when SOAPArray + SOAPGenerator.encode_tag(buf, name, attrs, indent) + data.traverse do |child, *rank| + data.position = data.sparse ? rank : nil + yield(child, false) + end + else + raise EncodingStyleError.new( + "Unknown object:#{ data } in this encodingStyle.") + end + end + + def encode_data_end(buf, ns, qualified, data, parent, indent = '') + name = if qualified and data.elename.namespace + ns.name(data.elename) + else + data.elename.name + end + cr = data.is_a?(SOAPCompoundtype) + SOAPGenerator.encode_tag_end(buf, name, indent, cr) + end + + + ### + ## decode interface. + # + class SOAPTemporalObject + attr_accessor :parent + attr_accessor :position + attr_accessor :id + attr_accessor :root + + def initialize + @parent = nil + @position = nil + @id = nil + @root = nil + end + end + + class SOAPUnknown < SOAPTemporalObject + attr_reader :type + attr_accessor :definedtype + attr_reader :extraattr + + def initialize(handler, elename, type, extraattr) + super() + @handler = handler + @elename = elename + @type = type + @extraattr = extraattr + @definedtype = nil + end + + def as_struct + o = SOAPStruct.decode(@elename, @type) + o.id = @id + o.root = @root + o.parent = @parent + o.position = @position + o.extraattr.update(@extraattr) + @handler.decode_parent(@parent, o) + o + end + + def as_string + o = SOAPString.decode(@elename) + o.id = @id + o.root = @root + o.parent = @parent + o.position = @position + o.extraattr.update(@extraattr) + @handler.decode_parent(@parent, o) + o + end + + def as_nil + o = SOAPNil.decode(@elename) + o.id = @id + o.root = @root + o.parent = @parent + o.position = @position + o.extraattr.update(@extraattr) + @handler.decode_parent(@parent, o) + o + end + end + + def decode_tag(ns, elename, attrs, parent) + # ToDo: check if @textbuf is empty... + @textbuf = '' + is_nil, type, arytype, root, offset, position, href, id, extraattr = + decode_attrs(ns, attrs) + o = nil + if is_nil + o = SOAPNil.decode(elename) + elsif href + o = SOAPReference.decode(elename, href) + @refpool << o + elsif @decode_typemap && + (parent.node.class != SOAPBody || @is_first_top_ele) + # multi-ref element should be parsed by decode_tag_by_type. + @is_first_top_ele = false + o = decode_tag_by_wsdl(ns, elename, type, parent.node, arytype, extraattr) + else + o = decode_tag_by_type(ns, elename, type, parent.node, arytype, extraattr) + end + + if o.is_a?(SOAPArray) + if offset + o.offset = decode_arypos(offset) + o.sparse = true + else + o.sparse = false + end + end + + o.parent = parent + o.id = id + o.root = root + o.position = position + + unless o.is_a?(SOAPTemporalObject) + @idpool << o if o.id + decode_parent(parent, o) + end + o + end + + def decode_tag_end(ns, node) + o = node.node + if o.is_a?(SOAPUnknown) + newnode = if /\A\s*\z/ =~ @textbuf + o.as_struct + else + o.as_string + end + if newnode.id + @idpool << newnode + end + node.replace_node(newnode) + o = node.node + end + if o.is_a?(SOAPCompoundtype) + o.definedtype = nil + end + + decode_textbuf(o) + @textbuf = '' + end + + def decode_text(ns, text) + # @textbuf is set at decode_tag_end. + @textbuf << text + end + + def decode_prologue + @refpool.clear + @idpool.clear + @is_first_top_ele = true + end + + def decode_epilogue + decode_resolve_id + end + + def decode_parent(parent, node) + case parent.node + when SOAPUnknown + newparent = parent.node.as_struct + node.parent = newparent + if newparent.id + @idpool << newparent + end + parent.replace_node(newparent) + decode_parent(parent, node) + + when SOAPStruct + parent.node.add(node.elename.name, node) + node.parent = parent.node + + when SOAPArray + if node.position + parent.node[*(decode_arypos(node.position))] = node + parent.node.sparse = true + else + parent.node.add(node) + end + node.parent = parent.node + + when SOAPBasetype + raise EncodingStyleError.new("SOAP base type must not have a child.") + + else + raise EncodingStyleError.new("Illegal parent: #{ parent.node }.") + end + end + +private + + def content_ranksize(typename) + typename.scan(/\[[\d,]*\]$/)[0] + end + + def content_typename(typename) + typename.sub(/\[,*\]$/, '') + end + + def create_arytype(ns, data) + XSD::QName.new(data.arytype.namespace, + content_typename(data.arytype.name) << '[' << data.size.join(',') << ']') + end + + def encode_attrs(ns, qualified, data, parent) + return {} if data.is_a?(SOAPReference) + attrs = {} + + if !parent || parent.encodingstyle != EncodingNamespace + if @generate_explicit_type + SOAPGenerator.assign_ns(attrs, ns, EnvelopeNamespace) + SOAPGenerator.assign_ns(attrs, ns, EncodingNamespace) + attrs[ns.name(AttrEncodingStyleName)] = EncodingNamespace + end + data.encodingstyle = EncodingNamespace + end + + if data.is_a?(SOAPNil) + attrs[ns.name(XSD::AttrNilName)] = XSD::NilValue + elsif @generate_explicit_type + if data.type.namespace + SOAPGenerator.assign_ns(attrs, ns, data.type.namespace) + end + if data.is_a?(SOAPArray) + if data.arytype.namespace + SOAPGenerator.assign_ns(attrs, ns, data.arytype.namespace) + end + attrs[ns.name(AttrArrayTypeName)] = ns.name(create_arytype(ns, data)) + if data.type.name + attrs[ns.name(XSD::AttrTypeName)] = ns.name(data.type) + end + elsif parent && parent.is_a?(SOAPArray) && (parent.arytype == data.type) + # No need to add. + elsif !data.type.namespace + # No need to add. + else + attrs[ns.name(XSD::AttrTypeName)] = ns.name(data.type) + end + end + + data.extraattr.each do |key, value| + SOAPGenerator.assign_ns(attrs, ns, key.namespace) + attrs[ns.name(key)] = value # ns.name(value) ? + end + if data.id + attrs['id'] = data.id + end + attrs + end + + def decode_tag_by_wsdl(ns, elename, typestr, parent, arytypestr, extraattr) + if parent.class == SOAPBody + # Unqualified name is allowed here. + type = @decode_typemap[elename] || @decode_typemap.find_name(elename.name) + unless type + raise EncodingStyleError.new("Unknown operation '#{ elename }'.") + end + o = SOAPStruct.new(elename) + o.definedtype = type + return o + end + + if parent.type == XSD::AnyTypeName + return decode_tag_by_type(ns, elename, typestr, parent, arytypestr, + extraattr) + end + + # parent.definedtype is nil means the parent is SOAPUnknown. SOAPUnknown is + # generated by decode_tag_by_type when its type is anyType. + parenttype = parent.definedtype || @decode_typemap[parent.type] + unless parenttype + raise EncodingStyleError.new("Unknown type '#{ parent.type }'.") + end + typename = parenttype.child_type(elename) + if typename + if (klass = TypeMap[typename]) + return klass.decode(elename) + elsif typename == XSD::AnyTypeName + return decode_tag_by_type(ns, elename, typestr, parent, arytypestr, + extraattr) + end + end + + type = if typename + @decode_typemap[typename] + else + parenttype.child_defined_complextype(elename) + end + unless type + raise EncodingStyleError.new("Unknown type '#{ typename }'.") + end + + case type.compoundtype + when :TYPE_STRUCT + o = SOAPStruct.decode(elename, typename) + o.definedtype = type + return o + when :TYPE_ARRAY + expected_arytype = type.find_arytype + actual_arytype = if arytypestr + XSD::QName.new(expected_arytype.namespace, + content_typename(expected_arytype.name) << + content_ranksize(arytypestr)) + else + expected_arytype + end + o = SOAPArray.decode(elename, typename, actual_arytype) + o.definedtype = type + return o + end + return nil + end + + def decode_tag_by_type(ns, elename, typestr, parent, arytypestr, extraattr) + if arytypestr + type = typestr ? ns.parse(typestr) : ValueArrayName + node = SOAPArray.decode(elename, type, ns.parse(arytypestr)) + node.extraattr.update(extraattr) + return node + end + + type = nil + if typestr + type = ns.parse(typestr) + elsif parent.is_a?(SOAPArray) + type = parent.arytype + else + # Since it's in dynamic(without any type) encoding process, + # assumes entity as its type itself. + # <SOAP-ENC:Array ...> => type Array in SOAP-ENC. + # <Country xmlns="foo"> => type Country in foo. + type = elename + end + + if (klass = TypeMap[type]) + klass.decode(elename) + else + # Unknown type... Struct or String + SOAPUnknown.new(self, elename, type, extraattr) + end + end + + def decode_textbuf(node) + case node + when XSD::XSDHexBinary, XSD::XSDBase64Binary + node.set_encoded(@textbuf) + when XSD::XSDString + if @charset + node.set(XSD::Charset.encoding_from_xml(@textbuf, @charset)) + else + node.set(@textbuf) + end + when SOAPNil + # Nothing to do. + when SOAPBasetype + node.set(@textbuf) + else + # Nothing to do... + end + end + + NilLiteralMap = { + 'true' => true, + '1' => true, + 'false' => false, + '0' => false + } + RootLiteralMap = { + '1' => 1, + '0' => 0 + } + def decode_attrs(ns, attrs) + is_nil = false + type = nil + arytype = nil + root = nil + offset = nil + position = nil + href = nil + id = nil + extraattr = {} + + attrs.each do |key, value| + qname = ns.parse(key) + case qname.namespace + when XSD::InstanceNamespace + case qname.name + when XSD::NilLiteral + is_nil = NilLiteralMap[value] or + raise EncodingStyleError.new("Cannot accept attribute value: #{ value } as the value of xsi:#{ XSD::NilLiteral } (expected 'true', 'false', '1', or '0').") + next + when XSD::AttrType + type = value + next + end + when EncodingNamespace + case qname.name + when AttrArrayType + arytype = value + next + when AttrRoot + root = RootLiteralMap[value] or + raise EncodingStyleError.new( + "Illegal root attribute value: #{ value }.") + next + when AttrOffset + offset = value + next + when AttrPosition + position = value + next + end + end + if key == 'href' + href = value + next + elsif key == 'id' + id = value + next + end + extraattr[qname] = value + end + + return is_nil, type, arytype, root, offset, position, href, id, extraattr + end + + def decode_arypos(position) + /^\[(.+)\]$/ =~ position + $1.split(',').collect { |s| s.to_i } + end + + def decode_resolve_id + count = @refpool.length # To avoid infinite loop + while !@refpool.empty? && count > 0 + @refpool = @refpool.find_all { |ref| + o = @idpool.find { |item| + ('#' << item.id == ref.refid) + } + unless o + raise EncodingStyleError.new("Unresolved reference: #{ ref.refid }.") + end + if o.is_a?(SOAPReference) + true + else + ref.__setobj__(o) + false + end + } + count -= 1 + end + end +end + +SOAPHandler.new + + +end +end diff --git a/lib/soap/generator.rb b/lib/soap/generator.rb new file mode 100644 index 000000000..6707aef19 --- /dev/null +++ b/lib/soap/generator.rb @@ -0,0 +1,206 @@ +=begin +SOAP4R - SOAP XML Instance Generator library. +Copyright (C) 2001, 2003 NAKAMURA, Hiroshi. + +This program is free software; you can redistribute it and/or modify it under +the terms of the GNU General Public License as published by the Free Software +Foundation; either version 2 of the License, or (at your option) any later +version. + +This program is distributed in the hope that it will be useful, but WITHOUT ANY +WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A +PRATICULAR PURPOSE. See the GNU General Public License for more details. + +You should have received a copy of the GNU General Public License along with +this program; if not, write to the Free Software Foundation, Inc., 675 Mass +Ave, Cambridge, MA 02139, USA. +=end + + +require 'xsd/ns' +require 'soap/soap' +require 'soap/baseData' +require 'soap/encodingstyle/handler' + + +module SOAP + + +### +## CAUTION: MT-unsafe +# +class SOAPGenerator + include SOAP + + class FormatEncodeError < Error; end + +public + + attr_accessor :charset + attr_accessor :default_encodingstyle + attr_accessor :generate_explicit_type + attr_accessor :pretty + + def initialize(opt = {}) + @reftarget = nil + @handlers = {} + @charset = opt[:charset] || XSD::Charset.encoding_label + @default_encodingstyle = opt[:default_encodingstyle] || EncodingNamespace + @generate_explicit_type = + opt.key?(:generate_explicit_type) ? opt[:generate_explicit_type] : true + @pretty = true # opt[:pretty] + end + + def generate(obj, io = nil) + prologue + @handlers.each do |uri, handler| + handler.encode_prologue + end + + io = '' if io.nil? + + ns = XSD::NS.new + io << xmldecl + encode_data(io, ns, true, obj, nil, 0) + + @handlers.each do |uri, handler| + handler.encode_epilogue + end + epilogue + + io + end + + def encode_data(buf, ns, qualified, obj, parent, indent) + if obj.is_a?(SOAPEnvelopeElement) + encode_element(buf, ns, qualified, obj, parent, indent) + return + end + + if @reftarget && !obj.precedents.empty? + @reftarget.add(obj.elename.name, obj) + ref = SOAPReference.new + ref.elename.name = obj.elename.name + ref.__setobj__(obj) + obj.precedents.clear # Avoid cyclic delay. + obj.encodingstyle = parent.encodingstyle + # SOAPReference is encoded here. + obj = ref + end + + encodingstyle = obj.encodingstyle + # Children's encodingstyle is derived from its parent. + encodingstyle ||= parent.encodingstyle if parent + obj.encodingstyle = encodingstyle + + handler = find_handler(encodingstyle || @default_encodingstyle) + unless handler + raise FormatEncodeError.new("Unknown encodingStyle: #{ encodingstyle }.") + end + + if !obj.elename.name + raise FormatEncodeError.new("Element name not defined: #{ obj }.") + end + + indent_str = ' ' * indent + child_indent = @pretty ? indent + 2 : indent + handler.encode_data(buf, ns, qualified, obj, parent, indent_str) do |child, child_q| + encode_data(buf, ns.clone_ns, child_q, child, obj, child_indent) + end + handler.encode_data_end(buf, ns, qualified, obj, parent, indent_str) + end + + def encode_element(buf, ns, qualified, obj, parent, indent) + indent_str = ' ' * indent + child_indent = @pretty ? indent + 2 : indent + attrs = {} + if obj.is_a?(SOAPBody) + @reftarget = obj + obj.encode(buf, ns, attrs, indent_str) do |child, child_q| + encode_data(buf, ns.clone_ns, child_q, child, obj, child_indent) + end + @reftarget = nil + else + if obj.is_a?(SOAPEnvelope) + # xsi:nil="true" can appear even if dumping without explicit type. + SOAPGenerator.assign_ns(attrs, ns, + XSD::InstanceNamespace, XSINamespaceTag) + if @generate_explicit_type + SOAPGenerator.assign_ns(attrs, ns, XSD::Namespace, XSDNamespaceTag) + end + end + obj.encode(buf, ns, attrs, indent_str) do |child, child_q| + encode_data(buf, ns.clone_ns, child_q, child, obj, child_indent) + end + end + end + + def self.assign_ns(attrs, ns, namespace, tag = nil) + unless ns.assigned?(namespace) + tag = ns.assign(namespace, tag) + attrs['xmlns:' << tag] = namespace + end + end + + def self.encode_tag(buf, elename, attrs = nil, indent = '') + if attrs + buf << "\n#{ indent }<#{ elename }" << + attrs.collect { |key, value| + %Q[ #{ key }="#{ value }"] + }.join << + '>' + else + buf << "\n#{ indent }<#{ elename }>" + end + end + + def self.encode_tag_end(buf, elename, indent = '', cr = nil) + if cr + buf << "\n#{ indent }</#{ elename }>" + else + buf << "</#{ elename }>" + end + end + + EncodeMap = { + '&' => '&', + '<' => '<', + '>' => '>', + '"' => '"', + '\'' => ''', + "\r" => '
' + } + EncodeCharRegexp = Regexp.new("[#{EncodeMap.keys.join}]") + def self.encode_str(str) + str.gsub(EncodeCharRegexp) { |c| EncodeMap[c] } + end + +private + + def prologue + end + + def epilogue + end + + def find_handler(encodingstyle) + unless @handlers.key?(encodingstyle) + handler = SOAP::EncodingStyle::Handler.handler(encodingstyle).new(@charset) + handler.generate_explicit_type = @generate_explicit_type + handler.encode_prologue + @handlers[encodingstyle] = handler + end + @handlers[encodingstyle] + end + + def xmldecl + if @charset + %Q[<?xml version="1.0" encoding="#{ @charset }" ?>] + else + %Q[<?xml version="1.0" ?>] + end + end +end + + +end diff --git a/lib/soap/mapping.rb b/lib/soap/mapping.rb new file mode 100644 index 000000000..8da1978a4 --- /dev/null +++ b/lib/soap/mapping.rb @@ -0,0 +1,21 @@ +=begin +SOAP4R - Ruby type mapping utility. +Copyright (C) 2003 NAKAMURA, Hiroshi. + +This program is free software; you can redistribute it and/or modify it under +the terms of the GNU General Public License as published by the Free Software +Foundation; either version 2 of the License, or (at your option) any later +version. + +This program is distributed in the hope that it will be useful, but WITHOUT ANY +WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A +PRATICULAR PURPOSE. See the GNU General Public License for more details. + +You should have received a copy of the GNU General Public License along with +this program; if not, write to the Free Software Foundation, Inc., 675 Mass +Ave, Cambridge, MA 02139, USA. +=end + + +require 'soap/mapping/mapping' +require 'soap/mapping/registry' diff --git a/lib/soap/mapping/factory.rb b/lib/soap/mapping/factory.rb new file mode 100644 index 000000000..2e5ddc1f1 --- /dev/null +++ b/lib/soap/mapping/factory.rb @@ -0,0 +1,310 @@ +=begin +SOAP4R - Mapping factory. +Copyright (C) 2000, 2001, 2002, 2003 NAKAMURA, Hiroshi. + +This program is free software; you can redistribute it and/or modify it under +the terms of the GNU General Public License as published by the Free Software +Foundation; either version 2 of the License, or (at your option) any later +version. + +This program is distributed in the hope that it will be useful, but WITHOUT ANY +WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A +PRATICULAR PURPOSE. See the GNU General Public License for more details. + +You should have received a copy of the GNU General Public License along with +this program; if not, write to the Free Software Foundation, Inc., 675 Mass +Ave, Cambridge, MA 02139, USA. +=end + + +module SOAP +module Mapping + + +class Factory + include TraverseSupport + + def obj2soap(soap_class, obj, info, map) + raise NotImplementError.new + # return soap_obj + end + + def soap2obj(obj_class, node, info, map) + raise NotImplementError.new + # return convert_succeeded_or_not, obj + end + + if Object.respond_to?(:allocate) + # ruby/1.7 or later. + def create_empty_object(klass) + klass.allocate + end + else + def create_empty_object(klass) + name = klass.name + # Below line is from TANAKA, Akira's amarshal.rb. + # See http://cvs.m17n.org/cgi-bin/viewcvs/amarshal/?cvsroot=ruby + ::Marshal.load(sprintf("\004\006o:%c%s\000", name.length + 5, name)) + end + end + + def set_instance_vars(obj, values) + values.each do |name, value| + setter = name + "=" + if obj.respond_to?(setter) + obj.__send__(setter, value) + else + obj.instance_eval("@#{ name } = value") + end + end + end + + def setiv2obj(obj, node, map) + return if node.nil? + vars = {} + node.each do |name, value| + vars[Mapping.elename2name(name)] = Mapping._soap2obj(value, map) + end + set_instance_vars(obj, vars) + end + + def setiv2soap(node, obj, map) + obj.instance_variables.each do |var| + name = var.sub(/^@/, '') + node.add(Mapping.name2elename(name), + Mapping._obj2soap(obj.instance_eval(var), map)) + end + end + + def addiv2soap(node, obj, map) + return if obj.instance_variables.empty? + ivars = SOAPStruct.new # Undefined type. + setiv2soap(ivars, obj, map) + node.add('ivars', ivars) + end + + # It breaks Thread.current[:SOAPMarshalDataKey]. + def mark_marshalled_obj(obj, soap_obj) + Thread.current[:SOAPMarshalDataKey][obj.__id__] = soap_obj + end + + # It breaks Thread.current[:SOAPMarshalDataKey]. + def mark_unmarshalled_obj(node, obj) + Thread.current[:SOAPMarshalDataKey][node.id] = obj + end + + def name2typename(name) + capitalize(name) + end + + def capitalize(target) + target.gsub(/^([a-z])/) { $1.tr!('[a-z]', '[A-Z]') } + end +end + +class StringFactory_ < Factory + def obj2soap(soap_class, obj, info, map) + begin + if XSD::Charset.is_ces(obj, $KCODE) + encoded = XSD::Charset.encoding_conv(obj, $KCODE, XSD::Charset.encoding) + soap_obj = soap_class.new(encoded) + else + return nil + end + rescue XSD::ValueSpaceError + return nil + end + mark_marshalled_obj(obj, soap_obj) + soap_obj + end + + def soap2obj(obj_class, node, info, map) + obj = XSD::Charset.encoding_conv(node.data, XSD::Charset.encoding, $KCODE) + mark_unmarshalled_obj(node, obj) + return true, obj + end +end + +class BasetypeFactory_ < Factory + def obj2soap(soap_class, obj, info, map) + soap_obj = nil + begin + soap_obj = soap_class.new(obj) + rescue XSD::ValueSpaceError + return nil + end + # Basetype except String should not be multiref-ed in SOAP/1.1. + soap_obj + end + + def soap2obj(obj_class, node, info, map) + obj = node.data + mark_unmarshalled_obj(node, obj) + return true, obj + end +end + +class DateTimeFactory_ < Factory + def obj2soap(soap_class, obj, info, map) + soap_obj = nil + begin + soap_obj = soap_class.new(obj) + rescue XSD::ValueSpaceError + return nil + end + mark_marshalled_obj(obj, soap_obj) + soap_obj + end + + def soap2obj(obj_class, node, info, map) + obj = nil + if obj_class == Time + obj = node.to_time + if obj.nil? + # Is out of range as a Time + return false + end + elsif obj_class == Date + obj = node.data + else + return false + end + mark_unmarshalled_obj(node, obj) + return true, obj + end +end + +class Base64Factory_ < Factory + def obj2soap(soap_class, obj, info, map) + soap_obj = soap_class.new(obj) + mark_marshalled_obj(obj, soap_obj) if soap_obj + soap_obj + end + + def soap2obj(obj_class, node, info, map) + obj = node.string + mark_unmarshalled_obj(node, obj) + return true, obj + end +end + +class ArrayFactory_ < Factory + # [[1], [2]] is converted to Array of Array, not 2-D Array. + # To create M-D Array, you must call Mapping.ary2md. + def obj2soap(soap_class, obj, info, map) + arytype = Mapping.obj2element(obj) + if arytype.name + arytype.namespace ||= RubyTypeNamespace + else + arytype = XSD::AnyTypeName + end + param = SOAPArray.new(ValueArrayName, 1, arytype) + mark_marshalled_obj(obj, param) + obj.each do |var| + param.add(Mapping._obj2soap(var, map)) + end + param + end + + def soap2obj(obj_class, node, info, map) + obj = create_empty_object(obj_class) + mark_unmarshalled_obj(node, obj) + node.soap2array(obj) do |elem| + elem ? Mapping._soap2obj(elem, map) : nil + end + return true, obj + end +end + +class TypedArrayFactory_ < Factory + def obj2soap(soap_class, obj, info, map) + arytype = info[:type] || info[0] + param = SOAPArray.new(ValueArrayName, 1, arytype) + mark_marshalled_obj(obj, param) + obj.each do |var| + param.add(Mapping._obj2soap(var, map)) + end + param + end + + def soap2obj(obj_class, node, info, map) + if node.rank > 1 + return false + end + arytype = info[:type] || info[0] + unless node.arytype == arytype + return false + end + obj = create_empty_object(obj_class) + mark_unmarshalled_obj(node, obj) + node.soap2array(obj) do |elem| + elem ? Mapping._soap2obj(elem, map) : nil + end + return true, obj + end +end + +class TypedStructFactory_ < Factory + def obj2soap(soap_class, obj, info, map) + type = info[:type] || info[0] + param = soap_class.new(type) + mark_marshalled_obj(obj, param) + if obj.class <= SOAP::Marshallable + setiv2soap(param, obj, map) + else + setiv2soap(param, obj, map) + end + param + end + + def soap2obj(obj_class, node, info, map) + type = info[:type] || info[0] + unless node.type == type + return false + end + obj = create_empty_object(obj_class) + mark_unmarshalled_obj(node, obj) + setiv2obj(obj, node, map) + return true, obj + end +end + +MapQName = XSD::QName.new(ApacheSOAPTypeNamespace, 'Map') +class HashFactory_ < Factory + def obj2soap(soap_class, obj, info, map) + if obj.default or + (obj.respond_to?(:default_proc) and obj.default_proc) + return nil + end + param = SOAPStruct.new(MapQName) + mark_marshalled_obj(obj, param) + obj.each do |key, value| + elem = SOAPStruct.new + elem.add("key", Mapping._obj2soap(key, map)) + elem.add("value", Mapping._obj2soap(value, map)) + # ApacheAxis allows only 'item' here. + param.add("item", elem) + end + param + end + + def soap2obj(obj_class, node, info, map) + unless node.type == MapQName + return false + end + if node.key?('default') + return false + end + obj = create_empty_object(obj_class) + mark_unmarshalled_obj(node, obj) + node.each do |key, value| + obj[Mapping._soap2obj(value['key'], map)] = + Mapping._soap2obj(value['value'], map) + end + return true, obj + end +end + + +end +end diff --git a/lib/soap/mapping/mapping.rb b/lib/soap/mapping/mapping.rb new file mode 100644 index 000000000..19eca5dab --- /dev/null +++ b/lib/soap/mapping/mapping.rb @@ -0,0 +1,218 @@ +=begin +SOAP4R - Ruby type mapping utility. +Copyright (C) 2000, 2001, 2003 NAKAMURA Hiroshi. + +This program is free software; you can redistribute it and/or modify it under +the terms of the GNU General Public License as published by the Free Software +Foundation; either version 2 of the License, or (at your option) any later +version. + +This program is distributed in the hope that it will be useful, but WITHOUT ANY +WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A +PRATICULAR PURPOSE. See the GNU General Public License for more details. + +You should have received a copy of the GNU General Public License along with +this program; if not, write to the Free Software Foundation, Inc., 675 Mass +Ave, Cambridge, MA 02139, USA. +=end + + +module SOAP + + +module Mapping + RubyTypeNamespace = 'http://www.ruby-lang.org/xmlns/ruby/type/1.6' + RubyTypeInstanceNamespace = + 'http://www.ruby-lang.org/xmlns/ruby/type-instance' + RubyCustomTypeNamespace = 'http://www.ruby-lang.org/xmlns/ruby/type/custom' + ApacheSOAPTypeNamespace = 'http://xml.apache.org/xml-soap' + + + # TraverseSupport breaks Thread.current[:SOAPMarshalDataKey]. + module TraverseSupport + def mark_marshalled_obj(obj, soap_obj) + Thread.current[:SOAPMarshalDataKey][obj.__id__] = soap_obj + end + + def mark_unmarshalled_obj(node, obj) + # node.id is not Object#id but SOAPReference#id + Thread.current[:SOAPMarshalDataKey][node.id] = obj + end + end + + + def self.obj2soap(obj, registry = nil, type = nil) + registry ||= Mapping::DefaultRegistry + Thread.current[:SOAPMarshalDataKey] = {} + soap_obj = _obj2soap(obj, registry, type) + Thread.current[:SOAPMarshalDataKey] = nil + soap_obj + end + + def self.soap2obj(node, registry = nil) + registry ||= Mapping::DefaultRegistry + Thread.current[:SOAPMarshalDataKey] = {} + obj = _soap2obj(node, registry) + Thread.current[:SOAPMarshalDataKey] = nil + obj + end + + def self.ary2soap(ary, type_ns = XSD::Namespace, typename = XSD::AnyTypeLiteral, registry = nil) + registry ||= Mapping::DefaultRegistry + type = XSD::QName.new(type_ns, typename) + soap_ary = SOAPArray.new(ValueArrayName, 1, type) + Thread.current[:SOAPMarshalDataKey] = {} + ary.each do |ele| + soap_ary.add(_obj2soap(ele, registry, type)) + end + Thread.current[:SOAPMarshalDataKey] = nil + soap_ary + end + + def self.ary2md(ary, rank, type_ns = XSD::Namespace, typename = XSD::AnyTypeLiteral, registry = nil) + registry ||= Mapping::DefaultRegistry + type = XSD::QName.new(type_ns, typename) + md_ary = SOAPArray.new(ValueArrayName, rank, type) + Thread.current[:SOAPMarshalDataKey] = {} + add_md_ary(md_ary, ary, [], registry) + Thread.current[:SOAPMarshalDataKey] = nil + md_ary + end + + def self.fault2exception(e, registry = nil) + registry ||= Mapping::DefaultRegistry + detail = if e.detail + soap2obj(e.detail, registry) || "" + else + "" + end + if detail.is_a?(Mapping::SOAPException) + begin + raise detail.to_e + rescue Exception => e2 + detail.set_backtrace(e2) + raise + end + else + e.detail = detail + e.set_backtrace( + if detail.is_a?(Array) + detail + else + [detail.to_s] + end + ) + raise + end + end + + def self._obj2soap(obj, registry, type = nil) + if referent = Thread.current[:SOAPMarshalDataKey][obj.__id__] + soap_obj = SOAPReference.new + soap_obj.__setobj__(referent) + soap_obj + else + registry.obj2soap(obj.class, obj, type) + end + end + + def self._soap2obj(node, registry) + if node.is_a?(SOAPReference) + target = node.__getobj__ + # target.id is not Object#id but SOAPReference#id + if referent = Thread.current[:SOAPMarshalDataKey][target.id] + return referent + else + return _soap2obj(target, registry) + end + end + return registry.soap2obj(node.class, node) + end + + + # Allow only (Letter | '_') (Letter | Digit | '-' | '_')* here. + # Caution: '.' is not allowed here. + # To follow XML spec., it should be NCName. + # (denied chars) => .[0-F][0-F] + # ex. a.b => a.2eb + # + def self.name2elename(name) + name.gsub(/([^a-zA-Z0-9:_\-]+)/n) { + '.' << $1.unpack('H2' * $1.size).join('.') + }.gsub(/::/n, '..') + end + + def self.elename2name(name) + name.gsub(/\.\./n, '::').gsub(/((?:\.[0-9a-fA-F]{2})+)/n) { + [$1.delete('.')].pack('H*') + } + end + + def self.class_from_name(name) + if /^[A-Z]/ !~ name + return nil + end + klass = ::Object + name.split('::').each do |klass_str| + if klass.const_defined?(klass_str) + klass = klass.const_get(klass_str) + else + return nil + end + end + klass + end + + def self.class2qname(klass) + name = if klass.class_variables.include?("@@schema_type") + klass.class_eval("@@schema_type") + else + nil + end + namespace = if klass.class_variables.include?("@@schema_ns") + klass.class_eval("@@schema_ns") + else + nil + end + XSD::QName.new(namespace, name) + end + + def self.class2element(klass) + type = Mapping.class2qname(klass) + type.name ||= Mapping.name2elename(klass.name) + type.namespace ||= RubyCustomTypeNamespace + type + end + + def self.obj2element(obj) + name = namespace = nil + ivars = obj.instance_variables + if ivars.include?("@schema_type") + name = obj.instance_eval("@schema_type") + end + if ivars.include?("@schema_ns") + namespace = obj.instance_eval("@schema_ns") + end + if !name or !namespace + class2qname(obj.class) + else + XSD::QName.new(namespace, name) + end + end + + class << Mapping + private + def add_md_ary(md_ary, ary, indices, registry) + for idx in 0..(ary.size - 1) + if ary[idx].is_a?(Array) + add_md_ary(md_ary, ary[idx], indices + [idx], registry) + else + md_ary[*(indices + [idx])] = _obj2soap(ary[idx], registry) + end + end + end + end +end + + +end diff --git a/lib/soap/mapping/registry.rb b/lib/soap/mapping/registry.rb new file mode 100644 index 000000000..568f34ac3 --- /dev/null +++ b/lib/soap/mapping/registry.rb @@ -0,0 +1,408 @@ +=begin +SOAP4R - Mapping registry. +Copyright (C) 2000, 2001, 2002, 2003 NAKAMURA, Hiroshi. + +This program is free software; you can redistribute it and/or modify it under +the terms of the GNU General Public License as published by the Free Software +Foundation; either version 2 of the License, or (at your option) any later +version. + +This program is distributed in the hope that it will be useful, but WITHOUT ANY +WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A +PRATICULAR PURPOSE. See the GNU General Public License for more details. + +You should have received a copy of the GNU General Public License along with +this program; if not, write to the Free Software Foundation, Inc., 675 Mass +Ave, Cambridge, MA 02139, USA. +=end + + +require 'soap/baseData' +require 'soap/mapping/mapping' +require 'soap/mapping/typeMap' +require 'soap/mapping/factory' +require 'soap/mapping/rubytypeFactory' + + +module SOAP + + +module Marshallable + # @@type_ns = Mapping::RubyCustomTypeNamespace +end + + +module Mapping + + +module MappedException; end + + +RubyTypeName = XSD::QName.new(RubyTypeInstanceNamespace, 'rubyType') + + +# Inner class to pass an exception. +class SOAPException; include Marshallable + attr_reader :excn_type_name, :message, :backtrace, :cause + def initialize(e) + @excn_type_name = Mapping.name2elename(e.class.to_s) + @message = e.message + @backtrace = e.backtrace + @cause = e + end + + def to_e + if @cause.is_a?(::Exception) + @cause.extend(::SOAP::Mapping::MappedException) + return @cause + end + klass = Mapping.class_from_name( + Mapping.elename2name(@excn_type_name.to_s)) + if klass.nil? + raise RuntimeError.new(@message) + end + unless klass <= ::Exception + raise NameError.new + end + obj = klass.new(@message) + obj.extend(::SOAP::Mapping::MappedException) + obj + end + + def set_backtrace(e) + e.set_backtrace( + if @backtrace.is_a?(Array) + @backtrace + else + [@backtrace.inspect] + end + ) + end +end + + +# For anyType object: SOAP::Mapping::Object not ::Object +class Object; include Marshallable + def set_property(name, value) + var_name = name + begin + instance_eval <<-EOS + def #{ var_name } + @#{ var_name } + end + + def #{ var_name }=(value) + @#{ var_name } = value + end + EOS + self.send(var_name + '=', value) + rescue SyntaxError + var_name = safe_name(var_name) + retry + end + + var_name + end + + def members + instance_variables.collect { |str| str[1..-1] } + end + + def [](name) + if self.respond_to?(name) + self.send(name) + else + self.send(safe_name(name)) + end + end + + def []=(name, value) + if self.respond_to?(name) + self.send(name + '=', value) + else + self.send(safe_name(name) + '=', value) + end + end + +private + + def safe_name(name) + require 'md5' + "var_" << MD5.new(name).hexdigest + end +end + + +class MappingError < Error; end + + +class Registry + class Map + def initialize(registry) + @map = [] + @registry = registry + end + + def obj2soap(klass, obj) + @map.each do |obj_class, soap_class, factory, info| + if klass == obj_class or + (info[:derived_class] and klass <= obj_class) + ret = factory.obj2soap(soap_class, obj, info, @registry) + return ret if ret + end + end + nil + end + + def soap2obj(klass, node) + @map.each do |obj_class, soap_class, factory, info| + if klass == soap_class or + (info[:derived_class] and klass <= soap_class) + conv, obj = factory.soap2obj(obj_class, node, info, @registry) + return true, obj if conv + end + end + return false + end + + # Give priority to former entry. + def init(init_map = []) + clear + init_map.reverse_each do |obj_class, soap_class, factory, info| + add(obj_class, soap_class, factory, info) + end + end + + # Give priority to latter entry. + def add(obj_class, soap_class, factory, info) + info ||= {} + @map.unshift([obj_class, soap_class, factory, info]) + end + + def clear + @map.clear + end + + def find_mapped_soap_class(target_obj_class) + @map.each do |obj_class, soap_class, factory, info| + if obj_class == target_obj_class + return soap_class + end + end + nil + end + + def find_mapped_obj_class(target_soap_class) + @map.each do |obj_class, soap_class, factory, info| + if soap_class == target_soap_class + return obj_class + end + end + nil + end + end + + StringFactory = StringFactory_.new + BasetypeFactory = BasetypeFactory_.new + DateTimeFactory = DateTimeFactory_.new + ArrayFactory = ArrayFactory_.new + Base64Factory = Base64Factory_.new + TypedArrayFactory = TypedArrayFactory_.new + TypedStructFactory = TypedStructFactory_.new + + HashFactory = HashFactory_.new + + SOAPBaseMap = [ + [::NilClass, ::SOAP::SOAPNil, BasetypeFactory], + [::TrueClass, ::SOAP::SOAPBoolean, BasetypeFactory], + [::FalseClass, ::SOAP::SOAPBoolean, BasetypeFactory], + [::String, ::SOAP::SOAPString, StringFactory], + [::DateTime, ::SOAP::SOAPDateTime, BasetypeFactory], + [::Date, ::SOAP::SOAPDateTime, BasetypeFactory], + [::Date, ::SOAP::SOAPDate, BasetypeFactory], + [::Time, ::SOAP::SOAPDateTime, BasetypeFactory], + [::Time, ::SOAP::SOAPTime, BasetypeFactory], + [::Float, ::SOAP::SOAPDouble, BasetypeFactory, + {:derived_class => true}], + [::Float, ::SOAP::SOAPFloat, BasetypeFactory, + {:derived_class => true}], + [::Integer, ::SOAP::SOAPInt, BasetypeFactory, + {:derived_class => true}], + [::Integer, ::SOAP::SOAPLong, BasetypeFactory, + {:derived_class => true}], + [::Integer, ::SOAP::SOAPInteger, BasetypeFactory, + {:derived_class => true}], + [::Integer, ::SOAP::SOAPShort, BasetypeFactory, + {:derived_class => true}], + [::URI::Generic, ::SOAP::SOAPAnyURI, BasetypeFactory, + {:derived_class => true}], + [::String, ::SOAP::SOAPBase64, Base64Factory], + [::String, ::SOAP::SOAPHexBinary, Base64Factory], + [::String, ::SOAP::SOAPDecimal, BasetypeFactory], + [::String, ::SOAP::SOAPDuration, BasetypeFactory], + [::String, ::SOAP::SOAPGYearMonth, BasetypeFactory], + [::String, ::SOAP::SOAPGYear, BasetypeFactory], + [::String, ::SOAP::SOAPGMonthDay, BasetypeFactory], + [::String, ::SOAP::SOAPGDay, BasetypeFactory], + [::String, ::SOAP::SOAPGMonth, BasetypeFactory], + [::String, ::SOAP::SOAPQName, BasetypeFactory], + + [::Array, ::SOAP::SOAPArray, ArrayFactory, + {:derived_class => true}], + + [::Hash, ::SOAP::SOAPStruct, HashFactory], + [::SOAP::Mapping::SOAPException, + ::SOAP::SOAPStruct, TypedStructFactory, + {:type => XSD::QName.new(RubyCustomTypeNamespace, "SOAPException")}], + ] + + RubyOriginalMap = [ + [::NilClass, ::SOAP::SOAPNil, BasetypeFactory], + [::TrueClass, ::SOAP::SOAPBoolean, BasetypeFactory], + [::FalseClass, ::SOAP::SOAPBoolean, BasetypeFactory], + [::String, ::SOAP::SOAPString, StringFactory], + [::DateTime, ::SOAP::SOAPDateTime, BasetypeFactory], + [::Date, ::SOAP::SOAPDateTime, BasetypeFactory], + [::Date, ::SOAP::SOAPDate, BasetypeFactory], + [::Time, ::SOAP::SOAPDateTime, BasetypeFactory], + [::Time, ::SOAP::SOAPTime, BasetypeFactory], + [::Float, ::SOAP::SOAPDouble, BasetypeFactory, + {:derived_class => true}], + [::Float, ::SOAP::SOAPFloat, BasetypeFactory, + {:derived_class => true}], + [::Integer, ::SOAP::SOAPInt, BasetypeFactory, + {:derived_class => true}], + [::Integer, ::SOAP::SOAPLong, BasetypeFactory, + {:derived_class => true}], + [::Integer, ::SOAP::SOAPInteger, BasetypeFactory, + {:derived_class => true}], + [::Integer, ::SOAP::SOAPShort, BasetypeFactory, + {:derived_class => true}], + [::URI::Generic, ::SOAP::SOAPAnyURI, BasetypeFactory, + {:derived_class => true}], + [::String, ::SOAP::SOAPBase64, Base64Factory], + [::String, ::SOAP::SOAPHexBinary, Base64Factory], + [::String, ::SOAP::SOAPDecimal, BasetypeFactory], + [::String, ::SOAP::SOAPDuration, BasetypeFactory], + [::String, ::SOAP::SOAPGYearMonth, BasetypeFactory], + [::String, ::SOAP::SOAPGYear, BasetypeFactory], + [::String, ::SOAP::SOAPGMonthDay, BasetypeFactory], + [::String, ::SOAP::SOAPGDay, BasetypeFactory], + [::String, ::SOAP::SOAPGMonth, BasetypeFactory], + [::String, ::SOAP::SOAPQName, BasetypeFactory], + + # Does not allow Array's subclass here. + [::Array, ::SOAP::SOAPArray, ArrayFactory], + + [::Hash, ::SOAP::SOAPStruct, HashFactory], + [::SOAP::Mapping::SOAPException, + ::SOAP::SOAPStruct, TypedStructFactory, + {:type => XSD::QName.new(RubyCustomTypeNamespace, "SOAPException")}], + ] + + def initialize(config = {}) + @config = config + @map = Map.new(self) + if @config[:allow_original_mapping] + allow_original_mapping = true + @map.init(RubyOriginalMap) + else + allow_original_mapping = false + @map.init(SOAPBaseMap) + end + + allow_untyped_struct = @config.key?(:allow_untyped_struct) ? + @config[:allow_untyped_struct] : true + @rubytype_factory = RubytypeFactory.new( + :allow_untyped_struct => allow_untyped_struct, + :allow_original_mapping => allow_original_mapping + ) + @default_factory = @rubytype_factory + @excn_handler_obj2soap = nil + @excn_handler_soap2obj = nil + end + + def add(obj_class, soap_class, factory, info = nil) + @map.add(obj_class, soap_class, factory, info) + end + alias :set :add + + # This mapping registry ignores type hint. + def obj2soap(klass, obj, type = nil) + ret = nil + if obj.is_a?(SOAPStruct) || obj.is_a?(SOAPArray) + obj.replace do |ele| + Mapping._obj2soap(ele, self) + end + return obj + elsif obj.is_a?(SOAPBasetype) + return obj + end + begin + ret = @map.obj2soap(klass, obj) || + @default_factory.obj2soap(klass, obj, nil, self) + rescue MappingError + end + return ret if ret + + if @excn_handler_obj2soap + ret = @excn_handler_obj2soap.call(obj) { |yield_obj| + Mapping._obj2soap(yield_obj, self) + } + end + return ret if ret + + raise MappingError.new("Cannot map #{ klass.name } to SOAP/OM.") + end + + def soap2obj(klass, node) + if node.extraattr.key?(RubyTypeName) + conv, obj = @rubytype_factory.soap2obj(klass, node, nil, self) + return obj if conv + else + conv, obj = @map.soap2obj(klass, node) + return obj if conv + conv, obj = @default_factory.soap2obj(klass, node, nil, self) + return obj if conv + end + + if @excn_handler_soap2obj + begin + return @excn_handler_soap2obj.call(node) { |yield_node| + Mapping._soap2obj(yield_node, self) + } + rescue Exception + end + end + + raise MappingError.new("Cannot map #{ node.type.name } to Ruby object.") + end + + def default_factory=(factory) + @default_factory = factory + end + + def excn_handler_obj2soap=(handler) + @excn_handler_obj2soap = handler + end + + def excn_handler_soap2obj=(handler) + @excn_handler_soap2obj = handler + end + + def find_mapped_soap_class(obj_class) + @map.find_mapped_soap_class(obj_class) + end + + def find_mapped_obj_class(soap_class) + @map.find_mapped_obj_class(soap_class) + end +end + + +DefaultRegistry = Registry.new +RubyOriginalRegistry = Registry.new(:allow_original_mapping => true) + + +end +end diff --git a/lib/soap/mapping/rubytypeFactory.rb b/lib/soap/mapping/rubytypeFactory.rb new file mode 100644 index 000000000..0a3f502df --- /dev/null +++ b/lib/soap/mapping/rubytypeFactory.rb @@ -0,0 +1,437 @@ +=begin +SOAP4R - Ruby type mapping factory. +Copyright (C) 2000, 2001, 2002, 2003 NAKAMURA, Hiroshi. + +This program is free software; you can redistribute it and/or modify it under +the terms of the GNU General Public License as published by the Free Software +Foundation; either version 2 of the License, or (at your option) any later +version. + +This program is distributed in the hope that it will be useful, but WITHOUT ANY +WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A +PRATICULAR PURPOSE. See the GNU General Public License for more details. + +You should have received a copy of the GNU General Public License along with +this program; if not, write to the Free Software Foundation, Inc., 675 Mass +Ave, Cambridge, MA 02139, USA. +=end + + +module SOAP +module Mapping + + +class RubytypeFactory < Factory + TYPE_STRING = 'String' + TYPE_ARRAY = 'Array' + TYPE_REGEXP = 'Regexp' + TYPE_RANGE = 'Range' + TYPE_CLASS = 'Class' + TYPE_MODULE = 'Module' + TYPE_SYMBOL = 'Symbol' + TYPE_STRUCT = 'Struct' + TYPE_HASH = 'Map' + + def initialize(config = {}) + @config = config + @allow_untyped_struct = @config.key?(:allow_untyped_struct) ? + @config[:allow_untyped_struct] : true + @allow_original_mapping = @config.key?(:allow_original_mapping) ? + @config[:allow_original_mapping] : false + end + + def obj2soap(soap_class, obj, info, map) + param = nil + case obj + when String + unless @allow_original_mapping + return nil + end + unless XSD::Charset.is_ces(obj, $KCODE) + return nil + end + encoded = XSD::Charset.encoding_conv(obj, $KCODE, XSD::Charset.encoding) + param = SOAPStruct.new(XSD::QName.new(RubyTypeNamespace, TYPE_STRING)) + mark_marshalled_obj(obj, param) + param.add('string', SOAPString.new(encoded)) + if obj.class != String + param.extraattr[RubyTypeName] = obj.class.name + end + addiv2soap(param, obj, map) + when Array + unless @allow_original_mapping + return nil + end + arytype = Mapping.obj2element(obj) + if arytype.name + arytype.namespace ||= RubyTypeNamespace + else + arytype = XSD::AnyTypeName + end + if obj.instance_variables.empty? + param = SOAPArray.new(ValueArrayName, 1, arytype) + mark_marshalled_obj(obj, param) + obj.each do |var| + param.add(Mapping._obj2soap(var, map)) + end + else + param = SOAPStruct.new(XSD::QName.new(RubyTypeNamespace, TYPE_ARRAY)) + mark_marshalled_obj(obj, param) + ary = SOAPArray.new(ValueArrayName, 1, arytype) + obj.each do |var| + ary.add(Mapping._obj2soap(var, map)) + end + param.add('array', ary) + addiv2soap(param, obj, map) + end + if obj.class != Array + param.extraattr[RubyTypeName] = obj.class.name + end + when Regexp + param = SOAPStruct.new(XSD::QName.new(RubyTypeNamespace, TYPE_REGEXP)) + mark_marshalled_obj(obj, param) + if obj.class != Regexp + param.extraattr[RubyTypeName] = obj.class.name + end + param.add('source', SOAPBase64.new(obj.source)) + if obj.respond_to?('options') + # Regexp#options is from Ruby/1.7 + options = obj.options + else + options = 0 + obj.inspect.sub(/^.*\//, '').each_byte do |c| + options += case c + when ?i + 1 + when ?x + 2 + when ?m + 4 + when ?n + 16 + when ?e + 32 + when ?s + 48 + when ?u + 64 + end + end + end + param.add('options', SOAPInt.new(options)) + addiv2soap(param, obj, map) + when Range + param = SOAPStruct.new(XSD::QName.new(RubyTypeNamespace, TYPE_RANGE)) + mark_marshalled_obj(obj, param) + if obj.class != Range + param.extraattr[RubyTypeName] = obj.class.name + end + param.add('begin', Mapping._obj2soap(obj.begin, map)) + param.add('end', Mapping._obj2soap(obj.end, map)) + param.add('exclude_end', SOAP::SOAPBoolean.new(obj.exclude_end?)) + addiv2soap(param, obj, map) + when Hash + unless @allow_original_mapping + return nil + end + if obj.respond_to?(:default_proc) && obj.default_proc + raise TypeError.new("cannot dump hash with default proc") + end + param = SOAPStruct.new(XSD::QName.new(RubyTypeNamespace, TYPE_HASH)) + mark_marshalled_obj(obj, param) + if obj.class != Hash + param.extraattr[RubyTypeName] = obj.class.name + end + obj.each do |key, value| + elem = SOAPStruct.new # Undefined type. + elem.add("key", Mapping._obj2soap(key, map)) + elem.add("value", Mapping._obj2soap(value, map)) + param.add("item", elem) + end + param.add('default', Mapping._obj2soap(obj.default, map)) + addiv2soap(param, obj, map) + when Class + if obj.name.empty? + raise TypeError.new("Can't dump anonymous class #{ obj }.") + end + param = SOAPStruct.new(XSD::QName.new(RubyTypeNamespace, TYPE_CLASS)) + mark_marshalled_obj(obj, param) + param.add('name', SOAPString.new(obj.name)) + addiv2soap(param, obj, map) + when Module + if obj.name.empty? + raise TypeError.new("Can't dump anonymous module #{ obj }.") + end + param = SOAPStruct.new(XSD::QName.new(RubyTypeNamespace, TYPE_MODULE)) + mark_marshalled_obj(obj, param) + param.add('name', SOAPString.new(obj.name)) + addiv2soap(param, obj, map) + when Symbol + param = SOAPStruct.new(XSD::QName.new(RubyTypeNamespace, TYPE_SYMBOL)) + mark_marshalled_obj(obj, param) + param.add('id', SOAPString.new(obj.id2name)) + addiv2soap(param, obj, map) + when Exception + typestr = Mapping.name2elename(obj.class.to_s) + param = SOAPStruct.new(XSD::QName.new(RubyTypeNamespace, typestr)) + mark_marshalled_obj(obj, param) + param.add('message', Mapping._obj2soap(obj.message, map)) + param.add('backtrace', Mapping._obj2soap(obj.backtrace, map)) + addiv2soap(param, obj, map) + when Struct + param = SOAPStruct.new(XSD::QName.new(RubyTypeNamespace, TYPE_STRUCT)) + mark_marshalled_obj(obj, param) + param.add('type', ele_type = SOAPString.new(obj.class.to_s)) + ele_member = SOAPStruct.new + obj.members.each do |member| + ele_member.add(Mapping.name2elename(member), + Mapping._obj2soap(obj[member], map)) + end + param.add('member', ele_member) + addiv2soap(param, obj, map) + when IO, Binding, Continuation, Data, Dir, File::Stat, MatchData, Method, + Proc, Thread, ThreadGroup + return nil + when ::SOAP::Mapping::Object + param = SOAPStruct.new(XSD::AnyTypeName) + mark_marshalled_obj(obj, param) + setiv2soap(param, obj, map) # addiv2soap? + else + if obj.class.name.empty? + raise TypeError.new("Can't dump anonymous class #{ obj }.") + end + if check_singleton(obj) + raise TypeError.new("singleton can't be dumped #{ obj }") + end + type = Mapping.class2element(obj.class) + param = SOAPStruct.new(type) + mark_marshalled_obj(obj, param) + if obj.class <= Marshallable + setiv2soap(param, obj, map) + else + setiv2soap(param, obj, map) # Should not be marshalled? + end + end + param + end + + def soap2obj(obj_class, node, info, map) + rubytype = node.extraattr[RubyTypeName] + if rubytype or node.type.namespace == RubyTypeNamespace + rubytype2obj(node, map, rubytype) + elsif node.type == XSD::AnyTypeName or node.type == XSD::AnySimpleTypeName + anytype2obj(node, map) + else + unknowntype2obj(node, map) + end + end + +private + + def check_singleton(obj) + unless singleton_methods_true(obj).empty? + return true + end + singleton_class = class << obj; self; end + if !singleton_class.instance_variables.empty? or + !(singleton_class.ancestors - obj.class.ancestors).empty? + return true + end + false + end + + if RUBY_VERSION >= '1.8.0' + def singleton_methods_true(obj) + obj.singleton_methods(true) + end + else + def singleton_methods_true(obj) + obj.singleton_methods + end + end + + def rubytype2obj(node, map, rubytype) + obj = nil + case node.class + when SOAPString + obj = string2obj(node, map, rubytype) + obj.replace(node.data) + return true, obj + when SOAPArray + obj = array2obj(node, map, rubytype) + node.soap2array(obj) do |elem| + elem ? Mapping._soap2obj(elem, map) : nil + end + return true, obj + end + + case node.type.name + when TYPE_STRING + obj = string2obj(node, map, rubytype) + obj.replace(node['string'].data) + setiv2obj(obj, node['ivars'], map) + when TYPE_ARRAY + obj = array2obj(node, map, rubytype) + node['array'].soap2array(obj) do |elem| + elem ? Mapping._soap2obj(elem, map) : nil + end + setiv2obj(obj, node['ivars'], map) + when TYPE_REGEXP + klass = rubytype ? Mapping.class_from_name(rubytype) : Regexp + obj = create_empty_object(klass) + mark_unmarshalled_obj(node, obj) + source = node['source'].string + options = node['options'].data || 0 + obj.instance_eval { initialize(source, options) } + setiv2obj(obj, node['ivars'], map) + when TYPE_RANGE + klass = rubytype ? Mapping.class_from_name(rubytype) : Range + obj = create_empty_object(klass) + mark_unmarshalled_obj(node, obj) + first = Mapping._soap2obj(node['begin'], map) + last = Mapping._soap2obj(node['end'], map) + exclude_end = node['exclude_end'].data + obj.instance_eval { initialize(first, last, exclude_end) } + setiv2obj(obj, node['ivars'], map) + when TYPE_HASH + unless @allow_original_mapping + return false + end + klass = rubytype ? Mapping.class_from_name(rubytype) : Hash + obj = create_empty_object(klass) + mark_unmarshalled_obj(node, obj) + node.each do |key, value| + next unless key == 'item' + obj[Mapping._soap2obj(value['key'], map)] = + Mapping._soap2obj(value['value'], map) + end + if node.key?('default') + obj.default = Mapping._soap2obj(node['default'], map) + end + setiv2obj(obj, node['ivars'], map) + when TYPE_CLASS + obj = Mapping.class_from_name(node['name'].data) + setiv2obj(obj, node['ivars'], map) + when TYPE_MODULE + obj = Mapping.class_from_name(node['name'].data) + setiv2obj(obj, node['ivars'], map) + when TYPE_SYMBOL + obj = node['id'].data.intern + setiv2obj(obj, node['ivars'], map) + when TYPE_STRUCT + typestr = Mapping.elename2name(node['type'].data) + klass = Mapping.class_from_name(typestr) + if klass.nil? + klass = Mapping.class_from_name(name2typename(typestr)) + end + if klass.nil? + return false + end + unless klass <= ::Struct + return false + end + obj = create_empty_object(klass) + mark_unmarshalled_obj(node, obj) + node['member'].each do |name, value| + obj[Mapping.elename2name(name)] = + Mapping._soap2obj(value, map) + end + setiv2obj(obj, node['ivars'], map) + else + conv, obj = exception2obj(node, map) + unless conv + return false + end + setiv2obj(obj, node['ivars'], map) + end + return true, obj + end + + def exception2obj(node, map) + typestr = Mapping.elename2name(node.type.name) + klass = Mapping.class_from_name(typestr) + if klass.nil? + return false + end + unless klass <= Exception + return false + end + message = Mapping._soap2obj(node['message'], map) + backtrace = Mapping._soap2obj(node['backtrace'], map) + obj = create_empty_object(klass) + obj = obj.exception(message) + mark_unmarshalled_obj(node, obj) + obj.set_backtrace(backtrace) + setiv2obj(obj, node['ivars'], map) + return true, obj + end + + def anytype2obj(node, map) + case node + when SOAPBasetype + return true, node.data + when SOAPStruct + klass = ::SOAP::Mapping::Object + obj = klass.new + mark_unmarshalled_obj(node, obj) + node.each do |name, value| + obj.set_property(name, Mapping._soap2obj(value, map)) + end + return true, obj + else + return false + end + end + + def unknowntype2obj(node, map) + if node.is_a?(SOAPStruct) + obj = struct2obj(node, map) + return true, obj if obj + if !@allow_untyped_struct + return false + end + return anytype2obj(node, map) + else + # Basetype which is not defined... + return false + end + end + + def struct2obj(node, map) + obj = nil + typestr = Mapping.elename2name(node.type.name) + klass = Mapping.class_from_name(typestr) + if klass.nil? + klass = Mapping.class_from_name(name2typename(typestr)) + end + if klass.nil? + return nil + end + klass_type = Mapping.class2qname(klass) + return nil unless node.type.match(klass_type) + obj = create_empty_object(klass) + mark_unmarshalled_obj(node, obj) + setiv2obj(obj, node, map) + obj + end + + # Only creates empty array. Do String#replace it with real string. + def array2obj(node, map, rubytype) + klass = rubytype ? Mapping.class_from_name(rubytype) : Array + obj = create_empty_object(klass) + mark_unmarshalled_obj(node, obj) + obj + end + + # Only creates empty string. Do String#replace it with real string. + def string2obj(node, map, rubytype) + klass = rubytype ? Mapping.class_from_name(rubytype) : String + obj = create_empty_object(klass) + mark_unmarshalled_obj(node, obj) + obj + end +end + + +end +end diff --git a/lib/soap/mapping/typeMap.rb b/lib/soap/mapping/typeMap.rb new file mode 100644 index 000000000..c62f1482c --- /dev/null +++ b/lib/soap/mapping/typeMap.rb @@ -0,0 +1,52 @@ +=begin +SOAP4R - Base type mapping definition +Copyright (C) 2000, 2001, 2002, 2003 NAKAMURA, Hiroshi. + +This program is free software; you can redistribute it and/or modify it under +the terms of the GNU General Public License as published by the Free Software +Foundation; either version 2 of the License, or (at your option) any later +version. + +This program is distributed in the hope that it will be useful, but WITHOUT ANY +WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A +PRATICULAR PURPOSE. See the GNU General Public License for more details. + +You should have received a copy of the GNU General Public License along with +this program; if not, write to the Free Software Foundation, Inc., 675 Mass +Ave, Cambridge, MA 02139, USA. +=end + + +module SOAP + + +TypeMap = { + XSD::XSDAnySimpleType::Type => SOAPAnySimpleType, + XSD::XSDString::Type => SOAPString, + XSD::XSDBoolean::Type => SOAPBoolean, + XSD::XSDDecimal::Type => SOAPDecimal, + XSD::XSDFloat::Type => SOAPFloat, + XSD::XSDDouble::Type => SOAPDouble, + XSD::XSDDuration::Type => SOAPDuration, + XSD::XSDDateTime::Type => SOAPDateTime, + XSD::XSDTime::Type => SOAPTime, + XSD::XSDDate::Type => SOAPDate, + XSD::XSDGYearMonth::Type => SOAPGYearMonth, + XSD::XSDGYear::Type => SOAPGYear, + XSD::XSDGMonthDay::Type => SOAPGMonthDay, + XSD::XSDGDay::Type => SOAPGDay, + XSD::XSDGMonth::Type => SOAPGMonth, + XSD::XSDHexBinary::Type => SOAPHexBinary, + XSD::XSDBase64Binary::Type => SOAPBase64, + XSD::XSDAnyURI::Type => SOAPAnyURI, + XSD::XSDQName::Type => SOAPQName, + XSD::XSDInteger::Type => SOAPInteger, + XSD::XSDLong::Type => SOAPLong, + XSD::XSDInt::Type => SOAPInt, + XSD::XSDShort::Type => SOAPShort, + + SOAP::SOAPBase64::Type => SOAPBase64, +} + + +end diff --git a/lib/soap/mapping/wsdlRegistry.rb b/lib/soap/mapping/wsdlRegistry.rb new file mode 100644 index 000000000..4b5beabe7 --- /dev/null +++ b/lib/soap/mapping/wsdlRegistry.rb @@ -0,0 +1,146 @@ +=begin +SOAP4R - WSDL mapping registry. +Copyright (C) 2000, 2001, 2002, 2003 NAKAMURA, Hiroshi. + +This program is free software; you can redistribute it and/or modify it under +the terms of the GNU General Public License as published by the Free Software +Foundation; either version 2 of the License, or (at your option) any later +version. + +This program is distributed in the hope that it will be useful, but WITHOUT ANY +WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A +PRATICULAR PURPOSE. See the GNU General Public License for more details. + +You should have received a copy of the GNU General Public License along with +this program; if not, write to the Free Software Foundation, Inc., 675 Mass +Ave, Cambridge, MA 02139, USA. +=end + + +require 'soap/baseData' +require 'soap/mapping/mapping' +require 'soap/mapping/typeMap' + + +module SOAP +module Mapping + + +class WSDLRegistry + include TraverseSupport + + attr_reader :complextypes + + def initialize(complextypes, config = {}) + @complextypes = complextypes + @config = config + @excn_handler_obj2soap = nil + # For mapping AnyType element. + @rubytype_factory = RubytypeFactory.new( + :allow_untyped_struct => true, + :allow_original_mapping => true + ) + end + + def obj2soap(klass, obj, type_qname) + soap_obj = nil + if obj.nil? + soap_obj = SOAPNil.new + elsif obj.is_a?(XSD::NSDBase) + soap_obj = soap2soap(obj, type_qname) + elsif (type = @complextypes[type_qname]) + case type.compoundtype + when :TYPE_STRUCT + soap_obj = struct2soap(obj, type_qname, type) + when :TYPE_ARRAY + soap_obj = array2soap(obj, type_qname, type) + end + elsif (type = TypeMap[type_qname]) + soap_obj = base2soap(obj, type) + elsif type_qname == XSD::AnyTypeName + soap_obj = @rubytype_factory.obj2soap(nil, obj, nil, nil) + end + return soap_obj if soap_obj + + if @excn_handler_obj2soap + soap_obj = @excn_handler_obj2soap.call(obj) { |yield_obj| + Mapping._obj2soap(yield_obj, self) + } + end + return soap_obj if soap_obj + + raise MappingError.new("Cannot map #{ klass.name } to SOAP/OM.") + end + + def soap2obj(klass, node) + raise RuntimeError.new("#{ self } is for obj2soap only.") + end + + def excn_handler_obj2soap=(handler) + @excn_handler_obj2soap = handler + end + +private + + def soap2soap(obj, type_qname) + if obj.is_a?(SOAPBasetype) + obj + elsif obj.is_a?(SOAPStruct) && (type = @complextypes[type_qname]) + soap_obj = obj + mark_marshalled_obj(obj, soap_obj) + elements2soap(obj, soap_obj, type.content.elements) + soap_obj + elsif obj.is_a?(SOAPArray) && (type = @complextypes[type_qname]) + soap_obj = obj + contenttype = type.child_type + mark_marshalled_obj(obj, soap_obj) + obj.replace do |ele| + Mapping._obj2soap(ele, self, contenttype) + end + soap_obj + else + nil + end + end + + def base2soap(obj, type) + soap_obj = nil + if type <= XSD::XSDString + soap_obj = type.new(XSD::Charset.is_ces(obj, $KCODE) ? + XSD::Charset.encoding_conv(obj, $KCODE, XSD::Charset.encoding) : obj) + mark_marshalled_obj(obj, soap_obj) + else + soap_obj = type.new(obj) + end + soap_obj + end + + def struct2soap(obj, type_qname, type) + soap_obj = SOAPStruct.new(type_qname) + mark_marshalled_obj(obj, soap_obj) + elements2soap(obj, soap_obj, type.content.elements) + soap_obj + end + + def array2soap(obj, type_qname, type) + contenttype = type.child_type + soap_obj = SOAPArray.new(ValueArrayName, 1, contenttype) + mark_marshalled_obj(obj, soap_obj) + obj.each do |item| + soap_obj.add(Mapping._obj2soap(item, self, contenttype)) + end + soap_obj + end + + def elements2soap(obj, soap_obj, elements) + elements.each do |element| + name = element.name.name + child_obj = obj.instance_eval("@#{ name }") + soap_obj.add(name, Mapping._obj2soap(child_obj, self, element.type)) + end + end +end + + +end +end diff --git a/lib/soap/marshal.rb b/lib/soap/marshal.rb new file mode 100644 index 000000000..1dc4e1355 --- /dev/null +++ b/lib/soap/marshal.rb @@ -0,0 +1,71 @@ +=begin +SOAP4R - Marshalling/Unmarshalling Ruby's object using SOAP Encoding. +Copyright (C) 2001, 2003 NAKAMURA, Hiroshi. + +This program is free software; you can redistribute it and/or modify it under +the terms of the GNU General Public License as published by the Free Software +Foundation; either version 2 of the License, or (at your option) any later +version. + +This program is distributed in the hope that it will be useful, but WITHOUT ANY +WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A +PRATICULAR PURPOSE. See the GNU General Public License for more details. + +You should have received a copy of the GNU General Public License along with +this program; if not, write to the Free Software Foundation, Inc., 675 Mass +Ave, Cambridge, MA 02139, USA. +=end + + +# The original version of the marshal.rb to marshal/unmarshal Ruby's object +# using SOAP Encoding was written by Michael Neumann. His valuable comments +# and his program inspired me to write this. Thanks. + + +require "soap/mapping" +require "soap/processor" + + +module SOAP + + +module Marshal + # Trying xsd:dateTime data to be recovered as aTime. aDateTime if it fails. + MarshalMappingRegistry = Mapping::Registry.new(:allow_original_mapping => true) + MarshalMappingRegistry.add( + Time, + ::SOAP::SOAPDateTime, + ::SOAP::Mapping::Registry::DateTimeFactory +) + + class << self + public + def dump(obj, io = nil) + marshal(obj, MarshalMappingRegistry, io) + end + + def load(stream) + unmarshal(stream, MarshalMappingRegistry) + end + + def marshal(obj, mapping_registry = MarshalMappingRegistry, io = nil) + elename = Mapping.name2elename(obj.class.to_s) + soap_obj = Mapping.obj2soap(obj, mapping_registry) + body = SOAPBody.new + body.add(elename, soap_obj) + SOAP::Processor.marshal(nil, body, {}, io) + end + + def unmarshal(stream, mapping_registry = MarshalMappingRegistry) + header, body = SOAP::Processor.unmarshal(stream) + Mapping.soap2obj(body.root_node, mapping_registry) + end + end + +end + + +end + + +SOAPMarshal = SOAP::Marshal diff --git a/lib/soap/netHttpClient.rb b/lib/soap/netHttpClient.rb new file mode 100644 index 000000000..14003b34d --- /dev/null +++ b/lib/soap/netHttpClient.rb @@ -0,0 +1,101 @@ +=begin +SOAP4R - net/http wrapper +Copyright (C) 2003 NAKAMURA, Hiroshi. + +This program is free software; you can redistribute it and/or modify it under +the terms of the GNU General Public License as published by the Free Software +Foundation; either version 2 of the License, or (at your option) any later +version. + +This program is distributed in the hope that it will be useful, but WITHOUT ANY +WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A +PRATICULAR PURPOSE. See the GNU General Public License for more details. + +You should have received a copy of the GNU General Public License along with +this program; if not, write to the Free Software Foundation, Inc., 675 Mass +Ave, Cambridge, MA 02139, USA. +=end + + +require 'net/http' + + +module SOAP + + +class NetHttpClient + + attr_accessor :proxy + attr_accessor :debug_dev + attr_reader :session_manager + + class SessionManager + attr_accessor :connect_timeout + attr_accessor :send_timeout + attr_accessor :receive_timeout + end + + class Response + attr_reader :content + attr_reader :status + attr_reader :reason + attr_reader :contenttype + + def initialize(res) + @status = res.code.to_i + @reason = res.message + @contenttype = res['content-type'] + @content = res.body + end + end + + def initialize(proxy = nil, agent = nil) + @proxy = proxy ? URI.parse(proxy) : nil + @agent = agent + @debug_dev = nil + @session_manager = SessionManager.new + end + + def reset(url) + # ignored. + end + + def post(url, req_body, header = {}) + url = URI.parse(url) + extra = header.dup + extra['User-Agent'] = @agent if @agent + res = start(url) { |http| + http.post(url.instance_eval('path_query'), req_body, extra) + } + Response.new(res) + end + + def get_content(url, header = {}) + url = URI.parse(url) + extra = header.dup + extra['User-Agent'] = @agent if @agent + res = start(url) { |http| + http.get(url.instance_eval('path_query'), extra) + } + res.body + end + +private + + def start(url) + proxy_host = @proxy ? @proxy.host : nil + proxy_port = @proxy ? @proxy.port : nil + response = nil + Net::HTTP::Proxy(proxy_host, proxy_port).start(url.host, url.port) { |http| + if http.respond_to?(:set_debug_output) + http.set_debug_output(@debug_dev) + end + response, = yield(http) + http.finish + } + response + end +end + + +end diff --git a/lib/soap/parser.rb b/lib/soap/parser.rb new file mode 100644 index 000000000..67782566c --- /dev/null +++ b/lib/soap/parser.rb @@ -0,0 +1,252 @@ +=begin +SOAP4R - SOAP XML Instance Parser library. +Copyright (C) 2001, 2003 NAKAMURA, Hiroshi. + +This program is free software; you can redistribute it and/or modify it under +the terms of the GNU General Public License as published by the Free Software +Foundation; either version 2 of the License, or (at your option) any later +version. + +This program is distributed in the hope that it will be useful, but WITHOUT ANY +WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A +PRATICULAR PURPOSE. See the GNU General Public License for more details. + +You should have received a copy of the GNU General Public License along with +this program; if not, write to the Free Software Foundation, Inc., 675 Mass +Ave, Cambridge, MA 02139, USA. +=end + + +require 'xsd/ns' +require 'xsd/xmlparser' +require 'soap/soap' +require 'soap/baseData' +require 'soap/encodingstyle/handler' + + +module SOAP + + +class Parser + include SOAP + + class ParseError < Error; end + class FormatDecodeError < ParseError; end + class UnexpectedElementError < ParseError; end + +private + + class ParseFrame + attr_reader :node + attr_reader :name + attr_reader :ns, :encodingstyle + + class NodeContainer + def initialize(node) + @node = node + end + + def node + @node + end + + def replace_node(node) + @node = node + end + end + + public + + def initialize(ns, name, node, encodingstyle) + @ns = ns + @name = name + self.node = node + @encodingstyle = encodingstyle + end + + def node=(node) + @node = NodeContainer.new(node) + end + end + +public + + attr_accessor :default_encodingstyle + attr_accessor :decode_typemap + attr_accessor :allow_unqualified_element + + def initialize(opt = {}) + @parser = XSD::XMLParser.create_parser(self, opt) + @parsestack = nil + @lastnode = nil + @handlers = {} + @default_encodingstyle = opt[:default_encodingstyle] || EncodingNamespace + @decode_typemap = opt[:decode_typemap] || nil + @allow_unqualified_element = opt[:allow_unqualified_element] || false + end + + def charset + @parser.charset + end + + def parse(string_or_readable) + @parsestack = [] + @lastnode = nil + + @handlers.each do |uri, handler| + handler.decode_prologue + end + + @parser.do_parse(string_or_readable) + + unless @parsestack.empty? + raise FormatDecodeError.new("Unbalanced tag in XML.") + end + + @handlers.each do |uri, handler| + handler.decode_epilogue + end + + @lastnode + end + + def start_element(name, attrs) + lastframe = @parsestack.last + ns = parent = parent_encodingstyle = nil + if lastframe + ns = lastframe.ns.clone_ns + parent = lastframe.node + parent_encodingstyle = lastframe.encodingstyle + else + ns = XSD::NS.new + parent = ParseFrame::NodeContainer.new(nil) + parent_encodingstyle = nil + end + + attrs = XSD::XMLParser.filter_ns(ns, attrs) + encodingstyle = find_encodingstyle(ns, attrs) + + # Children's encodingstyle is derived from its parent. + encodingstyle ||= parent_encodingstyle || @default_encodingstyle + + node = decode_tag(ns, name, attrs, parent, encodingstyle) + + @parsestack << ParseFrame.new(ns, name, node, encodingstyle) + end + + def characters(text) + lastframe = @parsestack.last + if lastframe + # Need not to be cloned because character does not have attr. + ns = lastframe.ns + parent = lastframe.node + encodingstyle = lastframe.encodingstyle + decode_text(ns, text, encodingstyle) + else + # Ignore Text outside of SOAP Envelope. + p text if $DEBUG + end + end + + def end_element(name) + lastframe = @parsestack.pop + unless name == lastframe.name + raise UnexpectedElementError.new("Closing element name '#{ name }' does not match with opening element '#{ lastframe.name }'.") + end + decode_tag_end(lastframe.ns, lastframe.node, lastframe.encodingstyle) + @lastnode = lastframe.node.node + end + +private + + def find_encodingstyle(ns, attrs) + attrs.each do |key, value| + if (ns.compare(EnvelopeNamespace, AttrEncodingStyle, key)) + return value + end + end + nil + end + + def decode_tag(ns, name, attrs, parent, encodingstyle) + ele = ns.parse(name) + + # Envelope based parsing. + if ((ele.namespace == EnvelopeNamespace) || + (@allow_unqualified_element && ele.namespace.nil?)) + o = decode_soap_envelope(ns, ele, attrs, parent) + return o if o + end + + # Encoding based parsing. + handler = find_handler(encodingstyle) + if handler + return handler.decode_tag(ns, ele, attrs, parent) + else + raise FormatDecodeError.new("Unknown encodingStyle: #{ encodingstyle }.") + end + end + + def decode_tag_end(ns, node, encodingstyle) + return unless encodingstyle + + handler = find_handler(encodingstyle) + if handler + return handler.decode_tag_end(ns, node) + else + raise FormatDecodeError.new("Unknown encodingStyle: #{ encodingstyle }.") + end + end + + def decode_text(ns, text, encodingstyle) + handler = find_handler(encodingstyle) + + if handler + handler.decode_text(ns, text) + else + # How should I do? + end + end + + def decode_soap_envelope(ns, ele, attrs, parent) + o = nil + if ele.name == EleEnvelope + o = SOAPEnvelope.new + elsif ele.name == EleHeader + unless parent.node.is_a?(SOAPEnvelope) + raise FormatDecodeError.new("Header should be a child of Envelope.") + end + o = SOAPHeader.new + parent.node.header = o + elsif ele.name == EleBody + unless parent.node.is_a?(SOAPEnvelope) + raise FormatDecodeError.new("Body should be a child of Envelope.") + end + o = SOAPBody.new + parent.node.body = o + elsif ele.name == EleFault + unless parent.node.is_a?(SOAPBody) + raise FormatDecodeError.new("Fault should be a child of Body.") + end + o = SOAPFault.new + parent.node.fault = o + end + o.parent = parent if o + o + end + + def find_handler(encodingstyle) + unless @handlers.key?(encodingstyle) + handler_factory = SOAP::EncodingStyle::Handler.handler(encodingstyle) || + SOAP::EncodingStyle::Handler.handler(EncodingNamespace) + handler = handler_factory.new(@parser.charset) + handler.decode_typemap = @decode_typemap + handler.decode_prologue + @handlers[encodingstyle] = handler + end + @handlers[encodingstyle] + end +end + + +end diff --git a/lib/soap/processor.rb b/lib/soap/processor.rb new file mode 100644 index 000000000..47df772a9 --- /dev/null +++ b/lib/soap/processor.rb @@ -0,0 +1,79 @@ +=begin +SOAP4R - marshal/unmarshal interface. +Copyright (C) 2000, 2001, 2003 NAKAMURA, Hiroshi. + +This program is free software; you can redistribute it and/or modify it under +the terms of the GNU General Public License as published by the Free Software +Foundation; either version 2 of the License, or (at your option) any later +version. + +This program is distributed in the hope that it will be useful, but WITHOUT ANY +WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A +PRATICULAR PURPOSE. See the GNU General Public License for more details. + +You should have received a copy of the GNU General Public License along with +this program; if not, write to the Free Software Foundation, Inc., 675 Mass +Ave, Cambridge, MA 02139, USA. +=end + + +require 'xsd/datatypes' +require 'soap/soap' +require 'soap/element' +require 'soap/parser' +require 'soap/generator' +require 'soap/encodingstyle/soapHandler' +require 'soap/encodingstyle/literalHandler' +require 'soap/encodingstyle/aspDotNetHandler' + + +module SOAP + + +module Processor + @@default_parser_option = {} + + class << self + public + + def marshal(header, body, opt = {}, io = nil) + env = SOAPEnvelope.new(header, body) + generator = create_generator(opt) + generator.generate(env, io) + end + + def unmarshal(stream, opt = {}) + parser = create_parser(opt) + env = parser.parse(stream) + if env + return env.header, env.body + else + return nil, nil + end + end + + def default_parser_option=(rhs) + @@default_parser_option = rhs + end + + def default_parser_option + @@default_parser_option + end + + private + + def create_generator(opt) + SOAPGenerator.new(opt) + end + + def create_parser(opt) + if opt.empty? + opt = @@default_parser_option + end + ::SOAP::Parser.new(opt) + end + end +end + + +end diff --git a/lib/soap/rpc/cgistub.rb b/lib/soap/rpc/cgistub.rb new file mode 100644 index 000000000..f016e3105 --- /dev/null +++ b/lib/soap/rpc/cgistub.rb @@ -0,0 +1,214 @@ +=begin +SOAP4R - CGI stub library +Copyright (C) 2001, 2003 NAKAMURA, Hiroshi. + +This program is free software; you can redistribute it and/or modify it under +the terms of the GNU General Public License as published by the Free Software +Foundation; either version 2 of the License, or (at your option) any later +version. + +This program is distributed in the hope that it will be useful, but WITHOUT ANY +WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A +PRATICULAR PURPOSE. See the GNU General Public License for more details. + +You should have received a copy of the GNU General Public License along with +this program; if not, write to the Free Software Foundation, Inc., 675 Mass +Ave, Cambridge, MA 02139, USA. +=end + + +require 'soap/streamHandler' +require 'webrick/httpresponse' +require 'webrick/httpstatus' +require 'logger' +require 'soap/rpc/router' + + +module SOAP +module RPC + + +### +# SYNOPSIS +# CGIStub.new +# +# DESCRIPTION +# To be written... +# +class CGIStub < Logger::Application + include SOAP + + # There is a client which does not accept the media-type which is defined in + # SOAP spec. + attr_accessor :mediatype + + class CGIError < Error; end + + class SOAPRequest + ALLOWED_LENGTH = 1024 * 1024 + + def initialize(stream = $stdin) + @method = ENV['REQUEST_METHOD'] + @size = ENV['CONTENT_LENGTH'].to_i || 0 + @contenttype = ENV['CONTENT_TYPE'] + @charset = nil + @soapaction = ENV['HTTP_SOAPAction'] + @source = stream + @body = nil + end + + def init + validate + @charset = StreamHandler.parse_media_type(@contenttype) + @body = @source.read(@size) + self + end + + def dump + @body.dup + end + + def soapaction + @soapaction + end + + def charset + @charset + end + + def to_s + "method: #{ @method }, size: #{ @size }" + end + + private + + def validate # raise CGIError + if @method != 'POST' + raise CGIError.new("Method '#{ @method }' not allowed.") + end + + if @size > ALLOWED_LENGTH + raise CGIError.new("Content-length too long.") + end + end + end + + def initialize(appname, default_namespace) + super(appname) + set_log(STDERR) + self.level = INFO + @default_namespace = default_namespace + @router = SOAP::RPC::Router.new(appname) + @remote_user = ENV['REMOTE_USER'] || 'anonymous' + @remote_host = ENV['REMOTE_HOST'] || ENV['REMOTE_ADDR'] || 'unknown' + @request = nil + @response = nil + @mediatype = MediaType + on_init + end + + def add_servant(obj, namespace = @default_namespace, soapaction = nil) + RPC.defined_methods(obj).each do |name| + qname = XSD::QName.new(namespace, name) + param_size = obj.method(name).arity.abs + params = (1..param_size).collect { |i| "p#{ i }" } + param_def = SOAP::RPC::SOAPMethod.create_param_def(params) + @router.add_method(obj, qname, soapaction, name, param_def) + end + end + + def on_init + # Override this method in derived class to call 'add_method' to add methods. + end + + def mapping_registry + @router.mapping_registry + end + + def mapping_registry=(value) + @router.mapping_registry = value + end + + def add_method(receiver, name, *param) + add_method_with_namespace_as(@default_namespace, receiver, + name, name, *param) + end + + def add_method_as(receiver, name, name_as, *param) + add_method_with_namespace_as(@default_namespace, receiver, + name, name_as, *param) + end + + def add_method_with_namespace(namespace, receiver, name, *param) + add_method_with_namespace_as(namespace, receiver, name, name, *param) + end + + def add_method_with_namespace_as(namespace, receiver, name, name_as, *param) + param_def = if param.size == 1 and param[0].is_a?(Array) + param[0] + else + SOAP::RPC::SOAPMethod.create_param_def(param) + end + qname = XSD::QName.new(namespace, name_as) + @router.add_method(receiver, qname, nil, name, param_def) + end + + def route(request_string, charset) + @router.route(request_string, charset) + end + + def create_fault_response(e) + @router.create_fault_response(e) + end + +private + + def run + prologue + + httpversion = WEBrick::HTTPVersion.new('1.0') + @response = WEBrick::HTTPResponse.new({:HTTPVersion => httpversion}) + begin + log(INFO) { "Received a request from '#{ @remote_user }@#{ @remote_host }'." } + # SOAP request parsing. + @request = SOAPRequest.new.init + req_charset = @request.charset + req_string = @request.dump + log(DEBUG) { "XML Request: #{req_string}" } + res_string, is_fault = route(req_string, req_charset) + log(DEBUG) { "XML Response: #{res_string}" } + + @response['Cache-Control'] = 'private' + if req_charset + @response['content-type'] = "#{@mediatype}; charset=\"#{req_charset}\"" + else + @response['content-type'] = @mediatype + end + if is_fault + @response.status = WEBrick::HTTPStatus::RC_INTERNAL_SERVER_ERROR + end + @response.body = res_string + rescue Exception + res_string = create_fault_response($!) + @response['Cache-Control'] = 'private' + @response['content-type'] = @mediatype + @response.status = WEBrick::HTTPStatus::RC_INTERNAL_SERVER_ERROR + ensure + buf = '' + @response.send_response(buf) + buf.sub!(/^[^\r]+\r\n/, '') # Trim status line. + log(DEBUG) { "SOAP CGI Response:\n#{ buf }" } + print buf + epilogue + end + + 0 + end + + def prologue; end + def epilogue; end +end + + +end +end diff --git a/lib/soap/rpc/driver.rb b/lib/soap/rpc/driver.rb new file mode 100644 index 000000000..76fd14e34 --- /dev/null +++ b/lib/soap/rpc/driver.rb @@ -0,0 +1,189 @@ +=begin +SOAP4R - SOAP RPC driver +Copyright (C) 2000, 2001, 2003 NAKAMURA, Hiroshi. + +This program is free software; you can redistribute it and/or modify it under +the terms of the GNU General Public License as published by the Free Software +Foundation; either version 2 of the License, or (at your option) any later +version. + +This program is distributed in the hope that it will be useful, but WITHOUT ANY +WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A +PRATICULAR PURPOSE. See the GNU General Public License for more details. + +You should have received a copy of the GNU General Public License along with +this program; if not, write to the Free Software Foundation, Inc., 675 Mass +Ave, Cambridge, MA 02139, USA. +=end + + +require 'soap/soap' +require 'soap/mapping' +require 'soap/rpc/rpc' +require 'soap/rpc/proxy' +require 'soap/rpc/element' +require 'soap/streamHandler' + + +module SOAP +module RPC + + +class Driver +public + class EmptyResponseError < Error; end + + attr_accessor :mapping_registry + attr_accessor :soapaction + attr_reader :endpoint_url + attr_reader :wiredump_dev + attr_reader :wiredump_file_base + attr_reader :httpproxy + + def initialize(endpoint_url, namespace, soapaction = nil) + @endpoint_url = endpoint_url + @namespace = namespace + @mapping_registry = nil # for unmarshal + @soapaction = soapaction + @wiredump_dev = nil + @wiredump_file_base = nil + @httpproxy = ENV['httpproxy'] || ENV['HTTP_PROXY'] + @handler = HTTPPostStreamHandler.new(@endpoint_url, @httpproxy, + XSD::Charset.encoding_label) + @proxy = Proxy.new(@handler, @soapaction) + @proxy.allow_unqualified_element = true + end + + def endpoint_url=(endpoint_url) + @endpoint_url = endpoint_url + if @handler + @handler.endpoint_url = @endpoint_url + @handler.reset + end + end + + def wiredump_dev=(dev) + @wiredump_dev = dev + if @handler + @handler.wiredump_dev = @wiredump_dev + @handler.reset + end + end + + def wiredump_file_base=(base) + @wiredump_file_base = base + end + + def httpproxy=(httpproxy) + @httpproxy = httpproxy + if @handler + @handler.proxy = @httpproxy + @handler.reset + end + end + + def default_encodingstyle + @proxy.default_encodingstyle + end + + def default_encodingstyle=(encodingstyle) + @proxy.default_encodingstyle = encodingstyle + end + + + ### + ## Method definition interfaces. + # + # params: [[param_def...]] or [paramname, paramname, ...] + # param_def: See proxy.rb. Sorry. + + def add_method(name, *params) + add_method_with_soapaction_as(name, name, @soapaction, *params) + end + + def add_method_as(name, name_as, *params) + add_method_with_soapaction_as(name, name_as, @soapaction, *params) + end + + def add_method_with_soapaction(name, soapaction, *params) + add_method_with_soapaction_as(name, name, soapaction, *params) + end + + def add_method_with_soapaction_as(name, name_as, soapaction, *params) + param_def = if params.size == 1 and params[0].is_a?(Array) + params[0] + else + SOAPMethod.create_param_def(params) + end + qname = XSD::QName.new(@namespace, name_as) + @proxy.add_method(qname, soapaction, name, param_def) + add_rpc_method_interface(name, param_def) + end + + + ### + ## Driving interface. + # + def invoke(headers, body) + if @wiredump_file_base + @handler.wiredump_file_base = + @wiredump_file_base + '_' << body.elename.name + end + @proxy.invoke(headers, body) + end + + def call(name, *params) + # Convert parameters: params array => SOAPArray => members array + params = Mapping.obj2soap(params, @mapping_registry).to_a + if @wiredump_file_base + @handler.wiredump_file_base = @wiredump_file_base + '_' << name + end + + # Then, call @proxy.call like the following. + header, body = @proxy.call(nil, name, *params) + unless body + raise EmptyResponseError.new("Empty response.") + end + + begin + @proxy.check_fault(body) + rescue SOAP::FaultError => e + Mapping.fault2exception(e) + end + + ret = body.response ? Mapping.soap2obj(body.response, @mapping_registry) : nil + if body.outparams + outparams = body.outparams.collect { |outparam| Mapping.soap2obj(outparam) } + return [ret].concat(outparams) + else + return ret + end + end + + def reset_stream + @handler.reset + end + +private + + def add_rpc_method_interface(name, param_def) + param_names = [] + i = 0 + @proxy.method[name].each_param_name(RPC::SOAPMethod::IN, + RPC::SOAPMethod::INOUT) do |param_name| + i += 1 + param_names << "arg#{ i }" + end + + callparam = (param_names.collect { |pname| ", " + pname }).join + self.instance_eval <<-EOS + def #{ name }(#{ param_names.join(", ") }) + call("#{ name }"#{ callparam }) + end + EOS + end +end + + +end +end diff --git a/lib/soap/rpc/element.rb b/lib/soap/rpc/element.rb new file mode 100644 index 000000000..d1e1931fa --- /dev/null +++ b/lib/soap/rpc/element.rb @@ -0,0 +1,278 @@ +=begin +SOAP4R - RPC element definition. +Copyright (C) 2000, 2001, 2003 NAKAMURA, Hiroshi. + +This program is free software; you can redistribute it and/or modify it under +the terms of the GNU General Public License as published by the Free Software +Foundation; either version 2 of the License, or (at your option) any later +version. + +This program is distributed in the hope that it will be useful, but WITHOUT ANY +WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A +PRATICULAR PURPOSE. See the GNU General Public License for more details. + +You should have received a copy of the GNU General Public License along with +this program; if not, write to the Free Software Foundation, Inc., 675 Mass +Ave, Cambridge, MA 02139, USA. +=end + + +require 'soap/baseData' + + +module SOAP + +# Add method definitions for RPC to common definition in element.rb +class SOAPBody < SOAPStruct + public + + def request + root_node + end + + def response + if !@is_fault + if void? + nil + else + # Initial element is [retval]. + root_node[0] + end + else + root_node + end + end + + def outparams + if !@is_fault and !void? + op = root_node[1..-1] + op = nil if op && op.empty? + op + else + nil + end + end + + def void? + root_node.nil? # || root_node.is_a?(SOAPNil) + end + + def fault + if @is_fault + self['fault'] + else + nil + end + end + + def fault=(fault) + @is_fault = true + add_member('fault', fault) + end +end + + +module RPC + + +class RPCError < Error; end +class MethodDefinitionError < RPCError; end +class ParameterError < RPCError; end + +class SOAPMethod < SOAPStruct + RETVAL = 'retval' + IN = 'in' + OUT = 'out' + INOUT = 'inout' + + attr_reader :param_def + attr_reader :inparam + attr_reader :outparam + + def initialize(qname, param_def = nil) + super(nil) + @elename = qname + @encodingstyle = nil + + @param_def = param_def + + @signature = [] + @inparam_names = [] + @inoutparam_names = [] + @outparam_names = [] + + @inparam = {} + @outparam = {} + @retval_name = nil + + init_param(@param_def) if @param_def + end + + def have_outparam? + @outparam_names.size > 0 + end + + def each_param_name(*type) + @signature.each do |io_type, name, param_type| + if type.include?(io_type) + yield(name) + end + end + end + + def set_param(params) + params.each do |param, data| + @inparam[param] = data + data.elename.name = param + end + end + + def set_outparam(params) + params.each do |param, data| + @outparam[param] = data + data.elename.name = param + end + end + + def SOAPMethod.create_param_def(param_names) + param_def = [] + param_names.each do |param_name| + param_def.push([IN, param_name, nil]) + end + param_def.push([RETVAL, 'return', nil]) + param_def + end + +private + + def init_param(param_def) + param_def.each do |io_type, name, param_type| + case io_type + when IN + @signature.push([IN, name, param_type]) + @inparam_names.push(name) + when OUT + @signature.push([OUT, name, param_type]) + @outparam_names.push(name) + when INOUT + @signature.push([INOUT, name, param_type]) + @inoutparam_names.push(name) + when RETVAL + if (@retval_name) + raise MethodDefinitionError.new('Duplicated retval') + end + @retval_name = name + else + raise MethodDefinitionError.new("Unknown type: #{ io_type }") + end + end + end +end + + +class SOAPMethodRequest < SOAPMethod + attr_accessor :soapaction + + def SOAPMethodRequest.create_request(qname, *params) + param_def = [] + param_value = [] + i = 0 + params.each do |param| + param_name = "p#{ i }" + i += 1 + param_def << [IN, nil, param_name] + param_value << [param_name, param] + end + param_def << [RETVAL, nil, 'return'] + o = new(qname, param_def) + o.set_param(param_value) + o + end + + def initialize(qname, param_def = nil, soapaction = nil) + check_elename(qname) + super(qname, param_def) + @soapaction = soapaction + end + + def each + each_param_name(IN, INOUT) do |name| + unless @inparam[name] + raise ParameterError.new("Parameter: #{ name } was not given.") + end + yield(name, @inparam[name]) + end + end + + def dup + req = self.class.new(@elename.dup, @param_def, @soapaction) + req.encodingstyle = @encodingstyle + req + end + + def create_method_response + SOAPMethodResponse.new( + XSD::QName.new(@elename.namespace, @elename.name + 'Response'), + @param_def) + end + +private + + def check_elename(qname) + # NCName & ruby's method name + unless /\A[\w_][\w\d_\-]*\z/ =~ qname.name + raise MethodDefinitionError.new("Element name '#{qname.name}' not allowed") + end + end +end + + +class SOAPMethodResponse < SOAPMethod + + def initialize(qname, param_def = nil) + super(qname, param_def) + @retval = nil + end + + def retval=(retval) + @retval = retval + @retval.elename = @retval.elename.dup_name('return') + end + + def each + if @retval_name and !@retval.is_a?(SOAPVoid) + yield(@retval_name, @retval) + end + + each_param_name(OUT, INOUT) do |param_name| + unless @outparam[param_name] + raise ParameterError.new("Parameter: #{ param_name } was not given.") + end + yield(param_name, @outparam[param_name]) + end + end +end + + +# To return(?) void explicitly. +# def foo(input_var) +# ... +# return SOAP::RPC::SOAPVoid.new +# end +class SOAPVoid < XSD::XSDAnySimpleType + include SOAPBasetype + extend SOAPModuleUtils + Name = XSD::QName.new(Mapping::RubyCustomTypeNamespace, nil) + +public + def initialize() + @elename = Name + @id = nil + @precedents = [] + @parent = nil + end +end + + +end +end diff --git a/lib/soap/rpc/proxy.rb b/lib/soap/rpc/proxy.rb new file mode 100644 index 000000000..39a095838 --- /dev/null +++ b/lib/soap/rpc/proxy.rb @@ -0,0 +1,147 @@ +=begin +SOAP4R - RPC Proxy library. +Copyright (C) 2000, 2003 NAKAMURA, Hiroshi. + +This program is free software; you can redistribute it and/or modify it under +the terms of the GNU General Public License as published by the Free Software +Foundation; either version 2 of the License, or (at your option) any later +version. + +This program is distributed in the hope that it will be useful, but WITHOUT ANY +WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A +PRATICULAR PURPOSE. See the GNU General Public License for more details. + +You should have received a copy of the GNU General Public License along with +this program; if not, write to the Free Software Foundation, Inc., 675 Mass +Ave, Cambridge, MA 02139, USA. +=end + + +require 'soap/soap' +require 'soap/processor' +require 'soap/mapping' +require 'soap/rpc/rpc' +require 'soap/rpc/element' +require 'soap/streamHandler' + + +module SOAP +module RPC + + +class Proxy + include SOAP + +public + + attr_accessor :soapaction + attr_accessor :allow_unqualified_element, :default_encodingstyle + attr_reader :method + + def initialize(stream_handler, soapaction = nil) + @handler = stream_handler + @soapaction = soapaction + @method = {} + @allow_unqualified_element = false + @default_encodingstyle = nil + end + + class Request + include RPC + + public + + attr_reader :method + attr_reader :namespace + attr_reader :name + + def initialize(model, values) + @method = model.dup + @namespace = @method.elename.namespace + @name = @method.elename.name + + params = {} + + if ((values.size == 1) and (values[0].is_a?(Hash))) + params = values[0] + else + i = 0 + @method.each_param_name(SOAPMethod::IN, SOAPMethod::INOUT) do |name| + params[name] = values[i] || SOAPNil.new + i += 1 + end + end + @method.set_param(params) + end + end + + def add_method(qname, soapaction, name, param_def) + @method[name] = SOAPMethodRequest.new(qname, param_def, soapaction) + end + + def create_request(name, *values) + if (@method.key?(name)) + method = @method[name] + method.encodingstyle = @default_encodingstyle if @default_encodingstyle + else + raise SOAP::RPC::MethodDefinitionError.new( + "Method: #{ name } not defined.") + end + + Request.new(method, values) + end + + def invoke(req_header, req_body, soapaction = nil) + if req_header and !req_header.is_a?(SOAPHeader) + req_header = create_header(req_header) + end + if !req_body.is_a?(SOAPBody) + req_body = SOAPBody.new(req_body) + end + opt = create_options + send_string = Processor.marshal(req_header, req_body, opt) + data = @handler.send(send_string, soapaction) + if data.receive_string.empty? + return nil, nil + end + res_charset = StreamHandler.parse_media_type(data.receive_contenttype) + opt = create_options + opt[:charset] = res_charset + res_header, res_body = Processor.unmarshal(data.receive_string, opt) + return res_header, res_body + end + + def call(headers, name, *values) + req = create_request(name, *values) + return invoke(headers, req.method, req.method.soapaction || @soapaction) + end + + def check_fault(body) + if body.fault + raise SOAP::FaultError.new(body.fault) + end + end + +private + + def create_header(headers) + header = SOAPHeader.new() + headers.each do |content, mustunderstand, encodingstyle| + header.add(SOAPHeaderItem.new(content, mustunderstand, encodingstyle)) + end + header + end + + def create_options + opt = {} + opt[:default_encodingstyle] = @default_encodingstyle + if @allow_unqualified_element + opt[:allow_unqualified_element] = true + end + opt + end +end + + +end +end diff --git a/lib/soap/rpc/router.rb b/lib/soap/rpc/router.rb new file mode 100644 index 000000000..20396a4a7 --- /dev/null +++ b/lib/soap/rpc/router.rb @@ -0,0 +1,176 @@ +=begin +SOAP4R - RPC Routing library +Copyright (C) 2001, 2002 NAKAMURA, Hiroshi. + +This program is free software; you can redistribute it and/or modify it under +the terms of the GNU General Public License as published by the Free Software +Foundation; either version 2 of the License, or (at your option) any later +version. + +This program is distributed in the hope that it will be useful, but WITHOUT ANY +WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A +PRATICULAR PURPOSE. See the GNU General Public License for more details. + +You should have received a copy of the GNU General Public License along with +this program; if not, write to the Free Software Foundation, Inc., 675 Mass +Ave, Cambridge, MA 02139, USA. +=end + + +require 'soap/soap' +require 'soap/processor' +require 'soap/mapping' +require 'soap/rpc/rpc' +require 'soap/rpc/element' + + +module SOAP +module RPC + + +class Router + include SOAP + + attr_reader :actor + attr_accessor :allow_unqualified_element + attr_accessor :default_encodingstyle + attr_accessor :mapping_registry + + def initialize(actor) + @actor = actor + @receiver = {} + @method_name = {} + @method = {} + @allow_unqualified_element = false + @default_encodingstyle = nil + @mapping_registry = nil + end + + def add_method(receiver, qname, soapaction, name, param_def) + fqname = fqname(qname) + @receiver[fqname] = receiver + @method_name[fqname] = name + @method[fqname] = RPC::SOAPMethodRequest.new(qname, param_def, soapaction) + end + + def add_header_handler + raise NotImplementedError.new + end + + # Routing... + def route(soap_string, charset = nil) + opt = options + opt[:charset] = charset + is_fault = false + begin + header, body = Processor.unmarshal(soap_string, opt) + # So far, header is omitted... + soap_request = body.request + unless soap_request.is_a?(SOAPStruct) + raise RPCRoutingError.new("Not an RPC style.") + end + soap_response = dispatch(soap_request) + rescue Exception + soap_response = fault($!) + is_fault = true + end + + header = SOAPHeader.new + body = SOAPBody.new(soap_response) + response_string = Processor.marshal(header, body, opt) + + return response_string, is_fault + end + + # Create fault response string. + def create_fault_response(e, charset = nil) + header = SOAPHeader.new + soap_response = fault(e) + body = SOAPBody.new(soap_response) + opt = options + opt[:charset] = charset + Processor.marshal(header, body, opt) + end + +private + + # Create new response. + def create_response(qname, result) + name = fqname(qname) + if (@method.key?(name)) + method = @method[name] + else + raise RPCRoutingError.new("Method: #{ name } not defined.") + end + + soap_response = method.create_method_response + if soap_response.have_outparam? + unless result.is_a?(Array) + raise RPCRoutingError.new("Out parameter was not returned.") + end + outparams = {} + i = 1 + soap_response.each_param_name('out', 'inout') do |outparam| + outparams[outparam] = Mapping.obj2soap(result[i], @mapping_registry) + i += 1 + end + soap_response.set_outparam(outparams) + soap_response.retval = Mapping.obj2soap(result[0], @mapping_registry) + else + soap_response.retval = Mapping.obj2soap(result, @mapping_registry) + end + soap_response + end + + # Create fault response. + def fault(e) + detail = Mapping::SOAPException.new(e) + SOAPFault.new( + SOAPString.new('Server'), + SOAPString.new(e.to_s), + SOAPString.new(@actor), + Mapping.obj2soap(detail, @mapping_registry)) + end + + # Dispatch to defined method. + def dispatch(soap_method) + request_struct = Mapping.soap2obj(soap_method, @mapping_registry) + values = soap_method.collect { |key, value| request_struct[key] } + method = lookup(soap_method.elename, values) + unless method + raise RPCRoutingError.new( + "Method: #{ soap_method.elename } not supported.") + end + + result = method.call(*values) + create_response(soap_method.elename, result) + end + + # Method lookup + def lookup(qname, values) + name = fqname(qname) + # It may be necessary to check all part of method signature... + if @method.member?(name) + @receiver[name].method(@method_name[name].intern) + else + nil + end + end + + def fqname(qname) + "#{ qname.namespace }:#{ qname.name }" + end + + def options + opt = {} + opt[:default_encodingstyle] = @default_encodingstyle + if @allow_unqualified_element + opt[:allow_unqualified_element] = true + end + opt + end +end + + +end +end diff --git a/lib/soap/rpc/rpc.rb b/lib/soap/rpc/rpc.rb new file mode 100644 index 000000000..c3cb3228f --- /dev/null +++ b/lib/soap/rpc/rpc.rb @@ -0,0 +1,36 @@ +=begin +SOAP4R - RPC utility. +Copyright (C) 2003 NAKAMURA, Hiroshi. + +This program is free software; you can redistribute it and/or modify it under +the terms of the GNU General Public License as published by the Free Software +Foundation; either version 2 of the License, or (at your option) any later +version. + +This program is distributed in the hope that it will be useful, but WITHOUT ANY +WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A +PRATICULAR PURPOSE. See the GNU General Public License for more details. + +You should have received a copy of the GNU General Public License along with +this program; if not, write to the Free Software Foundation, Inc., 675 Mass +Ave, Cambridge, MA 02139, USA. +=end + + +module SOAP + + +module RPC + ServerException = Mapping::MappedException + + def self.defined_methods(obj) + if obj.is_a?(Module) + obj.methods - Module.methods + else + obj.methods - Kernel.instance_methods(true) + end + end +end + + +end diff --git a/lib/soap/rpc/soaplet.rb b/lib/soap/rpc/soaplet.rb new file mode 100644 index 000000000..1a4ef99b7 --- /dev/null +++ b/lib/soap/rpc/soaplet.rb @@ -0,0 +1,167 @@ +=begin +SOAP4R - SOAP handler servlet for WEBrick +Copyright (C) 2001, 2002, 2003 NAKAMURA, Hiroshi. + +This program is free software; you can redistribute it and/or modify it under +the terms of the GNU General Public License as published by the Free Software +Foundation; either version 2 of the License, or (at your option) any later +version. + +This program is distributed in the hope that it will be useful, but WITHOUT ANY +WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A +PRATICULAR PURPOSE. See the GNU General Public License for more details. + +You should have received a copy of the GNU General Public License along with +this program; if not, write to the Free Software Foundation, Inc., 675 Mass +Ave, Cambridge, MA 02139, USA. +=end + + +require 'webrick/httpservlet/abstract' +require 'webrick/httpstatus' +require 'soap/rpc/router' +require 'soap/streamHandler' + +module SOAP +module RPC + + +class SOAPlet < WEBrick::HTTPServlet::AbstractServlet +public + attr_reader :app_scope_router + + def initialize + @router_map = {} + @app_scope_router = ::SOAP::RPC::Router.new(self.class.name) + end + + # Add servant klass whose object has request scope. A servant object is + # instanciated for each request. + # + # Bare in mind that servant klasses are distinguished by HTTP SOAPAction + # header in request. Client which calls request-scoped servant must have a + # SOAPAction header which is a namespace of the servant klass. + # I mean, use Driver#add_method_with_soapaction instead of Driver#add_method + # at client side. + # + def add_rpc_request_servant(klass, namespace, mapping_registry = nil) + router = RequestRouter.new(klass, namespace, mapping_registry) + add_router(namespace, router) + end + + # Add servant object which has application scope. + def add_rpc_servant(obj, namespace) + router = @app_scope_router + SOAPlet.add_servant_to_router(router, obj, namespace) + add_router(namespace, router) + end + alias add_servant add_rpc_servant + + + ### + ## Servlet interfaces for WEBrick. + # + def get_instance(config, *options) + @config = config + self + end + + def require_path_info? + false + end + + def do_GET(req, res) + res.header['Allow'] = 'POST' + raise WEBrick::HTTPStatus::MethodNotAllowed, "GET request not allowed." + end + + def do_POST(req, res) + namespace = parse_soapaction(req.meta_vars['HTTP_SOAPACTION']) + router = lookup_router(namespace) + + is_fault = false + + charset = ::SOAP::StreamHandler.parse_media_type(req['content-type']) + begin + response_stream, is_fault = router.route(req.body, charset) + rescue Exception => e + response_stream = router.create_fault_response(e) + is_fault = true + end + + res.body = response_stream + res['content-type'] = "text/xml; charset=\"#{charset}\"" + if response_stream.is_a?(IO) + res.chunked = true + end + + if is_fault + res.status = WEBrick::HTTPStatus::RC_INTERNAL_SERVER_ERROR + end + end + +private + + class RequestRouter < ::SOAP::RPC::Router + def initialize(klass, namespace, mapping_registry = nil) + super(namespace) + if mapping_registry + self.mapping_registry = mapping_registry + end + @klass = klass + @namespace = namespace + end + + def route(soap_string) + obj = @klass.new + namespace = self.actor + router = ::SOAP::RPC::Router.new(@namespace) + SOAPlet.add_servant_to_router(router, obj, namespace) + router.route(soap_string) + end + end + + def add_router(namespace, router) + @router_map[namespace] = router + end + + def parse_soapaction(soapaction) + if /^"(.*)"$/ =~ soapaction + soapaction = $1 + end + if soapaction.empty? + return nil + end + soapaction + end + + def lookup_router(namespace) + if namespace + @router_map[namespace] || @app_scope_router + else + @app_scope_router + end + end + + class << self + public + def add_servant_to_router(router, obj, namespace) + ::SOAP::RPC.defined_methods(obj).each do |name| + add_servant_method_to_router(router, obj, namespace, name) + end + end + + def add_servant_method_to_router(router, obj, namespace, name) + qname = XSD::QName.new(namespace, name) + soapaction = nil + method = obj.method(name) + param_def = ::SOAP::RPC::SOAPMethod.create_param_def( + (1..method.arity.abs).collect { |i| "p#{ i }" }) + router.add_method(obj, qname, soapaction, name, param_def) + end + end +end + + +end +end diff --git a/lib/soap/rpc/standaloneServer.rb b/lib/soap/rpc/standaloneServer.rb new file mode 100644 index 000000000..bc8ab18f4 --- /dev/null +++ b/lib/soap/rpc/standaloneServer.rb @@ -0,0 +1,116 @@ +=begin +SOAP4R - WEBrick Server +Copyright (C) 2003 by NAKAMURA, Hiroshi + +This program is free software; you can redistribute it and/or modify it under +the terms of the GNU General Public License as published by the Free Software +Foundation; either version 2 of the License, or (at your option) any later +version. + +This program is distributed in the hope that it will be useful, but WITHOUT ANY +WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A +PRATICULAR PURPOSE. See the GNU General Public License for more details. + +You should have received a copy of the GNU General Public License along with +this program; if not, write to the Free Software Foundation, Inc., 675 Mass +Ave, Cambridge, MA 02139, USA. +=end + + +require 'logger' +require 'soap/rpc/soaplet' +require 'soap/streamHandler' + +# require 'webrick' +require 'webrick/compat.rb' +require 'webrick/version.rb' +require 'webrick/config.rb' +require 'webrick/log.rb' +require 'webrick/server.rb' +require 'webrick/utils.rb' +require 'webrick/accesslog' +# require 'webrick/htmlutils.rb' +require 'webrick/httputils.rb' +# require 'webrick/cookie.rb' +require 'webrick/httpversion.rb' +require 'webrick/httpstatus.rb' +require 'webrick/httprequest.rb' +require 'webrick/httpresponse.rb' +require 'webrick/httpserver.rb' +# require 'webrick/httpservlet.rb' +# require 'webrick/httpauth.rb' + + +module SOAP +module RPC + + +class StandaloneServer < Logger::Application + attr_reader :server + + def initialize(app_name, namespace, host = "0.0.0.0", port = 8080) + super(app_name) + @logdev = Logger.new(STDERR) + @logdev.level = INFO + @namespace = namespace + @server = WEBrick::HTTPServer.new( + :BindAddress => host, + :Logger => logdev, + :AccessLog => [[logdev, WEBrick::AccessLog::COMBINED_LOG_FORMAT]], + :Port => port + ) + @soaplet = ::SOAP::RPC::SOAPlet.new + on_init + @server.mount('/', @soaplet) + end + + def on_init + # define extra methods in derived class. + end + + def add_rpc_request_servant(klass, namespace = @namespace, mapping_registry = nil) + @soaplet.add_rpc_request_servant(klass, namespace, mapping_registry) + end + + def add_rpc_servant(obj, namespace = @namespace) + @soaplet.add_rpc_servant(obj, namespace) + end + alias add_servant add_rpc_servant + + def mapping_registry + @soaplet.app_scope_router.mapping_registry + end + + def mapping_registry=(mapping_registry) + @soaplet.app_scope_router.mapping_registry = mapping_registry + end + + def add_method(obj, name, *param) + add_method_as(obj, name, name, *param) + end + + def add_method_as(obj, name, name_as, *param) + qname = XSD::QName.new(@namespace, name_as) + soapaction = nil + method = obj.method(name) + param_def = if param.size == 1 and param[0].is_a?(Array) + param[0] + elsif param.empty? + ::SOAP::RPC::SOAPMethod.create_param_def( + (1..method.arity.abs).collect { |i| "p#{ i }" }) + else + SOAP::RPC::SOAPMethod.create_param_def(param) + end + @soaplet.app_scope_router.add_method(obj, qname, soapaction, name, param_def) + end + +private + + def run + @server.start + end +end + + +end +end diff --git a/lib/soap/soap.rb b/lib/soap/soap.rb new file mode 100644 index 000000000..313af3a05 --- /dev/null +++ b/lib/soap/soap.rb @@ -0,0 +1,112 @@ +=begin +SOAP4R - Base definitions. +Copyright (C) 2000, 2001, 2002, 2003 NAKAMURA, Hiroshi. + +This program is free software; you can redistribute it and/or modify it under +the terms of the GNU General Public License as published by the Free Software +Foundation; either version 2 of the License, or (at your option) any later +version. + +This program is distributed in the hope that it will be useful, but WITHOUT ANY +WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A +PRATICULAR PURPOSE. See the GNU General Public License for more details. + +You should have received a copy of the GNU General Public License along with +this program; if not, write to the Free Software Foundation, Inc., 675 Mass +Ave, Cambridge, MA 02139, USA. +=end + + +require 'xsd/qname' +require 'xsd/charset' + + +module SOAP + + +Version = '1.5.0' + +EnvelopeNamespace = 'http://schemas.xmlsoap.org/soap/envelope/' +EncodingNamespace = 'http://schemas.xmlsoap.org/soap/encoding/' +LiteralNamespace = 'http://xml.apache.org/xml-soap/literalxml' + +NextActor = 'http://schemas.xmlsoap.org/soap/actor/next' + +EleEnvelope = 'Envelope' +EleHeader = 'Header' +EleBody = 'Body' +EleFault = 'Fault' +EleFaultString = 'faultstring' +EleFaultActor = 'faultactor' +EleFaultCode = 'faultcode' +EleFaultDetail = 'detail' + +AttrMustUnderstand = 'mustUnderstand' +AttrEncodingStyle = 'encodingStyle' +AttrActor = 'actor' +AttrRoot = 'root' +AttrArrayType = 'arrayType' +AttrOffset = 'offset' +AttrPosition = 'position' +ValueArray = 'Array' + +EleEnvelopeName = XSD::QName.new(EnvelopeNamespace, EleEnvelope) +EleHeaderName = XSD::QName.new(EnvelopeNamespace, EleHeader) +EleBodyName = XSD::QName.new(EnvelopeNamespace, EleBody) +EleFaultName = XSD::QName.new(EnvelopeNamespace, EleFault) +EleFaultStringName = XSD::QName.new(nil, EleFaultString) +EleFaultActorName = XSD::QName.new(nil, EleFaultActor) +EleFaultCodeName = XSD::QName.new(nil, EleFaultCode) +EleFaultDetailName = XSD::QName.new(nil, EleFaultDetail) +AttrEncodingStyleName = XSD::QName.new(EnvelopeNamespace, AttrEncodingStyle) +AttrRootName = XSD::QName.new(EncodingNamespace, AttrRoot) +AttrArrayTypeName = XSD::QName.new(EncodingNamespace, AttrArrayType) +AttrOffsetName = XSD::QName.new(EncodingNamespace, AttrOffset) +AttrPositionName = XSD::QName.new(EncodingNamespace, AttrPosition) +ValueArrayName = XSD::QName.new(EncodingNamespace, ValueArray) + +Base64Literal = 'base64' + +SOAPNamespaceTag = 'env' +XSDNamespaceTag = 'xsd' +XSINamespaceTag = 'xsi' + +MediaType = 'text/xml' + +class Error < StandardError; end + +class StreamError < Error; end +class HTTPStreamError < StreamError; end +class PostUnavailableError < HTTPStreamError; end +class MPostUnavailableError < HTTPStreamError; end + +class ArrayIndexOutOfBoundsError < Error; end +class ArrayStoreError < Error; end + +class RPCRoutingError < Error; end + +class FaultError < Error + attr_reader :faultcode + attr_reader :faultstring + attr_reader :faultactor + attr_accessor :detail + + def initialize(fault) + @faultcode = fault.faultcode + @faultstring = fault.faultstring + @faultactor = fault.faultactor + @detail = fault.detail + super(self.to_s) + end + + def to_s + str = nil + if @faultstring && @faultstring.respond_to?('data') + str = @faultstring.data + end + str || '(No faultstring)' + end +end + + +end diff --git a/lib/soap/streamHandler.rb b/lib/soap/streamHandler.rb new file mode 100644 index 000000000..c0ca3955f --- /dev/null +++ b/lib/soap/streamHandler.rb @@ -0,0 +1,188 @@ +=begin +SOAP4R - Stream handler. +Copyright (C) 2000, 2001, 2003 NAKAMURA, Hiroshi. + +This program is free software; you can redistribute it and/or modify it under +the terms of the GNU General Public License as published by the Free Software +Foundation; either version 2 of the License, or (at your option) any later +version. + +This program is distributed in the hope that it will be useful, but WITHOUT ANY +WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A +PRATICULAR PURPOSE. See the GNU General Public License for more details. + +You should have received a copy of the GNU General Public License along with +this program; if not, write to the Free Software Foundation, Inc., 675 Mass +Ave, Cambridge, MA 02139, USA. +=end + + +require 'soap/soap' + + +module SOAP + + +class StreamHandler + Client = begin + require 'http-access2' + if HTTPAccess2::VERSION < "2.0" + raise LoadError.new("http-access/2.0 or later is required.") + end + HTTPAccess2::Client + rescue LoadError + STDERR.puts "Loading http-access2 failed. Net/http is used." if $DEBUG + require 'soap/netHttpClient' + SOAP::NetHttpClient + end + + RUBY_VERSION_STRING = "ruby #{ RUBY_VERSION } (#{ RUBY_RELEASE_DATE }) [#{ RUBY_PLATFORM }]" + %q$Id$ =~ /: (\S+),v (\S+)/ + RCS_FILE, RCS_REVISION = $1, $2 + + class ConnectionData + attr_accessor :send_string + attr_accessor :send_contenttype + attr_accessor :receive_string + attr_accessor :receive_contenttype + + def initialize + @send_string = nil + @send_contenttype = nil + @receive_string = nil + @receive_contenttype = nil + @bag = {} + end + + def [](idx) + @bag[idx] + end + + def []=(idx, value) + @bag[idx] = value + end + end + + attr_accessor :endpoint_url + + def initialize(endpoint_url) + @endpoint_url = endpoint_url + end + + def self.parse_media_type(str) + if /^#{ MediaType }(?:\s*;\s*charset=([^"]+|"[^"]+"))?$/i !~ str + raise StreamError.new("Illegal media type."); + end + charset = $1 + charset.gsub!(/"/, '') if charset + charset + end + + def self.create_media_type(charset) + "#{ MediaType }; charset=#{ charset }" + end +end + + +class HTTPPostStreamHandler < StreamHandler + include SOAP + +public + + attr_accessor :wiredump_dev + attr_accessor :wiredump_file_base + attr_accessor :charset + + NofRetry = 10 # [times] + ConnectTimeout = 20 # [sec] + SendTimeout = 60 # [sec] + ReceiveTimeout = 60 # [sec] + + def initialize(endpoint_url, proxy = nil, charset = nil) + super(endpoint_url) + @proxy = proxy || ENV['http_proxy'] || ENV['HTTP_PROXY'] + @charset = charset || XSD::Charset.charset_label($KCODE) + @wiredump_dev = nil # Set an IO to get wiredump. + @wiredump_file_base = nil + @client = Client.new(@proxy, "SOAP4R/#{ Version }") + @client.session_manager.connect_timeout = ConnectTimeout + @client.session_manager.send_timeout = SendTimeout + @client.session_manager.receive_timeout = ReceiveTimeout + end + + def proxy=(proxy) + @proxy = proxy + @client.proxy = @proxy + end + + def send(soap_string, soapaction = nil, charset = @charset) + send_post(soap_string, soapaction, charset) + end + + def reset + @client.reset(@endpoint_url) + end + +private + + def send_post(soap_string, soapaction, charset) + data = ConnectionData.new + data.send_string = soap_string + data.send_contenttype = StreamHandler.create_media_type(charset) + + wiredump_dev = if @wiredump_dev && @wiredump_dev.respond_to?("<<") + @wiredump_dev + else + nil + end + @client.debug_dev = wiredump_dev + + if @wiredump_file_base + filename = @wiredump_file_base + '_request.xml' + f = File.open(filename, "w") + f << soap_string + f.close + end + + extra = {} + extra['Content-Type'] = data.send_contenttype + extra['SOAPAction'] = "\"#{ soapaction }\"" + + wiredump_dev << "Wire dump:\n\n" if wiredump_dev + begin + res = @client.post(@endpoint_url, soap_string, extra) + rescue + @client.reset(@endpoint_url) + raise + end + wiredump_dev << "\n\n" if wiredump_dev + + receive_string = res.content + + if @wiredump_file_base + filename = @wiredump_file_base + '_response.xml' + f = File.open(filename, "w") + f << receive_string + f.close + end + + case res.status + when 405 + raise PostUnavailableError.new("#{ res.status }: #{ res.reason }") + when 200, 500 + # Nothing to do. + else + raise HTTPStreamError.new("#{ res.status }: #{ res.reason }") + end + + data.receive_string = receive_string + data.receive_contenttype = res.contenttype + + return data + end + + CRLF = "\r\n" +end + + +end diff --git a/lib/soap/wsdlDriver.rb b/lib/soap/wsdlDriver.rb new file mode 100644 index 000000000..849effcb0 --- /dev/null +++ b/lib/soap/wsdlDriver.rb @@ -0,0 +1,490 @@ +=begin +SOAP4R - SOAP WSDL driver +Copyright (C) 2002, 2003 NAKAMURA, Hiroshi. + +This program is free software; you can redistribute it and/or modify it under +the terms of the GNU General Public License as published by the Free Software +Foundation; either version 2 of the License, or (at your option) any later +version. + +This program is distributed in the hope that it will be useful, but WITHOUT ANY +WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A +PRATICULAR PURPOSE. See the GNU General Public License for more details. + +You should have received a copy of the GNU General Public License along with +this program; if not, write to the Free Software Foundation, Inc., 675 Mass +Ave, Cambridge, MA 02139, USA. +=end + + +require 'wsdl/parser' +require 'wsdl/importer' +require 'xsd/qname' +require 'soap/element' +require 'soap/baseData' +require 'soap/streamHandler' +require 'soap/mapping' +require 'soap/mapping/wsdlRegistry' +require 'soap/rpc/rpc' +require 'soap/rpc/element' +require 'soap/processor' +require 'logger' + + +module SOAP + + +class WSDLDriverFactory + class FactoryError < StandardError; end + + attr_reader :wsdl + + def initialize(wsdl, logdev = nil) + @logdev = logdev + @wsdl = import(wsdl) + end + + def create_driver(servicename = nil, portname = nil, opt = {}) + service = if servicename + @wsdl.service(XSD::QName.new(@wsdl.targetnamespace, servicename)) + else + @wsdl.services[0] + end + if service.nil? + raise FactoryError.new("Service #{ servicename } not found in WSDL.") + end + port = if portname + service.ports[XSD::QName.new(@wsdl.targetnamespace, portname)] + else + service.ports[0] + end + if port.nil? + raise FactoryError.new("Port #{ portname } not found in WSDL.") + end + if port.soap_address.nil? + raise FactoryError.new("soap:address element not found in WSDL.") + end + WSDLDriver.new(@wsdl, port, @logdev, opt) + end + + # Backward compatibility. + alias createDriver create_driver + +private + + def import(location) + WSDL::Importer.import(location) + end +end + + +class WSDLDriver + class << self + def __attr_proxy(symbol, assignable = false) + name = symbol.to_s + module_eval <<-EOD + def #{name} + @servant.#{name} + end + EOD + if assignable + module_eval <<-EOD + def #{name}=(rhs) + @servant.#{name} = rhs + end + EOD + end + end + end + + __attr_proxy :opt + __attr_proxy :logdev, true + __attr_proxy :mapping_registry, true # for RPC unmarshal + __attr_proxy :wsdl_mapping_registry, true # for RPC marshal + __attr_proxy :endpoint_url, true + __attr_proxy :wiredump_dev, true + __attr_proxy :wiredump_file_base, true + __attr_proxy :httpproxy, true + + __attr_proxy :default_encodingstyle, true + __attr_proxy :allow_unqualified_element, true + __attr_proxy :generate_explicit_type, true + + def reset_stream + @servant.reset_stream + end + + # Backward compatibility. + alias generateEncodeType= generate_explicit_type= + + class Servant__ + include Logger::Severity + include SOAP + + attr_reader :opt + attr_accessor :logdev + attr_accessor :mapping_registry + attr_accessor :wsdl_mapping_registry + attr_reader :endpoint_url + attr_reader :wiredump_dev + attr_reader :wiredump_file_base + attr_reader :httpproxy + + attr_accessor :default_encodingstyle + attr_accessor :allow_unqualified_element + attr_accessor :generate_explicit_type + + class Mapper + def initialize(elements, types) + @elements = elements + @types = types + end + + def obj2ele(obj, name) + if ele = @elements[name] + _obj2ele(obj, ele) + elsif type = @types[name] + obj2type(obj, type) + else + raise RuntimeError.new("Cannot find name #{name} in schema.") + end + end + + def ele2obj(ele, *arg) + raise NotImplementedError.new + end + + private + + def _obj2ele(obj, ele) + o = nil + if ele.type + if type = @types[ele.type] + o = obj2type(obj, type) + elsif type = TypeMap[ele.type] + o = base2soap(obj, type) + else + raise RuntimeError.new("Cannot find type #{ele.type}.") + end + o.elename = ele.name + elsif ele.local_complextype + o = SOAPElement.new(ele.name) + ele.local_complextype.each_element do |child_name, child_ele| + o.add(_obj2ele(find_attribute(obj, child_name.name), child_ele)) + end + else + raise RuntimeError.new("Illegal schema?") + end + o + end + + def obj2type(obj, type) + o = SOAPElement.new(type.name) + type.each_element do |child_name, child_ele| + o.add(_obj2ele(find_attribute(obj, child_name.name), child_ele)) + end + o + end + + def _ele2obj(ele) + raise NotImplementedError.new + end + + def base2soap(obj, type) + soap_obj = nil + if type <= XSD::XSDString + soap_obj = type.new(XSD::Charset.is_ces(obj, $KCODE) ? + XSD::Charset.encoding_conv(obj, $KCODE, XSD::Charset.encoding) : obj) + else + soap_obj = type.new(obj) + end + soap_obj + end + + def find_attribute(obj, attr_name) + if obj.respond_to?(attr_name) + obj.__send__(attr_name) + elsif obj.is_a?(Hash) + obj[attr_name] || obj[attr_name.intern] + else + obj.instance_eval("@#{ attr_name }") + end + end + end + + def initialize(host, wsdl, port, logdev, opt) + @host = host + @wsdl = wsdl + @port = port + @logdev = logdev + @opt = opt.dup + @mapping_registry = nil # for rpc unmarshal + @wsdl_mapping_registry = nil # for rpc marshal + @endpoint_url = nil + @wiredump_dev = nil + @wiredump_file_base = nil + @httpproxy = ENV['http_proxy'] || ENV['HTTP_PROXY'] + + @wsdl_elements = @wsdl.collect_elements + @wsdl_types = @wsdl.collect_complextypes + @rpc_decode_typemap = @wsdl_types + @wsdl.soap_rpc_complextypes(port.find_binding) + @wsdl_mapping_registry = Mapping::WSDLRegistry.new(@rpc_decode_typemap) + @doc_mapper = Mapper.new(@wsdl_elements, @wsdl_types) + @default_encodingstyle = EncodingNamespace + @allow_unqualified_element = true + @generate_explicit_type = false + + create_handler + @operations = {} + # Convert a map which key is QName, to a Hash which key is String. + @port.inputoperation_map.each do |op_name, op_info| + @operations[op_name.name] = op_info + add_method_interface(op_info) + end + end + + def endpoint_url=(endpoint_url) + @endpoint_url = endpoint_url + if @handler + @handler.endpoint_url = @endpoint_url + @handler.reset + end + log(DEBUG) { "endpoint_url=: set endpoint_url #{ @endpoint_url }." } + end + + def wiredump_dev=(dev) + @wiredump_dev = dev + if @handler + @handler.wiredump_dev = @wiredump_dev + @handler.reset + end + end + + def wiredump_file_base=(base) + @wiredump_file_base = base + end + + def httpproxy=(httpproxy) + @httpproxy = httpproxy + if @handler + @handler.proxy = @httpproxy + @handler.reset + end + log(DEBUG) { "httpproxy=: set httpproxy #{ @httpproxy }." } + end + + def reset_stream + @handler.reset + end + + def rpc_send(method_name, *params) + log(INFO) { "call: calling method '#{ method_name }'." } + log(DEBUG) { "call: parameters '#{ params.inspect }'." } + + op_info = @operations[method_name] + parts_names = op_info.bodyparts.collect { |part| part.name } + obj = create_method_obj(parts_names, params) + method = Mapping.obj2soap(obj, @wsdl_mapping_registry, op_info.optype_name) + method.elename = op_info.op_name + method.type = XSD::QName.new # Request should not be typed. + req_header = nil + req_body = SOAPBody.new(method) + + if @wiredump_file_base + @handler.wiredump_file_base = @wiredump_file_base + '_' << method_name + end + + begin + opt = create_options + opt[:decode_typemap] = @rpc_decode_typemap + res_header, res_body = invoke(req_header, req_body, op_info, opt) + if res_body.fault + raise SOAP::FaultError.new(res_body.fault) + end + rescue SOAP::FaultError => e + Mapping.fault2exception(e) + end + + ret = res_body.response ? + Mapping.soap2obj(res_body.response, @mapping_registry) : nil + + if res_body.outparams + outparams = res_body.outparams.collect { |outparam| + Mapping.soap2obj(outparam) + } + return [ret].concat(outparams) + else + return ret + end + end + + # req_header: [[element, mustunderstand, encodingstyle(QName/String)], ...] + # req_body: SOAPBasetype/SOAPCompoundtype + def document_send(name, header_obj, body_obj) + log(INFO) { "document_send: sending document '#{ name }'." } + op_info = @operations[name] + req_header = header_from_obj(header_obj, op_info) + req_body = body_from_obj(body_obj, op_info) + opt = create_options + res_header, res_body = invoke(req_header, req_body, op_info, opt) + if res_body.fault + raise SOAP::FaultError.new(res_body.fault) + end + res_body_obj = res_body.response ? + Mapping.soap2obj(res_body.response, @mapping_registry) : nil + return res_header, res_body_obj + end + + private + + def create_handler + endpoint_url = @endpoint_url || @port.soap_address.location + @handler = HTTPPostStreamHandler.new(endpoint_url, @httpproxy, + XSD::Charset.encoding_label) + @handler.wiredump_dev = @wiredump_dev + end + + def create_method_obj(names, params) + o = Object.new + for idx in 0 ... params.length + o.instance_eval("@#{ names[idx] } = params[idx]") + end + o + end + + def invoke(req_header, req_body, op_info, opt) + send_string = Processor.marshal(req_header, req_body, opt) + log(DEBUG) { "invoke: sending string #{ send_string }" } + data = @handler.send(send_string, op_info.soapaction) + log(DEBUG) { "invoke: received string #{ data.receive_string }" } + if data.receive_string.empty? + return nil, nil + end + res_charset = StreamHandler.parse_media_type(data.receive_contenttype) + opt[:charset] = res_charset + res_header, res_body = Processor.unmarshal(data.receive_string, opt) + return res_header, res_body + end + + def header_from_obj(obj, op_info) + if obj.is_a?(SOAPHeader) + obj + elsif op_info.headerparts.empty? + if obj.nil? + nil + else + raise RuntimeError.new("No header definition in schema.") + end + elsif op_info.headerparts.size == 1 + part = op_info.headerparts[0] + header = SOAPHeader.new() + header.add(headeritem_from_obj(obj, part.element || part.eletype)) + header + else + header = SOAPHeader.new() + op_info.headerparts.each do |part| + child = obj[part.elename.name] + ele = headeritem_from_obj(child, part.element || part.eletype) + header.add(ele) + end + header + end + end + + def headeritem_from_obj(obj, name) + if obj.nil? + SOAPElement.new(name) + elsif obj.is_a?(SOAPHeaderItem) + obj + else + @doc_mapper.obj2ele(obj, name) + end + end + + def body_from_obj(obj, op_info) + if obj.is_a?(SOAPBody) + obj + elsif op_info.bodyparts.empty? + if obj.nil? + nil + else + raise RuntimeError.new("No body found in schema.") + end + elsif op_info.bodyparts.size == 1 + part = op_info.bodyparts[0] + ele = bodyitem_from_obj(obj, part.element || part.type) + SOAPBody.new(ele) + else + body = SOAPBody.new + op_info.bodyparts.each do |part| + child = obj[part.elename.name] + ele = bodyitem_from_obj(child, part.element || part.type) + body.add(ele.elename.name, ele) + end + body + end + end + + def bodyitem_from_obj(obj, name) + if obj.nil? + SOAPElement.new(name) + elsif obj.is_a?(SOAPElement) + obj + else + @doc_mapper.obj2ele(obj, name) + end + end + + def add_method_interface(op_info) + case op_info.style + when :document + add_document_method_interface(op_info.op_name.name) + when :rpc + parts_names = op_info.bodyparts.collect { |part| part.name } + add_rpc_method_interface(op_info.op_name.name, parts_names) + else + raise RuntimeError.new("Unknown style: #{op_info.style}") + end + end + + def add_document_method_interface(name) + @host.instance_eval <<-EOS + def #{ name }(headers, body) + @servant.document_send(#{ name.dump }, headers, body) + end + EOS + end + + def add_rpc_method_interface(name, parts_names) + i = 0 + param_names = parts_names.collect { |orgname| i += 1; "arg#{ i }" } + callparam_str = (param_names.collect { |pname| ", " + pname }).join + @host.instance_eval <<-EOS + def #{ name }(#{ param_names.join(", ") }) + @servant.rpc_send(#{ name.dump }#{ callparam_str }) + end + EOS + end + + def create_options + opt = @opt.dup + opt[:default_encodingstyle] = @default_encodingstyle + opt[:allow_unqualified_element] = @allow_unqualified_element + opt[:generate_explicit_type] = @generate_explicit_type + opt + end + + def log(sev) + @logdev.add(sev, nil, self.class) { yield } if @logdev + end + end + + def initialize(wsdl, port, logdev, opt) + @servant = Servant__.new(self, wsdl, port, logdev, opt) + end +end + + +end + + |