An object interface to LDAP entries.
Return the Treequel::Directory the Model will use for searches, creating it if it hasn’t been created already. The default Directory will be created by calling Treequel.directory_from_config.
# File lib/treequel/model.rb, line 82
def self::directory
self.directory = Treequel.directory_from_config unless @directory
return @directory
end
Set the Treequel::Directory that should be
used for searches. The receiving class will also be set as the
results_class of the newdirectory.
# File lib/treequel/model.rb, line 90
def self::directory=( newdirectory )
@directory = newdirectory
@directory.results_class = self if @directory
end
Never freeze converted values in Model objects.
# File lib/treequel/model.rb, line 184
def self::freeze_converted_values?; false; end
Inheritance callback – add a class-specific objectclass registry to inheriting classes.
# File lib/treequel/model.rb, line 97
def self::inherited( subclass )
super
subclass.instance_variable_set( :@objectclass_registry, SET_HASH.dup )
subclass.instance_variable_set( :@base_registry, SET_HASH.dup )
end
Return the mixins that should be applied to an entry with the given
dn.
# File lib/treequel/model.rb, line 167
def self::mixins_for_dn( dn )
dn_tuples = dn.downcase.split( %r\s*,\s*/ )
dn_keys = dn_tuples.reverse.inject(['']) do |keys, dnpair|
dnpair += ',' + keys.last unless keys.last.empty?
keys << dnpair
end
# Get the union of all of the mixin sets for the DN and all of its parents
union = self.base_registry.
values_at( *dn_keys ).
inject {|set1,set2| set1 | set2 }
return union
end
Return the mixins that should be applied to an entry with the given
objectclasses.
# File lib/treequel/model.rb, line 149
def self::mixins_for_objectclasses( *objectclasses )
return self.objectclass_registry[:top] if objectclasses.empty?
ocsymbols = objectclasses.flatten.collect {|oc| oc.untaint.to_sym }
# Get the union of all of the mixin sets for the objectclasses in question
mixins = self.objectclass_registry.
values_at( *ocsymbols ).
inject {|set1,set2| set1 | set2 }
# Return the mixins whose objectClass requirements are met by the
# specified objectclasses
return mixins.delete_if do |mixin|
!mixin.model_objectclasses.all? {|oc| ocsymbols.include?(oc) }
end
end
Override the default to extend new instances with applicable mixins if their entry is set.
# File lib/treequel/model.rb, line 207
def initialize( directory, dn, entry=nil, from_directory=false )
if from_directory
super( directory, dn, entry )
@dirty = false
else
super( directory, dn )
@values = symbolify_keys( entry ? entry : self.rdn_attributes )
@dirty = true
end
self.apply_applicable_mixins( @dn, @entry )
self.after_initialize
end
Create a new Treequel::Model object with the given
entry hash from the specified directory.
Overrides Treequel::Branch.new_from_entry
to pass the from_directory flag to mark it as unmodified.
# File lib/treequel/model.rb, line 190
def self::new_from_entry( entry, directory )
entry = Treequel::HashUtilities.stringify_keys( entry )
dnvals = entry.delete( 'dn' ) or
raise ArgumentError, "no 'dn' attribute for entry"
Treequel.logger.debug "Creating %p from entry: %p in directory: %s" %
[ self, dnvals.first, directory ]
return self.new( directory, dnvals.first, entry, true )
end
Register the given mixin for the specified
objectclasses. Instances that have all the specified
objectclasses will be extended with the mixin,
which should be a Module extended with Treequel::Model::ObjectClass.
# File lib/treequel/model.rb, line 107
def self::register_mixin( mixin )
objectclasses = mixin.model_objectclasses
bases = mixin.model_bases
bases << '' if bases.empty?
Treequel.logger.debug "registering %p [objectClasses: %p, base DNs: %p]" %
[ mixin, objectclasses, bases ]
# Register it with each of its objectClasses
objectclasses.each do |oc|
@objectclass_registry[ oc.to_sym ].add( mixin )
end
# ...and each of its bases
bases.each do |dn|
@base_registry[ dn.downcase ].add( mixin )
end
end
Unregister the given mixin for the specified
objectclasses.
# File lib/treequel/model.rb, line 128
def self::unregister_mixin( mixin )
objectclasses = mixin.model_objectclasses
bases = mixin.model_bases
bases << '' if bases.empty?
Treequel.logger.debug "un-registering %p [objectclasses: %p, base DNs: %p]" %
[ mixin, objectclasses, bases ]
# Unregister it from each of its bases
bases.each do |dn|
@base_registry[ dn.downcase ].delete( mixin )
end
# ...and each of its objectClasses
objectclasses.each do |oc|
@objectclass_registry[ oc.to_sym ].delete( mixin )
end
end
Index set operator – set attribute attrname to a new
value. Overridden to make Model
objects defer writing changes until #save is called.
# File lib/treequel/model.rb, line 265
def []=( attrname, value )
attrtype = self.find_attribute_type( attrname.to_sym ) or
raise ArgumentError, "unknown attribute %p" % [ attrname ]
value = Array( value ) unless attrtype.single?
self.mark_dirty
if value.nil?
@values.delete( attrtype.name.to_sym )
else
@values[ attrtype.name.to_sym ] = value
end
# If the objectClasses change, we (may) need to re-apply mixins
if attrname.to_s.downcase == 'objectclass'
self.log.debug " objectClass change -- reapplying mixins"
self.apply_applicable_mixins( self.dn )
else
self.log.debug " no objectClass changes -- no need to reapply mixins"
end
return value
end
Delete the specified attributes. Overridden to make Model objects defer writing changes until #save is called.
# File lib/treequel/model.rb, line 302
def delete( *attributes )
return super if attributes.empty?
self.log.debug "Deleting attributes: %p" % [ attributes ]
self.mark_dirty
attributes.flatten.each do |attribute|
# With a hash, delete each value for each key
if attribute.is_a?( Hash )
self.delete_specific_values( attribute )
# With an array of attributes to delete, replace
# MULTIPLE attribute types with an empty array, and SINGLE
# attribute types with nil
elsif attribute.respond_to?( :to_sym )
attrtype = self.find_attribute_type( attribute.to_sym )
if attrtype.single?
@values[ attribute.to_sym ] = nil
else
@values[ attribute.to_sym ] = []
end
else
raise ArgumentError,
"can't convert a %p to a Symbol or a Hash" % [ attribute.class ]
end
end
return true
end
Like delete, but runs destroy hooks before and after deleting.
# File lib/treequel/model.rb, line 485
def destroy( opts={} )
opts = DEFAULT_DESTROY_OPTIONS.merge( opts )
self.before_destroy or raise Treequel::BeforeHookFailed, :destroy
self.delete
self.after_destroy
return true
rescue Treequel::BeforeHookFailed => err
self.log.info( err.message )
raise if opts[:raise_on_failure]
end
Diff the specified values for the given attribute
against those in the directory entry and return LDAP::Mod objects for any differences.
# File lib/treequel/model.rb, line 425
def diff_with_entry( attribute, values )
mods = []
attribute = attribute.to_s
entry = self.entry || {}
entry_values = entry.key?( attribute ) ? entry[attribute] : []
# Workaround for the fact that Time has a #to_ary, causing it to become an
# Array of integers when cast via Array().
values = [ values ] if values.is_a?( Time )
values = Array( values ).compact.
collect {|val| self.get_converted_attribute(attribute, val) }
self.log.debug " comparing %s values to entry: %p vs. %p" %
[ attribute, values, entry_values ]
# If the attributes on the server are the same as the local ones,
# it's a NOOP.
if values.sort == entry_values.sort
self.log.debug " no change."
return nil
# If the directory doesn't have this attribute, but the local
# object does, it's an ADD
elsif entry_values.empty?
self.log.debug " ADD %s: %p" % [ attribute, values ]
return LDAP::Mod.new( LDAP::LDAP_MOD_ADD, attribute, values )
# ...or if the local value doesn't have anything for this attribute
# but the directory does, it's a DEL
elsif values.empty?
self.log.debug " DELETE %s" % [ attribute ]
return LDAP::Mod.new( LDAP::LDAP_MOD_DELETE, attribute )
# ...otherwise it's a REPLACE
else
self.log.debug " REPLACE %s: %p with %p" %
[ attribute, entry_values, values ]
return LDAP::Mod.new( LDAP::LDAP_MOD_REPLACE, attribute, values )
end
end
Returns the validation errors associated with this object.
# File lib/treequel/model.rb, line 335
def errors
return @errors ||= Treequel::Model::Errors.new
end
Return the Treequel::Model::ObjectClass mixins that have been applied to the receiver.
# File lib/treequel/model.rb, line 520
def extensions
eigenclass = ( class << self; self; end )
return eigenclass.included_modules.find_all do |mod|
(class << mod; self; end).include?(Treequel::Model::ObjectClass)
end
end
Copy initializer – re-apply mixins to duplicates, too.
# File lib/treequel/model.rb, line 223
def initialize_copy( other )
super
self.apply_applicable_mixins( @dn, @entry )
self.after_initialize
end
Return a human-readable representation of the receiving object, suitable for debugging.
# File lib/treequel/model.rb, line 529
def inspect
return "#<%s:0x%x (%s): %s>" % [
self.class.name,
self.object_id * 2,
self.loaded? ?
self.extensions.map( &:name ).join( ', ' ) :
'not yet loaded',
self.dn
]
end
Return the pending modifications for the object as an LDIF string.
# File lib/treequel/model.rb, line 469
def modification_ldif
mods = self.modifications
return LDAP::LDIF.mods_to_ldif( self.dn, mods )
end
Return any pending changes in the model object as an Array of LDAP::Mod objects.
# File lib/treequel/model.rb, line 408
def modifications
return unless self.modified?
self.log.debug "Gathering modifications..."
mods = []
@values.sort_by {|k, _| k.to_s }.each do |attribute, vals|
self.log.debug " finding mods for %s" % [ attribute ]
mod = self.diff_with_entry( attribute, vals ) or next
mods << mod
end
return mods
end
Tests whether the object has been modified since it was loaded from the directory.
# File lib/treequel/model.rb, line 251
def modified?
return @dirty ? true : false
end
Mark the object as unmodified.
# File lib/treequel/model.rb, line 257
def reset_dirty_flag
@dirty = false
end
Returns true if the receiver responds to the given method.
# File lib/treequel/model.rb, line 510
def respond_to?( sym, include_priv=false )
return super if caller(1).first =~ %r{/r?spec/} &&
caller(1).first !~ %rrespond_to/ # RSpec workaround
return true if super
plainsym, _ = attribute_from_method( sym )
return self.find_attribute_type( plainsym ) ? true : false
end
Revert to the attributes in the directory, discarding any pending changes.
# File lib/treequel/model.rb, line 476
def revert
self.clear_caches
@dirty = false
return true
end
Write any pending changes in the model object to the directory. The valid
opts are:
:raise_on_failure
raise a Treequel::ValidationFailed or Treequel::BeforeHookFailed if either the validations or before_{save,create}.
# File lib/treequel/model.rb, line 370
def save( opts={} )
opts = DEFAULT_SAVE_OPTIONS.merge( opts )
self.log.debug "Saving %s..." % [ self.dn ]
raise Treequel::ValidationFailed, self.errors unless self.valid?( opts )
self.log.debug " validation succeeded."
unless mods = self.modifications
self.log.debug " no modifications... no save necessary."
return false
end
self.log.debug " got %d modifications." % [ mods.length ]
self.before_save( mods ) or
raise Treequel::BeforeHookFailed, :save
if self.exists?
self.log.debug " already exists, so updating."
self.update( mods )
else
self.log.debug " doesn't exist, so creating."
self.create( mods )
end
self.after_save( mods )
return true
rescue Treequel::BeforeHookFailed => err
self.log.info( err.message )
raise if opts[:raise_on_failure]
rescue Treequel::ValidationFailed => err
self.log.error( "Save aborted: validation failed." )
self.log.info( err.errors.full_messages.join(', ') )
raise if opts[:raise_on_failure]
end
Override Treequel::Branch#search to inject the ‘objectClass’ attribute to the selected attribute list if there is one.
# File lib/treequel/model.rb, line 501
def search( scope=:subtree, filter='(objectClass=*)', parameters={}, &block )
parameters[:selectattrs] |= ['objectClass'] unless
!parameters.key?( :selectattrs ) || parameters[ :selectattrs ].empty?
super
end
Return true if the model object passes all of its validations.
# File lib/treequel/model.rb, line 341
def valid?( opts={} )
self.errors.clear
self.validate( opts )
return self.errors.empty? ? true : false
end
Validate the object with the specified options, appending
validation errors onto the errors
object.
# File lib/treequel/model.rb, line 350
def validate( options={} )
options = DEFAULT_VALIDATION_OPTIONS.merge( options )
self.before_validation or
raise Treequel::BeforeHookFailed, :validation
self.errors.add( :objectClass, 'must have at least one' ) if self.object_classes.empty?
super( options )
self.log.debug "Validations failed:\s %s" % [ self.errors.full_messages.join("\n ") ] if
self.errors.count.nonzero?
self.after_validation
end
Apply mixins that are applicable considering the receiver’s DN and the
objectClasses from the given entryhash merged with any unsaved
values.
# File lib/treequel/model.rb, line 699
def apply_applicable_mixins( dn, entryhash=nil )
objectclasses = @values[:objectClass] ||
(entryhash && entryhash['objectClass'])
return unless objectclasses
# self.log.debug "Applying mixins applicable to %s" % [ dn ]
schema = self.directory.schema
ocs = objectclasses.collect do |oc_oid|
explicit_oc = schema.object_classes[ oc_oid ]
explicit_oc.ancestors.collect {|oc| oc.name }
end.flatten.uniq
# self.log.debug " got %d candidate objectClasses: %p" % [ ocs.length, ocs ]
# The applicable mixins are those in the intersection of the ones
# inferred by its objectclasses and those that apply to its DN
oc_mixins = self.class.mixins_for_objectclasses( *ocs )
dn_mixins = self.class.mixins_for_dn( dn )
mixins = ( oc_mixins & dn_mixins )
# self.log.debug " %d mixins remain after intersection: %p" % [ mixins.length, mixins ]
mixins.each {|mod| self.extend(mod) }
end
Create the entry for the object, using the specified mods to
set the attributes.
# File lib/treequel/model.rb, line 563
def create( mods )
self.log.debug " entry doesn't exist: creating..."
self.before_create( mods ) or
raise Treequel::BeforeHookFailed, :create
super( mods )
self.after_create( mods )
end
Delete specific key/value pairs from the entry.
# File lib/treequel/model.rb, line 573
def delete_specific_values( pairs )
self.log.debug " hash-delete..."
# Ensure the value exists, and its values converted and cached, as
# the delete needs Ruby object instead of string comparison
pairs.each do |key, vals|
next unless self[ key ]
self.log.debug " deleting %p: %p" % [ key, vals ]
@values[ key ].delete_if {|val| vals.include?(val) }
end
end
Search for the Treequel::Schema::AttributeType
associated with sym.
# File lib/treequel/model.rb, line 588
def find_attribute_type( name )
attrtype = nil
# Try both the name as-is, and the camelCased version of it
camelcased_sym = name.to_s.gsub( %r_(\w)/ ) { $1.upcase }.to_sym
attrtype = self.valid_attribute_type( name ) ||
self.valid_attribute_type( camelcased_sym )
return attrtype
end
Overridden to apply applicable mixins to lazily-loaded objects once their entry has been looked up.
# File lib/treequel/model.rb, line 688
def lookup_entry
if entryhash = super
self.apply_applicable_mixins( self.dn, entryhash )
end
return entryhash
end
Make a predicate method body for the given attrtype.
# File lib/treequel/model.rb, line 673
def make_predicate( attrtype )
self.log.debug "Generating an attribute predicate for %p" % [ attrtype ]
attrname = attrtype.name
if attrtype.single?
self.log.debug " attribute is SINGLE, so generating a scalar predicate..."
return lambda { self[attrname] ? true : false }
else
self.log.debug " attribute isn't SINGLE, so generating an array predicate..."
return lambda { self[attrname].any? {|val| val} }
end
end
Make a reader method body for the given attrtype.
# File lib/treequel/model.rb, line 645
def make_reader( attrtype )
self.log.debug "Generating an attribute reader for %p" % [ attrtype ]
attrname = attrtype.name
return lambda do |*args|
if args.empty?
self[ attrname ]
else
self.traverse_branch( attrname, *args )
end
end
end
Make a writer method body for the given attrtype.
# File lib/treequel/model.rb, line 659
def make_writer( attrtype )
self.log.debug "Generating an attribute writer for %p" % [ attrtype ]
attrname = attrtype.name
if attrtype.single?
self.log.debug " attribute is SINGLE, so generating a scalar writer..."
return lambda {|newvalue| self[attrname] = newvalue }
else
self.log.debug " attribute isn't SINGLE, so generating an array writer..."
return lambda {|*newvalues| self[attrname] = newvalues.flatten }
end
end
Mark the object as having been modified.
# File lib/treequel/model.rb, line 547
def mark_dirty
@dirty = true
end
Proxy method – Handle calls to missing methods by searching for an attribute.
# File lib/treequel/model.rb, line 601
def method_missing( sym, *args )
self.log.debug "Dynamic dispatch to %p with args: %p" % [ sym, args ]
# First, if the entry hasn't yet been loaded, try loading it to make sure the
# object is already extended with any applicable objectClass mixins. If that ends
# up defining the method in question, call it.
if !@entry && self.entry
self.log.debug " entry wasn't loaded, looking for methods added by loading it..."
meth = begin
self.method( sym )
rescue NoMethodError, NameError => err
self.log.debug " it still didn't define %p: %s: %s" %
[ sym, err.class.name, err.message ]
nil
end
return meth.call( *args ) if meth
end
# self.log.debug " checking to see if it's a traversal call"
# Next, super to rdn-traversal if it looks like a reader but has arguments
plainsym, methodtype = attribute_from_method( sym )
return super if methodtype == :reader && !args.empty?
# Now make a method body for a new method based on what attributeType it is if
# it's a valid attribute
attrtype = self.find_attribute_type( plainsym ) or return super
methodbody = case methodtype
when :writer
self.make_writer( attrtype )
when :predicate
self.make_predicate( attrtype )
else
self.make_reader( attrtype )
end
# Define the new method and call it by fetching the corresponding Method object
# so we don't loop back through #method_missing if something goes wrong
self.class.send( :define_method, sym, &methodbody )
return self.method( sym ).call( *args )
end
Update the object’s entry with the specified mods.
# File lib/treequel/model.rb, line 553
def update( mods )
self.log.debug " entry already exists: updating..."
self.before_update( mods ) or
raise Treequel::BeforeHookFailed, :update
self.modify( mods )
self.after_update( mods )
end
| / | Search |
|---|---|
| ? | Show this help |