Skip to content

Commit

Permalink
Added filtering using the TraversalPostion API, updated Docs and Rspe…
Browse files Browse the repository at this point in the history
…c. [#17]
  • Loading branch information
andreasronge committed Jan 14, 2009
1 parent ef24122 commit a120d74
Show file tree
Hide file tree
Showing 9 changed files with 215 additions and 17 deletions.
18 changes: 15 additions & 3 deletions README.rdoc
Expand Up @@ -837,10 +837,22 @@ It is also possible to traverse both incoming and outgoing relationships, exampl
==== Traversing Nodes With a Filter

It's possible to filter which nodes should be returned from the traverser by
using the filter method.
The filter method takes a block with the parameter of type TraversalPosition which
using the filter function.

==== Filtering: Using Evaluation in the Context of the Current Node
If the provided filter function does not take any parameter it will be evaluted in the context
of the current node being traversed.
That means that one can writer filter functions like this:

@sweden.traverse.outgoing(:contains, :trips).filter { name == 'sweden' }

==== Filtering: Using the TraversalPostion
The filter method can take a block with the parameter of type TraversalPosition which
contains information about current node, how many nodes has been returned, depth etc.
This information can be used in order to decide if the node should included in the traversal search result.
It that case the provided filter function will not be evaluated in the context of the current node
as described above.

The information contained in the TraversalPostion can be used in order to decide if the node should included in the traversal search result.
If the provided block returns true then the node will be included in the search result.

The TraversalPosition is a wrapper of java interface TraversalPosition, see
Expand Down
1 change: 1 addition & 0 deletions lib/neo4j.rb
Expand Up @@ -20,6 +20,7 @@
require 'neo4j/relations/relation_info'
require 'neo4j/relations/dynamic_relation'
require 'neo4j/relations/relations'
require 'neo4j/relations/traversal_position'
require 'neo4j/relations/has_n'
require 'neo4j/relations/relation_traverser'
require 'neo4j/relations/node_traverser'
Expand Down
3 changes: 1 addition & 2 deletions lib/neo4j/neo.rb
Expand Up @@ -140,18 +140,17 @@ def find_node(id)



#
# Loads a Neo node
# Expects the neo property 'classname' to exist.
# That property is used to load the ruby instance
#
# :api: private
def load_node(neo_node)
return nil unless neo_node.has_property('classname')
_load neo_node.get_property('classname'), neo_node
end


#
# Loads a Neo relationship
# If the neo property 'classname' to exist it will use that to create an instance of that class.
# Otherwise it will create an instance of Neo4j::Relations::DynamicRelation that represent 'rel'
Expand Down
4 changes: 3 additions & 1 deletion lib/neo4j/relations/has_n.rb
Expand Up @@ -13,7 +13,6 @@ def initialize(node, type, &filter)
@node = node
@type = RelationshipType.instance(type)
@traverser = NodeTraverser.new(node.internal_node)
@filter = filter
@info = node.class.relations_info[type.to_sym]

if @info[:outgoing]
Expand All @@ -23,8 +22,11 @@ def initialize(node, type, &filter)
@type = RelationshipType.instance(other_class_type)
@traverser.incoming(other_class_type)
end

@traverser.filter(&filter) unless filter.nil?
end


# Sets the depth of the traversal.
# Default is 1 if not specified.
#
Expand Down
31 changes: 24 additions & 7 deletions lib/neo4j/relations/node_traverser.rb
Expand Up @@ -4,7 +4,7 @@ module Relations
class IllegalTraversalArguments < StandardError; end

# Enables traversing nodes
# TODO duplicated code, see RelationTraverser, Inheritance ?
# Contains state about one specific traversal to be performed.
class NodeTraverser
include Enumerable

Expand All @@ -13,10 +13,24 @@ class NodeTraverser
def initialize(internal_node)
@internal_node = internal_node
@stop_evaluator = DepthStopEvaluator.new(1)
# what types of relationships and which directions should be traversed
@types_and_dirs = []
@types_and_dirs = [] # what types of relationships and which directions should be traversed
@traverser_order = org.neo4j.api.core.Traverser::Order::BREADTH_FIRST
@returnable_evaluator = org.neo4j.api.core.ReturnableEvaluator::ALL_BUT_START_NODE
end

# Sets the depth of the traversal.
# Default is 1 if not specified.
#
# ==== Example
# morpheus.traverse.outgoing(:friends).depth(:all).each { ... }
# morpheus.traverse.outgoing(:friends).depth(3).each { ... }
#
# ==== Arguments
# d<Fixnum,Symbol>:: the depth or :all if traversing to the end of the network.
# ==== Return
# self
#
# :api: public
def depth(d)
if d == :all
@stop_evaluator = org.neo4j.api.core.StopEvaluator::END_OF_GRAPH
Expand All @@ -25,6 +39,11 @@ def depth(d)
end
self
end

def filter(&proc)
@returnable_evaluator = ReturnableEvaluator.new proc
self
end

def outgoing(*types)
types.each do |type|
Expand Down Expand Up @@ -70,10 +89,8 @@ def iterator
raise IllegalTraversalArguments.new "Unknown type of relationship. Needs to know which type(s) of relationship in order to traverse. Please use the outgoing, incoming or both method."
end

@internal_node.traverse(org.neo4j.api.core.Traverser::Order::BREADTH_FIRST,
@stop_evaluator,
org.neo4j.api.core.ReturnableEvaluator::ALL_BUT_START_NODE,
@types_and_dirs.to_java(:object)).iterator
@internal_node.traverse(@traverser_order, @stop_evaluator,
@returnable_evaluator, @types_and_dirs.to_java(:object)).iterator
end

def to_s
Expand Down
42 changes: 39 additions & 3 deletions lib/neo4j/relations/relations.rb
@@ -1,11 +1,47 @@
#
# This files contains common private classes used by the Neo4j::Relations module
# This files contains common private classes that implements various Neo4j java interfaces.
# This classes are only used inside this Relations module
#

module Neo4j
module Relations

# Wrapper for the neo4j StopEvalutor interface.
# Wrapper for org.neo4j.api.core.ReturnableEvaluator
#
# :api: private
class ReturnableEvaluator
include org.neo4j.api.core.ReturnableEvaluator

def initialize(proc)
@proc = proc
end

def isReturnableNode( traversal_position )
# if the Proc takes one argument that we give it the traversal_position
result = if @proc.arity == 1
# wrap the traversal_position in the Neo4j.rb TraversalPostion object
@proc.call TraversalPosition.new(traversal_position)
else # otherwise we eval the proc in the context of the current node
# do not include the start node
return false if traversal_position.isStartNode()
eval_context = Neo4j::load(traversal_position.currentNode.getId)
eval_context.instance_eval(&@proc)
end

# java does not treat nil as false so we need to do instead
(result)? true : false
end

# public boolean isReturnableNode( TraversalPosition position )
# {
# // Return nodes until we've reached 5 nodes or end of graph
# return position.returnedNodesCount() < 5;
# }

end


# Wrapper for the neo4j org.neo4j.api.core.StopEvalutor interface.
# Used in the Neo4j Traversers.
#
# :api: private
Expand All @@ -22,7 +58,7 @@ def isStopNode(pos)
end


# Wrapper for the Java RelationshipType interface.
# Wrapper for the Java org.neo4j.api.core.RelationshipType interface.
# Each type is a singelton.
#
# :api: private
Expand Down
47 changes: 47 additions & 0 deletions lib/neo4j/relations/traversal_position.rb
@@ -0,0 +1,47 @@
module Neo4j
module Relations

# Wrapper for org.neo4j.api.core.TraversalPosition
# See Javadoc for org.neo4j.api.core.TraversalPosition
# It can be used as a parameter in traversals filter functions.
#
# :api: public
class TraversalPosition
def initialize(traversal_position)
@traversal_position = traversal_position
end

# Return the current node.
def current_node
Neo4j.load(@traversal_position.currentNode.getId)
end

# Returns the previous node, may be nil.
def previous_node
return nil if @traversal_position.previousNode.nil?
Neo4j.load(@traversal_position.previousNode.getId)
end

# Return the last relationship traversed, may be nil.
def last_relationship_traversed
relation = @traversal_position.lastRelationshipTraversed()
Neo4j.instance.load_relationship(relation) unless relation.nil?
end

# Returns the current traversal depth.
def depth
@traversal_position.depth
end

# Returns true if the current position is the start node, false otherwise.
def start_node?
@traversal_position.isStartNode
end

# Returns the number of nodes returned by traverser so far.
def returned_nodes_count
@traversal_position.returnedNodesCount
end
end
end
end
2 changes: 1 addition & 1 deletion test/neo4j/has_n_spec.rb
Expand Up @@ -245,7 +245,7 @@ class Customer
end

it "should find the order using a filter: customer.orders{ order_id == '2'}" do
pending "filter not implemented yet, see ticket 17"
# pending "filter not implemented yet, see ticket 17"
# given
customer = Customer.new
order1 = Order.new{|n| n.order_id = '1'}
Expand Down
84 changes: 84 additions & 0 deletions test/neo4j/node_traverser_spec.rb
Expand Up @@ -30,6 +30,7 @@ class TestNode
include Neo4j::NodeMixin
has_n :friends
has_n :parents
property :name
end
end

Expand Down Expand Up @@ -183,6 +184,89 @@ class TestNode
t11_incoming.should include(t, t1)
end

it "should find outgoing nodes using a filter function that will be evaluated in the context of the current node" do
# given
a = TestNode.new {|n| n.name = 'a'}
b = TestNode.new {|n| n.name = 'b'}
c = TestNode.new {|n| n.name = 'c'}
a.friends << b << c

# when
result = a.traverse.outgoing(:friends).filter{ name == 'b'}.to_a

# then
result.size.should == 1
result.should include(b)
end

end

describe 'traversing using the TraversalPostion information' do
before(:all) do
undefine_class :TestNode # make sure it is not already defined

class TestNode
include Neo4j::NodeMixin
has_n :friends
has_n :parents
property :name
end

# given
@a = TestNode.new {|n| n.name = 'a'}
@b = TestNode.new {|n| n.name = 'b'}
@c = TestNode.new {|n| n.name = 'c'}
@p = TestNode.new {|n| n.name = 'p'}
@a.friends << @b << @c
@a.parents << @p
end

it "should work with the TraversalPosition#current_node parameter" do
# when
result = @a.traverse.outgoing(:friends).filter{|tp| tp.current_node.name == 'b'}.to_a

# then
result.size.should == 1
result.should include(@b)
end

it "should work with the TraversalPosition#previous_node parameter" do
# when
result = @a.traverse.outgoing(:friends).filter{|tp| tp.previous_node.name == 'a' unless tp.previous_node.nil?}.to_a

# then
result.size.should == 2
result.should include(@b,@c)
end

it "should work with the TraversalPosition#last_relationship_traversed parameter" do
# when
result = @a.traverse.outgoing(:friends, :parents).filter do |tp|
tp.last_relationship_traversed.relationship_type == :parents unless tp.last_relationship_traversed.nil?
end.to_a

# then
result.size.should == 1
result.should include(@p)
end

it "should work with the TraversalPosition#depth parameter" do
# when
result = @a.traverse.outgoing(:friends, :parents).filter { |tp| tp.depth == 0 }.to_a

# then
result.size.should == 1
result.should include(@a)
end

it "should work with the TraversalPosition#returned_nodes_count parameter" do
# when
result = @a.traverse.outgoing(:friends, :parents).filter { |tp| tp.returned_nodes_count < 2 }.to_a

# then
result.size.should == 2
end

end


Expand Down

0 comments on commit a120d74

Please sign in to comment.