caruby-core 1.4.2 → 1.4.3

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 (52) hide show
  1. data/History.txt +10 -0
  2. data/lib/caruby/cli/command.rb +10 -8
  3. data/lib/caruby/database/fetched_matcher.rb +28 -39
  4. data/lib/caruby/database/lazy_loader.rb +101 -0
  5. data/lib/caruby/database/persistable.rb +190 -167
  6. data/lib/caruby/database/persistence_service.rb +21 -7
  7. data/lib/caruby/database/persistifier.rb +185 -0
  8. data/lib/caruby/database/reader.rb +106 -176
  9. data/lib/caruby/database/saved_matcher.rb +56 -0
  10. data/lib/caruby/database/search_template_builder.rb +1 -1
  11. data/lib/caruby/database/sql_executor.rb +8 -7
  12. data/lib/caruby/database/store_template_builder.rb +134 -61
  13. data/lib/caruby/database/writer.rb +252 -52
  14. data/lib/caruby/database.rb +88 -67
  15. data/lib/caruby/domain/attribute_initializer.rb +16 -0
  16. data/lib/caruby/domain/attribute_metadata.rb +161 -72
  17. data/lib/caruby/domain/id_alias.rb +22 -0
  18. data/lib/caruby/domain/inversible.rb +91 -0
  19. data/lib/caruby/domain/merge.rb +116 -35
  20. data/lib/caruby/domain/properties.rb +1 -1
  21. data/lib/caruby/domain/reference_visitor.rb +207 -71
  22. data/lib/caruby/domain/resource_attributes.rb +93 -80
  23. data/lib/caruby/domain/resource_dependency.rb +22 -97
  24. data/lib/caruby/domain/resource_introspection.rb +21 -28
  25. data/lib/caruby/domain/resource_inverse.rb +134 -0
  26. data/lib/caruby/domain/resource_metadata.rb +41 -19
  27. data/lib/caruby/domain/resource_module.rb +42 -33
  28. data/lib/caruby/import/java.rb +8 -9
  29. data/lib/caruby/migration/migrator.rb +20 -7
  30. data/lib/caruby/migration/resource_module.rb +0 -2
  31. data/lib/caruby/resource.rb +132 -351
  32. data/lib/caruby/util/cache.rb +4 -1
  33. data/lib/caruby/util/class.rb +48 -1
  34. data/lib/caruby/util/collection.rb +54 -18
  35. data/lib/caruby/util/inflector.rb +7 -0
  36. data/lib/caruby/util/options.rb +35 -31
  37. data/lib/caruby/util/partial_order.rb +1 -1
  38. data/lib/caruby/util/properties.rb +2 -2
  39. data/lib/caruby/util/stopwatch.rb +16 -8
  40. data/lib/caruby/util/transitive_closure.rb +1 -1
  41. data/lib/caruby/util/visitor.rb +342 -328
  42. data/lib/caruby/version.rb +1 -1
  43. data/lib/caruby/yard/resource_metadata_handler.rb +8 -0
  44. data/lib/caruby.rb +2 -0
  45. metadata +10 -9
  46. data/lib/caruby/database/saved_merger.rb +0 -131
  47. data/lib/caruby/domain/annotatable.rb +0 -25
  48. data/lib/caruby/domain/annotation.rb +0 -23
  49. data/lib/caruby/import/annotatable_class.rb +0 -28
  50. data/lib/caruby/import/annotation_class.rb +0 -27
  51. data/lib/caruby/import/annotation_module.rb +0 -67
  52. data/lib/caruby/migration/resource.rb +0 -8
@@ -0,0 +1,56 @@
1
+ require 'caruby/database/fetched_matcher'
2
+
3
+ module CaRuby
4
+ class Database
5
+ # Proc that matches saved result sources to targets.
6
+ class SavedMatcher < FetchedMatcher
7
+ # Initializes a new SavedMatcher.
8
+ def initialize
9
+ super
10
+ end
11
+
12
+ private
13
+
14
+ # Returns a target => source match hash for the given targets and sources.
15
+ #
16
+ # @param (see FetchedMatcher#initialize)
17
+ # @return (see FetchedMatcher#initialize)
18
+ def match_fetched(sources, targets)
19
+ # match source => target based on the key
20
+ matches = super
21
+ # match residual targets, if any, on a relaxed criterion
22
+ if matches.size != targets.size and not sources.empty? then
23
+ match_fetched_residual(sources, targets, matches)
24
+ end
25
+ matches
26
+ end
27
+
28
+ # Adds to the given target => source matches hash for the unmatched targets and sources
29
+ # using {#match_minimal}.
30
+ #
31
+ # @param sources (see #match_fetched)
32
+ # @param targets (see #match_fetched)
33
+ # @param [{Resource => Resource}] the source => target matches so far
34
+ def match_fetched_residual(sources, targets, matches)
35
+ unmtchd_tgts = targets.to_set - matches.keys.delete_if { |tgt| tgt.identifier }
36
+ unmtchd_srcs = sources.to_set - matches.values
37
+ min_mtchs = match_minimal(unmtchd_srcs, unmtchd_tgts)
38
+ matches.merge!(min_mtchs)
39
+ end
40
+
41
+ #@param [<Resource>] sources the source objects to match
42
+ #@param [<Resource>] targets the potential match target objects
43
+ # @return (see #match_saved)
44
+ def match_minimal(sources, targets)
45
+ matches = {}
46
+ unmatched = Set === sources ? sources.to_set : sources.dup
47
+ targets.each do |tgt|
48
+ src = unmatched.detect { |src| tgt.minimal_match?(src) } || next
49
+ matches[src] = tgt
50
+ unmatched.delete(src)
51
+ end
52
+ matches
53
+ end
54
+ end
55
+ end
56
+ end
@@ -31,7 +31,7 @@ module CaRuby
31
31
  # the searchable attribute => value hash
32
32
  ref_hash, nonref_hash = hash.hash_partition { |attr, value| Resource === value }
33
33
  # make the search template from the non-reference attributes
34
- tmpl = obj.class.new(nonref_hash)
34
+ tmpl = obj.class.new.merge_attributes(nonref_hash)
35
35
  # get references for the search template
36
36
  unless ref_hash.empty? then
37
37
  logger.debug { "Collecting search reference parameters for #{obj.qp} from attributes #{ref_hash.keys.to_series}..." }
@@ -42,7 +42,8 @@ module CaRuby
42
42
  end
43
43
 
44
44
  # Connects to the database, yields the DBI handle to the given block and disconnects.
45
- # Returns the execution result.
45
+ #
46
+ # @return [Array] the execution result
46
47
  def execute
47
48
  logger.debug { "Connecting to database with user #{@username}, address #{@address}..." }
48
49
  result = DBI.connect(@address, @username, @password, "driver"=>"com.mysql.jdbc.Driver") { |dbh| yield dbh }
@@ -54,17 +55,17 @@ module CaRuby
54
55
 
55
56
  def default_driver_string(db_type)
56
57
  case db_type.downcase
57
- when 'mysql' then 'jdbc:mysql'
58
- when 'oracle' then 'Oracle'
59
- else raise CaRuby::ConfigurationError.new("Default database connection driver string could not be determined for database type #{db_type}")
58
+ when 'mysql' then 'jdbc:mysql'
59
+ when 'oracle' then 'Oracle'
60
+ else raise CaRuby::ConfigurationError.new("Default database connection driver string could not be determined for database type #{db_type}")
60
61
  end
61
62
  end
62
63
 
63
64
  def default_port(db_type)
64
65
  case db_type.downcase
65
- when 'mysql' then 3306
66
- when 'oracle' then 1521
67
- else raise CaRuby::ConfigurationError.new("Default database connection port could not be determined for database type #{db_type}")
66
+ when 'mysql' then 3306
67
+ when 'oracle' then 1521
68
+ else raise CaRuby::ConfigurationError.new("Default database connection port could not be determined for database type #{db_type}")
68
69
  end
69
70
  end
70
71
 
@@ -3,17 +3,29 @@ require 'caruby/domain/reference_visitor'
3
3
  module CaRuby
4
4
  # StoreTemplateBuilder creates a template suitable for a create or update database operation.
5
5
  class StoreTemplateBuilder
6
- # Creates a new StoreTemplateBuilder for the given database.
6
+ # Creates a new StoreTemplateBuilder for the given database. The attributes to merge into
7
+ # the template are determined by the block given to this initializer, filtered as follows:
8
+ # * If the save operation is a create, then exclude the auto-generated attributes.
9
+ # * If the visited object has an identifier, then include only those attributes
10
+ # which {AttributeMetadata#cascade_update_to_create?} or have an identifier.
7
11
  #
8
12
  # @param [Database] database the target database
9
- # @yield [ref] the required selector block which determines the attributes copied into the template
13
+ # @yield [ref] the required selector block which determines which attributes are copied into the template
10
14
  # @yieldparam [Resource] ref the domain object to copy
11
15
  def initialize(database)
12
16
  @database = database
13
- unless block_given? then raise ArgumentError.new("StoreTemplateBuilder is missing the required template copy attribute selector block") end
14
- # the domain attributes to copy
15
- mgbl = Proc.new { |src, tgt| yield src }
16
- # copy the reference
17
+ unless block_given? then
18
+ raise ArgumentError.new("StoreTemplateBuilder is missing the required template copy attribute selector block")
19
+ end
20
+
21
+ # the mergeable attributes filter the given block with exclusions
22
+ @mergeable = Proc.new { |obj| mergeable_attributes(obj, yield(obj)) }
23
+ # the storable prerequisite reference visitor
24
+ @prereq_vstr = ReferenceVisitor.new(:prune_cycle) { |ref| savable_cascaded_attributes(ref) }
25
+
26
+ # the savable attributes filter the given block with exclusions
27
+ savable = Proc.new { |obj| savable_attributes(obj, yield(obj)) }
28
+ # the domain attributes to copy is determined by the constructor caller
17
29
  # caTissue alert - must copy all of the non-domain attributes rather than just the identifier,
18
30
  # since caTissue auto-generated Specimen update requires the parent collection status. This
19
31
  # is the only known occurrence of a referenced object required non-identifier attribute.
@@ -24,10 +36,8 @@ module CaRuby
24
36
  copy_proxied_save_references(src, copy)
25
37
  copy
26
38
  end
27
- # the template copier
28
- @copy_vstr = CopyVisitor.new(:mergeable => mgbl, :copier => copier) { |src, tgt| savable_cascaded_attributes(src) }
29
- # the storable prerequisite reference visitor
30
- @prereq_vstr = ReferenceVisitor.new(:prune_cycle) { |ref| savable_cascaded_attributes(ref) }
39
+ # the template copier
40
+ @copy_vstr = CopyVisitor.new(:mergeable => savable, :copier => copier) { |ref| savable_cascaded_attributes(ref) }
31
41
  end
32
42
 
33
43
  # Returns a new domain object which serves as the argument for obj create or update.
@@ -40,16 +50,14 @@ module CaRuby
40
50
  # caCORE alert - +caCORE+ expects the store argument to be carefully prepared prior to
41
51
  # the create or update. build_storable_template culls the target object with a template
42
52
  # which includes only those references which are necessary for the store to succeed.
43
- # This method ensures that mandatory independent references exist. Dependent references
44
- # are included in the template but are not created before submission to +caCORE+ create
45
- # or update. These fine distinctions are implicit application rules which are explicated
46
- # in the +caRuby+ application domain class definition using ResourceMetadata methods.
47
- #
48
- # caCORE alert - +caCORE+ occasionally induces a stack overflow if a create argument
49
- # contains a reference cycle. The template fixes this.
53
+ # This template builder ensures that mandatory independent references exist. Cascaded
54
+ # dependent references are included in the template but are not created before submission
55
+ # to +caCORE+. These reference attribute distinctions are implicit application rules which
56
+ # are explicated in the +caRuby+ application domain class definition using ResourceMetadata
57
+ # methods.
50
58
  #
51
- # caCORE alert - +caCORE+ create raises an error if a create argument directly or
52
- # indirectly references a domain objects without an identifier, even if the
59
+ # caCORE alert - +caCORE+ create issues an error if a create argument directly or
60
+ # indirectly references a non-cascaded domain object without an identifier, even if the
53
61
  # reference is not relevant to the create. The template returned by this method elides
54
62
  # all non-essential references.
55
63
  #
@@ -67,7 +75,9 @@ module CaRuby
67
75
  #
68
76
  # @param [Resource] obj the domain object to save
69
77
  # @return [Resource] the template to use as the caCORE argument
70
- def build_template(obj)
78
+ def build_template(obj, autogenerated=false)
79
+ # set the database operation subject
80
+ @subject = obj
71
81
  # prepare the object for a store operation
72
82
  ensure_storable(obj)
73
83
  # copy the cascade hierarchy
@@ -87,7 +97,8 @@ module CaRuby
87
97
  # java.lang.IllegalArgumentException: id to load is required for loading
88
98
  # The server log stack trace indicates a bizlogic line that offers a clue to the offending reference.
89
99
  def ensure_storable(obj)
90
- # Add defaults, which might introduce independent references.
100
+ # Add defaults, which might introduce independent references. Enable the lazy loader to fetch
101
+ # create references from the database where needed to build defaults.
91
102
  obj.add_defaults
92
103
  # create the prerequisite references if necessary
93
104
  prereqs = collect_prerequisites(obj)
@@ -96,42 +107,83 @@ module CaRuby
96
107
  @database.ensure_exists(prereqs)
97
108
  logger.debug { "Prerequisite references for #{obj.qp} exist: #{prereqs.map { |ref| ref }.to_series}." }
98
109
  end
99
- # If obj is being created then add defaults again, since fetched independent references might introduce new defaults.
100
- obj.add_defaults unless obj.identifier
101
110
  # Verify that the object is complete
102
111
  obj.validate
103
112
  end
104
-
105
- # Filters the {ResourceAttributes#updatable_domain_attributes} to exclude
106
- # the {ResourceAttributes#proxied_cascaded_attributes} whose reference value
107
- # does not have an identifier. These references will be created by proxy
108
- # instead.
113
+
114
+ # Returns the attributes to visit in building the template for the given
115
+ # domain object. The visitable attributes consist of the following:
116
+ # * The {ResourceAttributes#unproxied_cascaded_attributes} filtered as follows:
117
+ # * If the database operation is a create, then exclude the cascaded attributes.
118
+ # * If the given object has an identifier, then exclude the attributes which
119
+ # have the the :no_cascade_update_to_create flag set.
120
+ # * The {ResourceAttributes#proxied_cascaded_attributes} are included if and
121
+ # only if every referenced object has an identifier, and therefore does not
122
+ # need to be proxied.
123
+ #
124
+ # caTissue alert - caTissue ignores some references, e.g. Participant CPR, and auto-generates
125
+ # the values instead. Therefore, the create template builder excludes these auto-generated
126
+ # attributes. After the create, the auto-generated references are merged into the created
127
+ # object graph and the references are updated if necessary.
109
128
  #
110
129
  # @param [Resource] obj the domain object copied to the update template
111
130
  # @return [<Symbol>] the reference attributes to include in the update template
112
131
  def savable_cascaded_attributes(obj)
113
- # always include the unproxied cascaded references
114
- unproxied = obj.class.unproxied_cascaded_attributes
115
- if obj.identifier then
116
- unproxied = unproxied.filter do |attr|
117
- obj.class.attribute_metadata(attr).cascade_update_to_create? or
118
- obj.send(attr).to_enum.all? { |ref| ref.identifier }
119
- end
132
+ # The starting set of candidate attributes is the unproxied cascaded references.
133
+ unproxied = savable_attributes(obj, obj.class.unproxied_cascaded_attributes)
134
+ # The proxied attributes to save.
135
+ proxied = savable_proxied_attributes(obj)
136
+ # The combined set of savable attributes
137
+ proxied.empty? ? unproxied : unproxied + proxied
138
+ end
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.
142
+ #
143
+ # @param [Resource] obj the visited domain object
144
+ # @param [ResourceAttributes::Filter] the savable attribute filter
145
+ # @return [ResourceAttributes::Filter] the composed attribute filter
146
+ def mergeable_attributes(obj, attributes)
147
+ # If this is an update, then there is no filter on the given attributes.
148
+ return attributes if @subject.identifier
149
+ # This is a create: ignore the optional auto-generated attributes.
150
+ mas = obj.mandatory_attributes.to_set
151
+ attributes.compose { |attr_md| mas.include?(attr_md.to_sym) or not attr_md.autogenerated? }
152
+ end
153
+
154
+ # Composes the given attributes, if necessary, to exclude attributes as follows:
155
+ # * If the save operation is a create, then exclude the auto-generated attributes.
156
+ # * If the visited object has an identifier, then include only those attributes
157
+ # which {AttributeMetadata#cascade_update_to_create?} or have an identifier.
158
+ #
159
+ # @param (see #mergeable_attributes)
160
+ # @return (see #mergeable_attributes)
161
+ def savable_attributes(obj, attributes)
162
+ mgbl = mergeable_attributes(obj, attributes)
163
+ return mgbl if obj.identifier.nil?
164
+ # The currently visited object is an update: include attributes which
165
+ # either cascade update to create or have saved references.
166
+ mgbl.compose do |attr_md|
167
+ attr_md.cascade_update_to_create? or Persistable.saved?(obj.send(attr_md.to_sym))
120
168
  end
121
-
169
+ end
170
+
171
+ # Returns the proxied attributes to save. A proxied attribute is included only if the proxied
172
+ # dependents have an identifier, since those without an identifer are created separately via
173
+ # the proxy.
174
+ #
175
+ # @param [Resource] obj the visited domain object
176
+ # @return [<Attribute>] the proxied cascaded attributes with an unsaved reference
177
+ def savable_proxied_attributes(obj)
122
178
  # Include a proxied reference only if the proxied dependents have an identifier,
123
179
  # since those without an identifer are created separately via the proxy.
124
- proxied = obj.class.proxied_cascaded_attributes.reject do |attr|
180
+ obj.class.proxied_cascaded_attributes.reject do |attr|
125
181
  ref = obj.send(attr)
126
182
  case ref
127
- when Enumerable then
128
- ref.any? { |dep| not dep.identifier }
129
- when Resource then
130
- not ref.identifier
183
+ when Enumerable then ref.any? { |dep| not dep.identifier }
184
+ when Resource then not ref.identifier
131
185
  end
132
186
  end
133
-
134
- proxied.empty? ? unproxied : unproxied + proxied
135
187
  end
136
188
 
137
189
  # Copies proxied references as needed.
@@ -148,17 +200,21 @@ module CaRuby
148
200
  def copy_proxied_save_references(obj, template)
149
201
  return unless obj.identifier
150
202
  obj.class.proxied_cascaded_attributes.each do |attr|
203
+ # the proxy source
151
204
  ref = obj.send(attr)
152
205
  case ref
153
- when Enumerable then
154
- coll = template.send(attr)
155
- ref.each do |dep|
156
- copy = copy_proxied_save_reference(obj, attr, template, dep)
157
- coll << copy if copy
158
- end
159
- when Resource then
160
- copy = copy_proxied_save_reference(obj, attr, template, ref)
161
- template.set_attribute(attr, copy) if copy
206
+ when Enumerable then
207
+ # recurse on the source collection
208
+ coll = template.send(attr)
209
+ ref.each do |dep|
210
+ copy = copy_proxied_save_reference(obj, attr, template, dep)
211
+ coll << copy if copy
212
+ end
213
+ when Resource then
214
+ # copy the source
215
+ copy = copy_proxied_save_reference(obj, attr, template, ref)
216
+ # set the attribute to the copy
217
+ template.set_attribute(attr, copy) if copy
162
218
  end
163
219
  end
164
220
  end
@@ -174,27 +230,44 @@ module CaRuby
174
230
  # map references to either the copied owner or a new copy of the reference
175
231
  tvh = vh.transform { |value| Resource === value ? (value == obj ? template : value.copy) : value }
176
232
  # the copy with the adjusted values
177
- copy = proxied.class.new(tvh)
233
+ copy = proxied.class.new.merge_attributes(tvh)
178
234
  logger.debug { "Created #{obj.qp} proxied #{attribute} save template copy #{proxied.pp_s}." }
179
235
  copy
180
236
  end
181
237
 
182
- # Returns the references which must be created in order to store obj.
238
+ # @param [Resource] obj the domain object to store
239
+ # @return [<Resource>] the references which must be created in order to store the object
183
240
  def collect_prerequisites(obj)
184
241
  prereqs = Set.new
242
+ # visit the cascaded attributes
185
243
  @prereq_vstr.visit(obj) do |stbl|
186
- stbl.class.storable_prerequisite_attributes.each do |attr|
244
+ # Check each mergeable attribute for prerequisites. The mergeable attributes includes
245
+ # both cascaded and independent attributes. The selection block filters for independent
246
+ # domain objects which don't have an identifier.
247
+ @mergeable.call(stbl).each_pair do |attr, attr_md|
248
+ # Cascaded attributes are not prerequisite, since they are created when the owner is created.
249
+ # Note that each non-prerequisite cascaded reference is still visited in order to ensure
250
+ # that each independent object referenced by a cascaded reference is recognized as a
251
+ # candidate prerequisite.
252
+ next if attr_md.cascaded?
187
253
  # add qualified prerequisite attribute references
188
- stbl.send(attr).enumerate do |prereq|
189
- # add the prerequisite unless it is the object being created, was already created or is
190
- # in the owner hierarchy
191
- unless prereq == obj or prereq.identifier or prereq.owner_ancestor?(obj) then
192
- prereqs << prereq
193
- end
254
+ stbl.send(attr).enumerate do |ref|
255
+ # Add the prerequisite if it satisfies the prerequisite? condition.
256
+ prereqs << ref if prerequisite?(ref, obj)
194
257
  end
195
258
  end
196
259
  end
197
260
  prereqs
198
261
  end
262
+
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.
265
+ #
266
+ # @param [Resource] ref the reference to check
267
+ # @param [Resource] obj the object being stored
268
+ # @return [Boolean] whether the reference should exist before storing the object
269
+ def prerequisite?(ref, obj)
270
+ not (ref == obj or ref.identifier or ref.owner_ancestor?(obj))
271
+ end
199
272
  end
200
273
  end
@@ -1,7 +1,7 @@
1
1
  require 'caruby/util/collection'
2
2
  require 'caruby/util/pretty_print'
3
3
  require 'caruby/domain/reference_visitor'
4
- require 'caruby/database/saved_merger'
4
+ require 'caruby/database/saved_matcher'
5
5
  require 'caruby/database/store_template_builder'
6
6
 
7
7
  module CaRuby
@@ -11,9 +11,17 @@ module CaRuby
11
11
  # Adds store capability to this Database.
12
12
  def initialize
13
13
  super
14
- @cr_tmpl_bldr = StoreTemplateBuilder.new(self) { |ref| ref.class.creatable_domain_attributes }
14
+ @ftchd_vstr = ReferenceVisitor.new { |tgt| tgt.class.fetched_domain_attributes }
15
+ @cr_tmpl_bldr = StoreTemplateBuilder.new(self) { |ref| creatable_domain_attributes(ref) }
15
16
  @upd_tmpl_bldr = StoreTemplateBuilder.new(self) { |ref| updatable_domain_attributes(ref) }
16
- @svd_mrgr = SavedMerger.new(self)
17
+ # the save result => argument reference matcher
18
+ svd_mtchr = SavedMatcher.new
19
+ # the save (result, argument) synchronization visitor
20
+ @svd_sync_vstr = MatchVisitor.new(:matcher => svd_mtchr) { |ref| ref.class.dependent_attributes }
21
+ # the attributes to merge from the save result
22
+ mgbl = Proc.new { |ref| ref.class.domain_attributes }
23
+ # the save result => argument merge visitor
24
+ @svd_mrg_vstr = MergeVisitor.new(:matcher => svd_mtchr, :mergeable => mgbl) { |ref| ref.class.dependent_attributes }
17
25
  end
18
26
 
19
27
  # Creates the specified domain object obj and returns obj. The pre-condition for this method is as
@@ -106,8 +114,6 @@ module CaRuby
106
114
  # @raise [DatabaseError] if the database operation fails
107
115
  def save(obj)
108
116
  logger.debug { "Storing #{obj}..." }
109
- # add defaults now, since a default key value could be used in the existence check
110
- obj.add_defaults
111
117
  # if obj exists then update it, otherwise create it
112
118
  exists?(obj) ? update(obj) : create(obj)
113
119
  end
@@ -134,7 +140,7 @@ module CaRuby
134
140
  raise ArgumentError.new("Database ensure_exists is missing a domain object argument") if obj.nil_or_empty?
135
141
  obj.enumerate { |ref| find(ref, :create) unless ref.identifier }
136
142
  end
137
-
143
+
138
144
  # Returns whether there is already the given obj operation in progress that is not in the scope of
139
145
  # an operation performed on a dependent obj owner, i.e. a second obj save operation of the same type
140
146
  # is only allowed if the obj operation was delegated to an owner save which in turn saves the dependent
@@ -144,8 +150,8 @@ module CaRuby
144
150
  # @param [Symbol] operation the +:create+ or +:update+ save operation
145
151
  # @return [Boolean] whether the save operation is redundant
146
152
  def recursive_save?(obj, operation)
147
- @operations.detect { |op| op.type == operation and op.subject == obj } and
148
- @operations.last.subject != obj.owner
153
+ @operations.any? { |op| op.type == operation and op.subject == obj } and
154
+ not obj.owner_ancestor?(@operations.last.subject)
149
155
  end
150
156
 
151
157
  private
@@ -167,8 +173,8 @@ module CaRuby
167
173
  # A dependent of an uncreated owner can be created by creating the owner.
168
174
  # Otherwise, create obj from a template.
169
175
  create_as_dependent(obj) or
170
- create_from_template(obj) or
171
- raise DatabaseError.new("#{obj.class.qp} is not creatable in context #{print_operations}")
176
+ create_from_template(obj) or
177
+ raise DatabaseError.new("#{obj.class.qp} is not creatable in context #{print_operations}")
172
178
  ensure
173
179
  # since obj now has an id, removed from transients set
174
180
  @transients.delete(obj)
@@ -188,17 +194,16 @@ module CaRuby
188
194
  # @return [Resource] dep
189
195
  def create_as_dependent(dep)
190
196
  # bail if not dependent or owner is not set
191
- owner = dep.owner || return
192
- unless owner.identifier then
193
- logger.debug { "Adding #{owner.qp} dependent #{dep.qp} defaults..." }
194
- dep.add_defaults
195
- logger.debug { "Ensuring that dependent #{dep.qp} owner #{owner.qp} exists..." }
196
- ensure_exists(owner)
197
+ 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)
197
202
  end
198
203
 
199
204
  # If the dependent was created as a side-effect of creating the owner, then we are done.
200
205
  if dep.identifier then
201
- logger.debug { "Created dependent #{dep.qp} by saving owner #{owner.qp}." }
206
+ logger.debug { "Created dependent #{dep.qp} by saving owner #{ownr.qp}." }
202
207
  return dep
203
208
  end
204
209
 
@@ -274,10 +279,9 @@ module CaRuby
274
279
  # The create template. Independent saved references are created as necessary.
275
280
  tmpl = build_create_template(obj)
276
281
  save_with_template(obj, tmpl) { |svc| svc.create(tmpl) }
277
-
278
282
  # If obj is a top-level create, then ensure that remaining references exist.
279
283
  if @operations.first.subject == obj then
280
- refs = obj.suspend_lazy_loader { obj.references.reject { |ref| ref.identifier } }
284
+ refs = obj.references.reject { |ref| ref.identifier }
281
285
  logger.debug { "Ensuring that created #{obj.qp} references exist: #{refs.qp}..." } unless refs.empty?
282
286
  refs.each { |ref| ensure_exists(ref) }
283
287
  end
@@ -285,8 +289,10 @@ module CaRuby
285
289
  obj
286
290
  end
287
291
 
292
+ # @param (see #create)
293
+ # @return (see #build_save_template)
288
294
  def build_create_template(obj)
289
- @cr_tmpl_bldr.build_template(obj)
295
+ build_save_template(obj, @cr_tmpl_bldr)
290
296
  end
291
297
  #
292
298
  # caCORE alert - application create logic might ignore a non-domain attribute value,
@@ -313,19 +319,64 @@ module CaRuby
313
319
  end
314
320
  end
315
321
 
316
- # Returns the {MetadataAttributes#updatable_domain_attributes} which are either
317
- # {AttributeMetadata#cascade_update_to_create?} or have identifiers for all
318
- # references in the attribute value.
322
+ # Returns the {MetadataAttributes#creatable_domain_attributes} which are not contravened by a
323
+ # one-to-one independent pending create.
324
+ #
325
+ # @param (see #create)
326
+ # @return [<Symbol>] the attributes to include in the create template
327
+ def creatable_domain_attributes(obj)
328
+ # filter the creatable attributes
329
+ obj.class.creatable_domain_attributes.compose do |attr_md|
330
+ if exclude_pending_create_attribute?(obj, attr_md) then
331
+ # Avoid printing duplicate log message.
332
+ if obj != @cr_dom_attr_log_obj then
333
+ logger.debug { "Excluded #{obj.qp} #{attr_md} in the create template since it references a 1:1 bidirectional independent pending create." }
334
+ @cr_dom_attr_log_obj = obj
335
+ end
336
+ false
337
+ else
338
+ true
339
+ end
340
+ end
341
+ end
342
+
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.
346
+ #
347
+ # @param obj (see #create)
348
+ # @param [AttributeMetadata] attr_md candidate attribute metadata
349
+ # @return [Boolean] whether the attribute should not be included in the create template
350
+ def exclude_pending_create_attribute?(obj, attr_md)
351
+ attr_md.one_to_one_bidirectional_independent? and
352
+ obj.identifier.nil? and
353
+ 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)
357
+ end
358
+
359
+ # @param [Resource] obj the object to check
360
+ # @return [Boolean] whether the penultimate create operation is on the object
361
+ def pending_create?(obj)
362
+ op = penultimate_create_operation
363
+ op and op.subject == obj
364
+ end
365
+
366
+ # @return [Operation] the create operation which scopes the innermost create operation
367
+ def penultimate_create_operation
368
+ @operations.reverse_each { |op| return op if op.type == :create and op != @operations.last }
369
+ nil
370
+ end
371
+
372
+ # Returns the {MetadataAttributes#updatable_domain_attributes}.
319
373
  #
320
374
  # @param (see #update)
321
375
  # @return the attributes to include in the update template
322
376
  def updatable_domain_attributes(obj)
323
- obj.class.updatable_domain_attributes.filter do |attr|
324
- obj.class.attribute_metadata(attr).cascade_update_to_create? or
325
- obj.send(attr).to_enum.all? { |ref| ref.identifier }
326
- end
377
+ obj.class.updatable_domain_attributes
327
378
  end
328
-
379
+
329
380
  # @param (see #update)
330
381
  def update_object(obj)
331
382
  # database identifier is required for update
@@ -389,8 +440,16 @@ module CaRuby
389
440
  end
390
441
 
391
442
  # @param (see #update)
443
+ # @return (see #build_save_template)
392
444
  def build_update_template(obj)
393
- @upd_tmpl_bldr.build_template(obj)
445
+ build_save_template(obj, @upd_tmpl_bldr)
446
+ end
447
+
448
+ # @param obj (see #save)
449
+ # @param [StoreTemplateBuilder] builder the builder to use
450
+ # @return [Resource] the template to use as the save argument
451
+ def build_save_template(obj, builder)
452
+ builder.build_template(obj)
394
453
  end
395
454
 
396
455
  # @param (see #delete)
@@ -421,49 +480,190 @@ module CaRuby
421
480
  sync_saved(obj, result)
422
481
  end
423
482
 
424
- def sync_saved(obj, result)
425
- # delegate to the merge visitor
426
- src = @svd_mrgr.merge(obj, result)
427
- # make the saved object persistent, if not already so
428
- persistify(obj)
483
+ # Synchronizes the content of the given saved domain object and the save result source as follows:
484
+ # 1. The save result source is first synchronized with the database content as necessary.
485
+ # 2. Then the source is merged into the target.
486
+ # 3. If the target must be resaved based on the call to {#update_saved?}, then the source
487
+ # result is resaved.
488
+ # 4. Each target dependent which differs from the corresponding source dependent is saved.
489
+ #
490
+ # @param [Resource] target the saved domain object
491
+ # @param [Resource] source the caCORE save result
492
+ def sync_saved(target, source)
493
+ # clear the toxic source attributes
494
+ detoxify(source)
495
+ # sync the save result source with the database
496
+ sync_saved_result_with_database(source, target)
497
+ # merge the source into the target
498
+ merge_saved(target, source)
429
499
 
430
500
  # If saved must be updated, then update recursively.
431
501
  # Otherwise, save dependents as needed.
432
- if update_saved?(obj, src) then
433
- logger.debug { "Updating saved #{obj} to store unsaved attributes..." }
434
- # call update_savedect(saved) rather than update(saved) to bypass the redundant update check
435
- perform(:update, obj) { update_object(obj) }
502
+ if update_saved?(target, source) then
503
+ logger.debug { "Updating saved #{target} to store unsaved attributes..." }
504
+ # call update_object(saved) rather than update(saved) to bypass the redundant update check
505
+ perform(:update, target) { update_object(target) }
436
506
  else
437
507
  # recursively save the dependents
438
- save_dependents(obj)
508
+ save_changed_dependents(target)
439
509
  end
440
510
  end
441
-
511
+
512
+ # Synchronizes the given saved target result source with the database content.
513
+ # The source is synchronized by {#sync_save_result}.
514
+ #
515
+ # @param (see #sync_saved)
516
+ def sync_saved_result_with_database(source, target)
517
+ @svd_sync_vstr.visit(source, target) { |src, tgt| sync_save_result(src, tgt) }
518
+ end
519
+
520
+ # Merges the database content into the given saved domain object.
521
+ # Dependents are merged recursively.
522
+ #
523
+ # caTissue alert - the auto-generated references are not necessarily valid, e.g. the auto-generated
524
+ # SpecimenRequirement characteristics tissue site is nil rather than the default 'Not Specified'.
525
+ # This results in an obscure downstream error when creating an CPR which auto-generates a SCG
526
+ # which auto-generates a Specimen which copies the invalid characteristics. The work-around for
527
+ # this bug is to add defaults to auto-generated references. Then, if the content differs from
528
+ # the database, the difference induces an update of the reference.
529
+ #
530
+ # @param (see #sync_saved)
531
+ # @return [Resource] the merged target object
532
+ def merge_saved(target, source)
533
+ logger.debug { "Merging saved result #{source} into saved #{target.qp}..." }
534
+ # Update each saved reference snapshot to reflect the database state and add a lazy loader if necessary.
535
+ @svd_mrg_vstr.visit(source, target) do |src, tgt|
536
+ # capture the id
537
+ prev_id = tgt.identifier
538
+ persistify_object(tgt, src)
539
+ # if tgt is an auto-generated reference, then add defaults
540
+ if target != tgt and prev_id.nil? then tgt.add_defaults end
541
+ end
542
+ logger.debug { "Merged saved result #{source} into saved #{target.qp}." }
543
+ end
544
+
545
+ # Synchronizes the given save result source object to reflect the database content, as follows:
546
+ # * If the save result has autogenerated non-domain attributes, then the source is refetched.
547
+ # * Each of the dependent {#synchronization_attributes} is fetched.
548
+ # * Inverses are set consistently within the save result object graph.
549
+ #
550
+ # @param (see #sync_saved)
551
+ def sync_save_result(source, target)
552
+ logger.debug { "Synchronizing #{target} save result #{source} with the database..." }
553
+ # If the target was created, then refetch and merge the source if necessary to reflect auto-generated
554
+ # non-domain attribute values.
555
+ if target.identifier.nil? then sync_created_result_object(source) end
556
+ # If there are auto-generated attributes, then merge them into the save result.
557
+ sync_save_result_references(source, target)
558
+ # Set inverses consistently in the source object graph
559
+ set_inverses(source)
560
+ logger.debug { "Synchronized #{target} save result #{source} with the database." }
561
+ end
562
+
563
+ # Refetches the given create result source if there are any {ResourceAttributes#autogenerated_nondomain_attributes}
564
+ # which must be fetched to reflect the database state.
565
+ #
566
+ # @param source (see #sync_saved)
567
+ def sync_created_result_object(source)
568
+ attrs = source.class.autogenerated_nondomain_attributes
569
+ return if attrs.empty?
570
+ logger.debug { "Refetch #{source} to reflect auto-generated database content for attributes #{attrs.to_series}..." }
571
+ find(source)
572
+ end
573
+
574
+ # Fetches the {#synchronization_attributes} into the given target save result source.
575
+ #
576
+ # @param (see #sync_saved)
577
+ def sync_save_result_references(source, target)
578
+ attrs = synchronization_attributes(source, target)
579
+ return if attrs.empty?
580
+ logger.debug { "Fetching the saved #{target.qp} attributes #{attrs.to_series} into save result #{source.qp}..." }
581
+ attrs.each { |attr| sync_save_result_attribute(source, attr) }
582
+ logger.debug { "Fetched the saved #{target.qp} attributes #{attrs.to_series} into the save result #{source.qp}." }
583
+ end
584
+
585
+ # @see #sync_save_result_references
586
+ def sync_save_result_attribute(source, attribute)
587
+ # fetch the value
588
+ fetched = fetch_association(source, attribute)
589
+ # set the attribute
590
+ source.set_attribute(attribute, fetched)
591
+ end
592
+
593
+ # Returns the saved target attributes which must be fetched to reflect the database content, consisting
594
+ # of the following:
595
+ # * {Persistable#saved_fetch_attributes}
596
+ # * {ResourceAttributes#domain_attributes} which include a source reference without an identifier
597
+ #
598
+ # @param (see #sync_saved)
599
+ # @return [<Symbol>] the attributes which must be fetched
600
+ def synchronization_attributes(source, target)
601
+ # the target save operation
602
+ op = @operations.last
603
+ # the attributes to fetch
604
+ attrs = target.saved_fetch_attributes(op).to_set
605
+ # the pending create, if any
606
+ pndg_op = penultimate_create_operation
607
+ pndg = pndg_op.subject if pndg_op
608
+ # add in the domain attributes whose identifier was not set in the result
609
+ source.class.saved_domain_attributes.select do |attr|
610
+ srcval = source.send(attr)
611
+ tgtval = target.send(attr)
612
+ if Persistable.unsaved?(srcval) then
613
+ logger.debug { "Fetching save result #{source.qp} #{attr} since a referenced object identifier was not set in the result..." }
614
+ attrs << attr
615
+ elsif srcval.nil_or_empty? and Persistable.unsaved?(tgtval) and tgtval != pndg then
616
+ logger.debug { "Fetching save result #{source.qp} #{attr} since the target #{target.qp} value #{tgtval.qp} is missing an identifier..." }
617
+ attrs << attr
618
+ end
619
+ end
620
+ attrs
621
+ end
622
+
442
623
  # Saves the given domain object dependents.
443
624
  #
444
625
  # @param [Resource] obj the owner domain object
445
- def save_dependents(obj)
446
- obj.dependents.each { |dep| save_dependent(obj, dep) }
626
+ def save_changed_dependents(obj)
627
+ # JRuby alert - copy the Resource dependents call result to an array, since iteration based on
628
+ # Forwardable enum_for breaks here with an obscure Java ConcurrentModificationException
629
+ # in the CaTissue SCG save test case. TODO - isolate and fix at source.
630
+ obj.class.dependent_attributes.each do |attr|
631
+ deps = obj.send(attr).to_enum
632
+ logger.debug { "Saving the #{obj} #{attr} dependents #{deps.qp} which have changed..." } unless deps.empty?
633
+ deps.each { |dep| save_dependent_if_changed(obj, attr, dep) }
634
+ end
447
635
  end
448
636
 
449
637
  # Saves the given dependent domain object if necessary.
450
638
  # Recursively saves the obj dependents as necessary.
451
639
  #
452
640
  # @param [Resource] obj the dependent domain object to save
453
- def save_dependent(owner, dep)
454
- if dep.identifier.nil? then
455
- logger.debug { "Creating dependent #{dep.qp}..." }
456
- return create(dep)
641
+ def save_dependent_if_changed(owner, attribute, dependent)
642
+ if dependent.identifier.nil? then
643
+ logger.debug { "Creating #{owner.qp} #{attribute} dependent #{dependent.qp}..." }
644
+ return create(dependent)
457
645
  end
458
- changes = dep.changed_attributes
459
- logger.debug { "#{owner.qp} dependent #{dep.qp} changed for attributes #{changes.to_series}." } unless changes.empty?
460
- if changes.any? { |attr| not dep.class.attribute_metadata(attr).dependent? } then
461
- logger.debug { "Updating changed #{owner.qp} dependent #{dep.qp}..." }
462
- update(dep)
646
+ changes = dependent.changed_attributes
647
+ logger.debug { "#{owner.qp} #{attribute} dependent #{dependent.qp} changed for attributes #{changes.to_series}." } unless changes.empty?
648
+ if changes.any? { |attr| not dependent.class.attribute_metadata(attr).dependent? } then
649
+ # the owner save operation
650
+ op = operations.last
651
+ # The dependent is auto-generated if the owner was created or auto-generated and
652
+ # the dependent attribute is auto-generated.
653
+ ag = (op.type == :create or op.autogenerated?) && owner.class.attribute_metadata(attribute).autogenerated?
654
+ logger.debug { "Updating the changed #{owner.qp} #{attribute} dependent #{dependent.qp}..." }
655
+ perform(:update, dependent, :autogenerated => ag) { update_object(dependent) }
463
656
  else
464
- save_dependents(dep)
657
+ save_changed_dependents(dependent)
465
658
  end
466
659
  end
660
+
661
+ # Creates the given dependent domain object.
662
+ #
663
+ # @param (see #save_dependent)
664
+ def create_dependent(owner, dep)
665
+ create(dep)
666
+ end
467
667
  end
468
668
  end
469
669
  end