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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: fc7b80996e579a5e5799166881384286103a4f9f
4
- data.tar.gz: 043ff54398c18eee3edffb4cea5ecafea8bf584b
3
+ metadata.gz: 3e412a6567468dcb49dab1daa8bc699ea2205812
4
+ data.tar.gz: e41e914e33fc66e39fcb2b64fdd528d41364a8ee
5
5
  SHA512:
6
- metadata.gz: 9cf9b0d7bb610199dcdd21472ee9e20ce4a0cdd1301166d8f43ff7e03826e27c2fc32a29f841b16f6fb12f0ceaab0f92e7bf4d08d6ad8bb85e223c71603f7405
7
- data.tar.gz: 673692c667c8204105cbea4041c95728afd5d07ee80723ae389bdfa8d75c9f0e432a5139e9d6b7d9e6a98146e3da12df2cbe0e2c15e3e46d9a65ec50b5528651
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
- aggregates :generic_files
21
+ ordered_aggregation :generic_files, through: :list_source
21
22
  end
22
23
 
23
24
  image = Image.create(id: 'my_image')
24
- image.generic_files = [generic_file2, generic_file1]
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"
@@ -3,7 +3,7 @@ module ActiveFedora::Aggregation
3
3
  delegate :first, to: :ordered_reader
4
4
 
5
5
  def ordered_reader
6
- OrderedReader.new(owner).to_a
6
+ OrderedReader.new(owner).to_a.map(&:target)
7
7
  end
8
8
 
9
9
  def proxy_class
@@ -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.target
13
- proxy = proxy.next
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
 
@@ -1,5 +1,5 @@
1
1
  module ActiveFedora
2
2
  module Aggregation
3
- VERSION = "0.4.2"
3
+ VERSION = "0.5.0"
4
4
  end
5
5
  end
@@ -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,8 @@
1
+ module ActiveFedora
2
+ module Orders
3
+ class CollectionProxy < ActiveFedora::Associations::CollectionProxy
4
+ attr_reader :association
5
+ delegate :append_target, :insert_target_at, :delete_at, to: :association
6
+ end
7
+ end
8
+ end
@@ -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.2
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-09 00:00:00.000000000 Z
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: '0'
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: '0'
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