Skip to content

Commit 3ec52e3

Browse files
committed
Add Postgis Geography codec
1 parent 3155ba0 commit 3ec52e3

3 files changed

Lines changed: 240 additions & 41 deletions

File tree

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

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -47,7 +47,18 @@ enum BuiltinCodec {
4747
public boolean isSupported() {
4848
return this.jtsPresent;
4949
}
50-
}, VECTOR("vector");
50+
},
51+
POSTGIS_GEOGRAPHY("geography") {
52+
53+
private final boolean jtsPresent = isPresent(BuiltinDynamicCodecs.class.getClassLoader(), "org.locationtech.jts.geom.Geometry");
54+
55+
@Override
56+
public boolean isSupported() {
57+
return this.jtsPresent;
58+
}
59+
},
60+
61+
VECTOR("vector");
5162

5263
private final String name;
5364

@@ -62,6 +73,8 @@ public Iterable<Codec<?>> createCodec(ByteBufAllocator byteBufAllocator, int oid
6273
return Collections.singletonList(new HStoreCodec(byteBufAllocator, oid));
6374
case POSTGIS_GEOMETRY:
6475
return Collections.singletonList(new PostgisGeometryCodec(oid));
76+
case POSTGIS_GEOGRAPHY:
77+
return Collections.singletonList(new PostgisGeographyCodec(oid));
6578
case VECTOR:
6679
VectorCodec vectorCodec = new VectorCodec(byteBufAllocator, oid, typarray);
6780
List<Codec<?>> codecs = new ArrayList<>(3);
Lines changed: 108 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,108 @@
1+
package io.r2dbc.postgresql.codec;
2+
3+
import io.netty.buffer.ByteBuf;
4+
import io.netty.buffer.Unpooled;
5+
import io.r2dbc.postgresql.client.EncodedParameter;
6+
import io.r2dbc.postgresql.message.Format;
7+
import io.r2dbc.postgresql.util.Assert;
8+
import io.r2dbc.postgresql.util.ByteBufUtils;
9+
import org.jspecify.annotations.Nullable;
10+
import org.locationtech.jts.geom.Geometry;
11+
import org.locationtech.jts.geom.GeometryFactory;
12+
import org.locationtech.jts.io.ParseException;
13+
import org.locationtech.jts.io.WKBReader;
14+
import org.locationtech.jts.io.WKBWriter;
15+
import reactor.core.publisher.Mono;
16+
17+
import java.util.Collections;
18+
19+
import static io.r2dbc.postgresql.client.EncodedParameter.NULL_VALUE;
20+
import static io.r2dbc.postgresql.message.Format.FORMAT_BINARY;
21+
import static io.r2dbc.postgresql.message.Format.FORMAT_TEXT;
22+
23+
final class PostgisGeographyCodec implements Codec<Geometry>, CodecMetadata {
24+
25+
private static final Class<Geometry> TYPE = Geometry.class;
26+
27+
private final GeometryFactory geometryFactory = new GeometryFactory();
28+
29+
private final int oid;
30+
31+
/**
32+
* Create a new {@link PostgisGeographyCodec}.
33+
*/
34+
PostgisGeographyCodec(int oid) {
35+
this.oid = oid;
36+
}
37+
38+
@Override
39+
public boolean canDecode(int dataType, Format format, Class<?> type) {
40+
Assert.requireNonNull(format, "format must not be null");
41+
Assert.requireNonNull(type, "type must not be null");
42+
43+
// Object = Geography or Geography = type (Geography subtype)
44+
return dataType == this.oid && (type.isAssignableFrom(TYPE) || TYPE.isAssignableFrom(type));
45+
}
46+
47+
@Override
48+
public boolean canEncode(Object value) {
49+
Assert.requireNonNull(value, "value must not be null");
50+
51+
return TYPE.isInstance(value);
52+
}
53+
54+
@Override
55+
public boolean canEncodeNull(Class<?> type) {
56+
Assert.requireNonNull(type, "type must not be null");
57+
58+
return TYPE.isAssignableFrom(type);
59+
}
60+
61+
@Override
62+
public @Nullable Geometry decode(@Nullable ByteBuf buffer, int dataType, Format format, Class<? extends Geometry> type) {
63+
if (buffer == null) {
64+
return null;
65+
}
66+
67+
Assert.isTrue(format == FORMAT_TEXT, "format must be FORMAT_TEXT");
68+
69+
try {
70+
return new WKBReader(this.geometryFactory).read(WKBReader.hexToBytes(ByteBufUtils.decode(buffer)));
71+
} catch (ParseException e) {
72+
throw new IllegalArgumentException(e);
73+
}
74+
}
75+
76+
@Override
77+
public EncodedParameter encode(Object value) {
78+
Assert.requireType(value, Geometry.class, "value must be Geometry type");
79+
Geometry geometry = (Geometry) value;
80+
81+
WKBWriter writer = new WKBWriter(2, true);
82+
83+
return new EncodedParameter(FORMAT_BINARY, this.oid, Mono.fromSupplier(
84+
() -> Unpooled.wrappedBuffer(writer.write(geometry))
85+
));
86+
}
87+
88+
@Override
89+
public EncodedParameter encode(Object value, int dataType) {
90+
return encode(value);
91+
}
92+
93+
@Override
94+
public EncodedParameter encodeNull() {
95+
return new EncodedParameter(FORMAT_BINARY, this.oid, NULL_VALUE);
96+
}
97+
98+
@Override
99+
public Class<?> type() {
100+
return TYPE;
101+
}
102+
103+
@Override
104+
public Iterable<PostgresTypeIdentifier> getDataTypes() {
105+
return Collections.singleton(AbstractCodec.getDataType(this.oid));
106+
}
107+
108+
}

‎src/test/java/io/r2dbc/postgresql/codec/PostgisGeometryCodecUnitTest.java‎

Lines changed: 118 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -55,7 +55,9 @@ final class PostgisGeometryCodecUnitTests {
5555

5656
private static final int dataType = 23456;
5757

58-
private final PostgisGeometryCodec codec = new PostgisGeometryCodec(dataType);
58+
private final PostgisGeometryCodec geometryCodec = new PostgisGeometryCodec(dataType);
59+
60+
private final PostgisGeographyCodec geographyCodec = new PostgisGeographyCodec(dataType);
5961

6062
private final WKBWriter wkbWriter = new WKBWriter();
6163

@@ -65,91 +67,167 @@ final class PostgisGeometryCodecUnitTests {
6567

6668
@Test
6769
void canDecodeNoFormat() {
68-
assertThatIllegalArgumentException().isThrownBy(() -> this.codec.canDecode(dataType, null, Geometry.class))
70+
assertThatIllegalArgumentException().isThrownBy(() -> this.geometryCodec.canDecode(dataType, null, Geometry.class))
6971
.withMessage("format must not be null");
7072
}
7173

7274
@Test
7375
void canDecodeNoClass() {
74-
assertThatIllegalArgumentException().isThrownBy(() -> this.codec.canDecode(dataType, FORMAT_TEXT, null))
76+
assertThatIllegalArgumentException().isThrownBy(() -> this.geometryCodec.canDecode(dataType, FORMAT_TEXT, null))
7577
.withMessage("type must not be null");
7678
}
7779

7880
@Test
79-
void canDecode() {
80-
assertThat(this.codec.canDecode(dataType, FORMAT_TEXT, Geometry.class)).isTrue();
81-
assertThat(this.codec.canDecode(dataType, FORMAT_BINARY, Geometry.class)).isTrue();
82-
83-
assertThat(this.codec.canDecode(dataType, FORMAT_TEXT, Point.class)).isTrue();
84-
assertThat(this.codec.canDecode(dataType, FORMAT_TEXT, MultiPoint.class)).isTrue();
85-
assertThat(this.codec.canDecode(dataType, FORMAT_TEXT, LineString.class)).isTrue();
86-
assertThat(this.codec.canDecode(dataType, FORMAT_TEXT, LinearRing.class)).isTrue();
87-
assertThat(this.codec.canDecode(dataType, FORMAT_TEXT, MultiLineString.class)).isTrue();
88-
assertThat(this.codec.canDecode(dataType, FORMAT_TEXT, Polygon.class)).isTrue();
89-
assertThat(this.codec.canDecode(dataType, FORMAT_TEXT, MultiPolygon.class)).isTrue();
90-
assertThat(this.codec.canDecode(dataType, FORMAT_TEXT, GeometryCollection.class)).isTrue();
81+
void canDecodeGeometry() {
82+
assertThat(this.geometryCodec.canDecode(dataType, FORMAT_TEXT, Geometry.class)).isTrue();
83+
assertThat(this.geometryCodec.canDecode(dataType, FORMAT_BINARY, Geometry.class)).isTrue();
84+
85+
assertThat(this.geometryCodec.canDecode(dataType, FORMAT_TEXT, Point.class)).isTrue();
86+
assertThat(this.geometryCodec.canDecode(dataType, FORMAT_TEXT, MultiPoint.class)).isTrue();
87+
assertThat(this.geometryCodec.canDecode(dataType, FORMAT_TEXT, LineString.class)).isTrue();
88+
assertThat(this.geometryCodec.canDecode(dataType, FORMAT_TEXT, LinearRing.class)).isTrue();
89+
assertThat(this.geometryCodec.canDecode(dataType, FORMAT_TEXT, MultiLineString.class)).isTrue();
90+
assertThat(this.geometryCodec.canDecode(dataType, FORMAT_TEXT, Polygon.class)).isTrue();
91+
assertThat(this.geometryCodec.canDecode(dataType, FORMAT_TEXT, MultiPolygon.class)).isTrue();
92+
assertThat(this.geometryCodec.canDecode(dataType, FORMAT_TEXT, GeometryCollection.class)).isTrue();
93+
94+
assertThat(this.geometryCodec.canDecode(VARCHAR.getObjectId(), FORMAT_BINARY, Geometry.class)).isFalse();
95+
assertThat(this.geometryCodec.canDecode(JSON.getObjectId(), FORMAT_TEXT, Geometry.class)).isFalse();
96+
assertThat(this.geometryCodec.canDecode(JSONB.getObjectId(), FORMAT_BINARY, Geometry.class)).isFalse();
97+
}
9198

92-
assertThat(this.codec.canDecode(VARCHAR.getObjectId(), FORMAT_BINARY, Geometry.class)).isFalse();
93-
assertThat(this.codec.canDecode(JSON.getObjectId(), FORMAT_TEXT, Geometry.class)).isFalse();
94-
assertThat(this.codec.canDecode(JSONB.getObjectId(), FORMAT_BINARY, Geometry.class)).isFalse();
99+
@Test
100+
void canDecodeGeography() {
101+
assertThat(this.geographyCodec.canDecode(dataType, FORMAT_TEXT, Geometry.class)).isTrue();
102+
assertThat(this.geographyCodec.canDecode(dataType, FORMAT_BINARY, Geometry.class)).isTrue();
103+
104+
assertThat(this.geographyCodec.canDecode(dataType, FORMAT_TEXT, Point.class)).isTrue();
105+
assertThat(this.geographyCodec.canDecode(dataType, FORMAT_TEXT, MultiPoint.class)).isTrue();
106+
assertThat(this.geographyCodec.canDecode(dataType, FORMAT_TEXT, LineString.class)).isTrue();
107+
assertThat(this.geographyCodec.canDecode(dataType, FORMAT_TEXT, LinearRing.class)).isTrue();
108+
assertThat(this.geographyCodec.canDecode(dataType, FORMAT_TEXT, MultiLineString.class)).isTrue();
109+
assertThat(this.geographyCodec.canDecode(dataType, FORMAT_TEXT, Polygon.class)).isTrue();
110+
assertThat(this.geographyCodec.canDecode(dataType, FORMAT_TEXT, MultiPolygon.class)).isTrue();
111+
assertThat(this.geographyCodec.canDecode(dataType, FORMAT_TEXT, GeometryCollection.class)).isTrue();
112+
113+
assertThat(this.geographyCodec.canDecode(VARCHAR.getObjectId(), FORMAT_BINARY, Geometry.class)).isFalse();
114+
assertThat(this.geographyCodec.canDecode(JSON.getObjectId(), FORMAT_TEXT, Geometry.class)).isFalse();
115+
assertThat(this.geographyCodec.canDecode(JSONB.getObjectId(), FORMAT_BINARY, Geometry.class)).isFalse();
95116
}
96117

97118
@Test
98-
void canEncodeNoValue() {
99-
assertThatIllegalArgumentException().isThrownBy(() -> this.codec.canEncode(null))
119+
void canEncodeNoValueGeometry() {
120+
assertThatIllegalArgumentException().isThrownBy(() -> this.geometryCodec.canEncode(null))
100121
.withMessage("value must not be null");
101122
}
102123

124+
void canEncodeNoValueGeography() {
125+
assertThatIllegalArgumentException().isThrownBy(() -> this.geographyCodec.canEncode(null))
126+
.withMessage("value must not be null");
127+
}
128+
129+
@Test
130+
void canEncodeGeometry() {
131+
assertThat(this.geometryCodec.canEncode(this.geometryFactory.createPoint())).isTrue();
132+
assertThat(this.geometryCodec.canEncode(this.geometryFactory.createMultiPoint())).isTrue();
133+
assertThat(this.geometryCodec.canEncode(this.geometryFactory.createLineString())).isTrue();
134+
assertThat(this.geometryCodec.canEncode(this.geometryFactory.createLinearRing())).isTrue();
135+
assertThat(this.geometryCodec.canEncode(this.geometryFactory.createMultiLineString())).isTrue();
136+
assertThat(this.geometryCodec.canEncode(this.geometryFactory.createPolygon())).isTrue();
137+
assertThat(this.geometryCodec.canEncode(this.geometryFactory.createMultiPolygon())).isTrue();
138+
assertThat(this.geometryCodec.canEncode(this.geometryFactory.createGeometryCollection())).isTrue();
139+
140+
assertThat(this.geometryCodec.canEncode("Geometry")).isFalse();
141+
assertThat(this.geometryCodec.canEncode(1)).isFalse();
142+
}
143+
103144
@Test
104-
void canEncode() {
105-
assertThat(this.codec.canEncode(this.geometryFactory.createPoint())).isTrue();
106-
assertThat(this.codec.canEncode(this.geometryFactory.createMultiPoint())).isTrue();
107-
assertThat(this.codec.canEncode(this.geometryFactory.createLineString())).isTrue();
108-
assertThat(this.codec.canEncode(this.geometryFactory.createLinearRing())).isTrue();
109-
assertThat(this.codec.canEncode(this.geometryFactory.createMultiLineString())).isTrue();
110-
assertThat(this.codec.canEncode(this.geometryFactory.createPolygon())).isTrue();
111-
assertThat(this.codec.canEncode(this.geometryFactory.createMultiPolygon())).isTrue();
112-
assertThat(this.codec.canEncode(this.geometryFactory.createGeometryCollection())).isTrue();
145+
void canEncodeGeography() {
146+
assertThat(this.geographyCodec.canEncode(this.geometryFactory.createPoint())).isTrue();
147+
assertThat(this.geographyCodec.canEncode(this.geometryFactory.createMultiPoint())).isTrue();
148+
assertThat(this.geographyCodec.canEncode(this.geometryFactory.createLineString())).isTrue();
149+
assertThat(this.geographyCodec.canEncode(this.geometryFactory.createLinearRing())).isTrue();
150+
assertThat(this.geographyCodec.canEncode(this.geometryFactory.createMultiLineString())).isTrue();
151+
assertThat(this.geographyCodec.canEncode(this.geometryFactory.createPolygon())).isTrue();
152+
assertThat(this.geographyCodec.canEncode(this.geometryFactory.createMultiPolygon())).isTrue();
153+
assertThat(this.geographyCodec.canEncode(this.geometryFactory.createGeometryCollection())).isTrue();
154+
155+
assertThat(this.geographyCodec.canEncode("Geography")).isFalse();
156+
assertThat(this.geographyCodec.canEncode(1)).isFalse();
157+
}
113158

114-
assertThat(this.codec.canEncode("Geometry")).isFalse();
115-
assertThat(this.codec.canEncode(1)).isFalse();
159+
@Test
160+
@SuppressWarnings("unchecked")
161+
void decodeGeometry() {
162+
byte[] pointBytes = this.wkbWriter.write(this.point);
163+
ByteBuf pointByteBuf = ByteBufUtils.encode(TEST, WKBWriter.toHex(pointBytes));
164+
165+
assertThat(this.geometryCodec.decode(pointByteBuf, dataType, FORMAT_TEXT, Geometry.class)).isEqualTo(this.point);
116166
}
117167

118168
@Test
119169
@SuppressWarnings("unchecked")
120-
void decode() {
170+
void decodeGeography() {
121171
byte[] pointBytes = this.wkbWriter.write(this.point);
122172
ByteBuf pointByteBuf = ByteBufUtils.encode(TEST, WKBWriter.toHex(pointBytes));
123173

124-
assertThat(this.codec.decode(pointByteBuf, dataType, FORMAT_TEXT, Geometry.class)).isEqualTo(this.point);
174+
assertThat(this.geographyCodec.decode(pointByteBuf, dataType, FORMAT_TEXT, Geometry.class)).isEqualTo(this.point);
175+
}
176+
177+
@Test
178+
@SuppressWarnings("unchecked")
179+
void decodeNoByteBufGeometry() {
180+
assertThat(this.geometryCodec.decode(null, dataType, FORMAT_TEXT, Geometry.class)).isNull();
125181
}
126182

127183
@Test
128184
@SuppressWarnings("unchecked")
129-
void decodeNoByteBuf() {
130-
assertThat(this.codec.decode(null, dataType, FORMAT_TEXT, Geometry.class)).isNull();
185+
void decodeNoByteBufGeography() {
186+
assertThat(this.geographyCodec.decode(null, dataType, FORMAT_TEXT, Geometry.class)).isNull();
187+
}
188+
189+
@Test
190+
void encodeGeometry() {
191+
ByteBuf encoded = Unpooled.wrappedBuffer(new WKBWriter(2, true).write(this.point));
192+
193+
ParameterAssert.assertThat(this.geometryCodec.encode(this.point))
194+
.hasFormat(FORMAT_BINARY)
195+
.hasType(dataType)
196+
.hasValue(encoded);
131197
}
132198

133199
@Test
134-
void encode() {
200+
void encodeGeography() {
135201
ByteBuf encoded = Unpooled.wrappedBuffer(new WKBWriter(2, true).write(this.point));
136202

137-
ParameterAssert.assertThat(this.codec.encode(this.point))
203+
ParameterAssert.assertThat(this.geographyCodec.encode(this.point))
138204
.hasFormat(FORMAT_BINARY)
139205
.hasType(dataType)
140206
.hasValue(encoded);
141207
}
142208

143209
@Test
144-
void encodeNoValue() {
145-
assertThatIllegalArgumentException().isThrownBy(() -> this.codec.encode(null))
210+
void encodeNoValueGeometry() {
211+
assertThatIllegalArgumentException().isThrownBy(() -> this.geometryCodec.encode(null))
212+
.withMessage("value must not be null");
213+
}
214+
215+
@Test
216+
void encodeNoValueGeography() {
217+
assertThatIllegalArgumentException().isThrownBy(() -> this.geometryCodec.encode(null))
146218
.withMessage("value must not be null");
147219
}
148220

149221
@Test
150-
void encodeNull() {
222+
void encodeNullGeometry() {
151223
assertThat(new PostgisGeometryCodec(dataType).encodeNull())
152224
.isEqualTo(new EncodedParameter(FORMAT_BINARY, dataType, NULL_VALUE));
153225
}
154226

227+
@Test
228+
void encodeNullGeography() {
229+
assertThat(new PostgisGeographyCodec(dataType).encodeNull())
230+
.isEqualTo(new EncodedParameter(FORMAT_BINARY, dataType, NULL_VALUE));
231+
}
232+
155233
}

0 commit comments

Comments
 (0)