caruby-core 1.4.2 → 1.4.3

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