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

apoc.path.PathExplorer Maven / Gradle / Ivy

The 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.path;

import static apoc.path.PathExplorer.NodeFilter.*;

import apoc.algo.Cover;
import apoc.result.GraphResult;
import apoc.result.NodeResult;
import apoc.result.PathResult;
import apoc.util.Util;
import apoc.util.collection.Iterables;
import java.util.*;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import java.util.stream.StreamSupport;
import org.neo4j.graphdb.Node;
import org.neo4j.graphdb.Path;
import org.neo4j.graphdb.Relationship;
import org.neo4j.graphdb.Transaction;
import org.neo4j.graphdb.traversal.*;
import org.neo4j.kernel.impl.coreapi.InternalTransaction;
import org.neo4j.procedure.Context;
import org.neo4j.procedure.Description;
import org.neo4j.procedure.Name;
import org.neo4j.procedure.NotThreadSafe;
import org.neo4j.procedure.Procedure;

public class PathExplorer {
    public static final Uniqueness UNIQUENESS = Uniqueness.RELATIONSHIP_PATH;
    public static final boolean BFS = true;

    @Context
    public Transaction tx;

    @NotThreadSafe
    @Procedure("apoc.path.expand")
    @Description(
            "Returns `PATH` values expanded from the start `NODE` following the given `RELATIONSHIP` types from min-depth to max-depth.")
    public Stream explorePath(
            @Name("startNode") Object start,
            @Name("relFilter") String pathFilter,
            @Name("labelFilter") String labelFilter,
            @Name("minDepth") long minLevel,
            @Name("maxDepth") long maxLevel) {
        List nodes = Util.nodeList((InternalTransaction) tx, start);
        return explorePathPrivate(
                        nodes,
                        pathFilter,
                        labelFilter,
                        minLevel,
                        maxLevel,
                        BFS,
                        UNIQUENESS,
                        false,
                        -1,
                        null,
                        null,
                        true)
                .map(PathResult::new);
    }

    @NotThreadSafe
    @Procedure("apoc.path.expandConfig")
    @Description(
            "Returns `PATH` values expanded from the start `NODE` with the given `RELATIONSHIP` types from min-depth to max-depth.")
    public Stream expandConfig(
            @Name("startNode") Object start, @Name("config") Map config) {
        return expandConfigPrivate(start, config).map(PathResult::new);
    }

    @NotThreadSafe
    @Procedure("apoc.path.subgraphNodes")
    @Description(
            "Returns the `NODE` values in the sub-graph reachable from the start `NODE` following the given `RELATIONSHIP` types to max-depth.")
    public Stream subgraphNodes(
            @Name("startNode") Object start, @Name("config") Map config) {
        Map configMap = new HashMap<>(config);
        configMap.put("uniqueness", "NODE_GLOBAL");

        if (config.containsKey("minLevel")
                && !config.get("minLevel").equals(0L)
                && !config.get("minLevel").equals(1L)) {
            throw new IllegalArgumentException("minLevel can only be 0 or 1 in subgraphNodes()");
        }

        return expandConfigPrivate(start, configMap)
                .map(path -> path == null ? new NodeResult(null) : new NodeResult(path.endNode()));
    }

    @NotThreadSafe
    @Procedure("apoc.path.subgraphAll")
    @Description(
            "Returns the sub-graph reachable from the start `NODE` following the given `RELATIONSHIP` types to max-depth.")
    public Stream subgraphAll(
            @Name("startNode") Object start, @Name("config") Map config) {
        Map configMap = new HashMap<>(config);
        configMap.remove("optional"); // not needed, will return empty collections anyway if no results
        configMap.put("uniqueness", "NODE_GLOBAL");

        if (config.containsKey("minLevel")
                && !config.get("minLevel").equals(0L)
                && !config.get("minLevel").equals(1L)) {
            throw new IllegalArgumentException("minLevel can only be 0 or 1 in subgraphAll()");
        }

        List subgraphNodes =
                expandConfigPrivate(start, configMap).map(Path::endNode).collect(Collectors.toList());
        List subgraphRels = Cover.coverNodes(subgraphNodes).collect(Collectors.toList());

        return Stream.of(new GraphResult(subgraphNodes, subgraphRels));
    }

    @NotThreadSafe
    @Procedure("apoc.path.spanningTree")
    @Description(
            "Returns spanning tree `PATH` values expanded from the start `NODE` following the given `RELATIONSHIP` types to max-depth.")
    public Stream spanningTree(
            @Name("startNode") Object start, @Name("config") Map config) {
        Map configMap = new HashMap<>(config);
        configMap.put("uniqueness", "NODE_GLOBAL");

        if (config.containsKey("minLevel")
                && !config.get("minLevel").equals(0L)
                && !config.get("minLevel").equals(1L)) {
            throw new IllegalArgumentException("minLevel can only be 0 or 1 in spanningTree()");
        }

        return expandConfigPrivate(start, configMap).map(PathResult::new);
    }

    private Uniqueness getUniqueness(String uniqueness) {
        for (Uniqueness u : Uniqueness.values()) {
            if (u.name().equalsIgnoreCase(uniqueness)) return u;
        }
        return UNIQUENESS;
    }

    private Stream expandConfigPrivate(@Name("start") Object start, @Name("config") Map config) {
        List nodes = Util.nodeList((InternalTransaction) tx, start);

        String uniqueness = (String) config.getOrDefault("uniqueness", UNIQUENESS.name());
        String relationshipFilter = (String) config.getOrDefault("relationshipFilter", null);
        String labelFilter = (String) config.getOrDefault("labelFilter", null);
        long minLevel = Util.toLong(config.getOrDefault("minLevel", "-1"));
        long maxLevel = Util.toLong(config.getOrDefault("maxLevel", "-1"));
        boolean bfs = Util.toBoolean(config.getOrDefault("bfs", true));
        boolean filterStartNode = Util.toBoolean(config.getOrDefault("filterStartNode", false));
        long limit = Util.toLong(config.getOrDefault("limit", "-1"));
        boolean optional = Util.toBoolean(config.getOrDefault("optional", false));
        String sequence = (String) config.getOrDefault("sequence", null);
        boolean beginSequenceAtStart = Util.toBoolean(config.getOrDefault("beginSequenceAtStart", true));

        List endNodes = Util.nodeList((InternalTransaction) tx, config.get("endNodes"));
        List terminatorNodes = Util.nodeList((InternalTransaction) tx, config.get("terminatorNodes"));
        List whitelistNodes =
                Util.nodeList((InternalTransaction) tx, config.get("whitelistNodes")); // DEPRECATED REMOVE 6.0
        List blacklistNodes =
                Util.nodeList((InternalTransaction) tx, config.get("blacklistNodes")); // DEPRECATED REMOVE 6.0
        List allowlistNodes = Util.nodeList((InternalTransaction) tx, config.get("allowlistNodes"));
        List denylistNodes = Util.nodeList((InternalTransaction) tx, config.get("denylistNodes"));
        EnumMap> nodeFilter = new EnumMap<>(NodeFilter.class);

        if (endNodes != null && !endNodes.isEmpty()) {
            nodeFilter.put(END_NODES, endNodes);
        }

        if (terminatorNodes != null && !terminatorNodes.isEmpty()) {
            nodeFilter.put(TERMINATOR_NODES, terminatorNodes);
        }

        // If allowlist/denylist is specified use that (and only that)
        // Else check for the *deprecated* config items
        if (allowlistNodes != null && !allowlistNodes.isEmpty()) {
            nodeFilter.put(ALLOWLIST_NODES, allowlistNodes);
        } else if (whitelistNodes != null && !whitelistNodes.isEmpty()) {
            nodeFilter.put(ALLOWLIST_NODES, whitelistNodes);
        }

        if (denylistNodes != null && !denylistNodes.isEmpty()) {
            nodeFilter.put(DENYLIST_NODES, denylistNodes);
        } else if (blacklistNodes != null && !blacklistNodes.isEmpty()) {
            nodeFilter.put(DENYLIST_NODES, blacklistNodes);
        }

        Stream results = explorePathPrivate(
                nodes,
                relationshipFilter,
                labelFilter,
                minLevel,
                maxLevel,
                bfs,
                getUniqueness(uniqueness),
                filterStartNode,
                limit,
                nodeFilter,
                sequence,
                beginSequenceAtStart);

        if (optional) {
            return optionalStream(results);
        } else {
            return results;
        }
    }

    private Stream explorePathPrivate(
            Iterable startNodes,
            String pathFilter,
            String labelFilter,
            long minLevel,
            long maxLevel,
            boolean bfs,
            Uniqueness uniqueness,
            boolean filterStartNode,
            long limit,
            EnumMap> nodeFilter,
            String sequence,
            boolean beginSequenceAtStart) {

        Traverser traverser = traverse(
                tx.traversalDescription(),
                startNodes,
                pathFilter,
                labelFilter,
                minLevel,
                maxLevel,
                uniqueness,
                bfs,
                filterStartNode,
                nodeFilter,
                sequence,
                beginSequenceAtStart);

        if (limit == -1) {
            return Iterables.stream(traverser);
        } else {
            return Iterables.stream(traverser).limit(limit);
        }
    }

    /**
     * If the stream is empty, returns a stream of a single null value, otherwise returns the equivalent of the input stream
     * @param stream the input stream
     * @return a stream of a single null value if the input stream is empty, otherwise returns the equivalent of the input stream
     */
    private Stream optionalStream(Stream stream) {
        Stream optionalStream;
        Iterator itr = stream.iterator();
        if (itr.hasNext()) {
            optionalStream = StreamSupport.stream(Spliterators.spliteratorUnknownSize(itr, 0), false);
        } else {
            List listOfNull = new ArrayList<>();
            listOfNull.add(null);
            optionalStream = listOfNull.stream();
        }

        return optionalStream;
    }

    public static Traverser traverse(
            TraversalDescription td,
            Iterable startNodes,
            String pathFilter,
            String labelFilter,
            long minLevel,
            long maxLevel,
            Uniqueness uniqueness,
            boolean bfs,
            boolean filterStartNode,
            EnumMap> nodeFilter,
            String sequence,
            boolean beginSequenceAtStart) {
        // based on the pathFilter definition now the possible relationships and directions must be shown

        td = bfs ? td.breadthFirst() : td.depthFirst();

        // if `sequence` is present, it overrides `labelFilter` and `relationshipFilter`
        if (sequence != null && !sequence.trim().isEmpty()) {
            String[] sequenceSteps = sequence.split(",");
            List labelSequenceList = new ArrayList<>();
            List relSequenceList = new ArrayList<>();

            for (int index = 0; index < sequenceSteps.length; index++) {
                List seq =
                        (beginSequenceAtStart ? index : index - 1) % 2 == 0 ? labelSequenceList : relSequenceList;
                seq.add(sequenceSteps[index]);
            }

            td = td.expand(new RelationshipSequenceExpander(relSequenceList, beginSequenceAtStart));
            td = td.evaluator(new LabelSequenceEvaluator(
                    labelSequenceList, filterStartNode, beginSequenceAtStart, (int) minLevel));
        } else {
            if (pathFilter != null && !pathFilter.trim().isEmpty()) {
                td = td.expand(new RelationshipSequenceExpander(pathFilter.trim(), beginSequenceAtStart));
            }

            if (labelFilter != null && sequence == null && !labelFilter.trim().isEmpty()) {
                td = td.evaluator(new LabelSequenceEvaluator(
                        labelFilter.trim(), filterStartNode, beginSequenceAtStart, (int) minLevel));
            }
        }

        if (minLevel != -1) td = td.evaluator(Evaluators.fromDepth((int) minLevel));
        if (maxLevel != -1) td = td.evaluator(Evaluators.toDepth((int) maxLevel));

        if (nodeFilter != null && !nodeFilter.isEmpty()) {
            List endNodes = nodeFilter.getOrDefault(END_NODES, Collections.EMPTY_LIST);
            List terminatorNodes = nodeFilter.getOrDefault(TERMINATOR_NODES, Collections.EMPTY_LIST);
            List denylistNodes = nodeFilter.getOrDefault(DENYLIST_NODES, Collections.EMPTY_LIST);
            List allowlistNodes;

            if (nodeFilter.containsKey(ALLOWLIST_NODES)) {
                // need to add to new list since we may need to add to it later
                // encounter "can't add to abstractList" error if we don't do this
                allowlistNodes = new ArrayList<>(nodeFilter.get(ALLOWLIST_NODES));
            } else {
                allowlistNodes = Collections.EMPTY_LIST;
            }

            if (!denylistNodes.isEmpty()) {
                td = td.evaluator(NodeEvaluators.denylistNodeEvaluator(filterStartNode, denylistNodes));
            }

            Evaluator endAndTerminatorNodeEvaluator = NodeEvaluators.endAndTerminatorNodeEvaluator(
                    filterStartNode, (int) minLevel, endNodes, terminatorNodes);
            if (endAndTerminatorNodeEvaluator != null) {
                td = td.evaluator(endAndTerminatorNodeEvaluator);
            }

            if (!allowlistNodes.isEmpty()) {
                // ensure endNodes and terminatorNodes are allowlisted
                allowlistNodes.addAll(endNodes);
                allowlistNodes.addAll(terminatorNodes);
                td = td.evaluator(NodeEvaluators.allowlistNodeEvaluator(filterStartNode, allowlistNodes));
            }
        }

        td = td.uniqueness(uniqueness); // this is how Cypher works !! Uniqueness.RELATIONSHIP_PATH
        // uniqueness should be set as last on the TraversalDescription
        return td.traverse(startNodes);
    }

    // keys to node filter map
    enum NodeFilter {
        ALLOWLIST_NODES,
        DENYLIST_NODES,
        END_NODES,
        TERMINATOR_NODES
    }
}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy