jinx 2.1.1
Sign up to get free protection for your applications and to get access to all the features.
- data/.gitignore +14 -0
- data/.rspec +3 -0
- data/.yardopts +1 -0
- data/Gemfile +6 -0
- data/Gemfile.lock +27 -0
- data/History.md +6 -0
- data/LEGAL +5 -0
- data/LICENSE +22 -0
- data/README.md +44 -0
- data/Rakefile +41 -0
- data/examples/family/README.md +10 -0
- data/examples/family/ext/build.xml +35 -0
- data/examples/family/ext/src/family/Address.java +68 -0
- data/examples/family/ext/src/family/Child.java +24 -0
- data/examples/family/ext/src/family/DomainObject.java +26 -0
- data/examples/family/ext/src/family/Household.java +36 -0
- data/examples/family/ext/src/family/Parent.java +48 -0
- data/examples/family/ext/src/family/Person.java +42 -0
- data/examples/family/lib/family.rb +15 -0
- data/examples/family/lib/family/address.rb +6 -0
- data/examples/family/lib/family/domain_object.rb +6 -0
- data/examples/family/lib/family/household.rb +6 -0
- data/examples/family/lib/family/parent.rb +16 -0
- data/examples/family/lib/family/person.rb +6 -0
- data/examples/model/README.md +25 -0
- data/examples/model/ext/build.xml +35 -0
- data/examples/model/ext/src/domain/Child.java +192 -0
- data/examples/model/ext/src/domain/Dependent.java +29 -0
- data/examples/model/ext/src/domain/DomainObject.java +26 -0
- data/examples/model/ext/src/domain/Independent.java +83 -0
- data/examples/model/ext/src/domain/Parent.java +129 -0
- data/examples/model/ext/src/domain/Person.java +14 -0
- data/examples/model/lib/model.rb +13 -0
- data/examples/model/lib/model/child.rb +13 -0
- data/examples/model/lib/model/domain_object.rb +6 -0
- data/examples/model/lib/model/independent.rb +11 -0
- data/examples/model/lib/model/parent.rb +17 -0
- data/jinx.gemspec +22 -0
- data/lib/jinx.rb +3 -0
- data/lib/jinx/active_support/README.txt +2 -0
- data/lib/jinx/active_support/core_ext/string.rb +7 -0
- data/lib/jinx/active_support/core_ext/string/inflections.rb +167 -0
- data/lib/jinx/active_support/inflections.rb +55 -0
- data/lib/jinx/active_support/inflector.rb +398 -0
- data/lib/jinx/cli/application.rb +36 -0
- data/lib/jinx/cli/command.rb +214 -0
- data/lib/jinx/helpers/array.rb +108 -0
- data/lib/jinx/helpers/boolean.rb +42 -0
- data/lib/jinx/helpers/case_insensitive_hash.rb +39 -0
- data/lib/jinx/helpers/class.rb +149 -0
- data/lib/jinx/helpers/collection.rb +33 -0
- data/lib/jinx/helpers/collections.rb +11 -0
- data/lib/jinx/helpers/collector.rb +20 -0
- data/lib/jinx/helpers/conditional_enumerator.rb +21 -0
- data/lib/jinx/helpers/enumerable.rb +242 -0
- data/lib/jinx/helpers/enumerate.rb +35 -0
- data/lib/jinx/helpers/error.rb +15 -0
- data/lib/jinx/helpers/file_separator.rb +65 -0
- data/lib/jinx/helpers/filter.rb +52 -0
- data/lib/jinx/helpers/flattener.rb +38 -0
- data/lib/jinx/helpers/hash.rb +12 -0
- data/lib/jinx/helpers/hashable.rb +502 -0
- data/lib/jinx/helpers/inflector.rb +36 -0
- data/lib/jinx/helpers/key_transformer_hash.rb +43 -0
- data/lib/jinx/helpers/lazy_hash.rb +44 -0
- data/lib/jinx/helpers/log.rb +106 -0
- data/lib/jinx/helpers/math.rb +12 -0
- data/lib/jinx/helpers/merge.rb +60 -0
- data/lib/jinx/helpers/module.rb +18 -0
- data/lib/jinx/helpers/multi_enumerator.rb +31 -0
- data/lib/jinx/helpers/options.rb +92 -0
- data/lib/jinx/helpers/os.rb +19 -0
- data/lib/jinx/helpers/partial_order.rb +37 -0
- data/lib/jinx/helpers/pretty_print.rb +207 -0
- data/lib/jinx/helpers/set.rb +8 -0
- data/lib/jinx/helpers/stopwatch.rb +76 -0
- data/lib/jinx/helpers/transformer.rb +24 -0
- data/lib/jinx/helpers/transitive_closure.rb +55 -0
- data/lib/jinx/helpers/uniquifier.rb +50 -0
- data/lib/jinx/helpers/validation.rb +33 -0
- data/lib/jinx/helpers/visitor.rb +370 -0
- data/lib/jinx/import/class_path_modifier.rb +77 -0
- data/lib/jinx/import/java.rb +337 -0
- data/lib/jinx/importer.rb +240 -0
- data/lib/jinx/metadata.rb +155 -0
- data/lib/jinx/metadata/attribute_enumerator.rb +73 -0
- data/lib/jinx/metadata/dependency.rb +244 -0
- data/lib/jinx/metadata/id_alias.rb +23 -0
- data/lib/jinx/metadata/introspector.rb +179 -0
- data/lib/jinx/metadata/inverse.rb +170 -0
- data/lib/jinx/metadata/java_property.rb +169 -0
- data/lib/jinx/metadata/propertied.rb +500 -0
- data/lib/jinx/metadata/property.rb +401 -0
- data/lib/jinx/metadata/property_characteristics.rb +114 -0
- data/lib/jinx/resource.rb +862 -0
- data/lib/jinx/resource/copy_visitor.rb +36 -0
- data/lib/jinx/resource/inversible.rb +90 -0
- data/lib/jinx/resource/match_visitor.rb +180 -0
- data/lib/jinx/resource/matcher.rb +20 -0
- data/lib/jinx/resource/merge_visitor.rb +73 -0
- data/lib/jinx/resource/mergeable.rb +185 -0
- data/lib/jinx/resource/reference_enumerator.rb +49 -0
- data/lib/jinx/resource/reference_path_visitor.rb +38 -0
- data/lib/jinx/resource/reference_visitor.rb +55 -0
- data/lib/jinx/resource/unique.rb +35 -0
- data/lib/jinx/version.rb +3 -0
- data/spec/defaults_spec.rb +30 -0
- data/spec/definitions/model/alias/child.rb +5 -0
- data/spec/definitions/model/base/child.rb +5 -0
- data/spec/definitions/model/base/domain_object.rb +5 -0
- data/spec/definitions/model/base/independent.rb +5 -0
- data/spec/definitions/model/defaults/child.rb +5 -0
- data/spec/definitions/model/dependency/child.rb +5 -0
- data/spec/definitions/model/dependency/parent.rb +6 -0
- data/spec/definitions/model/inverse/child.rb +5 -0
- data/spec/definitions/model/inverse/independent.rb +5 -0
- data/spec/definitions/model/inverse/parent.rb +5 -0
- data/spec/definitions/model/mandatory/child.rb +6 -0
- data/spec/dependency_spec.rb +47 -0
- data/spec/family_spec.rb +64 -0
- data/spec/inverse_spec.rb +53 -0
- data/spec/mandatory_spec.rb +43 -0
- data/spec/metadata_spec.rb +68 -0
- data/spec/resource_spec.rb +30 -0
- data/spec/spec_helper.rb +3 -0
- data/spec/support/model.rb +19 -0
- data/test/fixtures/line_separator/cr_line_sep.txt +1 -0
- data/test/fixtures/line_separator/crlf_line_sep.txt +3 -0
- data/test/fixtures/line_separator/lf_line_sep.txt +3 -0
- data/test/fixtures/mixed/ext/build.xml +35 -0
- data/test/fixtures/mixed/ext/src/mixed/Case/Example.java +5 -0
- data/test/helper.rb +7 -0
- data/test/lib/jinx/command_test.rb +41 -0
- data/test/lib/jinx/helpers/boolean_test.rb +27 -0
- data/test/lib/jinx/helpers/class_test.rb +60 -0
- data/test/lib/jinx/helpers/collections_test.rb +402 -0
- data/test/lib/jinx/helpers/file_separator_test.rb +29 -0
- data/test/lib/jinx/helpers/inflector_test.rb +11 -0
- data/test/lib/jinx/helpers/lazy_hash_test.rb +32 -0
- data/test/lib/jinx/helpers/module_test.rb +24 -0
- data/test/lib/jinx/helpers/options_test.rb +66 -0
- data/test/lib/jinx/helpers/partial_order_test.rb +41 -0
- data/test/lib/jinx/helpers/pretty_print_test.rb +83 -0
- data/test/lib/jinx/helpers/stopwatch_test.rb +16 -0
- data/test/lib/jinx/helpers/transitive_closure_test.rb +80 -0
- data/test/lib/jinx/helpers/visitor_test.rb +288 -0
- data/test/lib/jinx/import/java_test.rb +78 -0
- data/test/lib/jinx/import/mixed_case_test.rb +16 -0
- metadata +272 -0
@@ -0,0 +1,33 @@
|
|
1
|
+
module Jinx
|
2
|
+
# Raised when an object fails a validation test.
|
3
|
+
class ValidationError < RuntimeError; end
|
4
|
+
end
|
5
|
+
|
6
|
+
class Object
|
7
|
+
# Returns whether this object is nil, false, empty, or a whitespace string.
|
8
|
+
# This method is borrowed from Rails ActiveSupport.
|
9
|
+
#
|
10
|
+
# @example
|
11
|
+
# ''.blank? => true
|
12
|
+
# nil.blank? => true
|
13
|
+
# false.blank? => true
|
14
|
+
# [].blank? => true
|
15
|
+
# [[]].blank? => false
|
16
|
+
# @return [Boolean] whether this object is nil, false, empty, or a whitespace string
|
17
|
+
# @see {#nil_or_empty?}
|
18
|
+
def blank?
|
19
|
+
respond_to?(:empty?) ? empty? : !self
|
20
|
+
end
|
21
|
+
|
22
|
+
# Returns whether this object is nil, empty, or a whitespace string.
|
23
|
+
# This method differs from {#blank?} in that +false+ is an allowed value.
|
24
|
+
#
|
25
|
+
# @example
|
26
|
+
# ''.nil_or_empty? => true
|
27
|
+
# nil.nil_or_empty? => true
|
28
|
+
# false.nil_or_empty? => false
|
29
|
+
# @return [Boolean] whether this object is nil, empty, or a whitespace string
|
30
|
+
def nil_or_empty?
|
31
|
+
blank? and self != false
|
32
|
+
end
|
33
|
+
end
|
@@ -0,0 +1,370 @@
|
|
1
|
+
require 'jinx/helpers/collections'
|
2
|
+
|
3
|
+
require 'jinx/helpers/options'
|
4
|
+
|
5
|
+
# Enumerator overwrites to_enum, so include it first
|
6
|
+
require 'enumerator'
|
7
|
+
require 'generator'
|
8
|
+
|
9
|
+
module Jinx
|
10
|
+
# Error raised on a visit failure.
|
11
|
+
class VisitError < RuntimeError; end
|
12
|
+
|
13
|
+
# Visitor traverses items and applies an operation, e.g.:
|
14
|
+
# class Node
|
15
|
+
# attr_accessor :children, :value
|
16
|
+
# def initialize(value, parent=nil)
|
17
|
+
# @value = value
|
18
|
+
# @children = []
|
19
|
+
# @parent = parent
|
20
|
+
# @parent.children << self if @parent
|
21
|
+
# end
|
22
|
+
# end
|
23
|
+
# parent = Node.new(1)
|
24
|
+
# child = Node.new(2, parent)
|
25
|
+
# multiplier = 2
|
26
|
+
# Jinx::Visitor.new { |node| node.children }.visit(parent) { |node| node.value *= multiplier } #=> 2
|
27
|
+
# parent.value #=> 2
|
28
|
+
# child.value #=> 4
|
29
|
+
#
|
30
|
+
# The visit result is the result of evaluating the operation block on the initial visited node.
|
31
|
+
# Visiting a collection returns an array of the result of visiting each member of the collection,
|
32
|
+
# e.g. augmenting the preceding example:
|
33
|
+
# parent2 = Node.new(3)
|
34
|
+
# child2 = Node.new(4, parent2)
|
35
|
+
# Jinx::Visitor.new { |node| node.children }.visit([parent, parent2]) { |node| node.value *= multiplier } #=> [2, 6]
|
36
|
+
# Each visit captures the visit result in the +visited+ hash, e.g.:
|
37
|
+
# parent = Node.new(1)
|
38
|
+
# child = Node.new(2, parent)
|
39
|
+
# visitor = Jinx::Visitor.new { |node| node.children }
|
40
|
+
# visitor.visit([parent]) { |node| node.value += 1 }
|
41
|
+
# parent.value #=> 2
|
42
|
+
# visitor.visited[parent] #=> 2
|
43
|
+
# child.value #=> 3
|
44
|
+
# visitor.visited[child] #=> 3
|
45
|
+
#
|
46
|
+
# A +return+ from the operation block terminates the visit and exits from the defining scope with the block return value,
|
47
|
+
# e.g. given the preceding example:
|
48
|
+
# def increment(parent, limit)
|
49
|
+
# Jinx::Visitor.new { |node| node.children }.visit(parent) { |node| node.value < limit ? node.value += 1 : return }
|
50
|
+
# end
|
51
|
+
# increment(parent, 2) #=> nil
|
52
|
+
# parent.value #=> 2
|
53
|
+
# child.value #=> 2
|
54
|
+
#
|
55
|
+
# The to_enum method allows navigator iteration, e.g.:
|
56
|
+
# Jinx::Visitor.new { |node| node.children }.to_enum(parent).detect { |node| node.value == 2 }
|
57
|
+
class Visitor
|
58
|
+
|
59
|
+
attr_reader :options, :visited, :lineage, :cycles
|
60
|
+
|
61
|
+
# Creates a new Visitor which traverses the child objects returned by the navigator block.
|
62
|
+
# The navigator block takes a parent node argument and returns an enumerator on the children
|
63
|
+
# to visit.
|
64
|
+
#
|
65
|
+
# The options is a _symbol_, _symbol_ => _value_ hash or nil. A _symbol_ argument is the same
|
66
|
+
# as +{+_symbol_ +=> true}+. Supported options include the following:
|
67
|
+
# * +:depth_first+: +true+, +false+ or a Proc. If the value is a Proc, then
|
68
|
+
# value determines whether a child is visited depth-first. See the {#visit} method for more
|
69
|
+
# information.
|
70
|
+
#
|
71
|
+
# If the :operator option is set, then the visit operator block is called when a node is visited.
|
72
|
+
# The operator block argument is the visited node.
|
73
|
+
#
|
74
|
+
# @param [Symbol, {Symbol => Object}] opts the visit options. A symbol argument is the same
|
75
|
+
# as symbol => true
|
76
|
+
# @option opts [String] :depth_first depth-first traversal
|
77
|
+
# @option opts [Proc] :operator the operator applied to each visited node
|
78
|
+
# @option opts [String] :prune_cycle flag indicating whether to exclude cycles to the root in a visit
|
79
|
+
# @option opts [Boolean] :verbose print navigation log messages
|
80
|
+
# @yield [parent] returns an enumerator on the children to visit
|
81
|
+
# @yieldparam parent the current node
|
82
|
+
def initialize(opts=nil, &navigator)
|
83
|
+
raise ArgumentError.new('Visitor cannot be created without a navigator block') unless block_given?
|
84
|
+
@navigator = navigator
|
85
|
+
@options = Options.to_hash(opts)
|
86
|
+
@depth_first_flag = @options[:depth_first]
|
87
|
+
@prune_cycle_flag = @options[:prune_cycle]
|
88
|
+
@lineage = []
|
89
|
+
@cycles = []
|
90
|
+
@visited = {}
|
91
|
+
@verbose = Options.get(:verbose, opts, false)
|
92
|
+
@exclude = Set.new
|
93
|
+
end
|
94
|
+
|
95
|
+
# Navigates to node and the children returned by this Visitor's navigator block.
|
96
|
+
# Applies the optional operator block to each child node if the block is given to this method.
|
97
|
+
# Returns the result of the operator block if given, or the node itself otherwise.
|
98
|
+
#
|
99
|
+
# The nodes to visit from a parent node are determined in the following sequence:
|
100
|
+
# * Return if the parent node has already been visited.
|
101
|
+
# * If depth_first, then call the navigator block defined in the initializer on
|
102
|
+
# the parent node and visit each child node.
|
103
|
+
# * Visit the parent node.
|
104
|
+
# * If not depth-first, then call the navigator block defined in the initializer
|
105
|
+
# on the parent node and visit each child node.
|
106
|
+
# The :depth option value constrains child traversal to that number of levels.
|
107
|
+
#
|
108
|
+
# This method first clears the _visited_ hash, unless the :visited option was set in the initializer.
|
109
|
+
#
|
110
|
+
# @param node the root object to visit
|
111
|
+
# @yield [visited] an operator applied to each visited object
|
112
|
+
# @yieldparam visited the object currently being visited
|
113
|
+
# @return the result of the yield block on node, or node itself if no block is given
|
114
|
+
def visit(node, &operator)
|
115
|
+
visit_root(node, &operator)
|
116
|
+
end
|
117
|
+
|
118
|
+
# @param node the node to check
|
119
|
+
# @return [Boolean] whether the node was visited
|
120
|
+
def visited?(node)
|
121
|
+
@visited.has_key?(node)
|
122
|
+
end
|
123
|
+
|
124
|
+
# @return the top node visited
|
125
|
+
def root
|
126
|
+
@lineage.first
|
127
|
+
end
|
128
|
+
|
129
|
+
# @return the current node being visited
|
130
|
+
def current
|
131
|
+
@lineage.last
|
132
|
+
end
|
133
|
+
|
134
|
+
# @return the node most recently passed as an argument to this visitor's navigator block,
|
135
|
+
# or nil if visiting the first node
|
136
|
+
def from
|
137
|
+
@lineage[-2]
|
138
|
+
end
|
139
|
+
|
140
|
+
alias :parent :from
|
141
|
+
|
142
|
+
# @return [Enumerable] iterator over each visited node
|
143
|
+
def to_enum(node)
|
144
|
+
# JRuby could use Generator instead, but that results in dire behavior on any error
|
145
|
+
# by crashing with an elided Java lineage trace.
|
146
|
+
VisitorEnumerator.new(self, node)
|
147
|
+
end
|
148
|
+
|
149
|
+
# Returns a new visitor that traverses a collection of parent nodes in lock-step fashion using
|
150
|
+
# this visitor. The synced {#visit} method applies the visit operator block to an array of child
|
151
|
+
# nodes taken from each parent node, e.g.:
|
152
|
+
# parent1 = Node.new(1)
|
153
|
+
# child11 = Node.new(2, parent1)
|
154
|
+
# child12 = Node.new(3, parent1)
|
155
|
+
# parent2 = Node.new(1)
|
156
|
+
# child21 = Node.new(3, parent2)
|
157
|
+
# Jinx::Visitor.new { |node| node.children }.sync.to_enum.to_a #=> [
|
158
|
+
# [parent1, parent2],
|
159
|
+
# [child11, child21],
|
160
|
+
# [child12, nil]
|
161
|
+
# ]
|
162
|
+
#
|
163
|
+
# By default, the children are grouped in enumeration order. If a block is given to this method,
|
164
|
+
# then the block is called to match child nodes, e.g. using the above example:
|
165
|
+
# visitor = Jinx::Visitor.new { |node| node.children }
|
166
|
+
# synced = visitor.sync { |nodes, others| nodes.to_compact_hash { others.detect { |other| node.value == other.value } } }
|
167
|
+
# synced.to_enum.to_a #=> [
|
168
|
+
# [parent1, parent2],
|
169
|
+
# [child11, nil],
|
170
|
+
# [child12, child21]
|
171
|
+
# ]
|
172
|
+
#
|
173
|
+
# @yield [nodes, others] matches node in others (optional)
|
174
|
+
# @yieldparam [<Resource>] nodes the visited nodes to match
|
175
|
+
# @yieldparam [<Resource>] others the candidates for matching the node
|
176
|
+
def sync(&matcher)
|
177
|
+
SyncVisitor.new(self, &matcher)
|
178
|
+
end
|
179
|
+
|
180
|
+
# Returns a new Visitor which determines which nodes to visit by applying the given block
|
181
|
+
# to this visitor. The filter block arguments consist of a parent node and an array of
|
182
|
+
# children nodes for the parent. The block can return nil, a single node to visit or a
|
183
|
+
# collection of nodes to visit.
|
184
|
+
#
|
185
|
+
# @example
|
186
|
+
# visitor = Jinx::Visitor.new { |person| person.children }
|
187
|
+
# # Joe has age 55 and children aged 17 and 24, who have children aged [1] and [6, 3], resp.
|
188
|
+
# visitor.to_enum(joe) { |person| person.age } #=> [55, 20, 1, 24, 6, 3]
|
189
|
+
# # The filter navigates to the children sorted by age of parents 21 or older.
|
190
|
+
# filter = visitor.filter { |parent, children| children.sort { |c1, c2| c1.age <=> c2.age } if parent.age >= 21 }
|
191
|
+
# filter.to_enum(joe) { |person| person.age } #=> [55, 24, 3, 6]
|
192
|
+
#
|
193
|
+
# @return [Visitor] the filter visitor
|
194
|
+
# @yield [parent, children] the filter to select which of the children to visit next
|
195
|
+
# @yieldparam parent the currently visited node
|
196
|
+
# @yieldparam children the nodes slated by this Visitor to visit next
|
197
|
+
# @raise [ArgumentError] if a block is not given to this method
|
198
|
+
def filter
|
199
|
+
Jinx.fail(ArgumentError, "A filter block is not given to the visitor filter method") unless block_given?
|
200
|
+
Visitor.new(@options) { |node| yield(node, node_children(node)) }
|
201
|
+
end
|
202
|
+
|
203
|
+
protected
|
204
|
+
|
205
|
+
# Resets this visitor's state in preparation for a new visit.
|
206
|
+
def clear
|
207
|
+
# clear the lineage
|
208
|
+
@lineage.clear
|
209
|
+
# if the visited hash is not shared, then clear it
|
210
|
+
@visited.clear unless @options.has_key?(:visited)
|
211
|
+
# clear the cycles
|
212
|
+
@cycles.clear
|
213
|
+
end
|
214
|
+
|
215
|
+
# Returns the children to visit for the given node.
|
216
|
+
def node_children(node)
|
217
|
+
children = @navigator.call(node)
|
218
|
+
return Array::EMPTY_ARRAY if children.nil?
|
219
|
+
Enumerable === children ? children.to_a.compact : [children]
|
220
|
+
end
|
221
|
+
|
222
|
+
private
|
223
|
+
|
224
|
+
# @return [Boolean] whether the depth-first flag is set
|
225
|
+
def depth_first?
|
226
|
+
!!@depth_first_flag
|
227
|
+
end
|
228
|
+
|
229
|
+
# Visits the root node and all descendants.
|
230
|
+
def visit_root(node, &operator)
|
231
|
+
clear
|
232
|
+
prune_cycle_nodes(node) if @prune_cycle_flag
|
233
|
+
# visit the root node
|
234
|
+
visit_recursive(node, &operator)
|
235
|
+
end
|
236
|
+
|
237
|
+
# Excludes the internal nodes in cycles starting and ending at the given root.
|
238
|
+
def prune_cycle_nodes(root)
|
239
|
+
@exclude.clear
|
240
|
+
# visit the root, which will detect cycles, and remove the visited nodes afterwords
|
241
|
+
@prune_cycle_flag = false
|
242
|
+
to_enum(root).collect.each { |node| @visited.delete(node) }
|
243
|
+
@prune_cycle_flag = true
|
244
|
+
# add each cyclic internal node to the exclude list
|
245
|
+
@cycles.each { |cycle| cycle[1...-1].each { |node| @exclude << node } if cycle.first == root }
|
246
|
+
end
|
247
|
+
|
248
|
+
def visit_recursive(node, &operator)
|
249
|
+
# bail if no node or the node is specifically excluded
|
250
|
+
return if node.nil? or @exclude.include?(node)
|
251
|
+
# return the visited value if the node has already been visited
|
252
|
+
if @visited.has_key?(node) then
|
253
|
+
#capture a cycle
|
254
|
+
index = @lineage.index(node)
|
255
|
+
if index then
|
256
|
+
cycle = @lineage[index..-1] << node
|
257
|
+
@cycles << cycle
|
258
|
+
end
|
259
|
+
return @visited[node]
|
260
|
+
end
|
261
|
+
# return nil if the node has not been visited but has been navigated in a depth-first visit
|
262
|
+
return if @lineage.include?(node)
|
263
|
+
# all systems go: visit the node graph
|
264
|
+
visit_node_and_children(node, &operator)
|
265
|
+
end
|
266
|
+
|
267
|
+
# Visits the given node and its children. If this visitor is #{depth_first?}, then the
|
268
|
+
# operator is applied to the children before the given node. Otherwise, the operator is
|
269
|
+
# applied to the children after the given node. The default operator returns the visited
|
270
|
+
# node itself.
|
271
|
+
#
|
272
|
+
# @param node the node to visit
|
273
|
+
# @yield (see #visit)
|
274
|
+
# @yieldparam (see #visit)
|
275
|
+
def visit_node_and_children(node, &operator)
|
276
|
+
# set the current node
|
277
|
+
@lineage.push(node)
|
278
|
+
# if depth-first, then visit the children before the current node
|
279
|
+
visit_children(node, &operator) if depth_first?
|
280
|
+
# apply the operator to the current node, if given
|
281
|
+
result = @visited[node] = block_given? ? yield(node) : node
|
282
|
+
logger.debug { "#{self} visited #{node.qp} with result #{result.qp}" } if @verbose
|
283
|
+
# if not depth-first, then visit the children after the current node
|
284
|
+
visit_children(node, &operator) unless depth_first?
|
285
|
+
@lineage.pop
|
286
|
+
# return the visit result
|
287
|
+
result
|
288
|
+
end
|
289
|
+
|
290
|
+
def visit_children(parent, &operator)
|
291
|
+
@navigator.call(parent).enumerate { |child| visit_recursive(child, &operator) }
|
292
|
+
end
|
293
|
+
|
294
|
+
class VisitorEnumerator
|
295
|
+
include Enumerable
|
296
|
+
|
297
|
+
def initialize(visitor, node)
|
298
|
+
@visitor = visitor
|
299
|
+
@root = node
|
300
|
+
end
|
301
|
+
|
302
|
+
def each
|
303
|
+
@visitor.visit(@root) { |node| yield(node) }
|
304
|
+
end
|
305
|
+
end
|
306
|
+
|
307
|
+
class SyncVisitor < Visitor
|
308
|
+
# @param [Visitor] visitor the Visitor which will visit synchronized input
|
309
|
+
# @yield (see Visitor#sync)
|
310
|
+
def initialize(visitor, &matcher)
|
311
|
+
# the next node to visit is an array of child node pairs matched by the given matcher block
|
312
|
+
super() { |nodes| match_children(visitor, nodes, &matcher) }
|
313
|
+
end
|
314
|
+
|
315
|
+
# Visits the given pair of nodes.
|
316
|
+
#
|
317
|
+
# Raises ArgumentError if nodes does not consist of either two node arguments or one two-item Array
|
318
|
+
# argument.
|
319
|
+
def visit(*nodes)
|
320
|
+
if nodes.size == 1 then
|
321
|
+
nodes = nodes.first
|
322
|
+
Jinx.fail(ArgumentError, "Sync visitor requires a pair of entry nodes") unless nodes.size == 2
|
323
|
+
end
|
324
|
+
super(nodes)
|
325
|
+
end
|
326
|
+
|
327
|
+
# Returns an Enumerable which applies the given block to each matched node starting at the given nodes.
|
328
|
+
#
|
329
|
+
# Raises ArgumentError if nodes does not consist of either two node arguments or one two-item Array
|
330
|
+
# argument.
|
331
|
+
def to_enum(*nodes)
|
332
|
+
if nodes.size == 1 then
|
333
|
+
nodes = nodes.first
|
334
|
+
Jinx.fail(ArgumentError, "Sync visitor requires a pair of entry nodes") unless nodes.size == 2
|
335
|
+
end
|
336
|
+
super(nodes)
|
337
|
+
end
|
338
|
+
|
339
|
+
private
|
340
|
+
|
341
|
+
# Returns an array of arrays of matched children from the given parent nodes. The children are matched
|
342
|
+
# using the block given to this method, if supplied, or by index otherwise.
|
343
|
+
#
|
344
|
+
# @see #sync a usage example
|
345
|
+
# @yield (see Visitor#sync)
|
346
|
+
def match_children(visitor, nodes)
|
347
|
+
# the parent nodes
|
348
|
+
p1, p2 = nodes
|
349
|
+
# this visitor's children
|
350
|
+
c1 = visitor.node_children(p1)
|
351
|
+
c2 = p2 ? visitor.node_children(p2) : []
|
352
|
+
|
353
|
+
# Apply the matcher block on each of this visitor's children and the other children.
|
354
|
+
# If no block is given, then group the children by index, which is the transpose of the array of
|
355
|
+
# children arrays.
|
356
|
+
if block_given? then
|
357
|
+
# Match each item in the first children array to an item from the second children array using
|
358
|
+
# then given block.
|
359
|
+
matches = yield(c1, c2)
|
360
|
+
c1.map { |c| [c, matches[c]] }
|
361
|
+
else
|
362
|
+
# Ensure that both children arrays are the same size.
|
363
|
+
others = c2.size <= c1.size ? c2.fill(nil, c2.size...c1.size) : c2[0, c1.size]
|
364
|
+
# The children grouped by index is the transpose of the array of children arrays.
|
365
|
+
[c1, others].transpose
|
366
|
+
end
|
367
|
+
end
|
368
|
+
end
|
369
|
+
end
|
370
|
+
end
|
@@ -0,0 +1,77 @@
|
|
1
|
+
module Jinx
|
2
|
+
# Helper class which adds files to the Java class path.
|
3
|
+
class ClassPathModifier
|
4
|
+
# Adds the directories in the given path and all Java jar files contained in
|
5
|
+
# the directories to the Java classpath.
|
6
|
+
#
|
7
|
+
# @quirk Java The jar files found by this method are added to the classpath
|
8
|
+
# in sort order. Java applications usually add jars in sort order. For examle,
|
9
|
+
# the Apache Ant directory-based classpath tasks are in sort order, although
|
10
|
+
# this is not stipulated in the documentation. Well-behaved Java libraries are
|
11
|
+
# not dependent on the sort order of included jar files. For poorly-behaved
|
12
|
+
# Java libraries, ensure that the classpath is in the expected order. If the
|
13
|
+
# classpath must be in a non-sorted order, then call {#add_to_classpath}
|
14
|
+
# on each jar file instead.
|
15
|
+
#
|
16
|
+
# @param [String] path the colon or semi-colon separated directories
|
17
|
+
def expand_to_class_path(path)
|
18
|
+
# the path separator
|
19
|
+
sep = path[WINDOWS_PATH_SEP] ? WINDOWS_PATH_SEP : UNIX_PATH_SEP
|
20
|
+
# the path directories
|
21
|
+
dirs = path.split(sep).map { |dir| File.expand_path(dir) }
|
22
|
+
expanded = expand_jars(dirs)
|
23
|
+
expanded.each { |dir| add_to_classpath(dir) }
|
24
|
+
end
|
25
|
+
|
26
|
+
# Adds the given jar file or directory to the classpath.
|
27
|
+
#
|
28
|
+
# @param [String] file the jar file or directory to add
|
29
|
+
def add_to_classpath(file)
|
30
|
+
unless File.exist?(file) then
|
31
|
+
logger.warn("File to place on Java classpath does not exist: #{file}")
|
32
|
+
return
|
33
|
+
end
|
34
|
+
if File.extname(file) == '.jar' then
|
35
|
+
# require is preferred to classpath append for a jar file.
|
36
|
+
require file
|
37
|
+
else
|
38
|
+
# A directory must end in a slash since JRuby uses an URLClassLoader.
|
39
|
+
if File.directory?(file) then
|
40
|
+
last = file[-1, 1]
|
41
|
+
if last == "\\" then
|
42
|
+
file = file[0...-1] + '/'
|
43
|
+
elsif last != '/' then
|
44
|
+
file = file + '/'
|
45
|
+
end
|
46
|
+
end
|
47
|
+
# Append the file to the classpath.
|
48
|
+
$CLASSPATH << file
|
49
|
+
end
|
50
|
+
end
|
51
|
+
|
52
|
+
private
|
53
|
+
|
54
|
+
# The Windows semi-colon path separator.
|
55
|
+
WINDOWS_PATH_SEP = ';'
|
56
|
+
|
57
|
+
# The Unix colon path separator.
|
58
|
+
UNIX_PATH_SEP = ':'
|
59
|
+
|
60
|
+
# Expands the given directories to include the contained jar files.
|
61
|
+
# If a directory contains jar files, then the jar files are included in
|
62
|
+
# the resulting array. Otherwise, the directory itself is included in
|
63
|
+
# the resulting array.
|
64
|
+
#
|
65
|
+
# @param [<String>] directories the directories containing jars to add
|
66
|
+
# @return [<String>] each directory or its jars
|
67
|
+
def expand_jars(directories)
|
68
|
+
# If there are jar files, then the file list is the sorted jar files.
|
69
|
+
# Otherwise, the file list is a singleton directory array.
|
70
|
+
expanded = directories.map do |dir|
|
71
|
+
jars = Dir[File.join(dir , "**", "*.jar")].sort
|
72
|
+
jars.empty? ? [dir] : jars
|
73
|
+
end
|
74
|
+
expanded.flatten
|
75
|
+
end
|
76
|
+
end
|
77
|
+
end
|