activefedora-aggregation 0.2.1 → 0.3.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (33) hide show
  1. checksums.yaml +4 -4
  2. data/Gemfile +1 -1
  3. data/README.md +2 -33
  4. data/Rakefile +1 -1
  5. data/activefedora-aggregation.gemspec +1 -0
  6. data/bin/jetty_wait +6 -0
  7. data/circle.yml +11 -0
  8. data/lib/active_fedora/aggregation/aggregation_extension.rb +26 -0
  9. data/lib/active_fedora/aggregation/appends_to_aggregation.rb +31 -0
  10. data/lib/active_fedora/aggregation/association.rb +58 -33
  11. data/lib/active_fedora/aggregation/base_extension.rb +53 -14
  12. data/lib/active_fedora/aggregation/builder.rb +8 -20
  13. data/lib/active_fedora/aggregation/decorating_repository.rb +29 -0
  14. data/lib/active_fedora/aggregation/decorator_list.rb +15 -0
  15. data/lib/active_fedora/aggregation/decorator_with_arguments.rb +15 -0
  16. data/lib/active_fedora/aggregation/link_inserter.rb +56 -0
  17. data/lib/active_fedora/aggregation/null_proxy.rb +23 -0
  18. data/lib/active_fedora/aggregation/ordered_proxy.rb +57 -0
  19. data/lib/active_fedora/aggregation/ordered_reader.rb +23 -0
  20. data/lib/active_fedora/aggregation/persist_links.rb +9 -0
  21. data/lib/active_fedora/aggregation/proxy.rb +1 -2
  22. data/lib/active_fedora/aggregation/proxy_owner.rb +24 -0
  23. data/lib/active_fedora/aggregation/proxy_repository.rb +38 -0
  24. data/lib/active_fedora/aggregation/reflection.rb +18 -1
  25. data/lib/active_fedora/aggregation/version.rb +1 -1
  26. data/lib/active_fedora/aggregation.rb +14 -3
  27. data/lib/active_fedora/filter/association.rb +70 -0
  28. data/lib/active_fedora/filter/builder.rb +14 -0
  29. data/lib/active_fedora/filter/reflection.rb +21 -0
  30. data/lib/active_fedora/filter.rb +11 -0
  31. metadata +35 -6
  32. data/lib/active_fedora/aggregation/proxy_container.rb +0 -111
  33. data/lib/active_fedora/aggregation/through_association.rb +0 -25
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: eaf698c46d501172f3b2db824f2349612a7fb1e5
4
- data.tar.gz: abef6dc0592da76db879e6909f4b171c59b6dcdb
3
+ metadata.gz: e6accff4705ce11b36750f93b831e532f7276e1c
4
+ data.tar.gz: f603f7707698d4ca8b239da7f3f8dd489cdb0a58
5
5
  SHA512:
6
- metadata.gz: bbc6ee9983e47539e1908376659b976e4d98b0711f76a35941e0efdb497ba3367ce006aa06857134f6d300480b20344b915667dd5b3fb961f9e5223da5ee8f46
7
- data.tar.gz: 20b7af1f37c2ebcc843d32b967caa077ed0f8f312823ae5e3998cf8fa4c2324731e02fb26a1a7e20e3a8464571f0db0cc8db3713f86377b0e6c6788b1749753c
6
+ metadata.gz: 76293d09bec958fb7a1a645806d2736d62d184cd0a27d38a12cd380db2f2cca49b9ba9e0eb9d1d9432e383738afd70580f14b83a00dc826b1755309cfb00598a
7
+ data.tar.gz: 045f5653e592fc45e9ae62b3da06ef2d4d9cbeb70fabd2f0aa5fe27aa3e1f8a2a6e6e835b2ac79dd3f59e43d73a14b044adde6dc06690b0e485059478c9f922b
data/Gemfile CHANGED
@@ -3,4 +3,4 @@ source 'https://rubygems.org'
3
3
  # Specify your gem's dependencies in activefedora-aggregation.gemspec
4
4
  gemspec
5
5
  gem 'byebug' unless ENV['CI']
6
-
6
+ gem 'active-fedora', github: "projecthydra/active_fedora"
data/README.md CHANGED
@@ -1,4 +1,4 @@
1
- [![Gem Version](https://badge.fury.io/rb/activefedora-aggregation.svg)](http://badge.fury.io/rb/activefedora-aggregation) [![Build Status](https://travis-ci.org/curationexperts/activefedora-aggregation.svg?branch=v0.1.0)](https://travis-ci.org/curationexperts/activefedora-aggregation)
1
+ [![Gem Version](https://badge.fury.io/rb/activefedora-aggregation.svg)](http://badge.fury.io/rb/activefedora-aggregation) [![Build Status](https://circleci.com/gh/projecthydra-labs/activefedora-aggregation.svg?style=shield&circle-token=:circle-token)](https://circleci.com/gh/projecthydra-labs/activefedora-aggregation)
2
2
  # ActiveFedora::Aggregation
3
3
 
4
4
  Aggregations for ActiveFedora.
@@ -21,35 +21,4 @@ image.save
21
21
 
22
22
  ```
23
23
 
24
- Now the "generic\_files" method returns an ordered list of GenericFile objects.
25
-
26
- Here's what the graph looks like:
27
-
28
- ```ttl
29
- <http://127.0.0.1:8983/fedora/rest/dev/my_image> a <http://www.w3.org/ns/ldp#RDFSource>,
30
- <http://www.w3.org/ns/ldp#Container>;
31
- <http://www.w3.org/ns/ldp#contains> <http://127.0.0.1:8983/fedora/rest/dev/my_image/files>;
32
- <info:fedora/fedora-system:def/model#hasModel> "Image" .
33
-
34
- <http://127.0.0.1:8983/fedora/rest/dev/my_image/files> a <http://www.w3.org/ns/ldp#RDFSource>,
35
- <http://www.w3.org/ns/ldp#Container>;
36
- <http://www.iana.org/assignments/relation/first> <http://127.0.0.1:8983/fedora/rest/dev/my_image/files/5a5af870-594b-4966-93f6-0348402583f0>;
37
- <http://www.iana.org/assignments/relation/last> <http://127.0.0.1:8983/fedora/rest/dev/my_image/files/9cc70b3d-c9d7-4cfc-b504-adbcb0bdfb3d>;
38
- <http://www.w3.org/ns/ldp#contains> <http://127.0.0.1:8983/fedora/rest/dev/my_image/files/5a5af870-594b-4966-93f6-0348402583f0>,
39
- <http://127.0.0.1:8983/fedora/rest/dev/my_image/files/9cc70b3d-c9d7-4cfc-b504-adbcb0bdfb3d>;
40
- <info:fedora/fedora-system:def/model#hasModel> "ActiveFedora::Aggregation::ProxyContainer" .
41
-
42
- <http://127.0.0.1:8983/fedora/rest/dev/my_image/files/5a5af870-594b-4966-93f6-0348402583f0> a <http://www.w3.org/ns/ldp#RDFSource>,
43
- <http://www.w3.org/ns/ldp#Container>;
44
- <http://www.iana.org/assignments/relation/next> <http://127.0.0.1:8983/fedora/rest/dev/my_image/files/9cc70b3d-c9d7-4cfc-b504-adbcb0bdfb3d>;
45
- <http://www.openarchives.org/ore/terms/proxyFor> <http://127.0.0.1:8983/fedora/rest/dev/file2>;
46
- <http://www.openarchives.org/ore/terms/proxyIn> <http://127.0.0.1:8983/fedora/rest/dev/my_image/files>;
47
- <info:fedora/fedora-system:def/model#hasModel> "ActiveFedora::Aggregation::Proxy" .
48
-
49
- <http://127.0.0.1:8983/fedora/rest/dev/my_image/files/9cc70b3d-c9d7-4cfc-b504-adbcb0bdfb3d> a <http://www.w3.org/ns/ldp#RDFSource>,
50
- <http://www.w3.org/ns/ldp#Container>;
51
- <http://www.iana.org/assignments/relation/prev> <http://127.0.0.1:8983/fedora/rest/dev/my_image/files/5a5af870-594b-4966-93f6-0348402583f0>;
52
- <http://www.openarchives.org/ore/terms/proxyFor> <http://127.0.0.1:8983/fedora/rest/dev/file1>;
53
- <http://www.openarchives.org/ore/terms/proxyIn> <http://127.0.0.1:8983/fedora/rest/dev/my_image/files>;
54
- <info:fedora/fedora-system:def/model#hasModel> "ActiveFedora::Aggregation::Proxy" .
55
- ```
24
+ Now the `generic\_files` method returns an ordered list of GenericFile objects.
data/Rakefile CHANGED
@@ -6,7 +6,7 @@ require 'rspec/core/rake_task'
6
6
  RSpec::Core::RakeTask.new(:spec)
7
7
 
8
8
  require 'jettywrapper'
9
- Jettywrapper.hydra_jetty_version = "v8.3.0"
9
+ Jettywrapper.hydra_jetty_version = "v8.3.1"
10
10
 
11
11
  desc 'Spin up hydra-jetty and run specs'
12
12
  task ci: ['jetty:clean'] do
@@ -24,4 +24,5 @@ Gem::Specification.new do |spec|
24
24
  spec.add_development_dependency "rake", "~> 10.0"
25
25
  spec.add_development_dependency "rspec", "~> 3.2"
26
26
  spec.add_development_dependency "jettywrapper"
27
+ spec.add_development_dependency "pry-byebug"
27
28
  end
data/bin/jetty_wait ADDED
@@ -0,0 +1,6 @@
1
+ #!/usr/bin/env sh
2
+
3
+ until $(curl --output /dev/null --silent --head --fail http://localhost:8983/solr); do
4
+ printf '.'
5
+ sleep 1
6
+ done
data/circle.yml ADDED
@@ -0,0 +1,11 @@
1
+ machine:
2
+ ruby:
3
+ version: 2.2.0
4
+ dependencies:
5
+ pre:
6
+ - gem install bundler
7
+ post:
8
+ - bundle exec rake jetty:clean
9
+ - cd jetty && java -Djetty.port=8983 -Dsolr.solr.home=/home/ubuntu/activefedora-aggregation/jetty/solr -XX:MaxPermSize=128m -Xmx256m -jar start.jar:
10
+ background: true
11
+ - bin/jetty_wait
@@ -0,0 +1,26 @@
1
+ # This module is mixed into classes that declare 'aggregates ...'
2
+ #
3
+ module ActiveFedora::Aggregation
4
+ module AggregationExtension
5
+ extend ActiveSupport::Concern
6
+ include PersistLinks
7
+
8
+ included do
9
+ after_destroy :remove_aggregation_proxies_from_solr
10
+
11
+ # Doesn't use after_save because we need this callback to come after the autosave callback
12
+ after_create :persist_aggregation_links
13
+ after_update :persist_aggregation_links
14
+ end
15
+
16
+ private
17
+
18
+ # The proxies, being nested under the object, are automatically destroyed
19
+ # this cleans up their records from solr.
20
+ def remove_aggregation_proxies_from_solr
21
+ query = ActiveFedora::SolrQueryBuilder.construct_query_for_rel(proxyIn: id, has_model: Proxy.to_class_uri)
22
+ ActiveFedora::SolrService.instance.conn.delete_by_query(query, params: {'softCommit' => true})
23
+ end
24
+
25
+ end
26
+ end
@@ -0,0 +1,31 @@
1
+ module ActiveFedora::Aggregation
2
+ class AppendsToAggregation < SimpleDelegator
3
+ attr_reader :parent_node
4
+ # @param [#next, #prev] proxy The proxy to add behavior to.
5
+ # @param [#head, #tail] parent_node The aggregation to append proxies to.
6
+ def initialize(proxy, parent_node)
7
+ @parent_node = parent_node
8
+ super(proxy)
9
+ end
10
+
11
+ def is_a?(klass)
12
+ __getobj__.is_a?(klass)
13
+ end
14
+
15
+ def save(*args)
16
+ insert_link do
17
+ super
18
+ end
19
+ end
20
+
21
+ private
22
+
23
+ def insert_link
24
+ result = yield
25
+ if result
26
+ LinkInserter.new(parent_node, self).call
27
+ end
28
+ result
29
+ end
30
+ end
31
+ end
@@ -1,59 +1,84 @@
1
1
  module ActiveFedora::Aggregation
2
- class Association
2
+ class Association < ::ActiveFedora::Associations::IndirectlyContainsAssociation
3
+ delegate :first, to: :ordered_reader
3
4
 
4
- # @param [ActiveFedora::Base] parent
5
- # @param [Reflection] reflection
6
- # @opts options [String] class_name name of the class in the association
7
- def initialize(parent, reflection)
8
- @parent = parent
9
- @reflection = reflection
5
+ def ordered_reader
6
+ OrderedReader.new(owner).to_a
10
7
  end
11
8
 
12
- def klass
13
- @reflection.klass
9
+ def proxy_class
10
+ @proxy_class ||= ProxyRepository.new(owner, super)
14
11
  end
15
12
 
16
- def == other
17
- container.to_a == other
13
+ def options
14
+ @all_options ||= default_options.merge(super)
18
15
  end
19
16
 
20
- def create(&block)
21
- klass.create(&block).tap do |created|
22
- container << created
17
+ # Implements the ids reader method, e.g. foo.item_ids for Foo.has_many :items
18
+ def ids_reader
19
+ if loaded?
20
+ load_target.map do |record|
21
+ record.id
22
+ end
23
+ else
24
+ proxies = load_proxies_from_solr(fl: 'id, next_ssim, proxyFor_ssim')
25
+ create_linked_list(@owner.head_id, proxies)
23
26
  end
24
- save #causes the (head/tail) pointers on the aggregation to be persisted
25
27
  end
26
28
 
27
- def save
28
- container.save
29
+ private
30
+
31
+ # Write a query to find the proxies
32
+ def construct_proxy_query
33
+ @proxy_query ||= begin
34
+ clauses = { 'proxyIn' => @owner.id }
35
+ clauses[:has_model] = ActiveFedora::Aggregation::Proxy.to_class_uri
36
+ ActiveFedora::SolrQueryBuilder.construct_query_for_rel(clauses)
37
+ end
29
38
  end
30
39
 
31
- def target=(vals)
32
- container.target=(vals)
40
+ # Finds the proxies
41
+ # @param opts [Hash] Options that will be passed through to ActiveFedora::SolrService.query.
42
+ def load_proxies_from_solr(opts = Hash.new)
43
+ finder_query = construct_proxy_query
44
+ rows = 1000
45
+ ActiveFedora::SolrService.query(finder_query, { rows: rows }.merge(opts))
33
46
  end
34
47
 
35
- def target_ids=(vals)
36
- container.target_ids=(vals)
48
+ # @param [String, NilClass] first_id
49
+ # @param [Array<Hash>] remainder
50
+ # @param [Array] list
51
+ def create_linked_list(first_id, remainder, list=[])
52
+ return list if remainder.empty?
53
+
54
+ index = remainder.find_index { |n| n.fetch('id') == first_id }
55
+ first = remainder.delete_at(index)
56
+ next_id = first['next_ssim'].try(:first)
57
+ create_linked_list(next_id, remainder, list + [first.fetch('proxyFor_ssim').first])
37
58
  end
38
59
 
39
- def target_ids
40
- container.target_ids
60
+ def default_options
61
+ { through: default_proxy_class, foreign_key: :target, has_member_relation: reflection.predicate, inserted_content_relation: content_relation }
41
62
  end
42
63
 
43
- def container
44
- @container ||= begin
45
- ProxyContainer.find_or_initialize(klass.uri_to_id(uri)).tap do |container|
46
- container.parent = @parent
47
- end
48
- end
64
+ def content_relation
65
+ default_proxy_class.constantize.reflect_on_association(:target).predicate
49
66
  end
50
67
 
51
- def first
52
- container.first
68
+ def default_proxy_class
69
+ 'ActiveFedora::Aggregation::Proxy'
53
70
  end
54
71
 
55
- def uri
56
- @parent.uri + '/files'
72
+ def raise_on_type_mismatch(record)
73
+ super
74
+ if type_validator
75
+ type_validator.validate!(self,record)
76
+ end
77
+ end
78
+
79
+ def type_validator
80
+ options[:type_validator]
57
81
  end
82
+
58
83
  end
59
84
  end
@@ -1,23 +1,62 @@
1
1
  module ActiveFedora::Aggregation
2
2
  module BaseExtension
3
+ extend ActiveSupport::Concern
3
4
 
4
- ##
5
- # Create an aggregation association on the class
6
- # @example
7
- # class Image < ActiveFedora::Base
8
- # aggregates :generic_files
9
- # end
10
- def aggregates(name, options={})
11
- Builder.build(self, name, options)
5
+ # Queries the RDF graph to find all records that include this object in their aggregations
6
+ # Since any class may be the target of an aggregation, this must be on every class extending
7
+ # from ActiveFedora::Base
8
+ # @return [Array] records that include this object in their aggregations
9
+ def aggregated_by
10
+ # In theory you should be able to find the aggregation predicate (ie ore:aggregates)
11
+ # but Fedora does not return that predicate due to this bug in FCREPO:
12
+ # https://jira.duraspace.org/browse/FCREPO-1497
13
+ # so we have to look up the proxies asserting RDF::Vocab::ORE.proxyFor
14
+ # and return their containers.
15
+ proxy_class.where(proxyFor_ssim: id).map(&:container)
12
16
  end
13
17
 
14
- def create_reflection(macro, name, options, active_fedora)
15
- if macro == :aggregation
16
- Reflection.new(macro, name, options, active_fedora).tap do |reflection|
17
- add_reflection name, reflection
18
+ private
19
+
20
+ def proxy_class
21
+ ActiveFedora::Aggregation::Proxy
22
+ end
23
+
24
+ module ClassMethods
25
+ ##
26
+ # Create an aggregation association on the class
27
+ # @example
28
+ # class Image < ActiveFedora::Base
29
+ # aggregates :generic_files
30
+ # end
31
+ def aggregates(name, options={})
32
+ Builder.build(self, name, options)
33
+ end
34
+
35
+ ##
36
+ # Create an association filter on the class
37
+ # @example
38
+ # class Image < ActiveFedora::Base
39
+ # aggregates :generic_files
40
+ # filters_association :generic_files, as: :large_files, condition: :big_file?
41
+ # end
42
+ def filters_association(extending_from, options={})
43
+ name = options.delete(:as)
44
+ ActiveFedora::Filter::Builder.build(self, name, options.merge(extending_from: extending_from))
45
+ end
46
+
47
+ def create_reflection(macro, name, options, active_fedora)
48
+ case macro
49
+ when :aggregation
50
+ Reflection.new(macro, name, options, active_fedora).tap do |reflection|
51
+ add_reflection name, reflection
52
+ end
53
+ when :filter
54
+ ActiveFedora::Filter::Reflection.new(macro, name, options, active_fedora).tap do |reflection|
55
+ add_reflection name, reflection
56
+ end
57
+ else
58
+ super
18
59
  end
19
- else
20
- super
21
60
  end
22
61
  end
23
62
  end
@@ -1,14 +1,17 @@
1
1
  module ActiveFedora::Aggregation
2
2
  class Builder < ActiveFedora::Associations::Builder::CollectionAssociation
3
+ include ActiveFedora::AutosaveAssociation::AssociationBuilderExtension
3
4
  self.macro = :aggregation
5
+ self.valid_options += [
6
+ :type_validator
7
+ ]
4
8
 
5
9
  def build
6
10
  reflection = super
7
- configure_dependency
8
- model.has_and_belongs_to_many :proxies, predicate: ::RDF::Vocab::ORE.aggregates, class_name: 'ActiveFedora::Aggregation::Proxy', inverse_of: :containers
9
11
  model.belongs_to :head, predicate: ::RDF::Vocab::IANA['first'], class_name: 'ActiveFedora::Aggregation::Proxy'
10
12
  model.belongs_to :tail, predicate: ::RDF::Vocab::IANA.last, class_name: 'ActiveFedora::Aggregation::Proxy'
11
13
 
14
+ model.include AggregationExtension
12
15
  reflection
13
16
  end
14
17
 
@@ -17,6 +20,9 @@ module ActiveFedora::Aggregation
17
20
  mixin.redefine_method("#{name.to_s.singularize}_ids") do
18
21
  association(name).ids_reader
19
22
  end
23
+ mixin.redefine_method("ordered_#{name.to_s.pluralize}") do
24
+ association(name).ordered_reader
25
+ end
20
26
  end
21
27
 
22
28
  def self.define_writers(mixin, name)
@@ -26,23 +32,5 @@ module ActiveFedora::Aggregation
26
32
  end
27
33
  end
28
34
 
29
- private
30
-
31
- def configure_dependency
32
- define_save_dependency_method
33
- model.after_save dependency_method_name
34
- end
35
-
36
- def define_save_dependency_method
37
- name = self.name
38
- model.send(:define_method, dependency_method_name) do
39
- send(name).save
40
- end
41
- end
42
-
43
- def dependency_method_name
44
- "aggregator_dependent_for_#{name}"
45
- end
46
-
47
35
  end
48
36
  end
@@ -0,0 +1,29 @@
1
+ module ActiveFedora::Aggregation
2
+ ##
3
+ # Decorates the results of a repository with the given decorator.
4
+ class DecoratingRepository < SimpleDelegator
5
+ attr_reader :decorator
6
+ # @param [#new] decorator A decorator.
7
+ # @param [#find, #new] base_repository A repository to decorate.
8
+ def initialize(decorator, base_repository)
9
+ @decorator = decorator
10
+ super(base_repository)
11
+ end
12
+
13
+ def find(id)
14
+ decorate(super(id))
15
+ end
16
+
17
+ def new(*args)
18
+ result = decorate(super(*args))
19
+ yield result if block_given?
20
+ result
21
+ end
22
+
23
+ private
24
+
25
+ def decorate(obj)
26
+ decorator.new(obj)
27
+ end
28
+ end
29
+ end
@@ -0,0 +1,15 @@
1
+ module ActiveFedora::Aggregation
2
+ # A composite object to allow for a list of decorators to be treated like one.
3
+ class DecoratorList
4
+ attr_reader :decorators
5
+ def initialize(*decorators)
6
+ @decorators = decorators
7
+ end
8
+
9
+ def new(undecorated_object)
10
+ decorators.inject(undecorated_object) do |obj, decorator|
11
+ decorator.new(obj)
12
+ end
13
+ end
14
+ end
15
+ end
@@ -0,0 +1,15 @@
1
+ module ActiveFedora::Aggregation
2
+ # This is an Adapter to allow a Decorator to respond to the interface #new()
3
+ # but still require other arguments to instantiate.
4
+ class DecoratorWithArguments
5
+ attr_reader :decorator, :args
6
+ def initialize(decorator, *args)
7
+ @decorator = decorator
8
+ @args = args
9
+ end
10
+
11
+ def new(obj)
12
+ decorator.new(obj, *args)
13
+ end
14
+ end
15
+ end
@@ -0,0 +1,56 @@
1
+ module ActiveFedora::Aggregation
2
+ class LinkInserter
3
+ attr_reader :root, :proxy
4
+ # @param [ProxyOwner] root the node representing the aggregation
5
+ # @param [Proxy] proxy the proxy to add to the aggregation
6
+ def initialize(root, proxy)
7
+ @root = root
8
+ @proxy = proxy
9
+ end
10
+
11
+ def call
12
+ if root.head
13
+ append
14
+ else
15
+ set
16
+ end
17
+ proxy.container = root
18
+ persist_nodes!
19
+ end
20
+
21
+ private
22
+
23
+ def append
24
+ Appender.new(root, proxy).call
25
+ end
26
+
27
+ def set
28
+ root.head = proxy
29
+ root.tail = proxy
30
+ end
31
+
32
+ def persist_nodes!
33
+ [proxy, proxy.prev].uniq.compact.each(&:save!)
34
+ end
35
+
36
+ class Appender
37
+ attr_reader :root, :proxy
38
+ def initialize(root, proxy)
39
+ @root = root
40
+ @proxy = proxy
41
+ end
42
+
43
+ def call
44
+ last_proxy.next = proxy
45
+ proxy.prev = last_proxy
46
+ root.tail = proxy
47
+ end
48
+
49
+ private
50
+
51
+ def last_proxy
52
+ @last_proxy ||= root.tail
53
+ end
54
+ end
55
+ end
56
+ end
@@ -0,0 +1,23 @@
1
+ module ActiveFedora::Aggregation
2
+ # A null proxy to simplify logic.
3
+ class NullProxy
4
+ include Singleton
5
+
6
+ attr_writer :prev, :next
7
+ def prev
8
+ self
9
+ end
10
+
11
+ def next
12
+ self
13
+ end
14
+
15
+ def reload
16
+ self
17
+ end
18
+
19
+ def changed?
20
+ false
21
+ end
22
+ end
23
+ end
@@ -0,0 +1,57 @@
1
+ module ActiveFedora::Aggregation
2
+ # A proxy which knows how to delete itself from an aggregation.
3
+ class OrderedProxy < SimpleDelegator
4
+ attr_reader :parent_node
5
+ # @param [#next, #prev, #delete] proxy The proxy to add behavior to.
6
+ # @param [#delete_proxy!] parent_node The aggregation to delete proxies
7
+ # from.
8
+ def initialize(proxy, parent_node)
9
+ @parent_node = parent_node
10
+ super(proxy)
11
+ end
12
+
13
+ def is_a?(klass)
14
+ __getobj__.is_a?(klass)
15
+ end
16
+
17
+ def delete(*args)
18
+ link_node_if_true do
19
+ super
20
+ end
21
+ end
22
+
23
+ protected
24
+
25
+ def link_node_if_true
26
+ # Have to precache these or AF tries to access this node?
27
+ next_or_null
28
+ prev_or_null
29
+ yield.tap do |result|
30
+ if result
31
+ # Have to reload proxies because otherwise you persist them with bad
32
+ # referencing triples.
33
+ [next_or_null, prev_or_null, parent_node].each(&:reload)
34
+ prev_or_null.next = self.next
35
+ next_or_null.prev = prev
36
+ changed_nodes.each(&:save!)
37
+ parent_node.delete_proxy!(self)
38
+ end
39
+ end
40
+ end
41
+
42
+ def next_or_null
43
+ self.next || NullProxy.instance
44
+ end
45
+
46
+ def prev_or_null
47
+ self.prev || NullProxy.instance
48
+ end
49
+
50
+ private
51
+
52
+ def changed_nodes
53
+ [self.prev, self.next].uniq.compact.select(&:changed?)
54
+ end
55
+
56
+ end
57
+ end
@@ -0,0 +1,23 @@
1
+ module ActiveFedora::Aggregation
2
+ class OrderedReader
3
+ include Enumerable
4
+ attr_reader :root
5
+ def initialize(root)
6
+ @root = root
7
+ end
8
+
9
+ def each
10
+ proxy = first_head
11
+ while proxy
12
+ yield proxy.target
13
+ proxy = proxy.next
14
+ end
15
+ end
16
+
17
+ private
18
+
19
+ def first_head
20
+ root.head
21
+ end
22
+ end
23
+ end
@@ -0,0 +1,9 @@
1
+ module ActiveFedora::Aggregation
2
+ module PersistLinks
3
+ # If the head or tail pointer was updated (in an autosave callback), then persist them
4
+ def persist_aggregation_links
5
+ return true unless @new_record_before_save
6
+ save if changes.key?("head_id") or changes.key?("tail_id")
7
+ end
8
+ end
9
+ end
@@ -1,7 +1,6 @@
1
1
  module ActiveFedora::Aggregation
2
2
  class Proxy < ActiveFedora::Base
3
- # HABTM is actually only belongs to one
4
- has_and_belongs_to_many :containers, predicate: ::RDF::Vocab::ORE.proxyIn, class_name: 'ActiveFedora::Base'
3
+ belongs_to :container, predicate: ::RDF::Vocab::ORE.proxyIn, class_name: 'ActiveFedora::Base'
5
4
  belongs_to :target, predicate: ::RDF::Vocab::ORE.proxyFor, class_name: 'ActiveFedora::Base'
6
5
  belongs_to :next, predicate: ::RDF::Vocab::IANA.next, class_name: 'ActiveFedora::Aggregation::Proxy'
7
6
  belongs_to :prev, predicate: ::RDF::Vocab::IANA.prev, class_name: 'ActiveFedora::Aggregation::Proxy'
@@ -0,0 +1,24 @@
1
+ module ActiveFedora::Aggregation
2
+ ##
3
+ # Decorates a proxy owner such that it knows how to delete a proxy from its
4
+ # ordered list.
5
+ class ProxyOwner < SimpleDelegator
6
+ def is_a?(klass)
7
+ __getobj__.is_a?(klass)
8
+ end
9
+
10
+ # @param [#next, #prev] proxy A Proxy link to delete.
11
+ def delete_proxy!(proxy)
12
+ if proxy == head || head.nil? # Head is nil if proxy is now deleted.
13
+ self.head = proxy.next
14
+ end
15
+ if proxy == tail || tail.nil? # Head is nil if proxy is now deleted.
16
+ self.tail = proxy.prev
17
+ end
18
+ [head, tail].uniq.compact.each(&:reload)
19
+ if changed?
20
+ save!
21
+ end
22
+ end
23
+ end
24
+ end
@@ -0,0 +1,38 @@
1
+ module ActiveFedora::Aggregation
2
+ ##
3
+ # Repository for Proxies. This repository is responsible for decorating a
4
+ # proxy such that it's useful for aggregation and is an attempt at
5
+ # centralizing hard-coded dependencies without a dependency injection
6
+ # container.
7
+ class ProxyRepository < SimpleDelegator
8
+ attr_reader :owner, :base_proxy_factory
9
+ # @param [ActiveFedora::Base] owner The node which proxy will assert order
10
+ # on.
11
+ # @param [#find, #new] base_proxy_factory The base factory which returns proxies
12
+ # that will be decorated.
13
+ # @return [#find, #new] A repository which can return proxies useful for
14
+ # aggregation.
15
+ def initialize(owner, base_proxy_factory)
16
+ @owner = owner
17
+ @base_proxy_factory = base_proxy_factory
18
+ super(repository)
19
+ end
20
+
21
+ private
22
+
23
+ def repository
24
+ DecoratingRepository.new(proxy_decorator, base_proxy_factory)
25
+ end
26
+
27
+ def proxy_decorator
28
+ DecoratorList.new(
29
+ DecoratorWithArguments.new(OrderedProxy, proxy_owner),
30
+ DecoratorWithArguments.new(AppendsToAggregation, proxy_owner)
31
+ )
32
+ end
33
+
34
+ def proxy_owner
35
+ @proxy_owner ||= ProxyOwner.new(owner)
36
+ end
37
+ end
38
+ end
@@ -1,7 +1,24 @@
1
1
  module ActiveFedora::Aggregation
2
2
  class Reflection < ActiveFedora::Reflection::AssociationReflection
3
3
  def association_class
4
- ThroughAssociation
4
+ Association
5
+ end
6
+
7
+ def klass
8
+ @klass ||= begin
9
+ klass = class_name.constantize
10
+ klass.respond_to?(:uri_to_id) ? klass : ActiveFedora::Base
11
+ rescue NameError => e
12
+ # If the NameError is a result of the class having a
13
+ # NameError (e.g. NoMethodError) within it then raise the error.
14
+ raise e if Object.const_defined? class_name
15
+ # Otherwise the NameError was a result of not being able to find the class
16
+ ActiveFedora::Base
17
+ end
18
+ end
19
+
20
+ def predicate
21
+ @options[:predicate] || ::RDF::Vocab::ORE.aggregates
5
22
  end
6
23
 
7
24
  def collection?
@@ -1,5 +1,5 @@
1
1
  module ActiveFedora
2
2
  module Aggregation
3
- VERSION = "0.2.1"
3
+ VERSION = "0.3.0"
4
4
  end
5
5
  end
@@ -2,20 +2,31 @@ require 'active_fedora/aggregation/version'
2
2
  require 'active_support'
3
3
  require 'active-fedora'
4
4
  require 'rdf/vocab'
5
+ require 'active_fedora/filter'
5
6
 
6
7
  module ActiveFedora
7
8
  module Aggregation
8
9
  extend ActiveSupport::Autoload
9
10
  eager_autoload do
10
- autoload :ProxyContainer
11
11
  autoload :Association
12
+ autoload :AggregationExtension
12
13
  autoload :Proxy
13
14
  autoload :Builder
14
- autoload :ThroughAssociation
15
15
  autoload :Reflection
16
16
  autoload :BaseExtension
17
+ autoload :LinkInserter
18
+ autoload :OrderedReader
19
+ autoload :PersistLinks
20
+ autoload :OrderedProxy
21
+ autoload :AppendsToAggregation
22
+ autoload :ProxyOwner
23
+ autoload :NullProxy
24
+ autoload :DecoratingRepository
25
+ autoload :DecoratorWithArguments
26
+ autoload :DecoratorList
27
+ autoload :ProxyRepository
17
28
  end
18
29
 
19
- ActiveFedora::Base.extend BaseExtension
30
+ ActiveFedora::Base.include BaseExtension
20
31
  end
21
32
  end
@@ -0,0 +1,70 @@
1
+ module ActiveFedora::Filter
2
+ class Association < ::ActiveFedora::Associations::CollectionAssociation
3
+ # @param [Array] records a list of records to replace the current association with
4
+ # @raise [ArgumentError] if one of the records doesn't match the prescribed condition
5
+ def writer(records)
6
+ records.each { |r| validate_assertion!(r) }
7
+ existing_matching_records.each do |r|
8
+ extending_from.delete(r)
9
+ end
10
+ extending_from.concat(records)
11
+ end
12
+
13
+ def delete(records)
14
+ extending_from.delete(records)
15
+ end
16
+
17
+ # @param [Array] records a list of records to append to the current association
18
+ # @raise [ArgumentError] if one of the records doesn't match the prescribed condition
19
+ def concat(records)
20
+ records.flatten.each { |r| validate_assertion!(r) }
21
+ extending_from.concat(records)
22
+ end
23
+
24
+ def ids_reader
25
+ load_target
26
+ super
27
+ end
28
+
29
+ private
30
+
31
+ # target should never be cached as part of this objects state, because
32
+ # extending_from.target could change and we want to reflect those changes
33
+ def target
34
+ find_target
35
+ end
36
+
37
+ def find_target?
38
+ true
39
+ end
40
+
41
+ def find_target
42
+ existing_matching_records
43
+ end
44
+
45
+ # We can't create an association scope on here until we can figure a way to
46
+ # index/query the condition in Solr
47
+ def association_scope
48
+ nil
49
+ end
50
+
51
+ def existing_matching_records
52
+ extending_from.reader.to_a.select do |r|
53
+ validate_assertion(r)
54
+ end
55
+ end
56
+
57
+ def extending_from
58
+ owner.association(options.fetch(:extending_from))
59
+ end
60
+
61
+ def validate_assertion(record)
62
+ record.send(options.fetch(:condition))
63
+ end
64
+
65
+ def validate_assertion!(record)
66
+ raise ArgumentError, "#{record.class} with ID: #{record.id} was expected to #{options.fetch(:condition)}, but it was false" unless validate_assertion(record)
67
+
68
+ end
69
+ end
70
+ end
@@ -0,0 +1,14 @@
1
+ module ActiveFedora::Filter
2
+ class Builder < ActiveFedora::Associations::Builder::CollectionAssociation
3
+ self.macro = :filter
4
+ self.valid_options = [:extending_from, :condition]
5
+
6
+ def self.define_readers(mixin, name)
7
+ super
8
+ mixin.redefine_method("#{name.to_s.singularize}_ids") do
9
+ association(name).ids_reader
10
+ end
11
+ end
12
+ end
13
+ end
14
+
@@ -0,0 +1,21 @@
1
+ module ActiveFedora::Filter
2
+ class Reflection < ActiveFedora::Reflection::AssociationReflection
3
+ def association_class
4
+ Association
5
+ end
6
+
7
+ # delegates to extending_from
8
+ def klass
9
+ extending_from.klass
10
+ end
11
+
12
+ def extending_from
13
+ @extending_from ||= active_fedora.reflect_on_association(options.fetch(:extending_from))
14
+ end
15
+
16
+ def collection?
17
+ true
18
+ end
19
+ end
20
+ end
21
+
@@ -0,0 +1,11 @@
1
+ module ActiveFedora
2
+ module Filter
3
+ extend ActiveSupport::Autoload
4
+
5
+ eager_autoload do
6
+ autoload :Builder
7
+ autoload :Reflection
8
+ autoload :Association
9
+ end
10
+ end
11
+ end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: activefedora-aggregation
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.2.1
4
+ version: 0.3.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Justin Coyne
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2015-06-11 00:00:00.000000000 Z
11
+ date: 2015-07-23 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: activesupport
@@ -108,6 +108,20 @@ dependencies:
108
108
  - - ">="
109
109
  - !ruby/object:Gem::Version
110
110
  version: '0'
111
+ - !ruby/object:Gem::Dependency
112
+ name: pry-byebug
113
+ requirement: !ruby/object:Gem::Requirement
114
+ requirements:
115
+ - - ">="
116
+ - !ruby/object:Gem::Version
117
+ version: '0'
118
+ type: :development
119
+ prerelease: false
120
+ version_requirements: !ruby/object:Gem::Requirement
121
+ requirements:
122
+ - - ">="
123
+ - !ruby/object:Gem::Version
124
+ version: '0'
111
125
  description:
112
126
  email:
113
127
  - justin@curationexperts.com
@@ -125,16 +139,32 @@ files:
125
139
  - Rakefile
126
140
  - activefedora-aggregation.gemspec
127
141
  - bin/console
142
+ - bin/jetty_wait
128
143
  - bin/setup
144
+ - circle.yml
129
145
  - lib/active_fedora/aggregation.rb
146
+ - lib/active_fedora/aggregation/aggregation_extension.rb
147
+ - lib/active_fedora/aggregation/appends_to_aggregation.rb
130
148
  - lib/active_fedora/aggregation/association.rb
131
149
  - lib/active_fedora/aggregation/base_extension.rb
132
150
  - lib/active_fedora/aggregation/builder.rb
151
+ - lib/active_fedora/aggregation/decorating_repository.rb
152
+ - lib/active_fedora/aggregation/decorator_list.rb
153
+ - lib/active_fedora/aggregation/decorator_with_arguments.rb
154
+ - lib/active_fedora/aggregation/link_inserter.rb
155
+ - lib/active_fedora/aggregation/null_proxy.rb
156
+ - lib/active_fedora/aggregation/ordered_proxy.rb
157
+ - lib/active_fedora/aggregation/ordered_reader.rb
158
+ - lib/active_fedora/aggregation/persist_links.rb
133
159
  - lib/active_fedora/aggregation/proxy.rb
134
- - lib/active_fedora/aggregation/proxy_container.rb
160
+ - lib/active_fedora/aggregation/proxy_owner.rb
161
+ - lib/active_fedora/aggregation/proxy_repository.rb
135
162
  - lib/active_fedora/aggregation/reflection.rb
136
- - lib/active_fedora/aggregation/through_association.rb
137
163
  - lib/active_fedora/aggregation/version.rb
164
+ - lib/active_fedora/filter.rb
165
+ - lib/active_fedora/filter/association.rb
166
+ - lib/active_fedora/filter/builder.rb
167
+ - lib/active_fedora/filter/reflection.rb
138
168
  homepage: http://github.org/curationexperts/activefedora-aggregation
139
169
  licenses:
140
170
  - APACHE2
@@ -155,9 +185,8 @@ required_rubygems_version: !ruby/object:Gem::Requirement
155
185
  version: '0'
156
186
  requirements: []
157
187
  rubyforge_project:
158
- rubygems_version: 2.4.8
188
+ rubygems_version: 2.4.5
159
189
  signing_key:
160
190
  specification_version: 4
161
191
  summary: Aggregations for active-fedora
162
192
  test_files: []
163
- has_rdoc:
@@ -1,111 +0,0 @@
1
- module ActiveFedora::Aggregation
2
- class ProxyContainer < ActiveFedora::Base
3
- type ::RDF::Vocab::LDP.IndirectContainer
4
-
5
- attr_writer :parent
6
-
7
- property :membership_resource, predicate: ::RDF::Vocab::LDP.membershipResource
8
- property :member_relation, predicate: ::RDF::Vocab::LDP.hasMemberRelation
9
- property :inserted_content_relation, predicate: ::RDF::Vocab::LDP.insertedContentRelation
10
-
11
- after_initialize :default_relations
12
-
13
- def parent
14
- @parent || raise("Parent hasn't been set on #{self.class}")
15
- end
16
-
17
- def default_relations
18
- self.member_relation = [::RDF::URI.new("http://pcdm.org/hasMember")] # TODO wrong predicate!
19
- self.inserted_content_relation = [::RDF::Vocab::ORE.proxyFor]
20
- end
21
-
22
- def first
23
- parent.head.target
24
- end
25
-
26
- # This can be a very expensive operation. avoid if possible
27
- def to_a
28
- @target ||= list_of_proxies.map(&:target)
29
- end
30
-
31
- def target= (collection)
32
- link_target(build_proxies(collection))
33
- end
34
-
35
- def target_ids=(object_ids)
36
- link_target(build_proxies_with_ids(object_ids))
37
- end
38
-
39
- # Set the links on the nodes in the list
40
- def link_target(new_proxies)
41
- new_proxies.each_with_index do |proxy, idx|
42
- proxy.next_id = new_proxies[idx+1].id unless new_proxies.length - 1 <= idx
43
- proxy.prev_id = new_proxies[idx-1].id unless idx == 0
44
- end
45
-
46
- parent.head = new_proxies.first
47
- parent.tail = new_proxies.last
48
- parent.proxies = new_proxies
49
- end
50
-
51
- # TODO clear out the old proxies (or reuse them)
52
- def build_proxies(objects)
53
- # need to create the proxies before we can add the links otherwise the linked to resource won't exist
54
- objects.map do |object|
55
- Proxy.create(id: mint_proxy_id, target: object)
56
- end
57
- end
58
-
59
- # TODO clear out the old proxies (or reuse them)
60
- def build_proxies_with_ids(object_ids)
61
- # need to create the proxies before we can add the links otherwise the linked to resource won't exist
62
- object_ids.map do |file_id|
63
- Proxy.create(id: mint_proxy_id, target_id: file_id)
64
- end
65
- end
66
-
67
- def target_ids
68
- list_of_proxies.map(&:target_id)
69
- end
70
-
71
- # @param obj [ActiveFedora::Base]
72
- def << (obj)
73
- node = if persisted?
74
- parent.proxies.create(id: mint_proxy_id, target: obj, prev: parent.tail)
75
- else
76
- parent.proxies.build(id: mint_proxy_id, target: obj, prev: parent.tail)
77
- end
78
- # set the old tail, if present, to have this new proxy as its next
79
- parent.tail.update(next: node) if parent.tail
80
- # update the tail to point at the new node
81
- parent.tail = node
82
- # if this is the first node, set it to be the head
83
- parent.head = node unless parent.head
84
- reset_target!
85
- end
86
-
87
- def mint_proxy_id
88
- "#{id}/#{SecureRandom.uuid}"
89
- end
90
-
91
- def self.find_or_initialize(id)
92
- find(id)
93
- rescue ActiveFedora::ObjectNotFoundError
94
- new(id)
95
- end
96
-
97
- def reset_target!
98
- @proxy_list = nil
99
- @target = nil
100
- end
101
-
102
- # return the proxies in order
103
- def list_of_proxies
104
- @proxy_list ||= if parent.head
105
- parent.head.as_list
106
- else
107
- []
108
- end
109
- end
110
- end
111
- end
@@ -1,25 +0,0 @@
1
- module ActiveFedora::Aggregation
2
- class ThroughAssociation
3
- def initialize(owner, reflection)
4
- @owner, @reflection = owner, reflection
5
- end
6
-
7
- # has_one :aggregation
8
- # has_many :generic_files, through: :aggregation
9
- def reader
10
- @file_association ||= Association.new(@owner, @reflection)
11
- end
12
-
13
- def writer(vals)
14
- reader.target = vals
15
- end
16
-
17
- def ids_writer(vals)
18
- reader.target_ids = vals
19
- end
20
-
21
- def ids_reader
22
- reader.target_ids
23
- end
24
- end
25
- end