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