caruby-core 1.4.2 → 1.4.3

Sign up to get free protection for your applications and to get access to all the features.
Files changed (52) hide show
  1. data/History.txt +10 -0
  2. data/lib/caruby/cli/command.rb +10 -8
  3. data/lib/caruby/database/fetched_matcher.rb +28 -39
  4. data/lib/caruby/database/lazy_loader.rb +101 -0
  5. data/lib/caruby/database/persistable.rb +190 -167
  6. data/lib/caruby/database/persistence_service.rb +21 -7
  7. data/lib/caruby/database/persistifier.rb +185 -0
  8. data/lib/caruby/database/reader.rb +106 -176
  9. data/lib/caruby/database/saved_matcher.rb +56 -0
  10. data/lib/caruby/database/search_template_builder.rb +1 -1
  11. data/lib/caruby/database/sql_executor.rb +8 -7
  12. data/lib/caruby/database/store_template_builder.rb +134 -61
  13. data/lib/caruby/database/writer.rb +252 -52
  14. data/lib/caruby/database.rb +88 -67
  15. data/lib/caruby/domain/attribute_initializer.rb +16 -0
  16. data/lib/caruby/domain/attribute_metadata.rb +161 -72
  17. data/lib/caruby/domain/id_alias.rb +22 -0
  18. data/lib/caruby/domain/inversible.rb +91 -0
  19. data/lib/caruby/domain/merge.rb +116 -35
  20. data/lib/caruby/domain/properties.rb +1 -1
  21. data/lib/caruby/domain/reference_visitor.rb +207 -71
  22. data/lib/caruby/domain/resource_attributes.rb +93 -80
  23. data/lib/caruby/domain/resource_dependency.rb +22 -97
  24. data/lib/caruby/domain/resource_introspection.rb +21 -28
  25. data/lib/caruby/domain/resource_inverse.rb +134 -0
  26. data/lib/caruby/domain/resource_metadata.rb +41 -19
  27. data/lib/caruby/domain/resource_module.rb +42 -33
  28. data/lib/caruby/import/java.rb +8 -9
  29. data/lib/caruby/migration/migrator.rb +20 -7
  30. data/lib/caruby/migration/resource_module.rb +0 -2
  31. data/lib/caruby/resource.rb +132 -351
  32. data/lib/caruby/util/cache.rb +4 -1
  33. data/lib/caruby/util/class.rb +48 -1
  34. data/lib/caruby/util/collection.rb +54 -18
  35. data/lib/caruby/util/inflector.rb +7 -0
  36. data/lib/caruby/util/options.rb +35 -31
  37. data/lib/caruby/util/partial_order.rb +1 -1
  38. data/lib/caruby/util/properties.rb +2 -2
  39. data/lib/caruby/util/stopwatch.rb +16 -8
  40. data/lib/caruby/util/transitive_closure.rb +1 -1
  41. data/lib/caruby/util/visitor.rb +342 -328
  42. data/lib/caruby/version.rb +1 -1
  43. data/lib/caruby/yard/resource_metadata_handler.rb +8 -0
  44. data/lib/caruby.rb +2 -0
  45. metadata +10 -9
  46. data/lib/caruby/database/saved_merger.rb +0 -131
  47. data/lib/caruby/domain/annotatable.rb +0 -25
  48. data/lib/caruby/domain/annotation.rb +0 -23
  49. data/lib/caruby/import/annotatable_class.rb +0 -28
  50. data/lib/caruby/import/annotation_class.rb +0 -27
  51. data/lib/caruby/import/annotation_module.rb +0 -67
  52. data/lib/caruby/migration/resource.rb +0 -8
@@ -0,0 +1,22 @@
1
+ module CaRuby
2
+ # Mix-in for Java classes which have an +id+ property.
3
+ # Since +id+ is a reserved Ruby method, this mix-in defines an +identifier+ attribute
4
+ # which fronts the +id+ property.
5
+ module IdAlias
6
+ # Returns the identifier.
7
+ # This method delegates to the Java +id+ property reader method.
8
+ #
9
+ # @return [Integer] the identifier value
10
+ def identifier
11
+ getId
12
+ end
13
+
14
+ # Sets the identifier to the given value.
15
+ # This method delegates to the Java +id+ property writer method.
16
+ #
17
+ # @param [Integer] value the value to set
18
+ def identifier=(value)
19
+ setId(value)
20
+ end
21
+ end
22
+ end
@@ -0,0 +1,91 @@
1
+ require 'caruby/util/log'
2
+ require 'caruby/database/persistable'
3
+ require 'caruby/util/pretty_print'
4
+
5
+ module CaRuby
6
+ # {Resource} inverse integrity aspect mix-in.
7
+ module Inversible
8
+ # Sets an attribute inverse by calling the attribute writer method with the other argument.
9
+ # If other is non-nil, then the inverse writer method is called on self.
10
+ #
11
+ # @param other [Resource] the attribute value to set
12
+ # @param [Symbol] writer the attribute writer method
13
+ # @param [Symbol] inv_writer the attribute inverse writer method defined for the other object
14
+ def set_inverse(other, writer, inv_writer)
15
+ other.send(inv_writer, self) if other
16
+ send(writer, other)
17
+ end
18
+
19
+ # Sets a non-collection attribute value in a way which enforces inverse integrity.
20
+ #
21
+ # @param [Object] newval the value to set
22
+ # @param [(Symbol, Symbol)] accessors the reader and writer methods to use in setting the
23
+ # attribute
24
+ # @param [Symbol] inverse_writer the inverse attribute writer method
25
+ def set_inversible_noncollection_attribute(newval, accessors, inverse_writer)
26
+ rdr, wtr = accessors
27
+ # the previous value
28
+ oldval = send(rdr)
29
+ # bail if no change
30
+ return newval if newval.equal?(oldval)
31
+
32
+ # clear the previous inverse
33
+ logger.debug { "Moving #{qp} from #{oldval.qp} to #{newval.qp}..." } if oldval and newval
34
+ if oldval then
35
+ clr_wtr = self.class === oldval && oldval.send(rdr).equal?(self) ? wtr : inverse_writer
36
+ oldval.send(clr_wtr, nil)
37
+ end
38
+ # call the writer
39
+ send(wtr, newval)
40
+ # call the inverse writer on self
41
+ if newval then
42
+ newval.send(inverse_writer, self)
43
+ logger.debug { "Moved #{qp} from #{oldval.qp} to #{newval.qp}." } if oldval
44
+ end
45
+
46
+ newval
47
+ end
48
+
49
+ # Sets a collection attribute value in a way which enforces inverse integrity.
50
+ # The inverse of the attribute is a collection accessed by calling inverse on newval.
51
+ #
52
+ # @param [Resource] newval the new attribute reference value
53
+ # @param [(Symbol, Symbol)] accessors the reader and writer to use in setting
54
+ # the attribute
55
+ # @param [Symbol] inverse the inverse collection attribute to which
56
+ # this domain object will be added
57
+ # @yield a factory to create a new collection on demand (default is an Array)
58
+ def add_to_inverse_collection(newval, accessors, inverse)
59
+ rdr, wtr = accessors
60
+ # the current inverse
61
+ oldval = send(rdr)
62
+ # no-op if no change
63
+ return newval if oldval == newval
64
+
65
+ # delete self from the current inverse reference collection
66
+ if oldval then
67
+ coll = oldval.send(inverse)
68
+ coll.delete(self) if coll
69
+ end
70
+
71
+ # call the writer on this object
72
+ send(wtr, newval)
73
+ # add self to the inverse collection
74
+ if newval then
75
+ coll = newval.send(inverse)
76
+ if coll.nil? then
77
+ coll = block_given? ? yield : Array.new
78
+ newval.set_attribute(inverse, coll)
79
+ end
80
+ coll << self
81
+ if oldval then
82
+ logger.debug { "Moved #{qp} from #{rdr} #{oldval.qp} #{inverse} to #{newval.qp}." }
83
+ else
84
+ logger.debug { "Added #{qp} to #{rdr} #{newval.qp} #{inverse}." }
85
+ end
86
+ end
87
+
88
+ newval
89
+ end
90
+ end
91
+ end
@@ -1,5 +1,5 @@
1
1
  module CaRuby
2
- # A Mergeable supports merging attribute values.
2
+ # A Mergeable supports merging {Resource} attribute values.
3
3
  module Mergeable
4
4
  # Merges the values of the other attributes into this object and returns self.
5
5
  # The other argument can be either a Hash or an object whose class responds to the
@@ -17,21 +17,25 @@ module CaRuby
17
17
  # An unrecognized attribute is ignored.
18
18
  #
19
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
20
+ # this object. The default attributes is this mergeable's class
22
21
  # {ResourceAttributes#mergeable_attributes}.
23
22
  #
24
- # #merge_attribute is called on each attribute with the merger block given to this
25
- # method.
23
+ # The merge is performed by calling {#merge_attribute} on each attribute with the matches
24
+ # and merger block given to this method.
26
25
  #
27
26
  # @param [Mergeable, {Symbol => Object}] other the source domain object or value hash to merge from
28
27
  # @param [<Symbol>, nil] attributes the attributes to merge (default {ResourceAttributes#nondomain_attributes})
28
+ # @param [{Resource => Resource}, nil] the optional merge source => target reference matches
29
+ # @yield [attribute, oldval, newval] the optional merger block
30
+ # @yieldparam [Symbol] attribute the merge target attribute
31
+ # @yieldparam oldval the current merge attribute value
32
+ # @yieldparam newval the new merge attribute value
29
33
  # @return [Mergeable] self
30
34
  # @raise [ArgumentError] if none of the following are true:
31
35
  # * other is a Hash
32
36
  # * attributes is non-nil
33
37
  # * the other class responds to +mergeable_attributes+
34
- def merge_attributes(other, attributes=nil, &merger) # :yields: attribute, oldval, newval
38
+ def merge_attributes(other, attributes=nil, matches=nil, &merger)
35
39
  return self if other.nil? or other.equal?(self)
36
40
  attributes = [attributes] if Symbol === attributes
37
41
  attributes ||= self.class.mergeable_attributes
@@ -39,51 +43,128 @@ module CaRuby
39
43
  # if the source object is not a hash, then convert it to an attribute => value hash
40
44
  vh = Hashable === other ? other : other.value_hash(attributes)
41
45
  # merge the value hash
42
- suspend_lazy_loader do
43
- vh.each { |attr, value| merge_attribute(attr, value, &merger) }
44
- end
46
+ vh.each { |attr, value| merge_attribute(attr, value, matches, &merger) }
45
47
  self
46
48
  end
47
49
 
48
50
  alias :merge :merge_attributes
49
51
 
50
52
  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
53
+
54
+ # Merges the value newval into the attribute as follows:
55
+ # * If the value is nil, empty or equal to the current attribute value, then no merge
56
+ # is performed.
57
+ # * Otherwise, if a merger block is given to this method, then that block is called
58
+ # to perform the merge.
59
+ # * Otherwise, if the attribute is a non-domain attribute and the current value is non-nil,
60
+ # then no merge is performed.
61
+ # * Otherwise, if the attribute is a non-domain attribute and the current value is nil,
62
+ # then set the attribute to the newval.
63
+ # * Otherwise, if the attribute is a domain non-collection attribute, then newval is recursively
64
+ # merged into the current referenced domain object.
65
+ # * Otherwise, attribute is a domain collection attribute and matching newval members are
66
+ # merged into the corresponding current collection members and non-matching newval members
67
+ # are added to the current collection.
61
68
  #
62
- # Returns the merged value.
63
- def merge_attribute(attribute, value, &merger) # :yields: attribute, oldval, newval
69
+ # @param [Symbol] attribute the merge attribute
70
+ # @param newval the value to merge
71
+ # @param [{Resource => Resource}, nil] the optional merge source => target reference matches
72
+ # @yield (see #merge_attributes)
73
+ # @yieldparam (see #merge_attributes)
74
+ # @return the merged attribute value
75
+ def merge_attribute(attribute, newval, matches=nil)
64
76
  # the previous value
65
77
  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
78
+ # If nothing to merge or a block can take over, then bail.
79
+ if newval.nil? or mergeable__equal?(oldval, newval) then
80
+ return oldval
73
81
  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)
82
+ return yield(attribute, oldval, value)
83
+ end
84
+
85
+ # Discriminate between a domain and non-domain attribute.
86
+ attr_md = self.class.attribute_metadata(attribute)
87
+ if attr_md.domain? then
88
+ merge_domain_attribute_value(attr_md, oldval, newval, matches)
79
89
  else
80
- oldval
90
+ merge_nondomain_attribute_value(attr_md, oldval, newval)
81
91
  end
82
92
  end
83
93
 
84
94
  private
85
95
 
86
- # Fixes a rare Java TreeSet aberration: comparison uses the TreeSet comparator rather than an element-wise comparator.
96
+ # @see #merge_attribute
97
+ def merge_nondomain_attribute_value(attr_md, oldval, newval)
98
+ oldval.nil? ? send(attr_md.writer, newval) : oldval
99
+ end
100
+
101
+ # @see #merge_attribute
102
+ def merge_domain_attribute_value(attr_md, oldval, newval, matches)
103
+ # the dependent owner writer method, if any
104
+ inv_md = attr_md.inverse_attribute_metadata
105
+ if inv_md and not inv_md.collection? then
106
+ owtr = inv_md.writer
107
+ end
108
+
109
+ # If the attribute is a collection, then merge the matches into the current attribute
110
+ # collection value and add each unmatched source to the collection.
111
+ # Otherwise, if the attribute is not yet set and there is a new value, then set it
112
+ # to the new value match or the new value itself if unmatched.
113
+ if attr_md.collection? then
114
+ # TODO - refactor into method
115
+ if oldval.nil? then
116
+ raise ValidationError.new("Merge into #{qp} #{attr_md} with nil collection value is not supported")
117
+ end
118
+ # the references to add
119
+ adds = []
120
+ logger.debug { "Merging #{newval.qp} into #{qp} #{attr_md} #{oldval.qp}..." } unless newval.empty?
121
+ newval.each do |src|
122
+ # If the match target is in the current collection, then update the matched
123
+ # target from the source.
124
+ # Otherwise, if there is no match or the match is a new reference created
125
+ # from the match, then add the match to the oldval collection.
126
+ if matches && matches.has_key?(src) then
127
+ # the source match
128
+ tgt = matches[src]
129
+ if oldval.include?(tgt) then
130
+ tgt.merge_attributes(src)
131
+ else
132
+ adds << tgt
133
+ end
134
+ else
135
+ adds << src
136
+ end
137
+ end
138
+ # add the unmatched sources
139
+ logger.debug { "Adding #{qp} #{attr_md} unmatched #{adds.qp}..." } unless adds.empty?
140
+ adds.each do |ref|
141
+ # If there is an owner writer attribute, then add the ref to the attribute collection by
142
+ # delegating to the owner writer. Otherwise, add the ref to the attribute collection directly.
143
+ owtr ? delegate_to_inverse_setter(attr_md, ref, owtr) : oldval << ref
144
+ end
145
+ oldval
146
+ elsif newval.nil? then
147
+ # no merge source
148
+ oldval
149
+ elsif oldval then
150
+ # merge the source into the target
151
+ oldval.merge(newval)
152
+ else
153
+ # No target; set the attribute to the source.
154
+ # the target is either a source match or the source itself
155
+ ref = matches[newval] if matches
156
+ ref ||= newval
157
+ logger.debug { "Setting #{qp} #{attr_md} reference #{ref.qp}..." }
158
+ # If the target is a dependent, then set the dependent owner, which will in turn
159
+ # set the attribute to the dependent. Otherwise, set the attribute to the target.
160
+ owtr ? delegate_to_inverse_setter(attr_md, ref, owtr) : send(attr_md.writer, ref)
161
+ end
162
+ newval
163
+ end
164
+
165
+ # Java alert - Java TreeSet comparison uses the TreeSet comparator rather than an
166
+ # element-wise comparator. Work around this rare aberration by converting the TreeSet
167
+ # to a Ruby Set.
87
168
  def mergeable__equal?(v1, v2)
88
169
  Java::JavaUtil::TreeSet === v1 && Java::JavaUtil::TreeSet === v2 ? v1.to_set == v2.to_set : v1 == v2
89
170
  end
@@ -44,7 +44,7 @@ module CaRuby
44
44
  # Loads the properties in the following low-to-high precedence order:
45
45
  # * the home file +.+_application_+.yaml+, where _application_ is the application name
46
46
  # * the given property file
47
- # * the environment varialables
47
+ # * the environment variables
48
48
  def load_properties(file)
49
49
  # canonicalize the file path
50
50
  file = File.expand_path(file)
@@ -27,8 +27,8 @@ module CaRuby
27
27
  # @yield [ref] selects which attributes to visit next
28
28
  # @yieldparam [Resource] ref the currently visited domain object
29
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 }
30
+ raise ArgumentError.new("Reference visitor missing domain reference selector") unless block_given?
31
+ @selector = selector
32
32
  # delegate to Visitor with the visit selector block
33
33
  super { |parent| references_to_visit(parent) }
34
34
  # the visited reference => parent attribute hash
@@ -74,8 +74,8 @@ module CaRuby
74
74
  # @yield [ref, others] matches ref in others (optional)
75
75
  # @yieldparam [Resource] ref the domain object to match
76
76
  # @yieldparam [<Resource>] the candidates for matching ref
77
- def sync
78
- block_given? ? super : super { |ref, others| ref.match_in(others) }
77
+ def sync(&matcher)
78
+ MatchVisitor.new(:matcher => matcher, &@selector)
79
79
  end
80
80
 
81
81
  protected
@@ -86,13 +86,20 @@ module CaRuby
86
86
  end
87
87
 
88
88
  private
89
+
90
+ # @param [Resource] parent the referencing domain object
91
+ # @return [<Resource>] the domain attributes to visit next
92
+ def attributes_to_visit(parent)
93
+ @selector.call(parent)
94
+ end
89
95
 
90
- # @return the domain objects to visit next for the given parent
96
+ # @param [Resource] parent the referencing domain object
97
+ # @return [<Resource>] the referenced domain objects to visit next for the given parent
91
98
  def references_to_visit(parent)
92
- attributes = @slctr.call(parent)
93
- if attributes.nil? then return Array::EMPTY_ARRAY end
99
+ attrs = attributes_to_visit(parent)
100
+ if attrs.nil? then return Array::EMPTY_ARRAY end
94
101
  refs = []
95
- attributes.each do | attr|
102
+ attrs.each do | attr|
96
103
  # the reference(s) to visit
97
104
  value = parent.send(attr)
98
105
  # associate each reference to visit with the current visited attribute
@@ -104,45 +111,44 @@ module CaRuby
104
111
  if DETAIL_DEBUG then
105
112
  logger.debug { "Visiting #{parent.qp} references: #{refs.qp}" }
106
113
  logger.debug { " lineage: #{lineage.qp}" }
107
- logger.debug { " attributes: #{@ref_attr_hash.qp}..." }
114
+ logger.debug { " attributes: #{attrs.qp}..." }
108
115
  end
116
+
109
117
  refs
110
118
  end
111
119
  end
112
-
113
- # A MergeVisitor merges a domain object's visitable attributes transitive closure into a target.
114
- class MergeVisitor < ReferenceVisitor
120
+
121
+ # A MatchVisitor visits two domain objects' visitable attributes transitive closure in lock-step.
122
+ class MatchVisitor < ReferenceVisitor
115
123
 
116
124
  attr_reader :matches
117
125
 
118
- # Creates a new MergeVisitor on domain attributes.
126
+ # Creates a new visitor which matches source and target domain object references.
119
127
  # The domain attributes to visit are determined by calling the selector block given to
120
- # this initializer as described in {ReferenceVisitor#initialize}.
128
+ # this initializer. The selector arguments consist of the match source and target.
121
129
  #
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
+ # @param (see ReferenceVisitor#initialize)
131
+ # @option opts [Proc] :mergeable the block which determines which attributes are merged
132
+ # @option opts [Proc] :matchable the block which determines which attributes to match
133
+ # (default is the visit selector)
134
+ # @option opts [Proc] :matcher the block which matches sources to targets
135
+ # @option opts [Proc] :copier the block which copies an unmatched source
136
+ # @yield (see ReferenceVisitor#initialize)
137
+ # @yieldparam [Resource] source the matched source object
138
+ def initialize(opts=nil)
130
139
  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)
140
+ opts = Options.to_hash(opts)
141
+ @matcher = opts.delete(:matcher) || Resource.method(:match_all)
142
+ @matchable = opts.delete(:matchable)
143
+ @copier = opts.delete(:copier)
135
144
  # the source => target matches
136
145
  @matches = {}
137
146
  # the class => {id => target} hash
138
147
  @id_mtchs = LazyHash.new { Hash.new }
139
- super do |src|
140
- tgt = @matches[src]
141
- yield(src, tgt) if tgt
142
- end
148
+ super { |src| yield(src) if @matches[src] }
143
149
  end
144
150
 
145
- # Visits the source and target and returns a recursive copy of obj and each of its visitable references.
151
+ # Visits the source and target.
146
152
  #
147
153
  # If a block is given to this method, then this method returns the evaluation of the block on the visited
148
154
  # source reference and its matching copy, if any. The default return value is the target which matches
@@ -150,15 +156,15 @@ module CaRuby
150
156
  #
151
157
  # caCORE alert = caCORE does not enforce reference identity integrity, i.e. a search on object _a_
152
158
  # 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.
159
+ # where _a.identifier_ == _a'.identifier_. This visit method remedies this caCORE defect by matching
160
+ # source references on a previously matched identifier where possible.
155
161
  #
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
162
+ # @param [Resource] source the match visit source
163
+ # @param [Resource] target the match visit target
164
+ # @yield [target, source] the optional block to call on the matched source and target
160
165
  # @yieldparam [Resource] source the visited source domain object
161
- def visit(target, source)
166
+ # @yieldparam [Resource] target the domain object which matches the visited source
167
+ def visit(source, target, &block)
162
168
  # clear the match hashes
163
169
  @matches.clear
164
170
  @id_mtchs.clear
@@ -166,57 +172,96 @@ module CaRuby
166
172
  add_match(source, target)
167
173
  # visit the source reference. the visit block merges each source reference into
168
174
  # 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
175
+ super(source) { |src| visit_matched(src, &block) }
174
176
  end
175
177
 
176
178
  private
177
-
178
- # Merges the given source object into the target object.
179
+
180
+ # Visits the given source domain object.
179
181
  #
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))
182
+ # @param [Resource] source the match visit source
183
+ # @yield [target, source] the optional block to call on the matched source and target
184
+ # @yieldparam [Resource] source the visited source domain object
185
+ # @yieldparam [Resource] target the domain object which matches the visited source
186
+ def visit_matched(source)
187
+ target = match_for_visited(source)
188
+ # match the matchable references, if any
189
+ if @matchable then
190
+ attrs = @matchable.call(source) - attributes_to_visit(source)
191
+ attrs.each { |attr| match_reference(source, target, attr) }
192
+ end
193
+ block_given? ? yield(source, target) : target
194
+ end
195
+
196
+ # @param source (see #match_visited)
197
+ # @return [<Resource>] the domain objects referenced by the source to visit next
198
+ def references_to_visit(source)
199
+ # the source match
200
+ target = match_for_visited(source)
201
+ # the attributes to visit
202
+ attrs = attributes_to_visit(source)
203
+ # the matched source references
204
+ match_references(source, target, attrs).keys
205
+ end
206
+
207
+ # @param source (see #match_visited)
208
+ # @return [<Resource>] the source match
209
+ # @raise [ValidationError] if there is no match
210
+ def match_for_visited(source)
211
+ target = @matches[source]
212
+ if target.nil? then raise ValidationError.new("Match visitor target not found for #{source}") end
187
213
  target
188
214
  end
189
215
 
190
- # Matches the given sources to targets. The match is performed by this visitor's matcher Proc.
216
+ # @param [Resource] source (see #match_visited)
217
+ # @param [Resource] target the source match
218
+ # @param [<Symbol>] attributes the attributes to match on
219
+ # @return [{Resource => Resource}] the referenced attribute matches
220
+ def match_references(source, target, attributes)
221
+ # collect the references to visit
222
+ matches = {}
223
+ attributes.each do |attr|
224
+ matches.merge!(match_reference(source, target, attr))
225
+ end
226
+ matches
227
+ end
228
+
229
+ # Matches the given source and target attribute references.
230
+ # The match is performed by this visitor's matcher Proc.
191
231
  #
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
232
+ # @param source (see #visit)
233
+ # @param target (see #visit)
234
+ # @return [{Resource => Resource}] the referenced source => target matches
235
+ def match_reference(source, target, attribute)
236
+ srcs = source.send(attribute).to_enum
237
+ tgts = target.send(attribute).to_enum
238
+
239
+ # the match targets
197
240
  mtchd_tgts = Set.new
198
241
  # capture the matched targets and the the unmatched sources
199
- unmtchd_srcs = sources.reject do |src|
242
+ unmtchd_srcs = srcs.reject do |src|
200
243
  # the prior match, if any
201
- tgt = match(src)
244
+ tgt = match_for(src)
202
245
  mtchd_tgts << tgt if tgt
203
246
  end
247
+
204
248
  # the unmatched targets
205
- unmtchd_tgts = targets.difference(mtchd_tgts)
206
-
249
+ unmtchd_tgts = tgts.difference(mtchd_tgts)
207
250
  # match the residual targets and sources
208
251
  rsd_mtchs = @matcher.call(unmtchd_srcs, unmtchd_tgts)
209
252
  # add residual matches
210
253
  rsd_mtchs.each { |src, tgt| add_match(src, tgt) }
254
+
211
255
  # The source => target match hash.
212
256
  # 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?
257
+ matches = srcs.to_compact_hash { |src| match_for(src) or copy_unmatched(src) }
258
+ logger.debug { "Match visitor matched #{matches.qp}." } unless matches.empty?
259
+
215
260
  matches
216
261
  end
217
262
 
218
263
  # @return the target matching the given source
219
- def match(source)
264
+ def match_for(source)
220
265
  @matches[source] or identifier_match(source)
221
266
  end
222
267
 
@@ -232,7 +277,8 @@ module CaRuby
232
277
  @matches[source] = tgt if tgt
233
278
  end
234
279
 
235
- # @return [Resource, nil] a copy of the given source if this ReferenceVisitor has a copier, nil otherwise
280
+ # @return [Resource, nil] a copy of the given source if this ReferenceVisitor has a copier,
281
+ # nil otherwise
236
282
  def copy_unmatched(source)
237
283
  return unless @copier
238
284
  copy = @copier.call(source)
@@ -240,13 +286,99 @@ module CaRuby
240
286
  end
241
287
  end
242
288
 
289
+ # A MergeVisitor merges a domain object's visitable attributes transitive closure into a target.
290
+ class MergeVisitor < MatchVisitor
291
+ # Creates a new MergeVisitor on domain attributes.
292
+ # The domain attributes to visit are determined by calling the selector block given to
293
+ # this initializer as described in {ReferenceVisitor#initialize}.
294
+ #
295
+ # @param (see MatchVisitor#initialize)
296
+ # @option opts [Proc] :mergeable the block which determines which attributes are merged
297
+ # @option opts [Proc] :matcher the block which matches sources to targets
298
+ # @option opts [Proc] :copier the block which copies an unmatched source
299
+ # @yield (see MatchVisitor#initialize)
300
+ # @yieldparam (see MatchVisitor#initialize)
301
+ def initialize(opts=nil, &selector)
302
+ opts = Options.to_hash(opts)
303
+ # Merge is depth-first, since the source references must be matched, and created if necessary,
304
+ # before they can be merged into the target.
305
+ opts[:depth_first] = true
306
+ @mergeable = opts.delete(:mergeable) || selector
307
+ # each mergeable attribute is matchable
308
+ unless @mergeable == selector then
309
+ opts[:matchable] = @mergeable
310
+ end
311
+ super
312
+ end
313
+
314
+ # Visits the source and target and returns a recursive copy of obj and each of its visitable references.
315
+ #
316
+ # If a block is given to this method, then this method returns the evaluation of the block on the visited
317
+ # source reference and its matching copy, if any. The default return value is the target which matches
318
+ # source.
319
+ #
320
+ # caCORE alert = caCORE does not enforce reference identity integrity, i.e. a search on object _a_
321
+ # with database record references _a_ => _b_ => _a_, the search result might be _a_ => _b_ => _a'_,
322
+ # where _a.identifier_ == _a'.identifier_. This visit method remedies the caCORE defect by matching source
323
+ # references on a previously matched identifier where possible.
324
+ #
325
+ # @param [Resource] source the domain object to merge from
326
+ # @param [Resource] target the domain object to merge into
327
+ # @yield [target, source] the optional block to call on the visited source domain object and its matching target
328
+ # @yieldparam [Resource] target the domain object which matches the visited source
329
+ # @yieldparam [Resource] source the visited source domain object
330
+ def visit(source, target)
331
+ # visit the source reference. the visit block merges each source reference into
332
+ # the matching target reference.
333
+ super(source, target) do |src, tgt|
334
+ merge(src, tgt)
335
+ block_given? ? yield(src, tgt) : tgt
336
+ end
337
+ end
338
+
339
+ private
340
+
341
+ # Merges the given source object into the target object.
342
+ #
343
+ # @param [Resource] source the domain object to merge from
344
+ # @param [Resource] target the domain object to merge into
345
+ # @return [Resource] the merged target
346
+ def merge(source, target)
347
+ # the domain attributes to merge
348
+ attrs = @mergeable.call(source)
349
+ logger.debug { format_merge_log_message(source, target, attrs) }
350
+ # merge the non-domain attributes
351
+ target.merge_attributes(source)
352
+ # merge the source domain attributes into the target
353
+ target.merge(source, attrs, @matches)
354
+ end
355
+
356
+ # @param source (see #merge)
357
+ # @param target (see #merge)
358
+ # @param attributes (see Mergeable#merge)
359
+ # @return [String] the log message
360
+ def format_merge_log_message(source, target, attributes)
361
+ attr_clause = " including domain attributes #{attributes.to_series}" unless attributes.empty?
362
+ "Merging #{source.qp} into #{target.qp}#{attr_clause}..."
363
+ end
364
+ end
365
+
243
366
  # A CopyVisitor copies a domain object's visitable attributes transitive closure.
244
367
  class CopyVisitor < MergeVisitor
245
368
  # Creates a new CopyVisitor with the options described in {MergeVisitor#initialize}.
246
369
  # 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 }
370
+ #
371
+ # @param (see MergeVisitor#initialize)
372
+ # @option opts [Proc] :mergeable the mergeable domain attribute selector
373
+ # @option opts [Proc] :matcher the match block
374
+ # @option opts [Proc] :copier the unmatched source copy block
375
+ # @yield (see MergeVisitor#initialize)
376
+ # @yieldparam (see MergeVisitor#initialize)
377
+ def initialize(opts=nil)
378
+ opts = Options.to_hash(opts)
379
+ opts[:copier] ||= Proc.new { |src| src.copy }
380
+ # no match forces a copy
381
+ opts[:matcher] = Proc.new { Hash::EMPTY_HASH }
250
382
  super
251
383
  end
252
384
 
@@ -254,9 +386,13 @@ module CaRuby
254
386
  #
255
387
  # If a block is given to this method, then the block is called with the visited
256
388
  # source reference and its matching copy target.
257
- def visit(source, &block) # :yields: target, source
389
+ #
390
+ # @param (see MergeVisitor#visit)
391
+ # @yield (see MergeVisitor#visit)
392
+ # @yieldparam (see MergeVisitor#visit)
393
+ def visit(source, &block)
258
394
  target = @copier.call(source)
259
- super(target, source, &block)
395
+ super(source, target, &block)
260
396
  end
261
397
  end
262
398