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