activefedora-aggregation 0.4.2 → 0.5.0
Sign up to get free protection for your applications and to get access to all the features.
- 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
|