Skip to content

Commit

Permalink
Use epochSecond of LocalDateTime for ZonedDateTime in Bolt
Browse files Browse the repository at this point in the history
So that values of `DateTime` type (represented with `ZonedDateTime`)
are not converted to UTC. Previously Bolt server converted
`ZonedDateTime` to epochSecond in UTC before sending it and expected to
receive value in UTC as well. This forced clients to perform a
conversion from UTC and to UTC. Such conversion can be especially hard
with named timezones. Now clients will receive unadjusted value which
can be directly converted to `LocalDateTime` and used together with
the timezone info.
  • Loading branch information
lutovich committed Mar 27, 2018
1 parent 899dbf4 commit 4fbdb98
Show file tree
Hide file tree
Showing 2 changed files with 62 additions and 8 deletions.
Expand Up @@ -20,6 +20,7 @@
package org.neo4j.bolt.v2.messaging;

import java.io.IOException;
import java.time.Instant;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.time.LocalTime;
Expand Down Expand Up @@ -171,7 +172,7 @@ public void writeLocalDateTime( LocalDateTime localDateTime ) throws IOException
@Override
public void writeDateTime( ZonedDateTime zonedDateTime ) throws IOException
{
long epochSecondUTC = zonedDateTime.toEpochSecond();
long epochSecondLocal = zonedDateTime.toLocalDateTime().toEpochSecond( UTC );
int nano = zonedDateTime.getNano();

ZoneId zone = zonedDateTime.getZone();
Expand All @@ -180,7 +181,7 @@ public void writeDateTime( ZonedDateTime zonedDateTime ) throws IOException
int offsetSeconds = ((ZoneOffset) zone).getTotalSeconds();

packStructHeader( 3, DATE_TIME_WITH_ZONE_OFFSET );
pack( epochSecondUTC );
pack( epochSecondLocal );
pack( nano );
pack( offsetSeconds );
}
Expand All @@ -189,7 +190,7 @@ public void writeDateTime( ZonedDateTime zonedDateTime ) throws IOException
String zoneId = zone.getId();

packStructHeader( 3, DATE_TIME_WITH_ZONE_NAME );
pack( epochSecondUTC );
pack( epochSecondLocal );
pack( nano );
pack( zoneId );
}
Expand Down Expand Up @@ -284,18 +285,25 @@ private LocalDateTimeValue unpackLocalDateTime() throws IOException

private DateTimeValue unpackDateTimeWithZoneOffset() throws IOException
{
long epochSecondUTC = unpackLong();
long epochSecondLocal = unpackLong();
long nano = unpackLong();
int offsetSeconds = unpackInteger();
return datetime( epochSecondUTC, nano, ZoneOffset.ofTotalSeconds( offsetSeconds ) );
return datetime( newZonedDateTime( epochSecondLocal, nano, ZoneOffset.ofTotalSeconds( offsetSeconds ) ) );
}

private DateTimeValue unpackDateTimeWithZoneName() throws IOException
{
long epochSecondUTC = unpackLong();
long epochSecondLocal = unpackLong();
long nano = unpackLong();
String zoneId = unpackString();
return datetime( epochSecondUTC, nano, ZoneId.of( zoneId ) );
return datetime( newZonedDateTime( epochSecondLocal, nano, ZoneId.of( zoneId ) ) );
}

private static ZonedDateTime newZonedDateTime( long epochSecondLocal, long nano, ZoneId zoneId )
{
Instant instant = Instant.ofEpochSecond( epochSecondLocal, nano );
LocalDateTime localDateTime = LocalDateTime.ofInstant( instant, UTC );
return ZonedDateTime.of( localDateTime, zoneId );
}
}
}
Expand Up @@ -23,8 +23,11 @@

import java.io.IOException;
import java.io.UncheckedIOException;
import java.nio.ByteBuffer;
import java.time.LocalDateTime;
import java.time.ZoneId;
import java.time.ZoneOffset;
import java.time.ZonedDateTime;
import java.time.temporal.ChronoField;
import java.time.temporal.ValueRange;
import java.util.concurrent.ThreadLocalRandom;
Expand All @@ -35,7 +38,6 @@
import org.neo4j.bolt.v1.messaging.Neo4jPack;
import org.neo4j.bolt.v1.packstream.PackedInputArray;
import org.neo4j.bolt.v1.packstream.PackedOutputArray;
import org.neo4j.values.storable.TimeZones;
import org.neo4j.values.AnyValue;
import org.neo4j.values.storable.CoordinateReferenceSystem;
import org.neo4j.values.storable.DateTimeValue;
Expand All @@ -45,8 +47,11 @@
import org.neo4j.values.storable.LocalTimeValue;
import org.neo4j.values.storable.PointValue;
import org.neo4j.values.storable.TimeValue;
import org.neo4j.values.storable.TimeZones;
import org.neo4j.values.virtual.ListValue;

import static java.nio.charset.StandardCharsets.UTF_8;
import static java.time.ZoneOffset.UTC;
import static java.time.temporal.ChronoField.DAY_OF_MONTH;
import static java.time.temporal.ChronoField.EPOCH_DAY;
import static java.time.temporal.ChronoField.HOUR_OF_DAY;
Expand All @@ -58,6 +63,8 @@
import static java.time.temporal.ChronoField.YEAR;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.fail;
import static org.neo4j.bolt.v1.packstream.PackStream.INT_16;
import static org.neo4j.bolt.v1.packstream.PackStream.INT_32;
import static org.neo4j.values.storable.CoordinateReferenceSystem.Cartesian;
import static org.neo4j.values.storable.CoordinateReferenceSystem.Cartesian_3D;
import static org.neo4j.values.storable.CoordinateReferenceSystem.WGS84;
Expand Down Expand Up @@ -244,6 +251,45 @@ public void shouldPackAndUnpackListsOfDateTimeWithTimeZoneOffset()
testPackingAndUnpacking( () -> randomList( this::randomDateTimeWithTimeZoneOffset ) );
}

@Test
public void shouldPackLocalDateTimeWithTimeZoneOffset()
{
LocalDateTime localDateTime = LocalDateTime.of( 2015, 3, 23, 19, 15, 59, 10 );
ZoneOffset offset = ZoneOffset.ofHoursMinutes( -5, -15 );
ZonedDateTime zonedDateTime = ZonedDateTime.of( localDateTime, offset );

PackedOutputArray packedOutput = pack( datetime( zonedDateTime ) );
ByteBuffer buffer = ByteBuffer.wrap( packedOutput.bytes() );

buffer.getShort(); // skip struct header
assertEquals( INT_32, buffer.get() );
assertEquals( localDateTime.toEpochSecond( UTC ), buffer.getInt() );
assertEquals( localDateTime.getNano(), buffer.get() );
assertEquals( INT_16, buffer.get() );
assertEquals( offset.getTotalSeconds(), buffer.getShort() );
}

@Test
public void shouldPackLocalDateTimeWithTimeZoneId()
{
LocalDateTime localDateTime = LocalDateTime.of( 1999, 12, 30, 9, 49, 20, 999999999 );
ZoneId zoneId = ZoneId.of( "Europe/Stockholm" );
ZonedDateTime zonedDateTime = ZonedDateTime.of( localDateTime, zoneId );

PackedOutputArray packedOutput = pack( datetime( zonedDateTime ) );
ByteBuffer buffer = ByteBuffer.wrap( packedOutput.bytes() );

buffer.getShort(); // skip struct header
assertEquals( INT_32, buffer.get() );
assertEquals( localDateTime.toEpochSecond( UTC ), buffer.getInt() );
assertEquals( INT_32, buffer.get() );
assertEquals( localDateTime.getNano(), buffer.getInt() );
buffer.getShort(); // skip zoneId string header
byte[] zoneIdBytes = new byte[zoneId.getId().getBytes( UTF_8 ).length];
buffer.get( zoneIdBytes );
assertEquals( zoneId.getId(), new String( zoneIdBytes, UTF_8 ) );
}

private static <T extends AnyValue> void testPackingAndUnpacking( Supplier<T> randomValueGenerator )
{
testPackingAndUnpacking( index -> randomValueGenerator.get() );
Expand Down

0 comments on commit 4fbdb98

Please sign in to comment.