Class: RubySync::Connectors::BaseConnector

Inherits:
Object
  • Object
show all
Includes:
ConnectorEventProcessing, Utilities
Defined in:
lib/ruby_sync/connectors/base_connector.rb

Instance Attribute Summary collapse

Class Method Summary collapse

Instance Method Summary collapse

Methods included from Utilities

#base_path, #call_if_exists, #connector_called, #ensure_dir_exists, #find_base_path, #get_preference, #get_preference_file_path, #include_in_search_path, #log_progress, #pipeline_called, #set_preference, #something_called, #with_rescue

Constructor Details

#initialize(options = {}) ⇒ BaseConnector

Returns a new instance of BaseConnector.



48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
# File 'lib/ruby_sync/connectors/base_connector.rb', line 48

def initialize options={}
  base_path # call this once to get the working directory before anything else
            # in the connector changes the cwd
  options = self.class.default_options.merge(options)
  once_only = false
  self.name = options[:name]
  self.is_vault = options[:is_vault]
  if is_vault && !can_act_as_vault?
    raise Exception.new("#{self.class.name} can't act as an identity vault.")
  end
  options.each do |key, value|
    if self.respond_to? "#{key}="
      self.send("#{key}=", value) 
    else
      log.debug "#{name}: doesn't respond to #{key}="
    end
  end
end

Instance Attribute Details

#is_vaultObject

Returns the value of attribute is_vault.



26
27
28
# File 'lib/ruby_sync/connectors/base_connector.rb', line 26

def is_vault
  @is_vault
end

#nameObject

Returns the value of attribute name.



26
27
28
# File 'lib/ruby_sync/connectors/base_connector.rb', line 26

def name
  @name
end

#once_onlyObject

Returns the value of attribute once_only.



26
27
28
# File 'lib/ruby_sync/connectors/base_connector.rb', line 26

def once_only
  @once_only
end

Class Method Details

.class_for(connector_name) ⇒ Object

Ensures that the named connector is loaded and returns its class object



403
404
405
406
# File 'lib/ruby_sync/connectors/base_connector.rb', line 403

def self.class_for connector_name
  name = class_name_for connector_name
  (name)? eval("::"+name) : nil
end

.class_name_for(connector_name) ⇒ Object

Ensures that the named connector is loaded and returns its class name.



409
410
411
412
413
414
415
416
417
# File 'lib/ruby_sync/connectors/base_connector.rb', line 409

def self.class_name_for connector_name
  filename = "#{connector_name}_connector"
  class_name = filename.camelize
  eval "defined? #{class_name}" or
  $".include?(filename) or
  require filename or
  raise Exception.new("Can't find connector '#{filename}'")
  class_name
end

.fieldsObject

Return an array of possible fields for this connector. Implementations should override this to query the datasource for possible fields.



398
399
400
# File 'lib/ruby_sync/connectors/base_connector.rb', line 398

def self.fields
  nil
end

.sample_configObject

Override this to return a string that will be included within the class definition of of configurations based on your connector.



70
71
# File 'lib/ruby_sync/connectors/base_connector.rb', line 70

def self.sample_config
end

Instance Method Details

#associate(association, path) ⇒ Object

Store association for the given path



217
218
219
220
221
222
223
224
225
226
227
# File 'lib/ruby_sync/connectors/base_connector.rb', line 217

def associate association, path
  DBM.open(path_to_association_dbm_filename) do |dbm|
    assocs_string = dbm[path.to_s]
    assocs = (assocs_string)? Marshal.load(assocs_string) : {}
    assocs[association.context] = association.key
    dbm[path.to_s] = Marshal.dump(assocs)
  end
  DBM.open(association_to_path_dbm_filename) do |dbm|
    dbm[association.to_s] = path
  end
end

#association_contextObject

The context to be used to for all associations created where this connector is the client.



299
300
301
# File 'lib/ruby_sync/connectors/base_connector.rb', line 299

def association_context
  self.name
end

#association_for(context, path) ⇒ Object

Return the association object given the association context and path. This should only be called on the vault.



282
283
284
285
286
# File 'lib/ruby_sync/connectors/base_connector.rb', line 282

def association_for(context, path)
  raise "#{name} is not a vault." unless is_vault?
  key = association_key_for context, path
  key and RubySync::Association.new(context, key)
end

#association_key_for(context, path) ⇒ Object

Could be more efficient for the default case where the associations are actually stored as a serialized hash but then it wouldn’t be as generic and other implementations would have to reimplement it. def association_key_for context, path

raise "#{name} is not a vault." unless is_vault?
associations_for(path).each do |assoc|
  (c, key) = assoc.split(RubySync::Association.delimiter, 2)
  return key if c == context 
end
return nil

end



271
272
273
274
275
276
277
# File 'lib/ruby_sync/connectors/base_connector.rb', line 271

def association_key_for context, path
  DBM.open(path_to_association_dbm_filename) do |dbm|
    assocs_string = dbm[path.to_s]
    assocs = (assocs_string)? Marshal.load(assocs_string) : {}
    assocs[context.to_s]
  end
end

#association_to_path_dbm_filenameObject

Stores paths indexed by association_context:association_key



38
39
40
# File 'lib/ruby_sync/connectors/base_connector.rb', line 38

def association_to_path_dbm_filename
  dbm_path + "_assoc_to_path"
end

#associations_for(path) ⇒ Object

Default implementation does nothing



237
238
239
240
241
242
243
# File 'lib/ruby_sync/connectors/base_connector.rb', line 237

def associations_for path
  DBM.open(path_to_association_dbm_filename) do |dbm|
    assocs_string = dbm[path.to_s]
    assocs = (assocs_string)? Marshal.load(assocs_string) : {}
    assocs.values
  end
end

#can_act_as_vault?Boolean

Whether this connector is capable of acting as a vault. The vault is responsible for storing the association key of the client application and must be able to retrieve records for that association key. Typically, databases and directories can act as vaults, text documents and HR or finance applications probably can’t. To enable a connector to act as a vault, define the following methods:

> path_for_foreign_key(pipeline_id, key)

> foreign_key_for(path)

and associate_with_foreign_key(key, path).

Returns:

  • (Boolean)


207
208
209
210
211
212
213
# File 'lib/ruby_sync/connectors/base_connector.rb', line 207

def can_act_as_vault?
  defined? associate and
  defined? path_for_association and
  defined? association_key_for and
  defined? remove_association and
  defined? associations_for
end

#cleanObject



311
312
313
314
# File 'lib/ruby_sync/connectors/base_connector.rb', line 311

def clean
  remove_associations
  remove_mirror
end

#create_operations_for(record) ⇒ Object

Return an array of operations that would create the given record if applied to an empty hash.



354
355
356
# File 'lib/ruby_sync/connectors/base_connector.rb', line 354

def create_operations_for record
  record.keys.map {|key| RubySync::Operation.new(:add, key, record[key])}
end

#dbm_pathObject

set a default dbm path



30
# File 'lib/ruby_sync/connectors/base_connector.rb', line 30

def dbm_path() "#{base_path}/db/#{name}"; end

#digest(o) ⇒ Object



115
116
117
# File 'lib/ruby_sync/connectors/base_connector.rb', line 115

def digest(o)
  Digest::MD5.hexdigest(Marshal.dump(o))
end

#each_changeObject

Subclasses must override this to interface with the external system and generate an event for every change that affects items within the scope of this connector. todo: Make the default behaviour to build a database of the key of each entry with a hash of the contents of the entry. Then to compare that against each entry to see if it has changed.



93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
# File 'lib/ruby_sync/connectors/base_connector.rb', line 93

def each_change
  DBM.open(self.mirror_dbm_filename) do |dbm|
    # scan existing entries to see if any new or modified
    each_entry do |path, entry|
      digest = digest(entry)
      unless stored_digest = dbm[path.to_s] and digest == stored_digest
        operations = create_operations_for(entry)
        yield RubySync::Event.add(self, path, nil, operations) 
        dbm[path.to_s] = digest
      end
    end
    
    # scan dbm to find deleted
    dbm.each do |key, stored_hash|
      unless self[key]
        yield RubySync::Event.delete(self, key)
        dbm.delete key
      end
    end
  end        
end

#each_entryObject

Subclasses must override this to interface with the external system and generate entries for every entry in the scope passing the entry path (id) and its data (as a hash of arrays). This method will be called repeatedly until the connector is stopped.



83
84
85
# File 'lib/ruby_sync/connectors/base_connector.rb', line 83

def each_entry
  raise "Not implemented"
end

#entry_for_own_association_key(key) ⇒ Object

Returns the entry matching the association key. This is only called on the client.



187
188
189
# File 'lib/ruby_sync/connectors/base_connector.rb', line 187

def entry_for_own_association_key(key)
  self[path_for_own_association_key(key)]
end

#find_associated(association) ⇒ Object

Should only be called on the vault. Returns the entry associated with the association passed. Some connectors may wish to override this if they have a more efficient way of retrieving the record for a given association.



292
293
294
295
# File 'lib/ruby_sync/connectors/base_connector.rb', line 292

def find_associated association
  path = path_for_association association
  path and self[path]
end

#has_entry_for_key?(key) ⇒ Boolean

True if there is an entry matching the association key. Only called on the client. Override if you have a quicker way of determining whether an entry exists for given key than retrieving the entry.

Returns:

  • (Boolean)


194
195
196
# File 'lib/ruby_sync/connectors/base_connector.rb', line 194

def has_entry_for_key?(key)
  entry_for_own_association_key(key)
end

#is_delete_echo?(event) ⇒ Boolean

Attempts to delete non-existent items may occur due to echoing. Many systems won’t be able to record the fact that an entry has been deleted by rubysync because after the delete, there is no entry left to record the information in. Therefore, they may issue a notification that the item has been deleted. This becomes an event and the connector won’t know that it caused the delete. The story usually has a reasonably happy ending though. The inappropriate delete event is processed by the pipeline and a delete attempt is made on the datastore that actually triggered the original delete event in the first place. Most of the time, there will be no entry there for it to delete and it will fail harmlessly. Problems may arise, however, if the original delete event was the result of manipulation in the pipeline and the original entry is in fact supposed to stay there. For example, say a student in an enrolment system was marked as not enrolled anymore. This modify event is translated by the pipeline that connects to the identity vault to become a delete because only the enrolment system is interested in non-enrolled students. As the student is removed from the identity vault, a new delete event is generated targeted back and the enrolment system. If the pipeline has been configured to honour delete requests from the vault to the enrolment system then the students entry in the enrolment system would be deleted.

Returns:

  • (Boolean)


331
332
333
# File 'lib/ruby_sync/connectors/base_connector.rb', line 331

def is_delete_echo? event
  false #TODO implement delete event caching
end

#is_echo?(event) ⇒ Boolean

Returns:

  • (Boolean)


335
# File 'lib/ruby_sync/connectors/base_connector.rb', line 335

def is_echo? event; false end

#is_vault?Boolean

Returns:

  • (Boolean)


163
164
165
# File 'lib/ruby_sync/connectors/base_connector.rb', line 163

def is_vault?
  @is_vault
end

#mirror_dbm_filenameObject

Stores a hash for each entry so we can tell when entries are added, deleted or modified



44
45
46
# File 'lib/ruby_sync/connectors/base_connector.rb', line 44

def mirror_dbm_filename
  dbm_path + "_mirror"
end

#own_association_key_for(path) ⇒ Object

Returns the association key for the given path. Called if this connector is the client. The default implementation returns the path itself. If there is a more efficient key for looking up an entry in the client, override to return that instead.



173
174
175
# File 'lib/ruby_sync/connectors/base_connector.rb', line 173

def own_association_key_for(path)
  path
end

#path_for_association(association) ⇒ Object



229
230
231
232
233
234
# File 'lib/ruby_sync/connectors/base_connector.rb', line 229

def path_for_association association
  is_vault? or return path_for_own_association_key(association.key)
  DBM.open(association_to_path_dbm_filename) do |dbm|
    dbm[association.to_s]
  end
end

#path_for_own_association_key(key) ⇒ Object

Returns the appropriate entry for the association key. This key will have been provided by a previous call to the association_key method. This will only be called on the client connector. It is not expected that the client will have to store this key.



182
183
184
# File 'lib/ruby_sync/connectors/base_connector.rb', line 182

def path_for_own_association_key(key)
  key
end

#path_to_association_dbm_filenameObject

Stores association keys indexed by path:association_context



33
34
35
# File 'lib/ruby_sync/connectors/base_connector.rb', line 33

def path_to_association_dbm_filename
  dbm_path + "_path_to_assoc"
end

#perform_operations(operations, record = {}) ⇒ Object

Performs the given operations on the given record. The record is a Hash in which each key is a field name and each value is an array of values for that field. Operations is an Array of RubySync::Operation objects to be performed on the record.



363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
# File 'lib/ruby_sync/connectors/base_connector.rb', line 363

def perform_operations operations, record={}
  operations.each do |op|
    unless op.instance_of? RubySync::Operation
      log.warn "!!!!!!!!!!  PROBLEM, DUMP FOLLOWS: !!!!!!!!!!!!!!"
      p op
    end
    case op.type
    when :add
      if record[op.subject]
        existing = record[op.subject].as_array
        (existing & op.values).empty? or
          raise Exception.new("Attempt to add duplicate elements to #{name}")
        record[op.subject] =  existing + op.values
      else
        record[op.subject] = op.values
      end
    when :replace
      record[op.subject] = op.values
    when :delete
      if value == nil || value == "" || value == []
        record.delete(op.subject)
      else
        record[op.subject] -= values
      end
    else
      raise Exception.new("Unknown operation '#{op.type}'")
    end
  end
  return record
end

#remove_association(association) ⇒ Object

Default implementation does nothing



246
247
248
249
250
251
252
253
254
255
256
# File 'lib/ruby_sync/connectors/base_connector.rb', line 246

def remove_association association
  path = nil
  DBM.open(association_to_path_dbm_filename) do |dbm|
    return unless path =dbm.delete(association.to_s)
  end
  DBM.open(path_to_association_dbm_filename) do |dbm|
    assocs_string = dbm[path]
    assocs = (assocs_string)? Marshal.load(assocs_string) : {}
    assocs.delete(association.context) and dbm[path.to_s] = Marshal.dump(assocs)
  end
end

#remove_associationsObject



307
308
309
# File 'lib/ruby_sync/connectors/base_connector.rb', line 307

def remove_associations
  File.delete_if_exists(["#{association_to_path_dbm_filename}.db","#{path_to_association_dbm_filename}.db"])
end

#remove_mirrorObject



303
304
305
# File 'lib/ruby_sync/connectors/base_connector.rb', line 303

def remove_mirror
  File.delete_if_exists(["#{mirror_dbm_filename}.db"])
end

#start(&blk) ⇒ Object

Call each_change repeatedly (or once if in once_only mode) to generate events. Should generally only be called by the pipeline to which it is attached.



127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
# File 'lib/ruby_sync/connectors/base_connector.rb', line 127

def start &blk
  log.info "#{name}: Started"
  @running = true
  started()
  while @running
    each_change do |event|
      if event.type == :force_resync
        each_entry &blk
        next
      end
      if is_delete_echo?(event) || is_echo?(event)
        log.debug "Ignoring echoed event"
      else
        call_if_exists :source_transform, event
        yield(event)
      end
    end

    if once_only
      log.debug "#{name}: Stopped"
      @running = false
    else
      log.debug "#{name}: sleeping"
      sleep 1
    end
  end
  stopped
end

#startedObject

Override this to perform actions that must be performed the when the connector starts running. (Eg, opening network connections)



75
76
# File 'lib/ruby_sync/connectors/base_connector.rb', line 75

def started
end

#stopObject

Politely stop the connector.



157
158
159
160
# File 'lib/ruby_sync/connectors/base_connector.rb', line 157

def stop
  log.info "#{name}: Attempting to stop"
  @running = false
end

#stoppedObject

Override this to perform actions that must be performed when the connector exits (eg closing network conections).



121
# File 'lib/ruby_sync/connectors/base_connector.rb', line 121

def stopped; end

#test_add(id, details) ⇒ Object

Called by unit tests to inject data



338
339
340
# File 'lib/ruby_sync/connectors/base_connector.rb', line 338

def test_add id, details
  add id, details
end

#test_delete(id) ⇒ Object

Called by unit tests to delete a record



348
349
350
# File 'lib/ruby_sync/connectors/base_connector.rb', line 348

def test_delete id
  delete id
end

#test_modify(id, details) ⇒ Object

Called by unit tests to modify data



343
344
345
# File 'lib/ruby_sync/connectors/base_connector.rb', line 343

def test_modify id, details
  modify id, details
end