Skip to content

Commit

Permalink
Add PostgisGeometryCodec
Browse files Browse the repository at this point in the history
[resolves #483][#491]

Signed-off-by: Seunghun Lee <waydi1@gmail.com>
  • Loading branch information
earlbread authored and mp911de committed Feb 16, 2022
1 parent 636cd83 commit 6be02d9
Show file tree
Hide file tree
Showing 4 changed files with 303 additions and 3 deletions.
7 changes: 7 additions & 0 deletions pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,7 @@
<scram-client.version>2.1</scram-client.version>
<spring-framework.version>5.3.14</spring-framework.version>
<testcontainers.version>1.16.2</testcontainers.version>
<jts-core.version>1.18.1</jts-core.version>
</properties>

<licenses>
Expand Down Expand Up @@ -156,6 +157,12 @@
<version>${jsr305.version}</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>org.locationtech.jts</groupId>
<artifactId>jts-core</artifactId>
<version>${jts-core.version}</version>
<scope>provided</scope>
</dependency>

<!-- Test Dependencies -->
<dependency>
Expand Down
34 changes: 31 additions & 3 deletions src/main/java/io/r2dbc/postgresql/codec/BuiltinDynamicCodecs.java
Original file line number Diff line number Diff line change
Expand Up @@ -33,9 +33,24 @@ public class BuiltinDynamicCodecs implements CodecRegistrar {

private static final Object EMPTY = new Object();

enum BuiltinCodec {
private interface CodecSupport {
default boolean isSupported() {
return true;
}
}

enum BuiltinCodec implements CodecSupport {

HSTORE("hstore"),
POSTGIS_GEOMETRY("geometry") {
@Override
public boolean isSupported() {
String className = "org.locationtech.jts.geom.Geometry";
ClassLoader classLoader = getClass().getClassLoader();

HSTORE("hstore");
return isPresent(classLoader, className);
}
};

private final String name;

Expand All @@ -48,6 +63,8 @@ public Codec<?> createCodec(ByteBufAllocator byteBufAllocator, int oid) {
switch (this) {
case HSTORE:
return new HStoreCodec(byteBufAllocator, oid);
case POSTGIS_GEOMETRY:
return new PostgisGeometryCodec(byteBufAllocator, oid);
default:
throw new UnsupportedOperationException(String.format("Codec %s for OID %d not supported", name(), oid));
}
Expand Down Expand Up @@ -81,7 +98,9 @@ public Publisher<Void> register(PostgresqlConnection connection, ByteBufAllocato
String typname = row.get("typname", String.class);

BuiltinCodec lookup = BuiltinCodec.lookup(typname);
registry.addLast(lookup.createCodec(byteBufAllocator, oid));
if (lookup.isSupported()) {
registry.addLast(lookup.createCodec(byteBufAllocator, oid));
}

return EMPTY;
})
Expand All @@ -96,4 +115,13 @@ private static String getPlaceholders() {
return Arrays.stream(BuiltinCodec.values()).map(s -> "'" + s.getName() + "'").collect(Collectors.joining(","));
}

private static boolean isPresent(ClassLoader classLoader, String fullyQualifiedClassName) {
try {
classLoader.loadClass(fullyQualifiedClassName);
return true;
} catch (ClassNotFoundException e) {
return false;
}
}

}
121 changes: 121 additions & 0 deletions src/main/java/io/r2dbc/postgresql/codec/PostgisGeometryCodec.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,121 @@
/*
* 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.ByteBufAllocator;
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.locationtech.jts.geom.Geometry;
import org.locationtech.jts.io.ParseException;
import org.locationtech.jts.io.WKBReader;
import reactor.core.publisher.Mono;

import javax.annotation.Nullable;
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 PostgisGeometryCodec implements Codec<Geometry>, CodecMetadata {

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

private final ByteBufAllocator byteBufAllocator;

private final int oid;

/**
* Create a new {@link PostgisGeometryCodec}.
*
* @param byteBufAllocator the type handled by this codec
*/
PostgisGeometryCodec(ByteBufAllocator byteBufAllocator, int oid) {
this.byteBufAllocator = Assert.requireNonNull(byteBufAllocator, "byteBufAllocator must not be null");
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");

return dataType == this.oid && 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 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().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;

return new EncodedParameter(Format.FORMAT_TEXT, oid, Mono.fromSupplier(
() -> ByteBufUtils.encode(byteBufAllocator, geometry.toText())
));
}

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

@Override
public EncodedParameter encodeNull() {
return new EncodedParameter(FORMAT_BINARY, 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
@@ -0,0 +1,144 @@
package io.r2dbc.postgresql.codec;

import io.netty.buffer.ByteBuf;
import io.r2dbc.postgresql.client.EncodedParameter;
import io.r2dbc.postgresql.client.ParameterAssert;
import io.r2dbc.postgresql.util.ByteBufUtils;
import org.junit.jupiter.api.Test;
import org.locationtech.jts.geom.Coordinate;
import org.locationtech.jts.geom.Geometry;
import org.locationtech.jts.geom.GeometryCollection;
import org.locationtech.jts.geom.GeometryFactory;
import org.locationtech.jts.geom.LineString;
import org.locationtech.jts.geom.LinearRing;
import org.locationtech.jts.geom.MultiLineString;
import org.locationtech.jts.geom.MultiPoint;
import org.locationtech.jts.geom.MultiPolygon;
import org.locationtech.jts.geom.Point;
import org.locationtech.jts.geom.Polygon;
import org.locationtech.jts.geom.PrecisionModel;
import org.locationtech.jts.io.WKBWriter;

import static io.r2dbc.postgresql.client.EncodedParameter.NULL_VALUE;
import static io.r2dbc.postgresql.codec.PostgresqlObjectId.JSON;
import static io.r2dbc.postgresql.codec.PostgresqlObjectId.JSONB;
import static io.r2dbc.postgresql.codec.PostgresqlObjectId.VARCHAR;
import static io.r2dbc.postgresql.message.Format.FORMAT_BINARY;
import static io.r2dbc.postgresql.message.Format.FORMAT_TEXT;
import static io.r2dbc.postgresql.util.TestByteBufAllocator.TEST;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException;

/**
* Unit tests for {@link PostgisGeometryCodec }.
*/
final class PostgisGeometryCodecUnitTests {

private static final int WGS84_SRID = 4326;

private static final int dataType = 23456;

private final PostgisGeometryCodec codec = new PostgisGeometryCodec(TEST, dataType);

private final WKBWriter wkbWriter = new WKBWriter();

private final GeometryFactory geometryFactory = new GeometryFactory(new PrecisionModel(), WGS84_SRID);

private final Point point = geometryFactory.createPoint(new Coordinate(1.0, 1.0));

@Test
void constructorNoByteBufAllocator() {
assertThatIllegalArgumentException().isThrownBy(() -> new PostgisGeometryCodec(null, dataType))
.withMessage("byteBufAllocator must not be null");
}

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

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

@Test
void canDecode() {
assertThat(codec.canDecode(dataType, FORMAT_TEXT, Geometry.class)).isTrue();
assertThat(codec.canDecode(dataType, FORMAT_BINARY, Geometry.class)).isTrue();

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

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

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

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

assertThat(codec.canEncode("Geometry")).isFalse();
assertThat(codec.canEncode(1)).isFalse();
}

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

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

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

@Test
void encode() {
ByteBuf encoded = ByteBufUtils.encode(TEST, point.toText());

ParameterAssert.assertThat(codec.encode(point))
.hasFormat(FORMAT_TEXT)
.hasType(dataType)
.hasValue(encoded);
}

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

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

}

0 comments on commit 6be02d9

Please sign in to comment.