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