From 4c5a85ac5110c10bd1b04c0037b3a7651036a65c Mon Sep 17 00:00:00 2001 From: Andrew Bowman Date: Fri, 27 Apr 2018 04:17:22 -0700 Subject: [PATCH] Added whitelistNodes and blacklistNodes, to finish up node-specific filtering for path expanders. (#796) --- docs/expand.adoc | 18 +- docs/overview.adoc | 22 +- .../apoc/path/LabelSequenceEvaluator.java | 82 +++++ src/main/java/apoc/path/NodeEvaluators.java | 104 ++++++ src/main/java/apoc/path/PathExplorer.java | 177 ++++------ src/test/java/apoc/path/ExpandPathTest.java | 92 ----- src/test/java/apoc/path/NodeFilterTest.java | 330 ++++++++++++++++++ 7 files changed, 604 insertions(+), 221 deletions(-) create mode 100644 src/main/java/apoc/path/LabelSequenceEvaluator.java create mode 100644 src/main/java/apoc/path/NodeEvaluators.java create mode 100644 src/test/java/apoc/path/NodeFilterTest.java diff --git a/docs/expand.adoc b/docs/expand.adoc index 4645aca443..ddf4524ec7 100644 --- a/docs/expand.adoc +++ b/docs/expand.adoc @@ -282,15 +282,21 @@ For huge graphs a traverser can hog all the memory in the JVM, causing OutOfMemo | NONE | No restriction (the user will have to manage it) |=== -.endNodes and terminatorNodes +.Node filters -As of the February 2018 APOC releases, if the end nodes of the expansion are known ahead of time (such as when testing reachability), then these nodes can be passed in as `endNodes` or `terminatorNodes`. +While label filters use labels to allow whitelisting, blacklisting, and restrictions on which kind of nodes can end or terminate expansion, +you can also filter based upon actual nodes. -This restricts the returned paths (or nodes) to only these nodes (or nodes with the given ids, if an integer list is passed). +Each of these config parameter accepts a list of nodes, or a list of node ids. -For `endNodes`, expansion continues past end nodes. - -For `terminatorNodes`, expansion down a path stops when a terminator node is reached. +[opts=header,cols="m,a,a"] +|=== +| config parameter | description | added in +| endNodes | Only these nodes can end returned paths, and expansion will continue past these nodes, if possible. | Winter 2018 APOC releases. +| terminatorNodes | Only these nodes can end returned paths, and expansion won't continue past these nodes. | Winter 2018 APOC releases. +| whitelistNodes | Only these nodes are allowed in the expansion (though endNodes and terminatorNodes will also be allowed, if present). | Spring 2018 APOC releases. +| blacklistNodes | None of the paths returned will include these nodes. | Spring 2018 APOC releases. +|=== .General Examples diff --git a/docs/overview.adoc b/docs/overview.adoc index bf076c89bd..e2b874b21a 100644 --- a/docs/overview.adoc +++ b/docs/overview.adoc @@ -1156,19 +1156,21 @@ For huge graphs a traverser can hog all the memory in the JVM, causing OutOfMemo |=== -=== End nodes and terminator nodes +== Node Filters -As of the January 2018 APOC releases, you can optionally use `endNodes` and `terminatorNodes` params in the config param map when the end nodes of the expansion are known. +While label filters use labels to allow whitelisting, blacklisting, and restrictions on which kind of nodes can end or terminate expansion, +you can also filter based upon actual nodes. -When `endNodes` are present, only these end nodes must be at the end of the expanded paths. -Expansion continues beyond end nodes. -This behavior is similar to the end node filter `>` in the label filters. +Each of these config parameter accepts a list of nodes, or a list of node ids. -Nodes given as `terminatorNodes` behave just like `endNodes` (they must be at the end of expanded paths), but stops traversal beyond the terminator nodes. -This behavior is similar to the termination filter `/` in the label filters. - -`endNodes` and/or `terminatorNodes` do not conflict with each other (an end node will be returned even if not present in the terminator nodes, and vice versa), -and they can freely be used along with the labelFilter, but a node can only be included by unanimous agreement from endNodes+terminatoNodes and the labelFilter. +[opts=header,cols="m,a,a"] +|=== +| config parameter | description | added in +| endNodes | Only these nodes can end returned paths, and expansion will continue past these nodes, if possible. | Winter 2018 APOC releases. +| terminatorNodes | Only these nodes can end returned paths, and expansion won't continue past these nodes. | Winter 2018 APOC releases. +| whitelistNodes | Only these nodes are allowed in the expansion (though endNodes and terminatorNodes will also be allowed, if present). | Spring 2018 APOC releases. +| blacklistNodes | None of the paths returned will include these nodes. | Spring 2018 APOC releases. +|=== == Parallel Node Search diff --git a/src/main/java/apoc/path/LabelSequenceEvaluator.java b/src/main/java/apoc/path/LabelSequenceEvaluator.java new file mode 100644 index 0000000000..c10fee0406 --- /dev/null +++ b/src/main/java/apoc/path/LabelSequenceEvaluator.java @@ -0,0 +1,82 @@ +package apoc.path; + +import org.neo4j.graphdb.Node; +import org.neo4j.graphdb.Path; +import org.neo4j.graphdb.traversal.Evaluation; +import org.neo4j.graphdb.traversal.Evaluator; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; + +import static org.neo4j.graphdb.traversal.Evaluation.EXCLUDE_AND_CONTINUE; +import static org.neo4j.graphdb.traversal.Evaluation.INCLUDE_AND_CONTINUE; + +// when no commas present, acts as a pathwide label filter +public class LabelSequenceEvaluator implements Evaluator { + private List sequenceMatchers; + + private Evaluation whitelistAllowedEvaluation; + private boolean endNodesOnly; + private boolean filterStartNode; + private boolean beginSequenceAtStart; + private long minLevel = -1; + + public LabelSequenceEvaluator(String labelSequence, boolean filterStartNode, boolean beginSequenceAtStart, int minLevel) { + List labelSequenceList; + + // parse sequence + if (labelSequence != null && !labelSequence.isEmpty()) { + labelSequenceList = Arrays.asList(labelSequence.split(",")); + } else { + labelSequenceList = Collections.emptyList(); + } + + initialize(labelSequenceList, filterStartNode, beginSequenceAtStart, minLevel); + } + + public LabelSequenceEvaluator(List labelSequenceList, boolean filterStartNode, boolean beginSequenceAtStart, int minLevel) { + initialize(labelSequenceList, filterStartNode, beginSequenceAtStart, minLevel); + } + + private void initialize(List labelSequenceList, boolean filterStartNode, boolean beginSequenceAtStart, int minLevel) { + this.filterStartNode = filterStartNode; + this.beginSequenceAtStart = beginSequenceAtStart; + this.minLevel = minLevel; + sequenceMatchers = new ArrayList<>(labelSequenceList.size()); + + for (String labelFilterString : labelSequenceList) { + LabelMatcherGroup matcherGroup = new LabelMatcherGroup().addLabels(labelFilterString.trim()); + sequenceMatchers.add(matcherGroup); + endNodesOnly = endNodesOnly || matcherGroup.isEndNodesOnly(); + } + + // if true for one matcher, need to set true for all matchers + if (endNodesOnly) { + for (LabelMatcherGroup group : sequenceMatchers) { + group.setEndNodesOnly(endNodesOnly); + } + } + + whitelistAllowedEvaluation = endNodesOnly ? EXCLUDE_AND_CONTINUE : INCLUDE_AND_CONTINUE; + } + + @Override + public Evaluation evaluate(Path path) { + int depth = path.length(); + Node node = path.endNode(); + boolean belowMinLevel = depth < minLevel; + + // if start node shouldn't be filtered, exclude/include based on if using termination/endnode filter or not + // minLevel evaluator will separately enforce exclusion if we're below minLevel + if (depth == 0 && (!filterStartNode || !beginSequenceAtStart)) { + return whitelistAllowedEvaluation; + } + + // the user may want the sequence to begin at the start node (default), or the sequence may only apply from the next node on + LabelMatcherGroup matcherGroup = sequenceMatchers.get((beginSequenceAtStart ? depth : depth - 1) % sequenceMatchers.size()); + + return matcherGroup.evaluate(node, belowMinLevel); + } +} \ No newline at end of file diff --git a/src/main/java/apoc/path/NodeEvaluators.java b/src/main/java/apoc/path/NodeEvaluators.java new file mode 100644 index 0000000000..d11ace13e7 --- /dev/null +++ b/src/main/java/apoc/path/NodeEvaluators.java @@ -0,0 +1,104 @@ +package apoc.path; + +import org.neo4j.graphdb.Node; +import org.neo4j.graphdb.Path; +import org.neo4j.graphdb.traversal.Evaluation; +import org.neo4j.graphdb.traversal.Evaluator; +import org.neo4j.graphdb.traversal.Evaluators; + +import java.util.HashSet; +import java.util.List; +import java.util.Set; + +/** + * Static factory methods for obtaining node evaluators + */ +public final class NodeEvaluators { + // non-instantiable + private NodeEvaluators() {}; + + /** + * Returns an evaluator which handles end nodes and terminator nodes + * Returns null if both lists are empty + */ + public static Evaluator endAndTerminatorNodeEvaluator(List endNodes, List terminatorNodes) { + Evaluator endNodeEvaluator = null; + Evaluator terminatorNodeEvaluator = null; + + if (!endNodes.isEmpty()) { + Node[] nodes = endNodes.toArray(new Node[endNodes.size()]); + endNodeEvaluator = Evaluators.includeWhereEndNodeIs(nodes); + } + + if (!terminatorNodes.isEmpty()) { + Node[] nodes = terminatorNodes.toArray(new Node[terminatorNodes.size()]); + terminatorNodeEvaluator = Evaluators.pruneWhereEndNodeIs(nodes); + } + + if (endNodeEvaluator != null || terminatorNodeEvaluator != null) { + return new EndAndTerminatorNodeEvaluator(endNodeEvaluator, terminatorNodeEvaluator); + } + + return null; + } + + public static Evaluator whitelistNodeEvaluator(List whitelistNodes) { + return new WhitelistNodeEvaluator(whitelistNodes); + } + + public static Evaluator blacklistNodeEvaluator(List blacklistNodes) { + return new BlacklistNodeEvaluator(blacklistNodes); + } + + // The evaluators from pruneWhereEndNodeIs and includeWhereEndNodeIs interfere with each other, this makes them play nice + private static class EndAndTerminatorNodeEvaluator implements Evaluator { + private Evaluator endNodeEvaluator; + private Evaluator terminatorNodeEvaluator; + + public EndAndTerminatorNodeEvaluator(Evaluator endNodeEvaluator, Evaluator terminatorNodeEvaluator) { + this.endNodeEvaluator = endNodeEvaluator; + this.terminatorNodeEvaluator = terminatorNodeEvaluator; + } + + @Override + public Evaluation evaluate(Path path) { + // at least one has to give a thumbs up to include + boolean includes = evalIncludes(endNodeEvaluator, path) || evalIncludes(terminatorNodeEvaluator, path); + // prune = terminatorNodeEvaluator != null && !terminatorNodeEvaluator.evaluate(path).continues() + // negate this to get continues result + boolean continues = terminatorNodeEvaluator == null || terminatorNodeEvaluator.evaluate(path).continues(); + + return Evaluation.of(includes, continues); + } + + private boolean evalIncludes(Evaluator eval, Path path) { + return eval != null && eval.evaluate(path).includes(); + } + } + + private static class BlacklistNodeEvaluator implements Evaluator { + private Set blacklistSet; + + public BlacklistNodeEvaluator(List blacklistNodes) { + blacklistSet = new HashSet<>(blacklistNodes); + } + + @Override + public Evaluation evaluate(Path path) { + return blacklistSet.contains(path.endNode()) ? Evaluation.EXCLUDE_AND_PRUNE : Evaluation.INCLUDE_AND_CONTINUE; + } + } + + private static class WhitelistNodeEvaluator implements Evaluator { + private Set whitelistSet; + + public WhitelistNodeEvaluator(List whitelistNodes) { + whitelistSet = new HashSet<>(whitelistNodes); + } + + @Override + public Evaluation evaluate(Path path) { + return whitelistSet.contains(path.endNode()) ? Evaluation.INCLUDE_AND_CONTINUE : Evaluation.EXCLUDE_AND_PRUNE; + } + } +} diff --git a/src/main/java/apoc/path/PathExplorer.java b/src/main/java/apoc/path/PathExplorer.java index 89413b0f4a..5e5aa0fcdf 100644 --- a/src/main/java/apoc/path/PathExplorer.java +++ b/src/main/java/apoc/path/PathExplorer.java @@ -3,14 +3,16 @@ import apoc.algo.Cover; import apoc.result.GraphResult; import apoc.result.NodeResult; -import org.neo4j.procedure.Description; import apoc.result.PathResult; import apoc.util.Util; -import org.neo4j.graphdb.*; -import org.neo4j.graphdb.traversal.Evaluation; +import org.neo4j.graphdb.GraphDatabaseService; +import org.neo4j.graphdb.Node; +import org.neo4j.graphdb.Path; +import org.neo4j.graphdb.Relationship; import org.neo4j.graphdb.traversal.*; import org.neo4j.logging.Log; import org.neo4j.procedure.Context; +import org.neo4j.procedure.Description; import org.neo4j.procedure.Name; import org.neo4j.procedure.Procedure; @@ -19,8 +21,7 @@ import java.util.stream.Stream; import java.util.stream.StreamSupport; -import static org.neo4j.graphdb.traversal.Evaluation.*; - +import static apoc.path.PathExplorer.NodeFilter.*; public class PathExplorer { public static final Uniqueness UNIQUENESS = Uniqueness.RELATIONSHIP_PATH; @@ -39,7 +40,7 @@ public Stream explorePath(@Name("start") Object start , @Name("minLevel") long minLevel , @Name("maxLevel") long maxLevel ) throws Exception { List nodes = startToNodes(start); - return explorePathPrivate(nodes, pathFilter, labelFilter, minLevel, maxLevel, BFS, UNIQUENESS, false, -1, Collections.emptyList(), Collections.emptyList(), null, true).map( PathResult::new ); + return explorePathPrivate(nodes, pathFilter, labelFilter, minLevel, maxLevel, BFS, UNIQUENESS, false, -1, null, null, true).map( PathResult::new ); } // @@ -142,13 +143,32 @@ private Stream expandConfigPrivate(@Name("start") Object start, @Name("con boolean filterStartNode = Util.toBoolean(config.getOrDefault("filterStartNode", false)); long limit = Util.toLong(config.getOrDefault("limit", "-1")); boolean optional = Util.toBoolean(config.getOrDefault("optional", false)); - List endNodes = startToNodes(config.get("endNodes")); - List terminatorNodes = startToNodes(config.get("terminatorNodes")); String sequence = (String) config.getOrDefault("sequence", null); boolean beginSequenceAtStart = Util.toBoolean(config.getOrDefault("beginSequenceAtStart", true)); + List endNodes = startToNodes(config.get("endNodes")); + List terminatorNodes = startToNodes(config.get("terminatorNodes")); + List whitelistNodes = startToNodes(config.get("whitelistNodes")); + List blacklistNodes = startToNodes(config.get("blacklistNodes")); + EnumMap> nodeFilter = new EnumMap<>(NodeFilter.class); - Stream results = explorePathPrivate(nodes, relationshipFilter, labelFilter, minLevel, maxLevel, bfs, getUniqueness(uniqueness), filterStartNode, limit, endNodes, terminatorNodes, sequence, beginSequenceAtStart); + if (endNodes != null && !endNodes.isEmpty()) { + nodeFilter.put(END_NODES, endNodes); + } + + if (terminatorNodes != null && !terminatorNodes.isEmpty()) { + nodeFilter.put(TERMINATOR_NODES, terminatorNodes); + } + + if (whitelistNodes != null && !whitelistNodes.isEmpty()) { + nodeFilter.put(WHITELIST_NODES, whitelistNodes); + } + + if (blacklistNodes != null && !blacklistNodes.isEmpty()) { + nodeFilter.put(BLACKLIST_NODES, blacklistNodes); + } + + Stream results = explorePathPrivate(nodes, relationshipFilter, labelFilter, minLevel, maxLevel, bfs, getUniqueness(uniqueness), filterStartNode, limit, nodeFilter, sequence, beginSequenceAtStart); if (optional) { return optionalStream(results); @@ -166,12 +186,11 @@ private Stream explorePathPrivate(Iterable startNodes, Uniqueness uniqueness, boolean filterStartNode, long limit, - List endNodes, - List terminatorNodes, + EnumMap> nodeFilter, String sequence, boolean beginSequenceAtStart) { - Traverser traverser = traverse(db.traversalDescription(), startNodes, pathFilter, labelFilter, minLevel, maxLevel, uniqueness,bfs,filterStartNode, endNodes, terminatorNodes, sequence, beginSequenceAtStart); + Traverser traverser = traverse(db.traversalDescription(), startNodes, pathFilter, labelFilter, minLevel, maxLevel, uniqueness,bfs,filterStartNode, nodeFilter, sequence, beginSequenceAtStart); if (limit == -1) { return traverser.stream(); @@ -208,8 +227,7 @@ public static Traverser traverse(TraversalDescription traversalDescription, Uniqueness uniqueness, boolean bfs, boolean filterStartNode, - List endNodes, - List terminatorNodes, + EnumMap> nodeFilter, String sequence, boolean beginSequenceAtStart) { TraversalDescription td = traversalDescription; @@ -243,116 +261,49 @@ public static Traverser traverse(TraversalDescription traversalDescription, if (minLevel != -1) td = td.evaluator(Evaluators.fromDepth((int) minLevel)); if (maxLevel != -1) td = td.evaluator(Evaluators.toDepth((int) maxLevel)); - Evaluator endNodeEvaluator = null; - Evaluator terminatorNodeEvaluator = null; - - if (!endNodes.isEmpty()) { - Node[] nodes = endNodes.toArray(new Node[endNodes.size()]); - endNodeEvaluator = Evaluators.includeWhereEndNodeIs(nodes); - } - - if (!terminatorNodes.isEmpty()) { - Node[] nodes = terminatorNodes.toArray(new Node[terminatorNodes.size()]); - terminatorNodeEvaluator = Evaluators.pruneWhereEndNodeIs(nodes); - } - - if (endNodeEvaluator != null || terminatorNodeEvaluator != null) { - td = td.evaluator(new EndAndTerminatorNodeEvaluator(endNodeEvaluator, terminatorNodeEvaluator)); - } - - 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); - } - - // when no commas present, acts as a pathwide label filter - public static class LabelSequenceEvaluator implements Evaluator { - private List sequenceMatchers; - private Evaluation whitelistAllowedEvaluation; - private boolean endNodesOnly; - private boolean filterStartNode; - private boolean beginSequenceAtStart; - private long minLevel = -1; + if (nodeFilter != null && !nodeFilter.isEmpty()) { + List endNodes = nodeFilter.getOrDefault(END_NODES, Collections.EMPTY_LIST); + List terminatorNodes = nodeFilter.getOrDefault(TERMINATOR_NODES, Collections.EMPTY_LIST); + List blacklistNodes = nodeFilter.getOrDefault(BLACKLIST_NODES, Collections.EMPTY_LIST); + List whitelistNodes; - public LabelSequenceEvaluator(String labelSequence, boolean filterStartNode, boolean beginSequenceAtStart, int minLevel) { - List labelSequenceList; - - // parse sequence - if (labelSequence != null && !labelSequence.isEmpty()) { - labelSequenceList = Arrays.asList(labelSequence.split(",")); + if (nodeFilter.containsKey(WHITELIST_NODES)) { + // need to add to new list since we may need to add to it later + // encounter "can't add to abstractList" error if we don't do this + whitelistNodes = new ArrayList<>(nodeFilter.get(WHITELIST_NODES)); } else { - labelSequenceList = Collections.emptyList(); + whitelistNodes = Collections.EMPTY_LIST; } - initialize(labelSequenceList, filterStartNode, beginSequenceAtStart, minLevel); - } - - public LabelSequenceEvaluator(List labelSequenceList, boolean filterStartNode, boolean beginSequenceAtStart, int minLevel) { - initialize(labelSequenceList, filterStartNode, beginSequenceAtStart, minLevel); - } - - private void initialize(List labelSequenceList, boolean filterStartNode, boolean beginSequenceAtStart, int minLevel) { - this.filterStartNode = filterStartNode; - this.beginSequenceAtStart = beginSequenceAtStart; - this.minLevel = minLevel; - sequenceMatchers = new ArrayList<>(labelSequenceList.size()); - - for (String labelFilterString : labelSequenceList) { - LabelMatcherGroup matcherGroup = new LabelMatcherGroup().addLabels(labelFilterString.trim()); - sequenceMatchers.add(matcherGroup); - endNodesOnly = endNodesOnly || matcherGroup.isEndNodesOnly(); + if (!blacklistNodes.isEmpty()) { + td = td.evaluator(NodeEvaluators.blacklistNodeEvaluator(blacklistNodes)); } - // if true for one matcher, need to set true for all matchers - if (endNodesOnly) { - for (LabelMatcherGroup group : sequenceMatchers) { - group.setEndNodesOnly(endNodesOnly); - } + Evaluator endAndTerminatorNodeEvaluator = NodeEvaluators.endAndTerminatorNodeEvaluator(endNodes, terminatorNodes); + if (endAndTerminatorNodeEvaluator != null) { + td = td.evaluator(endAndTerminatorNodeEvaluator); } - whitelistAllowedEvaluation = endNodesOnly ? EXCLUDE_AND_CONTINUE : INCLUDE_AND_CONTINUE; - } - - @Override - public Evaluation evaluate(Path path) { - int depth = path.length(); - Node node = path.endNode(); - boolean belowMinLevel = depth < minLevel; - - // if start node shouldn't be filtered, exclude/include based on if using termination/endnode filter or not - // minLevel evaluator will separately enforce exclusion if we're below minLevel - if (depth == 0 && (!filterStartNode || !beginSequenceAtStart)) { - return whitelistAllowedEvaluation; + if (!whitelistNodes.isEmpty()) { + // ensure endNodes and terminatorNodes are whitelisted + whitelistNodes.addAll(endNodes); + whitelistNodes.addAll(terminatorNodes); + td = td.evaluator(NodeEvaluators.whitelistNodeEvaluator(whitelistNodes)); } - - // the user may want the sequence to begin at the start node (default), or the sequence may only apply from the next node on - LabelMatcherGroup matcherGroup = sequenceMatchers.get((beginSequenceAtStart ? depth : depth - 1) % sequenceMatchers.size()); - - return matcherGroup.evaluate(node, belowMinLevel); } - } - - // The evaluators from pruneWhereEndNodeIs and includeWhereEndNodeIs interfere with each other, this makes them play nice - public static class EndAndTerminatorNodeEvaluator implements Evaluator { - private Evaluator endNodeEvaluator; - private Evaluator terminatorNodeEvaluator; - - public EndAndTerminatorNodeEvaluator(Evaluator endNodeEvaluator, Evaluator terminatorNodeEvaluator) { - this.endNodeEvaluator = endNodeEvaluator; - this.terminatorNodeEvaluator = terminatorNodeEvaluator; - } - - @Override - public Evaluation evaluate(Path path) { - boolean includes = evalIncludes(endNodeEvaluator, path) || evalIncludes(terminatorNodeEvaluator, path); - boolean continues = terminatorNodeEvaluator == null || terminatorNodeEvaluator.evaluate(path).continues(); - return Evaluation.of(includes, continues); - } + 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); + } - private boolean evalIncludes(Evaluator eval, Path path) { - return eval != null && eval.evaluate(path).includes(); - } + // keys to node filter map + enum NodeFilter { + WHITELIST_NODES, + BLACKLIST_NODES, + END_NODES, + TERMINATOR_NODES } -} + +} \ No newline at end of file diff --git a/src/test/java/apoc/path/ExpandPathTest.java b/src/test/java/apoc/path/ExpandPathTest.java index f5a86ba399..5c6804d095 100644 --- a/src/test/java/apoc/path/ExpandPathTest.java +++ b/src/test/java/apoc/path/ExpandPathTest.java @@ -325,96 +325,4 @@ public void testCompoundLabelWorksInBlacklist() { assertEquals("Gene Hackman", node.getProperty("name")); }); } - - @Test - public void testTerminatorNodesPruneExpansion() { - db.execute("MATCH (c:Person) WHERE c.name in ['Clint Eastwood', 'Gene Hackman'] SET c:Western"); - - TestUtil.testResult(db, - "MATCH (k:Person {name:'Keanu Reeves'}), (gene:Person {name:'Gene Hackman'}), (clint:Person {name:'Clint Eastwood'}) " + - "CALL apoc.path.subgraphNodes(k, {relationshipFilter:'ACTED_IN|PRODUCED|DIRECTED', terminatorNodes:[gene, clint]}) yield node " + - "return node", - result -> { - - List> maps = Iterators.asList(result); - assertEquals(1, maps.size()); - Node node = (Node) maps.get(0).get("node"); - assertEquals("Gene Hackman", node.getProperty("name")); - }); - } - - @Test - public void testEndNodesContinueTraversal() { - db.execute("MATCH (c:Person) WHERE c.name in ['Clint Eastwood', 'Gene Hackman'] SET c:Western WITH c WHERE c.name = 'Clint Eastwood' SET c:Blacklist"); - - TestUtil.testResult(db, - "MATCH (k:Person {name:'Keanu Reeves'}), (gene:Person {name:'Gene Hackman'}), (clint:Person {name:'Clint Eastwood'}) " + - "CALL apoc.path.subgraphNodes(k, {relationshipFilter:'ACTED_IN|PRODUCED|DIRECTED', endNodes:[gene, clint]}) yield node " + - "return node", - result -> { - - List> maps = Iterators.asList(result); - assertEquals(2, maps.size()); - Node node = (Node) maps.get(0).get("node"); - assertEquals("Gene Hackman", node.getProperty("name"));; - node = (Node) maps.get(1).get("node"); - assertEquals("Clint Eastwood", node.getProperty("name")); - }); - } - - @Test - public void testEndNodesAndTerminatorNodesReturnExpectedResults() { - db.execute("MATCH (c:Person) WHERE c.name in ['Clint Eastwood', 'Gene Hackman'] SET c:Western WITH c WHERE c.name = 'Clint Eastwood' SET c:Blacklist"); - - TestUtil.testResult(db, - "MATCH (k:Person {name:'Keanu Reeves'}), (gene:Person {name:'Gene Hackman'}), (clint:Person {name:'Clint Eastwood'}) " + - "CALL apoc.path.subgraphNodes(k, {relationshipFilter:'ACTED_IN|PRODUCED|DIRECTED', endNodes:[gene], terminatorNodes:[clint]}) yield node " + - "return node", - result -> { - - List> maps = Iterators.asList(result); - assertEquals(2, maps.size()); - Node node = (Node) maps.get(0).get("node"); - assertEquals("Gene Hackman", node.getProperty("name"));; - node = (Node) maps.get(1).get("node"); - assertEquals("Clint Eastwood", node.getProperty("name")); - }); - } - - @Test - public void testEndNodesWithTerminationFilterPrunesExpansion() { - db.execute("MATCH (c:Person) WHERE c.name in ['Clint Eastwood', 'Gene Hackman'] SET c:Western"); - - TestUtil.testResult(db, - "MATCH (k:Person {name:'Keanu Reeves'}), (gene:Person {name:'Gene Hackman'}), (clint:Person {name:'Clint Eastwood'}) " + - "CALL apoc.path.subgraphNodes(k, {relationshipFilter:'ACTED_IN|PRODUCED|DIRECTED', labelFilter:'/Western', endNodes:[clint, gene]}) yield node " + - "return node", - result -> { - - List> maps = Iterators.asList(result); - assertEquals(1, maps.size()); - Node node = (Node) maps.get(0).get("node"); - assertEquals("Gene Hackman", node.getProperty("name")); - }); - } - - @Test - public void testTerminatorNodesWithEndNodeFilterPrunesExpansion() { - db.execute("MATCH (c:Person) WHERE c.name in ['Clint Eastwood', 'Gene Hackman'] SET c:Western"); - - TestUtil.testResult(db, - "MATCH (k:Person {name:'Keanu Reeves'}), (gene:Person {name:'Gene Hackman'}), (clint:Person {name:'Clint Eastwood'}) " + - "CALL apoc.path.subgraphNodes(k, {relationshipFilter:'ACTED_IN|PRODUCED|DIRECTED', labelFilter:'>Western', terminatorNodes" + - ":[clint, gene]}) yield node " + - "return node", - result -> { - - List> maps = Iterators.asList(result); - assertEquals(1, maps.size()); - Node node = (Node) maps.get(0).get("node"); - assertEquals("Gene Hackman", node.getProperty("name")); - }); - } - - } diff --git a/src/test/java/apoc/path/NodeFilterTest.java b/src/test/java/apoc/path/NodeFilterTest.java new file mode 100644 index 0000000000..231819796c --- /dev/null +++ b/src/test/java/apoc/path/NodeFilterTest.java @@ -0,0 +1,330 @@ +package apoc.path; + +import apoc.util.TestUtil; +import apoc.util.Util; +import org.junit.After; +import org.junit.AfterClass; +import org.junit.BeforeClass; +import org.junit.Test; +import org.neo4j.graphdb.GraphDatabaseService; +import org.neo4j.graphdb.Node; +import org.neo4j.graphdb.Transaction; +import org.neo4j.helpers.collection.Iterators; +import org.neo4j.test.TestGraphDatabaseFactory; + +import java.util.List; +import java.util.Map; + +import static org.junit.Assert.assertEquals; + +/** + * Test path expanders with node filters (where we already have the nodes that will be used for the whitelist, blacklist, endnodes, and terminator nodes + */ +public class NodeFilterTest { + private static GraphDatabaseService db; + + public NodeFilterTest() throws Exception { + } + + @BeforeClass + public static void setUp() throws Exception { + db = new TestGraphDatabaseFactory().newImpermanentDatabase(); + TestUtil.registerProcedure(db, PathExplorer.class); + String movies = Util.readResourceFile("movies.cypher"); + String bigbrother = "MATCH (per:Person) MERGE (bb:BigBrother {name : 'Big Brother' }) MERGE (bb)-[:FOLLOWS]->(per)"; + try (Transaction tx = db.beginTx()) { + db.execute(movies); + db.execute(bigbrother); + tx.success(); + } + } + + @AfterClass + public static void tearDown() { + db.shutdown(); + } + + @After + public void removeOtherLabels() { + db.execute("OPTIONAL MATCH (c:Western) REMOVE c:Western WITH DISTINCT 1 as ignore OPTIONAL MATCH (c:Blacklist) REMOVE c:Blacklist"); + } + + @Test + public void testTerminatorNodesPruneExpansion() { + db.execute("MATCH (c:Person) WHERE c.name in ['Clint Eastwood', 'Gene Hackman'] SET c:Western"); + + TestUtil.testResult(db, + "MATCH (k:Person {name:'Keanu Reeves'}), (gene:Person {name:'Gene Hackman'}), (clint:Person {name:'Clint Eastwood'}) " + + "CALL apoc.path.subgraphNodes(k, {relationshipFilter:'ACTED_IN|PRODUCED|DIRECTED', terminatorNodes:[gene, clint]}) yield node " + + "return node", + result -> { + + List> maps = Iterators.asList(result); + assertEquals(1, maps.size()); + Node node = (Node) maps.get(0).get("node"); + assertEquals("Gene Hackman", node.getProperty("name")); + }); + } + + @Test + public void testEndNodesContinueTraversal() { + db.execute("MATCH (c:Person) WHERE c.name in ['Clint Eastwood', 'Gene Hackman'] SET c:Western WITH c WHERE c.name = 'Clint Eastwood' SET c:Blacklist"); + + TestUtil.testResult(db, + "MATCH (k:Person {name:'Keanu Reeves'}), (gene:Person {name:'Gene Hackman'}), (clint:Person {name:'Clint Eastwood'}) " + + "CALL apoc.path.subgraphNodes(k, {relationshipFilter:'ACTED_IN|PRODUCED|DIRECTED', endNodes:[gene, clint]}) yield node " + + "return node", + result -> { + + List> maps = Iterators.asList(result); + assertEquals(2, maps.size()); + Node node = (Node) maps.get(0).get("node"); + assertEquals("Gene Hackman", node.getProperty("name")); + node = (Node) maps.get(1).get("node"); + assertEquals("Clint Eastwood", node.getProperty("name")); + }); + } + + @Test + public void testEndNodesAndTerminatorNodesReturnExpectedResults() { + db.execute("MATCH (c:Person) WHERE c.name in ['Clint Eastwood', 'Gene Hackman'] SET c:Western WITH c WHERE c.name = 'Clint Eastwood' SET c:Blacklist"); + + TestUtil.testResult(db, + "MATCH (k:Person {name:'Keanu Reeves'}), (gene:Person {name:'Gene Hackman'}), (clint:Person {name:'Clint Eastwood'}) " + + "CALL apoc.path.subgraphNodes(k, {relationshipFilter:'ACTED_IN|PRODUCED|DIRECTED', endNodes:[gene], terminatorNodes:[clint]}) yield node " + + "return node", + result -> { + + List> maps = Iterators.asList(result); + assertEquals(2, maps.size()); + Node node = (Node) maps.get(0).get("node"); + assertEquals("Gene Hackman", node.getProperty("name")); + node = (Node) maps.get(1).get("node"); + assertEquals("Clint Eastwood", node.getProperty("name")); + }); + } + + @Test + public void testEndNodesAndTerminatorNodesReturnExpectedResultsReversed() { + db.execute("MATCH (c:Person) WHERE c.name in ['Clint Eastwood', 'Gene Hackman'] SET c:Western WITH c WHERE c.name = 'Clint Eastwood' SET c:Blacklist"); + + TestUtil.testResult(db, + "MATCH (k:Person {name:'Keanu Reeves'}), (gene:Person {name:'Gene Hackman'}), (clint:Person {name:'Clint Eastwood'}) " + + "CALL apoc.path.subgraphNodes(k, {relationshipFilter:'ACTED_IN|PRODUCED|DIRECTED', terminatorNodes:[gene], endNodes:[clint]}) yield node " + + "return node", + result -> { + + List> maps = Iterators.asList(result); + assertEquals(1, maps.size()); + Node node = (Node) maps.get(0).get("node"); + assertEquals("Gene Hackman", node.getProperty("name")); + }); + } + + @Test + public void testTerminatorNodesOverruleEndNodes1() { + db.execute("MATCH (c:Person) WHERE c.name in ['Clint Eastwood', 'Gene Hackman'] SET c:Western WITH c WHERE c.name = 'Clint Eastwood' SET c:Blacklist"); + + TestUtil.testResult(db, + "MATCH (k:Person {name:'Keanu Reeves'}), (gene:Person {name:'Gene Hackman'}), (clint:Person {name:'Clint Eastwood'}) " + + "CALL apoc.path.subgraphNodes(k, {relationshipFilter:'ACTED_IN|PRODUCED|DIRECTED', terminatorNodes:[gene], endNodes:[clint, gene]}) yield node " + + "return node", + result -> { + + List> maps = Iterators.asList(result); + assertEquals(1, maps.size()); + Node node = (Node) maps.get(0).get("node"); + assertEquals("Gene Hackman", node.getProperty("name")); + }); + } + + @Test + public void testTerminatorNodesOverruleEndNodes2() { + db.execute("MATCH (c:Person) WHERE c.name in ['Clint Eastwood', 'Gene Hackman'] SET c:Western WITH c WHERE c.name = 'Clint Eastwood' SET c:Blacklist"); + + TestUtil.testResult(db, + "MATCH (k:Person {name:'Keanu Reeves'}), (gene:Person {name:'Gene Hackman'}), (clint:Person {name:'Clint Eastwood'}) " + + "CALL apoc.path.subgraphNodes(k, {relationshipFilter:'ACTED_IN|PRODUCED|DIRECTED', terminatorNodes:[gene, clint], endNodes:[clint, gene]}) yield node " + + "return node", + result -> { + + List> maps = Iterators.asList(result); + assertEquals(1, maps.size()); + Node node = (Node) maps.get(0).get("node"); + assertEquals("Gene Hackman", node.getProperty("name")); + }); + } + + @Test + public void testEndNodesWithTerminationFilterPrunesExpansion() { + db.execute("MATCH (c:Person) WHERE c.name in ['Clint Eastwood', 'Gene Hackman'] SET c:Western"); + + TestUtil.testResult(db, + "MATCH (k:Person {name:'Keanu Reeves'}), (gene:Person {name:'Gene Hackman'}), (clint:Person {name:'Clint Eastwood'}) " + + "CALL apoc.path.subgraphNodes(k, {relationshipFilter:'ACTED_IN|PRODUCED|DIRECTED', labelFilter:'/Western', endNodes:[clint, gene]}) yield node " + + "return node", + result -> { + + List> maps = Iterators.asList(result); + assertEquals(1, maps.size()); + Node node = (Node) maps.get(0).get("node"); + assertEquals("Gene Hackman", node.getProperty("name")); + }); + } + + @Test + public void testTerminatorNodesWithEndNodeFilterPrunesExpansion() { + db.execute("MATCH (c:Person) WHERE c.name in ['Clint Eastwood', 'Gene Hackman'] SET c:Western"); + + TestUtil.testResult(db, + "MATCH (k:Person {name:'Keanu Reeves'}), (gene:Person {name:'Gene Hackman'}), (clint:Person {name:'Clint Eastwood'}) " + + "CALL apoc.path.subgraphNodes(k, {relationshipFilter:'ACTED_IN|PRODUCED|DIRECTED', labelFilter:'>Western', terminatorNodes" + + ":[clint, gene]}) yield node " + + "return node", + result -> { + + List> maps = Iterators.asList(result); + assertEquals(1, maps.size()); + Node node = (Node) maps.get(0).get("node"); + assertEquals("Gene Hackman", node.getProperty("name")); + }); + } + + @Test + public void testBlacklistNodesInPathPrunesPath() { + db.execute("MATCH (c:Person) WHERE c.name in ['Clint Eastwood', 'Gene Hackman'] SET c:Western"); + + TestUtil.testResult(db, + "MATCH (k:Person {name:'Keanu Reeves'}), (gene:Person {name:'Gene Hackman'}), (clint:Person {name:'Clint Eastwood'}), (unforgiven:Movie{title:'Unforgiven'}) " + + "CALL apoc.path.subgraphNodes(k, {relationshipFilter:'ACTED_IN|PRODUCED|DIRECTED', labelFilter:'>Western', blacklistNodes:[unforgiven]}) yield node " + + "return node", + result -> { + + List> maps = Iterators.asList(result); + assertEquals(1, maps.size()); + Node node = (Node) maps.get(0).get("node"); + assertEquals("Gene Hackman", node.getProperty("name")); + }); + } + + @Test + public void testBlacklistNodesWithEndNodesPrunesPath() { + db.execute("MATCH (c:Person) WHERE c.name in ['Clint Eastwood', 'Gene Hackman'] SET c:Western"); + + TestUtil.testResult(db, + "MATCH (k:Person {name:'Keanu Reeves'}), (gene:Person {name:'Gene Hackman'}), (clint:Person {name:'Clint Eastwood'}), (unforgiven:Movie{title:'Unforgiven'}) " + + "CALL apoc.path.subgraphNodes(k, {relationshipFilter:'ACTED_IN|PRODUCED|DIRECTED', endNodes:[clint, gene], blacklistNodes:[unforgiven]}) yield node " + + "return node", + result -> { + + List> maps = Iterators.asList(result); + assertEquals(1, maps.size()); + Node node = (Node) maps.get(0).get("node"); + assertEquals("Gene Hackman", node.getProperty("name")); + }); + } + + @Test + public void testBlacklistNodesOverridesAllOtherNodeFilters() { + db.execute("MATCH (c:Person) WHERE c.name in ['Clint Eastwood', 'Gene Hackman'] SET c:Western"); + + TestUtil.testResult(db, + "MATCH (k:Person {name:'Keanu Reeves'}), (gene:Person {name:'Gene Hackman'}), (clint:Person {name:'Clint Eastwood'}), (unforgiven:Movie{title:'Unforgiven'}), (replacements:Movie{title:'The Replacements'})\n" + + "WITH k, clint, gene, [k, gene, clint, unforgiven, replacements] as whitelist\n" + + "CALL apoc.path.subgraphNodes(k, {relationshipFilter:'ACTED_IN|PRODUCED|DIRECTED', terminatorNodes:[clint], endNodes:[clint], whitelistNodes:whitelist, blacklistNodes:[clint]}) yield node return node", + result -> { + + List> maps = Iterators.asList(result); + assertEquals(0, maps.size()); + }); + } + + @Test + public void testWhitelistNodes() { + db.execute("MATCH (c:Person) WHERE c.name in ['Clint Eastwood', 'Gene Hackman'] SET c:Western"); + + TestUtil.testResult(db, + "MATCH (k:Person {name:'Keanu Reeves'}), (gene:Person {name:'Gene Hackman'}), (clint:Person {name:'Clint Eastwood'}), (unforgiven:Movie{title:'Unforgiven'}), (replacements:Movie{title:'The Replacements'})\n" + + "WITH k, clint, gene, [k, gene, clint, unforgiven, replacements] as whitelist\n" + + "CALL apoc.path.subgraphNodes(k, {relationshipFilter:'ACTED_IN|PRODUCED|DIRECTED', endNodes:[clint, gene], whitelistNodes:whitelist}) yield node return node", + result -> { + + List> maps = Iterators.asList(result); + assertEquals(2, maps.size()); + Node node = (Node) maps.get(0).get("node"); + assertEquals("Gene Hackman", node.getProperty("name")); + node = (Node) maps.get(1).get("node"); + assertEquals("Clint Eastwood", node.getProperty("name")); + }); + } + + @Test + public void testWhitelistNodesIncludesEndNodes() { + db.execute("MATCH (c:Person) WHERE c.name in ['Clint Eastwood', 'Gene Hackman'] SET c:Western"); + + TestUtil.testResult(db, + "MATCH (k:Person {name:'Keanu Reeves'}), (gene:Person {name:'Gene Hackman'}), (clint:Person {name:'Clint Eastwood'}), (unforgiven:Movie{title:'Unforgiven'}), (replacements:Movie{title:'The Replacements'})\n" + + "WITH k, clint, gene, [k, gene, unforgiven, replacements] as whitelist\n" + + "CALL apoc.path.subgraphNodes(k, {relationshipFilter:'ACTED_IN|PRODUCED|DIRECTED', endNodes:[clint, gene], whitelistNodes:whitelist}) yield node return node", + result -> { + + List> maps = Iterators.asList(result); + assertEquals(2, maps.size()); + Node node = (Node) maps.get(0).get("node"); + assertEquals("Gene Hackman", node.getProperty("name")); + node = (Node) maps.get(1).get("node"); + assertEquals("Clint Eastwood", node.getProperty("name")); + }); + } + + @Test + public void testWhitelistNodesIncludesTerminatorNodes() { + db.execute("MATCH (c:Person) WHERE c.name in ['Clint Eastwood', 'Gene Hackman'] SET c:Western"); + + TestUtil.testResult(db, + "MATCH (k:Person {name:'Keanu Reeves'}), (gene:Person {name:'Gene Hackman'}), (clint:Person {name:'Clint Eastwood'}), (unforgiven:Movie{title:'Unforgiven'}), (replacements:Movie{title:'The Replacements'}) \n" + + "WITH k, clint, gene, [k, gene, unforgiven, replacements] as whitelist \n" + + "CALL apoc.path.subgraphNodes(k, {relationshipFilter:'ACTED_IN|PRODUCED|DIRECTED', terminatorNodes:[clint], whitelistNodes:whitelist}) yield node return node", + result -> { + + List> maps = Iterators.asList(result); + assertEquals(1, maps.size()); + Node node = (Node) maps.get(0).get("node"); + assertEquals("Clint Eastwood", node.getProperty("name")); + }); + } + + @Test + public void testWhitelistNodesAndLabelFiltersMustAgreeToInclude1() { + db.execute("MATCH (c:Person) WHERE c.name in ['Clint Eastwood', 'Gene Hackman'] SET c:Western"); + + TestUtil.testResult(db, + "MATCH (k:Person {name:'Keanu Reeves'}), (replacements:Movie{title:'The Replacements'}) \n" + + "WITH k, [k, replacements] as whitelist \n" + + "CALL apoc.path.subgraphNodes(k, {relationshipFilter:'ACTED_IN|PRODUCED|DIRECTED', labelFilter:'+Person', whitelistNodes:whitelist}) yield node return node", + result -> { + + List> maps = Iterators.asList(result); + assertEquals(1, maps.size()); + Node node = (Node) maps.get(0).get("node"); + assertEquals("Keanu Reeves", node.getProperty("name")); + }); + } + + @Test + public void testWhitelistNodesAndLabelFiltersMustAgreeToInclude2() { + db.execute("MATCH (c:Person) WHERE c.name in ['Clint Eastwood', 'Gene Hackman'] SET c:Western"); + + TestUtil.testResult(db, + "MATCH (k:Person {name:'Keanu Reeves'}), (replacements:Movie{title:'The Replacements'}) \n" + + "WITH k, [k] as whitelist \n" + + "CALL apoc.path.subgraphNodes(k, {relationshipFilter:'ACTED_IN|PRODUCED|DIRECTED', labelFilter:'+Person|+Movie', whitelistNodes:whitelist}) yield node return node", + result -> { + + List> maps = Iterators.asList(result); + assertEquals(1, maps.size()); + Node node = (Node) maps.get(0).get("node"); + assertEquals("Keanu Reeves", node.getProperty("name")); + }); + } +}