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

#as_array, #call_if_exists, #connector_called, #effective_operations, #ensure_dir_exists, #get_preference, #get_preference_file_path, #include_in_search_path, #log_progress, #perform_operations, #pipeline_called, #set_preference, #something_called, #with_rescue

Constructor Details

#initialize(options = {}) ⇒ BaseConnector

Returns a new instance of BaseConnector.



53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
# File 'lib/ruby_sync/connectors/base_connector.rb', line 53

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 "#{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.



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

def is_vault
  @is_vault
end

#nameObject

Returns the value of attribute name.



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

def name
  @name
end

#once_onlyObject

Returns the value of attribute once_only.



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

def once_only
  @once_only
end

#pipelineObject

Returns the value of attribute pipeline.



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

def pipeline
  @pipeline
end

Class Method Details

.class_for(connector_name) ⇒ Object

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



376
377
378
379
# File 'lib/ruby_sync/connectors/base_connector.rb', line 376

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

.class_name_for(connector_name) ⇒ Object

Return the class name for a path style connector name



382
383
384
# File 'lib/ruby_sync/connectors/base_connector.rb', line 382

def self.class_name_for connector_name
  '::' + "#{connector_name}_connector".camelize
end

.fieldsObject

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



371
372
373
# File 'lib/ruby_sync/connectors/base_connector.rb', line 371

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.



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

def self.sample_config
end

Instance Method Details

#add(id, operations) ⇒ Object

Subclasses must override this. Called by perform_add to actually store the new record in the datastore. Returned value will be used as the association id if this connector is acting as the client.



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

def add id, operations
  raise "add method not implemented"
end

#associate(association, path) ⇒ Object

Store association for the given path



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

def associate association, path
  YAML::DBM.open(path_to_association_dbm_filename) do |dbm|
    assocs = dbm[path.to_s] || {}
    assocs[association.context.to_s] = association.key.to_s
    dbm[path.to_s] = 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.



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

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.



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

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



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

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

#association_to_path_dbm_filenameObject

Stores paths indexed by association_context:association_key



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

def association_to_path_dbm_filename
  dbm_path + "_assoc_to_path"
end

#associations_for(path) ⇒ Object



262
263
264
265
266
267
# File 'lib/ruby_sync/connectors/base_connector.rb', line 262

def associations_for path
  YAML::DBM.open(path_to_association_dbm_filename) do |dbm|
    assocs =  dbm[path.to_s]
    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.

Returns:

  • (Boolean)


234
235
236
237
238
239
240
# File 'lib/ruby_sync/connectors/base_connector.rb', line 234

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



320
321
322
323
# File 'lib/ruby_sync/connectors/base_connector.rb', line 320

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.



363
364
365
# File 'lib/ruby_sync/connectors/base_connector.rb', line 363

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

#dbm_pathObject

set a default dbm path



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

def dbm_path()
  p = "#{base_path}/db"
  ::FileUtils.mkdir_p p
  ::File.join(p,name)
end

#digest(o) ⇒ Object



134
135
136
# File 'lib/ruby_sync/connectors/base_connector.rb', line 134

def digest(o)
  Digest::MD5.hexdigest(o.to_yaml)
end

#each_changeObject

Subclasses MAY override this to interface with the external system and generate an event for every change that affects items within the scope of this connector.

The default behaviour is to compare a hash of each entry in the database with a stored hash of its previous value and generate add, modify and delete events appropriately. This is normally a very inefficient way to operate so overriding this method is highly recommended if you can detect changes in a more efficient manner.

This method will be called repeatedly until the connector is stopped.



108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
# File 'lib/ruby_sync/connectors/base_connector.rb', line 108

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
        if is_vault? and @pipeline
          association = association_for @pipeline.association_context, key
          remove_association association
        end
      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.



92
93
94
# File 'lib/ruby_sync/connectors/base_connector.rb', line 92

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.



220
221
222
# File 'lib/ruby_sync/connectors/base_connector.rb', line 220

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.



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

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)


227
228
229
# File 'lib/ruby_sync/connectors/base_connector.rb', line 227

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)


340
341
342
# File 'lib/ruby_sync/connectors/base_connector.rb', line 340

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

#is_echo?(event) ⇒ Boolean

Returns:

  • (Boolean)


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

def is_echo? event; false end

#is_vault?Boolean

Returns:

  • (Boolean)


196
197
198
# File 'lib/ruby_sync/connectors/base_connector.rb', line 196

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



49
50
51
# File 'lib/ruby_sync/connectors/base_connector.rb', line 49

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.



206
207
208
# File 'lib/ruby_sync/connectors/base_connector.rb', line 206

def own_association_key_for(path)
  path
end

#path_for_association(association) ⇒ Object



255
256
257
258
259
260
# File 'lib/ruby_sync/connectors/base_connector.rb', line 255

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.



215
216
217
# File 'lib/ruby_sync/connectors/base_connector.rb', line 215

def path_for_own_association_key(key)
  key
end

#path_to_association_dbm_filenameObject

Stores association keys indexed by path:association_context



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

def path_to_association_dbm_filename
  dbm_path + "_path_to_assoc"
end

#remove_association(association) ⇒ Object



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

def remove_association association
  path = nil
  DBM.open(association_to_path_dbm_filename) do |dbm|
    return unless path =dbm.delete(association.to_s)
  end
  YAML::DBM.open(path_to_association_dbm_filename) do |dbm|
    assocs = dbm[path.to_s]
    assocs.delete(association.context) and dbm[path.to_s] = assocs
  end
end

#remove_associationsObject



316
317
318
# File 'lib/ruby_sync/connectors/base_connector.rb', line 316

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

#remove_mirrorObject



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

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.



143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
# File 'lib/ruby_sync/connectors/base_connector.rb', line 143

def start &blk
  log.debug "#{name}: Started"
  @running = true
  sync_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
  sync_stopped
end

#startedObject

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



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

def started
end

#stopObject

Politely stop the connector.



190
191
192
193
# File 'lib/ruby_sync/connectors/base_connector.rb', line 190

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).



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

def stopped; end

#sync_startedObject

Called by start() before first call to each_change or each_entry



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

def sync_started; end

#sync_stoppedObject

Called by start() after last call to each_change or each_entry



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

def sync_stopped; end

#test_add(id, details) ⇒ Object

Called by unit tests to inject data



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

def test_add id, details
  add id, details
end

#test_delete(id) ⇒ Object

Called by unit tests to delete a record



357
358
359
# File 'lib/ruby_sync/connectors/base_connector.rb', line 357

def test_delete id
  delete id
end

#test_modify(id, details) ⇒ Object

Called by unit tests to modify data



352
353
354
# File 'lib/ruby_sync/connectors/base_connector.rb', line 352

def test_modify id, details
  modify id, details
end