tictoc-savon 0.7.9
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- data/.autotest +5 -0
- data/CHANGELOG +176 -0
- data/LICENSE +20 -0
- data/README.rdoc +64 -0
- data/Rakefile +50 -0
- data/lib/savon.rb +35 -0
- data/lib/savon/client.rb +131 -0
- data/lib/savon/core_ext.rb +8 -0
- data/lib/savon/core_ext/array.rb +31 -0
- data/lib/savon/core_ext/datetime.rb +10 -0
- data/lib/savon/core_ext/hash.rb +107 -0
- data/lib/savon/core_ext/net_http.rb +19 -0
- data/lib/savon/core_ext/object.rb +16 -0
- data/lib/savon/core_ext/string.rb +69 -0
- data/lib/savon/core_ext/symbol.rb +8 -0
- data/lib/savon/core_ext/uri.rb +10 -0
- data/lib/savon/logger.rb +56 -0
- data/lib/savon/request.rb +138 -0
- data/lib/savon/response.rb +174 -0
- data/lib/savon/soap.rb +302 -0
- data/lib/savon/version.rb +5 -0
- data/lib/savon/wsdl.rb +137 -0
- data/lib/savon/wsdl_stream.rb +85 -0
- data/lib/savon/wsse.rb +163 -0
- data/spec/basic_spec_helper.rb +11 -0
- data/spec/endpoint_helper.rb +23 -0
- data/spec/fixtures/gzip/gzip_response_fixture.rb +7 -0
- data/spec/fixtures/gzip/message.gz +0 -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/wsdl_fixture.yml +42 -0
- data/spec/fixtures/wsdl/xml/authentication.xml +63 -0
- data/spec/fixtures/wsdl/xml/geotrust.xml +156 -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 +26 -0
- data/spec/integration/http_basic_auth_spec.rb +16 -0
- data/spec/integration/server.rb +51 -0
- data/spec/savon/client_spec.rb +86 -0
- data/spec/savon/core_ext/array_spec.rb +49 -0
- data/spec/savon/core_ext/datetime_spec.rb +21 -0
- data/spec/savon/core_ext/hash_spec.rb +190 -0
- data/spec/savon/core_ext/net_http_spec.rb +38 -0
- data/spec/savon/core_ext/object_spec.rb +34 -0
- data/spec/savon/core_ext/string_spec.rb +99 -0
- data/spec/savon/core_ext/symbol_spec.rb +12 -0
- data/spec/savon/core_ext/uri_spec.rb +19 -0
- data/spec/savon/request_spec.rb +117 -0
- data/spec/savon/response_spec.rb +179 -0
- data/spec/savon/soap_spec.rb +202 -0
- data/spec/savon/wsdl_spec.rb +107 -0
- data/spec/savon/wsse_spec.rb +132 -0
- data/spec/spec.opts +4 -0
- data/spec/spec_helper.rb +5 -0
- metadata +229 -0
@@ -0,0 +1,8 @@
|
|
1
|
+
require "savon/core_ext/object"
|
2
|
+
require "savon/core_ext/string"
|
3
|
+
require "savon/core_ext/symbol"
|
4
|
+
require "savon/core_ext/datetime"
|
5
|
+
require "savon/core_ext/array"
|
6
|
+
require "savon/core_ext/hash"
|
7
|
+
require "savon/core_ext/uri"
|
8
|
+
require "savon/core_ext/net_http"
|
@@ -0,0 +1,31 @@
|
|
1
|
+
class Array
|
2
|
+
|
3
|
+
# Translates the Array into SOAP compatible XML. See: Hash.to_soap_xml.
|
4
|
+
def to_soap_xml(key, escape_xml = true, attributes = {})
|
5
|
+
xml = Builder::XmlMarkup.new
|
6
|
+
|
7
|
+
each_with_index do |item, index|
|
8
|
+
attrs = tag_attributes attributes, index
|
9
|
+
case item
|
10
|
+
when Hash then xml.tag!(key, attrs) { xml << item.to_soap_xml }
|
11
|
+
else xml.tag!(key, attrs) { xml << (escape_xml ? item.to_soap_value : item.to_soap_value!) }
|
12
|
+
end
|
13
|
+
end
|
14
|
+
|
15
|
+
xml.target!
|
16
|
+
end
|
17
|
+
|
18
|
+
private
|
19
|
+
|
20
|
+
# Takes a Hash of +attributes+ and the +index+ for which to return attributes
|
21
|
+
# for duplicate tags.
|
22
|
+
def tag_attributes(attributes, index)
|
23
|
+
return {} if attributes.empty?
|
24
|
+
|
25
|
+
attributes.inject({}) do |hash, (key, value)|
|
26
|
+
value = value[index] if value.kind_of? Array
|
27
|
+
hash.merge key => value
|
28
|
+
end
|
29
|
+
end
|
30
|
+
|
31
|
+
end
|
@@ -0,0 +1,107 @@
|
|
1
|
+
class Hash
|
2
|
+
|
3
|
+
# Returns the values from the soap:Body element or an empty Hash in case the soap:Body tag could
|
4
|
+
# not be found.
|
5
|
+
def find_soap_body
|
6
|
+
envelope = self[keys.first] || {}
|
7
|
+
body_key = envelope.keys.find { |key| /.+:Body/ =~ key } rescue nil
|
8
|
+
body_key ? envelope[body_key].map_soap_response : {}
|
9
|
+
end
|
10
|
+
|
11
|
+
# Translates the Hash into SOAP request compatible XML.
|
12
|
+
#
|
13
|
+
# { :find_user => { :id => 123, "wsdl:Key" => "api" } }.to_soap_xml
|
14
|
+
# # => "<findUser><id>123</id><wsdl:Key>api</wsdl:Key></findUser>"
|
15
|
+
#
|
16
|
+
# ==== Mapping
|
17
|
+
#
|
18
|
+
# * Hash keys specified as Symbols are converted to lowerCamelCase Strings
|
19
|
+
# * Hash keys specified as Strings are not converted and may contain namespaces
|
20
|
+
# * DateTime values are converted to xs:dateTime Strings
|
21
|
+
# * Objects responding to to_datetime (except Strings) are converted to xs:dateTime Strings
|
22
|
+
# * TrueClass and FalseClass objects are converted to "true" and "false" Strings
|
23
|
+
# * All other objects are expected to be converted to Strings using to_s
|
24
|
+
#
|
25
|
+
# An example:
|
26
|
+
#
|
27
|
+
# { :magic_request => {
|
28
|
+
# :perform_move => true,
|
29
|
+
# "perform_at" => DateTime.new(2010, 11, 22, 11, 22, 33)
|
30
|
+
# }
|
31
|
+
# }.to_soap_xml
|
32
|
+
#
|
33
|
+
# <magicRequest>
|
34
|
+
# <performMove>true</performMove>
|
35
|
+
# <perform_at>2012-06-11T10:42:21</perform_at>
|
36
|
+
# </magicRequest>
|
37
|
+
#
|
38
|
+
# ==== Escaped XML values
|
39
|
+
#
|
40
|
+
# By default, special characters in XML String values are escaped.
|
41
|
+
#
|
42
|
+
# ==== Fixed order of XML tags
|
43
|
+
#
|
44
|
+
# In case your service requires the tags to be in a specific order (parameterOrder), you have two
|
45
|
+
# options. The first is to specify your body as an XML string. The second is to specify the order
|
46
|
+
# through an additional array stored under the +:order!+ key.
|
47
|
+
#
|
48
|
+
# { :name => "Eve", :id => 123, :order! => [:id, :name] }.to_soap_xml
|
49
|
+
# # => "<id>123</id><name>Eve</name>"
|
50
|
+
#
|
51
|
+
# ==== XML attributes
|
52
|
+
#
|
53
|
+
# If you need attributes, you could either go with an XML string or add another hash under the
|
54
|
+
# +:attributes!+ key.
|
55
|
+
#
|
56
|
+
# { :person => "Eve", :attributes! => { :person => { :id => 666 } } }.to_soap_xml
|
57
|
+
# # => '<person id="666">Eve</person>'
|
58
|
+
def to_soap_xml
|
59
|
+
xml = Builder::XmlMarkup.new
|
60
|
+
attributes = delete(:attributes!) || {}
|
61
|
+
|
62
|
+
order.each do |key|
|
63
|
+
attrs = attributes[key] || {}
|
64
|
+
value = self[key]
|
65
|
+
escape_xml = key.to_s[-1, 1] != "!"
|
66
|
+
key = key.to_soap_key
|
67
|
+
|
68
|
+
case value
|
69
|
+
when Array then xml << value.to_soap_xml(key, escape_xml, attrs)
|
70
|
+
when Hash then xml.tag!(key, attrs) { xml << value.to_soap_xml }
|
71
|
+
else xml.tag!(key, attrs) { xml << (escape_xml ? value.to_soap_value : value.to_soap_value!) }
|
72
|
+
end
|
73
|
+
end
|
74
|
+
|
75
|
+
xml.target!
|
76
|
+
end
|
77
|
+
|
78
|
+
# Maps keys and values of a Hash created from SOAP response XML to more convenient Ruby Objects.
|
79
|
+
def map_soap_response
|
80
|
+
inject({}) do |hash, (key, value)|
|
81
|
+
value = case value
|
82
|
+
when Hash then value["xsi:nil"] ? nil : value.map_soap_response
|
83
|
+
when Array then value.map { |val| val.map_soap_response rescue val }
|
84
|
+
when String then value.map_soap_response
|
85
|
+
end
|
86
|
+
|
87
|
+
hash.merge key.strip_namespace.snakecase.to_sym => value
|
88
|
+
end
|
89
|
+
end
|
90
|
+
|
91
|
+
private
|
92
|
+
|
93
|
+
# Deletes and returns an Array of keys stored under the :order! key. Defaults to return the actual
|
94
|
+
# keys of this Hash if no :order! key could be found. Raises an ArgumentError in case the :order!
|
95
|
+
# Array does not match the Hash keys.
|
96
|
+
def order
|
97
|
+
order = delete :order!
|
98
|
+
order = keys unless order.kind_of? Array
|
99
|
+
|
100
|
+
missing, spurious = keys - order, order - keys
|
101
|
+
raise ArgumentError, "Missing elements in :order! #{missing.inspect}" unless missing.empty?
|
102
|
+
raise ArgumentError, "Spurious elements in :order! #{spurious.inspect}" unless spurious.empty?
|
103
|
+
|
104
|
+
order
|
105
|
+
end
|
106
|
+
|
107
|
+
end
|
@@ -0,0 +1,19 @@
|
|
1
|
+
module Net
|
2
|
+
class HTTP
|
3
|
+
|
4
|
+
# Sets the endpoint +address+ and +port+.
|
5
|
+
def endpoint(address, port)
|
6
|
+
@address, @port = address, port
|
7
|
+
end
|
8
|
+
|
9
|
+
# Convenience method for setting SSL client authentication through a Hash of +options+.
|
10
|
+
def ssl_client_auth(options)
|
11
|
+
self.use_ssl = true
|
12
|
+
self.cert = options[:cert] if options[:cert]
|
13
|
+
self.key = options[:key] if options[:key]
|
14
|
+
self.ca_file = options[:ca_file] if options[:ca_file]
|
15
|
+
self.verify_mode = options[:verify_mode] if options[:verify_mode]
|
16
|
+
end
|
17
|
+
|
18
|
+
end
|
19
|
+
end
|
@@ -0,0 +1,16 @@
|
|
1
|
+
class Object
|
2
|
+
|
3
|
+
# Returns +true+ if the Object is nil, false or empty. Implementation from ActiveSupport.
|
4
|
+
def blank?
|
5
|
+
respond_to?(:empty?) ? empty? : !self
|
6
|
+
end unless defined? blank?
|
7
|
+
|
8
|
+
# Returns the Object as a SOAP request compliant value.
|
9
|
+
def to_soap_value
|
10
|
+
return to_s unless respond_to? :to_datetime
|
11
|
+
to_datetime.to_soap_value
|
12
|
+
end
|
13
|
+
|
14
|
+
alias_method :to_soap_value!, :to_soap_value
|
15
|
+
|
16
|
+
end
|
@@ -0,0 +1,69 @@
|
|
1
|
+
class String
|
2
|
+
|
3
|
+
# Returns a random String of a given +length+.
|
4
|
+
def self.random(length = 100)
|
5
|
+
(0...length).map { ("a".."z").to_a[rand(26)] }.join
|
6
|
+
end
|
7
|
+
|
8
|
+
# Returns the String in snake_case.
|
9
|
+
def snakecase
|
10
|
+
str = dup
|
11
|
+
str.gsub! /::/, '/'
|
12
|
+
str.gsub! /([A-Z]+)([A-Z][a-z])/, '\1_\2'
|
13
|
+
str.gsub! /([a-z\d])([A-Z])/, '\1_\2'
|
14
|
+
str.tr! ".", "_"
|
15
|
+
str.tr! "-", "_"
|
16
|
+
str.downcase!
|
17
|
+
str
|
18
|
+
end
|
19
|
+
|
20
|
+
# Returns the String in lowerCamelCase.
|
21
|
+
def lower_camelcase
|
22
|
+
str = dup
|
23
|
+
str.gsub!(/\/(.?)/) { "::#{$1.upcase}" }
|
24
|
+
str.gsub!(/(?:_+|-+)([a-z])/) { $1.upcase }
|
25
|
+
str.gsub!(/(\A|\s)([A-Z])/) { $1 + $2.downcase }
|
26
|
+
str
|
27
|
+
end
|
28
|
+
|
29
|
+
# Returns whether the String starts with a given +prefix+.
|
30
|
+
def starts_with?(prefix)
|
31
|
+
prefix = prefix.to_s
|
32
|
+
self[0, prefix.length] == prefix
|
33
|
+
end unless defined? starts_with?
|
34
|
+
|
35
|
+
# Returns whether the String ends with a given +suffix+.
|
36
|
+
def ends_with?(suffix)
|
37
|
+
suffix = suffix.to_s
|
38
|
+
self[-suffix.length, suffix.length] == suffix
|
39
|
+
end unless defined? ends_with?
|
40
|
+
|
41
|
+
# Returns the String without namespace.
|
42
|
+
def strip_namespace
|
43
|
+
split(":").last
|
44
|
+
end
|
45
|
+
|
46
|
+
# Translates SOAP response values to Ruby Objects.
|
47
|
+
def map_soap_response
|
48
|
+
return DateTime.parse(self) if Savon::SOAP::DateTimeRegexp === self
|
49
|
+
return true if self.strip.downcase == "true"
|
50
|
+
return false if self.strip.downcase == "false"
|
51
|
+
self
|
52
|
+
end
|
53
|
+
|
54
|
+
# Returns the Object as a SOAP request compliant key.
|
55
|
+
def to_soap_key
|
56
|
+
self[-1, 1] == "!" ? chop : self
|
57
|
+
end
|
58
|
+
|
59
|
+
# Returns the String as a SOAP value. Escapes special characters for XML.
|
60
|
+
def to_soap_value
|
61
|
+
CGI.escapeHTML self
|
62
|
+
end
|
63
|
+
|
64
|
+
# Convert the String into a SOAP value without escaping special characters.
|
65
|
+
def to_soap_value!
|
66
|
+
self
|
67
|
+
end
|
68
|
+
|
69
|
+
end
|
data/lib/savon/logger.rb
ADDED
@@ -0,0 +1,56 @@
|
|
1
|
+
module Savon
|
2
|
+
|
3
|
+
# = Savon::Logger
|
4
|
+
#
|
5
|
+
# Savon::Logger can be mixed into classes to provide logging behavior.
|
6
|
+
#
|
7
|
+
# By default, the Logger mixin uses {Ruby's Logger}[http://ruby-doc.org/stdlib/libdoc/logger/rdoc/]
|
8
|
+
# from the standard library, a log level of :debug and is pointing to STDOUT.
|
9
|
+
module Logger
|
10
|
+
|
11
|
+
module ClassMethods
|
12
|
+
|
13
|
+
# Sets whether to log.
|
14
|
+
def log=(log)
|
15
|
+
@log = log
|
16
|
+
end
|
17
|
+
|
18
|
+
# Returns whether to log. Defaults to +true+.
|
19
|
+
def log?
|
20
|
+
@log != false
|
21
|
+
end
|
22
|
+
|
23
|
+
# Sets the logger.
|
24
|
+
def logger=(logger)
|
25
|
+
@logger = logger
|
26
|
+
end
|
27
|
+
|
28
|
+
# Returns the logger. Defaults to an instance of +Logger+ writing to STDOUT.
|
29
|
+
def logger
|
30
|
+
@logger ||= ActiveRecord::Base.logger rescue ::Logger.new STDOUT
|
31
|
+
end
|
32
|
+
|
33
|
+
# Sets the log level.
|
34
|
+
def log_level=(log_level)
|
35
|
+
@log_level = log_level
|
36
|
+
end
|
37
|
+
|
38
|
+
# Returns the log level. Defaults to +debug+.
|
39
|
+
def log_level
|
40
|
+
@log_level ||= :debug
|
41
|
+
end
|
42
|
+
|
43
|
+
end
|
44
|
+
|
45
|
+
# Extends the class including this module with its ClassMethods.
|
46
|
+
def self.included(base)
|
47
|
+
base.extend ClassMethods
|
48
|
+
end
|
49
|
+
|
50
|
+
# Logs a given +message+.
|
51
|
+
def log(message)
|
52
|
+
self.class.logger.send self.class.log_level, message if self.class.log?
|
53
|
+
end
|
54
|
+
|
55
|
+
end
|
56
|
+
end
|
@@ -0,0 +1,138 @@
|
|
1
|
+
module Savon
|
2
|
+
|
3
|
+
# = Savon::Request
|
4
|
+
#
|
5
|
+
# Savon::Request handles both WSDL and SOAP requests.
|
6
|
+
#
|
7
|
+
# == The Net::HTTP object
|
8
|
+
#
|
9
|
+
# You can access the Net::HTTP object used for both WSDL and SOAP requests via:
|
10
|
+
#
|
11
|
+
# client.request.http
|
12
|
+
#
|
13
|
+
# Here's an example of how to set open and read timeouts on the Net::HTTP object.
|
14
|
+
#
|
15
|
+
# client.request.http.open_timeout = 30
|
16
|
+
# client.request.http.read_timeout = 30
|
17
|
+
#
|
18
|
+
# Please refer to the {Net::HTTP documentation}[http://ruby-doc.org/stdlib/libdoc/net/http/rdoc/]
|
19
|
+
# for more information.
|
20
|
+
#
|
21
|
+
# == HTTP basic authentication
|
22
|
+
#
|
23
|
+
# Setting credentials for HTTP basic authentication:
|
24
|
+
#
|
25
|
+
# client.request.basic_auth "username", "password"
|
26
|
+
#
|
27
|
+
# == SSL client authentication
|
28
|
+
#
|
29
|
+
# You can use the methods provided by Net::HTTP to set SSL client authentication or use a shortcut:
|
30
|
+
#
|
31
|
+
# client.request.http.ssl_client_auth(
|
32
|
+
# :cert => OpenSSL::X509::Certificate.new(File.read("client_cert.pem")),
|
33
|
+
# :key => OpenSSL::PKey::RSA.new(File.read("client_key.pem"), "password if one exists"),
|
34
|
+
# :ca_file => "cacert.pem",
|
35
|
+
# :verify_mode => OpenSSL::SSL::VERIFY_PEER
|
36
|
+
# )
|
37
|
+
#
|
38
|
+
# == HTTP headers
|
39
|
+
#
|
40
|
+
# There's an accessor for the Hash of HTTP headers sent with any SOAP call:
|
41
|
+
#
|
42
|
+
# client.request.headers["custom"] = "header"
|
43
|
+
class Request
|
44
|
+
include Logger
|
45
|
+
|
46
|
+
# Content-Types by SOAP version.
|
47
|
+
ContentType = { 1 => "text/xml;charset=UTF-8", 2 => "application/soap+xml;charset=UTF-8" }
|
48
|
+
|
49
|
+
# Expects a WSDL or SOAP +endpoint+ and accepts a custom +proxy+ address.
|
50
|
+
def initialize(endpoint, options = {})
|
51
|
+
@endpoint = URI endpoint
|
52
|
+
@proxy = URI options[:proxy] || ""
|
53
|
+
headers["Accept-encoding"] = "gzip,deflate" if options[:gzip]
|
54
|
+
end
|
55
|
+
|
56
|
+
# Returns the endpoint URI.
|
57
|
+
attr_reader :endpoint
|
58
|
+
|
59
|
+
# Returns the proxy URI.
|
60
|
+
attr_reader :proxy
|
61
|
+
|
62
|
+
# Returns the HTTP headers for a SOAP request.
|
63
|
+
def headers
|
64
|
+
@headers ||= {}
|
65
|
+
end
|
66
|
+
|
67
|
+
# Sets the HTTP headers for a SOAP request.
|
68
|
+
def headers=(headers)
|
69
|
+
@headers = headers if headers.kind_of? Hash
|
70
|
+
end
|
71
|
+
|
72
|
+
# Sets the +username+ and +password+ for HTTP basic authentication.
|
73
|
+
def basic_auth(username, password)
|
74
|
+
@basic_auth = [username, password]
|
75
|
+
end
|
76
|
+
|
77
|
+
# Retrieves WSDL document and returns the Net::HTTP response.
|
78
|
+
def wsdl
|
79
|
+
log "Retrieving WSDL from: #{@endpoint}"
|
80
|
+
http.endpoint @endpoint.host, @endpoint.port
|
81
|
+
http.use_ssl = @endpoint.ssl?
|
82
|
+
http.start { |h| h.request request(:wsdl) }
|
83
|
+
end
|
84
|
+
|
85
|
+
# Executes a SOAP request using a given Savon::SOAP instance and returns the Net::HTTP response.
|
86
|
+
def soap(soap)
|
87
|
+
@soap = soap
|
88
|
+
http.endpoint @soap.endpoint.host, @soap.endpoint.port
|
89
|
+
http.use_ssl = @soap.endpoint.ssl?
|
90
|
+
|
91
|
+
log_request
|
92
|
+
@response = http.start do |h|
|
93
|
+
h.request request(:soap) { |request| request.body = @soap.to_xml }
|
94
|
+
end
|
95
|
+
log_response
|
96
|
+
@response
|
97
|
+
end
|
98
|
+
|
99
|
+
# Returns the Net::HTTP object.
|
100
|
+
def http
|
101
|
+
@http ||= Net::HTTP::Proxy(@proxy.host, @proxy.port).new @endpoint.host, @endpoint.port
|
102
|
+
end
|
103
|
+
|
104
|
+
private
|
105
|
+
|
106
|
+
# Logs the SOAP request.
|
107
|
+
def log_request
|
108
|
+
log "SOAP request: #{@soap.endpoint}"
|
109
|
+
log soap_headers.merge(headers).map { |key, value| "#{key}: #{value}" }.join(", ")
|
110
|
+
log @soap.to_xml
|
111
|
+
end
|
112
|
+
|
113
|
+
# Logs the SOAP response.
|
114
|
+
def log_response
|
115
|
+
log "SOAP response (status #{@response.code}):"
|
116
|
+
log @response.body
|
117
|
+
end
|
118
|
+
|
119
|
+
# Returns a Net::HTTP request for a given +type+. Yields the request to an optional block.
|
120
|
+
def request(type)
|
121
|
+
request = case type
|
122
|
+
when :wsdl then Net::HTTP::Get.new @endpoint.request_uri
|
123
|
+
when :soap then Net::HTTP::Post.new @soap.endpoint.request_uri, soap_headers.merge(headers)
|
124
|
+
end
|
125
|
+
|
126
|
+
request.basic_auth(*@basic_auth) if @basic_auth
|
127
|
+
yield request if block_given?
|
128
|
+
request
|
129
|
+
end
|
130
|
+
|
131
|
+
# Returns a Hash containing the SOAP headers for an HTTP request.
|
132
|
+
def soap_headers
|
133
|
+
{ "Content-Type" => ContentType[@soap.version], "SOAPAction" => @soap.action }
|
134
|
+
end
|
135
|
+
|
136
|
+
end
|
137
|
+
end
|
138
|
+
|