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,71 @@
1
+ module ActiveFedora::Associations
2
+ class FilterAssociation < ::ActiveFedora::Associations::CollectionAssociation
3
+ # @param [Array] records a list of records to replace the current association with
4
+ # @raise [ArgumentError] if one of the records doesn't match the prescribed condition
5
+ def writer(records)
6
+ records.each { |r| validate_assertion!(r) }
7
+ existing_matching_records.each do |r|
8
+ extending_from.delete(r)
9
+ end
10
+ extending_from.concat(records)
11
+ end
12
+
13
+ delegate :delete, to: :extending_from
14
+
15
+ # @param [Array] records a list of records to append to the current association
16
+ # @raise [ArgumentError] if one of the records doesn't match the prescribed condition
17
+ def concat(records)
18
+ records.flatten.each { |r| validate_assertion!(r) }
19
+ extending_from.concat(records)
20
+ end
21
+
22
+ def ids_reader
23
+ load_target
24
+ super
25
+ end
26
+
27
+ def count_records
28
+ ids_reader.length
29
+ end
30
+
31
+ private
32
+
33
+ # target should never be cached as part of this objects state, because
34
+ # extending_from.target could change and we want to reflect those changes
35
+ def target
36
+ find_target
37
+ end
38
+
39
+ def find_target?
40
+ true
41
+ end
42
+
43
+ def find_target
44
+ existing_matching_records
45
+ end
46
+
47
+ # We can't create an association scope on here until we can figure a way to
48
+ # index/query the condition in Solr
49
+ def association_scope
50
+ nil
51
+ end
52
+
53
+ def existing_matching_records
54
+ extending_from.reader.to_a.select do |r|
55
+ validate_assertion(r)
56
+ end
57
+ end
58
+
59
+ def extending_from
60
+ owner.association(options.fetch(:extending_from))
61
+ end
62
+
63
+ def validate_assertion(record)
64
+ record.send(options.fetch(:condition))
65
+ end
66
+
67
+ def validate_assertion!(record)
68
+ raise ArgumentError, "#{record.class} with ID: #{record.id} was expected to #{options.fetch(:condition)}, but it was false" unless validate_assertion(record)
69
+ end
70
+ end
71
+ end
@@ -0,0 +1,149 @@
1
+ module ActiveFedora::Associations
2
+ class OrdersAssociation < ::ActiveFedora::Associations::CollectionAssociation
3
+ def initialize(*args)
4
+ super
5
+ @target = find_target
6
+ end
7
+
8
+ def inspect
9
+ "#<ActiveFedora::Associations::OrdersAssociation:#{object_id}>"
10
+ end
11
+
12
+ def reader(*args)
13
+ @proxy ||= ActiveFedora::Orders::CollectionProxy.new(self)
14
+ @null_proxy ||= ActiveFedora::Orders::CollectionProxy.new(self)
15
+ super
16
+ end
17
+
18
+ # Meant to override all nodes with the given nodes.
19
+ # @param [Array<ActiveFedora::Base>] nodes Nodes to set as ordered members
20
+ def target_writer(nodes)
21
+ target_reader.clear
22
+ target_reader.concat(nodes)
23
+ target_reader
24
+ end
25
+
26
+ def target_reader
27
+ @target_proxy ||= ActiveFedora::Orders::TargetProxy.new(self)
28
+ end
29
+
30
+ def find_reflection
31
+ reflection
32
+ end
33
+
34
+ def replace(new_ordered_list)
35
+ raise unless new_ordered_list.is_a? ActiveFedora::Orders::OrderedList
36
+ list_container.ordered_self = new_ordered_list
37
+ @target = find_target
38
+ end
39
+
40
+ def find_target
41
+ ordered_proxies
42
+ end
43
+
44
+ def load_target
45
+ @target = find_target
46
+ end
47
+
48
+ # Append a target node to the end of the order.
49
+ # @param [ActiveFedora::Base] record Record to append
50
+ def append_target(record, _skip_callbacks = false)
51
+ unless unordered_association.target.include?(record)
52
+ unordered_association.concat(record)
53
+ end
54
+ target.append_target(record, proxy_in: owner)
55
+ end
56
+
57
+ # Insert a target node in a specific position
58
+ # @param [Integer] loc Position to insert record.
59
+ # @param [ActiveFedora::Base] record Record to insert
60
+ def insert_target_at(loc, record)
61
+ unless unordered_association.target.include?(record)
62
+ unordered_association.concat(record)
63
+ end
64
+ target.insert_at(loc, record, proxy_in: owner)
65
+ end
66
+
67
+ # Insert a target ID in a specific position
68
+ # @param [Integer] loc Position to insert record ID
69
+ # @param [String] id ID of record to insert.
70
+ def insert_target_id_at(loc, id)
71
+ raise ArgumentError, "ID can not be nil" if id.nil?
72
+ unless unordered_association.ids_reader.include?(id)
73
+ raise ArgumentError, "#{id} is not a part of #{unordered_association.reflection.name}"
74
+ end
75
+ target.insert_proxy_for_at(loc, ActiveFedora::Base.id_to_uri(id), proxy_in: owner)
76
+ end
77
+
78
+ # Delete whatever node is at a specific position
79
+ # @param [Integer] loc Position to delete
80
+ delegate :delete_at, to: :target
81
+
82
+ # Delete all occurences of the specified target
83
+ # @param obj object to delete
84
+ delegate :delete_target, to: :target
85
+
86
+ # Delete multiple list nodes.
87
+ # @param [Array<ActiveFedora::Orders::ListNode>] records
88
+ def delete_records(records, _method = nil)
89
+ records.each do |record|
90
+ delete_record(record)
91
+ end
92
+ end
93
+
94
+ # Delete a list node
95
+ # @param [ActiveFedora::Orders::ListNode] record Node to delete.
96
+ def delete_record(record)
97
+ target.delete_node(record)
98
+ end
99
+
100
+ def insert_record(record, _force = true, _validate = true)
101
+ record.save_target
102
+ list_container.save
103
+ # NOTE: This turns out to be pretty cheap, but should we be doing it
104
+ # elsewhere?
105
+ unless list_container.changed?
106
+ owner.head = [list_container.head_id.first]
107
+ owner.tail = [list_container.tail_id.first]
108
+ owner.save
109
+ end
110
+ end
111
+
112
+ def scope(*_args)
113
+ @scope ||= ActiveFedora::Relation.new(klass)
114
+ end
115
+
116
+ private
117
+
118
+ def ordered_proxies
119
+ list_container.ordered_self
120
+ end
121
+
122
+ def create_list_node(record)
123
+ node = ListNode.new(RDF::URI.new("#{list_container.uri}##{::RDF::Node.new.id}"), list_container.resource)
124
+ node.proxyIn = owner
125
+ node.proxyFor = record
126
+ node
127
+ end
128
+
129
+ def association_scope
130
+ nil
131
+ end
132
+
133
+ def list_container
134
+ list_container_association.reader
135
+ end
136
+
137
+ def list_container_association
138
+ owner.association(options[:through])
139
+ end
140
+
141
+ def unordered_association
142
+ owner.association(unordered_reflection_name)
143
+ end
144
+
145
+ def unordered_reflection_name
146
+ reflection.unordered_reflection.name
147
+ end
148
+ end
149
+ end
@@ -131,7 +131,8 @@ module ActiveFedora
131
131
 
132
132
  def create_singleton_association(file_path)
133
133
  undeclared_files << file_path.to_sym
134
- association = Associations::BasicContainsAssociation.new(self, Reflection::AssociationReflection.new(:contains, file_path, nil, { class_name: 'ActiveFedora::File' }, self.class))
134
+
135
+ association = Associations::BasicContainsAssociation.new(self, Reflection::ContainsReflection.new(file_path, nil, { class_name: 'ActiveFedora::File' }, self.class))
135
136
  @association_cache[file_path.to_sym] = association
136
137
 
137
138
  singleton_class.send :define_method, accessor_name(file_path) do
@@ -31,23 +31,23 @@ module ActiveFedora
31
31
  super
32
32
  end
33
33
 
34
- # Raises an ActiveRecord::DangerousAttributeError exception when an
34
+ # Raises an ActiveFedora::DangerousAttributeError exception when an
35
35
  # \Active \Record method is defined in the model, otherwise +false+.
36
36
  #
37
37
  # class Person < ActiveRecord::Base
38
38
  # def save
39
- # 'already defined by Active Record'
39
+ # 'already defined by Active Fedora'
40
40
  # end
41
41
  # end
42
42
  #
43
43
  # Person.instance_method_already_implemented?(:save)
44
- # # => ActiveRecord::DangerousAttributeError: save is defined by Active Record. Check to make sure that you don't have an attribute or method with the same name.
44
+ # # => ActiveFedora::DangerousAttributeError: save is defined by Active Record. Check to make sure that you don't have an attribute or method with the same name.
45
45
  #
46
46
  # Person.instance_method_already_implemented?(:name)
47
47
  # # => false
48
48
  def instance_method_already_implemented?(method_name)
49
49
  if dangerous_attribute_method?(method_name)
50
- raise DangerousAttributeError, "#{method_name} is defined by Active Record. Check to make sure that you don't have an attribute or method with the same name."
50
+ raise DangerousAttributeError, "#{method_name} is defined by Active Fedora. Check to make sure that you don't have an attribute or method with the same name."
51
51
  end
52
52
 
53
53
  if superclass == Base
@@ -153,6 +153,7 @@ module ActiveFedora
153
153
  # Reloads the attributes of the object as usual and clears <tt>marked_for_destruction</tt> flag.
154
154
  def reload
155
155
  @marked_for_destruction = false
156
+ @destroyed_by_association = nil
156
157
  super
157
158
  end
158
159
 
@@ -172,6 +173,19 @@ module ActiveFedora
172
173
  @marked_for_destruction
173
174
  end
174
175
 
176
+ # Records the association that is being destroyed and destroying this
177
+ # record in the process.
178
+ def destroyed_by_association=(reflection)
179
+ @destroyed_by_association = reflection
180
+ end
181
+
182
+ # Returns the association for the parent being destroyed.
183
+ #
184
+ # Used to avoid updating the counter cache unnecessarily.
185
+ def destroyed_by_association
186
+ @destroyed_by_association
187
+ end
188
+
175
189
  # Returns whether or not this record has been changed in any way (including whether
176
190
  # any of its nested autosave associations are likewise changed)
177
191
  def changed_for_autosave?
@@ -51,6 +51,7 @@ module ActiveFedora
51
51
  include LoadableFromJson
52
52
  include Schema
53
53
  include Pathing
54
+ include Aggregation::BaseExtension
54
55
  end
55
56
 
56
57
  ActiveSupport.run_load_hooks(:active_fedora, Base)
@@ -44,7 +44,7 @@ module ActiveFedora
44
44
  end
45
45
 
46
46
  def self.reinitialize_repo
47
- ActiveFedora.fedora.init_base_path
47
+ ActiveFedora.reset_fedora!
48
48
  end
49
49
 
50
50
  def self.log(message)
@@ -121,4 +121,7 @@ module ActiveFedora #:nodoc:
121
121
  # Raised when you try to set a URI to an already persisted Base object.
122
122
  class AlreadyPersistedError < ActiveFedoraError
123
123
  end
124
+
125
+ class DangerousAttributeError < ActiveFedoraError
126
+ end
124
127
  end
@@ -2,7 +2,6 @@ module ActiveFedora
2
2
  class Fedora
3
3
  def initialize(config)
4
4
  @config = config
5
- init_base_path
6
5
  end
7
6
 
8
7
  def host
@@ -26,11 +25,10 @@ module ActiveFedora
26
25
  end
27
26
 
28
27
  def connection
29
- # The InboundRelationConnection does provide more data, useful for
30
- # things like ldp:IndirectContainers, but it's imposes a significant
31
- # performance penalty on every request
32
- # @connection ||= InboundRelationConnection.new(CachingConnection.new(authorized_connection))
33
- @connection ||= CachingConnection.new(authorized_connection, omit_ldpr_interaction_model: true)
28
+ @connection ||= begin
29
+ init_base_path
30
+ build_connection
31
+ end
34
32
  end
35
33
 
36
34
  def clean_connection
@@ -46,14 +44,18 @@ module ActiveFedora
46
44
 
47
45
  # Call this to create a Container Resource to act as the base path for this connection
48
46
  def init_base_path
47
+ return if @initialized
48
+ connection = build_connection
49
+
49
50
  connection.head(root_resource_path)
50
51
  ActiveFedora::Base.logger.info "Attempted to init base path `#{root_resource_path}`, but it already exists" if ActiveFedora::Base.logger
52
+ @initialized = true
51
53
  false
52
54
  rescue Ldp::NotFound
53
55
  unless host.downcase.end_with?("/rest")
54
56
  ActiveFedora::Base.logger.warn "Fedora URL (#{host}) does not end with /rest. This could be a problem. Check your fedora.yml config"
55
57
  end
56
- connection.put(root_resource_path, BLANK).success?
58
+ @initialized = connection.put(root_resource_path, BLANK).success?
57
59
  end
58
60
 
59
61
  # Remove a leading slash from the base_path
@@ -61,6 +63,14 @@ module ActiveFedora
61
63
  @root_resource_path ||= base_path.sub(SLASH, BLANK)
62
64
  end
63
65
 
66
+ def build_connection
67
+ # The InboundRelationConnection does provide more data, useful for
68
+ # things like ldp:IndirectContainers, but it's imposes a significant
69
+ # performance penalty on every request
70
+ # @connection ||= InboundRelationConnection.new(CachingConnection.new(authorized_connection))
71
+ CachingConnection.new(authorized_connection, omit_ldpr_interaction_model: true)
72
+ end
73
+
64
74
  def authorized_connection
65
75
  options = {}
66
76
  options[:ssl] = ssl_options if ssl_options
@@ -22,13 +22,22 @@ module ActiveFedora::File::Streaming
22
22
  @headers = headers
23
23
  end
24
24
 
25
- def each
25
+ def each(no_of_requests_limit = 3, &block)
26
+ raise ArgumentError, 'HTTP redirect too deep' if no_of_requests_limit == 0
26
27
  Net::HTTP.start(uri.host, uri.port) do |http|
27
28
  request = Net::HTTP::Get.new uri, headers
28
29
  http.request request do |response|
29
- raise "Couldn't get data from Fedora (#{uri}). Response: #{response.code}" unless response.is_a?(Net::HTTPSuccess)
30
- response.read_body do |chunk|
31
- yield chunk
30
+ case response
31
+ when Net::HTTPSuccess
32
+ response.read_body do |chunk|
33
+ yield chunk
34
+ end
35
+ when Net::HTTPRedirection
36
+ no_of_requests_limit -= 1
37
+ @uri = URI(response["location"])
38
+ each(no_of_requests_limit, &block)
39
+ else
40
+ raise "Couldn't get data from Fedora (#{uri}). Response: #{response.code}"
32
41
  end
33
42
  end
34
43
  end
@@ -0,0 +1,12 @@
1
+ module ActiveFedora
2
+ module Orders
3
+ extend ActiveSupport::Autoload
4
+
5
+ eager_autoload do
6
+ autoload :CollectionProxy
7
+ autoload :ListNode
8
+ autoload :OrderedList
9
+ autoload :TargetProxy
10
+ end
11
+ end
12
+ end
@@ -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, :insert_target_id_at, :delete_at, to: :association
6
+ end
7
+ end
8
+ end
@@ -0,0 +1,161 @@
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_uri
45
+ g.proxy_in = proxy_in.try(:uri)
46
+ g.next = self.next.try(:rdf_subject)
47
+ g.prev = 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_uri
63
+ RDF::URI(ActiveFedora::Base.id_to_uri(target_id)) if target_id
64
+ end
65
+
66
+ def target_id
67
+ MaybeID.new(@target.try(:id) || proxy_for).value
68
+ end
69
+
70
+ # Persists target if it's been accessed or set.
71
+ def save_target
72
+ if @target
73
+ @target.save
74
+ else
75
+ true
76
+ end
77
+ end
78
+
79
+ def proxy_in_id
80
+ MaybeID.new(@proxy_in.try(:id) || proxy_in).value
81
+ end
82
+
83
+ # Returns an ID whether or not the given value is a URI.
84
+ class MaybeID
85
+ attr_reader :uri_or_id
86
+ def initialize(uri_or_id)
87
+ @uri_or_id = uri_or_id
88
+ end
89
+
90
+ def value
91
+ id_composite.new([uri_or_id], translator).to_a.first
92
+ end
93
+
94
+ private
95
+
96
+ def id_composite
97
+ ActiveFedora::Associations::IDComposite
98
+ end
99
+
100
+ def translator
101
+ ActiveFedora::Base.translate_uri_to_id
102
+ end
103
+ end
104
+
105
+ # Methods necessary for association functionality
106
+ def destroyed?
107
+ false
108
+ end
109
+
110
+ def marked_for_destruction?
111
+ false
112
+ end
113
+
114
+ def valid?
115
+ true
116
+ end
117
+
118
+ def changed_for_autosave?
119
+ true
120
+ end
121
+
122
+ def new_record?
123
+ @target && @target.new_record?
124
+ end
125
+
126
+ private
127
+
128
+ attr_reader :next_uri, :prev_uri, :graph, :node_cache
129
+
130
+ class Builder
131
+ attr_reader :uri, :graph
132
+ def initialize(uri, graph)
133
+ @uri = uri
134
+ @graph = graph
135
+ end
136
+
137
+ def populate(instance)
138
+ instance.proxy_for = resource.proxy_for.first
139
+ instance.proxy_in = resource.proxy_in.first
140
+ instance.next_uri = resource.next.first
141
+ instance.prev_uri = resource.prev.first
142
+ end
143
+
144
+ private
145
+
146
+ def resource
147
+ @resource ||= Resource.new(uri, graph)
148
+ end
149
+ end
150
+
151
+ class Resource < ActiveTriples::Resource
152
+ property :proxy_for, predicate: ::RDF::Vocab::ORE.proxyFor, cast: false
153
+ property :proxy_in, predicate: ::RDF::Vocab::ORE.proxyIn, cast: false
154
+ property :next, predicate: ::RDF::Vocab::IANA.next, cast: false
155
+ property :prev, predicate: ::RDF::Vocab::IANA.prev, cast: false
156
+ def final_parent
157
+ parent
158
+ end
159
+ end
160
+ end
161
+ end