Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add direct containers #788

Merged
merged 1 commit into from
May 7, 2015
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 5 additions & 3 deletions lib/active_fedora.rb
Original file line number Diff line number Diff line change
Expand Up @@ -52,9 +52,9 @@ module ActiveFedora #:nodoc:
autoload :CleanConnection
autoload :Config
autoload :Core
autoload_under 'core' do
autoload :FedoraIdTranslator
autoload :FedoraUriTranslator
autoload_under 'containers' do
autoload :Container
autoload :DirectContainer
end
autoload :Datastream
autoload :Datastreams
Expand All @@ -64,8 +64,10 @@ module ActiveFedora #:nodoc:
autoload :File
autoload :FileConfigurator
autoload :FilePathBuilder
autoload :FileRelation
autoload :FilesHash
autoload :FixityService
autoload :Identifiable
autoload :Indexing
autoload :IndexingService
autoload :InheritableAccessors
Expand Down
24 changes: 24 additions & 0 deletions lib/active_fedora/associations.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand All @@ -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'
Expand Down Expand Up @@ -77,6 +80,27 @@ def association_instance_set(name, association)

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 has_many(name, options={})
Builder::HasMany.build(self, name, options)
end
Expand Down
23 changes: 23 additions & 0 deletions lib/active_fedora/associations/builder/directly_contains.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
module ActiveFedora::Associations::Builder
class DirectlyContains < CollectionAssociation #:nodoc:
self.macro = :directly_contains
self.valid_options += [:has_member_relation, :is_member_of_relation]
self.valid_options -= [:predicate]

def build
reflection = super
configure_dependency
reflection
end

def validate_options
super
if !options[:has_member_relation] && !options[:is_member_of_relation]
raise ArgumentError, "You must specify a predicate for #{name}"
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

{name} ?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

where is name defined?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In the superclass

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Uh, nope. Good catch.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

elsif !options[:has_member_relation].kind_of?(RDF::URI) && !options[:is_member_of_relation].kind_of?(RDF::URI)
raise ArgumentError, "Predicate must be a kind of RDF::URI"
end
end
end
end

9 changes: 9 additions & 0 deletions lib/active_fedora/associations/container_proxy.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
module ActiveFedora
module Associations
class ContainerProxy < CollectionProxy
def initialize(association)
@association = association
end
end
end
end
2 changes: 1 addition & 1 deletion lib/active_fedora/associations/contains_association.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
60 changes: 60 additions & 0 deletions lib/active_fedora/associations/directly_contains_association.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
module ActiveFedora
module Associations
class DirectlyContainsAssociation < CollectionAssociation #:nodoc:

def insert_record(record, force = true, validate = true)
container.save!
if force
record.save!
else
record.save(validate: validate)
end
end

def reader
@records ||= ContainerProxy.new(self)
end

def find_target
query_node = if container_predicate = options[:has_member_relation]
owner
else
container_predicate = ::RDF::Vocab::LDP.contains
container
end

uris = query_node.resource.query(predicate: container_predicate).map { |r| r.object.to_s }

uris.map { |object_uri| klass.find(klass.uri_to_id(object_uri)) }
end

def container
@container ||= begin
DirectContainer.find_or_initialize(ActiveFedora::Base.uri_to_id(uri)).tap do |container|
container.parent = @owner
container.has_member_relation = Array(options[:has_member_relation])
container.is_member_of_relation = Array(options[:is_member_of_relation])
end
end
end

protected

def count_records
load_target.size
end

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
2 changes: 1 addition & 1 deletion lib/active_fedora/autosave_association.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
1 change: 1 addition & 0 deletions lib/active_fedora/base.rb
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ class Base
extend LdpCache::ClassMethods

include Core
include Identifiable
include Persistence
include Indexing
include Scoping
Expand Down
30 changes: 30 additions & 0 deletions lib/active_fedora/containers/container.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
module ActiveFedora
# This is the base class for ldp containers, it is not an ldp:BasicContainer
class Container < ActiveFedora::Base
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe a comment that makes it clear that this isn't an object representation of a BasicContainer


property :membership_resource, predicate: ::RDF::Vocab::LDP.membershipResource
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Validations for these necessary?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's already validated. If you remove this line the tests fail.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Because of LDP?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah, no, because it's only using Container through that relation. I could do Container.new.save! and it'd run fine

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

because: NoMethodError: undefined methodmembership_resource='`

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I meant a presence validation.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't understand how that would happen. We're always building it correctly, so why have a check that costs cycles and brings no benefit?

property :has_member_relation, predicate: ::RDF::Vocab::LDP.hasMemberRelation
property :is_member_of_relation, predicate: ::RDF::Vocab::LDP.isMemberOfRelation

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)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should this be on AF::Base?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't see a reason to move it there unless there's anyone else who would want to use this.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I guess I'm not sure what's special about Containers that would make us want to find_and_initialize here but not there.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's a very specific method and is hardly ever useful. We've received complaints about the API being overly broad, so I don't want to expand it unless you can show me a case where you would use it.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

+1 for keeping the API narrow.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm also 👍 on keeping the API small, but that seems like a motivation to make this method private, or to consider a builder pattern, not to tuck it away in the subclass. My concern would be that the outcome here is an API that is both big and inconsistent.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@no-reply I don't know what that means. Care to make a pull request or show me what you're talking about?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@no-reply Specifically in relation to the original comment which said to move this method to AF::Base. Seems like keeping it just on containers keeps the AF::Base api smaller.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@jcoyne I mean, this keeps the AF::Base API smaller, but just trades that concern with an inconsistent access API, no?

I'm not willing to hold this up just for this, but it was a concern

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, this is not a blocker. It's a philosophical/design difference that we should probably resolve elsewhere.

find(id)
rescue ActiveFedora::ObjectNotFoundError
new(id)
end
end
end


7 changes: 7 additions & 0 deletions lib/active_fedora/containers/direct_container.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
module ActiveFedora
class DirectContainer < Container
type ::RDF::Vocab::LDP.DirectContainer


end
end
52 changes: 4 additions & 48 deletions lib/active_fedora/core.rb
Original file line number Diff line number Diff line change
@@ -1,38 +1,18 @@
module ActiveFedora
module Core
extend ActiveSupport::Autoload
extend ActiveSupport::Concern

autoload :FedoraIdTranslator
autoload :FedoraUriTranslator

included do
##
# :singleton-method:
#
# Accepts a logger conforming to the interface of Log4r which can be
# retrieved on both a class and instance level by calling +logger+.
mattr_accessor :logger, instance_writer: false

##
# :singleton-method
#
# Accepts a proc that takes an id and transforms it to a URI
mattr_reader :translate_id_to_uri do
FedoraIdTranslator
end

def self.translate_id_to_uri=(translator)
@@translate_id_to_uri = translator || FedoraIdTranslator
end

##
# :singleton-method
#
# Accepts a proc that takes a uri and transforms it to an id
mattr_reader :translate_uri_to_id do
FedoraUriTranslator
end

def self.translate_uri_to_id=(translator)
@@translate_uri_to_id = translator || FedoraUriTranslator
end
end

def ldp_source
Expand Down Expand Up @@ -141,30 +121,6 @@ def to_class_uri(attrs = {})
name
end

##
# Transforms an id into a uri
# if translate_id_to_uri is set it uses that proc, otherwise just the default
def id_to_uri(id)
translate_id_to_uri.call(id)
end

##
# Transforms a uri into an id
# if translate_uri_to_id is set it uses that proc, otherwise just the default
def uri_to_id(uri)
translate_uri_to_id.call(uri)
end

##
# Provides the common interface for ActiveTriples::Identifiable
def from_uri(uri,_)
begin
self.find(uri_to_id(uri))
rescue ActiveFedora::ObjectNotFoundError, Ldp::Gone
ActiveTriples::Resource.new(uri)
end
end

private

def relation
Expand Down
21 changes: 21 additions & 0 deletions lib/active_fedora/file.rb
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,9 @@ class File
generate_method 'content'

extend ActiveModel::Callbacks
include Identifiable
include Scoping
extend Querying
define_model_callbacks :save, :create, :destroy
define_model_callbacks :initialize, only: :after

Expand Down Expand Up @@ -42,6 +45,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
Expand All @@ -66,6 +76,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
Expand Down Expand Up @@ -217,6 +231,9 @@ def links
@links ||= Ldp::Response.links(ldp_source.head)
end

def self.relation
FileRelation.new(self)
end

# Rack::Test::UploadedFile is often set via content=, however it's not an IO, though it wraps an io object.
def behaves_like_io?(obj)
Expand Down Expand Up @@ -253,6 +270,10 @@ def save(*)
changed_attributes.clear
end

def save!(*attrs)
save(*attrs)
end

def retrieve_content
ldp_source.get.body
end
Expand Down
7 changes: 7 additions & 0 deletions lib/active_fedora/file_relation.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
module ActiveFedora
class FileRelation < Relation
def load_from_fedora(id, _)
klass.new(klass.id_to_uri(id))
end
end
end
Loading