This is an automated email from the ASF dual-hosted git repository. xiazcy pushed a commit to branch multi-label-experiment in repository https://gitbox.apache.org/repos/asf/tinkerpop.git
commit ebae85332201eec72e3e6ec42b6c96a22b23311d Author: Yang Xia <[email protected]> AuthorDate: Fri Jun 5 14:45:39 2026 -0700 implement edge multi-label --- AGENTS.md | 31 ++++++ .../grammar/DefaultGremlinBaseVisitor.java | 8 ++ .../language/grammar/TraversalMethodVisitor.java | 28 +++++ .../grammar/TraversalSourceSpawnMethodVisitor.java | 27 +++-- .../traversal/dsl/graph/GraphTraversal.java | 35 +++++-- .../traversal/dsl/graph/GraphTraversalSource.java | 52 ++++++++-- .../gremlin/process/traversal/dsl/graph/__.java | 7 ++ .../step/map/AbstractAddEdgeStepPlaceholder.java | 5 + .../map/AbstractAddElementStepPlaceholder.java | 7 +- .../step/map/AbstractAddVertexStepPlaceholder.java | 6 ++ .../traversal/step/map/AddEdgeStartStep.java | 32 +++++- .../step/map/AddEdgeStartStepPlaceholder.java | 10 +- .../process/traversal/step/map/AddEdgeStep.java | 32 +++++- .../traversal/step/map/AddEdgeStepPlaceholder.java | 10 +- .../traversal/step/map/AddVertexStartStep.java | 11 ++ .../step/map/AddVertexStartStepPlaceholder.java | 10 +- .../process/traversal/step/map/AddVertexStep.java | 11 ++ .../step/map/AddVertexStepPlaceholder.java | 10 +- .../process/traversal/step/map/ElementMapStep.java | 6 +- .../process/traversal/step/map/MergeEdgeStep.java | 113 ++++++++++++++++++--- .../traversal/step/map/PropertyMapStep.java | 6 +- .../traversal/step/sideEffect/AddLabelStep.java | 18 ++-- .../traversal/step/sideEffect/DropLabelsStep.java | 18 ++-- .../process/traversal/step/util/WithOptions.java | 18 ++++ .../process/traversal/step/util/event/Event.java | 26 +++++ .../traversal/step/util/event/EventUtil.java | 20 ++++ .../structure/io/binary/types/EdgeSerializer.java | 63 +++++++++--- .../io/graphson/GraphSONSerializersV4.java | 12 ++- .../structure/util/detached/DetachedEdge.java | 34 +++++++ .../structure/util/reference/ReferenceEdge.java | 31 ++++++ .../gremlin/structure/util/star/StarGraph.java | 63 +++++++++++- gremlin-language/src/main/antlr4/Gremlin.g4 | 8 +- .../tinkergraph/structure/AbstractTinkerGraph.java | 20 ++++ .../gremlin/tinkergraph/structure/TinkerEdge.java | 89 +++++++++++++++- .../tinkergraph/structure/TinkerElement.java | 2 +- .../gremlin/tinkergraph/structure/TinkerGraph.java | 30 ++++++ .../structure/TinkerTransactionGraph.java | 34 +++++++ .../tinkergraph/structure/TinkerVertex.java | 43 ++------ .../step/sideEffect/LabelMutationStepTest.java | 16 +-- .../structure/TinkerVertexMultiLabelTest.java | 12 ++- 40 files changed, 877 insertions(+), 137 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index 9695bfbf86..f1826478d2 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -71,3 +71,34 @@ These rules apply to any AI/IDE assistant operating on this repository. 1. Prefer no change over an unsafe or speculative change. 2. Ask for clarification. + +## Beads Issue Tracker + +This project uses **bd (beads)** for issue tracking. Run `bd prime` to see full workflow context and commands. + +### Quick Reference + +```bash +bd ready # Find available work +bd show <id> # View issue details +bd update <id> --claim # Claim work +bd close <id> # Complete work +``` + +### Rules + +- Use `bd` for ALL task tracking — do NOT use TodoWrite, TaskCreate, or markdown TODO lists +- Run `bd prime` for detailed command reference and session close protocol +- Use `bd remember` for persistent knowledge — do NOT use MEMORY.md files + +**Architecture in one line:** issues live in a local Dolt DB; sync uses `refs/dolt/data` on your git remote; `.beads/issues.jsonl` is a passive export. See https://github.com/gastownhall/beads/blob/main/docs/SYNC_CONCEPTS.md for details and anti-patterns. + +## Session Completion + +**When ending a work session**, you MUST complete ALL steps below. + +**MANDATORY WORKFLOW:** + +1. **File issues for remaining work** - Create issues for anything that needs follow-up +2. **Run quality gates** (if code changed) - Tests, linters, builds +3. **Update issue status** - Close finished work, update in-progress items \ No newline at end of file diff --git a/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/language/grammar/DefaultGremlinBaseVisitor.java b/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/language/grammar/DefaultGremlinBaseVisitor.java index 6a8cb92d8d..5257be977a 100644 --- a/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/language/grammar/DefaultGremlinBaseVisitor.java +++ b/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/language/grammar/DefaultGremlinBaseVisitor.java @@ -167,6 +167,14 @@ public class DefaultGremlinBaseVisitor<T> extends AbstractParseTreeVisitor<T> im * {@inheritDoc} */ @Override public T visitTraversalMethod_addE_String(final GremlinParser.TraversalMethod_addE_StringContext ctx) { notImplemented(ctx); return null; } + /** + * {@inheritDoc} + */ + @Override public T visitTraversalMethod_addE_StringVarargs(final GremlinParser.TraversalMethod_addE_StringVarargsContext ctx) { notImplemented(ctx); return null; } + /** + * {@inheritDoc} + */ + @Override public T visitTraversalMethod_addE_Empty(final GremlinParser.TraversalMethod_addE_EmptyContext ctx) { notImplemented(ctx); return null; } /** * {@inheritDoc} */ diff --git a/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/language/grammar/TraversalMethodVisitor.java b/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/language/grammar/TraversalMethodVisitor.java index b12a20111d..cc65794ed6 100644 --- a/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/language/grammar/TraversalMethodVisitor.java +++ b/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/language/grammar/TraversalMethodVisitor.java @@ -30,6 +30,7 @@ import org.apache.tinkerpop.gremlin.process.traversal.dsl.graph.GraphTraversal; import org.apache.tinkerpop.gremlin.process.traversal.step.GValue; import org.apache.tinkerpop.gremlin.structure.Column; import org.apache.tinkerpop.gremlin.structure.Direction; +import org.apache.tinkerpop.gremlin.structure.Edge; import org.apache.tinkerpop.gremlin.structure.T; import org.apache.tinkerpop.gremlin.structure.VertexProperty.Cardinality; @@ -204,6 +205,33 @@ public class TraversalMethodVisitor extends TraversalRootVisitor<GraphTraversal> } } + /** + * {@inheritDoc} + */ + @Override + public GraphTraversal visitTraversalMethod_addE_StringVarargs(final GremlinParser.TraversalMethod_addE_StringVarargsContext ctx) { + final List<GremlinParser.StringArgumentContext> args = ctx.stringArgument(); + final Object firstLiteralOrVar = antlr.argumentVisitor.visitStringArgument(args.get(0)); + final String firstLabel = firstLiteralOrVar instanceof String ? (String) firstLiteralOrVar : ((GValue<String>) firstLiteralOrVar).get(); + final Object secondLiteralOrVar = antlr.argumentVisitor.visitStringArgument(args.get(1)); + final String secondLabel = secondLiteralOrVar instanceof String ? (String) secondLiteralOrVar : ((GValue<String>) secondLiteralOrVar).get(); + + final String[] moreLabels = new String[args.size() - 2]; + for (int i = 2; i < args.size(); i++) { + final Object literalOrVar = antlr.argumentVisitor.visitStringArgument(args.get(i)); + moreLabels[i - 2] = literalOrVar instanceof String ? (String) literalOrVar : ((GValue<String>) literalOrVar).get(); + } + return this.graphTraversal.addE(firstLabel, secondLabel, moreLabels); + } + + /** + * {@inheritDoc} + */ + @Override + public GraphTraversal visitTraversalMethod_addE_Empty(final GremlinParser.TraversalMethod_addE_EmptyContext ctx) { + return this.graphTraversal.addE(Edge.DEFAULT_LABEL); + } + /** * {@inheritDoc} */ diff --git a/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/language/grammar/TraversalSourceSpawnMethodVisitor.java b/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/language/grammar/TraversalSourceSpawnMethodVisitor.java index c2ca344c99..5ce9b41c09 100644 --- a/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/language/grammar/TraversalSourceSpawnMethodVisitor.java +++ b/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/language/grammar/TraversalSourceSpawnMethodVisitor.java @@ -57,17 +57,32 @@ public class TraversalSourceSpawnMethodVisitor extends DefaultGremlinBaseVisitor */ @Override public GraphTraversal visitTraversalSourceSpawnMethod_addE(final GremlinParser.TraversalSourceSpawnMethod_addEContext ctx) { - if (ctx.stringArgument() != null) { - final Object literalOrVar = antlr.argumentVisitor.visitStringArgument(ctx.stringArgument()); - if (GValue.valueInstanceOf(literalOrVar, String.class)) { - return this.traversalSource.addE((GValue<String>) literalOrVar); + final List<GremlinParser.StringArgumentContext> stringArgs = ctx.stringArgument(); + if (stringArgs != null && !stringArgs.isEmpty()) { + if (stringArgs.size() == 1) { + final Object literalOrVar = antlr.argumentVisitor.visitStringArgument(stringArgs.get(0)); + if (GValue.valueInstanceOf(literalOrVar, String.class)) { + return this.traversalSource.addE((GValue<String>) literalOrVar); + } else { + return this.traversalSource.addE((String) literalOrVar); + } } else { - return this.traversalSource.addE((String) literalOrVar); + // Multi-label: addE("a", "b", ...) + final Object firstLiteralOrVar = antlr.argumentVisitor.visitStringArgument(stringArgs.get(0)); + final String firstLabel = firstLiteralOrVar instanceof String ? (String) firstLiteralOrVar : ((GValue<String>) firstLiteralOrVar).get(); + final Object secondLiteralOrVar = antlr.argumentVisitor.visitStringArgument(stringArgs.get(1)); + final String secondLabel = secondLiteralOrVar instanceof String ? (String) secondLiteralOrVar : ((GValue<String>) secondLiteralOrVar).get(); + final String[] moreLabels = new String[stringArgs.size() - 2]; + for (int i = 2; i < stringArgs.size(); i++) { + final Object literalOrVar = antlr.argumentVisitor.visitStringArgument(stringArgs.get(i)); + moreLabels[i - 2] = literalOrVar instanceof String ? (String) literalOrVar : ((GValue<String>) literalOrVar).get(); + } + return this.traversalSource.addE(firstLabel, secondLabel, moreLabels); } } else if (ctx.nestedTraversal() != null) { return this.traversalSource.addE(anonymousVisitor.visitNestedTraversal(ctx.nestedTraversal())); } else { - throw new IllegalArgumentException("addE with empty arguments is not valid."); + return this.traversalSource.addE(); } } diff --git a/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/process/traversal/dsl/graph/GraphTraversal.java b/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/process/traversal/dsl/graph/GraphTraversal.java index 31c9874af5..126c48ce73 100644 --- a/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/process/traversal/dsl/graph/GraphTraversal.java +++ b/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/process/traversal/dsl/graph/GraphTraversal.java @@ -234,6 +234,7 @@ import java.util.Comparator; import java.util.Date; import java.util.Iterator; import java.util.LinkedHashMap; +import java.util.LinkedHashSet; import java.util.List; import java.util.Map; import java.util.Optional; @@ -1464,12 +1465,11 @@ public interface GraphTraversal<S, E> extends Traversal<S, E> { if (null == l) throw new IllegalArgumentException("vertexLabel cannot be null"); } this.asAdmin().getGremlinLang().addStep(Symbols.addV, label1, label2, moreLabels); - this.asAdmin().addStep(new AddVertexStepPlaceholder<>(this.asAdmin(), label1)); - // Add the AddLabelStep directly to avoid double-recording in GremlinLang. - // The addV step above already recorded all labels; calling t.addLabel() would - // record an additional addLabel() step in GremlinLang, producing incorrect output - // like g.addV("a","b").addLabel("b") instead of g.addV("a","b"). - return this.asAdmin().addStep(new AddLabelStep<>(this.asAdmin(), label2, moreLabels)); + final Set<String> allLabels = new LinkedHashSet<>(); + allLabels.add(label1); + allLabels.add(label2); + Collections.addAll(allLabels, moreLabels); + return this.asAdmin().addStep(new AddVertexStepPlaceholder<>(this.asAdmin(), allLabels)); } /** @@ -1629,6 +1629,29 @@ public interface GraphTraversal<S, E> extends Traversal<S, E> { return this.asAdmin().addStep(step); } + /** + * Multi-label edge creation. + * + * @param label1 the first label + * @param label2 the second label + * @param moreLabels additional labels + * @return the traversal with the {@link AddEdgeStepContract} added + * @since 4.0.0 + */ + public default GraphTraversal<S, Edge> addE(final String label1, final String label2, final String... moreLabels) { + if (null == label1) throw new IllegalArgumentException("edgeLabel cannot be null"); + if (null == label2) throw new IllegalArgumentException("edgeLabel cannot be null"); + for (final String l : moreLabels) { + if (null == l) throw new IllegalArgumentException("edgeLabel cannot be null"); + } + this.asAdmin().getGremlinLang().addStep(Symbols.addE, label1, label2, moreLabels); + final Set<String> allLabels = new LinkedHashSet<>(); + allLabels.add(label1); + allLabels.add(label2); + Collections.addAll(allLabels, moreLabels); + return this.asAdmin().addStep(new AddEdgeStepPlaceholder<>(this.asAdmin(), allLabels)); + } + /** * Provide {@code from()}-modulation to respective steps. * diff --git a/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/process/traversal/dsl/graph/GraphTraversalSource.java b/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/process/traversal/dsl/graph/GraphTraversalSource.java index 8777de2d14..288841e8ee 100644 --- a/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/process/traversal/dsl/graph/GraphTraversalSource.java +++ b/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/process/traversal/dsl/graph/GraphTraversalSource.java @@ -31,7 +31,6 @@ import org.apache.tinkerpop.gremlin.process.traversal.step.GValue; import org.apache.tinkerpop.gremlin.process.traversal.step.branch.UnionStep; import org.apache.tinkerpop.gremlin.process.traversal.step.map.AddEdgeStartStepPlaceholder; import org.apache.tinkerpop.gremlin.process.traversal.step.map.AddVertexStartStepPlaceholder; -import org.apache.tinkerpop.gremlin.process.traversal.step.sideEffect.AddLabelStep; import org.apache.tinkerpop.gremlin.process.traversal.step.map.CallStep; import org.apache.tinkerpop.gremlin.process.traversal.step.map.CallStepPlaceholder; import org.apache.tinkerpop.gremlin.process.traversal.step.map.GraphStep; @@ -53,9 +52,12 @@ import org.apache.tinkerpop.gremlin.structure.util.StringFactory; import org.apache.tinkerpop.gremlin.structure.util.empty.EmptyGraph; import java.util.Arrays; +import java.util.Collections; import java.util.LinkedHashMap; +import java.util.LinkedHashSet; import java.util.Map; import java.util.Optional; +import java.util.Set; import java.util.function.BinaryOperator; import java.util.function.Supplier; import java.util.function.UnaryOperator; @@ -383,12 +385,11 @@ public class GraphTraversalSource implements TraversalSource { final GraphTraversalSource clone = this.clone(); clone.gremlinLang.addStep(GraphTraversal.Symbols.addV, label1, label2, moreLabels); final GraphTraversal.Admin<Vertex, Vertex> traversal = new DefaultGraphTraversal<>(clone); - traversal.addStep(new AddVertexStartStepPlaceholder(traversal, label1)); - // Add the AddLabelStep directly to avoid double-recording in GremlinLang. - // The addV step above already recorded all labels; calling t.addLabel() would - // record an additional addLabel() step in GremlinLang, producing incorrect output - // like g.addV("a","b").addLabel("b") instead of g.addV("a","b"). - return traversal.addStep(new AddLabelStep<>(traversal, label2, moreLabels)); + final Set<String> allLabels = new LinkedHashSet<>(); + allLabels.add(label1); + allLabels.add(label2); + Collections.addAll(allLabels, moreLabels); + return traversal.addStep(new AddVertexStartStepPlaceholder(traversal, allLabels)); } /** @@ -427,6 +428,43 @@ public class GraphTraversalSource implements TraversalSource { return traversal.addStep(new AddEdgeStartStepPlaceholder(traversal, label)); } + /** + * Spawns a {@link GraphTraversal} by adding an edge with multiple labels. + * + * @param label1 the first label + * @param label2 the second label + * @param moreLabels additional labels + * @return the traversal with the edge added + * @since 4.0.0 + */ + public GraphTraversal<Edge, Edge> addE(final String label1, final String label2, final String... moreLabels) { + if (null == label1) throw new IllegalArgumentException("edgeLabel cannot be null"); + if (null == label2) throw new IllegalArgumentException("edgeLabel cannot be null"); + for (final String l : moreLabels) { + if (null == l) throw new IllegalArgumentException("edgeLabel cannot be null"); + } + final GraphTraversalSource clone = this.clone(); + clone.gremlinLang.addStep(GraphTraversal.Symbols.addE, label1, label2, moreLabels); + final GraphTraversal.Admin<Edge, Edge> traversal = new DefaultGraphTraversal<>(clone); + final Set<String> allLabels = new LinkedHashSet<>(); + allLabels.add(label1); + allLabels.add(label2); + Collections.addAll(allLabels, moreLabels); + return traversal.addStep(new AddEdgeStartStepPlaceholder(traversal, allLabels)); + } + + /** + * Spawns a {@link GraphTraversal} by adding an edge with the provider's default label. + * + * @since 4.0.0 + */ + public GraphTraversal<Edge, Edge> addE() { + final GraphTraversalSource clone = this.clone(); + clone.gremlinLang.addStep(GraphTraversal.Symbols.addE); + final GraphTraversal.Admin<Edge, Edge> traversal = new DefaultGraphTraversal<>(clone); + return traversal.addStep(new AddEdgeStartStepPlaceholder(traversal, (String) null)); + } + /** * Spawns a {@link GraphTraversal} by doing a merge (i.e. upsert) style operation for an {@link Vertex} using a * {@code Map} as an argument. The {@code Map} represents search criteria and will match each of the supplied diff --git a/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/process/traversal/dsl/graph/__.java b/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/process/traversal/dsl/graph/__.java index a9ad25b924..8324ca6095 100644 --- a/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/process/traversal/dsl/graph/__.java +++ b/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/process/traversal/dsl/graph/__.java @@ -738,6 +738,13 @@ public class __ { return __.<A>start().addE(edgeLabel); } + /** + * @see GraphTraversal#addE(String, String, String...) + */ + public static <A> GraphTraversal<A, Edge> addE(final String label1, final String label2, final String... moreLabels) { + return __.<A>start().addE(label1, label2, moreLabels); + } + /** * @see GraphTraversal#addE(GValue) */ diff --git a/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/process/traversal/step/map/AbstractAddEdgeStepPlaceholder.java b/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/process/traversal/step/map/AbstractAddEdgeStepPlaceholder.java index fb1ce7e8c9..95d377c317 100644 --- a/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/process/traversal/step/map/AbstractAddEdgeStepPlaceholder.java +++ b/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/process/traversal/step/map/AbstractAddEdgeStepPlaceholder.java @@ -29,6 +29,7 @@ import org.apache.tinkerpop.gremlin.structure.util.reference.ReferenceVertex; import java.util.Collection; import java.util.List; import java.util.Objects; +import java.util.Set; public abstract class AbstractAddEdgeStepPlaceholder<S> extends AbstractAddElementStepPlaceholder<S, Edge, Event.EdgeAddedEvent> implements AddEdgeStepContract<S> { protected Traversal.Admin<?, ?> from; @@ -42,6 +43,10 @@ public abstract class AbstractAddEdgeStepPlaceholder<S> extends AbstractAddEleme super(traversal, label); } + public AbstractAddEdgeStepPlaceholder(Traversal.Admin traversal, Set<String> labels) { + super(traversal, labels); + } + public AbstractAddEdgeStepPlaceholder(Traversal.Admin traversal, Traversal.Admin<S, String> labelTraversal) { super(traversal, labelTraversal); } diff --git a/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/process/traversal/step/map/AbstractAddElementStepPlaceholder.java b/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/process/traversal/step/map/AbstractAddElementStepPlaceholder.java index 830fef2c80..a2015b4613 100644 --- a/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/process/traversal/step/map/AbstractAddElementStepPlaceholder.java +++ b/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/process/traversal/step/map/AbstractAddElementStepPlaceholder.java @@ -61,7 +61,7 @@ public abstract class AbstractAddElementStepPlaceholder<S, E extends Element, X protected AbstractAddElementStepPlaceholder(final Traversal.Admin traversal, final GValue<String> label) { super(traversal); this.label = label == null ? this.getDefaultLabel() : label; - if (label.isVariable()) { + if (label != null && label.isVariable()) { traversal.getGValueManager().register(label); } } @@ -75,6 +75,11 @@ public abstract class AbstractAddElementStepPlaceholder<S, E extends Element, X addTraversal(labelTraversal); } + protected AbstractAddElementStepPlaceholder(final Traversal.Admin traversal, final Set<String> labels) { + super(traversal); + this.label = (labels == null || labels.isEmpty()) ? this.getDefaultLabel() : labels; + } + protected abstract String getDefaultLabel(); @Override diff --git a/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/process/traversal/step/map/AbstractAddVertexStepPlaceholder.java b/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/process/traversal/step/map/AbstractAddVertexStepPlaceholder.java index 66aa3df116..3e0f0c1c0f 100644 --- a/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/process/traversal/step/map/AbstractAddVertexStepPlaceholder.java +++ b/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/process/traversal/step/map/AbstractAddVertexStepPlaceholder.java @@ -25,6 +25,7 @@ import org.apache.tinkerpop.gremlin.process.traversal.step.util.event.Event; import org.apache.tinkerpop.gremlin.structure.Vertex; import java.util.Objects; +import java.util.Set; public abstract class AbstractAddVertexStepPlaceholder<S> extends AbstractAddElementStepPlaceholder<S, Vertex, Event.VertexAddedEvent> implements AddVertexStepContract<S>, GValueHolder<S, Vertex> { @@ -46,6 +47,11 @@ public abstract class AbstractAddVertexStepPlaceholder<S> extends AbstractAddEle userProvidedLabel = vertexLabelTraversal != null; } + protected AbstractAddVertexStepPlaceholder(final Traversal.Admin traversal, final Set<String> labels) { + super(traversal, labels); + userProvidedLabel = labels != null && !labels.isEmpty(); + } + @Override protected String getDefaultLabel() { return Vertex.DEFAULT_LABEL; diff --git a/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/process/traversal/step/map/AddEdgeStartStep.java b/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/process/traversal/step/map/AddEdgeStartStep.java index 628587a509..99ccd7ce9e 100644 --- a/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/process/traversal/step/map/AddEdgeStartStep.java +++ b/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/process/traversal/step/map/AddEdgeStartStep.java @@ -19,7 +19,10 @@ package org.apache.tinkerpop.gremlin.process.traversal.step.map; +import java.util.Arrays; import java.util.Collections; +import java.util.Iterator; +import java.util.LinkedHashSet; import java.util.List; import java.util.Map; import java.util.Set; @@ -67,6 +70,15 @@ public class AddEdgeStartStep extends AbstractStep<Edge, Edge> implements AddEdg this.internalParameters.set(this, T.label, edgeLabelTraversal); } + public AddEdgeStartStep(final Traversal.Admin traversal, final Set<String> labels) { + super(traversal); + if (labels == null || labels.isEmpty()) { + this.internalParameters.set(this, T.label, Edge.DEFAULT_LABEL); + } else { + this.internalParameters.set(this, T.label, labels); + } + } + @Override public <S, E> List<Traversal.Admin<S, E>> getLocalChildren() { return this.internalParameters.getTraversals(); @@ -121,7 +133,21 @@ public class AddEdgeStartStep extends AbstractStep<Edge, Edge> implements AddEdg // a dead traverser to trigger the traversal final Traverser.Admin traverser = generator.generate(1, (Step) this, 1); - final String edgeLabel = (String) this.internalParameters.get(traverser, T.label, () -> Edge.DEFAULT_LABEL).get(0); + final Object labelParam = this.internalParameters.get(traverser, T.label, () -> Edge.DEFAULT_LABEL).get(0); + final String edgeLabel; + final Set<String> additionalLabels; + if (labelParam instanceof Set) { + final Set<String> labelSet = (Set<String>) labelParam; + final Iterator<String> iter = labelSet.iterator(); + edgeLabel = iter.next(); + additionalLabels = new LinkedHashSet<>(); + while (iter.hasNext()) { + additionalLabels.add(iter.next()); + } + } else { + edgeLabel = (String) labelParam; + additionalLabels = Collections.emptySet(); + } // FROM/TO must be set and must be vertices Object theTo = this.internalParameters.get(traverser, TO, () -> null).get(0); @@ -165,6 +191,10 @@ public class AddEdgeStartStep extends AbstractStep<Edge, Edge> implements AddEdg } final Edge edge = fromVertex.addEdge(edgeLabel, toVertex, this.internalParameters.getKeyValues(traverser, TO, FROM, T.label)); + if (!additionalLabels.isEmpty()) { + final String[] extra = additionalLabels.toArray(new String[0]); + edge.addLabel(extra[0], Arrays.copyOfRange(extra, 1, extra.length)); + } EventUtil.registerEdgeCreation(callbackRegistry, getTraversal(), edge); return generator.generate(edge, this, 1L); } else diff --git a/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/process/traversal/step/map/AddEdgeStartStepPlaceholder.java b/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/process/traversal/step/map/AddEdgeStartStepPlaceholder.java index 6042823e17..bbf4d429f3 100644 --- a/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/process/traversal/step/map/AddEdgeStartStepPlaceholder.java +++ b/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/process/traversal/step/map/AddEdgeStartStepPlaceholder.java @@ -24,6 +24,8 @@ import org.apache.tinkerpop.gremlin.process.traversal.lambda.GValueConstantTrave import org.apache.tinkerpop.gremlin.process.traversal.step.GValue; import org.apache.tinkerpop.gremlin.structure.Edge; +import java.util.Set; + public class AddEdgeStartStepPlaceholder extends AbstractAddEdgeStepPlaceholder<Edge> { public AddEdgeStartStepPlaceholder(final Traversal.Admin traversal, final String edgeLabel) { @@ -40,10 +42,16 @@ public class AddEdgeStartStepPlaceholder extends AbstractAddEdgeStepPlaceholder< (Traversal.Admin<Edge, String>) edgeLabelTraversal); } + public AddEdgeStartStepPlaceholder(final Traversal.Admin traversal, final Set<String> labels) { + super(traversal, labels); + } + @Override public AddEdgeStartStep asConcreteStep() { AddEdgeStartStep step; - if (label instanceof Traversal) { + if (label instanceof Set) { + step = new AddEdgeStartStep(traversal, (Set<String>) label); + } else if (label instanceof Traversal) { step = new AddEdgeStartStep(traversal, ((Traversal<?, String>) label).asAdmin()); } else if (label instanceof GValue) { step = new AddEdgeStartStep(traversal, ((GValue<String>) label).get()); diff --git a/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/process/traversal/step/map/AddEdgeStep.java b/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/process/traversal/step/map/AddEdgeStep.java index 753d9ae993..7073a7e80f 100644 --- a/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/process/traversal/step/map/AddEdgeStep.java +++ b/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/process/traversal/step/map/AddEdgeStep.java @@ -18,7 +18,10 @@ */ package org.apache.tinkerpop.gremlin.process.traversal.step.map; +import java.util.Arrays; import java.util.Collections; +import java.util.Iterator; +import java.util.LinkedHashSet; import java.util.List; import java.util.Map; import java.util.Set; @@ -62,6 +65,15 @@ public class AddEdgeStep<S> extends ScalarMapStep<S, Edge> implements AddEdgeSte this.internalParameters.set(this, T.label, edgeLabelTraversal); } + public AddEdgeStep(final Traversal.Admin traversal, final Set<String> labels) { + super(traversal); + if (labels == null || labels.isEmpty()) { + this.internalParameters.set(this, T.label, Edge.DEFAULT_LABEL); + } else { + this.internalParameters.set(this, T.label, labels); + } + } + @Override public <S, E> List<Traversal.Admin<S, E>> getLocalChildren() { return this.internalParameters.getTraversals(); @@ -109,7 +121,21 @@ public class AddEdgeStep<S> extends ScalarMapStep<S, Edge> implements AddEdgeSte @Override protected Edge map(final Traverser.Admin<S> traverser) { - final String edgeLabel = this.internalParameters.get(traverser, T.label, () -> Edge.DEFAULT_LABEL).get(0); + final Object labelParam = this.internalParameters.get(traverser, T.label, () -> Edge.DEFAULT_LABEL).get(0); + final String edgeLabel; + final Set<String> additionalLabels; + if (labelParam instanceof Set) { + final Set<String> labelSet = (Set<String>) labelParam; + final Iterator<String> iter = labelSet.iterator(); + edgeLabel = iter.next(); + additionalLabels = new LinkedHashSet<>(); + while (iter.hasNext()) { + additionalLabels.add(iter.next()); + } + } else { + edgeLabel = (String) labelParam; + additionalLabels = Collections.emptySet(); + } Vertex toVertex = getAdjacentVertex(this.internalParameters, TO, traverser, edgeLabel); Vertex fromVertex = getAdjacentVertex(this.internalParameters, FROM, traverser, edgeLabel); @@ -135,6 +161,10 @@ public class AddEdgeStep<S> extends ScalarMapStep<S, Edge> implements AddEdgeSte } final Edge edge = fromVertex.addEdge(edgeLabel, toVertex, this.internalParameters.getKeyValues(traverser, TO, FROM, T.label)); + if (!additionalLabels.isEmpty()) { + final String[] extra = additionalLabels.toArray(new String[0]); + edge.addLabel(extra[0], Arrays.copyOfRange(extra, 1, extra.length)); + } EventUtil.registerEdgeCreation(callbackRegistry, getTraversal(), edge); return edge; } diff --git a/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/process/traversal/step/map/AddEdgeStepPlaceholder.java b/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/process/traversal/step/map/AddEdgeStepPlaceholder.java index a6a5a4466a..a992a1314a 100644 --- a/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/process/traversal/step/map/AddEdgeStepPlaceholder.java +++ b/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/process/traversal/step/map/AddEdgeStepPlaceholder.java @@ -24,6 +24,8 @@ import org.apache.tinkerpop.gremlin.process.traversal.lambda.GValueConstantTrave import org.apache.tinkerpop.gremlin.process.traversal.step.GValue; import org.apache.tinkerpop.gremlin.structure.Edge; +import java.util.Set; + public class AddEdgeStepPlaceholder<S> extends AbstractAddEdgeStepPlaceholder<S> { public AddEdgeStepPlaceholder(final Traversal.Admin traversal, final String edgeLabel) { @@ -39,10 +41,16 @@ public class AddEdgeStepPlaceholder<S> extends AbstractAddEdgeStepPlaceholder<S> new ConstantTraversal<>(Edge.DEFAULT_LABEL) : edgeLabelTraversal); } + public AddEdgeStepPlaceholder(final Traversal.Admin traversal, final Set<String> labels) { + super(traversal, labels); + } + @Override public AddEdgeStep<S> asConcreteStep() { AddEdgeStep<S> step; - if (label instanceof Traversal) { + if (label instanceof Set) { + step = new AddEdgeStep<>(traversal, (Set<String>) label); + } else if (label instanceof Traversal) { step = new AddEdgeStep<>(traversal, ((Traversal<S, String>) label).asAdmin()); } else if (label instanceof GValue) { step = new AddEdgeStep<>(traversal, ((GValue<String>) label).get()); diff --git a/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/process/traversal/step/map/AddVertexStartStep.java b/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/process/traversal/step/map/AddVertexStartStep.java index 7718cc2883..5f109ae426 100644 --- a/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/process/traversal/step/map/AddVertexStartStep.java +++ b/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/process/traversal/step/map/AddVertexStartStep.java @@ -63,6 +63,17 @@ public class AddVertexStartStep extends AbstractStep<Vertex, Vertex> implements userProvidedLabel = vertexLabelTraversal != null; } + public AddVertexStartStep(final Traversal.Admin traversal, final Set<String> labels) { + super(traversal); + if (labels == null || labels.isEmpty()) { + this.internalParameters.set(this, T.label, Vertex.DEFAULT_LABEL); + userProvidedLabel = false; + } else { + this.internalParameters.set(this, T.label, labels); + userProvidedLabel = true; + } + } + @Override public boolean hasUserProvidedLabel() { return userProvidedLabel; diff --git a/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/process/traversal/step/map/AddVertexStartStepPlaceholder.java b/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/process/traversal/step/map/AddVertexStartStepPlaceholder.java index e84f4a7f1c..a1b5fa2449 100644 --- a/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/process/traversal/step/map/AddVertexStartStepPlaceholder.java +++ b/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/process/traversal/step/map/AddVertexStartStepPlaceholder.java @@ -24,6 +24,8 @@ import org.apache.tinkerpop.gremlin.process.traversal.step.GValue; import org.apache.tinkerpop.gremlin.process.traversal.step.GValueHolder; import org.apache.tinkerpop.gremlin.structure.Vertex; +import java.util.Set; + public class AddVertexStartStepPlaceholder extends AbstractAddVertexStepPlaceholder<Vertex> implements AddVertexStepContract<Vertex>, GValueHolder<Vertex, Vertex> { @@ -40,10 +42,16 @@ public class AddVertexStartStepPlaceholder extends AbstractAddVertexStepPlacehol new ConstantTraversal<>(Vertex.DEFAULT_LABEL) : (Traversal.Admin<Vertex,String>) vertexLabelTraversal); } + public AddVertexStartStepPlaceholder(final Traversal.Admin traversal, final Set<String> labels) { + super(traversal, labels); + } + @Override public AddVertexStartStep asConcreteStep() { AddVertexStartStep step; - if (label instanceof Traversal) { + if (label instanceof Set) { + step = new AddVertexStartStep(traversal, (Set<String>) label); + } else if (label instanceof Traversal) { step = new AddVertexStartStep(traversal, ((Traversal<?, String>) label).asAdmin()); } else if (label instanceof GValue) { step = new AddVertexStartStep(traversal, ((GValue<String>) label).get()); diff --git a/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/process/traversal/step/map/AddVertexStep.java b/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/process/traversal/step/map/AddVertexStep.java index b015920e17..4c2b505b53 100644 --- a/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/process/traversal/step/map/AddVertexStep.java +++ b/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/process/traversal/step/map/AddVertexStep.java @@ -59,6 +59,17 @@ public class AddVertexStep<S> extends ScalarMapStep<S, Vertex> implements AddVer userProvidedLabel = vertexLabelTraversal != null; } + public AddVertexStep(final Traversal.Admin traversal, final Set<String> labels) { + super(traversal); + if (labels == null || labels.isEmpty()) { + this.internalParameters.set(this, T.label, Vertex.DEFAULT_LABEL); + userProvidedLabel = false; + } else { + this.internalParameters.set(this, T.label, labels); + userProvidedLabel = true; + } + } + @Override public boolean hasUserProvidedLabel() { return userProvidedLabel; diff --git a/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/process/traversal/step/map/AddVertexStepPlaceholder.java b/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/process/traversal/step/map/AddVertexStepPlaceholder.java index 505bc47c1f..b749760051 100644 --- a/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/process/traversal/step/map/AddVertexStepPlaceholder.java +++ b/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/process/traversal/step/map/AddVertexStepPlaceholder.java @@ -22,6 +22,8 @@ import org.apache.tinkerpop.gremlin.process.traversal.Traversal; import org.apache.tinkerpop.gremlin.process.traversal.lambda.GValueConstantTraversal; import org.apache.tinkerpop.gremlin.process.traversal.step.GValue; +import java.util.Set; + public class AddVertexStepPlaceholder<S> extends AbstractAddVertexStepPlaceholder<S> { public AddVertexStepPlaceholder(Traversal.Admin traversal, String label) { @@ -36,10 +38,16 @@ public class AddVertexStepPlaceholder<S> extends AbstractAddVertexStepPlaceholde super(traversal, vertexLabelTraversal); } + public AddVertexStepPlaceholder(Traversal.Admin traversal, Set<String> labels) { + super(traversal, labels); + } + @Override public AddVertexStep<S> asConcreteStep() { AddVertexStep<S> step; - if (label instanceof Traversal) { + if (label instanceof Set) { + step = new AddVertexStep<>(traversal, (Set<String>) label); + } else if (label instanceof Traversal) { step = new AddVertexStep<>(traversal, ((Traversal<S, String>) label).asAdmin()); } else if (label instanceof GValue) { step = new AddVertexStep<>(traversal, ((GValue<String>) label).get()); diff --git a/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/process/traversal/step/map/ElementMapStep.java b/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/process/traversal/step/map/ElementMapStep.java index 370b0cd059..b05e620849 100644 --- a/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/process/traversal/step/map/ElementMapStep.java +++ b/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/process/traversal/step/map/ElementMapStep.java @@ -25,7 +25,6 @@ import org.apache.tinkerpop.gremlin.process.traversal.step.GraphComputing; import org.apache.tinkerpop.gremlin.process.traversal.step.TraversalParent; import org.apache.tinkerpop.gremlin.process.traversal.step.util.Parameters; import org.apache.tinkerpop.gremlin.process.traversal.step.util.WithOptions; -import org.apache.tinkerpop.gremlin.process.traversal.strategy.decoration.OptionsStrategy; import org.apache.tinkerpop.gremlin.process.traversal.traverser.TraverserRequirement; import org.apache.tinkerpop.gremlin.structure.Direction; import org.apache.tinkerpop.gremlin.structure.Edge; @@ -131,10 +130,7 @@ public class ElementMapStep<K,E> extends ScalarMapStep<Element, Map<K, E>> imple * or source-level {@code g.with("multilabel")}. */ private boolean isMultilabelEnabled() { - if (this.multilabel) return true; - return getTraversal().getStrategies().getStrategy(OptionsStrategy.class) - .map(os -> os.getOptions().containsKey("multilabel") || os.getOptions().containsKey(WithOptions.multilabel)) - .orElse(false); + return WithOptions.isMultilabelEnabled(this.multilabel, getTraversal()); } public String[] getPropertyKeys() { diff --git a/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/process/traversal/step/map/MergeEdgeStep.java b/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/process/traversal/step/map/MergeEdgeStep.java index 3535143192..261ab78eb8 100644 --- a/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/process/traversal/step/map/MergeEdgeStep.java +++ b/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/process/traversal/step/map/MergeEdgeStep.java @@ -20,6 +20,7 @@ package org.apache.tinkerpop.gremlin.process.traversal.step.map; import java.util.ArrayList; import java.util.Arrays; +import java.util.Collections; import java.util.HashMap; import java.util.Iterator; import java.util.LinkedHashMap; @@ -166,17 +167,21 @@ public class MergeEdgeStep<S> extends MergeElementStep<S, Edge, Map<Object, Obje final Graph graph = getGraph(); final Object edgeId = search.get(T.id); - final String edgeLabel = (String) search.get(T.label); + final Object labelValue = search.get(T.label); final Object fromId = search.get(Direction.OUT); final Object toId = search.get(Direction.IN); + // Extract a single label string for use in outE(label)/inE(label) calls. + // For multi-label (collection), we use hasLabel() chaining instead. + final String singleLabel = labelValue instanceof String ? (String) labelValue : null; + final boolean hasLabelConstraint = labelValue != null; + GraphTraversal t; if (edgeId != null) { // g.E(eid).hasLabel(label).where(outV().hasId(fromId)).where(inV().hasId(toId)); t = graph.traversal().E(edgeId); - if (edgeLabel != null) - t = t.hasLabel(edgeLabel); + t = applyLabelConstraint(t, labelValue); if (fromId != null) t = t.where(outV().hasId(fromId)); if (toId != null) @@ -186,10 +191,14 @@ public class MergeEdgeStep<S> extends MergeElementStep<S, Edge, Map<Object, Obje // g.V(fromId).outE(label).where(inV().hasId(toId)); t = graph.traversal().V(fromId); - if (edgeLabel != null) - t = t.outE(edgeLabel); - else + if (singleLabel != null) + t = t.outE(singleLabel); + else { t = t.outE(); + // apply multi-label constraint after outE() if it's a collection + if (hasLabelConstraint && singleLabel == null) + t = applyLabelConstraint(t, labelValue); + } if (toId != null) t = t.where(inV().hasId(toId)); @@ -197,17 +206,20 @@ public class MergeEdgeStep<S> extends MergeElementStep<S, Edge, Map<Object, Obje // g.V(toId).inE(edgeLabel); t = graph.traversal().V(toId); - if (edgeLabel != null) - t = t.inE(edgeLabel); - else + if (singleLabel != null) + t = t.inE(singleLabel); + else { t = t.inE(); + // apply multi-label constraint after inE() if it's a collection + if (hasLabelConstraint && singleLabel == null) + t = applyLabelConstraint(t, labelValue); + } } else { // g.E().hasLabel(label) t = graph.traversal().E(); - if (edgeLabel != null) - t = t.hasLabel(edgeLabel); + t = applyLabelConstraint(t, labelValue); } @@ -222,6 +234,26 @@ public class MergeEdgeStep<S> extends MergeElementStep<S, Edge, Map<Object, Obje return CloseableIterator.of(t); } + /** + * Apply label constraint(s) to the traversal. Supports both single String labels and + * Collection labels with AND semantics (edge must have ALL specified labels). + */ + protected GraphTraversal applyLabelConstraint(GraphTraversal t, final Object labelValue) { + if (labelValue == null) { + return t; + } + if (labelValue instanceof String) { + return t.hasLabel((String) labelValue); + } else if (labelValue instanceof java.util.Collection) { + // Multi-label: AND semantics - must have ALL specified labels + for (final Object label : (java.util.Collection<?>) labelValue) { + t = t.hasLabel((String) label); + } + return t; + } + return t; + } + protected Map<?,?> resolveVertices(final Map map, final Traverser.Admin<S> traverser) { resolveVertex(Merge.outV, Direction.OUT, map, traverser, outVTraversal); resolveVertex(Merge.inV, Direction.IN, map, traverser, inVTraversal); @@ -279,14 +311,34 @@ public class MergeEdgeStep<S> extends MergeElementStep<S, Edge, Map<Object, Obje traverser.set((S) e); // assume good input from GraphTraversal - folks might drop in a T here even though it is immutable - final Map<String, ?> onMatchMap = materializeMap(traverser, onMatchTraversal); + final Map onMatchMap = materializeMap(traverser, onMatchTraversal); validateMapInput(onMatchMap, true); onMatchMap.forEach((key, value) -> { + // Handle T.label replacement for multi-label support + if (T.label.equals(key) || T.label.getAccessor().equals(key)) { + // Drop all existing labels and replace with new ones + e.dropLabels(); + if (value instanceof String) { + e.addLabel((String) value); + } else if (value instanceof java.util.Collection) { + final java.util.Collection<?> labels = (java.util.Collection<?>) value; + if (!labels.isEmpty()) { + final String[] labelArray = labels.stream() + .map(l -> (String) l) + .toArray(String[]::new); + e.addLabel(labelArray[0], + Arrays.copyOfRange(labelArray, 1, labelArray.length)); + } + // Empty collection = use default label behavior (already handled by dropLabels()) + } + return; + } + // trigger callbacks for eventing - in this case, it's a EdgePropertyChangedEvent. if there's no // registry/callbacks then just set the property - EventUtil.registerEdgePropertyChange(callbackRegistry, getTraversal(), e, key, value); - e.property(key, value); + EventUtil.registerEdgePropertyChange(callbackRegistry, getTraversal(), e, (String) key, value); + e.property((String) key, value); }); }); @@ -317,7 +369,30 @@ public class MergeEdgeStep<S> extends MergeElementStep<S, Edge, Map<Object, Obje final Vertex fromV = resolveVertex(onCreateMap.get(Direction.OUT)); final Vertex toV = resolveVertex(onCreateMap.get(Direction.IN)); - final String label = (String) onCreateMap.getOrDefault(T.label, Edge.DEFAULT_LABEL); + final Object labelValue = onCreateMap.getOrDefault(T.label, Edge.DEFAULT_LABEL); + final String primaryLabel; + final java.util.Collection<String> additionalLabels; + + if (labelValue instanceof String) { + primaryLabel = (String) labelValue; + additionalLabels = Collections.emptyList(); + } else if (labelValue instanceof java.util.Collection) { + final java.util.Collection<?> labelCollection = (java.util.Collection<?>) labelValue; + if (labelCollection.isEmpty()) { + primaryLabel = Edge.DEFAULT_LABEL; + additionalLabels = Collections.emptyList(); + } else { + final Iterator<?> iter = labelCollection.iterator(); + primaryLabel = (String) iter.next(); + additionalLabels = new ArrayList<>(); + while (iter.hasNext()) { + additionalLabels.add((String) iter.next()); + } + } + } else { + primaryLabel = Edge.DEFAULT_LABEL; + additionalLabels = Collections.emptyList(); + } final List<Object> properties = new ArrayList<>(); @@ -329,7 +404,13 @@ public class MergeEdgeStep<S> extends MergeElementStep<S, Edge, Map<Object, Obje properties.add(e.getValue()); } - final Edge edge = fromV.addEdge(label, toV, properties.toArray()); + final Edge edge = fromV.addEdge(primaryLabel, toV, properties.toArray()); + + // add any additional labels from a multi-label collection + if (!additionalLabels.isEmpty()) { + final String[] extraLabels = additionalLabels.toArray(new String[0]); + edge.addLabel(extraLabels[0], Arrays.copyOfRange(extraLabels, 1, extraLabels.length)); + } // trigger callbacks for eventing - in this case, it's a VertexAddedEvent EventUtil.registerEdgeCreationWithGenericEventRegistry(callbackRegistry, getTraversal(), edge); diff --git a/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/process/traversal/step/map/PropertyMapStep.java b/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/process/traversal/step/map/PropertyMapStep.java index 25bc143bc3..665ea10e4b 100644 --- a/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/process/traversal/step/map/PropertyMapStep.java +++ b/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/process/traversal/step/map/PropertyMapStep.java @@ -25,7 +25,6 @@ import org.apache.tinkerpop.gremlin.process.traversal.step.Configuring; import org.apache.tinkerpop.gremlin.process.traversal.step.TraversalParent; import org.apache.tinkerpop.gremlin.process.traversal.step.util.Parameters; import org.apache.tinkerpop.gremlin.process.traversal.step.util.WithOptions; -import org.apache.tinkerpop.gremlin.process.traversal.strategy.decoration.OptionsStrategy; import org.apache.tinkerpop.gremlin.process.traversal.traverser.TraverserRequirement; import org.apache.tinkerpop.gremlin.process.traversal.util.TraversalProduct; import org.apache.tinkerpop.gremlin.process.traversal.util.TraversalUtil; @@ -220,10 +219,7 @@ public class PropertyMapStep<K,E> extends ScalarMapStep<Element, Map<K, E>> * or source-level {@code g.with("multilabel")}. */ private boolean isMultilabelEnabled() { - if (this.multilabel) return true; - return getTraversal().getStrategies().getStrategy(OptionsStrategy.class) - .map(os -> os.getOptions().containsKey("multilabel") || os.getOptions().containsKey(WithOptions.multilabel)) - .orElse(false); + return WithOptions.isMultilabelEnabled(this.multilabel, getTraversal()); } protected Object getElementId(Element element){ diff --git a/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/process/traversal/step/sideEffect/AddLabelStep.java b/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/process/traversal/step/sideEffect/AddLabelStep.java index c239623601..1cb2d01108 100644 --- a/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/process/traversal/step/sideEffect/AddLabelStep.java +++ b/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/process/traversal/step/sideEffect/AddLabelStep.java @@ -26,7 +26,6 @@ import org.apache.tinkerpop.gremlin.process.traversal.step.util.event.CallbackRe import org.apache.tinkerpop.gremlin.process.traversal.step.util.event.Event; import org.apache.tinkerpop.gremlin.process.traversal.step.util.event.EventUtil; import org.apache.tinkerpop.gremlin.process.traversal.step.util.event.ListCallbackRegistry; -import org.apache.tinkerpop.gremlin.process.traversal.strategy.decoration.EventStrategy; import org.apache.tinkerpop.gremlin.process.traversal.traverser.TraverserRequirement; import org.apache.tinkerpop.gremlin.process.traversal.util.TraversalUtil; import org.apache.tinkerpop.gremlin.structure.Element; @@ -36,9 +35,9 @@ import org.apache.tinkerpop.gremlin.structure.util.StringFactory; import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; +import java.util.LinkedHashSet; import java.util.List; import java.util.Objects; -import java.util.Optional; import java.util.Set; /** @@ -48,11 +47,11 @@ import java.util.Set; * @since 4.0.0 */ public class AddLabelStep<S extends Element> extends SideEffectStep<S> - implements Mutating<Event.ElementPropertyChangedEvent>, TraversalParent { + implements Mutating<Event.ElementLabelChangedEvent>, TraversalParent { private final String[] labels; private Traversal.Admin<S, String> labelTraversal; - private CallbackRegistry<Event.ElementPropertyChangedEvent> callbackRegistry; + private CallbackRegistry<Event.ElementLabelChangedEvent> callbackRegistry; public AddLabelStep(final Traversal.Admin traversal, final String label, final String... moreLabels) { super(traversal); @@ -76,6 +75,7 @@ public class AddLabelStep<S extends Element> extends SideEffectStep<S> @Override protected void sideEffect(final Traverser.Admin<S> traverser) { final Element element = traverser.get(); + final Set<String> oldLabels = new LinkedHashSet<>(element.labels()); if (this.labelTraversal != null) { final List<String> collectedLabels = new ArrayList<>(); @@ -93,16 +93,14 @@ public class AddLabelStep<S extends Element> extends SideEffectStep<S> Arrays.copyOfRange(this.labels, 1, this.labels.length)); } - // trigger event callbacks - final Optional<EventStrategy> optEventStrategy = getTraversal().getStrategies().getStrategy(EventStrategy.class); - if (EventUtil.hasAnyCallbacks(callbackRegistry) && optEventStrategy.isPresent()) { - final EventStrategy es = optEventStrategy.get(); - EventUtil.registerPropertyChange(callbackRegistry, es, element, null, null, new Object[0]); + // trigger event callbacks only if labels actually changed + if (!oldLabels.equals(element.labels())) { + EventUtil.registerLabelChange(callbackRegistry, getTraversal(), element, oldLabels, element.labels()); } } @Override - public CallbackRegistry<Event.ElementPropertyChangedEvent> getMutatingCallbackRegistry() { + public CallbackRegistry<Event.ElementLabelChangedEvent> getMutatingCallbackRegistry() { if (null == callbackRegistry) callbackRegistry = new ListCallbackRegistry<>(); return callbackRegistry; } diff --git a/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/process/traversal/step/sideEffect/DropLabelsStep.java b/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/process/traversal/step/sideEffect/DropLabelsStep.java index 14e5d0634f..9812b0b786 100644 --- a/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/process/traversal/step/sideEffect/DropLabelsStep.java +++ b/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/process/traversal/step/sideEffect/DropLabelsStep.java @@ -26,7 +26,6 @@ import org.apache.tinkerpop.gremlin.process.traversal.step.util.event.CallbackRe import org.apache.tinkerpop.gremlin.process.traversal.step.util.event.Event; import org.apache.tinkerpop.gremlin.process.traversal.step.util.event.EventUtil; import org.apache.tinkerpop.gremlin.process.traversal.step.util.event.ListCallbackRegistry; -import org.apache.tinkerpop.gremlin.process.traversal.strategy.decoration.EventStrategy; import org.apache.tinkerpop.gremlin.process.traversal.traverser.TraverserRequirement; import org.apache.tinkerpop.gremlin.process.traversal.util.TraversalUtil; import org.apache.tinkerpop.gremlin.structure.Element; @@ -35,8 +34,8 @@ import org.apache.tinkerpop.gremlin.structure.util.StringFactory; import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; +import java.util.LinkedHashSet; import java.util.List; -import java.util.Optional; import java.util.Set; /** @@ -46,12 +45,12 @@ import java.util.Set; * @since 4.0.0 */ public class DropLabelsStep<S extends Element> extends SideEffectStep<S> - implements Mutating<Event.ElementPropertyChangedEvent>, TraversalParent { + implements Mutating<Event.ElementLabelChangedEvent>, TraversalParent { private final boolean dropAll; private final String[] labels; private Traversal.Admin<S, String> labelTraversal; - private CallbackRegistry<Event.ElementPropertyChangedEvent> callbackRegistry; + private CallbackRegistry<Event.ElementLabelChangedEvent> callbackRegistry; /** * Constructor for dropLabels() - removes all labels. @@ -89,6 +88,7 @@ public class DropLabelsStep<S extends Element> extends SideEffectStep<S> @Override protected void sideEffect(final Traverser.Admin<S> traverser) { final Element element = traverser.get(); + final Set<String> oldLabels = new LinkedHashSet<>(element.labels()); if (this.labelTraversal != null) { final String label = TraversalUtil.apply(traverser, this.labelTraversal); @@ -102,16 +102,14 @@ public class DropLabelsStep<S extends Element> extends SideEffectStep<S> Arrays.copyOfRange(this.labels, 1, this.labels.length)); } - // trigger event callbacks - final Optional<EventStrategy> optEventStrategy = getTraversal().getStrategies().getStrategy(EventStrategy.class); - if (EventUtil.hasAnyCallbacks(callbackRegistry) && optEventStrategy.isPresent()) { - final EventStrategy es = optEventStrategy.get(); - EventUtil.registerPropertyChange(callbackRegistry, es, element, null, null, new Object[0]); + // trigger event callbacks only if labels actually changed + if (!oldLabels.equals(element.labels())) { + EventUtil.registerLabelChange(callbackRegistry, getTraversal(), element, oldLabels, element.labels()); } } @Override - public CallbackRegistry<Event.ElementPropertyChangedEvent> getMutatingCallbackRegistry() { + public CallbackRegistry<Event.ElementLabelChangedEvent> getMutatingCallbackRegistry() { if (null == callbackRegistry) callbackRegistry = new ListCallbackRegistry<>(); return callbackRegistry; } diff --git a/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/process/traversal/step/util/WithOptions.java b/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/process/traversal/step/util/WithOptions.java index 9be8e4f939..8b7df79f02 100644 --- a/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/process/traversal/step/util/WithOptions.java +++ b/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/process/traversal/step/util/WithOptions.java @@ -18,8 +18,10 @@ */ package org.apache.tinkerpop.gremlin.process.traversal.step.util; +import org.apache.tinkerpop.gremlin.process.traversal.Traversal; import org.apache.tinkerpop.gremlin.process.traversal.dsl.graph.GraphTraversal; import org.apache.tinkerpop.gremlin.process.traversal.step.map.IndexStep; +import org.apache.tinkerpop.gremlin.process.traversal.strategy.decoration.OptionsStrategy; import org.apache.tinkerpop.gremlin.structure.Edge; import org.apache.tinkerpop.gremlin.structure.Element; import org.apache.tinkerpop.gremlin.structure.Graph; @@ -102,4 +104,20 @@ public class WithOptions { * @since 4.0.0 */ public static final String multilabel = Graph.Hidden.hide("tinkerpop.multilabel"); + + /** + * The user-facing key for multilabel configuration used with {@code g.with("multilabel")}. + */ + public static final String MULTILABEL_KEY = "multilabel"; + + /** + * Checks whether multi-label output is enabled for the given traversal, either at the step level + * (via step-level configuration) or at the source level (via {@code g.with("multilabel")}). + */ + public static boolean isMultilabelEnabled(final boolean stepLevelEnabled, final Traversal.Admin<?, ?> traversal) { + if (stepLevelEnabled) return true; + return traversal.getStrategies().getStrategy(OptionsStrategy.class) + .map(os -> os.getOptions().containsKey(MULTILABEL_KEY) || os.getOptions().containsKey(multilabel)) + .orElse(false); + } } diff --git a/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/process/traversal/step/util/event/Event.java b/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/process/traversal/step/util/event/Event.java index 4a9d9c06e6..f02e6bc63f 100644 --- a/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/process/traversal/step/util/event/Event.java +++ b/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/process/traversal/step/util/event/Event.java @@ -27,6 +27,7 @@ import org.apache.tinkerpop.gremlin.structure.Vertex; import org.apache.tinkerpop.gremlin.structure.VertexProperty; import java.util.Iterator; +import java.util.Set; /** * A representation of some action that occurs on a {@link Graph} for a {@link Traversal}. @@ -213,6 +214,31 @@ public interface Event { } } + /** + * An event that represents a label change on an element (vertex or edge). + * Fired when labels are added or removed via addLabel()/dropLabel()/dropLabels() steps. + */ + class ElementLabelChangedEvent implements Event { + private final Element element; + private final Set<String> oldLabels; + private final Set<String> newLabels; + + public ElementLabelChangedEvent(final Element element, final Set<String> oldLabels, final Set<String> newLabels) { + this.element = element; + this.oldLabels = oldLabels; + this.newLabels = newLabels; + } + + public Element getElement() { return element; } + public Set<String> getOldLabels() { return oldLabels; } + public Set<String> getNewLabels() { return newLabels; } + + @Override + public void fireEvent(final Iterator<MutationListener> eventListeners) { + // Label change events are dispatched directly via CallbackRegistry, not MutationListener. + } + } + /** * A base class for {@link Property} mutation events. */ diff --git a/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/process/traversal/step/util/event/EventUtil.java b/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/process/traversal/step/util/event/EventUtil.java index 2fce97e150..85d8072f89 100644 --- a/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/process/traversal/step/util/event/EventUtil.java +++ b/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/process/traversal/step/util/event/EventUtil.java @@ -27,6 +27,8 @@ import org.apache.tinkerpop.gremlin.structure.Property; import org.apache.tinkerpop.gremlin.structure.Vertex; import org.apache.tinkerpop.gremlin.structure.VertexProperty; +import java.util.Set; + /** * Logic for registering events with the {@link EventStrategy} and the callback registry. Extracting this logic allows * providers to reuse these utilities more readily. @@ -202,6 +204,24 @@ public final class EventUtil { } } + /** + * Register a label change event with the callback registry. + */ + @SuppressWarnings("unchecked") + public static void registerLabelChange(final CallbackRegistry<Event.ElementLabelChangedEvent> callbackRegistry, + final Traversal.Admin<?, ?> traversal, final Element element, + final Set<String> oldLabels, final Set<String> newLabels) { + if (hasAnyCallbacks(callbackRegistry)) { + final EventStrategy eventStrategy = ((Traversal.Admin<Object, Object>) traversal) + .getStrategies().getStrategy(EventStrategy.class).orElse(null); + final Element detachedElement = eventStrategy != null ? eventStrategy.detach(element) : element; + final Event.ElementLabelChangedEvent event = new Event.ElementLabelChangedEvent(detachedElement, oldLabels, newLabels); + for (EventCallback<Event.ElementLabelChangedEvent> c : callbackRegistry.getCallbacks()) { + c.accept(event); + } + } + } + /** * Register a vertex property addition event with the callback registry. */ diff --git a/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/structure/io/binary/types/EdgeSerializer.java b/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/structure/io/binary/types/EdgeSerializer.java index 6d614d5126..fe48d6f6ae 100644 --- a/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/structure/io/binary/types/EdgeSerializer.java +++ b/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/structure/io/binary/types/EdgeSerializer.java @@ -30,8 +30,11 @@ import org.apache.tinkerpop.gremlin.structure.util.detached.DetachedVertex; import org.apache.tinkerpop.gremlin.structure.util.reference.ReferenceEdge; import java.io.IOException; +import java.util.ArrayList; import java.util.Collections; +import java.util.LinkedHashSet; import java.util.List; +import java.util.Set; /** * @author Stephen Mallette (http://stephen.genoprime.com) @@ -44,25 +47,43 @@ public class EdgeSerializer extends SimpleTypeSerializer<Edge> { @Override protected Edge readValue(final Buffer buffer, final GraphBinaryReader context) throws IOException { final Object id = context.read(buffer); - // reading single string value for now according to GraphBinaryV4 - final String label = (String) context.readValue(buffer, List.class, false).get(0); + // Read all labels as List<String> for multi-label support + final List<String> labelList = context.readValue(buffer, List.class, false); final Object inVId = context.read(buffer); - // reading single string value for now according to GraphBinaryV4 - final String inVLabel = (String) context.readValue(buffer, List.class, false).get(0); + // Read all inVertex labels as List<String> for multi-label support + final List<String> inVLabelList = context.readValue(buffer, List.class, false); final Object outVId = context.read(buffer); - // reading single string value for now according to GraphBinaryV4 - final String outVLabel = (String) context.readValue(buffer, List.class, false).get(0); + // Read all outVertex labels as List<String> for multi-label support + final List<String> outVLabelList = context.readValue(buffer, List.class, false); // discard the parent vertex context.read(buffer); final List<Property> properties = context.read(buffer); - final DetachedVertex inV = DetachedVertex.build().setId(inVId).setLabel(inVLabel).create(); - final DetachedVertex outV = DetachedVertex.build().setId(outVId).setLabel(outVLabel).create(); + final DetachedVertex.Builder inVBuilder = DetachedVertex.build().setId(inVId); + if (inVLabelList.size() == 1) { + inVBuilder.setLabel(inVLabelList.get(0)); + } else if (!inVLabelList.isEmpty()) { + inVBuilder.setLabels(new LinkedHashSet<>(inVLabelList)); + } + final DetachedVertex inV = inVBuilder.create(); + + final DetachedVertex.Builder outVBuilder = DetachedVertex.build().setId(outVId); + if (outVLabelList.size() == 1) { + outVBuilder.setLabel(outVLabelList.get(0)); + } else if (!outVLabelList.isEmpty()) { + outVBuilder.setLabels(new LinkedHashSet<>(outVLabelList)); + } + final DetachedVertex outV = outVBuilder.create(); - final DetachedEdge.Builder builder = DetachedEdge.build().setId(id).setLabel(label).setInV(inV).setOutV(outV); + final DetachedEdge.Builder builder = DetachedEdge.build().setId(id).setInV(inV).setOutV(outV); + if (labelList.size() == 1) { + builder.setLabel(labelList.get(0)); + } else if (!labelList.isEmpty()) { + builder.setLabels(new LinkedHashSet<>(labelList)); + } if (properties != null) { for (final Property p : properties) { @@ -77,16 +98,28 @@ public class EdgeSerializer extends SimpleTypeSerializer<Edge> { protected void writeValue(final Edge value, final Buffer buffer, final GraphBinaryWriter context) throws IOException { context.write(value.id(), buffer); - // wrapping label into list here for now according to GraphBinaryV4, but we aren't allowing null label yet - if (value.label() == null) { - throw new IOException("Unexpected null value when nullable is false"); + // Write all edge labels as List<String> for multi-label support + final Set<String> edgeLabels = value.labels(); + if (edgeLabels == null || edgeLabels.isEmpty()) { + throw new IOException("Unexpected null or empty labels when nullable is false"); } - context.writeValue(Collections.singletonList(value.label()), buffer, false); + context.writeValue(new ArrayList<>(edgeLabels), buffer, false); context.write(value.inVertex().id(), buffer); - context.writeValue(Collections.singletonList(value.inVertex().label()), buffer, false); + // Write all inVertex labels as List<String> for multi-label support + final Set<String> inVLabels = value.inVertex().labels(); + if (inVLabels == null || inVLabels.isEmpty()) { + throw new IOException("Unexpected null or empty labels when nullable is false"); + } + context.writeValue(new ArrayList<>(inVLabels), buffer, false); + context.write(value.outVertex().id(), buffer); - context.writeValue(Collections.singletonList(value.outVertex().label()), buffer, false); + // Write all outVertex labels as List<String> for multi-label support + final Set<String> outVLabels = value.outVertex().labels(); + if (outVLabels == null || outVLabels.isEmpty()) { + throw new IOException("Unexpected null or empty labels when nullable is false"); + } + context.writeValue(new ArrayList<>(outVLabels), buffer, false); // we don't serialize the parent Vertex for edges. context.write(null, buffer); diff --git a/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/structure/io/graphson/GraphSONSerializersV4.java b/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/structure/io/graphson/GraphSONSerializersV4.java index bc1962f8db..afd27256fc 100644 --- a/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/structure/io/graphson/GraphSONSerializersV4.java +++ b/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/structure/io/graphson/GraphSONSerializersV4.java @@ -146,7 +146,7 @@ class GraphSONSerializersV4 { jsonGenerator.writeStartObject(); jsonGenerator.writeObjectField(GraphSONTokens.ID, edge.id()); - writeLabel(jsonGenerator, GraphSONTokens.LABEL, edge.label()); + writeLabels(jsonGenerator, GraphSONTokens.LABEL, edge.labels()); writeTypeForGraphObjectIfUntyped(jsonGenerator, typeInfo, GraphSONTokens.EDGE); writeVertex(GraphSONTokens.IN, edge.inVertex(), jsonGenerator); writeVertex(GraphSONTokens.OUT, edge.outVertex(), jsonGenerator); @@ -160,7 +160,7 @@ class GraphSONSerializersV4 { jsonGenerator.writeFieldName(vertexDirection); jsonGenerator.writeStartObject(); jsonGenerator.writeObjectField(GraphSONTokens.ID, v.id()); - writeLabel(jsonGenerator, GraphSONTokens.LABEL, v.label()); + writeLabels(jsonGenerator, GraphSONTokens.LABEL, v.labels()); jsonGenerator.writeEndObject(); } @@ -439,8 +439,14 @@ class GraphSONSerializersV4 { e.setId(deserializationContext.readValue(jsonParser, Object.class)); } else if (jsonParser.getCurrentName().equals(GraphSONTokens.LABEL)) { jsonParser.nextToken(); + final java.util.Set<String> labels = new java.util.LinkedHashSet<>(); while (jsonParser.nextToken() != JsonToken.END_ARRAY) { - e.setLabel(jsonParser.getText()); + labels.add(jsonParser.getText()); + } + if (labels.size() == 1) { + e.setLabel(labels.iterator().next()); + } else if (!labels.isEmpty()) { + e.setLabels(labels); } } else if (jsonParser.getCurrentName().equals(GraphSONTokens.OUT)) { jsonParser.nextToken(); diff --git a/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/structure/util/detached/DetachedEdge.java b/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/structure/util/detached/DetachedEdge.java index 2c91afb615..77f53876d4 100644 --- a/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/structure/util/detached/DetachedEdge.java +++ b/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/structure/util/detached/DetachedEdge.java @@ -30,8 +30,10 @@ import java.util.ArrayList; import java.util.Collections; import java.util.HashMap; import java.util.Iterator; +import java.util.LinkedHashSet; import java.util.List; import java.util.Map; +import java.util.Set; /** * Represents an {@link Edge} that is disconnected from a {@link Graph}. "Disconnection" can mean detachment from @@ -51,6 +53,7 @@ public class DetachedEdge extends DetachedElement<Edge> implements Edge { private DetachedVertex outVertex; private DetachedVertex inVertex; + private Set<String> edgeLabels; private DetachedEdge() {} @@ -59,6 +62,12 @@ public class DetachedEdge extends DetachedElement<Edge> implements Edge { this.outVertex = DetachedFactory.detach(edge.outVertex(), false); this.inVertex = DetachedFactory.detach(edge.inVertex(), false); + // Capture all labels from the source edge for multi-label support. + final Set<String> srcLabels = edge.labels(); + if (srcLabels.size() > 1) { + this.edgeLabels = new LinkedHashSet<>(srcLabels); + } + // only serialize properties if requested, the graph supports it and there are meta properties present. // this prevents unnecessary object creation of a new HashMap of a new HashMap which will just be empty. // it will use Collections.emptyMap() by default @@ -113,6 +122,23 @@ public class DetachedEdge extends DetachedElement<Edge> implements Edge { return StringFactory.edgeString(this); } + @Override + public Set<String> labels() { + if (this.edgeLabels != null) { + return Collections.unmodifiableSet(this.edgeLabels); + } + // Fall back to single label from parent + return this.label != null ? Collections.singleton(this.label) : Collections.emptySet(); + } + + @Override + public String label() { + if (this.edgeLabels != null && !this.edgeLabels.isEmpty()) { + return this.edgeLabels.iterator().next(); + } + return this.label != null ? this.label : Edge.DEFAULT_LABEL; + } + @Override public Vertex inVertex() { return this.inVertex; @@ -180,6 +206,14 @@ public class DetachedEdge extends DetachedElement<Edge> implements Edge { return this; } + public Builder setLabels(final Set<String> labels) { + e.edgeLabels = labels != null ? new LinkedHashSet<>(labels) : null; + if (labels != null && !labels.isEmpty()) { + e.label = labels.iterator().next(); + } + return this; + } + public Builder setOutV(final DetachedVertex v) { e.outVertex = v; return this; diff --git a/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/structure/util/reference/ReferenceEdge.java b/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/structure/util/reference/ReferenceEdge.java index 6f716022ef..8f797b96b3 100644 --- a/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/structure/util/reference/ReferenceEdge.java +++ b/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/structure/util/reference/ReferenceEdge.java @@ -28,6 +28,8 @@ import org.apache.tinkerpop.gremlin.util.iterator.IteratorUtils; import java.util.Collections; import java.util.Iterator; +import java.util.LinkedHashSet; +import java.util.Set; /** * @author Marko A. Rodriguez (http://markorodriguez.com) @@ -36,6 +38,7 @@ public class ReferenceEdge extends ReferenceElement<Edge> implements Edge { private ReferenceVertex inVertex; private ReferenceVertex outVertex; + private Set<String> edgeLabels; private ReferenceEdge() { @@ -45,6 +48,11 @@ public class ReferenceEdge extends ReferenceElement<Edge> implements Edge { super(edge); this.inVertex = new ReferenceVertex(edge.inVertex()); this.outVertex = new ReferenceVertex(edge.outVertex()); + // Capture all labels from the source edge for multi-label support. + final Set<String> srcLabels = edge.labels(); + if (srcLabels.size() > 1) { + this.edgeLabels = new LinkedHashSet<>(srcLabels); + } } public ReferenceEdge(final Object id, final String label, final ReferenceVertex inVertex, final ReferenceVertex outVertex) { @@ -53,6 +61,29 @@ public class ReferenceEdge extends ReferenceElement<Edge> implements Edge { this.outVertex = outVertex; } + public ReferenceEdge(final Object id, final Set<String> labels, final ReferenceVertex inVertex, final ReferenceVertex outVertex) { + super(id, labels != null && !labels.isEmpty() ? labels.iterator().next() : Edge.DEFAULT_LABEL); + this.edgeLabels = labels != null ? new LinkedHashSet<>(labels) : null; + this.inVertex = inVertex; + this.outVertex = outVertex; + } + + @Override + public Set<String> labels() { + if (this.edgeLabels != null) { + return Collections.unmodifiableSet(this.edgeLabels); + } + return this.label != null ? Collections.singleton(this.label) : Collections.emptySet(); + } + + @Override + public String label() { + if (this.edgeLabels != null && !this.edgeLabels.isEmpty()) { + return this.edgeLabels.iterator().next(); + } + return this.label != null ? this.label : Edge.DEFAULT_LABEL; + } + @Override public <V> Property<V> property(final String key, final V value) { throw Element.Exceptions.propertyAdditionNotSupported(); diff --git a/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/structure/util/star/StarGraph.java b/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/structure/util/star/StarGraph.java index 8daaf64aef..1981a3257b 100644 --- a/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/structure/util/star/StarGraph.java +++ b/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/structure/util/star/StarGraph.java @@ -43,6 +43,7 @@ import java.util.Collections; import java.util.HashMap; import java.util.HashSet; import java.util.Iterator; +import java.util.LinkedHashSet; import java.util.List; import java.util.Map; import java.util.NoSuchElementException; @@ -206,6 +207,12 @@ public final class StarGraph implements Graph, Serializable { final StarGraph starGraph = new StarGraph(); final StarVertex starVertex = (StarVertex) starGraph.addVertex(T.id, vertex.id(), T.label, vertex.label()); + // Copy multi-labels from source vertex + final Set<String> srcLabels = vertex.labels(); + if (srcLabels.size() > 1) { + starVertex.setLabels(srcLabels); + } + final boolean supportsMetaProperties = vertex.graph().features().vertex().supportsMetaProperties(); vertex.properties().forEachRemaining(vp -> { @@ -216,11 +223,19 @@ public final class StarGraph implements Graph, Serializable { vertex.edges(Direction.IN).forEachRemaining(edge -> { final Edge starEdge = starVertex.addInEdge(edge.label(), starGraph.addVertex(T.id, edge.outVertex().id()), T.id, edge.id()); edge.properties().forEachRemaining(p -> starEdge.property(p.key(), p.value())); + final Set<String> edgeSrcLabels = edge.labels(); + if (edgeSrcLabels.size() > 1) { + ((StarGraph.StarEdge) starEdge).setLabels(edgeSrcLabels); + } }); vertex.edges(Direction.OUT).forEachRemaining(edge -> { final Edge starEdge = starVertex.addOutEdge(edge.label(), starGraph.addVertex(T.id, edge.inVertex().id()), T.id, edge.id()); edge.properties().forEachRemaining(p -> starEdge.property(p.key(), p.value())); + final Set<String> edgeSrcLabels = edge.labels(); + if (edgeSrcLabels.size() > 1) { + ((StarGraph.StarEdge) starEdge).setLabels(edgeSrcLabels); + } }); return starGraph; } @@ -308,7 +323,7 @@ public final class StarGraph implements Graph, Serializable { public abstract class StarElement<E extends Element> implements Element, Attachable<E> { protected final Object id; - protected final String label; + protected String label; protected StarElement(final Object id, final String label) { this.id = id; @@ -352,12 +367,35 @@ public final class StarGraph implements Graph, Serializable { public final class StarVertex extends StarElement<Vertex> implements Vertex { + private Set<String> vertexLabels; protected Map<String, List<Edge>> outEdges = null; protected Map<String, List<Edge>> inEdges = null; protected Map<String, List<VertexProperty>> vertexProperties = null; public StarVertex(final Object id, final String label) { super(id, label); + this.vertexLabels = new LinkedHashSet<>(); + this.vertexLabels.add(internStrings ? label.intern() : label); + } + + @Override + public Set<String> labels() { + return Collections.unmodifiableSet(this.vertexLabels); + } + + @Override + public String label() { + return this.vertexLabels.isEmpty() ? this.label : this.vertexLabels.iterator().next(); + } + + /** + * Sets the vertex labels from a source set. Used when copying labels from a multi-label vertex. + */ + public void setLabels(final Set<String> labels) { + if (labels != null && !labels.isEmpty()) { + this.vertexLabels = new LinkedHashSet<>(labels); + this.label = this.vertexLabels.iterator().next(); + } } private void dropEdgeProperty(Object id) { @@ -774,11 +812,34 @@ public final class StarGraph implements Graph, Serializable { public abstract class StarEdge extends StarElement<Edge> implements Edge { + private Set<String> edgeLabels; protected final Object otherId; private StarEdge(final Object id, final String label, final Object otherId) { super(id, label); this.otherId = otherId; + this.edgeLabels = new LinkedHashSet<>(); + this.edgeLabels.add(internStrings ? label.intern() : label); + } + + @Override + public Set<String> labels() { + return Collections.unmodifiableSet(this.edgeLabels); + } + + @Override + public String label() { + return this.edgeLabels.isEmpty() ? this.label : this.edgeLabels.iterator().next(); + } + + /** + * Sets the edge labels from a source set. Used when copying labels from a multi-label edge. + */ + public void setLabels(final Set<String> labels) { + if (labels != null && !labels.isEmpty()) { + this.edgeLabels = new LinkedHashSet<>(labels); + this.label = this.edgeLabels.iterator().next(); + } } @Override diff --git a/gremlin-language/src/main/antlr4/Gremlin.g4 b/gremlin-language/src/main/antlr4/Gremlin.g4 index af76bc0593..2d7fa316a6 100644 --- a/gremlin-language/src/main/antlr4/Gremlin.g4 +++ b/gremlin-language/src/main/antlr4/Gremlin.g4 @@ -111,7 +111,9 @@ traversalSourceSpawnMethod ; traversalSourceSpawnMethod_addE - : K_ADDE LPAREN stringArgument RPAREN + : K_ADDE LPAREN RPAREN + | K_ADDE LPAREN stringArgument RPAREN + | K_ADDE LPAREN stringArgument COMMA stringArgument (COMMA stringArgument)* RPAREN | K_ADDE LPAREN nestedTraversal RPAREN ; @@ -325,7 +327,9 @@ traversalMethod_E ; traversalMethod_addE - : K_ADDE LPAREN stringArgument RPAREN #traversalMethod_addE_String + : K_ADDE LPAREN RPAREN #traversalMethod_addE_Empty + | K_ADDE LPAREN stringArgument RPAREN #traversalMethod_addE_String + | K_ADDE LPAREN stringArgument (COMMA stringArgument)+ RPAREN #traversalMethod_addE_StringVarargs | K_ADDE LPAREN nestedTraversal RPAREN #traversalMethod_addE_Traversal ; diff --git a/tinkergraph-gremlin/src/main/java/org/apache/tinkerpop/gremlin/tinkergraph/structure/AbstractTinkerGraph.java b/tinkergraph-gremlin/src/main/java/org/apache/tinkerpop/gremlin/tinkergraph/structure/AbstractTinkerGraph.java index cf9f72bd14..b2b5b99281 100644 --- a/tinkergraph-gremlin/src/main/java/org/apache/tinkerpop/gremlin/tinkergraph/structure/AbstractTinkerGraph.java +++ b/tinkergraph-gremlin/src/main/java/org/apache/tinkerpop/gremlin/tinkergraph/structure/AbstractTinkerGraph.java @@ -292,6 +292,26 @@ public abstract class AbstractTinkerGraph implements Graph { protected abstract void addInEdge(final TinkerVertex vertex, final String label, final Edge edge); + /** + * Add an edge to the per-vertex adjacency maps under the given label. + * Used when labels are added to an existing edge. + * + * @param edge the edge to register + * @param label the label under which to register the edge + * @since 4.0.0 + */ + public abstract void addEdgeToAdjacency(final TinkerEdge edge, final String label); + + /** + * Remove an edge from the per-vertex adjacency maps for the given label. + * Used when labels are removed from an existing edge. + * + * @param edge the edge to unregister + * @param label the label from which to unregister the edge + * @since 4.0.0 + */ + public abstract void removeEdgeFromAdjacency(final TinkerEdge edge, final String label); + /** * Called when a vertex's labels are modified to allow the graph to update any internal label indices. * The default implementation is a no-op since TinkerGraph does not maintain a separate label index. diff --git a/tinkergraph-gremlin/src/main/java/org/apache/tinkerpop/gremlin/tinkergraph/structure/TinkerEdge.java b/tinkergraph-gremlin/src/main/java/org/apache/tinkerpop/gremlin/tinkergraph/structure/TinkerEdge.java index 3ced83454e..b37913a0aa 100644 --- a/tinkergraph-gremlin/src/main/java/org/apache/tinkerpop/gremlin/tinkergraph/structure/TinkerEdge.java +++ b/tinkergraph-gremlin/src/main/java/org/apache/tinkerpop/gremlin/tinkergraph/structure/TinkerEdge.java @@ -30,6 +30,7 @@ import org.apache.tinkerpop.gremlin.util.iterator.IteratorUtils; import java.util.Collections; import java.util.Iterator; +import java.util.LinkedHashSet; import java.util.Map; import java.util.Set; import java.util.concurrent.ConcurrentHashMap; @@ -40,6 +41,7 @@ import java.util.stream.Collectors; */ public class TinkerEdge extends TinkerElement implements Edge { + protected final Set<String> edgeLabels; protected Map<String, Property> properties; protected Vertex inVertex = null; @@ -62,8 +64,30 @@ public class TinkerEdge extends TinkerElement implements Edge { } } + protected TinkerEdge(final Object id, final Vertex outVertex, final Set<String> labels, final Vertex inVertex) { + this(id, outVertex, labels, inVertex, 0); + } + + protected TinkerEdge(final Object id, final Vertex outVertex, final Set<String> labels, final Vertex inVertex, final long currentVersion) { + this(id, (AbstractTinkerGraph) outVertex.graph(), outVertex.id(), + (labels == null || labels.isEmpty()) ? Edge.DEFAULT_LABEL : labels.iterator().next(), + inVertex.id(), currentVersion, false); + this.edgeLabels.clear(); + if (labels != null && !labels.isEmpty()) { + this.edgeLabels.addAll(labels); + } else { + this.edgeLabels.add(Edge.DEFAULT_LABEL); + } + if (!isTxMode) { + this.inVertex = inVertex; + this.outVertex = outVertex; + } + } + private TinkerEdge(final Object id, AbstractTinkerGraph graph, final Object outVertexId, final String label, final Object inVertexId, final long currentVersion, final Boolean skipIndexUpdate) { super(id, label, currentVersion); + this.edgeLabels = new LinkedHashSet<>(); + this.edgeLabels.add(label); isTxMode = graph instanceof TinkerTransactionGraph; this.graph = graph; if (isTxMode) { @@ -107,7 +131,66 @@ public class TinkerEdge extends TinkerElement implements Edge { @Override public Set<String> labels() { - return Collections.singleton(this.label); + return Collections.unmodifiableSet(this.edgeLabels); + } + + @Override + @Deprecated + public String label() { + return this.edgeLabels.iterator().next(); + } + + @Override + public void addLabel(final String label, final String... labels) { + graph.touch(this); + ElementHelper.validateLabel(label); + for (final String l : labels) { + ElementHelper.validateLabel(l); + } + // Remove default label if it was the only label + if (this.edgeLabels.size() == 1 && this.edgeLabels.contains(Edge.DEFAULT_LABEL)) { + this.graph.removeEdgeFromAdjacency(this, Edge.DEFAULT_LABEL); + this.edgeLabels.remove(Edge.DEFAULT_LABEL); + } + this.edgeLabels.add(label); + this.graph.addEdgeToAdjacency(this, label); + for (final String l : labels) { + this.edgeLabels.add(l); + this.graph.addEdgeToAdjacency(this, l); + } + this.label = this.edgeLabels.iterator().next(); + } + + @Override + public void dropLabels() { + graph.touch(this); + for (final String l : new LinkedHashSet<>(this.edgeLabels)) { + this.graph.removeEdgeFromAdjacency(this, l); + } + this.edgeLabels.clear(); + this.edgeLabels.add(Edge.DEFAULT_LABEL); + this.graph.addEdgeToAdjacency(this, Edge.DEFAULT_LABEL); + this.label = this.edgeLabels.iterator().next(); + } + + @Override + public void dropLabel(final String label, final String... labels) { + graph.touch(this); + if (this.edgeLabels.contains(label)) { + this.graph.removeEdgeFromAdjacency(this, label); + this.edgeLabels.remove(label); + } + for (final String l : labels) { + if (this.edgeLabels.contains(l)) { + this.graph.removeEdgeFromAdjacency(this, l); + this.edgeLabels.remove(l); + } + } + if (this.edgeLabels.isEmpty()) { + this.edgeLabels.add(Edge.DEFAULT_LABEL); + this.graph.addEdgeToAdjacency(this, Edge.DEFAULT_LABEL); + } + this.label = this.edgeLabels.iterator().next(); } @Override @@ -129,11 +212,15 @@ public class TinkerEdge extends TinkerElement implements Edge { if (!isTxMode) { // shallow copy for non-tx mode final TinkerEdge edge = new TinkerEdge(id, outVertex, label, inVertex, currentVersion); + edge.edgeLabels.clear(); + edge.edgeLabels.addAll(this.edgeLabels); edge.properties = properties; return edge; } final TinkerEdge edge = new TinkerEdge(id, graph, outVertexId, label, inVertexId, currentVersion, true); + edge.edgeLabels.clear(); + edge.edgeLabels.addAll(this.edgeLabels); if (properties != null) { final Map<String, Property> cloned = new ConcurrentHashMap<>(properties.size()); diff --git a/tinkergraph-gremlin/src/main/java/org/apache/tinkerpop/gremlin/tinkergraph/structure/TinkerElement.java b/tinkergraph-gremlin/src/main/java/org/apache/tinkerpop/gremlin/tinkergraph/structure/TinkerElement.java index 8aa2d1db03..d8587b71fa 100644 --- a/tinkergraph-gremlin/src/main/java/org/apache/tinkerpop/gremlin/tinkergraph/structure/TinkerElement.java +++ b/tinkergraph-gremlin/src/main/java/org/apache/tinkerpop/gremlin/tinkergraph/structure/TinkerElement.java @@ -27,7 +27,7 @@ import org.apache.tinkerpop.gremlin.structure.util.ElementHelper; public abstract class TinkerElement implements Element { protected final Object id; - protected final String label; + protected String label; protected boolean removed = false; protected long currentVersion; diff --git a/tinkergraph-gremlin/src/main/java/org/apache/tinkerpop/gremlin/tinkergraph/structure/TinkerGraph.java b/tinkergraph-gremlin/src/main/java/org/apache/tinkerpop/gremlin/tinkergraph/structure/TinkerGraph.java index f9b3bc4811..b780b71281 100644 --- a/tinkergraph-gremlin/src/main/java/org/apache/tinkerpop/gremlin/tinkergraph/structure/TinkerGraph.java +++ b/tinkergraph-gremlin/src/main/java/org/apache/tinkerpop/gremlin/tinkergraph/structure/TinkerGraph.java @@ -303,6 +303,36 @@ public class TinkerGraph extends AbstractTinkerGraph { edges.add(edge); } + @Override + public void addEdgeToAdjacency(final TinkerEdge edge, final String label) { + final TinkerVertex outV = (TinkerVertex) edge.outVertex(); + final TinkerVertex inV = (TinkerVertex) edge.inVertex(); + if (null == outV.outEdges) outV.outEdges = new HashMap<>(); + outV.outEdges.computeIfAbsent(label, k -> new HashSet<>()).add(edge); + if (null == inV.inEdges) inV.inEdges = new HashMap<>(); + inV.inEdges.computeIfAbsent(label, k -> new HashSet<>()).add(edge); + } + + @Override + public void removeEdgeFromAdjacency(final TinkerEdge edge, final String label) { + final TinkerVertex outV = (TinkerVertex) edge.outVertex(); + final TinkerVertex inV = (TinkerVertex) edge.inVertex(); + if (outV.outEdges != null) { + final Set<Edge> edges = outV.outEdges.get(label); + if (edges != null) { + edges.remove(edge); + if (edges.isEmpty()) outV.outEdges.remove(label); + } + } + if (inV.inEdges != null) { + final Set<Edge> edges = inV.inEdges.get(label); + if (edges != null) { + edges.remove(edge); + if (edges.isEmpty()) inV.inEdges.remove(label); + } + } + } + /** * Return TinkerGraph feature set. * <p/> diff --git a/tinkergraph-gremlin/src/main/java/org/apache/tinkerpop/gremlin/tinkergraph/structure/TinkerTransactionGraph.java b/tinkergraph-gremlin/src/main/java/org/apache/tinkerpop/gremlin/tinkergraph/structure/TinkerTransactionGraph.java index e6673e4be6..ad1c59a511 100644 --- a/tinkergraph-gremlin/src/main/java/org/apache/tinkerpop/gremlin/tinkergraph/structure/TinkerTransactionGraph.java +++ b/tinkergraph-gremlin/src/main/java/org/apache/tinkerpop/gremlin/tinkergraph/structure/TinkerTransactionGraph.java @@ -393,6 +393,40 @@ public final class TinkerTransactionGraph extends AbstractTinkerGraph { edges.add(edge.id()); } + @Override + public void addEdgeToAdjacency(final TinkerEdge edge, final String label) { + final TinkerVertex outV = (TinkerVertex) edge.outVertex(); + final TinkerVertex inV = (TinkerVertex) edge.inVertex(); + touch(outV); + touch(inV); + if (null == outV.outEdgesId) outV.outEdgesId = new ConcurrentHashMap<>(); + outV.outEdgesId.computeIfAbsent(label, k -> ConcurrentHashMap.newKeySet()).add(edge.id()); + if (null == inV.inEdgesId) inV.inEdgesId = new ConcurrentHashMap<>(); + inV.inEdgesId.computeIfAbsent(label, k -> ConcurrentHashMap.newKeySet()).add(edge.id()); + } + + @Override + public void removeEdgeFromAdjacency(final TinkerEdge edge, final String label) { + final TinkerVertex outV = (TinkerVertex) edge.outVertex(); + final TinkerVertex inV = (TinkerVertex) edge.inVertex(); + touch(outV); + touch(inV); + if (outV.outEdgesId != null) { + final Set<Object> edges = outV.outEdgesId.get(label); + if (edges != null) { + edges.remove(edge.id()); + if (edges.isEmpty()) outV.outEdgesId.remove(label); + } + } + if (inV.inEdgesId != null) { + final Set<Object> edges = inV.inEdgesId.get(label); + if (edges != null) { + edges.remove(edge.id()); + if (edges.isEmpty()) inV.inEdgesId.remove(label); + } + } + } + /** * Return TinkerGraph feature set. * <p/> diff --git a/tinkergraph-gremlin/src/main/java/org/apache/tinkerpop/gremlin/tinkergraph/structure/TinkerVertex.java b/tinkergraph-gremlin/src/main/java/org/apache/tinkerpop/gremlin/tinkergraph/structure/TinkerVertex.java index 67c5ad4692..f3a97beb13 100644 --- a/tinkergraph-gremlin/src/main/java/org/apache/tinkerpop/gremlin/tinkergraph/structure/TinkerVertex.java +++ b/tinkergraph-gremlin/src/main/java/org/apache/tinkerpop/gremlin/tinkergraph/structure/TinkerVertex.java @@ -61,53 +61,31 @@ public class TinkerVertex extends TinkerElement implements Vertex { protected final Set<String> vertexLabels; protected TinkerVertex(final Object id, final String label, final AbstractTinkerGraph graph) { - super(id, label); - this.graph = graph; - this.isTxMode = graph instanceof TinkerTransactionGraph; - this.allowNullPropertyValues = graph.features().vertex().supportsNullPropertyValues(); - this.vertexLabels = new LinkedHashSet<>(); - this.vertexLabels.add(null == label ? Vertex.DEFAULT_LABEL : label); + this(id, Collections.singleton(label != null ? label : Vertex.DEFAULT_LABEL), graph, -1); } protected TinkerVertex(final Object id, final String label, final AbstractTinkerGraph graph, final long currentVersion) { - super(id, label, currentVersion); - this.graph = graph; - this.isTxMode = graph instanceof TinkerTransactionGraph; - this.allowNullPropertyValues = graph.features().vertex().supportsNullPropertyValues(); - this.vertexLabels = new LinkedHashSet<>(); - this.vertexLabels.add(null == label ? Vertex.DEFAULT_LABEL : label); + this(id, Collections.singleton(label != null ? label : Vertex.DEFAULT_LABEL), graph, currentVersion); } /** * Constructs a TinkerVertex with multiple labels. */ protected TinkerVertex(final Object id, final Set<String> labels, final AbstractTinkerGraph graph) { - super(id, (labels == null || labels.isEmpty()) ? Vertex.DEFAULT_LABEL : labels.iterator().next()); - this.graph = graph; - this.isTxMode = graph instanceof TinkerTransactionGraph; - this.allowNullPropertyValues = graph.features().vertex().supportsNullPropertyValues(); - if (labels == null || labels.isEmpty()) { - this.vertexLabels = new LinkedHashSet<>(); - this.vertexLabels.add(Vertex.DEFAULT_LABEL); - } else { - this.vertexLabels = new LinkedHashSet<>(labels); - } + this(id, labels, graph, -1); } /** - * Constructs a TinkerVertex with multiple labels and a specific version (for transactional graphs). + * Canonical constructor. Constructs a TinkerVertex with multiple labels and a specific version (for transactional graphs). */ protected TinkerVertex(final Object id, final Set<String> labels, final AbstractTinkerGraph graph, final long currentVersion) { super(id, (labels == null || labels.isEmpty()) ? Vertex.DEFAULT_LABEL : labels.iterator().next(), currentVersion); this.graph = graph; this.isTxMode = graph instanceof TinkerTransactionGraph; this.allowNullPropertyValues = graph.features().vertex().supportsNullPropertyValues(); - if (labels == null || labels.isEmpty()) { - this.vertexLabels = new LinkedHashSet<>(); - this.vertexLabels.add(Vertex.DEFAULT_LABEL); - } else { - this.vertexLabels = new LinkedHashSet<>(labels); - } + this.vertexLabels = (labels == null || labels.isEmpty()) + ? new LinkedHashSet<>(Collections.singleton(Vertex.DEFAULT_LABEL)) + : new LinkedHashSet<>(labels); } @Override @@ -135,6 +113,7 @@ public class TinkerVertex extends TinkerElement implements Vertex { this.vertexLabels.add(label); Collections.addAll(this.vertexLabels, labels); + this.label = this.vertexLabels.iterator().next(); this.graph.updateVertexLabelIndex(this); } @@ -142,6 +121,7 @@ public class TinkerVertex extends TinkerElement implements Vertex { public void dropLabels() { this.vertexLabels.clear(); this.vertexLabels.add(Vertex.DEFAULT_LABEL); + this.label = this.vertexLabels.iterator().next(); this.graph.updateVertexLabelIndex(this); } @@ -155,6 +135,7 @@ public class TinkerVertex extends TinkerElement implements Vertex { if (this.vertexLabels.isEmpty()) { this.vertexLabels.add(Vertex.DEFAULT_LABEL); } + this.label = this.vertexLabels.iterator().next(); this.graph.updateVertexLabelIndex(this); } @@ -168,9 +149,7 @@ public class TinkerVertex extends TinkerElement implements Vertex { return vertex; } - final TinkerVertex vertex = new TinkerVertex(id, label, graph, currentVersion); - vertex.vertexLabels.clear(); - vertex.vertexLabels.addAll(this.vertexLabels); + final TinkerVertex vertex = new TinkerVertex(id, new LinkedHashSet<>(vertexLabels), graph, currentVersion); if (inEdgesId != null) vertex.inEdgesId = CollectionUtil.clone((ConcurrentHashMap<String, Set<Object>>) inEdgesId); diff --git a/tinkergraph-gremlin/src/test/java/org/apache/tinkerpop/gremlin/tinkergraph/process/traversal/step/sideEffect/LabelMutationStepTest.java b/tinkergraph-gremlin/src/test/java/org/apache/tinkerpop/gremlin/tinkergraph/process/traversal/step/sideEffect/LabelMutationStepTest.java index 58ff4fbce1..7ac5c88015 100644 --- a/tinkergraph-gremlin/src/test/java/org/apache/tinkerpop/gremlin/tinkergraph/process/traversal/step/sideEffect/LabelMutationStepTest.java +++ b/tinkergraph-gremlin/src/test/java/org/apache/tinkerpop/gremlin/tinkergraph/process/traversal/step/sideEffect/LabelMutationStepTest.java @@ -78,12 +78,14 @@ public class LabelMutationStepTest { assertThat(v.labels(), containsInAnyOrder("person", "manager")); } - @Test(expected = UnsupportedOperationException.class) - public void shouldThrowWhenAddingLabelToEdgeViaTraversal() { + @Test + public void shouldAddLabelToEdgeViaTraversal() { final Vertex v1 = g.addV("person").next(); final Vertex v2 = g.addV("person").next(); - v1.addEdge("knows", v2); + final Edge e = v1.addEdge("knows", v2); g.E().addLabel("friend").iterate(); + assertThat(e.labels(), hasSize(2)); + assertThat(e.labels(), containsInAnyOrder("knows", "friend")); } // --- dropLabel step tests --- @@ -112,12 +114,14 @@ public class LabelMutationStepTest { assertThat(v.labels(), containsInAnyOrder("employee")); } - @Test(expected = UnsupportedOperationException.class) - public void shouldThrowWhenDroppingLabelsOnEdgeViaTraversal() { + @Test + public void shouldDropLabelsOnEdgeViaTraversal() { final Vertex v1 = g.addV("person").next(); final Vertex v2 = g.addV("person").next(); - v1.addEdge("knows", v2); + final Edge e = v1.addEdge("knows", v2); g.E().dropLabels().iterate(); + assertThat(e.labels(), hasSize(1)); + assertThat(e.labels(), containsInAnyOrder(Edge.DEFAULT_LABEL)); } // --- addV multi-label tests --- diff --git a/tinkergraph-gremlin/src/test/java/org/apache/tinkerpop/gremlin/tinkergraph/structure/TinkerVertexMultiLabelTest.java b/tinkergraph-gremlin/src/test/java/org/apache/tinkerpop/gremlin/tinkergraph/structure/TinkerVertexMultiLabelTest.java index 5fc84f8de2..27387d60e9 100644 --- a/tinkergraph-gremlin/src/test/java/org/apache/tinkerpop/gremlin/tinkergraph/structure/TinkerVertexMultiLabelTest.java +++ b/tinkergraph-gremlin/src/test/java/org/apache/tinkerpop/gremlin/tinkergraph/structure/TinkerVertexMultiLabelTest.java @@ -147,20 +147,24 @@ public class TinkerVertexMultiLabelTest { assertThat(v.labels(), containsInAnyOrder(Vertex.DEFAULT_LABEL)); } - @Test(expected = UnsupportedOperationException.class) - public void shouldThrowWhenAddingLabelToEdge() { + @Test + public void shouldAddLabelToEdge() { final Vertex v1 = g.addV("person").next(); final Vertex v2 = g.addV("person").next(); final Edge e = v1.addEdge("knows", v2); e.addLabel("friend"); + assertThat(e.labels(), hasSize(2)); + assertThat(e.labels(), containsInAnyOrder("knows", "friend")); } - @Test(expected = UnsupportedOperationException.class) - public void shouldThrowWhenDroppingLabelsOnEdge() { + @Test + public void shouldDropLabelsOnEdge() { final Vertex v1 = g.addV("person").next(); final Vertex v2 = g.addV("person").next(); final Edge e = v1.addEdge("knows", v2); e.dropLabels(); + assertThat(e.labels(), hasSize(1)); + assertThat(e.labels(), containsInAnyOrder(Edge.DEFAULT_LABEL)); } @Test
