caruby-core 1.5.5 → 2.1.1
Sign up to get free protection for your applications and to get access to all the features.
- data/Gemfile +9 -0
- data/History.md +5 -1
- data/lib/caruby.rb +3 -5
- data/lib/caruby/caruby-src.tar.gz +0 -0
- data/lib/caruby/database.rb +53 -69
- data/lib/caruby/database/application_service.rb +25 -0
- data/lib/caruby/database/cache.rb +60 -0
- data/lib/caruby/database/fetched_matcher.rb +52 -38
- data/lib/caruby/database/lazy_loader.rb +4 -4
- data/lib/caruby/database/operation.rb +34 -0
- data/lib/caruby/database/persistable.rb +171 -86
- data/lib/caruby/database/persistence_service.rb +32 -34
- data/lib/caruby/database/persistifier.rb +100 -43
- data/lib/caruby/database/reader.rb +107 -85
- data/lib/caruby/database/reader_template_builder.rb +60 -0
- data/lib/caruby/database/saved_matcher.rb +3 -3
- data/lib/caruby/database/sql_executor.rb +88 -17
- data/lib/caruby/database/writer.rb +213 -177
- data/lib/caruby/database/writer_template_builder.rb +334 -0
- data/lib/caruby/{util → helpers}/controlled_value.rb +0 -0
- data/lib/caruby/{util → helpers}/coordinate.rb +4 -4
- data/lib/caruby/{util → helpers}/person.rb +3 -3
- data/lib/caruby/{util → helpers}/properties.rb +7 -9
- data/lib/caruby/{util → helpers}/roman.rb +2 -2
- data/lib/caruby/{util → helpers}/version.rb +1 -1
- data/lib/caruby/json/deserializer.rb +2 -2
- data/lib/caruby/json/serializer.rb +49 -7
- data/lib/caruby/metadata.rb +30 -0
- data/lib/caruby/metadata/java_property.rb +21 -0
- data/lib/caruby/metadata/propertied.rb +191 -0
- data/lib/caruby/metadata/property.rb +22 -0
- data/lib/caruby/metadata/property_characteristics.rb +201 -0
- data/lib/caruby/migration/migratable.rb +11 -182
- data/lib/caruby/rdbi/driver/jdbc.rb +446 -0
- data/lib/caruby/resource.rb +20 -823
- data/lib/caruby/version.rb +1 -1
- data/test/lib/caruby/database/cache_test.rb +54 -0
- data/test/lib/caruby/{util → helpers}/controlled_value_test.rb +3 -5
- data/test/lib/caruby/{util → helpers}/person_test.rb +4 -6
- data/test/lib/caruby/helpers/properties_test.rb +34 -0
- data/test/lib/caruby/{util → helpers}/roman_test.rb +2 -3
- data/test/lib/caruby/{util → helpers}/version_test.rb +2 -3
- data/test/lib/helper.rb +7 -0
- metadata +161 -214
- data/lib/caruby/cli/application.rb +0 -36
- data/lib/caruby/cli/command.rb +0 -202
- data/lib/caruby/csv/csv_mapper.rb +0 -159
- data/lib/caruby/csv/csvio.rb +0 -203
- data/lib/caruby/database/search_template_builder.rb +0 -56
- data/lib/caruby/database/store_template_builder.rb +0 -278
- data/lib/caruby/domain.rb +0 -193
- data/lib/caruby/domain/attribute.rb +0 -584
- data/lib/caruby/domain/attributes.rb +0 -628
- data/lib/caruby/domain/dependency.rb +0 -225
- data/lib/caruby/domain/id_alias.rb +0 -22
- data/lib/caruby/domain/importer.rb +0 -183
- data/lib/caruby/domain/introspection.rb +0 -176
- data/lib/caruby/domain/inverse.rb +0 -172
- data/lib/caruby/domain/inversible.rb +0 -90
- data/lib/caruby/domain/java_attribute.rb +0 -173
- data/lib/caruby/domain/merge.rb +0 -185
- data/lib/caruby/domain/metadata.rb +0 -142
- data/lib/caruby/domain/mixin.rb +0 -35
- data/lib/caruby/domain/properties.rb +0 -95
- data/lib/caruby/domain/reference_visitor.rb +0 -428
- data/lib/caruby/domain/uniquify.rb +0 -50
- data/lib/caruby/import/java.rb +0 -387
- data/lib/caruby/migration/migrator.rb +0 -918
- data/lib/caruby/migration/resource_module.rb +0 -9
- data/lib/caruby/migration/uniquify.rb +0 -17
- data/lib/caruby/util/attribute_path.rb +0 -44
- data/lib/caruby/util/cache.rb +0 -56
- data/lib/caruby/util/class.rb +0 -149
- data/lib/caruby/util/collection.rb +0 -1152
- data/lib/caruby/util/domain_extent.rb +0 -46
- data/lib/caruby/util/file_separator.rb +0 -65
- data/lib/caruby/util/inflector.rb +0 -27
- data/lib/caruby/util/log.rb +0 -95
- data/lib/caruby/util/math.rb +0 -12
- data/lib/caruby/util/merge.rb +0 -59
- data/lib/caruby/util/module.rb +0 -18
- data/lib/caruby/util/options.rb +0 -97
- data/lib/caruby/util/partial_order.rb +0 -35
- data/lib/caruby/util/pretty_print.rb +0 -204
- data/lib/caruby/util/stopwatch.rb +0 -74
- data/lib/caruby/util/topological_sync_enumerator.rb +0 -62
- data/lib/caruby/util/transitive_closure.rb +0 -55
- data/lib/caruby/util/tree.rb +0 -48
- data/lib/caruby/util/trie.rb +0 -37
- data/lib/caruby/util/uniquifier.rb +0 -30
- data/lib/caruby/util/validation.rb +0 -20
- data/lib/caruby/util/visitor.rb +0 -365
- data/lib/caruby/util/weak_hash.rb +0 -36
- data/test/lib/caruby/csv/csv_mapper_test.rb +0 -40
- data/test/lib/caruby/csv/csvio_test.rb +0 -69
- data/test/lib/caruby/database/persistable_test.rb +0 -92
- data/test/lib/caruby/domain/domain_test.rb +0 -112
- data/test/lib/caruby/domain/inversible_test.rb +0 -99
- data/test/lib/caruby/domain/reference_visitor_test.rb +0 -130
- data/test/lib/caruby/import/java_test.rb +0 -80
- data/test/lib/caruby/import/mixed_case_test.rb +0 -14
- data/test/lib/caruby/migration/test_case.rb +0 -102
- data/test/lib/caruby/test_case.rb +0 -230
- data/test/lib/caruby/util/cache_test.rb +0 -23
- data/test/lib/caruby/util/class_test.rb +0 -61
- data/test/lib/caruby/util/collection_test.rb +0 -398
- data/test/lib/caruby/util/command_test.rb +0 -55
- data/test/lib/caruby/util/domain_extent_test.rb +0 -60
- data/test/lib/caruby/util/file_separator_test.rb +0 -30
- data/test/lib/caruby/util/inflector_test.rb +0 -12
- data/test/lib/caruby/util/lazy_hash_test.rb +0 -34
- data/test/lib/caruby/util/merge_test.rb +0 -83
- data/test/lib/caruby/util/module_test.rb +0 -25
- data/test/lib/caruby/util/options_test.rb +0 -59
- data/test/lib/caruby/util/partial_order_test.rb +0 -42
- data/test/lib/caruby/util/pretty_print_test.rb +0 -85
- data/test/lib/caruby/util/properties_test.rb +0 -50
- data/test/lib/caruby/util/stopwatch_test.rb +0 -18
- data/test/lib/caruby/util/topological_sync_enumerator_test.rb +0 -69
- data/test/lib/caruby/util/transitive_closure_test.rb +0 -67
- data/test/lib/caruby/util/tree_test.rb +0 -23
- data/test/lib/caruby/util/trie_test.rb +0 -14
- data/test/lib/caruby/util/visitor_test.rb +0 -278
- data/test/lib/caruby/util/weak_hash_test.rb +0 -45
- data/test/lib/examples/clinical_trials/migration/migration_test.rb +0 -58
- data/test/lib/examples/clinical_trials/migration/test_case.rb +0 -38
@@ -1,142 +0,0 @@
|
|
1
|
-
require 'caruby/util/collection'
|
2
|
-
require 'caruby/import/java'
|
3
|
-
require 'caruby/domain/java_attribute'
|
4
|
-
require 'caruby/domain/introspection'
|
5
|
-
require 'caruby/domain/inverse'
|
6
|
-
require 'caruby/domain/dependency'
|
7
|
-
require 'caruby/domain/attributes'
|
8
|
-
require 'caruby/json/deserializer'
|
9
|
-
|
10
|
-
module CaRuby
|
11
|
-
module Domain
|
12
|
-
# Exception raised if a meta-data setting is missing or invalid.
|
13
|
-
class MetadataError < RuntimeError; end
|
14
|
-
|
15
|
-
# Adds introspected metadata to a Class.
|
16
|
-
module Metadata
|
17
|
-
include Introspection, Inverse, Dependency, Attributes, JSON::Deserializer
|
18
|
-
|
19
|
-
# @return [Module] the {Domain} module context
|
20
|
-
attr_accessor :domain_module
|
21
|
-
|
22
|
-
def self.extended(klass)
|
23
|
-
super
|
24
|
-
klass.class_eval do
|
25
|
-
# Add this class's metadata.
|
26
|
-
introspect
|
27
|
-
# Add the {attribute=>value} argument constructor.
|
28
|
-
class << self
|
29
|
-
def new(opts=nil)
|
30
|
-
obj = super()
|
31
|
-
obj.merge_attributes(opts) if opts
|
32
|
-
obj
|
33
|
-
end
|
34
|
-
end
|
35
|
-
end
|
36
|
-
end
|
37
|
-
|
38
|
-
# @return the domain type for attribute, or nil if attribute is not a domain attribute
|
39
|
-
def domain_type(attribute)
|
40
|
-
attr_md = attribute_metadata(attribute)
|
41
|
-
attr_md.type if attr_md.domain?
|
42
|
-
end
|
43
|
-
|
44
|
-
# Returns an empty value for the given attribute.
|
45
|
-
# * If this class is not abstract, then the empty value is the initialized value.
|
46
|
-
# * Otherwise, if the attribute is a Java primitive number then zero.
|
47
|
-
# * Otherwise, if the attribute is a Java primitive boolean then +false+.
|
48
|
-
# * Otherwise, the empty value is nil.
|
49
|
-
#
|
50
|
-
# @param [Symbol] attribute the target attribute
|
51
|
-
# @return [Numeric, Boolean, Enumerable, nil] the empty attribute value
|
52
|
-
def empty_value(attribute)
|
53
|
-
if abstract? then
|
54
|
-
attr_md = attribute_metadata(attribute)
|
55
|
-
# the Java property type
|
56
|
-
jtype = attr_md.property_descriptor.property_type if JavaAttribute === attr_md
|
57
|
-
# A primitive is either a boolean or a number (String is not primitive).
|
58
|
-
if jtype and jtype.primitive? then
|
59
|
-
type.name == 'boolean' ? false : 0
|
60
|
-
end
|
61
|
-
else
|
62
|
-
# Since this class is not abstract, create a prototype instance on demand and make
|
63
|
-
# a copy of the initialized collection value from that instance.
|
64
|
-
@prototype ||= new
|
65
|
-
value = @prototype.send(attribute) || return
|
66
|
-
value.class.new
|
67
|
-
end
|
68
|
-
end
|
69
|
-
|
70
|
-
# Prints this classifier's content to the log.
|
71
|
-
def pretty_print(q)
|
72
|
-
# the Java property descriptors
|
73
|
-
property_descriptors = java_attributes.wrap { |attr| attribute_metadata(attr).property_descriptor }
|
74
|
-
# build a map of relevant display label => attributes
|
75
|
-
prop_printer = property_descriptors.wrap { |pd| PROP_DESC_PRINTER.wrap(pd) }
|
76
|
-
prop_syms = property_descriptors.map { |pd| pd.name.to_sym }.to_set
|
77
|
-
aliases = @alias_std_attr_map.keys - attributes.to_a - prop_syms
|
78
|
-
alias_attr_hash = aliases.to_compact_hash { |aliaz| @alias_std_attr_map[aliaz] }
|
79
|
-
dependents_printer = dependent_attributes.wrap { |attr| DEPENDENT_ATTR_PRINTER.wrap(attribute_metadata(attr)) }
|
80
|
-
owner_printer = owners.wrap { |type| TYPE_PRINTER.wrap(type) }
|
81
|
-
inverses = @attributes.to_compact_hash do |attr|
|
82
|
-
attr_md = attribute_metadata(attr)
|
83
|
-
"#{attr_md.type.qp}.#{attr_md.inverse}" if attr_md.inverse
|
84
|
-
end
|
85
|
-
domain_attr_printer = domain_attributes.to_compact_hash { |attr| domain_type(attr).qp }
|
86
|
-
map = {
|
87
|
-
"Java properties" => prop_printer,
|
88
|
-
"standard attributes" => attributes,
|
89
|
-
"aliases to standard attributes" => alias_attr_hash,
|
90
|
-
"secondary key" => secondary_key_attributes,
|
91
|
-
"mandatory attributes" => mandatory_attributes,
|
92
|
-
"domain attributes" => domain_attr_printer,
|
93
|
-
"creatable domain attributes" => creatable_domain_attributes,
|
94
|
-
"updatable domain attributes" => updatable_domain_attributes,
|
95
|
-
"fetched domain attributes" => fetched_domain_attributes,
|
96
|
-
"cascaded domain attributes" => cascaded_attributes,
|
97
|
-
"owners" => owner_printer,
|
98
|
-
"owner attributes" => owner_attributes,
|
99
|
-
"inverse attributes" => inverses,
|
100
|
-
"dependent attributes" => dependents_printer,
|
101
|
-
"default values" => defaults
|
102
|
-
}.delete_if { |key, value| value.nil_or_empty? }
|
103
|
-
|
104
|
-
# one indented line per entry, all but the last line ending in a comma
|
105
|
-
content = map.map { |label, value| " #{label}=>#{format_print_value(value)}" }.join(",\n")
|
106
|
-
# print the content to the log
|
107
|
-
q.text("#{qp} structure:\n#{content}")
|
108
|
-
end
|
109
|
-
|
110
|
-
protected
|
111
|
-
|
112
|
-
def self.extend_class(klass, mod)
|
113
|
-
klass.extend(self)
|
114
|
-
klass.add_metadata(mod)
|
115
|
-
end
|
116
|
-
|
117
|
-
private
|
118
|
-
|
119
|
-
# A proc to print the unqualified class name.
|
120
|
-
TYPE_PRINTER = PrintWrapper.new { |type| type.qp }
|
121
|
-
|
122
|
-
DEPENDENT_ATTR_PRINTER = PrintWrapper.new do |attr_md|
|
123
|
-
flags = []
|
124
|
-
flags << :logical if attr_md.logical?
|
125
|
-
flags << :autogenerated if attr_md.autogenerated?
|
126
|
-
flags << :disjoint if attr_md.disjoint?
|
127
|
-
flags.empty? ? "#{attr_md}" : "#{attr_md}(#{flags.join(',')})"
|
128
|
-
end
|
129
|
-
|
130
|
-
# A proc to print the property descriptor name.
|
131
|
-
PROP_DESC_PRINTER = PrintWrapper.new { |pd| pd.name }
|
132
|
-
|
133
|
-
def format_print_value(value)
|
134
|
-
case value
|
135
|
-
when String then value
|
136
|
-
when Class then value.qp
|
137
|
-
else value.pp_s(:single_line)
|
138
|
-
end
|
139
|
-
end
|
140
|
-
end
|
141
|
-
end
|
142
|
-
end
|
data/lib/caruby/domain/mixin.rb
DELETED
@@ -1,35 +0,0 @@
|
|
1
|
-
require 'caruby/domain/metadata'
|
2
|
-
|
3
|
-
module CaRuby
|
4
|
-
module Domain
|
5
|
-
# Mixin extends a module to add meta-data to included classes.
|
6
|
-
module Mixin
|
7
|
-
# Adds {Metadata} to an included class.
|
8
|
-
#
|
9
|
-
# @example
|
10
|
-
# module CaRuby
|
11
|
-
# module Resource
|
12
|
-
# def self.included(mod)
|
13
|
-
# mod.extend(Domain::Mixin)
|
14
|
-
# end
|
15
|
-
# end
|
16
|
-
# end
|
17
|
-
# module ClinicalTrials
|
18
|
-
# module Resource
|
19
|
-
# include CaRuby::Resource
|
20
|
-
# end
|
21
|
-
# class Subject
|
22
|
-
# include Resource #=> introspects the Subject meta-data
|
23
|
-
# end
|
24
|
-
# end
|
25
|
-
#
|
26
|
-
# @param [Module] class_or_module the included module, usually a class
|
27
|
-
def included(class_or_module)
|
28
|
-
super
|
29
|
-
if Class === class_or_module then
|
30
|
-
Metadata.ensure_metadata_introspected(class_or_module)
|
31
|
-
end
|
32
|
-
end
|
33
|
-
end
|
34
|
-
end
|
35
|
-
end
|
@@ -1,95 +0,0 @@
|
|
1
|
-
require 'caruby/util/properties'
|
2
|
-
require 'caruby/util/collection'
|
3
|
-
require 'caruby/util/options'
|
4
|
-
|
5
|
-
module CaRuby
|
6
|
-
module Domain
|
7
|
-
# CaRuby::Domain::Properties specializes the generic CaRuby::Properties class for domain properties.
|
8
|
-
class Properties < CaRuby::Properties
|
9
|
-
|
10
|
-
# The application login userid environment variable.
|
11
|
-
USER_ENV_VAR_SUFFIX = 'USER'
|
12
|
-
|
13
|
-
# The application login password environment variable.
|
14
|
-
PASSWORD_ENV_VAR_SUFFIX = 'PASSWORD'
|
15
|
-
|
16
|
-
# The application service user property name.
|
17
|
-
USER_PROP = :user
|
18
|
-
|
19
|
-
# The application service password property name.
|
20
|
-
PASSWORD_PROP = :password
|
21
|
-
|
22
|
-
# The application Java jar location.
|
23
|
-
PATH_PROP = :path
|
24
|
-
|
25
|
-
attr_reader :application
|
26
|
-
|
27
|
-
# Creates a new Properties.
|
28
|
-
#
|
29
|
-
# Supported options include the CaRuby::Properties options as well as the following:
|
30
|
-
# * :application - the application name
|
31
|
-
#
|
32
|
-
# The application name is used as a prefix for application-specific upper-case environment variables
|
33
|
-
# and lower-case file names, e.g. +CATISSUE_USER+ for the +caTissue+ application login username
|
34
|
-
# environment variable. The default application name is +caBIG+.
|
35
|
-
#
|
36
|
-
# The user properties file is always loaded if it exists. This file's name is a period followed by the
|
37
|
-
# lower-case application name, located in the home directory, e.g. +~/.catissue.yaml+ for application
|
38
|
-
# +caTissue+.
|
39
|
-
def initialize(file=nil, options=nil)
|
40
|
-
@application = Options.get(:application, options, "caBIG")
|
41
|
-
super(file, options)
|
42
|
-
end
|
43
|
-
|
44
|
-
# Loads the properties in the following low-to-high precedence order:
|
45
|
-
# * the home file +.+_application_+.yaml+, where _application_ is the application name
|
46
|
-
# * the given property file
|
47
|
-
# * the environment variables
|
48
|
-
def load_properties(file)
|
49
|
-
# canonicalize the file path
|
50
|
-
file = File.expand_path(file)
|
51
|
-
# load the home properties file, if it exists
|
52
|
-
user_file = File.expand_path("~/.#{@application.downcase}.yaml")
|
53
|
-
super(user_file) if user_file != file and File.exists?(user_file)
|
54
|
-
# load the given file
|
55
|
-
super(file)
|
56
|
-
# the environment variables take precedence
|
57
|
-
load_environment_properties
|
58
|
-
# validate the required properties
|
59
|
-
validate_properties
|
60
|
-
end
|
61
|
-
|
62
|
-
private
|
63
|
-
|
64
|
-
def load_environment_properties
|
65
|
-
user = ENV[user_env_var]
|
66
|
-
if user then
|
67
|
-
self[USER_PROP] = user
|
68
|
-
logger.info("#{@application} login user obtained from environment property #{user_env_var} value '#{user}'.")
|
69
|
-
end
|
70
|
-
password = ENV[password_env_var]
|
71
|
-
if password then
|
72
|
-
self[PASSWORD_PROP] = password
|
73
|
-
logger.info("#{@application} login password obtained from environment property #{password_env_var} value.")
|
74
|
-
end
|
75
|
-
path = ENV[path_env_var]
|
76
|
-
if path then
|
77
|
-
self[PATH_PROP] = path
|
78
|
-
logger.info("#{@application} Java library path obtained from environment property #{path_env_var} value '#{path}'.")
|
79
|
-
end
|
80
|
-
end
|
81
|
-
|
82
|
-
def user_env_var
|
83
|
-
"#{@application}_#{USER_PROP}".upcase
|
84
|
-
end
|
85
|
-
|
86
|
-
def password_env_var
|
87
|
-
"#{@application}_#{PASSWORD_PROP}".upcase
|
88
|
-
end
|
89
|
-
|
90
|
-
def path_env_var
|
91
|
-
"#{@application}_#{PATH_PROP}".upcase
|
92
|
-
end
|
93
|
-
end
|
94
|
-
end
|
95
|
-
end
|
@@ -1,428 +0,0 @@
|
|
1
|
-
require 'enumerator'
|
2
|
-
require 'generator'
|
3
|
-
require 'caruby/util/options'
|
4
|
-
require 'caruby/util/collection'
|
5
|
-
require 'caruby/util/validation'
|
6
|
-
require 'caruby/util/visitor'
|
7
|
-
require 'caruby/util/math'
|
8
|
-
|
9
|
-
module CaRuby
|
10
|
-
# A ReferenceVisitor traverses reference attributes.
|
11
|
-
class ReferenceVisitor < Visitor
|
12
|
-
private
|
13
|
-
|
14
|
-
# Flag to print a detailed debugging visit message
|
15
|
-
DETAIL_DEBUG = false
|
16
|
-
|
17
|
-
public
|
18
|
-
|
19
|
-
attr_reader :ref_attr_hash
|
20
|
-
|
21
|
-
# Creates a new ReferenceVisitor on domain reference attributes.
|
22
|
-
#
|
23
|
-
# If a selector block is given to this initializer, then the reference attributes to visit
|
24
|
-
# are determined by calling the block. Otherwise, the {Domain::Attributes#saved_domain_attributes}
|
25
|
-
# are visited.
|
26
|
-
#
|
27
|
-
# @param options (see Visitor#initialize)
|
28
|
-
# @yield [ref] selects which attributes to visit next
|
29
|
-
# @yieldparam [Resource] ref the currently visited domain object
|
30
|
-
def initialize(options=nil, &selector)
|
31
|
-
raise ArgumentError.new("Reference visitor missing domain reference selector") unless block_given?
|
32
|
-
@selector = selector
|
33
|
-
# delegate to Visitor with the visit selector block
|
34
|
-
super { |parent| references_to_visit(parent) }
|
35
|
-
# the visited reference => parent attribute hash
|
36
|
-
@ref_attr_hash = {}
|
37
|
-
# TODO - reconcile @excludes here with Visitor exclude.
|
38
|
-
# refactor usage and interaction with prune_cycles.
|
39
|
-
# eliminate if possible.
|
40
|
-
@excludes = []
|
41
|
-
end
|
42
|
-
|
43
|
-
# @return [Symbol, nil] the parent attribute which was visited to get to the current visited domain object
|
44
|
-
def attribute
|
45
|
-
@ref_attr_hash[current]
|
46
|
-
end
|
47
|
-
|
48
|
-
# Excludes obj from the next visit.
|
49
|
-
# Exclusions are cleared after visit is completed.
|
50
|
-
def exclude(obj)
|
51
|
-
@excludes << obj
|
52
|
-
end
|
53
|
-
|
54
|
-
# Performs Visitor::visit and clears exclusions.
|
55
|
-
def visit(obj)
|
56
|
-
if DETAIL_DEBUG then
|
57
|
-
logger.debug { "Visiting #{obj.qp} from navigation path #{lineage.qp}..." }
|
58
|
-
end
|
59
|
-
|
60
|
-
# TODO - current, attribute and parent are nil when a value is expected.
|
61
|
-
# Uncomment below, build test cases, analyze and fix.
|
62
|
-
#
|
63
|
-
# puts "Visit #{obj.qp} current: #{self.current.qp} parent: #{self.parent.qp} attribute: #{self.attribute}"
|
64
|
-
# puts " lineage:#{lineage.qp}n attributes:#{@ref_attr_hash.qp}"
|
65
|
-
# puts " reference => attribute hash: #{@ref_attr_hash.qp}"
|
66
|
-
|
67
|
-
result = super
|
68
|
-
@excludes.clear
|
69
|
-
result
|
70
|
-
end
|
71
|
-
|
72
|
-
# Adds a default matcher block if necessary and delegates to {Visitor#sync}. The default matcher block
|
73
|
-
# calls {Resource#match_in} to match the candidate domain objects to visit.
|
74
|
-
#
|
75
|
-
# @yield [ref, others] matches ref in others (optional)
|
76
|
-
# @yieldparam [Resource] ref the domain object to match
|
77
|
-
# @yieldparam [<Resource>] the candidates for matching ref
|
78
|
-
def sync(&matcher)
|
79
|
-
MatchVisitor.new(:matcher => matcher, &@selector)
|
80
|
-
end
|
81
|
-
|
82
|
-
protected
|
83
|
-
|
84
|
-
def clear
|
85
|
-
super
|
86
|
-
@ref_attr_hash.clear
|
87
|
-
end
|
88
|
-
|
89
|
-
private
|
90
|
-
|
91
|
-
# @param [Resource] parent the referencing domain object
|
92
|
-
# @return [<Resource>] the domain attributes to visit next
|
93
|
-
def attributes_to_visit(parent)
|
94
|
-
@selector.call(parent)
|
95
|
-
end
|
96
|
-
|
97
|
-
# @param [Resource] parent the referencing domain object
|
98
|
-
# @return [<Resource>] the referenced domain objects to visit next for the given parent
|
99
|
-
def references_to_visit(parent)
|
100
|
-
attrs = attributes_to_visit(parent)
|
101
|
-
if attrs.nil? then return Array::EMPTY_ARRAY end
|
102
|
-
refs = []
|
103
|
-
attrs.each do | attr|
|
104
|
-
# the reference(s) to visit
|
105
|
-
value = parent.send(attr)
|
106
|
-
# associate each reference to visit with the current visited attribute
|
107
|
-
value.enumerate do |ref|
|
108
|
-
@ref_attr_hash[ref] = attr
|
109
|
-
refs << ref
|
110
|
-
end
|
111
|
-
end
|
112
|
-
if DETAIL_DEBUG then
|
113
|
-
logger.debug { "Visiting #{parent.qp} references: #{refs.qp}" }
|
114
|
-
logger.debug { " lineage: #{lineage.qp}" }
|
115
|
-
logger.debug { " attributes: #{attrs.qp}..." }
|
116
|
-
end
|
117
|
-
|
118
|
-
refs
|
119
|
-
end
|
120
|
-
end
|
121
|
-
|
122
|
-
# A MatchVisitor visits two domain objects' visitable attributes transitive closure in lock-step.
|
123
|
-
class MatchVisitor < ReferenceVisitor
|
124
|
-
|
125
|
-
attr_reader :matches
|
126
|
-
|
127
|
-
# Creates a new visitor which matches source and target domain object references.
|
128
|
-
# The domain attributes to visit are determined by calling the selector block given to
|
129
|
-
# this initializer. The selector arguments consist of the match source and target.
|
130
|
-
#
|
131
|
-
# @param (see ReferenceVisitor#initialize)
|
132
|
-
# @option opts [Proc] :mergeable the block which determines which attributes are merged
|
133
|
-
# @option opts [Proc] :matchable the block which determines which attributes to match
|
134
|
-
# (default is the visit selector)
|
135
|
-
# @option opts [Proc] :matcher the block which matches sources to targets
|
136
|
-
# @option opts [Proc] :copier the block which copies an unmatched source
|
137
|
-
# @yield (see ReferenceVisitor#initialize)
|
138
|
-
# @yieldparam [Resource] source the matched source object
|
139
|
-
def initialize(opts=nil)
|
140
|
-
raise ArgumentError.new("Reference visitor missing domain reference selector") unless block_given?
|
141
|
-
opts = Options.to_hash(opts)
|
142
|
-
@matcher = opts.delete(:matcher) || Resource.method(:match_all)
|
143
|
-
@matchable = opts.delete(:matchable)
|
144
|
-
@copier = opts.delete(:copier)
|
145
|
-
# the source => target matches
|
146
|
-
@matches = {}
|
147
|
-
# the class => {id => target} hash
|
148
|
-
@id_mtchs = LazyHash.new { Hash.new }
|
149
|
-
super { |src| yield(src) if @matches[src] }
|
150
|
-
end
|
151
|
-
|
152
|
-
# Visits the source and target.
|
153
|
-
#
|
154
|
-
# If a block is given to this method, then this method returns the evaluation of the block on the visited
|
155
|
-
# source reference and its matching copy, if any. The default return value is the target which matches
|
156
|
-
# source.
|
157
|
-
#
|
158
|
-
# caCORE alert = caCORE does not enforce reference identity integrity, i.e. a search on object _a_
|
159
|
-
# with database record references _a_ => _b_ => _a_, the search result might be _a_ => _b_ => _a'_,
|
160
|
-
# where _a.identifier_ == _a'.identifier_. This visit method remedies this caCORE defect by matching
|
161
|
-
# source references on a previously matched identifier where possible.
|
162
|
-
#
|
163
|
-
# @param [Resource] source the match visit source
|
164
|
-
# @param [Resource] target the match visit target
|
165
|
-
# @yield [target, source] the optional block to call on the matched source and target
|
166
|
-
# @yieldparam [Resource] source the visited source domain object
|
167
|
-
# @yieldparam [Resource] target the domain object which matches the visited source
|
168
|
-
def visit(source, target, &block)
|
169
|
-
# clear the match hashes
|
170
|
-
@matches.clear
|
171
|
-
@id_mtchs.clear
|
172
|
-
# seed the matches with the top-level source => target
|
173
|
-
add_match(source, target)
|
174
|
-
# visit the source reference. the visit block merges each source reference into
|
175
|
-
# the matching target reference.
|
176
|
-
super(source) { |src| visit_matched(src, &block) }
|
177
|
-
end
|
178
|
-
|
179
|
-
private
|
180
|
-
|
181
|
-
# Visits the given source domain object.
|
182
|
-
#
|
183
|
-
# @param [Resource] source the match visit source
|
184
|
-
# @yield [target, source] the optional block to call on the matched source and target
|
185
|
-
# @yieldparam [Resource] source the visited source domain object
|
186
|
-
# @yieldparam [Resource] target the domain object which matches the visited source
|
187
|
-
def visit_matched(source)
|
188
|
-
tgt = match_for_visited(source)
|
189
|
-
# match the matchable references, if any
|
190
|
-
if @matchable then
|
191
|
-
attrs = @matchable.call(source) - attributes_to_visit(source)
|
192
|
-
attrs.each { |attr| match_reference(source, tgt, attr) }
|
193
|
-
end
|
194
|
-
block_given? ? yield(source, tgt) : tgt
|
195
|
-
end
|
196
|
-
|
197
|
-
# @param source (see #match_visited)
|
198
|
-
# @return [<Resource>] the domain objects referenced by the source to visit next
|
199
|
-
def references_to_visit(source)
|
200
|
-
# the source match
|
201
|
-
target = match_for_visited(source)
|
202
|
-
# the attributes to visit
|
203
|
-
attrs = attributes_to_visit(source)
|
204
|
-
# the matched source references
|
205
|
-
match_references(source, target, attrs).keys
|
206
|
-
end
|
207
|
-
|
208
|
-
# @param source (see #match_visited)
|
209
|
-
# @return [<Resource>] the source match
|
210
|
-
# @raise [ValidationError] if there is no match
|
211
|
-
def match_for_visited(source)
|
212
|
-
target = @matches[source]
|
213
|
-
if target.nil? then raise ValidationError.new("Match visitor target not found for #{source}") end
|
214
|
-
target
|
215
|
-
end
|
216
|
-
|
217
|
-
# @param [Resource] source (see #match_visited)
|
218
|
-
# @param [Resource] target the source match
|
219
|
-
# @param [<Symbol>] attributes the attributes to match on
|
220
|
-
# @return [{Resource => Resource}] the referenced attribute matches
|
221
|
-
def match_references(source, target, attributes)
|
222
|
-
# collect the references to visit
|
223
|
-
matches = {}
|
224
|
-
attributes.each do |attr|
|
225
|
-
matches.merge!(match_reference(source, target, attr))
|
226
|
-
end
|
227
|
-
matches
|
228
|
-
end
|
229
|
-
|
230
|
-
# Matches the given source and target attribute references.
|
231
|
-
# The match is performed by this visitor's matcher Proc.
|
232
|
-
#
|
233
|
-
# @param source (see #visit)
|
234
|
-
# @param target (see #visit)
|
235
|
-
# @return [{Resource => Resource}] the referenced source => target matches
|
236
|
-
def match_reference(source, target, attribute)
|
237
|
-
srcs = source.send(attribute).to_enum
|
238
|
-
tgts = target.send(attribute).to_enum
|
239
|
-
|
240
|
-
# the match targets
|
241
|
-
mtchd_tgts = Set.new
|
242
|
-
# capture the matched targets and the the unmatched sources
|
243
|
-
unmtchd_srcs = srcs.reject do |src|
|
244
|
-
# the prior match, if any
|
245
|
-
tgt = match_for(src)
|
246
|
-
mtchd_tgts << tgt if tgt
|
247
|
-
end
|
248
|
-
|
249
|
-
# the unmatched targets
|
250
|
-
unmtchd_tgts = tgts.difference(mtchd_tgts)
|
251
|
-
# match the residual targets and sources
|
252
|
-
rsd_mtchs = @matcher.call(unmtchd_srcs, unmtchd_tgts)
|
253
|
-
# add residual matches
|
254
|
-
rsd_mtchs.each { |src, tgt| add_match(src, tgt) }
|
255
|
-
|
256
|
-
# The source => target match hash.
|
257
|
-
# If there is a copier, then copy each unmatched source.
|
258
|
-
matches = srcs.to_compact_hash { |src| match_for(src) or copy_unmatched(src) }
|
259
|
-
logger.debug { "Match visitor matched #{matches.qp}." } unless matches.empty?
|
260
|
-
|
261
|
-
matches
|
262
|
-
end
|
263
|
-
|
264
|
-
# @return the target matching the given source
|
265
|
-
def match_for(source)
|
266
|
-
@matches[source] or identifier_match(source)
|
267
|
-
end
|
268
|
-
|
269
|
-
def add_match(source, target)
|
270
|
-
@matches[source] = target
|
271
|
-
@id_mtchs[source.class][source.identifier] = target if source.identifier
|
272
|
-
target
|
273
|
-
end
|
274
|
-
|
275
|
-
# @return the target matching the given source on the identifier, if any
|
276
|
-
def identifier_match(source)
|
277
|
-
tgt = @id_mtchs[source.class][source.identifier] if source.identifier
|
278
|
-
@matches[source] = tgt if tgt
|
279
|
-
end
|
280
|
-
|
281
|
-
# @return [Resource, nil] a copy of the given source if this ReferenceVisitor has a copier,
|
282
|
-
# nil otherwise
|
283
|
-
def copy_unmatched(source)
|
284
|
-
return unless @copier
|
285
|
-
copy = @copier.call(source)
|
286
|
-
add_match(source, copy)
|
287
|
-
end
|
288
|
-
end
|
289
|
-
|
290
|
-
# A MergeVisitor merges a domain object's visitable attributes transitive closure into a target.
|
291
|
-
class MergeVisitor < MatchVisitor
|
292
|
-
# Creates a new MergeVisitor on domain attributes.
|
293
|
-
# The domain attributes to visit are determined by calling the selector block given to
|
294
|
-
# this initializer as described in {ReferenceVisitor#initialize}.
|
295
|
-
#
|
296
|
-
# @param (see MatchVisitor#initialize)
|
297
|
-
# @option opts [Proc] :mergeable the block which determines which attributes are merged
|
298
|
-
# @option opts [Proc] :matcher the block which matches sources to targets
|
299
|
-
# @option opts [Proc] :copier the block which copies an unmatched source
|
300
|
-
# @yield (see MatchVisitor#initialize)
|
301
|
-
# @yieldparam (see MatchVisitor#initialize)
|
302
|
-
def initialize(opts=nil, &selector)
|
303
|
-
opts = Options.to_hash(opts)
|
304
|
-
# Merge is depth-first, since the source references must be matched, and created if necessary,
|
305
|
-
# before they can be merged into the target.
|
306
|
-
opts[:depth_first] = true
|
307
|
-
@mergeable = opts.delete(:mergeable) || selector
|
308
|
-
# each mergeable attribute is matchable
|
309
|
-
unless @mergeable == selector then
|
310
|
-
opts[:matchable] = @mergeable
|
311
|
-
end
|
312
|
-
super
|
313
|
-
end
|
314
|
-
|
315
|
-
# Visits the source and target and returns a recursive copy of obj and each of its visitable references.
|
316
|
-
#
|
317
|
-
# If a block is given to this method, then this method returns the evaluation of the block on the visited
|
318
|
-
# source reference and its matching copy, if any. The default return value is the target which matches
|
319
|
-
# source.
|
320
|
-
#
|
321
|
-
# caCORE alert = caCORE does not enforce reference identity integrity, i.e. a search on object _a_
|
322
|
-
# with database record references _a_ => _b_ => _a_, the search result might be _a_ => _b_ => _a'_,
|
323
|
-
# where _a.identifier_ == _a'.identifier_. This visit method remedies the caCORE defect by matching source
|
324
|
-
# references on a previously matched identifier where possible.
|
325
|
-
#
|
326
|
-
# @param [Resource] source the domain object to merge from
|
327
|
-
# @param [Resource] target the domain object to merge into
|
328
|
-
# @yield [target, source] the optional block to call on the visited source domain object and its matching target
|
329
|
-
# @yieldparam [Resource] target the domain object which matches the visited source
|
330
|
-
# @yieldparam [Resource] source the visited source domain object
|
331
|
-
def visit(source, target)
|
332
|
-
# visit the source reference. the visit block merges each source reference into
|
333
|
-
# the matching target reference.
|
334
|
-
super(source, target) do |src, tgt|
|
335
|
-
merge(src, tgt)
|
336
|
-
block_given? ? yield(src, tgt) : tgt
|
337
|
-
end
|
338
|
-
end
|
339
|
-
|
340
|
-
private
|
341
|
-
|
342
|
-
# Merges the given source object into the target object.
|
343
|
-
#
|
344
|
-
# @param [Resource] source the domain object to merge from
|
345
|
-
# @param [Resource] target the domain object to merge into
|
346
|
-
# @return [Resource] the merged target
|
347
|
-
def merge(source, target)
|
348
|
-
# trivial case
|
349
|
-
return target if source.equal?(target)
|
350
|
-
# the domain attributes to merge
|
351
|
-
attrs = @mergeable.call(source)
|
352
|
-
logger.debug { format_merge_log_message(source, target, attrs) }
|
353
|
-
# merge the non-domain attributes
|
354
|
-
target.merge_attributes(source)
|
355
|
-
# merge the source domain attributes into the target
|
356
|
-
target.merge(source, attrs, @matches)
|
357
|
-
end
|
358
|
-
|
359
|
-
# @param source (see #merge)
|
360
|
-
# @param target (see #merge)
|
361
|
-
# @param attributes (see Mergeable#merge)
|
362
|
-
# @return [String] the log message
|
363
|
-
def format_merge_log_message(source, target, attributes)
|
364
|
-
attr_clause = " including domain attributes #{attributes.to_series}" unless attributes.empty?
|
365
|
-
"Merging #{source.qp} into #{target.qp}#{attr_clause}..."
|
366
|
-
end
|
367
|
-
end
|
368
|
-
|
369
|
-
# A CopyVisitor copies a domain object's visitable attributes transitive closure.
|
370
|
-
class CopyVisitor < MergeVisitor
|
371
|
-
# Creates a new CopyVisitor with the options described in {MergeVisitor#initialize}.
|
372
|
-
# The default :copier option is {Resource#copy}.
|
373
|
-
#
|
374
|
-
# @param (see MergeVisitor#initialize)
|
375
|
-
# @option opts [Proc] :mergeable the mergeable domain attribute selector
|
376
|
-
# @option opts [Proc] :matcher the match block
|
377
|
-
# @option opts [Proc] :copier the unmatched source copy block
|
378
|
-
# @yield (see MergeVisitor#initialize)
|
379
|
-
# @yieldparam (see MergeVisitor#initialize)
|
380
|
-
def initialize(opts=nil)
|
381
|
-
opts = Options.to_hash(opts)
|
382
|
-
opts[:copier] ||= Proc.new { |src| src.copy }
|
383
|
-
# no match forces a copy
|
384
|
-
opts[:matcher] = Proc.new { Hash::EMPTY_HASH }
|
385
|
-
super
|
386
|
-
end
|
387
|
-
|
388
|
-
# Visits obj and returns a recursive copy of obj and each of its visitable references.
|
389
|
-
#
|
390
|
-
# If a block is given to this method, then the block is called with the visited
|
391
|
-
# source reference and its matching copy target.
|
392
|
-
#
|
393
|
-
# @param (see MergeVisitor#visit)
|
394
|
-
# @yield (see MergeVisitor#visit)
|
395
|
-
# @yieldparam (see MergeVisitor#visit)
|
396
|
-
def visit(source)
|
397
|
-
target = @copier.call(source)
|
398
|
-
super(source, target)
|
399
|
-
end
|
400
|
-
end
|
401
|
-
|
402
|
-
# A ReferencePathVisitorFactory creates a ReferenceVisitor that traverses an attributes path.
|
403
|
-
#
|
404
|
-
# For example, given the attributes:
|
405
|
-
# treatment: BioMaterial -> Treatment
|
406
|
-
# measurement: Treatment -> BioMaterial
|
407
|
-
# then a path visitor given by:
|
408
|
-
# ReferencePathVisitorFactory.create(BioMaterial, [:treatment, :measurement])
|
409
|
-
# visits all biomaterial, treatments and measurements derived directly or indirectly from a starting BioMaterial instance.
|
410
|
-
class ReferencePathVisitorFactory
|
411
|
-
# @return a new ReferenceVisitor that visits the given path attributes starting at an instance of type
|
412
|
-
def self.create(type, attributes, options=nil)
|
413
|
-
# augment the attributes path as a [class, attribute] path
|
414
|
-
path = []
|
415
|
-
attributes.each do |attr|
|
416
|
-
path << [type, attr]
|
417
|
-
type = type.domain_type(attr)
|
418
|
-
end
|
419
|
-
|
420
|
-
# make the visitor
|
421
|
-
visitor = ReferenceVisitor.new(options) do |ref|
|
422
|
-
# collect the path reference attributes whose source match the ref type up to the next position in the path
|
423
|
-
max = visitor.lineage.size.min(path.size)
|
424
|
-
(0...max).map { |i| path[i].last if ref.class == path[i].first }.compact
|
425
|
-
end
|
426
|
-
end
|
427
|
-
end
|
428
|
-
end
|