0
votes

I have a tree structure like this:

enter image description here

Each node has id property, e.g. 'root', 'child0', and so on, and expanded property meaning that current branch is expanded, i.e. 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?

1

1 Answers

0
votes

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"
        }
      ]
    }
  ]
}