Repository: neo4j-contrib/neo4j-apoc-procedures
Pull request #75 (2016-08-02 12:47:04)

add support for "/" operator in label evaluator


Patch:
diff --git a/build.gradle b/build.gradle
index 963791d25..adad07b9d 100644
--- a/build.gradle
+++ b/build.gradle
@@ -10,7 +10,7 @@ sourceCompatibility = 1.8
 targetCompatibility = 1.8
 
 ext {
-    neo4jVersion = "3.0.3"
+    neo4jVersion = "3.0.4"
 }
 repositories {
      maven { url "http://repo.maven.apache.org/maven2" }
diff --git a/docs/expand.adoc b/docs/expand.adoc
index b9fe7d19a..8f1634c7e 100644
--- a/docs/expand.adoc
+++ b/docs/expand.adoc
@@ -25,13 +25,14 @@ Syntax: `[<]RELATIONSHIP_TYPE1[>]|[<]RELATIONSHIP_TYPE2[>]|...`
 
 Label Filter
 
-Syntax: `[+-]LABEL1|LABEL2|...`
+Syntax: `[+-/]LABEL1|LABEL2|...`
 
 [opts=header,cols="m,m,a"]
 |===
 | input | label | result
 | +Friend | Friend | include label (whitelist)
 | -Foe | Foe | exclude label (blacklist)
+| /Friend | Friend | stop traversal after reaching a friend (but include him)
 |===
 
 == Examples
diff --git a/pom.xml b/pom.xml
index cb2c21099..ccde14338 100644
--- a/pom.xml
+++ b/pom.xml
@@ -12,7 +12,7 @@
   <description>A collection of useful Neo4j Procedures</description>
 
   <properties>
-    <neo4j.version>3.0.3</neo4j.version>
+    <neo4j.version>3.0.4</neo4j.version>
   </properties>
 
   <organization>
diff --git a/readme.adoc b/readme.adoc
index 3183691ec..0ffa41477 100644
--- a/readme.adoc
+++ b/readme.adoc
@@ -641,13 +641,14 @@ Syntax: `[<]RELATIONSHIP_TYPE1[>]|[<]RELATIONSHIP_TYPE2[>]|...`
 
 ==== Label Filter
 
-Syntax: `[+-]LABEL1|LABEL2|...`
+Syntax: `[+-/]LABEL1|LABEL2|...`
 
 [opts=header,cols="m,m,a"]
 |===
 | input | label | result
 | +Friend | Friend | include label (whitelist)
 | -Foe | Foe | exclude label (blacklist)
+| /Friend | Friend | stop traversal after reaching a friend (but include him)
 |===
 
 === Parallel Node Search 
diff --git a/src/main/java/apoc/cypher/Cypher.java b/src/main/java/apoc/cypher/Cypher.java
index f4b3137ce..c67d4c54f 100644
--- a/src/main/java/apoc/cypher/Cypher.java
+++ b/src/main/java/apoc/cypher/Cypher.java
@@ -88,7 +88,7 @@ public class Cypher {
             long time = System.currentTimeMillis();
             int row = 0;
             while (result.hasNext()) {
-                if (tx.shouldBeTerminated()) break;
+                if (tx.getReasonIfTerminated()!=null) break;
                 queue.put(new RowResult(row++, result.next()));
             }
             queue.put(new RowResult(-1, toMap(result.getQueryStatistics(), System.currentTimeMillis() - time, row)));
@@ -162,7 +162,7 @@ public class Cypher {
         final String statement = compiled(fragment, params.keySet());
         Collection<Object> coll = (Collection<Object>) value;
         return coll.parallelStream().flatMap((v) -> {
-            if (tx.shouldBeTerminated()) return Stream.of(MapResult.empty());
+            if (tx.getReasonIfTerminated()!=null) return Stream.of(MapResult.empty());
             Map<String, Object> parallelParams = new HashMap<>(params);
             parallelParams.replace(key, v);
             return api.execute(statement, parallelParams).stream().map(MapResult::new);
@@ -244,7 +244,7 @@ public class Cypher {
         for (Object o : coll) {
             partition.add(o);
             if (partition.size() == batchSize) {
-                if (tx.shouldBeTerminated()) return Stream.of(MapResult.empty());
+                if (tx.getReasonIfTerminated()!=null) return Stream.of(MapResult.empty());
                 futures.add(submit(api, statement, params, key, partition));
                 partition = new ArrayList<>(batchSize);
             }
@@ -309,7 +309,7 @@ public class Cypher {
         @Override
         public boolean tryAdvance(Consumer<? super T> action) {
             try {
-                if (tx.shouldBeTerminated()) return false;
+                if (tx.getReasonIfTerminated()!=null) return false;
                 T entry = queue.poll(100, TimeUnit.MILLISECONDS);
                 if (entry == null || entry == tombstone) return false;
                 action.accept(entry);
diff --git a/src/main/java/apoc/path/PathExplorer.java b/src/main/java/apoc/path/PathExplorer.java
index 45b25a2c1..e77762fa2 100644
--- a/src/main/java/apoc/path/PathExplorer.java
+++ b/src/main/java/apoc/path/PathExplorer.java
@@ -14,6 +14,8 @@ import org.neo4j.procedure.Procedure;
 import java.util.*;
 import java.util.stream.Stream;
 
+import static org.neo4j.graphdb.traversal.Evaluation.*;
+
 
 public class PathExplorer {
 	private static final String VERSION = "0.5";
@@ -131,12 +133,12 @@ public class PathExplorer {
 	}
 
 	public static class LabelEvaluator implements Evaluator {
-		private boolean included = true;
+		private char operator;
 		private Set<String> labels = new HashSet<String>();
 		public LabelEvaluator(String labelFilter) {
 			// parse the filter
-			if (labelFilter ==  null || labelFilter.equalsIgnoreCase("")) labelFilter = "-"; // exclude nothing
-			included = labelFilter.startsWith("+");
+			if (labelFilter ==  null || labelFilter.isEmpty()) labelFilter = "-"; // exclude nothing
+			operator = labelFilter.charAt(0);
 			String work = labelFilter.substring(1); // remove the + or -
 			// split on |
 			String[] defs = work.split("\\|") ;
@@ -145,25 +147,27 @@ public class PathExplorer {
 				labels.add(def);
 			}
 		}
-		
-		
+
 		@Override
 		public Evaluation evaluate(Path path) {
 			Node check = path.endNode();
-			if (included) {
-				if (labelExists(check)) {
-					return Evaluation.INCLUDE_AND_CONTINUE;
-				} else {
-					return Evaluation.EXCLUDE_AND_PRUNE;
-				}
-			} else {
-				if (labelExists(check)) {
-					return Evaluation.EXCLUDE_AND_PRUNE;
-				} else {
-					return Evaluation.INCLUDE_AND_CONTINUE;
-				}
+			Evaluation result;
+			switch (operator) {
+				case '+':
+					result = labelExists(check) ? INCLUDE_AND_CONTINUE : EXCLUDE_AND_PRUNE;
+					break;
+				case '-':
+					result = labelExists(check) ? EXCLUDE_AND_PRUNE : INCLUDE_AND_CONTINUE;
+					break;
+				case '/':
+					result = labelExists(check) ? INCLUDE_AND_PRUNE : EXCLUDE_AND_CONTINUE;
+					break;
+				default:
+					throw new IllegalArgumentException("evaluator uses unknown operator " + operator);
 			}
+			return result;
 		}
+
 		private boolean labelExists(Node node) {
 			for ( Label lab : node.getLabels() ) {
 				if (labels.contains(lab.name())) {
diff --git a/src/main/java/apoc/spatial/Geocode.java b/src/main/java/apoc/spatial/Geocode.java
index 97381720d..a7cef154f 100644
--- a/src/main/java/apoc/spatial/Geocode.java
+++ b/src/main/java/apoc/spatial/Geocode.java
@@ -64,7 +64,7 @@ public class Geocode {
             long msSinceLastCall = currentTimeMillis() - lastCallTime;
             while (msSinceLastCall < throttleInMs) {
                 try {
-                    if (kernelTransaction.shouldBeTerminated()) return;
+                    if (kernelTransaction.getReasonIfTerminated()!=null) return;
                     long msToWait = throttleInMs - msSinceLastCall;
                     Thread.sleep(Math.min(msToWait, 1000));
                 } catch (InterruptedException e) {
diff --git a/src/main/java/apoc/util/Utils.java b/src/main/java/apoc/util/Utils.java
index 33f6e89b7..21ff9a63f 100644
--- a/src/main/java/apoc/util/Utils.java
+++ b/src/main/java/apoc/util/Utils.java
@@ -44,7 +44,7 @@ public class Utils {
         long started = System.currentTimeMillis();
         while (System.currentTimeMillis()-started < duration) {
             Thread.sleep(5);
-            if (transaction.shouldBeTerminated()) {
+            if (transaction.getReasonIfTerminated()!=null) {
                 return;
             }
         }


Test patch:
diff --git a/src/test/java/apoc/path/ExpandPathTest.java b/src/test/java/apoc/path/ExpandPathTest.java
index aa7aeca0e..f792349f9 100644
--- a/src/test/java/apoc/path/ExpandPathTest.java
+++ b/src/test/java/apoc/path/ExpandPathTest.java
@@ -1,25 +1,20 @@
 package apoc.path;
 
-import static org.hamcrest.MatcherAssert.assertThat;
-import static org.hamcrest.core.IsEqual.equalTo;
 import static org.junit.Assert.assertEquals;
 
-import java.io.IOException;
-import java.io.InputStream;
-import java.io.InputStreamReader;
-import java.io.Reader;
-import java.io.UnsupportedEncodingException;
-import java.util.Scanner;
-
 import apoc.util.Util;
 import org.junit.*;
 import org.neo4j.graphdb.GraphDatabaseService;
-import org.neo4j.graphdb.Result;
+import org.neo4j.graphdb.Path;
 import org.neo4j.graphdb.Transaction;
+import org.neo4j.helpers.collection.Iterators;
 import org.neo4j.test.TestGraphDatabaseFactory;
 
 import apoc.util.TestUtil;
 
+import java.util.List;
+import java.util.Map;
+
 public class ExpandPathTest {
     private static GraphDatabaseService db;
 
@@ -43,8 +38,7 @@ public class ExpandPathTest {
     public static void tearDown() {
         db.shutdown();
     }
-		
-	
+
 	@Test
 	public void testExplorePathRelationshipsTest() throws Throwable {
 		String query = "MATCH (m:Movie {title: 'The Matrix'}) CALL apoc.path.expand(m,'<ACTED_IN|PRODUCED>|FOLLOWS','-',0,2) yield path return count(*) as c";
@@ -62,4 +56,21 @@ public class ExpandPathTest {
 		String query = "MATCH (m:Movie {title: 'The Matrix'}) CALL apoc.path.expand(m,null,'-BigBrother',0,2) yield path return count(*) as c";
 		TestUtil.testCall(db, query, (row) -> assertEquals(44L,row.get("c")));
 	}
+
+	@Test
+	public void testExplorePathWithTerminationLabel() {
+		db.execute("MATCH (c:Person{name:'Clint Eastwood'}) SET c:Western");
+
+		TestUtil.testResult(db,
+				"MATCH (k:Person {name:'Keanu Reeves'}) " +
+				"CALL apoc.path.expandConfig(k, {relationshipFilter:'ACTED_IN|PRODUCED|DIRECTED', labelFilter:'/Western', uniqueness: 'NODE_GLOBAL'}) yield path " +
+				"return path",
+				result -> {
+
+					List<Map<String, Object>> maps = Iterators.asList(result);
+					assertEquals(1, maps.size());
+					Path path = (Path) maps.get(0).get("path");
+					assertEquals("Clint Eastwood", path.endNode().getProperty("name"));
+				});
+	}
 }