valkyrie 1.1.2 → 1.2.0.rc1
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +5 -5
- data/.rubocop.yml +1 -1
- data/.rubocop_todo.yml +5 -0
- data/CHANGELOG.md +28 -0
- data/README.md +43 -7
- data/db/migrate/20180802220739_add_optimistic_locking_to_orm_resources.rb +6 -0
- data/lib/generators/valkyrie/templates/resource.rb.erb +0 -1
- data/lib/valkyrie/change_set.rb +4 -0
- data/lib/valkyrie/id.rb +5 -0
- data/lib/valkyrie/metadata_adapter.rb +1 -0
- data/lib/valkyrie/persistence/composite_persister.rb +12 -1
- data/lib/valkyrie/persistence/fedora/list_node.rb +3 -3
- data/lib/valkyrie/persistence/fedora/metadata_adapter.rb +4 -0
- data/lib/valkyrie/persistence/fedora/permissive_schema.rb +5 -0
- data/lib/valkyrie/persistence/fedora/persister/alternate_identifier.rb +0 -1
- data/lib/valkyrie/persistence/fedora/persister/model_converter.rb +2 -0
- data/lib/valkyrie/persistence/fedora/persister/orm_converter.rb +33 -3
- data/lib/valkyrie/persistence/fedora/persister.rb +57 -16
- data/lib/valkyrie/persistence/fedora/query_service.rb +2 -0
- data/lib/valkyrie/persistence/fedora.rb +2 -0
- data/lib/valkyrie/persistence/memory/metadata_adapter.rb +4 -0
- data/lib/valkyrie/persistence/memory/persister.rb +37 -14
- data/lib/valkyrie/persistence/memory/query_service.rb +2 -0
- data/lib/valkyrie/persistence/optimistic_lock_token.rb +34 -0
- data/lib/valkyrie/persistence/postgres/metadata_adapter.rb +11 -2
- data/lib/valkyrie/persistence/postgres/orm_converter.rb +35 -5
- data/lib/valkyrie/persistence/postgres/persister.rb +10 -12
- data/lib/valkyrie/persistence/postgres/query_service.rb +5 -4
- data/lib/valkyrie/persistence/postgres/resource_converter.rb +21 -1
- data/lib/valkyrie/persistence/postgres/resource_factory.rb +22 -18
- data/lib/valkyrie/persistence/solr/metadata_adapter.rb +5 -1
- data/lib/valkyrie/persistence/solr/model_converter.rb +32 -2
- data/lib/valkyrie/persistence/solr/orm_converter.rb +22 -5
- data/lib/valkyrie/persistence/solr/persister.rb +2 -0
- data/lib/valkyrie/persistence/solr/query_service.rb +2 -0
- data/lib/valkyrie/persistence/solr/repository.rb +21 -8
- data/lib/valkyrie/persistence/solr/resource_factory.rb +6 -3
- data/lib/valkyrie/persistence.rb +10 -3
- data/lib/valkyrie/resource/access_controls.rb +0 -1
- data/lib/valkyrie/resource.rb +31 -8
- data/lib/valkyrie/specs/shared_specs/change_set_persister.rb +0 -1
- data/lib/valkyrie/specs/shared_specs/metadata_adapter.rb +9 -0
- data/lib/valkyrie/specs/shared_specs/persister.rb +134 -7
- data/lib/valkyrie/specs/shared_specs/queries.rb +20 -2
- data/lib/valkyrie/specs/shared_specs/resource.rb +0 -1
- data/lib/valkyrie/specs/shared_specs/storage_adapter.rb +0 -1
- data/lib/valkyrie/storage/fedora.rb +29 -20
- data/lib/valkyrie/types.rb +11 -1
- data/lib/valkyrie/version.rb +1 -1
- data/lib/valkyrie.rb +0 -2
- metadata +7 -6
- data/config/fedora.yml +0 -10
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
|
-
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
2
|
+
SHA256:
|
3
|
+
metadata.gz: 8a320a108f82f79f88b27eaeb267b7b134499e346dccaae4f93165ff95e47fa5
|
4
|
+
data.tar.gz: bebc13ba65b1e1e2d69fd9a9c9408f8af8ef4484984a6de568db4c90c34eb15f
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 8a32748fc47406abdb640ce4191e6271c61a0813cb2774a444ed5c6a45a5a34fd8e599f37afab22ac6c76d71d25bcaee28df6801a3c57add7617b064fc5ed3fb
|
7
|
+
data.tar.gz: b353cc383371599df898f626b6e211294a34f5e351dad0fc0521a11841e79c04da3699c1e867f54e102c09379cba0366fa62e408738c3f7fa0f2637a20d59a14
|
data/.rubocop.yml
CHANGED
data/.rubocop_todo.yml
CHANGED
data/CHANGELOG.md
CHANGED
@@ -1,3 +1,31 @@
|
|
1
|
+
# v1.2.0.RC1 2018-08-09
|
2
|
+
|
3
|
+
## Changes since last release
|
4
|
+
|
5
|
+
* Support for single values.
|
6
|
+
[Documentation](https://github.com/samvera-labs/valkyrie/wiki/Using-Types#singular-values)
|
7
|
+
* Optimistic Locking.
|
8
|
+
[Documentation](https://github.com/samvera-labs/valkyrie/wiki/Optimistic-Locking)
|
9
|
+
* Remove reliance on ActiveFedora for Fedora Storage Adapter.
|
10
|
+
* Only load adapters if referenced.
|
11
|
+
* Postgres Adapter uses transactions for `save_all`
|
12
|
+
* Resources now include `id` attribute by default.
|
13
|
+
|
14
|
+
## Special Thanks
|
15
|
+
|
16
|
+
This release was made possible by a community sprint undertaken between Penn
|
17
|
+
State University Libraries & Princeton University Library. Thanks to the
|
18
|
+
following participants who made it happen:
|
19
|
+
|
20
|
+
* [awead](https://github.com/awead)
|
21
|
+
* [cam156](https://github.com/cam156)
|
22
|
+
* [DanCoughlin](https://github.com/DanCoughlin)
|
23
|
+
* [escowles](https://github.com/escowles)
|
24
|
+
* [hackmastera](https://github.com/hackmastera)
|
25
|
+
* [jrgriffiniii](https://github.com/jrgriffiniii)
|
26
|
+
* [mtribone](https://github.com/mtribone)
|
27
|
+
* [tpendragon](https://github.com/tpendragon)
|
28
|
+
|
1
29
|
# v1.1.2 2018-06-08
|
2
30
|
|
3
31
|
## Changes since last release
|
data/README.md
CHANGED
@@ -5,21 +5,30 @@ Valkyrie is a gem for enabling multiple backends for storage of files and metada
|
|
5
5
|
[![CircleCI](https://circleci.com/gh/samvera-labs/valkyrie.svg?style=svg)](https://circleci.com/gh/samvera-labs/valkyrie)
|
6
6
|
[![Coverage Status](https://coveralls.io/repos/github/samvera-labs/valkyrie/badge.svg?branch=master)](https://coveralls.io/github/samvera-labs/valkyrie?branch=master)
|
7
7
|
[![Stories in Ready](https://badge.waffle.io/samvera-labs/valkyrie.png?label=ready&title=Ready)](https://waffle.io/samvera-labs/valkyrie)
|
8
|
+
[![Yard Docs](http://img.shields.io/badge/yard-docs-blue.svg)](http://rubydoc.info/github/samvera-labs/valkyrie)
|
8
9
|
|
10
|
+
## Primary Contacts
|
11
|
+
|
12
|
+
### Product Owner
|
13
|
+
|
14
|
+
[Carolyn Cole](https://github.com/cam156)
|
15
|
+
|
16
|
+
### Technical Lead
|
17
|
+
|
18
|
+
[Trey Pendragon](https://github.com/tpendragon)
|
9
19
|
|
10
20
|
## Installation
|
11
21
|
|
12
22
|
Add this line to your application's Gemfile:
|
13
23
|
|
14
|
-
```
|
15
|
-
gem 'valkyrie'
|
24
|
+
```
|
25
|
+
gem 'valkyrie'
|
16
26
|
```
|
17
27
|
|
18
28
|
And then execute:
|
19
29
|
|
20
30
|
$ bundle
|
21
31
|
|
22
|
-
|
23
32
|
## Configuration
|
24
33
|
|
25
34
|
Valkyrie is configured in two places: an initializer that registers the persistence options and a YAML
|
@@ -30,6 +39,7 @@ configuration file that sets which options are used by default in which environm
|
|
30
39
|
Here is a sample initializer that registers a couple adapters and storage adapters, in each case linking an
|
31
40
|
instance with a short name that can be used to refer to it in your application:
|
32
41
|
|
42
|
+
|
33
43
|
```
|
34
44
|
# frozen_string_literal: true
|
35
45
|
require 'valkyrie'
|
@@ -50,7 +60,7 @@ Rails.application.config.to_prepare do
|
|
50
60
|
)
|
51
61
|
|
52
62
|
Valkyrie::StorageAdapter.register(
|
53
|
-
Valkyrie::Storage::Fedora.new(connection:
|
63
|
+
Valkyrie::Storage::Fedora.new(connection: Ldp::Client.new("http://localhost:8988/rest")),
|
54
64
|
:fedora
|
55
65
|
)
|
56
66
|
|
@@ -69,7 +79,7 @@ The initializer registers two `Valkyrie::MetadataAdapter` instances for storing
|
|
69
79
|
|
70
80
|
Other adapter options include `Valkyrie::Persistence::BufferedPersister` for buffering in memory before bulk
|
71
81
|
updating another persister, `Valkyrie::Persistence::CompositePersister` for storing in more than one adapter
|
72
|
-
at once, and `Valkyrie::Persistence::
|
82
|
+
at once, `Valkyrie::Persistence::Solr` for storing in Solr, and `Valkyrie::Persistence::Fedora` for storing in Fedora.
|
73
83
|
|
74
84
|
The initializer also registers three `Valkyrie::StorageAdapter` instances for storing files:
|
75
85
|
* `:disk` which stores files on disk
|
@@ -101,9 +111,23 @@ For each environment, you must set two values:
|
|
101
111
|
|
102
112
|
The values are the short names used in your initializer.
|
103
113
|
|
114
|
+
Further details can be found on the [the Wiki](https://github.com/samvera-labs/valkyrie/wiki/Persistence).
|
104
115
|
|
105
116
|
## Usage
|
106
117
|
|
118
|
+
### The Public API
|
119
|
+
|
120
|
+
Valkyrie's public API is defined by the shared specs that are used to test each of its core classes.
|
121
|
+
This include change sets, resources, persisters, adapters, and queries. When creating your own kinds of
|
122
|
+
these kinds of classes, you should use these shared specs to test your classes for conformance to
|
123
|
+
Valkyrie's API.
|
124
|
+
|
125
|
+
When breaking changes are introduced, necessitating a major version change, the shared specs will reflect
|
126
|
+
this. Likewise, non-breaking changes to Valkyrie can be defined as code changes that do not cause any
|
127
|
+
errors with the current shared specs.
|
128
|
+
|
129
|
+
Using the shared specs in your own models is described in more [detail](https://github.com/samvera-labs/valkyrie/wiki/Shared-Specs).
|
130
|
+
|
107
131
|
### Define a Custom Work
|
108
132
|
|
109
133
|
Define a custom work class:
|
@@ -112,12 +136,13 @@ Define a custom work class:
|
|
112
136
|
# frozen_string_literal: true
|
113
137
|
class MyModel < Valkyrie::Resource
|
114
138
|
include Valkyrie::Resource::AccessControls
|
115
|
-
attribute :id, Valkyrie::Types::ID.optional # Optional to allow auto-generation of IDs
|
116
139
|
attribute :title, Valkyrie::Types::Set # Sets are unordered
|
117
140
|
attribute :authors, Valkyrie::Types::Array # Arrays are ordered
|
118
141
|
end
|
119
142
|
```
|
120
143
|
|
144
|
+
Defining resource attributes is explained in greater detail within the [Wiki](https://github.com/samvera-labs/valkyrie/wiki/Using-Types).
|
145
|
+
|
121
146
|
#### Work Types Generator
|
122
147
|
|
123
148
|
To create a custom Valkyrie model in your application, you can use the Rails generator. For example, to
|
@@ -153,6 +178,10 @@ objects = adapter.query_service.find_all
|
|
153
178
|
Valkyrie.config.metadata_adapter.query_service.find_all_of_model(model: MyModel)
|
154
179
|
```
|
155
180
|
|
181
|
+
The usage of `ChangeSets` in writing data are further documented [here](https://github.com/samvera-labs/valkyrie/wiki/ChangeSets-and-Dirty-Tracking).
|
182
|
+
|
183
|
+
### Concurrency Support (Optimistic Locking)
|
184
|
+
By default, it is assumed that a Valkyrie repository implementation shall use a solution supporting concurrent updates for resources (multiple resources can be updated simultaneously using a Gem such as [Sidekiq](https://github.com/mperham/sidekiq)). In order to handle the possibility of multiple updates applied to the same resource corrupting data, Valkyrie supports optimistic locking. For further details, please reference the [overview of optimistic locking for Valkyrie resources](https://github.com/samvera-labs/valkyrie/wiki/Optimistic-Locking).
|
156
185
|
|
157
186
|
## Installing a Development environment
|
158
187
|
|
@@ -183,7 +212,7 @@ Valkyrie.config.metadata_adapter.query_service.find_all_of_model(model: MyModel)
|
|
183
212
|
|
184
213
|
1. `docker-machine create default`
|
185
214
|
1. `docker-machine start default`
|
186
|
-
1. `eval "$(docker-machine env)"
|
215
|
+
1. `eval "$(docker-machine env)"`
|
187
216
|
|
188
217
|
#### Starting the development mode dependencies
|
189
218
|
1. Start Solr, Fedora, and PostgreSQL with `rake docker:dev:daemon` (or `rake docker:dev:up` in a separate shell to run them in the foreground)
|
@@ -204,6 +233,13 @@ Valkyrie.config.metadata_adapter.query_service.find_all_of_model(model: MyModel)
|
|
204
233
|
|
205
234
|
The development and test stacks use fully contained virtual volumes and bind all services to different ports, so they can be running at the same time without issue.
|
206
235
|
|
236
|
+
## Get Help
|
237
|
+
|
238
|
+
If you have any questions regarding Valkyrie you can send a message to [the
|
239
|
+
Samvera community tech list](mailto:samvera-tech@googlegroups.com) or the `#valkyrie`
|
240
|
+
channel in the [Samvera community Slack
|
241
|
+
team](https://wiki.duraspace.org/pages/viewpage.action?pageId=87460391#Getintouch!-Slack).
|
242
|
+
|
207
243
|
## License
|
208
244
|
|
209
245
|
Valkyrie is available under [the Apache 2.0 license](../LICENSE).
|
@@ -2,7 +2,6 @@
|
|
2
2
|
# Generated with `rails generate valkyrie:model <%= class_name %>`
|
3
3
|
class <%= class_name %> < Valkyrie::Resource
|
4
4
|
include Valkyrie::Resource::AccessControls
|
5
|
-
attribute :id, Valkyrie::Types::ID.optional
|
6
5
|
<%- attributes.each do |att| -%>
|
7
6
|
attribute :<%= att.name %>, Valkyrie::Types::<%= (att.type == :array) ? 'Array' : 'Set' %>
|
8
7
|
<%- end -%>
|
data/lib/valkyrie/change_set.rb
CHANGED
@@ -24,12 +24,15 @@ module Valkyrie
|
|
24
24
|
property :append_id, virtual: true
|
25
25
|
|
26
26
|
# Set ID of record this one should be appended to.
|
27
|
+
# We use append_id to add a member/child onto an existing list of members.
|
27
28
|
# @param append_id [Valkyrie::ID]
|
28
29
|
def append_id=(append_id)
|
29
30
|
super(Valkyrie::ID.new(append_id))
|
30
31
|
end
|
31
32
|
|
32
33
|
# Returns whether or not a given field has multiple values.
|
34
|
+
# Multiple values are useful for fields like creator, author, title, etc.
|
35
|
+
# where there may be more than one value for a field that is stored and returned in the UI
|
33
36
|
# @param field_name [Symbol]
|
34
37
|
# @return [Boolean]
|
35
38
|
def multiple?(field_name)
|
@@ -37,6 +40,7 @@ module Valkyrie
|
|
37
40
|
end
|
38
41
|
|
39
42
|
# Returns whether or not a given field is required.
|
43
|
+
# Useful for distinguishing required fields in a form and for validation
|
40
44
|
# @param field_name [Symbol]
|
41
45
|
# @return [Boolean]
|
42
46
|
def required?(field_name)
|
data/lib/valkyrie/id.rb
CHANGED
@@ -1,6 +1,8 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
module Valkyrie
|
3
3
|
# A simple ID class to keep IDs distinguished from strings
|
4
|
+
# In order for an object to be queryable via joins, it needs
|
5
|
+
# to be added as a reference via a Valkyrie::ID rather than just a string ID.
|
4
6
|
class ID
|
5
7
|
attr_reader :id
|
6
8
|
delegate :empty?, to: :id
|
@@ -19,8 +21,11 @@ module Valkyrie
|
|
19
21
|
end
|
20
22
|
alias == eql?
|
21
23
|
|
24
|
+
# @deprecated Please use {.uri_for} instead
|
22
25
|
def to_uri
|
23
26
|
return RDF::Literal.new(id.to_s, datatype: RDF::URI("http://example.com/valkyrie_id")) if id.to_s.include?("://")
|
27
|
+
warn "[DEPRECATION] `to_uri` is deprecated and will be removed in the next major release. " \
|
28
|
+
"Called from #{Gem.location_of_caller.join(':')}"
|
24
29
|
::RDF::URI(ActiveFedora::Base.id_to_uri(id))
|
25
30
|
end
|
26
31
|
|
@@ -8,6 +8,7 @@ module Valkyrie
|
|
8
8
|
self.adapters = {}
|
9
9
|
class << self
|
10
10
|
# Register an adapter by a short name.
|
11
|
+
# Registering an adapter by a short name makes the adapter easier to find and reference.
|
11
12
|
# @param adapter [#persister,#query_service] Adapter to register.
|
12
13
|
# @param short_name [Symbol] Name to register it under.
|
13
14
|
def register(adapter, short_name)
|
@@ -19,7 +19,15 @@ module Valkyrie::Persistence
|
|
19
19
|
|
20
20
|
# (see Valkyrie::Persistence::Memory::Persister#save)
|
21
21
|
def save(resource:)
|
22
|
-
|
22
|
+
# Assume the first persister is the canonical data store; that's the optlock we want
|
23
|
+
first, *rest = *persisters
|
24
|
+
cached_resource = first.save(resource: resource)
|
25
|
+
# Don't pass opt lock tokens to other persisters
|
26
|
+
internal_resource = cached_resource.dup
|
27
|
+
internal_resource.send("#{Valkyrie::Persistence::Attributes::OPTIMISTIC_LOCK}=", []) if internal_resource.optimistic_locking_enabled?
|
28
|
+
rest.inject(internal_resource) { |m, persister| persister.save(resource: m) }
|
29
|
+
# return the one with the desired opt lock token
|
30
|
+
cached_resource
|
23
31
|
end
|
24
32
|
|
25
33
|
# (see Valkyrie::Persistence::Memory::Persister#save_all)
|
@@ -27,6 +35,9 @@ module Valkyrie::Persistence
|
|
27
35
|
resources.map do |resource|
|
28
36
|
save(resource: resource)
|
29
37
|
end
|
38
|
+
rescue Valkyrie::Persistence::StaleObjectError
|
39
|
+
# clear out any IDs returned to reduce potential confusion
|
40
|
+
raise Valkyrie::Persistence::StaleObjectError
|
30
41
|
end
|
31
42
|
|
32
43
|
# (see Valkyrie::Persistence::Memory::Persister#delete)
|
@@ -18,7 +18,7 @@ module Valkyrie::Persistence::Fedora
|
|
18
18
|
end
|
19
19
|
|
20
20
|
# Returns the next proxy or a tail sentinel.
|
21
|
-
# @return [
|
21
|
+
# @return [RDF::URI]
|
22
22
|
def next
|
23
23
|
@next ||=
|
24
24
|
if next_uri
|
@@ -31,13 +31,13 @@ module Valkyrie::Persistence::Fedora
|
|
31
31
|
end
|
32
32
|
|
33
33
|
# Returns the previous proxy or a head sentinel.
|
34
|
-
# @return [
|
34
|
+
# @return [RDF::URI]
|
35
35
|
def prev
|
36
36
|
@prev ||= node_cache.fetch(prev_uri) if prev_uri
|
37
37
|
end
|
38
38
|
|
39
39
|
# Graph representation of node.
|
40
|
-
# @return [
|
40
|
+
# @return [Valkyrie::Persistence::Fedora::ListNode::Resource]
|
41
41
|
def to_graph
|
42
42
|
return RDF::Graph.new if target_id.blank?
|
43
43
|
g = Resource.new(rdf_subject)
|
@@ -24,6 +24,10 @@ module Valkyrie::Persistence::Fedora
|
|
24
24
|
Valkyrie::Persistence::Fedora::Persister.new(adapter: self)
|
25
25
|
end
|
26
26
|
|
27
|
+
def id
|
28
|
+
@id ||= Valkyrie::ID.new(Digest::MD5.hexdigest(connection_prefix))
|
29
|
+
end
|
30
|
+
|
27
31
|
def resource_factory
|
28
32
|
Valkyrie::Persistence::Fedora::Persister::ResourceFactory.new(adapter: self)
|
29
33
|
end
|
@@ -63,6 +63,11 @@ module Valkyrie::Persistence::Fedora
|
|
63
63
|
uri_for(:valkyrie_time)
|
64
64
|
end
|
65
65
|
|
66
|
+
# @return [RDF::URI]
|
67
|
+
def self.optimistic_lock_token
|
68
|
+
uri_for(:optimistic_lock_token)
|
69
|
+
end
|
70
|
+
|
66
71
|
# Cast the property to a URI in the namespace
|
67
72
|
# @param property [Symbol]
|
68
73
|
# @return [RDF::URI]
|
@@ -22,6 +22,8 @@ module Valkyrie::Persistence::Fedora
|
|
22
22
|
graph_resource
|
23
23
|
end
|
24
24
|
|
25
|
+
# Filter resource properties to remove properties that should not be persisted to Fedora.
|
26
|
+
# * new_record is a virtual property for marking unsaved objects
|
25
27
|
def properties
|
26
28
|
resource_attributes.keys - [:new_record]
|
27
29
|
end
|
@@ -11,7 +11,7 @@ module Valkyrie::Persistence::Fedora
|
|
11
11
|
end
|
12
12
|
|
13
13
|
def convert
|
14
|
-
Valkyrie::Types::Anything[attributes]
|
14
|
+
populate_native_lock(Valkyrie::Types::Anything[attributes])
|
15
15
|
end
|
16
16
|
|
17
17
|
def attributes
|
@@ -20,6 +20,17 @@ module Valkyrie::Persistence::Fedora
|
|
20
20
|
.merge(id: id, new_record: false)
|
21
21
|
end
|
22
22
|
|
23
|
+
# Get Fedora's lastModified value from the LDP response
|
24
|
+
def populate_native_lock(resource)
|
25
|
+
return resource unless resource.respond_to?(Valkyrie::Persistence::Attributes::OPTIMISTIC_LOCK)
|
26
|
+
lastmod = object.response_graph.first_object([nil, RDF::URI("http://fedora.info/definitions/v4/repository#lastModified"), nil])
|
27
|
+
return resource unless lastmod
|
28
|
+
|
29
|
+
token = Valkyrie::Persistence::OptimisticLockToken.new(adapter_id: "native-#{adapter.id}", token: DateTime.parse(lastmod.to_s).httpdate)
|
30
|
+
resource.send(Valkyrie::Persistence::Attributes::OPTIMISTIC_LOCK) << token
|
31
|
+
resource
|
32
|
+
end
|
33
|
+
|
23
34
|
def id
|
24
35
|
id_property.present? ? Valkyrie::ID.new(id_property) : adapter.uri_to_id(object.subject_uri)
|
25
36
|
end
|
@@ -237,6 +248,18 @@ module Valkyrie::Persistence::Fedora
|
|
237
248
|
end
|
238
249
|
end
|
239
250
|
|
251
|
+
class ValkyrieOptimisticLockToken < ::Valkyrie::ValueMapper
|
252
|
+
FedoraValue.register(self)
|
253
|
+
def self.handles?(value)
|
254
|
+
value.statement.object.is_a?(RDF::Literal) && value.statement.object.datatype == PermissiveSchema.optimistic_lock_token
|
255
|
+
end
|
256
|
+
|
257
|
+
def result
|
258
|
+
value.statement.object = Valkyrie::Persistence::OptimisticLockToken.new(adapter_id: value.adapter.id, token: value.statement.object.to_s)
|
259
|
+
calling_mapper.for(Property.new(statement: value.statement, scope: value.scope, adapter: value.adapter)).result
|
260
|
+
end
|
261
|
+
end
|
262
|
+
|
240
263
|
class InternalURI < ::Valkyrie::ValueMapper
|
241
264
|
FedoraValue.register(self)
|
242
265
|
def self.handles?(value)
|
@@ -295,10 +318,17 @@ module Valkyrie::Persistence::Fedora
|
|
295
318
|
@property = property
|
296
319
|
end
|
297
320
|
|
321
|
+
# Apply as a single value by default, if there are multiple then
|
322
|
+
# create an array. Done to support single values - if the resource is
|
323
|
+
# a Set or Array then it'll cast the single value back to an array
|
324
|
+
# appropriately.
|
298
325
|
def apply_to(hsh)
|
299
326
|
return if blacklist?(key)
|
300
|
-
hsh[key.to_sym]
|
301
|
-
|
327
|
+
hsh[key.to_sym] = if hsh.key?(key.to_sym)
|
328
|
+
Array.wrap(hsh[key.to_sym]) + cast_array(values)
|
329
|
+
else
|
330
|
+
values
|
331
|
+
end
|
302
332
|
end
|
303
333
|
|
304
334
|
def key
|
@@ -6,6 +6,8 @@ module Valkyrie::Persistence::Fedora
|
|
6
6
|
require 'valkyrie/persistence/fedora/persister/alternate_identifier'
|
7
7
|
attr_reader :adapter
|
8
8
|
delegate :connection, :base_path, :resource_factory, to: :adapter
|
9
|
+
|
10
|
+
# @note (see Valkyrie::Persistence::Memory::Persister#initialize)
|
9
11
|
def initialize(adapter:)
|
10
12
|
@adapter = adapter
|
11
13
|
end
|
@@ -13,21 +15,26 @@ module Valkyrie::Persistence::Fedora
|
|
13
15
|
# (see Valkyrie::Persistence::Memory::Persister#save)
|
14
16
|
def save(resource:)
|
15
17
|
initialize_repository
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
18
|
+
internal_resource = resource.dup
|
19
|
+
internal_resource.created_at ||= Time.current
|
20
|
+
internal_resource.updated_at ||= Time.current
|
21
|
+
validate_lock_token(internal_resource)
|
22
|
+
native_lock = native_lock_token(internal_resource)
|
23
|
+
generate_lock_token(internal_resource)
|
24
|
+
orm = resource_factory.from_resource(resource: internal_resource)
|
25
|
+
alternate_resources = find_or_create_alternate_ids(internal_resource)
|
26
|
+
|
27
|
+
if !orm.new? || internal_resource.id
|
28
|
+
cleanup_alternate_resources(internal_resource) if alternate_resources
|
29
|
+
orm.update { |req| update_request_headers(req, native_lock) }
|
25
30
|
else
|
26
31
|
orm.create
|
27
32
|
end
|
28
33
|
persisted_resource = resource_factory.to_resource(object: orm)
|
29
34
|
|
30
35
|
alternate_resources ? save_reference_to_resource(persisted_resource, alternate_resources) : persisted_resource
|
36
|
+
rescue Ldp::PreconditionFailed
|
37
|
+
raise Valkyrie::Persistence::StaleObjectError, internal_resource.id.to_s
|
31
38
|
end
|
32
39
|
|
33
40
|
# (see Valkyrie::Persistence::Memory::Persister#save_all)
|
@@ -35,6 +42,9 @@ module Valkyrie::Persistence::Fedora
|
|
35
42
|
resources.map do |resource|
|
36
43
|
save(resource: resource)
|
37
44
|
end
|
45
|
+
rescue Valkyrie::Persistence::StaleObjectError
|
46
|
+
# blank out the message / id
|
47
|
+
raise Valkyrie::Persistence::StaleObjectError
|
38
48
|
end
|
39
49
|
|
40
50
|
# (see Valkyrie::Persistence::Memory::Persister#delete)
|
@@ -70,13 +80,6 @@ module Valkyrie::Persistence::Fedora
|
|
70
80
|
|
71
81
|
private
|
72
82
|
|
73
|
-
def ensure_multiple_values!(resource)
|
74
|
-
bad_keys = resource.attributes.except(:internal_resource, :created_at, :updated_at, :new_record, :id, :references).select do |_k, v|
|
75
|
-
!v.nil? && !v.is_a?(Array)
|
76
|
-
end
|
77
|
-
raise ::Valkyrie::Persistence::UnsupportedDatatype, "#{resource}: #{bad_keys.keys} have non-array values, which can not be persisted by Valkyrie. Cast to arrays." unless bad_keys.keys.empty?
|
78
|
-
end
|
79
|
-
|
80
83
|
def find_or_create_alternate_ids(resource)
|
81
84
|
return nil unless resource.try(:alternate_ids)
|
82
85
|
|
@@ -107,5 +110,43 @@ module Valkyrie::Persistence::Fedora
|
|
107
110
|
|
108
111
|
resource
|
109
112
|
end
|
113
|
+
|
114
|
+
# @note Fedora's last modified response is not granular enough to produce an effective lock token
|
115
|
+
# therefore, we use the same implementation as the memory adapter. This could fail to lock a
|
116
|
+
# resource if Fedora updated this resource between the time it was saved and Valkyrie created
|
117
|
+
# the token.
|
118
|
+
def generate_lock_token(resource)
|
119
|
+
return unless resource.optimistic_locking_enabled?
|
120
|
+
token = Valkyrie::Persistence::OptimisticLockToken.new(adapter_id: adapter.id, token: Time.now.to_r)
|
121
|
+
resource.send("#{Valkyrie::Persistence::Attributes::OPTIMISTIC_LOCK}=", token)
|
122
|
+
end
|
123
|
+
|
124
|
+
def validate_lock_token(resource)
|
125
|
+
return unless resource.optimistic_locking_enabled?
|
126
|
+
return if resource.id.blank?
|
127
|
+
|
128
|
+
current_lock_token = resource[Valkyrie::Persistence::Attributes::OPTIMISTIC_LOCK].find { |lock_token| lock_token.adapter_id == adapter.id }
|
129
|
+
return if current_lock_token.blank?
|
130
|
+
|
131
|
+
retrieved_lock_tokens = adapter.query_service.find_by(id: resource.id)[Valkyrie::Persistence::Attributes::OPTIMISTIC_LOCK]
|
132
|
+
retrieved_lock_token = retrieved_lock_tokens.find { |lock_token| lock_token.adapter_id == adapter.id }
|
133
|
+
return if retrieved_lock_token.blank?
|
134
|
+
|
135
|
+
raise Valkyrie::Persistence::StaleObjectError, resource.id.to_s unless current_lock_token == retrieved_lock_token
|
136
|
+
end
|
137
|
+
|
138
|
+
# Retrieve the lock token that holds Fedora's system-managed last-modified date
|
139
|
+
def native_lock_token(resource)
|
140
|
+
return unless resource.optimistic_locking_enabled?
|
141
|
+
resource[Valkyrie::Persistence::Attributes::OPTIMISTIC_LOCK].find { |lock_token| lock_token.adapter_id == "native-#{adapter.id}" }
|
142
|
+
end
|
143
|
+
|
144
|
+
# Set Fedora request headers:
|
145
|
+
# * `Prefer: handling=lenient; received="minimal"` allows us to avoid sending all server-managed triples
|
146
|
+
# * `If-Unmodified-Since` triggers Fedora's server-side optimistic locking
|
147
|
+
def update_request_headers(request, lock_token)
|
148
|
+
request.headers["Prefer"] = "handling=lenient; received=\"minimal\""
|
149
|
+
request.headers["If-Unmodified-Since"] = lock_token.token if lock_token
|
150
|
+
end
|
110
151
|
end
|
111
152
|
end
|
@@ -3,6 +3,8 @@
|
|
3
3
|
module Valkyrie::Persistence
|
4
4
|
# Implements the DataMapper Pattern to store metadata into Fedora
|
5
5
|
module Fedora
|
6
|
+
require 'active_triples'
|
7
|
+
require 'active_fedora'
|
6
8
|
require 'valkyrie/persistence/fedora/permissive_schema'
|
7
9
|
require 'valkyrie/persistence/fedora/metadata_adapter'
|
8
10
|
require 'valkyrie/persistence/fedora/persister'
|
@@ -6,8 +6,10 @@ module Valkyrie::Persistence::Memory
|
|
6
6
|
class Persister
|
7
7
|
attr_reader :adapter
|
8
8
|
delegate :cache, to: :adapter
|
9
|
+
|
9
10
|
# @param adapter [Valkyrie::Persistence::Memory::MetadataAdapter] The memory adapter which
|
10
11
|
# holds the cache for this persister.
|
12
|
+
# @note Many persister methods are part of Valkyrie's public API, but instantiation itself is not
|
11
13
|
def initialize(adapter)
|
12
14
|
@adapter = adapter
|
13
15
|
end
|
@@ -16,13 +18,18 @@ module Valkyrie::Persistence::Memory
|
|
16
18
|
# @return [Valkyrie::Resource] The resource with an `#id` value generated by the
|
17
19
|
# persistence backend.
|
18
20
|
def save(resource:)
|
19
|
-
resource
|
20
|
-
|
21
|
-
resource
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
21
|
+
raise Valkyrie::Persistence::StaleObjectError, resource.id unless valid_lock?(resource)
|
22
|
+
|
23
|
+
# duplicate the resource so we are not creating side effects on the caller's resource
|
24
|
+
internal_resource = resource.dup
|
25
|
+
|
26
|
+
internal_resource = generate_id(internal_resource) if internal_resource.id.blank?
|
27
|
+
internal_resource.created_at ||= Time.current
|
28
|
+
internal_resource.updated_at = Time.current
|
29
|
+
internal_resource.new_record = false
|
30
|
+
generate_lock_token(internal_resource)
|
31
|
+
normalize_dates!(internal_resource)
|
32
|
+
cache[internal_resource.id] = internal_resource
|
26
33
|
end
|
27
34
|
|
28
35
|
# @param resources [Array<Valkyrie::Resource>] List of resources to save.
|
@@ -32,6 +39,9 @@ module Valkyrie::Persistence::Memory
|
|
32
39
|
resources.map do |resource|
|
33
40
|
save(resource: resource)
|
34
41
|
end
|
42
|
+
rescue Valkyrie::Persistence::StaleObjectError
|
43
|
+
# Re-raising with no error message to prevent confusion
|
44
|
+
raise Valkyrie::Persistence::StaleObjectError
|
35
45
|
end
|
36
46
|
|
37
47
|
# @param resource [Valkyrie::Resource] The resource to delete from the persistence
|
@@ -51,13 +61,6 @@ module Valkyrie::Persistence::Memory
|
|
51
61
|
resource.new(id: SecureRandom.uuid)
|
52
62
|
end
|
53
63
|
|
54
|
-
def ensure_multiple_values!(resource)
|
55
|
-
bad_keys = resource.attributes.except(:internal_resource, :created_at, :updated_at, :new_record, :id).select do |_k, v|
|
56
|
-
!v.nil? && !v.is_a?(Array)
|
57
|
-
end
|
58
|
-
raise ::Valkyrie::Persistence::UnsupportedDatatype, "#{resource}: #{bad_keys.keys} have non-array values, which can not be persisted by Valkyrie. Cast to arrays." unless bad_keys.keys.empty?
|
59
|
-
end
|
60
|
-
|
61
64
|
def normalize_dates!(resource)
|
62
65
|
resource.attributes.each { |k, v| resource.send("#{k}=", normalize_date_values(v)) }
|
63
66
|
end
|
@@ -72,5 +75,25 @@ module Valkyrie::Persistence::Memory
|
|
72
75
|
return value.to_datetime.utc if value.is_a?(Time)
|
73
76
|
value
|
74
77
|
end
|
78
|
+
|
79
|
+
def generate_lock_token(resource)
|
80
|
+
return unless resource.optimistic_locking_enabled?
|
81
|
+
token = Valkyrie::Persistence::OptimisticLockToken.new(adapter_id: adapter.id, token: Time.now.to_r)
|
82
|
+
resource.send("#{Valkyrie::Persistence::Attributes::OPTIMISTIC_LOCK}=", token)
|
83
|
+
end
|
84
|
+
|
85
|
+
def valid_lock?(resource)
|
86
|
+
return true unless resource.optimistic_locking_enabled?
|
87
|
+
|
88
|
+
cached_resource = cache[resource.id]
|
89
|
+
return true if cached_resource.blank?
|
90
|
+
|
91
|
+
resource_lock_tokens = resource[Valkyrie::Persistence::Attributes::OPTIMISTIC_LOCK]
|
92
|
+
resource_value = resource_lock_tokens.find { |lock_token| lock_token.adapter_id == adapter.id }
|
93
|
+
return true if resource_value.blank?
|
94
|
+
|
95
|
+
cached_value = cached_resource[Valkyrie::Persistence::Attributes::OPTIMISTIC_LOCK].first
|
96
|
+
cached_value == resource_value
|
97
|
+
end
|
75
98
|
end
|
76
99
|
end
|