jinx 2.1.1
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/.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
|