active-fedora 9.10.4 → 9.11.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.
Files changed (48) hide show
  1. checksums.yaml +4 -4
  2. data/.rubocop.yml +3 -0
  3. data/History.txt +0 -13
  4. data/lib/active_fedora.rb +7 -0
  5. data/lib/active_fedora/aggregation.rb +11 -0
  6. data/lib/active_fedora/aggregation/base_extension.rb +17 -0
  7. data/lib/active_fedora/aggregation/list_source.rb +103 -0
  8. data/lib/active_fedora/aggregation/ordered_reader.rb +27 -0
  9. data/lib/active_fedora/aggregation/proxy.rb +18 -0
  10. data/lib/active_fedora/associations.rb +40 -0
  11. data/lib/active_fedora/associations/builder/aggregation.rb +51 -0
  12. data/lib/active_fedora/associations/builder/filter.rb +18 -0
  13. data/lib/active_fedora/associations/builder/orders.rb +63 -0
  14. data/lib/active_fedora/associations/filter_association.rb +71 -0
  15. data/lib/active_fedora/associations/orders_association.rb +149 -0
  16. data/lib/active_fedora/attached_files.rb +2 -1
  17. data/lib/active_fedora/attribute_methods.rb +4 -4
  18. data/lib/active_fedora/autosave_association.rb +14 -0
  19. data/lib/active_fedora/base.rb +1 -0
  20. data/lib/active_fedora/cleaner.rb +1 -1
  21. data/lib/active_fedora/errors.rb +3 -0
  22. data/lib/active_fedora/fedora.rb +17 -7
  23. data/lib/active_fedora/file/streaming.rb +13 -4
  24. data/lib/active_fedora/orders.rb +12 -0
  25. data/lib/active_fedora/orders/collection_proxy.rb +8 -0
  26. data/lib/active_fedora/orders/list_node.rb +161 -0
  27. data/lib/active_fedora/orders/ordered_list.rb +264 -0
  28. data/lib/active_fedora/orders/target_proxy.rb +60 -0
  29. data/lib/active_fedora/reflection.rb +215 -42
  30. data/lib/active_fedora/validations.rb +1 -1
  31. data/lib/active_fedora/version.rb +1 -1
  32. data/lib/generators/active_fedora/config/solr/templates/solr/config/schema.xml +1 -1
  33. data/solr/config/schema.xml +1 -1
  34. data/spec/integration/attributes_spec.rb +8 -0
  35. data/spec/integration/file_spec.rb +22 -0
  36. data/spec/integration/has_many_associations_spec.rb +23 -0
  37. data/spec/integration/versionable_spec.rb +6 -6
  38. data/spec/unit/active_fedora_spec.rb +1 -1
  39. data/spec/unit/aggregation/list_source_spec.rb +134 -0
  40. data/spec/unit/aggregation/ordered_reader_spec.rb +43 -0
  41. data/spec/unit/fedora_spec.rb +1 -1
  42. data/spec/unit/filter_spec.rb +133 -0
  43. data/spec/unit/ordered_spec.rb +369 -0
  44. data/spec/unit/orders/list_node_spec.rb +151 -0
  45. data/spec/unit/orders/ordered_list_spec.rb +335 -0
  46. data/spec/unit/orders/reflection_spec.rb +22 -0
  47. data/spec/unit/reflection_spec.rb +2 -4
  48. metadata +25 -3
@@ -0,0 +1,264 @@
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 -(other)
62
+ other.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, proxy_in: nil)
86
+ node = build_node(new_node_subject)
87
+ node.target = target
88
+ node.proxy_in = proxy_in
89
+ if loc == 0
90
+ append_to(node, head)
91
+ else
92
+ append_to(node, ordered_reader.take(loc).last)
93
+ end
94
+ end
95
+
96
+ # @param [Integer] loc Location to insert target at
97
+ # @param [String] proxy_for proxyFor to add
98
+ def insert_proxy_for_at(loc, proxy_for, proxy_in: nil)
99
+ node = build_node(new_node_subject)
100
+ node.proxy_for = proxy_for
101
+ node.proxy_in = proxy_in
102
+ if loc == 0
103
+ append_to(node, head)
104
+ else
105
+ append_to(node, ordered_reader.take(loc).last)
106
+ end
107
+ end
108
+
109
+ # @param [ListNode] node Node to delete
110
+ def delete_node(node)
111
+ node = ordered_reader.find { |x| x == node }
112
+ if node
113
+ prev_node = node.prev
114
+ next_node = node.next
115
+ node.prev.next = next_node
116
+ node.next.prev = prev_node
117
+ @changed = true
118
+ node
119
+ end
120
+ end
121
+
122
+ # @param [Integer] loc Index of node to delete.
123
+ def delete_at(loc)
124
+ return nil if loc.nil?
125
+ arr = ordered_reader.take(loc + 1)
126
+ delete_node(arr.last) if arr.length == loc + 1
127
+ end
128
+
129
+ # @param obj target of node to delete.
130
+ def delete_target(obj)
131
+ nodes_to_remove = ordered_reader.select { |list_node| obj.id == list_node.target_id }
132
+ nodes_to_remove.each { |list_node| delete_node(list_node) }
133
+ nodes_to_remove.last.try(:target)
134
+ end
135
+
136
+ # @return [Boolean] Whether this list was changed since instantiation.
137
+ def changed?
138
+ @changed
139
+ end
140
+
141
+ # Marks this ordered list as about to change. Useful for when changing
142
+ # proxies individually.
143
+ def order_will_change!
144
+ @changed = true
145
+ end
146
+
147
+ # @return [::RDF::Graph] Graph representation of this list.
148
+ def to_graph
149
+ ::RDF::Graph.new.tap do |g|
150
+ array = to_a
151
+ array.map(&:to_graph).each do |resource_graph|
152
+ g << resource_graph
153
+ end
154
+ end
155
+ end
156
+
157
+ # Marks this list as not changed.
158
+ def changes_committed!
159
+ @changed = false
160
+ end
161
+
162
+ # @return IDs of all ordered targets, in order
163
+ def target_ids
164
+ to_a.map(&:target_id)
165
+ end
166
+
167
+ # @return The node all proxies are a proxy in.
168
+ # @note If there are multiple proxy_ins this will log a warning and return
169
+ # the first.
170
+ def proxy_in
171
+ proxies = to_a.map(&:proxy_in_id).compact.uniq
172
+ if proxies.length > 1
173
+ ActiveFedora::Base.logger.warn "WARNING: List contains nodes aggregated under different URIs. Returning only the first." if ActiveFedora::Base.logger
174
+ end
175
+ proxies.first
176
+ end
177
+
178
+ private
179
+
180
+ attr_reader :node_cache
181
+
182
+ def append_to(source, append_node)
183
+ source.prev = append_node
184
+ if append_node.next
185
+ append_node.next.prev = source
186
+ source.next = append_node.next
187
+ else
188
+ self.tail = source
189
+ end
190
+ append_node.next = source
191
+ @changed = true
192
+ end
193
+
194
+ def ordered_reader
195
+ ActiveFedora::Aggregation::OrderedReader.new(self)
196
+ end
197
+
198
+ def build_node(subject = nil)
199
+ return nil unless subject
200
+ node_cache.fetch(subject) do
201
+ ActiveFedora::Orders::ListNode.new(node_cache, subject, graph)
202
+ end
203
+ end
204
+
205
+ def new_node_subject
206
+ node = ::RDF::URI("##{::RDF::Node.new.id}")
207
+ while node_cache.key?(node)
208
+ node = ::RDF::URI("##{::RDF::Node.new.id}")
209
+ end
210
+ node
211
+ end
212
+
213
+ class NodeCache
214
+ def initialize
215
+ @cache ||= {}
216
+ end
217
+
218
+ def fetch(uri)
219
+ @cache[uri] ||= yield if block_given?
220
+ end
221
+
222
+ def key?(key)
223
+ @cache.key?(key)
224
+ end
225
+ end
226
+
227
+ class Sentinel
228
+ attr_reader :parent
229
+ attr_writer :next, :prev
230
+ def initialize(parent, next_node: nil, prev_node: nil)
231
+ @parent = parent
232
+ @next = next_node
233
+ @prev = prev_node
234
+ end
235
+
236
+ attr_reader :next
237
+
238
+ attr_reader :prev
239
+
240
+ def nil?
241
+ true
242
+ end
243
+
244
+ def rdf_subject
245
+ nil
246
+ end
247
+ end
248
+
249
+ class HeadSentinel < Sentinel
250
+ def initialize(*args)
251
+ super
252
+ @next ||= TailSentinel.new(parent, prev_node: self)
253
+ end
254
+ end
255
+
256
+ class TailSentinel < Sentinel
257
+ def initialize(*args)
258
+ super
259
+ prev.next = self if prev && prev.next != self
260
+ end
261
+ end
262
+ end
263
+ end
264
+ end
@@ -0,0 +1,60 @@
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 insert_at(loc, record)
23
+ association.insert_target_at(loc, record)
24
+ self
25
+ end
26
+
27
+ # Deletes the element at the specified index, returning that element, or nil if
28
+ # the index is out of range.
29
+ def delete_at(loc)
30
+ result = association.delete_at(loc)
31
+ result.target if result
32
+ end
33
+
34
+ # Deletes all items from self that are equal to obj.
35
+ # @param obj the object to remove from the list
36
+ # @return the last deleted item, or nil if no matching item is found
37
+ def delete(obj)
38
+ association.delete_target(obj)
39
+ end
40
+
41
+ def clear
42
+ association.delete_at(0) while to_ary.present?
43
+ end
44
+
45
+ def to_ary
46
+ association.reader.map(&:target).dup
47
+ end
48
+ alias to_a to_ary
49
+
50
+ def ==(other)
51
+ case other
52
+ when TargetProxy
53
+ super
54
+ when Array
55
+ to_a == other
56
+ end
57
+ end
58
+ end
59
+ end
60
+ end
@@ -14,12 +14,32 @@ module ActiveFedora
14
14
  class << self
15
15
  def create(macro, name, scope, options, active_fedora)
16
16
  klass = case macro
17
- when :has_many, :belongs_to, :has_and_belongs_to_many, :contains, :directly_contains, :directly_contains_one, :indirectly_contains
18
- AssociationReflection
19
- when :rdf, :singular_rdf
17
+ when :has_many
18
+ HasManyReflection
19
+ when :belongs_to
20
+ BelongsToReflection
21
+ when :has_and_belongs_to_many
22
+ HasAndBelongsToManyReflection
23
+ when :contains
24
+ ContainsReflection
25
+ when :directly_contains
26
+ DirectlyContainsReflection
27
+ when :directly_contains_one
28
+ DirectlyContainsOneReflection
29
+ when :indirectly_contains
30
+ IndirectlyContainsReflection
31
+ when :rdf
20
32
  RDFPropertyReflection
33
+ when :singular_rdf
34
+ SingularRDFPropertyReflection
35
+ when :filter
36
+ FilterReflection
37
+ when :aggregation, :orders
38
+ OrdersReflection
39
+ else
40
+ raise "Unsupported Macro: #{macro}"
21
41
  end
22
- reflection = klass.new(macro, name, scope, options, active_fedora)
42
+ reflection = klass.new(name, scope, options, active_fedora)
23
43
  add_reflection(active_fedora, name, reflection)
24
44
  reflection
25
45
  end
@@ -185,11 +205,6 @@ module ActiveFedora
185
205
 
186
206
  attr_reader :scope
187
207
 
188
- # Returns the macro type.
189
- #
190
- # <tt>has_many :clients</tt> returns <tt>:has_many</tt>
191
- attr_reader :macro
192
-
193
208
  # Returns the hash of options used for the macro.
194
209
  #
195
210
  # <tt>has_many :clients</tt> returns +{}+
@@ -197,8 +212,7 @@ module ActiveFedora
197
212
 
198
213
  attr_reader :active_fedora
199
214
 
200
- def initialize(macro, name, scope, options, active_fedora)
201
- @macro = macro
215
+ def initialize(name, scope, options, active_fedora)
202
216
  @name = name
203
217
  @scope = scope
204
218
  @options = options
@@ -249,7 +263,7 @@ module ActiveFedora
249
263
  class AssociationReflection < MacroReflection #:nodoc:
250
264
  attr_accessor :parent_reflection # Reflection
251
265
 
252
- def initialize(macro, name, scope, options, active_fedora)
266
+ def initialize(name, scope, options, active_fedora)
253
267
  super
254
268
  @constructable = calculate_constructable(macro, options)
255
269
  end
@@ -300,13 +314,6 @@ module ActiveFedora
300
314
  end
301
315
  alias chain collect_join_chain # todo
302
316
 
303
- # Returns whether or not this association reflection is for a collection
304
- # association. Returns +true+ if the +macro+ is either +has_many+ or
305
- # +has_and_belongs_to_many+, +false+ otherwise.
306
- def collection?
307
- [:has_many, :has_and_belongs_to_many, :directly_contains, :indirectly_contains].include?(macro)
308
- end
309
-
310
317
  def has_inverse?
311
318
  inverse_name
312
319
  end
@@ -317,6 +324,20 @@ module ActiveFedora
317
324
  @inverse_of ||= klass._reflect_on_association inverse_name
318
325
  end
319
326
 
327
+ # Returns the macro type.
328
+ #
329
+ # <tt>has_many :clients</tt> returns <tt>:has_many</tt>
330
+ def macro
331
+ raise NotImplementedError
332
+ end
333
+
334
+ # Returns whether or not this association reflection is for a collection
335
+ # association. Returns +true+ if the +macro+ is either +has_many+ or
336
+ # +has_and_belongs_to_many+, +false+ otherwise.
337
+ def collection?
338
+ false
339
+ end
340
+
320
341
  # Returns whether or not the association should be validated as part of
321
342
  # the parent's validation.
322
343
  #
@@ -331,26 +352,7 @@ module ActiveFedora
331
352
  end
332
353
 
333
354
  def association_class
334
- case macro
335
- when :contains
336
- Associations::BasicContainsAssociation
337
- when :belongs_to
338
- Associations::BelongsToAssociation
339
- when :has_and_belongs_to_many
340
- Associations::HasAndBelongsToManyAssociation
341
- when :has_many
342
- Associations::HasManyAssociation
343
- when :singular_rdf
344
- Associations::SingularRDF
345
- when :rdf
346
- Associations::RDF
347
- when :directly_contains
348
- Associations::DirectlyContainsAssociation
349
- when :directly_contains_one
350
- Associations::DirectlyContainsOneAssociation
351
- when :indirectly_contains
352
- Associations::IndirectlyContainsAssociation
353
- end
355
+ raise NotImplementedError
354
356
  end
355
357
 
356
358
  VALID_AUTOMATIC_INVERSE_MACROS = [:has_many, :has_and_belongs_to_many, :belongs_to].freeze
@@ -358,15 +360,15 @@ module ActiveFedora
358
360
 
359
361
  # Returns +true+ if +self+ is a +belongs_to+ reflection.
360
362
  def belongs_to?
361
- macro == :belongs_to
363
+ false
362
364
  end
363
365
 
364
366
  def has_many?
365
- macro == :has_many
367
+ false
366
368
  end
367
369
 
368
370
  def has_and_belongs_to_many?
369
- macro == :has_and_belongs_to_many
371
+ false
370
372
  end
371
373
 
372
374
  private
@@ -451,12 +453,122 @@ module ActiveFedora
451
453
  end
452
454
  end
453
455
 
456
+ class HasManyReflection < AssociationReflection # :nodoc:
457
+ def macro
458
+ :has_many
459
+ end
460
+
461
+ def collection?
462
+ true
463
+ end
464
+
465
+ def has_many?
466
+ true
467
+ end
468
+
469
+ def association_class
470
+ Associations::HasManyAssociation
471
+ end
472
+ end
473
+
474
+ class BelongsToReflection < AssociationReflection # :nodoc:
475
+ def macro
476
+ :belongs_to
477
+ end
478
+
479
+ def belongs_to?
480
+ true
481
+ end
482
+
483
+ def association_class
484
+ Associations::BelongsToAssociation
485
+ end
486
+ end
487
+
488
+ class HasAndBelongsToManyReflection < AssociationReflection # :nodoc:
489
+ def macro
490
+ :has_and_belongs_to_many
491
+ end
492
+
493
+ def collection?
494
+ true
495
+ end
496
+
497
+ def has_and_belongs_to_many?
498
+ true
499
+ end
500
+
501
+ def association_class
502
+ Associations::HasAndBelongsToManyAssociation
503
+ end
504
+ end
505
+
506
+ class ContainsReflection < AssociationReflection # :nodoc:
507
+ def macro
508
+ :contains
509
+ end
510
+
511
+ def association_class
512
+ Associations::BasicContainsAssociation
513
+ end
514
+ end
515
+
516
+ class DirectlyContainsReflection < AssociationReflection # :nodoc:
517
+ def macro
518
+ :directly_contains
519
+ end
520
+
521
+ def collection?
522
+ true
523
+ end
524
+
525
+ def association_class
526
+ Associations::DirectlyContainsAssociation
527
+ end
528
+ end
529
+
530
+ class DirectlyContainsOneReflection < AssociationReflection # :nodoc:
531
+ def macro
532
+ :directly_contains_one
533
+ end
534
+
535
+ def association_class
536
+ Associations::DirectlyContainsOneAssociation
537
+ end
538
+ end
539
+
540
+ class IndirectlyContainsReflection < AssociationReflection # :nodoc:
541
+ def macro
542
+ :indirectly_contains
543
+ end
544
+
545
+ def collection?
546
+ true
547
+ end
548
+
549
+ def association_class
550
+ Associations::IndirectlyContainsAssociation
551
+ end
552
+ end
553
+
454
554
  class RDFPropertyReflection < AssociationReflection
455
555
  def initialize(*args)
456
556
  super
457
557
  active_fedora.index_config[name] = build_index_config
458
558
  end
459
559
 
560
+ def macro
561
+ :rdf
562
+ end
563
+
564
+ def association_class
565
+ Associations::RDF
566
+ end
567
+
568
+ def collection?
569
+ true
570
+ end
571
+
460
572
  def derive_foreign_key
461
573
  name
462
574
  end
@@ -473,5 +585,66 @@ module ActiveFedora
473
585
  ActiveFedora::Indexing::Map::IndexObject.new(predicate_for_solr) { |index| index.as :symbol }
474
586
  end
475
587
  end
588
+
589
+ class SingularRDFPropertyReflection < RDFPropertyReflection
590
+ def macro
591
+ :singular_rdf
592
+ end
593
+
594
+ def collection?
595
+ false
596
+ end
597
+
598
+ def association_class
599
+ Associations::SingularRDF
600
+ end
601
+ end
602
+
603
+ class FilterReflection < AssociationReflection
604
+ def macro
605
+ :filter
606
+ end
607
+
608
+ def association_class
609
+ Associations::FilterAssociation
610
+ end
611
+
612
+ # delegates to extending_from
613
+ delegate :klass, to: :extending_from
614
+
615
+ def extending_from
616
+ @extending_from ||= active_fedora._reflect_on_association(options.fetch(:extending_from))
617
+ end
618
+
619
+ def collection?
620
+ true
621
+ end
622
+ end
623
+
624
+ class OrdersReflection < AssociationReflection
625
+ def macro
626
+ :orders
627
+ end
628
+
629
+ def association_class
630
+ Associations::OrdersAssociation
631
+ end
632
+
633
+ def collection?
634
+ true
635
+ end
636
+
637
+ def class_name
638
+ klass.to_s
639
+ end
640
+
641
+ def unordered_reflection
642
+ options[:unordered_reflection]
643
+ end
644
+
645
+ def klass
646
+ ActiveFedora::Orders::ListNode
647
+ end
648
+ end
476
649
  end
477
650
  end