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.
- data/History.txt +4 -0
- data/LEGAL +5 -0
- data/LICENSE +22 -0
- data/README.md +51 -0
- data/doc/website/css/site.css +1 -5
- data/doc/website/images/avatar.png +0 -0
- data/doc/website/images/favicon.ico +0 -0
- data/doc/website/images/logo.png +0 -0
- data/doc/website/index.html +82 -0
- data/doc/website/install.html +87 -0
- data/doc/website/quick_start.html +87 -0
- data/doc/website/tissue.html +85 -0
- data/doc/website/uom.html +10 -0
- data/lib/caruby.rb +3 -0
- data/lib/caruby/active_support/README.txt +2 -0
- data/lib/caruby/active_support/core_ext/string.rb +7 -0
- data/lib/caruby/active_support/core_ext/string/inflections.rb +167 -0
- data/lib/caruby/active_support/inflections.rb +55 -0
- data/lib/caruby/active_support/inflector.rb +398 -0
- data/lib/caruby/cli/application.rb +36 -0
- data/lib/caruby/cli/command.rb +169 -0
- data/lib/caruby/csv/csv_mapper.rb +157 -0
- data/lib/caruby/csv/csvio.rb +185 -0
- data/lib/caruby/database.rb +252 -0
- data/lib/caruby/database/fetched_matcher.rb +66 -0
- data/lib/caruby/database/persistable.rb +432 -0
- data/lib/caruby/database/persistence_service.rb +162 -0
- data/lib/caruby/database/reader.rb +599 -0
- data/lib/caruby/database/saved_merger.rb +131 -0
- data/lib/caruby/database/search_template_builder.rb +59 -0
- data/lib/caruby/database/sql_executor.rb +75 -0
- data/lib/caruby/database/store_template_builder.rb +200 -0
- data/lib/caruby/database/writer.rb +469 -0
- data/lib/caruby/domain/annotatable.rb +25 -0
- data/lib/caruby/domain/annotation.rb +23 -0
- data/lib/caruby/domain/attribute_metadata.rb +447 -0
- data/lib/caruby/domain/java_attribute_metadata.rb +160 -0
- data/lib/caruby/domain/merge.rb +91 -0
- data/lib/caruby/domain/properties.rb +95 -0
- data/lib/caruby/domain/reference_visitor.rb +289 -0
- data/lib/caruby/domain/resource_attributes.rb +528 -0
- data/lib/caruby/domain/resource_dependency.rb +205 -0
- data/lib/caruby/domain/resource_introspection.rb +159 -0
- data/lib/caruby/domain/resource_metadata.rb +117 -0
- data/lib/caruby/domain/resource_module.rb +285 -0
- data/lib/caruby/domain/uniquify.rb +38 -0
- data/lib/caruby/import/annotatable_class.rb +28 -0
- data/lib/caruby/import/annotation_class.rb +27 -0
- data/lib/caruby/import/annotation_module.rb +67 -0
- data/lib/caruby/import/java.rb +338 -0
- data/lib/caruby/migration/migratable.rb +167 -0
- data/lib/caruby/migration/migrator.rb +533 -0
- data/lib/caruby/migration/resource.rb +8 -0
- data/lib/caruby/migration/resource_module.rb +11 -0
- data/lib/caruby/migration/uniquify.rb +20 -0
- data/lib/caruby/resource.rb +969 -0
- data/lib/caruby/util/attribute_path.rb +46 -0
- data/lib/caruby/util/cache.rb +53 -0
- data/lib/caruby/util/class.rb +99 -0
- data/lib/caruby/util/collection.rb +1053 -0
- data/lib/caruby/util/controlled_value.rb +35 -0
- data/lib/caruby/util/coordinate.rb +75 -0
- data/lib/caruby/util/domain_extent.rb +49 -0
- data/lib/caruby/util/file_separator.rb +65 -0
- data/lib/caruby/util/inflector.rb +20 -0
- data/lib/caruby/util/log.rb +95 -0
- data/lib/caruby/util/math.rb +12 -0
- data/lib/caruby/util/merge.rb +59 -0
- data/lib/caruby/util/module.rb +34 -0
- data/lib/caruby/util/options.rb +92 -0
- data/lib/caruby/util/partial_order.rb +36 -0
- data/lib/caruby/util/person.rb +119 -0
- data/lib/caruby/util/pretty_print.rb +184 -0
- data/lib/caruby/util/properties.rb +112 -0
- data/lib/caruby/util/stopwatch.rb +66 -0
- data/lib/caruby/util/topological_sync_enumerator.rb +53 -0
- data/lib/caruby/util/transitive_closure.rb +45 -0
- data/lib/caruby/util/tree.rb +48 -0
- data/lib/caruby/util/trie.rb +37 -0
- data/lib/caruby/util/uniquifier.rb +30 -0
- data/lib/caruby/util/validation.rb +48 -0
- data/lib/caruby/util/version.rb +56 -0
- data/lib/caruby/util/visitor.rb +351 -0
- data/lib/caruby/util/weak_hash.rb +36 -0
- data/lib/caruby/version.rb +3 -0
- 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
|