Skip to content

Commit 9dc4903

Browse files
committed
Convert request bindings to gremlin-lang string format
Moving parameters from binary-serialized maps to string representations makes the request side pure text, decoupling Gremlin language evolution from GraphBinary versioning. New types can be introduced in minor/patch versions without touching GraphBinary, eliminating the need for a major version bump across the ecosystem for every new request-side type. The asParameter() fallback is replaced with an unsupportedType flag that records the class name and falls back to toString(). A flag is used rather than throwing because embedded Traversals build GremlinLang as a side effect but never send it, so unknown types must not break execution.
1 parent eabfcba commit 9dc4903

24 files changed

Lines changed: 1187 additions & 134 deletions

File tree

CHANGELOG.asciidoc

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,8 @@ image::https://raw.githubusercontent.com/apache/tinkerpop/master/docs/static/ima
2525
[[release-4-0-0]]
2626
=== TinkerPop 4.0.0 (Release Date: NOT OFFICIALLY RELEASED YET)
2727
28+
* Modified request parameters from `Map<String, Object>` to gremlin-lang compatible `String`.
29+
2830
[[release-4-0-0-beta-2]]
2931
=== TinkerPop 4.0.0-beta.2 (April 1, 2026)
3032

docs/src/upgrade/release-4.x.x.asciidoc

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,15 @@ image::gremlins-wildest-dreams.png[width=185]
3030
Please see the link:https://github.com/apache/tinkerpop/blob/4.0.0/CHANGELOG.asciidoc#release-4-0-0[changelog] for a
3131
complete list of all the modifications that are part of this release.
3232
33+
=== Upgrading for Users
34+
35+
==== Parameter Format
36+
37+
Parameters have replaced bindings. Parameters are no longer `Map<String, Object>` instead they are `String`. The string is gremlin-lang map.
38+
39+
Notably, because parameters now travel as maps, these maps must be gremlin-lang compatible. One of the things that aren't is
40+
`#jsr223` control flags. This means there will be gaps in gremlin-groovy support.
41+
3342
== TinkerPop 4.0.0-beta.2
3443
3544
*Release Date: April 1, 2026*

gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/language/grammar/GremlinQueryParser.java

Lines changed: 96 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -18,14 +18,17 @@
1818
*/
1919
package org.apache.tinkerpop.gremlin.language.grammar;
2020

21-
import org.antlr.v4.runtime.CharStream;
2221
import org.antlr.v4.runtime.CharStreams;
2322
import org.antlr.v4.runtime.CommonTokenStream;
2423
import org.antlr.v4.runtime.atn.PredictionMode;
2524
import org.apache.tinkerpop.gremlin.process.traversal.Traversal;
2625
import org.slf4j.Logger;
2726
import org.slf4j.LoggerFactory;
2827

28+
import java.util.Collection;
29+
import java.util.Map;
30+
import javax.lang.model.SourceVersion;
31+
2932
/**
3033
* Parses Gremlin strings to an {@code Object}, typically to a {@link Traversal}.
3134
*/
@@ -44,21 +47,15 @@ public static Object parse(final String query) {
4447
* Parse Gremlin string using a specified {@link GremlinAntlrToJava} object.
4548
*/
4649
public static Object parse(final String query, final GremlinVisitor<Object> visitor) {
47-
final CharStream in = CharStreams.fromString(query);
48-
final GremlinLexer lexer = new GremlinLexer(in);
49-
lexer.removeErrorListeners();
50-
lexer.addErrorListener(errorListener);
51-
50+
final GremlinLexer lexer = createLexer(query);
5251
final CommonTokenStream tokens = new CommonTokenStream(lexer);
5352

5453
// Setup error handler on parser
55-
final GremlinParser parser = new GremlinParser(tokens);
54+
final GremlinParser parser = createParser(tokens);
5655
// SLL prediction mode is faster than the LL prediction mode when parsing the grammar,
5756
// but it does not cover parsing all types of input. We use the SLL by default, and fallback
5857
// to LL mode if fails to parse the query.
5958
parser.getInterpreter().setPredictionMode(PredictionMode.SLL);
60-
parser.removeErrorListeners();
61-
parser.addErrorListener(errorListener);
6259

6360
GremlinParser.QueryListContext queryContext;
6461
try {
@@ -91,4 +88,94 @@ public static Object parse(final String query, final GremlinVisitor<Object> visi
9188
throw new GremlinParserException("Failed to interpret Gremlin query: " + ex.getMessage(), ex);
9289
}
9390
}
91+
92+
/**
93+
* Parses a gremlin-lang map literal string into a {@code Map<String, Object>} for use as parameters.
94+
* <p>
95+
* Uses {@link ParameterMapVisitor} to prevent traversal injection and validates that all keys are strings
96+
* and no values contain traversals.
97+
*
98+
* @param parameterMapString the gremlin-lang map literal string (e.g. {@code [x:1,y:"marko"]}) or {@code null}/empty
99+
* @return the parsed and validated parameter map
100+
* @throws GremlinParserException if parsing fails or validation detects invalid content
101+
*/
102+
public static Map<String, Object> parseParameters(final String parameterMapString) {
103+
if (parameterMapString == null || parameterMapString.isEmpty()) {
104+
return Map.of();
105+
}
106+
107+
final GremlinParser parser = createParser(parameterMapString);
108+
final GremlinParser.GenericMapLiteralContext mapCtx = parser.genericMapLiteral();
109+
110+
final ParameterMapVisitor visitor = new ParameterMapVisitor(new GremlinAntlrToJava());
111+
final Map<Object, Object> rawMap = (Map<Object, Object>) visitor.visitGenericMapLiteral(mapCtx);
112+
113+
if (rawMap == null) {
114+
return Map.of();
115+
}
116+
117+
for (final Map.Entry<?, ?> entry : rawMap.entrySet()) {
118+
if (!(entry.getKey() instanceof String)) {
119+
throw new GremlinParserException(
120+
String.format("Parameter map keys must be String, found: %s",
121+
entry.getKey() == null ? "null" : entry.getKey().getClass().getSimpleName()));
122+
}
123+
final String key = (String) entry.getKey();
124+
if (!SourceVersion.isIdentifier(key)) {
125+
throw new GremlinParserException(
126+
String.format("Parameter map key must be a valid identifier: %s", key));
127+
}
128+
validateParameterValue(entry.getValue());
129+
}
130+
131+
return (Map<String, Object>) (Map<?, ?>) rawMap;
132+
}
133+
134+
/**
135+
* Recursively validates that a parameter value does not contain a {@link Traversal}. Nested validation is needed
136+
* because steps like mergeV iterate map values, so a Traversal hiding inside a nested map or collection would still
137+
* be dangerous.
138+
*/
139+
private static void validateParameterValue(final Object value) {
140+
if (value instanceof Traversal) {
141+
throw new GremlinParserException("Traversals are not allowed as parameter values");
142+
}
143+
if (value instanceof Map) {
144+
for (final Object v : ((Map<?, ?>) value).values()) {
145+
validateParameterValue(v);
146+
}
147+
}
148+
if (value instanceof Collection) {
149+
for (final Object v : (Collection<?>) value) {
150+
validateParameterValue(v);
151+
}
152+
}
153+
}
154+
155+
/**
156+
* Creates a {@link GremlinParser} from the given input string.
157+
*/
158+
private static GremlinParser createParser(final String input) {
159+
return createParser(new CommonTokenStream(createLexer(input)));
160+
}
161+
162+
/**
163+
* Creates a {@link GremlinParser} from the given {@link GremlinLexer}.
164+
*/
165+
private static GremlinParser createParser(final CommonTokenStream tokens) {
166+
final GremlinParser parser = new GremlinParser(tokens);
167+
parser.removeErrorListeners();
168+
parser.addErrorListener(errorListener);
169+
return parser;
170+
}
171+
172+
/**
173+
* Creates a {@link GremlinLexer} from the given input string with error listeners configured.
174+
*/
175+
private static GremlinLexer createLexer(final String input) {
176+
final GremlinLexer lexer = new GremlinLexer(CharStreams.fromString(input));
177+
lexer.removeErrorListeners();
178+
lexer.addErrorListener(errorListener);
179+
return lexer;
180+
}
94181
}
Lines changed: 102 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,102 @@
1+
/*
2+
* Licensed to the Apache Software Foundation (ASF) under one
3+
* or more contributor license agreements. See the NOTICE file
4+
* distributed with this work for additional information
5+
* regarding copyright ownership. The ASF licenses this file
6+
* to you under the Apache License, Version 2.0 (the
7+
* "License"); you may not use this file except in compliance
8+
* with the License. You may obtain a copy of the License at
9+
*
10+
* http://www.apache.org/licenses/LICENSE-2.0
11+
*
12+
* Unless required by applicable law or agreed to in writing,
13+
* software distributed under the License is distributed on an
14+
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
15+
* KIND, either express or implied. See the License for the
16+
* specific language governing permissions and limitations
17+
* under the License.
18+
*/
19+
package org.apache.tinkerpop.gremlin.language.grammar;
20+
21+
/**
22+
* A visitor for parsing parameter map strings that prevents traversal injection.
23+
* <p>
24+
* Extends {@link GenericLiteralVisitor} and overrides traversal-related visit methods
25+
* to throw, ensuring that no traversal can be constructed or executed during the
26+
* parameter map parsing walk. This is critical for security because
27+
* {@code visitTerminatedTraversal} in the base class would execute the traversal
28+
* immediately via {@link TraversalTerminalMethodVisitor}.
29+
*/
30+
public class ParameterMapVisitor extends GenericLiteralVisitor {
31+
32+
private static final int DEFAULT_MAX_NESTING_DEPTH = 32;
33+
34+
private final int maxNestingDepth;
35+
private int currentNestingDepth = 0;
36+
37+
public ParameterMapVisitor(final GremlinAntlrToJava antlr) {
38+
this(antlr, DEFAULT_MAX_NESTING_DEPTH);
39+
}
40+
41+
public ParameterMapVisitor(final GremlinAntlrToJava antlr, final int maxNestingDepth) {
42+
super(antlr);
43+
this.maxNestingDepth = maxNestingDepth;
44+
}
45+
46+
/**
47+
* Overridden to prevent nested traversal construction in parameter maps.
48+
*/
49+
@Override
50+
public Object visitNestedTraversal(final GremlinParser.NestedTraversalContext ctx) {
51+
throw new GremlinParserException("Traversals are not allowed in parameter maps");
52+
}
53+
54+
/**
55+
* Overridden to prevent terminated traversal execution in parameter maps.
56+
* This is the critical override because the base class would execute the traversal
57+
* immediately via {@link TraversalTerminalMethodVisitor}.
58+
*/
59+
@Override
60+
public Object visitTerminatedTraversal(final GremlinParser.TerminatedTraversalContext ctx) {
61+
throw new GremlinParserException("Traversals are not allowed in parameter maps");
62+
}
63+
64+
@Override
65+
public Object visitGenericMapLiteral(final GremlinParser.GenericMapLiteralContext ctx) {
66+
currentNestingDepth++;
67+
if (currentNestingDepth > maxNestingDepth) {
68+
throw new GremlinParserException("Parameter map nesting depth exceeds maximum of " + maxNestingDepth);
69+
}
70+
try {
71+
return super.visitGenericMapLiteral(ctx);
72+
} finally {
73+
currentNestingDepth--;
74+
}
75+
}
76+
77+
@Override
78+
public Object visitGenericCollectionLiteral(final GremlinParser.GenericCollectionLiteralContext ctx) {
79+
currentNestingDepth++;
80+
if (currentNestingDepth > maxNestingDepth) {
81+
throw new GremlinParserException("Parameter map nesting depth exceeds maximum of " + maxNestingDepth);
82+
}
83+
try {
84+
return super.visitGenericCollectionLiteral(ctx);
85+
} finally {
86+
currentNestingDepth--;
87+
}
88+
}
89+
90+
@Override
91+
public Object visitGenericSetLiteral(final GremlinParser.GenericSetLiteralContext ctx) {
92+
currentNestingDepth++;
93+
if (currentNestingDepth > maxNestingDepth) {
94+
throw new GremlinParserException("Parameter map nesting depth exceeds maximum of " + maxNestingDepth);
95+
}
96+
try {
97+
return super.visitGenericSetLiteral(ctx);
98+
} finally {
99+
currentNestingDepth--;
100+
}
101+
}
102+
}

0 commit comments

Comments
 (0)