Skip to content

Commit 0868f2a

Browse files
committed
#423 - Add r2dbc-postgresql Geotypes to simple types.
R2DBC Postgres Geo-types are now considered simple types that are passed-thru to the driver without further mapping. Types such as io.r2dbc.postgresql.codec.Circle or io.r2dbc.postgresql.codec.Box can be used directly in domain models and as bind parameters.
1 parent 7d3bd1f commit 0868f2a

File tree

4 files changed

+252
-9
lines changed

4 files changed

+252
-9
lines changed

pom.xml

+2-2
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,7 @@
3535
<r2dbc-spi-test.version>0.8.0.RELEASE</r2dbc-spi-test.version>
3636
<mssql-jdbc.version>7.1.2.jre8-preview</mssql-jdbc.version>
3737
<mariadb-jdbc.version>2.5.4</mariadb-jdbc.version>
38-
<r2dbc-releasetrain.version>Arabba-SR6</r2dbc-releasetrain.version>
38+
<r2dbc-releasetrain.version>Arabba-BUILD-SNAPSHOT</r2dbc-releasetrain.version>
3939
<reactive-streams.version>1.0.3</reactive-streams.version>
4040
<netty>4.1.47.Final</netty>
4141
</properties>
@@ -221,7 +221,7 @@
221221
<dependency>
222222
<groupId>io.r2dbc</groupId>
223223
<artifactId>r2dbc-postgresql</artifactId>
224-
<scope>test</scope>
224+
<optional>true</optional>
225225
</dependency>
226226

227227
<dependency>

src/main/java/org/springframework/data/r2dbc/dialect/PostgresDialect.java

+166-4
Original file line numberDiff line numberDiff line change
@@ -3,15 +3,28 @@
33
import java.net.InetAddress;
44
import java.net.URI;
55
import java.net.URL;
6+
import java.util.ArrayList;
67
import java.util.Arrays;
78
import java.util.Collection;
9+
import java.util.Collections;
810
import java.util.HashSet;
11+
import java.util.List;
912
import java.util.Set;
1013
import java.util.UUID;
14+
import java.util.function.Consumer;
15+
import java.util.stream.Stream;
1116

17+
import org.springframework.core.convert.converter.Converter;
18+
import org.springframework.data.convert.ReadingConverter;
19+
import org.springframework.data.convert.WritingConverter;
20+
import org.springframework.data.geo.Box;
21+
import org.springframework.data.geo.Circle;
22+
import org.springframework.data.geo.Point;
23+
import org.springframework.data.geo.Polygon;
1224
import org.springframework.data.mapping.model.SimpleTypeHolder;
1325
import org.springframework.data.relational.core.dialect.ArrayColumns;
1426
import org.springframework.data.util.Lazy;
27+
import org.springframework.lang.NonNull;
1528
import org.springframework.r2dbc.core.binding.BindMarkersFactory;
1629
import org.springframework.util.ClassUtils;
1730

@@ -25,15 +38,25 @@ public class PostgresDialect extends org.springframework.data.relational.core.di
2538

2639
private static final Set<Class<?>> SIMPLE_TYPES;
2740

41+
private static final boolean GEO_TYPES_PRESENT = ClassUtils.isPresent("io.r2dbc.postgresql.codec.Polygon",
42+
PostgresDialect.class.getClassLoader());
43+
2844
static {
2945

3046
Set<Class<?>> simpleTypes = new HashSet<>(Arrays.asList(UUID.class, URL.class, URI.class, InetAddress.class));
3147

32-
if (ClassUtils.isPresent("io.r2dbc.postgresql.codec.Json", PostgresDialect.class.getClassLoader())) {
48+
// conditional Postgres JSON support.
49+
ifClassPresent("io.r2dbc.postgresql.codec.Json", simpleTypes::add);
3350

34-
simpleTypes
35-
.add(ClassUtils.resolveClassName("io.r2dbc.postgresql.codec.Json", PostgresDialect.class.getClassLoader()));
36-
}
51+
// conditional Postgres Geo support.
52+
Stream.of("io.r2dbc.postgresql.codec.Box", //
53+
"io.r2dbc.postgresql.codec.Circle", //
54+
"io.r2dbc.postgresql.codec.Line", //
55+
"io.r2dbc.postgresql.codec.Lseg", //
56+
"io.r2dbc.postgresql.codec.Point", //
57+
"io.r2dbc.postgresql.codec.Path", //
58+
"io.r2dbc.postgresql.codec.Polygon") //
59+
.forEach(s -> ifClassPresent(s, simpleTypes::add));
3760

3861
SIMPLE_TYPES = simpleTypes;
3962
}
@@ -76,6 +99,23 @@ public ArrayColumns getArraySupport() {
7699
return this.arrayColumns.get();
77100
}
78101

102+
/*
103+
* (non-Javadoc)
104+
* @see org.springframework.data.r2dbc.dialect.Dialect#getConverters()
105+
*/
106+
@Override
107+
public Collection<Object> getConverters() {
108+
109+
if (GEO_TYPES_PRESENT) {
110+
return Arrays.asList(FromPostgresPointConverter.INSTANCE, ToPostgresPointConverter.INSTANCE, //
111+
FromPostgresCircleConverter.INSTANCE, ToPostgresCircleConverter.INSTANCE, //
112+
FromPostgresBoxConverter.INSTANCE, ToPostgresBoxConverter.INSTANCE, //
113+
FromPostgresPolygonConverter.INSTANCE, ToPostgresPolygonConverter.INSTANCE);
114+
}
115+
116+
return Collections.emptyList();
117+
}
118+
79119
private static class R2dbcArrayColumns implements ArrayColumns {
80120

81121
private final ArrayColumns delegate;
@@ -107,4 +147,126 @@ public Class<?> getArrayType(Class<?> userType) {
107147
}
108148
}
109149

150+
/**
151+
* If the class is present on the class path, invoke the specified consumer {@code action} with the class object,
152+
* otherwise do nothing.
153+
*
154+
* @param action block to be executed if a value is present.
155+
*/
156+
private static void ifClassPresent(String className, Consumer<Class<?>> action) {
157+
158+
if (ClassUtils.isPresent(className, PostgresDialect.class.getClassLoader())) {
159+
action.accept(ClassUtils.resolveClassName(className, PostgresDialect.class.getClassLoader()));
160+
}
161+
}
162+
163+
@ReadingConverter
164+
private enum FromPostgresBoxConverter implements Converter<io.r2dbc.postgresql.codec.Box, Box> {
165+
166+
INSTANCE;
167+
168+
@Override
169+
public Box convert(io.r2dbc.postgresql.codec.Box source) {
170+
return new Box(FromPostgresPointConverter.INSTANCE.convert(source.getA()),
171+
FromPostgresPointConverter.INSTANCE.convert(source.getB()));
172+
}
173+
}
174+
175+
@WritingConverter
176+
private enum ToPostgresBoxConverter implements Converter<Box, io.r2dbc.postgresql.codec.Box> {
177+
178+
INSTANCE;
179+
180+
@Override
181+
public io.r2dbc.postgresql.codec.Box convert(Box source) {
182+
return io.r2dbc.postgresql.codec.Box.of(ToPostgresPointConverter.INSTANCE.convert(source.getFirst()),
183+
ToPostgresPointConverter.INSTANCE.convert(source.getSecond()));
184+
}
185+
}
186+
187+
@ReadingConverter
188+
private enum FromPostgresCircleConverter implements Converter<io.r2dbc.postgresql.codec.Circle, Circle> {
189+
190+
INSTANCE;
191+
192+
@Override
193+
public Circle convert(io.r2dbc.postgresql.codec.Circle source) {
194+
return new Circle(source.getCenter().getX(), source.getCenter().getY(), source.getRadius());
195+
}
196+
}
197+
198+
@WritingConverter
199+
private enum ToPostgresCircleConverter implements Converter<Circle, io.r2dbc.postgresql.codec.Circle> {
200+
201+
INSTANCE;
202+
203+
@Override
204+
public io.r2dbc.postgresql.codec.Circle convert(Circle source) {
205+
return io.r2dbc.postgresql.codec.Circle.of(source.getCenter().getX(), source.getCenter().getY(),
206+
source.getRadius().getValue());
207+
}
208+
}
209+
210+
@ReadingConverter
211+
private enum FromPostgresPolygonConverter implements Converter<io.r2dbc.postgresql.codec.Polygon, Polygon> {
212+
213+
INSTANCE;
214+
215+
@Override
216+
public Polygon convert(io.r2dbc.postgresql.codec.Polygon source) {
217+
218+
List<io.r2dbc.postgresql.codec.Point> sourcePoints = source.getPoints();
219+
List<Point> targetPoints = new ArrayList<>(sourcePoints.size());
220+
221+
for (io.r2dbc.postgresql.codec.Point sourcePoint : sourcePoints) {
222+
targetPoints.add(FromPostgresPointConverter.INSTANCE.convert(sourcePoint));
223+
}
224+
225+
return new Polygon(targetPoints);
226+
}
227+
}
228+
229+
@WritingConverter
230+
private enum ToPostgresPolygonConverter implements Converter<Polygon, io.r2dbc.postgresql.codec.Polygon> {
231+
232+
INSTANCE;
233+
234+
@Override
235+
public io.r2dbc.postgresql.codec.Polygon convert(Polygon source) {
236+
237+
List<Point> sourcePoints = source.getPoints();
238+
List<io.r2dbc.postgresql.codec.Point> targetPoints = new ArrayList<>(sourcePoints.size());
239+
240+
for (Point sourcePoint : sourcePoints) {
241+
targetPoints.add(ToPostgresPointConverter.INSTANCE.convert(sourcePoint));
242+
}
243+
244+
return io.r2dbc.postgresql.codec.Polygon.of(targetPoints);
245+
}
246+
}
247+
248+
@ReadingConverter
249+
private enum FromPostgresPointConverter implements Converter<io.r2dbc.postgresql.codec.Point, Point> {
250+
251+
INSTANCE;
252+
253+
@Override
254+
@NonNull
255+
public Point convert(io.r2dbc.postgresql.codec.Point source) {
256+
return new Point(source.getX(), source.getY());
257+
}
258+
}
259+
260+
@WritingConverter
261+
private enum ToPostgresPointConverter implements Converter<Point, io.r2dbc.postgresql.codec.Point> {
262+
263+
INSTANCE;
264+
265+
@Override
266+
@NonNull
267+
public io.r2dbc.postgresql.codec.Point convert(Point source) {
268+
return io.r2dbc.postgresql.codec.Point.of(source.getX(), source.getY());
269+
}
270+
}
271+
110272
}

src/test/java/org/springframework/data/r2dbc/core/PostgresIntegrationTests.java

+83-2
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,14 @@
2020

2121
import io.r2dbc.postgresql.PostgresqlConnectionConfiguration;
2222
import io.r2dbc.postgresql.PostgresqlConnectionFactory;
23+
import io.r2dbc.postgresql.codec.Box;
24+
import io.r2dbc.postgresql.codec.Circle;
2325
import io.r2dbc.postgresql.codec.EnumCodec;
26+
import io.r2dbc.postgresql.codec.Line;
27+
import io.r2dbc.postgresql.codec.Lseg;
28+
import io.r2dbc.postgresql.codec.Path;
29+
import io.r2dbc.postgresql.codec.Point;
30+
import io.r2dbc.postgresql.codec.Polygon;
2431
import io.r2dbc.postgresql.extension.CodecRegistrar;
2532
import io.r2dbc.spi.ConnectionFactory;
2633
import lombok.AllArgsConstructor;
@@ -36,7 +43,6 @@
3643

3744
import org.junit.Before;
3845
import org.junit.ClassRule;
39-
import org.junit.Ignore;
4046
import org.junit.Test;
4147

4248
import org.springframework.dao.DataAccessException;
@@ -133,7 +139,6 @@ public void shouldReadAndWriteMultiDimensionArrays() {
133139
}
134140

135141
@Test // gh-411
136-
@Ignore("Depends on https://github.com/pgjdbc/r2dbc-postgresql/issues/301")
137142
public void shouldWriteAndReadEnumValuesUsingDriverInternals() {
138143

139144
CodecRegistrar codecRegistrar = EnumCodec.builder().withEnum("state_enum", State.class).build();
@@ -183,6 +188,63 @@ static class StateConverter extends EnumWriteSupport<State> {
183188

184189
}
185190

191+
@Test // gh-423
192+
public void shouldReadAndWriteGeoTypes() {
193+
194+
GeoType geoType = new GeoType();
195+
geoType.thePoint = Point.of(1, 2);
196+
geoType.theBox = Box.of(Point.of(3, 4), Point.of(1, 2));
197+
geoType.theCircle = Circle.of(1, 2, 3);
198+
geoType.theLine = Line.of(1, 2, 3, 4);
199+
geoType.theLseg = Lseg.of(Point.of(1, 2), Point.of(3, 4));
200+
geoType.thePath = Path.open(Point.of(1, 2), Point.of(3, 4));
201+
geoType.thePolygon = Polygon.of(Point.of(1, 2), Point.of(3, 4), Point.of(5, 6), Point.of(1, 2));
202+
geoType.springDataBox = new org.springframework.data.geo.Box(new org.springframework.data.geo.Point(3, 4),
203+
new org.springframework.data.geo.Point(1, 2));
204+
geoType.springDataCircle = new org.springframework.data.geo.Circle(1, 2, 3);
205+
geoType.springDataPoint = new org.springframework.data.geo.Point(1, 2);
206+
geoType.springDataPolygon = new org.springframework.data.geo.Polygon(new org.springframework.data.geo.Point(1, 2),
207+
new org.springframework.data.geo.Point(3, 4), new org.springframework.data.geo.Point(5, 6),
208+
new org.springframework.data.geo.Point(1, 2));
209+
210+
template.execute("DROP TABLE IF EXISTS geo_type");
211+
template.execute("CREATE TABLE geo_type (" //
212+
+ "id serial PRIMARY KEY," //
213+
+ "the_point POINT," //
214+
+ "the_box BOX," //
215+
+ "the_circle CIRCLE," //
216+
+ "the_line LINE," //
217+
+ "the_lseg LSEG," //
218+
+ "the_path PATH," //
219+
+ "the_polygon POLYGON," //
220+
+ "spring_data_box BOX," //
221+
+ "spring_data_circle CIRCLE," //
222+
+ "spring_data_point POINT," //
223+
+ "spring_data_polygon POLYGON" //
224+
+ ")");
225+
226+
R2dbcEntityTemplate template = new R2dbcEntityTemplate(client,
227+
new DefaultReactiveDataAccessStrategy(PostgresDialect.INSTANCE));
228+
229+
GeoType saved = template.insert(geoType).block();
230+
GeoType loaded = template.select(Query.empty(), GeoType.class) //
231+
.blockLast();
232+
233+
assertThat(saved.id).isEqualTo(loaded.id);
234+
assertThat(saved.thePoint).isEqualTo(loaded.thePoint);
235+
assertThat(saved.theBox).isEqualTo(loaded.theBox);
236+
assertThat(saved.theCircle).isEqualTo(loaded.theCircle);
237+
assertThat(saved.theLine).isEqualTo(loaded.theLine);
238+
assertThat(saved.theLseg).isEqualTo(loaded.theLseg);
239+
assertThat(saved.thePath).isEqualTo(loaded.thePath);
240+
assertThat(saved.thePolygon).isEqualTo(loaded.thePolygon);
241+
assertThat(saved.springDataBox).isEqualTo(loaded.springDataBox);
242+
assertThat(saved.springDataCircle).isEqualTo(loaded.springDataCircle);
243+
assertThat(saved.springDataPoint).isEqualTo(loaded.springDataPoint);
244+
assertThat(saved.springDataPolygon).isEqualTo(loaded.springDataPolygon);
245+
assertThat(saved).isEqualTo(loaded);
246+
}
247+
186248
private void insert(EntityWithArrays object) {
187249

188250
client.insert() //
@@ -219,4 +281,23 @@ static class EntityWithArrays {
219281
int[][] multidimensionalArray;
220282
List<Integer> collectionArray;
221283
}
284+
285+
@Data
286+
static class GeoType {
287+
288+
@Id Integer id;
289+
290+
Point thePoint;
291+
Box theBox;
292+
Circle theCircle;
293+
Line theLine;
294+
Lseg theLseg;
295+
Path thePath;
296+
Polygon thePolygon;
297+
298+
org.springframework.data.geo.Box springDataBox;
299+
org.springframework.data.geo.Circle springDataCircle;
300+
org.springframework.data.geo.Point springDataPoint;
301+
org.springframework.data.geo.Polygon springDataPolygon;
302+
}
222303
}

src/test/java/org/springframework/data/r2dbc/testing/PostgresTestSupport.java

+1-1
Original file line numberDiff line numberDiff line change
@@ -81,7 +81,7 @@ private static ExternalDatabase local() {
8181
.database("postgres") //
8282
.username("postgres") //
8383
.password("") //
84-
.jdbcUrl("jdbc:postgresql://localhost/postgres") //
84+
.jdbcUrl("jdbc:postgresql://localhost:5432/postgres") //
8585
.build();
8686
}
8787

0 commit comments

Comments
 (0)