require 'active_support/core_ext/enumerable' require 'active_support/core_ext/module/delegation' require 'active_support/core_ext/object/blank' module ActiveFedora class InverseOfAssociationNotFoundError < RuntimeError #:nodoc: def initialize(reflection, associated_class = nil) super("Could not find the inverse association for #{reflection.name} (#{reflection.options[:inverse_of].inspect} in #{associated_class.nil? ? reflection.class_name : associated_class.name})") end end module Associations extend ActiveSupport::Concern extend ActiveSupport::Autoload autoload :Association autoload :SingularAssociation autoload :RDF autoload :SingularRDF autoload :CollectionAssociation autoload :CollectionProxy autoload :ContainerProxy autoload :HasManyAssociation autoload :BelongsToAssociation autoload :HasAndBelongsToManyAssociation autoload :BasicContainsAssociation autoload :DirectlyContainsAssociation autoload :DirectlyContainsOneAssociation autoload :IndirectlyContainsAssociation autoload :ContainsAssociation autoload :FilterAssociation autoload :OrdersAssociation autoload :DeleteProxy autoload :ContainedFinder autoload :RecordComposite autoload :IDComposite autoload :NullValidator module Builder autoload :Association, 'active_fedora/associations/builder/association' autoload :SingularAssociation, 'active_fedora/associations/builder/singular_association' autoload :CollectionAssociation, 'active_fedora/associations/builder/collection_association' autoload :BelongsTo, 'active_fedora/associations/builder/belongs_to' autoload :HasMany, 'active_fedora/associations/builder/has_many' autoload :HasAndBelongsToMany, 'active_fedora/associations/builder/has_and_belongs_to_many' autoload :Contains, 'active_fedora/associations/builder/contains' autoload :DirectlyContains, 'active_fedora/associations/builder/directly_contains' autoload :DirectlyContainsOne, 'active_fedora/associations/builder/directly_contains_one' autoload :IndirectlyContains, 'active_fedora/associations/builder/indirectly_contains' autoload :Property, 'active_fedora/associations/builder/property' autoload :SingularProperty, 'active_fedora/associations/builder/singular_property' autoload :Aggregation, 'active_fedora/associations/builder/aggregation' autoload :Filter, 'active_fedora/associations/builder/filter' autoload :Orders, 'active_fedora/associations/builder/orders' end eager_autoload do autoload :AssociationScope end # Clears out the association cache. def clear_association_cache #:nodoc: @association_cache.clear if persisted? end # :nodoc: attr_reader :association_cache # Returns the association instance for the given name, instantiating it if it doesn't already exist def association(name) #:nodoc: association = association_instance_get(name) if association.nil? reflection = self.class._reflect_on_association(name) association = reflection.association_class.new(self, reflection) if reflection association_instance_set(name, association) if association end association end def delete(*) reflections.each_pair do |name, reflection| association(name.to_sym).delete_all if reflection.macro == :has_many end super end private # Returns the specified association instance if it responds to :loaded?, nil otherwise. def association_instance_get(name) raise "use a symbol" if name.is_a? String @association_cache[name] end # Set the specified association instance. def association_instance_set(name, association) @association_cache[name] = association end module ClassMethods # This method is used to declare an ldp:DirectContainer on a resource # you must specify an is_member_of_relation or a has_member_relation # # @param [String] name the handle to refer to this child as # @param [Hash] options # @option options [String] :class_name ('ActiveFedora::File') The name of the class that will represent the contained resources # @option options [RDF::URI] :has_member_relation the rdf predicate to use for the ldp:hasMemberRelation # @option options [RDF::URI] :is_member_of_relation the rdf predicate to use for the ldp:isMemberOfRelation # # example: # class FooHistory < ActiveFedora::Base # directly_contains :files, has_member_relation: # ::RDF::URI.new("http://example.com/hasFiles"), class_name: 'Thing' # directly_contains :other_stuff, is_member_of_relation: # ::RDF::URI.new("http://example.com/isContainedBy"), class_name: 'Thing' # end # def directly_contains(name, options = {}) Builder::DirectlyContains.build(self, name, { class_name: 'ActiveFedora::File' }.merge(options)) end def directly_contains_one(name, options = {}) Builder::DirectlyContainsOne.build(self, name, { class_name: 'ActiveFedora::File' }.merge(options)) end # This method is used to declare an ldp:IndirectContainer on a resource # you must specify an is_member_of_relation or a has_member_relation # # @param [String] name the handle to refer to this child as # @param [Hash] options # @option options [String] :class_name ('ActiveFedora::File') The name of the class that will represent the contained resources # @option options [RDF::URI] :has_member_relation the rdf predicate to use for the ldp:hasMemberRelation # @option options [RDF::URI] :is_member_of_relation the rdf predicate to use for the ldp:isMemberOfRelation # @option options [RDF::URI] :inserted_content_relation the rdf predicate to use for the ldp:insertedContentRelation # @option options [String] :through name of a class to represent the interstitial node # @option options [Symbol] :foreign_key property that points at the remote resource # # example: # class Proxy < ActiveFedora::Base # belongs_to :proxy_for, predicate: ::RDF::URI.new('http://www.openarchives.org/ore/terms/proxyFor'), class_name: 'ActiveFedora::Base' # end # # class FooHistory < ActiveFedora::Base # indirectly_contains :files, has_member_relation: RDF::Vocab::ORE.aggregates, # inserted_content_relation: RDF::Vocab::ORE.proxyFor, class_name: 'Thing', # through: 'Proxy', foreign_key: :proxy_for # # indirectly_contains :other_stuff, is_member_of_relation: # ::RDF::URI.new("http://example.com/isContainedBy"), class_name: 'Thing', # through: 'Proxy', foreign_key: :proxy_for # end # def indirectly_contains(name, options = {}) Builder::IndirectlyContains.build(self, name, options) end def has_many(name, options = {}) Builder::HasMany.build(self, name, options) end # This method is used to specify the details of a contained resource. # Pass the name as the first argument and a hash of options as the second argument # Note that this method doesn't actually execute the block, but stores it, to be executed # by any the implementation of the resource(specified as :class_name) # # @param [String] name the handle to refer to this child as # @param [Hash] options # @option options [Class] :class_name The class that will represent this child, should extend ``ActiveFedora::File'' or ``ActiveFedora::Base'' # @option options [String] :url # @option options [Boolean] :autocreate Always create this resource on new objects # @yield block executed by some types of child resources def contains(name, options = {}, &block) options[:block] = block if block raise ArgumentError, "You must provide a name (dsid) for the datastream" unless name Associations::Builder::Contains.build(self, name.to_sym, options) end # Specifies a one-to-one association with another class. This method should only be used # if this class contains the foreign key. # # Methods will be added for retrieval and query for a single associated object, for which # this object holds an id: # # [association()] # Returns the associated object. +nil+ is returned if none is found. # [association=(associate)] # Assigns the associate object, extracts the primary key, and sets it as the foreign key. # # (+association+ is replaced with the symbol passed as the first argument, so # belongs_to :author would add among others author.nil?.) # # === Example # # A Post class declares belongs_to :author, which will add: # * Post#author (similar to Author.find(author_id)) # * Post#author=(author) # The declaration can also include an options hash to specialize the behavior of the association. # # === Options # # [:predicate] # the association predicate to use when storing the association +REQUIRED+ # [: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_one :author will by default be linked to the Author class, but # if the real class name is Person, you'll have to specify it with this option. # # Option examples: # belongs_to :firm, predicate: OurVocab.clientOf # belongs_to :author, class_name: "Person", predicate: OurVocab.authorOf def belongs_to(name, options = {}) Builder::BelongsTo.build(self, name, options) Builder::SingularProperty.build(self, name, options) 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. # [:predicate] # REQUIRED Specify the predicate to use when storing the relationship. # [:inverse_of] # Specify the predicate to use when storing the relationship on the foreign object. If it is not provided, the relationship will not set the foriegn association. # # Option examples: # has_and_belongs_to_many :projects, predicate: OurVocab.worksOn # has_and_belongs_to_many :nations, class_name: "Country", predicate: OurVocab.isCitizenOf # has_and_belongs_to_many :topics, predicate: RDF::FOAF.isPrimaryTopicOf, inverse_of: :is_topic_of def has_and_belongs_to_many(name, options = {}) Builder::HasAndBelongsToMany.build(self, name, options) Builder::Property.build(self, name, options.slice(:class_name, :predicate)) end ## # Allows ordering of an association # @example # class Image < ActiveFedora::Base # contains :list_resource, class_name: # "ActiveFedora::Aggregation::ListSource" # orders :generic_files, through: :list_resource # end def orders(name, options = {}) Builder::Orders.build(self, name, options) end ## # Convenience method for building an ordered aggregation. # @example # class Image < ActiveFedora::Base # ordered_aggregation :members, through: :list_source # end def ordered_aggregation(name, options = {}) Builder::Aggregation.build(self, name, options) end ## # Create an association filter on the class # @example # class Image < ActiveFedora::Base # aggregates :generic_files # filters_association :generic_files, as: :large_files, condition: :big_file? # end def filters_association(extending_from, options = {}) name = options.delete(:as) Builder::Filter.build(self, name, options.merge(extending_from: extending_from)) end end end end