Skip to content
Draft
Show file tree
Hide file tree
Changes from all 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
29 changes: 29 additions & 0 deletions src/main/java/io/codemine/java/postgresql/codecs/Box2d.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
package io.codemine.java.postgresql.codecs;

/** PostGIS {@code box2d} type. */
public record Box2d(double xmin, double ymin, double xmax, double ymax) {

/** Builds a canonical box from any two opposite corners. */
public static Box2d of(double x1, double y1, double x2, double y2) {
return new Box2d(Math.min(x1, x2), Math.min(y1, y2), Math.max(x1, x2), Math.max(y1, y2));
}

@Override
public String toString() {
StringBuilder sb = new StringBuilder();
appendInTextTo(sb);
return sb.toString();
}

void appendInTextTo(StringBuilder sb) {
sb.append("BOX(")
.append(xmin)
.append(' ')
.append(ymin)
.append(',')
.append(xmax)
.append(' ')
.append(ymax)
.append(')');
}
}
69 changes: 69 additions & 0 deletions src/main/java/io/codemine/java/postgresql/codecs/Box2dCodec.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
package io.codemine.java.postgresql.codecs;

import java.io.ByteArrayOutputStream;
import java.nio.ByteBuffer;
import java.nio.charset.StandardCharsets;
import java.util.Random;

/** Codec for PostGIS {@code box2d} values. */
final class Box2dCodec implements Codec<Box2d> {

@Override
public String name() {
return "box2d";
}

@Override
public void encodeInText(StringBuilder sb, Box2d value) {
value.appendInTextTo(sb);
}

@Override
public Codec.ParsingResult<Box2d> decodeInText(CharSequence input, int offset)
throws Codec.DecodingException {
String text = input.subSequence(offset, input.length()).toString().trim();
if (!text.startsWith("BOX(") || !text.endsWith(")")) {
throw new Codec.DecodingException(input, offset, "Invalid box2d literal: " + text);
}
String[] halves = text.substring(4, text.length() - 1).split(",", -1);
if (halves.length != 2) {
throw new Codec.DecodingException(input, offset, "Invalid box2d literal: " + text);
}
double[] lower = parsePair(halves[0], input, offset);
double[] upper = parsePair(halves[1], input, offset);
return new Codec.ParsingResult<>(
new Box2d(lower[0], lower[1], upper[0], upper[1]), input.length());
}

@Override
public void encodeInBinary(Box2d value, ByteArrayOutputStream out) {
out.writeBytes(encodeInTextToString(value).getBytes(StandardCharsets.UTF_8));
}

@Override
public Box2d decodeInBinary(ByteBuffer buf, int length) throws Codec.DecodingException {
byte[] bytes = new byte[length];
buf.get(bytes);
return decodeInText(new String(bytes, StandardCharsets.UTF_8), 0).value;
}

@Override
public Box2d random(Random r, int size) {
return Box2d.of(
randomValue(r, size), randomValue(r, size), randomValue(r, size), randomValue(r, size));
}

private static double[] parsePair(CharSequence text, CharSequence input, int offset)
throws Codec.DecodingException {
String[] parts = text.toString().trim().split("\\s+", -1);
if (parts.length != 2) {
throw new Codec.DecodingException(input, offset, "Invalid box2d coordinate pair: " + text);
}
return new double[] {Double.parseDouble(parts[0]), Double.parseDouble(parts[1])};
}

private static double randomValue(Random r, int size) {
double raw = (r.nextDouble() * 2.0 - 1.0) * Math.max(size, 1);
return Math.rint(raw * 1_000_000d) / 1_000_000d;
}
}
39 changes: 39 additions & 0 deletions src/main/java/io/codemine/java/postgresql/codecs/Box3d.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
package io.codemine.java.postgresql.codecs;

/** PostGIS {@code box3d} type. */
public record Box3d(double xmin, double ymin, double zmin, double xmax, double ymax, double zmax) {

/** Builds a canonical box from any two opposite corners. */
public static Box3d of(double x1, double y1, double z1, double x2, double y2, double z2) {
return new Box3d(
Math.min(x1, x2),
Math.min(y1, y2),
Math.min(z1, z2),
Math.max(x1, x2),
Math.max(y1, y2),
Math.max(z1, z2));
}

@Override
public String toString() {
StringBuilder sb = new StringBuilder();
appendInTextTo(sb);
return sb.toString();
}

void appendInTextTo(StringBuilder sb) {
sb.append("BOX3D(")
.append(xmin)
.append(' ')
.append(ymin)
.append(' ')
.append(zmin)
.append(',')
.append(xmax)
.append(' ')
.append(ymax)
.append(' ')
.append(zmax)
.append(')');
}
}
76 changes: 76 additions & 0 deletions src/main/java/io/codemine/java/postgresql/codecs/Box3dCodec.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
package io.codemine.java.postgresql.codecs;

import java.io.ByteArrayOutputStream;
import java.nio.ByteBuffer;
import java.nio.charset.StandardCharsets;
import java.util.Random;

/** Codec for PostGIS {@code box3d} values. */
final class Box3dCodec implements Codec<Box3d> {

@Override
public String name() {
return "box3d";
}

@Override
public void encodeInText(StringBuilder sb, Box3d value) {
value.appendInTextTo(sb);
}

@Override
public Codec.ParsingResult<Box3d> decodeInText(CharSequence input, int offset)
throws Codec.DecodingException {
String text = input.subSequence(offset, input.length()).toString().trim();
if (!text.startsWith("BOX3D(") || !text.endsWith(")")) {
throw new Codec.DecodingException(input, offset, "Invalid box3d literal: " + text);
}
String[] halves = text.substring(6, text.length() - 1).split(",", -1);
if (halves.length != 2) {
throw new Codec.DecodingException(input, offset, "Invalid box3d literal: " + text);
}
double[] lower = parseTriple(halves[0], input, offset);
double[] upper = parseTriple(halves[1], input, offset);
return new Codec.ParsingResult<>(
new Box3d(lower[0], lower[1], lower[2], upper[0], upper[1], upper[2]), input.length());
}

@Override
public void encodeInBinary(Box3d value, ByteArrayOutputStream out) {
out.writeBytes(encodeInTextToString(value).getBytes(StandardCharsets.UTF_8));
}

@Override
public Box3d decodeInBinary(ByteBuffer buf, int length) throws Codec.DecodingException {
byte[] bytes = new byte[length];
buf.get(bytes);
return decodeInText(new String(bytes, StandardCharsets.UTF_8), 0).value;
}

@Override
public Box3d random(Random r, int size) {
return Box3d.of(
randomValue(r, size),
randomValue(r, size),
randomValue(r, size),
randomValue(r, size),
randomValue(r, size),
randomValue(r, size));
}

private static double[] parseTriple(CharSequence text, CharSequence input, int offset)
throws Codec.DecodingException {
String[] parts = text.toString().trim().split("\\s+", -1);
if (parts.length != 3) {
throw new Codec.DecodingException(input, offset, "Invalid box3d coordinate triple: " + text);
}
return new double[] {
Double.parseDouble(parts[0]), Double.parseDouble(parts[1]), Double.parseDouble(parts[2])
};
}

private static double randomValue(Random r, int size) {
double raw = (r.nextDouble() * 2.0 - 1.0) * Math.max(size, 1);
return Math.rint(raw * 1_000_000d) / 1_000_000d;
}
}
33 changes: 31 additions & 2 deletions src/main/java/io/codemine/java/postgresql/codecs/Codec.java
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,11 @@ public interface Codec<A> {
Codec<Path> PATH = new PathCodec();
Codec<Polygon> POLYGON = new PolygonCodec();
Codec<Circle> CIRCLE = new CircleCodec();
Codec<Box2d> BOX2D = new Box2dCodec();
Codec<Box3d> BOX3D = new Box3dCodec();
Codec<Geometry> GEOMETRY = new GeometryCodec();
Codec<Geography> GEOGRAPHY = new GeographyCodec();
Codec<GeometryDump> GEOMETRY_DUMP = new GeometryDumpCodec();
Codec<Cidr> CIDR = new CidrCodec();
Codec<Macaddr8> MACADDR8 = new Macaddr8Codec();
Codec<Bit> BIT = new BitCodec();
Expand Down Expand Up @@ -221,6 +226,20 @@ default int jdbcType() {
return java.sql.Types.OTHER;
}

/**
* Returns {@code true} if this codec supports the PostgreSQL binary wire format.
*
* <p>By default, codecs only support the textual format, and binary support must be explicitly
* implemented by overriding the relevant methods.
*
* <p>All standard scalar types do support binary format, but some extensions don't. E.g., the
* {@code box2d} and {@code box3d} types provided by PostGIS only support text format, and their
* codecs return {@code false} here.
*/
default boolean supportsBinaryFormat() {
return false;
}

// -----------------------------------------------------------------------
// Textual wire format
// -----------------------------------------------------------------------
Expand Down Expand Up @@ -283,9 +302,14 @@ default A decodeInTextFromString(String input) throws DecodingException {
*
* <p>The byte order is always <b>big-endian</b>, as required by the PostgreSQL wire protocol.
*
* <p>The default implementation throws {@link UnsupportedOperationException}, so binary decoding
* must be explicitly implemented by overriding this method.
*
* @throws UnsupportedOperationException if binary encoding is not implemented for this type
*/
void encodeInBinary(A value, ByteArrayOutputStream out);
default void encodeInBinary(A value, ByteArrayOutputStream out) {
throw new UnsupportedOperationException("Binary encoding not supported for type " + typeSig());
}

/**
* Convenience overload that encodes the value into a freshly-allocated byte array and returns it.
Expand Down Expand Up @@ -316,10 +340,15 @@ default ByteBuffer encodeInBinaryToByteBuffer(A value) {
* <p>NULL handling ({@code length == -1}) must be performed by the caller before invoking this
* method.
*
* <p>The default implementation throws {@link UnsupportedOperationException}, so binary decoding
* must be explicitly implemented by overriding this method.
*
* @throws DecodingException if the binary data is malformed
* @throws UnsupportedOperationException if binary decoding is not implemented for this type
*/
A decodeInBinary(ByteBuffer buf, int length) throws DecodingException;
default A decodeInBinary(ByteBuffer buf, int length) throws DecodingException {
throw new UnsupportedOperationException("Binary decoding not supported for type " + typeSig());
}

/**
* Decodes a value from a byte array in the PostgreSQL binary wire format. Convenience wrapper
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -534,4 +534,14 @@ public Field(String name, Function<Z, A> accessor, Codec<A> codec) {
this.codec = codec;
}
}

@Override
public boolean supportsBinaryFormat() {
for (Field field : fields) {
if (!field.codec.supportsBinaryFormat()) {
return false;
}
}
return true;
}
}
11 changes: 11 additions & 0 deletions src/main/java/io/codemine/java/postgresql/codecs/Geography.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
package io.codemine.java.postgresql.codecs;

import java.util.Objects;

/** PostGIS {@code geography} payload modeled as a structured geometry tree. */
public record Geography(Geometry value) {
/** Creates a geography value backed by a non-null geometry tree. */
public Geography {
Objects.requireNonNull(value, "value");
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
package io.codemine.java.postgresql.codecs;

import java.io.ByteArrayOutputStream;
import java.nio.ByteBuffer;
import java.util.List;
import java.util.Random;

/** Codec for PostGIS {@code geography} values. */
final class GeographyCodec implements Codec<Geography> {

@Override
public String name() {
return "geography";
}

@Override
public void encodeInText(StringBuilder sb, Geography value) {
sb.append(GeometrySerde.encodeHex(GeometrySerde.encodeGeometry(value.value())));
}

@Override
public Codec.ParsingResult<Geography> decodeInText(CharSequence input, int offset)
throws Codec.DecodingException {
byte[] bytes = GeometrySerde.decodeHex(input.subSequence(offset, input.length()));
return new Codec.ParsingResult<>(
new Geography(GeometrySerde.decodeGeometry(ByteBuffer.wrap(bytes), bytes.length)),
input.length());
}

@Override
public void encodeInBinary(Geography value, ByteArrayOutputStream out) {
out.writeBytes(GeometrySerde.encodeGeometry(value.value()));
}

@Override
public Geography decodeInBinary(ByteBuffer buf, int length) throws Codec.DecodingException {
return new Geography(GeometrySerde.decodeGeometry(buf, length));
}

@Override
public Codec<List<Geography>> inDim() {
return new ArrayCodec<>(this, ':');
}

@Override
public Geography random(Random r, int size) {
return GeometrySerde.randomGeography(r, size);
}
}
Loading
Loading