activefedora-aggregation 0.4.2 → 0.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/README.md +20 -5
- data/activefedora-aggregation.gemspec +4 -1
- data/lib/active_fedora/aggregation/association.rb +1 -1
- data/lib/active_fedora/aggregation/base_extension.rb +40 -0
- data/lib/active_fedora/aggregation/list_source.rb +86 -0
- data/lib/active_fedora/aggregation/ordered_reader.rb +8 -2
- data/lib/active_fedora/aggregation/version.rb +1 -1
- data/lib/active_fedora/aggregation.rb +2 -0
- data/lib/active_fedora/orders/aggregation_builder.rb +49 -0
- data/lib/active_fedora/orders/association.rb +133 -0
- data/lib/active_fedora/orders/builder.rb +43 -0
- data/lib/active_fedora/orders/collection_proxy.rb +8 -0
- data/lib/active_fedora/orders/list_node.rb +157 -0
- data/lib/active_fedora/orders/ordered_list.rb +253 -0
- data/lib/active_fedora/orders/reflection.rb +24 -0
- data/lib/active_fedora/orders/target_proxy.rb +43 -0
- data/lib/active_fedora/orders.rb +16 -0
- metadata +30 -6
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 3e412a6567468dcb49dab1daa8bc699ea2205812
|
4
|
+
data.tar.gz: e41e914e33fc66e39fcb2b64fdd528d41364a8ee
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 52481ea56a4dec434bd0da1affc85137d9212db3832fcce27cfaf24ac300393ff31fdb26eb0f0385d0c7f52fe044df2fff3e26f1d6e12458e77a1e66e122c0e9
|
7
|
+
data.tar.gz: 429d9ec648f2391f8a013bb3a459c5e78f3cdcd972d0ed85fe15f71c1ee1f14bf2d959476085545c7980854f11f712150df7b99c2bb2ccce5173f10aee88b247
|
data/README.md
CHANGED
@@ -15,15 +15,30 @@ end
|
|
15
15
|
|
16
16
|
generic_file1 = GenericFile.create(id: 'file1')
|
17
17
|
generic_file2 = GenericFile.create(id: 'file2')
|
18
|
+
generic_file3 = GenericFile.create(id: 'file2')
|
18
19
|
|
19
20
|
class Image < ActiveFedora::Base
|
20
|
-
|
21
|
+
ordered_aggregation :generic_files, through: :list_source
|
21
22
|
end
|
22
23
|
|
23
24
|
image = Image.create(id: 'my_image')
|
24
|
-
image.
|
25
|
+
image.ordered_generic_file_proxies.append_target generic_file2
|
26
|
+
image.ordered_generic_file_proxies.append_target generic_file1
|
25
27
|
image.save
|
26
|
-
|
28
|
+
image.generic_files # => [generic_file2, generic_file]
|
29
|
+
image.ordered_generic_files # => [generic_file2, generic_file]
|
30
|
+
|
31
|
+
# Not all generic files must be ordered.
|
32
|
+
image.generic_files += [generic_file3]
|
33
|
+
image.generic_files # => [generic_file2, generic_file, generic_file3]
|
34
|
+
image.ordered_generic_files # => [generic_file2, generic_file]
|
35
|
+
|
36
|
+
# non-ordered accessor is not ordered.
|
37
|
+
image.ordered_generic_file_proxies.insert_at(0, generic_file3)
|
38
|
+
image.generic_files # => [generic_file2, generic_file, generic_file3]
|
39
|
+
image.ordered_generic_files # => [generic_file3, generic_file2, generic_file]
|
40
|
+
|
41
|
+
# Deletions
|
42
|
+
image.ordered_generic_file_proxies.delete_at(1)
|
43
|
+
image.ordered_generic_files # => [generic_file3, generic_file]
|
27
44
|
```
|
28
|
-
|
29
|
-
Now the `generic_files` method returns an ordered list of GenericFile objects.
|
@@ -17,8 +17,11 @@ Gem::Specification.new do |spec|
|
|
17
17
|
spec.require_paths = ["lib"]
|
18
18
|
|
19
19
|
spec.add_dependency 'activesupport'
|
20
|
-
spec.add_dependency 'active-fedora'
|
20
|
+
spec.add_dependency 'active-fedora', '~> 9.5'
|
21
21
|
spec.add_dependency 'rdf-vocab', '~> 0.8.1'
|
22
|
+
# Lock to RDF 1.1.16 because 1.1.17 causes a bug from ruby 2.2
|
23
|
+
# https://github.com/ruby-rdf/rdf/pull/213
|
24
|
+
spec.add_dependency 'rdf', '~>1.1.16.0'
|
22
25
|
|
23
26
|
spec.add_development_dependency "bundler", "~> 1.8"
|
24
27
|
spec.add_development_dependency "rake", "~> 10.0"
|
@@ -12,11 +12,25 @@ module ActiveFedora::Aggregation
|
|
12
12
|
# https://jira.duraspace.org/browse/FCREPO-1497
|
13
13
|
# so we have to look up the proxies asserting RDF::Vocab::ORE.proxyFor
|
14
14
|
# and return their containers.
|
15
|
+
return [] unless id
|
15
16
|
proxy_class.where(proxyFor_ssim: id).map(&:container)
|
16
17
|
end
|
17
18
|
|
19
|
+
def ordered_by
|
20
|
+
ordered_by_ids.lazy.map{ |x| ActiveFedora::Base.find(x) }
|
21
|
+
end
|
22
|
+
|
18
23
|
private
|
19
24
|
|
25
|
+
def ordered_by_ids
|
26
|
+
if id.present?
|
27
|
+
ActiveFedora::SolrService.query("{!join from=proxy_in_ssi to=id}ordered_targets_ssim:#{id}")
|
28
|
+
.map{|x| x["id"]}
|
29
|
+
else
|
30
|
+
[]
|
31
|
+
end
|
32
|
+
end
|
33
|
+
|
20
34
|
def proxy_class
|
21
35
|
ActiveFedora::Aggregation::Proxy
|
22
36
|
end
|
@@ -32,6 +46,28 @@ module ActiveFedora::Aggregation
|
|
32
46
|
Builder.build(self, name, options)
|
33
47
|
end
|
34
48
|
|
49
|
+
##
|
50
|
+
# Allows ordering of an association
|
51
|
+
# @example
|
52
|
+
# class Image < ActiveFedora::Base
|
53
|
+
# contains :list_resource, class_name:
|
54
|
+
# "ActiveFedora::Aggregation::ListSource"
|
55
|
+
# orders :generic_files, through: :list_resource
|
56
|
+
# end
|
57
|
+
def orders(name, options={})
|
58
|
+
ActiveFedora::Orders::Builder.build(self, name, options)
|
59
|
+
end
|
60
|
+
|
61
|
+
##
|
62
|
+
# Convenience method for building an ordered aggregation.
|
63
|
+
# @example
|
64
|
+
# class Image < ActiveFedora::Base
|
65
|
+
# ordered_aggregation :members, through: :list_source
|
66
|
+
# end
|
67
|
+
def ordered_aggregation(name, options={})
|
68
|
+
ActiveFedora::Orders::AggregationBuilder.build(self, name, options)
|
69
|
+
end
|
70
|
+
|
35
71
|
##
|
36
72
|
# Create an association filter on the class
|
37
73
|
# @example
|
@@ -54,6 +90,10 @@ module ActiveFedora::Aggregation
|
|
54
90
|
ActiveFedora::Filter::Reflection.new(macro, name, options, active_fedora).tap do |reflection|
|
55
91
|
add_reflection name, reflection
|
56
92
|
end
|
93
|
+
when :orders
|
94
|
+
ActiveFedora::Orders::Reflection.new(macro, name, options, active_fedora).tap do |reflection|
|
95
|
+
add_reflection name, reflection
|
96
|
+
end
|
57
97
|
else
|
58
98
|
super
|
59
99
|
end
|
@@ -0,0 +1,86 @@
|
|
1
|
+
module ActiveFedora
|
2
|
+
module Aggregation
|
3
|
+
class ListSource < ActiveFedora::Base
|
4
|
+
property :head, predicate: ::RDF::Vocab::IANA['first'], multiple: false
|
5
|
+
property :tail, predicate: ::RDF::Vocab::IANA.last, multiple: false
|
6
|
+
property :nodes, predicate: ::RDF::DC::hasPart
|
7
|
+
|
8
|
+
def save(*args)
|
9
|
+
return true if has_unpersisted_proxy_for? || !changed?
|
10
|
+
persist_ordered_self if ordered_self.changed?
|
11
|
+
super
|
12
|
+
end
|
13
|
+
|
14
|
+
def changed?
|
15
|
+
super || ordered_self.changed?
|
16
|
+
end
|
17
|
+
|
18
|
+
# Ordered list representation of proxies in graph.
|
19
|
+
def ordered_self
|
20
|
+
@ordered_self ||= ordered_list_factory.new(resource, head_subject, tail_subject)
|
21
|
+
end
|
22
|
+
|
23
|
+
# Allow this to be set so that -=, += will work.
|
24
|
+
# @param [ActiveFedora::Orders::OrderedList] An ordered list object this
|
25
|
+
# graph should contain.
|
26
|
+
def ordered_self=(new_ordered_self)
|
27
|
+
@ordered_self = new_ordered_self
|
28
|
+
end
|
29
|
+
|
30
|
+
# Serializing head/tail/nodes slows things down CONSIDERABLY, and is not
|
31
|
+
# useful.
|
32
|
+
# @note This method is used by ActiveFedora::Base upstream for indexing,
|
33
|
+
# at https://github.com/projecthydra/active_fedora/blob/master/lib/active_fedora/profile_indexing_service.rb.
|
34
|
+
def serializable_hash(options=nil)
|
35
|
+
options ||= {}
|
36
|
+
options[:except] ||= []
|
37
|
+
options[:except] += [:head, :tail, :nodes]
|
38
|
+
super
|
39
|
+
end
|
40
|
+
|
41
|
+
def to_solr(solr_doc={})
|
42
|
+
super.merge({
|
43
|
+
ordered_targets_ssim: ordered_self.target_ids,
|
44
|
+
proxy_in_ssi: ordered_self.proxy_in.to_s
|
45
|
+
})
|
46
|
+
end
|
47
|
+
|
48
|
+
private
|
49
|
+
|
50
|
+
def persist_ordered_self
|
51
|
+
nodes_will_change!
|
52
|
+
# Delete old statements
|
53
|
+
ordered_list_factory.new(resource, head_subject, tail_subject).to_graph.statements.each do |s|
|
54
|
+
resource.delete s
|
55
|
+
end
|
56
|
+
# Assert head and tail
|
57
|
+
self.head = ordered_self.head.next.rdf_subject
|
58
|
+
self.tail = ordered_self.tail.prev.rdf_subject
|
59
|
+
graph = ordered_self.to_graph
|
60
|
+
resource << graph
|
61
|
+
# Set node subjects to a term in AF JUST so that AF will persist the
|
62
|
+
# sub-graphs.
|
63
|
+
# TODO: Find a way to fix this.
|
64
|
+
self.nodes = nil
|
65
|
+
self.nodes += graph.subjects.to_a
|
66
|
+
ordered_self.changes_committed!
|
67
|
+
end
|
68
|
+
|
69
|
+
def has_unpersisted_proxy_for?
|
70
|
+
ordered_self.flat_map(&:target).compact.select(&:new_record?).find{|x| x.respond_to?(:uri)}
|
71
|
+
end
|
72
|
+
|
73
|
+
def head_subject
|
74
|
+
head_id.first
|
75
|
+
end
|
76
|
+
|
77
|
+
def tail_subject
|
78
|
+
tail_id.first
|
79
|
+
end
|
80
|
+
|
81
|
+
def ordered_list_factory
|
82
|
+
ActiveFedora::Orders::OrderedList
|
83
|
+
end
|
84
|
+
end
|
85
|
+
end
|
86
|
+
end
|
@@ -1,4 +1,6 @@
|
|
1
1
|
module ActiveFedora::Aggregation
|
2
|
+
##
|
3
|
+
# Lazily iterates over a doubly linked list, fixing up nodes if necessary.
|
2
4
|
class OrderedReader
|
3
5
|
include Enumerable
|
4
6
|
attr_reader :root
|
@@ -9,8 +11,12 @@ module ActiveFedora::Aggregation
|
|
9
11
|
def each
|
10
12
|
proxy = first_head
|
11
13
|
while proxy
|
12
|
-
yield proxy.
|
13
|
-
|
14
|
+
yield proxy unless proxy.nil?
|
15
|
+
next_proxy = proxy.next
|
16
|
+
if next_proxy && next_proxy.prev != proxy
|
17
|
+
next_proxy.try(:prev=, proxy)
|
18
|
+
end
|
19
|
+
proxy = next_proxy
|
14
20
|
end
|
15
21
|
end
|
16
22
|
|
@@ -3,6 +3,7 @@ require 'active_support'
|
|
3
3
|
require 'active-fedora'
|
4
4
|
require 'rdf/vocab'
|
5
5
|
require 'active_fedora/filter'
|
6
|
+
require 'active_fedora/orders'
|
6
7
|
|
7
8
|
module ActiveFedora
|
8
9
|
module Aggregation
|
@@ -25,6 +26,7 @@ module ActiveFedora
|
|
25
26
|
autoload :DecoratorWithArguments
|
26
27
|
autoload :DecoratorList
|
27
28
|
autoload :ProxyRepository
|
29
|
+
autoload :ListSource
|
28
30
|
end
|
29
31
|
|
30
32
|
ActiveFedora::Base.include BaseExtension
|
@@ -0,0 +1,49 @@
|
|
1
|
+
module ActiveFedora::Orders
|
2
|
+
class AggregationBuilder < ActiveFedora::Associations::Builder::Association
|
3
|
+
self.valid_options = [:through, :class_name, :has_member_relation, :type_validator]
|
4
|
+
|
5
|
+
def self.build(model, name, options)
|
6
|
+
new(model, name, options).build
|
7
|
+
end
|
8
|
+
|
9
|
+
def build
|
10
|
+
model.indirectly_contains name, {has_member_relation: has_member_relation, through: proxy_class, foreign_key: proxy_foreign_key, inserted_content_relation: inserted_content_relation}.merge(indirect_options)
|
11
|
+
model.contains contains_key, class_name: list_source_class
|
12
|
+
model.orders name, through: contains_key
|
13
|
+
end
|
14
|
+
|
15
|
+
private
|
16
|
+
|
17
|
+
def indirect_options
|
18
|
+
{
|
19
|
+
class_name: options[:class_name],
|
20
|
+
type_validator: options[:type_validator]
|
21
|
+
}.select{ |k, v| v.present? }
|
22
|
+
end
|
23
|
+
|
24
|
+
def has_member_relation
|
25
|
+
options[:has_member_relation] || ::RDF::DC.hasPart
|
26
|
+
end
|
27
|
+
|
28
|
+
def inserted_content_relation
|
29
|
+
::RDF::Vocab::ORE::proxyFor
|
30
|
+
end
|
31
|
+
|
32
|
+
def proxy_class
|
33
|
+
"ActiveFedora::Aggregation::Proxy"
|
34
|
+
end
|
35
|
+
|
36
|
+
def proxy_foreign_key
|
37
|
+
:target
|
38
|
+
end
|
39
|
+
|
40
|
+
def contains_key
|
41
|
+
options[:through]
|
42
|
+
end
|
43
|
+
|
44
|
+
def list_source_class
|
45
|
+
"ActiveFedora::Aggregation::ListSource"
|
46
|
+
end
|
47
|
+
end
|
48
|
+
end
|
49
|
+
|
@@ -0,0 +1,133 @@
|
|
1
|
+
module ActiveFedora::Orders
|
2
|
+
class Association < ::ActiveFedora::Associations::CollectionAssociation
|
3
|
+
|
4
|
+
def initialize(*args)
|
5
|
+
super
|
6
|
+
@target = find_target
|
7
|
+
end
|
8
|
+
|
9
|
+
def reader(*args)
|
10
|
+
@proxy ||= ActiveFedora::Orders::CollectionProxy.new(self)
|
11
|
+
super
|
12
|
+
end
|
13
|
+
|
14
|
+
# Meant to override all nodes with the given nodes.
|
15
|
+
# @param [Array<ActiveFedora::Base>] nodes Nodes to set as ordered members
|
16
|
+
def target_writer(nodes)
|
17
|
+
target_reader.clear
|
18
|
+
target_reader.concat(nodes)
|
19
|
+
target_reader
|
20
|
+
end
|
21
|
+
|
22
|
+
def target_reader
|
23
|
+
@target_proxy ||= TargetProxy.new(self)
|
24
|
+
end
|
25
|
+
|
26
|
+
def find_reflection
|
27
|
+
reflection
|
28
|
+
end
|
29
|
+
|
30
|
+
def replace(new_ordered_list)
|
31
|
+
raise unless new_ordered_list.kind_of? ActiveFedora::Orders::OrderedList
|
32
|
+
list_container.ordered_self = new_ordered_list
|
33
|
+
@target = find_target
|
34
|
+
end
|
35
|
+
|
36
|
+
|
37
|
+
def find_target
|
38
|
+
ordered_proxies
|
39
|
+
end
|
40
|
+
|
41
|
+
def load_target
|
42
|
+
@target = find_target
|
43
|
+
end
|
44
|
+
|
45
|
+
# Append a target node to the end of the order.
|
46
|
+
# @param [ActiveFedora::Base] record Record to append
|
47
|
+
def append_target(record, skip_callbacks=false, &block)
|
48
|
+
unless unordered_association.target.include?(record)
|
49
|
+
unordered_association.concat(record)
|
50
|
+
end
|
51
|
+
target.append_target(record, proxy_in: owner)
|
52
|
+
end
|
53
|
+
|
54
|
+
# Insert a target node in a specific position
|
55
|
+
# @param [Integer] loc Position to insert record.
|
56
|
+
# @param [ActiveFedora::Base] record Record to insert
|
57
|
+
def insert_target_at(loc, record)
|
58
|
+
unless unordered_association.target.include?(record)
|
59
|
+
unordered_association.concat(record)
|
60
|
+
end
|
61
|
+
target.insert_at(loc, record)
|
62
|
+
end
|
63
|
+
|
64
|
+
# Delete whatever node is at a specific position
|
65
|
+
# @param [Integer] loc Position to delete
|
66
|
+
def delete_at(loc)
|
67
|
+
target.delete_at(loc)
|
68
|
+
end
|
69
|
+
|
70
|
+
# Delete multiple list nodes.
|
71
|
+
# @param [Array<ActiveFedora::Orders::ListNode>] records
|
72
|
+
def delete_records(records, _method=nil)
|
73
|
+
records.each do |record|
|
74
|
+
delete_record(record)
|
75
|
+
end
|
76
|
+
end
|
77
|
+
|
78
|
+
# Delete a list node
|
79
|
+
# @param [ActiveFedora::Orders::ListNode] record Node to delete.
|
80
|
+
def delete_record(record)
|
81
|
+
target.delete_node(record)
|
82
|
+
end
|
83
|
+
|
84
|
+
def insert_record(record, force=true, validate=true)
|
85
|
+
record.save_target
|
86
|
+
list_container.save
|
87
|
+
# NOTE: This turns out to be pretty cheap, but should we be doing it
|
88
|
+
# elsewhere?
|
89
|
+
unless list_container.changed?
|
90
|
+
owner.head = [list_container.head_id.first]
|
91
|
+
owner.tail = [list_container.tail_id.first]
|
92
|
+
owner.save
|
93
|
+
end
|
94
|
+
end
|
95
|
+
|
96
|
+
def scope(*args)
|
97
|
+
@scope ||= ActiveFedora::Relation.new(klass)
|
98
|
+
end
|
99
|
+
|
100
|
+
private
|
101
|
+
|
102
|
+
def ordered_proxies
|
103
|
+
list_container.ordered_self
|
104
|
+
end
|
105
|
+
|
106
|
+
def create_list_node(record)
|
107
|
+
node = ListNode.new(RDF::URI.new("#{list_container.uri}##{::RDF::Node.new.id}"), list_container.resource)
|
108
|
+
node.proxyIn = owner
|
109
|
+
node.proxyFor = record
|
110
|
+
node
|
111
|
+
end
|
112
|
+
|
113
|
+
def association_scope
|
114
|
+
nil
|
115
|
+
end
|
116
|
+
|
117
|
+
def list_container
|
118
|
+
list_container_association.reader
|
119
|
+
end
|
120
|
+
|
121
|
+
def list_container_association
|
122
|
+
owner.association(options[:through])
|
123
|
+
end
|
124
|
+
|
125
|
+
def unordered_association
|
126
|
+
owner.association(ordered_reflection_name)
|
127
|
+
end
|
128
|
+
|
129
|
+
def ordered_reflection_name
|
130
|
+
reflection.ordered_reflection.name
|
131
|
+
end
|
132
|
+
end
|
133
|
+
end
|
@@ -0,0 +1,43 @@
|
|
1
|
+
module ActiveFedora::Orders
|
2
|
+
class Builder < ActiveFedora::Associations::Builder::CollectionAssociation
|
3
|
+
include ActiveFedora::AutosaveAssociation::AssociationBuilderExtension
|
4
|
+
self.macro = :orders
|
5
|
+
self.valid_options += [:through, :ordered_reflection]
|
6
|
+
|
7
|
+
def self.define_readers(mixin, name)
|
8
|
+
super
|
9
|
+
mixin.redefine_method(target_accessor(name)) do
|
10
|
+
association(name).target_reader
|
11
|
+
end
|
12
|
+
mixin.redefine_method("#{target_accessor(name)}=") do |nodes|
|
13
|
+
association(name).target_writer(nodes)
|
14
|
+
end
|
15
|
+
end
|
16
|
+
|
17
|
+
def initialize(model, name, options)
|
18
|
+
@original_name = name
|
19
|
+
@model = model
|
20
|
+
name = :"ordered_#{name.to_s.singularize}_proxies"
|
21
|
+
options = {ordered_reflection: ordered_reflection}.merge(options)
|
22
|
+
super
|
23
|
+
end
|
24
|
+
|
25
|
+
def build
|
26
|
+
super.tap do
|
27
|
+
model.property :head, predicate: ::RDF::Vocab::IANA['first']
|
28
|
+
model.property :tail, predicate: ::RDF::Vocab::IANA.last
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
32
|
+
private
|
33
|
+
|
34
|
+
def self.target_accessor(name)
|
35
|
+
name.to_s.gsub("_proxies","").pluralize
|
36
|
+
end
|
37
|
+
|
38
|
+
def ordered_reflection
|
39
|
+
model.reflect_on_association(@original_name)
|
40
|
+
end
|
41
|
+
end
|
42
|
+
end
|
43
|
+
|
@@ -0,0 +1,157 @@
|
|
1
|
+
module ActiveFedora::Orders
|
2
|
+
class ListNode
|
3
|
+
attr_reader :rdf_subject
|
4
|
+
attr_accessor :prev, :next, :target
|
5
|
+
attr_writer :next_uri, :prev_uri
|
6
|
+
attr_accessor :proxy_in, :proxy_for
|
7
|
+
def initialize(node_cache, rdf_subject, graph=RDF::Graph.new)
|
8
|
+
@rdf_subject = rdf_subject
|
9
|
+
@graph = graph
|
10
|
+
@node_cache = node_cache
|
11
|
+
Builder.new(rdf_subject, graph).populate(self)
|
12
|
+
end
|
13
|
+
|
14
|
+
# Returns the next proxy or a tail sentinel.
|
15
|
+
# @return [ActiveFedora::Orders::ListNode]
|
16
|
+
def next
|
17
|
+
@next ||=
|
18
|
+
if next_uri
|
19
|
+
node_cache.fetch(next_uri) do
|
20
|
+
node = self.class.new(node_cache, next_uri, graph)
|
21
|
+
node.prev = self
|
22
|
+
node
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
# Returns the previous proxy or a head sentinel.
|
28
|
+
# @return [ActiveFedora::Orders::ListNode]
|
29
|
+
def prev
|
30
|
+
@prev ||=
|
31
|
+
if prev_uri
|
32
|
+
node_cache.fetch(prev_uri) do
|
33
|
+
node = self.class.new(node_cache, prev_uri, graph)
|
34
|
+
node.next = self
|
35
|
+
node
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
40
|
+
# Graph representation of node.
|
41
|
+
# @return [ActiveFedora::Orders::ListNode::Resource]
|
42
|
+
def to_graph
|
43
|
+
g = Resource.new(rdf_subject)
|
44
|
+
g.proxy_for = target.try(:uri)
|
45
|
+
g.proxy_in = proxy_in.try(:uri)
|
46
|
+
g.next = self.next.try(:rdf_subject)
|
47
|
+
g.prev = self.prev.try(:rdf_subject)
|
48
|
+
g
|
49
|
+
end
|
50
|
+
|
51
|
+
# Object representation of proxyFor
|
52
|
+
# @return [ActiveFedora::Base]
|
53
|
+
def target
|
54
|
+
@target ||=
|
55
|
+
if proxy_for.present?
|
56
|
+
node_cache.fetch(proxy_for) do
|
57
|
+
ActiveFedora::Base.from_uri(proxy_for, nil)
|
58
|
+
end
|
59
|
+
end
|
60
|
+
end
|
61
|
+
|
62
|
+
def target_id
|
63
|
+
MaybeID.new(@target.try(:id) || proxy_for).value
|
64
|
+
end
|
65
|
+
|
66
|
+
# Persists target if it's been accessed or set.
|
67
|
+
def save_target
|
68
|
+
if @target
|
69
|
+
@target.save
|
70
|
+
else
|
71
|
+
true
|
72
|
+
end
|
73
|
+
end
|
74
|
+
|
75
|
+
def proxy_in_id
|
76
|
+
MaybeID.new(@proxy_in.try(:id) || proxy_in).value
|
77
|
+
end
|
78
|
+
|
79
|
+
# Returns an ID whether or not the given value is a URI.
|
80
|
+
class MaybeID
|
81
|
+
attr_reader :uri_or_id
|
82
|
+
def initialize(uri_or_id)
|
83
|
+
@uri_or_id = uri_or_id
|
84
|
+
end
|
85
|
+
|
86
|
+
def value
|
87
|
+
id_composite.new([uri_or_id], translator).to_a.first
|
88
|
+
end
|
89
|
+
|
90
|
+
private
|
91
|
+
|
92
|
+
def id_composite
|
93
|
+
ActiveFedora::Associations::IDComposite
|
94
|
+
end
|
95
|
+
|
96
|
+
def translator
|
97
|
+
ActiveFedora::Base.translate_uri_to_id
|
98
|
+
end
|
99
|
+
end
|
100
|
+
|
101
|
+
# Methods necessary for association functionality
|
102
|
+
def destroyed?
|
103
|
+
false
|
104
|
+
end
|
105
|
+
|
106
|
+
def marked_for_destruction?
|
107
|
+
false
|
108
|
+
end
|
109
|
+
|
110
|
+
def valid?
|
111
|
+
true
|
112
|
+
end
|
113
|
+
|
114
|
+
def changed_for_autosave?
|
115
|
+
true
|
116
|
+
end
|
117
|
+
|
118
|
+
def new_record?
|
119
|
+
@target && @target.new_record?
|
120
|
+
end
|
121
|
+
|
122
|
+
private
|
123
|
+
|
124
|
+
attr_reader :next_uri, :prev_uri, :graph, :node_cache
|
125
|
+
|
126
|
+
class Builder
|
127
|
+
attr_reader :uri, :graph
|
128
|
+
def initialize(uri, graph)
|
129
|
+
@uri = uri
|
130
|
+
@graph = graph
|
131
|
+
end
|
132
|
+
|
133
|
+
def populate(instance)
|
134
|
+
instance.proxy_for = resource.proxy_for.first
|
135
|
+
instance.proxy_in = resource.proxy_in.first
|
136
|
+
instance.next_uri = resource.next.first
|
137
|
+
instance.prev_uri = resource.prev.first
|
138
|
+
end
|
139
|
+
|
140
|
+
private
|
141
|
+
|
142
|
+
def resource
|
143
|
+
@resource ||= Resource.new(uri, graph)
|
144
|
+
end
|
145
|
+
end
|
146
|
+
|
147
|
+
class Resource < ActiveTriples::Resource
|
148
|
+
property :proxy_for, predicate: ::RDF::Vocab::ORE.proxyFor, cast: false
|
149
|
+
property :proxy_in, predicate: ::RDF::Vocab::ORE.proxyIn, cast: false
|
150
|
+
property :next, predicate: ::RDF::Vocab::IANA.next, cast: false
|
151
|
+
property :prev, predicate: ::RDF::Vocab::IANA.prev, cast: false
|
152
|
+
def final_parent
|
153
|
+
parent
|
154
|
+
end
|
155
|
+
end
|
156
|
+
end
|
157
|
+
end
|
@@ -0,0 +1,253 @@
|
|
1
|
+
module ActiveFedora
|
2
|
+
module Orders
|
3
|
+
##
|
4
|
+
# Ruby object representation of an ORE doubly linked list.
|
5
|
+
class OrderedList
|
6
|
+
include Enumerable
|
7
|
+
attr_reader :graph, :head_subject, :tail_subject
|
8
|
+
attr_writer :head, :tail
|
9
|
+
delegate :each, to: :ordered_reader
|
10
|
+
delegate :length, to: :to_a
|
11
|
+
# @param [::RDF::Enumerable] graph Enumerable where ORE statements are
|
12
|
+
# stored.
|
13
|
+
# @param [::RDF::URI] head_subject URI of head node in list.
|
14
|
+
# @param [::RDF::URI] tail_subject URI of tail node in list.
|
15
|
+
def initialize(graph, head_subject, tail_subject)
|
16
|
+
@graph = graph
|
17
|
+
@head_subject = head_subject
|
18
|
+
@tail_subject = tail_subject
|
19
|
+
@node_cache ||= NodeCache.new
|
20
|
+
@changed = false
|
21
|
+
tail
|
22
|
+
end
|
23
|
+
|
24
|
+
# @return [HeadSentinel] Sentinel for the top of the list. If not empty,
|
25
|
+
# head.next is the first element.
|
26
|
+
def head
|
27
|
+
@head ||= HeadSentinel.new(self, next_node: build_node(head_subject))
|
28
|
+
end
|
29
|
+
|
30
|
+
# @return [TailSentinel] Sentinel for the bottom of the list. If not
|
31
|
+
# empty, tail.prev is the first element.
|
32
|
+
def tail
|
33
|
+
@tail ||=
|
34
|
+
begin
|
35
|
+
if tail_subject
|
36
|
+
TailSentinel.new(self, prev_node: build_node(tail_subject))
|
37
|
+
else
|
38
|
+
head.next
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|
42
|
+
|
43
|
+
# @param [Integer] key Position of the proxy
|
44
|
+
# @return [ListNode] Node for the proxy at the given position
|
45
|
+
def [](key)
|
46
|
+
list = ordered_reader.take(key+1)
|
47
|
+
list[key]
|
48
|
+
end
|
49
|
+
|
50
|
+
# @return [ListNode] Last node in the list.
|
51
|
+
def last
|
52
|
+
if empty?
|
53
|
+
nil
|
54
|
+
else
|
55
|
+
tail.prev
|
56
|
+
end
|
57
|
+
end
|
58
|
+
|
59
|
+
# @param [Array<ListNode>] Nodes to remove.
|
60
|
+
# @return [OrderedList] List with node removed.
|
61
|
+
def -(nodes)
|
62
|
+
nodes.each do |node|
|
63
|
+
delete_node(node)
|
64
|
+
end
|
65
|
+
self
|
66
|
+
end
|
67
|
+
|
68
|
+
# @return [Boolean]
|
69
|
+
def empty?
|
70
|
+
head.next == tail
|
71
|
+
end
|
72
|
+
|
73
|
+
# @param [ActiveFedora::Base] target Target to append to list.
|
74
|
+
# @option [::RDF::URI, ActiveFedora::Base] :proxy_in Proxy in to
|
75
|
+
# assert on the created node.
|
76
|
+
def append_target(target, proxy_in: nil)
|
77
|
+
node = build_node(new_node_subject)
|
78
|
+
node.target = target
|
79
|
+
node.proxy_in = proxy_in
|
80
|
+
append_to(node, tail.prev)
|
81
|
+
end
|
82
|
+
|
83
|
+
# @param [Integer] loc Location to insert target at
|
84
|
+
# @param [ActiveFedora::Base] target Target to insert
|
85
|
+
def insert_at(loc, target)
|
86
|
+
node = build_node(new_node_subject)
|
87
|
+
node.target = target
|
88
|
+
if loc == 0
|
89
|
+
append_to(node, head)
|
90
|
+
else
|
91
|
+
append_to(node, ordered_reader.take(loc).last)
|
92
|
+
end
|
93
|
+
end
|
94
|
+
|
95
|
+
# @param [ListNode] node Node to delete
|
96
|
+
def delete_node(node)
|
97
|
+
node = ordered_reader.find{|x| x == node}
|
98
|
+
if node
|
99
|
+
prev_node = node.prev
|
100
|
+
next_node = node.next
|
101
|
+
node.prev.next = next_node
|
102
|
+
node.next.prev = prev_node
|
103
|
+
@changed = true
|
104
|
+
end
|
105
|
+
self
|
106
|
+
end
|
107
|
+
|
108
|
+
# @param [Integer] loc Index of node to delete.
|
109
|
+
def delete_at(loc)
|
110
|
+
return self if loc == nil
|
111
|
+
arr = ordered_reader.take(loc+1)
|
112
|
+
if arr.length == loc+1
|
113
|
+
delete_node(arr.last)
|
114
|
+
else
|
115
|
+
self
|
116
|
+
end
|
117
|
+
end
|
118
|
+
|
119
|
+
# @return [Boolean] Whether this list was changed since instantiation.
|
120
|
+
def changed?
|
121
|
+
@changed
|
122
|
+
end
|
123
|
+
|
124
|
+
# @return [::RDF::Graph] Graph representation of this list.
|
125
|
+
def to_graph
|
126
|
+
::RDF::Graph.new.tap do |g|
|
127
|
+
array = to_a
|
128
|
+
array.map(&:to_graph).each do |resource_graph|
|
129
|
+
g << resource_graph
|
130
|
+
end
|
131
|
+
end
|
132
|
+
end
|
133
|
+
|
134
|
+
# Marks this list as not changed.
|
135
|
+
def changes_committed!
|
136
|
+
@changed = false
|
137
|
+
end
|
138
|
+
|
139
|
+
# @return IDs of all ordered targets, in order
|
140
|
+
def target_ids
|
141
|
+
to_a.map(&:target_id)
|
142
|
+
end
|
143
|
+
|
144
|
+
# @return The node all proxies are a proxy in.
|
145
|
+
# @note If there are multiple proxy_ins this will log a warning and return
|
146
|
+
# the first.
|
147
|
+
def proxy_in
|
148
|
+
proxies = to_a.map(&:proxy_in_id).compact.uniq
|
149
|
+
if proxies.length > 1
|
150
|
+
ActiveFedora::Base.logger.warn "WARNING: List contains nodes aggregated under different URIs. Returning only the first." if ActiveFedora::Base.logger
|
151
|
+
end
|
152
|
+
proxies.first
|
153
|
+
end
|
154
|
+
|
155
|
+
private
|
156
|
+
|
157
|
+
attr_reader :node_cache
|
158
|
+
|
159
|
+
def append_to(source, append_node)
|
160
|
+
source.prev = append_node
|
161
|
+
if append_node.next
|
162
|
+
append_node.next.prev = source
|
163
|
+
source.next = append_node.next
|
164
|
+
else
|
165
|
+
self.tail = source
|
166
|
+
end
|
167
|
+
append_node.next = source
|
168
|
+
@changed = true
|
169
|
+
end
|
170
|
+
|
171
|
+
def ordered_reader
|
172
|
+
ActiveFedora::Aggregation::OrderedReader.new(self)
|
173
|
+
end
|
174
|
+
|
175
|
+
def build_node(subject=nil)
|
176
|
+
return nil unless subject
|
177
|
+
node_cache.fetch(subject) do
|
178
|
+
ActiveFedora::Orders::ListNode.new(node_cache, subject, graph)
|
179
|
+
end
|
180
|
+
end
|
181
|
+
|
182
|
+
def new_node_subject
|
183
|
+
node = ::RDF::URI("##{::RDF::Node.new.id}")
|
184
|
+
while node_cache.has_key?(node)
|
185
|
+
node = ::RDF::URI("##{::RDF::Node.new.id}")
|
186
|
+
end
|
187
|
+
node
|
188
|
+
end
|
189
|
+
|
190
|
+
class NodeCache
|
191
|
+
def initialize
|
192
|
+
@cache ||= {}
|
193
|
+
end
|
194
|
+
|
195
|
+
def fetch(uri)
|
196
|
+
if @cache[uri]
|
197
|
+
@cache[uri]
|
198
|
+
else
|
199
|
+
if block_given?
|
200
|
+
@cache[uri] = yield
|
201
|
+
end
|
202
|
+
end
|
203
|
+
end
|
204
|
+
|
205
|
+
def has_key?(key)
|
206
|
+
@cache.has_key?(key)
|
207
|
+
end
|
208
|
+
end
|
209
|
+
|
210
|
+
class Sentinel
|
211
|
+
attr_reader :parent
|
212
|
+
attr_writer :next, :prev
|
213
|
+
def initialize(parent, next_node: nil, prev_node: nil)
|
214
|
+
@parent = parent
|
215
|
+
@next = next_node
|
216
|
+
@prev = prev_node
|
217
|
+
end
|
218
|
+
|
219
|
+
def next
|
220
|
+
@next
|
221
|
+
end
|
222
|
+
|
223
|
+
def prev
|
224
|
+
@prev
|
225
|
+
end
|
226
|
+
|
227
|
+
def nil?
|
228
|
+
true
|
229
|
+
end
|
230
|
+
|
231
|
+
def rdf_subject
|
232
|
+
nil
|
233
|
+
end
|
234
|
+
end
|
235
|
+
|
236
|
+
class HeadSentinel < Sentinel
|
237
|
+
def initialize(*args)
|
238
|
+
super
|
239
|
+
@next ||= TailSentinel.new(parent, prev_node: self)
|
240
|
+
end
|
241
|
+
end
|
242
|
+
|
243
|
+
class TailSentinel < Sentinel
|
244
|
+
def initialize(*args)
|
245
|
+
super
|
246
|
+
if prev && prev.next != self
|
247
|
+
prev.next = self
|
248
|
+
end
|
249
|
+
end
|
250
|
+
end
|
251
|
+
end
|
252
|
+
end
|
253
|
+
end
|
@@ -0,0 +1,24 @@
|
|
1
|
+
module ActiveFedora::Orders
|
2
|
+
class Reflection < ActiveFedora::Reflection::AssociationReflection
|
3
|
+
def association_class
|
4
|
+
Association
|
5
|
+
end
|
6
|
+
|
7
|
+
def collection?
|
8
|
+
true
|
9
|
+
end
|
10
|
+
|
11
|
+
def class_name
|
12
|
+
klass.to_s
|
13
|
+
end
|
14
|
+
|
15
|
+
def ordered_reflection
|
16
|
+
options[:ordered_reflection]
|
17
|
+
end
|
18
|
+
|
19
|
+
def klass
|
20
|
+
ActiveFedora::Orders::ListNode
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
@@ -0,0 +1,43 @@
|
|
1
|
+
module ActiveFedora
|
2
|
+
module Orders
|
3
|
+
class TargetProxy
|
4
|
+
attr_reader :association
|
5
|
+
delegate :+, to: :to_a
|
6
|
+
def initialize(association)
|
7
|
+
@association = association
|
8
|
+
end
|
9
|
+
|
10
|
+
def <<(obj)
|
11
|
+
association.append_target(obj)
|
12
|
+
self
|
13
|
+
end
|
14
|
+
|
15
|
+
def concat(objs)
|
16
|
+
objs.each do |obj|
|
17
|
+
self.<<(obj)
|
18
|
+
end
|
19
|
+
self
|
20
|
+
end
|
21
|
+
|
22
|
+
def clear
|
23
|
+
while to_ary.present?
|
24
|
+
association.delete_at(0)
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
def to_ary
|
29
|
+
association.reader.map(&:target).dup
|
30
|
+
end
|
31
|
+
alias_method :to_a, :to_ary
|
32
|
+
|
33
|
+
def ==(other_obj)
|
34
|
+
case other_obj
|
35
|
+
when TargetProxy
|
36
|
+
super
|
37
|
+
when Array
|
38
|
+
to_a == other_obj
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|
42
|
+
end
|
43
|
+
end
|
@@ -0,0 +1,16 @@
|
|
1
|
+
module ActiveFedora
|
2
|
+
module Orders
|
3
|
+
extend ActiveSupport::Autoload
|
4
|
+
|
5
|
+
eager_autoload do
|
6
|
+
autoload :AggregationBuilder
|
7
|
+
autoload :Association
|
8
|
+
autoload :Builder
|
9
|
+
autoload :CollectionProxy
|
10
|
+
autoload :Reflection
|
11
|
+
autoload :ListNode
|
12
|
+
autoload :OrderedList
|
13
|
+
autoload :TargetProxy
|
14
|
+
end
|
15
|
+
end
|
16
|
+
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.
|
4
|
+
version: 0.5.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-10-
|
11
|
+
date: 2015-10-21 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: activesupport
|
@@ -28,16 +28,16 @@ dependencies:
|
|
28
28
|
name: active-fedora
|
29
29
|
requirement: !ruby/object:Gem::Requirement
|
30
30
|
requirements:
|
31
|
-
- - "
|
31
|
+
- - "~>"
|
32
32
|
- !ruby/object:Gem::Version
|
33
|
-
version: '
|
33
|
+
version: '9.5'
|
34
34
|
type: :runtime
|
35
35
|
prerelease: false
|
36
36
|
version_requirements: !ruby/object:Gem::Requirement
|
37
37
|
requirements:
|
38
|
-
- - "
|
38
|
+
- - "~>"
|
39
39
|
- !ruby/object:Gem::Version
|
40
|
-
version: '
|
40
|
+
version: '9.5'
|
41
41
|
- !ruby/object:Gem::Dependency
|
42
42
|
name: rdf-vocab
|
43
43
|
requirement: !ruby/object:Gem::Requirement
|
@@ -52,6 +52,20 @@ dependencies:
|
|
52
52
|
- - "~>"
|
53
53
|
- !ruby/object:Gem::Version
|
54
54
|
version: 0.8.1
|
55
|
+
- !ruby/object:Gem::Dependency
|
56
|
+
name: rdf
|
57
|
+
requirement: !ruby/object:Gem::Requirement
|
58
|
+
requirements:
|
59
|
+
- - "~>"
|
60
|
+
- !ruby/object:Gem::Version
|
61
|
+
version: 1.1.16.0
|
62
|
+
type: :runtime
|
63
|
+
prerelease: false
|
64
|
+
version_requirements: !ruby/object:Gem::Requirement
|
65
|
+
requirements:
|
66
|
+
- - "~>"
|
67
|
+
- !ruby/object:Gem::Version
|
68
|
+
version: 1.1.16.0
|
55
69
|
- !ruby/object:Gem::Dependency
|
56
70
|
name: bundler
|
57
71
|
requirement: !ruby/object:Gem::Requirement
|
@@ -152,6 +166,7 @@ files:
|
|
152
166
|
- lib/active_fedora/aggregation/decorator_list.rb
|
153
167
|
- lib/active_fedora/aggregation/decorator_with_arguments.rb
|
154
168
|
- lib/active_fedora/aggregation/link_inserter.rb
|
169
|
+
- lib/active_fedora/aggregation/list_source.rb
|
155
170
|
- lib/active_fedora/aggregation/null_proxy.rb
|
156
171
|
- lib/active_fedora/aggregation/ordered_proxy.rb
|
157
172
|
- lib/active_fedora/aggregation/ordered_reader.rb
|
@@ -165,6 +180,15 @@ files:
|
|
165
180
|
- lib/active_fedora/filter/association.rb
|
166
181
|
- lib/active_fedora/filter/builder.rb
|
167
182
|
- lib/active_fedora/filter/reflection.rb
|
183
|
+
- lib/active_fedora/orders.rb
|
184
|
+
- lib/active_fedora/orders/aggregation_builder.rb
|
185
|
+
- lib/active_fedora/orders/association.rb
|
186
|
+
- lib/active_fedora/orders/builder.rb
|
187
|
+
- lib/active_fedora/orders/collection_proxy.rb
|
188
|
+
- lib/active_fedora/orders/list_node.rb
|
189
|
+
- lib/active_fedora/orders/ordered_list.rb
|
190
|
+
- lib/active_fedora/orders/reflection.rb
|
191
|
+
- lib/active_fedora/orders/target_proxy.rb
|
168
192
|
homepage: http://github.org/curationexperts/activefedora-aggregation
|
169
193
|
licenses:
|
170
194
|
- APACHE2
|