diff --git a/lib/active_fedora.rb b/lib/active_fedora.rb index 78d7239f5..9db10246f 100644 --- a/lib/active_fedora.rb +++ b/lib/active_fedora.rb @@ -56,6 +56,10 @@ module ActiveFedora #:nodoc: autoload :FedoraIdTranslator autoload :FedoraUriTranslator end + autoload_under 'containers' do + autoload :Container + autoload :DirectContainer + end autoload :Datastream autoload :Datastreams autoload :DelegatedAttribute diff --git a/lib/active_fedora/associations.rb b/lib/active_fedora/associations.rb index 33cb5d65c..d6165b375 100644 --- a/lib/active_fedora/associations.rb +++ b/lib/active_fedora/associations.rb @@ -19,11 +19,13 @@ module Associations autoload :SingularRDF, 'active_fedora/associations/singular_rdf' autoload :CollectionAssociation, 'active_fedora/associations/collection_association' autoload :CollectionProxy, 'active_fedora/associations/collection_proxy' + autoload :ContainerProxy, 'active_fedora/associations/container_proxy' 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 :ContainsAssociation, 'active_fedora/associations/contains_association' + autoload :DirectlyContainsAssociation, 'active_fedora/associations/directly_contains_association' module Builder autoload :Association, 'active_fedora/associations/builder/association' @@ -34,6 +36,7 @@ module Builder 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 :Property, 'active_fedora/associations/builder/property' autoload :SingularProperty, 'active_fedora/associations/builder/singular_property' @@ -77,6 +80,23 @@ def association_instance_set(name, association) module ClassMethods + # This method is used to declare an ldp:DirectContainer on a resource + # + # @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] :predicate required the rdf predicate to use for the ldp:hasMemberRelation + # + # example: + # class FooHistory < ActiveFedora::Base + # directly_contains :files, predicate: + # ::RDF::URI.new("http://example.com/hasFiles"), class_name: 'Thing' + # end + # + def directly_contains(name, options={}) + Builder::DirectlyContains.build(self, name, { class_name: 'ActiveFedora::File' }.merge(options)) + end + def has_many(name, options={}) Builder::HasMany.build(self, name, options) end diff --git a/lib/active_fedora/associations/builder/directly_contains.rb b/lib/active_fedora/associations/builder/directly_contains.rb new file mode 100644 index 000000000..abd83439e --- /dev/null +++ b/lib/active_fedora/associations/builder/directly_contains.rb @@ -0,0 +1,21 @@ +module ActiveFedora::Associations::Builder + class DirectlyContains < CollectionAssociation #:nodoc: + self.macro = :directly_contains + + def build + reflection = super + configure_dependency + reflection + end + + def validate_options + super + if !options[:predicate] + raise ArgumentError, "You must specify a predicate for #{name}" + elsif !options[:predicate].kind_of?(RDF::URI) + raise ArgumentError, "Predicate must be a kind of RDF::URI" + end + end + end +end + diff --git a/lib/active_fedora/associations/container_proxy.rb b/lib/active_fedora/associations/container_proxy.rb new file mode 100644 index 000000000..ff9528051 --- /dev/null +++ b/lib/active_fedora/associations/container_proxy.rb @@ -0,0 +1,9 @@ +module ActiveFedora + module Associations + class ContainerProxy < CollectionProxy + def initialize(association) + @association = association + end + end + end +end diff --git a/lib/active_fedora/associations/contains_association.rb b/lib/active_fedora/associations/contains_association.rb index 42802151d..9152fdb6d 100644 --- a/lib/active_fedora/associations/contains_association.rb +++ b/lib/active_fedora/associations/contains_association.rb @@ -9,7 +9,7 @@ def reader(force_reload = false) def find_target reflection.build_association(target_uri).tap do |record| configure_datastream(record) if reflection.options[:block] - end + end end def target_uri diff --git a/lib/active_fedora/associations/directly_contains_association.rb b/lib/active_fedora/associations/directly_contains_association.rb new file mode 100644 index 000000000..1ee15b50e --- /dev/null +++ b/lib/active_fedora/associations/directly_contains_association.rb @@ -0,0 +1,54 @@ +module ActiveFedora + module Associations + class DirectlyContainsAssociation < CollectionAssociation #:nodoc: + + def insert_record(record, force = true, validate = true) + container.save! + if record.new_record? + if force + record.save! + else + return false unless record.save(validate: validate) + end + end + + return true + end + + def reader + @records ||= ContainerProxy.new(self) + end + + def find_target + uris = owner.resource.query(predicate: options[:predicate]).map { |r| r.object.to_s } + if klass <= ActiveFedora::File # a subclass of file + uris.map { |uri| klass.new(uri) } + else + uris.map { |uri| klass.find(klass.uri_to_id(uri)) } + end + end + + def container + @container ||= begin + DirectContainer.find_or_initialize(ActiveFedora::Base.uri_to_id(uri)).tap do |container| + container.parent = @owner + container.member_relation = [@reflection.predicate] + end + end + end + + protected + def initialize_attributes(record) #:nodoc: + record.uri = ActiveFedora::Base.id_to_uri(container.mint_id) + set_inverse_instance(record) + end + + private + + def uri + raise "Can't get uri. Owner isn't saved" if @owner.new_record? + "#{@owner.uri}/#{@reflection.name}" + end + end + end +end diff --git a/lib/active_fedora/autosave_association.rb b/lib/active_fedora/autosave_association.rb index f1a7abf2a..9da039e46 100644 --- a/lib/active_fedora/autosave_association.rb +++ b/lib/active_fedora/autosave_association.rb @@ -75,7 +75,7 @@ module ActiveFedora module AutosaveAssociation extend ActiveSupport::Concern - ASSOCIATION_TYPES = %w{ HasMany BelongsTo HasAndBelongsToMany } + ASSOCIATION_TYPES = %w{ HasMany BelongsTo HasAndBelongsToMany DirectlyContains } module AssociationBuilderExtension #:nodoc: def self.included(base) diff --git a/lib/active_fedora/containers/container.rb b/lib/active_fedora/containers/container.rb new file mode 100644 index 000000000..0129bb70f --- /dev/null +++ b/lib/active_fedora/containers/container.rb @@ -0,0 +1,28 @@ +module ActiveFedora + class Container < ActiveFedora::Base + + property :membership_resource, predicate: ::RDF::Vocab::LDP.membershipResource + property :member_relation, predicate: ::RDF::Vocab::LDP.hasMemberRelation + + def parent + @parent || raise("Parent hasn't been set on #{self.class}") + end + + def parent=(parent) + @parent = parent + self.membership_resource = [::RDF::URI(parent.uri)] + end + + def mint_id + "#{id}/#{SecureRandom.uuid}" + end + + def self.find_or_initialize(id) + find(id) + rescue ActiveFedora::ObjectNotFoundError + new(id) + end + end +end + + diff --git a/lib/active_fedora/containers/direct_container.rb b/lib/active_fedora/containers/direct_container.rb new file mode 100644 index 000000000..2a65b76d7 --- /dev/null +++ b/lib/active_fedora/containers/direct_container.rb @@ -0,0 +1,7 @@ +module ActiveFedora + class DirectContainer < Container + type ::RDF::Vocab::LDP.DirectContainer + + + end +end diff --git a/lib/active_fedora/file.rb b/lib/active_fedora/file.rb index 5ac0deb67..da9dba23a 100644 --- a/lib/active_fedora/file.rb +++ b/lib/active_fedora/file.rb @@ -42,6 +42,13 @@ def initialize(parent_or_url_or_hash = nil, path=nil, options={}) @attributes = {}.with_indifferent_access end + def ==(comparison_object) + comparison_object.equal?(self) || + (comparison_object.instance_of?(self.class) && + comparison_object.uri == uri && + !comparison_object.new_record?) + end + def ldp_source @ldp_source || raise("NO source") end @@ -66,6 +73,10 @@ def new_record? !@exists && ldp_source.new? end + def destroyed? + false + end + def uri= uri @ldp_source = Ldp::Resource::BinarySource.new(ldp_connection, uri, '', ActiveFedora.fedora.host + ActiveFedora.fedora.base_path) end @@ -253,6 +264,10 @@ def save(*) changed_attributes.clear end + def save!(*attrs) + save(*attrs) + end + def retrieve_content ldp_source.get.body end diff --git a/lib/active_fedora/reflection.rb b/lib/active_fedora/reflection.rb index 58660421c..bc2e4ed6d 100644 --- a/lib/active_fedora/reflection.rb +++ b/lib/active_fedora/reflection.rb @@ -10,7 +10,7 @@ module Reflection # :nodoc: module ClassMethods def create_reflection(macro, name, options, active_fedora) klass = case macro - when :has_many, :belongs_to, :has_and_belongs_to_many, :contains + when :has_many, :belongs_to, :has_and_belongs_to_many, :contains, :directly_contains AssociationReflection when :rdf, :singular_rdf RDFPropertyReflection @@ -167,7 +167,7 @@ class AssociationReflection < MacroReflection #:nodoc: def initialize(macro, name, options, active_fedora) super - @collection = [:has_many, :has_and_belongs_to_many].include?(macro) + @collection = [:has_many, :has_and_belongs_to_many, :directly_contains].include?(macro) end @@ -258,6 +258,8 @@ def association_class Associations::SingularRDF when :rdf Associations::RDF + when :directly_contains + Associations::DirectlyContainsAssociation end end diff --git a/spec/integration/collection_association_spec.rb b/spec/integration/collection_association_spec.rb index f89a788aa..d62ebcbbe 100644 --- a/spec/integration/collection_association_spec.rb +++ b/spec/integration/collection_association_spec.rb @@ -76,20 +76,20 @@ class Book < ActiveFedora::Base before do class Item < ActiveFedora::Base end - class Container < ActiveFedora::Base + class SpecContainer < ActiveFedora::Base has_many :items end end after do Object.send(:remove_const, :Item) - Object.send(:remove_const, :Container) + Object.send(:remove_const, :SpecContainer) end - let(:instance) { Container.new } + let(:instance) { SpecContainer.new } subject { instance.items } it "raises an error" do - expect { subject }.to raise_error "No :inverse_of or :predicate attribute was set or could be inferred for has_many :items on Container" + expect { subject }.to raise_error "No :inverse_of or :predicate attribute was set or could be inferred for has_many :items on SpecContainer" end end diff --git a/spec/integration/complex_rdf_datastream_spec.rb b/spec/integration/complex_rdf_datastream_spec.rb index fcca47007..c59cee9ff 100644 --- a/spec/integration/complex_rdf_datastream_spec.rb +++ b/spec/integration/complex_rdf_datastream_spec.rb @@ -180,7 +180,7 @@ class EbuCore < RDF::Vocabulary("http://www.ebu.ch/metadata/ontologies/ebucore#" property :title end - class Container < ActiveFedora::Base + class SpecContainer < ActiveFedora::Base contains :info, class_name: 'SpecDatastream' end @@ -203,10 +203,10 @@ class Program < ActiveTriples::Resource after(:each) do Object.send(:remove_const, :SpecDatastream) - Object.send(:remove_const, :Container) + Object.send(:remove_const, :SpecContainer) end - let(:parent) { Container.new id: '124' } + let(:parent) { SpecContainer.new id: '124' } let (:file) { parent.info } diff --git a/spec/integration/direct_container_spec.rb b/spec/integration/direct_container_spec.rb new file mode 100644 index 000000000..fcd7025c7 --- /dev/null +++ b/spec/integration/direct_container_spec.rb @@ -0,0 +1,105 @@ +require 'spec_helper' + +describe "Direct containers" do + describe "#directly_contains" do + context "when the class is ActiveFedora::File" do + before do + class FooHistory < ActiveFedora::Base + directly_contains :files, predicate: ::RDF::URI.new("http://example.com/hasFiles") + end + end + after do + Object.send(:remove_const, :FooHistory) + end + + let(:file) { o.files.build } + let(:reloaded) { FooHistory.find(o.id) } + + context "when the object exists" do + let(:o) { FooHistory.create } + + before do + file.content = "HMMM" + o.save + end + + describe "#first" do + subject { reloaded.files.first } + it "has the content" do + expect(subject.content).to eq 'HMMM' + end + end + + describe "#to_a" do + subject { reloaded.files } + it "has the content" do + expect(subject.to_a).to eq [file] + end + end + + describe "#append" do + let(:file2) { o.files.build } + it "has two files" do + expect(o.files).to eq [file, file2] + end + + context "and then saved/reloaded" do + before do + file2.content = "Derp" + o.save! + end + it "has two files" do + expect(reloaded.files).to eq [file, file2] + end + end + end + end + + context "when the object is new" do + let(:o) { FooHistory.new } + let(:file) { o.files.build } + + it "fails" do + # This is the expected behavior right now. In the future make the uri get assigned by autosave. + expect { o.files.build }.to raise_error "Can't get uri. Owner isn't saved" + end + end + end + + context "when the class is a subclass of ActiveFedora::File" do + before do + class SubFile < ActiveFedora::File; end + class FooHistory < ActiveFedora::Base + directly_contains :files, predicate: ::RDF::URI.new("http://example.com/hasFiles"), class_name: 'SubFile' + end + end + after do + Object.send(:remove_const, :FooHistory) + Object.send(:remove_const, :SubFile) + end + + let(:o) { FooHistory.create } + let(:file) { o.files.build } + let(:reloaded) { FooHistory.find(o.id) } + + describe "#build" do + subject { file } + it { is_expected.to be_kind_of SubFile } + end + + context "when the object exists" do + before do + file.content = "HMMM" + o.save + end + + describe "#first" do + subject { reloaded.files.first } + it "has the content" do + expect(subject.content).to eq 'HMMM' + end + end + end + end + end +end diff --git a/spec/unit/files_hash_spec.rb b/spec/unit/files_hash_spec.rb index 3148d71f2..2481a3fd7 100644 --- a/spec/unit/files_hash_spec.rb +++ b/spec/unit/files_hash_spec.rb @@ -2,18 +2,18 @@ describe ActiveFedora::FilesHash do before do - class Container; end - allow(Container).to receive(:child_resource_reflections).and_return(file: reflection) + class FilesContainer; end + allow(FilesContainer).to receive(:child_resource_reflections).and_return(file: reflection) allow(container).to receive(:association).with(:file).and_return(association) allow(container).to receive(:undeclared_files).and_return([]) end - after { Object.send(:remove_const, :Container) } + after { Object.send(:remove_const, :FilesContainer) } let(:reflection) { double('reflection') } let(:association) { double('association', reader: object) } let(:object) { double('object') } - let(:container) { Container.new } + let(:container) { FilesContainer.new } subject { ActiveFedora::FilesHash.new(container) }