Skip to content

Defer unknown zone information failures to datetime value access stage #1273

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 2 commits into from
Jul 21, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 5 additions & 1 deletion driver/src/main/java/org/neo4j/driver/Value.java
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
*/
package org.neo4j.driver;

import java.time.DateTimeException;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.time.LocalTime;
Expand Down Expand Up @@ -197,7 +198,8 @@ public interface Value extends MapAccessor, MapAccessorWithDefaultValue {
* 64-bit precision. This is why these types return java {@link Long} and
* {@link Double}, respectively.
*
* @return the value as a Java Object
* @return the value as a Java Object.
* @throws DateTimeException if zone information supplied by server is not supported by driver runtime. Applicable to datetime values only.
*/
Object asObject();

Expand Down Expand Up @@ -416,12 +418,14 @@ public interface Value extends MapAccessor, MapAccessorWithDefaultValue {
/**
* @return the value as a {@link java.time.OffsetDateTime}, if possible.
* @throws Uncoercible if value types are incompatible.
* @throws DateTimeException if zone information supplied by server is not supported by driver runtime.
*/
OffsetDateTime asOffsetDateTime();

/**
* @return the value as a {@link ZonedDateTime}, if possible.
* @throws Uncoercible if value types are incompatible.
* @throws DateTimeException if zone information supplied by server is not supported by driver runtime.
*/
ZonedDateTime asZonedDateTime();

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@
import static org.neo4j.driver.Values.value;

import java.io.IOException;
import java.time.DateTimeException;
import java.time.Instant;
import java.time.LocalDate;
import java.time.LocalDateTime;
Expand All @@ -37,6 +38,7 @@
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.function.Supplier;
import org.neo4j.driver.Value;
import org.neo4j.driver.exceptions.ClientException;
import org.neo4j.driver.exceptions.ProtocolException;
Expand All @@ -54,12 +56,12 @@
import org.neo4j.driver.internal.value.NodeValue;
import org.neo4j.driver.internal.value.PathValue;
import org.neo4j.driver.internal.value.RelationshipValue;
import org.neo4j.driver.internal.value.UnsupportedDateTimeValue;
import org.neo4j.driver.types.Node;
import org.neo4j.driver.types.Path;
import org.neo4j.driver.types.Relationship;

public class CommonValueUnpacker implements ValueUnpacker {

public static final byte DATE = 'D';
public static final int DATE_STRUCT_SIZE = 1;

Expand Down Expand Up @@ -189,28 +191,28 @@ private Value unpackStruct(long size, byte type) throws IOException {
case DATE_TIME_WITH_ZONE_OFFSET:
if (!dateTimeUtcEnabled) {
ensureCorrectStructSize(TypeConstructor.DATE_TIME, DATE_TIME_STRUCT_SIZE, size);
return unpackDateTimeWithZoneOffset();
return unpackDateTime(ZoneMode.OFFSET, BaselineMode.LEGACY);
} else {
throw instantiateExceptionForUnknownType(type);
}
case DATE_TIME_WITH_ZONE_OFFSET_UTC:
if (dateTimeUtcEnabled) {
ensureCorrectStructSize(TypeConstructor.DATE_TIME, DATE_TIME_STRUCT_SIZE, size);
return unpackDateTimeUtcWithZoneOffset();
return unpackDateTime(ZoneMode.OFFSET, BaselineMode.UTC);
} else {
throw instantiateExceptionForUnknownType(type);
}
case DATE_TIME_WITH_ZONE_ID:
if (!dateTimeUtcEnabled) {
ensureCorrectStructSize(TypeConstructor.DATE_TIME, DATE_TIME_STRUCT_SIZE, size);
return unpackDateTimeWithZoneId();
return unpackDateTime(ZoneMode.ZONE_ID, BaselineMode.LEGACY);
} else {
throw instantiateExceptionForUnknownType(type);
}
case DATE_TIME_WITH_ZONE_ID_UTC:
if (dateTimeUtcEnabled) {
ensureCorrectStructSize(TypeConstructor.DATE_TIME, DATE_TIME_STRUCT_SIZE, size);
return unpackDateTimeUtcWithZoneId();
return unpackDateTime(ZoneMode.ZONE_ID, BaselineMode.UTC);
} else {
throw instantiateExceptionForUnknownType(type);
}
Expand Down Expand Up @@ -374,34 +376,26 @@ private Value unpackLocalDateTime() throws IOException {
return value(LocalDateTime.ofEpochSecond(epochSecondUtc, nano, UTC));
}

private Value unpackDateTimeWithZoneOffset() throws IOException {
long epochSecondLocal = unpacker.unpackLong();
int nano = Math.toIntExact(unpacker.unpackLong());
int offsetSeconds = Math.toIntExact(unpacker.unpackLong());
return value(newZonedDateTime(epochSecondLocal, nano, ZoneOffset.ofTotalSeconds(offsetSeconds)));
}

private Value unpackDateTimeUtcWithZoneOffset() throws IOException {
long epochSecondLocal = unpacker.unpackLong();
int nano = Math.toIntExact(unpacker.unpackLong());
int offsetSeconds = Math.toIntExact(unpacker.unpackLong());
ZoneOffset offset = ZoneOffset.ofTotalSeconds(offsetSeconds);
return value(newZonedDateTimeUsingUtcBaseline(epochSecondLocal, nano, offset));
}

private Value unpackDateTimeWithZoneId() throws IOException {
long epochSecondLocal = unpacker.unpackLong();
int nano = Math.toIntExact(unpacker.unpackLong());
String zoneIdString = unpacker.unpackString();
return value(newZonedDateTime(epochSecondLocal, nano, ZoneId.of(zoneIdString)));
}

private Value unpackDateTimeUtcWithZoneId() throws IOException {
long epochSecondLocal = unpacker.unpackLong();
int nano = Math.toIntExact(unpacker.unpackLong());
String zoneIdString = unpacker.unpackString();
ZoneId zoneId = ZoneId.of(zoneIdString);
return value(newZonedDateTimeUsingUtcBaseline(epochSecondLocal, nano, zoneId));
private Value unpackDateTime(ZoneMode unpackOffset, BaselineMode useUtcBaseline) throws IOException {
var epochSecondLocal = unpacker.unpackLong();
var nano = Math.toIntExact(unpacker.unpackLong());
Supplier<ZoneId> zoneIdSupplier;
if (unpackOffset == ZoneMode.OFFSET) {
var offsetSeconds = Math.toIntExact(unpacker.unpackLong());
zoneIdSupplier = () -> ZoneOffset.ofTotalSeconds(offsetSeconds);
} else {
var zoneIdString = unpacker.unpackString();
zoneIdSupplier = () -> ZoneId.of(zoneIdString);
}
ZoneId zoneId;
try {
zoneId = zoneIdSupplier.get();
} catch (DateTimeException e) {
return new UnsupportedDateTimeValue(e);
}
return useUtcBaseline == BaselineMode.UTC
? value(newZonedDateTimeUsingUtcBaseline(epochSecondLocal, nano, zoneId))
: value(newZonedDateTime(epochSecondLocal, nano, zoneId));
}

private Value unpackDuration() throws IOException {
Expand Down Expand Up @@ -450,4 +444,14 @@ protected int getNodeFields() {
protected int getRelationshipFields() {
return RELATIONSHIP_FIELDS;
}

private enum ZoneMode {
OFFSET,
ZONE_ID
}

private enum BaselineMode {
UTC,
LEGACY
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
/*
* Copyright (c) "Neo4j"
* Neo4j Sweden AB [http://neo4j.com]
*
* This file is part of Neo4j.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.neo4j.driver.internal.value;

import java.lang.reflect.InvocationTargetException;
import java.time.DateTimeException;
import java.time.OffsetDateTime;
import java.time.ZonedDateTime;
import org.neo4j.driver.internal.types.InternalTypeSystem;
import org.neo4j.driver.types.Type;

public class UnsupportedDateTimeValue extends ValueAdapter {
final DateTimeException exception;

public UnsupportedDateTimeValue(DateTimeException exception) {
this.exception = exception;
}

@Override
public OffsetDateTime asOffsetDateTime() {
throw instantiateDateTimeException();
}

@Override
public ZonedDateTime asZonedDateTime() {
throw instantiateDateTimeException();
}

@Override
public Object asObject() {
throw instantiateDateTimeException();
}

@Override
public Type type() {
return InternalTypeSystem.TYPE_SYSTEM.DATE_TIME();
}

@Override
public boolean equals(Object obj) {
return this == obj;
}

@Override
public int hashCode() {
return System.identityHashCode(this);
}

@Override
public String toString() {
return "Unsupported datetime value.";
}

private DateTimeException instantiateDateTimeException() {
DateTimeException newException;
try {
newException = exception
.getClass()
.getDeclaredConstructor(String.class, Throwable.class)
.newInstance(exception.getMessage(), exception);
} catch (NoSuchMethodException
| InvocationTargetException
| InstantiationException
| IllegalAccessException e) {
newException = new DateTimeException(exception.getMessage(), exception);
}
return newException;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,116 @@
/*
* Copyright (c) "Neo4j"
* Neo4j Sweden AB [http://neo4j.com]
*
* This file is part of Neo4j.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.neo4j.driver.internal.value;

import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNotEquals;
import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.mockito.BDDMockito.given;
import static org.mockito.MockitoAnnotations.openMocks;

import java.time.DateTimeException;
import java.time.OffsetDateTime;
import java.time.ZonedDateTime;
import java.util.List;
import java.util.function.Function;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Named;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.Arguments;
import org.junit.jupiter.params.provider.MethodSource;
import org.mockito.Mock;
import org.neo4j.driver.internal.types.InternalTypeSystem;

public class UnsupportedDateTimeValueTest {
@Mock
private DateTimeException exception;

@BeforeEach
void beforeEach() {
openMocks(this);
}

@MethodSource("throwingDateTimeAccessMethods")
@ParameterizedTest
void shouldThrowOnDateTimeAccess(Function<UnsupportedDateTimeValue, ?> throwingMethod) {
// GIVEN
given(exception.getMessage()).willReturn("message");
var value = new UnsupportedDateTimeValue(exception);

// WHEN
var actualException = assertThrows(DateTimeException.class, () -> throwingMethod.apply(value));

// THEN
assertEquals(actualException.getMessage(), exception.getMessage());
assertEquals(actualException.getCause(), exception);
}

static List<Arguments> throwingDateTimeAccessMethods() {
return List.of(
Arguments.of(Named.<Function<UnsupportedDateTimeValue, ?>>of(
"asOffsetDateTime", UnsupportedDateTimeValue::asOffsetDateTime)),
Arguments.of(Named.<Function<UnsupportedDateTimeValue, ?>>of(
"asOffsetDateTime(OffsetDateTime)", v -> v.asOffsetDateTime(OffsetDateTime.now()))),
Arguments.of(Named.<Function<UnsupportedDateTimeValue, ?>>of(
"asZonedDateTime", UnsupportedDateTimeValue::asZonedDateTime)),
Arguments.of(Named.<Function<UnsupportedDateTimeValue, ?>>of(
"asZonedDateTime(ZonedDateTime)", v -> v.asZonedDateTime(ZonedDateTime.now()))),
Arguments.of(Named.<Function<UnsupportedDateTimeValue, ?>>of(
"asObject", UnsupportedDateTimeValue::asObject)));
}

@Test
void shouldEqualToItself() {
// GIVEN
var value = new UnsupportedDateTimeValue(exception);

// WHEN & THEN
assertEquals(value, value);
}

@Test
void shouldNotEqualToAnotherInstance() {
// GIVEN
var value0 = new UnsupportedDateTimeValue(exception);
var value1 = new UnsupportedDateTimeValue(exception);

// WHEN & THEN
assertNotEquals(value0, value1);
}

@Test
void shouldSupplyIdentityHashcode() {
// GIVEN
var value0 = new UnsupportedDateTimeValue(exception);
var value1 = new UnsupportedDateTimeValue(exception);

// WHEN & THEN
assertNotEquals(value0.hashCode(), value1.hashCode());
}

@Test
void shouldSupplyDateTimeType() {
// GIVEN
var value = new UnsupportedDateTimeValue(exception);

// WHEN & THEN
assertEquals(InternalTypeSystem.TYPE_SYSTEM.DATE_TIME(), value.type());
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -22,14 +22,17 @@
import lombok.Setter;
import neo4j.org.testkit.backend.messages.AbstractResultNext;
import org.neo4j.driver.Record;
import org.neo4j.driver.Value;

@Setter
@Getter
public class ResultNext extends AbstractResultNext {
private static final String DATE_TIME = "DATE_TIME";
private ResultNextBody data;

@Override
protected neo4j.org.testkit.backend.messages.responses.TestkitResponse createResponse(Record record) {
record.values().stream().filter(v -> DATE_TIME.equals(v.type().name())).forEach(Value::asObject);
return neo4j.org.testkit.backend.messages.responses.Record.builder()
.data(neo4j.org.testkit.backend.messages.responses.Record.RecordBody.builder()
.values(record)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -83,12 +83,6 @@ public class StartTest implements TestkitRequest {
"^.*\\.TestOptimizations\\.test_uses_implicit_default_arguments_multi_query$", skipMessage);
COMMON_SKIP_PATTERN_TO_REASON.put(
"^.*\\.TestOptimizations\\.test_uses_implicit_default_arguments_multi_query_nested$", skipMessage);
COMMON_SKIP_PATTERN_TO_REASON.put(
"^.*\\.test_unknown_then_known_zoned_date_time(_patched)?$",
"Unknown zone names make the driver close the connection.");
COMMON_SKIP_PATTERN_TO_REASON.put(
"^.*\\.test_unknown_zoned_date_time(_patched)?$",
"Unknown zone names make the driver close the connection.");

ASYNC_SKIP_PATTERN_TO_REASON.putAll(COMMON_SKIP_PATTERN_TO_REASON);

Expand Down