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,160 @@
|
|
1
|
+
require 'caruby/util/inflector'
|
2
|
+
require 'caruby/domain/attribute_metadata'
|
3
|
+
|
4
|
+
module CaRuby
|
5
|
+
# The attribute metadata for an introspected Java property.
|
6
|
+
class JavaAttributeMetadata < AttributeMetadata
|
7
|
+
|
8
|
+
# This attribute's Java property descriptor.
|
9
|
+
attr_reader :property_descriptor
|
10
|
+
|
11
|
+
# This attribute's Java property [reader, writer] accessors, e.g. +[:getActivityStatus, :setActivityStatus]+.
|
12
|
+
attr_reader :property_accessors
|
13
|
+
|
14
|
+
# Creates a Ruby Attribute symbol corresponding to the given Ruby Java class wrapper klazz
|
15
|
+
# and Java property_descriptor.
|
16
|
+
#
|
17
|
+
# The attribute name is the lower-case, underscore property descriptor name with the alterations
|
18
|
+
# described in {JavaAttributeMetadata.to_attribute_symbol} and {Class#unocclude_reserved_method}.
|
19
|
+
#
|
20
|
+
# The attribute type is inferred as follows:
|
21
|
+
# * If the property descriptor return type is a primitive Java type, then that type is returned.
|
22
|
+
# * If the return type is a parameterized collection, then the parameter type is returned.
|
23
|
+
# * If the return type is an unparameterized collection, then this method infers the type from
|
24
|
+
# the property name, e.g. +StudyProtocolCollection+type is inferred as +StudyProtocol+
|
25
|
+
# by stripping the +Collection+ suffix, capitalizing the prefix and looking for a class of
|
26
|
+
# that name in the {ResourceMetadata#domain_module}.
|
27
|
+
# * If the declarer class metadata configuration includes a +domain_attributes+ property, then
|
28
|
+
# the type specified in that property is returned.
|
29
|
+
# * Otherwise, this method returns Java::Javalang::Object.
|
30
|
+
#
|
31
|
+
# The optional restricted_type argument restricts the attribute to a subclass of the declared
|
32
|
+
# property type.
|
33
|
+
def initialize(pd, declarer, restricted_type=nil)
|
34
|
+
symbol = create_standard_attribute_symbol(pd, declarer)
|
35
|
+
super(symbol, declarer, restricted_type)
|
36
|
+
@property_descriptor = pd
|
37
|
+
# deficient Java introspector does not recognize 'is' prefix for a Boolean property
|
38
|
+
rm = declarer.property_read_method(pd)
|
39
|
+
raise ArgumentError.new("Property does not have a read method: #{declarer.qp}.#{pd.name}") unless rm
|
40
|
+
reader = rm.name.to_sym
|
41
|
+
unless declarer.method_defined?(reader) then
|
42
|
+
reader = "is#{reader.to_s.capitalize_first}".to_sym
|
43
|
+
unless declarer.method_defined?(reader) then
|
44
|
+
raise ArgumentError.new("Reader method not found for #{declarer} property #{pd.name}")
|
45
|
+
end
|
46
|
+
end
|
47
|
+
unless pd.write_method then
|
48
|
+
raise ArgumentError.new("Property does not have a write method: #{declarer.qp}.#{pd.name}")
|
49
|
+
end
|
50
|
+
writer = pd.write_method.name.to_sym
|
51
|
+
unless declarer.method_defined?(writer) then
|
52
|
+
raise ArgumentError.new("Writer method not found for #{declarer} property #{pd.name}")
|
53
|
+
end
|
54
|
+
@property_accessors = [reader, writer]
|
55
|
+
qualify(:collection) if collection_java_class?
|
56
|
+
end
|
57
|
+
|
58
|
+
def type
|
59
|
+
@type ||= infer_type
|
60
|
+
end
|
61
|
+
|
62
|
+
# Returns a lower-case, underscore symbol for the given property_name.
|
63
|
+
# A name ending in 'Collection' is changed to a pluralization.
|
64
|
+
#
|
65
|
+
# @example
|
66
|
+
# JavaAttributeMetadata.to_attribute_symbol('specimenEventCollection') #=> :specimen_events
|
67
|
+
def self.to_attribute_symbol(property_name)
|
68
|
+
name = if property_name =~ /(.+)Collection$/ then
|
69
|
+
property_name[0...-'Collection'.length].pluralize.underscore
|
70
|
+
else
|
71
|
+
property_name.underscore
|
72
|
+
end
|
73
|
+
name.to_sym
|
74
|
+
end
|
75
|
+
|
76
|
+
private
|
77
|
+
|
78
|
+
# @param pd the Java property descriptor
|
79
|
+
# @param [Class] klass the declarer
|
80
|
+
# @return [String] the lower-case, underscore symbol for the given property descriptor
|
81
|
+
def create_standard_attribute_symbol(pd, klass)
|
82
|
+
propname = pd.name
|
83
|
+
name = propname.underscore
|
84
|
+
renamed = klass.unocclude_reserved_method(pd)
|
85
|
+
if renamed then
|
86
|
+
logger.debug { "Renamed #{klass.qp} reserved Ruby method #{name} to #{renamed}." }
|
87
|
+
renamed
|
88
|
+
else
|
89
|
+
JavaAttributeMetadata.to_attribute_symbol(propname)
|
90
|
+
end
|
91
|
+
end
|
92
|
+
|
93
|
+
# Returns whether java_class is an +Iterable+.
|
94
|
+
def collection_java_class?
|
95
|
+
@property_descriptor.property_type.interfaces.any? { |xfc| xfc.java_object == Java::JavaLang::Iterable.java_class }
|
96
|
+
end
|
97
|
+
|
98
|
+
# Returns the type for the specified klass property descriptor pd as described in {#initialize}.
|
99
|
+
def infer_type
|
100
|
+
collection_java_class? ? infer_collection_type : infer_non_collection_type
|
101
|
+
end
|
102
|
+
|
103
|
+
# Returns the domain type for this attribute's Java Collection property descriptor.
|
104
|
+
# If the property type is parameterized by a single domain class, then that generic type argument is the domain type.
|
105
|
+
# Otherwise, the type is inferred from the property name as described in {#infer_collection_type_from_name}.
|
106
|
+
def infer_collection_type
|
107
|
+
generic_parameter_type or infer_collection_type_from_name or Java::JavaLang::Object
|
108
|
+
end
|
109
|
+
|
110
|
+
def infer_non_collection_type
|
111
|
+
prop_type = @property_descriptor.property_type
|
112
|
+
if prop_type.primitive then
|
113
|
+
Class.to_ruby(prop_type)
|
114
|
+
else
|
115
|
+
@declarer.domain_module.domain_type_with_name(prop_type.name) or Class.to_ruby(prop_type)
|
116
|
+
end
|
117
|
+
end
|
118
|
+
|
119
|
+
def configured_type
|
120
|
+
name = @declarer.class.configuration.domain_type_name(to_sym) || return
|
121
|
+
@declarer.domain_module.domain_type_with_name(name) or java_to_ruby_class(name)
|
122
|
+
end
|
123
|
+
|
124
|
+
# Returns the domain type of this attribute's property descriptor Collection generic type argument, or nil if none.
|
125
|
+
def generic_parameter_type
|
126
|
+
method = @property_descriptor.readMethod || return
|
127
|
+
prop_type = method.genericReturnType
|
128
|
+
return unless Java::JavaLangReflect::ParameterizedType === prop_type
|
129
|
+
arg_types = prop_type.actualTypeArguments
|
130
|
+
return unless arg_types.size == 1
|
131
|
+
arg_type = arg_types[0]
|
132
|
+
klass = java_to_ruby_class(arg_type)
|
133
|
+
logger.debug { "Inferred #{declarer.qp} #{self} domain type #{klass.qp} from generic parameter #{arg_type.name}." } if klass
|
134
|
+
klass
|
135
|
+
end
|
136
|
+
|
137
|
+
def java_to_ruby_class(java_type)
|
138
|
+
java_type = java_type.name unless String === java_type
|
139
|
+
@declarer.domain_module.domain_type_with_name(java_type) or Class.to_ruby(java_type)
|
140
|
+
end
|
141
|
+
|
142
|
+
# Returns the domain type for this attribute's collection Java property descriptor name.
|
143
|
+
# By convention, caBIG domain collection properties often begin with a domain type
|
144
|
+
# name and end in 'Collection'. This method strips the Collection suffix and checks
|
145
|
+
# whether the prefix is a domain class.
|
146
|
+
#
|
147
|
+
# For example, the type of the property named +distributionProtocolCollection+
|
148
|
+
# is inferred as +DistributionProtocol+ by stripping the +Collection+ suffix,
|
149
|
+
# capitalizing the prefix and looking for a class of that name in this classifier's
|
150
|
+
# domain_module.
|
151
|
+
def infer_collection_type_from_name
|
152
|
+
prop_name = @property_descriptor.name
|
153
|
+
index = prop_name =~ /Collection$/
|
154
|
+
index ||= prop_name.length
|
155
|
+
prefix = prop_name[0...1].upcase + prop_name[1...index]
|
156
|
+
logger.debug { "Inferring #{declarer.qp} #{self} domain type from attribute name prefix #{prefix}..." }
|
157
|
+
@declarer.domain_module.domain_type_with_name(prefix)
|
158
|
+
end
|
159
|
+
end
|
160
|
+
end
|
@@ -0,0 +1,91 @@
|
|
1
|
+
module CaRuby
|
2
|
+
# A Mergeable supports merging attribute values.
|
3
|
+
module Mergeable
|
4
|
+
# Merges the values of the other attributes into this object and returns self.
|
5
|
+
# The other argument can be either a Hash or an object whose class responds to the
|
6
|
+
# +mergeable_attributes+ method.
|
7
|
+
# The optional attributes argument can be either a single attribute symbol or a
|
8
|
+
# collection of attribute symbols.
|
9
|
+
#
|
10
|
+
# A hash argument consists of attribute name => value associations.
|
11
|
+
# For example, given a Mergeable +person+ object with attributes +ssn+ and +children+, the call:
|
12
|
+
# person.merge_attributes(:ssn => '555-55-5555', :children => children)
|
13
|
+
# is equivalent to:
|
14
|
+
# person.ssn ||= '555-55-5555'
|
15
|
+
# person.children ||= []
|
16
|
+
# person.children.merge(children, :deep)
|
17
|
+
# An unrecognized attribute is ignored.
|
18
|
+
#
|
19
|
+
# If other is not a Hash, then the other object's attributes values are merged into
|
20
|
+
# this object. The default attributes is the intersection of this object's
|
21
|
+
# mergeable attributes and the other object's mergeable attributes as determined by
|
22
|
+
# {ResourceAttributes#mergeable_attributes}.
|
23
|
+
#
|
24
|
+
# #merge_attribute is called on each attribute with the merger block given to this
|
25
|
+
# method.
|
26
|
+
#
|
27
|
+
# @param [Mergeable, {Symbol => Object}] other the source domain object or value hash to merge from
|
28
|
+
# @param [<Symbol>, nil] attributes the attributes to merge (default {ResourceAttributes#nondomain_attributes})
|
29
|
+
# @return [Mergeable] self
|
30
|
+
# @raise [ArgumentError] if none of the following are true:
|
31
|
+
# * other is a Hash
|
32
|
+
# * attributes is non-nil
|
33
|
+
# * the other class responds to +mergeable_attributes+
|
34
|
+
def merge_attributes(other, attributes=nil, &merger) # :yields: attribute, oldval, newval
|
35
|
+
return self if other.nil? or other.equal?(self)
|
36
|
+
attributes = [attributes] if Symbol === attributes
|
37
|
+
attributes ||= self.class.mergeable_attributes
|
38
|
+
|
39
|
+
# if the source object is not a hash, then convert it to an attribute => value hash
|
40
|
+
vh = Hashable === other ? other : other.value_hash(attributes)
|
41
|
+
# merge the value hash
|
42
|
+
suspend_lazy_loader do
|
43
|
+
vh.each { |attr, value| merge_attribute(attr, value, &merger) }
|
44
|
+
end
|
45
|
+
self
|
46
|
+
end
|
47
|
+
|
48
|
+
alias :merge :merge_attributes
|
49
|
+
|
50
|
+
alias :merge! :merge
|
51
|
+
|
52
|
+
# Merges value into attribute as follows:
|
53
|
+
# * if the value is nil, empty or equal to the current attribute value, then no merge
|
54
|
+
# is performed
|
55
|
+
# * otherwise, if the merger block is given to this method, then that block is called
|
56
|
+
# to perform the merge
|
57
|
+
# * otherwise, if the current value responds to the merge! method, then that method
|
58
|
+
# is called recursively on the current value
|
59
|
+
# * otherwise, if the current value is nil, then the attribute is set to value
|
60
|
+
# * otherwise, no merge is performed
|
61
|
+
#
|
62
|
+
# Returns the merged value.
|
63
|
+
def merge_attribute(attribute, value, &merger) # :yields: attribute, oldval, newval
|
64
|
+
# the previous value
|
65
|
+
oldval = send(attribute)
|
66
|
+
|
67
|
+
# if nothing to merge, then return the unchanged previous value.
|
68
|
+
# otherwise, if a merge block is given, then call it.
|
69
|
+
# otherwise, if nothing to merge into then set the attribute to the new value.
|
70
|
+
# otherwise, if the previous value is mergeable, then merge the new value into it.
|
71
|
+
if value.nil_or_empty? or mergeable__equal?(oldval, value) then
|
72
|
+
oldval
|
73
|
+
elsif block_given? then
|
74
|
+
yield(attribute, oldval, value)
|
75
|
+
elsif oldval.nil? then
|
76
|
+
send("#{attribute}=", value)
|
77
|
+
elsif oldval.respond_to?(:merge!) then
|
78
|
+
oldval.merge!(value)
|
79
|
+
else
|
80
|
+
oldval
|
81
|
+
end
|
82
|
+
end
|
83
|
+
|
84
|
+
private
|
85
|
+
|
86
|
+
# Fixes a rare Java TreeSet aberration: comparison uses the TreeSet comparator rather than an element-wise comparator.
|
87
|
+
def mergeable__equal?(v1, v2)
|
88
|
+
Java::JavaUtil::TreeSet === v1 && Java::JavaUtil::TreeSet === v2 ? v1.to_set == v2.to_set : v1 == v2
|
89
|
+
end
|
90
|
+
end
|
91
|
+
end
|
@@ -0,0 +1,95 @@
|
|
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 varialables
|
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
|
@@ -0,0 +1,289 @@
|
|
1
|
+
require 'enumerator'
|
2
|
+
require 'generator'
|
3
|
+
require 'caruby/util/options'
|
4
|
+
require 'caruby/util/collection'
|
5
|
+
require 'caruby/util/visitor'
|
6
|
+
require 'caruby/util/math'
|
7
|
+
|
8
|
+
module CaRuby
|
9
|
+
# A ReferenceVisitor traverses reference attributes.
|
10
|
+
class ReferenceVisitor < Visitor
|
11
|
+
private
|
12
|
+
|
13
|
+
# Flag to print a detailed debugging visit message
|
14
|
+
DETAIL_DEBUG = false
|
15
|
+
|
16
|
+
public
|
17
|
+
|
18
|
+
attr_reader :ref_attr_hash
|
19
|
+
|
20
|
+
# Creates a new ReferenceVisitor on domain reference attributes.
|
21
|
+
#
|
22
|
+
# If a selector block is given to this initializer, then the reference attributes to visit
|
23
|
+
# are determined by calling the block. Otherwise, the {ResourceAttributes#saved_domain_attributes}
|
24
|
+
# are visited.
|
25
|
+
#
|
26
|
+
# @param options (see Visitor#initialize)
|
27
|
+
# @yield [ref] selects which attributes to visit next
|
28
|
+
# @yieldparam [Resource] ref the currently visited domain object
|
29
|
+
def initialize(options=nil, &selector)
|
30
|
+
# use the default attributes if no block given
|
31
|
+
@slctr = selector || Proc.new { |obj| obj.class.saved_domain_attributes }
|
32
|
+
# delegate to Visitor with the visit selector block
|
33
|
+
super { |parent| references_to_visit(parent) }
|
34
|
+
# the visited reference => parent attribute hash
|
35
|
+
@ref_attr_hash = {}
|
36
|
+
# TODO - reconcile @excludes here with Visitor exclude.
|
37
|
+
# refactor usage and interaction with prune_cycles.
|
38
|
+
# eliminate if possible.
|
39
|
+
@excludes = []
|
40
|
+
end
|
41
|
+
|
42
|
+
# @return [Symbol, nil] the parent attribute which was visited to get to the current visited domain object
|
43
|
+
def attribute
|
44
|
+
@ref_attr_hash[current]
|
45
|
+
end
|
46
|
+
|
47
|
+
# Excludes obj from the next visit.
|
48
|
+
# Exclusions are cleared after visit is completed.
|
49
|
+
def exclude(obj)
|
50
|
+
@excludes << obj
|
51
|
+
end
|
52
|
+
|
53
|
+
# Performs Visitor::visit and clears exclusions.
|
54
|
+
def visit(obj)
|
55
|
+
if DETAIL_DEBUG then
|
56
|
+
logger.debug { "Visiting #{obj.qp} from navigation path #{lineage.qp}..." }
|
57
|
+
end
|
58
|
+
|
59
|
+
# TODO - current, attribute and parent are nil when a value is expected.
|
60
|
+
# Uncomment below, build test cases, analyze and fix.
|
61
|
+
#
|
62
|
+
# puts "Visit #{obj.qp} current: #{self.current.qp} parent: #{self.parent.qp} attribute: #{self.attribute}"
|
63
|
+
# puts " lineage:#{lineage.qp}n attributes:#{@ref_attr_hash.qp}"
|
64
|
+
# puts " reference => attribute hash: #{@ref_attr_hash.qp}"
|
65
|
+
|
66
|
+
result = super
|
67
|
+
@excludes.clear
|
68
|
+
result
|
69
|
+
end
|
70
|
+
|
71
|
+
# Adds a default matcher block if necessary and delegates to {Visitor#sync}. The default matcher block
|
72
|
+
# calls {CaRuby::Resource#match_in} to match the candidate domain objects to visit.
|
73
|
+
#
|
74
|
+
# @yield [ref, others] matches ref in others (optional)
|
75
|
+
# @yieldparam [Resource] ref the domain object to match
|
76
|
+
# @yieldparam [<Resource>] the candidates for matching ref
|
77
|
+
def sync
|
78
|
+
block_given? ? super : super { |ref, others| ref.match_in(others) }
|
79
|
+
end
|
80
|
+
|
81
|
+
protected
|
82
|
+
|
83
|
+
def clear
|
84
|
+
super
|
85
|
+
@ref_attr_hash.clear
|
86
|
+
end
|
87
|
+
|
88
|
+
private
|
89
|
+
|
90
|
+
# @return the domain objects to visit next for the given parent
|
91
|
+
def references_to_visit(parent)
|
92
|
+
attributes = @slctr.call(parent)
|
93
|
+
if attributes.nil? then return Array::EMPTY_ARRAY end
|
94
|
+
refs = []
|
95
|
+
attributes.each do | attr|
|
96
|
+
# the reference(s) to visit
|
97
|
+
value = parent.send(attr)
|
98
|
+
# associate each reference to visit with the current visited attribute
|
99
|
+
value.enumerate do |ref|
|
100
|
+
@ref_attr_hash[ref] = attr
|
101
|
+
refs << ref
|
102
|
+
end
|
103
|
+
end
|
104
|
+
if DETAIL_DEBUG then
|
105
|
+
logger.debug { "Visiting #{parent.qp} references: #{refs.qp}" }
|
106
|
+
logger.debug { " lineage: #{lineage.qp}" }
|
107
|
+
logger.debug { " attributes: #{@ref_attr_hash.qp}..." }
|
108
|
+
end
|
109
|
+
refs
|
110
|
+
end
|
111
|
+
end
|
112
|
+
|
113
|
+
# A MergeVisitor merges a domain object's visitable attributes transitive closure into a target.
|
114
|
+
class MergeVisitor < ReferenceVisitor
|
115
|
+
|
116
|
+
attr_reader :matches
|
117
|
+
|
118
|
+
# Creates a new MergeVisitor on domain attributes.
|
119
|
+
# The domain attributes to visit are determined by calling the selector block given to
|
120
|
+
# this initializer as described in {ReferenceVisitor#initialize}.
|
121
|
+
#
|
122
|
+
# @param [Hash] options the visit options
|
123
|
+
# @option options [Proc] :mergeable the mergeable domain attribute selector
|
124
|
+
# @option options [Proc] :matcher the match block
|
125
|
+
# @option options [Proc] :copier the unmatched source copy block
|
126
|
+
# @yield [source, target] the visit domain attribute selector block
|
127
|
+
# @yieldparam [Resource] source the current merge source domain object
|
128
|
+
# @yieldparam [Resource] target the current merge target domain object
|
129
|
+
def initialize(options=nil, &selector)
|
130
|
+
raise ArgumentError.new("Reference visitor missing domain reference selector") unless block_given?
|
131
|
+
options = Options.to_hash(options)
|
132
|
+
@mergeable = options.delete(:mergeable) || selector
|
133
|
+
@matcher = options.delete(:matcher) || Resource.method(:match_all)
|
134
|
+
@copier = options.delete(:copier)
|
135
|
+
# the source => target matches
|
136
|
+
@matches = {}
|
137
|
+
# the class => {id => target} hash
|
138
|
+
@id_mtchs = LazyHash.new { Hash.new }
|
139
|
+
super do |src|
|
140
|
+
tgt = @matches[src]
|
141
|
+
yield(src, tgt) if tgt
|
142
|
+
end
|
143
|
+
end
|
144
|
+
|
145
|
+
# Visits the source and target and returns a recursive copy of obj and each of its visitable references.
|
146
|
+
#
|
147
|
+
# If a block is given to this method, then this method returns the evaluation of the block on the visited
|
148
|
+
# source reference and its matching copy, if any. The default return value is the target which matches
|
149
|
+
# source.
|
150
|
+
#
|
151
|
+
# caCORE alert = caCORE does not enforce reference identity integrity, i.e. a search on object _a_
|
152
|
+
# with database record references _a_ => _b_ => _a_, the search result might be _a_ => _b_ => _a'_,
|
153
|
+
# where _a.identifier_ == _a'.identifier_. This visit method remedies the caCORE defect by matching source
|
154
|
+
# references on a previously matched identifier where possible.
|
155
|
+
#
|
156
|
+
# @param [Resource] target the domain object to merge into
|
157
|
+
# @param [Resource] source the domain object to merge from
|
158
|
+
# @yield [target, source] the optional block to call on the visited source domain object and its matching target
|
159
|
+
# @yieldparam [Resource] target the domain object which matches the visited source
|
160
|
+
# @yieldparam [Resource] source the visited source domain object
|
161
|
+
def visit(target, source)
|
162
|
+
# clear the match hashes
|
163
|
+
@matches.clear
|
164
|
+
@id_mtchs.clear
|
165
|
+
# seed the matches with the top-level source => target
|
166
|
+
add_match(source, target)
|
167
|
+
# visit the source reference. the visit block merges each source reference into
|
168
|
+
# the matching target reference.
|
169
|
+
super(source) do |src|
|
170
|
+
tgt = match(src) || next
|
171
|
+
merge(tgt, src)
|
172
|
+
block_given? ? yield(tgt, src) : tgt
|
173
|
+
end
|
174
|
+
end
|
175
|
+
|
176
|
+
private
|
177
|
+
|
178
|
+
# Merges the given source object into the target object.
|
179
|
+
#
|
180
|
+
# @param [Resource] target thedomain object to merge into
|
181
|
+
# @param [Resource] source the domain object to merge from
|
182
|
+
def merge(target, source)
|
183
|
+
# the domain attributes to merge; non-domain attributes are always merged
|
184
|
+
attrs = @mergeable.call(source, target)
|
185
|
+
# Match each source reference to a target reference.
|
186
|
+
target.merge_match(source, attrs, &method(:match_all))
|
187
|
+
target
|
188
|
+
end
|
189
|
+
|
190
|
+
# Matches the given sources to targets. The match is performed by this visitor's matcher Proc.
|
191
|
+
#
|
192
|
+
# @param [<Resource>] sources the domain objects to match
|
193
|
+
# @param [<Resource>] targets the match candidates
|
194
|
+
# @return [{Resource => Resource}] the source => target matches
|
195
|
+
def match_all(sources, targets)
|
196
|
+
# the match targets
|
197
|
+
mtchd_tgts = Set.new
|
198
|
+
# capture the matched targets and the the unmatched sources
|
199
|
+
unmtchd_srcs = sources.reject do |src|
|
200
|
+
# the prior match, if any
|
201
|
+
tgt = match(src)
|
202
|
+
mtchd_tgts << tgt if tgt
|
203
|
+
end
|
204
|
+
# the unmatched targets
|
205
|
+
unmtchd_tgts = targets.difference(mtchd_tgts)
|
206
|
+
|
207
|
+
# match the residual targets and sources
|
208
|
+
rsd_mtchs = @matcher.call(unmtchd_srcs, unmtchd_tgts)
|
209
|
+
# add residual matches
|
210
|
+
rsd_mtchs.each { |src, tgt| add_match(src, tgt) }
|
211
|
+
# The source => target match hash.
|
212
|
+
# If there is a copier, then copy each unmatched source.
|
213
|
+
matches = sources.to_compact_hash { |src| match(src) or copy_unmatched(src) }
|
214
|
+
logger.debug { "Merge visitor matched #{matches.qp}." } unless matches.empty?
|
215
|
+
matches
|
216
|
+
end
|
217
|
+
|
218
|
+
# @return the target matching the given source
|
219
|
+
def match(source)
|
220
|
+
@matches[source] or identifier_match(source)
|
221
|
+
end
|
222
|
+
|
223
|
+
def add_match(source, target)
|
224
|
+
@matches[source] = target
|
225
|
+
@id_mtchs[source.class][source.identifier] = target if source.identifier
|
226
|
+
target
|
227
|
+
end
|
228
|
+
|
229
|
+
# @return the target matching the given source on the identifier, if any
|
230
|
+
def identifier_match(source)
|
231
|
+
tgt = @id_mtchs[source.class][source.identifier] if source.identifier
|
232
|
+
@matches[source] = tgt if tgt
|
233
|
+
end
|
234
|
+
|
235
|
+
# @return [Resource, nil] a copy of the given source if this ReferenceVisitor has a copier, nil otherwise
|
236
|
+
def copy_unmatched(source)
|
237
|
+
return unless @copier
|
238
|
+
copy = @copier.call(source)
|
239
|
+
add_match(source, copy)
|
240
|
+
end
|
241
|
+
end
|
242
|
+
|
243
|
+
# A CopyVisitor copies a domain object's visitable attributes transitive closure.
|
244
|
+
class CopyVisitor < MergeVisitor
|
245
|
+
# Creates a new CopyVisitor with the options described in {MergeVisitor#initialize}.
|
246
|
+
# The default :copier option is {Resource#copy}.
|
247
|
+
def initialize(options=nil) # :yields: source
|
248
|
+
options = Options.to_hash(options)
|
249
|
+
options[:copier] ||= Proc.new { |src| src.copy }
|
250
|
+
super
|
251
|
+
end
|
252
|
+
|
253
|
+
# Visits obj and returns a recursive copy of obj and each of its visitable references.
|
254
|
+
#
|
255
|
+
# If a block is given to this method, then the block is called with the visited
|
256
|
+
# source reference and its matching copy target.
|
257
|
+
def visit(source, &block) # :yields: target, source
|
258
|
+
target = @copier.call(source)
|
259
|
+
super(target, source, &block)
|
260
|
+
end
|
261
|
+
end
|
262
|
+
|
263
|
+
# A ReferencePathVisitorFactory creates a ReferenceVisitor that traverses an attributes path.
|
264
|
+
#
|
265
|
+
# For example, given the attributes:
|
266
|
+
# treatment: BioMaterial -> Treatment
|
267
|
+
# measurement: Treatment -> BioMaterial
|
268
|
+
# then a path visitor given by:
|
269
|
+
# ReferencePathVisitorFactory.create(BioMaterial, [:treatment, :measurement])
|
270
|
+
# visits all biomaterial, treatments and measurements derived directly or indirectly from a starting BioMaterial instance.
|
271
|
+
class ReferencePathVisitorFactory
|
272
|
+
# @return a new ReferenceVisitor that visits the given path attributes starting at an instance of type
|
273
|
+
def self.create(type, attributes, options=nil)
|
274
|
+
# augment the attributes path as a [class, attribute] path
|
275
|
+
path = []
|
276
|
+
attributes.each do |attr|
|
277
|
+
path << [type, attr]
|
278
|
+
type = type.domain_type(attr)
|
279
|
+
end
|
280
|
+
|
281
|
+
# make the visitor
|
282
|
+
visitor = ReferenceVisitor.new(options) do |ref|
|
283
|
+
# collect the path reference attributes whose source match the ref type up to the next position in the path
|
284
|
+
max = visitor.lineage.size.min(path.size)
|
285
|
+
(0...max).map { |i| path[i].last if ref.class == path[i].first }.compact
|
286
|
+
end
|
287
|
+
end
|
288
|
+
end
|
289
|
+
end
|