caruby-core 1.4.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (86) hide show
  1. data/History.txt +4 -0
  2. data/LEGAL +5 -0
  3. data/LICENSE +22 -0
  4. data/README.md +51 -0
  5. data/doc/website/css/site.css +1 -5
  6. data/doc/website/images/avatar.png +0 -0
  7. data/doc/website/images/favicon.ico +0 -0
  8. data/doc/website/images/logo.png +0 -0
  9. data/doc/website/index.html +82 -0
  10. data/doc/website/install.html +87 -0
  11. data/doc/website/quick_start.html +87 -0
  12. data/doc/website/tissue.html +85 -0
  13. data/doc/website/uom.html +10 -0
  14. data/lib/caruby.rb +3 -0
  15. data/lib/caruby/active_support/README.txt +2 -0
  16. data/lib/caruby/active_support/core_ext/string.rb +7 -0
  17. data/lib/caruby/active_support/core_ext/string/inflections.rb +167 -0
  18. data/lib/caruby/active_support/inflections.rb +55 -0
  19. data/lib/caruby/active_support/inflector.rb +398 -0
  20. data/lib/caruby/cli/application.rb +36 -0
  21. data/lib/caruby/cli/command.rb +169 -0
  22. data/lib/caruby/csv/csv_mapper.rb +157 -0
  23. data/lib/caruby/csv/csvio.rb +185 -0
  24. data/lib/caruby/database.rb +252 -0
  25. data/lib/caruby/database/fetched_matcher.rb +66 -0
  26. data/lib/caruby/database/persistable.rb +432 -0
  27. data/lib/caruby/database/persistence_service.rb +162 -0
  28. data/lib/caruby/database/reader.rb +599 -0
  29. data/lib/caruby/database/saved_merger.rb +131 -0
  30. data/lib/caruby/database/search_template_builder.rb +59 -0
  31. data/lib/caruby/database/sql_executor.rb +75 -0
  32. data/lib/caruby/database/store_template_builder.rb +200 -0
  33. data/lib/caruby/database/writer.rb +469 -0
  34. data/lib/caruby/domain/annotatable.rb +25 -0
  35. data/lib/caruby/domain/annotation.rb +23 -0
  36. data/lib/caruby/domain/attribute_metadata.rb +447 -0
  37. data/lib/caruby/domain/java_attribute_metadata.rb +160 -0
  38. data/lib/caruby/domain/merge.rb +91 -0
  39. data/lib/caruby/domain/properties.rb +95 -0
  40. data/lib/caruby/domain/reference_visitor.rb +289 -0
  41. data/lib/caruby/domain/resource_attributes.rb +528 -0
  42. data/lib/caruby/domain/resource_dependency.rb +205 -0
  43. data/lib/caruby/domain/resource_introspection.rb +159 -0
  44. data/lib/caruby/domain/resource_metadata.rb +117 -0
  45. data/lib/caruby/domain/resource_module.rb +285 -0
  46. data/lib/caruby/domain/uniquify.rb +38 -0
  47. data/lib/caruby/import/annotatable_class.rb +28 -0
  48. data/lib/caruby/import/annotation_class.rb +27 -0
  49. data/lib/caruby/import/annotation_module.rb +67 -0
  50. data/lib/caruby/import/java.rb +338 -0
  51. data/lib/caruby/migration/migratable.rb +167 -0
  52. data/lib/caruby/migration/migrator.rb +533 -0
  53. data/lib/caruby/migration/resource.rb +8 -0
  54. data/lib/caruby/migration/resource_module.rb +11 -0
  55. data/lib/caruby/migration/uniquify.rb +20 -0
  56. data/lib/caruby/resource.rb +969 -0
  57. data/lib/caruby/util/attribute_path.rb +46 -0
  58. data/lib/caruby/util/cache.rb +53 -0
  59. data/lib/caruby/util/class.rb +99 -0
  60. data/lib/caruby/util/collection.rb +1053 -0
  61. data/lib/caruby/util/controlled_value.rb +35 -0
  62. data/lib/caruby/util/coordinate.rb +75 -0
  63. data/lib/caruby/util/domain_extent.rb +49 -0
  64. data/lib/caruby/util/file_separator.rb +65 -0
  65. data/lib/caruby/util/inflector.rb +20 -0
  66. data/lib/caruby/util/log.rb +95 -0
  67. data/lib/caruby/util/math.rb +12 -0
  68. data/lib/caruby/util/merge.rb +59 -0
  69. data/lib/caruby/util/module.rb +34 -0
  70. data/lib/caruby/util/options.rb +92 -0
  71. data/lib/caruby/util/partial_order.rb +36 -0
  72. data/lib/caruby/util/person.rb +119 -0
  73. data/lib/caruby/util/pretty_print.rb +184 -0
  74. data/lib/caruby/util/properties.rb +112 -0
  75. data/lib/caruby/util/stopwatch.rb +66 -0
  76. data/lib/caruby/util/topological_sync_enumerator.rb +53 -0
  77. data/lib/caruby/util/transitive_closure.rb +45 -0
  78. data/lib/caruby/util/tree.rb +48 -0
  79. data/lib/caruby/util/trie.rb +37 -0
  80. data/lib/caruby/util/uniquifier.rb +30 -0
  81. data/lib/caruby/util/validation.rb +48 -0
  82. data/lib/caruby/util/version.rb +56 -0
  83. data/lib/caruby/util/visitor.rb +351 -0
  84. data/lib/caruby/util/weak_hash.rb +36 -0
  85. data/lib/caruby/version.rb +3 -0
  86. 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