caruby-core 1.4.1
Sign up to get free protection for your applications and to get access to all the features.
- data/History.txt +4 -0
- data/LEGAL +5 -0
- data/LICENSE +22 -0
- data/README.md +51 -0
- data/doc/website/css/site.css +1 -5
- data/doc/website/images/avatar.png +0 -0
- data/doc/website/images/favicon.ico +0 -0
- data/doc/website/images/logo.png +0 -0
- data/doc/website/index.html +82 -0
- data/doc/website/install.html +87 -0
- data/doc/website/quick_start.html +87 -0
- data/doc/website/tissue.html +85 -0
- data/doc/website/uom.html +10 -0
- data/lib/caruby.rb +3 -0
- data/lib/caruby/active_support/README.txt +2 -0
- data/lib/caruby/active_support/core_ext/string.rb +7 -0
- data/lib/caruby/active_support/core_ext/string/inflections.rb +167 -0
- data/lib/caruby/active_support/inflections.rb +55 -0
- data/lib/caruby/active_support/inflector.rb +398 -0
- data/lib/caruby/cli/application.rb +36 -0
- data/lib/caruby/cli/command.rb +169 -0
- data/lib/caruby/csv/csv_mapper.rb +157 -0
- data/lib/caruby/csv/csvio.rb +185 -0
- data/lib/caruby/database.rb +252 -0
- data/lib/caruby/database/fetched_matcher.rb +66 -0
- data/lib/caruby/database/persistable.rb +432 -0
- data/lib/caruby/database/persistence_service.rb +162 -0
- data/lib/caruby/database/reader.rb +599 -0
- data/lib/caruby/database/saved_merger.rb +131 -0
- data/lib/caruby/database/search_template_builder.rb +59 -0
- data/lib/caruby/database/sql_executor.rb +75 -0
- data/lib/caruby/database/store_template_builder.rb +200 -0
- data/lib/caruby/database/writer.rb +469 -0
- data/lib/caruby/domain/annotatable.rb +25 -0
- data/lib/caruby/domain/annotation.rb +23 -0
- data/lib/caruby/domain/attribute_metadata.rb +447 -0
- data/lib/caruby/domain/java_attribute_metadata.rb +160 -0
- data/lib/caruby/domain/merge.rb +91 -0
- data/lib/caruby/domain/properties.rb +95 -0
- data/lib/caruby/domain/reference_visitor.rb +289 -0
- data/lib/caruby/domain/resource_attributes.rb +528 -0
- data/lib/caruby/domain/resource_dependency.rb +205 -0
- data/lib/caruby/domain/resource_introspection.rb +159 -0
- data/lib/caruby/domain/resource_metadata.rb +117 -0
- data/lib/caruby/domain/resource_module.rb +285 -0
- data/lib/caruby/domain/uniquify.rb +38 -0
- data/lib/caruby/import/annotatable_class.rb +28 -0
- data/lib/caruby/import/annotation_class.rb +27 -0
- data/lib/caruby/import/annotation_module.rb +67 -0
- data/lib/caruby/import/java.rb +338 -0
- data/lib/caruby/migration/migratable.rb +167 -0
- data/lib/caruby/migration/migrator.rb +533 -0
- data/lib/caruby/migration/resource.rb +8 -0
- data/lib/caruby/migration/resource_module.rb +11 -0
- data/lib/caruby/migration/uniquify.rb +20 -0
- data/lib/caruby/resource.rb +969 -0
- data/lib/caruby/util/attribute_path.rb +46 -0
- data/lib/caruby/util/cache.rb +53 -0
- data/lib/caruby/util/class.rb +99 -0
- data/lib/caruby/util/collection.rb +1053 -0
- data/lib/caruby/util/controlled_value.rb +35 -0
- data/lib/caruby/util/coordinate.rb +75 -0
- data/lib/caruby/util/domain_extent.rb +49 -0
- data/lib/caruby/util/file_separator.rb +65 -0
- data/lib/caruby/util/inflector.rb +20 -0
- data/lib/caruby/util/log.rb +95 -0
- data/lib/caruby/util/math.rb +12 -0
- data/lib/caruby/util/merge.rb +59 -0
- data/lib/caruby/util/module.rb +34 -0
- data/lib/caruby/util/options.rb +92 -0
- data/lib/caruby/util/partial_order.rb +36 -0
- data/lib/caruby/util/person.rb +119 -0
- data/lib/caruby/util/pretty_print.rb +184 -0
- data/lib/caruby/util/properties.rb +112 -0
- data/lib/caruby/util/stopwatch.rb +66 -0
- data/lib/caruby/util/topological_sync_enumerator.rb +53 -0
- data/lib/caruby/util/transitive_closure.rb +45 -0
- data/lib/caruby/util/tree.rb +48 -0
- data/lib/caruby/util/trie.rb +37 -0
- data/lib/caruby/util/uniquifier.rb +30 -0
- data/lib/caruby/util/validation.rb +48 -0
- data/lib/caruby/util/version.rb +56 -0
- data/lib/caruby/util/visitor.rb +351 -0
- data/lib/caruby/util/weak_hash.rb +36 -0
- data/lib/caruby/version.rb +3 -0
- metadata +186 -0
@@ -0,0 +1,469 @@
|
|
1
|
+
require 'caruby/util/collection'
|
2
|
+
require 'caruby/util/pretty_print'
|
3
|
+
require 'caruby/domain/reference_visitor'
|
4
|
+
require 'caruby/database/saved_merger'
|
5
|
+
require 'caruby/database/store_template_builder'
|
6
|
+
|
7
|
+
module CaRuby
|
8
|
+
class Database
|
9
|
+
# Database store operation mixin.
|
10
|
+
module Writer
|
11
|
+
# Adds store capability to this Database.
|
12
|
+
def initialize
|
13
|
+
super
|
14
|
+
@cr_tmpl_bldr = StoreTemplateBuilder.new(self) { |ref| ref.class.creatable_domain_attributes }
|
15
|
+
@upd_tmpl_bldr = StoreTemplateBuilder.new(self) { |ref| updatable_domain_attributes(ref) }
|
16
|
+
@svd_mrgr = SavedMerger.new(self)
|
17
|
+
end
|
18
|
+
|
19
|
+
# Creates the specified domain object obj and returns obj. The pre-condition for this method is as
|
20
|
+
# follows:
|
21
|
+
# * obj is a well-formed domain object with the necessary required attributes as determined by the
|
22
|
+
# {Resource#validate} method
|
23
|
+
# * obj does not have a database identifier attribute value
|
24
|
+
# * obj does not yet exist in the database
|
25
|
+
# The post-condition is that obj is created and assigned a database identifier.
|
26
|
+
#
|
27
|
+
# An object referenced by obj is created or updated if and only if the change operation on the referenced
|
28
|
+
# object is necesssary to create obj. This behavior differs from the standard +caCORE+ behavior in that
|
29
|
+
# the client does not need to embed implicit prescriptive application rules in the sequence of database
|
30
|
+
# operation calls. The goal is to reflect the caRuby client declarative intent:
|
31
|
+
# * caRuby will do whatever is necessary to reflect the object state to the database.
|
32
|
+
# * No other changes will be made to the database.
|
33
|
+
#
|
34
|
+
# By definition, a dependent object is not created directly. If obj is dependent and references its owner,
|
35
|
+
# then this method delegates to the owner by calling {#store} on the owner. The owner store operation will
|
36
|
+
# create or update the owner using {#store} and create the dependent obj in the process.
|
37
|
+
#
|
38
|
+
# _Note_: the dependent identifier is not set if the owner dependent attribute is a collection and
|
39
|
+
# the dependent class does not have secondary key attributes. In that case, there is no reliable +caCORE+
|
40
|
+
# query mechanism to match the obj dependent to a created or fetched dependent. The owner dependent
|
41
|
+
# collection content is replaced by new dependent objects fetched from the database, e.g. given a
|
42
|
+
# +dependent+ in the +owner+ +dependents+ collection, then:
|
43
|
+
# dependents_count = owner.dependents.size
|
44
|
+
# owner.dependents.include?(dependent) #=> true
|
45
|
+
# database.create(dependent).identifier #=> nil
|
46
|
+
# owner.dependents.include?(dependent) #=> false
|
47
|
+
# owner.dependents.size == dependents_count #=> true
|
48
|
+
#
|
49
|
+
# If obj is not dependent, then the create strategy is as follows:
|
50
|
+
# * add default attribute values using {Resource#add_defaults}
|
51
|
+
# * validate obj using {Resource#validate}
|
52
|
+
# * ensure that all saved independent reference attribute values exist in the database, creating
|
53
|
+
# them if necessary
|
54
|
+
# * ensure that all dependent reference attribute values can be created according to this set of rules
|
55
|
+
# * make a template for obj and its references that will result in saving obj saved attributes
|
56
|
+
# to the database
|
57
|
+
# * submit the template to the application service creation method.
|
58
|
+
# * copy the new database identifier to each created object, i.e. the transitive closure of obj and
|
59
|
+
# its dependents
|
60
|
+
#
|
61
|
+
# @param [Resource] the domain object to create
|
62
|
+
# @return [Resource] obj
|
63
|
+
# @raise [DatabaseError] if the database operation fails
|
64
|
+
def create(obj)
|
65
|
+
# guard against recursive call back into the same operation
|
66
|
+
# the only allowed recursive call is a dependent create which first creates the owner
|
67
|
+
if recursive_save?(obj, :create) then
|
68
|
+
raise DatabaseError.new("Create #{obj.qp} recursively called in context #{print_operations}")
|
69
|
+
elsif obj.identifier then
|
70
|
+
raise DatabaseError.new("Create unsuccessful since #{obj.qp} already has identifier #{obj.identifier}")
|
71
|
+
end
|
72
|
+
# create the object
|
73
|
+
perform(:create, obj) { create_object(obj) }
|
74
|
+
end
|
75
|
+
|
76
|
+
# Updates the specified domain object obj. The pre-condition for this method is that obj exists in the
|
77
|
+
# database and has a database identifier attribute value. The post-condition is that the database is
|
78
|
+
# changed to reflect the obj state. Each dependent object referenced by obj is also created or updated.
|
79
|
+
# No other object referenced by obj is changed.
|
80
|
+
#
|
81
|
+
# The update strategy is the same as the {#create} strategy with the following exceptions:
|
82
|
+
# * validate that obj has a database identifier
|
83
|
+
# * the template is submitted to the application service update method
|
84
|
+
#
|
85
|
+
# Raises DatabaseError if the database operation fails.
|
86
|
+
def update(obj)
|
87
|
+
# guard against a recursive call back into the same operation.
|
88
|
+
if recursive_save?(obj, :update) then
|
89
|
+
raise DatabaseError.new("Update #{obj.qp} recursively called in context #{print_operations}")
|
90
|
+
end
|
91
|
+
# update the object
|
92
|
+
perform(:update, obj) { update_object(obj) }
|
93
|
+
end
|
94
|
+
|
95
|
+
# Updates the specified domain object if it exists, otherwise creates a new domain object.
|
96
|
+
#
|
97
|
+
# The database is queried based on the object attributes. If a match is found,
|
98
|
+
# then the obj database identifier attribute is set and update is called.
|
99
|
+
# If no matching database record is found, then the object is persisted using create.
|
100
|
+
#
|
101
|
+
# @see #create
|
102
|
+
# @see #update
|
103
|
+
#
|
104
|
+
#@param [Resource] obj the domain object to save
|
105
|
+
# @return [Resource] obj
|
106
|
+
# @raise [DatabaseError] if the database operation fails
|
107
|
+
def save(obj)
|
108
|
+
logger.debug { "Storing #{obj}..." }
|
109
|
+
# add defaults now, since a default key value could be used in the existence check
|
110
|
+
obj.add_defaults
|
111
|
+
# if obj exists then update it, otherwise create it
|
112
|
+
exists?(obj) ? update(obj) : create(obj)
|
113
|
+
end
|
114
|
+
|
115
|
+
alias :store :save
|
116
|
+
|
117
|
+
# Deletes the specified domain object obj.
|
118
|
+
#
|
119
|
+
# Note that some applications restrict or forbid delete operations. Check the specific application
|
120
|
+
# documentation to determine whether deletion is supported.
|
121
|
+
#
|
122
|
+
#@param [Resource] obj the domain object to delete
|
123
|
+
# @raise [DatabaseError] if the database operation fails
|
124
|
+
def delete(obj)
|
125
|
+
perform(:delete, obj) { delete_object(obj) }
|
126
|
+
end
|
127
|
+
|
128
|
+
# Creates the domain object obj, if necessary.
|
129
|
+
#
|
130
|
+
# Raises ArgumentError if obj is nil or empty.
|
131
|
+
# Raises DatabaseError if obj could not be created.
|
132
|
+
# The return value is undefined.
|
133
|
+
def ensure_exists(obj)
|
134
|
+
raise ArgumentError.new("Database ensure_exists is missing a domain object argument.") if obj.nil_or_empty?
|
135
|
+
obj.enumerate { |ref| find(ref, :create) unless ref.identifier }
|
136
|
+
end
|
137
|
+
|
138
|
+
# Returns whether there is already the given obj operation in progress that is not in the scope of
|
139
|
+
# an operation performed on a dependent obj owner, i.e. a second obj save operation of the same type
|
140
|
+
# is only allowed if the obj operation was delegated to an owner save which in turn saves the dependent
|
141
|
+
# obj.
|
142
|
+
#
|
143
|
+
# @param [Resource] obj the domain object to save
|
144
|
+
# @param [Symbol] operation the +:create+ or +:update+ save operation
|
145
|
+
# @return [Boolean] whether the save operation is redundant
|
146
|
+
def recursive_save?(obj, operation)
|
147
|
+
@operations.detect { |op| op.type == operation and op.subject == obj } and
|
148
|
+
@operations.last.subject != obj.owner
|
149
|
+
end
|
150
|
+
|
151
|
+
private
|
152
|
+
|
153
|
+
# Creates obj as follows:
|
154
|
+
# * if obj has an uncreated owner, then store the owner, which in turn will create a physical dependent
|
155
|
+
# * otherwise, create a storable template. The template is a copy of obj containing a recursive copy
|
156
|
+
# of each saved obj reference and resolved independent references
|
157
|
+
# * submit the template to the create application service
|
158
|
+
# * update the obj dependency transitive closure content from the create result
|
159
|
+
# * add a lazy-loader to obj for unfetched domain references
|
160
|
+
#
|
161
|
+
# @param (see #create)
|
162
|
+
# @return [Resource] obj
|
163
|
+
def create_object(obj)
|
164
|
+
# add obj to the transients set
|
165
|
+
@transients << obj
|
166
|
+
begin
|
167
|
+
# A dependent of an uncreated owner can be created by creating the owner.
|
168
|
+
# Otherwise, create obj from a template.
|
169
|
+
create_as_dependent(obj) or
|
170
|
+
create_from_template(obj) or
|
171
|
+
raise DatabaseError.new("#{obj.class.qp} is not creatable in context #{print_operations}")
|
172
|
+
ensure
|
173
|
+
# since obj now has an id, removed from transients set
|
174
|
+
@transients.delete(obj)
|
175
|
+
end
|
176
|
+
# return the created object
|
177
|
+
obj
|
178
|
+
end
|
179
|
+
|
180
|
+
# Attempts to create the domain object dep as a dependent by storing its owner.
|
181
|
+
# Returns dep if dep is dependent and could be created, nil otherwise.
|
182
|
+
#
|
183
|
+
# A physical dependent is created by its parent.
|
184
|
+
# A logical dependent is created by its parent unless the parent already exists.
|
185
|
+
# If the logical parent exists, then dep must be created.
|
186
|
+
#
|
187
|
+
#@param [Resource] dep the dependent domain object to create
|
188
|
+
# @return [Resource] dep
|
189
|
+
def create_as_dependent(dep)
|
190
|
+
# bail if not dependent or owner is not set
|
191
|
+
owner = dep.owner || return
|
192
|
+
unless owner.identifier then
|
193
|
+
logger.debug { "Adding #{owner.qp} dependent #{dep.qp} defaults..." }
|
194
|
+
dep.add_defaults
|
195
|
+
logger.debug { "Ensuring that dependent #{dep.qp} owner #{owner.qp} exists..." }
|
196
|
+
ensure_exists(owner)
|
197
|
+
end
|
198
|
+
|
199
|
+
# If the dependent was created as a side-effect of creating the owner, then we are done.
|
200
|
+
if dep.identifier then
|
201
|
+
logger.debug { "Created dependent #{dep.qp} by saving owner #{owner.qp}." }
|
202
|
+
return dep
|
203
|
+
end
|
204
|
+
|
205
|
+
# If there is a saver proxy, then use the proxy.
|
206
|
+
if dep.class.method_defined?(:saver_proxy) then
|
207
|
+
save_with_proxy(dep)
|
208
|
+
# remove obj from transients to clear previous fetch, if any
|
209
|
+
@transients.delete(dep)
|
210
|
+
logger.debug { "Fetching #{dep.qp} to reflect the proxy save..." }
|
211
|
+
find(dep)
|
212
|
+
end
|
213
|
+
end
|
214
|
+
|
215
|
+
# Saves the given domain object using a proxy.
|
216
|
+
#
|
217
|
+
# @param [Resource] obj the proxied domain object
|
218
|
+
# @return [Resource] obj
|
219
|
+
# @raise [DatabaseError] if obj does not have a proxy
|
220
|
+
def save_with_proxy(obj)
|
221
|
+
proxy = obj.saver_proxy
|
222
|
+
if proxy.nil? then raise DatabaseError.new("#{obj.class.qp} does not have a proxy") end
|
223
|
+
logger.debug { "Saving #{obj.qp} by creating the proxy #{proxy}..." }
|
224
|
+
create(proxy)
|
225
|
+
logger.debug { "Created the #{obj.qp} proxy #{proxy}." }
|
226
|
+
logger.debug { "Udating the #{obj.qp} snapshot to reflect the proxy save..." }
|
227
|
+
obj.take_snapshot
|
228
|
+
obj
|
229
|
+
end
|
230
|
+
|
231
|
+
# Creates obj by submitting a template to the persistence service. Ensures that the domain
|
232
|
+
# objects referenced by the created obj exist and are correctly stored.
|
233
|
+
#
|
234
|
+
# caCORE alert - submitting the object directly for create runs into various caTissue bizlogic
|
235
|
+
# traps, e.g. Participant CPR is not cascaded but Participant bizlogic checks that each CPR
|
236
|
+
# referenced by Participant is ready to be created. It is treacherous to make assumptions
|
237
|
+
# about what caTissue bizlogic will or will not check. Therefore, the safer strategy is to
|
238
|
+
# build a template for submission that includes only the object cascaded and direct
|
239
|
+
# non-cascaded independent references. The independent references are created if necessary.
|
240
|
+
# The template thus includes only as much content as can safely pass through the caTissue
|
241
|
+
# bizlogic minefield.
|
242
|
+
#
|
243
|
+
# caCORE alert - caCORE create does not update the submitted object to reflect the created
|
244
|
+
# content. The create result is a separate object, which in turn does not always reflect
|
245
|
+
# the created content, e.g. caTissue ignores auto-generated attributes such as Container
|
246
|
+
# name. Work-around is to merge the create result into the object being created, being
|
247
|
+
# careful to merge only the fetched content in order to avoid the dreaded out-of-session
|
248
|
+
# error message. The post-create cascaded dependent hierarchy is traversed to capture
|
249
|
+
# the created state for each created object.
|
250
|
+
#
|
251
|
+
# The ignored content is handled separately by fetching the ignored content from
|
252
|
+
# the database, comparing it to the desired content as reflected in the submitted
|
253
|
+
# create argument object, and submitting a post-create caCORE update as necessary to
|
254
|
+
# force caCORE to reflect the desired content. This is complicated by the various
|
255
|
+
# auto-generation schemes, e.g. in caTissue, that require a careful fetch, match and
|
256
|
+
# merge logic to make sense of how what was actually created corresponds to the desired
|
257
|
+
# content expressed in the create argument object graph.
|
258
|
+
#
|
259
|
+
# There are thus several objects involved in the create process:
|
260
|
+
# * the object to create
|
261
|
+
# * the template for caCORE createObject submission
|
262
|
+
# * the caCORE createObject result
|
263
|
+
# * the post-create fetched object that reflects the persistent content
|
264
|
+
# * the template for post-create caCORE updateObject submission
|
265
|
+
#
|
266
|
+
# This object menagerie is unfortunate but unavoidable if we are to navigate the treacherous
|
267
|
+
# caCORE create process and ensure that:
|
268
|
+
# 1. the database reflects the create argument.
|
269
|
+
# 2. the created object reflects the database content.
|
270
|
+
#
|
271
|
+
# @param (see #create)
|
272
|
+
# @return obj
|
273
|
+
def create_from_template(obj)
|
274
|
+
# The create template. Independent saved references are created as necessary.
|
275
|
+
tmpl = build_create_template(obj)
|
276
|
+
save_with_template(obj, tmpl) { |svc| svc.create(tmpl) }
|
277
|
+
|
278
|
+
# If obj is a top-level create, then ensure that remaining references exist.
|
279
|
+
if @operations.first.subject == obj then
|
280
|
+
refs = obj.suspend_lazy_loader { obj.references.reject { |ref| ref.identifier } }
|
281
|
+
logger.debug { "Ensuring that created #{obj.qp} references exist: #{refs.qp}..." } unless refs.empty?
|
282
|
+
refs.each { |ref| ensure_exists(ref) }
|
283
|
+
end
|
284
|
+
|
285
|
+
obj
|
286
|
+
end
|
287
|
+
|
288
|
+
def build_create_template(obj)
|
289
|
+
@cr_tmpl_bldr.build_template(obj)
|
290
|
+
end
|
291
|
+
#
|
292
|
+
# caCORE alert - application create logic might ignore a non-domain attribute value,
|
293
|
+
# e.g. the caTissue StorageContainer auto-generated name attribute. In other cases, the application
|
294
|
+
# always ignores a non-domain attribute value, so the object should not be saved even if it differs
|
295
|
+
# from the stored result, e.g. the caTissue CollectionProtocolRegistration unsaved
|
296
|
+
# registration_date. The work-around is to check whether the create result
|
297
|
+
# differs from the create argument for the auto-generated updatable attributes, and, if so,
|
298
|
+
# to update the saved object.
|
299
|
+
#
|
300
|
+
# This method returns whether the saved obj differs from the stored source for any
|
301
|
+
# {ResourceAttributes#autogenerated_nondomain_attributes}.
|
302
|
+
#
|
303
|
+
# @param [Resource] the created domain object
|
304
|
+
# @param [Resource] the stored database content source domain object
|
305
|
+
# @return [Boolean] whether obj differs from the source on the the non-domain attributes
|
306
|
+
def update_saved?(obj, source)
|
307
|
+
obj.class.autogenerated_nondomain_attributes.any? do |attr|
|
308
|
+
intended = obj.send(attr)
|
309
|
+
stored = source.send(attr)
|
310
|
+
if intended != stored then
|
311
|
+
logger.debug { "Saved #{obj.qp} #{attr} value #{intended} differs from result value #{stored}..." }
|
312
|
+
end
|
313
|
+
end
|
314
|
+
end
|
315
|
+
|
316
|
+
# Returns the {MetadataAttributes#updatable_domain_attributes} which are either
|
317
|
+
# {AttributeMetadata#cascade_update_to_create?} or have identifiers for all
|
318
|
+
# references in the attribute value.
|
319
|
+
#
|
320
|
+
# @param (see #update)
|
321
|
+
# @return the attributes to include in the update template
|
322
|
+
def updatable_domain_attributes(obj)
|
323
|
+
obj.class.updatable_domain_attributes.filter do |attr|
|
324
|
+
obj.class.attribute_metadata(attr).cascade_update_to_create? or
|
325
|
+
obj.send(attr).to_enum.all? { |ref| ref.identifier }
|
326
|
+
end
|
327
|
+
end
|
328
|
+
|
329
|
+
# @param (see #update)
|
330
|
+
def update_object(obj)
|
331
|
+
# database identifier is required for update
|
332
|
+
if obj.identifier.nil? then
|
333
|
+
raise DatabaseError.new("Update target is missing a database identifier: #{obj}")
|
334
|
+
end
|
335
|
+
|
336
|
+
# if this object is proxied, then delegate to the proxy
|
337
|
+
if obj.class.method_defined?(:saver_proxy) then
|
338
|
+
return save_with_proxy(obj)
|
339
|
+
end
|
340
|
+
|
341
|
+
# if a changed dependent is saved with a proxy, then update that dependent first
|
342
|
+
proxied = updatable_proxied_dependents(obj)
|
343
|
+
unless proxied.empty? then
|
344
|
+
proxied.each { |dep| update(dep) }
|
345
|
+
end
|
346
|
+
|
347
|
+
# update using a template
|
348
|
+
tmpl = build_update_template(obj)
|
349
|
+
|
350
|
+
# call the caCORE service with an obj update template
|
351
|
+
save_with_template(obj, tmpl) { |svc| svc.update(tmpl) }
|
352
|
+
# take a snapshot of the updated content
|
353
|
+
obj.take_snapshot
|
354
|
+
end
|
355
|
+
|
356
|
+
# caTissue alert - the conditions for when and how to include a proxied dependent are
|
357
|
+
# are intricate and treacherous. So far as can be determined, in the case of a
|
358
|
+
# SpecimenPosition proxied by a TransferEventParameters, the sequence is as follows:
|
359
|
+
# * If a Specimen without a previous position is updated with a position, then
|
360
|
+
# the update template should not include the target position. Subsequent to the
|
361
|
+
# Specimen update, the TransferEventParameters proxy is created.
|
362
|
+
# This creates a new position in the database as a server side-effect. caRuby
|
363
|
+
# then fetches the new position and merges it into the target position.
|
364
|
+
# * If a Specimen with a previous position is updated, then the update template
|
365
|
+
# must reflect the current datbase position state. Therefore, caRuby first
|
366
|
+
# creates the proxy to update the database state.
|
367
|
+
# * The TransferEventParameters create must reference a Specimen with the current
|
368
|
+
# database position state, not the new position state.
|
369
|
+
# * Update of a Specimen with a current database position must reference a
|
370
|
+
# position which reflects that database state. This is true even if the position
|
371
|
+
# has not changed. The position must be complete and consistent with the database
|
372
|
+
# state. E.g. omitting the position storage container is accepted by caTissue
|
373
|
+
# but corrupts the database side and has adverse delayed effects.
|
374
|
+
# * Specimen create (but not auto-generated update) cannot include a position
|
375
|
+
# (although that might have changed in the 1.1.2 release). The target position
|
376
|
+
# must be created via the proxy after the Specimen is created.
|
377
|
+
#
|
378
|
+
# @param (see #update)
|
379
|
+
# @return [<Resource>] the #{ResourceAttributes#proxied_cascaded_attributes} dependents
|
380
|
+
# which are #{Persistable#changed?}
|
381
|
+
def updatable_proxied_dependents(obj)
|
382
|
+
attrs = obj.class.proxied_cascaded_attributes
|
383
|
+
return Array::EMPTY_ARRAY if attrs.empty?
|
384
|
+
deps = []
|
385
|
+
attrs.each do |attr|
|
386
|
+
obj.send(attr).enumerate { |dep| deps << dep if dep.identifier and dep.changed? }
|
387
|
+
end
|
388
|
+
deps
|
389
|
+
end
|
390
|
+
|
391
|
+
# @param (see #update)
|
392
|
+
def build_update_template(obj)
|
393
|
+
@upd_tmpl_bldr.build_template(obj)
|
394
|
+
end
|
395
|
+
|
396
|
+
# @param (see #delete)
|
397
|
+
# @raise [DatabaseError] if obj does not have an identifier
|
398
|
+
def delete_object(obj)
|
399
|
+
# database identifier is required for delete
|
400
|
+
if obj.identifier.nil? then
|
401
|
+
raise DatabaseError.new("Delete target is missing a database identifier: #{obj}")
|
402
|
+
end
|
403
|
+
persistence_service(obj).delete_object(obj)
|
404
|
+
end
|
405
|
+
|
406
|
+
# Saves the given template built from the given domain object obj. The persistence operation
|
407
|
+
# is performed by calling the #persistence_service update or create method on the template.
|
408
|
+
# If the template has an identifier, then the service update method is called. Otherwise, the service
|
409
|
+
# create method is called on the template. Dependents are saved as well, if necessary.
|
410
|
+
#
|
411
|
+
# @param obj (see #store)
|
412
|
+
# @param [Resource] template the obj template to submit to caCORE
|
413
|
+
def save_with_template(obj, template)
|
414
|
+
logger.debug { "Saving #{obj.qp} from template:\n#{template.dump}" }
|
415
|
+
# call the application service
|
416
|
+
# dispatch to the app service
|
417
|
+
svc = persistence_service(template)
|
418
|
+
result = template.identifier ? svc.update(template) : svc.create(template)
|
419
|
+
logger.debug { "Store #{obj.qp} with template #{template.qp} produced caCORE result: #{result}." }
|
420
|
+
# sync the result
|
421
|
+
sync_saved(obj, result)
|
422
|
+
end
|
423
|
+
|
424
|
+
def sync_saved(obj, result)
|
425
|
+
# delegate to the merge visitor
|
426
|
+
src = @svd_mrgr.merge(obj, result)
|
427
|
+
# make the saved object persistent, if not already so
|
428
|
+
persistify(obj)
|
429
|
+
|
430
|
+
# If saved must be updated, then update recursively.
|
431
|
+
# Otherwise, save dependents as needed.
|
432
|
+
if update_saved?(obj, src) then
|
433
|
+
logger.debug { "Updating saved #{obj} to store unsaved attributes..." }
|
434
|
+
# call update_savedect(saved) rather than update(saved) to bypass the redundant update check
|
435
|
+
perform(:update, obj) { update_object(obj) }
|
436
|
+
else
|
437
|
+
# recursively save the dependents
|
438
|
+
save_dependents(obj)
|
439
|
+
end
|
440
|
+
end
|
441
|
+
|
442
|
+
# Saves the given domain object dependents.
|
443
|
+
#
|
444
|
+
# @param [Resource] obj the owner domain object
|
445
|
+
def save_dependents(obj)
|
446
|
+
obj.dependents.each { |dep| save_dependent(obj, dep) }
|
447
|
+
end
|
448
|
+
|
449
|
+
# Saves the given dependent domain object if necessary.
|
450
|
+
# Recursively saves the obj dependents as necessary.
|
451
|
+
#
|
452
|
+
# @param [Resource] obj the dependent domain object to save
|
453
|
+
def save_dependent(owner, dep)
|
454
|
+
if dep.identifier.nil? then
|
455
|
+
logger.debug { "Creating dependent #{dep.qp}..." }
|
456
|
+
return create(dep)
|
457
|
+
end
|
458
|
+
changes = dep.changed_attributes
|
459
|
+
logger.debug { "#{owner.qp} dependent #{dep.qp} changed for attributes #{changes.to_series}." } unless changes.empty?
|
460
|
+
if changes.any? { |attr| not dep.class.attribute_metadata(attr).dependent? } then
|
461
|
+
logger.debug { "Updating changed #{owner.qp} dependent #{dep.qp}..." }
|
462
|
+
update(dep)
|
463
|
+
else
|
464
|
+
save_dependents(dep)
|
465
|
+
end
|
466
|
+
end
|
467
|
+
end
|
468
|
+
end
|
469
|
+
end
|