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.
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