caruby-core 1.4.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.
Files changed (86) hide show
  1. data/History.txt +4 -0
  2. data/LEGAL +5 -0
  3. data/LICENSE +22 -0
  4. data/README.md +51 -0
  5. data/doc/website/css/site.css +1 -5
  6. data/doc/website/images/avatar.png +0 -0
  7. data/doc/website/images/favicon.ico +0 -0
  8. data/doc/website/images/logo.png +0 -0
  9. data/doc/website/index.html +82 -0
  10. data/doc/website/install.html +87 -0
  11. data/doc/website/quick_start.html +87 -0
  12. data/doc/website/tissue.html +85 -0
  13. data/doc/website/uom.html +10 -0
  14. data/lib/caruby.rb +3 -0
  15. data/lib/caruby/active_support/README.txt +2 -0
  16. data/lib/caruby/active_support/core_ext/string.rb +7 -0
  17. data/lib/caruby/active_support/core_ext/string/inflections.rb +167 -0
  18. data/lib/caruby/active_support/inflections.rb +55 -0
  19. data/lib/caruby/active_support/inflector.rb +398 -0
  20. data/lib/caruby/cli/application.rb +36 -0
  21. data/lib/caruby/cli/command.rb +169 -0
  22. data/lib/caruby/csv/csv_mapper.rb +157 -0
  23. data/lib/caruby/csv/csvio.rb +185 -0
  24. data/lib/caruby/database.rb +252 -0
  25. data/lib/caruby/database/fetched_matcher.rb +66 -0
  26. data/lib/caruby/database/persistable.rb +432 -0
  27. data/lib/caruby/database/persistence_service.rb +162 -0
  28. data/lib/caruby/database/reader.rb +599 -0
  29. data/lib/caruby/database/saved_merger.rb +131 -0
  30. data/lib/caruby/database/search_template_builder.rb +59 -0
  31. data/lib/caruby/database/sql_executor.rb +75 -0
  32. data/lib/caruby/database/store_template_builder.rb +200 -0
  33. data/lib/caruby/database/writer.rb +469 -0
  34. data/lib/caruby/domain/annotatable.rb +25 -0
  35. data/lib/caruby/domain/annotation.rb +23 -0
  36. data/lib/caruby/domain/attribute_metadata.rb +447 -0
  37. data/lib/caruby/domain/java_attribute_metadata.rb +160 -0
  38. data/lib/caruby/domain/merge.rb +91 -0
  39. data/lib/caruby/domain/properties.rb +95 -0
  40. data/lib/caruby/domain/reference_visitor.rb +289 -0
  41. data/lib/caruby/domain/resource_attributes.rb +528 -0
  42. data/lib/caruby/domain/resource_dependency.rb +205 -0
  43. data/lib/caruby/domain/resource_introspection.rb +159 -0
  44. data/lib/caruby/domain/resource_metadata.rb +117 -0
  45. data/lib/caruby/domain/resource_module.rb +285 -0
  46. data/lib/caruby/domain/uniquify.rb +38 -0
  47. data/lib/caruby/import/annotatable_class.rb +28 -0
  48. data/lib/caruby/import/annotation_class.rb +27 -0
  49. data/lib/caruby/import/annotation_module.rb +67 -0
  50. data/lib/caruby/import/java.rb +338 -0
  51. data/lib/caruby/migration/migratable.rb +167 -0
  52. data/lib/caruby/migration/migrator.rb +533 -0
  53. data/lib/caruby/migration/resource.rb +8 -0
  54. data/lib/caruby/migration/resource_module.rb +11 -0
  55. data/lib/caruby/migration/uniquify.rb +20 -0
  56. data/lib/caruby/resource.rb +969 -0
  57. data/lib/caruby/util/attribute_path.rb +46 -0
  58. data/lib/caruby/util/cache.rb +53 -0
  59. data/lib/caruby/util/class.rb +99 -0
  60. data/lib/caruby/util/collection.rb +1053 -0
  61. data/lib/caruby/util/controlled_value.rb +35 -0
  62. data/lib/caruby/util/coordinate.rb +75 -0
  63. data/lib/caruby/util/domain_extent.rb +49 -0
  64. data/lib/caruby/util/file_separator.rb +65 -0
  65. data/lib/caruby/util/inflector.rb +20 -0
  66. data/lib/caruby/util/log.rb +95 -0
  67. data/lib/caruby/util/math.rb +12 -0
  68. data/lib/caruby/util/merge.rb +59 -0
  69. data/lib/caruby/util/module.rb +34 -0
  70. data/lib/caruby/util/options.rb +92 -0
  71. data/lib/caruby/util/partial_order.rb +36 -0
  72. data/lib/caruby/util/person.rb +119 -0
  73. data/lib/caruby/util/pretty_print.rb +184 -0
  74. data/lib/caruby/util/properties.rb +112 -0
  75. data/lib/caruby/util/stopwatch.rb +66 -0
  76. data/lib/caruby/util/topological_sync_enumerator.rb +53 -0
  77. data/lib/caruby/util/transitive_closure.rb +45 -0
  78. data/lib/caruby/util/tree.rb +48 -0
  79. data/lib/caruby/util/trie.rb +37 -0
  80. data/lib/caruby/util/uniquifier.rb +30 -0
  81. data/lib/caruby/util/validation.rb +48 -0
  82. data/lib/caruby/util/version.rb +56 -0
  83. data/lib/caruby/util/visitor.rb +351 -0
  84. data/lib/caruby/util/weak_hash.rb +36 -0
  85. data/lib/caruby/version.rb +3 -0
  86. metadata +186 -0
@@ -0,0 +1,66 @@
1
+ require 'benchmark'
2
+
3
+ # Stopwatch is a simple execution time accumulator.
4
+ class Stopwatch
5
+ # Time accumulates elapsed real time and total CPU time.
6
+ class Time
7
+ # The Benchmark::Tms wrapped by this Time.
8
+ attr_reader :tms
9
+
10
+ def initialize(tms=nil)
11
+ @tms = tms || Benchmark::Tms.new
12
+ end
13
+
14
+ # Returns the cumulative elapsed real clock time.
15
+ def elapsed
16
+ @tms.real
17
+ end
18
+
19
+ # Returns the cumulative CPU total time.
20
+ def cpu
21
+ @tms.total
22
+ end
23
+
24
+ # Adds the time to execute the given block to this time. Returns the split execution Time.
25
+ def split(&block)
26
+ stms = Benchmark.measure(&block)
27
+ @tms += stms
28
+ Time.new(stms)
29
+ end
30
+
31
+ def reset
32
+ @tms = Benchmark::Tms.new
33
+ end
34
+ end
35
+
36
+ # Executes the given block. Returns the execution Time.
37
+ def self.measure(&block)
38
+ new.run(&block)
39
+ end
40
+
41
+ # Creates a new idle Stopwatch.
42
+ def initialize
43
+ @time = Time.new
44
+ end
45
+
46
+ # Executes the given block. Accumulates the execution time in this Stopwatch.
47
+ # Returns the execution Time.
48
+ def run(&block)
49
+ @time.split(&block)
50
+ end
51
+
52
+ # Returns the cumulative elapsed real clock time spent in {#run} executions.
53
+ def elapsed
54
+ @time.elapsed
55
+ end
56
+
57
+ # Returns the cumulative CPU total time spent in {#run} executions for the current process and its children.
58
+ def cpu
59
+ @time.cpu
60
+ end
61
+
62
+ # Resets this Stopwatch's cumulative time to zero.
63
+ def reset
64
+ @time.reset
65
+ end
66
+ end
@@ -0,0 +1,53 @@
1
+ require 'caruby/util/collection'
2
+
3
+ class TopologicalSyncEnumerator
4
+ include Enumerable
5
+
6
+ def initialize(targets, sources, symbol, &matcher)
7
+ @tgts = targets
8
+ @srcs = sources
9
+ @mthd = symbol
10
+ @matcher = matcher || lambda { |tgt, srcs| srcs.first }
11
+ end
12
+
13
+ # Calls the given block on each matching target and source.
14
+ # Returns the matching target => source hash.
15
+ def each # :yields: target, source
16
+ # the parent hashes for targets and sources
17
+ pt = @tgts.to_compact_hash { |tgt| tgt.send(@mthd) }
18
+ ps = @srcs.to_compact_hash { |src| src.send(@mthd) }
19
+
20
+ # the child hashes
21
+ ct = LazyHash.new { Array.new }
22
+ cs = LazyHash.new { Array.new }
23
+
24
+ # collect the chidren and roots
25
+ rt = @tgts.reject { |tgt| p = pt[tgt]; ct[p] << tgt if p }
26
+ rs = @srcs.reject { |src| p = ps[src]; cs[p] << src if p }
27
+
28
+ # the match hash
29
+ matches = {}
30
+ # match recursively
31
+ each_match_recursive(rt, rs, ct, cs) do |tgt, src|
32
+ yield(tgt, src)
33
+ matches[tgt] = src
34
+ end
35
+
36
+ matches
37
+ end
38
+
39
+ private
40
+
41
+ def each_match_recursive(targets, sources, ct, cs, &block)
42
+ # copy the sources
43
+ srcs = sources.dup
44
+ # match each target, removing the matched source for the
45
+ # next iteration
46
+ targets.each do |tgt|
47
+ src = @matcher.call(tgt, srcs) || next
48
+ yield(tgt, src)
49
+ srcs.delete(src)
50
+ each_match_recursive(ct[tgt], cs[src], ct, cs, &block)
51
+ end
52
+ end
53
+ end
@@ -0,0 +1,45 @@
1
+ require 'caruby/util/visitor'
2
+
3
+ class Object
4
+ # Returns the transitive closure over a method or block, e.g.:
5
+ # class Node
6
+ # attr_reader :parent, :children
7
+ # def initialize(name, parent=nil)
8
+ # super()
9
+ # @name = name
10
+ # @parent = parent
11
+ # @children = []
12
+ # parent.children << self if parent
13
+ # end
14
+ #
15
+ # def to_s;
16
+ # end
17
+ # a = Node.new('a'); b = Node.new('b', a), c = Node.new('c', a); d = Node.new('d', c)
18
+ # a.transitive_closure { |node| node.children }.to_a.join(", ") #=> a, b, c, d
19
+ # a.transitive_closure(:children).to_a.join(", ") #=> a, b, c, d
20
+ # This method returns an array partially ordered by the closure method, i.e.
21
+ # each node occurs before all other nodes referenced directly or indirectly by the closure method.
22
+ #
23
+ # If a method symbol or name is provided, then that method is called. Otherwise, the block is called.
24
+ # In either case, the call is expected to return an object or Enumerable of objects which also respond
25
+ # to the method or block.
26
+ def transitive_closure(method=nil)
27
+ raise ArgumentError.new("Missing both a method argument and a block") if method.nil? and not block_given?
28
+ return transitive_closure() { |node| node.send(method) } if method
29
+ Visitor.new(:depth_first) { |node| yield node }.to_enum(self).to_a.reverse
30
+ end
31
+ end
32
+
33
+ module Enumerable
34
+ # Returns the transitive closure over all items in this Enumerable.
35
+ #
36
+ # @see Object#transitive_closure
37
+ def transitive_closure(method=nil, &navigator)
38
+ # delegate to Object if there is a method argument
39
+ return super(method, &navigator) if method
40
+ # this Enumerable's children are this Enumerable's contents
41
+ closure = super() { |node| node.equal?(self) ? self : yield(node) }
42
+ # remove this collection from the closure
43
+ closure[1..-1]
44
+ end
45
+ end
@@ -0,0 +1,48 @@
1
+ # A Tree consists of a root node and subtree children.
2
+ # A Tree can be decorated with an optional value.
3
+ class Tree
4
+ attr_reader :root, :children
5
+
6
+ attr_accessor :value
7
+
8
+ # Creates a Trie with the given root node.
9
+ def initialize(root=nil)
10
+ @root = root
11
+ @children = []
12
+ end
13
+
14
+ # Adds a subtree rooted at the given node as a child of this tree.
15
+ def <<(node)
16
+ @children << self.class.new(node)
17
+ self
18
+ end
19
+
20
+ # Returns the subtree at the given node path.
21
+ def subtree(*path)
22
+ return self if path.empty?
23
+ first = path.shift
24
+ tree = @children.detect { |child| child.root == first }
25
+ tree.subtree(*path) if tree
26
+ end
27
+
28
+ alias :[] :subtree
29
+
30
+ # Creates the given node path if it does not yet exist.
31
+ # Returns the subtree at the path.
32
+ def fill(*path)
33
+ return self if path.empty?
34
+ first = path.shift
35
+ tree = subtree(first)
36
+ if tree.nil? then
37
+ self << first
38
+ tree = @children.last
39
+ end
40
+ tree.fill(*path)
41
+ end
42
+
43
+ def to_s
44
+ root_s = @root.nil? || Symbol === root ? root.inspect : root.to_s
45
+ return "[#{root_s}]" if @children.empty?
46
+ "[#{root_s} -> #{@children.join(', ')}]"
47
+ end
48
+ end
@@ -0,0 +1,37 @@
1
+ require File.join(File.dirname(__FILE__), 'tree')
2
+
3
+ # A Trie[http://en.wikipedia.org/wiki/Trie] is an associative access tree structure.
4
+ #
5
+ # @example
6
+ # trie = Trie.new
7
+ # trie[:a, :b] = 1
8
+ # trie[:a, :b] #=> 1
9
+ # trie[:a, :c] #=> nil
10
+ class Trie
11
+ # Creates a new empty Trie.
12
+ def initialize
13
+ @tree = Tree.new
14
+ end
15
+
16
+ # @return the value at the given trie path
17
+ def [](*path)
18
+ tree = @tree[nil, *path]
19
+ tree.value if tree
20
+ end
21
+
22
+ # @return the top_level Tree for this trie
23
+ def to_tree
24
+ @tree
25
+ end
26
+
27
+ # Sets the value for a node path.
28
+ #
29
+ # @example
30
+ # trie = Trie.new
31
+ # trie[:a, :b] = 1
32
+ # trie[:a, :b] #=> 1
33
+ def []=(*path_and_value)
34
+ value = path_and_value.pop
35
+ @tree.fill(nil, *path_and_value).value = value
36
+ end
37
+ end
@@ -0,0 +1,30 @@
1
+ # A test utility class to generate id qualifiers.
2
+ class Uniquifier
3
+ # Returns a relatively unique integral qualifier. Successive calls to this method
4
+ # within the same time zone spaced more than a millisecond apart return different
5
+ # integers. Each generated qualifier is greater than the previous by an unspecified
6
+ # amount.
7
+ def self.qualifier
8
+ # the first date that this method could be called
9
+ @first ||= Date.new(2000, 01, 01)
10
+ # days as integer + milliseconds as fraction since the first date
11
+ diff = DateTime.now - @first
12
+ # shift a tenth of a milli up into the integer portion
13
+ decimillis = diff * 24 * 60 * 60 * 10000
14
+ # truncate the fraction
15
+ decimillis.truncate
16
+ end
17
+ end
18
+
19
+ class String
20
+ # Returns a relatively unique value obtained from the specified base value.
21
+ # Spaces are removed, e.g.:
22
+ # Uniquifier.uniquify('Test Name')
23
+ # might produce:
24
+ # Test_Name_3309388006
25
+ #
26
+ # The suffix is generated by {Uniquifier.qualifier}.
27
+ def uniquify
28
+ gsub(' ', '_') + "_#{Uniquifier.qualifier}"
29
+ end
30
+ end
@@ -0,0 +1,48 @@
1
+ # Raised when an object fails a validation test.
2
+ class ValidationError < RuntimeError; end
3
+
4
+ class Object
5
+ # Returns whether this object is nil, false, empty, or a whitespace string.
6
+ # For example, "", " ", +nil+, [], and {} are blank.
7
+ #
8
+ # This method is borrowed from Rails ActiveSupport.
9
+ def blank?
10
+ respond_to?(:empty?) ? empty? : !self
11
+ end
12
+
13
+ # Returns whether this object is nil, empty, or a whitespace string.
14
+ # For example, "", " ", +nil+, [], and {} return +true+.
15
+ #
16
+ # This method differs from blank? in that +false+ is an allowed value.
17
+ def nil_or_empty?
18
+ nil? or (respond_to?(:empty?) and empty?)
19
+ end
20
+ end
21
+
22
+ module Validation
23
+ # A Validator is a procedure which responds to the +validate(value)+ method.
24
+ class Validator < Proc
25
+ alias :validate :call
26
+ end
27
+
28
+ # Validates that each key value in the value_type_assns value => type hash is an instance of the associated class.
29
+ #
30
+ # Raises ValidationError if the value is missing.
31
+ # Raises TypeError if the value is not the specified type.
32
+ def validate_type(value_type_assns)
33
+ TYPE_VALIDATOR.validate(value_type_assns)
34
+ end
35
+
36
+ private
37
+
38
+ def self.create_type_validator
39
+ Validator.new do |value_type_assns|
40
+ value_type_assns.each do |value, type|
41
+ raise ArgumentError.new("Missing #{type.name} argument") if value.nil?
42
+ raise TypeError.new("Unsupported argument type; expected: #{type.name} found: #{value.class.name}") unless type === value
43
+ end
44
+ end
45
+ end
46
+
47
+ TYPE_VALIDATOR = create_type_validator
48
+ end
@@ -0,0 +1,56 @@
1
+ # A Version is an Array of version major and minor components that is comparable to
2
+ # another version identifier based on a precedence relationship.
3
+ class Version < Array
4
+ include Comparable
5
+
6
+ attr_reader :predecessor
7
+
8
+ # Creates a new Version from the given version components and optional predecessor.
9
+ #
10
+ # @example
11
+ # alpha = Version.new(1, '1alpha')
12
+ # Version.new(1, 1, alpha) > alpha #=> true
13
+ def initialize(*params)
14
+ @predecessor = params.pop if self.class === params.last
15
+ super(params)
16
+ end
17
+
18
+ # Returns the comparison of this version identifier to the other version identifier as follows:
19
+ # * if this version can be compared to other via the predecessor graph, then return that comparison result
20
+ # * otherwise, return a component-wise comparison
21
+ #
22
+ # @example
23
+ # beta = Version.new(1, '1beta')
24
+ # Version.new(1) < beta > #=> true
25
+ # Version.new(1, 1) < beta #=> true
26
+ # Version.new(1, 1, beta) > beta #=> true
27
+ def <=>(other)
28
+ return 0 if equal?(other)
29
+ raise ArgumentError.new("Comparand is not a #{self.class}: #{other}") unless self.class === other
30
+ return -1 if other.predecessor == self
31
+ return 1 unless predecessor.nil? or predecessor < other
32
+ each_with_index do |component, index|
33
+ return 1 unless index < other.length
34
+ other_component = other[index]
35
+ if String === other_component then
36
+ component = component.to_s
37
+ elsif String === component
38
+ other_component = other_component.to_s
39
+ end
40
+ cmp = (component <=> other_component)
41
+ return cmp unless cmp.zero?
42
+ end
43
+ length < other.length ? -1 : 0
44
+ end
45
+ end
46
+
47
+ class String
48
+ # Returns this String as a Version.
49
+ #
50
+ # @example
51
+ # "1.2.1alpha".to_version #=> [1, 2, "1alpha"]
52
+ def to_version
53
+ components = split('.').map { |component| component =~ /[\D]/ ? component : component.to_i }
54
+ Version.new(*components)
55
+ end
56
+ end
@@ -0,0 +1,351 @@
1
+ require 'caruby/util/collection'
2
+ require 'caruby/util/options'
3
+
4
+ # Enumerator overwrites to_enum, so include it first
5
+ require 'enumerator'
6
+ require 'generator'
7
+
8
+ # Error raised on a visit failure.
9
+ class VisitError < RuntimeError; end
10
+
11
+ # Visitor traverses items and applies an operation, e.g.:
12
+ # class Node
13
+ # attr_accessor :children, :value
14
+ # def initialize(value, parent=nil)
15
+ # @value = value
16
+ # @children = []
17
+ # @parent = parent
18
+ # @parent.children << self if @parent
19
+ # end
20
+ # end
21
+ # parent = Node.new(1)
22
+ # child = Node.new(2, parent)
23
+ # multiplier = 2
24
+ # Visitor.new { |node| node.children }.visit(parent) { |node| node.value *= multiplier } #=> 2
25
+ # parent.value #=> 2
26
+ # child.value #=> 4
27
+ #
28
+ # The visit result is the result of evaluating the operation block on the initial visited node.
29
+ # Visiting a collection returns an array of the result of visiting each member of the collection,
30
+ # e.g. augmenting the preceding example:
31
+ # parent2 = Node.new(3)
32
+ # child2 = Node.new(4, parent2)
33
+ # Visitor.new { |node| node.children }.visit([parent, parent2]) { |node| node.value *= multiplier } #=> [2, 6]
34
+ # Each visit captures the visit result in the +visited+ hash, e.g.:
35
+ # parent = Node.new(1)
36
+ # child = Node.new(2, parent)
37
+ # visitor = Visitor.new { |node| node.children }
38
+ # visitor.visit([parent]) { |node| node.value += 1 }
39
+ # parent.value #=> 2
40
+ # visitor.visited[parent] #=> 2
41
+ # child.value #=> 3
42
+ # visitor.visited[child] #=> 3
43
+ #
44
+ # A +return+ from the operation block terminates the visit and exits from the defining scope with the block return value,
45
+ # e.g. given the preceding example:
46
+ # def increment(parent, limit)
47
+ # Visitor.new { |node| node.children }.visit(parent) { |node| node.value < limit ? node.value += 1 : return }
48
+ # end
49
+ # increment(parent, 2) #=> nil
50
+ # parent.value #=> 2
51
+ # child.value #=> 2
52
+ #
53
+ # The to_enum method allows navigator iteration, e.g.:
54
+ # Visitor.new { |node| node.children }.to_enum(parent).detect { |node| node.value == 2 }
55
+ class Visitor
56
+
57
+ attr_reader :options, :visited, :lineage, :cycles
58
+
59
+ # Creates a new Visitor which traverses the child objects returned by the navigator block.
60
+ # The navigator block takes a parent argument and returns the children to visit. If the block
61
+ # return value is not nil and not a collection, then the returned object is visited. A nil or
62
+ # empty child is not visited.
63
+ #
64
+ # options is a symbol => value hash. A Symbol argument _symbol_ is the same as +{+_symbol_+=>true}+.
65
+ # Supported options include the follwing:
66
+ #
67
+ # The value of :depth_first can be +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 information.
69
+ #
70
+ # If the the :visited option is set, then the visited nodes are recorded in the :visited option hash.
71
+ # In that case, the {#visit} call does not clear the visited hash.
72
+ #
73
+ # If the :operator option is set, then the visit operator block is called when a node is visited.
74
+ # The operator block argument is the visited node.
75
+ #
76
+ # @param [Symbol, {Symbol => Object}] options the visit options. A symbol argument is the same
77
+ # as symbol => true
78
+ # @option options [String] :depth_first depth-first traversal
79
+ # @option options [Hash] :visited the hash to use when recording visited node => value associations
80
+ # @option options [Proc] :operator the visit operator block
81
+ # @option options [String] :prune_cycle flag indicating whether to exclude cycles to the root in a visit
82
+ # @yield [parent] the parent being visited
83
+ def initialize(options=nil, &navigator)
84
+ @navigator = navigator
85
+ @options = Options.to_hash(options)
86
+ @depth_first_flag = @options[:depth_first]
87
+ @visited = @options[:visited] || {}
88
+ @prune_cycle_flag = @options[:prune_cycle]
89
+ @lineage = []
90
+ @cycles = []
91
+ @exclude = Set.new
92
+ end
93
+
94
+ # Navigates to node and the children returned by this Visitor's navigator block.
95
+ # Applies the optional operator block to each child node if the block is given to this method.
96
+ # Returns the result of the operator block if given, or the node itself otherwise.
97
+ #
98
+ # The nodes to visit from a parent node are determined in the following sequence:
99
+ # * Return if the parent node has already been visited.
100
+ # * If depth_first, then call the navigator block defined in the initializer on
101
+ # the parent node and visit each child node.
102
+ # * Visit the parent node.
103
+ # * If not depth-first, then call the navigator block defined in the initializer
104
+ # on the parent node and visit each child node.
105
+ # The :depth option value constrains child traversal to that number of levels.
106
+ #
107
+ # This method first clears the _visited_ hash, unless the :visited option was set in the initializer.
108
+ #
109
+ # @param node the root object to visit
110
+ # @yield [visited] an operator applied to each visited object
111
+ # @yieldparam visited the object currently being visited
112
+ # @return the result of the yield block on node, or node itself if no block is given
113
+ def visit(node, &operator)
114
+ visit_root(node, &operator)
115
+ end
116
+
117
+ # @param node the node to check
118
+ # @return whether the node was visited
119
+ def visited?(node)
120
+ @visited.has_key?(node)
121
+ end
122
+
123
+ # @return the top node visited
124
+ def root
125
+ @lineage.first
126
+ end
127
+
128
+ # @return the current node being visited
129
+ def current
130
+ @lineage.last
131
+ end
132
+
133
+ # @return the node most recently passed as an argument to this visitor's navigator block, or nil if
134
+ # visiting the first node
135
+ def parent
136
+ @lineage[-2]
137
+ end
138
+
139
+ # @return [Enumerable] iterator over each visited node
140
+ def to_enum(node)
141
+ # could use Generator, but that results in dire behavior on any error by crashing with an elided Java lineage trace
142
+ VisitorEnumerator.new(self, node)
143
+ end
144
+
145
+ # Returns a new visitor that traverses a collection of parent nodes in lock-step fashion using this visitor.
146
+ # The synced {#visit} method applies the visit operator block to an array of child nodes taken
147
+ # from each parent node, e.g. given the class documentation example:
148
+ # parent1 = Node.new(1)
149
+ # child11 = Node.new(2, parent1)
150
+ # child12 = Node.new(3, parent1)
151
+ # parent2 = Node.new(1)
152
+ # child21 = Node.new(3, parent2)
153
+ # Visitor.new { |node| node.children }.sync.enum.to_a #=> [
154
+ # [parent1, parent2],
155
+ # [child11, child21],
156
+ # [child12, nil]
157
+ # ]
158
+ #
159
+ # By default, the children are grouped in enumeration order. If a block is given to this
160
+ # method, then the block is called to match child nodes, e.g. using the above example:
161
+ # visitor = Visitor.new { |node| node.children }
162
+ # synced = visitor.sync { |node, others| others.detect { |other| node.value == other.value }
163
+ # synced.enum.to_a #=> [
164
+ # [parent1, parent2],
165
+ # [child11, nil],
166
+ # [child12, child21]
167
+ # ]
168
+ #
169
+ # @yield [node, others] matches node in others (optional)
170
+ # @yieldparam [Resource] node the visited node to match
171
+ # @yieldparam [<Resource>] the candidates for matching the node
172
+ def sync(&matcher) # :yields: node, others
173
+ SyncVisitor.new(self, &matcher)
174
+ end
175
+
176
+ # Returns a new Visitor which determines which nodes to visit by applying the given block
177
+ # to this visitor, e.g.:
178
+ # Visitor.new { |node| node.children }.filter { |parent, children| children.first if parent.age >= 18 }
179
+ # navigates to the first child of parents 18 or older.
180
+ #
181
+ # The filter block arguments consist of a parent node and an array of children nodes for the parent.
182
+ # The block can return nil, a single node to visit or a collection of nodes to visit.
183
+ #
184
+ # @return [Visitor] the filter visitor
185
+ # @yield [parent, children] the filter to select which of the children to visit next
186
+ # @yieldparam parent the currently visited node
187
+ # @yieldparam children the nodes slated by this Visitor to visit next
188
+ # @raise [ArgumentError] if a block is not given to this method
189
+ def filter
190
+ raise ArgumentError.new("Filter block not given to visitor filter method") unless block_given?
191
+ Visitor.new(@options) { |node| yield(node, node_children(node)) }
192
+ end
193
+
194
+ protected
195
+
196
+ # Resets this visitor's state in preparation for a new visit.
197
+ def clear
198
+ # clear the lineage
199
+ @lineage.clear
200
+ # if the visited hash is not shared, then clear it
201
+ @visited.clear unless @options.has_key?(:visited)
202
+ # clear the cycles
203
+ @cycles.clear
204
+ end
205
+
206
+ # Sets the visited hash.
207
+ def visited=(hash)
208
+ @visited = hash ||= {}
209
+ end
210
+
211
+ # Visits the given node using the block given to this method.
212
+ # The default block returns node.
213
+ def visit_node(node)
214
+ @visited[node] = block_given? ? yield(node) : node
215
+ end
216
+
217
+ # Returns the children to visit for the given node.
218
+ def node_children(node)
219
+ children = @navigator.call(node)
220
+ return Array::EMPTY_ARRAY if children.nil?
221
+ Enumerable === children ? children.to_a.compact : [children]
222
+ end
223
+
224
+ private
225
+
226
+ # Visits the root node and all descendants.
227
+ def visit_root(node, &operator)
228
+ clear
229
+ prune_cycle_nodes(node) if @prune_cycle_flag
230
+ # visit the root node
231
+ visit_recursive(node, &operator)
232
+ end
233
+
234
+ # Excludes the internal nodes in cycles starting and ending at the given root.
235
+ def prune_cycle_nodes(root)
236
+ @exclude.clear
237
+ # visit the root, which will detect cycles, and remove the visited nodes afterwords
238
+ @prune_cycle_flag = false
239
+ to_enum(root).collect.each { |node| @visited.delete(node) }
240
+ @prune_cycle_flag = true
241
+ # add each cyclic internal node to the exclude list
242
+ @cycles.each { |cycle| cycle[1...-1].each { |node| @exclude << node } if cycle.first == root }
243
+ end
244
+
245
+ def visit_recursive(node, &operator)
246
+ return if node.nil? or @exclude.include?(node)
247
+ # return the visited value if the node has already been visited
248
+ if @visited.has_key?(node) then
249
+ #capture a cycle
250
+ index = @lineage.index(node)
251
+ if index then
252
+ cycle = @lineage[index..-1] << node
253
+ @cycles << cycle
254
+ end
255
+ return @visited[node]
256
+ end
257
+ # return nil if the node has not been visited but has been navigated in a depth-first visit
258
+ return if @lineage.include?(node)
259
+ visit_node_and_children(node, &operator)
260
+ end
261
+
262
+ def visit_node_and_children(node, &operator)
263
+ # set the current node
264
+ @lineage.push(node)
265
+ # if depth-first, then visit the children before the current node
266
+ visit_children(node, &operator) if @depth_first_flag
267
+ # visit the current node
268
+ result = visit_node(node, &operator)
269
+ # if not depth-first, then visit the children after the current node
270
+ visit_children(node, &operator) unless @depth_first_flag
271
+ @lineage.pop
272
+ # return the visit result
273
+ result
274
+ end
275
+
276
+ def visit_children(node, &operator)
277
+ children = node_children(node)
278
+ children.each { |child| visit_recursive(child, &operator) }
279
+ end
280
+
281
+ class VisitorEnumerator
282
+ include Enumerable
283
+
284
+ def initialize(visitor, node)
285
+ @visitor = visitor
286
+ @root = node
287
+ end
288
+
289
+ def each
290
+ @visitor.visit(@root) { |node| yield(node) }
291
+ end
292
+ end
293
+
294
+ class SyncVisitor < Visitor
295
+ # @param [Visitor] visitor the Visitor which will visit synchronized input
296
+ # @yield (see Visitor#sync)
297
+ def initialize(visitor, &matcher)
298
+ # the next node to visit is an array of child node pairs matched by the given matcher block
299
+ super() { |nodes| match_children(visitor, nodes, &matcher) }
300
+ end
301
+
302
+ # Visits the given pair of nodes.
303
+ #
304
+ # Raises ArgumentError if nodes does not consist of either two node arguments or one two-item Array
305
+ # argument.
306
+ def visit(*nodes)
307
+ if nodes.size == 1 then
308
+ nodes = nodes.first
309
+ raise ArgumentError.new("Sync visitor requires a pair of entry nodes.") unless nodes.size == 2
310
+ end
311
+ super(nodes)
312
+ end
313
+
314
+ # Returns an Enumerable which applies the given block to each matched node starting at the given nodes.
315
+ #
316
+ # Raises ArgumentError if nodes does not consist of either two node arguments or one two-item Array
317
+ # argument.
318
+ def to_enum(*nodes)
319
+ if nodes.size == 1 then
320
+ nodes = nodes.first
321
+ raise ArgumentError.new("Sync visitor requires a pair of entry nodes.") unless nodes.size == 2
322
+ end
323
+ super(nodes)
324
+ end
325
+
326
+ private
327
+
328
+ # Returns an array of arrays of matched children from the given parent nodes. The children are matched
329
+ # using the block given to this method, if supplied, or by index otherwise.
330
+ #
331
+ # @see #sync a usage example
332
+ def match_children(visitor, nodes) # :yields: child, others
333
+ # the parent nodes
334
+ p1, p2 = nodes
335
+ # this visitor's children
336
+ c1 = visitor.node_children(p1)
337
+ c2 = p2 ? visitor.node_children(p2) : []
338
+
339
+ # apply the matcher block on each of this visitor's children and the other children.
340
+ # if no block, then group the children by index, which is the transpose of the array of children arrays.
341
+ if block_given? then
342
+ c1.map { |c| [c, yield(c, c2)] }
343
+ else
344
+ # ensure that both children arrays are the same size
345
+ others = c2.size <= c1.size ? c2.fill(nil, c2.size...c1.size) : c2[0, c1.size]
346
+ # the children grouped by index is the transpose of the array of children arrays
347
+ [c1, others].transpose
348
+ end
349
+ end
350
+ end
351
+ end