caruby-core 1.4.1

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 (86) hide show
  1. data/History.txt +4 -0
  2. data/LEGAL +5 -0
  3. data/LICENSE +22 -0
  4. data/README.md +51 -0
  5. data/doc/website/css/site.css +1 -5
  6. data/doc/website/images/avatar.png +0 -0
  7. data/doc/website/images/favicon.ico +0 -0
  8. data/doc/website/images/logo.png +0 -0
  9. data/doc/website/index.html +82 -0
  10. data/doc/website/install.html +87 -0
  11. data/doc/website/quick_start.html +87 -0
  12. data/doc/website/tissue.html +85 -0
  13. data/doc/website/uom.html +10 -0
  14. data/lib/caruby.rb +3 -0
  15. data/lib/caruby/active_support/README.txt +2 -0
  16. data/lib/caruby/active_support/core_ext/string.rb +7 -0
  17. data/lib/caruby/active_support/core_ext/string/inflections.rb +167 -0
  18. data/lib/caruby/active_support/inflections.rb +55 -0
  19. data/lib/caruby/active_support/inflector.rb +398 -0
  20. data/lib/caruby/cli/application.rb +36 -0
  21. data/lib/caruby/cli/command.rb +169 -0
  22. data/lib/caruby/csv/csv_mapper.rb +157 -0
  23. data/lib/caruby/csv/csvio.rb +185 -0
  24. data/lib/caruby/database.rb +252 -0
  25. data/lib/caruby/database/fetched_matcher.rb +66 -0
  26. data/lib/caruby/database/persistable.rb +432 -0
  27. data/lib/caruby/database/persistence_service.rb +162 -0
  28. data/lib/caruby/database/reader.rb +599 -0
  29. data/lib/caruby/database/saved_merger.rb +131 -0
  30. data/lib/caruby/database/search_template_builder.rb +59 -0
  31. data/lib/caruby/database/sql_executor.rb +75 -0
  32. data/lib/caruby/database/store_template_builder.rb +200 -0
  33. data/lib/caruby/database/writer.rb +469 -0
  34. data/lib/caruby/domain/annotatable.rb +25 -0
  35. data/lib/caruby/domain/annotation.rb +23 -0
  36. data/lib/caruby/domain/attribute_metadata.rb +447 -0
  37. data/lib/caruby/domain/java_attribute_metadata.rb +160 -0
  38. data/lib/caruby/domain/merge.rb +91 -0
  39. data/lib/caruby/domain/properties.rb +95 -0
  40. data/lib/caruby/domain/reference_visitor.rb +289 -0
  41. data/lib/caruby/domain/resource_attributes.rb +528 -0
  42. data/lib/caruby/domain/resource_dependency.rb +205 -0
  43. data/lib/caruby/domain/resource_introspection.rb +159 -0
  44. data/lib/caruby/domain/resource_metadata.rb +117 -0
  45. data/lib/caruby/domain/resource_module.rb +285 -0
  46. data/lib/caruby/domain/uniquify.rb +38 -0
  47. data/lib/caruby/import/annotatable_class.rb +28 -0
  48. data/lib/caruby/import/annotation_class.rb +27 -0
  49. data/lib/caruby/import/annotation_module.rb +67 -0
  50. data/lib/caruby/import/java.rb +338 -0
  51. data/lib/caruby/migration/migratable.rb +167 -0
  52. data/lib/caruby/migration/migrator.rb +533 -0
  53. data/lib/caruby/migration/resource.rb +8 -0
  54. data/lib/caruby/migration/resource_module.rb +11 -0
  55. data/lib/caruby/migration/uniquify.rb +20 -0
  56. data/lib/caruby/resource.rb +969 -0
  57. data/lib/caruby/util/attribute_path.rb +46 -0
  58. data/lib/caruby/util/cache.rb +53 -0
  59. data/lib/caruby/util/class.rb +99 -0
  60. data/lib/caruby/util/collection.rb +1053 -0
  61. data/lib/caruby/util/controlled_value.rb +35 -0
  62. data/lib/caruby/util/coordinate.rb +75 -0
  63. data/lib/caruby/util/domain_extent.rb +49 -0
  64. data/lib/caruby/util/file_separator.rb +65 -0
  65. data/lib/caruby/util/inflector.rb +20 -0
  66. data/lib/caruby/util/log.rb +95 -0
  67. data/lib/caruby/util/math.rb +12 -0
  68. data/lib/caruby/util/merge.rb +59 -0
  69. data/lib/caruby/util/module.rb +34 -0
  70. data/lib/caruby/util/options.rb +92 -0
  71. data/lib/caruby/util/partial_order.rb +36 -0
  72. data/lib/caruby/util/person.rb +119 -0
  73. data/lib/caruby/util/pretty_print.rb +184 -0
  74. data/lib/caruby/util/properties.rb +112 -0
  75. data/lib/caruby/util/stopwatch.rb +66 -0
  76. data/lib/caruby/util/topological_sync_enumerator.rb +53 -0
  77. data/lib/caruby/util/transitive_closure.rb +45 -0
  78. data/lib/caruby/util/tree.rb +48 -0
  79. data/lib/caruby/util/trie.rb +37 -0
  80. data/lib/caruby/util/uniquifier.rb +30 -0
  81. data/lib/caruby/util/validation.rb +48 -0
  82. data/lib/caruby/util/version.rb +56 -0
  83. data/lib/caruby/util/visitor.rb +351 -0
  84. data/lib/caruby/util/weak_hash.rb +36 -0
  85. data/lib/caruby/version.rb +3 -0
  86. metadata +186 -0
@@ -0,0 +1,131 @@
1
+ require 'caruby/util/collection'
2
+ require 'caruby/util/pretty_print'
3
+ require 'caruby/domain/reference_visitor'
4
+ require 'caruby/database/fetched_matcher'
5
+ require 'caruby/database/store_template_builder'
6
+
7
+ module CaRuby
8
+ class Database
9
+ # Merges database content sources into saved targets.
10
+ class SavedMerger
11
+ # @param [Database] database the database performing the save
12
+ def initialize(database)
13
+ @database = database
14
+ # the save result matchers
15
+ cr_mtchr = FetchedMatcher.new(:relaxed)
16
+ upd_mtchr = FetchedMatcher.new
17
+ # the save result merge visitors
18
+ @cr_mrg_vstr = MergeVisitor.new(:matcher => cr_mtchr) { |src, tgt| mergeable_saved_attributes(tgt) }
19
+ @upd_mrg_vstr = MergeVisitor.new(:matcher => upd_mtchr) { |src, tgt| mergeable_saved_attributes(tgt) }
20
+ end
21
+
22
+
23
+ # Merges the database content into the given saved domain object.
24
+ # Dependents are merged recursively.
25
+ #
26
+ # @param [Resource] obj the saved domain object
27
+ # @param [Resource] result the caCORE result
28
+ # @return [Resource] the object representing the persistent state
29
+ def merge(saved, result)
30
+ # the sync source reflecting the database content
31
+ src = saved_source(saved, result)
32
+ # merge the source into obj, including all cascaded dependents
33
+ merge_saved(saved, src)
34
+ src
35
+ end
36
+
37
+ # @param [Resource] obj the saved domain object
38
+ # @param [Resource] result the caCORE result domain object
39
+ # @return [Resource] the source domain object which accurately reflects the database state
40
+ # @see #fetch_saved?
41
+ def saved_source(obj, result)
42
+ # The true stored content might need to be fetched from the database.
43
+ if obj.fetch_saved? then
44
+ tmpl = result.copy(:identifier)
45
+ logger.debug { "Fetching saved #{obj.qp} using template #{tmpl}..." }
46
+ tmpl.find
47
+ else
48
+ result
49
+ end
50
+ end
51
+
52
+ # Merges the content of the source domain object into the saved domain object obj.
53
+ # If obj differs from source, then obj is resaved. Dependents are merged recursively.
54
+ #
55
+ # @param [Resource] obj the saved domain object
56
+ # @param [Resource] source object holding the stored content
57
+ def merge_saved(obj, source)
58
+ logger.debug { "Merging database content #{source} into saved #{obj.qp}..." }
59
+ visitor = @database.mergeable_autogenerated_operation? ? @cr_mrg_vstr : @upd_mrg_vstr
60
+ visitor.visit(obj, source) do |tgt, src|
61
+ logger.debug { "Saved #{obj.qp} merge visitor merged database content #{src.qp} into #{tgt.qp}..." }
62
+ merge_saved_reference(tgt, src)
63
+ end
64
+ end
65
+
66
+ # Sets the target snapshot attribute values from the given source, if different.
67
+ #
68
+ # @param [Resource] target the saved domain object
69
+ # @param [Resource] source the domain object reflecting the database state
70
+ # @return [Resource] the synced target
71
+ def merge_saved_reference(target, source)
72
+ # set each unsaved non-domain attribute from the source to reflect the database value
73
+ target.copy_volatile_attributes(source)
74
+
75
+ # take a snapshot of the saved target
76
+ target.take_snapshot
77
+ logger.debug { "A snapshot was taken of the saved #{target.qp}." }
78
+
79
+ # the non-domain attribute => [target value, source value] difference hash
80
+ diff = target.diff(source)
81
+ # the difference attribute => source value hash, excluding nil source values
82
+ dvh = diff.transform { |vdiff| vdiff.last }.compact
83
+ return if dvh.empty?
84
+ logger.debug { "Saved #{target} differs from database content #{source.qp} as follows: #{diff.filter_on_key { |attr| dvh.has_key?(attr) }.qp}" }
85
+ logger.debug { "Setting saved #{target.qp} snapshot values from source values to reflect the database state: #{dvh.qp}..." }
86
+ # update the snapshot from the source value to reflect the database state
87
+ target.snapshot.merge!(dvh)
88
+
89
+ target
90
+ end
91
+
92
+ # Returns the dependent attributes that can be copied from a save result to
93
+ # the given save argument object. This method qualifies the obj class
94
+ # {AttributeMetadata#copyable_saved_attributes} by whether the attribute is
95
+ # actually auto-generated for the saved object, i.e. the object was itself
96
+ # created or auto-generated. If obj was created or auto-generated, then
97
+ # this method returns the {AttributeMetadata#copyable_saved_attributes}.
98
+ # Otherwise, this method returns the {AttributeMetadata#cascaded_attributes}.
99
+ #
100
+ # @param [Resource] obj the domain object which was saved
101
+ # @return [<Symbol>] the attributes to copy
102
+ def mergeable_saved_attributes(obj)
103
+ fa = obj.class.fetched_domain_attributes
104
+ obj.suspend_lazy_loader do
105
+ attrs = obj.class.cascaded_attributes.filter do |attr|
106
+ fa.include?(attr) or not obj.send(attr).nil_or_empty?
107
+ end
108
+ if @database.mergeable_autogenerated_operation? then
109
+ ag_attrs = mergeable_saved_autogenerated_attributes(obj)
110
+ unless ag_attrs.empty? then
111
+ logger.debug { "Adding #{obj.qp} mergeable saved auto-generated #{ag_attrs.to_series} to the merge set..." }
112
+ attrs = attrs.to_set.merge(ag_attrs)
113
+ end
114
+ end
115
+ logger.debug { "Mergeable saved #{obj.qp} attributes: #{attrs.qp}." } unless attrs.empty?
116
+ attrs
117
+ end
118
+ end
119
+
120
+ # Returns the autogenerated dependent attributes that can be copied from a save result to
121
+ # the given save argument object.
122
+ #
123
+ # @param [Resource] obj the domain object which was saved
124
+ # @return [<Symbol>] the attributes to copy, or nil if no such attributes
125
+ def mergeable_saved_autogenerated_attributes(obj)
126
+ attrs = obj.class.mergeable_saved_autogenerated_attributes
127
+ attrs.reject { |attr| obj.send(attr).nil_or_empty? }
128
+ end
129
+ end
130
+ end
131
+ end
@@ -0,0 +1,59 @@
1
+ require 'caruby/util/log'
2
+ require 'caruby/util/pretty_print'
3
+
4
+ module CaRuby
5
+ # SearchTemplateBuilder builds a template suitable for a caCORE saarch database operation.
6
+ class SearchTemplateBuilder
7
+ def initialize(database)
8
+ @database = database
9
+ end
10
+
11
+ # Returns a template for matching the domain object obj and the optional hash values.
12
+ # The default hash attributes are the {ResourceAttributes#searchable_attributes}.
13
+ # The template includes only the non-domain attributes of the hash references.
14
+ #
15
+ # caCORE alert - Because of caCORE API limitations, the obj searchable attribute
16
+ # values are limited to the following:
17
+ # * non-domain attribute values
18
+ # * non-collection domain attribute references which contain a key
19
+ #
20
+ # caCORE alert - the caCORE query builder breaks on reference cycles and
21
+ # is easily confused by extraneous references, so it is necessary to search
22
+ # with a template instead that contains only references essential to the
23
+ # search. Each reference is confirmed to exist and the reference content in
24
+ # the template consists entirely of the fetched identifier attribute.
25
+ def build_template(obj, hash=nil)
26
+ # split the attributes into reference and non-reference attributes.
27
+ # the new search template object is built from the non-reference attributes.
28
+ # the reference attributes values are copied and added.
29
+ logger.debug { "Building search template for #{obj.qp}..." }
30
+ hash ||= obj.value_hash(obj.class.searchable_attributes)
31
+ # the searchable attribute => value hash
32
+ ref_hash, nonref_hash = hash.hash_partition { |attr, value| Resource === value }
33
+ # make the search template from the non-reference attributes
34
+ tmpl = obj.class.new(nonref_hash)
35
+ # get references for the search template
36
+ unless ref_hash.empty? then
37
+ logger.debug { "Collecting search reference parameters for #{obj.qp} from attributes #{ref_hash.keys.to_series}..." }
38
+ end
39
+ ref_hash.each { |attr, ref| add_search_template_reference(tmpl, ref, attr) }
40
+ tmpl
41
+ end
42
+
43
+ private
44
+
45
+ # Sets the template attribute to a new search reference object created from source.
46
+ # The reference contains only the source identifier.
47
+ # Returns the search reference, or nil if source does not exist in the database.
48
+ def add_search_template_reference(template, source, attribute)
49
+ ref = source.identifier ? source.copy(:identifier) : source.copy
50
+ # Disable inverse integrity, since the template attribute assignment might have added a reference
51
+ # from ref to template, which introduces a template => ref => template cycle that causes a caCORE
52
+ # search infinite loop. Use the Java property writer instead.
53
+ writer = template.class.attribute_metadata(attribute).property_accessors.last
54
+ template.send(writer, ref)
55
+ logger.debug { "Search reference parameter #{attribute} for #{template.qp} set to #{ref} copied from #{source.qp}" }
56
+ ref
57
+ end
58
+ end
59
+ end
@@ -0,0 +1,75 @@
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'
8
+
9
+ module CaRuby
10
+ # SQLExecutor executes an SQL statement against the database.
11
+ # Use of this class requires the dbi gem.
12
+ # SQLExecutor is an auxiliary utility class and is not used by the rest of the CaRuby API.
13
+ class SQLExecutor
14
+ # Creates a new SQLExecutor with the given options.
15
+ #
16
+ # The default :database_host is the application :host property value, which in turn
17
+ # defaults to 'localhost'.
18
+ #
19
+ # The default :database_type is 'mysql'. The optional :database_port property overrides
20
+ # the default port for the database type.
21
+ #
22
+ # The default :database_driver is 'jdbc:mysql' for MySQL or 'Oracle' for Oracle.
23
+ #
24
+ # @option options [String] :database_host the database host
25
+ # @option options [String] :database the database name
26
+ # @option options [Integer] :database_port the database password (not the application login password)
27
+ # @option options [String] :database_type the DBI database type, e.g. +mysql+
28
+ # @option options [String] :database_driver the DBI connect driver string, e.g. +jdbc:mysql+
29
+ # @option options [String] :database_user the database username (not the application login name)
30
+ # @option options [String] :database_password the database password (not the application login password)
31
+ # Raises CaRuby::ConfigurationError if an option is invalid.
32
+ def initialize(options)
33
+ app_host = Options.get(:host, options, "localhost")
34
+ db_host = Options.get(:database_host, options, app_host)
35
+ db_type = Options.get(:database_type, options, "mysql")
36
+ db_driver = Options.get(:database_driver, options) { default_driver_string(db_type) }
37
+ db_port = Options.get(:database_port, options) { default_port(db_type) }
38
+ db_name = Options.get(:database, options) { raise_missing_option_exception(:database) }
39
+ @address = "dbi:#{db_driver}://#{db_host}:#{db_port}/#{db_name}"
40
+ @username = Options.get(:database_user, options) { raise_missing_option_exception(:database_user) }
41
+ @password = Options.get(:database_password, options) { raise_missing_option_exception(:database_password) }
42
+ end
43
+
44
+ # Connects to the database, yields the DBI handle to the given block and disconnects.
45
+ # Returns the execution result.
46
+ def execute
47
+ logger.debug { "Connecting to database with user #{@username}, address #{@address}..." }
48
+ result = DBI.connect(@address, @username, @password, "driver"=>"com.mysql.jdbc.Driver") { |dbh| yield dbh }
49
+ logger.debug { "Disconnected from the database." }
50
+ result
51
+ end
52
+
53
+ private
54
+
55
+ def default_driver_string(db_type)
56
+ 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}")
60
+ end
61
+ end
62
+
63
+ def default_port(db_type)
64
+ 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}")
68
+ end
69
+ end
70
+
71
+ def raise_missing_option_exception(option)
72
+ raise CaRuby::ConfigurationError.new("database connection property not found: #{option}")
73
+ end
74
+ end
75
+ end
@@ -0,0 +1,200 @@
1
+ require 'caruby/domain/reference_visitor'
2
+
3
+ module CaRuby
4
+ # StoreTemplateBuilder creates a template suitable for a create or update database operation.
5
+ class StoreTemplateBuilder
6
+ # Creates a new StoreTemplateBuilder for the given database.
7
+ #
8
+ # @param [Database] database the target database
9
+ # @yield [ref] the required selector block which determines the attributes copied into the template
10
+ # @yieldparam [Resource] ref the domain object to copy
11
+ def initialize(database)
12
+ @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
+ # caTissue alert - must copy all of the non-domain attributes rather than just the identifier,
18
+ # since caTissue auto-generated Specimen update requires the parent collection status. This
19
+ # is the only known occurrence of a referenced object required non-identifier attribute.
20
+ # The copy attributes are parameterized by the top-level save target.
21
+ copier = Proc.new do |src|
22
+ copy = src.copy
23
+ logger.debug { "Store template builder copied #{src.qp} into #{copy}." }
24
+ copy_proxied_save_references(src, copy)
25
+ copy
26
+ 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) }
31
+ end
32
+
33
+ # Returns a new domain object which serves as the argument for obj create or update.
34
+ #
35
+ # This method copies a portion of the obj object graph to a template object.
36
+ # The template object graph consists of copies of obj object graph which are necessary
37
+ # to store obj. The template object graph contains only those references which are
38
+ # essential to the store operation.
39
+ #
40
+ # caCORE alert - +caCORE+ expects the store argument to be carefully prepared prior to
41
+ # the create or update. build_storable_template culls the target object with a template
42
+ # 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.
50
+ #
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
53
+ # reference is not relevant to the create. The template returned by this method elides
54
+ # all non-essential references.
55
+ #
56
+ # caCORE alert - application business logic performs unnecessary verification
57
+ # of uncascaded references as if they were a cascaded create. This can result in
58
+ # an obscure ApplicationException. The server.log stack trace indicates the
59
+ # extraneous verification code. For example, +caTissue+ +NewSpecimenBizLogic.validateStorageContainer+
60
+ # is unnecessarily called on a SpecimenCollectionGroup (SCG) update. SCG does not
61
+ # cascade to Specimen, but caTissue considers the SCG update a Specimen create
62
+ # anyway if the SCG references a Specimen without an identifier. The Specimen
63
+ # business logic then raises an exception when it finds a StorageContainer
64
+ # without an identifier in the Specimen object graph. Therefore, an update must
65
+ # build a storable template which prunes the update object graph to exclude uncascaded
66
+ # objects. These uncascaded objects should be ignored by the application but aren't.
67
+ #
68
+ # @param [Resource] obj the domain object to save
69
+ # @return [Resource] the template to use as the caCORE argument
70
+ def build_template(obj)
71
+ # prepare the object for a store operation
72
+ ensure_storable(obj)
73
+ # copy the cascade hierarchy
74
+ logger.debug { "Building storable template for #{obj.qp}..." }
75
+ tmpl = @copy_vstr.visit(obj)
76
+ logger.debug { "Built #{obj.qp} template #{tmpl.qp} by mapping references #{@copy_vstr.matches.qp}" }
77
+ tmpl
78
+ end
79
+
80
+ private
81
+
82
+ # Ensure that the given domain object obj can be created or updated by setting the identifier for
83
+ # each independent reference in the create template object graph.
84
+ #
85
+ # caCORE alert - +caCORE+ raises an ApplicationException if an independent reference in the create or
86
+ # update argument does not have an identifier. The +caCORE+ server log error is as follows:
87
+ # java.lang.IllegalArgumentException: id to load is required for loading
88
+ # The server log stack trace indicates a bizlogic line that offers a clue to the offending reference.
89
+ def ensure_storable(obj)
90
+ # Add defaults, which might introduce independent references.
91
+ obj.add_defaults
92
+ # create the prerequisite references if necessary
93
+ prereqs = collect_prerequisites(obj)
94
+ unless prereqs.empty? then
95
+ logger.debug { "Ensuring references for #{obj.qp} exist: #{prereqs.map { |ref| ref.qp }.to_series}..." }
96
+ @database.ensure_exists(prereqs)
97
+ logger.debug { "Prerequisite references for #{obj.qp} exist: #{prereqs.map { |ref| ref }.to_series}." }
98
+ 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
+ # Verify that the object is complete
102
+ obj.validate
103
+ 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.
109
+ #
110
+ # @param [Resource] obj the domain object copied to the update template
111
+ # @return [<Symbol>] the reference attributes to include in the update template
112
+ 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
120
+ end
121
+
122
+ # Include a proxied reference only if the proxied dependents have an identifier,
123
+ # since those without an identifer are created separately via the proxy.
124
+ proxied = obj.class.proxied_cascaded_attributes.reject do |attr|
125
+ ref = obj.send(attr)
126
+ case ref
127
+ when Enumerable then
128
+ ref.any? { |dep| not dep.identifier }
129
+ when Resource then
130
+ not ref.identifier
131
+ end
132
+ end
133
+
134
+ proxied.empty? ? unproxied : unproxied + proxied
135
+ end
136
+
137
+ # Copies proxied references as needed.
138
+ #
139
+ # caTissue alert - even though Specimen save cascades to SpecimenPosition,
140
+ # SpecimenPosition cannot be updated directly. Rather than simply not
141
+ # cascading to the SpecimenPosition, caTissue checks a Specimen save argument
142
+ # to ensure that the SpecimenPosition reflects the current database state
143
+ # rather than the desired cascaded state. Play along with this bizarre
144
+ # mechanism by adding our own bizarre work-around mechanism to copy a
145
+ # proxied reference only if it has an identifier. This works only because
146
+ # another work-around in the #{CaRuby::Database::Writer} updates proxied
147
+ # references via the proxy create before building the update template.
148
+ def copy_proxied_save_references(obj, template)
149
+ return unless obj.identifier
150
+ obj.class.proxied_cascaded_attributes.each do |attr|
151
+ ref = obj.send(attr)
152
+ 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
162
+ end
163
+ end
164
+ end
165
+
166
+ # Copies a proxied reference.
167
+ #
168
+ # @return [Resource, nil] the copy, or nil if no copy is made
169
+ def copy_proxied_save_reference(obj, attribute, template, proxied)
170
+ # only copy an existing proxied
171
+ return unless proxied.identifier
172
+ # the proxied attribute => value hash
173
+ vh = proxied.value_hash
174
+ # map references to either the copied owner or a new copy of the reference
175
+ tvh = vh.transform { |value| Resource === value ? (value == obj ? template : value.copy) : value }
176
+ # the copy with the adjusted values
177
+ copy = proxied.class.new(tvh)
178
+ logger.debug { "Created #{obj.qp} proxied #{attribute} save template copy #{proxied.pp_s}." }
179
+ copy
180
+ end
181
+
182
+ # Returns the references which must be created in order to store obj.
183
+ def collect_prerequisites(obj)
184
+ prereqs = Set.new
185
+ @prereq_vstr.visit(obj) do |stbl|
186
+ stbl.class.storable_prerequisite_attributes.each do |attr|
187
+ # 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
194
+ end
195
+ end
196
+ end
197
+ prereqs
198
+ end
199
+ end
200
+ end