rforce 0.2.1 → 0.3
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/History.txt +5 -0
- data/Manifest.txt +8 -0
- data/README.txt +5 -4
- data/Rakefile +3 -2
- data/lib/rforce.rb +32 -280
- data/lib/rforce/binding.rb +202 -0
- data/lib/rforce/soap_pullable.rb +87 -0
- data/lib/rforce/soap_response_expat.rb +35 -0
- data/lib/rforce/soap_response_hpricot.rb +70 -0
- data/lib/rforce/soap_response_rexml.rb +34 -0
- data/lib/rforce/version.rb +3 -0
- data/spec/rforce_spec.rb +47 -13
- data/spec/soap-response.xml +5928 -0
- data/spec/spec_helper.rb +62 -0
- data/tasks/rspec.rake +1 -1
- data/tasks/timing.rake +21 -0
- metadata +22 -4
data/History.txt
CHANGED
data/Manifest.txt
CHANGED
@@ -3,7 +3,15 @@ Manifest.txt
|
|
3
3
|
README.txt
|
4
4
|
Rakefile
|
5
5
|
lib/rforce.rb
|
6
|
+
lib/rforce/binding.rb
|
7
|
+
lib/rforce/soap_pullable.rb
|
8
|
+
lib/rforce/soap_response_expat.rb
|
9
|
+
lib/rforce/soap_response_hpricot.rb
|
10
|
+
lib/rforce/soap_response_rexml.rb
|
11
|
+
lib/rforce/version.rb
|
6
12
|
spec/rforce_spec.rb
|
13
|
+
spec/soap-response.xml
|
7
14
|
spec/spec.opts
|
8
15
|
spec/spec_helper.rb
|
9
16
|
tasks/rspec.rake
|
17
|
+
tasks/timing.rake
|
data/README.txt
CHANGED
@@ -2,7 +2,8 @@
|
|
2
2
|
|
3
3
|
* http://rforce.rubyforge.org
|
4
4
|
* http://rubyforge.org/projects/rforce
|
5
|
-
* http://
|
5
|
+
* http://bitbucket.org/undees/rforce
|
6
|
+
* http://github.com/undees/rforce
|
6
7
|
|
7
8
|
== DESCRIPTION:
|
8
9
|
|
@@ -15,7 +16,7 @@ Rather than enforcing adherence to the sforce.com schema, RForce assumes you are
|
|
15
16
|
== SYNOPSIS:
|
16
17
|
|
17
18
|
binding = RForce::Binding.new \
|
18
|
-
'https://
|
19
|
+
'https://www.salesforce.com/services/Soap/u/10.0'
|
19
20
|
|
20
21
|
binding.login \
|
21
22
|
'email', 'password_with_token'
|
@@ -52,7 +53,7 @@ Rather than enforcing adherence to the sforce.com schema, RForce assumes you are
|
|
52
53
|
|
53
54
|
== LICENSE:
|
54
55
|
|
55
|
-
Copyright (c) 2005-
|
56
|
+
Copyright (c) 2005-2009 Ian Dees and contributors
|
56
57
|
|
57
58
|
Permission is hereby granted, free of charge, to any person obtaining
|
58
59
|
a copy of this software and associated documentation files (the
|
@@ -71,4 +72,4 @@ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
|
|
71
72
|
IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
|
72
73
|
CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
|
73
74
|
TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
|
74
|
-
SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
75
|
+
SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/Rakefile
CHANGED
@@ -1,12 +1,13 @@
|
|
1
1
|
# -*- ruby -*-
|
2
2
|
|
3
|
+
$:.unshift './lib'
|
3
4
|
require 'rubygems'
|
4
5
|
require 'hoe'
|
5
|
-
require '
|
6
|
+
require 'rforce/version'
|
6
7
|
|
7
8
|
Hoe.new('rforce', RForce::VERSION) do |p|
|
8
9
|
p.developer('Ian Dees', 'undees@gmail.com')
|
9
|
-
p.extra_deps = [['builder', '>= 2.0.0']]
|
10
|
+
p.extra_deps = [['builder', '>= 2.0.0'], ['facets', '>= 2.4']]
|
10
11
|
p.extra_dev_deps = [['hoe', '>= 1.7.0']]
|
11
12
|
p.remote_rdoc_dir = ''
|
12
13
|
end
|
data/lib/rforce.rb
CHANGED
@@ -1,6 +1,5 @@
|
|
1
1
|
=begin
|
2
|
-
|
3
|
-
Copyright (c) 2005-2008 Ian Dees
|
2
|
+
Copyright (c) 2005-2009 Ian Dees and contributors
|
4
3
|
|
5
4
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
6
5
|
of this software and associated documentation files (the "Software"), to deal
|
@@ -29,7 +28,7 @@ SOFTWARE.
|
|
29
28
|
#
|
30
29
|
# Example:
|
31
30
|
#
|
32
|
-
# binding = RForce::Binding.new '
|
31
|
+
# binding = RForce::Binding.new 'https://www.salesforce.com/services/Soap/u/10.0'
|
33
32
|
# binding.login 'username', 'password'
|
34
33
|
# answer = binding.search(
|
35
34
|
# :searchString =>
|
@@ -52,83 +51,35 @@ require 'net/https'
|
|
52
51
|
require 'uri'
|
53
52
|
require 'zlib'
|
54
53
|
require 'stringio'
|
55
|
-
|
56
|
-
require 'rexml/xpath'
|
54
|
+
|
57
55
|
require 'rubygems'
|
56
|
+
|
58
57
|
gem 'builder', '>= 2.0.0'
|
59
58
|
require 'builder'
|
60
59
|
|
60
|
+
gem 'facets', '>= 2.4'
|
61
|
+
require 'facets/openhash'
|
61
62
|
|
62
|
-
|
63
|
-
|
64
|
-
|
65
|
-
|
66
|
-
#to supplement the traditional way of indexing: hash[key]
|
67
|
-
module FlashHash
|
68
|
-
def method_missing(method, *args)
|
69
|
-
self[method]
|
70
|
-
end
|
71
|
-
end
|
72
|
-
|
73
|
-
#Turns an XML response from the server into a Ruby
|
74
|
-
#object whose methods correspond to nested XML elements.
|
75
|
-
class SoapResponse
|
76
|
-
include FlashHash
|
77
|
-
|
78
|
-
#Parses an XML string into structured data.
|
79
|
-
def initialize(content)
|
80
|
-
document = REXML::Document.new content
|
81
|
-
node = REXML::XPath.first document, '//soapenv:Body'
|
82
|
-
@parsed = SoapResponse.parse node
|
83
|
-
end
|
84
|
-
|
85
|
-
#Allows this object to act like a hash (and therefore
|
86
|
-
#as a FlashHash via the include above).
|
87
|
-
def [](symbol)
|
88
|
-
@parsed[symbol]
|
89
|
-
end
|
90
|
-
|
91
|
-
#Digests an XML DOM node into nested Ruby types.
|
92
|
-
def SoapResponse.parse(node)
|
93
|
-
#Convert text nodes into simple strings.
|
94
|
-
return node.text unless node.has_elements?
|
95
|
-
|
96
|
-
#Convert nodes with children into FlashHashes.
|
97
|
-
elements = {}
|
98
|
-
class << elements
|
99
|
-
include FlashHash
|
100
|
-
end
|
101
|
-
|
102
|
-
#Add all the element's children to the hash.
|
103
|
-
node.each_element do |e|
|
104
|
-
name = e.name.to_sym
|
105
|
-
|
106
|
-
case elements[name]
|
107
|
-
#The most common case: unique child element tags.
|
108
|
-
when NilClass: elements[name] = parse(e)
|
109
|
-
|
110
|
-
#Non-unique child elements become arrays:
|
63
|
+
require 'rforce/binding'
|
64
|
+
require 'rforce/soap_response_rexml'
|
65
|
+
begin; require 'rforce/soap_response_hpricot'; rescue LoadError; end
|
66
|
+
begin; require 'rforce/soap_response_expat'; rescue LoadError; end
|
111
67
|
|
112
|
-
#We've already created the array: just
|
113
|
-
#add the element.
|
114
|
-
when Array: elements[name] << parse(e)
|
115
68
|
|
116
|
-
|
117
|
-
|
118
|
-
|
119
|
-
|
120
|
-
elements[name] = [elements[name]]
|
121
|
-
elements[name] << parse(e)
|
122
|
-
end
|
123
|
-
end
|
124
|
-
|
125
|
-
return elements
|
126
|
-
end
|
69
|
+
module RForce
|
70
|
+
# Use the fastest XML parser available.
|
71
|
+
def self.parser(name)
|
72
|
+
RForce.const_get(name) rescue nil
|
127
73
|
end
|
128
|
-
|
129
|
-
|
74
|
+
|
75
|
+
SoapResponse =
|
76
|
+
parser(:SoapResponseExpat) ||
|
77
|
+
parser(:SoapResponseHpricot) ||
|
78
|
+
SoapResponseRexml
|
79
|
+
|
80
|
+
# Expand Ruby data structures into XML.
|
130
81
|
def expand(builder, args, xmlns = nil)
|
131
|
-
#Nest arrays: [:a, 1, :b, 2] => [[:a, 1], [:b, 2]]
|
82
|
+
# Nest arrays: [:a, 1, :b, 2] => [[:a, 1], [:b, 2]]
|
132
83
|
if (args.class == Array)
|
133
84
|
args.each_index{|i| args[i, 2] = [args[i, 2]]}
|
134
85
|
end
|
@@ -136,228 +87,29 @@ module RForce
|
|
136
87
|
args.each do |key, value|
|
137
88
|
attributes = xmlns ? {:xmlns => xmlns} : {}
|
138
89
|
|
139
|
-
#If the XML tag requires attributes,
|
140
|
-
#the tag name will contain a space
|
141
|
-
#followed by a string representation
|
142
|
-
#of a hash of attributes.
|
90
|
+
# If the XML tag requires attributes,
|
91
|
+
# the tag name will contain a space
|
92
|
+
# followed by a string representation
|
93
|
+
# of a hash of attributes.
|
143
94
|
#
|
144
|
-
#e.g. 'sObject {"xsi:type" => "Opportunity"}'
|
145
|
-
#becomes <sObject xsi:type="Opportunity>...</sObject>
|
95
|
+
# e.g. 'sObject {"xsi:type" => "Opportunity"}'
|
96
|
+
# becomes <sObject xsi:type="Opportunity>...</sObject>
|
146
97
|
if key.is_a? String
|
147
98
|
key, modifier = key.split(' ', 2)
|
148
99
|
|
149
100
|
attributes.merge!(eval(modifier)) if modifier
|
150
101
|
end
|
151
102
|
|
152
|
-
#Create an XML element and fill it with this
|
153
|
-
#value's sub-items.
|
103
|
+
# Create an XML element and fill it with this
|
104
|
+
# value's sub-items.
|
154
105
|
case value
|
155
|
-
when Hash, Array
|
106
|
+
when Hash, Array then
|
156
107
|
builder.tag!(key, attributes) do expand builder, value; end
|
157
108
|
|
158
|
-
when String
|
109
|
+
when String then
|
159
110
|
builder.tag!(key, attributes) { builder.text! value }
|
160
111
|
end
|
161
112
|
end
|
162
113
|
end
|
163
114
|
|
164
|
-
#Implements the connection to the SalesForce server.
|
165
|
-
class Binding
|
166
|
-
include RForce
|
167
|
-
|
168
|
-
DEFAULT_BATCH_SIZE = 10
|
169
|
-
attr_accessor :batch_size, :url, :assignment_rule_id, :use_default_rule, :update_mru, :client_id, :trigger_user_email,
|
170
|
-
:trigger_other_email, :trigger_auto_response_email
|
171
|
-
|
172
|
-
#Fill in the guts of this typical SOAP envelope
|
173
|
-
#with the session ID and the body of the SOAP request.
|
174
|
-
Envelope = <<-HERE
|
175
|
-
<?xml version="1.0" encoding="utf-8" ?>
|
176
|
-
<soap:Envelope xmlns:xsd="http://www.w3.org/2001/XMLSchema"
|
177
|
-
xmlns:soap="http://schemas.xmlsoap.org/soap/envelope/"
|
178
|
-
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
179
|
-
xmlns:partner="urn:partner.soap.sforce.com">
|
180
|
-
xmlns:spartner="urn:sobject.partner.soap.sforce.com">
|
181
|
-
<soap:Header>
|
182
|
-
<partner:SessionHeader soap:mustUnderstand='1'>
|
183
|
-
<partner:sessionId>%s</partner:sessionId>
|
184
|
-
</partner:SessionHeader>
|
185
|
-
<partner:QueryOptions soap:mustUnderstand='1'>
|
186
|
-
<partner:batchSize>%d</partner:batchSize>
|
187
|
-
</partner:QueryOptions>
|
188
|
-
%s
|
189
|
-
</soap:Header>
|
190
|
-
<soap:Body>
|
191
|
-
%s
|
192
|
-
</soap:Body>
|
193
|
-
</soap:Envelope>
|
194
|
-
HERE
|
195
|
-
|
196
|
-
AssignmentRuleHeaderUsingRuleId = '<partner:AssignmentRuleHeader soap:mustUnderstand="1"><partner:assignmentRuleId>%s</partner:assignmentRuleId></partner:AssignmentRuleHeader>'
|
197
|
-
AssignmentRuleHeaderUsingDefaultRule = '<partner:AssignmentRuleHeader soap:mustUnderstand="1"><partner:useDefaultRule>true</partner:useDefaultRule></partner:AssignmentRuleHeader>'
|
198
|
-
MruHeader = '<partner:MruHeader soap:mustUnderstand="1"><partner:updateMru>true</partner:updateMru></partner:MruHeader>'
|
199
|
-
ClientIdHeader = '<partner:CallOptions soap:mustUnderstand="1"><partner:client>%s</partner:client></partner:CallOptions>'
|
200
|
-
|
201
|
-
#Connect to the server securely.
|
202
|
-
def initialize(url, sid = nil)
|
203
|
-
init_server(url)
|
204
|
-
|
205
|
-
@session_id = sid
|
206
|
-
@batch_size = DEFAULT_BATCH_SIZE
|
207
|
-
end
|
208
|
-
|
209
|
-
|
210
|
-
def show_debug
|
211
|
-
ENV['SHOWSOAP'] == 'true'
|
212
|
-
end
|
213
|
-
|
214
|
-
|
215
|
-
def init_server(url)
|
216
|
-
@url = URI.parse(url)
|
217
|
-
@server = Net::HTTP.new(@url.host, @url.port)
|
218
|
-
@server.use_ssl = @url.scheme == 'https'
|
219
|
-
@server.verify_mode = OpenSSL::SSL::VERIFY_NONE
|
220
|
-
|
221
|
-
# run ruby with -d or env variable SHOWSOAP=true to see SOAP wiredumps.
|
222
|
-
@server.set_debug_output $stderr if show_debug
|
223
|
-
end
|
224
|
-
|
225
|
-
|
226
|
-
#Log in to the server and remember the session ID
|
227
|
-
#returned to us by SalesForce.
|
228
|
-
def login(user, password)
|
229
|
-
@user = user
|
230
|
-
@password = password
|
231
|
-
|
232
|
-
response = call_remote(:login, [:username, user, :password, password])
|
233
|
-
|
234
|
-
raise "Incorrect user name / password [#{response.fault}]" unless response.loginResponse
|
235
|
-
|
236
|
-
result = response[:loginResponse][:result]
|
237
|
-
@session_id = result[:sessionId]
|
238
|
-
|
239
|
-
init_server(result[:serverUrl])
|
240
|
-
|
241
|
-
response
|
242
|
-
end
|
243
|
-
|
244
|
-
|
245
|
-
#Call a method on the remote server. Arguments can be
|
246
|
-
#a hash or (if order is important) an array of alternating
|
247
|
-
#keys and values.
|
248
|
-
def call_remote(method, args)
|
249
|
-
#Create XML text from the arguments.
|
250
|
-
expanded = ''
|
251
|
-
@builder = Builder::XmlMarkup.new(:target => expanded)
|
252
|
-
expand(@builder, {method => args}, 'urn:partner.soap.sforce.com')
|
253
|
-
|
254
|
-
extra_headers = ""
|
255
|
-
extra_headers << (AssignmentRuleHeaderUsingRuleId % assignment_rule_id) if assignment_rule_id
|
256
|
-
extra_headers << AssignmentRuleHeaderUsingDefaultRule if use_default_rule
|
257
|
-
extra_headers << MruHeader if update_mru
|
258
|
-
extra_headers << (ClientIdHeader % client_id) if client_id
|
259
|
-
|
260
|
-
if trigger_user_email or trigger_other_email or trigger_auto_response_email
|
261
|
-
extra_headers << '<partner:EmailHeader soap:mustUnderstand="1">'
|
262
|
-
|
263
|
-
extra_headers << '<partner:triggerUserEmail>true</partner:triggerUserEmail>' if trigger_user_email
|
264
|
-
extra_headers << '<partner:triggerOtherEmail>true</partner:triggerOtherEmail>' if trigger_other_email
|
265
|
-
extra_headers << '<partner:triggerAutoResponseEmail>true</partner:triggerAutoResponseEmail>' if trigger_auto_response_email
|
266
|
-
|
267
|
-
extra_headers << '</partner:EmailHeader>'
|
268
|
-
end
|
269
|
-
|
270
|
-
#Fill in the blanks of the SOAP envelope with our
|
271
|
-
#session ID and the expanded XML of our request.
|
272
|
-
request = (Envelope % [@session_id, @batch_size, extra_headers, expanded])
|
273
|
-
|
274
|
-
# reset the batch size for the next request
|
275
|
-
@batch_size = DEFAULT_BATCH_SIZE
|
276
|
-
|
277
|
-
# gzip request
|
278
|
-
request = encode(request)
|
279
|
-
|
280
|
-
headers = {
|
281
|
-
'Connection' => 'Keep-Alive',
|
282
|
-
'Content-Type' => 'text/xml',
|
283
|
-
'SOAPAction' => '""',
|
284
|
-
'User-Agent' => 'activesalesforce rforce/1.0'
|
285
|
-
}
|
286
|
-
|
287
|
-
unless show_debug
|
288
|
-
headers['Accept-Encoding'] = 'gzip'
|
289
|
-
headers['Content-Encoding'] = 'gzip'
|
290
|
-
end
|
291
|
-
|
292
|
-
#Send the request to the server and read the response.
|
293
|
-
response = @server.post2(@url.path, request.lstrip, headers)
|
294
|
-
|
295
|
-
# decode if we have encoding
|
296
|
-
content = decode(response)
|
297
|
-
|
298
|
-
# Check to see if INVALID_SESSION_ID was raised and try to relogin in
|
299
|
-
if method != :login and @session_id and content =~ /sf:INVALID_SESSION_ID/
|
300
|
-
login(@user, @password)
|
301
|
-
|
302
|
-
# repackage and rencode request with the new session id
|
303
|
-
request = (Envelope % [@session_id, @batch_size, extra_headers, expanded])
|
304
|
-
request = encode(request)
|
305
|
-
|
306
|
-
#Send the request to the server and read the response.
|
307
|
-
response = @server.post2(@url.path, request.lstrip, headers)
|
308
|
-
|
309
|
-
content = decode(response)
|
310
|
-
end
|
311
|
-
|
312
|
-
SoapResponse.new(content)
|
313
|
-
end
|
314
|
-
|
315
|
-
|
316
|
-
# decode gzip
|
317
|
-
def decode(response)
|
318
|
-
encoding = response['Content-Encoding']
|
319
|
-
|
320
|
-
# return body if no encoding
|
321
|
-
if !encoding then return response.body end
|
322
|
-
|
323
|
-
# decode gzip
|
324
|
-
case encoding.strip
|
325
|
-
when 'gzip':
|
326
|
-
begin
|
327
|
-
gzr = Zlib::GzipReader.new(StringIO.new(response.body))
|
328
|
-
decoded = gzr.read
|
329
|
-
ensure
|
330
|
-
gzr.close
|
331
|
-
end
|
332
|
-
decoded
|
333
|
-
else
|
334
|
-
response.body
|
335
|
-
end
|
336
|
-
end
|
337
|
-
|
338
|
-
|
339
|
-
# encode gzip
|
340
|
-
def encode(request)
|
341
|
-
return request if show_debug
|
342
|
-
|
343
|
-
begin
|
344
|
-
ostream = StringIO.new
|
345
|
-
gzw = Zlib::GzipWriter.new(ostream)
|
346
|
-
gzw.write(request)
|
347
|
-
ostream.string
|
348
|
-
ensure
|
349
|
-
gzw.close
|
350
|
-
end
|
351
|
-
end
|
352
|
-
|
353
|
-
|
354
|
-
#Turns method calls on this object into remote SOAP calls.
|
355
|
-
def method_missing(method, *args)
|
356
|
-
unless args.size == 1 && [Hash, Array].include?(args[0].class)
|
357
|
-
raise 'Expected 1 Hash or Array argument'
|
358
|
-
end
|
359
|
-
|
360
|
-
call_remote method, args[0]
|
361
|
-
end
|
362
|
-
end
|
363
115
|
end
|
@@ -0,0 +1,202 @@
|
|
1
|
+
module RForce
|
2
|
+
# Implements the connection to the SalesForce server.
|
3
|
+
class Binding
|
4
|
+
include RForce
|
5
|
+
|
6
|
+
DEFAULT_BATCH_SIZE = 10
|
7
|
+
attr_accessor :batch_size, :url, :assignment_rule_id, :use_default_rule, :update_mru, :client_id, :trigger_user_email,
|
8
|
+
:trigger_other_email, :trigger_auto_response_email
|
9
|
+
|
10
|
+
# Fill in the guts of this typical SOAP envelope
|
11
|
+
# with the session ID and the body of the SOAP request.
|
12
|
+
Envelope = <<-HERE
|
13
|
+
<?xml version="1.0" encoding="utf-8" ?>
|
14
|
+
<soap:Envelope xmlns:xsd="http://www.w3.org/2001/XMLSchema"
|
15
|
+
xmlns:soap="http://schemas.xmlsoap.org/soap/envelope/"
|
16
|
+
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
17
|
+
xmlns:partner="urn:partner.soap.sforce.com">
|
18
|
+
xmlns:spartner="urn:sobject.partner.soap.sforce.com">
|
19
|
+
<soap:Header>
|
20
|
+
<partner:SessionHeader soap:mustUnderstand='1'>
|
21
|
+
<partner:sessionId>%s</partner:sessionId>
|
22
|
+
</partner:SessionHeader>
|
23
|
+
<partner:QueryOptions soap:mustUnderstand='1'>
|
24
|
+
<partner:batchSize>%d</partner:batchSize>
|
25
|
+
</partner:QueryOptions>
|
26
|
+
%s
|
27
|
+
</soap:Header>
|
28
|
+
<soap:Body>
|
29
|
+
%s
|
30
|
+
</soap:Body>
|
31
|
+
</soap:Envelope>
|
32
|
+
HERE
|
33
|
+
|
34
|
+
AssignmentRuleHeaderUsingRuleId = '<partner:AssignmentRuleHeader soap:mustUnderstand="1"><partner:assignmentRuleId>%s</partner:assignmentRuleId></partner:AssignmentRuleHeader>'
|
35
|
+
AssignmentRuleHeaderUsingDefaultRule = '<partner:AssignmentRuleHeader soap:mustUnderstand="1"><partner:useDefaultRule>true</partner:useDefaultRule></partner:AssignmentRuleHeader>'
|
36
|
+
MruHeader = '<partner:MruHeader soap:mustUnderstand="1"><partner:updateMru>true</partner:updateMru></partner:MruHeader>'
|
37
|
+
ClientIdHeader = '<partner:CallOptions soap:mustUnderstand="1"><partner:client>%s</partner:client></partner:CallOptions>'
|
38
|
+
|
39
|
+
# Connect to the server securely.
|
40
|
+
def initialize(url, sid = nil)
|
41
|
+
init_server(url)
|
42
|
+
|
43
|
+
@session_id = sid
|
44
|
+
@batch_size = DEFAULT_BATCH_SIZE
|
45
|
+
end
|
46
|
+
|
47
|
+
|
48
|
+
def show_debug
|
49
|
+
ENV['SHOWSOAP'] == 'true'
|
50
|
+
end
|
51
|
+
|
52
|
+
|
53
|
+
def init_server(url)
|
54
|
+
@url = URI.parse(url)
|
55
|
+
@server = Net::HTTP.new(@url.host, @url.port)
|
56
|
+
@server.use_ssl = @url.scheme == 'https'
|
57
|
+
@server.verify_mode = OpenSSL::SSL::VERIFY_NONE
|
58
|
+
|
59
|
+
# run ruby with -d or env variable SHOWSOAP=true to see SOAP wiredumps.
|
60
|
+
@server.set_debug_output $stderr if show_debug
|
61
|
+
end
|
62
|
+
|
63
|
+
|
64
|
+
# Log in to the server and remember the session ID
|
65
|
+
# returned to us by SalesForce.
|
66
|
+
def login(user, password)
|
67
|
+
@user = user
|
68
|
+
@password = password
|
69
|
+
|
70
|
+
response = call_remote(:login, [:username, user, :password, password])
|
71
|
+
|
72
|
+
raise "Incorrect user name / password [#{response.fault}]" unless response.loginResponse
|
73
|
+
|
74
|
+
result = response[:loginResponse][:result]
|
75
|
+
@session_id = result[:sessionId]
|
76
|
+
|
77
|
+
init_server(result[:serverUrl])
|
78
|
+
|
79
|
+
response
|
80
|
+
end
|
81
|
+
|
82
|
+
|
83
|
+
# Call a method on the remote server. Arguments can be
|
84
|
+
# a hash or (if order is important) an array of alternating
|
85
|
+
# keys and values.
|
86
|
+
def call_remote(method, args)
|
87
|
+
# Create XML text from the arguments.
|
88
|
+
expanded = ''
|
89
|
+
@builder = Builder::XmlMarkup.new(:target => expanded)
|
90
|
+
expand(@builder, {method => args}, 'urn:partner.soap.sforce.com')
|
91
|
+
|
92
|
+
extra_headers = ""
|
93
|
+
extra_headers << (AssignmentRuleHeaderUsingRuleId % assignment_rule_id) if assignment_rule_id
|
94
|
+
extra_headers << AssignmentRuleHeaderUsingDefaultRule if use_default_rule
|
95
|
+
extra_headers << MruHeader if update_mru
|
96
|
+
extra_headers << (ClientIdHeader % client_id) if client_id
|
97
|
+
|
98
|
+
if trigger_user_email or trigger_other_email or trigger_auto_response_email
|
99
|
+
extra_headers << '<partner:EmailHeader soap:mustUnderstand="1">'
|
100
|
+
|
101
|
+
extra_headers << '<partner:triggerUserEmail>true</partner:triggerUserEmail>' if trigger_user_email
|
102
|
+
extra_headers << '<partner:triggerOtherEmail>true</partner:triggerOtherEmail>' if trigger_other_email
|
103
|
+
extra_headers << '<partner:triggerAutoResponseEmail>true</partner:triggerAutoResponseEmail>' if trigger_auto_response_email
|
104
|
+
|
105
|
+
extra_headers << '</partner:EmailHeader>'
|
106
|
+
end
|
107
|
+
|
108
|
+
# Fill in the blanks of the SOAP envelope with our
|
109
|
+
# session ID and the expanded XML of our request.
|
110
|
+
request = (Envelope % [@session_id, @batch_size, extra_headers, expanded])
|
111
|
+
|
112
|
+
# reset the batch size for the next request
|
113
|
+
@batch_size = DEFAULT_BATCH_SIZE
|
114
|
+
|
115
|
+
# gzip request
|
116
|
+
request = encode(request)
|
117
|
+
|
118
|
+
headers = {
|
119
|
+
'Connection' => 'Keep-Alive',
|
120
|
+
'Content-Type' => 'text/xml',
|
121
|
+
'SOAPAction' => '""',
|
122
|
+
'User-Agent' => 'activesalesforce rforce/1.0'
|
123
|
+
}
|
124
|
+
|
125
|
+
unless show_debug
|
126
|
+
headers['Accept-Encoding'] = 'gzip'
|
127
|
+
headers['Content-Encoding'] = 'gzip'
|
128
|
+
end
|
129
|
+
|
130
|
+
# Send the request to the server and read the response.
|
131
|
+
response = @server.post2(@url.path, request.lstrip, headers)
|
132
|
+
|
133
|
+
# decode if we have encoding
|
134
|
+
content = decode(response)
|
135
|
+
|
136
|
+
# Check to see if INVALID_SESSION_ID was raised and try to relogin in
|
137
|
+
if method != :login and @session_id and content =~ /sf:INVALID_SESSION_ID/
|
138
|
+
login(@user, @password)
|
139
|
+
|
140
|
+
# repackage and rencode request with the new session id
|
141
|
+
request = (Envelope % [@session_id, @batch_size, extra_headers, expanded])
|
142
|
+
request = encode(request)
|
143
|
+
|
144
|
+
# Send the request to the server and read the response.
|
145
|
+
response = @server.post2(@url.path, request.lstrip, headers)
|
146
|
+
|
147
|
+
content = decode(response)
|
148
|
+
end
|
149
|
+
|
150
|
+
SoapResponse.new(content).parse
|
151
|
+
end
|
152
|
+
|
153
|
+
|
154
|
+
# decode gzip
|
155
|
+
def decode(response)
|
156
|
+
encoding = response['Content-Encoding']
|
157
|
+
|
158
|
+
# return body if no encoding
|
159
|
+
if !encoding then return response.body end
|
160
|
+
|
161
|
+
# decode gzip
|
162
|
+
case encoding.strip
|
163
|
+
when 'gzip' then
|
164
|
+
begin
|
165
|
+
gzr = Zlib::GzipReader.new(StringIO.new(response.body))
|
166
|
+
decoded = gzr.read
|
167
|
+
ensure
|
168
|
+
gzr.close
|
169
|
+
end
|
170
|
+
decoded
|
171
|
+
else
|
172
|
+
response.body
|
173
|
+
end
|
174
|
+
end
|
175
|
+
|
176
|
+
|
177
|
+
# encode gzip
|
178
|
+
def encode(request)
|
179
|
+
return request if show_debug
|
180
|
+
|
181
|
+
begin
|
182
|
+
ostream = StringIO.new
|
183
|
+
gzw = Zlib::GzipWriter.new(ostream)
|
184
|
+
gzw.write(request)
|
185
|
+
ostream.string
|
186
|
+
ensure
|
187
|
+
gzw.close
|
188
|
+
end
|
189
|
+
end
|
190
|
+
|
191
|
+
|
192
|
+
# Turns method calls on this object into remote SOAP calls.
|
193
|
+
def method_missing(method, *args)
|
194
|
+
unless args.size == 1 && [Hash, Array].include?(args[0].class)
|
195
|
+
raise 'Expected 1 Hash or Array argument'
|
196
|
+
end
|
197
|
+
|
198
|
+
call_remote method, args[0]
|
199
|
+
end
|
200
|
+
end
|
201
|
+
end
|
202
|
+
|