require 'active_support/core_ext/module/delegation'
require 'active_support/core_ext/object/blank'
module ActiveFedora
module Associations
extend ActiveSupport::Concern
autoload :HasManyAssociation, 'active_fedora/associations/has_many_association'
autoload :BelongsToAssociation, 'active_fedora/associations/belongs_to_association'
autoload :HasAndBelongsToManyAssociation, 'active_fedora/associations/has_and_belongs_to_many_association'
autoload :AssociationCollection, 'active_fedora/associations/association_collection'
autoload :AssociationProxy, 'active_fedora/associations/association_proxy'
private
# Returns the specified association instance if it responds to :loaded?, nil otherwise.
def association_instance_get(name)
ivar = "@#{name}"
if instance_variable_defined?(ivar)
association = instance_variable_get(ivar)
association if association.respond_to?(:loaded?)
end
end
# Set the specified association instance.
def association_instance_set(name, association)
instance_variable_set("@#{name}", association)
end
module ClassMethods
def has_many(association_id, options={})
raise "You must specify a property name for #{name}" if !options[:property]
has_relationship association_id.to_s, options[:property], :inbound => true
reflection = create_has_many_reflection(association_id, options)
collection_accessor_methods(reflection, HasManyAssociation)
end
def belongs_to(association_id, options = {})
raise "You must specify a property name for #{name}" if !options[:property]
has_relationship association_id.to_s, options[:property]
reflection = create_belongs_to_reflection(association_id, options)
association_accessor_methods(reflection, BelongsToAssociation)
# association_constructor_method(:build, reflection, BelongsToAssociation)
# association_constructor_method(:create, reflection, BelongsToAssociation)
#configure_dependency_for_belongs_to(reflection)
end
# Specifies a many-to-many relationship with another class. The relatioship is written to both classes simultaneously.
#
# Adds the following methods for retrieval and query:
#
# [collection(force_reload = false)]
# Returns an array of all the associated objects.
# An empty array is returned if none are found.
# [collection<<(object, ...)]
# Adds one or more objects to the collection by creating associations in the join table
# (collection.push and collection.concat are aliases to this method).
# Note that this operation instantly fires update sql without waiting for the save or update call on the
# parent object.
# [collection.delete(object, ...)]
# Removes one or more objects from the collection by removing their associations from the join table.
# This does not destroy the objects.
# [collection=objects]
# Replaces the collection's content by deleting and adding objects as appropriate.
# [collection_singular_ids]
# Returns an array of the associated objects' ids.
# [collection_singular_ids=ids]
# Replace the collection by the objects identified by the primary keys in +ids+.
# [collection.clear]
# Removes every object from the collection. This does not destroy the objects.
# [collection.empty?]
# Returns +true+ if there are no associated objects.
# [collection.size]
# Returns the number of associated objects.
#
# (+collection+ is replaced with the symbol passed as the first argument, so
# has_and_belongs_to_many :categories would add among others categories.empty?.)
#
# === Example
#
# A Developer class declares has_and_belongs_to_many :projects, which will add:
# * Developer#projects
# * Developer#projects<<
# * Developer#projects.delete
# * Developer#projects=
# * Developer#project_ids
# * Developer#project_ids=
# * Developer#projects.clear
# * Developer#projects.empty?
# * Developer#projects.size
# * Developer#projects.find(id)
# * Developer#projects.exists?(...)
# The declaration may include an options hash to specialize the behavior of the association.
#
# === Options
#
# [:class_name]
# Specify the class name of the association. Use it only if that name can't be inferred
# from the association name. So has_and_belongs_to_many :projects will by default be linked to the
# Project class, but if the real class name is SuperProject, you'll have to specify it with this option.
# [:property]
# REQUIRED Specify the predicate to use when storing the relationship.
#
# Option examples:
# has_and_belongs_to_many :projects, :property=>:works_on
# has_and_belongs_to_many :nations, :class_name => "Country", :property=>:is_citizen_of
def has_and_belongs_to_many(association_id, options = {}, &extension)
reflection = create_has_and_belongs_to_many_reflection(association_id, options, &extension)
collection_accessor_methods(reflection, HasAndBelongsToManyAssociation)
#configure_after_destroy_method_for_has_and_belongs_to_many(reflection)
#add_association_callbacks(reflection.name, options)
end
private
def create_has_many_reflection(association_id, options)
create_reflection(:has_many, association_id, options, self)
end
def create_belongs_to_reflection(association_id, options)
create_reflection(:belongs_to, association_id, options, self)
end
def create_has_and_belongs_to_many_reflection(association_id, options)
create_reflection(:has_and_belongs_to_many, association_id, options, self)
end
def association_accessor_methods(reflection, association_proxy_class)
redefine_method(reflection.name) do |*params|
force_reload = params.first unless params.empty?
association = association_instance_get(reflection.name)
if association.nil? || force_reload
association = association_proxy_class.new(self, reflection)
retval = force_reload ? reflection.klass.uncached { association.reload } : association.reload
if retval.nil? and association_proxy_class == BelongsToAssociation
association_instance_set(reflection.name, nil)
return nil
end
association_instance_set(reflection.name, association)
end
association.target.nil? ? nil : association
end
redefine_method("loaded_#{reflection.name}?") do
association = association_instance_get(reflection.name)
association && association.loaded?
end
redefine_method("#{reflection.name}=") do |new_value|
association = association_instance_get(reflection.name)
if association.nil? || association.target != new_value
association = association_proxy_class.new(self, reflection)
end
association.replace(new_value)
association_instance_set(reflection.name, new_value.nil? ? nil : association)
end
redefine_method("set_#{reflection.name}_target") do |target|
return if target.nil? and association_proxy_class == BelongsToAssociation
association = association_proxy_class.new(self, reflection)
association.target = target
association_instance_set(reflection.name, association)
end
redefine_method("#{reflection.name}_id=") do |new_value|
obj = new_value.empty? ? nil : reflection.klass.find(new_value)
send("#{reflection.name}=", obj)
end
redefine_method("#{reflection.name}_id") do
obj = send("#{reflection.name}")
obj.pid if obj
end
end
def collection_reader_method(reflection, association_proxy_class)
redefine_method(reflection.name) do |*params|
force_reload = params.first unless params.empty?
association = association_instance_get(reflection.name)
unless association
association = association_proxy_class.new(self, reflection)
association_instance_set(reflection.name, association)
end
association.reload if force_reload
association
end
redefine_method("#{reflection.name.to_s.singularize}_ids") do
send(reflection.name).map { |r| r.pid }
end
end
def collection_accessor_methods(reflection, association_proxy_class, writer = true)
collection_reader_method(reflection, association_proxy_class)
if writer
redefine_method("#{reflection.name}=") do |new_value|
# Loads proxy class instance (defined in collection_reader_method) if not already loaded
association = send(reflection.name)
association.replace(new_value)
association
end
redefine_method("#{reflection.name.to_s.singularize}_ids=") do |new_value|
ids = (new_value || []).reject { |nid| nid.blank? }
#TODO, like this when find() can return multiple records
#send("#{reflection.name}=", reflection.klass.find(ids))
send("#{reflection.name}=", ids.collect { |id| reflection.klass.find(id)})
end
end
end
end
end
end