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.
Files changed (52) hide show
  1. checksums.yaml +5 -5
  2. data/.rubocop.yml +1 -1
  3. data/.rubocop_todo.yml +5 -0
  4. data/CHANGELOG.md +28 -0
  5. data/README.md +43 -7
  6. data/db/migrate/20180802220739_add_optimistic_locking_to_orm_resources.rb +6 -0
  7. data/lib/generators/valkyrie/templates/resource.rb.erb +0 -1
  8. data/lib/valkyrie/change_set.rb +4 -0
  9. data/lib/valkyrie/id.rb +5 -0
  10. data/lib/valkyrie/metadata_adapter.rb +1 -0
  11. data/lib/valkyrie/persistence/composite_persister.rb +12 -1
  12. data/lib/valkyrie/persistence/fedora/list_node.rb +3 -3
  13. data/lib/valkyrie/persistence/fedora/metadata_adapter.rb +4 -0
  14. data/lib/valkyrie/persistence/fedora/permissive_schema.rb +5 -0
  15. data/lib/valkyrie/persistence/fedora/persister/alternate_identifier.rb +0 -1
  16. data/lib/valkyrie/persistence/fedora/persister/model_converter.rb +2 -0
  17. data/lib/valkyrie/persistence/fedora/persister/orm_converter.rb +33 -3
  18. data/lib/valkyrie/persistence/fedora/persister.rb +57 -16
  19. data/lib/valkyrie/persistence/fedora/query_service.rb +2 -0
  20. data/lib/valkyrie/persistence/fedora.rb +2 -0
  21. data/lib/valkyrie/persistence/memory/metadata_adapter.rb +4 -0
  22. data/lib/valkyrie/persistence/memory/persister.rb +37 -14
  23. data/lib/valkyrie/persistence/memory/query_service.rb +2 -0
  24. data/lib/valkyrie/persistence/optimistic_lock_token.rb +34 -0
  25. data/lib/valkyrie/persistence/postgres/metadata_adapter.rb +11 -2
  26. data/lib/valkyrie/persistence/postgres/orm_converter.rb +35 -5
  27. data/lib/valkyrie/persistence/postgres/persister.rb +10 -12
  28. data/lib/valkyrie/persistence/postgres/query_service.rb +5 -4
  29. data/lib/valkyrie/persistence/postgres/resource_converter.rb +21 -1
  30. data/lib/valkyrie/persistence/postgres/resource_factory.rb +22 -18
  31. data/lib/valkyrie/persistence/solr/metadata_adapter.rb +5 -1
  32. data/lib/valkyrie/persistence/solr/model_converter.rb +32 -2
  33. data/lib/valkyrie/persistence/solr/orm_converter.rb +22 -5
  34. data/lib/valkyrie/persistence/solr/persister.rb +2 -0
  35. data/lib/valkyrie/persistence/solr/query_service.rb +2 -0
  36. data/lib/valkyrie/persistence/solr/repository.rb +21 -8
  37. data/lib/valkyrie/persistence/solr/resource_factory.rb +6 -3
  38. data/lib/valkyrie/persistence.rb +10 -3
  39. data/lib/valkyrie/resource/access_controls.rb +0 -1
  40. data/lib/valkyrie/resource.rb +31 -8
  41. data/lib/valkyrie/specs/shared_specs/change_set_persister.rb +0 -1
  42. data/lib/valkyrie/specs/shared_specs/metadata_adapter.rb +9 -0
  43. data/lib/valkyrie/specs/shared_specs/persister.rb +134 -7
  44. data/lib/valkyrie/specs/shared_specs/queries.rb +20 -2
  45. data/lib/valkyrie/specs/shared_specs/resource.rb +0 -1
  46. data/lib/valkyrie/specs/shared_specs/storage_adapter.rb +0 -1
  47. data/lib/valkyrie/storage/fedora.rb +29 -20
  48. data/lib/valkyrie/types.rb +11 -1
  49. data/lib/valkyrie/version.rb +1 -1
  50. data/lib/valkyrie.rb +0 -2
  51. metadata +7 -6
  52. data/config/fedora.yml +0 -10
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
- SHA1:
3
- metadata.gz: 2b7dcc5a9a264acb744d602fd069a7c2ff5a1c11
4
- data.tar.gz: 82fdcd0161085c15492f9fb5fdd8515ffde5d7e3
2
+ SHA256:
3
+ metadata.gz: 8a320a108f82f79f88b27eaeb267b7b134499e346dccaae4f93165ff95e47fa5
4
+ data.tar.gz: bebc13ba65b1e1e2d69fd9a9c9408f8af8ef4484984a6de568db4c90c34eb15f
5
5
  SHA512:
6
- metadata.gz: ef3b35b27a29849cb2bd00e136d20018067c6852f1f3ead55f92fd202d1030b6ecc073fed84f43b724b9ba52aba193bd28d9b5eece9144ecac1b6edbabdc200c
7
- data.tar.gz: '0824a51c53a59ec27205872aa5e29b22ad8bd711ac035bd307d17db1b5b17e1178d159d30229467f87d33b755ee4d6b979719333f287fd6ce74d979d382ed58b'
6
+ metadata.gz: 8a32748fc47406abdb640ce4191e6271c61a0813cb2774a444ed5c6a45a5a34fd8e599f37afab22ac6c76d71d25bcaee28df6801a3c57add7617b064fc5ed3fb
7
+ data.tar.gz: b353cc383371599df898f626b6e211294a34f5e351dad0fc0521a11841e79c04da3699c1e867f54e102c09379cba0366fa62e408738c3f7fa0f2637a20d59a14
data/.rubocop.yml CHANGED
@@ -25,7 +25,7 @@ RSpec/MultipleExpectations:
25
25
  Enabled: false
26
26
  Rails/TimeZone:
27
27
  Enabled: false
28
- Style/PredicateName:
28
+ Naming/PredicateName:
29
29
  Exclude:
30
30
  - "lib/valkyrie/resource.rb"
31
31
  - "lib/valkyrie/persistence/solr/queries/default_paginator.rb"
data/.rubocop_todo.yml CHANGED
@@ -1,3 +1,8 @@
1
1
  Metrics/ClassLength:
2
2
  Exclude:
3
+ - 'lib/valkyrie/persistence/fedora/persister.rb'
3
4
  - 'lib/valkyrie/persistence/postgres/query_service.rb'
5
+
6
+ Metrics/MethodLength:
7
+ Exclude:
8
+ - 'lib/valkyrie/persistence/fedora/persister.rb'
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
- ```ruby
15
- gem 'valkyrie', github: 'samvera-labs/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: ActiveFedora.fedora.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::Solr` for storing in Solr.
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).
@@ -0,0 +1,6 @@
1
+ # frozen_string_literal: true
2
+ class AddOptimisticLockingToOrmResources < ActiveRecord::Migration[5.1]
3
+ def change
4
+ add_column :orm_resources, :lock_version, :integer
5
+ end
6
+ end
@@ -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 -%>
@@ -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
- persisters.inject(resource) { |m, persister| persister.save(resource: m) }
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 [ActiveFedora::Orders::ListNode]
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 [ActiveFedora::Orders::ListNode]
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 [ActiveFedora::Orders::ListNode::Resource]
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]
@@ -4,7 +4,6 @@ require 'valkyrie/types'
4
4
 
5
5
  module Valkyrie::Persistence::Fedora
6
6
  class AlternateIdentifier < ::Valkyrie::Resource
7
- attribute :id, ::Valkyrie::Types::ID.optional
8
7
  attribute :references, ::Valkyrie::Types::ID.optional
9
8
  end
10
9
  end
@@ -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
- hsh[key.to_sym] += cast_array(values)
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
- resource.created_at ||= Time.current
17
- resource.updated_at ||= Time.current
18
- ensure_multiple_values!(resource)
19
- orm = resource_factory.from_resource(resource: resource)
20
- alternate_resources = find_or_create_alternate_ids(resource)
21
-
22
- if !orm.new? || resource.id
23
- cleanup_alternate_resources(resource) if alternate_resources
24
- orm.update { |req| req.headers["Prefer"] = "handling=lenient; received=\"minimal\"" }
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
@@ -4,6 +4,8 @@ module Valkyrie::Persistence::Fedora
4
4
  class QueryService
5
5
  attr_reader :adapter
6
6
  delegate :connection, :resource_factory, to: :adapter
7
+
8
+ # @note (see Valkyrie::Persistence::Memory::QueryService#initialize)
7
9
  def initialize(adapter:)
8
10
  @adapter = adapter
9
11
  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'
@@ -23,5 +23,9 @@ module Valkyrie::Persistence::Memory
23
23
  def cache
24
24
  @cache ||= {}
25
25
  end
26
+
27
+ def id
28
+ @id ||= Valkyrie::ID.new(Digest::MD5.hexdigest(self.class.to_s))
29
+ end
26
30
  end
27
31
  end
@@ -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 = generate_id(resource) if resource.id.blank?
20
- resource.created_at ||= Time.current
21
- resource.updated_at = Time.current
22
- resource.new_record = false
23
- normalize_dates!(resource)
24
- ensure_multiple_values!(resource)
25
- cache[resource.id] = resource
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