class Treequel::Model
An object interface to LDAP
entries.
Constants
- AFTER_HOOKS
The hooks that are called after an action
- BEFORE_HOOKS
The hooks that are called before an action
- DEFAULT_DESTROY_OPTIONS
Defaults for
destroy
options- DEFAULT_SAVE_OPTIONS
Defaults for
save
options- DEFAULT_VALIDATION_OPTIONS
Defaults for
validate
options- HOOKS
Hooks the user can override
- SET_HASH
A prototype Hash that autovivifies its members as Sets, for use in the
objectclass_registry
and thebase_registry
Attributes
Unsaved attribute values hash
Public Class Methods
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 90 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 98 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 192 def self::freeze_converted_values?; false; end
Inheritance callback – add a class-specific objectclass registry to inheriting classes.
# File lib/treequel/model.rb, line 105 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 175 def self::mixins_for_dn( dn ) dn_tuples = dn.downcase.split( /\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 157 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.
Treequel::Branch::new
# File lib/treequel/model.rb, line 215 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 198 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 115 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 136 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
Public Instance Methods
Index set operator – set attribute attrname
to a new value
. Overridden to make Model
objects defer writing changes until Treequel::Model#save
is called.
# File lib/treequel/model.rb, line 273 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 Treequel::Model#save
is called.
Treequel::Branch#delete
# File lib/treequel/model.rb, line 310 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 495 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 435 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 343 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 530 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 231 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 539 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
Make the changes to the object specified by the given attributes
. Overridden to make Model
objects defer writing changes until Treequel::Model#save
is called.
# File lib/treequel/model.rb, line 300 def merge( attributes ) attributes.each do |attrname, value| self[ attrname ] = value end end
Return the pending modifications for the object as an LDIF string.
# File lib/treequel/model.rb, line 479 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 418 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 259 def modified? return @dirty ? true : false end
Mark the object as unmodified.
# File lib/treequel/model.rb, line 265 def reset_dirty_flag @dirty = false end
Returns true
if the receiver responds to the given method.
# File lib/treequel/model.rb, line 520 def respond_to?( sym, include_priv=false ) return super if caller(1).first =~ %r{/r?spec/} && caller(1).first !~ /respond_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 486 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
orTreequel::BeforeHookFailed
if either the validations or before_{save,create}.
# File lib/treequel/model.rb, line 378 def save( opts={} ) opts = DEFAULT_SAVE_OPTIONS.merge( opts ) self.log.debug "Saving %s..." % [ self.dn ] if opts[ :validate ] raise Treequel::ValidationFailed, self.errors unless self.valid?( opts ) self.log.debug " validation succeeded." end 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 Branch#search to inject the 'objectClass' attribute to the selected attribute list if there is one.
Treequel::Branch#search
# File lib/treequel/model.rb, line 511 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 349 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 358 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
Protected Instance Methods
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 718 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.
Treequel::Branch#create
# File lib/treequel/model.rb, line 573 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 583 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
Ensure the entry is loaded and then return a Method object for the method of the specified name
. Returns nil
if the method isn't defined.
# File lib/treequel/model.rb, line 612 def entry_method( name ) self.entry self.log.debug "Looking up entry method %p" % [ name ] return nil unless self.singleton_class.method_defined?( name ) return self.method( name ) end
Search for the Treequel::Schema::AttributeType
associated with sym
.
# File lib/treequel/model.rb, line 598 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( /_(\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.
Treequel::Branch#lookup_entry
# File lib/treequel/model.rb, line 707 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 692 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 664 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 678 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 557 def mark_dirty @dirty = true end
Proxy method – Handle calls to missing methods by searching for an attribute.
Treequel::Branch#method_missing
# File lib/treequel/model.rb, line 630 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 (( meth = self.entry_method(sym) )) return meth.call( *args ) end # 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
Hook method – return true if the method sym
is handled by method_missing
.
# File lib/treequel/model.rb, line 621 def respond_to_missing?( sym, include_all ) return true if self.entry_method( sym ) plainsym, _ = attribute_from_method( sym ) return true if self.find_attribute_type( plainsym ) return super end
Update the object's entry with the specified mods
.
# File lib/treequel/model.rb, line 563 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
Private Instance Methods
Given the symbol from an attribute accessor or predicate, return the name of the corresponding LDAP
attribute/
# File lib/treequel/model.rb, line 750 def attribute_from_method( methodname ) case methodname.to_s when /^(?:has_)?([a-z]\w*)\?$/i return $1.to_sym, :predicate when /^([a-z]\w*)(=)?$/i return $1.to_sym, ($2 ? :writer : :reader ) end end
Turn a String DN into a reversed set of DN attribute/value pairs
# File lib/treequel/model.rb, line 762 def make_dn_tuples( dn ) return dn.split( /\s*,\s*/ ).reverse end