commit 5a48a5214365a057a5c7065c0e4a5b2cc833bff4
Author: Marc Dequènes (Duck) <Duck@DuckCorp.org>
Date:   Sun Oct 18 17:11:38 2009 +0200

    [cleanup] improve elements classes generation (with a few more checks)

diff --git a/lib/ldap_shadows/aspect.rb b/lib/ldap_shadows/aspect.rb
deleted file mode 100644
index e9cb5ae..0000000
--- a/lib/ldap_shadows/aspect.rb
+++ /dev/null
@@ -1,27 +0,0 @@
-#--
-# LdapShadows, a Medium-level LDAP Access Library and Tool.
-# Copyright (c) 2009 Marc Dequènes (Duck) <Duck@DuckCorp.org>
-#
-# This program is free software: you can redistribute it and/or modify
-# it under the terms of the GNU General Public License as published by
-# the Free Software Foundation, either version 3 of the License, or
-# (at your option) any later version.
-#
-# This program is distributed in the hope that it will be useful,
-# but WITHOUT ANY WARRANTY; without even the implied warranty of
-# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
-# GNU General Public License for more details.
-#
-# You should have received a copy of the GNU General Public License
-# along with this program.  If not, see <http://www.gnu.org/licenses/>.
-#++
-
-
-require 'ldap_shadows/hooks'
-
-
-module LdapShadows
-  class LdapAspect
-    include Hooks
-  end
-end
diff --git a/lib/ldap_shadows/elements/aspect.rb b/lib/ldap_shadows/elements/aspect.rb
new file mode 100644
index 0000000..cbc6ca4
--- /dev/null
+++ b/lib/ldap_shadows/elements/aspect.rb
@@ -0,0 +1,29 @@
+#--
+# LdapShadows, a Medium-level LDAP Access Library and Tool.
+# Copyright (c) 2009 Marc Dequènes (Duck) <Duck@DuckCorp.org>
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program.  If not, see <http://www.gnu.org/licenses/>.
+#++
+
+
+require 'ldap_shadows/hooks'
+
+
+module LdapShadows
+module Elements
+  class LdapAspect
+    include Hooks
+  end
+end
+end
diff --git a/lib/ldap_shadows/elements/object.rb b/lib/ldap_shadows/elements/object.rb
new file mode 100755
index 0000000..368fe17
--- /dev/null
+++ b/lib/ldap_shadows/elements/object.rb
@@ -0,0 +1,731 @@
+#--
+# LdapShadows, a Medium-level LDAP Access Library and Tool.
+# Copyright (c) 2009 Marc Dequènes (Duck) <Duck@DuckCorp.org>
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program.  If not, see <http://www.gnu.org/licenses/>.
+#++
+
+
+require 'ldap_shadows/hooks'
+
+
+module LdapShadows
+module Elements
+  class LdapObject < ActiveLdap::Base
+    include Hooks
+
+    class_inheritable_accessor :presentation, :relations_info
+
+    ldap_mapping :prefix => '', :classes => ['top'], :scope => :sub
+
+    # temporary method until active_ldap is fixed: return a DN object (see activeldap#23932)
+    def dn_obj
+      ActiveLdap::DistinguishedName.parse(self.dn)
+    end
+
+    # temporary method until active_ldap is fixed: return a DN object (see activeldap#23932)
+    def self.base_obj
+      ActiveLdap::DistinguishedName.parse(self.base)
+    end
+
+    def name
+      name = self[dn_attribute].is_a?(Array) ? self[dn_attribute][0] : self[dn_attribute]
+      name.strip
+    end
+
+    def has_field?(field)
+      return false if field.downcase == "objectclass"
+      has_attribute?(field)
+    end
+
+    def human_name
+      attr_list = ['displayName', 'cn']
+      attr_list.unshift(self.class.presentation[:name_attribute]) if self.class.presentation.has_key?(:name_attribute)
+      attr_list.each do |attr|
+        if attr == 'dn'
+          return self.dn
+        elsif self.attribute_present?(attr)
+          val = self.send(attr, true)
+          return val[0].strip
+        end
+      end
+      return ""
+    end
+
+    def human_description
+      attr_list = ['description']
+      attr_list.unshift(self.class.presentation[:desc_attribute]) if self.class.presentation.has_key?(:desc_attribute)
+      attr_list.each do |attr|
+        if self.attribute_present?(attr)
+          return self[attr].is_a?(Array) ? self[attr][0] : self[attr]
+        end
+      end
+      return ""
+    end
+
+    def possible_relations
+      self.associations.collect {|assoc| assoc.to_s } - ['children']
+    end
+
+    def relations
+      rel_list = []
+
+      rel_list += self.class.presentation[:associated_relations] if self.class.presentation[:associated_relations]
+
+      aspects.each do |aspect|
+        aspect_data = self.class.shadow.get_aspect(aspect)
+        if defined?(aspect_data[:presentation][:associated_relations]) and aspect_data[:presentation][:associated_relations]
+          rel_list += aspect_data[:presentation][:associated_relations]
+        end
+      end
+
+      rel_list & possible_relations
+    end
+
+    def self.possible_aspects
+      return [] unless self.presentation[:allowed_aspects]
+      self.presentation[:allowed_aspects].collect{|key| key.to_s }.sort
+    end
+
+    def aspects
+      present_aspects = []
+      (self.class.presentation[:allowed_aspects] || []).each do |aspect|
+        aspect_data = self.class.shadow.get_aspect(aspect)
+        aspect_mapping = aspect_data[:mapping]
+        present_aspects << aspect if self.classes & aspect_mapping[:classes] == aspect_mapping[:classes]
+      end
+
+      present_aspects
+    end
+
+    def self.objectclasses_attr_list(objectclass_list)
+      objectclass_list = [objectclass_list] unless objectclass_list.is_a? Array
+      list = []
+      objectclass_list.each do |objectclass|
+        objectclass_obj = ActiveLdap::Base.schema.object_class(objectclass)
+        attr_list = objectclass_obj.must + objectclass_obj.may
+        list += attr_list.collect{|attr| attr.human_attribute_name }
+      end
+      list
+    end
+
+    def self.possible_attributes
+      self.objectclasses_attr_list(self.required_classes)
+    end
+
+    def self.possible_attributes_for_aspect(aspect)
+      aspect_data = self.shadow.get_aspect(aspect)
+      self.objectclasses_attr_list(aspect_data[:mapping][:classes])
+    end
+
+    def info_for_relation(rel)
+      return nil unless self.relations.include?(rel)
+
+      self.relations_info[rel.to_sym]
+    end
+
+    def organized_data
+      ignored_attrs = self.class.shadow.get_shadow_config[:hidden_attributes] || []
+      ignored_attrs += self.class.presentation[:hidden_attributes] || []
+      attr_list = self.nonempty_attributes - ignored_attrs
+
+      expert_attributes = (self.class.presentation[:expert_attributes] || [])
+      admin_attributes = attr_list.select do |attr|
+        ActiveLdap::Base.schema.attribute(attr).operational?
+      end
+
+      aspects = self.aspects
+      rel_list = self.possible_relations
+
+      # first pass to take aspects forced relations into account
+      obj_aspects = {}
+      aspects.each do |aspect|
+        aspect_data = self.class.shadow.get_aspect(aspect)
+
+        if defined?(aspect_data[:presentation][:associated_attributes]) and aspect_data[:presentation][:associated_attributes]
+          taken_attr_list = aspect_data[:presentation][:associated_attributes] & (attr_list + ignored_attrs)
+          unless taken_attr_list.empty?
+            obj_aspects[aspect] ||= {}
+            obj_aspects[aspect].merge!(fetch_attributes_data(taken_attr_list, expert_attributes, admin_attributes))
+            attr_list -= taken_attr_list
+          end
+        end
+
+        if defined?(aspect_data[:presentation][:associated_relations]) and aspect_data[:presentation][:associated_relations]
+          taken_rel_list = aspect_data[:presentation][:associated_relations] & rel_list
+          unless taken_rel_list.empty?
+            obj_aspects[aspect] ||= {}
+            obj_aspects[aspect].merge!(fetch_relations_data(taken_rel_list, expert_attributes))
+            rel_list -= taken_rel_list
+          end
+        end
+      end
+
+      # manage general attributes
+      obj_info = {}
+      if self.class.presentation[:associate_unclaimed_attributes]
+        taken_attr_list = attr_list
+      else
+        taken_attr_list = admin_attributes
+        if self.class.presentation.has_key?(:associated_attributes)
+          taken_attr_list += self.class.presentation[:associated_attributes]
+        end
+        taken_attr_list += self.class.possible_attributes
+      end
+      taken_attr_list = taken_attr_list.uniq & attr_list
+      obj_info = fetch_attributes_data(taken_attr_list, expert_attributes, admin_attributes)
+      attr_list -= taken_attr_list
+
+      # manage general relations
+      if self.class.presentation[:associated_relations]
+        taken_rel_list = self.class.presentation[:associated_relations] & rel_list
+        unless taken_rel_list.empty?
+          obj_info.merge!(fetch_relations_data(taken_rel_list, expert_attributes))
+          rel_list -= taken_rel_list
+        end
+      end
+
+      # second pass to dispath the remaining attributes
+      unless attr_list.empty?
+        aspects.each do |aspect|
+          taken_attr_list = (self.class.possible_attributes_for_aspect(aspect) & attr_list)
+          obj_aspects[aspect] ||= {}
+          obj_aspects[aspect].merge!(fetch_attributes_data(taken_attr_list, expert_attributes, admin_attributes))
+          attr_list -= taken_attr_list
+
+          break if attr_list.empty?
+        end
+      end
+
+      [obj_info, obj_aspects]
+    end
+
+    def item_modify_from_string(str)
+      unless str =~ /^([a-zA-Z]*(?::[a-zA-Z]+)?)(=|\+=|-=)(.*)$/
+        raise SyntaxError, _("modification parameter '%s' is invalid") % str
+      end
+      key = $1
+      op = $2
+      val = $3
+
+      item_modify(key, op, val)
+    end
+
+    def item_modify(key, op, val)
+      if key.index(":")
+        type, field = key.split(":")
+      else
+        type = nil
+        field = key
+      end
+
+      case type
+      when nil
+        item_modify_field(key, op, val)
+      when 'rel'
+        item_modify_relation(field, op, val)
+      when ''
+        case field
+        when 'aspects'
+          item_modify_aspects(op, val)
+        else
+          raise PreProcessingError, _("Unknown core field '%s'") % field
+        end
+      else
+        raise PreProcessingError, _("Unknown type '%s' for field '%s'") % [type, field]
+      end
+    end
+
+    def item_modify_field(field, op, val)
+      unless self.has_field?(field)
+        raise PreProcessingError, _("No such field '%s' in object '%s'") % [field, self.class.handle]
+      end
+
+      attr_info = ActiveLdap::Base.schema.attribute(field)
+      if attr_info.read_only?
+        raise PreProcessingError, _("The field '%s' cannot be modified (read only)") % field
+      end
+
+      if attr_info.binary?
+        unless File.exists?(val)
+          raise PreProcessingError, _("The field '%s' contains binary data, you must provide a filename instead of a direct value") % field
+        end
+
+        begin
+          val = File.read(val)
+        rescue
+          raise PreProcessingError, _("The file for the binary field '%s' cannot be read: ") % [field, $!]
+        end
+      end
+
+      old_val = self.send(field, true)
+
+      # if val is nil or the latest value is removed, then the attribute is removed from the object,
+      case op
+      when '='
+        return false if val == old_val
+
+        val = [val] if old_val.is_a? Enumerable
+        self.send(field + "=", val)
+
+      when '+='
+        if attr_info.single_value?
+          raise PreProcessingError, _("The field '%s' cannot hold more than one value") % field
+        end
+
+        return false if old_val.include?(val)
+        new_val = old_val << val
+        self.send(field + "=", new_val)
+
+      when '-='
+        return false unless old_val.include?(val)
+
+        new_val = old_val - [val]
+        self.send(field + "=", new_val)
+
+      else
+        raise SyntaxError, _("Unknown operator '%s'") % op
+      end
+
+      true
+    end
+
+    def item_modify_relation(rel, op, val)
+      unless self.relations.include?(rel)
+        raise PreProcessingError, _("No such relation '%s' for object '%s'") % [rel, self.class.handle]
+      end
+
+      rel_info = self.info_for_relation(rel)
+      if rel_info[:read_only]
+        raise PreProcessingError, _("The relation '%s' cannot be modified (read only)") % rel
+      end
+
+      # fetch remote object in relation, which will be the real 'val'
+      foreign_item_list = rel_info[:foreign_klass].find(:all, val)
+      if foreign_item_list.empty?
+        raise PreProcessingError, _("Foreign item '%s' for relation '%s' not found") % [val, rel]
+      end
+      if foreign_item_list.size > 1
+        raise WeirdError, _("Ambiguous item '%s' for relation '%s' (%s possible items)") %
+          [val, rel, foreign_item_list.size]
+      end
+
+      old_val = self.send(rel)
+      val = foreign_item_list.first
+
+      # if val is nil or the latest value is removed, then the association's attribute is removed from one side
+      case op
+      when '='
+        val = [val] if old_val.is_a? Enumerable
+        self.send(rel + "=", val)
+
+      when '+='
+        if rel_info[:single_value]
+          raise PreProcessingError, _("The relation '%s' cannot hold more than one foreign item") % rel
+        end
+
+        return false if old_val.include?(val)
+
+        self.send(rel) << val
+
+      when '-='
+        return false unless old_val.include?(val)
+
+        self.send(rel).delete(val)
+
+      else
+        raise SyntaxError, _("Unknown operator '%s'") % op
+      end
+
+      true
+    end
+
+    def item_modify_aspects(op, aspect)
+      unless self.class.possible_aspects.include?(aspect)
+        raise PreProcessingError, _("No such aspect '%s' for object '%s'") % [aspect, self.class.handle]
+      end
+
+      case op
+      when '='
+        raise PreProcessingError, _("The equality operator is not possible for aspects")
+
+      when '+='
+        return false if self.aspects.include?(aspect)
+
+        self.add_aspect(aspect)
+
+      when '-='
+        return false unless self.aspects.include?(aspect)
+
+        self.remove_aspect(aspect)
+
+      else
+        raise SyntaxError, _("Unknown operator '%s'") % op
+      end
+
+      true
+    end
+
+    def self.items_find_from_strings(str_list)
+      ldap_search_objects = "(objectClass=*)"
+      ldap_search_aspects = ""
+      ldap_search_fields = []
+      str_list.each do |str|
+        unless str =~ /^([a-zA-Z]*(?::[a-zA-Z]+)?)(=|~=)(.*)$/
+          raise SyntaxError, _("search parameter '%s' is invalid") % str
+        end
+        key = $1
+        op = $2
+        val = $3
+
+        if key.index(":")
+          type, field = key.split(":")
+        else
+          type = nil
+          field = key
+        end
+
+        case type
+        when nil
+          ldap_search_fields << ldap_search_string_field(field, op, val)
+        when 'rel'
+          raise PreProcessingError, _("Searching relations is not implemented yet")
+        when ''
+          case field
+          when 'objects'
+            ldap_search_objects = ldap_search_string_objects(field, op, val)
+          when 'aspects'
+            ldap_search_aspects = ldap_search_string_aspects(field, op, val)
+          else
+            raise PreProcessingError, _("Unknown core field '%s'") % field
+          end
+        else
+          raise PreProcessingError, _("Unknown type '%s' for field '%s'") % [type, field]
+        end
+      end
+      ldap_search_string = "(&" + ldap_search_objects + ldap_search_aspects + ldap_search_fields.join + ")"
+
+      LdapObject.find(:all, :scope => :sub, :filter => ldap_search_string, :attributes => ["objectClass"])
+    end
+
+    def self.ldap_search_string_field(field, op, val)
+      esc_val = self.connection.escape_filter_value(val)
+      case op
+      when "="
+        "(#{field}=#{esc_val})"
+
+      when "~="
+        raise PreProcessingError, _("Searching with regex is not implemented yet")
+
+      else
+        raise SyntaxError, _("Unknown operator '%s'") % op
+      end
+    end
+
+    def self.ldap_search_string_objects(field, op, val_list)
+      ldap_search_parts = val_list.split(",").collect do |val|
+        obj_hdl = val.downcase.singularize
+        obj_klass = $ldapctl.find_klass(obj_hdl)
+        raise PreProcessingError, _("No such object '%s'") % val if obj_klass.nil?
+
+        ldap_classes = obj_klass.required_classes
+
+        case op
+        when "="
+          "(&" + ldap_classes.collect{|cl| "(objectClass=#{cl})" }.join + ")"
+
+        when "~="
+          raise PreProcessingError, _("Searching with regex is not possible with objects")
+
+        else
+          raise SyntaxError, _("Unknown operator '%s'") % op
+        end
+      end
+      "(|" + ldap_search_parts.join + ")"
+    end
+
+    def self.ldap_search_string_aspects(field, op, val_list)
+      ldap_search_parts = val_list.split(",").collect do |val|
+        aspect_data = self.class.shadow.get_aspect(val)
+        raise PreProcessingError, _("No such aspect '%s'") % val if aspect_data.nil?
+
+        ldap_classes = aspect_data[:mapping][:classes]
+
+        case op
+        when "="
+          "(&" + ldap_classes.collect{|cl| "(objectClass=#{cl})" }.join + ")"
+
+        when "~="
+          raise PreProcessingError, _("Searching with regex is not possible with aspects")
+
+        else
+          raise SyntaxError, _("Unknown operator '%s'") % op
+        end
+      end
+      "(&" + ldap_search_parts.join + ")"
+    end
+
+    def self.find_raw_item_object(raw_item)
+      self.class.shadow.objects.each do |obj_hdl|
+        obj_klass = $ldapctl.find_klass(obj_hdl)
+        ldap_classes = obj_klass.required_classes
+        return obj_hdl if raw_item.classes & ldap_classes == ldap_classes
+      end
+      nil
+    end
+
+    def self.raw_item_info(raw_item)
+      obj_hdl = self.find_raw_item_object(raw_item)
+      if obj_hdl
+        obj_klass = $ldapctl.find_klass(obj_hdl)
+        item = obj_klass.new(raw_item.dn)
+        {:name => "#{obj_hdl}/#{item.name}", :item => item, :object => obj_klass}
+      else
+        {:name => "unknown/#{raw_item.dn}"}
+      end
+    end
+
+    def family_parent_dn
+      pdn = self.dn_obj.dup
+      pdn.shift
+      pdn
+    end
+
+    def family_parent
+      # nothing found when using LdapObject, that's weird, so using ActiveLdap::Base
+      # until further investigation
+      #LdapObject.find(:first, :base => self.family_parent_dn.to_s, :scope => :base)
+      ActiveLdap::Base.find(:first, :base => self.family_parent_dn.to_s, :scope => :base)
+    end
+
+    def family_children
+      LdapObject.find(:all, :base => self.dn, :scope => :one)
+    end
+
+    def family_children_dn
+      self.family_children.collect {|obj| obj.dn }
+    end
+
+    def family_siblings
+      # cannot substract, as the ruby object signature may be different
+      LdapObject.find(:all, :base => self.family_parent_dn.to_s, :scope => :one).select{|obj| obj.dn != self.dn }
+    end
+
+    def family_siblings_dn
+      self.family_siblings.collect {|obj| obj.dn }
+    end
+
+    def modified_attributes
+      list = []
+      prepare_data_for_saving do |data, ldap_data|
+        list = collect_modified_attributes(ldap_data, data)
+        false
+      end
+      list
+    end
+
+    def add_aspect(aspect)
+      return unless self.class.possible_aspects.include?(aspect)
+
+      aspect_data = self.class.shadow.get_aspect(aspect)
+      add_class(*aspect_data[:mapping][:classes])
+
+      # recursive dependency enforcement
+      depends = aspect_data[:mapping][:depend_aspects] || []
+      depends.each do |dep_aspect|
+        add_aspect(dep_aspect)
+      end
+    end
+
+    def remove_aspect(aspect)
+      return unless self.class.possible_aspects.include?(aspect)
+
+      aspect_data = self.class.shadow.get_aspect(aspect)
+      remove_class(*aspect_data[:mapping][:classes])
+    end
+
+    def nonempty_attributes
+      self.attributes.collect{|key, val| (val.nil? or val == []) ? nil : key }.compact
+    end
+
+    def missing_attributes
+      self.must.collect{|attr| attr.name } - self.nonempty_attributes - ['objectClass']
+    end
+
+    def delete(options = {})
+      before_delete_jobs
+      super(options)
+      after_save_jobs
+    end
+
+    def delete_recursive
+      # TODO: recursive instanciation and reverse recursive hook calls
+      before_delete_jobs
+      self.class.delete_all(nil, :scope => :sub, :base => self.dn)
+      after_delete_jobs
+    end
+
+    protected
+
+    def create_or_update
+      before_save_jobs
+      r = super
+      after_save_jobs
+      r
+    end
+
+    def before_save_jobs
+      check_hooks_before(:save)
+      check_missing_attributes
+    end
+
+    def before_delete_jobs
+      check_hooks_before(:delete)
+    end
+
+    def check_hooks_before(action)
+      case action
+      when :save
+        if self.new_entry?
+          self.class.hook_before_create(self.class.shadow, self)
+        else
+          self.class.hook_before_modify(self.class.shadow, self)
+        end
+      when :delete
+        self.class.hook_before_delete(self.class.shadow, self)
+      end
+
+      self.aspects.each do |aspect|
+        aklass = self.class.shadow.get_aspect_klass(aspect)
+        next if aklass.nil?
+
+        case action
+        when :save
+          if self.new_entry?
+            aklass.hook_before_create(self.class.shadow, self)
+          else
+            aklass.hook_before_modify(self.class.shadow, self)
+          end
+        when :delete
+          aklass.hook_before_delete(self.class.shadow, self)
+        end
+      end
+    end
+
+    def check_missing_attributes
+      missing_fields = self.missing_attributes
+      unless missing_fields.empty?
+        miss_str = []
+        missing_fields.each do |field|
+          str = Translator.translate_field_name(field)
+          str += " [#{field}]" if $program_options[:handles]
+          miss_str << str
+        end
+        raise PreProcessingError, _("Cannot save the item; the following fields are missing: %s") %
+          miss_str.join(", ")
+      end
+    end
+
+    def after_save_jobs
+      check_hooks_after(:save)
+    end
+
+    def after_delete_jobs
+      check_hooks_after(:delete)
+    end
+
+    def check_hooks_after(action)
+      self.aspects.each do |aspect|
+        aklass = self.class.shadow.get_aspect_klass(aspect)
+        next if aklass.nil?
+
+        case action
+        when :save
+          if self.new_entry?
+            aklass.hook_after_create(self.class.shadow, self)
+          else
+            aklass.hook_after_modify(self.class.shadow, self)
+          end
+        when :delete
+          aklass.hook_after_delete(self.class.shadow, self)
+        end
+      end
+
+      case action
+      when :save
+        if self.new_entry?
+          self.class.hook_after_create(self.class.shadow, self)
+        else
+          self.class.hook_after_modify(self.class.shadow, self)
+        end
+      when :delete
+        self.class.hook_after_delete(self.class.shadow, self)
+      end
+    end
+
+    def fetch_attributes_data(attr_list, expert_attributes, admin_attributes)
+      attr_data = self.attributes.collect do |key, val|
+        if attr_list.include?(key)
+          [key, {
+             :value => val,
+             :multiple => (val.is_a?(Array) ? val.size : 1),
+             :expert => expert_attributes.include?(key),
+             :admin => admin_attributes.include?(key),
+             :binary => ActiveLdap::Base.schema.attribute(key).binary?
+           }]
+        else
+          nil
+        end
+      end
+      Hash[attr_data.compact]
+    end
+
+    def fetch_relations_data(rel_list, expert_attributes)
+      rel_data = rel_list.collect do |rel|
+        data = self.send(rel)
+        if data.is_a? Enumerable
+          if data.empty?
+            value = nil
+          else
+            value = data.collect{|g| g.name }
+            multiple = true
+          end
+        else
+          # the exists? method also ensure the object is loaded
+          if data.exists?
+            value = data.name
+          else
+            value = nil
+          end
+          multiple = false
+        end
+
+        if value.nil?
+          nil
+        else
+          rel_key = "rel:" + rel
+          [rel_key, {
+             :value => value,
+             :multiple => multiple,
+             :expert => expert_attributes.include?(rel_key),
+             :admin => false,
+             :binary => false
+           }]
+        end
+      end
+      Hash[rel_data.compact]
+    end
+  end
+end
+end
diff --git a/lib/ldap_shadows/object.rb b/lib/ldap_shadows/object.rb
deleted file mode 100755
index d1b89a5..0000000
--- a/lib/ldap_shadows/object.rb
+++ /dev/null
@@ -1,730 +0,0 @@
-#--
-# LdapShadows, a Medium-level LDAP Access Library and Tool.
-# Copyright (c) 2009 Marc Dequènes (Duck) <Duck@DuckCorp.org>
-#
-# This program is free software: you can redistribute it and/or modify
-# it under the terms of the GNU General Public License as published by
-# the Free Software Foundation, either version 3 of the License, or
-# (at your option) any later version.
-#
-# This program is distributed in the hope that it will be useful,
-# but WITHOUT ANY WARRANTY; without even the implied warranty of
-# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
-# GNU General Public License for more details.
-#
-# You should have received a copy of the GNU General Public License
-# along with this program.  If not, see <http://www.gnu.org/licenses/>.
-#++
-
-
-require 'ldap_shadows/hooks'
-
-
-module LdapShadows
-  class LdapObject < ActiveLdap::Base
-    include Hooks
-
-    class_inheritable_accessor :presentation, :relations_info
-
-    ldap_mapping :prefix => '', :classes => ['top'], :scope => :sub
-
-    # temporary method until active_ldap is fixed: return a DN object (see activeldap#23932)
-    def dn_obj
-      ActiveLdap::DistinguishedName.parse(self.dn)
-    end
-
-    # temporary method until active_ldap is fixed: return a DN object (see activeldap#23932)
-    def self.base_obj
-      ActiveLdap::DistinguishedName.parse(self.base)
-    end
-
-    def name
-      name = self[dn_attribute].is_a?(Array) ? self[dn_attribute][0] : self[dn_attribute]
-      name.strip
-    end
-
-    def has_field?(field)
-      return false if field.downcase == "objectclass"
-      has_attribute?(field)
-    end
-
-    def human_name
-      attr_list = ['displayName', 'cn']
-      attr_list.unshift(self.class.presentation[:name_attribute]) if self.class.presentation.has_key?(:name_attribute)
-      attr_list.each do |attr|
-        if attr == 'dn'
-          return self.dn
-        elsif self.attribute_present?(attr)
-          val = self.send(attr, true)
-          return val[0].strip
-        end
-      end
-      return ""
-    end
-
-    def human_description
-      attr_list = ['description']
-      attr_list.unshift(self.class.presentation[:desc_attribute]) if self.class.presentation.has_key?(:desc_attribute)
-      attr_list.each do |attr|
-        if self.attribute_present?(attr)
-          return self[attr].is_a?(Array) ? self[attr][0] : self[attr]
-        end
-      end
-      return ""
-    end
-
-    def possible_relations
-      self.associations.collect {|assoc| assoc.to_s } - ['children']
-    end
-
-    def relations
-      rel_list = []
-
-      rel_list += self.class.presentation[:associated_relations] if self.class.presentation[:associated_relations]
-
-      aspects.each do |aspect|
-        aspect_data = self.class.shadow.get_aspect(aspect)
-        if defined?(aspect_data[:presentation][:associated_relations]) and aspect_data[:presentation][:associated_relations]
-          rel_list += aspect_data[:presentation][:associated_relations]
-        end
-      end
-
-      rel_list & possible_relations
-    end
-
-    def self.possible_aspects
-      return [] unless self.presentation[:allowed_aspects]
-      self.presentation[:allowed_aspects].collect{|key| key.to_s }.sort
-    end
-
-    def aspects
-      present_aspects = []
-      (self.class.presentation[:allowed_aspects] || []).each do |aspect|
-        aspect_data = self.class.shadow.get_aspect(aspect)
-        aspect_mapping = aspect_data[:mapping]
-        present_aspects << aspect if self.classes & aspect_mapping[:classes] == aspect_mapping[:classes]
-      end
-
-      present_aspects
-    end
-
-    def self.objectclasses_attr_list(objectclass_list)
-      objectclass_list = [objectclass_list] unless objectclass_list.is_a? Array
-      list = []
-      objectclass_list.each do |objectclass|
-        objectclass_obj = ActiveLdap::Base.schema.object_class(objectclass)
-        attr_list = objectclass_obj.must + objectclass_obj.may
-        list += attr_list.collect{|attr| attr.human_attribute_name }
-      end
-      list
-    end
-
-    def self.possible_attributes
-      self.objectclasses_attr_list(self.required_classes)
-    end
-
-    def self.possible_attributes_for_aspect(aspect)
-      aspect_data = self.shadow.get_aspect(aspect)
-      self.objectclasses_attr_list(aspect_data[:mapping][:classes])
-    end
-
-    def info_for_relation(rel)
-      return nil unless self.relations.include?(rel)
-
-      self.relations_info[rel.to_sym]
-    end
-
-    def organized_data
-      ignored_attrs = self.class.shadow.get_shadow_config[:hidden_attributes] || []
-      ignored_attrs += self.class.presentation[:hidden_attributes] || []
-      attr_list = self.nonempty_attributes - ignored_attrs
-
-      expert_attributes = (self.class.presentation[:expert_attributes] || [])
-      admin_attributes = attr_list.select do |attr|
-        ActiveLdap::Base.schema.attribute(attr).operational?
-      end
-
-      aspects = self.aspects
-      rel_list = self.possible_relations
-
-      # first pass to take aspects forced relations into account
-      obj_aspects = {}
-      aspects.each do |aspect|
-        aspect_data = self.class.shadow.get_aspect(aspect)
-
-        if defined?(aspect_data[:presentation][:associated_attributes]) and aspect_data[:presentation][:associated_attributes]
-          taken_attr_list = aspect_data[:presentation][:associated_attributes] & (attr_list + ignored_attrs)
-          unless taken_attr_list.empty?
-            obj_aspects[aspect] ||= {}
-            obj_aspects[aspect].merge!(fetch_attributes_data(taken_attr_list, expert_attributes, admin_attributes))
-            attr_list -= taken_attr_list
-          end
-        end
-
-        if defined?(aspect_data[:presentation][:associated_relations]) and aspect_data[:presentation][:associated_relations]
-          taken_rel_list = aspect_data[:presentation][:associated_relations] & rel_list
-          unless taken_rel_list.empty?
-            obj_aspects[aspect] ||= {}
-            obj_aspects[aspect].merge!(fetch_relations_data(taken_rel_list, expert_attributes))
-            rel_list -= taken_rel_list
-          end
-        end
-      end
-
-      # manage general attributes
-      obj_info = {}
-      if self.class.presentation[:associate_unclaimed_attributes]
-        taken_attr_list = attr_list
-      else
-        taken_attr_list = admin_attributes
-        if self.class.presentation.has_key?(:associated_attributes)
-          taken_attr_list += self.class.presentation[:associated_attributes]
-        end
-        taken_attr_list += self.class.possible_attributes
-      end
-      taken_attr_list = taken_attr_list.uniq & attr_list
-      obj_info = fetch_attributes_data(taken_attr_list, expert_attributes, admin_attributes)
-      attr_list -= taken_attr_list
-
-      # manage general relations
-      if self.class.presentation[:associated_relations]
-        taken_rel_list = self.class.presentation[:associated_relations] & rel_list
-        unless taken_rel_list.empty?
-          obj_info.merge!(fetch_relations_data(taken_rel_list, expert_attributes))
-          rel_list -= taken_rel_list
-        end
-      end
-
-      # second pass to dispath the remaining attributes
-      unless attr_list.empty?
-        aspects.each do |aspect|
-          taken_attr_list = (self.class.possible_attributes_for_aspect(aspect) & attr_list)
-          obj_aspects[aspect] ||= {}
-          obj_aspects[aspect].merge!(fetch_attributes_data(taken_attr_list, expert_attributes, admin_attributes))
-          attr_list -= taken_attr_list
-
-          break if attr_list.empty?
-        end
-      end
-
-      [obj_info, obj_aspects]
-    end
-
-    def item_modify_from_string(str)
-      unless str =~ /^([a-zA-Z]*(?::[a-zA-Z]+)?)(=|\+=|-=)(.*)$/
-        raise SyntaxError, _("modification parameter '%s' is invalid") % str
-      end
-      key = $1
-      op = $2
-      val = $3
-
-      item_modify(key, op, val)
-    end
-
-    def item_modify(key, op, val)
-      if key.index(":")
-        type, field = key.split(":")
-      else
-        type = nil
-        field = key
-      end
-
-      case type
-      when nil
-        item_modify_field(key, op, val)
-      when 'rel'
-        item_modify_relation(field, op, val)
-      when ''
-        case field
-        when 'aspects'
-          item_modify_aspects(op, val)
-        else
-          raise PreProcessingError, _("Unknown core field '%s'") % field
-        end
-      else
-        raise PreProcessingError, _("Unknown type '%s' for field '%s'") % [type, field]
-      end
-    end
-
-    def item_modify_field(field, op, val)
-      unless self.has_field?(field)
-        raise PreProcessingError, _("No such field '%s' in object '%s'") % [field, self.class.handle]
-      end
-
-      attr_info = ActiveLdap::Base.schema.attribute(field)
-      if attr_info.read_only?
-        raise PreProcessingError, _("The field '%s' cannot be modified (read only)") % field
-      end
-
-      if attr_info.binary?
-        unless File.exists?(val)
-          raise PreProcessingError, _("The field '%s' contains binary data, you must provide a filename instead of a direct value") % field
-        end
-
-        begin
-          val = File.read(val)
-        rescue
-          raise PreProcessingError, _("The file for the binary field '%s' cannot be read: ") % [field, $!]
-        end
-      end
-
-      old_val = self.send(field, true)
-
-      # if val is nil or the latest value is removed, then the attribute is removed from the object,
-      case op
-      when '='
-        return false if val == old_val
-
-        val = [val] if old_val.is_a? Enumerable
-        self.send(field + "=", val)
-
-      when '+='
-        if attr_info.single_value?
-          raise PreProcessingError, _("The field '%s' cannot hold more than one value") % field
-        end
-
-        return false if old_val.include?(val)
-        new_val = old_val << val
-        self.send(field + "=", new_val)
-
-      when '-='
-        return false unless old_val.include?(val)
-
-        new_val = old_val - [val]
-        self.send(field + "=", new_val)
-
-      else
-        raise SyntaxError, _("Unknown operator '%s'") % op
-      end
-
-      true
-    end
-
-    def item_modify_relation(rel, op, val)
-      unless self.relations.include?(rel)
-        raise PreProcessingError, _("No such relation '%s' for object '%s'") % [rel, self.class.handle]
-      end
-
-      rel_info = self.info_for_relation(rel)
-      if rel_info[:read_only]
-        raise PreProcessingError, _("The relation '%s' cannot be modified (read only)") % rel
-      end
-
-      # fetch remote object in relation, which will be the real 'val'
-      foreign_item_list = rel_info[:foreign_klass].find(:all, val)
-      if foreign_item_list.empty?
-        raise PreProcessingError, _("Foreign item '%s' for relation '%s' not found") % [val, rel]
-      end
-      if foreign_item_list.size > 1
-        raise WeirdError, _("Ambiguous item '%s' for relation '%s' (%s possible items)") %
-          [val, rel, foreign_item_list.size]
-      end
-
-      old_val = self.send(rel)
-      val = foreign_item_list.first
-
-      # if val is nil or the latest value is removed, then the association's attribute is removed from one side
-      case op
-      when '='
-        val = [val] if old_val.is_a? Enumerable
-        self.send(rel + "=", val)
-
-      when '+='
-        if rel_info[:single_value]
-          raise PreProcessingError, _("The relation '%s' cannot hold more than one foreign item") % rel
-        end
-
-        return false if old_val.include?(val)
-
-        self.send(rel) << val
-
-      when '-='
-        return false unless old_val.include?(val)
-
-        self.send(rel).delete(val)
-
-      else
-        raise SyntaxError, _("Unknown operator '%s'") % op
-      end
-
-      true
-    end
-
-    def item_modify_aspects(op, aspect)
-      unless self.class.possible_aspects.include?(aspect)
-        raise PreProcessingError, _("No such aspect '%s' for object '%s'") % [aspect, self.class.handle]
-      end
-
-      case op
-      when '='
-        raise PreProcessingError, _("The equality operator is not possible for aspects")
-
-      when '+='
-        return false if self.aspects.include?(aspect)
-
-        self.add_aspect(aspect)
-
-      when '-='
-        return false unless self.aspects.include?(aspect)
-
-        self.remove_aspect(aspect)
-
-      else
-        raise SyntaxError, _("Unknown operator '%s'") % op
-      end
-
-      true
-    end
-
-    def self.items_find_from_strings(str_list)
-      ldap_search_objects = "(objectClass=*)"
-      ldap_search_aspects = ""
-      ldap_search_fields = []
-      str_list.each do |str|
-        unless str =~ /^([a-zA-Z]*(?::[a-zA-Z]+)?)(=|~=)(.*)$/
-          raise SyntaxError, _("search parameter '%s' is invalid") % str
-        end
-        key = $1
-        op = $2
-        val = $3
-
-        if key.index(":")
-          type, field = key.split(":")
-        else
-          type = nil
-          field = key
-        end
-
-        case type
-        when nil
-          ldap_search_fields << ldap_search_string_field(field, op, val)
-        when 'rel'
-          raise PreProcessingError, _("Searching relations is not implemented yet")
-        when ''
-          case field
-          when 'objects'
-            ldap_search_objects = ldap_search_string_objects(field, op, val)
-          when 'aspects'
-            ldap_search_aspects = ldap_search_string_aspects(field, op, val)
-          else
-            raise PreProcessingError, _("Unknown core field '%s'") % field
-          end
-        else
-          raise PreProcessingError, _("Unknown type '%s' for field '%s'") % [type, field]
-        end
-      end
-      ldap_search_string = "(&" + ldap_search_objects + ldap_search_aspects + ldap_search_fields.join + ")"
-
-      LdapObject.find(:all, :scope => :sub, :filter => ldap_search_string, :attributes => ["objectClass"])
-    end
-
-    def self.ldap_search_string_field(field, op, val)
-      esc_val = self.connection.escape_filter_value(val)
-      case op
-      when "="
-        "(#{field}=#{esc_val})"
-
-      when "~="
-        raise PreProcessingError, _("Searching with regex is not implemented yet")
-
-      else
-        raise SyntaxError, _("Unknown operator '%s'") % op
-      end
-    end
-
-    def self.ldap_search_string_objects(field, op, val_list)
-      ldap_search_parts = val_list.split(",").collect do |val|
-        obj_hdl = val.downcase.singularize
-        obj_klass = $ldapctl.find_klass(obj_hdl)
-        raise PreProcessingError, _("No such object '%s'") % val if obj_klass.nil?
-
-        ldap_classes = obj_klass.required_classes
-
-        case op
-        when "="
-          "(&" + ldap_classes.collect{|cl| "(objectClass=#{cl})" }.join + ")"
-
-        when "~="
-          raise PreProcessingError, _("Searching with regex is not possible with objects")
-
-        else
-          raise SyntaxError, _("Unknown operator '%s'") % op
-        end
-      end
-      "(|" + ldap_search_parts.join + ")"
-    end
-
-    def self.ldap_search_string_aspects(field, op, val_list)
-      ldap_search_parts = val_list.split(",").collect do |val|
-        aspect_data = self.class.shadow.get_aspect(val)
-        raise PreProcessingError, _("No such aspect '%s'") % val if aspect_data.nil?
-
-        ldap_classes = aspect_data[:mapping][:classes]
-
-        case op
-        when "="
-          "(&" + ldap_classes.collect{|cl| "(objectClass=#{cl})" }.join + ")"
-
-        when "~="
-          raise PreProcessingError, _("Searching with regex is not possible with aspects")
-
-        else
-          raise SyntaxError, _("Unknown operator '%s'") % op
-        end
-      end
-      "(&" + ldap_search_parts.join + ")"
-    end
-
-    def self.find_raw_item_object(raw_item)
-      self.class.shadow.objects.each do |obj_hdl|
-        obj_klass = $ldapctl.find_klass(obj_hdl)
-        ldap_classes = obj_klass.required_classes
-        return obj_hdl if raw_item.classes & ldap_classes == ldap_classes
-      end
-      nil
-    end
-
-    def self.raw_item_info(raw_item)
-      obj_hdl = self.find_raw_item_object(raw_item)
-      if obj_hdl
-        obj_klass = $ldapctl.find_klass(obj_hdl)
-        item = obj_klass.new(raw_item.dn)
-        {:name => "#{obj_hdl}/#{item.name}", :item => item, :object => obj_klass}
-      else
-        {:name => "unknown/#{raw_item.dn}"}
-      end
-    end
-
-    def family_parent_dn
-      pdn = self.dn_obj.dup
-      pdn.shift
-      pdn
-    end
-
-    def family_parent
-      # nothing found when using LdapObject, that's weird, so using ActiveLdap::Base
-      # until further investigation
-      #LdapObject.find(:first, :base => self.family_parent_dn.to_s, :scope => :base)
-      ActiveLdap::Base.find(:first, :base => self.family_parent_dn.to_s, :scope => :base)
-    end
-
-    def family_children
-      LdapObject.find(:all, :base => self.dn, :scope => :one)
-    end
-
-    def family_children_dn
-      self.family_children.collect {|obj| obj.dn }
-    end
-
-    def family_siblings
-      # cannot substract, as the ruby object signature may be different
-      LdapObject.find(:all, :base => self.family_parent_dn.to_s, :scope => :one).select{|obj| obj.dn != self.dn }
-    end
-
-    def family_siblings_dn
-      self.family_siblings.collect {|obj| obj.dn }
-    end
-
-    def modified_attributes
-      list = []
-      prepare_data_for_saving do |data, ldap_data|
-        list = collect_modified_attributes(ldap_data, data)
-        false
-      end
-      list
-    end
-
-    def add_aspect(aspect)
-      return unless self.class.possible_aspects.include?(aspect)
-
-      aspect_data = self.class.shadow.get_aspect(aspect)
-      add_class(*aspect_data[:mapping][:classes])
-
-      # recursive dependency enforcement
-      depends = aspect_data[:mapping][:depend_aspects] || []
-      depends.each do |dep_aspect|
-        add_aspect(dep_aspect)
-      end
-    end
-
-    def remove_aspect(aspect)
-      return unless self.class.possible_aspects.include?(aspect)
-
-      aspect_data = self.class.shadow.get_aspect(aspect)
-      remove_class(*aspect_data[:mapping][:classes])
-    end
-
-    def nonempty_attributes
-      self.attributes.collect{|key, val| (val.nil? or val == []) ? nil : key }.compact
-    end
-
-    def missing_attributes
-      self.must.collect{|attr| attr.name } - self.nonempty_attributes - ['objectClass']
-    end
-
-    def delete(options = {})
-      before_delete_jobs
-      super(options)
-      after_save_jobs
-    end
-
-    def delete_recursive
-      # TODO: recursive instanciation and reverse recursive hook calls
-      before_delete_jobs
-      self.class.delete_all(nil, :scope => :sub, :base => self.dn)
-      after_delete_jobs
-    end
-
-    protected
-
-    def create_or_update
-      before_save_jobs
-      r = super
-      after_save_jobs
-      r
-    end
-
-    def before_save_jobs
-      check_hooks_before(:save)
-      check_missing_attributes
-    end
-
-    def before_delete_jobs
-      check_hooks_before(:delete)
-    end
-
-    def check_hooks_before(action)
-      case action
-      when :save
-        if self.new_entry?
-          self.class.hook_before_create(self.class.shadow, self)
-        else
-          self.class.hook_before_modify(self.class.shadow, self)
-        end
-      when :delete
-        self.class.hook_before_delete(self.class.shadow, self)
-      end
-
-      self.aspects.each do |aspect|
-        aklass = self.class.shadow.get_aspect_klass(aspect)
-        next if aklass.nil?
-
-        case action
-        when :save
-          if self.new_entry?
-            aklass.hook_before_create(self.class.shadow, self)
-          else
-            aklass.hook_before_modify(self.class.shadow, self)
-          end
-        when :delete
-          aklass.hook_before_delete(self.class.shadow, self)
-        end
-      end
-    end
-
-    def check_missing_attributes
-      missing_fields = self.missing_attributes
-      unless missing_fields.empty?
-        miss_str = []
-        missing_fields.each do |field|
-          str = Translator.translate_field_name(field)
-          str += " [#{field}]" if $program_options[:handles]
-          miss_str << str
-        end
-        raise PreProcessingError, _("Cannot save the item; the following fields are missing: %s") %
-          miss_str.join(", ")
-      end
-    end
-
-    def after_save_jobs
-      check_hooks_after(:save)
-    end
-
-    def after_delete_jobs
-      check_hooks_after(:delete)
-    end
-
-    def check_hooks_after(action)
-      self.aspects.each do |aspect|
-        aklass = self.class.shadow.get_aspect_klass(aspect)
-        next if aklass.nil?
-
-        case action
-        when :save
-          if self.new_entry?
-            aklass.hook_after_create(self.class.shadow, self)
-          else
-            aklass.hook_after_modify(self.class.shadow, self)
-          end
-        when :delete
-          aklass.hook_after_delete(self.class.shadow, self)
-        end
-      end
-
-      case action
-      when :save
-        if self.new_entry?
-          self.class.hook_after_create(self.class.shadow, self)
-        else
-          self.class.hook_after_modify(self.class.shadow, self)
-        end
-      when :delete
-        self.class.hook_after_delete(self.class.shadow, self)
-      end
-    end
-
-    def fetch_attributes_data(attr_list, expert_attributes, admin_attributes)
-      attr_data = self.attributes.collect do |key, val|
-        if attr_list.include?(key)
-          [key, {
-             :value => val,
-             :multiple => (val.is_a?(Array) ? val.size : 1),
-             :expert => expert_attributes.include?(key),
-             :admin => admin_attributes.include?(key),
-             :binary => ActiveLdap::Base.schema.attribute(key).binary?
-           }]
-        else
-          nil
-        end
-      end
-      Hash[attr_data.compact]
-    end
-
-    def fetch_relations_data(rel_list, expert_attributes)
-      rel_data = rel_list.collect do |rel|
-        data = self.send(rel)
-        if data.is_a? Enumerable
-          if data.empty?
-            value = nil
-          else
-            value = data.collect{|g| g.name }
-            multiple = true
-          end
-        else
-          # the exists? method also ensure the object is loaded
-          if data.exists?
-            value = data.name
-          else
-            value = nil
-          end
-          multiple = false
-        end
-
-        if value.nil?
-          nil
-        else
-          rel_key = "rel:" + rel
-          [rel_key, {
-             :value => value,
-             :multiple => multiple,
-             :expert => expert_attributes.include?(rel_key),
-             :admin => false,
-             :binary => false
-           }]
-        end
-      end
-      Hash[rel_data.compact]
-    end
-  end
-end
-
diff --git a/lib/ldap_shadows/shadow.rb b/lib/ldap_shadows/shadow.rb
index 26b81b3..531ea97 100755
--- a/lib/ldap_shadows/shadow.rb
+++ b/lib/ldap_shadows/shadow.rb
@@ -22,8 +22,8 @@ require 'jcode'
 require 'active_ldap'
 require 'ldap_shadows/activeldap_fixes'
 require 'facets/metaid'
-require 'ldap_shadows/object'
-require 'ldap_shadows/aspect'
+require 'ldap_shadows/elements/object'
+require 'ldap_shadows/elements/aspect'
 
 
 module LdapShadows
@@ -39,16 +39,14 @@ module LdapShadows
       @config = Config.instance
 
       # cannot use anonymous modules/classes, as active_ldap needs named classes for relations
-      module_name = "Shadow" + name.capitalize
+      module_name = "Shadow" + name.classify
       @container = LdapShadows::Shadows.module_eval(<<-EOS)
         module #{module_name}; self; end
       EOS
 
-      @objects = {}
+      @container_elements = {}
       @object_definitions = {}
       @aspects = {}
-      # TODO: should replace @aspects properly one day
-      @aspects2 = {}
     end
 
     def set_shadow_config(shadow_def_raw)
@@ -61,26 +59,13 @@ module LdapShadows
     end
 
     def add_aspect(aspect_name, aspect_def_raw)
-      aspect_def = @config.parse_and_validate(aspect_name, 'aspect', aspect_def_raw)
-      klass_name = "LdapAspect" + aspect_name.to_s.capitalize
-      klass_content = @config.load_hook_content(@name, 'aspect', aspect_name)
+      e_type = 'aspect'
 
-      begin
-        klass = @container.module_eval(<<-EOS)
-          class #{klass_name} < LdapAspect; self; end
-        EOS
-        klass.instance_variable_set(:@handle, aspect_name)
-        klass.instance_variable_set(:@shadow, self)
-        klass.meta_eval do
-          attr_reader :handle, :shadow
-        end
-        klass.class_eval(klass_content) unless klass_content.nil?
-      rescue
-        raise PreProcessingError, _("Could not load Aspect plugin '%s': %s") % [aspect_name, $!]
-      end
+      klass = add_container_element(e_type, aspect_name)
+      aspect_def = @config.parse_and_validate(aspect_name, e_type, aspect_def_raw)
 
+      # TODO: remove this crap
       @aspects[aspect_name.to_sym] = aspect_def
-      @aspects2[aspect_name] = klass
     end
 
     def get_aspect(aspect_name)
@@ -88,39 +73,23 @@ module LdapShadows
     end
 
     def get_aspect_klass(aspect_name)
-      @aspects2[aspect_name]
+      @container_elements['aspect'][aspect_name.to_s]
     end
 
     def add_object(object_name, object_def_raw)
-      object_def = @config.parse_and_validate(object_name, 'object', object_def_raw)
-      klass_name = "LdapObject" + object_name.to_s.capitalize
-      klass_content = @config.load_hook_content(@name, 'object', object_name)
+      e_type = 'object'
 
-      begin
-        klass = @container.module_eval(<<-EOS)
-          class #{klass_name} < LdapObject; self; end
-        EOS
-        klass.instance_variable_set(:@handle, object_name)
-        klass.instance_variable_set(:@shadow, self)
-        klass.meta_eval do
-          attr_reader :handle, :shadow
-        end
-        klass.class_eval(klass_content) unless klass_content.nil?
-      rescue
-        raise PreProcessingError, _("Could not load Object plugin '%s': %s") % [object_name, $!]
-      end
-
-      # configure class
+      klass = add_container_element(e_type, object_name)
+      object_def = @config.parse_and_validate(object_name, 'object', object_def_raw)
       klass.presentation = object_def[:presentation]
       klass.ldap_mapping object_def[:mapping]
 
-      # store definition for later relations processing
+      # TODO: remove this crap
       @object_definitions[object_name.to_sym] = object_def
-      @objects[object_name] = klass
     end
 
     def find_klass(object_name)
-      @objects[object_name.to_s]
+      @container_elements['object'][object_name.to_s]
     end
 
     # run it _once_ when all objects are loaded
@@ -169,7 +138,47 @@ module LdapShadows
     end
 
     def objects
-      @objects.keys.sort
+      @container_elements['object'].keys.sort
+    end
+
+    def aspects
+      @container_elements['aspect'].keys.sort
+    end
+
+    protected
+
+    def add_container_element(e_type, e_name)
+      superklass_location = LdapShadows::Elements
+      superklass_name = 'Ldap' + e_type.classify
+      unless superklass_location.const_defined?(superklass_name)
+        raise PreProcessingError, _("Element model '%s' does not exist") % e_type
+      end
+
+      klass_name = superklass_name + e_name.to_s.classify
+      if @container.const_defined?(klass_name)
+        raise PreProcessingError, _("Element '%s' for model '%s' as already been defined") % [e_name, e_type]
+      end
+
+      begin
+        klass = @container.module_eval(<<-EOS)
+          class #{klass_name} < #{superklass_location}::#{superklass_name}; self; end
+        EOS
+        klass.instance_variable_set(:@handle, e_name)
+        klass.instance_variable_set(:@shadow, self)
+        klass.meta_eval do
+          attr_reader :handle, :shadow
+        end
+
+        klass_content = @config.load_hook_content(@name, e_type, e_name)
+        klass.class_eval(klass_content) unless klass_content.nil?
+      rescue
+        raise PreProcessingError, _("Could not load '%s' plugin for '%s': %s") % [e_type, e_name, $!]
+      end
+
+      @container_elements[e_type] ||= {}
+      @container_elements[e_type][e_name] = klass
+
+      klass
     end
   end
 end
