valkyrie 2.0.0 → 2.1.2
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 +4 -4
- data/.circleci/config.yml +60 -56
- data/.lando.yml +40 -0
- data/.rubocop.yml +4 -1
- data/.tool-versions +1 -1
- data/Appraisals +4 -4
- data/CHANGELOG.md +136 -0
- data/README.md +21 -49
- data/Rakefile +21 -20
- data/db/config.yml +3 -10
- data/db/schema.rb +0 -40
- data/gemfiles/activerecord_5_2.gemfile +2 -0
- data/gemfiles/{activerecord_5_1.gemfile → activerecord_6_0.gemfile} +3 -1
- data/lib/generators/valkyrie/resource_generator.rb +3 -3
- data/lib/valkyrie.rb +33 -15
- data/lib/valkyrie/change_set.rb +3 -3
- data/lib/valkyrie/id.rb +26 -3
- data/lib/valkyrie/indexers/access_controls_indexer.rb +17 -17
- data/lib/valkyrie/logging.rb +72 -0
- data/lib/valkyrie/persistence/composite_persister.rb +1 -1
- data/lib/valkyrie/persistence/fedora.rb +2 -0
- data/lib/valkyrie/persistence/fedora/list_node.rb +46 -49
- data/lib/valkyrie/persistence/fedora/metadata_adapter.rb +2 -2
- data/lib/valkyrie/persistence/fedora/ordered_list.rb +90 -90
- data/lib/valkyrie/persistence/fedora/ordered_reader.rb +5 -5
- data/lib/valkyrie/persistence/fedora/permissive_schema.rb +3 -3
- data/lib/valkyrie/persistence/fedora/persister.rb +82 -83
- data/lib/valkyrie/persistence/fedora/persister/model_converter.rb +16 -17
- data/lib/valkyrie/persistence/fedora/persister/orm_converter.rb +38 -18
- data/lib/valkyrie/persistence/fedora/query_service.rb +54 -53
- data/lib/valkyrie/persistence/memory/persister.rb +33 -33
- data/lib/valkyrie/persistence/memory/query_service.rb +52 -34
- data/lib/valkyrie/persistence/postgres/orm_converter.rb +52 -52
- data/lib/valkyrie/persistence/postgres/query_service.rb +86 -33
- data/lib/valkyrie/persistence/postgres/resource_converter.rb +1 -1
- data/lib/valkyrie/persistence/shared/json_value_mapper.rb +4 -2
- data/lib/valkyrie/persistence/solr/model_converter.rb +337 -337
- data/lib/valkyrie/persistence/solr/orm_converter.rb +3 -3
- data/lib/valkyrie/persistence/solr/persister.rb +4 -17
- data/lib/valkyrie/persistence/solr/queries/find_all_query.rb +6 -0
- data/lib/valkyrie/persistence/solr/queries/find_members_query.rb +1 -1
- data/lib/valkyrie/persistence/solr/query_service.rb +42 -53
- data/lib/valkyrie/persistence/solr/repository.rb +2 -1
- data/lib/valkyrie/rdf_patches.rb +2 -2
- data/lib/valkyrie/resource.rb +36 -5
- data/lib/valkyrie/specs/shared_specs/change_set.rb +1 -1
- data/lib/valkyrie/specs/shared_specs/persister.rb +17 -6
- data/lib/valkyrie/specs/shared_specs/queries.rb +112 -9
- data/lib/valkyrie/storage/fedora.rb +17 -17
- data/lib/valkyrie/storage_adapter.rb +16 -13
- data/lib/valkyrie/types.rb +3 -1
- data/lib/valkyrie/version.rb +1 -1
- data/solr/config/solrconfig.xml +0 -10
- data/tasks/dev.rake +14 -51
- data/valkyrie.gemspec +4 -4
- metadata +40 -37
- data/.docker-stack/valkyrie-development/docker-compose.yml +0 -53
- data/.docker-stack/valkyrie-test/docker-compose.yml +0 -53
- data/db/seeds.rb +0 -8
- data/tasks/docker.rake +0 -31
@@ -21,65 +21,65 @@ module Valkyrie::Persistence::Postgres
|
|
21
21
|
|
22
22
|
private
|
23
23
|
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
)
|
24
|
+
# Construct a new Valkyrie Resource using the attributes retrieved from the database
|
25
|
+
# @return [Valkyrie::Resource]
|
26
|
+
def resource
|
27
|
+
resource_klass.new(
|
28
|
+
attributes.merge(
|
29
|
+
new_record: false,
|
30
|
+
Valkyrie::Persistence::Attributes::OPTIMISTIC_LOCK => lock_token
|
32
31
|
)
|
33
|
-
|
32
|
+
)
|
33
|
+
end
|
34
34
|
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
|
35
|
+
# Construct the optimistic lock token using the adapter and lock version for the Resource
|
36
|
+
# @return [Valkyrie::Persistence::OptimisticLockToken]
|
37
|
+
def lock_token
|
38
|
+
return lock_token_warning unless orm_object.class.column_names.include?("lock_version")
|
39
|
+
@lock_token ||=
|
40
|
+
Valkyrie::Persistence::OptimisticLockToken.new(
|
41
|
+
adapter_id: resource_factory.adapter_id,
|
42
|
+
token: orm_object.lock_version
|
43
|
+
)
|
44
|
+
end
|
45
45
|
|
46
|
-
|
47
|
-
|
48
|
-
|
49
|
-
|
50
|
-
|
51
|
-
|
52
|
-
|
53
|
-
|
54
|
-
|
46
|
+
# Issue a migration warning for previous releases of Valkyrie which did not support optimistic locking
|
47
|
+
def lock_token_warning
|
48
|
+
return nil unless resource_klass.optimistic_locking_enabled?
|
49
|
+
warn "[MIGRATION REQUIRED] You have loaded a resource from the Postgres adapter with " \
|
50
|
+
"optimistic locking enabled, but the necessary migrations have not been run. \n" \
|
51
|
+
"Please run `bin/rails valkyrie_engine:install:migrations && bin/rails db:migrate` " \
|
52
|
+
"to enable this feature for Postgres."
|
53
|
+
nil
|
54
|
+
end
|
55
55
|
|
56
|
-
|
57
|
-
|
58
|
-
|
59
|
-
|
60
|
-
|
56
|
+
# Retrieve the Class used to construct the Valkyrie Resource
|
57
|
+
# @return [Class]
|
58
|
+
def resource_klass
|
59
|
+
Valkyrie.config.resource_class_resolver.call(internal_resource)
|
60
|
+
end
|
61
61
|
|
62
|
-
|
63
|
-
|
64
|
-
|
65
|
-
|
66
|
-
|
62
|
+
# Access the String for the Valkyrie Resource type within the attributes
|
63
|
+
# @return [String]
|
64
|
+
def internal_resource
|
65
|
+
attributes[:internal_resource]
|
66
|
+
end
|
67
67
|
|
68
|
-
|
69
|
-
|
70
|
-
|
71
|
-
|
68
|
+
# @return [Hash] Valkyrie-style hash of attributes.
|
69
|
+
def attributes
|
70
|
+
@attributes ||= orm_object.attributes.merge(rdf_metadata).symbolize_keys
|
71
|
+
end
|
72
72
|
|
73
|
-
|
74
|
-
|
75
|
-
|
76
|
-
|
77
|
-
|
73
|
+
# Generate a Hash derived from Valkyrie Resource metadata encoded in the RDF
|
74
|
+
# @return [Hash]
|
75
|
+
def rdf_metadata
|
76
|
+
@rdf_metadata ||= RDFMetadata.new(orm_object.metadata).result
|
77
|
+
end
|
78
78
|
|
79
|
-
|
80
|
-
|
81
|
-
|
82
|
-
|
83
|
-
|
79
|
+
# Responsible for converting `metadata` JSON-B field in
|
80
|
+
# {Valkyrie::Persistence::Postgres::ORM::Resource} into an acceptable hash
|
81
|
+
# for {Valkyrie::Resource}
|
82
|
+
class RDFMetadata < ::Valkyrie::Persistence::Shared::JSONValueMapper
|
83
|
+
end
|
84
84
|
end
|
85
85
|
end
|
@@ -35,6 +35,13 @@ module Valkyrie::Persistence::Postgres
|
|
35
35
|
end
|
36
36
|
end
|
37
37
|
|
38
|
+
# Count all records for a specific resource type
|
39
|
+
# @param [Class] model
|
40
|
+
# @return integer
|
41
|
+
def count_all_of_model(model:)
|
42
|
+
orm_class.where(internal_resource: model.to_s).count
|
43
|
+
end
|
44
|
+
|
38
45
|
# Find a record using a Valkyrie ID, and map it to a Valkyrie Resource
|
39
46
|
# @param [Valkyrie::ID, String] id
|
40
47
|
# @return [Valkyrie::Resource]
|
@@ -94,30 +101,28 @@ module Valkyrie::Persistence::Postgres
|
|
94
101
|
find_inverse_references_by(resource: resource, property: :member_ids)
|
95
102
|
end
|
96
103
|
|
97
|
-
#
|
98
|
-
|
99
|
-
# @param [String] property
|
100
|
-
# @return [Array<Valkyrie::Resource>]
|
101
|
-
def find_references_by(resource:, property:)
|
104
|
+
# (see Valkyrie::Persistence::Memory::QueryService#find_references_by)
|
105
|
+
def find_references_by(resource:, property:, model: nil)
|
102
106
|
return [] if resource.id.blank? || resource[property].blank?
|
103
107
|
# only return ordered if needed to avoid performance penalties
|
104
108
|
if ordered_property?(resource: resource, property: property)
|
105
|
-
|
109
|
+
find_ordered_references_by(resource: resource, property: property, model: model)
|
106
110
|
else
|
107
|
-
|
111
|
+
find_unordered_references_by(resource: resource, property: property, model: model)
|
108
112
|
end
|
109
113
|
end
|
110
114
|
|
111
|
-
#
|
112
|
-
|
113
|
-
# @param [String] property
|
114
|
-
# @return [Array<Valkyrie::Resource>]
|
115
|
-
def find_inverse_references_by(resource: nil, id: nil, property:)
|
115
|
+
# (see Valkyrie::Persistence::Memory::QueryService#find_inverse_references_by)
|
116
|
+
def find_inverse_references_by(resource: nil, id: nil, property:, model: nil)
|
116
117
|
raise ArgumentError, "Provide resource or id" unless resource || id
|
117
118
|
ensure_persisted(resource) if resource
|
118
119
|
id ||= resource.id
|
119
120
|
internal_array = "{\"#{property}\": [{\"id\": \"#{id}\"}]}"
|
120
|
-
|
121
|
+
if model
|
122
|
+
run_query(find_inverse_references_with_type_query, internal_array, model)
|
123
|
+
else
|
124
|
+
run_query(find_inverse_references_query, internal_array)
|
125
|
+
end
|
121
126
|
end
|
122
127
|
|
123
128
|
# Execute a query in SQL for resource records and map them to Valkyrie
|
@@ -180,6 +185,21 @@ module Valkyrie::Persistence::Postgres
|
|
180
185
|
SQL
|
181
186
|
end
|
182
187
|
|
188
|
+
# Generate the SQL query for retrieving member resources in PostgreSQL using a
|
189
|
+
# JSON object literal (e. g. { "alternate_ids": [{"id": "d6e88f80-41b3-4dbf-a2a0-cd79e20f6d10"}] }).
|
190
|
+
# and resource type as arguments
|
191
|
+
# @see https://guides.rubyonrails.org/active_record_querying.html#array-conditions
|
192
|
+
# This uses JSON functions in order to retrieve JSON property values
|
193
|
+
# @see https://www.postgresql.org/docs/current/static/functions-json.html
|
194
|
+
# @return [String]
|
195
|
+
def find_inverse_references_with_type_query
|
196
|
+
<<-SQL
|
197
|
+
SELECT * FROM orm_resources WHERE
|
198
|
+
metadata @> ?
|
199
|
+
AND internal_resource = ?
|
200
|
+
SQL
|
201
|
+
end
|
202
|
+
|
183
203
|
# Generate the SQL query for retrieving member resources in PostgreSQL using a
|
184
204
|
# JSON object literal and resource ID as arguments.
|
185
205
|
# @see https://guides.rubyonrails.org/active_record_querying.html#array-conditions
|
@@ -197,6 +217,14 @@ module Valkyrie::Persistence::Postgres
|
|
197
217
|
SQL
|
198
218
|
end
|
199
219
|
|
220
|
+
def find_references_with_type_query
|
221
|
+
<<-SQL
|
222
|
+
SELECT DISTINCT member.* FROM orm_resources a,
|
223
|
+
jsonb_array_elements(a.metadata->?) AS b(member)
|
224
|
+
JOIN orm_resources member ON (b.member->>'id')::#{id_type} = member.id WHERE a.id = ? AND member.internal_resource = ?
|
225
|
+
SQL
|
226
|
+
end
|
227
|
+
|
200
228
|
def find_ordered_references_query
|
201
229
|
<<-SQL
|
202
230
|
SELECT member.* FROM orm_resources a,
|
@@ -206,6 +234,15 @@ module Valkyrie::Persistence::Postgres
|
|
206
234
|
SQL
|
207
235
|
end
|
208
236
|
|
237
|
+
def find_ordered_references_with_type_query
|
238
|
+
<<-SQL
|
239
|
+
SELECT member.* FROM orm_resources a,
|
240
|
+
jsonb_array_elements(a.metadata->?) WITH ORDINALITY AS b(member, member_pos)
|
241
|
+
JOIN orm_resources member ON (b.member->>'id')::#{id_type} = member.id WHERE a.id = ? AND member.internal_resource = ?
|
242
|
+
ORDER BY b.member_pos
|
243
|
+
SQL
|
244
|
+
end
|
245
|
+
|
209
246
|
# Constructs a Valkyrie::Persistence::CustomQueryContainer using this query service
|
210
247
|
# @return [Valkyrie::Persistence::CustomQueryContainer]
|
211
248
|
def custom_queries
|
@@ -214,30 +251,46 @@ module Valkyrie::Persistence::Postgres
|
|
214
251
|
|
215
252
|
private
|
216
253
|
|
217
|
-
|
218
|
-
|
219
|
-
|
220
|
-
|
221
|
-
|
254
|
+
def find_ordered_references_by(resource:, property:, model: nil)
|
255
|
+
if model
|
256
|
+
run_query(find_ordered_references_with_type_query, property, resource.id.to_s, model)
|
257
|
+
else
|
258
|
+
run_query(find_ordered_references_query, property, resource.id.to_s)
|
222
259
|
end
|
260
|
+
end
|
223
261
|
|
224
|
-
|
225
|
-
|
226
|
-
|
227
|
-
|
228
|
-
|
262
|
+
def find_unordered_references_by(resource:, property:, model: nil)
|
263
|
+
if model
|
264
|
+
run_query(find_references_with_type_query, property, resource.id.to_s, model)
|
265
|
+
else
|
266
|
+
run_query(find_references_query, property, resource.id.to_s)
|
229
267
|
end
|
268
|
+
end
|
230
269
|
|
231
|
-
|
232
|
-
|
233
|
-
|
234
|
-
|
235
|
-
|
236
|
-
|
237
|
-
end
|
270
|
+
# Determines whether or not an Object is a Valkyrie ID
|
271
|
+
# @param [Object] id
|
272
|
+
# @raise [ArgumentError]
|
273
|
+
def validate_id(id)
|
274
|
+
raise ArgumentError, 'id must be a Valkyrie::ID' unless id.is_a? Valkyrie::ID
|
275
|
+
end
|
238
276
|
|
239
|
-
|
240
|
-
|
241
|
-
|
277
|
+
# Determines whether or not a resource has been persisted
|
278
|
+
# @param [Object] resource
|
279
|
+
# @raise [ArgumentError]
|
280
|
+
def ensure_persisted(resource)
|
281
|
+
raise ArgumentError, 'resource is not saved' unless resource.persisted?
|
282
|
+
end
|
283
|
+
|
284
|
+
# Accesses the data type in PostgreSQL used for the primary key
|
285
|
+
# (For example, a UUID)
|
286
|
+
# @see https://api.rubyonrails.org/classes/ActiveRecord/ConnectionAdapters/SchemaCache.html#method-i-columns_hash
|
287
|
+
# @return [Symbol]
|
288
|
+
def id_type
|
289
|
+
@id_type ||= orm_class.columns_hash["id"].type
|
290
|
+
end
|
291
|
+
|
292
|
+
def ordered_property?(resource:, property:)
|
293
|
+
resource.ordered_attribute?(property)
|
294
|
+
end
|
242
295
|
end
|
243
296
|
end
|
@@ -20,7 +20,7 @@ module Valkyrie::Persistence::Postgres
|
|
20
20
|
orm_object.internal_resource = resource.internal_resource
|
21
21
|
process_lock_token(orm_object)
|
22
22
|
orm_object.disable_optimistic_locking! unless resource.optimistic_locking_enabled?
|
23
|
-
orm_object.metadata
|
23
|
+
orm_object.metadata = attributes
|
24
24
|
end
|
25
25
|
end
|
26
26
|
|
@@ -137,20 +137,22 @@ module Valkyrie::Persistence::Shared
|
|
137
137
|
# e. g. 1970-01-01
|
138
138
|
# @param [Object] value
|
139
139
|
# @return [Boolean]
|
140
|
+
# rubocop:disable Metrics/CyclomaticComplexity
|
140
141
|
def self.handles?(value)
|
141
142
|
return false unless value.is_a?(String)
|
142
|
-
return false unless value[4] == "-"
|
143
|
+
return false unless value[4] == "-" && value[10] == "T"
|
143
144
|
year = value.to_s[0..3]
|
144
145
|
return false unless year.length == 4 && year.to_i.to_s == year
|
145
146
|
DateTime.iso8601(value)
|
146
147
|
rescue
|
147
148
|
false
|
148
149
|
end
|
150
|
+
# rubocop:enable Metrics/CyclomaticComplexity
|
149
151
|
|
150
152
|
# Generates a Time object in the UTC from the datestamp string value
|
151
153
|
# @return [Time]
|
152
154
|
def result
|
153
|
-
DateTime.iso8601(value).
|
155
|
+
DateTime.iso8601(value).new_offset(0)
|
154
156
|
end
|
155
157
|
end
|
156
158
|
end
|
@@ -65,412 +65,412 @@ module Valkyrie::Persistence::Solr
|
|
65
65
|
|
66
66
|
private
|
67
67
|
|
68
|
-
|
69
|
-
|
70
|
-
|
71
|
-
|
72
|
-
|
73
|
-
|
74
|
-
|
75
|
-
|
76
|
-
|
77
|
-
|
78
|
-
|
79
|
-
|
80
|
-
end
|
68
|
+
# Maps Solr Document fields to attributes with single values
|
69
|
+
# Filters for fields which store the Valkyrie resource type
|
70
|
+
# @param [Hash] attribute_hash
|
71
|
+
# @return [Hash]
|
72
|
+
def add_single_values(attribute_hash)
|
73
|
+
attribute_hash.select do |k, v|
|
74
|
+
field = k.to_s.split("_").last
|
75
|
+
property = k.to_s.gsub("_#{field}", "")
|
76
|
+
next true if multivalued?(field)
|
77
|
+
next false if property == "internal_resource"
|
78
|
+
next false if v.length > 1
|
79
|
+
true
|
81
80
|
end
|
81
|
+
end
|
82
82
|
|
83
|
-
|
84
|
-
|
85
|
-
|
86
|
-
|
87
|
-
|
88
|
-
|
89
|
-
|
90
|
-
|
91
|
-
|
83
|
+
# Determines whether or not a field is multivalued
|
84
|
+
# @note this is tied to conventions in the Solr Schema
|
85
|
+
# @see https://github.com/samvera-labs/valkyrie/blob/master/solr/config/schema.xml
|
86
|
+
# @see https://lucene.apache.org/solr/guide/defining-fields.html#defining-fields
|
87
|
+
# @param [String] field
|
88
|
+
# @return [Boolean]
|
89
|
+
def multivalued?(field)
|
90
|
+
field.end_with?('m', 'mv')
|
91
|
+
end
|
92
92
|
|
93
|
-
|
94
|
-
|
95
|
-
|
96
|
-
|
97
|
-
|
98
|
-
|
93
|
+
# If optimistic locking is enabled for this Valkyrie Resource, generates a Hash containing the locking token
|
94
|
+
# @return [Hash]
|
95
|
+
def lock_hash
|
96
|
+
return {} unless resource.optimistic_locking_enabled? && lock_token.present?
|
97
|
+
{ _version_: lock_token }
|
98
|
+
end
|
99
99
|
|
100
|
-
|
101
|
-
|
102
|
-
|
103
|
-
|
104
|
-
|
105
|
-
|
106
|
-
|
107
|
-
|
108
|
-
end
|
100
|
+
# Retrieves the lock token from the resource attributes
|
101
|
+
# @return [String]
|
102
|
+
def lock_token
|
103
|
+
@lock_token ||= begin
|
104
|
+
found_token = resource[Valkyrie::Persistence::Attributes::OPTIMISTIC_LOCK]
|
105
|
+
.find { |token| token.adapter_id == resource_factory.adapter_id }
|
106
|
+
return if found_token.nil?
|
107
|
+
found_token.token
|
109
108
|
end
|
109
|
+
end
|
110
110
|
|
111
|
-
|
112
|
-
|
113
|
-
|
114
|
-
|
115
|
-
|
116
|
-
|
117
|
-
|
118
|
-
|
119
|
-
|
120
|
-
|
121
|
-
|
122
|
-
|
123
|
-
end
|
111
|
+
# Generates the Valkyrie Resource attribute Hash
|
112
|
+
# @return [Hash]
|
113
|
+
def attribute_hash
|
114
|
+
properties.each_with_object({}) do |property, hsh|
|
115
|
+
next if property == Valkyrie::Persistence::Attributes::OPTIMISTIC_LOCK
|
116
|
+
attr = resource_attributes[property]
|
117
|
+
mapper_val = SolrMapperValue.for(Property.new(property, attr)).result
|
118
|
+
unless mapper_val.respond_to?(:apply_to)
|
119
|
+
raise "Unable to cast #{resource_attributes[:internal_resource]}#" \
|
120
|
+
"#{property} which has been set to an instance of '#{attr.class}'"
|
121
|
+
end
|
122
|
+
mapper_val.apply_to(hsh)
|
124
123
|
end
|
124
|
+
end
|
125
125
|
|
126
|
-
|
127
|
-
|
128
|
-
|
129
|
-
|
130
|
-
|
131
|
-
|
132
|
-
|
133
|
-
|
134
|
-
|
135
|
-
|
136
|
-
|
137
|
-
|
138
|
-
|
139
|
-
|
140
|
-
|
141
|
-
|
142
|
-
|
143
|
-
|
144
|
-
|
145
|
-
|
146
|
-
|
147
|
-
|
148
|
-
|
149
|
-
|
150
|
-
|
151
|
-
|
152
|
-
end
|
126
|
+
# Accesses the keys for the attributes on the Valkyrie Resource
|
127
|
+
# @return [Array<Symbol>]
|
128
|
+
def properties
|
129
|
+
resource_attributes.keys - [:id, :created_at, :updated_at, :new_record]
|
130
|
+
end
|
131
|
+
|
132
|
+
# Access the attributes for the Valkyrie resources
|
133
|
+
# @return [Hash]
|
134
|
+
def resource_attributes
|
135
|
+
@resource_attributes ||= resource.attributes
|
136
|
+
end
|
137
|
+
|
138
|
+
##
|
139
|
+
# A container resource for holding a `key`, `value, and `scope` of a value
|
140
|
+
# in a resource together for casting.
|
141
|
+
class Property
|
142
|
+
attr_reader :key, :value, :scope
|
143
|
+
# @param key [Symbol] Property identifier.
|
144
|
+
# @param value [Object] Value or list of values which are underneath the
|
145
|
+
# key.
|
146
|
+
# @param scope [Object] The resource or point where the key and values
|
147
|
+
# came from.
|
148
|
+
def initialize(key, value, scope = [])
|
149
|
+
@key = key
|
150
|
+
@value = value
|
151
|
+
@scope = scope
|
153
152
|
end
|
153
|
+
end
|
154
154
|
|
155
|
-
|
156
|
-
|
157
|
-
|
158
|
-
|
159
|
-
|
160
|
-
|
161
|
-
|
162
|
-
|
163
|
-
|
164
|
-
|
165
|
-
|
166
|
-
|
167
|
-
|
155
|
+
##
|
156
|
+
# Represents a key/value combination in the solr document, used for isolating logic around
|
157
|
+
# how to apply a value to a hash.
|
158
|
+
class SolrRow
|
159
|
+
attr_reader :key, :fields, :values
|
160
|
+
# @param key [Symbol] Solr key.
|
161
|
+
# @param fields [Array<Symbol>] Field suffixes to index into.
|
162
|
+
# @param values [Array] Values to index into the given fields.
|
163
|
+
def initialize(key:, fields:, values:)
|
164
|
+
@key = key
|
165
|
+
@fields = Array.wrap(fields)
|
166
|
+
@values = Array.wrap(values)
|
167
|
+
end
|
168
168
|
|
169
|
-
|
170
|
-
|
171
|
-
|
172
|
-
|
173
|
-
|
174
|
-
|
175
|
-
|
176
|
-
end
|
177
|
-
hsh
|
169
|
+
# @param hsh [Hash] The solr hash to apply to.
|
170
|
+
# @return [Hash] The updated solr hash.
|
171
|
+
def apply_to(hsh)
|
172
|
+
return hsh if values.blank?
|
173
|
+
fields.each do |field|
|
174
|
+
hsh["#{key}_#{field}".to_sym] ||= []
|
175
|
+
hsh["#{key}_#{field}".to_sym] += values
|
178
176
|
end
|
177
|
+
hsh
|
179
178
|
end
|
179
|
+
end
|
180
180
|
|
181
|
-
|
182
|
-
|
183
|
-
|
184
|
-
|
185
|
-
|
181
|
+
##
|
182
|
+
# Wraps up multiple SolrRows to apply them all at once, while looking like
|
183
|
+
# just one.
|
184
|
+
class CompositeSolrRow
|
185
|
+
attr_reader :solr_rows
|
186
186
|
|
187
|
-
|
188
|
-
|
189
|
-
|
190
|
-
|
187
|
+
# @param [Array<Valkyrie::Persistence::Solr::Mapper::SolrRow>] solr_rows
|
188
|
+
def initialize(solr_rows)
|
189
|
+
@solr_rows = solr_rows
|
190
|
+
end
|
191
191
|
|
192
|
-
|
193
|
-
|
194
|
-
|
195
|
-
|
196
|
-
|
197
|
-
|
198
|
-
end
|
199
|
-
hsh
|
192
|
+
# Merge a Hash of attribute values into a logical row of Solr fields
|
193
|
+
# @param [Hash] hsh
|
194
|
+
# @see Valkyrie::Persistence::Solr::Mapper::SolrRow#apply_to
|
195
|
+
def apply_to(hsh)
|
196
|
+
solr_rows.each do |solr_row|
|
197
|
+
solr_row.apply_to(hsh)
|
200
198
|
end
|
199
|
+
hsh
|
201
200
|
end
|
201
|
+
end
|
202
202
|
|
203
|
-
|
204
|
-
|
205
|
-
|
203
|
+
# Container for casting mappers.
|
204
|
+
class SolrMapperValue < ::Valkyrie::ValueMapper
|
205
|
+
end
|
206
206
|
|
207
|
-
|
208
|
-
|
209
|
-
|
207
|
+
# Casts {Boolean} values into a recognizable string in Solr.
|
208
|
+
class BooleanPropertyValue < ::Valkyrie::ValueMapper
|
209
|
+
SolrMapperValue.register(self)
|
210
210
|
|
211
|
-
|
212
|
-
|
213
|
-
|
214
|
-
|
215
|
-
|
216
|
-
|
211
|
+
# Determines whether or not a Property value behaves like a boolean value
|
212
|
+
# @param [Object] value
|
213
|
+
# @return [Boolean]
|
214
|
+
def self.handles?(value)
|
215
|
+
value.is_a?(Property) && ([true, false].include? value.value)
|
216
|
+
end
|
217
217
|
|
218
|
-
|
219
|
-
|
220
|
-
|
221
|
-
|
222
|
-
|
223
|
-
end
|
218
|
+
# Constructs a SolrRow object for a Property with a Boolean value
|
219
|
+
# @note this prepends the string "boolean-" to the value indexed in Solr
|
220
|
+
# @return [SolrRow]
|
221
|
+
def result
|
222
|
+
calling_mapper.for(Property.new(value.key, "boolean-#{value.value}")).result
|
224
223
|
end
|
224
|
+
end
|
225
225
|
|
226
|
-
|
227
|
-
|
228
|
-
|
226
|
+
# Casts nested resources into a JSON string in solr.
|
227
|
+
class NestedObjectValue < ::Valkyrie::ValueMapper
|
228
|
+
SolrMapperValue.register(self)
|
229
229
|
|
230
|
-
|
231
|
-
|
232
|
-
|
233
|
-
|
234
|
-
|
235
|
-
|
230
|
+
# Determines whether or not a Property value is a Hash
|
231
|
+
# @param [Object] value
|
232
|
+
# @return [Boolean]
|
233
|
+
def self.handles?(value)
|
234
|
+
value.value.is_a?(Hash) || value.value.is_a?(Valkyrie::Resource)
|
235
|
+
end
|
236
236
|
|
237
|
-
|
238
|
-
|
239
|
-
|
240
|
-
|
241
|
-
|
242
|
-
|
243
|
-
|
244
|
-
|
245
|
-
end
|
237
|
+
# Constructs a SolrRow object for a Property with a Hash value
|
238
|
+
# @note this prepends the string "serialized-" to the value indexed in Solr
|
239
|
+
# This is indexed as a stored multivalued text
|
240
|
+
# @see https://lucene.apache.org/solr/guide/defining-fields.html#defining-fields
|
241
|
+
# @see https://github.com/samvera-labs/valkyrie/blob/master/solr/config/schema.xml
|
242
|
+
# @return [SolrRow]
|
243
|
+
def result
|
244
|
+
SolrRow.new(key: value.key, fields: ["tsim"], values: "serialized-#{value.value.to_json}")
|
246
245
|
end
|
246
|
+
end
|
247
247
|
|
248
|
-
|
249
|
-
|
250
|
-
|
248
|
+
# Casts enumerable values one by one.
|
249
|
+
class EnumerableValue < ::Valkyrie::ValueMapper
|
250
|
+
SolrMapperValue.register(self)
|
251
251
|
|
252
|
-
|
253
|
-
|
254
|
-
|
255
|
-
|
256
|
-
|
257
|
-
|
252
|
+
# Determines whether or not a Property value is an Array
|
253
|
+
# @param [Object] value
|
254
|
+
# @return [Boolean]
|
255
|
+
def self.handles?(value)
|
256
|
+
value.is_a?(Property) && value.value.is_a?(Array)
|
257
|
+
end
|
258
258
|
|
259
|
-
|
260
|
-
|
261
|
-
|
262
|
-
|
263
|
-
|
264
|
-
|
265
|
-
|
266
|
-
|
267
|
-
end
|
259
|
+
# Constructs a CompositeSolrRow object for a set of Property values
|
260
|
+
# @return [CompositeSolrRow]
|
261
|
+
def result
|
262
|
+
CompositeSolrRow.new(
|
263
|
+
value.value.map do |val|
|
264
|
+
calling_mapper.for(Property.new(value.key, val, value.value)).result
|
265
|
+
end
|
266
|
+
)
|
268
267
|
end
|
268
|
+
end
|
269
269
|
|
270
|
-
|
271
|
-
|
272
|
-
|
270
|
+
# Skips nil values.
|
271
|
+
class NilPropertyValue < ::Valkyrie::ValueMapper
|
272
|
+
SolrMapperValue.register(self)
|
273
273
|
|
274
|
-
|
275
|
-
|
276
|
-
|
277
|
-
|
278
|
-
|
279
|
-
|
274
|
+
# Determines whether or not a Property value responds as nil
|
275
|
+
# @param [Object] value
|
276
|
+
# @return [Boolean]
|
277
|
+
def self.handles?(value)
|
278
|
+
value.is_a?(Property) && value.value.nil?
|
279
|
+
end
|
280
280
|
|
281
|
-
|
282
|
-
|
283
|
-
|
284
|
-
|
285
|
-
end
|
281
|
+
# Constructs a SolrRow object for a Property with a nil value
|
282
|
+
# @return [SolrRow]
|
283
|
+
def result
|
284
|
+
SolrRow.new(key: value.key, fields: [], values: nil)
|
286
285
|
end
|
286
|
+
end
|
287
287
|
|
288
|
-
|
289
|
-
|
290
|
-
|
288
|
+
# Casts {Valkyrie::ID} values into a recognizable string in solr.
|
289
|
+
class IDPropertyValue < ::Valkyrie::ValueMapper
|
290
|
+
SolrMapperValue.register(self)
|
291
291
|
|
292
|
-
|
293
|
-
|
294
|
-
|
295
|
-
|
296
|
-
|
297
|
-
|
292
|
+
# Determines whether or not a Property value is a Valkyrie ID
|
293
|
+
# @param [Object] value
|
294
|
+
# @return [Boolean]
|
295
|
+
def self.handles?(value)
|
296
|
+
value.is_a?(Property) && value.value.is_a?(::Valkyrie::ID)
|
297
|
+
end
|
298
298
|
|
299
|
-
|
300
|
-
|
301
|
-
|
302
|
-
|
303
|
-
|
304
|
-
end
|
299
|
+
# Constructs a SolrRow object for the Property Valkyrie ID value
|
300
|
+
# @note this prepends the string "id-" to the value indexed in Solr
|
301
|
+
# @return [SolrRow]
|
302
|
+
def result
|
303
|
+
calling_mapper.for(Property.new(value.key, "id-#{value.value.id}")).result
|
305
304
|
end
|
305
|
+
end
|
306
306
|
|
307
|
-
|
308
|
-
|
309
|
-
|
307
|
+
# Casts {RDF::URI} values into a recognizable string in solr.
|
308
|
+
class URIPropertyValue < ::Valkyrie::ValueMapper
|
309
|
+
SolrMapperValue.register(self)
|
310
310
|
|
311
|
-
|
312
|
-
|
313
|
-
|
314
|
-
|
315
|
-
|
316
|
-
|
311
|
+
# Determines whether or not a Property value is a URI
|
312
|
+
# @param [Object] value
|
313
|
+
# @return [Boolean]
|
314
|
+
def self.handles?(value)
|
315
|
+
value.is_a?(Property) && value.value.is_a?(::RDF::URI)
|
316
|
+
end
|
317
317
|
|
318
|
-
|
319
|
-
|
320
|
-
|
321
|
-
|
322
|
-
|
323
|
-
end
|
318
|
+
# Constructs a SolrRow object for the Property URI value
|
319
|
+
# @note this prepends the string "uri-" to the value indexed in Solr
|
320
|
+
# @return [SolrRow]
|
321
|
+
def result
|
322
|
+
calling_mapper.for(Property.new(value.key, "uri-#{value.value}")).result
|
324
323
|
end
|
324
|
+
end
|
325
325
|
|
326
|
-
|
327
|
-
|
328
|
-
|
326
|
+
# Casts {Integer} values into a recognizable string in Solr.
|
327
|
+
class IntegerPropertyValue < ::Valkyrie::ValueMapper
|
328
|
+
SolrMapperValue.register(self)
|
329
329
|
|
330
|
-
|
331
|
-
|
332
|
-
|
333
|
-
|
334
|
-
|
335
|
-
|
330
|
+
# Determines whether or not a Property value is an Integer
|
331
|
+
# @param [Object] value
|
332
|
+
# @return [Boolean]
|
333
|
+
def self.handles?(value)
|
334
|
+
value.is_a?(Property) && value.value.is_a?(Integer)
|
335
|
+
end
|
336
336
|
|
337
|
-
|
338
|
-
|
339
|
-
|
340
|
-
|
341
|
-
|
342
|
-
end
|
337
|
+
# Constructs a SolrRow object for the Property Integer value
|
338
|
+
# @note this prepends the string "integer-" to the value indexed in Solr
|
339
|
+
# @return [SolrRow]
|
340
|
+
def result
|
341
|
+
calling_mapper.for(Property.new(value.key, "integer-#{value.value}")).result
|
343
342
|
end
|
343
|
+
end
|
344
344
|
|
345
|
-
|
346
|
-
|
347
|
-
|
348
|
-
|
349
|
-
|
350
|
-
|
345
|
+
# Casts {Float} values into a recognizable string in Solr.
|
346
|
+
class FloatPropertyValue < ::Valkyrie::ValueMapper
|
347
|
+
SolrMapperValue.register(self)
|
348
|
+
def self.handles?(value)
|
349
|
+
value.is_a?(Property) && value.value.is_a?(Float)
|
350
|
+
end
|
351
351
|
|
352
|
-
|
353
|
-
|
354
|
-
end
|
352
|
+
def result
|
353
|
+
calling_mapper.for(Property.new(value.key, "float-#{value.value}")).result
|
355
354
|
end
|
355
|
+
end
|
356
356
|
|
357
|
-
|
358
|
-
|
359
|
-
|
357
|
+
# Casts {DateTime} values into a recognizable string in Solr.
|
358
|
+
class DateTimePropertyValue < ::Valkyrie::ValueMapper
|
359
|
+
SolrMapperValue.register(self)
|
360
360
|
|
361
|
-
|
362
|
-
|
363
|
-
|
364
|
-
|
365
|
-
|
366
|
-
|
361
|
+
# Determines whether or not a Property value is a DateTime or Time
|
362
|
+
# @param [Object] value
|
363
|
+
# @return [Boolean]
|
364
|
+
def self.handles?(value)
|
365
|
+
value.is_a?(Property) && (value.value.is_a?(Time) || value.value.is_a?(DateTime))
|
366
|
+
end
|
367
367
|
|
368
|
-
|
369
|
-
|
370
|
-
|
371
|
-
|
372
|
-
|
373
|
-
|
368
|
+
# Constructs a SolrRow object for a datestamp derived from the value
|
369
|
+
# @note this prepends the string "datetime-" to the value indexed in Solr
|
370
|
+
# @return [SolrRow]
|
371
|
+
def result
|
372
|
+
calling_mapper.for(Property.new(value.key, "datetime-#{JSON.parse(to_datetime(value.value).to_json)}")).result
|
373
|
+
end
|
374
374
|
|
375
|
-
|
375
|
+
private
|
376
376
|
|
377
|
-
|
378
|
-
|
379
|
-
|
380
|
-
|
381
|
-
|
382
|
-
|
383
|
-
end
|
377
|
+
# Converts a value to a UTC timestamp if it is a DateTime or behaves like a Time value
|
378
|
+
# @param [Object] value
|
379
|
+
# @return [Time]
|
380
|
+
def to_datetime(value)
|
381
|
+
return value.new_offset(0) if value.is_a?(DateTime)
|
382
|
+
return value.to_datetime.new_offset(0) if value.respond_to?(:to_datetime)
|
384
383
|
end
|
384
|
+
end
|
385
385
|
|
386
|
-
|
387
|
-
|
388
|
-
|
389
|
-
|
390
|
-
|
386
|
+
# Handles casting language-tagged strings when there are both
|
387
|
+
# language-tagged and non-language-tagged strings in Solr. Assumes English
|
388
|
+
# for non-language-tagged strings.
|
389
|
+
class SharedStringPropertyValue < ::Valkyrie::ValueMapper
|
390
|
+
SolrMapperValue.register(self)
|
391
391
|
|
392
|
-
|
393
|
-
|
394
|
-
|
395
|
-
|
396
|
-
|
397
|
-
|
392
|
+
# Determines whether or not a Property value is a String whether or not the Property has an RDF literal specifying the language tag
|
393
|
+
# @param [Object] value
|
394
|
+
# @return [Boolean]
|
395
|
+
def self.handles?(value)
|
396
|
+
value.is_a?(Property) && value.value.is_a?(String) && value.scope.find { |x| x.is_a?(::RDF::Literal) }.present?
|
397
|
+
end
|
398
398
|
|
399
|
-
|
400
|
-
|
401
|
-
|
402
|
-
|
403
|
-
|
404
|
-
|
405
|
-
|
406
|
-
|
407
|
-
|
408
|
-
|
409
|
-
end
|
399
|
+
# Constructs a CompositeSolrRow object with the language-tagged literal value
|
400
|
+
# @return [CompositeSolrRow]
|
401
|
+
def result
|
402
|
+
CompositeSolrRow.new(
|
403
|
+
[
|
404
|
+
calling_mapper.for(Property.new(value.key, value.value)).result,
|
405
|
+
calling_mapper.for(Property.new("#{value.key}_lang", "eng")).result,
|
406
|
+
calling_mapper.for(Property.new("#{value.key}_type", "http://www.w3.org/1999/02/22-rdf-syntax-ns#langString")).result
|
407
|
+
]
|
408
|
+
)
|
410
409
|
end
|
410
|
+
end
|
411
411
|
|
412
|
-
|
413
|
-
|
414
|
-
|
412
|
+
# Handles casting strings.
|
413
|
+
class StringPropertyValue < ::Valkyrie::ValueMapper
|
414
|
+
SolrMapperValue.register(self)
|
415
415
|
|
416
|
-
|
417
|
-
|
418
|
-
|
419
|
-
|
420
|
-
|
421
|
-
|
416
|
+
# Determines whether or not a Property value is a String
|
417
|
+
# @param [Object] value
|
418
|
+
# @return [Boolean]
|
419
|
+
def self.handles?(value)
|
420
|
+
value.is_a?(Property) && value.value.is_a?(String)
|
421
|
+
end
|
422
422
|
|
423
|
-
|
424
|
-
|
425
|
-
|
426
|
-
|
427
|
-
|
423
|
+
# Constructs a SolrRow object with the String values and Solr field settings
|
424
|
+
# @return [SolrRow]
|
425
|
+
def result
|
426
|
+
SolrRow.new(key: value.key, fields: fields, values: value.value)
|
427
|
+
end
|
428
428
|
|
429
|
-
|
430
|
-
|
431
|
-
|
432
|
-
|
433
|
-
|
434
|
-
|
435
|
-
|
436
|
-
|
437
|
-
|
438
|
-
|
439
|
-
|
440
|
-
|
441
|
-
|
442
|
-
|
443
|
-
|
444
|
-
|
445
|
-
|
446
|
-
end
|
429
|
+
# Generates the Solr fields used during the indexing
|
430
|
+
# String are normally indexed using the following:
|
431
|
+
# - stored text
|
432
|
+
# - stored english text
|
433
|
+
# - stored single string
|
434
|
+
# - multivalued string
|
435
|
+
# - stored multivalued text
|
436
|
+
# - stored multivalued english text
|
437
|
+
# If the string is greater than 1000 characters in length, it is only indexed as a stored multivalued text
|
438
|
+
# @see https://lucene.apache.org/solr/guide/defining-fields.html#defining-fields
|
439
|
+
# @see https://github.com/samvera-labs/valkyrie/blob/master/solr/config/schema.xml
|
440
|
+
# @return [Array<Symbol>]
|
441
|
+
def fields
|
442
|
+
if value.value.length > 1000
|
443
|
+
[:tsim]
|
444
|
+
else
|
445
|
+
[:tsim, :ssim, :tesim, :tsi, :ssi, :tesi]
|
447
446
|
end
|
448
447
|
end
|
448
|
+
end
|
449
449
|
|
450
|
-
|
451
|
-
|
452
|
-
|
450
|
+
# Handles casting language-typed {RDF::Literal}s
|
451
|
+
class LiteralPropertyValue < ::Valkyrie::ValueMapper
|
452
|
+
SolrMapperValue.register(self)
|
453
453
|
|
454
|
-
|
455
|
-
|
456
|
-
|
457
|
-
|
458
|
-
|
459
|
-
|
454
|
+
# Determines whether or not a Property value is an RDF literal
|
455
|
+
# @param [Object] value
|
456
|
+
# @return [Boolean]
|
457
|
+
def self.handles?(value)
|
458
|
+
value.is_a?(Property) && value.value.is_a?(::RDF::Literal)
|
459
|
+
end
|
460
460
|
|
461
|
-
|
462
|
-
|
463
|
-
|
464
|
-
|
465
|
-
|
466
|
-
|
467
|
-
|
468
|
-
|
469
|
-
|
470
|
-
|
471
|
-
|
472
|
-
|
473
|
-
end
|
461
|
+
# Constructs a CompositeSolrRow object with the language-tagged literal value
|
462
|
+
# @return [CompositeSolrRow]
|
463
|
+
def result
|
464
|
+
key = value.key
|
465
|
+
val = value.value
|
466
|
+
CompositeSolrRow.new(
|
467
|
+
[
|
468
|
+
calling_mapper.for(Property.new(key, val.to_s)).result,
|
469
|
+
calling_mapper.for(Property.new("#{key}_lang", val.language.to_s)).result,
|
470
|
+
calling_mapper.for(Property.new("#{key}_type", val.datatype.to_s)).result
|
471
|
+
]
|
472
|
+
)
|
474
473
|
end
|
474
|
+
end
|
475
475
|
end
|
476
476
|
end
|