Skip to content

Commit

Permalink
3D point support in bolt
Browse files Browse the repository at this point in the history
Introduced a new pack stream type - FLOAT_TRIPLE that represents tree
`double` values and contains just a single type header. New 3D point
type uses FLOAT_TRIPLE to encode it's coordinate.

Fixed exception message to print correct hex value of the unexpected
type header. Previously it only contained first two hex digits, which
were always 'ff' for `byte` header values.
  • Loading branch information
lutovich committed Feb 21, 2018
1 parent 24be967 commit c477338
Show file tree
Hide file tree
Showing 5 changed files with 168 additions and 43 deletions.
Expand Up @@ -104,7 +104,7 @@ public class PackStream
public static final byte FALSE = (byte) 0xC2; public static final byte FALSE = (byte) 0xC2;
public static final byte TRUE = (byte) 0xC3; public static final byte TRUE = (byte) 0xC3;
public static final byte FLOAT_64_PAIR = (byte) 0xC4; public static final byte FLOAT_64_PAIR = (byte) 0xC4;
public static final byte RESERVED_C5 = (byte) 0xC5; public static final byte FLOAT_64_TRIPLE = (byte) 0xC5;
public static final byte RESERVED_C6 = (byte) 0xC6; public static final byte RESERVED_C6 = (byte) 0xC6;
public static final byte RESERVED_C7 = (byte) 0xC7; public static final byte RESERVED_C7 = (byte) 0xC7;
public static final byte INT_8 = (byte) 0xC8; public static final byte INT_8 = (byte) 0xC8;
Expand Down Expand Up @@ -196,6 +196,8 @@ private static PackType type( byte markerByte )
return PackType.FLOAT; return PackType.FLOAT;
case FLOAT_64_PAIR: case FLOAT_64_PAIR:
return PackType.FLOAT_PAIR; return PackType.FLOAT_PAIR;
case FLOAT_64_TRIPLE:
return PackType.FLOAT_TRIPLE;
case BYTES_8: case BYTES_8:
case BYTES_16: case BYTES_16:
case BYTES_32: case BYTES_32:
Expand Down Expand Up @@ -303,6 +305,11 @@ public void pack( double value1, double value2 ) throws IOException
throw new Unsupported( getClass(), PackType.FLOAT_PAIR ); throw new Unsupported( getClass(), PackType.FLOAT_PAIR );
} }


public void pack( double value1, double value2, double value3 ) throws IOException
{
throw new Unsupported( getClass(), PackType.FLOAT_TRIPLE );
}

public void pack( char character ) throws IOException public void pack( char character ) throws IOException
{ {
if ( character >= PACKED_CHAR_START_CHAR && character <= PACKED_CHAR_END_CHAR ) if ( character >= PACKED_CHAR_START_CHAR && character <= PACKED_CHAR_END_CHAR )
Expand Down Expand Up @@ -617,6 +624,11 @@ public double[] unpackDoublePair() throws IOException
throw new Unsupported( getClass(), PackType.FLOAT_PAIR ); throw new Unsupported( getClass(), PackType.FLOAT_PAIR );
} }


public double[] unpackDoubleTriple() throws IOException
{
throw new Unsupported( getClass(), PackType.FLOAT_TRIPLE );
}

public byte[] unpackBytes() throws IOException public byte[] unpackBytes() throws IOException
{ {
int size = unpackBytesHeader(); int size = unpackBytesHeader();
Expand Down Expand Up @@ -807,21 +819,7 @@ public static class Unexpected extends PackStreamException
public Unexpected( PackType expectedType, byte unexpectedMarkerByte ) public Unexpected( PackType expectedType, byte unexpectedMarkerByte )
{ {
super( "Wrong type received. Expected " + expectedType + ", received: " + type( unexpectedMarkerByte ) + super( "Wrong type received. Expected " + expectedType + ", received: " + type( unexpectedMarkerByte ) +
" " + "(" + toHexString( unexpectedMarkerByte ) + ")." ); " (0x" + Integer.toHexString( unexpectedMarkerByte ) + ")." );
}

private static String toHexString( byte unexpectedMarkerByte )
{
String s = Integer.toHexString( unexpectedMarkerByte );
if ( s.length() > 2 )
{
s = s.substring( 0, 2 );
}
else if ( s.length() < 2 )
{
return "0" + s;
}
return "0x" + s;
} }
} }


Expand Down
Expand Up @@ -35,6 +35,8 @@ public enum PackType
FLOAT, FLOAT,
/** two 64-bit floating point numbers */ /** two 64-bit floating point numbers */
FLOAT_PAIR, FLOAT_PAIR,
/** three 64-bit floating point numbers */
FLOAT_TRIPLE,
/** Binary data */ /** Binary data */
BYTES, BYTES,
/** Unicode string */ /** Unicode string */
Expand Down
Expand Up @@ -37,9 +37,7 @@
public class Neo4jPackV2 extends Neo4jPackV1 public class Neo4jPackV2 extends Neo4jPackV1
{ {
public static final byte POINT_2D = 'X'; public static final byte POINT_2D = 'X';

public static final byte POINT_3D = 'Y';
static final int MIN_POINT_DIMENSIONS = 2;
static final int MAX_POINT_DIMENSIONS = 2;


@Override @Override
public Neo4jPack.Packer newPacker( PackOutput output ) public Neo4jPack.Packer newPacker( PackOutput output )
Expand All @@ -63,13 +61,23 @@ private static class PackerV2 extends Neo4jPackV1.PackerV1
@Override @Override
public void writePoint( CoordinateReferenceSystem crs, double[] coordinate ) throws IOException public void writePoint( CoordinateReferenceSystem crs, double[] coordinate ) throws IOException
{ {
if ( coordinate.length != 2 ) if ( coordinate.length == 2 )
{
packStructHeader( 2, POINT_2D );
pack( crs.getCode() );
pack( coordinate[0], coordinate[1] );
}
else if ( coordinate.length == 3 )
{ {
throw new IllegalArgumentException( "Point with 2D coordinate expected but got crs=" + crs + ", coordinate=" + Arrays.toString( coordinate ) ); packStructHeader( 2, POINT_3D );
pack( crs.getCode() );
pack( coordinate[0], coordinate[1], coordinate[2] );
}
else
{
throw new IllegalArgumentException( "Point with 2D or 3D coordinate expected, " +
"got crs=" + crs + ", coordinate=" + Arrays.toString( coordinate ) );
} }
packStructHeader( 2, POINT_2D );
pack( crs.getCode() );
pack( coordinate[0], coordinate[1] );
} }


@Override @Override
Expand All @@ -79,6 +87,15 @@ public void pack( double value1, double value2 ) throws IOException
.writeDouble( value1 ) .writeDouble( value1 )
.writeDouble( value2 ); .writeDouble( value2 );
} }

@Override
public void pack( double value1, double value2, double value3 ) throws IOException
{
out.writeByte( PackStream.FLOAT_64_TRIPLE )
.writeDouble( value1 )
.writeDouble( value2 )
.writeDouble( value3 );
}
} }


private static class UnpackerV2 extends Neo4jPackV1.UnpackerV1 private static class UnpackerV2 extends Neo4jPackV1.UnpackerV1
Expand All @@ -93,9 +110,16 @@ protected AnyValue unpackStruct( char signature ) throws IOException
{ {
if ( signature == POINT_2D ) if ( signature == POINT_2D )
{ {
return unpackPoint(); return unpackPoint2D();
}
else if ( signature == POINT_3D )
{
return unpackPoint3D();
}
else
{
return super.unpackStruct( signature );
} }
return super.unpackStruct( signature );
} }


@Override @Override
Expand All @@ -109,12 +133,31 @@ public double[] unpackDoublePair() throws IOException
throw new PackStream.Unexpected( PackType.FLOAT_PAIR, markerByte ); throw new PackStream.Unexpected( PackType.FLOAT_PAIR, markerByte );
} }


private PointValue unpackPoint() throws IOException @Override
public double[] unpackDoubleTriple() throws IOException
{
byte markerByte = in.readByte();
if ( markerByte == PackStream.FLOAT_64_TRIPLE )
{
return new double[]{in.readDouble(), in.readDouble(), in.readDouble()};
}
throw new PackStream.Unexpected( PackType.FLOAT_TRIPLE, markerByte );
}

private PointValue unpackPoint2D() throws IOException
{ {
int crsCode = unpackInteger(); int crsCode = unpackInteger();
CoordinateReferenceSystem crs = CoordinateReferenceSystem.get( crsCode ); CoordinateReferenceSystem crs = CoordinateReferenceSystem.get( crsCode );
double[] coordinates = unpackDoublePair(); double[] coordinates = unpackDoublePair();
return pointValue( crs, coordinates ); return pointValue( crs, coordinates );
} }

private PointValue unpackPoint3D() throws IOException
{
int crsCode = unpackInteger();
CoordinateReferenceSystem crs = CoordinateReferenceSystem.get( crsCode );
double[] coordinates = unpackDoubleTriple();
return pointValue( crs, coordinates );
}
} }
} }
Expand Up @@ -36,6 +36,7 @@
import org.neo4j.bolt.v1.packstream.PackedInputArray; import org.neo4j.bolt.v1.packstream.PackedInputArray;
import org.neo4j.bolt.v1.packstream.PackedOutputArray; import org.neo4j.bolt.v1.packstream.PackedOutputArray;
import org.neo4j.bolt.v1.runtime.Neo4jError; import org.neo4j.bolt.v1.runtime.Neo4jError;
import org.neo4j.function.ThrowingConsumer;
import org.neo4j.kernel.api.exceptions.Status; import org.neo4j.kernel.api.exceptions.Status;
import org.neo4j.kernel.impl.util.ValueUtils; import org.neo4j.kernel.impl.util.ValueUtils;
import org.neo4j.values.AnyValue; import org.neo4j.values.AnyValue;
Expand Down Expand Up @@ -265,35 +266,59 @@ public void shouldPackUtf8() throws IOException


@Test @Test
public void shouldFailToPackDoublePair() throws IOException public void shouldFailToPackDoublePair() throws IOException
{
testUnsupportedPacking( PackType.FLOAT_PAIR, packer -> packer.pack( 1.0, 2.0 ) );
}

@Test
public void shouldFailToUnpackDoublePair() throws IOException
{
testUnsupportedUnpacking( PackType.FLOAT_PAIR, PackStream.FLOAT_64_PAIR, Neo4jPackV1.UnpackerV1::unpackDoublePair );
}

@Test
public void shouldFailToPackDoubleTriple() throws IOException
{
testUnsupportedPacking( PackType.FLOAT_TRIPLE, packer -> packer.pack( 1.0, 2.0, 3.0 ) );
}

@Test
public void shouldFailToUnpackDoubleTriple() throws IOException
{
testUnsupportedUnpacking( PackType.FLOAT_TRIPLE, PackStream.FLOAT_64_TRIPLE, Neo4jPackV1.UnpackerV1::unpackDoubleTriple );
}

private void testUnsupportedPacking( PackType type, ThrowingConsumer<Neo4jPackV1.PackerV1,IOException> packerConsumer )
throws IOException
{ {
PackedOutputArray output = new PackedOutputArray(); PackedOutputArray output = new PackedOutputArray();
Neo4jPackV1.PackerV1 packer = (Neo4jPackV1.PackerV1) neo4jPack.newPacker( output ); Neo4jPackV1.PackerV1 packer = (Neo4jPackV1.PackerV1) neo4jPack.newPacker( output );


try try
{ {
packer.pack( 42.0, 42.0 ); packerConsumer.accept( packer );
fail( "Exception expected" ); fail( "Exception expected" );
} }
catch ( PackStream.Unsupported e ) catch ( PackStream.Unsupported e )
{ {
assertThat( e.getMessage(), containsString( PackType.FLOAT_PAIR.toString() ) ); assertThat( e.getMessage(), containsString( type.toString() ) );
} }
} }


@Test private void testUnsupportedUnpacking( PackType type, byte marker, ThrowingConsumer<Neo4jPackV1.UnpackerV1,IOException> unpackerConsumer )
public void shouldFailToUnpackDoublePair() throws IOException throws IOException
{ {
PackedInputArray input = new PackedInputArray( new byte[]{PackStream.FLOAT_64_PAIR} ); PackedInputArray input = new PackedInputArray( new byte[]{marker} );
Neo4jPackV1.UnpackerV1 unpacker = (Neo4jPackV1.UnpackerV1) neo4jPack.newUnpacker( input ); Neo4jPackV1.UnpackerV1 unpacker = (Neo4jPackV1.UnpackerV1) neo4jPack.newUnpacker( input );


try try
{ {
unpacker.unpackDoublePair(); unpackerConsumer.accept( unpacker );
fail( "Exception expected" ); fail( "Exception expected" );
} }
catch ( PackStream.Unsupported e ) catch ( PackStream.Unsupported e )
{ {
assertThat( e.getMessage(), containsString( PackType.FLOAT_PAIR.toString() ) ); assertThat( e.getMessage(), containsString( type.toString() ) );
} }
} }
} }
Expand Up @@ -38,8 +38,6 @@
import static java.util.stream.Collectors.toList; import static java.util.stream.Collectors.toList;
import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertEquals;
import static org.junit.Assert.fail; import static org.junit.Assert.fail;
import static org.neo4j.bolt.v2.messaging.Neo4jPackV2.MAX_POINT_DIMENSIONS;
import static org.neo4j.bolt.v2.messaging.Neo4jPackV2.MIN_POINT_DIMENSIONS;
import static org.neo4j.values.storable.CoordinateReferenceSystem.Cartesian; import static org.neo4j.values.storable.CoordinateReferenceSystem.Cartesian;
import static org.neo4j.values.storable.CoordinateReferenceSystem.WGS84; import static org.neo4j.values.storable.CoordinateReferenceSystem.WGS84;
import static org.neo4j.values.storable.Values.doubleValue; import static org.neo4j.values.storable.Values.doubleValue;
Expand All @@ -54,13 +52,13 @@ public class Neo4jPackV2Test
public void shouldFailToPackPointWithIllegalDimensions() throws IOException public void shouldFailToPackPointWithIllegalDimensions() throws IOException
{ {
testPackingPointsWithWrongDimensions( 0 ); testPackingPointsWithWrongDimensions( 0 );
testPackingPointsWithWrongDimensions( MIN_POINT_DIMENSIONS - 1 ); testPackingPointsWithWrongDimensions( 1 );
testPackingPointsWithWrongDimensions( MAX_POINT_DIMENSIONS + 1 ); testPackingPointsWithWrongDimensions( 4 );
testPackingPointsWithWrongDimensions( 100 ); testPackingPointsWithWrongDimensions( 100 );
} }


@Test @Test
public void shouldFailToUnpackPointWithTooFewDimensions() throws IOException public void shouldFailToUnpack2DPointWithIncorrectCoordinate() throws IOException
{ {
Neo4jPackV2 neo4jPack = new Neo4jPackV2(); Neo4jPackV2 neo4jPack = new Neo4jPackV2();
PackedOutputArray output = new PackedOutputArray(); PackedOutputArray output = new PackedOutputArray();
Expand All @@ -81,12 +79,36 @@ public void shouldFailToUnpackPointWithTooFewDimensions() throws IOException
} }


@Test @Test
public void shouldPackAndUnpackPoints() throws IOException public void shouldFailToUnpack3DPointWithIncorrectCoordinate() throws IOException
{ {
for ( int i = MIN_POINT_DIMENSIONS; i <= MAX_POINT_DIMENSIONS; i++ ) Neo4jPackV2 neo4jPack = new Neo4jPackV2();
PackedOutputArray output = new PackedOutputArray();
Neo4jPack.Packer packer = neo4jPack.newPacker( output );

packer.packStructHeader( 2, Neo4jPackV2.POINT_3D );
packer.pack( intValue( Cartesian.getCode() ) );
((PackStream.Packer) packer).pack( 1.0, 100.1 );

try
{ {
testPackingAndUnpackingOfPoints( i ); unpack( output );
fail( "Exception expected" );
} }
catch ( PackStream.Unexpected ignore )
{
}
}

@Test
public void shouldPackAndUnpack2DPoints() throws IOException
{
testPackingAndUnpackingOfPoints( 2 );
}

@Test
public void shouldPackAndUnpack3DPoints() throws IOException
{
testPackingAndUnpackingOfPoints( 3 );
} }


@Test @Test
Expand All @@ -108,7 +130,7 @@ public void shouldPackDoublePair() throws IOException
@Test @Test
public void shouldUnpackDoublePair() throws IOException public void shouldUnpackDoublePair() throws IOException
{ {
ByteBuffer buffer = ByteBuffer.allocate( Byte.BYTES + Double.BYTES + Double.BYTES ); ByteBuffer buffer = ByteBuffer.allocate( Byte.BYTES + Double.BYTES * 2 );
buffer.put( PackStream.FLOAT_64_PAIR ); buffer.put( PackStream.FLOAT_64_PAIR );
buffer.putDouble( 199.25 ); buffer.putDouble( 199.25 );
buffer.putDouble( 42.4242 ); buffer.putDouble( 42.4242 );
Expand All @@ -121,6 +143,41 @@ public void shouldUnpackDoublePair() throws IOException
assertEquals( 42.4242, values[1], DELTA ); assertEquals( 42.4242, values[1], DELTA );
} }


@Test
public void shouldPackDoubleTriple() throws IOException
{
Neo4jPackV2 neo4jPack = new Neo4jPackV2();
PackedOutputArray output = new PackedOutputArray();
Neo4jPack.Packer packer = neo4jPack.newPacker( output );

((PackStream.Packer) packer).pack( 0.7, 25.25, 10.90 );

ByteBuffer buffer = ByteBuffer.wrap( output.bytes() );
assertEquals( PackStream.FLOAT_64_TRIPLE, buffer.get() );
assertEquals( 0.7, buffer.getDouble(), DELTA );
assertEquals( 25.25, buffer.getDouble(), DELTA );
assertEquals( 10.90, buffer.getDouble(), DELTA );
assertEquals( 0, buffer.remaining() );
}

@Test
public void shouldUnpackDoubleTriple() throws IOException
{
ByteBuffer buffer = ByteBuffer.allocate( Byte.BYTES + Double.BYTES * 3 );
buffer.put( PackStream.FLOAT_64_TRIPLE );
buffer.putDouble( 12.45 );
buffer.putDouble( 42.42 );
buffer.putDouble( 0.05 );

Neo4jPackV2 neo4jPack = new Neo4jPackV2();
Neo4jPack.Unpacker unpacker = neo4jPack.newUnpacker( new PackedInputArray( buffer.array() ) );

double[] values = ((PackStream.Unpacker) unpacker).unpackDoubleTriple();
assertEquals( 12.45, values[0], DELTA );
assertEquals( 42.42, values[1], DELTA );
assertEquals( 0.05, values[2], DELTA );
}

private static void testPackingAndUnpackingOfPoints( int dimension ) throws IOException private static void testPackingAndUnpackingOfPoints( int dimension ) throws IOException
{ {
List<PointValue> points = IntStream.range( 0, 1000 ) List<PointValue> points = IntStream.range( 0, 1000 )
Expand Down

0 comments on commit c477338

Please sign in to comment.