Skip to content
Open
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
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,18 @@ enum BuiltinCodec {
public boolean isSupported() {
return this.jtsPresent;
}
}, VECTOR("vector");
},
POSTGIS_GEOGRAPHY("geography") {

private final boolean jtsPresent = isPresent(BuiltinDynamicCodecs.class.getClassLoader(), "org.locationtech.jts.geom.Geometry");

@Override
public boolean isSupported() {
return this.jtsPresent;
}
},

VECTOR("vector");

private final String name;

Expand All @@ -62,6 +73,8 @@ public Iterable<Codec<?>> createCodec(ByteBufAllocator byteBufAllocator, int oid
return Collections.singletonList(new HStoreCodec(byteBufAllocator, oid));
case POSTGIS_GEOMETRY:
return Collections.singletonList(new PostgisGeometryCodec(oid));
case POSTGIS_GEOGRAPHY:
return Collections.singletonList(new PostgisGeographyCodec(oid));
case VECTOR:
VectorCodec vectorCodec = new VectorCodec(byteBufAllocator, oid, typarray);
List<Codec<?>> codecs = new ArrayList<>(3);
Expand Down
108 changes: 108 additions & 0 deletions src/main/java/io/r2dbc/postgresql/codec/PostgisGeographyCodec.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
package io.r2dbc.postgresql.codec;

import io.netty.buffer.ByteBuf;
import io.netty.buffer.Unpooled;
import io.r2dbc.postgresql.client.EncodedParameter;
import io.r2dbc.postgresql.message.Format;
import io.r2dbc.postgresql.util.Assert;
import io.r2dbc.postgresql.util.ByteBufUtils;
import org.jspecify.annotations.Nullable;
import org.locationtech.jts.geom.Geometry;
import org.locationtech.jts.geom.GeometryFactory;
import org.locationtech.jts.io.ParseException;
import org.locationtech.jts.io.WKBReader;
import org.locationtech.jts.io.WKBWriter;
import reactor.core.publisher.Mono;

import java.util.Collections;

import static io.r2dbc.postgresql.client.EncodedParameter.NULL_VALUE;
import static io.r2dbc.postgresql.message.Format.FORMAT_BINARY;
import static io.r2dbc.postgresql.message.Format.FORMAT_TEXT;

final class PostgisGeographyCodec implements Codec<Geometry>, CodecMetadata {

private static final Class<Geometry> TYPE = Geometry.class;

private final GeometryFactory geometryFactory = new GeometryFactory();

private final int oid;

/**
* Create a new {@link PostgisGeographyCodec}.
*/
PostgisGeographyCodec(int oid) {
this.oid = oid;
}

@Override
public boolean canDecode(int dataType, Format format, Class<?> type) {
Assert.requireNonNull(format, "format must not be null");
Assert.requireNonNull(type, "type must not be null");

// Object = Geography or Geography = type (Geography subtype)
return dataType == this.oid && (type.isAssignableFrom(TYPE) || TYPE.isAssignableFrom(type));
}

@Override
public boolean canEncode(Object value) {
Assert.requireNonNull(value, "value must not be null");

return TYPE.isInstance(value);
}

@Override
public boolean canEncodeNull(Class<?> type) {
Assert.requireNonNull(type, "type must not be null");

return TYPE.isAssignableFrom(type);
}

@Override
public @Nullable Geometry decode(@Nullable ByteBuf buffer, int dataType, Format format, Class<? extends Geometry> type) {
if (buffer == null) {
return null;
}

Assert.isTrue(format == FORMAT_TEXT, "format must be FORMAT_TEXT");

try {
return new WKBReader(this.geometryFactory).read(WKBReader.hexToBytes(ByteBufUtils.decode(buffer)));
} catch (ParseException e) {
throw new IllegalArgumentException(e);
}
}

@Override
public EncodedParameter encode(Object value) {
Assert.requireType(value, Geometry.class, "value must be Geometry type");
Geometry geometry = (Geometry) value;

WKBWriter writer = new WKBWriter(2, true);

return new EncodedParameter(FORMAT_BINARY, this.oid, Mono.fromSupplier(
() -> Unpooled.wrappedBuffer(writer.write(geometry))
));
}

@Override
public EncodedParameter encode(Object value, int dataType) {
return encode(value);
}

@Override
public EncodedParameter encodeNull() {
return new EncodedParameter(FORMAT_BINARY, this.oid, NULL_VALUE);
}

@Override
public Class<?> type() {
return TYPE;
}

@Override
public Iterable<PostgresTypeIdentifier> getDataTypes() {
return Collections.singleton(AbstractCodec.getDataType(this.oid));
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,9 @@ final class PostgisGeometryCodecUnitTests {

private static final int dataType = 23456;

private final PostgisGeometryCodec codec = new PostgisGeometryCodec(dataType);
private final PostgisGeometryCodec geometryCodec = new PostgisGeometryCodec(dataType);

private final PostgisGeographyCodec geographyCodec = new PostgisGeographyCodec(dataType);

private final WKBWriter wkbWriter = new WKBWriter();

Expand All @@ -64,92 +66,180 @@ final class PostgisGeometryCodecUnitTests {
private final Point point = this.geometryFactory.createPoint(new Coordinate(1.0, 1.0));

@Test
void canDecodeNoFormat() {
assertThatIllegalArgumentException().isThrownBy(() -> this.codec.canDecode(dataType, null, Geometry.class))
void canDecodeNoFormatGeometry() {
assertThatIllegalArgumentException().isThrownBy(() -> this.geometryCodec.canDecode(dataType, null, Geometry.class))
.withMessage("format must not be null");
}

@Test
void canDecodeNoFormatGeography() {
assertThatIllegalArgumentException().isThrownBy(() -> this.geographyCodec.canDecode(dataType, null, Geometry.class))
.withMessage("format must not be null");
}

@Test
void canDecodeNoClass() {
assertThatIllegalArgumentException().isThrownBy(() -> this.codec.canDecode(dataType, FORMAT_TEXT, null))
void canDecodeNoClassGeometry() {
assertThatIllegalArgumentException().isThrownBy(() -> this.geometryCodec.canDecode(dataType, FORMAT_TEXT, null))
.withMessage("type must not be null");
}

@Test
void canDecode() {
assertThat(this.codec.canDecode(dataType, FORMAT_TEXT, Geometry.class)).isTrue();
assertThat(this.codec.canDecode(dataType, FORMAT_BINARY, Geometry.class)).isTrue();
void canDecodeNoClassGeography() {
assertThatIllegalArgumentException().isThrownBy(() -> this.geographyCodec.canDecode(dataType, FORMAT_TEXT, null))
.withMessage("type must not be null");
}

assertThat(this.codec.canDecode(dataType, FORMAT_TEXT, Point.class)).isTrue();
assertThat(this.codec.canDecode(dataType, FORMAT_TEXT, MultiPoint.class)).isTrue();
assertThat(this.codec.canDecode(dataType, FORMAT_TEXT, LineString.class)).isTrue();
assertThat(this.codec.canDecode(dataType, FORMAT_TEXT, LinearRing.class)).isTrue();
assertThat(this.codec.canDecode(dataType, FORMAT_TEXT, MultiLineString.class)).isTrue();
assertThat(this.codec.canDecode(dataType, FORMAT_TEXT, Polygon.class)).isTrue();
assertThat(this.codec.canDecode(dataType, FORMAT_TEXT, MultiPolygon.class)).isTrue();
assertThat(this.codec.canDecode(dataType, FORMAT_TEXT, GeometryCollection.class)).isTrue();
@Test
void canDecodeGeometry() {
assertThat(this.geometryCodec.canDecode(dataType, FORMAT_TEXT, Geometry.class)).isTrue();
assertThat(this.geometryCodec.canDecode(dataType, FORMAT_BINARY, Geometry.class)).isTrue();

assertThat(this.geometryCodec.canDecode(dataType, FORMAT_TEXT, Point.class)).isTrue();
assertThat(this.geometryCodec.canDecode(dataType, FORMAT_TEXT, MultiPoint.class)).isTrue();
assertThat(this.geometryCodec.canDecode(dataType, FORMAT_TEXT, LineString.class)).isTrue();
assertThat(this.geometryCodec.canDecode(dataType, FORMAT_TEXT, LinearRing.class)).isTrue();
assertThat(this.geometryCodec.canDecode(dataType, FORMAT_TEXT, MultiLineString.class)).isTrue();
assertThat(this.geometryCodec.canDecode(dataType, FORMAT_TEXT, Polygon.class)).isTrue();
assertThat(this.geometryCodec.canDecode(dataType, FORMAT_TEXT, MultiPolygon.class)).isTrue();
assertThat(this.geometryCodec.canDecode(dataType, FORMAT_TEXT, GeometryCollection.class)).isTrue();

assertThat(this.geometryCodec.canDecode(VARCHAR.getObjectId(), FORMAT_BINARY, Geometry.class)).isFalse();
assertThat(this.geometryCodec.canDecode(JSON.getObjectId(), FORMAT_TEXT, Geometry.class)).isFalse();
assertThat(this.geometryCodec.canDecode(JSONB.getObjectId(), FORMAT_BINARY, Geometry.class)).isFalse();
}

assertThat(this.codec.canDecode(VARCHAR.getObjectId(), FORMAT_BINARY, Geometry.class)).isFalse();
assertThat(this.codec.canDecode(JSON.getObjectId(), FORMAT_TEXT, Geometry.class)).isFalse();
assertThat(this.codec.canDecode(JSONB.getObjectId(), FORMAT_BINARY, Geometry.class)).isFalse();
@Test
void canDecodeGeography() {
assertThat(this.geographyCodec.canDecode(dataType, FORMAT_TEXT, Geometry.class)).isTrue();
assertThat(this.geographyCodec.canDecode(dataType, FORMAT_BINARY, Geometry.class)).isTrue();

assertThat(this.geographyCodec.canDecode(dataType, FORMAT_TEXT, Point.class)).isTrue();
assertThat(this.geographyCodec.canDecode(dataType, FORMAT_TEXT, MultiPoint.class)).isTrue();
assertThat(this.geographyCodec.canDecode(dataType, FORMAT_TEXT, LineString.class)).isTrue();
assertThat(this.geographyCodec.canDecode(dataType, FORMAT_TEXT, LinearRing.class)).isTrue();
assertThat(this.geographyCodec.canDecode(dataType, FORMAT_TEXT, MultiLineString.class)).isTrue();
assertThat(this.geographyCodec.canDecode(dataType, FORMAT_TEXT, Polygon.class)).isTrue();
assertThat(this.geographyCodec.canDecode(dataType, FORMAT_TEXT, MultiPolygon.class)).isTrue();
assertThat(this.geographyCodec.canDecode(dataType, FORMAT_TEXT, GeometryCollection.class)).isTrue();

assertThat(this.geographyCodec.canDecode(VARCHAR.getObjectId(), FORMAT_BINARY, Geometry.class)).isFalse();
assertThat(this.geographyCodec.canDecode(JSON.getObjectId(), FORMAT_TEXT, Geometry.class)).isFalse();
assertThat(this.geographyCodec.canDecode(JSONB.getObjectId(), FORMAT_BINARY, Geometry.class)).isFalse();
}

@Test
void canEncodeNoValue() {
assertThatIllegalArgumentException().isThrownBy(() -> this.codec.canEncode(null))
void canEncodeNoValueGeometry() {
assertThatIllegalArgumentException().isThrownBy(() -> this.geometryCodec.canEncode(null))
.withMessage("value must not be null");
}

void canEncodeNoValueGeography() {
assertThatIllegalArgumentException().isThrownBy(() -> this.geographyCodec.canEncode(null))
.withMessage("value must not be null");
}

@Test
void canEncodeGeometry() {
assertThat(this.geometryCodec.canEncode(this.geometryFactory.createPoint())).isTrue();
assertThat(this.geometryCodec.canEncode(this.geometryFactory.createMultiPoint())).isTrue();
assertThat(this.geometryCodec.canEncode(this.geometryFactory.createLineString())).isTrue();
assertThat(this.geometryCodec.canEncode(this.geometryFactory.createLinearRing())).isTrue();
assertThat(this.geometryCodec.canEncode(this.geometryFactory.createMultiLineString())).isTrue();
assertThat(this.geometryCodec.canEncode(this.geometryFactory.createPolygon())).isTrue();
assertThat(this.geometryCodec.canEncode(this.geometryFactory.createMultiPolygon())).isTrue();
assertThat(this.geometryCodec.canEncode(this.geometryFactory.createGeometryCollection())).isTrue();

assertThat(this.geometryCodec.canEncode("Geometry")).isFalse();
assertThat(this.geometryCodec.canEncode(1)).isFalse();
}

@Test
void canEncode() {
assertThat(this.codec.canEncode(this.geometryFactory.createPoint())).isTrue();
assertThat(this.codec.canEncode(this.geometryFactory.createMultiPoint())).isTrue();
assertThat(this.codec.canEncode(this.geometryFactory.createLineString())).isTrue();
assertThat(this.codec.canEncode(this.geometryFactory.createLinearRing())).isTrue();
assertThat(this.codec.canEncode(this.geometryFactory.createMultiLineString())).isTrue();
assertThat(this.codec.canEncode(this.geometryFactory.createPolygon())).isTrue();
assertThat(this.codec.canEncode(this.geometryFactory.createMultiPolygon())).isTrue();
assertThat(this.codec.canEncode(this.geometryFactory.createGeometryCollection())).isTrue();
void canEncodeGeography() {
assertThat(this.geographyCodec.canEncode(this.geometryFactory.createPoint())).isTrue();
assertThat(this.geographyCodec.canEncode(this.geometryFactory.createMultiPoint())).isTrue();
assertThat(this.geographyCodec.canEncode(this.geometryFactory.createLineString())).isTrue();
assertThat(this.geographyCodec.canEncode(this.geometryFactory.createLinearRing())).isTrue();
assertThat(this.geographyCodec.canEncode(this.geometryFactory.createMultiLineString())).isTrue();
assertThat(this.geographyCodec.canEncode(this.geometryFactory.createPolygon())).isTrue();
assertThat(this.geographyCodec.canEncode(this.geometryFactory.createMultiPolygon())).isTrue();
assertThat(this.geographyCodec.canEncode(this.geometryFactory.createGeometryCollection())).isTrue();

assertThat(this.geographyCodec.canEncode("Geography")).isFalse();
assertThat(this.geographyCodec.canEncode(1)).isFalse();
}

@Test
@SuppressWarnings("unchecked")
void decodeGeometry() {
byte[] pointBytes = this.wkbWriter.write(this.point);
ByteBuf pointByteBuf = ByteBufUtils.encode(TEST, WKBWriter.toHex(pointBytes));

assertThat(this.codec.canEncode("Geometry")).isFalse();
assertThat(this.codec.canEncode(1)).isFalse();
assertThat(this.geometryCodec.decode(pointByteBuf, dataType, FORMAT_TEXT, Geometry.class)).isEqualTo(this.point);
}

@Test
@SuppressWarnings("unchecked")
void decode() {
void decodeGeography() {
byte[] pointBytes = this.wkbWriter.write(this.point);
ByteBuf pointByteBuf = ByteBufUtils.encode(TEST, WKBWriter.toHex(pointBytes));

assertThat(this.codec.decode(pointByteBuf, dataType, FORMAT_TEXT, Geometry.class)).isEqualTo(this.point);
assertThat(this.geographyCodec.decode(pointByteBuf, dataType, FORMAT_TEXT, Geometry.class)).isEqualTo(this.point);
}

@Test
@SuppressWarnings("unchecked")
void decodeNoByteBufGeometry() {
assertThat(this.geometryCodec.decode(null, dataType, FORMAT_TEXT, Geometry.class)).isNull();
}

@Test
@SuppressWarnings("unchecked")
void decodeNoByteBuf() {
assertThat(this.codec.decode(null, dataType, FORMAT_TEXT, Geometry.class)).isNull();
void decodeNoByteBufGeography() {
assertThat(this.geographyCodec.decode(null, dataType, FORMAT_TEXT, Geometry.class)).isNull();
}

@Test
void encode() {
void encodeGeometry() {
ByteBuf encoded = Unpooled.wrappedBuffer(new WKBWriter(2, true).write(this.point));

ParameterAssert.assertThat(this.codec.encode(this.point))
ParameterAssert.assertThat(this.geometryCodec.encode(this.point))
.hasFormat(FORMAT_BINARY)
.hasType(dataType)
.hasValue(encoded);
}

@Test
void encodeNoValue() {
assertThatIllegalArgumentException().isThrownBy(() -> this.codec.encode(null))
void encodeGeography() {
ByteBuf encoded = Unpooled.wrappedBuffer(new WKBWriter(2, true).write(this.point));

ParameterAssert.assertThat(this.geographyCodec.encode(this.point))
.hasFormat(FORMAT_BINARY)
.hasType(dataType)
.hasValue(encoded);
}

@Test
void encodeNoValueGeometry() {
assertThatIllegalArgumentException().isThrownBy(() -> this.geometryCodec.encode(null))
.withMessage("value must not be null");
}

@Test
void encodeNoValueGeography() {
assertThatIllegalArgumentException().isThrownBy(() -> this.geometryCodec.encode(null))
.withMessage("value must not be null");
}

@Test
void encodeNull() {
void encodeNullGeometry() {
assertThat(new PostgisGeometryCodec(dataType).encodeNull())
.isEqualTo(new EncodedParameter(FORMAT_BINARY, dataType, NULL_VALUE));
}

@Test
void encodeNullGeography() {
assertThat(new PostgisGeographyCodec(dataType).encodeNull())
.isEqualTo(new EncodedParameter(FORMAT_BINARY, dataType, NULL_VALUE));
}

}