caruby-core 1.5.5 → 2.1.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (126) hide show
  1. data/Gemfile +9 -0
  2. data/History.md +5 -1
  3. data/lib/caruby.rb +3 -5
  4. data/lib/caruby/caruby-src.tar.gz +0 -0
  5. data/lib/caruby/database.rb +53 -69
  6. data/lib/caruby/database/application_service.rb +25 -0
  7. data/lib/caruby/database/cache.rb +60 -0
  8. data/lib/caruby/database/fetched_matcher.rb +52 -38
  9. data/lib/caruby/database/lazy_loader.rb +4 -4
  10. data/lib/caruby/database/operation.rb +34 -0
  11. data/lib/caruby/database/persistable.rb +171 -86
  12. data/lib/caruby/database/persistence_service.rb +32 -34
  13. data/lib/caruby/database/persistifier.rb +100 -43
  14. data/lib/caruby/database/reader.rb +107 -85
  15. data/lib/caruby/database/reader_template_builder.rb +60 -0
  16. data/lib/caruby/database/saved_matcher.rb +3 -3
  17. data/lib/caruby/database/sql_executor.rb +88 -17
  18. data/lib/caruby/database/writer.rb +213 -177
  19. data/lib/caruby/database/writer_template_builder.rb +334 -0
  20. data/lib/caruby/{util → helpers}/controlled_value.rb +0 -0
  21. data/lib/caruby/{util → helpers}/coordinate.rb +4 -4
  22. data/lib/caruby/{util → helpers}/person.rb +3 -3
  23. data/lib/caruby/{util → helpers}/properties.rb +7 -9
  24. data/lib/caruby/{util → helpers}/roman.rb +2 -2
  25. data/lib/caruby/{util → helpers}/version.rb +1 -1
  26. data/lib/caruby/json/deserializer.rb +2 -2
  27. data/lib/caruby/json/serializer.rb +49 -7
  28. data/lib/caruby/metadata.rb +30 -0
  29. data/lib/caruby/metadata/java_property.rb +21 -0
  30. data/lib/caruby/metadata/propertied.rb +191 -0
  31. data/lib/caruby/metadata/property.rb +22 -0
  32. data/lib/caruby/metadata/property_characteristics.rb +201 -0
  33. data/lib/caruby/migration/migratable.rb +11 -182
  34. data/lib/caruby/rdbi/driver/jdbc.rb +446 -0
  35. data/lib/caruby/resource.rb +20 -823
  36. data/lib/caruby/version.rb +1 -1
  37. data/test/lib/caruby/database/cache_test.rb +54 -0
  38. data/test/lib/caruby/{util → helpers}/controlled_value_test.rb +3 -5
  39. data/test/lib/caruby/{util → helpers}/person_test.rb +4 -6
  40. data/test/lib/caruby/helpers/properties_test.rb +34 -0
  41. data/test/lib/caruby/{util → helpers}/roman_test.rb +2 -3
  42. data/test/lib/caruby/{util → helpers}/version_test.rb +2 -3
  43. data/test/lib/helper.rb +7 -0
  44. metadata +161 -214
  45. data/lib/caruby/cli/application.rb +0 -36
  46. data/lib/caruby/cli/command.rb +0 -202
  47. data/lib/caruby/csv/csv_mapper.rb +0 -159
  48. data/lib/caruby/csv/csvio.rb +0 -203
  49. data/lib/caruby/database/search_template_builder.rb +0 -56
  50. data/lib/caruby/database/store_template_builder.rb +0 -278
  51. data/lib/caruby/domain.rb +0 -193
  52. data/lib/caruby/domain/attribute.rb +0 -584
  53. data/lib/caruby/domain/attributes.rb +0 -628
  54. data/lib/caruby/domain/dependency.rb +0 -225
  55. data/lib/caruby/domain/id_alias.rb +0 -22
  56. data/lib/caruby/domain/importer.rb +0 -183
  57. data/lib/caruby/domain/introspection.rb +0 -176
  58. data/lib/caruby/domain/inverse.rb +0 -172
  59. data/lib/caruby/domain/inversible.rb +0 -90
  60. data/lib/caruby/domain/java_attribute.rb +0 -173
  61. data/lib/caruby/domain/merge.rb +0 -185
  62. data/lib/caruby/domain/metadata.rb +0 -142
  63. data/lib/caruby/domain/mixin.rb +0 -35
  64. data/lib/caruby/domain/properties.rb +0 -95
  65. data/lib/caruby/domain/reference_visitor.rb +0 -428
  66. data/lib/caruby/domain/uniquify.rb +0 -50
  67. data/lib/caruby/import/java.rb +0 -387
  68. data/lib/caruby/migration/migrator.rb +0 -918
  69. data/lib/caruby/migration/resource_module.rb +0 -9
  70. data/lib/caruby/migration/uniquify.rb +0 -17
  71. data/lib/caruby/util/attribute_path.rb +0 -44
  72. data/lib/caruby/util/cache.rb +0 -56
  73. data/lib/caruby/util/class.rb +0 -149
  74. data/lib/caruby/util/collection.rb +0 -1152
  75. data/lib/caruby/util/domain_extent.rb +0 -46
  76. data/lib/caruby/util/file_separator.rb +0 -65
  77. data/lib/caruby/util/inflector.rb +0 -27
  78. data/lib/caruby/util/log.rb +0 -95
  79. data/lib/caruby/util/math.rb +0 -12
  80. data/lib/caruby/util/merge.rb +0 -59
  81. data/lib/caruby/util/module.rb +0 -18
  82. data/lib/caruby/util/options.rb +0 -97
  83. data/lib/caruby/util/partial_order.rb +0 -35
  84. data/lib/caruby/util/pretty_print.rb +0 -204
  85. data/lib/caruby/util/stopwatch.rb +0 -74
  86. data/lib/caruby/util/topological_sync_enumerator.rb +0 -62
  87. data/lib/caruby/util/transitive_closure.rb +0 -55
  88. data/lib/caruby/util/tree.rb +0 -48
  89. data/lib/caruby/util/trie.rb +0 -37
  90. data/lib/caruby/util/uniquifier.rb +0 -30
  91. data/lib/caruby/util/validation.rb +0 -20
  92. data/lib/caruby/util/visitor.rb +0 -365
  93. data/lib/caruby/util/weak_hash.rb +0 -36
  94. data/test/lib/caruby/csv/csv_mapper_test.rb +0 -40
  95. data/test/lib/caruby/csv/csvio_test.rb +0 -69
  96. data/test/lib/caruby/database/persistable_test.rb +0 -92
  97. data/test/lib/caruby/domain/domain_test.rb +0 -112
  98. data/test/lib/caruby/domain/inversible_test.rb +0 -99
  99. data/test/lib/caruby/domain/reference_visitor_test.rb +0 -130
  100. data/test/lib/caruby/import/java_test.rb +0 -80
  101. data/test/lib/caruby/import/mixed_case_test.rb +0 -14
  102. data/test/lib/caruby/migration/test_case.rb +0 -102
  103. data/test/lib/caruby/test_case.rb +0 -230
  104. data/test/lib/caruby/util/cache_test.rb +0 -23
  105. data/test/lib/caruby/util/class_test.rb +0 -61
  106. data/test/lib/caruby/util/collection_test.rb +0 -398
  107. data/test/lib/caruby/util/command_test.rb +0 -55
  108. data/test/lib/caruby/util/domain_extent_test.rb +0 -60
  109. data/test/lib/caruby/util/file_separator_test.rb +0 -30
  110. data/test/lib/caruby/util/inflector_test.rb +0 -12
  111. data/test/lib/caruby/util/lazy_hash_test.rb +0 -34
  112. data/test/lib/caruby/util/merge_test.rb +0 -83
  113. data/test/lib/caruby/util/module_test.rb +0 -25
  114. data/test/lib/caruby/util/options_test.rb +0 -59
  115. data/test/lib/caruby/util/partial_order_test.rb +0 -42
  116. data/test/lib/caruby/util/pretty_print_test.rb +0 -85
  117. data/test/lib/caruby/util/properties_test.rb +0 -50
  118. data/test/lib/caruby/util/stopwatch_test.rb +0 -18
  119. data/test/lib/caruby/util/topological_sync_enumerator_test.rb +0 -69
  120. data/test/lib/caruby/util/transitive_closure_test.rb +0 -67
  121. data/test/lib/caruby/util/tree_test.rb +0 -23
  122. data/test/lib/caruby/util/trie_test.rb +0 -14
  123. data/test/lib/caruby/util/visitor_test.rb +0 -278
  124. data/test/lib/caruby/util/weak_hash_test.rb +0 -45
  125. data/test/lib/examples/clinical_trials/migration/migration_test.rb +0 -58
  126. data/test/lib/examples/clinical_trials/migration/test_case.rb +0 -38
@@ -0,0 +1,60 @@
1
+ require 'jinx/resource'
2
+ require 'jinx/helpers/pretty_print'
3
+
4
+ module CaRuby
5
+ class Database
6
+ module Reader
7
+ # TemplateBuilder builds a template suitable for a caCORE saarch database operation.
8
+ class TemplateBuilder
9
+ # Returns a template for matching the domain object obj and the optional hash values.
10
+ # The default hash attributes are the {Propertied#searchable_attributes}.
11
+ # The template includes only the non-domain attributes of the hash references.
12
+ #
13
+ # @quirk caCORE Because of caCORE API limitations, the obj searchable attribute
14
+ # values are limited to the following:
15
+ # * non-domain attribute values
16
+ # * non-collection domain attribute references which contain a key
17
+ #
18
+ # @quirk caCORE the caCORE query builder breaks on reference cycles and is easily confused
19
+ # by extraneous references, so it is necessary to search with a template instead that contains
20
+ # only references essential to the search. Each reference is confirmed to exist and the
21
+ # reference content in the template consists entirely of the fetched identifier attribute.
22
+ def build_template(obj, hash=nil)
23
+ # split the attributes into reference and non-reference attributes.
24
+ # the new search template object is built from the non-reference attributes.
25
+ # the reference attributes values are copied and added.
26
+ logger.debug { "Building search template for #{obj.qp}..." }
27
+ hash ||= obj.value_hash(obj.class.searchable_attributes)
28
+ # the searchable attribute => value hash
29
+ rh, nrh = hash.split { |pa, value| Jinx::Resource === value }
30
+ # make the search template from the non-reference attributes
31
+ tmpl = obj.class.new.merge_attributes(nrh)
32
+ # get references for the search template
33
+ unless rh.empty? then
34
+ logger.debug { "Collecting search reference parameters for #{obj.qp} from attributes #{rh.keys.to_series}..." }
35
+ end
36
+ rh.each { |pa, ref| add_search_template_reference(tmpl, ref, pa) }
37
+ tmpl
38
+ end
39
+
40
+ private
41
+
42
+ # Sets the template attribute to a new search reference object created from the given
43
+ # source domain object. The reference contains only the source identifier, if it exists,
44
+ # or the source non-domain attributes otherwise.
45
+ #
46
+ # @return [Jinx::Resource] the search reference
47
+ def add_search_template_reference(template, source, attribute)
48
+ ref = source.identifier ? source.copy(:identifier) : source.copy
49
+ # Disable inverse integrity, since the template attribute assignment might have added a reference
50
+ # from ref to template, which introduces a template => ref => template cycle that causes a caCORE
51
+ # search infinite loop. Use the Java property writer instead.
52
+ wtr = template.class.property(attribute).property_writer
53
+ template.send(wtr, ref)
54
+ logger.debug { "Search reference parameter #{attribute} for #{template.qp} set to #{ref} copied from #{source.qp}" }
55
+ ref
56
+ end
57
+ end
58
+ end
59
+ end
60
+ end
@@ -26,7 +26,7 @@ module CaRuby
26
26
  #
27
27
  # @param sources (see #match_fetched)
28
28
  # @param targets (see #match_fetched)
29
- # @param [{Resource => Resource}] the source => target matches so far
29
+ # @param [{Jinx::Resource => Jinx::Resource}] the source => target matches so far
30
30
  def match_fetched_residual(sources, targets, matches)
31
31
  unmtchd_tgts = targets.to_set - matches.keys.delete_if { |tgt| tgt.identifier }
32
32
  unmtchd_srcs = sources.to_set - matches.values
@@ -34,8 +34,8 @@ module CaRuby
34
34
  matches.merge!(min_mtchs)
35
35
  end
36
36
 
37
- #@param [<Resource>] sources the source objects to match
38
- #@param [<Resource>] targets the potential match target objects
37
+ #@param [<Jinx::Resource>] sources the source objects to match
38
+ #@param [<Jinx::Resource>] targets the potential match target objects
39
39
  # @return (see #match_saved)
40
40
  def match_minimal(sources, targets)
41
41
  matches = {}
@@ -1,10 +1,5 @@
1
- require 'rubygems'
2
- gem 'dbi'
3
-
4
- require 'dbi'
5
- require 'caruby/util/options'
6
- require 'caruby/util/log'
7
- require 'caruby/domain/properties'
1
+ require 'jinx/helpers/options'
2
+ require 'caruby/rdbi/driver/jdbc'
8
3
 
9
4
  module CaRuby
10
5
  # SQLExecutor executes an SQL statement against the database.
@@ -23,19 +18,22 @@ module CaRuby
23
18
  # The default database driver class is +com.mysql.jdbc.Driver+ for MySQL,
24
19
  # +oracle.jdbc.OracleDriver+ for Oracle.
25
20
  #
21
+ # The default database URI is +dbi:+_db_driver_+://+_db_host_+:+_db_port_+/+_db_name_.
22
+ #
26
23
  # @param [Hash] opts the connect options
27
24
  # @option opts [String] :database the mandatory database name
28
25
  # @option opts [String] :database_user the mandatory database username (not the application login name)
29
26
  # @option opts [String] :database_password the optional database password (not the application login password)
30
27
  # @option opts [String] :database_host the optional database host
31
28
  # @option opts [Integer] :database_port the optional database port number
32
- # @option opts [String] :database_type the optional DBI database type, e.g. +mysql+
29
+ # @option opts [Integer] :database_port the optional database port number
33
30
  # @option opts [String] :database_driver the optional DBI connect driver string, e.g. +jdbc:mysql+
31
+ # @option opts [String] :database_url the optional database connection URL
34
32
  # @option opts [String] :database_driver_class the optional DBI connect driver class name
35
33
  # @raise [CaRuby::ConfigurationError] if an option is invalid
36
34
  def initialize(opts)
37
35
  if opts.empty? then
38
- raise CaRuby::ConfigurationError.new("The caRuby database connection properties were not found.")
36
+ Jinx.fail(CaRuby::ConfigurationError, "The caRuby database connection properties were not found.")
39
37
  end
40
38
  app_host = Options.get(:host, opts, 'localhost')
41
39
  db_host = Options.get(:database_host, opts, app_host)
@@ -43,7 +41,8 @@ module CaRuby
43
41
  db_driver = Options.get(:database_driver, opts) { default_driver_string(db_type) }
44
42
  db_port = Options.get(:database_port, opts) { default_port(db_type) }
45
43
  db_name = Options.get(:database, opts) { raise_missing_option_exception(:database) }
46
- @address = "dbi:#{db_driver}://#{db_host}:#{db_port}/#{db_name}"
44
+ @db_url = Options.get(:database_url, opts) { "#{db_driver}://#{db_host}:#{db_port}/#{db_name}" }
45
+ @dbi_url = 'dbi:' + @db_url
47
46
  @username = Options.get(:database_user, opts) { raise_missing_option_exception(:database_user) }
48
47
  @password = Options.get(:database_password, opts)
49
48
  @driver_class = Options.get(:database_driver_class, opts, default_driver_class(db_type))
@@ -56,16 +55,56 @@ module CaRuby
56
55
  :database_port => db_port,
57
56
  :database_driver => db_driver,
58
57
  :database_driver_class => @driver_class,
59
- :address => @address
58
+ :database_url => @db_url
60
59
  }
61
60
  logger.debug { "Database connection parameters (excluding password): #{eff_opts.qp}" }
62
61
  end
63
62
 
64
63
  # Connects to the database, yields the DBI handle to the given block and disconnects.
65
64
  #
66
- # @return [Array] the execution result
65
+ # @yield [dbh] the transaction statements
66
+ # @yieldparam [RDBI::Database] dbh the database handle
67
67
  def execute
68
- DBI.connect(@address, @username, @password, 'driver'=> @driver_class) { |dbh| yield dbh }
68
+ RDBI.connect(:JDBC, :database => @db_url, :user => @username, :password => @password, :driver_class=> @driver_class) do |dbh|
69
+ yield dbh
70
+ end
71
+ end
72
+
73
+ # Runs the given query.
74
+ #
75
+ # @param [String] sql the SQL to execute
76
+ # @param [Array] args the SQL bindings
77
+ # @return [Array] the query result
78
+ def query(sql, *args)
79
+ fetched = nil
80
+ execute do |dbh|
81
+ res = dbh.execute(sql, *args)
82
+ fetched = res.fetch(:all)
83
+ res.finish
84
+ end
85
+ fetched
86
+ end
87
+
88
+ # Runs the given SQL or block in a transaction. If SQL is provided, then that
89
+ # SQL is executed. Otherwise, the block is called.
90
+ #
91
+ # @quirk RDBI RDBI converts nil args to 12. Work-around this bug by embedding
92
+ # +NULL+ in the SQL instead.
93
+ #
94
+ # @param [String] sql the SQL to execute
95
+ # @param [Array] args the SQL bindings
96
+ # @yield [dbh] the transaction statements
97
+ # @yieldparam [RDBI::Database] dbh the database handle
98
+ def transact(sql=nil, *args)
99
+ # Work-around for rcbi nil substitution.
100
+ if sql then
101
+ sql, *args = replace_nil_binds(sql, args)
102
+ transact { |dbh| dbh.execute(sql, *args) }
103
+ elsif block_given? then
104
+ execute { |dbh| dbh.transaction { yield dbh } }
105
+ else
106
+ raise ArgumentError.new("SQL executor is missing the required execution block")
107
+ end
69
108
  end
70
109
 
71
110
  private
@@ -73,12 +112,44 @@ module CaRuby
73
112
  MYSQL_DRIVER_CLASS_NAME = 'com.mysql.jdbc.Driver'
74
113
 
75
114
  ORACLE_DRIVER_CLASS_NAME = 'oracle.jdbc.OracleDriver'
115
+
116
+ # Replaces nil arguments with a +NULL+ literal in the given SQL.
117
+ #
118
+ # @param (see #transact)
119
+ # @return [Array] the (possibly modified) SQL followed by the non-nil arguments
120
+ def replace_nil_binds(sql, args)
121
+ nils = []
122
+ args.each_with_index { |value, i| nils << i if value.nil? }
123
+ unless nils.empty? then
124
+ logger.debug { "SQL executor working around RDBI bug by eliminating the nil arguments #{nils.to_series} for the SQL:\n#{sql}..." }
125
+ # Quoted ? is too much of a pain for this hack; bail out.
126
+ raise ArgumentError.new("RDBI work-around does not support quoted ? in transactional SQL: #{sql}") if sql =~ /'[^,]*[?][^,]*'/
127
+ prefix, binds_s, suffix = /(.+\s*values\s*\()([^)]*)(\).*)/i.match(sql).captures
128
+ sql = prefix
129
+ binds = binds_s.split('?')
130
+ last = binds_s[-1, 1]
131
+ del_cnt = 0
132
+ binds.each_with_index do |s, i|
133
+ sql << s
134
+ if nils.include?(i) then
135
+ args.delete_at(i - del_cnt)
136
+ del_cnt += 1
137
+ sql << 'NULL'
138
+ elsif i < binds.size - 1 or last == '?'
139
+ sql << '?'
140
+ end
141
+ end
142
+ sql << suffix
143
+ end
144
+ logger.debug { "SQL executor converted the SQL to:\n#{sql}\nwith arguments #{args.qp}" }
145
+ return args.unshift(sql)
146
+ end
76
147
 
77
148
  def default_driver_string(db_type)
78
149
  case db_type.downcase
79
150
  when 'mysql' then 'Jdbc:mysql'
80
151
  when 'oracle' then 'Oracle'
81
- else raise CaRuby::ConfigurationError.new("Default database connection driver string could not be determined for database type #{db_type}")
152
+ else Jinx.fail(CaRuby::ConfigurationError, "Default database connection driver string could not be determined for database type #{db_type}")
82
153
  end
83
154
  end
84
155
 
@@ -86,7 +157,7 @@ module CaRuby
86
157
  case db_type.downcase
87
158
  when 'mysql' then MYSQL_DRIVER_CLASS_NAME
88
159
  when 'oracle' then ORACLE_DRIVER_CLASS_NAME
89
- else raise CaRuby::ConfigurationError.new("Default database connection driver class could not be determined for database type #{db_type}")
160
+ else Jinx.fail(CaRuby::ConfigurationError, "Default database connection driver class could not be determined for database type #{db_type}")
90
161
  end
91
162
  end
92
163
 
@@ -94,12 +165,12 @@ module CaRuby
94
165
  case db_type.downcase
95
166
  when 'mysql' then 3306
96
167
  when 'oracle' then 1521
97
- else raise CaRuby::ConfigurationError.new("Default database connection port could not be determined for database type #{db_type}")
168
+ else Jinx.fail(CaRuby::ConfigurationError, "Default database connection port could not be determined for database type #{db_type}")
98
169
  end
99
170
  end
100
171
 
101
172
  def raise_missing_option_exception(option)
102
- raise CaRuby::ConfigurationError.new("Database connection property not found: #{option}")
173
+ Jinx.fail(CaRuby::ConfigurationError, "Database connection property not found: #{option}")
103
174
  end
104
175
  end
105
176
  end
@@ -1,8 +1,10 @@
1
- require 'caruby/util/collection'
2
- require 'caruby/util/pretty_print'
3
- require 'caruby/domain/reference_visitor'
1
+ require 'jinx/helpers/collection'
2
+ require 'jinx/helpers/pretty_print'
3
+ require 'jinx/resource/reference_visitor'
4
+ require 'jinx/resource/match_visitor'
5
+ require 'jinx/resource/merge_visitor'
4
6
  require 'caruby/database/saved_matcher'
5
- require 'caruby/database/store_template_builder'
7
+ require 'caruby/database/writer_template_builder'
6
8
 
7
9
  module CaRuby
8
10
  class Database
@@ -11,23 +13,23 @@ module CaRuby
11
13
  # Adds store capability to this Database.
12
14
  def initialize
13
15
  super
14
- @ftchd_vstr = ReferenceVisitor.new { |tgt| tgt.class.fetched_domain_attributes }
15
- @cr_tmpl_bldr = StoreTemplateBuilder.new(self) { |ref| creatable_domain_attributes(ref) }
16
- @upd_tmpl_bldr = StoreTemplateBuilder.new(self) { |ref| updatable_domain_attributes(ref) }
16
+ @ftchd_vstr = Jinx::ReferenceVisitor.new { |tgt| tgt.class.fetched_domain_attributes }
17
+ @cr_tmpl_bldr = TemplateBuilder.new(self) { |ref| creatable_domain_attributes(ref) }
18
+ @upd_tmpl_bldr = TemplateBuilder.new(self) { |ref| updatable_domain_attributes(ref) }
17
19
  # the save result => argument reference matcher
18
20
  svd_mtchr = SavedMatcher.new
19
21
  # the save (result, argument) synchronization visitor
20
- @svd_sync_vstr = MatchVisitor.new(:matcher => svd_mtchr) { |ref| ref.class.dependent_attributes }
22
+ @svd_sync_vstr = Jinx::MatchVisitor.new(:matcher => svd_mtchr) { |ref| ref.class.dependent_attributes }
21
23
  # the attributes to merge from the save result
22
24
  mgbl = Proc.new { |ref| ref.class.domain_attributes }
23
25
  # the save result => argument merge visitor
24
- @svd_mrg_vstr = MergeVisitor.new(:matcher => svd_mtchr, :mergeable => mgbl) { |ref| ref.class.dependent_attributes }
26
+ @svd_mrg_vstr = Jinx::MergeVisitor.new(:matcher => svd_mtchr, :mergeable => mgbl) { |ref| ref.class.dependent_attributes }
25
27
  end
26
28
 
27
29
  # Creates the specified domain object obj and returns obj. The pre-condition for this method is as
28
30
  # follows:
29
31
  # * obj is a well-formed domain object with the necessary required attributes as determined by the
30
- # {Resource#validate} method
32
+ # {Jinx::Resource#validate} method
31
33
  # * obj does not have a database identifier attribute value
32
34
  # * obj does not yet exist in the database
33
35
  # The post-condition is that obj is created and assigned a database identifier.
@@ -55,8 +57,8 @@ module CaRuby
55
57
  # owner.dependents.size == dependents_count #=> true
56
58
  #
57
59
  # If obj is not dependent, then the create strategy is as follows:
58
- # * add default attribute values using {Resource#add_defaults}
59
- # * validate obj using {Resource#validate}
60
+ # * add default attribute values using {Jinx::Resource#add_defaults}
61
+ # * validate obj using {Jinx::Resource#validate}
60
62
  # * ensure that all saved independent reference attribute values exist in the database, creating
61
63
  # them if necessary
62
64
  # * ensure that all dependent reference attribute values can be created according to this set of rules
@@ -66,16 +68,16 @@ module CaRuby
66
68
  # * copy the new database identifier to each created object, i.e. the transitive closure of obj and
67
69
  # its dependents
68
70
  #
69
- # @param [Resource] the domain object to create
70
- # @return [Resource] obj
71
+ # @param [Jinx::Resource] the domain object to create
72
+ # @return [Jinx::Resource] obj
71
73
  # @raise [DatabaseError] if the database operation fails
72
74
  def create(obj)
73
75
  # guard against recursive call back into the same operation
74
76
  # the only allowed recursive call is a dependent create which first creates the owner
75
77
  if recursive_save?(obj, :create) then
76
- raise DatabaseError.new("Create #{obj.qp} recursively called in context #{print_operations}")
78
+ Jinx.fail(DatabaseError, "Create #{obj.qp} recursively called in context #{print_operations}")
77
79
  elsif obj.identifier then
78
- raise DatabaseError.new("Create unsuccessful since #{obj.qp} already has identifier #{obj.identifier}")
80
+ Jinx.fail(DatabaseError, "Create unsuccessful since #{obj.qp} already has identifier #{obj.identifier}")
79
81
  end
80
82
  # create the object
81
83
  perform(:create, obj) { create_object(obj) }
@@ -90,11 +92,12 @@ module CaRuby
90
92
  # * validate that obj has a database identifier
91
93
  # * the template is submitted to the application service update method
92
94
  #
93
- # Raises DatabaseError if the database operation fails.
95
+ # @param [Jinx::Resource] the object to update
96
+ # @raise [DatabaseError] if the database operation fails
94
97
  def update(obj)
95
98
  # guard against a recursive call back into the same operation.
96
99
  if recursive_save?(obj, :update) then
97
- raise DatabaseError.new("Update #{obj.qp} recursively called in context #{print_operations}")
100
+ Jinx.fail(DatabaseError, "Update #{obj.qp} recursively called in context #{print_operations}")
98
101
  end
99
102
  # update the object
100
103
  perform(:update, obj) { update_object(obj) }
@@ -109,8 +112,8 @@ module CaRuby
109
112
  # @see #create
110
113
  # @see #update
111
114
  #
112
- #@param [Resource] obj the domain object to save
113
- # @return [Resource] obj
115
+ #@param [Jinx::Resource] obj the domain object to save
116
+ # @return [Jinx::Resource] obj
114
117
  # @raise [DatabaseError] if the database operation fails
115
118
  def save(obj)
116
119
  logger.debug { "Saving #{obj}..." }
@@ -125,7 +128,7 @@ module CaRuby
125
128
  # Note that some applications restrict or forbid delete operations. Check the specific application
126
129
  # documentation to determine whether deletion is supported.
127
130
  #
128
- # @param [Resource] obj the domain object to delete
131
+ # @param [Jinx::Resource] obj the domain object to delete
129
132
  # @raise [DatabaseError] if the database operation fails
130
133
  def delete(obj)
131
134
  perform(:delete, obj) { delete_object(obj) }
@@ -133,51 +136,61 @@ module CaRuby
133
136
 
134
137
  # Creates the domain object obj, if necessary.
135
138
  #
136
- # @param [Resource, <Resource>] obj the domain object or collection of domain objects to find or create
139
+ # @param [Jinx::Resource, <Jinx::Resource>] obj the domain object or collection of domain objects to find or create
137
140
  # @raise [ArgumentError] if obj is nil or empty
138
141
  # @raise [DatabaseError] if obj could not be created
139
142
  def ensure_exists(obj)
140
- raise ArgumentError.new("Database ensure_exists is missing a domain object argument") if obj.nil_or_empty?
143
+ Jinx.fail(ArgumentError, "Database ensure_exists is missing a domain object argument") if obj.nil_or_empty?
141
144
  obj.enumerate { |ref| find(ref, :create) unless ref.identifier }
142
145
 
143
146
  end
144
147
 
145
148
  private
146
149
 
147
- # Returns whether there is already the given object operation in progress that is not in the scope of
148
- # an operation performed on a dependent obj owner, i.e. a second obj save operation of the same type
149
- # is only allowed if the obj operation was delegated to an owner save which in turn saves the dependent
150
- # obj.
150
+ # Returns whether the given operation is already in progress on the same object.
151
+ # The operation is only allowed if it is in the scope of a dependent owner
152
+ # operation of the same type.
153
+ #
154
+ # For example, if a StudyEvent is owned by a Study, then the following is allowed:
155
+ # create StudyEvent
156
+ # ...
157
+ # create Study
158
+ # ...
159
+ # create StudyEvent
160
+ #
161
+ # same type on the same dependent is only allowed if the precursor operation was delegated to an owner save which
162
+ # in turn saves the dependent.
151
163
  #
152
- # @param [Resource] obj the domain object to save
164
+ # @param [Jinx::Resource] obj the domain object to save
153
165
  # @param [Symbol] operation the +:create+ or +:update+ save operation
154
166
  # @return [Boolean] whether the save operation is redundant
155
167
  def recursive_save?(obj, operation)
156
168
  @operations.any? { |op| op.type == operation and op.subject == obj } and
157
- not obj.owner_ancestor?(@operations.last.subject)
169
+ not obj.owner_ancestor?(@operations.last.subject) and
170
+ not obj.dependent_update_only?(@operations.last.subject)
158
171
  end
159
172
 
160
- # Creates obj as follows:
173
+ # Creates the given object as follows:
161
174
  # * if obj has an uncreated owner, then store the owner, which in turn will create a physical dependent
162
- # * otherwise, create a storable template. The template is a copy of obj containing a recursive copy
175
+ # * otherwise, create a savable template. The template is a copy of obj containing a recursive copy
163
176
  # of each saved obj reference and resolved independent references
164
177
  # * submit the template to the create application service
165
178
  # * update the obj dependency transitive closure content from the create result
166
179
  # * add a lazy-loader to obj for unfetched domain references
167
180
  #
168
181
  # @param (see #create)
169
- # @return [Resource] obj
182
+ # @return [Jinx::Resource] obj
170
183
  def create_object(obj)
171
184
  # add obj to the transients set
172
185
  @transients << obj
173
186
  begin
174
187
  # A dependent is created by saving the owner.
175
188
  # Otherwise, create the object from a template.
176
- owner = cascaded_owner(obj)
189
+ owner = cascaded_dependent_owner(obj)
177
190
  result = create_dependent(owner, obj) if owner
178
191
  result ||= create_from_template(obj)
179
192
  if result.nil? then
180
- raise DatabaseError.new("#{obj.class.qp} is not creatable in context #{print_operations}")
193
+ Jinx.fail(DatabaseError, "#{obj.class.qp} is not creatable in context #{print_operations}")
181
194
  end
182
195
  ensure
183
196
  # since obj now has an id, removed from transients set
@@ -194,8 +207,8 @@ module CaRuby
194
207
  # A logical dependent is created by its parent unless the parent already exists.
195
208
  # If the logical parent exists, then dep must be created.
196
209
  #
197
- #@param [Resource] dep the dependent domain object to create
198
- # @return [Resource] dep
210
+ # @param [Jinx::Resource] dep the dependent domain object to create
211
+ # @return [Jinx::Resource] dep
199
212
  def create_dependent(owner, dep)
200
213
  if owner.identifier.nil? then
201
214
  logger.debug { "Adding #{owner.qp} dependent #{dep.qp} defaults..." }
@@ -211,22 +224,29 @@ module CaRuby
211
224
 
212
225
  # If there is a saver proxy, then use the proxy.
213
226
  if dep.class.method_defined?(:saver_proxy) then
214
- save_with_proxy(dep)
215
- # remove obj from transients to clear previous fetch, if any
216
- @transients.delete(dep)
217
- logger.debug { "Fetching #{dep.qp} to reflect the proxy save..." }
218
- find(dep)
227
+ saved = save_with_proxy(dep)
228
+ if saved then
229
+ # remove obj from transients to clear previous fetch, if any
230
+ @transients.delete(dep)
231
+ logger.debug { "Fetching #{dep.qp} to reflect the proxy save..." }
232
+ find(dep)
233
+ end
234
+ dep
219
235
  end
220
236
  end
221
237
 
222
238
  # Saves the given domain object using a proxy.
223
239
  #
224
- # @param [Resource] obj the proxied domain object
225
- # @return [Resource] obj
240
+ # @param [Jinx::Resource] obj the proxied domain object
241
+ # @return [Jinx::Resource] obj
226
242
  # @raise [DatabaseError] if obj does not have a proxy
227
243
  def save_with_proxy(obj)
228
244
  proxy = obj.saver_proxy
229
- if proxy.nil? then raise DatabaseError.new("#{obj.class.qp} does not have a proxy") end
245
+ if proxy.nil? then Jinx.fail(DatabaseError, "#{obj.class.qp} does not have a proxy") end
246
+ if recursive_save?(proxy, :create) then
247
+ logger.debug { "Foregoing #{obj.qp} save, since it will be handled by creating the proxy #{proxy}." }
248
+ return
249
+ end
230
250
  logger.debug { "Saving #{obj.qp} by creating the proxy #{proxy}..." }
231
251
  create(proxy)
232
252
  logger.debug { "Created the #{obj.qp} proxy #{proxy}." }
@@ -235,17 +255,18 @@ module CaRuby
235
255
  obj
236
256
  end
237
257
 
238
- # Creates obj by submitting a template to the persistence service. Ensures that the domain
239
- # objects referenced by the created obj exist and are correctly stored.
258
+ # Creates the given domain object by submitting a template to the persistence service.
259
+ # This method ensures that the domain objects referenced by the created object exist
260
+ # and are correctly stored.
240
261
  #
241
262
  # @quirk caCORE submitting the object directly for create runs into various caTissue bizlogic
242
263
  # traps, e.g. Participant CPR is not cascaded but Participant bizlogic checks that each CPR
243
264
  # referenced by Participant is ready to be created. It is treacherous to make assumptions
244
265
  # about what caTissue bizlogic will or will not check. Therefore, the safer strategy is to
245
- # build a template for submission that includes only the object cascaded and direct
246
- # non-cascaded independent references. The independent references are created if necessary.
247
- # The template thus includes only as much content as can safely pass through the caTissue
248
- # bizlogic minefield.
266
+ # build a template for submission that includes only the cascaded and direct non-cascaded
267
+ # independent references. The independent references are created if necessary. The template
268
+ # thus includes only as much content as can safely pass through the caTissue bizlogic
269
+ # minefield.
249
270
  #
250
271
  # @quirk caCORE caCORE create does not update the submitted object to reflect the created
251
272
  # content. The create result is a separate object, which in turn does not always reflect
@@ -295,43 +316,45 @@ module CaRuby
295
316
  def build_create_template(obj)
296
317
  build_save_template(obj, @cr_tmpl_bldr)
297
318
  end
298
- #
319
+
299
320
  # @quirk caCORE application create logic might ignore a non-domain attribute value,
300
- # e.g. the caTissue StorageContainer auto-generated name attribute. In other cases, the application
301
- # always ignores a non-domain attribute value, so the object should not be saved even if it differs
302
- # from the stored result, e.g. the caTissue CollectionProtocolRegistration unsaved
303
- # registration_date. The work-around is to check whether the create result
304
- # differs from the create argument for the auto-generated updatable attributes, and, if so,
305
- # to update the saved object.
306
- #
307
- # This method returns whether the saved obj differs from the stored source for any
308
- # {Attributes#autogenerated_nondomain_attributes}.
309
- #
310
- # @param [Resource] the created domain object
311
- # @param [Resource] the stored database content source domain object
312
- # @return [Boolean] whether obj differs from the source on the the non-domain attributes
321
+ # e.g. the caTissue StorageContainer auto-generated name attribute. In other cases,
322
+ # the application always ignores a non-domain attribute value, e.g. the caTissue
323
+ # CollectionProtocolRegistration registration_date. The work-around is to
324
+ # check whether the create result differs from the create argument for the
325
+ # {Propertied#autogenerated_nondomain_attributes}, and, if so, to perform an update
326
+ # on the save argument.
327
+ #
328
+ # This method returns whether the save result differs from the save source for any
329
+ # {Propertied#autogenerated_nondomain_attributes}.
330
+ #
331
+ # @param [Jinx::Resource] obj the save argument
332
+ # @param [Jinx::Resource] source the save result
333
+ # @return [Boolean] whether the save result differs from the save source on the
334
+ # autogenerated non-domain attributes
313
335
  def update_saved?(obj, source)
314
- obj.class.autogenerated_nondomain_attributes.any? do |attr|
315
- intended = obj.send(attr)
316
- stored = source.send(attr)
336
+ obj.class.autogenerated_nondomain_attributes.any? do |pa|
337
+ intended = obj.send(pa)
338
+ stored = source.send(pa)
317
339
  if intended != stored then
318
- logger.debug { "Saved #{obj.qp} #{attr} value #{intended} differs from result value #{stored}..." }
340
+ logger.debug { "Saved #{obj.qp} #{pa} value #{intended} differs from result value #{stored}..." }
341
+ true
319
342
  end
320
343
  end
321
344
  end
322
345
 
323
- # Returns the {MetadataAttributes#creatable_domain_attributes} which are not contravened by a
324
- # one-to-one independent pending create.
346
+ # Returns the {MetadataPropertied#creatable_domain_attributes} which are not contravened
347
+ # by a one-to-one independent pending create.
325
348
  #
326
349
  # @param (see #create)
327
350
  # @return [<Symbol>] the attributes to include in the create template
328
351
  def creatable_domain_attributes(obj)
329
352
  # filter the creatable attributes
330
- obj.class.creatable_domain_attributes.compose do |attr_md|
331
- if exclude_pending_create_attribute?(obj, attr_md) then
353
+ obj.class.creatable_domain_attributes.compose do |pa|
354
+ if exclude_pending_create_attribute?(obj, pa) then
332
355
  # Avoid printing duplicate log message.
333
356
  if obj != @cr_dom_attr_log_obj then
334
- logger.debug { "Excluded #{obj.qp} #{attr_md} in the create template since it references a 1:1 bidirectional independent pending create." }
357
+ logger.debug { "Excluded #{obj.qp} #{pa} in the create template since it references a 1:1 bidirectional independent pending create." }
335
358
  @cr_dom_attr_log_obj = obj
336
359
  end
337
360
  false
@@ -343,24 +366,24 @@ module CaRuby
343
366
 
344
367
  # Returns whether the given creatable domain attribute with value obj satisfies
345
368
  # each of the following conditions:
346
- # * the attribute is {Attribute#independent?}
347
- # * the attribute is not an {Attribute#owner?}
369
+ # * the attribute is {Property#independent?}
370
+ # * the attribute is not an {Property#owner?}
348
371
  # * the obj value is unsaved
349
372
  # * the attribute is not mandatory
350
373
  # * the attribute references a {#pending_create?} save context.
351
374
  #
352
375
  # @param obj (see #create)
353
- # @param [Attribute] attr_md the candidate attribute metadata
376
+ # @param [Property] pa the candidate attribute metadata
354
377
  # @return [Boolean] whether the attribute should not be included in the create template
355
- def exclude_pending_create_attribute?(obj, attr_md)
356
- attr_md.independent? and
357
- not attr_md.owner? and
378
+ def exclude_pending_create_attribute?(obj, pa)
379
+ pa.independent? and
380
+ not pa.owner? and
358
381
  obj.identifier.nil? and
359
- not obj.mandatory_attributes.include?(attr_md.to_sym) and
360
- exclude_pending_create_value?(obj.send(attr_md.to_sym))
382
+ not obj.mandatory_attributes.include?(pa.to_sym) and
383
+ exclude_pending_create_value?(obj.send(pa.to_sym))
361
384
  end
362
385
 
363
- # @param [Resource, <Resource>, nil] value the referenced value
386
+ # @param [Jinx::Resource, <Jinx::Resource>, nil] value the referenced value
364
387
  # @return [Boolean] whether the value includes a {#pending_create?} save context object
365
388
  def exclude_pending_create_value?(value)
366
389
  return false if value.nil?
@@ -371,7 +394,7 @@ module CaRuby
371
394
  end
372
395
  end
373
396
 
374
- # @param [Resource] obj the object to check
397
+ # @param [Jinx::Resource] obj the object to check
375
398
  # @return [Boolean] whether the penultimate create operation is on the object
376
399
  def pending_create?(obj)
377
400
  op = penultimate_create_operation
@@ -384,7 +407,7 @@ module CaRuby
384
407
  nil
385
408
  end
386
409
 
387
- # Returns the {MetadataAttributes#updatable_domain_attributes}.
410
+ # Returns the {MetadataPropertied#updatable_domain_attributes}.
388
411
  #
389
412
  # @param (see #update)
390
413
  # @return the attributes to include in the update template
@@ -396,49 +419,39 @@ module CaRuby
396
419
  def update_object(obj)
397
420
  # database identifier is required for update
398
421
  if obj.identifier.nil? then
399
- raise DatabaseError.new("Update target is missing a database identifier: #{obj}")
422
+ Jinx.fail(DatabaseError, "Update target is missing a database identifier: #{obj}")
400
423
  end
401
424
 
402
- # if this object is proxied, then delegate to the proxy
425
+ # If this object is proxied, then delegate to the proxy.
403
426
  if obj.class.method_defined?(:saver_proxy) then
404
427
  return save_with_proxy(obj)
405
428
  end
406
429
 
407
- # if a changed dependent is saved with a proxy, then update that dependent first
430
+ # If a changed dependent is saved with a proxy, then update that dependent first.
408
431
  proxied = updatable_proxied_dependents(obj)
409
432
  unless proxied.empty? then
410
433
  proxied.each { |dep| update(dep) }
411
434
  end
412
435
 
413
- # update a cascaded dependent by updating the owner
414
- owner = cascaded_owner(obj)
436
+ # Update a cascaded dependent by updating the owner.
437
+ owner = cascaded_dependent_owner(obj)
415
438
  if owner then return update_cascaded_dependent(owner, obj) end
416
-
417
- # Not cascaded dependent; update using a template,
418
- tmpl = build_update_template(obj)
419
- # call the caCORE service with an obj update template
420
- save_with_template(obj, tmpl) { |svc| svc.update(tmpl) }
421
- # take a snapshot of the updated content
422
- obj.take_snapshot
439
+ # Not a cascaded dependent; update using a template.
440
+ update_from_template(obj)
423
441
  end
424
442
 
425
- # Returns the owner that can cascade update to the given object.
426
- # The owner is the #{Resource#effective_owner_attribute} value
427
- # for which the owner attribute {Attribute#inverse_metadata}
428
- # is {Attribute#cascaded?}.
443
+ # Returns the owner that can cascade update to the given object. The owner is the
444
+ # {Jinx::Resource#effective_owner_attribute} value for which the owner attribute
445
+ # {Property#inverse_property} is {Property#cascaded?}.
429
446
  #
430
- # @param [Resource] obj the domain object to update
431
- # @return [Resource, nil] the owner which can cascade an update to the object, or nil if none
447
+ # @param [Jinx::Resource] obj the domain object to update
448
+ # @return [Jinx::Resource, nil] the owner which can cascade an update to the object, or nil if none
432
449
  # @raise [DatabaseError] if the domain object is a cascaded dependent but does not have an owner
433
- def cascaded_owner(obj)
434
- return unless obj.class.cascaded_dependent?
435
- # the owner attribute
436
- oattr = obj.effective_owner_attribute
437
- if oattr.nil? then raise DatabaseError.new("Dependent #{obj} does not have an owner") end
438
- dep_md = obj.class.attribute_metadata(oattr).inverse_metadata
439
- if dep_md and dep_md.cascaded? then
440
- obj.send(oattr)
441
- end
450
+ def cascaded_dependent_owner(obj)
451
+ # the owner attribute and value
452
+ op, oref = obj.effective_owner_property_value || return
453
+ dp = op.inverse_property
454
+ oref if dp and dp.cascaded?
442
455
  end
443
456
 
444
457
  def update_cascaded_dependent(owner, obj)
@@ -477,14 +490,14 @@ module CaRuby
477
490
  # must be created via the proxy after the Specimen is created.
478
491
  #
479
492
  # @param (see #update)
480
- # @return [<Resource>] the #{Attributes#proxied_savable_template_attributes} dependents
493
+ # @return [<Jinx::Resource>] the #{Propertied#proxied_savable_template_attributes} dependents
481
494
  # which are #{Persistable#changed?}
482
495
  def updatable_proxied_dependents(obj)
483
- attrs = obj.class.proxied_savable_template_attributes
484
- return Array::EMPTY_ARRAY if attrs.empty?
496
+ pas = obj.class.proxied_savable_template_attributes
497
+ return Array::EMPTY_ARRAY if pas.empty?
485
498
  deps = []
486
- attrs.each do |attr|
487
- obj.send(attr).enumerate { |dep| deps << dep if dep.identifier and dep.changed? }
499
+ pas.each do |pa|
500
+ obj.send(pa).enumerate { |dep| deps << dep if dep.identifier and dep.changed? }
488
501
  end
489
502
  deps
490
503
  end
@@ -496,8 +509,8 @@ module CaRuby
496
509
  end
497
510
 
498
511
  # @param obj (see #save)
499
- # @param [StoreTemplateBuilder] builder the builder to use
500
- # @return [Resource] the template to use as the save argument
512
+ # @param [TemplateBuilder] builder the builder to use
513
+ # @return [Jinx::Resource] the template to use as the save argument
501
514
  def build_save_template(obj, builder)
502
515
  builder.build_template(obj)
503
516
  end
@@ -507,7 +520,7 @@ module CaRuby
507
520
  def delete_object(obj)
508
521
  # database identifier is required for delete
509
522
  if obj.identifier.nil? then
510
- raise DatabaseError.new("Delete target is missing a database identifier: #{obj}")
523
+ Jinx.fail(DatabaseError, "Delete target is missing a database identifier: #{obj}")
511
524
  end
512
525
  persistence_service(obj.class).delete_object(obj)
513
526
  end
@@ -518,7 +531,7 @@ module CaRuby
518
531
  # create method is called on the template. Dependents are saved as well, if necessary.
519
532
  #
520
533
  # @param obj (see #store)
521
- # @param [Resource] template the obj template to submit to caCORE
534
+ # @param [Jinx::Resource] template the obj template to submit to caCORE
522
535
  def save_with_template(obj, template)
523
536
  logger.debug { "Saving #{obj.qp} from template:\n#{template.dump}" }
524
537
  # dispatch to the application service
@@ -537,15 +550,16 @@ module CaRuby
537
550
  result
538
551
  end
539
552
 
540
- # Synchronizes the content of the given saved domain object and the save result source as follows:
541
- # 1. The save result source is first synchronized with the database content as necessary.
542
- # 2. Then the source is merged into the target.
543
- # 3. If the target must be resaved based on the call to {#update_saved?}, then the source
544
- # result is resaved.
545
- # 4. Each target dependent which differs from the corresponding source dependent is saved.
546
- #
547
- # @param [Resource] target the saved domain object
548
- # @param [Resource] source the caCORE save result
553
+ # Synchronizes the content of the given saved argument and result as follows:
554
+ # 1. The save result is first synchronized with the database content as necessary.
555
+ # 2. Then the result is merged into the argument.
556
+ # 3. If the argument must be resaved based on the call to {#update_saved?}, then the
557
+ # argument is resaved.
558
+ # 4. Each argument dependent which differs from the corresponding result dependent
559
+ # is saved.
560
+ #
561
+ # @param [Jinx::Resource] target the save argument
562
+ # @param [Jinx::Resource] source the caCORE save result
549
563
  def sync_saved(target, source)
550
564
  # clear the toxic source attributes
551
565
  detoxify(source)
@@ -564,7 +578,7 @@ module CaRuby
564
578
  save_changed_dependents(target)
565
579
  end
566
580
 
567
- # Synchronizes the given saved target result source with the database content.
581
+ # Synchronizes the given save result with the database content.
568
582
  # The source is synchronized by {#sync_save_result}.
569
583
  #
570
584
  # @param (see #sync_saved)
@@ -583,7 +597,7 @@ module CaRuby
583
597
  # the database, the difference induces an update of the reference.
584
598
  #
585
599
  # @param (see #sync_saved)
586
- # @return [Resource] the merged target object
600
+ # @return [Jinx::Resource] the merged target object
587
601
  def merge_saved(target, source)
588
602
  logger.debug { "Merging saved result #{source} into saved #{target.qp}..." }
589
603
  # Update each saved reference snapshot to reflect the database state and add a lazy loader
@@ -612,33 +626,48 @@ module CaRuby
612
626
  return if source == target
613
627
  # If the target was created, then refetch and merge the source if necessary to reflect auto-generated
614
628
  # non-domain attribute values.
615
- if target.identifier.nil? then sync_created_result_object(source) end
629
+ if target.identifier.nil? then sync_created_result_object(target, source) end
616
630
  # If there are auto-generated attributes, then merge them into the save result.
617
631
  sync_save_result_references(source, target)
618
632
  # Set inverses consistently in the source object graph
619
633
  set_inverses(source)
620
634
  end
621
635
 
622
- # Refetches the given create result source if there are any {Attributes#autogenerated_nondomain_attributes}
636
+ # Refetches the given create result if there are any {#autogenerated_nondomain_attributes}
623
637
  # which must be fetched to reflect the database state.
624
638
  #
625
- # @param source (see #sync_saved)
626
- def sync_created_result_object(source)
627
- attrs = source.class.autogenerated_nondomain_attributes
628
- return if attrs.empty?
629
- logger.debug { "Refetch #{source} to reflect auto-generated database content for attributes #{attrs.to_series}..." }
639
+ # @param (see #sync_save_result)
640
+ def sync_created_result_object(target, source)
641
+ pas = nondomain_sync_attributes(target, source)
642
+ return if pas.empty?
643
+ logger.debug { "Refetch #{source} to reflect auto-generated database content for attributes #{pas.to_series}..." }
630
644
  find(source)
631
645
  end
632
646
 
633
- # Fetches the {#synchronization_attributes} into the given target save result source.
647
+ # Returns the attributes which must be fetched into the given source prior to a merge with
648
+ # the save argument target. This default implementation returns the subject class's
649
+ # {Propertied#autogenerated_nondomain_attributes} if the save is a create, the empty array
650
+ # otherwise. Subclasses can specialize this method for any special cases that depend on the
651
+ # instance state.
652
+ #
653
+ # @return [<Symbol>] the attributes which must be fetched into the source
654
+ def nondomain_sync_attributes(target, source)
655
+ if target.identifier.nil? then
656
+ source.class.autogenerated_nondomain_attributes
657
+ else
658
+ Array::EMPTY_ARRAY
659
+ end
660
+ end
661
+
662
+ # Fetches the {#synchronization_attributes} into the given save result.
634
663
  #
635
664
  # @param (see #sync_saved)
636
665
  def sync_save_result_references(source, target)
637
- attrs = synchronization_attributes(source, target)
638
- return if attrs.empty?
639
- logger.debug { "Fetching the saved #{target.qp} attributes #{attrs.to_series} into save result #{source.qp}..." }
640
- attrs.each { |attr| sync_save_result_attribute(source, attr) }
641
- logger.debug { "Fetched the saved #{target.qp} attributes #{attrs.to_series} into the save result #{source.qp}." }
666
+ pas = synchronization_attributes(source, target)
667
+ return if pas.empty?
668
+ logger.debug { "Fetching the saved #{target.qp} attributes #{pas.to_series} into the save result #{source.qp}..." }
669
+ pas.each { |pa| sync_save_result_attribute(source, pa) }
670
+ logger.debug { "Fetched the saved #{target.qp} attributes #{pas.to_series} into the save result #{source.qp}." }
642
671
  end
643
672
 
644
673
  # @see #sync_save_result_references
@@ -646,13 +675,13 @@ module CaRuby
646
675
  # fetch the value
647
676
  fetched = fetch_association(source, attribute)
648
677
  # set the attribute
649
- source.set_attribute(attribute, fetched)
678
+ source.set_property_value(attribute, fetched)
650
679
  end
651
680
 
652
- # Returns the saved target attributes which must be fetched to reflect the database content, consisting
653
- # of the following:
654
- # * {Persistable#saved_fetch_attributes}
655
- # * {Attributes#domain_attributes} which include a source reference without an identifier
681
+ # Returns the saved target attributes which must be fetched to reflect the database content,
682
+ # consisting of the following:
683
+ # * {Persistable#saved_attributes_to_fetch}
684
+ # * {Propertied#domain_attributes} which include a source reference without an identifier
656
685
  #
657
686
  # @param (see #sync_saved)
658
687
  # @return [<Symbol>] the attributes which must be fetched
@@ -660,63 +689,69 @@ module CaRuby
660
689
  # the target save operation
661
690
  op = @operations.last
662
691
  # the attributes to fetch
663
- attrs = target.saved_fetch_attributes(op).to_set
692
+ pas = target.saved_attributes_to_fetch(op).to_set
664
693
  # the pending create, if any
665
694
  pndg_op = penultimate_create_operation
666
695
  pndg = pndg_op.subject if pndg_op
667
696
  # add in the domain attributes whose identifier was not set in the result
668
- source.class.saved_domain_attributes.select do |attr|
669
- srcval = source.send(attr)
670
- tgtval = target.send(attr)
697
+ source.class.sync_domain_attributes.select do |pa|
698
+ srcval = source.send(pa)
699
+ tgtval = target.send(pa)
671
700
  if Persistable.unsaved?(srcval) then
672
- logger.debug { "Fetching save result #{source.qp} #{attr} since a referenced object identifier was not set in the result..." }
673
- attrs << attr
701
+ logger.debug { "Fetching the save result #{source.qp} #{pa} since a referenced object identifier was not set in the result..." }
702
+ pas << pa
674
703
  elsif srcval.nil_or_empty? and Persistable.unsaved?(tgtval) and tgtval != pndg then
675
- logger.debug { "Fetching save result #{source.qp} #{attr} since the target #{target.qp} value #{tgtval.qp} is missing an identifier..." }
676
- attrs << attr
704
+ logger.debug { "Fetching the save result #{source.qp} #{pa} since the target #{target.qp} value #{tgtval.qp} is missing an identifier..." }
705
+ pas << pa
677
706
  end
678
707
  end
679
- attrs
708
+ pas
680
709
  end
681
710
 
682
711
  # Saves the given domain object dependents.
683
712
  #
684
- # @param [Resource] obj the owner domain object
713
+ # @param [Jinx::Resource] obj the owner domain object
685
714
  def save_changed_dependents(obj)
686
- obj.class.dependent_attributes.each do |attr|
687
- deps = obj.send(attr)
688
- logger.debug { "Saving the #{obj} #{attr} dependents #{deps.to_enum.qp(:single_line)} which have changed..." } unless deps.nil_or_empty?
689
- deps.enumerate { |dep| save_dependent_if_changed(obj, attr, dep) }
715
+ obj.class.dependent_attributes.each_property do |prop|
716
+ deps = obj.dependents(prop)
717
+ unless deps.nil_or_empty? then
718
+ logger.debug { "Saving the #{obj} dependents #{deps.to_enum.qp(:single_line)} which have changed..." }
719
+ end
720
+ deps.enumerate { |dep| save_dependent_if_changed(obj, prop, dep) }
690
721
  end
691
722
  end
692
723
 
693
724
  # Saves the given dependent domain object if necessary.
694
725
  # Recursively saves the obj dependents as necessary.
695
726
  #
696
- # @param [Resource] owner the dependent owner
697
- # @param [Symbol] attribute the dependent attribute
698
- # @param [Resource] dependent the dependent to save
699
- def save_dependent_if_changed(owner, attribute, dependent)
727
+ # @param [Jinx::Resource] owner the dependent owner
728
+ # @param [Property] property the dependent property
729
+ # @param [Jinx::Resource] dependent the dependent to save
730
+ def save_dependent_if_changed(owner, property, dependent)
731
+ # If the dependent identifier is missing, then the owner save did not cascade to the
732
+ # dependent. Create the dependent here.
700
733
  if dependent.identifier.nil? then
701
- logger.debug { "Creating #{owner.qp} #{attribute} dependent #{dependent.qp}..." }
734
+ logger.debug { "Creating #{owner.qp} #{property} dependent #{dependent.qp}..." }
702
735
  return create(dependent)
703
736
  end
737
+ # Collect the changed attributes.
704
738
  changes = dependent.changed_attributes
705
- # If the dependent identifier is missing, then the identifier was probably created on demand.
706
- logger.debug { "#{owner.qp} #{attribute} dependent #{dependent.qp} changed for attributes #{changes.to_series}." } unless changes.empty?
707
- if changes.any? { |attr| not dependent.class.attribute_metadata(attr).dependent? } then
739
+ logger.debug { "#{owner.qp} #{property} dependent #{dependent.qp} changed for attributes #{changes.to_series}." } unless changes.empty?
740
+ # If any changed attribute is a reference to dependents, then check for
741
+ # special auto-generation handling. Otherwise, simpl save the referenced
742
+ # dependents.
743
+ if changes.any? { |pa| not dependent.class.property(pa).dependent? } then
708
744
  # the owner save operation
709
745
  op = operations.last
710
746
  # The dependent is auto-generated if the owner was created or auto-generated and
711
747
  # the dependent attribute is auto-generated.
712
- attr_md = owner.class.attribute_metadata(attribute)
713
- ag = (op.type == :create or op.autogenerated?) && attr_md.autogenerated?
714
- logger.debug { "Updating the changed #{owner.qp} #{attribute} dependent #{dependent.qp}..." }
748
+ ag = (op.type == :create or op.autogenerated?) && property.autogenerated?
749
+ logger.debug { "Updating the changed #{owner.qp} #{property} dependent #{dependent.qp}..." }
715
750
  if ag then
716
- logger.debug { "Adding defaults to the auto-generated #{owner.qp} #{attribute} dependent #{dependent.qp}..." }
751
+ logger.debug { "Adding defaults to the auto-generated #{owner.qp} #{property} dependent #{dependent.qp}..." }
717
752
  dependent.add_defaults_autogenerated
718
753
  end
719
- update_changed_dependent(owner, attribute, dependent, ag)
754
+ update_changed_dependent(owner, property, dependent, ag)
720
755
  else
721
756
  save_changed_dependents(dependent)
722
757
  end
@@ -725,7 +760,8 @@ module CaRuby
725
760
  # Updates the given dependent.
726
761
  #
727
762
  # @param (see #save_dependent_if_changed)
728
- def update_changed_dependent(owner, attribute, dependent, autogenerated)
763
+ # @param [Boolean] autogenerated flag indicating whether the dependent was auto-generated
764
+ def update_changed_dependent(owner, property, dependent, autogenerated)
729
765
  perform(:update, dependent, :autogenerated => autogenerated) { update_object(dependent) }
730
766
  end
731
767
  end