Skip to content

Commit 6be02d9

Browse files
earlbreadmp911de
authored andcommitted
Add PostgisGeometryCodec
[resolves #483][#491] Signed-off-by: Seunghun Lee <[email protected]>
1 parent 636cd83 commit 6be02d9

File tree

4 files changed

+303
-3
lines changed

4 files changed

+303
-3
lines changed

pom.xml

+7
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,7 @@
5252
<scram-client.version>2.1</scram-client.version>
5353
<spring-framework.version>5.3.14</spring-framework.version>
5454
<testcontainers.version>1.16.2</testcontainers.version>
55+
<jts-core.version>1.18.1</jts-core.version>
5556
</properties>
5657

5758
<licenses>
@@ -156,6 +157,12 @@
156157
<version>${jsr305.version}</version>
157158
<scope>provided</scope>
158159
</dependency>
160+
<dependency>
161+
<groupId>org.locationtech.jts</groupId>
162+
<artifactId>jts-core</artifactId>
163+
<version>${jts-core.version}</version>
164+
<scope>provided</scope>
165+
</dependency>
159166

160167
<!-- Test Dependencies -->
161168
<dependency>

src/main/java/io/r2dbc/postgresql/codec/BuiltinDynamicCodecs.java

+31-3
Original file line numberDiff line numberDiff line change
@@ -33,9 +33,24 @@ public class BuiltinDynamicCodecs implements CodecRegistrar {
3333

3434
private static final Object EMPTY = new Object();
3535

36-
enum BuiltinCodec {
36+
private interface CodecSupport {
37+
default boolean isSupported() {
38+
return true;
39+
}
40+
}
41+
42+
enum BuiltinCodec implements CodecSupport {
43+
44+
HSTORE("hstore"),
45+
POSTGIS_GEOMETRY("geometry") {
46+
@Override
47+
public boolean isSupported() {
48+
String className = "org.locationtech.jts.geom.Geometry";
49+
ClassLoader classLoader = getClass().getClassLoader();
3750

38-
HSTORE("hstore");
51+
return isPresent(classLoader, className);
52+
}
53+
};
3954

4055
private final String name;
4156

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

83100
BuiltinCodec lookup = BuiltinCodec.lookup(typname);
84-
registry.addLast(lookup.createCodec(byteBufAllocator, oid));
101+
if (lookup.isSupported()) {
102+
registry.addLast(lookup.createCodec(byteBufAllocator, oid));
103+
}
85104

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

118+
private static boolean isPresent(ClassLoader classLoader, String fullyQualifiedClassName) {
119+
try {
120+
classLoader.loadClass(fullyQualifiedClassName);
121+
return true;
122+
} catch (ClassNotFoundException e) {
123+
return false;
124+
}
125+
}
126+
99127
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,121 @@
1+
/*
2+
* Copyright 2022 the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package io.r2dbc.postgresql.codec;
18+
19+
import io.netty.buffer.ByteBuf;
20+
import io.netty.buffer.ByteBufAllocator;
21+
import io.r2dbc.postgresql.client.EncodedParameter;
22+
import io.r2dbc.postgresql.message.Format;
23+
import io.r2dbc.postgresql.util.Assert;
24+
import io.r2dbc.postgresql.util.ByteBufUtils;
25+
import org.locationtech.jts.geom.Geometry;
26+
import org.locationtech.jts.io.ParseException;
27+
import org.locationtech.jts.io.WKBReader;
28+
import reactor.core.publisher.Mono;
29+
30+
import javax.annotation.Nullable;
31+
import java.util.Collections;
32+
33+
import static io.r2dbc.postgresql.client.EncodedParameter.NULL_VALUE;
34+
import static io.r2dbc.postgresql.message.Format.FORMAT_BINARY;
35+
import static io.r2dbc.postgresql.message.Format.FORMAT_TEXT;
36+
37+
final class PostgisGeometryCodec implements Codec<Geometry>, CodecMetadata {
38+
39+
private static final Class<Geometry> TYPE = Geometry.class;
40+
41+
private final ByteBufAllocator byteBufAllocator;
42+
43+
private final int oid;
44+
45+
/**
46+
* Create a new {@link PostgisGeometryCodec}.
47+
*
48+
* @param byteBufAllocator the type handled by this codec
49+
*/
50+
PostgisGeometryCodec(ByteBufAllocator byteBufAllocator, int oid) {
51+
this.byteBufAllocator = Assert.requireNonNull(byteBufAllocator, "byteBufAllocator must not be null");
52+
this.oid = oid;
53+
}
54+
55+
@Override
56+
public boolean canDecode(int dataType, Format format, Class<?> type) {
57+
Assert.requireNonNull(format, "format must not be null");
58+
Assert.requireNonNull(type, "type must not be null");
59+
60+
return dataType == this.oid && TYPE.isAssignableFrom(type);
61+
}
62+
63+
@Override
64+
public boolean canEncode(Object value) {
65+
Assert.requireNonNull(value, "value must not be null");
66+
67+
return TYPE.isInstance(value);
68+
}
69+
70+
@Override
71+
public boolean canEncodeNull(Class<?> type) {
72+
Assert.requireNonNull(type, "type must not be null");
73+
74+
return TYPE.isAssignableFrom(type);
75+
}
76+
77+
@Override
78+
public Geometry decode(@Nullable ByteBuf buffer, int dataType, Format format, Class<? extends Geometry> type) {
79+
if (buffer == null) {
80+
return null;
81+
}
82+
83+
Assert.isTrue(format == FORMAT_TEXT, "format must be FORMAT_TEXT");
84+
85+
try {
86+
return new WKBReader().read(WKBReader.hexToBytes(ByteBufUtils.decode(buffer)));
87+
} catch (ParseException e) {
88+
throw new IllegalArgumentException(e);
89+
}
90+
}
91+
92+
@Override
93+
public EncodedParameter encode(Object value) {
94+
Assert.requireType(value, Geometry.class, "value must be Geometry type");
95+
Geometry geometry = (Geometry) value;
96+
97+
return new EncodedParameter(Format.FORMAT_TEXT, oid, Mono.fromSupplier(
98+
() -> ByteBufUtils.encode(byteBufAllocator, geometry.toText())
99+
));
100+
}
101+
102+
@Override
103+
public EncodedParameter encode(Object value, int dataType) {
104+
return encode(value);
105+
}
106+
107+
@Override
108+
public EncodedParameter encodeNull() {
109+
return new EncodedParameter(FORMAT_BINARY, oid, NULL_VALUE);
110+
}
111+
112+
@Override
113+
public Class<?> type() {
114+
return TYPE;
115+
}
116+
117+
@Override
118+
public Iterable<PostgresTypeIdentifier> getDataTypes() {
119+
return Collections.singleton(AbstractCodec.getDataType(this.oid));
120+
}
121+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,144 @@
1+
package io.r2dbc.postgresql.codec;
2+
3+
import io.netty.buffer.ByteBuf;
4+
import io.r2dbc.postgresql.client.EncodedParameter;
5+
import io.r2dbc.postgresql.client.ParameterAssert;
6+
import io.r2dbc.postgresql.util.ByteBufUtils;
7+
import org.junit.jupiter.api.Test;
8+
import org.locationtech.jts.geom.Coordinate;
9+
import org.locationtech.jts.geom.Geometry;
10+
import org.locationtech.jts.geom.GeometryCollection;
11+
import org.locationtech.jts.geom.GeometryFactory;
12+
import org.locationtech.jts.geom.LineString;
13+
import org.locationtech.jts.geom.LinearRing;
14+
import org.locationtech.jts.geom.MultiLineString;
15+
import org.locationtech.jts.geom.MultiPoint;
16+
import org.locationtech.jts.geom.MultiPolygon;
17+
import org.locationtech.jts.geom.Point;
18+
import org.locationtech.jts.geom.Polygon;
19+
import org.locationtech.jts.geom.PrecisionModel;
20+
import org.locationtech.jts.io.WKBWriter;
21+
22+
import static io.r2dbc.postgresql.client.EncodedParameter.NULL_VALUE;
23+
import static io.r2dbc.postgresql.codec.PostgresqlObjectId.JSON;
24+
import static io.r2dbc.postgresql.codec.PostgresqlObjectId.JSONB;
25+
import static io.r2dbc.postgresql.codec.PostgresqlObjectId.VARCHAR;
26+
import static io.r2dbc.postgresql.message.Format.FORMAT_BINARY;
27+
import static io.r2dbc.postgresql.message.Format.FORMAT_TEXT;
28+
import static io.r2dbc.postgresql.util.TestByteBufAllocator.TEST;
29+
import static org.assertj.core.api.Assertions.assertThat;
30+
import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException;
31+
32+
/**
33+
* Unit tests for {@link PostgisGeometryCodec }.
34+
*/
35+
final class PostgisGeometryCodecUnitTests {
36+
37+
private static final int WGS84_SRID = 4326;
38+
39+
private static final int dataType = 23456;
40+
41+
private final PostgisGeometryCodec codec = new PostgisGeometryCodec(TEST, dataType);
42+
43+
private final WKBWriter wkbWriter = new WKBWriter();
44+
45+
private final GeometryFactory geometryFactory = new GeometryFactory(new PrecisionModel(), WGS84_SRID);
46+
47+
private final Point point = geometryFactory.createPoint(new Coordinate(1.0, 1.0));
48+
49+
@Test
50+
void constructorNoByteBufAllocator() {
51+
assertThatIllegalArgumentException().isThrownBy(() -> new PostgisGeometryCodec(null, dataType))
52+
.withMessage("byteBufAllocator must not be null");
53+
}
54+
55+
@Test
56+
void canDecodeNoFormat() {
57+
assertThatIllegalArgumentException().isThrownBy(() -> codec.canDecode(dataType, null, Geometry.class))
58+
.withMessage("format must not be null");
59+
}
60+
61+
@Test
62+
void canDecodeNoClass() {
63+
assertThatIllegalArgumentException().isThrownBy(() -> codec.canDecode(dataType, FORMAT_TEXT, null))
64+
.withMessage("type must not be null");
65+
}
66+
67+
@Test
68+
void canDecode() {
69+
assertThat(codec.canDecode(dataType, FORMAT_TEXT, Geometry.class)).isTrue();
70+
assertThat(codec.canDecode(dataType, FORMAT_BINARY, Geometry.class)).isTrue();
71+
72+
assertThat(codec.canDecode(dataType, FORMAT_TEXT, Point.class)).isTrue();
73+
assertThat(codec.canDecode(dataType, FORMAT_TEXT, MultiPoint.class)).isTrue();
74+
assertThat(codec.canDecode(dataType, FORMAT_TEXT, LineString.class)).isTrue();
75+
assertThat(codec.canDecode(dataType, FORMAT_TEXT, LinearRing.class)).isTrue();
76+
assertThat(codec.canDecode(dataType, FORMAT_TEXT, MultiLineString.class)).isTrue();
77+
assertThat(codec.canDecode(dataType, FORMAT_TEXT, Polygon.class)).isTrue();
78+
assertThat(codec.canDecode(dataType, FORMAT_TEXT, MultiPolygon.class)).isTrue();
79+
assertThat(codec.canDecode(dataType, FORMAT_TEXT, GeometryCollection.class)).isTrue();
80+
81+
assertThat(codec.canDecode(VARCHAR.getObjectId(), FORMAT_BINARY, Geometry.class)).isFalse();
82+
assertThat(codec.canDecode(JSON.getObjectId(), FORMAT_TEXT, Geometry.class)).isFalse();
83+
assertThat(codec.canDecode(JSONB.getObjectId(), FORMAT_BINARY, Geometry.class)).isFalse();
84+
}
85+
86+
@Test
87+
void canEncodeNoValue() {
88+
assertThatIllegalArgumentException().isThrownBy(() -> codec.canEncode(null))
89+
.withMessage("value must not be null");
90+
}
91+
92+
@Test
93+
void canEncode() {
94+
assertThat(codec.canEncode(geometryFactory.createPoint())).isTrue();
95+
assertThat(codec.canEncode(geometryFactory.createMultiPoint())).isTrue();
96+
assertThat(codec.canEncode(geometryFactory.createLineString())).isTrue();
97+
assertThat(codec.canEncode(geometryFactory.createLinearRing())).isTrue();
98+
assertThat(codec.canEncode(geometryFactory.createMultiLineString())).isTrue();
99+
assertThat(codec.canEncode(geometryFactory.createPolygon())).isTrue();
100+
assertThat(codec.canEncode(geometryFactory.createMultiPolygon())).isTrue();
101+
assertThat(codec.canEncode(geometryFactory.createGeometryCollection())).isTrue();
102+
103+
assertThat(codec.canEncode("Geometry")).isFalse();
104+
assertThat(codec.canEncode(1)).isFalse();
105+
}
106+
107+
@Test
108+
@SuppressWarnings("unchecked")
109+
void decode() {
110+
byte[] pointBytes = wkbWriter.write(point);
111+
ByteBuf pointByteBuf = ByteBufUtils.encode(TEST, WKBWriter.toHex(pointBytes));
112+
113+
assertThat(codec.decode(pointByteBuf, dataType, FORMAT_TEXT, Geometry.class)).isEqualTo(point);
114+
}
115+
116+
@Test
117+
@SuppressWarnings("unchecked")
118+
void decodeNoByteBuf() {
119+
assertThat(codec.decode(null, dataType, FORMAT_TEXT, Geometry.class)).isNull();
120+
}
121+
122+
@Test
123+
void encode() {
124+
ByteBuf encoded = ByteBufUtils.encode(TEST, point.toText());
125+
126+
ParameterAssert.assertThat(codec.encode(point))
127+
.hasFormat(FORMAT_TEXT)
128+
.hasType(dataType)
129+
.hasValue(encoded);
130+
}
131+
132+
@Test
133+
void encodeNoValue() {
134+
assertThatIllegalArgumentException().isThrownBy(() -> codec.encode(null))
135+
.withMessage("value must not be null");
136+
}
137+
138+
@Test
139+
void encodeNull() {
140+
assertThat(new PostgisGeometryCodec(TEST, dataType).encodeNull())
141+
.isEqualTo(new EncodedParameter(FORMAT_BINARY, dataType, NULL_VALUE));
142+
}
143+
144+
}

0 commit comments

Comments
 (0)