active-fedora 9.10.4 → 9.11.0

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