caruby-core 1.4.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- data/History.txt +4 -0
- data/LEGAL +5 -0
- data/LICENSE +22 -0
- data/README.md +51 -0
- data/doc/website/css/site.css +1 -5
- data/doc/website/images/avatar.png +0 -0
- data/doc/website/images/favicon.ico +0 -0
- data/doc/website/images/logo.png +0 -0
- data/doc/website/index.html +82 -0
- data/doc/website/install.html +87 -0
- data/doc/website/quick_start.html +87 -0
- data/doc/website/tissue.html +85 -0
- data/doc/website/uom.html +10 -0
- data/lib/caruby.rb +3 -0
- data/lib/caruby/active_support/README.txt +2 -0
- data/lib/caruby/active_support/core_ext/string.rb +7 -0
- data/lib/caruby/active_support/core_ext/string/inflections.rb +167 -0
- data/lib/caruby/active_support/inflections.rb +55 -0
- data/lib/caruby/active_support/inflector.rb +398 -0
- data/lib/caruby/cli/application.rb +36 -0
- data/lib/caruby/cli/command.rb +169 -0
- data/lib/caruby/csv/csv_mapper.rb +157 -0
- data/lib/caruby/csv/csvio.rb +185 -0
- data/lib/caruby/database.rb +252 -0
- data/lib/caruby/database/fetched_matcher.rb +66 -0
- data/lib/caruby/database/persistable.rb +432 -0
- data/lib/caruby/database/persistence_service.rb +162 -0
- data/lib/caruby/database/reader.rb +599 -0
- data/lib/caruby/database/saved_merger.rb +131 -0
- data/lib/caruby/database/search_template_builder.rb +59 -0
- data/lib/caruby/database/sql_executor.rb +75 -0
- data/lib/caruby/database/store_template_builder.rb +200 -0
- data/lib/caruby/database/writer.rb +469 -0
- data/lib/caruby/domain/annotatable.rb +25 -0
- data/lib/caruby/domain/annotation.rb +23 -0
- data/lib/caruby/domain/attribute_metadata.rb +447 -0
- data/lib/caruby/domain/java_attribute_metadata.rb +160 -0
- data/lib/caruby/domain/merge.rb +91 -0
- data/lib/caruby/domain/properties.rb +95 -0
- data/lib/caruby/domain/reference_visitor.rb +289 -0
- data/lib/caruby/domain/resource_attributes.rb +528 -0
- data/lib/caruby/domain/resource_dependency.rb +205 -0
- data/lib/caruby/domain/resource_introspection.rb +159 -0
- data/lib/caruby/domain/resource_metadata.rb +117 -0
- data/lib/caruby/domain/resource_module.rb +285 -0
- data/lib/caruby/domain/uniquify.rb +38 -0
- data/lib/caruby/import/annotatable_class.rb +28 -0
- data/lib/caruby/import/annotation_class.rb +27 -0
- data/lib/caruby/import/annotation_module.rb +67 -0
- data/lib/caruby/import/java.rb +338 -0
- data/lib/caruby/migration/migratable.rb +167 -0
- data/lib/caruby/migration/migrator.rb +533 -0
- data/lib/caruby/migration/resource.rb +8 -0
- data/lib/caruby/migration/resource_module.rb +11 -0
- data/lib/caruby/migration/uniquify.rb +20 -0
- data/lib/caruby/resource.rb +969 -0
- data/lib/caruby/util/attribute_path.rb +46 -0
- data/lib/caruby/util/cache.rb +53 -0
- data/lib/caruby/util/class.rb +99 -0
- data/lib/caruby/util/collection.rb +1053 -0
- data/lib/caruby/util/controlled_value.rb +35 -0
- data/lib/caruby/util/coordinate.rb +75 -0
- data/lib/caruby/util/domain_extent.rb +49 -0
- data/lib/caruby/util/file_separator.rb +65 -0
- data/lib/caruby/util/inflector.rb +20 -0
- data/lib/caruby/util/log.rb +95 -0
- data/lib/caruby/util/math.rb +12 -0
- data/lib/caruby/util/merge.rb +59 -0
- data/lib/caruby/util/module.rb +34 -0
- data/lib/caruby/util/options.rb +92 -0
- data/lib/caruby/util/partial_order.rb +36 -0
- data/lib/caruby/util/person.rb +119 -0
- data/lib/caruby/util/pretty_print.rb +184 -0
- data/lib/caruby/util/properties.rb +112 -0
- data/lib/caruby/util/stopwatch.rb +66 -0
- data/lib/caruby/util/topological_sync_enumerator.rb +53 -0
- data/lib/caruby/util/transitive_closure.rb +45 -0
- data/lib/caruby/util/tree.rb +48 -0
- data/lib/caruby/util/trie.rb +37 -0
- data/lib/caruby/util/uniquifier.rb +30 -0
- data/lib/caruby/util/validation.rb +48 -0
- data/lib/caruby/util/version.rb +56 -0
- data/lib/caruby/util/visitor.rb +351 -0
- data/lib/caruby/util/weak_hash.rb +36 -0
- data/lib/caruby/version.rb +3 -0
- metadata +186 -0
@@ -0,0 +1,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
|