caruby-core 1.4.7 → 1.4.9

Sign up to get free protection for your applications and to get access to all the features.
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