Skip to content

Commit 69d0fbe

Browse files
committed
enable binary transfer for PGgeometry and PGgeography.
1 parent 099108b commit 69d0fbe

File tree

13 files changed

+520
-65
lines changed

13 files changed

+520
-65
lines changed

README.md

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -20,8 +20,9 @@ It is originally based on [postgis-java](https://github.com/postgis/postgis-java
2020
* Support for wrapped connections (like used in WildFly and c3p0 connection pooling)
2121
* Use generic Java types where possible and simplify/streamline API
2222
* Clean up code to basically only work on [WKB](https://en.wikipedia.org/wiki/Well-known_text#Well-known_binary)/EWKB implementations to reduce code duplication and focus on the actual database format
23+
* Support for binary transfer of geometry data (if enabled in PostgreSQL JDBC driver, see [PR#2556](https://github.com/pgjdbc/pgjdbc/pull/2556))
2324
* Support for the latest PostgreSQL and PostGIS versions
24-
* Recommended are PostgreSQL 14 and PostGIS 3.2.0
25+
* Recommended are PostgreSQL 14 and PostGIS 3.2.1
2526
* Supported are versions starting from PostgreSQL 9.6 and PostGIS 2.3
2627
* Support for JDK 11+ (there is an older [branch for JDK 8](https://github.com/sebasbaumh/postgis-java-ng/tree/jdk8))
2728
* The license is still LGPL
@@ -49,11 +50,11 @@ There is a Maven artifact in the official Maven repository, so just add this to
4950
<dependency>
5051
<groupId>io.github.sebasbaumh</groupId>
5152
<artifactId>postgis-java-ng</artifactId>
52-
<version>22.2.0</version>
53+
<version>22.3.0</version>
5354
</dependency>
5455
```
5556

56-
The version reflects the year of the release, e.g. `22.2.0` is a version released in 2022.
57+
The version reflects the year of the release, e.g. `22.3.0` is a version released in 2022.
5758

5859
The API differs a bit from [postgis-java](https://github.com/postgis/postgis-java) with the main point being a different namespace (`io.github.sebasbaumh.postgis`) as publishing a project to Maven Central requires to own that namespace.
5960
In addition the class structure is a bit different (see below) to support arc geometries and reduce boilerplate code, but you should be able to adapt to it easily.

pom.xml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
<groupId>io.github.sebasbaumh</groupId>
55
<artifactId>postgis-java-ng</artifactId>
66
<!-- version for release -->
7-
<version>22.2.1-SNAPSHOT</version>
7+
<version>22.3.0-SNAPSHOT</version>
88
<packaging>jar</packaging>
99

1010
<name>PostGIS Java bindings</name>

src/main/java/io/github/sebasbaumh/postgis/DriverWrapper.java

Lines changed: 98 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,8 +27,15 @@
2727

2828
import java.lang.reflect.Method;
2929
import java.sql.Connection;
30+
import java.sql.ResultSet;
3031
import java.sql.SQLException;
32+
import java.sql.Statement;
33+
import java.util.ArrayList;
34+
import java.util.Arrays;
35+
import java.util.Collection;
36+
import java.util.HashSet;
3137
import java.util.Properties;
38+
import java.util.Set;
3239
import java.util.logging.Level;
3340
import java.util.logging.Logger;
3441

@@ -39,6 +46,9 @@
3946
import org.eclipse.jdt.annotation.NonNullByDefault;
4047
import org.postgresql.Driver;
4148
import org.postgresql.PGConnection;
49+
import org.postgresql.core.BaseConnection;
50+
import org.postgresql.core.Oid;
51+
import org.postgresql.core.QueryExecutor;
4252

4353
/**
4454
* Wraps the PostGreSQL Driver to transparently add the PostGIS Object Classes. This avoids the need of explicit
@@ -99,6 +109,43 @@ public DriverWrapper()
99109
{
100110
}
101111

112+
/**
113+
* Tries to get a {@link QueryExecutor} from the given {@link Connection}, supports wrapped connections.
114+
* @param conn {@link Connection}
115+
* @return {@link QueryExecutor}
116+
* @throws SQLException if the {@link Connection} is no {@link BaseConnection}.
117+
*/
118+
private static QueryExecutor getQueryExecutor(Connection conn) throws SQLException
119+
{
120+
// try to get underlying PGConnection
121+
PGConnection pgconn = tryUnwrap(conn);
122+
// if instance is found, add the geometry types to the connection
123+
if (pgconn instanceof BaseConnection)
124+
{
125+
return ((BaseConnection) pgconn).getQueryExecutor();
126+
}
127+
// try to unwrap connections coming from c3p0 connection pools
128+
try
129+
{
130+
Class<?> clazzC3P0ProxyConnection = Class.forName("com.mchange.v2.c3p0.C3P0ProxyConnection");
131+
if (clazzC3P0ProxyConnection.isInstance(conn))
132+
{
133+
// use method Object rawConnectionOperation(Method m, Object target, Object[] args)
134+
Method mrawConnectionOperation = clazzC3P0ProxyConnection.getMethod("rawConnectionOperation",
135+
Method.class, Object.class, Object[].class);
136+
Method mAddDataType = BaseConnection.class.getMethod("getQueryExecutor");
137+
return (QueryExecutor) mrawConnectionOperation.invoke(conn, mAddDataType, null, null);
138+
}
139+
}
140+
catch (ReflectiveOperationException | SecurityException | IllegalArgumentException ex)
141+
{
142+
// ignore all errors here
143+
}
144+
// BaseConnection could not be found
145+
throw new SQLException(
146+
"Connection is neither an org.postgresql.core.BaseConnection, nor a Connection wrapped around an org.postgresql.core.BaseConnection.");
147+
}
148+
102149
/**
103150
* Mangles the PostGIS URL to return the original PostGreSQL URL
104151
* @param url String containing the url to be "mangled"
@@ -202,6 +249,57 @@ public static void registerDataTypes(PGConnection pgconn) throws SQLException
202249
pgconn.addDataType("\"public\".\"box3d\"", io.github.sebasbaumh.postgis.PGbox3d.class);
203250
}
204251

252+
/**
253+
* Registers all datatypes for binary transfer on the given connection, supports wrapped connections.
254+
* <p>
255+
* NOTE: this is experimental and only necessary until PostgreSQL JDBC driver is able to register types for binary
256+
* transfer.
257+
* @param conn {@link Connection}
258+
* @throws SQLException if the {@link Connection} is no {@link BaseConnection}.
259+
*/
260+
// FIX: this is experimental and only necessary until PostgreSQL JDBC driver is able to register types for binary
261+
// transfer
262+
// see https://github.com/pgjdbc/pgjdbc/pull/2556
263+
public static void registerDataTypesForBinaryTransfer(Connection conn) throws SQLException
264+
{
265+
// try to get query executor
266+
QueryExecutor executor = getQueryExecutor(conn);
267+
// collect oids of geometry types
268+
ArrayList<Integer> geometryOids = new ArrayList<Integer>();
269+
try (Statement st = conn.createStatement())
270+
{
271+
try (ResultSet rs = st
272+
.executeQuery("SELECT oid,typname FROM pg_type WHERE typname IN ('geometry','geography')"))
273+
{
274+
while (rs.next())
275+
{
276+
geometryOids.add(rs.getInt(1));
277+
}
278+
}
279+
}
280+
281+
// this is kind of a hack as it currently is not possible to get already enabled oids from the connection get
282+
// base oids like in PgConnection
283+
Collection<Integer> supportedBinaryOids = Arrays.asList(Oid.BYTEA, Oid.INT2, Oid.INT4, Oid.INT8, Oid.FLOAT4,
284+
Oid.FLOAT8, Oid.NUMERIC, Oid.TIME, Oid.DATE, Oid.TIMETZ, Oid.TIMESTAMP, Oid.TIMESTAMPTZ,
285+
Oid.BYTEA_ARRAY, Oid.INT2_ARRAY, Oid.INT4_ARRAY, Oid.INT8_ARRAY, Oid.OID_ARRAY, Oid.FLOAT4_ARRAY,
286+
Oid.FLOAT8_ARRAY, Oid.VARCHAR_ARRAY, Oid.TEXT_ARRAY, Oid.POINT, Oid.BOX, Oid.UUID);
287+
Set<Integer> receiveOids = new HashSet<Integer>(supportedBinaryOids);
288+
Set<Integer> sendOids = new HashSet<Integer>(supportedBinaryOids);
289+
// do the same as the original PostgreSQL JDBC driver
290+
sendOids.remove(Oid.DATE);
291+
292+
// add geometry oids
293+
receiveOids.addAll(geometryOids);
294+
sendOids.addAll(geometryOids);
295+
296+
// finally set oids
297+
// TODO: this replaces the already set oids and should be changed later (when additional API functions are
298+
// merged to pgjdbc)
299+
executor.setBinaryReceiveOids(receiveOids);
300+
executor.setBinarySendOids(sendOids);
301+
}
302+
205303
/**
206304
* Tries to turn the given {@link Connection} into a {@link PGConnection}, supports wrapped connections and
207305
* JBoss/WildFly WrappedConnections.

src/main/java/io/github/sebasbaumh/postgis/PGgeometrybase.java

Lines changed: 95 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@
3030

3131
import org.eclipse.jdt.annotation.DefaultLocation;
3232
import org.eclipse.jdt.annotation.NonNullByDefault;
33+
import org.postgresql.util.PGBinaryObject;
3334
import org.postgresql.util.PGobject;
3435

3536
import io.github.sebasbaumh.postgis.binary.BinaryParser;
@@ -41,13 +42,23 @@
4142
* @author Phillip Ross
4243
*/
4344
@NonNullByDefault({ DefaultLocation.PARAMETER, DefaultLocation.RETURN_TYPE })
44-
public abstract class PGgeometrybase extends PGobject
45+
public abstract class PGgeometrybase extends PGobject implements PGBinaryObject
4546
{
4647
/* JDK 1.5 Serialization */
4748
private static final long serialVersionUID = 0x100;
4849

50+
/**
51+
* Underlying geometry.
52+
*/
53+
@Nullable
4954
protected Geometry geometry;
5055

56+
/**
57+
* Geometry data as bytes.
58+
*/
59+
@Nullable
60+
private byte[] geometryData;
61+
5162
/**
5263
* Constructs an instance.
5364
* @param type type of this {@link PGobject}
@@ -98,19 +109,49 @@ public boolean equals(@Nullable Object obj)
98109
return Objects.equals(this.geometry, other.geometry);
99110
}
100111

112+
/**
113+
* Gets the binary value.
114+
* @return binary value on success, else null
115+
*/
116+
@Nullable
117+
private byte[] getBinaryValue()
118+
{
119+
// short cut
120+
if (this.geometryData != null)
121+
{
122+
return this.geometryData;
123+
}
124+
// check if geometry is there
125+
if (this.geometry != null)
126+
{
127+
// build geometry data and remember it
128+
byte[] data = BinaryWriter.writeBinary(geometry);
129+
this.geometryData = data;
130+
return data;
131+
}
132+
// no geometry
133+
return null;
134+
}
135+
101136
/**
102137
* Gets the underlying {@link Geometry}.
103-
* @return {@link Geometry}
138+
* @return {@link Geometry} on success, else null
104139
*/
140+
@Nullable
105141
public Geometry getGeometry()
106142
{
107143
return geometry;
108144
}
109145

146+
@Nullable
110147
@Override
111148
public String getValue()
112149
{
113-
return BinaryWriter.writeHexed(geometry);
150+
if (geometry != null)
151+
{
152+
return BinaryWriter.writeHexed(geometry);
153+
}
154+
return null;
114155
}
115156

116157
@Override
@@ -119,25 +160,72 @@ public int hashCode()
119160
return Objects.hashCode(geometry);
120161
}
121162

163+
@Override
164+
public int lengthInBytes()
165+
{
166+
byte[] data = getBinaryValue();
167+
if (data != null)
168+
{
169+
return data.length;
170+
}
171+
// no geometry
172+
return 0;
173+
}
174+
175+
@Override
176+
public void setByteValue(@SuppressWarnings("null") byte[] value, int offset) throws SQLException
177+
{
178+
// parse the given bytes
179+
this.geometry = BinaryParser.parse(value, offset);
180+
}
181+
122182
/**
123183
* Sets the underlying {@link Geometry}.
124-
* @param newgeom {@link Geometry}
184+
* @param newgeom {@link Geometry} (can be null)
125185
*/
126-
public void setGeometry(Geometry newgeom)
186+
public void setGeometry(@Nullable Geometry newgeom)
127187
{
128188
this.geometry = newgeom;
189+
// reset binary data
190+
this.geometryData = null;
129191
}
130192

131193
@Override
132194
public void setValue(@SuppressWarnings("null") @Nonnull String value) throws SQLException
133195
{
134-
geometry = BinaryParser.parse(value);
196+
this.geometry = BinaryParser.parse(value);
197+
// reset binary data
198+
this.geometryData = null;
199+
}
200+
201+
@Override
202+
public void toBytes(@SuppressWarnings("null") byte[] bytes, int offset)
203+
{
204+
byte[] data = getBinaryValue();
205+
if (data != null)
206+
{
207+
// make sure array is large enough
208+
if ((bytes.length - offset) <= data.length)
209+
{
210+
// copy data
211+
System.arraycopy(data, 0, bytes, offset, data.length);
212+
}
213+
else
214+
{
215+
throw new IllegalArgumentException(
216+
"byte array is too small, expected: " + data.length + " got: " + (bytes.length - offset));
217+
}
218+
}
219+
else
220+
{
221+
throw new IllegalStateException("no geometry has been set");
222+
}
135223
}
136224

137225
@Override
138226
public String toString()
139227
{
140-
return geometry.toString();
228+
return String.valueOf(geometry);
141229
}
142230

143231
}

src/main/java/io/github/sebasbaumh/postgis/binary/BinaryParser.java

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,18 @@ private BinaryParser()
6060
{
6161
}
6262

63+
/**
64+
* Parse a hex encoded geometry
65+
* @param value byte array containing the data to be parsed
66+
* @param offset offset
67+
* @return resulting geometry for the parsed data
68+
* @throws IllegalArgumentException if a contained geometry is of the wrong type or the encoding type is unknown
69+
*/
70+
public static Geometry parse(byte[] value, int offset)
71+
{
72+
return parseGeometry(new BinaryValueGetter(value, offset));
73+
}
74+
6375
/**
6476
* Parse a hex encoded geometry
6577
* @param value String containing the data to be parsed
@@ -68,7 +80,7 @@ private BinaryParser()
6880
*/
6981
public static Geometry parse(String value)
7082
{
71-
return parseGeometry(new ValueGetter(value));
83+
return parseGeometry(new StringValueGetter(value));
7284
}
7385

7486
/**

0 commit comments

Comments
 (0)