I created a maven project with a prototype of the custom procedure. I just got something to work to get you started. It works, but I don't know how you want to define the input parameters. I just specified the root node and the property of each node to test. The procedure terminates when it gets to a terminal node without any children or no node has the property to test. In your case, this is the 'DS' node. You could change the algorithm to pass the 'DS' node and terminate when it is reached. Anyways, you can use this to modify it for your needs.
I did't try to optimize it or make it production ready. It is just a quick prototype.
package customFunctions;
import org.neo4j.graphdb.Direction;
import org.neo4j.graphdb.Node;
import org.neo4j.graphdb.Relationship;
import org.neo4j.procedure.Description;
import org.neo4j.procedure.Name;
import org.neo4j.procedure.Procedure;
import java.util.ArrayList;
import java.util.List;
import java.util.Objects;
import java.util.stream.Stream;
public class CustomProcedure {
@Procedure(name = "custom.traverseGraph")
@Description("Get list of nodes from root node that traverse the path with highest values of property 'prop'")
public Stream<TraversalResult> traverseTree(@Name("root") Node rootNode, @Name("property") String prop) {
Objects.requireNonNull(rootNode);
Objects.requireNonNull(prop);
List<Node> listOfNodes = new ArrayList<>();
processNode(rootNode, prop, listOfNodes);
return Stream.of(TraversalResult.of(listOfNodes));
}
private void processNode(Node node, String property, List<Node> nodes) {
boolean nodeFound = false;
Long currentMaxValue = Long.MIN_VALUE;
Node childNodeWithMaxValue = null;
Iterable<Relationship> relationships = node.getRelationships(Direction.OUTGOING);
for (Relationship relationship : relationships) {
Node childNode = relationship.getOtherNode(node);
if (childNode.hasProperty(property)) {
Long propertyValue = (Long) childNode.getProperty(property);
if (propertyValue > currentMaxValue) {
childNodeWithMaxValue = childNode;
currentMaxValue = propertyValue;
nodeFound = true;
}
}
}
if (nodeFound) {
nodes.add(childNodeWithMaxValue);
processNode(childNodeWithMaxValue, property, nodes);
}
}
public static class TraversalResult {
public List<Node> nodes;
private TraversalResult(List<Node> nodes) {
this.nodes = nodes;
}
public static TraversalResult of(List<Node> nodes) {
return new TraversalResult(nodes);
}
}
}
Test class showing how to test the procedure using an embedded instance.
import customFunctions.CustomProcedure;
import org.junit.jupiter.api.*;
import org.neo4j.driver.*;
import org.neo4j.driver.types.Node;
import org.neo4j.harness.Neo4j;
import org.neo4j.harness.Neo4jBuilders;
import java.util.Arrays;
import java.util.List;
import java.util.stream.Collectors;
class CustomProcedureTests {
static Driver driver;
@BeforeAll
static void setup\_db() {
Neo4j neo4j = Neo4jBuilders.newInProcessBuilder()
.withProcedure(CustomProcedure.class)
.build();
driver \= GraphDatabase.driver(neo4j.boltURI(), Config.builder()
.withoutEncryption()
.build());
}
@BeforeEach
void delete\_data() {
String cypher = "merge(A:External {name:'A'})\\n" +
"merge(B:Node {name:'B'}) set B.sens\_value = 80\\n" +
"merge(C:Node {name:'C'}) set C.sens\_value = 70\\n" +
"merge(D:Node {name:'D'}) set D.sens\_value = 50\\n" +
"merge(E:Node {name:'E'}) set E.sens\_value = 90\\n" +
"merge(F:Node {name:'F'}) set F.sens\_value = 10\\n" +
"merge(G:Node {name:'G'}) set G.sens\_value = 40\\n" +
"merge(H:Node {name:'H'}) set H.sens\_value = 60\\n" +
"merge(I:Node {name:'I'}) set I.sens\_value = 50\\n" +
"merge(J:Node {name:'J'}) set J.sens\_value = 30\\n" +
"merge(K:Node {name:'K'}) set K.sens\_value = 45\\n" +
"merge(L:Node {name:'L'}) set L.sens\_value = 55\\n" +
"merge(DS:Node {name:'DS'})\\n" +
"merge(A)-\[:RELATION\]->(B)\\n" +
"merge(A)-\[:RELATION\]->(C)\\n" +
"merge(A)-\[:RELATION\]->(D)\\n" +
"merge(D)-\[:RELATION\]->(E)\\n" +
"merge(E)-\[:RELATION\]->(F)\\n" +
"merge(F)-\[:RELATION\]->(DS)\\n" +
"merge(B)-\[:RELATION\]->(G)\\n" +
"merge(G)-\[:RELATION\]->(I)\\n" +
"merge(I)-\[:RELATION\]->(DS)\\n" +
"merge(G)-\[:RELATION\]->(H)\\n" +
"merge(H)-\[:RELATION\]->(DS)\\n" +
"merge(B)-\[:RELATION\]->(J)\\n" +
"merge(J)-\[:RELATION\]->(DS)\\n" +
"merge(C)-\[:RELATION\]->(K)\\n" +
"merge(K)-\[:RELATION\]->(L)\\n" +
"merge(L)-\[:RELATION\]->(DS)";
try (Session session = driver.session()) {
session.run("match(n) detach delete n");
session.run(cypher);
}
}
@Test
@DisplayName("Test TraverseGraph Scenarios")
void test() {
String cypher = "match (a:External {name: 'A'}) " +
"call custom.traverseGraph(a, 'sens\_value') yield nodes " +
"return nodes ";
List<Node> result = getCypherResults(cypher);
List<String> listOfIds = result.stream().map(n -> n.get("name").asString()).collect(Collectors.toList());
Assertions.assertIterableEquals(Arrays.asList("B", "G", "H"), listOfIds);
}
private List<Node> getCypherResults(String cypher) {
try (Session session = driver.session()) {
Result result = session.run(cypher);
return result.single().get("nodes").asList(Value::asNode);
}
}
}
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0
http://maven.apache.org/xsd/maven-4.0.0.xsd">
4.0.0
<groupId>com.domain.CustomProcedure</groupId>
<artifactId>customProcedure</artifactId>
<version>0.0.1</version>
<packaging>jar</packaging>
<name>Custom Procedure to Traverse Graph</name>
<description>Prototype of a custom procedure to find the optimal path based on a property value</description>
<properties> <java.version>11</java.version>
<maven.compiler.source>${java.version}</maven.compiler.source>
<maven.compiler.target>${java.version}</maven.compiler.target>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
<neo4j.version>4.4.8</neo4j.version>
<neo4j-java-driver.version>4.4.6</neo4j-java-driver.version>
<maven-shade-plugin.version>3.2.4</maven-shade-plugin.version>
<maven-compiler-plugin.version>3.8.1</maven-compiler-plugin.version>
<maven-surefire-plugin.version>2.22.2</maven-surefire-plugin.version>
<maven-failsafe-plugin.version>2.22.2</maven-failsafe-plugin.version>
</properties>
<dependencies> <dependency> <!-- This gives us the Procedure API our runtime code uses.
We have a \`provided\` scope on it, because when this is deployed in a Neo4j Instance, the API will be provided by Neo4j. If you add non-Neo4j dependencies to this project, their scope should normally be \`compile\` --> <groupId>org.neo4j</groupId>
<artifactId>neo4j</artifactId>
<version>${neo4j.version}</version>
<scope>provided</scope>
</dependency>
<!-- Test Dependencies -->
<dependency>
<!-- This is used for a utility that lets us start Neo4j with
a specific Procedure, which is nice for writing tests. --> <groupId>org.neo4j.test</groupId>
<artifactId>neo4j-harness</artifactId>
<version>${neo4j.version}</version>
<scope>test</scope>
</dependency>
<dependency> <!-- Used to send cypher statements to our procedure. -->
<groupId>org.neo4j.driver</groupId>
<artifactId>neo4j-java-driver</artifactId>
<version>${neo4j-java-driver.version}</version>
<scope>test</scope>
</dependency>
<dependency> <groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter-api</artifactId>
<version>5.8.2</version>
<scope>test</scope>
</dependency>
</dependencies>
<build> <plugins> <plugin> <groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>${maven-compiler-plugin.version}</version>
</plugin> <plugin> <groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-surefire-plugin</artifactId>
<version>${maven-surefire-plugin.version}</version>
<configuration> <skipTests>true</skipTests>
</configuration> </plugin> <plugin> <groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-failsafe-plugin</artifactId>
<version>${maven-failsafe-plugin.version}</version>
</plugin> <plugin> <!-- This generates a jar-file with our procedure code,
plus any dependencies marked as \`compile\` scope. This should then be deployed in the \`plugins\` directory of each Neo4j instance in your deployment. After a restart, the procedure is available for calling. --> <groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-shade-plugin</artifactId>
<version>${maven-shade-plugin.version}</version>
<configuration> <createDependencyReducedPom>false</createDependencyReducedPom>
</configuration> <executions> <execution> <phase>package</phase>
<goals> <goal>shade</goal>
</goals> </execution> </executions> </plugin> </plugins> </build></project>
These three files should be all you need.