Skip to content

Support for geospatial types - point, line, box, polygon etc #306

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
wants to merge 8 commits into from
Closed
Show file tree
Hide file tree
Changes from 7 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 5 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -343,7 +343,7 @@ This reference table shows the type mapping between [PostgreSQL][p] and Java dat
| [`bit`][psql-bit-ref] | Not yet supported.|
| [`bit varying`][psql-bit-ref] | Not yet supported.|
| [`boolean or bool`][psql-boolean-ref] | [`Boolean`][java-boolean-ref]|
| [`box`][psql-box-ref] | Not yet supported.|
| [`box`][psql-box-ref] | **`Box`**|
| [`bytea`][psql-bytea-ref] | [**`ByteBuffer`**][java-ByteBuffer-ref], [`byte[]`][java-byte-ref], [`Blob`][r2dbc-blob-ref]|
| [`character`][psql-character-ref] | [`String`][java-string-ref]|
| [`character varying`][psql-character-ref] | [`String`][java-string-ref]|
Expand All @@ -358,17 +358,17 @@ This reference table shows the type mapping between [PostgreSQL][p] and Java dat
| [`interval`][psql-interval-ref] | Not yet supported.|
| [`json`][psql-json-ref] | **`Json`**, [`String`][java-string-ref]. Reading: `ByteBuf`[`byte[]`][java-primitive-ref], [`ByteBuffer`][java-ByteBuffer-ref], [`String`][java-string-ref], [`InputStream`][java-inputstream-ref]|
| [`jsonb`][psql-json-ref] | **`Json`**, [`String`][java-string-ref]. Reading: `ByteBuf`[`byte[]`][java-primitive-ref], [`ByteBuffer`][java-ByteBuffer-ref], [`String`][java-string-ref], [`InputStream`][java-inputstream-ref]|
| [`line`][psql-line-ref] | Not yet supported.|
| [`lseg`][psql-lseq-ref] | Not yet supported.|
| [`line`][psql-line-ref] | **`Line`**|
| [`lseg`][psql-lseq-ref] | **`Lseg`**|
| [`macaddr`][psql-macaddr-ref] | Not yet supported.|
| [`macaddr8`][psql-macaddr8-ref] | Not yet supported.|
| [`money`][psql-money-ref] | Not yet supported.|
| [`numeric`][psql-bignumeric-ref] | [`BigDecimal`][java-bigdecimal-ref], [`Boolean`][java-boolean-ref], [`Byte`][java-byte-ref], [`Short`][java-short-ref], [`Integer`][java-integer-ref], [`Long`][java-long-ref], [`BigInteger`][java-biginteger-ref]|
| [`oid`][psql-oid-ref] | [**`Integer`**][java-integer-ref], [`Boolean`][java-boolean-ref], [`Byte`][java-byte-ref], [`Short`][java-short-ref], [`Long`][java-long-ref], [`BigDecimal`][java-bigdecimal-ref], [`BigInteger`][java-biginteger-ref]|
| [`path`][psql-path-ref] | Not yet supported.|
| [`path`][psql-path-ref] | **`Path`**|
| [`pg_lsn`][psql-pg_lsn-ref] | Not yet supported.|
| [`point`][psql-point-ref] | **`Point`**|
| [`polygon`][psql-polygon-ref] | Not yet supported.|
| [`polygon`][psql-polygon-ref] | **`Polygon`**|
| [`real`][psql-real-ref] | [**`Float`**][java-float-ref], [`Double`][java-double-ref], [`Boolean`][java-boolean-ref], [`Byte`][java-byte-ref], [`Short`][java-short-ref], [`Integer`][java-integer-ref], [`Long`][java-long-ref], [`BigDecimal`][java-bigdecimal-ref], [`BigInteger`][java-biginteger-ref]|
| [`smallint`][psql-smallint-ref] | [**`Short`**][java-short-ref], [`Boolean`][java-boolean-ref], [`Byte`][java-byte-ref], [`Integer`][java-integer-ref], [`Long`][java-long-ref], [`BigDecimal`][java-bigdecimal-ref], [`BigInteger`][java-biginteger-ref]|
| [`smallserial`][psql-smallserial-ref] | [**`Integer`**][java-integer-ref], [`Boolean`][java-boolean-ref], [`Byte`][java-byte-ref], [`Short`][java-short-ref], [`Long`][java-long-ref], [`BigDecimal`][java-bigdecimal-ref], [`BigInteger`][java-biginteger-ref]|
Expand Down
85 changes: 85 additions & 0 deletions src/main/java/io/r2dbc/postgresql/codec/AbstractGeometryCodec.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
package io.r2dbc.postgresql.codec;

import io.netty.buffer.ByteBuf;
import io.netty.buffer.ByteBufAllocator;
import io.r2dbc.postgresql.client.Parameter;
import io.r2dbc.postgresql.message.Format;
import io.r2dbc.postgresql.type.PostgresqlObjectId;
import io.r2dbc.postgresql.util.Assert;
import io.r2dbc.postgresql.util.ByteBufUtils;

import java.util.ArrayList;
import java.util.List;

abstract class AbstractGeometryCodec<T> extends AbstractCodec<T> {

protected final PostgresqlObjectId postgresqlObjectId;

protected final ByteBufAllocator byteBufAllocator;

AbstractGeometryCodec(Class<T> type, PostgresqlObjectId postgresqlObjectId, ByteBufAllocator byteBufAllocator) {
super(type);
this.postgresqlObjectId = Assert.requireNonNull(postgresqlObjectId, "postgresqlObjectId must not be null");
this.byteBufAllocator = Assert.requireNonNull(byteBufAllocator, "byteBufAllocator must not be null");
}

abstract T doDecodeBinary(ByteBuf byteBuffer);

abstract T doDecodeText(String text);

abstract ByteBuf doEncodeBinary(T value);

@Override
boolean doCanDecode(PostgresqlObjectId type, Format format) {
Assert.requireNonNull(type, "type must not be null");
Assert.requireNonNull(format, "format must not be null");
return postgresqlObjectId == type;
}

@Override
T doDecode(ByteBuf buffer, PostgresqlObjectId dataType, Format format, Class<? extends T> type) {
Assert.requireNonNull(buffer, "byteBuf must not be null");
Assert.requireNonNull(type, "type must not be null");
Assert.requireNonNull(format, "format must not be null");
if (format == Format.FORMAT_BINARY) {
return doDecodeBinary(buffer);
}
return doDecodeText(ByteBufUtils.decode(buffer));
}

@Override
Parameter doEncode(T value) {
Assert.requireNonNull(value, "value must not be null");
return create(this.postgresqlObjectId, Format.FORMAT_BINARY, () -> doEncodeBinary(value));
}

@Override
public Parameter encodeNull() {
return createNull(postgresqlObjectId, Format.FORMAT_BINARY);
}

protected List<String> tokenizeTextData(String string) {
List<String> tokens = new ArrayList<>();

for (int p = 0, s = 0; p < string.length(); p++) {
char c = string.charAt(p);

if (c == '(' || c == '[' || c == '<' || c == '{') {
s++;
continue;
}

if (c == ',' || c == ')' || c == ']' || c == '>' || c == '}') {
if (s != p) {
tokens.add(string.substring(s, p));
s = p + 1;
} else {
s++;
}
}
}

return tokens;
}

}
58 changes: 58 additions & 0 deletions src/main/java/io/r2dbc/postgresql/codec/Box.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
package io.r2dbc.postgresql.codec;

import io.r2dbc.postgresql.util.Assert;

import java.util.Objects;

/**
* Value object that maps to the {@code box} datatype in Postgres.
* <p>
* Uses {@code double} to represent the coordinates.
*/
public final class Box {

private final Point a;

private final Point b;

private Box(Point a, Point b) {
this.a = Assert.requireNonNull(a, "point A must not be null");
this.b = Assert.requireNonNull(b, "point B must not be null");
}

public static Box of(Point a, Point b) {
return new Box(a, b);
}

public Point getA() {
return this.a;
}

public Point getB() {
return this.b;
}

@Override
public boolean equals(Object o) {
if (this == o) {
return true;
}
if (o == null || getClass() != o.getClass()) {
return false;
}
Box box = (Box) o;
return (this.a.equals(box.a) && this.b.equals(box.b))
|| (this.a.equals(box.b) && this.b.equals(box.a));
}

@Override
public int hashCode() {
return Objects.hash(this.a, this.b);
}

@Override
public String toString() {
return "(" + this.a + "," + this.b + ')';
}

}
42 changes: 42 additions & 0 deletions src/main/java/io/r2dbc/postgresql/codec/BoxCodec.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
package io.r2dbc.postgresql.codec;

import io.netty.buffer.ByteBuf;
import io.netty.buffer.ByteBufAllocator;
import io.r2dbc.postgresql.type.PostgresqlObjectId;

import java.util.List;

final class BoxCodec extends AbstractGeometryCodec<Box> {

BoxCodec(ByteBufAllocator byteBufAllocator) {
super(Box.class, PostgresqlObjectId.BOX, byteBufAllocator);
}

@Override
Box doDecodeBinary(ByteBuf buffer) {
double x1 = buffer.readDouble();
double y1 = buffer.readDouble();
double x2 = buffer.readDouble();
double y2 = buffer.readDouble();
return Box.of(Point.of(x1, y1), Point.of(x2, y2));
}

@Override
Box doDecodeText(String text) {
List<String> tokens = tokenizeTextData(text);
return Box.of(
Point.of(Double.parseDouble(tokens.get(0)), Double.parseDouble(tokens.get(1))),
Point.of(Double.parseDouble(tokens.get(2)), Double.parseDouble(tokens.get(3)))
);
}

@Override
ByteBuf doEncodeBinary(Box value) {
return this.byteBufAllocator.buffer(32)
.writeDouble(value.getA().getX())
.writeDouble(value.getA().getY())
.writeDouble(value.getB().getX())
.writeDouble(value.getB().getY());
}

}
63 changes: 17 additions & 46 deletions src/main/java/io/r2dbc/postgresql/codec/CircleCodec.java
Original file line number Diff line number Diff line change
Expand Up @@ -18,70 +18,41 @@

import io.netty.buffer.ByteBuf;
import io.netty.buffer.ByteBufAllocator;
import io.r2dbc.postgresql.client.Parameter;
import io.r2dbc.postgresql.message.Format;
import io.r2dbc.postgresql.type.PostgresqlObjectId;
import io.r2dbc.postgresql.util.Assert;
import io.r2dbc.postgresql.util.ByteBufUtils;

import static io.r2dbc.postgresql.message.Format.FORMAT_BINARY;
import static io.r2dbc.postgresql.type.PostgresqlObjectId.CIRCLE;
import java.util.List;

final class CircleCodec extends AbstractCodec<Circle> {
import static io.r2dbc.postgresql.type.PostgresqlObjectId.CIRCLE;

private final ByteBufAllocator byteBufAllocator;
final class CircleCodec extends AbstractGeometryCodec<Circle> {

CircleCodec(ByteBufAllocator byteBufAllocator) {
super(Circle.class);
this.byteBufAllocator = Assert.requireNonNull(byteBufAllocator, "byteBufAllocator must not be null");
super(Circle.class, CIRCLE, byteBufAllocator);
}

@Override
boolean doCanDecode(PostgresqlObjectId type, Format format) {
Assert.requireNonNull(type, "type must not be null");

return CIRCLE == type;
Circle doDecodeBinary(ByteBuf byteBuffer) {
double x = byteBuffer.readDouble();
double y = byteBuffer.readDouble();
double r = byteBuffer.readDouble();
return new Circle(Point.of(x, y), r);
}

@Override
Circle doDecode(ByteBuf buffer, PostgresqlObjectId dataType, Format format, Class<? extends Circle> type) {
Assert.requireNonNull(buffer, "byteBuf must not be null");
Assert.requireNonNull(type, "type must not be null");
Assert.requireNonNull(format, "format must not be null");

if (format == FORMAT_BINARY) {
double x = buffer.readDouble();
double y = buffer.readDouble();
double r = buffer.readDouble();
return new Circle(Point.of(x, y), r);
}

String decodedAsString = ByteBufUtils.decode(buffer);
String parenRemovedVal = decodedAsString.replaceAll("[()<>]", "");
String[] coordinatesAsString = parenRemovedVal.split(",");
double x = Double.parseDouble(coordinatesAsString[0]);
double y = Double.parseDouble(coordinatesAsString[1]);
double r = Double.parseDouble(coordinatesAsString[2]);
Circle doDecodeText(String text) {
List<String> tokens = tokenizeTextData(text);
double x = Double.parseDouble(tokens.get(0));
double y = Double.parseDouble(tokens.get(1));
double r = Double.parseDouble(tokens.get(2));
return new Circle(Point.of(x, y), r);
}

/**
* @param value the {@code value}.
* @return Circle in string format as understood by Postgresql - &lt(x,y),r&gt
*/
@Override
Parameter doEncode(Circle value) {
Assert.requireNonNull(value, "value must not be null");
ByteBuf doEncodeBinary(Circle value) {
Point center = value.getCenter();
return create(CIRCLE, FORMAT_BINARY, () -> this.byteBufAllocator.buffer(lengthInBytes())
return this.byteBufAllocator.buffer(lengthInBytes())
.writeDouble(center.getX())
.writeDouble(center.getY())
.writeDouble(value.getRadius()));
}

@Override
public Parameter encodeNull() {
return createNull(CIRCLE, FORMAT_BINARY);
.writeDouble(value.getRadius());
}

int lengthInBytes() {
Expand Down
7 changes: 6 additions & 1 deletion src/main/java/io/r2dbc/postgresql/codec/DefaultCodecs.java
Original file line number Diff line number Diff line change
Expand Up @@ -97,7 +97,12 @@ public DefaultCodecs(ByteBufAllocator byteBufAllocator) {

//Geometry
new CircleCodec(byteBufAllocator),
new PointCodec(byteBufAllocator)
new PointCodec(byteBufAllocator),
new BoxCodec(byteBufAllocator),
new LineCodec(byteBufAllocator),
new LsegCodec(byteBufAllocator),
new PathCodec(byteBufAllocator),
new PolygonCodec(byteBufAllocator)
));
}

Expand Down
64 changes: 64 additions & 0 deletions src/main/java/io/r2dbc/postgresql/codec/Line.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
package io.r2dbc.postgresql.codec;

import java.util.Objects;

/**
* Value object that maps to the {@code line} datatype in Postgres.
* <p>
* Uses {@code double} to represent the coordinates.
*/
public final class Line {

private final double a;
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We should add a bit of Javadoc to the fields.


private final double b;

private final double c;

private Line(double a, double b, double c) {
this.a = a;
this.b = b;
this.c = c;
}

public static Line of(double a, double b, double c) {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Would it make sense to add factory methods accepting two points or even x1/y1, x2/y2?

return new Line(a, b, c);
}

public double getA() {
return this.a;
}

public double getB() {
return this.b;
}

public double getC() {
return this.c;
}

@Override
public boolean equals(Object o) {
if (this == o) {
return true;
}
if (o == null || getClass() != o.getClass()) {
return false;
}
Line line = (Line) o;
return Double.compare(line.a, this.a) == 0 &&
Double.compare(line.b, this.b) == 0 &&
Double.compare(line.c, this.c) == 0;
}

@Override
public int hashCode() {
return Objects.hash(this.a, this.b, this.c);
}

@Override
public String toString() {
return "{" + this.a + "," + this.b + "," + this.c + '}';
}

}
Loading