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.
- data/History.txt +10 -0
- data/lib/caruby/cli/command.rb +10 -8
- data/lib/caruby/database/fetched_matcher.rb +28 -39
- data/lib/caruby/database/lazy_loader.rb +101 -0
- data/lib/caruby/database/persistable.rb +190 -167
- data/lib/caruby/database/persistence_service.rb +21 -7
- data/lib/caruby/database/persistifier.rb +185 -0
- data/lib/caruby/database/reader.rb +106 -176
- data/lib/caruby/database/saved_matcher.rb +56 -0
- data/lib/caruby/database/search_template_builder.rb +1 -1
- data/lib/caruby/database/sql_executor.rb +8 -7
- data/lib/caruby/database/store_template_builder.rb +134 -61
- data/lib/caruby/database/writer.rb +252 -52
- data/lib/caruby/database.rb +88 -67
- data/lib/caruby/domain/attribute_initializer.rb +16 -0
- data/lib/caruby/domain/attribute_metadata.rb +161 -72
- data/lib/caruby/domain/id_alias.rb +22 -0
- data/lib/caruby/domain/inversible.rb +91 -0
- data/lib/caruby/domain/merge.rb +116 -35
- data/lib/caruby/domain/properties.rb +1 -1
- data/lib/caruby/domain/reference_visitor.rb +207 -71
- data/lib/caruby/domain/resource_attributes.rb +93 -80
- data/lib/caruby/domain/resource_dependency.rb +22 -97
- data/lib/caruby/domain/resource_introspection.rb +21 -28
- data/lib/caruby/domain/resource_inverse.rb +134 -0
- data/lib/caruby/domain/resource_metadata.rb +41 -19
- data/lib/caruby/domain/resource_module.rb +42 -33
- data/lib/caruby/import/java.rb +8 -9
- data/lib/caruby/migration/migrator.rb +20 -7
- data/lib/caruby/migration/resource_module.rb +0 -2
- data/lib/caruby/resource.rb +132 -351
- data/lib/caruby/util/cache.rb +4 -1
- data/lib/caruby/util/class.rb +48 -1
- data/lib/caruby/util/collection.rb +54 -18
- data/lib/caruby/util/inflector.rb +7 -0
- data/lib/caruby/util/options.rb +35 -31
- data/lib/caruby/util/partial_order.rb +1 -1
- data/lib/caruby/util/properties.rb +2 -2
- data/lib/caruby/util/stopwatch.rb +16 -8
- data/lib/caruby/util/transitive_closure.rb +1 -1
- data/lib/caruby/util/visitor.rb +342 -328
- data/lib/caruby/version.rb +1 -1
- data/lib/caruby/yard/resource_metadata_handler.rb +8 -0
- data/lib/caruby.rb +2 -0
- metadata +10 -9
- data/lib/caruby/database/saved_merger.rb +0 -131
- data/lib/caruby/domain/annotatable.rb +0 -25
- data/lib/caruby/domain/annotation.rb +0 -23
- data/lib/caruby/import/annotatable_class.rb +0 -28
- data/lib/caruby/import/annotation_class.rb +0 -27
- data/lib/caruby/import/annotation_module.rb +0 -67
- 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
|
data/lib/caruby/domain/merge.rb
CHANGED
@@ -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
|
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
|
-
#
|
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)
|
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
|
-
|
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
|
-
# *
|
54
|
-
# is performed
|
55
|
-
# *
|
56
|
-
# to perform the merge
|
57
|
-
# *
|
58
|
-
#
|
59
|
-
# *
|
60
|
-
#
|
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
|
-
#
|
63
|
-
|
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
|
-
|
68
|
-
|
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
|
-
|
76
|
-
|
77
|
-
|
78
|
-
|
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
|
-
#
|
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
|
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
|
-
|
31
|
-
@
|
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
|
-
|
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
|
-
# @
|
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
|
-
|
93
|
-
if
|
99
|
+
attrs = attributes_to_visit(parent)
|
100
|
+
if attrs.nil? then return Array::EMPTY_ARRAY end
|
94
101
|
refs = []
|
95
|
-
|
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: #{
|
114
|
+
logger.debug { " attributes: #{attrs.qp}..." }
|
108
115
|
end
|
116
|
+
|
109
117
|
refs
|
110
118
|
end
|
111
119
|
end
|
112
|
-
|
113
|
-
# A
|
114
|
-
class
|
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
|
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
|
128
|
+
# this initializer. The selector arguments consist of the match source and target.
|
121
129
|
#
|
122
|
-
# @param
|
123
|
-
# @option
|
124
|
-
# @option
|
125
|
-
#
|
126
|
-
# @
|
127
|
-
# @
|
128
|
-
# @
|
129
|
-
|
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
|
-
|
132
|
-
@
|
133
|
-
@
|
134
|
-
@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
|
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
|
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
|
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]
|
157
|
-
# @param [Resource]
|
158
|
-
# @yield [target, source] the optional block to call on the
|
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
|
-
|
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)
|
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
|
-
#
|
179
|
+
|
180
|
+
# Visits the given source domain object.
|
179
181
|
#
|
180
|
-
# @param [Resource]
|
181
|
-
# @
|
182
|
-
|
183
|
-
|
184
|
-
|
185
|
-
|
186
|
-
|
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
|
-
#
|
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
|
193
|
-
# @param
|
194
|
-
# @return [{Resource => Resource}] the source => target matches
|
195
|
-
def
|
196
|
-
|
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 =
|
242
|
+
unmtchd_srcs = srcs.reject do |src|
|
200
243
|
# the prior match, if any
|
201
|
-
tgt =
|
244
|
+
tgt = match_for(src)
|
202
245
|
mtchd_tgts << tgt if tgt
|
203
246
|
end
|
247
|
+
|
204
248
|
# the unmatched targets
|
205
|
-
unmtchd_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 =
|
214
|
-
logger.debug { "
|
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
|
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,
|
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
|
-
|
248
|
-
|
249
|
-
|
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
|
-
|
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(
|
395
|
+
super(source, target, &block)
|
260
396
|
end
|
261
397
|
end
|
262
398
|
|