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.
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