johnreitano-savon 0.7.2.1
Sign up to get free protection for your applications and to get access to all the features.
- data/CHANGELOG +124 -0
- data/README.textile +75 -0
- data/Rakefile +45 -0
- data/lib/savon/client.rb +84 -0
- data/lib/savon/core_ext/datetime.rb +8 -0
- data/lib/savon/core_ext/hash.rb +78 -0
- data/lib/savon/core_ext/net_http.rb +20 -0
- data/lib/savon/core_ext/object.rb +21 -0
- data/lib/savon/core_ext/string.rb +47 -0
- data/lib/savon/core_ext/symbol.rb +8 -0
- data/lib/savon/core_ext/uri.rb +10 -0
- data/lib/savon/core_ext.rb +3 -0
- data/lib/savon/request.rb +149 -0
- data/lib/savon/response.rb +99 -0
- data/lib/savon/soap.rb +176 -0
- data/lib/savon/wsdl.rb +122 -0
- data/lib/savon/wsse.rb +136 -0
- data/lib/savon.rb +34 -0
- data/spec/basic_spec_helper.rb +12 -0
- data/spec/endpoint_helper.rb +22 -0
- data/spec/fixtures/response/response_fixture.rb +36 -0
- data/spec/fixtures/response/xml/authentication.xml +14 -0
- data/spec/fixtures/response/xml/multi_ref.xml +39 -0
- data/spec/fixtures/response/xml/soap_fault.xml +8 -0
- data/spec/fixtures/response/xml/soap_fault12.xml +18 -0
- data/spec/fixtures/wsdl/wsdl_fixture.rb +37 -0
- data/spec/fixtures/wsdl/xml/authentication.xml +63 -0
- data/spec/fixtures/wsdl/xml/namespaced_actions.xml +307 -0
- data/spec/fixtures/wsdl/xml/no_namespace.xml +115 -0
- data/spec/http_stubs.rb +23 -0
- data/spec/integration/http_basic_auth_spec.rb +16 -0
- data/spec/integration/server.rb +51 -0
- data/spec/savon/client_spec.rb +77 -0
- data/spec/savon/core_ext/datetime_spec.rb +12 -0
- data/spec/savon/core_ext/hash_spec.rb +138 -0
- data/spec/savon/core_ext/net_http_spec.rb +38 -0
- data/spec/savon/core_ext/object_spec.rb +40 -0
- data/spec/savon/core_ext/string_spec.rb +68 -0
- data/spec/savon/core_ext/symbol_spec.rb +11 -0
- data/spec/savon/core_ext/uri_spec.rb +15 -0
- data/spec/savon/request_spec.rb +89 -0
- data/spec/savon/response_spec.rb +137 -0
- data/spec/savon/savon_spec.rb +23 -0
- data/spec/savon/soap_spec.rb +171 -0
- data/spec/savon/wsdl_spec.rb +84 -0
- data/spec/savon/wsse_spec.rb +132 -0
- data/spec/spec_helper.rb +5 -0
- metadata +218 -0
data/lib/savon/wsdl.rb
ADDED
@@ -0,0 +1,122 @@
|
|
1
|
+
module Savon
|
2
|
+
|
3
|
+
# Savon::WSDL
|
4
|
+
#
|
5
|
+
# Represents the WSDL document.
|
6
|
+
class WSDL
|
7
|
+
|
8
|
+
# Initializer, expects a Savon::Request.
|
9
|
+
def initialize(request)
|
10
|
+
@request = request
|
11
|
+
end
|
12
|
+
|
13
|
+
# Sets whether to use the WSDL.
|
14
|
+
attr_writer :enabled
|
15
|
+
|
16
|
+
# Returns whether to use the WSDL. Defaults to +true+.
|
17
|
+
def enabled?
|
18
|
+
@enabled.nil? ? true : @enabled
|
19
|
+
end
|
20
|
+
|
21
|
+
# Returns the namespace URI of the WSDL.
|
22
|
+
def namespace_uri
|
23
|
+
@namespace_uri ||= stream.namespace_uri
|
24
|
+
end
|
25
|
+
|
26
|
+
# Returns an Array of available SOAP actions.
|
27
|
+
def soap_actions
|
28
|
+
@soap_actions ||= stream.operations.keys
|
29
|
+
end
|
30
|
+
|
31
|
+
# Returns a Hash of SOAP operations including their corresponding
|
32
|
+
# SOAP actions and inputs.
|
33
|
+
def operations
|
34
|
+
@operations ||= stream.operations
|
35
|
+
end
|
36
|
+
|
37
|
+
# Returns the SOAP endpoint.
|
38
|
+
def soap_endpoint
|
39
|
+
@soap_endpoint ||= stream.soap_endpoint
|
40
|
+
end
|
41
|
+
|
42
|
+
# Returns +true+ for available methods and SOAP actions.
|
43
|
+
def respond_to?(method)
|
44
|
+
return true if soap_actions.include? method
|
45
|
+
super
|
46
|
+
end
|
47
|
+
|
48
|
+
# Returns the raw WSDL document.
|
49
|
+
def to_s
|
50
|
+
@document ||= @request.wsdl.body
|
51
|
+
end
|
52
|
+
|
53
|
+
private
|
54
|
+
|
55
|
+
# Returns the Savon::WSDLStream.
|
56
|
+
def stream
|
57
|
+
unless @stream
|
58
|
+
@stream = WSDLStream.new
|
59
|
+
REXML::Document.parse_stream to_s, @stream
|
60
|
+
end
|
61
|
+
@stream
|
62
|
+
end
|
63
|
+
|
64
|
+
end
|
65
|
+
|
66
|
+
# Savon::WSDLStream
|
67
|
+
#
|
68
|
+
# Stream listener for parsing the WSDL document.
|
69
|
+
class WSDLStream
|
70
|
+
|
71
|
+
# The main sections of a WSDL document.
|
72
|
+
Sections = %w(definitions types message portType binding service)
|
73
|
+
|
74
|
+
def initialize
|
75
|
+
@depth, @operations = 0, {}
|
76
|
+
end
|
77
|
+
|
78
|
+
# Returns the namespace URI.
|
79
|
+
attr_reader :namespace_uri
|
80
|
+
|
81
|
+
# Returns the SOAP operations.
|
82
|
+
attr_reader :operations
|
83
|
+
|
84
|
+
# Returns the SOAP endpoint.
|
85
|
+
attr_reader :soap_endpoint
|
86
|
+
|
87
|
+
# Hook method called when the stream parser encounters a starting tag.
|
88
|
+
def tag_start(tag, attrs)
|
89
|
+
@depth += 1
|
90
|
+
tag = tag.strip_namespace
|
91
|
+
|
92
|
+
@section = tag.to_sym if @depth <= 2 && Sections.include?(tag)
|
93
|
+
@namespace_uri ||= attrs["targetNamespace"] if @section == :definitions
|
94
|
+
@soap_endpoint ||= URI(attrs["location"]) if @section == :service && tag == "address"
|
95
|
+
|
96
|
+
operation_from tag, attrs if @section == :binding && tag == "operation"
|
97
|
+
end
|
98
|
+
|
99
|
+
# Hook method called when the stream parser encounters a closing tag.
|
100
|
+
def tag_end(tag)
|
101
|
+
@depth -= 1
|
102
|
+
end
|
103
|
+
|
104
|
+
# Stores available operations from a given tag +name+ and +attrs+.
|
105
|
+
def operation_from(tag, attrs)
|
106
|
+
@input = attrs["name"] if attrs["name"]
|
107
|
+
|
108
|
+
if attrs["soapAction"]
|
109
|
+
@action = !attrs["soapAction"].blank? ? attrs["soapAction"] : @input
|
110
|
+
@input = @action.split("/").last if !@input || @input.empty?
|
111
|
+
|
112
|
+
@operations[@input.snakecase.to_sym] = { :action => @action, :input => @input }
|
113
|
+
@input, @action = nil, nil
|
114
|
+
end
|
115
|
+
end
|
116
|
+
|
117
|
+
# Catches calls to unimplemented hook methods.
|
118
|
+
def method_missing(method, *args)
|
119
|
+
end
|
120
|
+
|
121
|
+
end
|
122
|
+
end
|
data/lib/savon/wsse.rb
ADDED
@@ -0,0 +1,136 @@
|
|
1
|
+
module Savon
|
2
|
+
|
3
|
+
# Savon::WSSE
|
4
|
+
#
|
5
|
+
# Represents parameters for WSSE authentication.
|
6
|
+
class WSSE
|
7
|
+
|
8
|
+
# Base address for WSSE docs.
|
9
|
+
BaseAddress = "http://docs.oasis-open.org/wss/2004/01"
|
10
|
+
|
11
|
+
# Namespace for WS Security Secext.
|
12
|
+
WSENamespace = BaseAddress + "/oasis-200401-wss-wssecurity-secext-1.0.xsd"
|
13
|
+
|
14
|
+
# Namespace for WS Security Utility.
|
15
|
+
WSUNamespace = BaseAddress + "/oasis-200401-wss-wssecurity-utility-1.0.xsd"
|
16
|
+
|
17
|
+
# URI for "wsse:Password/@Type" #PasswordText.
|
18
|
+
PasswordTextURI = BaseAddress + "/oasis-200401-wss-username-token-profile-1.0#PasswordText"
|
19
|
+
|
20
|
+
# URI for "wsse:Password/@Type" #PasswordDigest.
|
21
|
+
PasswordDigestURI = BaseAddress + "/oasis-200401-wss-username-token-profile-1.0#PasswordDigest"
|
22
|
+
|
23
|
+
# Global WSSE username.
|
24
|
+
@@username = nil
|
25
|
+
|
26
|
+
# Returns the global WSSE username.
|
27
|
+
def self.username
|
28
|
+
@@username
|
29
|
+
end
|
30
|
+
|
31
|
+
# Sets the global WSSE username.
|
32
|
+
def self.username=(username)
|
33
|
+
@@username = username.to_s if username.respond_to? :to_s
|
34
|
+
@@username = nil if username.nil?
|
35
|
+
end
|
36
|
+
|
37
|
+
# Global WSSE password.
|
38
|
+
@@password = nil
|
39
|
+
|
40
|
+
# Returns the global WSSE password.
|
41
|
+
def self.password
|
42
|
+
@@password
|
43
|
+
end
|
44
|
+
|
45
|
+
# Sets the global WSSE password.
|
46
|
+
def self.password=(password)
|
47
|
+
@@password = password.to_s if password.respond_to? :to_s
|
48
|
+
@@password = nil if password.nil?
|
49
|
+
end
|
50
|
+
|
51
|
+
# Global setting of whether to use WSSE digest.
|
52
|
+
@@digest = false
|
53
|
+
|
54
|
+
# Returns the global setting of whether to use WSSE digest.
|
55
|
+
def self.digest?
|
56
|
+
@@digest
|
57
|
+
end
|
58
|
+
|
59
|
+
# Global setting of whether to use WSSE digest.
|
60
|
+
def self.digest=(digest)
|
61
|
+
@@digest = digest
|
62
|
+
end
|
63
|
+
|
64
|
+
# Sets the WSSE username per request.
|
65
|
+
def username=(username)
|
66
|
+
@username = username.to_s if username.respond_to? :to_s
|
67
|
+
@username = nil if username.nil?
|
68
|
+
end
|
69
|
+
|
70
|
+
# Returns the WSSE username. Defaults to the global setting.
|
71
|
+
def username
|
72
|
+
@username || self.class.username
|
73
|
+
end
|
74
|
+
|
75
|
+
# Sets the WSSE password per request.
|
76
|
+
def password=(password)
|
77
|
+
@password = password.to_s if password.respond_to? :to_s
|
78
|
+
@password = nil if password.nil?
|
79
|
+
end
|
80
|
+
|
81
|
+
# Returns the WSSE password. Defaults to the global setting.
|
82
|
+
def password
|
83
|
+
@password || self.class.password
|
84
|
+
end
|
85
|
+
|
86
|
+
# Sets whether to use WSSE digest per request.
|
87
|
+
attr_writer :digest
|
88
|
+
|
89
|
+
# Returns whether to use WSSE digest. Defaults to the global setting.
|
90
|
+
def digest?
|
91
|
+
@digest || self.class.digest?
|
92
|
+
end
|
93
|
+
|
94
|
+
# Returns the XML for a WSSE header or an empty String unless both
|
95
|
+
# username and password were specified.
|
96
|
+
def header
|
97
|
+
return "" unless username && password
|
98
|
+
|
99
|
+
builder = Builder::XmlMarkup.new
|
100
|
+
builder.wsse :Security, "xmlns:wsse" => WSENamespace do |xml|
|
101
|
+
xml.wsse :UsernameToken, "xmlns:wsu" => WSUNamespace do
|
102
|
+
xml.wsse :Username, username
|
103
|
+
xml.wsse :Nonce, nonce
|
104
|
+
xml.wsu :Created, timestamp
|
105
|
+
xml.wsse :Password, password_node, :Type => password_type
|
106
|
+
end
|
107
|
+
end
|
108
|
+
end
|
109
|
+
|
110
|
+
private
|
111
|
+
|
112
|
+
# Returns the WSSE password. Encrypts the password for digest authentication.
|
113
|
+
def password_node
|
114
|
+
return password unless digest?
|
115
|
+
|
116
|
+
token = nonce + timestamp + password
|
117
|
+
Base64.encode64(Digest::SHA1.hexdigest(token)).chomp!
|
118
|
+
end
|
119
|
+
|
120
|
+
# Returns the URI for the "wsse:Password/@Type" attribute.
|
121
|
+
def password_type
|
122
|
+
digest? ? PasswordDigestURI : PasswordTextURI
|
123
|
+
end
|
124
|
+
|
125
|
+
# Returns a WSSE nonce.
|
126
|
+
def nonce
|
127
|
+
@nonce ||= Digest::SHA1.hexdigest String.random + timestamp
|
128
|
+
end
|
129
|
+
|
130
|
+
# Returns a WSSE timestamp.
|
131
|
+
def timestamp
|
132
|
+
@timestamp ||= Time.now.strftime Savon::SOAPDateTimeFormat
|
133
|
+
end
|
134
|
+
|
135
|
+
end
|
136
|
+
end
|
data/lib/savon.rb
ADDED
@@ -0,0 +1,34 @@
|
|
1
|
+
module Savon
|
2
|
+
|
3
|
+
# Supported SOAP versions.
|
4
|
+
SOAPVersions = [1, 2]
|
5
|
+
|
6
|
+
# SOAP xs:dateTime format.
|
7
|
+
SOAPDateTimeFormat = "%Y-%m-%dT%H:%M:%S"
|
8
|
+
|
9
|
+
# SOAP xs:dateTime Regexp.
|
10
|
+
SOAPDateTimeRegexp = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}/
|
11
|
+
|
12
|
+
# Raised in case of an HTTP error.
|
13
|
+
class HTTPError < StandardError; end
|
14
|
+
|
15
|
+
# Raised in case of a SOAP fault.
|
16
|
+
class SOAPFault < StandardError; end
|
17
|
+
|
18
|
+
end
|
19
|
+
|
20
|
+
# standard libs
|
21
|
+
%w(logger net/https openssl base64 digest/sha1 rexml/document).each do |lib|
|
22
|
+
require lib
|
23
|
+
end
|
24
|
+
|
25
|
+
# gems
|
26
|
+
require "rubygems"
|
27
|
+
%w(builder crack/xml).each do |gem|
|
28
|
+
require gem
|
29
|
+
end
|
30
|
+
|
31
|
+
# core files
|
32
|
+
%w(core_ext wsse soap request response wsdl client).each do |file|
|
33
|
+
require File.dirname(__FILE__) + "/savon/#{file}"
|
34
|
+
end
|
@@ -0,0 +1,22 @@
|
|
1
|
+
class EndpointHelper
|
2
|
+
|
3
|
+
# Returns the WSDL endpoint for a given +type+ of request.
|
4
|
+
def self.wsdl_endpoint(type = nil)
|
5
|
+
case type
|
6
|
+
when :no_namespace then "http://nons.example.com/Service?wsdl"
|
7
|
+
when :namespaced_actions then "http://nsactions.example.com/Service?wsdl"
|
8
|
+
else soap_endpoint(type)
|
9
|
+
end
|
10
|
+
end
|
11
|
+
|
12
|
+
# Returns the SOAP endpoint for a given +type+ of request.
|
13
|
+
def self.soap_endpoint(type = nil)
|
14
|
+
case type
|
15
|
+
when :soap_fault then "http://soapfault.example.com/Service?wsdl"
|
16
|
+
when :http_error then "http://httperror.example.com/Service?wsdl"
|
17
|
+
when :invalid then "http://invalid.example.com/Service?wsdl"
|
18
|
+
else "http://example.com/validation/1.0/AuthenticationService"
|
19
|
+
end
|
20
|
+
end
|
21
|
+
|
22
|
+
end
|
@@ -0,0 +1,36 @@
|
|
1
|
+
class ResponseFixture
|
2
|
+
|
3
|
+
def self.authentication(value = nil)
|
4
|
+
case value
|
5
|
+
when :to_hash
|
6
|
+
{ :success => true,
|
7
|
+
:authentication_value => {
|
8
|
+
:token => "a68d1d6379b62ff339a0e0c69ed4d9cf",
|
9
|
+
:token_hash => "AAAJxA;cIedoT;mY10ExZwG6JuKgp2OYKxow==",
|
10
|
+
:client => "radclient"
|
11
|
+
}
|
12
|
+
}
|
13
|
+
else
|
14
|
+
@@authentication ||= load_fixture :authentication
|
15
|
+
end
|
16
|
+
end
|
17
|
+
|
18
|
+
def self.soap_fault
|
19
|
+
@@soap_fault ||= load_fixture :soap_fault
|
20
|
+
end
|
21
|
+
|
22
|
+
def self.soap_fault12
|
23
|
+
@@soap_fault12 ||= load_fixture :soap_fault12
|
24
|
+
end
|
25
|
+
|
26
|
+
def self.multi_ref
|
27
|
+
@@multi_ref ||= load_fixture :multi_ref
|
28
|
+
end
|
29
|
+
|
30
|
+
private
|
31
|
+
|
32
|
+
def self.load_fixture(fixture)
|
33
|
+
File.read File.dirname(__FILE__) + "/xml/#{fixture}.xml"
|
34
|
+
end
|
35
|
+
|
36
|
+
end
|
@@ -0,0 +1,14 @@
|
|
1
|
+
<soap:Envelope xmlns:soap="http://schemas.xmlsoap.org/soap/envelope/">
|
2
|
+
<soap:Body>
|
3
|
+
<ns2:authenticateResponse xmlns:ns2="http://v1_0.ws.user.example.com">
|
4
|
+
<return>
|
5
|
+
<authenticationValue>
|
6
|
+
<token>a68d1d6379b62ff339a0e0c69ed4d9cf</token>
|
7
|
+
<tokenHash>AAAJxA;cIedoT;mY10ExZwG6JuKgp2OYKxow==</tokenHash>
|
8
|
+
<client>radclient</client>
|
9
|
+
</authenticationValue>
|
10
|
+
<success>true</success>
|
11
|
+
</return>
|
12
|
+
</ns2:authenticateResponse>
|
13
|
+
</soap:Body>
|
14
|
+
</soap:Envelope>
|
@@ -0,0 +1,39 @@
|
|
1
|
+
<soapenv:Envelope xmlns:soapenv="http://schemas.xmlsoap.org/soap/envelope/" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
|
2
|
+
<soapenv:Body>
|
3
|
+
<ns1:listResponse soapenv:encodingStyle="http://schemas.xmlsoap.org/soap/encoding/" xmlns:ns1="http://ws.example.com">
|
4
|
+
<listReturn soapenc:arrayType="ns2:HistoryEntry[3]" xsi:type="soapenc:Array" xmlns:ns2="http://ws.example.com/ws/history" xmlns:soapenc="http://schemas.xmlsoap.org/soap/encoding/">
|
5
|
+
<listReturn href="#id0"/>
|
6
|
+
<listReturn href="#id1"/>
|
7
|
+
<listReturn href="#id2"/>
|
8
|
+
</listReturn>
|
9
|
+
</ns1:listResponse>
|
10
|
+
<multiRef id="id1" soapenc:root="0" soapenv:encodingStyle="http://schemas.xmlsoap.org/soap/encoding/" xsi:type="ns5:HistoryEntry" xmlns:ns5="http://ws.example.com/ws/history" xmlns:soapenc="http://schemas.xmlsoap.org/soap/encoding/">
|
11
|
+
<date xsi:type="xsd:dateTime">2009-09-22T13:47:23.000Z</date>
|
12
|
+
<location xsi:type="soapenc:string">Archive</location>
|
13
|
+
<mailId href="#id9"/>
|
14
|
+
<referenceId href="#id8"/>
|
15
|
+
<state xsi:type="soapenc:string">Original</state>
|
16
|
+
<subject xsi:type="soapenc:string">Mail from 09-22-2009: Misc</subject>
|
17
|
+
</multiRef>
|
18
|
+
<multiRef id="id0" soapenc:root="0" soapenv:encodingStyle="http://schemas.xmlsoap.org/soap/encoding/" xsi:type="ns6:HistoryEntry" xmlns:ns6="http://ws.example.com/ws/history" xmlns:soapenc="http://schemas.xmlsoap.org/soap/encoding/">
|
19
|
+
<date xsi:type="xsd:dateTime">2009-04-30T06:38:34.000Z</date>
|
20
|
+
<location xsi:type="soapenc:string">Archive</location>
|
21
|
+
<mailId href="#id10"/>
|
22
|
+
<referenceId href="#id8"/>
|
23
|
+
<state xsi:type="soapenc:string">Original</state>
|
24
|
+
<subject xsi:type="soapenc:string">Mail from 04-29-2009: Technical support</subject>
|
25
|
+
</multiRef>
|
26
|
+
<multiRef id="id2" soapenc:root="0" soapenv:encodingStyle="http://schemas.xmlsoap.org/soap/encoding/" xsi:type="ns7:HistoryEntry" xmlns:ns7="http://ws.example.com/ws/history" xmlns:soapenc="http://schemas.xmlsoap.org/soap/encoding/">
|
27
|
+
<date xsi:type="xsd:dateTime">2009-12-18T15:43:21.000Z</date>
|
28
|
+
<location xsi:type="soapenc:string">Archive</location>
|
29
|
+
<mailId href="#id11"/>
|
30
|
+
<referenceId href="#id8"/>
|
31
|
+
<state xsi:type="soapenc:string">Original</state>
|
32
|
+
<subject xsi:type="soapenc:string">Mail from 12-17-2009: Misc</subject>
|
33
|
+
</multiRef>
|
34
|
+
<multiRef id="id11" soapenc:root="0" soapenv:encodingStyle="http://schemas.xmlsoap.org/soap/encoding/" xsi:type="soapenc:int" xmlns:soapenc="http://schemas.xmlsoap.org/soap/encoding/">972219</multiRef>
|
35
|
+
<multiRef id="id10" soapenc:root="0" soapenv:encodingStyle="http://schemas.xmlsoap.org/soap/encoding/" xsi:type="soapenc:int" xmlns:soapenc="http://schemas.xmlsoap.org/soap/encoding/">708021</multiRef>
|
36
|
+
<multiRef id="id8" soapenc:root="0" soapenv:encodingStyle="http://schemas.xmlsoap.org/soap/encoding/" xsi:type="soapenc:int" xmlns:soapenc="http://schemas.xmlsoap.org/soap/encoding/">0</multiRef>
|
37
|
+
<multiRef id="id9" soapenc:root="0" soapenv:encodingStyle="http://schemas.xmlsoap.org/soap/encoding/" xsi:type="soapenc:int" xmlns:soapenc="http://schemas.xmlsoap.org/soap/encoding/">855763</multiRef>
|
38
|
+
</soapenv:Body>
|
39
|
+
</soapenv:Envelope>
|
@@ -0,0 +1,18 @@
|
|
1
|
+
<soap:Envelope xmlns:soap="http://schemas.xmlsoap.org/soap/envelope/" xmlns:m="http://www.example.org/timeouts">
|
2
|
+
<soap:Body>
|
3
|
+
<soap:Fault>
|
4
|
+
<Code>
|
5
|
+
<Value>soap:Sender</Value>
|
6
|
+
<Subcode>
|
7
|
+
<Value>m:MessageTimeout</Value>
|
8
|
+
</Subcode>
|
9
|
+
</Code>
|
10
|
+
<Reason>
|
11
|
+
<Text xml:lang="en">Sender Timeout</Text>
|
12
|
+
</Reason>
|
13
|
+
<Detail>
|
14
|
+
<m:MaxTime>P5M</m:MaxTime>
|
15
|
+
</Detail>
|
16
|
+
</soap:Fault>
|
17
|
+
</soap:Body>
|
18
|
+
</soap:Envelope>
|