Skip to content

Commit a4e433e

Browse files
committed
Add Char, Duration, Binary support to gremlin-lang. Squash with GLV implemenetations later
1 parent bc9621f commit a4e433e

7 files changed

Lines changed: 286 additions & 3 deletions

File tree

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

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1343,7 +1343,19 @@ protected void notImplemented(final ParseTree ctx) {
13431343
* {@inheritDoc}
13441344
*/
13451345
@Override public T visitUuidLiteral(final GremlinParser.UuidLiteralContext ctx) { notImplemented(ctx); return null; }
1346-
/**
1346+
/**
1347+
* {@inheritDoc}
1348+
*/
1349+
@Override public T visitCharacterLiteral(final GremlinParser.CharacterLiteralContext ctx) { notImplemented(ctx); return null; }
1350+
/**
1351+
* {@inheritDoc}
1352+
*/
1353+
@Override public T visitDurationLiteral(final GremlinParser.DurationLiteralContext ctx) { notImplemented(ctx); return null; }
1354+
/**
1355+
* {@inheritDoc}
1356+
*/
1357+
@Override public T visitBinaryLiteral(final GremlinParser.BinaryLiteralContext ctx) { notImplemented(ctx); return null; }
1358+
/**
13471359
* {@inheritDoc}
13481360
*/
13491361
@Override public T visitPageRankStringConstant(final GremlinParser.PageRankStringConstantContext ctx) { notImplemented(ctx); return null; }

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

Lines changed: 50 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,8 +31,12 @@
3131

3232
import java.math.BigDecimal;
3333
import java.math.BigInteger;
34+
import java.nio.ByteBuffer;
35+
import java.time.Duration;
3436
import java.time.OffsetDateTime;
37+
import java.time.format.DateTimeParseException;
3538
import java.util.ArrayList;
39+
import java.util.Base64;
3640
import java.util.HashSet;
3741
import java.util.LinkedHashMap;
3842
import java.util.List;
@@ -518,6 +522,46 @@ public Object visitUuidLiteral(final GremlinParser.UuidLiteralContext ctx) {
518522
return UUID.fromString((String) antlr.genericVisitor.visitStringLiteral(ctx.stringLiteral()));
519523
}
520524

525+
/**
526+
* {@inheritDoc}
527+
*/
528+
@Override
529+
public Object visitCharacterLiteral(final GremlinParser.CharacterLiteralContext ctx) {
530+
final String text = ctx.getText();
531+
// strip the 'c' suffix, then strip quotes, then unescape
532+
final String withoutSuffix = text.substring(0, text.length() - 1);
533+
final String inner = StringEscapeUtils.unescapeJava(stripQuotes(withoutSuffix));
534+
return inner.charAt(0);
535+
}
536+
537+
/**
538+
* {@inheritDoc}
539+
*/
540+
@Override
541+
public Object visitDurationLiteral(final GremlinParser.DurationLiteralContext ctx) {
542+
final String iso8601 = (String) antlr.genericVisitor.visitStringLiteral(ctx.stringLiteral());
543+
try {
544+
return Duration.parse(iso8601);
545+
} catch (DateTimeParseException e) {
546+
throw new GremlinParserException(
547+
String.format("Invalid Duration literal: '%s' is not a valid ISO-8601 duration", iso8601));
548+
}
549+
}
550+
551+
/**
552+
* {@inheritDoc}
553+
*/
554+
@Override
555+
public Object visitBinaryLiteral(final GremlinParser.BinaryLiteralContext ctx) {
556+
final String base64 = (String) antlr.genericVisitor.visitStringLiteral(ctx.stringLiteral());
557+
try {
558+
return ByteBuffer.wrap(Base64.getDecoder().decode(base64));
559+
} catch (IllegalArgumentException e) {
560+
throw new GremlinParserException(
561+
String.format("Invalid Binary literal: '%s' is not valid base64", base64));
562+
}
563+
}
564+
521565
/**
522566
* {@inheritDoc}
523567
*/
@@ -533,7 +577,12 @@ public Object visitNumericLiteral(final GremlinParser.NumericLiteralContext ctx)
533577
*/
534578
@Override
535579
public Object visitStringLiteral(final GremlinParser.StringLiteralContext ctx) {
536-
return StringEscapeUtils.unescapeJava(stripQuotes(ctx.getText()));
580+
final String text = ctx.getText();
581+
// handle explicit 's' suffix for string literals
582+
if (ctx.StringSuffixLiteral() != null || ctx.EmptyStringSuffixLiteral() != null) {
583+
return StringEscapeUtils.unescapeJava(stripQuotes(text.substring(0, text.length() - 1)));
584+
}
585+
return StringEscapeUtils.unescapeJava(stripQuotes(text));
537586
}
538587

539588
/**

gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/process/traversal/GremlinLang.java

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,8 @@
3737
import java.util.Date;
3838
import java.math.BigDecimal;
3939
import java.math.BigInteger;
40+
import java.nio.ByteBuffer;
41+
import java.time.Duration;
4042
import java.time.OffsetDateTime;
4143
import java.util.ArrayList;
4244
import java.util.HashMap;
@@ -48,6 +50,7 @@
4850
import java.util.Objects;
4951
import java.util.Set;
5052
import java.util.UUID;
53+
import java.util.Base64;
5154
import java.util.concurrent.atomic.AtomicInteger;
5255

5356
import static org.apache.tinkerpop.gremlin.util.DatetimeHelper.format;
@@ -147,6 +150,25 @@ private String argAsString(final Object arg) {
147150
return String.format("UUID(\"%s\")", arg);
148151
}
149152

153+
if (arg instanceof Character)
154+
return String.format("\"%s\"c", StringEscapeUtils.escapeJava(arg.toString()));
155+
156+
if (arg instanceof Duration)
157+
return String.format("Duration(\"%s\")", arg);
158+
159+
if (arg instanceof ByteBuffer) {
160+
// duplicate() shares the underlying data but gives an independent position cursor,
161+
// so reading bytes for base64 encoding doesn't mutate the caller's buffer state
162+
final ByteBuffer buf = ((ByteBuffer) arg).duplicate();
163+
final byte[] bytes = new byte[buf.remaining()];
164+
buf.get(bytes);
165+
return String.format("Binary(\"%s\")", Base64.getEncoder().encodeToString(bytes));
166+
}
167+
168+
if (arg instanceof byte[]) {
169+
return String.format("Binary(\"%s\")", Base64.getEncoder().encodeToString((byte[]) arg));
170+
}
171+
150172
if (arg instanceof Enum) {
151173
// special handling for enums with additional interfaces
152174
if (arg instanceof T)

gremlin-core/src/test/java/org/apache/tinkerpop/gremlin/language/grammar/GeneralLiteralVisitorTest.java

Lines changed: 127 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,8 @@
3333

3434
import java.lang.reflect.Constructor;
3535
import java.math.BigInteger;
36+
import java.nio.ByteBuffer;
37+
import java.time.Duration;
3638
import java.time.LocalDateTime;
3739
import java.time.OffsetDateTime;
3840
import java.time.ZoneOffset;
@@ -51,6 +53,7 @@
5153
import static org.hamcrest.number.OrderingComparison.lessThan;
5254
import static org.junit.Assert.assertEquals;
5355
import static org.junit.Assert.assertTrue;
56+
import static org.junit.Assert.fail;
5457

5558
/**
5659
* Generic Literal visitor test
@@ -238,6 +241,11 @@ public static Iterable<Object[]> generateTestParameters() {
238241
{"'abc\\u2300def'", "abc\u2300def"},
239242
{"'\u2300'", "\u2300"},
240243
{"'abc\u2300def'", "abc\u2300def"},
244+
// explicit 's' suffix for string literals
245+
{"\"hello\"s", "hello"},
246+
{"'hello's", "hello"},
247+
{"\"\"s", "Empty"},
248+
{"\"a\"s", "a"},
241249
});
242250
}
243251

@@ -880,4 +888,123 @@ public void shouldParse() {
880888
assertEquals(expected, new GenericLiteralVisitor(new GremlinAntlrToJava()).visitTraversalCardinality(ctx));
881889
}
882890
}
891+
892+
@RunWith(Parameterized.class)
893+
public static class ValidCharacterLiteralTest {
894+
@Parameterized.Parameter(value = 0)
895+
public String script;
896+
897+
@Parameterized.Parameter(value = 1)
898+
public Character expected;
899+
900+
@Parameterized.Parameters(name = "{0}")
901+
public static Iterable<Object[]> generateTestParameters() {
902+
return Arrays.asList(new Object[][]{
903+
{"\"a\"c", 'a'},
904+
{"'a'c", 'a'},
905+
{"\"\\\"\"c", '"'},
906+
{"'\\''c", '\''},
907+
{"\"\\\\\"c", '\\'},
908+
});
909+
}
910+
911+
@Test
912+
public void shouldParse() {
913+
final GremlinLexer lexer = new GremlinLexer(CharStreams.fromString(script));
914+
final GremlinParser parser = new GremlinParser(new CommonTokenStream(lexer));
915+
final GremlinParser.CharacterLiteralContext ctx = parser.characterLiteral();
916+
assertEquals(expected, new GenericLiteralVisitor(new GremlinAntlrToJava()).visitCharacterLiteral(ctx));
917+
}
918+
}
919+
920+
@RunWith(Parameterized.class)
921+
public static class ValidDurationLiteralTest {
922+
@Parameterized.Parameter(value = 0)
923+
public String script;
924+
925+
@Parameterized.Parameter(value = 1)
926+
public Duration expected;
927+
928+
@Parameterized.Parameters(name = "{0}")
929+
public static Iterable<Object[]> generateTestParameters() {
930+
return Arrays.asList(new Object[][]{
931+
{"Duration(\"PT2H30M\")", Duration.ofHours(2).plusMinutes(30)},
932+
{"Duration(\"PT0S\")", Duration.ZERO},
933+
{"Duration(\"P1DT12H\")", Duration.ofDays(1).plusHours(12)},
934+
{"Duration(\"PT-30S\")", Duration.ofSeconds(-30)},
935+
});
936+
}
937+
938+
@Test
939+
public void shouldParse() {
940+
final GremlinLexer lexer = new GremlinLexer(CharStreams.fromString(script));
941+
final GremlinParser parser = new GremlinParser(new CommonTokenStream(lexer));
942+
final GremlinParser.DurationLiteralContext ctx = parser.durationLiteral();
943+
assertEquals(expected, new GenericLiteralVisitor(new GremlinAntlrToJava()).visitDurationLiteral(ctx));
944+
}
945+
}
946+
947+
public static class InvalidDurationLiteralTest {
948+
@Test
949+
public void shouldFailOnInvalidDuration() {
950+
final GremlinLexer lexer = new GremlinLexer(CharStreams.fromString("Duration(\"not-a-duration\")"));
951+
final GremlinParser parser = new GremlinParser(new CommonTokenStream(lexer));
952+
final GremlinParser.DurationLiteralContext ctx = parser.durationLiteral();
953+
try {
954+
new GenericLiteralVisitor(new GremlinAntlrToJava()).visitDurationLiteral(ctx);
955+
fail("Invalid Duration value should have thrown exception");
956+
} catch (GremlinParserException gpe) {
957+
assertThat(gpe.getMessage().contains("Invalid Duration literal:"), Matchers.is(true));
958+
}
959+
}
960+
961+
@Test(expected = GremlinParserException.class)
962+
public void shouldFailOnEmptyDuration() {
963+
final GremlinLexer lexer = new GremlinLexer(CharStreams.fromString("Duration(\"\")"));
964+
final GremlinParser parser = new GremlinParser(new CommonTokenStream(lexer));
965+
final GremlinParser.DurationLiteralContext ctx = parser.durationLiteral();
966+
new GenericLiteralVisitor(new GremlinAntlrToJava()).visitDurationLiteral(ctx);
967+
}
968+
}
969+
970+
@RunWith(Parameterized.class)
971+
public static class ValidBinaryLiteralTest {
972+
@Parameterized.Parameter(value = 0)
973+
public String script;
974+
975+
@Parameterized.Parameter(value = 1)
976+
public ByteBuffer expected;
977+
978+
@Parameterized.Parameters(name = "{0}")
979+
public static Iterable<Object[]> generateTestParameters() {
980+
return Arrays.asList(new Object[][]{
981+
{"Binary(\"AQID\")", ByteBuffer.wrap(new byte[]{1, 2, 3})},
982+
{"Binary(\"\")", ByteBuffer.wrap(new byte[]{})},
983+
{"Binary(\"AA==\")", ByteBuffer.wrap(new byte[]{0})},
984+
});
985+
}
986+
987+
@Test
988+
public void shouldParse() {
989+
final GremlinLexer lexer = new GremlinLexer(CharStreams.fromString(script));
990+
final GremlinParser parser = new GremlinParser(new CommonTokenStream(lexer));
991+
final GremlinParser.BinaryLiteralContext ctx = parser.binaryLiteral();
992+
assertEquals(expected, new GenericLiteralVisitor(new GremlinAntlrToJava()).visitBinaryLiteral(ctx));
993+
}
994+
}
995+
996+
public static class InvalidBinaryLiteralTest {
997+
@Test
998+
public void shouldFailOnInvalidBase64() {
999+
final GremlinLexer lexer = new GremlinLexer(CharStreams.fromString("Binary(\"!!!not-base64\")"));
1000+
final GremlinParser parser = new GremlinParser(new CommonTokenStream(lexer));
1001+
final GremlinParser.BinaryLiteralContext ctx = parser.binaryLiteral();
1002+
try {
1003+
new GenericLiteralVisitor(new GremlinAntlrToJava()).visitBinaryLiteral(ctx);
1004+
fail("Invalid Binary/base64 value should have thrown exception");
1005+
} catch (GremlinParserException gpe) {
1006+
assertThat(gpe.getMessage().contains("Invalid Binary literal:"), Matchers.is(true));
1007+
}
1008+
}
1009+
}
8831010
}

gremlin-core/src/test/java/org/apache/tinkerpop/gremlin/process/traversal/GremlinLangTest.java

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,8 @@
3737

3838
import java.math.BigDecimal;
3939
import java.math.BigInteger;
40+
import java.nio.ByteBuffer;
41+
import java.time.Duration;
4042
import java.util.Arrays;
4143
import java.util.Collections;
4244
import java.util.Date;
@@ -128,6 +130,19 @@ public static Iterable<Object[]> generateTestParameters() {
128130
{g.inject(GValue.of("x", "x")).V(GValue.of("ids", new int[]{1, 2, 3})), "g.inject(x).V(ids)"},
129131
{newG().inject(GValue.of(null, "test1"), GValue.of("xx2", "test2")), "g.inject(\"test1\",xx2)"},
130132
{newG().inject(new HashSet<>(Arrays.asList(1, 2))), "g.inject({1,2})"},
133+
// Character literals
134+
{g.inject('a'), "g.inject(\"a\"c)"},
135+
{g.inject('"'), "g.inject(\"\\\"\"c)"},
136+
{g.inject('\\'), "g.inject(\"\\\\\"c)"},
137+
// Duration literals
138+
{g.inject(Duration.ofHours(2).plusMinutes(30)), "g.inject(Duration(\"PT2H30M\"))"},
139+
{g.inject(Duration.ZERO), "g.inject(Duration(\"PT0S\"))"},
140+
{g.inject(Duration.ofSeconds(-30)), "g.inject(Duration(\"PT-30S\"))"},
141+
// Binary literals
142+
{g.inject(ByteBuffer.wrap(new byte[]{1, 2, 3})), "g.inject(Binary(\"AQID\"))"},
143+
{g.inject(ByteBuffer.wrap(new byte[]{})), "g.inject(Binary(\"\"))"},
144+
{g.inject(new byte[]{1, 2, 3}), "g.inject(Binary(\"AQID\"))"},
145+
{g.inject(new byte[]{}), "g.inject(Binary(\"\"))"},
131146
});
132147
}
133148

@@ -169,4 +184,19 @@ public void shouldAllowToUseSameParameterTwice() {
169184
assertEquals(value, gremlin.getParameters().get("ids"));
170185
}
171186
}
187+
188+
public static class BinaryTests {
189+
@Test
190+
public void shouldEncodeByteBufferWithPositionOffset() {
191+
final ByteBuffer buf = ByteBuffer.allocate(4);
192+
buf.put((byte) 0);
193+
buf.put((byte) 1);
194+
buf.put((byte) 2);
195+
buf.put((byte) 3);
196+
buf.flip();
197+
buf.get(); // advance position by 1, remaining is [1, 2, 3]
198+
final String gremlin = g.inject(buf).asAdmin().getGremlinLang().getGremlin();
199+
assertEquals("g.inject(Binary(\"AQID\"))", gremlin);
200+
}
201+
}
172202
}

0 commit comments

Comments
 (0)