caruby-core 1.4.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (86) hide show
  1. data/History.txt +4 -0
  2. data/LEGAL +5 -0
  3. data/LICENSE +22 -0
  4. data/README.md +51 -0
  5. data/doc/website/css/site.css +1 -5
  6. data/doc/website/images/avatar.png +0 -0
  7. data/doc/website/images/favicon.ico +0 -0
  8. data/doc/website/images/logo.png +0 -0
  9. data/doc/website/index.html +82 -0
  10. data/doc/website/install.html +87 -0
  11. data/doc/website/quick_start.html +87 -0
  12. data/doc/website/tissue.html +85 -0
  13. data/doc/website/uom.html +10 -0
  14. data/lib/caruby.rb +3 -0
  15. data/lib/caruby/active_support/README.txt +2 -0
  16. data/lib/caruby/active_support/core_ext/string.rb +7 -0
  17. data/lib/caruby/active_support/core_ext/string/inflections.rb +167 -0
  18. data/lib/caruby/active_support/inflections.rb +55 -0
  19. data/lib/caruby/active_support/inflector.rb +398 -0
  20. data/lib/caruby/cli/application.rb +36 -0
  21. data/lib/caruby/cli/command.rb +169 -0
  22. data/lib/caruby/csv/csv_mapper.rb +157 -0
  23. data/lib/caruby/csv/csvio.rb +185 -0
  24. data/lib/caruby/database.rb +252 -0
  25. data/lib/caruby/database/fetched_matcher.rb +66 -0
  26. data/lib/caruby/database/persistable.rb +432 -0
  27. data/lib/caruby/database/persistence_service.rb +162 -0
  28. data/lib/caruby/database/reader.rb +599 -0
  29. data/lib/caruby/database/saved_merger.rb +131 -0
  30. data/lib/caruby/database/search_template_builder.rb +59 -0
  31. data/lib/caruby/database/sql_executor.rb +75 -0
  32. data/lib/caruby/database/store_template_builder.rb +200 -0
  33. data/lib/caruby/database/writer.rb +469 -0
  34. data/lib/caruby/domain/annotatable.rb +25 -0
  35. data/lib/caruby/domain/annotation.rb +23 -0
  36. data/lib/caruby/domain/attribute_metadata.rb +447 -0
  37. data/lib/caruby/domain/java_attribute_metadata.rb +160 -0
  38. data/lib/caruby/domain/merge.rb +91 -0
  39. data/lib/caruby/domain/properties.rb +95 -0
  40. data/lib/caruby/domain/reference_visitor.rb +289 -0
  41. data/lib/caruby/domain/resource_attributes.rb +528 -0
  42. data/lib/caruby/domain/resource_dependency.rb +205 -0
  43. data/lib/caruby/domain/resource_introspection.rb +159 -0
  44. data/lib/caruby/domain/resource_metadata.rb +117 -0
  45. data/lib/caruby/domain/resource_module.rb +285 -0
  46. data/lib/caruby/domain/uniquify.rb +38 -0
  47. data/lib/caruby/import/annotatable_class.rb +28 -0
  48. data/lib/caruby/import/annotation_class.rb +27 -0
  49. data/lib/caruby/import/annotation_module.rb +67 -0
  50. data/lib/caruby/import/java.rb +338 -0
  51. data/lib/caruby/migration/migratable.rb +167 -0
  52. data/lib/caruby/migration/migrator.rb +533 -0
  53. data/lib/caruby/migration/resource.rb +8 -0
  54. data/lib/caruby/migration/resource_module.rb +11 -0
  55. data/lib/caruby/migration/uniquify.rb +20 -0
  56. data/lib/caruby/resource.rb +969 -0
  57. data/lib/caruby/util/attribute_path.rb +46 -0
  58. data/lib/caruby/util/cache.rb +53 -0
  59. data/lib/caruby/util/class.rb +99 -0
  60. data/lib/caruby/util/collection.rb +1053 -0
  61. data/lib/caruby/util/controlled_value.rb +35 -0
  62. data/lib/caruby/util/coordinate.rb +75 -0
  63. data/lib/caruby/util/domain_extent.rb +49 -0
  64. data/lib/caruby/util/file_separator.rb +65 -0
  65. data/lib/caruby/util/inflector.rb +20 -0
  66. data/lib/caruby/util/log.rb +95 -0
  67. data/lib/caruby/util/math.rb +12 -0
  68. data/lib/caruby/util/merge.rb +59 -0
  69. data/lib/caruby/util/module.rb +34 -0
  70. data/lib/caruby/util/options.rb +92 -0
  71. data/lib/caruby/util/partial_order.rb +36 -0
  72. data/lib/caruby/util/person.rb +119 -0
  73. data/lib/caruby/util/pretty_print.rb +184 -0
  74. data/lib/caruby/util/properties.rb +112 -0
  75. data/lib/caruby/util/stopwatch.rb +66 -0
  76. data/lib/caruby/util/topological_sync_enumerator.rb +53 -0
  77. data/lib/caruby/util/transitive_closure.rb +45 -0
  78. data/lib/caruby/util/tree.rb +48 -0
  79. data/lib/caruby/util/trie.rb +37 -0
  80. data/lib/caruby/util/uniquifier.rb +30 -0
  81. data/lib/caruby/util/validation.rb +48 -0
  82. data/lib/caruby/util/version.rb +56 -0
  83. data/lib/caruby/util/visitor.rb +351 -0
  84. data/lib/caruby/util/weak_hash.rb +36 -0
  85. data/lib/caruby/version.rb +3 -0
  86. 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