/* * 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<T> implements VertexProperty<T> { 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 <U> Iterator<Property<U>> properties(String... propertyKeys) { throw VertexProperty.Exceptions.metaPropertiesNotSupported(); } @Override public Object id() { return id; } @Override public <V> Property<V> 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<String, Collection<VertexProperty>> properties = new HashMap<>(); private final Map<String, VertexProperty.Cardinality> cardinalities = new HashMap<>(); private final Set<Neo4JEdge> outEdges = new HashSet<>(); private final Set<Neo4JEdge> inEdges = new HashSet<>(); private final Set<String> outEdgeLabels = new HashSet<>(); private final Set<String> inEdgeLabels = new HashSet<>(); private final SortedSet<String> labelsAdded = new TreeSet<>(); private final SortedSet<String> labelsRemoved = new TreeSet<>(); private final SortedSet<String> labels; private final Set<String> additionalLabels; private Object generatedId = null; private boolean outEdgesLoaded = false; private boolean inEdgesLoaded = false; private boolean dirty = false; private SortedSet<String> matchLabels; private SortedSet<String> originalLabels; private Set<String> graphLabels; private Set<String> removedProperties = new HashSet<>(); private Map<String, Collection<VertexProperty>> originalProperties; private Map<String, VertexProperty.Cardinality> originalCardinalities; Neo4JVertex(Neo4JGraph graph, Neo4JSession session, Neo4JElementIdProvider<?> vertexIdProvider, Neo4JElementIdProvider<?> edgeIdProvider, Collection<String> 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: * <p> * (alias:Label1:Label2) * </p> * * @param alias The node alias, <code>null</code> 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: * <p> * alias.id = $id AND (alias:Label1 OR alias:Label2) * </p> * * @param alias The node alias. * @param idParameterName The name of the parameter that contains the vertex id. * @return the Cypher MATCH predicate or <code>null</code> 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: * <p> * MATCH (alias) WHERE alias.id = $id AND (alias:Label1 OR alias:Label2) * </p> * * @param alias The node alias. * @param idParameterName The name of the parameter that contains the vertex id. * @return the Cypher MATCH predicate or <code>null</code> 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<Object> identifiers, String alias, StringBuilder builder, Map<String, Object> 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<Edge> 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<String> set = new HashSet<>(Arrays.asList(labels)); // parameters Map<String, Object> 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<String> 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<Object> 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<Edge> 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<Edge> 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<String> 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<Object> 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<Edge> 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<Edge> 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<Object> 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<Edge> 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<Edge> 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<Vertex> 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<String> set = new HashSet<>(Arrays.asList(labels)); // parameters Map<String, Object> 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<String> 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<Object> 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<Vertex> query = session.vertices(result); // return copy since elements can be deleted in the middle of the loop Iterator<Vertex> 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<String> 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<Object> 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<Vertex> query = session.vertices(result); // return copy since elements can be deleted in the middle of the loop Iterator<Vertex> 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<Object> 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<Vertex> query = session.vertices(result); // return copy since elements can be deleted in the middle of the loop Iterator<Vertex> 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 <V> VertexProperty<V> 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<V> property = new Neo4JVertexProperty<>(this, propertyIdProvider.incrementAndGet(), name, value); // check cardinality switch (cardinality) { case list: // get existing list for key Collection<VertexProperty> 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<VertexProperty> 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 <V> VertexProperty<V> 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<V>)iterator.next(); } return VertexProperty.empty(); } // exception throw Vertex.Exceptions.multiplePropertiesExistForProvidedKey(key); } return VertexProperty.empty(); } /** * {@inheritDoc} */ @Override @SuppressWarnings("unchecked") public <V> Iterator<VertexProperty<V>> 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<V>)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<V>)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<V>)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<String, Object> statementParameters() { // define collector Collector<Map.Entry<String, Collection<VertexProperty>>, Map<String, Object>, Map<String, Object>> collector = Collector.of( HashMap::new, (map, entry) -> { // key & value String key = entry.getKey(); Collection<VertexProperty> list = entry.getValue(); // check cardinality if (cardinalities.get(key) == VertexProperty.Cardinality.single) { // iterator Iterator<VertexProperty> 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<String, Object> 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<String> labels = Stream.concat(this.labels.stream(), additionalLabels.stream()).collect(Collectors.toCollection(TreeSet::new)); try { // parameters Map<String, Object> 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<String, Object> 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<String, Object> 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<String> 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<String> 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); } }