undees / rforce (http://rforce.rubyforge.org)

A simple, usable binding to the SalesForce API.

Clone this repository (size: 107.8 KB): HTTPS / SSH
$ hg clone http://bitbucket.org/undees/rforce/

Changed (Δ31.4 KB):

raw changeset »

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)

Up to file-list Rakefile:

1
1
# -*- ruby -*-
2
2
3
$:.unshift './lib'
3
4
require 'rubygems'
4
5
require 'hoe'
5
require './lib/rforce.rb'
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

Up to file-list lib/rforce/version.rb:

1
module RForce
2
  VERSION = '0.3'
3
end