Skip to content

Commit

Permalink
Reduced acceptance test
Browse files Browse the repository at this point in the history
  • Loading branch information
fickludd committed Oct 6, 2017
1 parent 73b5598 commit 2d46225
Show file tree
Hide file tree
Showing 2 changed files with 55 additions and 297 deletions.
Expand Up @@ -19,12 +19,7 @@
*/ */
package org.neo4j.internal.cypher.acceptance; package org.neo4j.internal.cypher.acceptance;


import java.util.ArrayList;
import java.util.Collections; import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set; import java.util.Set;
import java.util.stream.Stream; import java.util.stream.Stream;


Expand All @@ -33,313 +28,108 @@
import org.neo4j.graphdb.Label; import org.neo4j.graphdb.Label;
import org.neo4j.graphdb.Node; import org.neo4j.graphdb.Node;
import org.neo4j.graphdb.Path; import org.neo4j.graphdb.Path;
import org.neo4j.graphdb.PathExpanderBuilder;
import org.neo4j.graphdb.RelationshipType; import org.neo4j.graphdb.RelationshipType;
import org.neo4j.graphdb.traversal.Evaluation; import org.neo4j.graphdb.traversal.Evaluation;
import org.neo4j.graphdb.traversal.Evaluator; import org.neo4j.graphdb.traversal.Evaluator;
import org.neo4j.graphdb.traversal.Evaluators; import org.neo4j.graphdb.traversal.Evaluators;
import org.neo4j.graphdb.traversal.TraversalDescription; import org.neo4j.graphdb.traversal.TraversalDescription;
import org.neo4j.graphdb.traversal.Traverser;
import org.neo4j.graphdb.traversal.Uniqueness; import org.neo4j.graphdb.traversal.Uniqueness;
import org.neo4j.helpers.collection.Pair;
import org.neo4j.procedure.Context; import org.neo4j.procedure.Context;
import org.neo4j.procedure.Description; import org.neo4j.procedure.Description;
import org.neo4j.procedure.Name; import org.neo4j.procedure.Name;
import org.neo4j.procedure.Procedure; import org.neo4j.procedure.Procedure;


import static org.neo4j.graphdb.Direction.BOTH;
import static org.neo4j.graphdb.Direction.INCOMING;
import static org.neo4j.graphdb.Direction.OUTGOING;
import static org.neo4j.graphdb.traversal.Evaluation.EXCLUDE_AND_CONTINUE; import static org.neo4j.graphdb.traversal.Evaluation.EXCLUDE_AND_CONTINUE;
import static org.neo4j.graphdb.traversal.Evaluation.EXCLUDE_AND_PRUNE; import static org.neo4j.graphdb.traversal.Evaluation.EXCLUDE_AND_PRUNE;
import static org.neo4j.graphdb.traversal.Evaluation.INCLUDE_AND_CONTINUE; import static org.neo4j.graphdb.traversal.Evaluation.INCLUDE_AND_CONTINUE;
import static org.neo4j.graphdb.traversal.Evaluation.INCLUDE_AND_PRUNE;


public class TestProcedure public class TestProcedure
{ {
@Context @Context
public GraphDatabaseService db; public GraphDatabaseService db;


@Procedure("org.neo4j.test") @Procedure( "org.neo4j.movieTraversal" )
@Description("org.neo4j.test") @Description( "org.neo4j.movieTraversal" )
public Stream<TestResult> test() throws Exception { public Stream<PathResult> movieTraversal( @Name( "start" ) Node start ) throws Exception
return db.findNodes( Label.label("Tweet" ) ).stream().map( TestResult::new ); {
} TraversalDescription td =

db.traversalDescription()
@Procedure("org.neo4j.testTraversal") .breadthFirst()
@Description("org.neo4j.testTraversal") .relationships( RelationshipType.withName( "ACTED_IN" ), Direction.BOTH )
public Stream<TestResult> testTraversal() throws Exception { .relationships( RelationshipType.withName( "PRODUCED" ), Direction.BOTH )
TraversalDescription td = db.traversalDescription(); .relationships( RelationshipType.withName( "DIRECTED" ), Direction.BOTH )
return db.findNodes( Label.label("Tweet" ) ) .evaluator( Evaluators.fromDepth( 3 ) )
.stream() .evaluator( new LabelEvaluator( "Western", 1, 3 ) )
.flatMap( node -> td.traverse( node ).stream().map( path -> new TestResult( path.startNode() ) ) ); .uniqueness( Uniqueness.NODE_GLOBAL );
}

return td.traverse( start ).stream().map( PathResult::new );
@Procedure("apoc.path.expandConfig")
@Description("apoc.path.expandConfig(startNode <id>|Node|list, {minLevel,maxLevel,uniqueness,relationshipFilter,labelFilter,uniqueness:'RELATIONSHIP_PATH',bfs:true, filterStartNode:false}) yield path expand from start node following the given relationships from min to max-level adhering to the label filters")
public Stream<PathResult> expandConfig(@Name("start") Object start, @Name("config") Map<String,Object> config) throws Exception {
return expandConfigPrivate(start, config).map( PathResult::new );
}

private Stream<Path> expandConfigPrivate(@Name("start") Object start, @Name("config") Map<String,Object> config) throws Exception {
List<Node> nodes = startToNodes(start);

String uniqueness = (String) config.getOrDefault("uniqueness", Uniqueness.RELATIONSHIP_PATH.name());
String relationshipFilter = (String) config.getOrDefault("relationshipFilter", null);
String labelFilter = (String) config.getOrDefault("labelFilter", null);
long minLevel = toLong(config.getOrDefault("minLevel", "-1"));
long maxLevel = toLong(config.getOrDefault("maxLevel", "-1"));
boolean bfs = toBoolean(config.getOrDefault("bfs",true));
boolean filterStartNode = toBoolean(config.getOrDefault("filterStartNode", true));
long limit = toLong(config.getOrDefault("limit", "-1"));

return explorePathPrivate(nodes, relationshipFilter, labelFilter, minLevel, maxLevel, bfs,
getUniqueness(uniqueness), filterStartNode, limit);
}

public static Long toLong(Object value) {
if (value == null) return null;
if (value instanceof Number) return ((Number)value).longValue();
try {
return Long.parseLong(value.toString());
} catch (NumberFormatException e) {
return null;
}
}

public static boolean toBoolean(Object value) {
if ((value == null || value instanceof Number && (((Number) value).longValue()) == 0L || value instanceof String && (value.equals("") || ((String) value).equalsIgnoreCase("false") || ((String) value).equalsIgnoreCase("no")|| ((String) value).equalsIgnoreCase("0"))|| value instanceof Boolean && value.equals(false))) {
return false;
}
return true;
}

private Stream<Path> explorePathPrivate(Iterable<Node> startNodes
, String pathFilter
, String labelFilter
, long minLevel
, long maxLevel, boolean bfs, Uniqueness uniqueness, boolean filterStartNode, long limit) {
// LabelFilter
// -|Label|:Label|:Label excluded label list
// +:Label or :Label include labels

Traverser traverser = traverse(db.traversalDescription(), startNodes, pathFilter, labelFilter, minLevel, maxLevel, uniqueness,bfs,filterStartNode,limit);
return traverser.stream();
}

public static Traverser traverse(TraversalDescription traversalDescription, Iterable<Node> startNodes, String pathFilter, String labelFilter, long minLevel, long maxLevel, Uniqueness uniqueness, boolean bfs, boolean filterStartNode, long limit) {
TraversalDescription td = traversalDescription;
// based on the pathFilter definition now the possible relationships and directions must be shown

td = bfs ? td.breadthFirst() : td.depthFirst();

Iterable<Pair<RelationshipType, Direction>> relDirIterable = RelationshipTypeAndDirections.parse(pathFilter);

for (Pair<RelationshipType, Direction> pair: relDirIterable) {
if (pair.first() == null) {
td = td.expand( PathExpanderBuilder.allTypes(pair.other()).build());
} else {
td = td.relationships(pair.first(), pair.other());
}
}

if (minLevel != -1) td = td.evaluator( Evaluators.fromDepth((int) minLevel));
if (maxLevel != -1) td = td.evaluator(Evaluators.toDepth((int) maxLevel));

if (labelFilter != null && !labelFilter.trim().isEmpty()) {
td = td.evaluator(new LabelEvaluator(labelFilter, filterStartNode, limit, (int) minLevel));
}

td = td.uniqueness(uniqueness); // this is how Cypher works !! Uniqueness.RELATIONSHIP_PATH
// uniqueness should be set as last on the TraversalDescription
return td.traverse(startNodes);
}

@SuppressWarnings("unchecked")
private List<Node> startToNodes(Object start) throws Exception {
if (start == null) return Collections.emptyList();
if (start instanceof Node) {
return Collections.singletonList((Node) start);
}
if (start instanceof Number) {
return Collections.singletonList(db.getNodeById(((Number) start).longValue()));
}
if (start instanceof List) {
List list = (List) start;
if (list.isEmpty()) return Collections.emptyList();

Object first = list.get(0);
if (first instanceof Node) return (List<Node>)list;
if (first instanceof Number) {
List<Node> nodes = new ArrayList<>();
for (Number n : ((List<Number>)list)) nodes.add(db.getNodeById(n.longValue()));
return nodes;
}
}
throw new Exception("Unsupported data type for start parameter a Node or an Identifier (long) of a Node must be given!");
}

private Uniqueness getUniqueness(String uniqueness) {
for (Uniqueness u : Uniqueness.values()) {
if (u.name().equalsIgnoreCase(uniqueness)) return u;
}
return Uniqueness.RELATIONSHIP_PATH;
}

public static class PathResult {
public Path path;

public PathResult(Path path) {
this.path = path;
}
} }


public static class TestResult public static class PathResult
{ {
public String value; public Path path;


public TestResult( Node node ) PathResult( Path path )
{ {
this.value = "NodeWithId"+value; this.path = path;
} }
} }


public static class LabelEvaluator implements Evaluator public static class LabelEvaluator implements Evaluator
{ {
private Set<String> whitelistLabels;
private Set<String> blacklistLabels;
private Set<String> terminationLabels;
private Set<String> endNodeLabels; private Set<String> endNodeLabels;
private Evaluation whitelistAllowedEvaluation;
private boolean endNodesOnly;
private boolean filterStartNode;
private long limit = -1; private long limit = -1;
private long minLevel = -1; private long minLevel = -1;
private long resultCount = 0; private long resultCount = 0;


public LabelEvaluator(String labelFilter, boolean filterStartNode, long limit, int minLevel) { LabelEvaluator( String endNodeLabel, long limit, int minLevel )
this.filterStartNode = filterStartNode; {
this.limit = limit; this.limit = limit;
this.minLevel = minLevel; this.minLevel = minLevel;
Map<Character, Set<String>> labelMap = new HashMap<>(4);

if (labelFilter != null && !labelFilter.isEmpty()) {

// parse the filter
// split on |
String[] defs = labelFilter.split("\\|");
Set<String> labels = null;

for (String def : defs) {
char operator = def.charAt(0);
switch (operator) {
case '+':
case '-':
case '/':
case '>':
labels = labelMap.computeIfAbsent(operator, character -> new HashSet<>());
def = def.substring(1);
}

if (def.startsWith(":")) {
def = def.substring(1);
}


if (!def.isEmpty()) { endNodeLabels = Collections.singleton( endNodeLabel );
labels.add(def);
}
}
}

whitelistLabels = labelMap.computeIfAbsent('+', character -> Collections.emptySet());
blacklistLabels = labelMap.computeIfAbsent('-', character -> Collections.emptySet());
terminationLabels = labelMap.computeIfAbsent('/', character -> Collections.emptySet());
endNodeLabels = labelMap.computeIfAbsent('>', character -> Collections.emptySet());
endNodesOnly = !terminationLabels.isEmpty() || !endNodeLabels.isEmpty();
whitelistAllowedEvaluation = endNodesOnly ? EXCLUDE_AND_CONTINUE : INCLUDE_AND_CONTINUE;
} }


@Override @Override
public Evaluation evaluate(Path path) { public Evaluation evaluate( Path path )
{
int depth = path.length(); int depth = path.length();
Node check = path.endNode(); Node check = path.endNode();


// if start node shouldn't be filtered, exclude/include based on if using termination/endnode filter or not if ( depth < minLevel )
// minLevel evaluator will separately enforce exclusion if we're below minLevel {
if (depth == 0 && !filterStartNode) { return EXCLUDE_AND_CONTINUE;
return whitelistAllowedEvaluation;
} }


// below minLevel always exclude; continue if blacklist and whitelist allow it if ( limit != -1 && resultCount >= limit )
if (depth < minLevel) { {
return labelExists(check, blacklistLabels) || !whitelistAllowed(check) ? EXCLUDE_AND_PRUNE : EXCLUDE_AND_CONTINUE;
}

// cut off expansion when we reach the limit
if (limit != -1 && resultCount >= limit) {
return EXCLUDE_AND_PRUNE; return EXCLUDE_AND_PRUNE;
} }


Evaluation result = labelExists(check, blacklistLabels) ? EXCLUDE_AND_PRUNE : return labelExists( check, endNodeLabels ) ? countIncludeAndContinue() : EXCLUDE_AND_CONTINUE;
labelExists(check, terminationLabels) ? filterEndNode(check, true) :
labelExists(check, endNodeLabels) ? filterEndNode(check, false) :
whitelistAllowed(check) ? whitelistAllowedEvaluation : EXCLUDE_AND_PRUNE;

return result;
} }


private boolean labelExists(Node node, Set<String> labels) { private boolean labelExists( Node node, Set<String> labels )
if (labels.isEmpty()) { {
if ( labels.isEmpty() )
{
return false; return false;
} }


for ( Label lab : node.getLabels() ) { for ( Label lab : node.getLabels() )
if (labels.contains(lab.name())) { {
if ( labels.contains( lab.name() ) )
{
return true; return true;
} }
} }
return false; return false;
} }


private boolean whitelistAllowed(Node node) { private Evaluation countIncludeAndContinue()
return whitelistLabels.isEmpty() || labelExists(node, whitelistLabels); {
}

private Evaluation filterEndNode(Node node, boolean isTerminationFilter) {
resultCount++; resultCount++;
return isTerminationFilter || !whitelistAllowed(node) ? INCLUDE_AND_PRUNE : INCLUDE_AND_CONTINUE; return INCLUDE_AND_CONTINUE;
}
}

public static abstract class RelationshipTypeAndDirections {

public static final char BACKTICK = '`';

public static List<Pair<RelationshipType, Direction>> parse(String pathFilter) {
List<Pair<RelationshipType, Direction>> relsAndDirs = new ArrayList<>();
if (pathFilter == null) {
relsAndDirs.add(Pair.of(null, BOTH)); // todo can we remove this?
} else {
String[] defs = pathFilter.split("\\|");
for (String def : defs) {
relsAndDirs.add(Pair.of(relationshipTypeFor(def), directionFor(def)));
}
}
return relsAndDirs;
}

private static Direction directionFor(String type) {
if (type.contains("<")) return INCOMING;
if (type.contains(">")) return OUTGOING;
return BOTH;
}

private static RelationshipType relationshipTypeFor(String name) {
if (name.indexOf(BACKTICK) > -1) name = name.substring(name.indexOf(BACKTICK)+1,name.lastIndexOf(BACKTICK));
else {
name = name.replaceAll("[<>:]", "");
}
return name.trim().isEmpty() ? null : RelationshipType.withName(name);
} }
} }
} }

0 comments on commit 2d46225

Please sign in to comment.