diff --git a/src/main/java/io/r2dbc/postgresql/codec/AbstractPostgisCodec.java b/src/main/java/io/r2dbc/postgresql/codec/AbstractPostgisCodec.java new file mode 100644 index 00000000..f5b2a355 --- /dev/null +++ b/src/main/java/io/r2dbc/postgresql/codec/AbstractPostgisCodec.java @@ -0,0 +1,141 @@ +/* + * Copyright 2022 the original author or authors. + * + * 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 + * + * https://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 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; + +/** + * Shared PostGIS codec logic using {@link WKBReader} and {@link WKBWriter}. + * + * @param the Java type this codec represents, used to disambiguate {@code geometry} from {@code geography} values. + */ +abstract class AbstractPostgisCodec implements Codec, CodecMetadata { + + private final Class type; + + private final GeometryFactory geometryFactory = new GeometryFactory(); + + private final int oid; + + /** + * Create a new {@link AbstractPostgisCodec}. + */ + AbstractPostgisCodec(Class type, int oid) { + this.type = type; + 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 = T or T = type (T subtype) + return dataType == this.oid && (type.isAssignableFrom(this.type) || this.type.isAssignableFrom(type)); + } + + @Override + public boolean canEncode(Object value) { + Assert.requireNonNull(value, "value must not be null"); + + return this.type.isInstance(value); + } + + @Override + public boolean canEncodeNull(Class type) { + Assert.requireNonNull(type, "type must not be null"); + + return this.type.isAssignableFrom(type); + } + + @Override + public @Nullable T decode(@Nullable ByteBuf buffer, int dataType, Format format, Class type) { + if (buffer == null) { + return null; + } + + Assert.isTrue(format == FORMAT_TEXT, "format must be FORMAT_TEXT"); + + try { + Geometry geometry = new WKBReader(this.geometryFactory).read(WKBReader.hexToBytes(ByteBufUtils.decode(buffer))); + return wrap(geometry); + } catch (ParseException e) { + throw new IllegalArgumentException(e); + } + } + + @Override + public EncodedParameter encode(Object value) { + T typedValue = Assert.requireType(value, this.type, "value must be " + this.type.getSimpleName() + " type"); + Geometry geometry = unwrap(typedValue); + + 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 this.type; + } + + @Override + public Iterable getDataTypes() { + return Collections.singleton(AbstractCodec.getDataType(this.oid)); + } + + /** + * Wrap a decoded {@link Geometry} as the codec's Java type. + */ + abstract T wrap(Geometry geometry); + + /** + * Unwrap a value of the codec's Java type to the underlying {@link Geometry} to encode. + */ + abstract Geometry unwrap(T value); + +} diff --git a/src/main/java/io/r2dbc/postgresql/codec/BuiltinDynamicCodecs.java b/src/main/java/io/r2dbc/postgresql/codec/BuiltinDynamicCodecs.java index dfed3306..c31f25f2 100644 --- a/src/main/java/io/r2dbc/postgresql/codec/BuiltinDynamicCodecs.java +++ b/src/main/java/io/r2dbc/postgresql/codec/BuiltinDynamicCodecs.java @@ -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; @@ -62,6 +73,8 @@ public Iterable> 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> codecs = new ArrayList<>(3); diff --git a/src/main/java/io/r2dbc/postgresql/codec/Geography.java b/src/main/java/io/r2dbc/postgresql/codec/Geography.java new file mode 100644 index 00000000..4ab8043a --- /dev/null +++ b/src/main/java/io/r2dbc/postgresql/codec/Geography.java @@ -0,0 +1,81 @@ +/* + * Copyright 2022 the original author or authors. + * + * 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 + * + * https://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 io.r2dbc.postgresql.codec; + +import io.r2dbc.postgresql.util.Assert; +import org.locationtech.jts.geom.Geometry; + +import java.util.Objects; + +/** + * Wrapper around a JTS {@link Geometry} identifying it as a value bound for, or decoded from, a Postgres {@code geography} column. + *

JTS has no dedicated geodetic type, so {@code Geometry} alone is ambiguous between Postgres {@code geometry} and {@code geography} columns. This wrapper disambiguates the two so that + * {@link PostgisGeometryCodec} and {@link PostgisGeographyCodec} can each be matched unambiguously by Java type. + * + * @see PostgisGeographyCodec + */ +public final class Geography { + + private final Geometry geometry; + + private Geography(Geometry geometry) { + this.geometry = geometry; + } + + /** + * Wrap a {@link Geometry} to be encoded as a Postgres {@code geography} value. + * + * @param geometry the geometry to wrap + * @return the wrapped {@link Geography} + */ + public static Geography of(Geometry geometry) { + Assert.requireNonNull(geometry, "geometry must not be null"); + return new Geography(geometry); + } + + /** + * Returns the underlying {@link Geometry}. + * + * @return the underlying {@link Geometry} + */ + public Geometry getGeometry() { + return this.geometry; + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (!(o instanceof Geography)) { + return false; + } + Geography that = (Geography) o; + return this.geometry.equals(that.geometry); + } + + @Override + public int hashCode() { + return Objects.hashCode(this.geometry); + } + + @Override + public String toString() { + return "Geography[" + this.geometry + "]"; + } + +} diff --git a/src/main/java/io/r2dbc/postgresql/codec/PostgisGeographyCodec.java b/src/main/java/io/r2dbc/postgresql/codec/PostgisGeographyCodec.java new file mode 100644 index 00000000..8cac03fa --- /dev/null +++ b/src/main/java/io/r2dbc/postgresql/codec/PostgisGeographyCodec.java @@ -0,0 +1,27 @@ +package io.r2dbc.postgresql.codec; + +import org.locationtech.jts.geom.Geometry; + +/** + * PostGIS geography codec. + */ +final class PostgisGeographyCodec extends AbstractPostgisCodec { + + /** + * Create a new {@link PostgisGeographyCodec}. + */ + PostgisGeographyCodec(int oid) { + super(Geography.class, oid); + } + + @Override + Geography wrap(Geometry geometry) { + return Geography.of(geometry); + } + + @Override + Geometry unwrap(Geography value) { + return value.getGeometry(); + } + +} diff --git a/src/main/java/io/r2dbc/postgresql/codec/PostgisGeometryCodec.java b/src/main/java/io/r2dbc/postgresql/codec/PostgisGeometryCodec.java index 702a4197..01869df4 100644 --- a/src/main/java/io/r2dbc/postgresql/codec/PostgisGeometryCodec.java +++ b/src/main/java/io/r2dbc/postgresql/codec/PostgisGeometryCodec.java @@ -16,113 +16,30 @@ 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 org.locationtech.jts.io.WKTWriter; -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; /** * PostGIS codec using {@link WKBReader} and {@link WKTWriter}. */ -final class PostgisGeometryCodec implements Codec, CodecMetadata { - - private static final Class TYPE = Geometry.class; - - private final GeometryFactory geometryFactory = new GeometryFactory(); - - private final int oid; +final class PostgisGeometryCodec extends AbstractPostgisCodec { /** * Create a new {@link PostgisGeometryCodec}. */ PostgisGeometryCodec(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 = Geometry or Geometry = type (Geometry 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 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); + super(Geometry.class, oid); } @Override - public Class type() { - return TYPE; + Geometry wrap(Geometry geometry) { + return geometry; } @Override - public Iterable getDataTypes() { - return Collections.singleton(AbstractCodec.getDataType(this.oid)); + Geometry unwrap(Geometry value) { + return value; } } diff --git a/src/test/java/io/r2dbc/postgresql/codec/PostgisGeometryCodecUnitTest.java b/src/test/java/io/r2dbc/postgresql/codec/PostgisGeometryCodecUnitTest.java index 2ab20444..2ceb64e7 100644 --- a/src/test/java/io/r2dbc/postgresql/codec/PostgisGeometryCodecUnitTest.java +++ b/src/test/java/io/r2dbc/postgresql/codec/PostgisGeometryCodecUnitTest.java @@ -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(); @@ -64,92 +66,176 @@ 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 canDecodeNoClassGeography() { + assertThatIllegalArgumentException().isThrownBy(() -> this.geographyCodec.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 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(); + } + + @Test + void canDecodeGeography() { + assertThat(this.geographyCodec.canDecode(dataType, FORMAT_TEXT, Geography.class)).isTrue(); + assertThat(this.geographyCodec.canDecode(dataType, FORMAT_BINARY, Geography.class)).isTrue(); - 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(); + assertThat(this.geographyCodec.canDecode(dataType, FORMAT_TEXT, Geometry.class)).isFalse(); + assertThat(this.geographyCodec.canDecode(dataType, FORMAT_TEXT, Point.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(); + assertThat(this.geographyCodec.canDecode(VARCHAR.getObjectId(), FORMAT_BINARY, Geography.class)).isFalse(); + assertThat(this.geographyCodec.canDecode(JSON.getObjectId(), FORMAT_TEXT, Geography.class)).isFalse(); + assertThat(this.geographyCodec.canDecode(JSONB.getObjectId(), FORMAT_BINARY, Geography.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"); } @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 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(); + } - assertThat(this.codec.canEncode("Geometry")).isFalse(); - assertThat(this.codec.canEncode(1)).isFalse(); + @Test + void canEncodeGeography() { + assertThat(this.geographyCodec.canEncode(Geography.of(this.geometryFactory.createPoint()))).isTrue(); + assertThat(this.geographyCodec.canEncode(Geography.of(this.geometryFactory.createMultiPoint()))).isTrue(); + assertThat(this.geographyCodec.canEncode(Geography.of(this.geometryFactory.createLineString()))).isTrue(); + assertThat(this.geographyCodec.canEncode(Geography.of(this.geometryFactory.createLinearRing()))).isTrue(); + assertThat(this.geographyCodec.canEncode(Geography.of(this.geometryFactory.createMultiLineString()))).isTrue(); + assertThat(this.geographyCodec.canEncode(Geography.of(this.geometryFactory.createPolygon()))).isTrue(); + assertThat(this.geographyCodec.canEncode(Geography.of(this.geometryFactory.createMultiPolygon()))).isTrue(); + assertThat(this.geographyCodec.canEncode(Geography.of(this.geometryFactory.createGeometryCollection()))).isTrue(); + + assertThat(this.geographyCodec.canEncode(this.geometryFactory.createPoint())).isFalse(); + assertThat(this.geographyCodec.canEncode("Geography")).isFalse(); + assertThat(this.geographyCodec.canEncode(1)).isFalse(); } @Test @SuppressWarnings("unchecked") - void decode() { + void decodeGeometry() { 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.geometryCodec.decode(pointByteBuf, dataType, FORMAT_TEXT, Geometry.class)).isEqualTo(this.point); } @Test @SuppressWarnings("unchecked") - void decodeNoByteBuf() { - assertThat(this.codec.decode(null, dataType, FORMAT_TEXT, Geometry.class)).isNull(); + void decodeGeography() { + byte[] pointBytes = this.wkbWriter.write(this.point); + ByteBuf pointByteBuf = ByteBufUtils.encode(TEST, WKBWriter.toHex(pointBytes)); + + assertThat(this.geographyCodec.decode(pointByteBuf, dataType, FORMAT_TEXT, Geography.class)).isEqualTo(Geography.of(this.point)); } @Test - void encode() { + @SuppressWarnings("unchecked") + void decodeNoByteBufGeometry() { + assertThat(this.geometryCodec.decode(null, dataType, FORMAT_TEXT, Geometry.class)).isNull(); + } + + @Test + @SuppressWarnings("unchecked") + void decodeNoByteBufGeography() { + assertThat(this.geographyCodec.decode(null, dataType, FORMAT_TEXT, Geography.class)).isNull(); + } + + @Test + void encodeGeometry() { + ByteBuf encoded = Unpooled.wrappedBuffer(new WKBWriter(2, true).write(this.point)); + + ParameterAssert.assertThat(this.geometryCodec.encode(this.point)) + .hasFormat(FORMAT_BINARY) + .hasType(dataType) + .hasValue(encoded); + } + + @Test + void encodeGeography() { ByteBuf encoded = Unpooled.wrappedBuffer(new WKBWriter(2, true).write(this.point)); - ParameterAssert.assertThat(this.codec.encode(this.point)) + ParameterAssert.assertThat(this.geographyCodec.encode(Geography.of(this.point))) .hasFormat(FORMAT_BINARY) .hasType(dataType) .hasValue(encoded); } @Test - void encodeNoValue() { - assertThatIllegalArgumentException().isThrownBy(() -> this.codec.encode(null)) + void encodeNoValueGeometry() { + assertThatIllegalArgumentException().isThrownBy(() -> this.geometryCodec.encode(null)) .withMessage("value must not be null"); } @Test - void encodeNull() { + void encodeNoValueGeography() { + assertThatIllegalArgumentException().isThrownBy(() -> this.geographyCodec.encode(null)) + .withMessage("value must not be null"); + } + + @Test + 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)); + } + }