This is an automated email from the ASF dual-hosted git repository. spmallette pushed a commit to branch TINKERPOP-2596 in repository https://gitbox.apache.org/repos/asf/tinkerpop.git
commit de40eb9fe698696c26892f534dea1fe9c7e2d605 Author: Stephen Mallette <stepm...@amazon.com> AuthorDate: Fri Sep 10 15:45:55 2021 -0400 TINKERPOP-2596 Added datetime() to gremlin-language Added GroovyTranslator support by way of a new type serializer - in this way, standard Date/Timestamp translation of old can be preserved. --- CHANGELOG.asciidoc | 1 + docs/src/reference/gremlin-variants.asciidoc | 14 +++ docs/src/reference/the-traversal.asciidoc | 5 + docs/src/upgrade/release-3.5.x.asciidoc | 30 +++++ .../tinkerpop/gremlin/jsr223/CoreImports.java | 6 + .../language/grammar/GenericLiteralVisitor.java | 9 ++ .../traversal/translator/GroovyTranslator.java | 29 +++++ .../tinkerpop/gremlin/util/DatetimeHelper.java | 124 +++++++++++++++++++++ .../grammar/GeneralLiteralVisitorTest.java | 46 ++++++++ .../traversal/translator/GroovyTranslatorTest.java | 13 +++ .../tinkerpop/gremlin/util/DatetimeHelperTest.java | 105 +++++++++++++++++ .../server/GremlinServerHttpIntegrateTest.java | 14 +++ 12 files changed, 396 insertions(+) diff --git a/CHANGELOG.asciidoc b/CHANGELOG.asciidoc index 4f4eff4..457cf8d 100644 --- a/CHANGELOG.asciidoc +++ b/CHANGELOG.asciidoc @@ -28,6 +28,7 @@ image::https://raw.githubusercontent.com/apache/tinkerpop/master/docs/static/ima * Added the `ConnectedComponent` tokens required to properly process the `with()` of the `connectedComponent()` step. * Fixed `DotNetTranslator` bugs where translations produced Gremlin that failed due to ambiguous step calls to `has()`. * Fixed bug where `RepeatUnrollStrategy`, `InlineFilterStrategy` and `MessagePassingReductionStrategy` were all being applied more than necessary. +* Modified grammar to accept the `datetime()` function so that Gremlin scripts have a way to natively construct a `Date`. * Ensured `PathRetractionStrategy` is applied after `InlineFilterStrategy` which prevents an error in traverser mapping in certain conditions. * Deprecated `JsonBuilder` serialization for GraphSON and Gryo. * Allowed `null` string values in the Gremlin grammar. diff --git a/docs/src/reference/gremlin-variants.asciidoc b/docs/src/reference/gremlin-variants.asciidoc index 53778c5..3bd8029 100644 --- a/docs/src/reference/gremlin-variants.asciidoc +++ b/docs/src/reference/gremlin-variants.asciidoc @@ -670,6 +670,20 @@ In Groovy, `as`, `in`, and `not` are reserved words. Gremlin-Groovy does not all statically from the anonymous traversal `+__+` and therefore, must always be prefixed with `+__.+` For instance: `+g.V().as('a').in().as('b').where(__.not(__.as('a').out().as('b')))+` +Since Groovy has access to the full JVM as Java does, it is possible to construct `Date`-like objects directly, but +the Gremlin language does offer a `datetime()` function that is exposed in the Gremlin Console and as a function for +Gremlin scripts sent to Gremlin Server. The function accepts the following forms of dates and/or times using a default +time zone offset of UTC(+00:00): + +* `T00:35:44` +* `T00:35:44Z` +* `2018-03-22` +* `2018-03-22T00:35:44` +* `2018-03-22T00:35:44Z` +* `2018-03-22T00:35:44.741` +* `2018-03-22T00:35:44.741Z` +* `2018-03-22T00:35:44.741+1600` + [[gremlin-python]] == Gremlin-Python diff --git a/docs/src/reference/the-traversal.asciidoc b/docs/src/reference/the-traversal.asciidoc index 1b662eb..305c3ae 100644 --- a/docs/src/reference/the-traversal.asciidoc +++ b/docs/src/reference/the-traversal.asciidoc @@ -4886,4 +4886,9 @@ System.out.println(s.parameters); // OUTPUT: Optional[{_args_0=person, _args_2=marko, _args_1=name, _args_4=age, _args_3=knows}] ---- +Finally, the `GroovyTranslator` can take a `TypeTranslator` argument which allows some customization of how types get +converted to script form. The `DefaultTypeTranslator` is used if a specific implementation is not specified. A built-in +alternative to this implementation is the `LanguageTypeTranslator` which will prefer use of the Gremlin language +`datetime()` function rather than the JVM specific `Date` and `Timestamp` conversions. This translator can be helpful +when generating scripts that will be sent to Gremlin Server or Remote Graph Providers supporting the `datetime()` form. diff --git a/docs/src/upgrade/release-3.5.x.asciidoc b/docs/src/upgrade/release-3.5.x.asciidoc index a2e6d0a..ca3bf36 100644 --- a/docs/src/upgrade/release-3.5.x.asciidoc +++ b/docs/src/upgrade/release-3.5.x.asciidoc @@ -30,6 +30,36 @@ complete list of all the modifications that are part of this release. === Upgrading for Users +==== datetime() + +Gremlin in native programming languages can all construct a native date and time object. In Java, that would probably +be a `java.util.Date` object while in Javascript it would likely be the Node.js `Date`. In any of these cases, these +native objects would be serialized to millisecond-precision offset from the unix epoch to be sent over the wire to the +server (in embedded mode for Java, it would be up to the graph database to determine how the date is handled). + +The gap is in Gremlin scripts which do not have a way to natively construct dates and times other than by using Groovy +variants. As TinkerPop moves toward a more secure method of processing Gremlin scripts by way of the `gremlin-language` +model, it was clear that this gap needed to be filled. The new `datetime()` function can take a ISO-8601 formatted +datetime and internally produce a `Date` with a default time zone offset of UTC (+00:00). + +This functionality, while syntax of `gremlin-language`, is also exposed as a component of `gremlin-groovy` so that it +can be used in the Gremlin Console and through the `GremlinScriptEngine` in Gremlin Server. + +[source,text] +---- +gremlin> datetime('2022-10-02').toGMTString() +==>2 Oct 2022 00:00:00 GMT +gremlin> datetime('2022-10-02T00:00:00Z').toGMTString() +==>2 Oct 2022 00:00:00 GMT +gremlin> datetime('2022-10-02T00:00:00-0400').toGMTString() +==>2 Oct 2022 04:00:00 GMT +---- + +The above examples use the Java `Date` method `toGMTString()` to properly format the date for demonstration purposes. +From a Gremlin language perspective there are no functions that can be called on the return value of `datetime()`. + +See:link:https://issues.apache.org/jira/browse/TINKERPOP-2596[TINKERPOP-2596] + ==== Refinements to null Release 3.5.0 introduce the ability for there to be traversers that contained a `null` value. Since that time it has diff --git a/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/jsr223/CoreImports.java b/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/jsr223/CoreImports.java index 2897506..60865c7 100644 --- a/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/jsr223/CoreImports.java +++ b/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/jsr223/CoreImports.java @@ -140,6 +140,7 @@ import org.apache.tinkerpop.gremlin.structure.util.reference.ReferenceEdge; import org.apache.tinkerpop.gremlin.structure.util.reference.ReferenceProperty; import org.apache.tinkerpop.gremlin.structure.util.reference.ReferenceVertex; import org.apache.tinkerpop.gremlin.structure.util.reference.ReferenceVertexProperty; +import org.apache.tinkerpop.gremlin.util.DatetimeHelper; import org.apache.tinkerpop.gremlin.util.Gremlin; import org.apache.tinkerpop.gremlin.util.TimeUtil; import org.apache.tinkerpop.gremlin.util.function.Lambda; @@ -325,6 +326,11 @@ public final class CoreImports { uniqueMethods(Computer.class).forEach(METHOD_IMPORTS::add); uniqueMethods(TimeUtil.class).forEach(METHOD_IMPORTS::add); uniqueMethods(Lambda.class).forEach(METHOD_IMPORTS::add); + try { + METHOD_IMPORTS.add(DatetimeHelper.class.getMethod("datetime", String.class)); + } catch (Exception ex) { + throw new IllegalStateException("Could not load datetime() function to imports"); + } /////////// // ENUMS // diff --git a/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/language/grammar/GenericLiteralVisitor.java b/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/language/grammar/GenericLiteralVisitor.java index 5e7a6ca..1529392 100644 --- a/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/language/grammar/GenericLiteralVisitor.java +++ b/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/language/grammar/GenericLiteralVisitor.java @@ -26,6 +26,7 @@ import org.apache.tinkerpop.gremlin.process.traversal.step.TraversalOptionParent import org.apache.tinkerpop.gremlin.structure.Direction; import org.apache.tinkerpop.gremlin.structure.T; import org.apache.tinkerpop.gremlin.structure.VertexProperty; +import org.apache.tinkerpop.gremlin.util.DatetimeHelper; import java.math.BigDecimal; import java.math.BigInteger; @@ -389,6 +390,14 @@ public class GenericLiteralVisitor extends GremlinBaseVisitor<Object> { * {@inheritDoc} */ @Override + public Object visitDateLiteral(final GremlinParser.DateLiteralContext ctx) { + return DatetimeHelper.parse(getStringLiteral(ctx.stringLiteral())); + } + + /** + * {@inheritDoc} + */ + @Override public Object visitStringLiteral(final GremlinParser.StringLiteralContext ctx) { // Using Java string unescaping because it coincides with the Groovy rules: // https://docs.oracle.com/javase/tutorial/java/data/characters.html diff --git a/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/process/traversal/translator/GroovyTranslator.java b/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/process/traversal/translator/GroovyTranslator.java index c0a9f08..46234c0 100644 --- a/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/process/traversal/translator/GroovyTranslator.java +++ b/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/process/traversal/translator/GroovyTranslator.java @@ -39,11 +39,13 @@ import org.apache.tinkerpop.gremlin.structure.Edge; import org.apache.tinkerpop.gremlin.structure.Vertex; import org.apache.tinkerpop.gremlin.structure.VertexProperty; import org.apache.tinkerpop.gremlin.structure.util.StringFactory; +import org.apache.tinkerpop.gremlin.util.DatetimeHelper; import org.apache.tinkerpop.gremlin.util.function.Lambda; import java.math.BigDecimal; import java.math.BigInteger; import java.sql.Timestamp; +import java.time.Instant; import java.util.ArrayList; import java.util.Arrays; import java.util.Date; @@ -380,4 +382,31 @@ public final class GroovyTranslator implements Translator.ScriptTranslator { collect(Collectors.joining(", ")); } } + + /** + * An extension of the {@link DefaultTypeTranslator} that generates Gremlin that is compliant with + * {@code gremlin-language} scripts. Specifically, it will convert {@code Date} and {@code Timestamp} to use the + * {@code datetime()} function. Time zone offsets are resolved to where {@code 2018-03-22T00:35:44.741+1600} + * would be converted to {@code datetime('2018-03-21T08:35:44.741Z')}. More commonly {@code 2018-03-22} would simply + * generate {@code datetime('2018-03-22T00:00:00Z')}. + */ + public static class LanguageTypeTranslator extends DefaultTypeTranslator { + public LanguageTypeTranslator(final boolean withParameters) { + super(withParameters); + } + + @Override + protected String getSyntax(final Date o) { + return getDatetimeSyntax(o.toInstant()); + } + + @Override + protected String getSyntax(final Timestamp o) { + return getDatetimeSyntax(o.toInstant()); + } + + private static String getDatetimeSyntax(final Instant i) { + return String.format("datetime('%s')", DatetimeHelper.format(i)); + } + } } diff --git a/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/util/DatetimeHelper.java b/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/util/DatetimeHelper.java new file mode 100644 index 0000000..d52b176 --- /dev/null +++ b/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/util/DatetimeHelper.java @@ -0,0 +1,124 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.apache.tinkerpop.gremlin.util; + +import java.time.Instant; +import java.time.LocalDate; +import java.time.LocalDateTime; +import java.time.LocalTime; +import java.time.YearMonth; +import java.time.ZonedDateTime; +import java.time.format.DateTimeFormatter; +import java.time.format.DateTimeFormatterBuilder; +import java.time.format.ResolverStyle; +import java.time.temporal.ChronoField; +import java.time.temporal.TemporalAccessor; +import java.util.Date; + +import static java.time.ZoneOffset.UTC; +import static java.time.format.DateTimeFormatter.ISO_LOCAL_DATE; +import static java.time.format.DateTimeFormatter.ISO_LOCAL_DATE_TIME; +import static java.time.format.DateTimeFormatter.ISO_TIME; + +public final class DatetimeHelper { + + private static final DateTimeFormatter datetimeFormatter = new DateTimeFormatterBuilder() + .parseCaseInsensitive() + .append(ISO_LOCAL_DATE_TIME) + .optionalStart() + .appendOffset("+HHMMss", "Z").toFormatter(); + + private static final DateTimeFormatter timeFormatter = new DateTimeFormatterBuilder() + .parseCaseInsensitive() + .appendLiteral('T') + .append(ISO_TIME).toFormatter(); + + private static final DateTimeFormatter yearMonthFormatter = new DateTimeFormatterBuilder() + .parseCaseInsensitive() + .appendValue(ChronoField.YEAR) + .appendLiteral('-') + .appendValue(ChronoField.MONTH_OF_YEAR).toFormatter().withResolverStyle(ResolverStyle.LENIENT); + + private static final DateTimeFormatter formatter = new DateTimeFormatterBuilder() + .appendOptional(datetimeFormatter) + .appendOptional(ISO_LOCAL_DATE) + .appendOptional(timeFormatter) + .appendOptional(yearMonthFormatter) + .toFormatter(); + + private DatetimeHelper() {} + + /** + * Formats an {@code Instant} to a form of {@code 2018-03-22T00:35:44Z} at UTC. + */ + public static String format(final Instant d) { + return datetimeFormatter.format(d.atZone(UTC)); + } + + /** + * Parses a {@code String} representing a date and/or time to a {@code Date} object with a default time zone offset + * of UTC (+00:00). It can parse dates in any of the following formats. + * + * <ul> + * <li>T00:35:44</li> + * <li>T00:35:44Z</li> + * <li>2018-03-22</li> + * <li>2018-03-22T00:35:44</li> + * <li>2018-03-22T00:35:44Z</li> + * <li>2018-03-22T00:35:44.741</li> + * <li>2018-03-22T00:35:44.741Z</li> + * <li>2018-03-22T00:35:44.741+1600</li> + * </ul>> + * + */ + public static Date parse(final String d) { + final TemporalAccessor t = formatter.parse(d); + + if (!t.isSupported(ChronoField.HOUR_OF_DAY)) { + // no hours field so it must be a Date or a YearMonth + if (!t.isSupported(ChronoField.DAY_OF_MONTH)) { + // must be a YearMonth coz no day + return Date.from(YearMonth.from(t).atDay(1).atStartOfDay(UTC).toInstant()); + } else { + // must be a Date as the day is present + return Date.from(Instant.ofEpochSecond(LocalDate.from(t).atStartOfDay().toEpochSecond(UTC))); + } + } else if (!t.isSupported(ChronoField.MONTH_OF_YEAR)) { + // no month field so must be a Time + final Instant timeOnEpochDay = LocalDate.ofEpochDay(0) + .atTime(LocalTime.from(t)) + .atZone(UTC) + .toInstant(); + return Date.from(timeOnEpochDay); + } else if (t.isSupported(ChronoField.OFFSET_SECONDS)) { + // has all datetime components including an offset + return Date.from(ZonedDateTime.from(t).toInstant()); + } else { + // has all datetime components but no offset so throw in some UTC + return Date.from(ZonedDateTime.of(LocalDateTime.from(t), UTC).toInstant()); + } + } + + /** + * A proxy call to {@link #parse(String)} but allows for syntax similar to Gremlin grammar of {@code datetime()}. + */ + public static Date datetime(final String d) { + return parse(d); + } +} diff --git a/gremlin-core/src/test/java/org/apache/tinkerpop/gremlin/language/grammar/GeneralLiteralVisitorTest.java b/gremlin-core/src/test/java/org/apache/tinkerpop/gremlin/language/grammar/GeneralLiteralVisitorTest.java index 3dd36f3..4984aab 100644 --- a/gremlin-core/src/test/java/org/apache/tinkerpop/gremlin/language/grammar/GeneralLiteralVisitorTest.java +++ b/gremlin-core/src/test/java/org/apache/tinkerpop/gremlin/language/grammar/GeneralLiteralVisitorTest.java @@ -29,7 +29,12 @@ import org.junit.runners.Parameterized; import java.lang.reflect.Constructor; import java.math.BigDecimal; import java.math.BigInteger; +import java.time.Instant; +import java.time.ZoneId; +import java.time.ZoneOffset; +import java.time.ZonedDateTime; import java.util.Arrays; +import java.util.Date; import java.util.List; import java.util.Map; @@ -439,6 +444,47 @@ public class GeneralLiteralVisitorTest { } @RunWith(Parameterized.class) + public static class ValidDatetimeLiteralTest { + private static final ZoneId UTC = ZoneId.of("Z"); + + @Parameterized.Parameter(value = 0) + public String script; + + @Parameterized.Parameter(value = 1) + public Date expected; + + @Parameterized.Parameters(name = "{0}") + public static Iterable<Object[]> generateTestParameters() { + return Arrays.asList(new Object[][]{ + {"datetime('2018-03-22T00:35:44.741Z')", Date.from(ZonedDateTime.of(2018, 03, 22, 00, 35, 44, 741000000, UTC).toInstant())}, + {"datetime('2018-03-22T00:35:44.741-0000')", Date.from(ZonedDateTime.of(2018, 03, 22, 00, 35, 44, 741000000, UTC).toInstant())}, + {"datetime('2018-03-22T00:35:44.741+0000')", Date.from(ZonedDateTime.of(2018, 03, 22, 00, 35, 44, 741000000, UTC).toInstant())}, + {"datetime('2018-03-22T00:35:44.741-0300')", Date.from(ZonedDateTime.of(2018, 03, 22, 00, 35, 44, 741000000, ZoneOffset.ofHours(-3)).toInstant())}, + {"datetime('2018-03-22T00:35:44.741+1600')", Date.from(ZonedDateTime.of(2018, 03, 22, 00, 35, 44, 741000000, ZoneOffset.ofHours(16)).toInstant())}, + {"datetime('2018-03-22T00:35:44.741')", Date.from(ZonedDateTime.of(2018, 03, 22, 00, 35, 44, 741000000, UTC).toInstant())}, + {"datetime('2018-03-22T00:35:44Z')", Date.from(ZonedDateTime.of(2018, 03, 22, 00, 35, 44, 0, UTC).toInstant())}, + {"datetime('2018-03-22T00:35:44')", Date.from(ZonedDateTime.of(2018, 03, 22, 00, 35, 44, 0, UTC).toInstant())}, + {"datetime('2018-03-22')", Date.from(ZonedDateTime.of(2018, 03, 22, 0, 0, 0, 0, UTC).toInstant())}, + {"datetime('1018-03-22')", Date.from(ZonedDateTime.of(1018, 03, 22, 0, 0, 0, 0, UTC).toInstant())}, + {"datetime('9018-03-22')", Date.from(ZonedDateTime.of(9018, 03, 22, 0, 0, 0, 0, UTC).toInstant())}, + {"datetime('T00:00:00')", Date.from(ZonedDateTime.of(1970, 1, 1, 0, 0, 0, 0, UTC).toInstant())}, + {"datetime('T00:00:00Z')", Date.from(ZonedDateTime.of(1970, 1, 1, 0, 0, 0, 0, UTC).toInstant())}, + {"datetime('1000-001')", Date.from(ZonedDateTime.of(1000, 1, 1, 0, 0, 0, 0, UTC).toInstant())}, + }); + } + + @Test + public void shouldParse() { + final GremlinLexer lexer = new GremlinLexer(CharStreams.fromString(script)); + final GremlinParser parser = new GremlinParser(new CommonTokenStream(lexer)); + final GremlinParser.DateLiteralContext ctx = parser.dateLiteral(); + + final Date dt = (Date) GenericLiteralVisitor.getInstance().visitDateLiteral(ctx); + assertEquals(expected, dt); + } + } + + @RunWith(Parameterized.class) public static class ValidBooleanLiteralTest { @Parameterized.Parameter(value = 0) public String script; diff --git a/gremlin-core/src/test/java/org/apache/tinkerpop/gremlin/process/traversal/translator/GroovyTranslatorTest.java b/gremlin-core/src/test/java/org/apache/tinkerpop/gremlin/process/traversal/translator/GroovyTranslatorTest.java index 82d679d..3b75d40 100644 --- a/gremlin-core/src/test/java/org/apache/tinkerpop/gremlin/process/traversal/translator/GroovyTranslatorTest.java +++ b/gremlin-core/src/test/java/org/apache/tinkerpop/gremlin/process/traversal/translator/GroovyTranslatorTest.java @@ -41,6 +41,7 @@ import org.apache.tinkerpop.gremlin.util.function.Lambda; import org.junit.Test; import java.sql.Timestamp; +import java.time.ZonedDateTime; import java.util.Arrays; import java.util.Calendar; import java.util.Collections; @@ -49,6 +50,7 @@ import java.util.LinkedHashMap; import java.util.UUID; import java.util.function.Function; +import static java.time.ZoneOffset.UTC; import static org.apache.tinkerpop.gremlin.process.traversal.AnonymousTraversalSource.traversal; import static org.apache.tinkerpop.gremlin.process.traversal.dsl.graph.__.hasLabel; import static org.junit.Assert.assertEquals; @@ -156,6 +158,17 @@ public class GroovyTranslatorTest { } @Test + public void shouldTranslateDateUsingDatetimeFunction() { + final Translator.ScriptTranslator t = GroovyTranslator.of("g", + new GroovyTranslator.LanguageTypeTranslator(false)); + final Date datetime = Date.from(ZonedDateTime.of(2018, 03, 22, 00, 35, 44, 741000000, UTC).toInstant()); + final Date date = Date.from(ZonedDateTime.of(2018, 03, 22, 0, 0, 0, 0, UTC).toInstant()); + assertEquals("g.inject(datetime('2018-03-22T00:00:00Z'),datetime('2018-03-22T00:35:44.741Z'))", + t.translate(g.inject(date, datetime)).getScript()); + + } + + @Test public void shouldTranslateDirection() { assertTranslation("Direction.BOTH", Direction.BOTH); } diff --git a/gremlin-core/src/test/java/org/apache/tinkerpop/gremlin/util/DatetimeHelperTest.java b/gremlin-core/src/test/java/org/apache/tinkerpop/gremlin/util/DatetimeHelperTest.java new file mode 100644 index 0000000..f888a87 --- /dev/null +++ b/gremlin-core/src/test/java/org/apache/tinkerpop/gremlin/util/DatetimeHelperTest.java @@ -0,0 +1,105 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.apache.tinkerpop.gremlin.util; + +import org.junit.Test; +import org.junit.experimental.runners.Enclosed; +import org.junit.runner.RunWith; +import org.junit.runners.Parameterized; + +import java.time.Instant; +import java.time.ZoneId; +import java.time.ZoneOffset; +import java.time.ZonedDateTime; +import java.util.Arrays; +import java.util.Date; + +import static java.time.ZoneOffset.UTC; +import static org.junit.Assert.assertEquals; + +@RunWith(Enclosed.class) +public class DatetimeHelperTest { + + @RunWith(Parameterized.class) + public static class DatetimeHelperParseTest { + + @Parameterized.Parameter(value = 0) + public String d; + + @Parameterized.Parameter(value = 1) + public Date expected; + + @Parameterized.Parameters(name = "{0}") + public static Iterable<Object[]> generateTestParameters() { + return Arrays.asList(new Object[][]{ + {"2018-03-22T00:35:44.741Z", Date.from(ZonedDateTime.of(2018, 03, 22, 00, 35, 44, 741000000, UTC).toInstant())}, + {"2018-03-22T00:35:44.741-0000", Date.from(ZonedDateTime.of(2018, 03, 22, 00, 35, 44, 741000000, UTC).toInstant())}, + {"2018-03-22T00:35:44.741+0000", Date.from(ZonedDateTime.of(2018, 03, 22, 00, 35, 44, 741000000, UTC).toInstant())}, + {"2018-03-22T00:35:44.741-0300", Date.from(ZonedDateTime.of(2018, 03, 22, 00, 35, 44, 741000000, ZoneOffset.ofHours(-3)).toInstant())}, + {"2018-03-22T00:35:44.741+1600", Date.from(ZonedDateTime.of(2018, 03, 22, 00, 35, 44, 741000000, ZoneOffset.ofHours(16)).toInstant())}, + {"2018-03-22T00:35:44.741", Date.from(ZonedDateTime.of(2018, 03, 22, 00, 35, 44, 741000000, UTC).toInstant())}, + {"2018-03-22T00:35:44Z", Date.from(ZonedDateTime.of(2018, 03, 22, 00, 35, 44, 0, UTC).toInstant())}, + {"2018-03-22T00:35:44", Date.from(ZonedDateTime.of(2018, 03, 22, 00, 35, 44, 0, UTC).toInstant())}, + {"2018-03-22", Date.from(ZonedDateTime.of(2018, 03, 22, 0, 0, 0, 0, UTC).toInstant())}, + {"1018-03-22", Date.from(ZonedDateTime.of(1018, 03, 22, 0, 0, 0, 0, UTC).toInstant())}, + {"9018-03-22", Date.from(ZonedDateTime.of(9018, 03, 22, 0, 0, 0, 0, UTC).toInstant())}, + {"T00:00:00", Date.from(ZonedDateTime.of(1970, 1, 1, 0, 0, 0, 0, UTC).toInstant())}, + {"T00:00:00Z", Date.from(ZonedDateTime.of(1970, 1, 1, 0, 0, 0, 0, UTC).toInstant())}, + {"1000-001", Date.from(ZonedDateTime.of(1000, 1, 1, 0, 0, 0, 0, UTC).toInstant())}, + }); + } + + @Test + public void shouldParse() { + assertEquals(expected, DatetimeHelper.parse(d)); + } + } + + @RunWith(Parameterized.class) + public static class DatetimeHelperFormatTest { + + @Parameterized.Parameter(value = 0) + public String expected; + + @Parameterized.Parameter(value = 1) + public Instant d; + + @Parameterized.Parameters(name = "{0}") + public static Iterable<Object[]> generateTestParameters() { + return Arrays.asList(new Object[][]{ + {"2018-03-22T00:35:44.741Z", ZonedDateTime.of(2018, 03, 22, 00, 35, 44, 741000000, UTC).toInstant()}, + {"2018-03-22T00:35:44.741Z", ZonedDateTime.of(2018, 03, 22, 00, 35, 44, 741000000, UTC).toInstant()}, + {"2018-03-22T00:35:44.741Z", ZonedDateTime.of(2018, 03, 22, 00, 35, 44, 741000000, UTC).toInstant()}, + {"2018-03-22T03:35:44.741Z", ZonedDateTime.of(2018, 03, 22, 00, 35, 44, 741000000, ZoneOffset.ofHours(-3)).toInstant()}, + {"2018-03-21T08:35:44.741Z", ZonedDateTime.of(2018, 03, 22, 00, 35, 44, 741000000, ZoneOffset.ofHours(16)).toInstant()}, + {"2018-03-22T00:35:44Z", ZonedDateTime.of(2018, 03, 22, 00, 35, 44, 0, UTC).toInstant()}, + {"2018-03-22T00:00:00Z", ZonedDateTime.of(2018, 03, 22, 0, 0, 0, 0, UTC).toInstant()}, + {"1018-03-22T00:00:00Z", ZonedDateTime.of(1018, 03, 22, 0, 0, 0, 0, UTC).toInstant()}, + {"9018-03-22T00:00:00Z", ZonedDateTime.of(9018, 03, 22, 0, 0, 0, 0, UTC).toInstant()}, + {"1970-01-01T00:00:00Z", ZonedDateTime.of(1970, 1, 1, 0, 0, 0, 0, UTC).toInstant()}, + {"1000-01-01T00:00:00Z", ZonedDateTime.of(1000, 1, 1, 0, 0, 0, 0, UTC).toInstant()}, + }); + } + + @Test + public void shouldFormat() { + assertEquals(expected, DatetimeHelper.format(d)); + } + } +} diff --git a/gremlin-server/src/test/java/org/apache/tinkerpop/gremlin/server/GremlinServerHttpIntegrateTest.java b/gremlin-server/src/test/java/org/apache/tinkerpop/gremlin/server/GremlinServerHttpIntegrateTest.java index 05cd939..86c1c6e 100644 --- a/gremlin-server/src/test/java/org/apache/tinkerpop/gremlin/server/GremlinServerHttpIntegrateTest.java +++ b/gremlin-server/src/test/java/org/apache/tinkerpop/gremlin/server/GremlinServerHttpIntegrateTest.java @@ -865,4 +865,18 @@ public class GremlinServerHttpIntegrateTest extends AbstractGremlinServerIntegra assertEquals(0, node.get("result").get("data").get(GraphSONTokens.VALUEPROP).get(0).get(GraphSONTokens.VALUEPROP).asInt()); } } + + @Test + public void should200OnGETWithGremlinQueryStringArgumentCallingDatetimeFunction() throws Exception { + final CloseableHttpClient httpclient = HttpClients.createDefault(); + final HttpGet httpget = new HttpGet(TestClientFactory.createURLString("?gremlin=datetime%28%272018-03-22T00%3A35%3A44.741%2B1600%27%29")); + + try (final CloseableHttpResponse response = httpclient.execute(httpget)) { + assertEquals(200, response.getStatusLine().getStatusCode()); + assertEquals("application/json", response.getEntity().getContentType().getValue()); + final String json = EntityUtils.toString(response.getEntity()); + final JsonNode node = mapper.readTree(json); + assertEquals(1521621344741L, node.get("result").get("data").get(GraphSONTokens.VALUEPROP).get(0).get(GraphSONTokens.VALUEPROP).longValue()); + } + } }