caruby-core 1.4.7 → 1.4.9

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 (48) hide show
  1. data/History.txt +11 -0
  2. data/README.md +1 -1
  3. data/lib/caruby/cli/command.rb +27 -3
  4. data/lib/caruby/csv/csv_mapper.rb +2 -0
  5. data/lib/caruby/csv/csvio.rb +187 -169
  6. data/lib/caruby/database.rb +33 -16
  7. data/lib/caruby/database/lazy_loader.rb +23 -23
  8. data/lib/caruby/database/persistable.rb +32 -18
  9. data/lib/caruby/database/persistence_service.rb +20 -7
  10. data/lib/caruby/database/reader.rb +22 -21
  11. data/lib/caruby/database/search_template_builder.rb +7 -9
  12. data/lib/caruby/database/sql_executor.rb +52 -27
  13. data/lib/caruby/database/store_template_builder.rb +18 -13
  14. data/lib/caruby/database/writer.rb +107 -44
  15. data/lib/caruby/domain/attribute_metadata.rb +35 -25
  16. data/lib/caruby/domain/java_attribute_metadata.rb +43 -20
  17. data/lib/caruby/domain/merge.rb +9 -5
  18. data/lib/caruby/domain/reference_visitor.rb +4 -3
  19. data/lib/caruby/domain/resource_attributes.rb +52 -12
  20. data/lib/caruby/domain/resource_dependency.rb +129 -42
  21. data/lib/caruby/domain/resource_introspection.rb +1 -1
  22. data/lib/caruby/domain/resource_inverse.rb +20 -3
  23. data/lib/caruby/domain/resource_metadata.rb +20 -4
  24. data/lib/caruby/domain/resource_module.rb +190 -124
  25. data/lib/caruby/import/java.rb +39 -19
  26. data/lib/caruby/migration/migratable.rb +31 -6
  27. data/lib/caruby/migration/migrator.rb +126 -40
  28. data/lib/caruby/migration/uniquify.rb +0 -1
  29. data/lib/caruby/resource.rb +28 -5
  30. data/lib/caruby/util/attribute_path.rb +0 -2
  31. data/lib/caruby/util/class.rb +8 -5
  32. data/lib/caruby/util/collection.rb +5 -3
  33. data/lib/caruby/util/domain_extent.rb +0 -3
  34. data/lib/caruby/util/options.rb +10 -9
  35. data/lib/caruby/util/person.rb +41 -12
  36. data/lib/caruby/util/pretty_print.rb +1 -1
  37. data/lib/caruby/util/validation.rb +0 -28
  38. data/lib/caruby/version.rb +1 -1
  39. data/test/lib/caruby/import/java_test.rb +26 -9
  40. data/test/lib/caruby/migration/test_case.rb +103 -0
  41. data/test/lib/caruby/test_case.rb +231 -0
  42. data/test/lib/caruby/util/class_test.rb +2 -2
  43. data/test/lib/caruby/util/visitor_test.rb +3 -2
  44. data/test/lib/examples/galena/clinical_trials/migration/participant_test.rb +28 -0
  45. data/test/lib/examples/galena/clinical_trials/migration/test_case.rb +40 -0
  46. metadata +195 -170
  47. data/lib/caruby/domain/attribute_initializer.rb +0 -16
  48. data/test/lib/caruby/util/validation_test.rb +0 -14
@@ -4,10 +4,6 @@ require 'caruby/util/pretty_print'
4
4
  module CaRuby
5
5
  # SearchTemplateBuilder builds a template suitable for a caCORE saarch database operation.
6
6
  class SearchTemplateBuilder
7
- def initialize(database)
8
- @database = database
9
- end
10
-
11
7
  # Returns a template for matching the domain object obj and the optional hash values.
12
8
  # The default hash attributes are the {ResourceAttributes#searchable_attributes}.
13
9
  # The template includes only the non-domain attributes of the hash references.
@@ -42,16 +38,18 @@ module CaRuby
42
38
 
43
39
  private
44
40
 
45
- # Sets the template attribute to a new search reference object created from source.
46
- # The reference contains only the source identifier.
47
- # Returns the search reference, or nil if source does not exist in the database.
41
+ # Sets the template attribute to a new search reference object created from the given
42
+ # source domain object. The reference contains only the source identifier, if it exists,
43
+ # or the source non-domain attributes otherwise.
44
+ #
45
+ # @return [Resource] the search reference
48
46
  def add_search_template_reference(template, source, attribute)
49
47
  ref = source.identifier ? source.copy(:identifier) : source.copy
50
48
  # Disable inverse integrity, since the template attribute assignment might have added a reference
51
49
  # from ref to template, which introduces a template => ref => template cycle that causes a caCORE
52
50
  # search infinite loop. Use the Java property writer instead.
53
- writer = template.class.attribute_metadata(attribute).property_accessors.last
54
- template.send(writer, ref)
51
+ wtr = template.class.attribute_metadata(attribute).property_writer
52
+ template.send(wtr, ref)
55
53
  logger.debug { "Search reference parameter #{attribute} for #{template.qp} set to #{ref} copied from #{source.qp}" }
56
54
  ref
57
55
  end
@@ -13,53 +13,78 @@ module CaRuby
13
13
  class SQLExecutor
14
14
  # Creates a new SQLExecutor with the given options.
15
15
  #
16
- # The default :database_host is the application :host property value, which in turn
17
- # defaults to 'localhost'.
16
+ # The default database host is the application :host property value, which in turn
17
+ # defaults to +localhost+.
18
18
  #
19
- # The default :database_type is 'mysql'. The optional :database_port property overrides
19
+ # The default database type is +mysql+. The optional :database_port property overrides
20
20
  # the default port for the database type.
21
21
  #
22
- # The default :database_driver is 'jdbc:mysql' for MySQL or 'Oracle' for Oracle.
22
+ # The default database driver is +jdbc:mysql+ for MySQL, +Oracle+ for Oracle.
23
+ # The default database driver class is +com.mysql.jdbc.Driver+ for MySQL,
24
+ # +oracle.jdbc.OracleDriver+ for Oracle.
23
25
  #
24
- # @option options [String] :database_host the database host
25
- # @option options [String] :database the database name
26
- # @option options [Integer] :database_port the database password (not the application login password)
27
- # @option options [String] :database_type the DBI database type, e.g. +mysql+
28
- # @option options [String] :database_driver the DBI connect driver string, e.g. +jdbc:mysql+
29
- # @option options [String] :database_user the database username (not the application login name)
30
- # @option options [String] :database_password the database password (not the application login password)
31
- # Raises CaRuby::ConfigurationError if an option is invalid.
32
- def initialize(options)
33
- app_host = Options.get(:host, options, "localhost")
34
- db_host = Options.get(:database_host, options, app_host)
35
- db_type = Options.get(:database_type, options, "mysql")
36
- db_driver = Options.get(:database_driver, options) { default_driver_string(db_type) }
37
- db_port = Options.get(:database_port, options) { default_port(db_type) }
38
- db_name = Options.get(:database, options) { raise_missing_option_exception(:database) }
26
+ # @param [Hash] opts the connect options
27
+ # @option opts [String] :database the mandatory database name
28
+ # @option opts [String] :database_user the mandatory database username (not the application login name)
29
+ # @option opts [String] :database_password the optional database password (not the application login password)
30
+ # @option opts [String] :database_host the optional database host
31
+ # @option opts [Integer] :database_port the optional database port number
32
+ # @option opts [String] :database_type the optional DBI database type, e.g. +mysql+
33
+ # @option opts [String] :database_driver the optional DBI connect driver string, e.g. +jdbc:mysql+
34
+ # @option opts [String] :database_driver_class the optional DBI connect driver class name
35
+ # @raise [CaRuby::ConfigurationError] if an option is invalid
36
+ def initialize(opts)
37
+ app_host = Options.get(:host, opts, 'localhost')
38
+ db_host = Options.get(:database_host, opts, app_host)
39
+ db_type = Options.get(:database_type, opts, 'mysql')
40
+ db_driver = Options.get(:database_driver, opts) { default_driver_string(db_type) }
41
+ db_port = Options.get(:database_port, opts) { default_port(db_type) }
42
+ db_name = Options.get(:database, opts) { raise_missing_option_exception(:database) }
39
43
  @address = "dbi:#{db_driver}://#{db_host}:#{db_port}/#{db_name}"
40
- @username = Options.get(:database_user, options) { raise_missing_option_exception(:database_user) }
41
- @password = Options.get(:database_password, options) { raise_missing_option_exception(:database_password) }
44
+ @username = Options.get(:database_user, opts) { raise_missing_option_exception(:database_user) }
45
+ @password = Options.get(:database_password, opts)
46
+ @driver_class = Options.get(:database_driver_class, opts, default_driver_class(db_type))
47
+ # The effective connection options.
48
+ eff_opts = {
49
+ :database => db_name,
50
+ :database_host => db_host,
51
+ :database_user => @username,
52
+ :database_type => db_type,
53
+ :database_port => db_port,
54
+ :database_driver => db_driver,
55
+ :database_driver_class => @driver_class
56
+ }
57
+ logger.debug { "Database connection options (excluding password): #{eff_opts.qp}" }
42
58
  end
43
59
 
44
60
  # Connects to the database, yields the DBI handle to the given block and disconnects.
45
61
  #
46
62
  # @return [Array] the execution result
47
63
  def execute
48
- logger.debug { "Connecting to database with user #{@username}, address #{@address}..." }
49
- result = DBI.connect(@address, @username, @password, "driver"=>"com.mysql.jdbc.Driver") { |dbh| yield dbh }
50
- logger.debug { "Disconnected from the database." }
51
- result
64
+ DBI.connect(@address, @username, @password, 'driver'=> @driver_class) { |dbh| yield dbh }
52
65
  end
53
66
 
54
67
  private
68
+
69
+ MYSQL_DRIVER_CLASS_NAME = 'com.mysql.jdbc.Driver'
70
+
71
+ ORACLE_DRIVER_CLASS_NAME = 'oracle.jdbc.OracleDriver'
55
72
 
56
73
  def default_driver_string(db_type)
57
74
  case db_type.downcase
58
- when 'mysql' then 'jdbc:mysql'
75
+ when 'mysql' then 'Jdbc:mysql'
59
76
  when 'oracle' then 'Oracle'
60
77
  else raise CaRuby::ConfigurationError.new("Default database connection driver string could not be determined for database type #{db_type}")
61
78
  end
62
79
  end
80
+
81
+ def default_driver_class(db_type)
82
+ case db_type.downcase
83
+ when 'mysql' then MYSQL_DRIVER_CLASS_NAME
84
+ when 'oracle' then ORACLE_DRIVER_CLASS_NAME
85
+ else raise CaRuby::ConfigurationError.new("Default database connection driver class could not be determined for database type #{db_type}")
86
+ end
87
+ end
63
88
 
64
89
  def default_port(db_type)
65
90
  case db_type.downcase
@@ -70,7 +95,7 @@ module CaRuby
70
95
  end
71
96
 
72
97
  def raise_missing_option_exception(option)
73
- raise CaRuby::ConfigurationError.new("database connection property not found: #{option}")
98
+ raise CaRuby::ConfigurationError.new("Database connection property not found: #{option}")
74
99
  end
75
100
  end
76
101
  end
@@ -113,11 +113,11 @@ module CaRuby
113
113
 
114
114
  # Returns the attributes to visit in building the template for the given
115
115
  # domain object. The visitable attributes consist of the following:
116
- # * The {ResourceAttributes#unproxied_cascaded_attributes} filtered as follows:
116
+ # * The {ResourceAttributes#unproxied_save_template_attributes} filtered as follows:
117
117
  # * If the database operation is a create, then exclude the cascaded attributes.
118
118
  # * If the given object has an identifier, then exclude the attributes which
119
119
  # have the the :no_cascade_update_to_create flag set.
120
- # * The {ResourceAttributes#proxied_cascaded_attributes} are included if and
120
+ # * The {ResourceAttributes#proxied_save_template_attributes} are included if and
121
121
  # only if every referenced object has an identifier, and therefore does not
122
122
  # need to be proxied.
123
123
  #
@@ -130,15 +130,16 @@ module CaRuby
130
130
  # @return [<Symbol>] the reference attributes to include in the update template
131
131
  def savable_cascaded_attributes(obj)
132
132
  # The starting set of candidate attributes is the unproxied cascaded references.
133
- unproxied = savable_attributes(obj, obj.class.unproxied_cascaded_attributes)
133
+ unproxied = savable_attributes(obj, obj.class.unproxied_save_template_attributes)
134
134
  # The proxied attributes to save.
135
135
  proxied = savable_proxied_attributes(obj)
136
136
  # The combined set of savable attributes
137
137
  proxied.empty? ? unproxied : unproxied + proxied
138
138
  end
139
139
 
140
- # Composes the given attributes, if necessary, to exclude attributes as follows:
141
- # * If the save operation is a create, then exclude the auto-generated attributes.
140
+ # Filters the given attributes, if necessary, to exclude attributes as follows:
141
+ # * If the save operation is a create, then exclude the
142
+ # {AttributeMetadata#autogenerated_on_create?} attributes.
142
143
  #
143
144
  # @param [Resource] obj the visited domain object
144
145
  # @param [ResourceAttributes::Filter] the savable attribute filter
@@ -148,7 +149,7 @@ module CaRuby
148
149
  return attributes if @subject.identifier
149
150
  # This is a create: ignore the optional auto-generated attributes.
150
151
  mas = obj.mandatory_attributes.to_set
151
- attributes.compose { |attr_md| mas.include?(attr_md.to_sym) or not attr_md.autogenerated? }
152
+ attributes.compose { |attr_md| mas.include?(attr_md.to_sym) or not attr_md.autogenerated_on_create? }
152
153
  end
153
154
 
154
155
  # Composes the given attributes, if necessary, to exclude attributes as follows:
@@ -177,7 +178,7 @@ module CaRuby
177
178
  def savable_proxied_attributes(obj)
178
179
  # Include a proxied reference only if the proxied dependents have an identifier,
179
180
  # since those without an identifer are created separately via the proxy.
180
- obj.class.proxied_cascaded_attributes.reject do |attr|
181
+ obj.class.proxied_save_template_attributes.reject do |attr|
181
182
  ref = obj.send(attr)
182
183
  case ref
183
184
  when Enumerable then ref.any? { |dep| not dep.identifier }
@@ -199,7 +200,7 @@ module CaRuby
199
200
  # references via the proxy create before building the update template.
200
201
  def copy_proxied_save_references(obj, template)
201
202
  return unless obj.identifier
202
- obj.class.proxied_cascaded_attributes.each do |attr|
203
+ obj.class.proxied_save_template_attributes.each do |attr|
203
204
  # the proxy source
204
205
  ref = obj.send(attr)
205
206
  case ref
@@ -253,20 +254,24 @@ module CaRuby
253
254
  # add qualified prerequisite attribute references
254
255
  stbl.send(attr).enumerate do |ref|
255
256
  # Add the prerequisite if it satisfies the prerequisite? condition.
256
- prereqs << ref if prerequisite?(ref, obj)
257
+ prereqs << ref if prerequisite?(ref, obj, attr)
257
258
  end
258
259
  end
259
- end
260
+ end
260
261
  prereqs
261
262
  end
262
263
 
263
- # A referenced object is a target object save prerequisite if is the target object, was already created
264
- # or is in an immediate or recursive dependent of the target object.
264
+ # A referenced object is a target object save prerequisite if none of the follwing is true:
265
+ # * it is the target object
266
+ # * it was already created
267
+ # * it is in an immediate or recursive dependent of the target object
268
+ # * the current save operation is in the context of creating the referenced object
265
269
  #
266
270
  # @param [Resource] ref the reference to check
267
271
  # @param [Resource] obj the object being stored
272
+ # @param [Symbol] attribute the reference attribute
268
273
  # @return [Boolean] whether the reference should exist before storing the object
269
- def prerequisite?(ref, obj)
274
+ def prerequisite?(ref, obj, attribute)
270
275
  not (ref == obj or ref.identifier or ref.owner_ancestor?(obj))
271
276
  end
272
277
  end
@@ -113,7 +113,7 @@ module CaRuby
113
113
  # @return [Resource] obj
114
114
  # @raise [DatabaseError] if the database operation fails
115
115
  def save(obj)
116
- logger.debug { "Storing #{obj}..." }
116
+ logger.debug { "Saving #{obj}..." }
117
117
  # if obj exists then update it, otherwise create it
118
118
  exists?(obj) ? update(obj) : create(obj)
119
119
  end
@@ -140,8 +140,10 @@ module CaRuby
140
140
  raise ArgumentError.new("Database ensure_exists is missing a domain object argument") if obj.nil_or_empty?
141
141
  obj.enumerate { |ref| find(ref, :create) unless ref.identifier }
142
142
  end
143
+
144
+ private
143
145
 
144
- # Returns whether there is already the given obj operation in progress that is not in the scope of
146
+ # Returns whether there is already the given object operation in progress that is not in the scope of
145
147
  # an operation performed on a dependent obj owner, i.e. a second obj save operation of the same type
146
148
  # is only allowed if the obj operation was delegated to an owner save which in turn saves the dependent
147
149
  # obj.
@@ -153,9 +155,7 @@ module CaRuby
153
155
  @operations.any? { |op| op.type == operation and op.subject == obj } and
154
156
  not obj.owner_ancestor?(@operations.last.subject)
155
157
  end
156
-
157
- private
158
-
158
+
159
159
  # Creates obj as follows:
160
160
  # * if obj has an uncreated owner, then store the owner, which in turn will create a physical dependent
161
161
  # * otherwise, create a storable template. The template is a copy of obj containing a recursive copy
@@ -170,11 +170,14 @@ module CaRuby
170
170
  # add obj to the transients set
171
171
  @transients << obj
172
172
  begin
173
- # A dependent of an uncreated owner can be created by creating the owner.
174
- # Otherwise, create obj from a template.
175
- create_as_dependent(obj) or
176
- create_from_template(obj) or
173
+ # A dependent is created by saving the owner.
174
+ # Otherwise, create the object from a template.
175
+ owner = cascaded_owner(obj)
176
+ result = create_dependent(owner, obj) if owner
177
+ result ||= create_from_template(obj)
178
+ if result.nil? then
177
179
  raise DatabaseError.new("#{obj.class.qp} is not creatable in context #{print_operations}")
180
+ end
178
181
  ensure
179
182
  # since obj now has an id, removed from transients set
180
183
  @transients.delete(obj)
@@ -192,18 +195,16 @@ module CaRuby
192
195
  #
193
196
  #@param [Resource] dep the dependent domain object to create
194
197
  # @return [Resource] dep
195
- def create_as_dependent(dep)
196
- # bail if not dependent or owner is not set
197
- ownr = dep.owner || return
198
- unless ownr.identifier then
199
- logger.debug { "Adding #{ownr.qp} dependent #{dep.qp} defaults..." }
200
- logger.debug { "Ensuring that dependent #{dep.qp} owner #{ownr.qp} exists..." }
201
- ensure_exists(ownr)
198
+ def create_dependent(owner, dep)
199
+ if owner.identifier.nil? then
200
+ logger.debug { "Adding #{owner.qp} dependent #{dep.qp} defaults..." }
201
+ logger.debug { "Ensuring that dependent #{dep.qp} owner #{owner.qp} exists..." }
202
+ ensure_exists(owner)
202
203
  end
203
204
 
204
205
  # If the dependent was created as a side-effect of creating the owner, then we are done.
205
206
  if dep.identifier then
206
- logger.debug { "Created dependent #{dep.qp} by saving owner #{ownr.qp}." }
207
+ logger.debug { "Created dependent #{dep.qp} by saving owner #{owner.qp}." }
207
208
  return dep
208
209
  end
209
210
 
@@ -279,6 +280,7 @@ module CaRuby
279
280
  # The create template. Independent saved references are created as necessary.
280
281
  tmpl = build_create_template(obj)
281
282
  save_with_template(obj, tmpl) { |svc| svc.create(tmpl) }
283
+
282
284
  # If obj is a top-level create, then ensure that remaining references exist.
283
285
  if @operations.first.subject == obj then
284
286
  refs = obj.references.reject { |ref| ref.identifier }
@@ -340,20 +342,34 @@ module CaRuby
340
342
  end
341
343
  end
342
344
 
343
- # Returns whether the given creatable domain attribute with value obj is a
344
- # {AttributeMetadata#one_to_one_bidirectional_independent?}
345
- # unsaved optional attribute without an obj {#pending_create?} save context.
345
+ # Returns whether the given creatable domain attribute with value obj satisfies
346
+ # each of the following conditions:
347
+ # * the attribute is {AttributeMetadata#independent?}
348
+ # * the attribute is not an {AttributeMetadata#owner?}
349
+ # * the obj value is unsaved
350
+ # * the attribute is not mandatory
351
+ # * the attribute references a {#pending_create?} save context.
346
352
  #
347
353
  # @param obj (see #create)
348
354
  # @param [AttributeMetadata] attr_md candidate attribute metadata
349
355
  # @return [Boolean] whether the attribute should not be included in the create template
350
356
  def exclude_pending_create_attribute?(obj, attr_md)
351
- attr_md.one_to_one_bidirectional_independent? and
357
+ attr_md.independent? and
358
+ not attr_md.owner? and
352
359
  obj.identifier.nil? and
353
360
  not obj.mandatory_attributes.include?(attr_md.to_sym) and
354
- ref = obj.send(attr_md.to_sym) and
355
- ref.identifier.nil? and
356
- pending_create?(ref)
361
+ exclude_pending_create_value?(obj.send(attr_md.to_sym))
362
+ end
363
+
364
+ # @param [Resource, <Resource>, nil] value the referenced value
365
+ # @return [Boolean] whether the value includes a {#pending_create?} save context object
366
+ def exclude_pending_create_value?(value)
367
+ return false if value.nil?
368
+ if Enumerable === value then
369
+ value.any? { |ref| exclude_pending_create_value?(ref) }
370
+ else
371
+ value.identifier.nil? and pending_create?(value)
372
+ end
357
373
  end
358
374
 
359
375
  # @param [Resource] obj the object to check
@@ -395,6 +411,12 @@ module CaRuby
395
411
  proxied.each { |dep| update(dep) }
396
412
  end
397
413
 
414
+ # update a cascaded dependent by updating the owner
415
+ owner = cascaded_owner(obj)
416
+ result = update_dependent(owner, obj) if owner
417
+ # if not cascaded, then update directly with a template
418
+ result ||= create_from_template(obj)
419
+
398
420
  # update using a template
399
421
  tmpl = build_update_template(obj)
400
422
 
@@ -404,6 +426,38 @@ module CaRuby
404
426
  obj.take_snapshot
405
427
  end
406
428
 
429
+ # Returns the owner that can cascade update to the given object.
430
+ # The owner is the #{Resource#effective_owner_attribute_metadata} value
431
+ # for which the owner attribute {AttributeMetadata#inverse_attribute_metadata}
432
+ # is {AttributeMetadata#cascaded?}.
433
+ #
434
+ # @param [Resource] obj the domain object to update
435
+ # @return [Resource, nil] the owner which can cascade an update to the object, or nil if none
436
+ # @raise [DatabaseError] if the domain object is a cascaded dependent but does not have an owner
437
+ def cascaded_owner(obj)
438
+ return unless obj.class.cascaded_dependent?
439
+ # the owner attribute
440
+ oattr = obj.effective_owner_attribute
441
+ if oattr.nil? then raise DatabaseError.new("Dependent #{obj} does not have an owner") end
442
+ dep_md = obj.class.attribute_metadata(oattr).inverse_attribute_metadata
443
+ if dep_md and dep_md.cascaded? then
444
+ obj.send(oattr)
445
+ end
446
+ end
447
+
448
+ def update_dependent(owner, obj)
449
+ logger.debug { "Updating #{obj} by saving the owner #{owner}..." }
450
+ update(owner)
451
+ end
452
+
453
+ def update_from_template(obj)
454
+ tmpl = build_update_template(obj)
455
+ # call the caCORE service with an obj update template
456
+ save_with_template(obj, tmpl) { |svc| svc.update(tmpl) }
457
+ # take a snapshot of the updated content
458
+ obj.take_snapshot
459
+ end
460
+
407
461
  # caTissue alert - the conditions for when and how to include a proxied dependent are
408
462
  # are intricate and treacherous. So far as can be determined, in the case of a
409
463
  # SpecimenPosition proxied by a TransferEventParameters, the sequence is as follows:
@@ -427,10 +481,10 @@ module CaRuby
427
481
  # must be created via the proxy after the Specimen is created.
428
482
  #
429
483
  # @param (see #update)
430
- # @return [<Resource>] the #{ResourceAttributes#proxied_cascaded_attributes} dependents
484
+ # @return [<Resource>] the #{ResourceAttributes#proxied_save_template_attributes} dependents
431
485
  # which are #{Persistable#changed?}
432
486
  def updatable_proxied_dependents(obj)
433
- attrs = obj.class.proxied_cascaded_attributes
487
+ attrs = obj.class.proxied_save_template_attributes
434
488
  return Array::EMPTY_ARRAY if attrs.empty?
435
489
  deps = []
436
490
  attrs.each do |attr|
@@ -459,7 +513,7 @@ module CaRuby
459
513
  if obj.identifier.nil? then
460
514
  raise DatabaseError.new("Delete target is missing a database identifier: #{obj}")
461
515
  end
462
- persistence_service(obj).delete_object(obj)
516
+ persistence_service(obj.class).delete_object(obj)
463
517
  end
464
518
 
465
519
  # Saves the given template built from the given domain object obj. The persistence operation
@@ -471,15 +525,22 @@ module CaRuby
471
525
  # @param [Resource] template the obj template to submit to caCORE
472
526
  def save_with_template(obj, template)
473
527
  logger.debug { "Saving #{obj.qp} from template:\n#{template.dump}" }
474
- # call the application service
475
- # dispatch to the app service
476
- svc = persistence_service(template)
477
- result = template.identifier ? svc.update(template) : svc.create(template)
478
- logger.debug { "Store #{obj.qp} with template #{template.qp} produced caCORE result: #{result}." }
528
+ # dispatch to the application service
529
+ result = submit_save_template(obj, template)
479
530
  # sync the result
480
531
  sync_saved(obj, result)
481
532
  end
482
533
 
534
+ # Dispatches the given template to the application service.
535
+ #
536
+ # @param (see #save_with_template)
537
+ def submit_save_template(obj, template)
538
+ svc = persistence_service(template.class)
539
+ result = template.identifier ? svc.update(template) : svc.create(template)
540
+ logger.debug { "Store #{obj.qp} with template #{template.qp} produced caCORE result: #{result}." }
541
+ result
542
+ end
543
+
483
544
  # Synchronizes the content of the given saved domain object and the save result source as follows:
484
545
  # 1. The save result source is first synchronized with the database content as necessary.
485
546
  # 2. Then the source is merged into the target.
@@ -551,7 +612,6 @@ module CaRuby
551
612
  def sync_save_result(source, target)
552
613
  # Bail if the result is the same as the source, as occurs, e.g., with caTissue annotations.
553
614
  return if source == target
554
- logger.debug { "Synchronizing #{target} save result #{source} with the database..." }
555
615
  # If the target was created, then refetch and merge the source if necessary to reflect auto-generated
556
616
  # non-domain attribute values.
557
617
  if target.identifier.nil? then sync_created_result_object(source) end
@@ -559,7 +619,6 @@ module CaRuby
559
619
  sync_save_result_references(source, target)
560
620
  # Set inverses consistently in the source object graph
561
621
  set_inverses(source)
562
- logger.debug { "Synchronized #{target} save result #{source} with the database." }
563
622
  end
564
623
 
565
624
  # Refetches the given create result source if there are any {ResourceAttributes#autogenerated_nondomain_attributes}
@@ -626,9 +685,6 @@ module CaRuby
626
685
  #
627
686
  # @param [Resource] obj the owner domain object
628
687
  def save_changed_dependents(obj)
629
- # JRuby alert - copy the Resource dependents call result to an array, since iteration based on
630
- # Forwardable enum_for breaks here with an obscure Java ConcurrentModificationException
631
- # in the CaTissue SCG save test case. TODO - isolate and fix at source.
632
688
  obj.class.dependent_attributes.each do |attr|
633
689
  deps = obj.send(attr).to_enum
634
690
  logger.debug { "Saving the #{obj} #{attr} dependents #{deps.qp} which have changed..." } unless deps.empty?
@@ -639,7 +695,9 @@ module CaRuby
639
695
  # Saves the given dependent domain object if necessary.
640
696
  # Recursively saves the obj dependents as necessary.
641
697
  #
642
- # @param [Resource] obj the dependent domain object to save
698
+ # @param [Resource] owner the dependent owner
699
+ # @param [Symbol] attribute the dependent attribute
700
+ # @param [Resource] dependent the dependent to save
643
701
  def save_dependent_if_changed(owner, attribute, dependent)
644
702
  if dependent.identifier.nil? then
645
703
  logger.debug { "Creating #{owner.qp} #{attribute} dependent #{dependent.qp}..." }
@@ -652,19 +710,24 @@ module CaRuby
652
710
  op = operations.last
653
711
  # The dependent is auto-generated if the owner was created or auto-generated and
654
712
  # the dependent attribute is auto-generated.
655
- ag = (op.type == :create or op.autogenerated?) && owner.class.attribute_metadata(attribute).autogenerated?
713
+ attr_md = owner.class.attribute_metadata(attribute)
714
+ ag = (op.type == :create or op.autogenerated?) && attr_md.autogenerated?
656
715
  logger.debug { "Updating the changed #{owner.qp} #{attribute} dependent #{dependent.qp}..." }
657
- perform(:update, dependent, :autogenerated => ag) { update_object(dependent) }
716
+ if ag then
717
+ logger.debug { "Adding defaults to the auto-generated #{owner.qp} #{attribute} dependent #{dependent.qp}..." }
718
+ dependent.add_defaults_autogenerated
719
+ end
720
+ update_changed_dependent(owner, attribute, dependent, ag)
658
721
  else
659
722
  save_changed_dependents(dependent)
660
723
  end
661
724
  end
662
725
 
663
- # Creates the given dependent domain object.
726
+ # Updates the given dependent.
664
727
  #
665
- # @param (see #save_dependent)
666
- def create_dependent(owner, dep)
667
- create(dep)
728
+ # @param (see #save_dependent_if_changed)
729
+ def update_changed_dependent(owner, attribute, dependent, autogenerated)
730
+ perform(:update, dependent, :autogenerated => autogenerated) { update_object(dependent) }
668
731
  end
669
732
  end
670
733
  end