| commit 20: | bff9c8d03dd8 |
| parent 19: | 0fde0f5f476e |
| branch: | trunk |
Changed (Δ31.4 KB):
Rakefile (2 lines added, 1 lines removed)
lib/rforce.rb (7 lines added, 479 lines removed)
lib/rforce/binding.rb (202 lines added, 0 lines removed)
lib/rforce/flash_hash.rb (9 lines added, 0 lines removed)
lib/rforce/soap_response.rb (61 lines added, 0 lines removed)
lib/rforce/soap_response_expat.rb (143 lines added, 0 lines removed)
lib/rforce/soap_response_hpricot.rb (72 lines added, 0 lines removed)
lib/rforce/version.rb (3 lines added, 0 lines removed)
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') |
Up to file-list lib/rforce.rb:
1 |
1 |
=begin |
2 |
RForce v0.2 |
|
3 |
Copyright (c) 2005-2008 Ian Dees |
|
2 |
RForce v0.3 |
|
3 |
Copyright (c) 2005-2008 Ian Dees and contributors |
|
4 |
4 |
|
5 |
5 |
Permission is hereby granted, free of charge, to any person obtaining a copy |
6 |
6 |
of this software and associated documentation files (the "Software"), to deal |
| … | … | @@ -52,292 +52,19 @@ require 'net/https' |
52 |
52 |
require 'uri' |
53 |
53 |
require 'zlib' |
54 |
54 |
require 'stringio' |
55 |
require 'rexml/document' |
|
56 |
require 'rexml/xpath' |
|
57 |
require 'xml/parser' |
|
58 |
55 |
|
59 |
56 |
require 'rubygems' |
60 |
57 |
|
61 |
58 |
gem 'builder', '>= 2.0.0' |
62 |
59 |
require 'builder' |
63 |
60 |
|
64 |
gem 'hpricot', '>= 0.6' |
|
65 |
require 'hpricot' |
|
66 |
||
61 |
require 'rforce/flash_hash' |
|
62 |
require 'rforce/binding' |
|
63 |
require 'rforce/soap_response' |
|
64 |
require 'rforce/soap_response_expat' rescue nil |
|
65 |
require 'rforce/soap_response_hpricot' rescue nil |
|
67 |
66 |
|
68 |
67 |
module RForce |
69 |
VERSION = '0.2.1' |
|
70 |
||
71 |
#Allows indexing hashes like method calls: hash.key |
|
72 |
#to supplement the traditional way of indexing: hash[key] |
|
73 |
module FlashHash |
|
74 |
def method_missing(method, *args) |
|
75 |
self[method] |
|
76 |
end |
|
77 |
end |
|
78 |
||
79 |
class SoapResponseExpat |
|
80 |
include FlashHash |
|
81 |
||
82 |
attr_reader :hash_time |
|
83 |
||
84 |
SOAP_ENVELOPE = 'soapenv:Envelope' |
|
85 |
||
86 |
def initialize(content) |
|
87 |
@current_value, @stack, @parsed = nil, [], Hash.new |
|
88 |
||
89 |
namespaces = [] |
|
90 |
t = Time.now |
|
91 |
||
92 |
XML::Parser.new.parse( content ) do |type, name, data| |
|
93 |
# We are not interested in keeping the namespace declarations for the tag |
|
94 |
# names so lets deal with that here. |
|
95 |
unless name.nil? |
|
96 |
if name.index( ':' ) |
|
97 |
tag_ns, tag_name = name.split( ':' ) |
|
98 |
tag_name = name unless namespaces.include?( tag_ns ) |
|
99 |
else |
|
100 |
tag_name = name |
|
101 |
end |
|
102 |
end |
|
103 |
||
104 |
case type |
|
105 |
# name = The literal tag name of the element |
|
106 |
# tag_name = The name of the element minus the namespace that gets used |
|
107 |
# as the key in the hash |
|
108 |
# data = The attributes of the element |
|
109 |
when XML::Parser::START_ELEM |
|
110 |
# The attribute of the first element is the soap envelope which has |
|
111 |
# as attributes the allowed namespaces of all the tags so lets grab |
|
112 |
# them here to make sure we can strip them out appropriately since |
|
113 |
# they are not necessary in our hash. |
|
114 |
namespaces = data.keys.map { |k| k.split( ':' )[1] if k.match( 'xmlns:' ) }.compact if name.eql?( SOAP_ENVELOPE ) |
|
115 |
||
116 |
next if tag_name.eql?( SOAP_ENVELOPE ) |
|
117 |
||
118 |
@stack.push( Hash.new ) |
|
119 |
||
120 |
# name = The literal tag name of the element |
|
121 |
# tag_name = The name of the element minus the namespace that gets used |
|
122 |
# as the key in the hash |
|
123 |
# data = The actual data identified by the element |
|
124 |
when XML::Parser::CDATA |
|
125 |
@current_value = data.strip.empty? ? nil : data |
|
126 |
||
127 |
# name = The literal tag name of the element |
|
128 |
# tag_name = The name of the element minus the namespace that gets used |
|
129 |
# as the key in the hash |
|
130 |
# data = nil |
|
131 |
when XML::Parser::END_ELEM |
|
132 |
next if tag_name.eql?( SOAP_ENVELOPE ) |
|
133 |
||
134 |
working_hash = @stack.pop |
|
135 |
||
136 |
# We are either done or working on a certain depth in the current |
|
137 |
# stack. |
|
138 |
if @stack.empty? |
|
139 |
@parsed = working_hash |
|
140 |
break |
|
141 |
else |
|
142 |
index = @stack.size - 1 |
|
143 |
end |
|
144 |
||
145 |
# working_hash and @current_value have a mutually exclusive relationship. |
|
146 |
# If the current element doesn't have a value then it means that there |
|
147 |
# is a nested data structure. In this case then working_hash is populated |
|
148 |
# and @current_value is nil. Conversely, if @current_value has a value |
|
149 |
# then we do not have a nested data sctructure and working_hash will |
|
150 |
# be empty. |
|
151 |
use_value = ( working_hash.empty? ) ? @current_value : working_hash |
|
152 |
||
153 |
if @stack[index].keys.include?( tag_name.to_sym ) |
|
154 |
# This is here to handle the Id value being included twice and thus |
|
155 |
# producing an array. We skip the second instance so the array is |
|
156 |
# not created. |
|
157 |
if tag_name.eql?( 'Id' ) |
|
158 |
# If we don't clear out the current value here, then we introduce |
|
159 |
# a bug if the element after this Id should be null. An example |
|
160 |
# query is the following: |
|
161 |
# |
|
162 |
# Select Id, LeadId, ContactId from CampaignMember... |
|
163 |
# |
|
164 |
# For the members that are Contacts their LeadId will be null. If |
|
165 |
# we fail to clear out this data the resulting hash would be: |
|
166 |
# |
|
167 |
# { ... :records => [ |
|
168 |
# { :Id => '00v50000008pfHrAAI', :LeadId => '00v50000008pfHrAAI', :ContactId => '0035000000KwjIuAAJ' }, |
|
169 |
# { :Id => '00v50000008pfHsAAI', :LeadId => '00v50000008pfHsAAI', :ContactId => '0035000000KwjIvAAJ' }, |
|
170 |
# } |
|
171 |
# |
|
172 |
# Now you see the problem. When the @current_value = nil is present |
|
173 |
# we get the following: |
|
174 |
# |
|
175 |
# { ... :records => [ |
|
176 |
# { :Id => '00v50000008pfHrAAI', :LeadId => nil, :ContactId => '0035000000KwjIuAAJ' }, |
|
177 |
# { :Id => '00v50000008pfHsAAI', :LeadId => nil, :ContactId => '0035000000KwjIvAAJ' }, |
|
178 |
# } |
|
179 |
# |
|
180 |
# which is correct so don't get the bright idea of removing this |
|
181 |
# line |
|
182 |
@current_value = nil |
|
183 |
next |
|
184 |
end |
|
185 |
||
186 |
# We are here because the name of our current element is one that |
|
187 |
# already exists in the hash. If this is the first encounter with |
|
188 |
# the duplicate tag_name then we convert the existing value to an |
|
189 |
# array otherwise we push the value we are working with and add it |
|
190 |
# to the existing array. |
|
191 |
if @stack[index][tag_name.to_sym].is_a?( Array ) |
|
192 |
@stack[index][tag_name.to_sym] << use_value |
|
193 |
else |
|
194 |
@stack[index][tag_name.to_sym] = [ @stack[index][tag_name.to_sym] ] |
|
195 |
@stack[index][tag_name.to_sym] << use_value |
|
196 |
end |
|
197 |
else |
|
198 |
# We are here because the name of our current element has not been |
|
199 |
# assigned yet. |
|
200 |
@stack[index][tag_name.to_sym] = use_value |
|
201 |
end |
|
202 |
||
203 |
# We are done with the current tag so reset the data for the next one |
|
204 |
@current_value = nil |
|
205 |
end |
|
206 |
end |
|
207 |
||
208 |
@hash_time = Time.now - t |
|
209 |
||
210 |
self |
|
211 |
end |
|
212 |
||
213 |
def [](key) |
|
214 |
@parsed[key.to_sym] |
|
215 |
end |
|
216 |
end |
|
217 |
||
218 |
class SoapResponseHpricot |
|
219 |
#Parses an XML string into structured data. |
|
220 |
def initialize(content) |
|
221 |
document = Hpricot.XML(content) |
|
222 |
node = document%'soapenv:Body' |
|
223 |
||
224 |
@parsed = SoapResponseHpricot.parse node |
|
225 |
end |
|
226 |
||
227 |
#Allows this object to act like a hash (and therefore |
|
228 |
#as a FlashHash via the include above). |
|
229 |
def [](symbol) |
|
230 |
@parsed[symbol] |
|
231 |
end |
|
232 |
||
233 |
#Digests an XML DOM node into nested Ruby types. |
|
234 |
def SoapResponseHpricot.parse(node) |
|
235 |
#Convert text nodes into simple strings. |
|
236 |
children = node.children.reject do |c| |
|
237 |
c.is_a?(Hpricot::Text) && c.to_s.strip.empty? |
|
238 |
end |
|
239 |
||
240 |
if node.is_a?(Hpricot::Text) |
|
241 |
return node.inner_text |
|
242 |
end |
|
243 |
||
244 |
if children.first.is_a?(Hpricot::Text) |
|
245 |
return children.first |
|
246 |
end |
|
247 |
||
248 |
#Convert nodes with children into FlashHashes. |
|
249 |
elements = {} |
|
250 |
||
251 |
#Add all the element's children to the hash. |
|
252 |
children.each do |e| |
|
253 |
next if e.is_a?(Hpricot::Text) && e.to_s.strip.empty? |
|
254 |
name = e.name |
|
255 |
||
256 |
if name.include? ':' |
|
257 |
name = name.split(':').last |
|
258 |
end |
|
259 |
||
260 |
name = name.to_sym |
|
261 |
||
262 |
case elements[name] |
|
263 |
#The most common case: unique child element tags. |
|
264 |
when NilClass: elements[name] = parse(e) |
|
265 |
||
266 |
#Non-unique child elements become arrays: |
|
267 |
||
268 |
#We've already created the array: just |
|
269 |
#add the element. |
|
270 |
when Array: elements[name] << parse(e) |
|
271 |
||
272 |
#We haven't created the array yet: do so, |
|
273 |
#then put the existing element in, followed |
|
274 |
#by the new one. |
|
275 |
else |
|
276 |
elements[name] = [elements[name]] |
|
277 |
elements[name] << parse(e) |
|
278 |
end |
|
279 |
end |
|
280 |
||
281 |
return elements.empty? ? nil : elements |
|
282 |
end |
|
283 |
end |
|
284 |
||
285 |
#Turns an XML response from the server into a Ruby |
|
286 |
#object whose methods correspond to nested XML elements. |
|
287 |
class SoapResponse |
|
288 |
include FlashHash |
|
289 |
||
290 |
#Parses an XML string into structured data. |
|
291 |
def initialize(content) |
|
292 |
document = REXML::Document.new content |
|
293 |
node = REXML::XPath.first document, '//soapenv:Body' |
|
294 |
@parsed = SoapResponse.parse node |
|
295 |
end |
|
296 |
||
297 |
#Allows this object to act like a hash (and therefore |
|
298 |
#as a FlashHash via the include above). |
|
299 |
def [](symbol) |
|
300 |
@parsed[symbol] |
|
301 |
end |
|
302 |
||
303 |
#Digests an XML DOM node into nested Ruby types. |
|
304 |
def SoapResponse.parse(node) |
|
305 |
#Convert text nodes into simple strings. |
|
306 |
return node.text unless node.has_elements? |
|
307 |
||
308 |
#Convert nodes with children into FlashHashes. |
|
309 |
elements = {} |
|
310 |
class << elements |
|
311 |
include FlashHash |
|
312 |
end |
|
313 |
||
314 |
#Add all the element's children to the hash. |
|
315 |
node.each_element do |e| |
|
316 |
name = e.name.to_sym |
|
317 |
||
318 |
case elements[name] |
|
319 |
#The most common case: unique child element tags. |
|
320 |
when NilClass: elements[name] = parse(e) |
|
321 |
||
322 |
#Non-unique child elements become arrays: |
|
323 |
||
324 |
#We've already created the array: just |
|
325 |
#add the element. |
|
326 |
when Array: elements[name] << parse(e) |
|
327 |
||
328 |
#We haven't created the array yet: do so, |
|
329 |
#then put the existing element in, followed |
|
330 |
#by the new one. |
|
331 |
else |
|
332 |
elements[name] = [elements[name]] |
|
333 |
elements[name] << parse(e) |
|
334 |
end |
|
335 |
end |
|
336 |
||
337 |
return elements |
|
338 |
end |
|
339 |
end |
|
340 |
||
341 |
68 |
#Expand Ruby data structures into XML. |
342 |
69 |
def expand(builder, args, xmlns = nil) |
343 |
70 |
#Nest arrays: [:a, 1, :b, 2] => [[:a, 1], [:b, 2]] |
| … | … | @@ -373,203 +100,4 @@ module RForce |
373 |
100 |
end |
374 |
101 |
end |
375 |
102 |
|
376 |
#Implements the connection to the SalesForce server. |
|
377 |
class Binding |
|
378 |
include RForce |
|
379 |
||
380 |
DEFAULT_BATCH_SIZE = 10 |
|
381 |
attr_accessor :batch_size, :url, :assignment_rule_id, :use_default_rule, :update_mru, :client_id, :trigger_user_email, |
|
382 |
:trigger_other_email, :trigger_auto_response_email |
|
383 |
||
384 |
#Fill in the guts of this typical SOAP envelope |
|
385 |
#with the session ID and the body of the SOAP request. |
|
386 |
Envelope = <<-HERE |
|
387 |
<?xml version="1.0" encoding="utf-8" ?> |
|
388 |
<soap:Envelope xmlns:xsd="http://www.w3.org/2001/XMLSchema" |
|
389 |
xmlns:soap="http://schemas.xmlsoap.org/soap/envelope/" |
|
390 |
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" |
|
391 |
xmlns:partner="urn:partner.soap.sforce.com"> |
|
392 |
xmlns:spartner="urn:sobject.partner.soap.sforce.com"> |
|
393 |
<soap:Header> |
|
394 |
<partner:SessionHeader soap:mustUnderstand='1'> |
|
395 |
<partner:sessionId>%s</partner:sessionId> |
|
396 |
</partner:SessionHeader> |
|
397 |
<partner:QueryOptions soap:mustUnderstand='1'> |
|
398 |
<partner:batchSize>%d</partner:batchSize> |
|
399 |
</partner:QueryOptions> |
|
400 |
%s |
|
401 |
</soap:Header> |
|
402 |
<soap:Body> |
|
403 |
%s |
|
404 |
</soap:Body> |
|
405 |
</soap:Envelope> |
|
406 |
HERE |
|
407 |
||
408 |
AssignmentRuleHeaderUsingRuleId = '<partner:AssignmentRuleHeader soap:mustUnderstand="1"><partner:assignmentRuleId>%s</partner:assignmentRuleId></partner:AssignmentRuleHeader>' |
|
409 |
AssignmentRuleHeaderUsingDefaultRule = '<partner:AssignmentRuleHeader soap:mustUnderstand="1"><partner:useDefaultRule>true</partner:useDefaultRule></partner:AssignmentRuleHeader>' |
|
410 |
MruHeader = '<partner:MruHeader soap:mustUnderstand="1"><partner:updateMru>true</partner:updateMru></partner:MruHeader>' |
|
411 |
ClientIdHeader = '<partner:CallOptions soap:mustUnderstand="1"><partner:client>%s</partner:client></partner:CallOptions>' |
|
412 |
||
413 |
#Connect to the server securely. |
|
414 |
def initialize(url, sid = nil) |
|
415 |
init_server(url) |
|
416 |
||
417 |
@session_id = sid |
|
418 |
@batch_size = DEFAULT_BATCH_SIZE |
|
419 |
end |
|
420 |
||
421 |
||
422 |
def show_debug |
|
423 |
ENV['SHOWSOAP'] == 'true' |
|
424 |
end |
|
425 |
||
426 |
||
427 |
def init_server(url) |
|
428 |
@url = URI.parse(url) |
|
429 |
@server = Net::HTTP.new(@url.host, @url.port) |
|
430 |
@server.use_ssl = @url.scheme == 'https' |
|
431 |
@server.verify_mode = OpenSSL::SSL::VERIFY_NONE |
|
432 |
||
433 |
# run ruby with -d or env variable SHOWSOAP=true to see SOAP wiredumps. |
|
434 |
@server.set_debug_output $stderr if show_debug |
|
435 |
end |
|
436 |
||
437 |
||
438 |
#Log in to the server and remember the session ID |
|
439 |
#returned to us by SalesForce. |
|
440 |
def login(user, password) |
|
441 |
@user = user |
|
442 |
@password = password |
|
443 |
||
444 |
response = call_remote(:login, [:username, user, :password, password]) |
|
445 |
||
446 |
raise "Incorrect user name / password [#{response.fault}]" unless response.loginResponse |
|
447 |
||
448 |
result = response[:loginResponse][:result] |
|
449 |
@session_id = result[:sessionId] |
|
450 |
||
451 |
init_server(result[:serverUrl]) |
|
452 |
||
453 |
response |
|
454 |
end |
|
455 |
||
456 |
||
457 |
#Call a method on the remote server. Arguments can be |
|
458 |
#a hash or (if order is important) an array of alternating |
|
459 |
#keys and values. |
|
460 |
def call_remote(method, args) |
|
461 |
#Create XML text from the arguments. |
|
462 |
expanded = '' |
|
463 |
@builder = Builder::XmlMarkup.new(:target => expanded) |
|
464 |
expand(@builder, {method => args}, 'urn:partner.soap.sforce.com') |
|
465 |
||
466 |
extra_headers = "" |
|
467 |
extra_headers << (AssignmentRuleHeaderUsingRuleId % assignment_rule_id) if assignment_rule_id |
|
468 |
extra_headers << AssignmentRuleHeaderUsingDefaultRule if use_default_rule |
|
469 |
extra_headers << MruHeader if update_mru |
|
470 |
extra_headers << (ClientIdHeader % client_id) if client_id |
|
471 |
||
472 |
if trigger_user_email or trigger_other_email or trigger_auto_response_email |
|
473 |
extra_headers << '<partner:EmailHeader soap:mustUnderstand="1">' |
|
474 |
||
475 |
extra_headers << '<partner:triggerUserEmail>true</partner:triggerUserEmail>' if trigger_user_email |
|
476 |
extra_headers << '<partner:triggerOtherEmail>true</partner:triggerOtherEmail>' if trigger_other_email |
|
477 |
extra_headers << '<partner:triggerAutoResponseEmail>true</partner:triggerAutoResponseEmail>' if trigger_auto_response_email |
|
478 |
||
479 |
extra_headers << '</partner:EmailHeader>' |
|
480 |
end |
|
481 |
||
482 |
#Fill in the blanks of the SOAP envelope with our |
|
483 |
#session ID and the expanded XML of our request. |
|
484 |
request = (Envelope % [@session_id, @batch_size, extra_headers, expanded]) |
|
485 |
||
486 |
# reset the batch size for the next request |
|
487 |
@batch_size = DEFAULT_BATCH_SIZE |
|
488 |
||
489 |
# gzip request |
|
490 |
request = encode(request) |
|
491 |
||
492 |
headers = { |
|
493 |
'Connection' => 'Keep-Alive', |
|
494 |
'Content-Type' => 'text/xml', |
|
495 |
'SOAPAction' => '""', |
|
496 |
'User-Agent' => 'activesalesforce rforce/1.0' |
|
497 |
} |
|
498 |
||
499 |
unless show_debug |
|
500 |
headers['Accept-Encoding'] = 'gzip' |
|
501 |
headers['Content-Encoding'] = 'gzip' |
|
502 |
end |
|
503 |
||
504 |
#Send the request to the server and read the response. |
|
505 |
response = @server.post2(@url.path, request.lstrip, headers) |
|
506 |
||
507 |
# decode if we have encoding |
|
508 |
content = decode(response) |
|
509 |
||
510 |
# Check to see if INVALID_SESSION_ID was raised and try to relogin in |
|
511 |
if method != :login and @session_id and content =~ /sf:INVALID_SESSION_ID/ |
|
512 |
login(@user, @password) |
|
513 |
||
514 |
# repackage and rencode request with the new session id |
|
515 |
request = (Envelope % [@session_id, @batch_size, extra_headers, expanded]) |
|
516 |
request = encode(request) |
|
517 |
||
518 |
#Send the request to the server and read the response. |
|
519 |
response = @server.post2(@url.path, request.lstrip, headers) |
|
520 |
||
521 |
content = decode(response) |
|
522 |
end |
|
523 |
||
524 |
SoapResponse.new(content) |
|
525 |
end |
|
526 |
||
527 |
||
528 |
# decode gzip |
|
529 |
def decode(response) |
|
530 |
encoding = response['Content-Encoding'] |
|
531 |
||
532 |
# return body if no encoding |
|
533 |
if !encoding then return response.body end |
|
534 |
||
535 |
# decode gzip |
|
536 |
case encoding.strip |
|
537 |
when 'gzip': |
|
538 |
begin |
|
539 |
gzr = Zlib::GzipReader.new(StringIO.new(response.body)) |
|
540 |
decoded = gzr.read |
|
541 |
ensure |
|
542 |
gzr.close |
|
543 |
end |
|
544 |
decoded |
|
545 |
else |
|
546 |
response.body |
|
547 |
end |
|
548 |
end |
|
549 |
||
550 |
||
551 |
# encode gzip |
|
552 |
def encode(request) |
|
553 |
return request if show_debug |
|
554 |
||
555 |
begin |
|
556 |
ostream = StringIO.new |
|
557 |
gzw = Zlib::GzipWriter.new(ostream) |
|
558 |
gzw.write(request) |
|
559 |
ostream.string |
|
560 |
ensure |
|
561 |
gzw.close |
|
562 |
end |
|
563 |
end |
|
564 |
||
565 |
||
566 |
#Turns method calls on this object into remote SOAP calls. |
|
567 |
def method_missing(method, *args) |
|
568 |
unless args.size == 1 && [Hash, Array].include?(args[0].class) |
|
569 |
raise 'Expected 1 Hash or Array argument' |
|
570 |
end |
|
571 |
||
572 |
call_remote method, args[0] |
|
573 |
end |
|
574 |
end |
|
575 |
103 |
end |
Up to file-list lib/rforce/binding.rb:
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) |
|
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': |
|
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 |
Up to file-list lib/rforce/flash_hash.rb:
1 |
module RForce |
|
2 |
#Allows indexing hashes like method calls: hash.key |
|
3 |
#to supplement the traditional way of indexing: hash[key] |
|
4 |
module FlashHash |
|
5 |
def method_missing(method, *args) |
|
6 |
self[method] |
|
7 |
end |
|
8 |
end |
|
9 |
end |
Up to file-list lib/rforce/soap_response.rb:
1 |
require 'rexml/document' |
|
2 |
require 'rexml/xpath' |
|
3 |
||
4 |
||
5 |
module RForce |
|
6 |
#Turns an XML response from the server into a Ruby |
|
7 |
#object whose methods correspond to nested XML elements. |
|
8 |
class SoapResponse |
|
9 |
include FlashHash |
|
10 |
||
11 |
#Parses an XML string into structured data. |
|
12 |
def initialize(content) |
|
13 |
document = REXML::Document.new content |
|
14 |
node = REXML::XPath.first document, '//soapenv:Body' |
|
15 |
@parsed = SoapResponse.parse node |
|
16 |
end |
|
17 |
||
18 |
#Allows this object to act like a hash (and therefore |
|
19 |
#as a FlashHash via the include above). |
|
20 |
def [](symbol) |
|
21 |
@parsed[symbol] |
|
22 |
end |
|
23 |
||
24 |
#Digests an XML DOM node into nested Ruby types. |
|
25 |
def SoapResponse.parse(node) |
|
26 |
#Convert text nodes into simple strings. |
|
27 |
return node.text unless node.has_elements? |
|
28 |
||
29 |
#Convert nodes with children into FlashHashes. |
|
30 |
elements = {} |
|
31 |
class << elements |
|
32 |
include FlashHash |
|
33 |
end |
|
34 |
||
35 |
#Add all the element's children to the hash. |
|
36 |
node.each_element do |e| |
|
37 |
name = e.name.to_sym |
|
38 |
||
39 |
case elements[name] |
|
40 |
#The most common case: unique child element tags. |
|
41 |
when NilClass: elements[name] = parse(e) |
|
42 |
||
43 |
#Non-unique child elements become arrays: |
|
44 |
||
45 |
#We've already created the array: just |
|
46 |
#add the element. |
|
47 |
when Array: elements[name] << parse(e) |
|
48 |
||
49 |
#We haven't created the array yet: do so, |
|
50 |
#then put the existing element in, followed |
|
51 |
#by the new one. |
|
52 |
else |
|
53 |
elements[name] = [elements[name]] |
|
54 |
elements[name] << parse(e) |
|
55 |
end |
|
56 |
end |
|
57 |
||
58 |
return elements |
|
59 |
end |
|
60 |
end |
|
61 |
end |
Up to file-list lib/rforce/soap_response_expat.rb:
1 |
require 'xml/parser' |
|
2 |
||
3 |
||
4 |
module RForce |
|
5 |
class SoapResponseExpat |
|
6 |
include FlashHash |
|
7 |
||
8 |
attr_reader :hash_time |
|
9 |
||
10 |
SOAP_ENVELOPE = 'soapenv:Envelope' |
|
11 |
||
12 |
def initialize(content) |
|
13 |
@current_value, @stack, @parsed = nil, [], Hash.new |
|
14 |
||
15 |
namespaces = [] |
|
16 |
t = Time.now |
|
17 |
||
18 |
XML::Parser.new.parse( content ) do |type, name, data| |
|
19 |
# We are not interested in keeping the namespace declarations for the tag |
|
20 |
# names so lets deal with that here. |
|
21 |
unless name.nil? |
|
22 |
if name.index( ':' ) |
|
23 |
tag_ns, tag_name = name.split( ':' ) |
|
24 |
tag_name = name unless namespaces.include?( tag_ns ) |
|
25 |
else |
|
26 |
tag_name = name |
|
27 |
end |
|
28 |
end |
|
29 |
||
30 |
case type |
|
31 |
# name = The literal tag name of the element |
|
32 |
# tag_name = The name of the element minus the namespace that gets used |
|
33 |
# as the key in the hash |
|
34 |
# data = The attributes of the element |
|
35 |
when XML::Parser::START_ELEM |
|
36 |
# The attribute of the first element is the soap envelope which has |
|
37 |
# as attributes the allowed namespaces of all the tags so lets grab |
|
38 |
# them here to make sure we can strip them out appropriately since |
|
39 |
# they are not necessary in our hash. |
|
40 |
namespaces = data.keys.map { |k| k.split( ':' )[1] if k.match( 'xmlns:' ) }.compact if name.eql?( SOAP_ENVELOPE ) |
|
41 |
||
42 |
next if tag_name.eql?( SOAP_ENVELOPE ) |
|
43 |
||
44 |
@stack.push( Hash.new ) |
|
45 |
||
46 |
# name = The literal tag name of the element |
|
47 |
# tag_name = The name of the element minus the namespace that gets used |
|
48 |
# as the key in the hash |
|
49 |
# data = The actual data identified by the element |
|
50 |
when XML::Parser::CDATA |
|
51 |
@current_value = data.strip.empty? ? nil : data |
|
52 |
||
53 |
# name = The literal tag name of the element |
|
54 |
# tag_name = The name of the element minus the namespace that gets used |
|
55 |
# as the key in the hash |
|
56 |
# data = nil |
|
57 |
when XML::Parser::END_ELEM |
|
58 |
next if tag_name.eql?( SOAP_ENVELOPE ) |
|
59 |
||
60 |
working_hash = @stack.pop |
|
61 |
||
62 |
# We are either done or working on a certain depth in the current |
|
63 |
# stack. |
|
64 |
if @stack.empty? |
|
65 |
@parsed = working_hash |
|
66 |
break |
|
67 |
else |
|
68 |
index = @stack.size - 1 |
|
69 |
end |
|
70 |
||
71 |
# working_hash and @current_value have a mutually exclusive relationship. |
|
72 |
# If the current element doesn't have a value then it means that there |
|
73 |
# is a nested data structure. In this case then working_hash is populated |
|
74 |
# and @current_value is nil. Conversely, if @current_value has a value |
|
75 |
# then we do not have a nested data sctructure and working_hash will |
|
76 |
# be empty. |
|
77 |
use_value = ( working_hash.empty? ) ? @current_value : working_hash |
|
78 |
||
79 |
if @stack[index].keys.include?( tag_name.to_sym ) |
|
80 |
# This is here to handle the Id value being included twice and thus |
|
81 |
# producing an array. We skip the second instance so the array is |
|
82 |
# not created. |
|
83 |
if tag_name.eql?( 'Id' ) |
|
84 |
# If we don't clear out the current value here, then we introduce |
|
85 |
# a bug if the element after this Id should be null. An example |
|
86 |
# query is the following: |
|
87 |
# |
|
88 |
# Select Id, LeadId, ContactId from CampaignMember... |
|
89 |
# |
|
90 |
# For the members that are Contacts their LeadId will be null. If |
|
91 |
# we fail to clear out this data the resulting hash would be: |
|
92 |
# |
|
93 |
# { ... :records => [ |
|
94 |
# { :Id => '00v50000008pfHrAAI', :LeadId => '00v50000008pfHrAAI', :ContactId => '0035000000KwjIuAAJ' }, |
|
95 |
# { :Id => '00v50000008pfHsAAI', :LeadId => '00v50000008pfHsAAI', :ContactId => '0035000000KwjIvAAJ' }, |
|
96 |
# } |
|
97 |
# |
|
98 |
# Now you see the problem. When the @current_value = nil is present |
|
99 |
# we get the following: |
|
100 |
# |
|
101 |
# { ... :records => [ |
|
102 |
# { :Id => '00v50000008pfHrAAI', :LeadId => nil, :ContactId => '0035000000KwjIuAAJ' }, |
|
103 |
# { :Id => '00v50000008pfHsAAI', :LeadId => nil, :ContactId => '0035000000KwjIvAAJ' }, |
|
104 |
# } |
|
105 |
# |
|
106 |
# which is correct so don't get the bright idea of removing this |
|
107 |
# line |
|
108 |
@current_value = nil |
|
109 |
next |
|
110 |
end |
|
111 |
||
112 |
# We are here because the name of our current element is one that |
|
113 |
# already exists in the hash. If this is the first encounter with |
|
114 |
# the duplicate tag_name then we convert the existing value to an |
|
115 |
# array otherwise we push the value we are working with and add it |
|
116 |
# to the existing array. |
|
117 |
if @stack[index][tag_name.to_sym].is_a?( Array ) |
|
118 |
@stack[index][tag_name.to_sym] << use_value |
|
119 |
else |
|
120 |
@stack[index][tag_name.to_sym] = [ @stack[index][tag_name.to_sym] ] |
|
121 |
@stack[index][tag_name.to_sym] << use_value |
|
122 |
end |
|
123 |
else |
|
124 |
# We are here because the name of our current element has not been |
|
125 |
# assigned yet. |
|
126 |
@stack[index][tag_name.to_sym] = use_value |
|
127 |
end |
|
128 |
||
129 |
# We are done with the current tag so reset the data for the next one |
|
130 |
@current_value = nil |
|
131 |
end |
|
132 |
end |
|
133 |
||
134 |
@hash_time = Time.now - t |
|
135 |
||
136 |
self |
|
137 |
end |
|
138 |
||
139 |
def [](key) |
|
140 |
@parsed[key.to_sym] |
|
141 |
end |
|
142 |
end |
|
143 |
end |
Up to file-list lib/rforce/soap_response_hpricot.rb:
1 |
require 'hpricot' |
|
2 |
||
3 |
||
4 |
module RForce |
|
5 |
class SoapResponseHpricot |
|
6 |
#Parses an XML string into structured data. |
|
7 |
def initialize(content) |
|
8 |
document = Hpricot.XML(content) |
|
9 |
node = document%'soapenv:Body' |
|
10 |
||
11 |
@parsed = SoapResponseHpricot.parse node |
|
12 |
end |
|
13 |
||
14 |
#Allows this object to act like a hash (and therefore |
|
15 |
#as a FlashHash via the include above). |
|
16 |
def [](symbol) |
|
17 |
@parsed[symbol] |
|
18 |
end |
|
19 |
||
20 |
#Digests an XML DOM node into nested Ruby types. |
|
21 |
def SoapResponseHpricot.parse(node) |
|
22 |
#Convert text nodes into simple strings. |
|
23 |
children = node.children.reject do |c| |
|
24 |
c.is_a?(Hpricot::Text) && c.to_s.strip.empty? |
|
25 |
end |
|
26 |
||
27 |
if node.is_a?(Hpricot::Text) |
|
28 |
return node.inner_text |
|
29 |
end |
|
30 |
||
31 |
if children.first.is_a?(Hpricot::Text) |
|
32 |
return children.first |
|
33 |
end |
|
34 |
||
35 |
#Convert nodes with children into FlashHashes. |
|
36 |
elements = {} |
|
37 |
||
38 |
#Add all the element's children to the hash. |
|
39 |
children.each do |e| |
|
40 |
next if e.is_a?(Hpricot::Text) && e.to_s.strip.empty? |
|
41 |
name = e.name |
|
42 |
||
43 |
if name.include? ':' |
|
44 |
name = name.split(':').last |
|
45 |
end |
|
46 |
||
47 |
name = name.to_sym |
|
48 |
||
49 |
case elements[name] |
|
50 |
#The most common case: unique child element tags. |
|
51 |
when NilClass: elements[name] = parse(e) |
|
52 |
||
53 |
#Non-unique child elements become arrays: |
|
54 |
||
55 |
#We've already created the array: just |
|
56 |
#add the element. |
|
57 |
when Array: elements[name] << parse(e) |
|
58 |
||
59 |
#We haven't created the array yet: do so, |
|
60 |
#then put the existing element in, followed |
|
61 |
#by the new one. |
|
62 |
else |
|
63 |
elements[name] = [elements[name]] |
|
64 |
elements[name] << parse(e) |
|
65 |
end |
|
66 |
end |
|
67 |
||
68 |
return elements.empty? ? nil : elements |
|
69 |
end |
|
70 |
end |
|
71 |
end |
|
72 |
