caruby-core 1.4.1
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.
- 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
|