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

com.steelbridgelabs.oss.neo4j.structure.Neo4JVertex Maven / Gradle / Ivy

There is a newer version: 0.4.6
Show newest version
/*
 *  Copyright 2016 SteelBridge Laboratories, LLC.
 *
 *  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.
 *
 *  For more information: http://steelbridgelabs.com
 */

package com.steelbridgelabs.oss.neo4j.structure;

import com.steelbridgelabs.oss.neo4j.structure.summary.ResultSummaryLogger;
import org.apache.tinkerpop.gremlin.structure.Direction;
import org.apache.tinkerpop.gremlin.structure.Edge;
import org.apache.tinkerpop.gremlin.structure.Element;
import org.apache.tinkerpop.gremlin.structure.Graph;
import org.apache.tinkerpop.gremlin.structure.Property;
import org.apache.tinkerpop.gremlin.structure.Vertex;
import org.apache.tinkerpop.gremlin.structure.VertexProperty;
import org.apache.tinkerpop.gremlin.structure.util.ElementHelper;
import org.apache.tinkerpop.gremlin.structure.util.StringFactory;
import org.neo4j.driver.Record;
import org.neo4j.driver.Result;
import org.neo4j.driver.Value;
import org.neo4j.driver.internal.types.TypeRepresentation;
import org.neo4j.driver.types.Node;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.NoSuchElementException;
import java.util.Objects;
import java.util.Set;
import java.util.SortedSet;
import java.util.TreeSet;
import java.util.concurrent.atomic.AtomicLong;
import java.util.stream.Collector;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import java.util.stream.StreamSupport;

/**
 * @author Rogelio J. Baucells
 */
public class Neo4JVertex extends Neo4JElement implements Vertex {

    private static class Neo4JVertexProperty implements VertexProperty {

        private final Neo4JVertex vertex;
        private final Object id;
        private final String name;
        private final T value;

        public Neo4JVertexProperty(Neo4JVertex vertex, Object id, String name, T value) {
            Objects.requireNonNull(vertex, "vertex cannot be null");
            Objects.requireNonNull(id, "id cannot be null");
            Objects.requireNonNull(name, "name cannot be null");
            Objects.requireNonNull(value, "value cannot be null");
            // store fields
            this.vertex = vertex;
            this.id = id;
            this.name = name;
            this.value = value;
        }

        @Override
        public Vertex element() {
            return vertex;
        }

        @Override
        public  Iterator> properties(String... propertyKeys) {
            throw VertexProperty.Exceptions.metaPropertiesNotSupported();
        }

        @Override
        public Object id() {
            return id;
        }

        @Override
        public  Property property(String key, V value) {
            throw VertexProperty.Exceptions.metaPropertiesNotSupported();
        }

        @Override
        public String key() {
            return name;
        }

        @Override
        public T value() throws NoSuchElementException {
            return value;
        }

        @Override
        public boolean isPresent() {
            return true;
        }

        @Override
        public void remove() {
            // check cardinality
            Cardinality cardinality = vertex.cardinalities.get(name);
            if (cardinality != null) {
                // check it is single value
                if (cardinality != Cardinality.single) {
                    // get list of properties in vertex
                    Collection vertexProperties = vertex.properties.get(name);
                    if (vertexProperties != null) {
                        // remove this instance from list
                        vertexProperties.remove(this);
                        // check properties are empty, remove key from vertex properties
                        if (vertexProperties.isEmpty()) {
                            // remove property
                            vertex.properties.remove(name);
                            // remove cardinality
                            vertex.cardinalities.remove(name);
                            // mark property as removed
                            vertex.removedProperties.add(name);
                            // mark vertex as dirty
                            vertex.dirty = true;
                            // notify session
                            vertex.session.dirtyVertex(vertex);
                        }
                    }
                }
                else {
                    // remove property
                    vertex.properties.remove(name);
                    // remove cardinality
                    vertex.cardinalities.remove(name);
                    // mark property as removed
                    vertex.removedProperties.add(name);
                    // mark vertex as dirty
                    vertex.dirty = true;
                    // notify session
                    vertex.session.dirtyVertex(vertex);
                }
            }
        }

        @Override
        public boolean equals(final Object object) {
            return object instanceof VertexProperty && ElementHelper.areEqual(this, object);
        }

        @Override
        public int hashCode() {
            return ElementHelper.hashCode((Element)this);
        }

        @Override
        public String toString() {
            return StringFactory.propertyString(this);
        }
    }

    public static final String LabelDelimiter = "::";

    private static final AtomicLong propertyIdProvider = new AtomicLong(0L);

    private final Object id;
    private final Neo4JGraph graph;
    private final Neo4JReadPartition partition;
    private final Neo4JSession session;
    private final Neo4JElementIdProvider vertexIdProvider;
    private final Neo4JElementIdProvider edgeIdProvider;
    private final Map> properties = new HashMap<>();
    private final Map cardinalities = new HashMap<>();
    private final Set outEdges = new HashSet<>();
    private final Set inEdges = new HashSet<>();
    private final Set outEdgeLabels = new HashSet<>();
    private final Set inEdgeLabels = new HashSet<>();
    private final SortedSet labelsAdded = new TreeSet<>();
    private final SortedSet labelsRemoved = new TreeSet<>();
    private final SortedSet labels;
    private final Set additionalLabels;

    private Object generatedId = null;
    private boolean outEdgesLoaded = false;
    private boolean inEdgesLoaded = false;
    private boolean dirty = false;
    private SortedSet matchLabels;
    private SortedSet originalLabels;
    private Set graphLabels;
    private Set removedProperties = new HashSet<>();
    private Map> originalProperties;
    private Map originalCardinalities;

    Neo4JVertex(Neo4JGraph graph, Neo4JSession session, Neo4JElementIdProvider vertexIdProvider, Neo4JElementIdProvider edgeIdProvider, Collection labels) {
        Objects.requireNonNull(graph, "graph cannot be null");
        Objects.requireNonNull(session, "session cannot be null");
        Objects.requireNonNull(vertexIdProvider, "vertexIdProvider cannot be null");
        Objects.requireNonNull(edgeIdProvider, "edgeIdProvider cannot be null");
        Objects.requireNonNull(labels, "labels cannot be null");
        // store fields
        this.graph = graph;
        this.partition = graph.getPartition();
        this.additionalLabels = graph.vertexLabels();
        this.session = session;
        this.vertexIdProvider = vertexIdProvider;
        this.edgeIdProvider = edgeIdProvider;
        this.labels = new TreeSet<>(labels);
        // this is the original set of labels
        this.originalLabels = Collections.emptySortedSet();
        // labels used to match vertex in database
        this.matchLabels = Collections.emptySortedSet();
        // graph labels
        this.graphLabels = additionalLabels;
        // initialize original properties and cardinalities
        this.originalProperties = new HashMap<>();
        this.originalCardinalities = new HashMap<>();
        // generate id
        this.id = vertexIdProvider.generate();
        // this is a new vertex, everything is in memory
        outEdgesLoaded = true;
        inEdgesLoaded = true;
    }

    Neo4JVertex(Neo4JGraph graph, Neo4JSession session, Neo4JElementIdProvider vertexIdProvider, Neo4JElementIdProvider edgeIdProvider, Node node) {
        Objects.requireNonNull(graph, "graph cannot be null");
        Objects.requireNonNull(session, "session cannot be null");
        Objects.requireNonNull(vertexIdProvider, "vertexIdProvider cannot be null");
        Objects.requireNonNull(edgeIdProvider, "edgeIdProvider cannot be null");
        Objects.requireNonNull(node, "node cannot be null");
        // store fields
        this.graph = graph;
        this.partition = graph.getPartition();
        this.additionalLabels = graph.vertexLabels();
        this.session = session;
        this.vertexIdProvider = vertexIdProvider;
        this.edgeIdProvider = edgeIdProvider;
        // from node
        this.id = vertexIdProvider.get(node);
        // graph labels (additional & partition labels in original node)
        this.graphLabels = StreamSupport.stream(node.labels().spliterator(), false).filter(label -> additionalLabels.contains(label) && !partition.validateLabel(label)).collect(Collectors.toSet());
        // labels, do not store additional && partition labels
        this.labels = StreamSupport.stream(node.labels().spliterator(), false).filter(label -> !graphLabels.contains(label)).collect(Collectors.toCollection(TreeSet::new));
        // this is the original set of labels
        this.originalLabels = new TreeSet<>(this.labels);
        // labels used to match the vertex in the database
        this.matchLabels = StreamSupport.stream(node.labels().spliterator(), false).collect(Collectors.toCollection(TreeSet::new));
        // id field name (if any)
        String idFieldName = vertexIdProvider.fieldName();
        // copy properties from node, exclude identifier
        StreamSupport.stream(node.keys().spliterator(), false).filter(key -> !key.equals(idFieldName)).forEach(key -> {
            // value
            Value value = node.get(key);
            TypeRepresentation type = (TypeRepresentation)value.type();
            // process value type
            switch (type.constructor()) {
                case LIST:
                    // process values
                    properties.put(key, value.asList().stream().map(item -> new Neo4JVertexProperty<>(this, propertyIdProvider.incrementAndGet(), key, item)).collect(Collectors.toList()));
                    // cardinality
                    cardinalities.put(key, VertexProperty.Cardinality.list);
                    break;
                case MAP:
                    throw new RuntimeException("TODO: implement maps");
                default:
                    // add property
                    properties.put(key, Collections.singletonList(new Neo4JVertexProperty<>(this, propertyIdProvider.incrementAndGet(), key, value.asObject())));
                    // cardinality
                    cardinalities.put(key, VertexProperty.Cardinality.single);
                    break;
            }
        });
        // initialize original properties and cardinalities
        this.originalProperties = new HashMap<>(properties);
        this.originalCardinalities = new HashMap<>(cardinalities);
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public Object id() {
        return id != null ? id : generatedId;
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public String label() {
        // labels separated by "::"
        return String.join(LabelDelimiter, labels);
    }

    public String[] labels() {
        return labels.toArray(new String[0]);
    }

    public boolean addLabel(String label) {
        Objects.requireNonNull(label, "label cannot be null");
        // exclude partition
        if (!partition.validateLabel(label))
            throw new IllegalArgumentException("Invalid label, label name cannot be the same as Graph partition labels");
        // add label to set
        if (labels.add(label)) {
            // notify session
            session.dirtyVertex(this);
            // we need to update labels
            labelsAdded.add(label);
            // indicate label was added
            return true;
        }
        return false;
    }

    public boolean removeLabel(String label) {
        Objects.requireNonNull(label, "label cannot be null");
        // exclude partition
        if (!partition.validateLabel(label))
            throw new IllegalArgumentException("Invalid label, label name cannot be removed since it is part of the Graph partition");
        // prevent additional labels from being removed
        if (additionalLabels.contains(label))
            throw new IllegalArgumentException("Invalid label, label name cannot be removed since it is part of additional labels for vertices");
        // remove label from set
        if (labels.remove(label)) {
            // check this label was previously added in this session
            if (!labelsAdded.remove(label)) {
                // notify session
                session.dirtyVertex(this);
                // we need to update labels
                labelsRemoved.add(label);
            }
            // indicate label was removed
            return true;
        }
        return false;
    }

    /**
     * Generates a Cypher MATCH pattern for the vertex, example:
     * 

* (alias:Label1:Label2) *

* * @param alias The node alias, null if not required. * @return the Cypher MATCH clause. */ public String matchPattern(String alias) { // generate match pattern if (alias != null) return "(" + alias + processLabels(matchLabels, false) + ")"; // pattern without alias return "(" + processLabels(matchLabels, false) + ")"; } /** * Generates a Cypher MATCH predicate for the vertex, example: *

* alias.id = $id AND (alias:Label1 OR alias:Label2) *

* * @param alias The node alias. * @param idParameterName The name of the parameter that contains the vertex id. * @return the Cypher MATCH predicate or null if not required to MATCH the vertex. */ public String matchPredicate(String alias, String idParameterName) { Objects.requireNonNull(alias, "alias cannot be null"); Objects.requireNonNull(idParameterName, "idParameterName cannot be null"); // get partition Neo4JReadPartition partition = graph.getPartition(); // create match predicate return vertexIdProvider.matchPredicateOperand(alias) + " = $" + idParameterName + (partition.usesMatchPredicate() ? " AND (" + partition.vertexMatchPredicate(alias) + ")" : ""); } /** * Generates a Cypher MATCH statement for the vertex, example: *

* MATCH (alias) WHERE alias.id = $id AND (alias:Label1 OR alias:Label2) *

* * @param alias The node alias. * @param idParameterName The name of the parameter that contains the vertex id. * @return the Cypher MATCH predicate or null if not required to MATCH the vertex. */ public String matchStatement(String alias, String idParameterName) { Objects.requireNonNull(alias, "alias cannot be null"); Objects.requireNonNull(idParameterName, "idParameterName cannot be null"); // create statement return "MATCH " + matchPattern(alias) + " WHERE " + matchPredicate(alias, idParameterName); } @Override public boolean isDirty() { return dirty || !labelsAdded.isEmpty() || !labelsRemoved.isEmpty(); } @Override public boolean isTransient() { return originalLabels.isEmpty(); } /** * {@inheritDoc} */ @Override public Edge addEdge(String label, Vertex vertex, Object... keyValues) { // validate label ElementHelper.validateLabel(label); // vertex must exist if (vertex == null) throw Graph.Exceptions.argumentCanNotBeNull("vertex"); // validate properties ElementHelper.legalPropertyKeyValueArray(keyValues); // transaction should be ready for io operations graph.tx().readWrite(); // add edge return session.addEdge(label, this, (Neo4JVertex)vertex, keyValues); } void removeEdge(Neo4JEdge edge) { // remove edge from internal references outEdges.remove(edge); inEdges.remove(edge); } private void processEdgesWhereClause(String vertexAlias, List identifiers, String alias, StringBuilder builder, Map parameters) { // generate match predicate String predicate = partition.vertexMatchPredicate(vertexAlias); // check identifiers are empty if (!identifiers.isEmpty()) { // filter edges builder.append(" AND NOT ").append(edgeIdProvider.matchPredicateOperand(alias)).append(" IN $ids"); // ids parameters parameters.put("ids", identifiers); // check we need to add in predicate if (predicate != null) { // append predicate builder.append(" AND ").append(predicate); } } else if (predicate != null) { // append WHERE builder.append(" AND ").append(predicate); } } /** * {@inheritDoc} */ @Override public Iterator edges(Direction direction, String... labels) { Objects.requireNonNull(direction, "direction cannot be null"); Objects.requireNonNull(labels, "labels cannot be null"); // transaction should be ready for io operations graph.tx().readWrite(); // load labels in hash set (remove duplicates) Set set = new HashSet<>(Arrays.asList(labels)); // parameters Map parameters = new HashMap<>(); // vertex id parameters.put("id", id()); // out edges if (direction == Direction.OUT) { // check we have all edges in memory if (!outEdgesLoaded) { // labels we need to query for Set relationshipLabels = set.stream().filter(item -> !outEdgeLabels.contains(item)).collect(Collectors.toSet()); // check query is required for labels if (set.isEmpty() || !relationshipLabels.isEmpty()) { // create string builder StringBuilder builder = new StringBuilder(); // match clause builder.append("MATCH ").append(matchPattern("n")).append("-[r").append(relationshipLabels.stream().map(label -> ":`" + label + "`").collect(Collectors.joining("|"))).append("]->(m").append(processLabels(Collections.emptySet(), true)).append(")").append(" WHERE ").append(vertexIdProvider.matchPredicateOperand("n")).append(" = $id"); // edge ids already in memory List identifiers = outEdges.stream().map(Neo4JEdge::id).filter(Objects::nonNull).collect(Collectors.toList()); // process where clause processEdgesWhereClause("m", identifiers, "r", builder, parameters); // return builder.append(" RETURN n, r, m"); // execute statement Result result = session.executeStatement(builder.toString(), parameters); // execute command Stream query = session.edges(result); // edges in memory plus the ones in database (return copy since edges can be deleted in the middle of the loop) Iterator iterator = Stream.concat((labels.length != 0 ? outEdges.stream().filter(edge -> set.contains(edge.label())) : outEdges.stream()).map(edge -> (Edge)edge), query) .collect(Collectors.toList()) .iterator(); // process summary (query has been already consumed by combine) ResultSummaryLogger.log(result.consume()); // after this line it is safe to update loaded flag and labels in memory outEdgesLoaded = labels.length == 0; outEdgeLabels.addAll(set); // return iterator return iterator; } } // edges in memory (return copy since edges can be deleted in the middle of the loop) return outEdges.stream().filter(edge -> labels.length == 0 || set.contains(edge.label())) .map(edge -> (Edge)edge) .collect(Collectors.toList()) .iterator(); } // in edges if (direction == Direction.IN) { // check we have all edges in memory if (!inEdgesLoaded) { // labels we need to query for Set relationshipLabels = set.stream().filter(item -> !inEdgeLabels.contains(item)).collect(Collectors.toSet()); // check query is required for labels if (set.isEmpty() || !relationshipLabels.isEmpty()) { // create string builder StringBuilder builder = new StringBuilder(); // match clause builder.append("MATCH ").append(matchPattern("n")).append("<-[r").append(relationshipLabels.stream().map(label -> ":`" + label + "`").collect(Collectors.joining("|"))).append("]-(m").append(processLabels(Collections.emptySet(), true)).append(")").append(" WHERE ").append(vertexIdProvider.matchPredicateOperand("n")).append(" = $id"); // edge ids already in memory List identifiers = inEdges.stream().map(Neo4JEdge::id).filter(Objects::nonNull).collect(Collectors.toList()); // process where clause processEdgesWhereClause("m", identifiers, "r", builder, parameters); // return builder.append(" RETURN n, r, m"); // execute statement Result result = session.executeStatement(builder.toString(), parameters); // execute command Stream query = session.edges(result); // edges in memory plus the ones in database (return copy since edges can be deleted in the middle of the loop) Iterator iterator = Stream.concat((labels.length != 0 ? inEdges.stream().filter(edge -> set.contains(edge.label())) : inEdges.stream()).map(edge -> (Edge)edge), query) .collect(Collectors.toList()) .iterator(); // process summary (query has been already consumed by combine) ResultSummaryLogger.log(result.consume()); // after this line it is safe to update loaded flag and labels in memory inEdgesLoaded = labels.length == 0; inEdgeLabels.addAll(set); // return iterator return iterator; } } // edges in memory (return copy since edges can be deleted in the middle of the loop) return inEdges.stream().filter(edge -> labels.length == 0 || set.contains(edge.label())) .map(edge -> (Edge)edge) .collect(Collectors.toList()) .iterator(); } // check we have all edges in memory if (!outEdgesLoaded || !inEdgesLoaded) { // check we have labels already in memory if (set.isEmpty() || !outEdgeLabels.containsAll(set) || !inEdgeLabels.containsAll(set)) { // create string builder StringBuilder builder = new StringBuilder(); // match clause builder.append("MATCH ").append(matchPattern("n")).append("-[r").append(set.stream().map(label -> ":`" + label + "`").collect(Collectors.joining("|"))).append("]-(m").append(processLabels(Collections.emptySet(), true)).append(")").append(" WHERE ").append(vertexIdProvider.matchPredicateOperand("n")).append(" = $id"); // edge ids already in memory List identifiers = Stream.concat(outEdges.stream(), inEdges.stream()).map(Neo4JEdge::id).filter(Objects::nonNull).collect(Collectors.toList()); // process where clause processEdgesWhereClause("m", identifiers, "r", builder, parameters); // return builder.append(" RETURN n, r, m"); // execute statement Result result = session.executeStatement(builder.toString(), parameters); // execute command Stream query = session.edges(result); // edges in memory plus the ones in database (return copy since edges can be deleted in the middle of the loop) Iterator iterator = Stream.concat(Stream.concat(labels.length != 0 ? outEdges.stream().filter(edge -> set.contains(edge.label())) : outEdges.stream(), labels.length != 0 ? inEdges.stream().filter(edge -> set.contains(edge.label())) : inEdges.stream()).map(edge -> (Edge)edge), query) .collect(Collectors.toList()) .iterator(); // process summary (query has been already consumed by combine) ResultSummaryLogger.log(result.consume()); // after this line it is safe to update loaded flags outEdgesLoaded = outEdgesLoaded || labels.length == 0; inEdgesLoaded = inEdgesLoaded || labels.length == 0; // update labels in memory outEdgeLabels.addAll(set); inEdgeLabels.addAll(set); // return iterator return iterator; } } // edges in memory (return copy since edges can be deleted in the middle of the loop) return Stream.concat(labels.length != 0 ? inEdges.stream().filter(edge -> set.contains(edge.label())) : inEdges.stream(), labels.length != 0 ? outEdges.stream().filter(edge -> set.contains(edge.label())) : outEdges.stream()) .map(edge -> (Edge)edge) .collect(Collectors.toList()) .iterator(); } /** * {@inheritDoc} */ @Override public Iterator vertices(Direction direction, String... labels) { Objects.requireNonNull(direction, "direction cannot be null"); Objects.requireNonNull(labels, "labels cannot be null"); // transaction should be ready for io operations graph.tx().readWrite(); // load labels in hash set (remove duplicates) Set set = new HashSet<>(Arrays.asList(labels)); // parameters Map parameters = new HashMap<>(); // vertex id parameters.put("id", id()); // out edges if (direction == Direction.OUT) { // check we have all edges in memory if (!outEdgesLoaded) { // labels we need to query for Set relationshipLabels = set.stream().filter(item -> !outEdgeLabels.contains(item)).collect(Collectors.toSet()); // check query is required for labels if (set.isEmpty() || !relationshipLabels.isEmpty()) { // create string builder StringBuilder builder = new StringBuilder(); // match clause builder.append("MATCH ").append(matchPattern("n")).append("-[r").append(relationshipLabels.stream().map(label -> ":`" + label + "`").collect(Collectors.joining("|"))).append("]->(m").append(processLabels(Collections.emptySet(), true)).append(")").append(" WHERE ").append(vertexIdProvider.matchPredicateOperand("n")).append(" = $id"); // edge ids already in memory List identifiers = outEdges.stream().map(Neo4JEdge::id).filter(Objects::nonNull).collect(Collectors.toList()); // process where clause processEdgesWhereClause("m", identifiers, "r", builder, parameters); // return builder.append(" RETURN m"); // execute statement Result result = session.executeStatement(builder.toString(), parameters); // execute command Stream query = session.vertices(result); // return copy since elements can be deleted in the middle of the loop Iterator iterator = Stream.concat((labels.length != 0 ? outEdges.stream().filter(edge -> set.contains(edge.label())) : outEdges.stream()).map(Edge::inVertex), query) .collect(Collectors.toList()) .iterator(); // process summary (query has been already consumed by collector) ResultSummaryLogger.log(result.consume()); // return iterator return iterator; } } // edges in memory (return copy since elements can be deleted in the middle of the loop) return (labels.length != 0 ? outEdges.stream().filter(edge -> set.contains(edge.label())) : outEdges.stream()).map(Edge::inVertex) .collect(Collectors.toList()) .iterator(); } // in edges if (direction == Direction.IN) { // check we have all edges in memory if (!inEdgesLoaded) { // labels we need to query for Set relationshipLabels = set.stream().filter(item -> !inEdgeLabels.contains(item)).collect(Collectors.toSet()); // check query is required for labels if (set.isEmpty() || !relationshipLabels.isEmpty()) { // create string builder StringBuilder builder = new StringBuilder(); // match clause builder.append("MATCH ").append(matchPattern("n")).append("<-[r").append(relationshipLabels.stream().map(label -> ":`" + label + "`").collect(Collectors.joining("|"))).append("]-(m").append(processLabels(Collections.emptySet(), true)).append(")").append(" WHERE ").append(vertexIdProvider.matchPredicateOperand("n")).append(" = $id"); // edge ids already in memory List identifiers = inEdges.stream().map(Neo4JEdge::id).filter(Objects::nonNull).collect(Collectors.toList()); // process where clause processEdgesWhereClause("m", identifiers, "r", builder, parameters); // return builder.append(" RETURN m"); // execute statement Result result = session.executeStatement(builder.toString(), parameters); // execute command Stream query = session.vertices(result); // return copy since elements can be deleted in the middle of the loop Iterator iterator = Stream.concat((labels.length != 0 ? inEdges.stream().filter(edge -> set.contains(edge.label())) : inEdges.stream()).map(Edge::outVertex), query) .collect(Collectors.toList()) .iterator(); // process summary (query has been already consumed by collector) ResultSummaryLogger.log(result.consume()); // return iterator return iterator; } } // edges in memory (return copy since elements can be deleted in the middle of the loop return (labels.length != 0 ? inEdges.stream().filter(edge -> set.contains(edge.label())) : inEdges.stream()).map(Edge::outVertex) .collect(Collectors.toList()) .iterator(); } // check we have all edges in memory if (!outEdgesLoaded || !inEdgesLoaded) { // check we have labels already in memory if (set.isEmpty() || !outEdgeLabels.containsAll(set) || !inEdgeLabels.containsAll(set)) { // create string builder StringBuilder builder = new StringBuilder(); // match clause builder.append("MATCH ").append(matchPattern("n")).append("-[r").append(set.stream().map(label -> ":`" + label + "`").collect(Collectors.joining("|"))).append("]-(m").append(processLabels(Collections.emptySet(), true)).append(")").append(" WHERE ").append(vertexIdProvider.matchPredicateOperand("n")).append(" = $id"); // edge ids already in memory List identifiers = Stream.concat(outEdges.stream(), inEdges.stream()).map(Neo4JEdge::id).filter(Objects::nonNull).collect(Collectors.toList()); // process where clause processEdgesWhereClause("m", identifiers, "r", builder, parameters); // return builder.append(" RETURN m"); // execute statement Result result = session.executeStatement(builder.toString(), parameters); // execute command Stream query = session.vertices(result); // return copy since elements can be deleted in the middle of the loop Iterator iterator = Stream.concat(Stream.concat((labels.length != 0 ? outEdges.stream().filter(edge -> set.contains(edge.label())) : outEdges.stream()).map(Edge::inVertex), (labels.length != 0 ? inEdges.stream().filter(edge -> set.contains(edge.label())) : inEdges.stream()).map(Edge::outVertex)), query) .collect(Collectors.toList()) .iterator(); // process summary (query has been already consumed by collector) ResultSummaryLogger.log(result.consume()); // return iterator return iterator; } } // edges in memory (return copy since edges can be deleted in the middle of the loop) return Stream.concat((labels.length != 0 ? outEdges.stream().filter(edge -> set.contains(edge.label())) : outEdges.stream()).map(Edge::inVertex), (labels.length != 0 ? inEdges.stream().filter(edge -> set.contains(edge.label())) : inEdges.stream()).map(Edge::outVertex)) .collect(Collectors.toList()) .iterator(); } /** * {@inheritDoc} */ @Override public VertexProperty property(VertexProperty.Cardinality cardinality, String name, V value, Object... keyValues) { ElementHelper.validateProperty(name, value); ElementHelper.legalPropertyKeyValueArray(keyValues); // check key values if (keyValues.length != 0) throw VertexProperty.Exceptions.metaPropertiesNotSupported(); // validate bolt support Neo4JBoltSupport.checkPropertyValue(value); // check cardinality VertexProperty.Cardinality existingCardinality = cardinalities.get(name); if (existingCardinality != null && existingCardinality != cardinality) throw new IllegalArgumentException(String.format(Locale.getDefault(), "Property %s has been defined with %s cardinality", name, existingCardinality)); // transaction should be ready for io operations graph.tx().readWrite(); // vertex property Neo4JVertexProperty property = new Neo4JVertexProperty<>(this, propertyIdProvider.incrementAndGet(), name, value); // check cardinality switch (cardinality) { case list: // get existing list for key Collection list = properties.get(name); if (list == null) { // initialize list list = new ArrayList<>(); // use list properties.put(name, list); // cardinality cardinalities.put(name, VertexProperty.Cardinality.list); } // add value to list, this will always call dirty method in session if (list.add(property)) { // notify session session.dirtyVertex(this); // update flag dirty = true; } break; case set: // get existing set for key Collection set = properties.get(name); if (set == null) { // initialize set set = new HashSet<>(); // use set properties.put(name, set); // cardinality cardinalities.put(name, VertexProperty.Cardinality.set); } // check value does not exist in collection, TODO: optimize this search if (set.stream().noneMatch(item -> item.value().equals(value))) { // add property to set set.add(property); // notify session session.dirtyVertex(this); // update flag dirty = true; } break; default: // use value (single) properties.put(name, Collections.singletonList(property)); // cardinality cardinalities.put(name, VertexProperty.Cardinality.single); // notify session session.dirtyVertex(this); // update flag dirty = true; break; } // return property return property; } /** * {@inheritDoc} */ @Override @SuppressWarnings("unchecked") public VertexProperty property(String key) { Objects.requireNonNull(key, "key cannot be null"); // check we have a property with the given key Collection collection = properties.get(key); if (collection != null) { // check size if (collection.size() == 1) { // iterator Iterator iterator = collection.iterator(); // advance iterator to first element if (iterator.hasNext()) { // first element return (VertexProperty)iterator.next(); } return VertexProperty.empty(); } // exception throw Vertex.Exceptions.multiplePropertiesExistForProvidedKey(key); } return VertexProperty.empty(); } /** * {@inheritDoc} */ @Override @SuppressWarnings("unchecked") public Iterator> properties(String... propertyKeys) { Objects.requireNonNull(propertyKeys, "propertyKeys cannot be null"); // check we have properties with key if (!properties.isEmpty()) { // no properties in filter if (propertyKeys.length == 0) { // all properties (return a copy since properties iterator can be modified by calling remove()) return properties.entrySet().stream() .flatMap(entry -> entry.getValue().stream()) .map(item -> (VertexProperty)item) .collect(Collectors.toList()) .iterator(); } // one property in filter if (propertyKeys.length == 1) { // get list for key Collection list = properties.get(propertyKeys[0]); if (list != null) { // all properties (return a copy since properties iterator can be modified by calling remove()) return list.stream() .map(item -> (VertexProperty)item) .collect(Collectors.toList()) .iterator(); } // nothing on key return Collections.emptyIterator(); } // loop property keys (return a copy since properties iterator can be modified by calling remove()) return Arrays.stream(propertyKeys) .flatMap(key -> ((Collection)properties.getOrDefault(key, Collections.EMPTY_LIST)).stream()) .map(item -> (VertexProperty)item) .collect(Collectors.toList()) .iterator(); } // nothing return Collections.emptyIterator(); } /** * {@inheritDoc} */ @Override public Graph graph() { return graph; } /** * {@inheritDoc} */ @Override public void remove() { // transaction should be ready for io operations graph.tx().readWrite(); // remove all edges outEdges.forEach(edge -> session.removeEdge(edge, false)); // remove vertex on session session.removeVertex(this); } void addInEdge(Neo4JEdge edge) { Objects.requireNonNull(edge, "edge cannot be null"); // add to set inEdges.add(edge); } void addOutEdge(Neo4JEdge edge) { Objects.requireNonNull(edge, "edge cannot be null"); // add to set outEdges.add(edge); } private Map statementParameters() { // define collector Collector>, Map, Map> collector = Collector.of( HashMap::new, (map, entry) -> { // key & value String key = entry.getKey(); Collection list = entry.getValue(); // check cardinality if (cardinalities.get(key) == VertexProperty.Cardinality.single) { // iterator Iterator iterator = list.iterator(); // add single value to map if (iterator.hasNext()) map.put(key, iterator.next().value()); } else { // add list of values map.put(key, list.stream().map(Property::value).collect(Collectors.toList())); } }, (map1, map2) -> map1, (map) -> map ); // process properties Map parameters = properties.entrySet().stream().collect(collector); // removed properties removedProperties.forEach(name -> parameters.put(name, null)); // append id field if required String idFieldName = vertexIdProvider.fieldName(); if (id != null && idFieldName != null) parameters.put(idFieldName, id); // return parameters return parameters; } @Override public Neo4JDatabaseCommand insertCommand() { // concat labels with additional labels on insertion SortedSet labels = Stream.concat(this.labels.stream(), additionalLabels.stream()).collect(Collectors.toCollection(TreeSet::new)); try { // parameters Map parameters = Collections.singletonMap("vp", statementParameters()); // check database side id generation is required if (id == null) { // create statement String statement = "CREATE (n" + processLabels(labels, false) + "$vp) RETURN " + vertexIdProvider.matchPredicateOperand("n"); // command statement return new Neo4JDatabaseCommand(statement, parameters, result -> { // check we received data if (result.hasNext()) { // record Record record = result.next(); // process node identifier generatedId = vertexIdProvider.processIdentifier(record.get(0).asObject()); } }); } // command statement return new Neo4JDatabaseCommand("CREATE (" + processLabels(labels, false) + "$vp)", parameters); } finally { // to find vertex in database (labels + additional labels) matchLabels = labels; } } @Override public Neo4JDatabaseCommand updateCommand() { // check we need to issue statement (adding a label and then removing it will set the vertex as dirty in session but nothing to do) if (dirty || !labelsAdded.isEmpty() || !labelsRemoved.isEmpty()) { // create builder StringBuilder builder = new StringBuilder(); // parameters Map parameters = new HashMap<>(); // match statement builder.append("MATCH ").append(matchPattern("v")).append(" WHERE ").append(matchPredicate("v", "id")); // id parameter parameters.put("id", id()); // check vertex is dirty if (dirty) { // set properties builder.append(" SET v = $vp"); // update parameters parameters.put("vp", statementParameters()); } // check labels were added if (!labelsAdded.isEmpty()) { // add labels builder.append(!dirty ? " SET v" : ", v").append(processLabels(labelsAdded, false)); } // check labels were removed if (!labelsRemoved.isEmpty()) { // remove labels builder.append(" REMOVE v").append(processLabels(labelsRemoved, false)); } // command statement return new Neo4JDatabaseCommand(builder.toString(), parameters); } return null; } @Override public Neo4JDatabaseCommand deleteCommand() { // create statement String statement = "MATCH " + matchPattern("v") + " WHERE " + matchPredicate("v", "id") + " DETACH DELETE v"; // parameters Map parameters = Collections.singletonMap("id", id()); // command statement return new Neo4JDatabaseCommand(statement, parameters); } void commit() { // commit labels labelsAdded.clear(); labelsRemoved.clear(); originalLabels = new TreeSet<>(labels); matchLabels = Stream.concat(originalLabels.stream(), graphLabels.stream()).collect(Collectors.toCollection(TreeSet::new)); // update property values originalProperties = new HashMap<>(properties); originalCardinalities = new HashMap<>(cardinalities); // reset removed properties removedProperties.clear(); // reset flags dirty = false; } void rollback() { // restore labels labelsAdded.clear(); labelsRemoved.clear(); labels.clear(); labels.addAll(originalLabels); matchLabels = Stream.concat(originalLabels.stream(), graphLabels.stream()).collect(Collectors.toCollection(TreeSet::new)); // restore property values properties.clear(); cardinalities.clear(); properties.putAll(originalProperties); cardinalities.putAll(originalCardinalities); // reset removed properties removedProperties.clear(); // reset flags outEdgesLoaded = false; inEdgesLoaded = false; dirty = false; } private String processLabels(Set labels, boolean addPartition) { Objects.requireNonNull(labels, "labels cannot be null"); // check we need to include partition in match if (addPartition) { // get labels from read partition to be applied in vertex patterns Set partitionLabels = partition.vertexMatchPatternLabels(); if (!partitionLabels.isEmpty()) { // make sure partition is in match pattern return Stream.concat(partitionLabels.stream(), labels.stream()).map(label -> ":`" + label + "`").collect(Collectors.joining("")); } } // labels return labels.stream().map(label -> ":`" + label + "`").collect(Collectors.joining("")); } /** * {@inheritDoc} */ @Override public boolean equals(final Object object) { // ElementHelper.areEqual is implemented on this.id(), handle the case of generated ids return object instanceof Vertex && (id != null ? ElementHelper.areEqual(this, object) : super.equals(object)); } /** * {@inheritDoc} */ @Override public int hashCode() { // ElementHelper.hashCode() is implemented on this.id(), handle the case of generated ids return id != null ? ElementHelper.hashCode(this) : super.hashCode(); } /** * {@inheritDoc} */ @Override public String toString() { return StringFactory.vertexString(this); } }