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



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

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



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

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.



367
368
369
# File 'lib/ruby_sync/connectors/base_connector.rb', line 367

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



238
239
240
241
242
243
244
245
246
247
# File 'lib/ruby_sync/connectors/base_connector.rb', line 238

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.



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

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.



285
286
287
288
289
# File 'lib/ruby_sync/connectors/base_connector.rb', line 285

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



275
276
277
278
279
280
# File 'lib/ruby_sync/connectors/base_connector.rb', line 275

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



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

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)


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

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



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

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.



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

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



128
129
130
# File 'lib/ruby_sync/connectors/base_connector.rb', line 128

def digest(o)
  Digest::MD5.hexdigest(o.to_yaml)
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.



102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
# File 'lib/ruby_sync/connectors/base_connector.rb', line 102

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.



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

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.



295
296
297
298
# File 'lib/ruby_sync/connectors/base_connector.rb', line 295

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)


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

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)


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

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

#is_echo?(event) ⇒ Boolean

Returns:

  • (Boolean)


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

def is_echo? event; false end

#is_vault?Boolean

Returns:

  • (Boolean)


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

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.



200
201
202
# File 'lib/ruby_sync/connectors/base_connector.rb', line 200

def own_association_key_for(path)
  path
end

#path_for_association(association) ⇒ Object



249
250
251
252
253
254
# File 'lib/ruby_sync/connectors/base_connector.rb', line 249

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.



209
210
211
# File 'lib/ruby_sync/connectors/base_connector.rb', line 209

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



264
265
266
267
268
269
270
271
272
273
# File 'lib/ruby_sync/connectors/base_connector.rb', line 264

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



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

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

#remove_mirrorObject



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

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.



137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
# File 'lib/ruby_sync/connectors/base_connector.rb', line 137

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)



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

def started
end

#stopObject

Politely stop the connector.



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

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



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

def stopped; end

#sync_startedObject

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



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

def sync_started; end

#sync_stoppedObject

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



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

def sync_stopped; end

#test_add(id, details) ⇒ Object

Called by unit tests to inject data



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

def test_add id, details
  add id, details
end

#test_delete(id) ⇒ Object

Called by unit tests to delete a record



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

def test_delete id
  delete id
end

#test_modify(id, details) ⇒ Object

Called by unit tests to modify data



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

def test_modify id, details
  modify id, details
end