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 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