/*
* Copyright (c) 2002-2009 "Neo Technology,"
* Network Engine for Objects in Lund AB [http://neotechnology.com]
*
* This file is part of Neo4j.
*
* Neo4j is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package org.neo4j.kernel.impl.traversal;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertTrue;
import static org.junit.Assert.fail;
import static org.neo4j.graphdb.Traverser.Order.BREADTH_FIRST;
import static org.neo4j.graphdb.Traverser.Order.DEPTH_FIRST;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;
import org.junit.Test;
import org.neo4j.graphdb.Direction;
import org.neo4j.graphdb.Node;
import org.neo4j.graphdb.NotFoundException;
import org.neo4j.graphdb.Relationship;
import org.neo4j.graphdb.RelationshipType;
import org.neo4j.graphdb.ReturnableEvaluator;
import org.neo4j.graphdb.StopEvaluator;
import org.neo4j.graphdb.TraversalPosition;
import org.neo4j.graphdb.Traverser;
import org.neo4j.graphdb.Traverser.Order;
import org.neo4j.kernel.impl.AbstractNeo4jTestCase;
import org.neo4j.kernel.impl.MyRelTypes;
public class TestTraversal extends AbstractNeo4jTestCase
{
// Tests the traverser factory for sanity checks with corrupted input
@Test
public void testSanityChecks1() throws Exception
{
// Valid data
Node root = getGraphDb().createNode();
RelationshipType[] traversableRels = new RelationshipType[] { MyRelTypes.TEST, };
// Null traversable relationships
this.sanityCheckTraverser( "Sanity check failed: null traversable "
+ "rels should throw an " + "IllegalArgumentException",
BREADTH_FIRST, root, null, Direction.OUTGOING,
StopEvaluator.END_OF_GRAPH, ReturnableEvaluator.ALL );
// Null stop evaluator
this.sanityCheckTraverser( "Sanity check failed: null stop eval "
+ "should throw an IllegalArgumentException", BREADTH_FIRST, root,
traversableRels[0], Direction.OUTGOING, null,
ReturnableEvaluator.ALL );
// Null returnable evaluator
this.sanityCheckTraverser( "Sanity check failed: null returnable "
+ "evaluator should throw an " + "IllegalArgumentException",
BREADTH_FIRST, root, traversableRels[0], Direction.OUTGOING,
StopEvaluator.END_OF_GRAPH, null );
root.delete();
}
@Test
public void testSanityChecks2() throws Exception
{
// ------------- with traverser direction -------------
// Valid data
Node root = getGraphDb().createNode();
RelationshipType[] traversableRels = new RelationshipType[] { MyRelTypes.TEST, };
Direction[] traversableDirs = new Direction[] { Direction.OUTGOING };
// Null traversable relationships
this.sanityCheckTraverser( "Sanity check failed: null traversable "
+ "rels should throw an " + "IllegalArgumentException",
BREADTH_FIRST, root, null, traversableDirs[0],
StopEvaluator.END_OF_GRAPH, ReturnableEvaluator.ALL );
// Null traversable directions
this.sanityCheckTraverser( "Sanity check failed: null traversable "
+ "rels should throw an " + "IllegalArgumentException",
BREADTH_FIRST, root, traversableRels[0], null,
StopEvaluator.END_OF_GRAPH, ReturnableEvaluator.ALL );
// Null stop evaluator
this.sanityCheckTraverser( "Sanity check failed: null stop eval "
+ "should throw an IllegalArgumentException", BREADTH_FIRST, root,
traversableRels[0], traversableDirs[0], null,
ReturnableEvaluator.ALL );
// Null returnable evaluator
this.sanityCheckTraverser( "Sanity check failed: null returnable "
+ "evaluator should throw an " + "IllegalArgumentException",
BREADTH_FIRST, root, traversableRels[0], traversableDirs[0],
StopEvaluator.END_OF_GRAPH, null );
// traversable relationships length not equal to traversable directions
// length
this.sanityCheckTraverser( "Sanity check failed: null returnable "
+ "evaluator should throw an " + "IllegalArgumentException",
BREADTH_FIRST, root, traversableRels[0], null,
StopEvaluator.END_OF_GRAPH, null );
this.sanityCheckTraverser( "Sanity check failed: null returnable "
+ "evaluator should throw an " + "IllegalArgumentException",
BREADTH_FIRST, root, null, traversableDirs[0],
StopEvaluator.END_OF_GRAPH, null );
root.delete();
}
// Tests the traverser factory for simple corrupted (null) input, used
// by testSanityChecks()
private void sanityCheckTraverser( String failMessage, Order type,
Node startNode, RelationshipType traversableRel, Direction direction,
StopEvaluator stopEval, ReturnableEvaluator retEval )
{
try
{
startNode.traverse( type, stopEval, retEval, traversableRel,
direction );
fail( failMessage );
}
catch ( IllegalArgumentException iae )
{
// This is ok
}
}
// Traverses the full test "ise-tree-like" population breadth first
// and verifies that it is returned in correct order
@Test
public void testBruteBreadthTraversal() throws Exception
{
Node root = this.buildIseTreePopulation();
RelationshipType[] traversableRels = new RelationshipType[] {
MyRelTypes.TEST, MyRelTypes.TEST_TRAVERSAL };
Traverser traverser = root.traverse( BREADTH_FIRST,
StopEvaluator.END_OF_GRAPH, ReturnableEvaluator.ALL,
traversableRels[0], Direction.BOTH, traversableRels[1],
Direction.BOTH );
try
{
this.assertLevelsOfNodes( traverser, new String[][] {
new String[] { "1" },
new String[] { "2", "3", "4" },
new String[] { "5", "6", "7", "8", "9" },
new String[] { "10", "11", "12", "13", "14" }
} );
assertTrue( "Too many nodes returned from traversal", traverser
.iterator().hasNext() == false );
}
catch ( java.util.NoSuchElementException nsee )
{
fail( "Too few nodes returned from traversal" );
}
finally
{
this.deleteNodeTreeRecursively( root, 0 );
}
}
private void assertNodes( Traverser traverser, String... expectedNodes )
{
Set<String> set = new HashSet<String>( Arrays.asList( expectedNodes ) );
for ( Node node : traverser )
{
assertTrue( set.remove( node.getProperty( "node.test.id" ) ) );
}
assertTrue( set.isEmpty() );
}
private void assertLevelsOfNodes( Traverser traverser, String[][] nodes )
{
Map<Integer, Collection<String>> map = new HashMap<Integer, Collection<String>>();
for ( Node node : traverser )
{
Collection<String> collection = map.get( traverser.currentPosition().depth() );
if ( collection == null )
{
collection = new ArrayList<String>();
map.put( traverser.currentPosition().depth(), collection );
}
String name = (String) node.getProperty( "node.test.id" );
collection.add( name );
}
for ( int i = 0; i < nodes.length; i++ )
{
Collection<String> expected = Arrays.asList( nodes[i] );
assertEquals( expected, map.get( i ) );
}
}
// Traverses the test "ise-tree-like" population breadth first,
// but only traverses "ise" (TEST) relationships (the population also
// contains
// "ise_clone" (TEST_TRAVERSAL) rels)
@Test
public void testMultiRelBreadthTraversal() throws Exception
{
Node root = this.buildIseTreePopulation();
RelationshipType[] traversableRels = new RelationshipType[] { MyRelTypes.TEST };
Traverser traverser = root.traverse( BREADTH_FIRST,
StopEvaluator.END_OF_GRAPH, ReturnableEvaluator.ALL,
traversableRels[0], Direction.BOTH );
try
{
this.assertLevelsOfNodes( traverser, new String[][] {
new String[] { "1" },
new String[] { "2", "3", "4" },
new String[] { "5", "6", "7" },
new String[] { "10", "11", "12", "13" },
} );
assertTrue( "Too many nodes returned from traversal", traverser
.iterator().hasNext() == false );
}
catch ( java.util.NoSuchElementException nsee )
{
fail( "Too few nodes returned from traversal" );
}
finally
{
this.deleteNodeTreeRecursively( root, 0 );
}
}
// Traverses the test "ise-tree-like" population breadth first,
// starting in the middle of the tree and traversing only in the
// "forward" direction
@Test
public void testDirectedBreadthTraversal() throws Exception
{
// Build test population
Node root = this.buildIseTreePopulation();
Node startNode = null;
// Get a node in the middle of the tree:
try
{
// a) Construct a returnable evaluator that returns node 2
ReturnableEvaluator returnEvaluator = new ReturnableEvaluator()
{
public boolean isReturnableNode( TraversalPosition pos )
{
try
{
Node node = pos.currentNode();
String key = "node.test.id";
String nodeId = (String) node.getProperty( key );
return nodeId.equals( "2" );
}
catch ( Exception e )
{
return false;
}
}
};
// b) create a traverser
Traverser toTheMiddleTraverser = root.traverse( BREADTH_FIRST,
StopEvaluator.END_OF_GRAPH, returnEvaluator, MyRelTypes.TEST,
Direction.BOTH );
// c) get the first node it returns
startNode = toTheMiddleTraverser.iterator().next();
}
catch ( Exception e )
{
e.printStackTrace();
fail( "Something went wrong when trying to get a start node "
+ "in the middle of the tree: " + e );
}
// Construct the real traverser
Traverser traverser = startNode.traverse( BREADTH_FIRST,
StopEvaluator.END_OF_GRAPH, ReturnableEvaluator.ALL,
MyRelTypes.TEST, Direction.OUTGOING );
try
{
this.assertNextNodeId( traverser, "2" );
this.assertNextNodeId( traverser, "5" );
this.assertNextNodeId( traverser, "6" );
this.assertNextNodeId( traverser, "10" );
this.assertNextNodeId( traverser, "11" );
this.assertNextNodeId( traverser, "12" );
this.assertNextNodeId( traverser, "13" );
assertTrue( "Too many nodes returned from traversal", traverser
.iterator().hasNext() == false );
}
catch ( java.util.NoSuchElementException nsee )
{
nsee.printStackTrace();
fail( "Too few nodes returned from traversal" );
}
finally
{
this.deleteNodeTreeRecursively( root, 0 );
}
}
// Traverses the full test "ise-tree-like" population depth first
// and verifies that it is returned in correct order
@Test
public void testBruteDepthTraversal() throws Exception
{
Node root = this.buildIseTreePopulation();
RelationshipType[] traversableRels = new RelationshipType[] {
MyRelTypes.TEST, MyRelTypes.TEST_TRAVERSAL };
Traverser traverser = root.traverse( DEPTH_FIRST,
StopEvaluator.END_OF_GRAPH, ReturnableEvaluator.ALL,
traversableRels[0], Direction.BOTH, traversableRels[1],
Direction.BOTH );
try
{
this.assertNodes( traverser, "1", "2", "3", "4", "5", "6", "7",
"8", "9", "10", "11", "12", "13", "14" );
assertTrue( "Too many nodes returned from traversal", traverser
.iterator().hasNext() == false );
}
catch ( java.util.NoSuchElementException nsee )
{
fail( "Too few nodes returned from traversal" );
}
finally
{
this.deleteNodeTreeRecursively( root, 0 );
}
}
// Traverses the test "ise-tree-like" population depth first,
// but only traverses "ise" relationships (the population also contains
// "ise_clone" rels)
@Test
public void testMultiRelDepthTraversal() throws Exception
{
Node root = this.buildIseTreePopulation();
RelationshipType[] traversableRels = new RelationshipType[] { MyRelTypes.TEST };
Traverser traverser = root.traverse( DEPTH_FIRST,
StopEvaluator.END_OF_GRAPH, ReturnableEvaluator.ALL,
traversableRels[0], Direction.BOTH );
try
{
assertNodes( traverser, "1", "2", "3", "4", "5", "6", "7",
"10", "11", "12", "13" );
assertTrue( "Too many nodes returned from traversal", traverser
.iterator().hasNext() == false );
}
catch ( java.util.NoSuchElementException nsee )
{
fail( "Too few nodes returned from traversal" );
}
finally
{
this.deleteNodeTreeRecursively( root, 0 );
}
}
// Verifies that the stop evaluator can stop based on the current node
@Test
public void testStopOnCurrentNode() throws Exception
{
// Build ise tree
Node root = this.buildIseTreePopulation();
// Traverse only ISE relationships
RelationshipType[] traversableRels = new RelationshipType[] { MyRelTypes.TEST };
// Construct a stop evaluator that stops on nodes 5, 6, 3 and 4
StopEvaluator stopEvaluator = new StopEvaluator()
{
public boolean isStopNode( TraversalPosition position )
{
try
{
Node node = position.currentNode();
String nodeId = (String) node.getProperty( "node.test.id" );
return nodeId.equals( "5" ) || nodeId.equals( "6" )
|| nodeId.equals( "3" ) || nodeId.equals( "4" );
}
catch ( Exception e )
{
return false;
}
}
};
// Create a traverser
Traverser traverser = root.traverse( BREADTH_FIRST, stopEvaluator,
ReturnableEvaluator.ALL, traversableRels[0], Direction.BOTH );
try
{
this.assertNextNodeId( traverser, "1" );
this.assertNextNodeId( traverser, "2" );
this.assertNextNodeId( traverser, "3" );
this.assertNextNodeId( traverser, "4" );
this.assertNextNodeId( traverser, "5" );
this.assertNextNodeId( traverser, "6" );
assertTrue( "Too many nodes returned from traversal", traverser
.iterator().hasNext() == false );
}
catch ( java.util.NoSuchElementException nsee )
{
fail( "Too few nodes returned from traversal" );
}
finally
{
this.deleteNodeTreeRecursively( root, 0 );
}
}
// Verifies that the stop evaluator can stop based on the previous node
@Test
public void testStopOnPreviousNode() throws Exception
{
// Build ise tree
Node root = this.buildIseTreePopulation();
// Traverse only ISE relationships
RelationshipType[] traversableRels = new RelationshipType[] { MyRelTypes.TEST };
// Construct a stop evaluator that stops on nodes 2, 3, and 4
// (ie root's children)
StopEvaluator stopEvaluator = new StopEvaluator()
{
public boolean isStopNode( TraversalPosition position )
{
try
{
Node node = position.previousNode();
String nodeId = (String) node.getProperty( "node.test.id" );
return nodeId.equals( "1" );
}
catch ( Exception e )
{
return false;
}
}
};
// Create a traverser
Traverser traverser = root.traverse( BREADTH_FIRST, stopEvaluator,
ReturnableEvaluator.ALL, traversableRels[0], Direction.BOTH );
try
{
this.assertNextNodeId( traverser, "1" );
this.assertNextNodeId( traverser, "2" );
this.assertNextNodeId( traverser, "3" );
this.assertNextNodeId( traverser, "4" );
assertTrue( "Too many nodes returned from traversal", traverser
.iterator().hasNext() == false );
}
catch ( java.util.NoSuchElementException nsee )
{
fail( "Too few nodes returned from traversal" );
}
finally
{
// Delete ise tree and commmit work
this.deleteNodeTreeRecursively( root, 0 );
}
}
// Verifies that the stop evaluator can stop based on the current depth
@Test
public void testStopOnDepth() throws Exception
{
// Build ise tree
Node root = this.buildIseTreePopulation();
// Traverse only ISE relationships
RelationshipType[] traversableRels = new RelationshipType[] { MyRelTypes.TEST };
// Construct a stop evaluator that stops on depth 2
StopEvaluator stopEvaluator = new StopEvaluator()
{
public boolean isStopNode( TraversalPosition position )
{
return position.depth() >= 2;
}
};
// Create a traverser
Traverser traverser = root.traverse( BREADTH_FIRST, stopEvaluator,
ReturnableEvaluator.ALL, traversableRels[0], Direction.BOTH );
try
{
this.assertNextNodeId( traverser, "1" );
this.assertNextNodeId( traverser, "2" );
this.assertNextNodeId( traverser, "3" );
this.assertNextNodeId( traverser, "4" );
this.assertNextNodeId( traverser, "5" );
this.assertNextNodeId( traverser, "6" );
this.assertNextNodeId( traverser, "7" );
assertTrue( "Too many nodes returned from traversal", traverser
.iterator().hasNext() == false );
}
catch ( java.util.NoSuchElementException nsee )
{
fail( "Too few nodes returned from traversal" );
}
finally
{
// Delete ise tree and commmit work
this.deleteNodeTreeRecursively( root, 0 );
}
}
// Verifies that the stop evaluator can stop based on the amount of
// returned nodes
@Test
public void testStopOnReturnedNodes() throws Exception
{
// Build ise tree
Node root = this.buildIseTreePopulation();
// Traverse only ISE relationships
RelationshipType[] traversableRels = new RelationshipType[] { MyRelTypes.TEST };
// Construct stop- and returnable evaluators that return 5 nodes
StopEvaluator stopEvaluator = new StopEvaluator()
{
public boolean isStopNode( TraversalPosition position )
{
// Stop traversing when we've returned 5 nodes
return position.returnedNodesCount() >= 5;
}
};
ReturnableEvaluator returnEvaluator = new ReturnableEvaluator()
{
public boolean isReturnableNode( TraversalPosition position )
{
// Return nodes until we've reached 5 nodes or end of graph
return position.returnedNodesCount() < 5;
}
};
// Create a traverser
Traverser traverser = root.traverse( BREADTH_FIRST, stopEvaluator,
returnEvaluator, traversableRels[0], Direction.BOTH );
try
{
this.assertLevelsOfNodes( traverser, new String[][] {
new String[] { "1" },
new String[] { "2", "3", "4" },
new String[] { "5" },
} );
assertTrue( "Too many nodes returned from traversal", traverser
.iterator().hasNext() == false );
}
catch ( java.util.NoSuchElementException nsee )
{
fail( "Too few nodes returned from traversal" );
}
finally
{
// Delete ise tree and commmit work
this.deleteNodeTreeRecursively( root, 0 );
}
}
// Verifies that the stop evaluator can stop based on the last
// traversed relationship
@Test
public void testStopOnLastRelationship() throws Exception
{
// Build ise tree
Node root = this.buildIseTreePopulation();
// Traverse only ISE relationships
RelationshipType[] traversableRels = new RelationshipType[] {
MyRelTypes.TEST, MyRelTypes.TEST_TRAVERSAL };
// Construct stop- and returnable evaluators that return 5 nodes
StopEvaluator stopEvaluator = new StopEvaluator()
{
public boolean isStopNode( TraversalPosition position )
{
// Stop when we got here by traversing a clone relationship
Relationship rel = position.lastRelationshipTraversed();
return rel != null
&& rel.getType() == MyRelTypes.TEST_TRAVERSAL;
}
};
// Create a traverser
Traverser traverser = root.traverse( BREADTH_FIRST, stopEvaluator,
ReturnableEvaluator.ALL, traversableRels[0], Direction.BOTH,
traversableRels[1], Direction.BOTH );
try
{
this.assertLevelsOfNodes( traverser, new String[][] {
new String[] { "1" },
new String[] { "2", "3", "4" },
new String[] { "5", "6", "7", "8", "9" },
new String[] { "10", "11", "12", "13" }
} );
assertTrue( "Too many nodes returned from traversal", traverser
.iterator().hasNext() == false );
}
catch ( java.util.NoSuchElementException nsee )
{
fail( "Too few nodes returned from traversal" );
}
finally
{
// Delete ise tree and commmit work
this.deleteNodeTreeRecursively( root, 0 );
}
}
// -- Utility operations
private Node buildIseTreePopulation() throws Exception
{
// Create population
Node[] nodeSpace = new Node[] { null, // empty
getGraphDb().createNode(), // 1 [root]
getGraphDb().createNode(), // 2
getGraphDb().createNode(), // 3
getGraphDb().createNode(), // 4
getGraphDb().createNode(), // 5
getGraphDb().createNode(), // 6
getGraphDb().createNode(), // 7
getGraphDb().createNode(), // 8
getGraphDb().createNode(), // 9
getGraphDb().createNode(), // 10
getGraphDb().createNode(), // 11
getGraphDb().createNode(), // 12
getGraphDb().createNode(), // 13
getGraphDb().createNode(), // 14
};
String key = "node.test.id";
for ( int i = 1; i < nodeSpace.length; i++ )
{
nodeSpace[i].setProperty( key, "" + i );
}
RelationshipType ise = MyRelTypes.TEST;
RelationshipType clone = MyRelTypes.TEST_TRAVERSAL;
// Bind it together
//
// ----(1)-------
// / \ \
// --(2)-- (3) (4)--
// / \ \ | \
// --(5)----- (6)---(7) (8) (9)
// / | \ \ |
// (10) (11)(12)(13) (14)
//
nodeSpace[1].createRelationshipTo( nodeSpace[2], ise );
nodeSpace[2].createRelationshipTo( nodeSpace[5], ise );
nodeSpace[5].createRelationshipTo( nodeSpace[10], ise );
nodeSpace[5].createRelationshipTo( nodeSpace[11], ise );
nodeSpace[5].createRelationshipTo( nodeSpace[12], ise );
nodeSpace[5].createRelationshipTo( nodeSpace[13], ise );
nodeSpace[2].createRelationshipTo( nodeSpace[6], ise );
nodeSpace[1].createRelationshipTo( nodeSpace[3], ise );
nodeSpace[1].createRelationshipTo( nodeSpace[4], ise );
nodeSpace[3].createRelationshipTo( nodeSpace[7], ise );
nodeSpace[6].createRelationshipTo( nodeSpace[7], clone );
nodeSpace[4].createRelationshipTo( nodeSpace[8], clone );
nodeSpace[4].createRelationshipTo( nodeSpace[9], clone );
nodeSpace[9].createRelationshipTo( nodeSpace[14], clone );
return nodeSpace[1]; // root
}
// Deletes a tree-like structure of nodes, starting with 'currentNode'.
// Works fine with trees, dies horribly on cyclic structures.
private void deleteNodeTreeRecursively( Node currentNode, int depth )
{
if ( depth > 100 )
{
throw new RuntimeException( "Recursive guard: depth = " + depth );
}
if ( currentNode == null )
{
return;
}
Iterable<Relationship> rels = currentNode.getRelationships();
for ( Relationship rel : rels )
{
if ( !rel.getStartNode().equals( currentNode ) )
{
continue;
}
Node endNode = rel.getEndNode();
rel.delete();
this.deleteNodeTreeRecursively( endNode, depth + 1 );
}
String msg = "Deleting " + currentNode + "\t[";
String id = (String) currentNode.getProperty( "node.test.id" );
msg += id + "]";
Iterable<Relationship> allRels = currentNode.getRelationships();
for ( Relationship rel : allRels )
{
rel.delete();
}
currentNode.delete();
}
private void assertNextNodeId( Traverser traverser, String property )
throws NotFoundException
{
Node node = traverser.iterator().next();
assertEquals( property, node.getProperty( "node.test.id" ) );
}
}