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
/*
 * Copyright (c) "Neo4j"
 * Neo4j Sweden AB [http://neo4j.com]
 *
 * This file is part of Neo4j.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *    http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package apoc.nodes;

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;

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.Util;
import apoc.util.collection.Iterables;
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.Objects;
import java.util.Optional;
import java.util.Set;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import java.util.stream.StreamSupport;
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.kernel.impl.coreapi.InternalTransaction;
import org.neo4j.procedure.Context;
import org.neo4j.procedure.Description;
import org.neo4j.procedure.Mode;
import org.neo4j.procedure.Name;
import org.neo4j.procedure.NotThreadSafe;
import org.neo4j.procedure.Procedure;
import org.neo4j.procedure.UserFunction;
import org.neo4j.storageengine.api.RelationshipSelection;

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 `LIST`.\n"
            + "This procedure can be limited on `RELATIONSHIP` values 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.getStartNode().getElementId(), (key) -> new ArrayList<>());
                        if (nodeDups.contains(relationship.getEndNode().getElementId())) {
                            return false;
                        }
                        nodeDups.add(relationship.getEndNode().getElementId());
                        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 `NODE` values 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 `NODE` values with the given ids.")
    public Stream get(@Name("nodes") Object ids) {
        return Util.nodeStream((InternalTransaction) tx, ids).map(NodeResult::new);
    }

    @Procedure(name = "apoc.nodes.delete", mode = Mode.WRITE)
    @Description("Deletes all `NODE` values with the given ids.")
    public Stream delete(@Name("nodes") Object ids, @Name("batchSize") long batchSize) {
        Iterator it = Util.nodeStream((InternalTransaction) 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 `RELATIONSHIP` values with the given ids.")
    public Stream rels(@Name("rels") Object ids) {
        return Util.relsStream((InternalTransaction) tx, ids).map(RelationshipResult::new);
    }

    @UserFunction("apoc.node.relationship.exists")
    @Description(
            "Returns a `BOOLEAN` based on whether the given `NODE` has a connecting `RELATIONSHIP` (or whether the given `NODE` has a connecting `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 = ((InternalTransaction) tx).elementIdMapper().nodeId(node.getElementId());
        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 -> org.neo4j.internal.kernel.api.helpers.Nodes.countIncoming(
                                    nodeCursor, typeId);
                            case OUTGOING -> org.neo4j.internal.kernel.api.helpers.Nodes.countOutgoing(
                                    nodeCursor, typeId);
                            case BOTH -> org.neo4j.internal.kernel.api.helpers.Nodes.countAll(nodeCursor, typeId);
                        };
                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 = ((InternalTransaction) tx).elementIdMapper().nodeId(start.getElementId());
        long endId = ((InternalTransaction) tx).elementIdMapper().nodeId(end.getElementId());
        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.");
            }

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

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

    @Procedure("apoc.nodes.collapse")
    @Description(
            "Merges `NODE` values together in the given `LIST`.\n"
                    + "The `NODE` values are then combined to become one `NODE`, with all labels of the previous `NODE` values attached to it, and all `RELATIONSHIP` values 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 (Objects.equals(startNode.getElementId(), node.getElementId())) {
                    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 j : array) {
            if (j == element) {
                return true;
            }
        }
        return false;
    }

    /**
     * @param ops
     * @param pairs
     * @return a int[][] where the first index is 0 for outgoing, 1 for incoming. second array contains rel type ids
     */
    private int[][] typedDirections(TokenRead ops, List> pairs) {
        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);
        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