For now I have implemented what I need using unmanaged extension, looks like it's near impossible to do it right and fast in Cypher.
package io.treev.treegraph.neo.extension;
import org.codehaus.jackson.JsonEncoding;
import org.codehaus.jackson.JsonGenerator;
import org.codehaus.jackson.map.ObjectMapper;
import org.neo4j.graphdb.*;
import org.neo4j.graphdb.traversal.TraversalDescription;
import org.neo4j.helpers.collection.IteratorUtil;
import javax.ws.rs.GET;
import javax.ws.rs.PathParam;
import javax.ws.rs.Produces;
import javax.ws.rs.core.Context;
import javax.ws.rs.core.MediaType;
import javax.ws.rs.core.Response;
import javax.ws.rs.core.StreamingOutput;
import java.io.IOException;
import java.util.Optional;
import java.util.stream.Stream;
import java.util.stream.StreamSupport;
@javax.ws.rs.Path("/")
public class TreeGraphResource {
private final GraphDatabaseService db;
private final ObjectMapper objectMapper;
public TreeGraphResource(@Context GraphDatabaseService graphDb) {
this.db = graphDb;
this.objectMapper = new ObjectMapper();
}
@GET
@javax.ws.rs.Path("/{nodeId}")
@Produces(MediaType.APPLICATION_JSON)
public Response readTree(@PathParam("nodeId") String nodeId) {
StreamingOutput stream = output -> {
JsonGenerator jg = objectMapper.getJsonFactory().createJsonGenerator(output, JsonEncoding.UTF8);
try (Transaction ignore = db.beginTx()) {
Node node = getNode(nodeId);
buildNode(node, jg);
}
jg.flush();
jg.close();
};
return Response.ok().entity(stream).type(MediaType.APPLICATION_JSON).build();
}
private static final String TREE_NODE_LABEL = "TreeNode";
private static final String ID_PROPERTY = "id";
private static final String EXPANDED_PROPERTY = "expanded";
private static final String CHILDREN_PROPERTY = "children";
private static final DynamicRelationshipType CHILD_OF_REL =
DynamicRelationshipType.withName("CHILD_OF");
private static final DynamicRelationshipType NEXT_SIBLING_OF_REL =
DynamicRelationshipType.withName("NEXT_SIBLING_OF");
private Node getNode(String nodeId) {
return db.findNode(DynamicLabel.label(TREE_NODE_LABEL), ID_PROPERTY, nodeId);
}
private void buildNode(Node node, JsonGenerator jg) {
try {
jg.writeStartObject();
jg.writeFieldName(ID_PROPERTY);
jg.writeString((String) node.getProperty(ID_PROPERTY));
Optional<Boolean> expandedOpt = isExpanded(node);
if (expandedOpt.isPresent()) {
boolean expanded = expandedOpt.get();
jg.writeFieldName(EXPANDED_PROPERTY);
jg.writeBoolean(expanded);
if (!expanded) {
int childrenCount = countChildren(node);
if (childrenCount > 0) {
jg.writeFieldName("childrenCount");
jg.writeNumber(childrenCount);
}
}
}
if (!expandedOpt.isPresent() || expandedOpt.get()) {
Stream<Node> children = getChildren(node);
if (children != null) {
jg.writeFieldName(CHILDREN_PROPERTY);
jg.writeStartArray();
children.forEach(child -> buildNode(child, jg));
jg.writeEndArray();
}
}
jg.writeEndObject();
} catch (IOException e) {
e.printStackTrace();
}
}
private Stream<Node> getChildren(Node node) {
if (node != null) {
Optional<Node> firstChildOpt = getFirstChild(node);
return firstChildOpt.map(firstChild -> {
TraversalDescription traversal =
db.traversalDescription().depthFirst().relationships(NEXT_SIBLING_OF_REL, Direction.INCOMING);
return StreamSupport.stream(traversal.traverse(firstChild).spliterator(), false).map(Path::endNode);
}).orElse(null);
} else {
return null;
}
}
private Optional<Node> getFirstChild(Node node) {
Iterable<Relationship> childrenRelations = node.getRelationships(CHILD_OF_REL, Direction.INCOMING);
return StreamSupport.stream(childrenRelations.spliterator(), false)
.map(Relationship::getStartNode)
.filter(relationship -> !relationship.hasRelationship(NEXT_SIBLING_OF_REL, Direction.OUTGOING))
.findFirst();
}
private int countChildren(Node node) {
if (node != null) {
return IteratorUtil.count(node.getRelationships(CHILD_OF_REL, Direction.INCOMING));
} else {
return 0;
}
}
private Optional<Boolean> isExpanded(Node node) {
Boolean expanded = (Boolean) node.getProperty(EXPANDED_PROPERTY, null);
return Optional.ofNullable(expanded);
}
}
Example response:
{
"id": "root",
"expanded": true,
"children": [
{
"id": "child10"
},
{
"id": "child0"
},
{
"id": "child1",
"expanded": true,
"children": [
{
"id": "child2",
"expanded": false,
"childrenCount": 1
},
{
"id": "child3"
},
{
"id": "child4"
}
]
}
]
}