jinx 2.1.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (149) hide show
  1. data/.gitignore +14 -0
  2. data/.rspec +3 -0
  3. data/.yardopts +1 -0
  4. data/Gemfile +6 -0
  5. data/Gemfile.lock +27 -0
  6. data/History.md +6 -0
  7. data/LEGAL +5 -0
  8. data/LICENSE +22 -0
  9. data/README.md +44 -0
  10. data/Rakefile +41 -0
  11. data/examples/family/README.md +10 -0
  12. data/examples/family/ext/build.xml +35 -0
  13. data/examples/family/ext/src/family/Address.java +68 -0
  14. data/examples/family/ext/src/family/Child.java +24 -0
  15. data/examples/family/ext/src/family/DomainObject.java +26 -0
  16. data/examples/family/ext/src/family/Household.java +36 -0
  17. data/examples/family/ext/src/family/Parent.java +48 -0
  18. data/examples/family/ext/src/family/Person.java +42 -0
  19. data/examples/family/lib/family.rb +15 -0
  20. data/examples/family/lib/family/address.rb +6 -0
  21. data/examples/family/lib/family/domain_object.rb +6 -0
  22. data/examples/family/lib/family/household.rb +6 -0
  23. data/examples/family/lib/family/parent.rb +16 -0
  24. data/examples/family/lib/family/person.rb +6 -0
  25. data/examples/model/README.md +25 -0
  26. data/examples/model/ext/build.xml +35 -0
  27. data/examples/model/ext/src/domain/Child.java +192 -0
  28. data/examples/model/ext/src/domain/Dependent.java +29 -0
  29. data/examples/model/ext/src/domain/DomainObject.java +26 -0
  30. data/examples/model/ext/src/domain/Independent.java +83 -0
  31. data/examples/model/ext/src/domain/Parent.java +129 -0
  32. data/examples/model/ext/src/domain/Person.java +14 -0
  33. data/examples/model/lib/model.rb +13 -0
  34. data/examples/model/lib/model/child.rb +13 -0
  35. data/examples/model/lib/model/domain_object.rb +6 -0
  36. data/examples/model/lib/model/independent.rb +11 -0
  37. data/examples/model/lib/model/parent.rb +17 -0
  38. data/jinx.gemspec +22 -0
  39. data/lib/jinx.rb +3 -0
  40. data/lib/jinx/active_support/README.txt +2 -0
  41. data/lib/jinx/active_support/core_ext/string.rb +7 -0
  42. data/lib/jinx/active_support/core_ext/string/inflections.rb +167 -0
  43. data/lib/jinx/active_support/inflections.rb +55 -0
  44. data/lib/jinx/active_support/inflector.rb +398 -0
  45. data/lib/jinx/cli/application.rb +36 -0
  46. data/lib/jinx/cli/command.rb +214 -0
  47. data/lib/jinx/helpers/array.rb +108 -0
  48. data/lib/jinx/helpers/boolean.rb +42 -0
  49. data/lib/jinx/helpers/case_insensitive_hash.rb +39 -0
  50. data/lib/jinx/helpers/class.rb +149 -0
  51. data/lib/jinx/helpers/collection.rb +33 -0
  52. data/lib/jinx/helpers/collections.rb +11 -0
  53. data/lib/jinx/helpers/collector.rb +20 -0
  54. data/lib/jinx/helpers/conditional_enumerator.rb +21 -0
  55. data/lib/jinx/helpers/enumerable.rb +242 -0
  56. data/lib/jinx/helpers/enumerate.rb +35 -0
  57. data/lib/jinx/helpers/error.rb +15 -0
  58. data/lib/jinx/helpers/file_separator.rb +65 -0
  59. data/lib/jinx/helpers/filter.rb +52 -0
  60. data/lib/jinx/helpers/flattener.rb +38 -0
  61. data/lib/jinx/helpers/hash.rb +12 -0
  62. data/lib/jinx/helpers/hashable.rb +502 -0
  63. data/lib/jinx/helpers/inflector.rb +36 -0
  64. data/lib/jinx/helpers/key_transformer_hash.rb +43 -0
  65. data/lib/jinx/helpers/lazy_hash.rb +44 -0
  66. data/lib/jinx/helpers/log.rb +106 -0
  67. data/lib/jinx/helpers/math.rb +12 -0
  68. data/lib/jinx/helpers/merge.rb +60 -0
  69. data/lib/jinx/helpers/module.rb +18 -0
  70. data/lib/jinx/helpers/multi_enumerator.rb +31 -0
  71. data/lib/jinx/helpers/options.rb +92 -0
  72. data/lib/jinx/helpers/os.rb +19 -0
  73. data/lib/jinx/helpers/partial_order.rb +37 -0
  74. data/lib/jinx/helpers/pretty_print.rb +207 -0
  75. data/lib/jinx/helpers/set.rb +8 -0
  76. data/lib/jinx/helpers/stopwatch.rb +76 -0
  77. data/lib/jinx/helpers/transformer.rb +24 -0
  78. data/lib/jinx/helpers/transitive_closure.rb +55 -0
  79. data/lib/jinx/helpers/uniquifier.rb +50 -0
  80. data/lib/jinx/helpers/validation.rb +33 -0
  81. data/lib/jinx/helpers/visitor.rb +370 -0
  82. data/lib/jinx/import/class_path_modifier.rb +77 -0
  83. data/lib/jinx/import/java.rb +337 -0
  84. data/lib/jinx/importer.rb +240 -0
  85. data/lib/jinx/metadata.rb +155 -0
  86. data/lib/jinx/metadata/attribute_enumerator.rb +73 -0
  87. data/lib/jinx/metadata/dependency.rb +244 -0
  88. data/lib/jinx/metadata/id_alias.rb +23 -0
  89. data/lib/jinx/metadata/introspector.rb +179 -0
  90. data/lib/jinx/metadata/inverse.rb +170 -0
  91. data/lib/jinx/metadata/java_property.rb +169 -0
  92. data/lib/jinx/metadata/propertied.rb +500 -0
  93. data/lib/jinx/metadata/property.rb +401 -0
  94. data/lib/jinx/metadata/property_characteristics.rb +114 -0
  95. data/lib/jinx/resource.rb +862 -0
  96. data/lib/jinx/resource/copy_visitor.rb +36 -0
  97. data/lib/jinx/resource/inversible.rb +90 -0
  98. data/lib/jinx/resource/match_visitor.rb +180 -0
  99. data/lib/jinx/resource/matcher.rb +20 -0
  100. data/lib/jinx/resource/merge_visitor.rb +73 -0
  101. data/lib/jinx/resource/mergeable.rb +185 -0
  102. data/lib/jinx/resource/reference_enumerator.rb +49 -0
  103. data/lib/jinx/resource/reference_path_visitor.rb +38 -0
  104. data/lib/jinx/resource/reference_visitor.rb +55 -0
  105. data/lib/jinx/resource/unique.rb +35 -0
  106. data/lib/jinx/version.rb +3 -0
  107. data/spec/defaults_spec.rb +30 -0
  108. data/spec/definitions/model/alias/child.rb +5 -0
  109. data/spec/definitions/model/base/child.rb +5 -0
  110. data/spec/definitions/model/base/domain_object.rb +5 -0
  111. data/spec/definitions/model/base/independent.rb +5 -0
  112. data/spec/definitions/model/defaults/child.rb +5 -0
  113. data/spec/definitions/model/dependency/child.rb +5 -0
  114. data/spec/definitions/model/dependency/parent.rb +6 -0
  115. data/spec/definitions/model/inverse/child.rb +5 -0
  116. data/spec/definitions/model/inverse/independent.rb +5 -0
  117. data/spec/definitions/model/inverse/parent.rb +5 -0
  118. data/spec/definitions/model/mandatory/child.rb +6 -0
  119. data/spec/dependency_spec.rb +47 -0
  120. data/spec/family_spec.rb +64 -0
  121. data/spec/inverse_spec.rb +53 -0
  122. data/spec/mandatory_spec.rb +43 -0
  123. data/spec/metadata_spec.rb +68 -0
  124. data/spec/resource_spec.rb +30 -0
  125. data/spec/spec_helper.rb +3 -0
  126. data/spec/support/model.rb +19 -0
  127. data/test/fixtures/line_separator/cr_line_sep.txt +1 -0
  128. data/test/fixtures/line_separator/crlf_line_sep.txt +3 -0
  129. data/test/fixtures/line_separator/lf_line_sep.txt +3 -0
  130. data/test/fixtures/mixed/ext/build.xml +35 -0
  131. data/test/fixtures/mixed/ext/src/mixed/Case/Example.java +5 -0
  132. data/test/helper.rb +7 -0
  133. data/test/lib/jinx/command_test.rb +41 -0
  134. data/test/lib/jinx/helpers/boolean_test.rb +27 -0
  135. data/test/lib/jinx/helpers/class_test.rb +60 -0
  136. data/test/lib/jinx/helpers/collections_test.rb +402 -0
  137. data/test/lib/jinx/helpers/file_separator_test.rb +29 -0
  138. data/test/lib/jinx/helpers/inflector_test.rb +11 -0
  139. data/test/lib/jinx/helpers/lazy_hash_test.rb +32 -0
  140. data/test/lib/jinx/helpers/module_test.rb +24 -0
  141. data/test/lib/jinx/helpers/options_test.rb +66 -0
  142. data/test/lib/jinx/helpers/partial_order_test.rb +41 -0
  143. data/test/lib/jinx/helpers/pretty_print_test.rb +83 -0
  144. data/test/lib/jinx/helpers/stopwatch_test.rb +16 -0
  145. data/test/lib/jinx/helpers/transitive_closure_test.rb +80 -0
  146. data/test/lib/jinx/helpers/visitor_test.rb +288 -0
  147. data/test/lib/jinx/import/java_test.rb +78 -0
  148. data/test/lib/jinx/import/mixed_case_test.rb +16 -0
  149. 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