I have a tree structure like this:
Each node has id
property, eg 'root', 'child0', and so on, and expanded
property meaning that current branch is expanded, ie its children should be included when querying.
So, what I need is to write Cypher query that will, starting from a node with a specified id
, read all its children nodes stopping at leaves and nodes that have expanded
property set to false
.
The result should be in a tree-like form, so I don't need to process it further.
I tried something like
MATCH (n:Node { id: 'root' }),
paths = (n)<-[:CHILD_OF*]-(children:Node { expanded: true })
RETURN collect(nodes(paths))
but then I get 'depth-first' paths (with a bunch of duplicated information) that I would need to post-process to get a tree out of them. And I couldn't do it cause it doesn't include ordering information of children (what NEXT_SIBLING_OF property is for).
Is there an idiomatic way to accomplish this in Cypher or should I fallback to writing an unmanaged extension?
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"
}
]
}
]
}
The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.