caruby-core 1.4.1

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