All Downloads are FREE. Search and download functionalities are using the official Maven repository.

apoc.nodes.Nodes Maven / Gradle / Ivy

There is a newer version: 5.24.0
Show newest version
package apoc.nodes;

import apoc.Pools;
import apoc.create.Create;
import apoc.refactor.util.PropertiesManager;
import apoc.refactor.util.RefactorConfig;
import apoc.result.LongResult;
import apoc.result.NodeResult;
import apoc.result.PathResult;
import apoc.result.RelationshipResult;
import apoc.result.VirtualNode;
import apoc.result.VirtualPath;
import apoc.result.VirtualPathResult;
import apoc.util.collection.Iterables;
import apoc.util.Util;
import org.apache.commons.lang3.tuple.Pair;
import org.neo4j.graphalgo.BasicEvaluationContext;
import org.neo4j.graphalgo.GraphAlgoFactory;
import org.neo4j.graphalgo.PathFinder;
import org.neo4j.graphdb.Direction;
import org.neo4j.graphdb.Entity;
import org.neo4j.graphdb.GraphDatabaseService;
import org.neo4j.graphdb.Label;
import org.neo4j.graphdb.Node;
import org.neo4j.graphdb.Path;
import org.neo4j.graphdb.PathExpander;
import org.neo4j.graphdb.PathExpanderBuilder;
import org.neo4j.graphdb.Relationship;
import org.neo4j.graphdb.RelationshipType;
import org.neo4j.graphdb.Transaction;
import org.neo4j.internal.kernel.api.CursorFactory;
import org.neo4j.internal.kernel.api.NodeCursor;
import org.neo4j.internal.kernel.api.Read;
import org.neo4j.internal.kernel.api.RelationshipTraversalCursor;
import org.neo4j.internal.kernel.api.TokenRead;
import org.neo4j.kernel.api.KernelTransaction;
import org.neo4j.procedure.Context;
import org.neo4j.procedure.Description;
import org.neo4j.procedure.Mode;
import org.neo4j.procedure.Name;
import org.neo4j.procedure.Procedure;
import org.neo4j.procedure.UserFunction;
import org.neo4j.storageengine.api.RelationshipSelection;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashMap;
import java.util.Iterator;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.Set;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import java.util.stream.StreamSupport;

import static apoc.path.RelationshipTypeAndDirections.format;
import static apoc.path.RelationshipTypeAndDirections.parse;
import static apoc.refactor.util.RefactorUtil.copyProperties;
import static apoc.util.Util.map;

public class Nodes {

    @Context
    public GraphDatabaseService db;

    @Context
    public Transaction tx;

    @Context
    public KernelTransaction ktx;

    @Context
    public Pools pools;
    
    @Procedure("apoc.nodes.cycles")
    @Description("Detects all path cycles in the given node list.\n" +
            "This procedure can be limited on relationships as well.")
    public Stream cycles(@Name("nodes") List nodes, @Name(value = "config",defaultValue = "{}") Map config) {
        NodesConfig conf = new NodesConfig(config);
        final List types = conf.getRelTypes();
        Stream paths = nodes.stream().flatMap(start -> {
            boolean allRels = types.isEmpty();
            final RelationshipType[] relTypes = types.stream().map(RelationshipType::withName).toArray(RelationshipType[]::new);
            final Iterable relationships = allRels
                    ? start.getRelationships(Direction.OUTGOING)
                    : start.getRelationships(Direction.OUTGOING, relTypes);

            PathExpanderBuilder expanderBuilder;
            if (allRels) {
                expanderBuilder = PathExpanderBuilder.allTypes(Direction.OUTGOING);
            } else {
                expanderBuilder = PathExpanderBuilder.empty();
                for (RelationshipType relType: relTypes) {
                    expanderBuilder = expanderBuilder.add(relType, Direction.OUTGOING);
                }
            }
            final PathExpander pathExpander = expanderBuilder.build();

            PathFinder finder = GraphAlgoFactory.shortestPath(
                    new BasicEvaluationContext(tx, db),
                    pathExpander,
                    conf.getMaxDepth());
            Map> dups = new HashMap<>();
            return Iterables.stream(relationships)
                    // to prevent duplicated (start and end nodes with double-rels)
                    .filter(relationship -> {
                        final List nodeDups = dups.computeIfAbsent(relationship.getStartNodeId(), (key) -> new ArrayList<>());
                        if (nodeDups.contains(relationship.getEndNodeId())) {
                            return false;
                        }
                        nodeDups.add(relationship.getEndNodeId());
                        return true;
                    })
                    .flatMap(relationship -> {
                        final Path path = finder.findSinglePath(relationship.getEndNode(), start);
                        if (path == null) return Stream.empty();
                        VirtualPath virtualPath = new VirtualPath(start);
                        virtualPath.addRel(relationship);
                        for (Relationship relPath : path.relationships()) {
                            virtualPath.addRel(relPath);
                        }
                        return Stream.of(virtualPath);
                    });
        });
        return paths.map(PathResult::new);
    }

    @Procedure(name = "apoc.nodes.link", mode = Mode.WRITE)
    @Description("Creates a linked list of the given nodes connected by the given relationship type.")
    public void link(@Name("nodes") List nodes, @Name("type") String type, @Name(value = "config",defaultValue = "{}") Map config) {
        RefactorConfig conf = new RefactorConfig(config);
        Iterator it = nodes.iterator();
        if (it.hasNext()) {
            RelationshipType relType = RelationshipType.withName(type);
            Node node = it.next();
            while (it.hasNext()) {
                Node next = it.next();
                final boolean createRelationship = !conf.isAvoidDuplicates() || (conf.isAvoidDuplicates() && !connected(node, next, type));
                if (createRelationship) {
                    node.createRelationshipTo(next, relType);
                }
                node = next;
            }
        }
    }

    @Procedure("apoc.nodes.get")
    @Description("Returns all nodes with the given ids.")
    public Stream get(@Name("nodes") Object ids) {
        return Util.nodeStream(tx, ids).map(NodeResult::new);
    }

    @Procedure(name = "apoc.nodes.delete", mode = Mode.WRITE)
    @Description("Deletes all nodes with the given ids.")
    public Stream delete(@Name("nodes") Object ids, @Name("batchSize") long batchSize) {
        Iterator it = Util.nodeStream(tx, ids).iterator();
        long count = 0;
        while (it.hasNext()) {
            final List batch = Util.take(it, (int)batchSize);
            count += Util.inTx(db, pools, (txInThread) -> {txInThread.execute("FOREACH (n in $nodes | DETACH DELETE n)",map("nodes",batch)).close();return batch.size();});
        }
        return Stream.of(new LongResult(count));
    }

    @Procedure("apoc.nodes.rels")
    @Description("Returns all relationships with the given ids.")
    public Stream rels(@Name("rels") Object ids) {
        return Util.relsStream(tx, ids).map(RelationshipResult::new);
    }

    @UserFunction("apoc.node.relationship.exists")
    @Description("Returns a boolean based on whether the given node has a relationship (or whether the given node has a relationship of the given type and direction).")
    public boolean hasRelationship(@Name("node") Node node, @Name(value = "relTypes", defaultValue = "") String types) {
        if (types == null || types.isEmpty()) return node.hasRelationship();
        long id = node.getId();
        try ( NodeCursor nodeCursor = ktx.cursors().allocateNodeCursor(ktx.cursorContext())) {

            ktx.dataRead().singleNode(id, nodeCursor);
            nodeCursor.next();
            TokenRead tokenRead = ktx.tokenRead();

            for (Pair pair : parse(types)) {
                int typeId = tokenRead.relationshipType(pair.getLeft().name());
                Direction direction = pair.getRight();

                int count;
                switch (direction) {
                    case INCOMING:
                        count = org.neo4j.internal.kernel.api.helpers.Nodes.countIncoming(nodeCursor, typeId);
                        break;
                    case OUTGOING:
                        count = org.neo4j.internal.kernel.api.helpers.Nodes.countOutgoing(nodeCursor, typeId);
                        break;
                    case BOTH:
                        count = org.neo4j.internal.kernel.api.helpers.Nodes.countAll(nodeCursor, typeId);
                        break;
                    default:
                        throw new UnsupportedOperationException("invalid direction " + direction);
                }
                if (count > 0) {
                    return true;
                }
            }
        }
        return false;
    }

    @UserFunction("apoc.nodes.connected")
    @Description("Returns true when a given node is directly connected to another given node.\n" +
            "This function is optimized for dense nodes.")
    public boolean connected(@Name("startNode") Node start, @Name("endNode") Node end, @Name(value = "types", defaultValue = "") String types)  {
        if (start == null || end == null) return false;
        if (start.equals(end)) return true;

        long startId = start.getId();
        long endId = end.getId();
        List> pairs = (types == null || types.isEmpty()) ? null : parse(types);

        Read dataRead = ktx.dataRead();
        TokenRead tokenRead = ktx.tokenRead();
        CursorFactory cursors = ktx.cursors();

        try (NodeCursor startNodeCursor = cursors.allocateNodeCursor(ktx.cursorContext());
             NodeCursor endNodeCursor = cursors.allocateNodeCursor(ktx.cursorContext())) {

            dataRead.singleNode(startId, startNodeCursor);
            if (!startNodeCursor.next()) {
                throw new IllegalArgumentException("node with id " + startId + " does not exist.");
            }

//            boolean startDense = startNodeCursor.supportsFastDegreeLookup();
            dataRead.singleNode(endId, endNodeCursor);
            if (!endNodeCursor.next()) {
                throw new IllegalArgumentException("node with id " + endId + " does not exist.");
            }
//            boolean endDense = endNodeCursor.supportsFastDegreeLookup();

            return connected(startNodeCursor, endId, typedDirections(tokenRead, pairs, true));


//            if (!startDense) return connected(startNodeCursor, endId, typedDirections(tokenRead, pairs, true));
//            if (!endDense) return connected(endNodeCursor, startId, typedDirections(tokenRead, pairs, false));
//            return connectedDense(startNodeCursor, endNodeCursor, typedDirections(tokenRead, pairs, true));
        }
    }

    @Procedure("apoc.nodes.collapse")
    @Description("Merges nodes together in the given list.\n" +
            "The nodes are then combined to become one node, with all labels of the previous nodes attached to it, and all relationships pointing to it.")
    public Stream collapse(@Name("nodes") List nodes, @Name(value = "config", defaultValue = "{}") Map config) {
        if (nodes == null || nodes.isEmpty()) return Stream.empty();
        if (nodes.size() == 1) return Stream.of(new VirtualPathResult(nodes.get(0), null, null));
        Set nodeSet = new LinkedHashSet<>(nodes);
        RefactorConfig conf = new RefactorConfig(config);
        VirtualNode first = createVirtualNode(nodeSet, conf);
        if (first.getRelationships().iterator().hasNext()) {
            return StreamSupport.stream(first.getRelationships().spliterator(), false)
                    .map(relationship -> new VirtualPathResult(relationship.getStartNode(), relationship, relationship.getEndNode()));
        } else {
            return Stream.of(new VirtualPathResult(first, null, null));
        }
    }

    private VirtualNode createVirtualNode(Set nodes, RefactorConfig conf) {
        Create create = new Create();
        Node first = nodes.iterator().next();
        List labels = Util.labelStrings(first);
        if (conf.isCollapsedLabel()) {
            labels.add("Collapsed");
        }
        VirtualNode virtualNode = (VirtualNode) create.vNodeFunction(labels, first.getAllProperties());
        createVirtualRelationships(nodes, virtualNode, first, conf);
        nodes.stream().skip(1).forEach(node -> {
            virtualNode.addLabels(node.getLabels());
            PropertiesManager.mergeProperties(node.getAllProperties(), virtualNode, conf);
            createVirtualRelationships(nodes, virtualNode, node, conf);
        });
        if (conf.isCountMerge()) {
            virtualNode.setProperty("count", nodes.size());
        }
        return virtualNode;
    }

    private void createVirtualRelationships(Set nodes, VirtualNode virtualNode, Node node, RefactorConfig refactorConfig) {
        node.getRelationships().forEach(relationship -> {
            Node startNode = relationship.getStartNode();
            Node endNode = relationship.getEndNode();

            if (nodes.contains(startNode) && nodes.contains(endNode)) {
                if (refactorConfig.isSelfRel()) {
                    createOrMergeVirtualRelationship(virtualNode, refactorConfig, relationship, virtualNode,  Direction.OUTGOING);
                }
            } else {
                if (startNode.getId() == node.getId()) {
                    createOrMergeVirtualRelationship(virtualNode, refactorConfig, relationship, endNode,  Direction.OUTGOING);
                } else {
                    createOrMergeVirtualRelationship(virtualNode, refactorConfig, relationship, startNode,  Direction.INCOMING);
                }
            }
        });
    }

    private void createOrMergeVirtualRelationship(VirtualNode virtualNode, RefactorConfig refactorConfig, Relationship source, Node node, Direction direction) {
        Iterable rels = virtualNode.getRelationships(direction, source.getType());
        Optional first = StreamSupport.stream(rels.spliterator(), false).filter(relationship -> relationship.getOtherNode(virtualNode).equals(node)).findFirst();
        if (refactorConfig.isMergeVirtualRels() && first.isPresent()) {
            mergeRelationship(source, first.get(), refactorConfig);
        } else {
            if (direction==Direction.OUTGOING)
               copyProperties(source, virtualNode.createRelationshipTo(node, source.getType()));
            if (direction==Direction.INCOMING) 
               copyProperties(source, virtualNode.createRelationshipFrom(node, source.getType()));
        }
    }

    private void mergeRelationship(Relationship source, Relationship target, RefactorConfig refactorConfig) {
        if (refactorConfig.isCountMerge()) {
            target.setProperty("count", (Integer) target.getProperty("count", 0) + 1);
        }
        PropertiesManager.mergeProperties(source.getAllProperties(), target, refactorConfig);
    }

    /**
     * TODO: be more efficient, in
     * @param start
     * @param end
     * @param typedDirections
     * @return
     */
    private boolean connected(NodeCursor start, long end, int[][] typedDirections) {
        try (RelationshipTraversalCursor relationship = ktx.cursors().allocateRelationshipTraversalCursor(ktx.cursorContext())) {
            start.relationships(relationship, RelationshipSelection.selection(Direction.BOTH));
            while (relationship.next()) {
                if (relationship.otherNodeReference() ==end) {
                    if (typedDirections==null) {
                        return true;
                    } else {
                        int direction = relationship.targetNodeReference() == end ? 0 : 1 ;
                        int[] types = typedDirections[direction];
                        if (arrayContains(types, relationship.type())) return true;
                    }
                }
            }
        }
        return false;
    }

    private boolean arrayContains(int[] array, int element) {
        for (int i=0; i> pairs, boolean outgoing) {
        if (pairs==null) return null;
        int from=0;int to=0;
        int[][] result = new int[2][pairs.size()];
        int outIdx = Direction.OUTGOING.ordinal();
        int inIdx = Direction.INCOMING.ordinal();
        for (Pair pair : pairs) {
            int type = ops.relationshipType(pair.getLeft().name());
            if (type == -1) continue;
            if (pair.getRight() != Direction.INCOMING) {
                result[outIdx][from++]= type;
            }
            if (pair.getRight() != Direction.OUTGOING) {
                result[inIdx][to++]= type;
            }
        }
        result[outIdx] = Arrays.copyOf(result[outIdx], from);
        result[inIdx] = Arrays.copyOf(result[inIdx], to);
        if (!outgoing) {
            int[] tmp = result[outIdx];
            result[outIdx] = result[inIdx];
            result[inIdx] = tmp;
        }
        return result;
    }

    @UserFunction("apoc.node.labels")
    @Description("Returns the labels for the given virtual node.")
    public List labels(@Name("node") Node node) {
        if (node == null) return null;
        Iterator




© 2015 - 2024 Weber Informatics LLC | Privacy Policy