caruby-core 1.4.2 → 1.4.3

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