This is an automated email from the ASF dual-hosted git repository. amanin pushed a commit to branch refactor/sql-store in repository https://gitbox.apache.org/repos/asf/sis.git
commit 325ce8ec615f8d91902fbdf2a411f1ba111d208a Author: Alexis Manin <[email protected]> AuthorDate: Tue Nov 12 16:15:08 2019 +0100 feat(SQLStore): Improve PostGIS geometry binding Add dynamic CRS support Add multi-geometry support --- .../java/org/apache/sis/internal/feature/ESRI.java | 23 ++-- .../apache/sis/internal/feature/Geometries.java | 22 +++- .../java/org/apache/sis/internal/feature/JTS.java | 31 +++-- .../org/apache/sis/internal/feature/Java2D.java | 14 +- .../org/apache/sis/internal/feature/jts/JTS.java | 2 +- .../sis/internal/sql/feature/ANSIMapping.java | 18 +-- .../apache/sis/internal/sql/feature/Analyzer.java | 6 +- .../internal/sql/feature/CRSIdentification.java | 91 +++++++++++++ .../sis/internal/sql/feature/ColumnAdapter.java | 69 +++++----- .../sis/internal/sql/feature/EWKBReader.java | 114 +++++++++++++---- .../sis/internal/sql/feature/FeatureAdapter.java | 82 ++++++++---- .../apache/sis/internal/sql/feature/Features.java | 6 +- .../sql/feature/GeometryIdentification.java | 71 ++--------- .../sis/internal/sql/feature/PostGISMapping.java | 141 +++++++++++++++++---- .../sis/internal/sql/feature/QueryFeatureSet.java | 10 +- 15 files changed, 478 insertions(+), 222 deletions(-) diff --git a/core/sis-feature/src/main/java/org/apache/sis/internal/feature/ESRI.java b/core/sis-feature/src/main/java/org/apache/sis/internal/feature/ESRI.java index c01dd00..cca1b26 100644 --- a/core/sis-feature/src/main/java/org/apache/sis/internal/feature/ESRI.java +++ b/core/sis-feature/src/main/java/org/apache/sis/internal/feature/ESRI.java @@ -18,6 +18,7 @@ package org.apache.sis.internal.feature; import java.nio.ByteBuffer; import java.util.Iterator; +import java.util.stream.Stream; import org.apache.sis.geometry.GeneralEnvelope; import org.apache.sis.math.Vector; @@ -150,7 +151,7 @@ final class ESRI extends Geometries<Geometry> { @Override public Geometry toPolygon(Geometry polyline) throws IllegalArgumentException { if (polyline instanceof Polygon) return polyline; - return createMultiPolygonImpl(polyline); + return createMultiPolygon(Stream.of(polyline)); } /** @@ -159,7 +160,7 @@ final class ESRI extends Geometries<Geometry> { * @throws ClassCastException if an element in the iterator is not an ESRI geometry. */ @Override - final Geometry tryMergePolylines(Object next, final Iterator<?> polylines) { + public final Geometry tryMergePolylines(Object next, final Iterator<?> polylines) { if (!(next instanceof MultiPath || next instanceof Point)) { return null; } @@ -213,15 +214,17 @@ add: for (;;) { } @Override - Polygon createMultiPolygonImpl(Object... polygonsOrLinearRings) { - final Polygon poly = new Polygon(); - for (final Object polr : polygonsOrLinearRings) { - if (polr instanceof MultiPath) { - poly.add((MultiPath) polr, false); - } else throw new UnsupportedOperationException("Unsupported geometry type: "+polr == null ? "null" : polr.getClass().getCanonicalName()); - } + public Polygon createMultiPolygon(Stream<?> polygonsOrLinearRings) { + return polygonsOrLinearRings.map(ESRI::toMultiPath).reduce( + new Polygon(), + (p, m) -> {p.add(m, false); return p;}, + (p1, p2) -> {p1.add(p2, false); return p1;} + ); + } - return poly; + private static MultiPath toMultiPath(Object polr) { + if (polr instanceof MultiPath) return (MultiPath) polr; + else throw new UnsupportedOperationException("Unsupported geometry type: "+polr == null ? "null" : polr.getClass().getCanonicalName()); } /** diff --git a/core/sis-feature/src/main/java/org/apache/sis/internal/feature/Geometries.java b/core/sis-feature/src/main/java/org/apache/sis/internal/feature/Geometries.java index 9a17aed..46881bf 100644 --- a/core/sis-feature/src/main/java/org/apache/sis/internal/feature/Geometries.java +++ b/core/sis-feature/src/main/java/org/apache/sis/internal/feature/Geometries.java @@ -21,6 +21,7 @@ import java.util.Optional; import java.util.function.Function; import java.util.logging.Level; import java.util.logging.LogRecord; +import java.util.stream.Stream; import org.opengis.geometry.DirectPosition; import org.opengis.geometry.Envelope; @@ -335,7 +336,7 @@ public abstract class Geometries<G> { * @return the merged polyline, or {@code null} if the first instance is not an implementation of this library. * @throws ClassCastException if an element in the iterator is not an implementation of this library. */ - abstract G tryMergePolylines(Object first, Iterator<?> polylines); + public abstract G tryMergePolylines(Object first, Iterator<?> polylines); /** * Merges a sequence of points or polylines into a single polyline instances. @@ -455,7 +456,7 @@ public abstract class Geometries<G> { maxY = splittedLeft[3]; Vector[] points2 = clockwiseRing(minX, minY, maxX, maxY); final G secondRect = createPolyline(2, points2); - return createMultiPolygonImpl(mainRect, secondRect); + return createMultiPolygon(Stream.of(mainRect, secondRect)); } /* Geotk original method had an option to insert a median point on wrappped around axis, but we have not ported @@ -498,9 +499,20 @@ public abstract class Geometries<G> { public abstract double[] getPoints(Object geometry); - abstract G createMultiPolygonImpl(final Object... polygonsOrLinearRings); + public abstract G createMultiPolygon(final Stream<?> polygonsOrLinearRings); - public static Object createMultiPolygon(final Object... polygonsOrLinearRings) { - return findStrategy(g -> g.createMultiPolygonImpl(polygonsOrLinearRings)); + public static Object createMultiPolygon_(final Stream polygonsOrLinearRings) { + return findStrategy(g -> g.createMultiPolygon(polygonsOrLinearRings)); + } + + /** + * Try and associate given coordinate reference system to the specified geometry. It should replace any previously + * set referencing information. + * + * @param target The geometry to embed referencing information into. + * @param toApply Referencing information to add. + */ + public void setCRS(G target, CoordinateReferenceSystem toApply) { + throw new UnsupportedOperationException("Not supported yet"); } } diff --git a/core/sis-feature/src/main/java/org/apache/sis/internal/feature/JTS.java b/core/sis-feature/src/main/java/org/apache/sis/internal/feature/JTS.java index 28e0fbe..ea6d1f9 100644 --- a/core/sis-feature/src/main/java/org/apache/sis/internal/feature/JTS.java +++ b/core/sis-feature/src/main/java/org/apache/sis/internal/feature/JTS.java @@ -20,6 +20,7 @@ import java.util.ArrayList; import java.util.Arrays; import java.util.Iterator; import java.util.List; +import java.util.stream.Stream; import org.opengis.referencing.crs.CoordinateReferenceSystem; import org.opengis.util.FactoryException; @@ -207,8 +208,8 @@ final class JTS extends Geometries<Geometry> { } @Override - public Geometry toPolygon(Geometry polyline) throws IllegalArgumentException { - if (polyline instanceof Polygon) return polyline; + public Polygon toPolygon(Geometry polyline) throws IllegalArgumentException { + if (polyline instanceof Polygon) return (Polygon) polyline; Polygon result = null; if (polyline instanceof LinearRing) { @@ -269,7 +270,7 @@ final class JTS extends Geometries<Geometry> { * @throws ClassCastException if an element in the iterator is not a JTS geometry. */ @Override - final Geometry tryMergePolylines(Object next, final Iterator<?> polylines) { + public final Geometry tryMergePolylines(Object next, final Iterator<?> polylines) { if (!(next instanceof MultiLineString || next instanceof LineString || next instanceof Point)) { return null; } @@ -332,16 +333,22 @@ add: for (;;) { } @Override - MultiPolygon createMultiPolygonImpl(Object... polygonsOrLinearRings) { - final Polygon[] polys = new Polygon[polygonsOrLinearRings.length]; - for (int i = 0 ; i < polys.length ; i++) { - Object o = polygonsOrLinearRings[i]; - if (o instanceof GeometryWrapper) o = ((GeometryWrapper) o).geometry; + public MultiPolygon createMultiPolygon(Stream<?> polygonsOrLinearRings) { + final Polygon[] polys = polygonsOrLinearRings + .map(this::castToPolygon) + .toArray(size -> new Polygon[size]); + return factory.createMultiPolygon(polys); + } - if (o instanceof Polygon) polys[i] = (Polygon) o; - else if (o instanceof LinearRing) polys[i] = factory.createPolygon((LinearRing) o); - } + private Polygon castToPolygon(Object input) { + if (input instanceof GeometryWrapper) input = ((GeometryWrapper) input).geometry; - return factory.createMultiPolygon(polys); + if (input instanceof Geometry) return toPolygon((Geometry) input); + else throw new IllegalArgumentException("Given argument cannot be cast to polygon"); + } + + @Override + public void setCRS(Geometry target, CoordinateReferenceSystem toApply) { + org.apache.sis.internal.feature.jts.JTS.setCoordinateReferenceSystem(target, toApply); } } diff --git a/core/sis-feature/src/main/java/org/apache/sis/internal/feature/Java2D.java b/core/sis-feature/src/main/java/org/apache/sis/internal/feature/Java2D.java index 7f03643..50a492b 100644 --- a/core/sis-feature/src/main/java/org/apache/sis/internal/feature/Java2D.java +++ b/core/sis-feature/src/main/java/org/apache/sis/internal/feature/Java2D.java @@ -27,6 +27,7 @@ import java.util.Iterator; import java.util.Spliterator; import java.util.function.Consumer; import java.util.stream.DoubleStream; +import java.util.stream.Stream; import java.util.stream.StreamSupport; import org.apache.sis.geometry.GeneralEnvelope; @@ -37,7 +38,6 @@ import org.apache.sis.setup.GeometryLibrary; import org.apache.sis.util.Classes; import org.apache.sis.util.Numbers; -import static org.apache.sis.util.ArgumentChecks.ensureNonEmpty; import static org.apache.sis.util.ArgumentChecks.ensureNonNull; @@ -201,7 +201,7 @@ final class Java2D extends Geometries<Shape> { * @throws ClassCastException if an element in the iterator is not a {@link Shape} or a {@link Point2D}. */ @Override - final Shape tryMergePolylines(Object next, final Iterator<?> polylines) { + public final Shape tryMergePolylines(Object next, final Iterator<?> polylines) { if (!(next instanceof Shape || next instanceof Point2D)) { return null; } @@ -257,12 +257,10 @@ add: for (;;) { } @Override - Shape createMultiPolygonImpl(Object... polygonsOrLinearRings) { - ensureNonEmpty("Polygons or linear rings to merge", polygonsOrLinearRings); - if (polygonsOrLinearRings.length == 1 && polygonsOrLinearRings[0] instanceof Shape) - return (Shape) polygonsOrLinearRings[0]; - final Iterator<Object> it = Arrays.asList(polygonsOrLinearRings).iterator(); - return tryMergePolylines(it.next(), it); + public Shape createMultiPolygon(Stream<?> polygonsOrLinearRings) { + final Iterator<?> it = polygonsOrLinearRings.iterator(); + if (it.hasNext()) return tryMergePolylines(it.next(), it); + throw new IllegalArgumentException("Empty input"); } @Override diff --git a/core/sis-feature/src/main/java/org/apache/sis/internal/feature/jts/JTS.java b/core/sis-feature/src/main/java/org/apache/sis/internal/feature/jts/JTS.java index d3e1174..39a9ffc 100644 --- a/core/sis-feature/src/main/java/org/apache/sis/internal/feature/jts/JTS.java +++ b/core/sis-feature/src/main/java/org/apache/sis/internal/feature/jts/JTS.java @@ -117,7 +117,7 @@ public final class JTS extends Static { return Optional.empty(); } else if (ud instanceof CoordinateReferenceSystem) { target.setUserData(toSet); - return Optional.of((CoordinateReferenceSystem)ud); + return Optional.of((CoordinateReferenceSystem) ud); } else if (ud instanceof Map) { final Map asMap = (Map) ud; // In case user-data contains other useful data, we don't switch from map to CRS. We also reset SRID. diff --git a/storage/sis-sqlstore/src/main/java/org/apache/sis/internal/sql/feature/ANSIMapping.java b/storage/sis-sqlstore/src/main/java/org/apache/sis/internal/sql/feature/ANSIMapping.java index ef6403b..ed1c429 100644 --- a/storage/sis-sqlstore/src/main/java/org/apache/sis/internal/sql/feature/ANSIMapping.java +++ b/storage/sis-sqlstore/src/main/java/org/apache/sis/internal/sql/feature/ANSIMapping.java @@ -55,18 +55,18 @@ public class ANSIMapping implements DialectMapping { case Types.DECIMAL: return forceCast(BigDecimal.class); case Types.CHAR: case Types.VARCHAR: - case Types.LONGVARCHAR: return new ColumnAdapter<>(String.class, ResultSet::getString); - case Types.DATE: return new ColumnAdapter<>(Date.class, ResultSet::getDate); - case Types.TIME: return new ColumnAdapter<>(LocalTime.class, ANSIMapping::toLocalTime); - case Types.TIMESTAMP: return new ColumnAdapter<>(Instant.class, ANSIMapping::toInstant); - case Types.TIME_WITH_TIMEZONE: return new ColumnAdapter<>(OffsetTime.class, ANSIMapping::toOffsetTime); - case Types.TIMESTAMP_WITH_TIMEZONE: return new ColumnAdapter<>(OffsetDateTime.class, ANSIMapping::toODT); + case Types.LONGVARCHAR: return new ColumnAdapter.Simple<>(String.class, ResultSet::getString); + case Types.DATE: return new ColumnAdapter.Simple<>(Date.class, ResultSet::getDate); + case Types.TIME: return new ColumnAdapter.Simple<>(LocalTime.class, ANSIMapping::toLocalTime); + case Types.TIMESTAMP: return new ColumnAdapter.Simple<>(Instant.class, ANSIMapping::toInstant); + case Types.TIME_WITH_TIMEZONE: return new ColumnAdapter.Simple<>(OffsetTime.class, ANSIMapping::toOffsetTime); + case Types.TIMESTAMP_WITH_TIMEZONE: return new ColumnAdapter.Simple<>(OffsetDateTime.class, ANSIMapping::toODT); case Types.BINARY: case Types.VARBINARY: - case Types.LONGVARBINARY: return new ColumnAdapter<>(byte[].class, ResultSet::getBytes); + case Types.LONGVARBINARY: return new ColumnAdapter.Simple<>(byte[].class, ResultSet::getBytes); case Types.ARRAY: return forceCast(Object[].class); case Types.OTHER: // Database-specific accessed via getObject and setObject. - case Types.JAVA_OBJECT: return new ColumnAdapter<>(Object.class, ResultSet::getObject); + case Types.JAVA_OBJECT: return new ColumnAdapter.Simple<>(Object.class, ResultSet::getObject); default: return null; } } @@ -96,7 +96,7 @@ public class ANSIMapping implements DialectMapping { } private static <T> ColumnAdapter<T> forceCast(final Class<T> targetType) { - return new ColumnAdapter<>(targetType, (r, i) -> forceCast(targetType, r, i)); + return new ColumnAdapter.Simple<>(targetType, (r, i) -> forceCast(targetType, r, i)); } private static <T> T forceCast(final Class<T> targetType, ResultSet source, final Integer columnIndex) throws SQLException { diff --git a/storage/sis-sqlstore/src/main/java/org/apache/sis/internal/sql/feature/Analyzer.java b/storage/sis-sqlstore/src/main/java/org/apache/sis/internal/sql/feature/Analyzer.java index 9878a5e..e58d0c0 100644 --- a/storage/sis-sqlstore/src/main/java/org/apache/sis/internal/sql/feature/Analyzer.java +++ b/storage/sis-sqlstore/src/main/java/org/apache/sis/internal/sql/feature/Analyzer.java @@ -353,13 +353,9 @@ final class Analyzer { for (SQLColumn col : spec.getColumns()) { i++; final ColumnAdapter<?> colAdapter = functions.toJavaType(col); - Class<?> type = colAdapter.javaType; + Class<?> type = colAdapter.getJavaType(); final String colName = col.naming.getColumnName(); final String attrName = col.naming.getAttributeName(); - if (type == null) { - warning(Resources.Keys.UnknownType_1, colName); - type = Object.class; - } final AttributeTypeBuilder<?> attribute = builder .addAttribute(type) diff --git a/storage/sis-sqlstore/src/main/java/org/apache/sis/internal/sql/feature/CRSIdentification.java b/storage/sis-sqlstore/src/main/java/org/apache/sis/internal/sql/feature/CRSIdentification.java new file mode 100644 index 0000000..a329184 --- /dev/null +++ b/storage/sis-sqlstore/src/main/java/org/apache/sis/internal/sql/feature/CRSIdentification.java @@ -0,0 +1,91 @@ +package org.apache.sis.internal.sql.feature; + +import java.sql.Connection; +import java.sql.PreparedStatement; +import java.sql.ResultSet; +import java.sql.SQLException; +import java.text.ParseException; + +import org.opengis.referencing.crs.CoordinateReferenceSystem; +import org.opengis.util.FactoryException; + +import org.apache.sis.io.wkt.Convention; +import org.apache.sis.io.wkt.WKTFormat; +import org.apache.sis.referencing.CRS; +import org.apache.sis.util.collection.BackingStoreException; +import org.apache.sis.util.collection.Cache; + +final class CRSIdentification implements SQLCloseable { + + final PreparedStatement wktFromSrid; + private final WKTFormat wktReader; + + private final Cache<Integer, CoordinateReferenceSystem> sessionCache; + + CRSIdentification(final Connection c, final Cache<Integer, CoordinateReferenceSystem> sessionCache) throws SQLException { + wktFromSrid = c.prepareStatement("SELECT auth_name, auth_srid, srtext FROM spatial_ref_sys WHERE srid=?"); + wktReader = new WKTFormat(null, null); + wktReader.setConvention(Convention.WKT1_COMMON_UNITS); + this.sessionCache = sessionCache; + } + + /** + * Try to fetch spatial system relative to given SRID. + * + * @param pgSrid The SRID as defined by the database (see + * <a href="http://postgis.refractions.net/documentation/manual-1.3/ch04.html#id2571265">Official PostGIS documentation</a> for details). + * @return If input was 0 or less, a null value is returned. Otherwise, return the CRS decoded from database WKT. + * @throws IllegalArgumentException If given SRID is above 0, but no coordinate system definition can be found for + * it in the database, or found object is not a database, or no WKT is available, but authority code is not + * supported by SIS. + * @throws IllegalStateException If more than one match is found for given SRID. + */ + CoordinateReferenceSystem fetchCrs(int pgSrid) throws IllegalArgumentException { + if (pgSrid <= 0) return null; + + return sessionCache.computeIfAbsent(pgSrid, this::fetch); + } + + private CoordinateReferenceSystem fetch(final int pgSrid) { + try { + wktFromSrid.setInt(1, pgSrid); + try (ResultSet result = wktFromSrid.executeQuery()) { + if (!result.next()) throw new IllegalArgumentException("No entry found for SRID " + pgSrid); + final String authority = result.getString(1); + final int authorityCode = result.getInt(2); + final String pgWkt = result.getString(3); + + // That should never happen, but if it does, there's a serious problem ! + if (result.next()) + throw new IllegalStateException("More than one definition available for SRID " + pgSrid); + + if (pgWkt == null || pgWkt.trim().isEmpty()) { + try { + return CRS.getAuthorityFactory(authority).createCoordinateReferenceSystem(Integer.toString(authorityCode)); + } catch (FactoryException e) { + throw new IllegalArgumentException(String.format( + "Input SRID (%d) does not provide any WKT, but its authority code (%s:%d) is not supported by SIS", + pgSrid, authority, authorityCode + ), e); + } + } + final Object parsedWkt = wktReader.parseObject(pgWkt); + if (parsedWkt instanceof CoordinateReferenceSystem) { + return (CoordinateReferenceSystem) parsedWkt; + } else throw new ParseException(String.format( + "WKT of given SRID cannot be interprated as a CRS.%nInput SRID: %d%nOutput type: %s", + pgSrid, parsedWkt.getClass().getCanonicalName() + ), 0); + } finally { + wktFromSrid.clearParameters(); + } + } catch (SQLException | ParseException e) { + throw new BackingStoreException(e); + } + } + + @Override + public void close() throws SQLException { + wktFromSrid.close(); + } +} diff --git a/storage/sis-sqlstore/src/main/java/org/apache/sis/internal/sql/feature/ColumnAdapter.java b/storage/sis-sqlstore/src/main/java/org/apache/sis/internal/sql/feature/ColumnAdapter.java index da13ae4..4d5602d 100644 --- a/storage/sis-sqlstore/src/main/java/org/apache/sis/internal/sql/feature/ColumnAdapter.java +++ b/storage/sis-sqlstore/src/main/java/org/apache/sis/internal/sql/feature/ColumnAdapter.java @@ -1,9 +1,8 @@ package org.apache.sis.internal.sql.feature; +import java.sql.Connection; import java.sql.ResultSet; -import java.sql.SQLException; import java.util.Optional; -import java.util.function.Function; import org.opengis.referencing.crs.CoordinateReferenceSystem; @@ -15,39 +14,51 @@ import static org.apache.sis.util.ArgumentChecks.ensureNonNull; * * @param <T> Type of object decoded from cell. */ -public class ColumnAdapter<T> implements SQLBiFunction<ResultSet, Integer, T> { - final Class<T> javaType; - private final SQLBiFunction<ResultSet, Integer, T> fetchValue; - private final CoordinateReferenceSystem crs; +public interface ColumnAdapter<T> { - public ColumnAdapter(Class<T> javaType, SQLBiFunction<ResultSet, Integer, T> fetchValue) { - this(javaType, fetchValue, null); - } - - public ColumnAdapter(Class<T> javaType, SQLBiFunction<ResultSet, Integer, T> fetchValue, final CoordinateReferenceSystem crs) { - ensureNonNull("Result java type", javaType); - ensureNonNull("Function for value retrieval", fetchValue); - this.javaType = javaType; - this.fetchValue = fetchValue; - this.crs = crs; - } - - @Override - public T apply(ResultSet resultSet, Integer integer) throws SQLException { - return fetchValue.apply(resultSet, integer); - } - - @Override - public <V> SQLBiFunction<ResultSet, Integer, V> andThen(Function<? super T, ? extends V> after) { - return fetchValue.andThen(after); - } + /** + * Gives a function ready to extract and interpret values of a result set for the column it has been designed for. + * + * @param target A read-only connection that can be used to load metadata and stuff related to target column. + * @return A function which will interpret values for the column this component has been created for. User will have + * to give it a well-positioned cursor (result set on the wanted line) as the index of the cell it must read on it. + */ + SQLBiFunction<ResultSet, Integer, T> prepare(final Connection target); /** * Note : This method could be used not only for geometric fields, but also on numeric ones representing 1D systems. * * @return Potentially an empty shell, or the default coordinate reference system for this column values. */ - public Optional<CoordinateReferenceSystem> getCrs() { - return Optional.ofNullable(crs); + default Optional<CoordinateReferenceSystem> getCrs() { + return Optional.empty(); + } + + /** + * + * @return The (possibly parent) type of objects read by this mapper. Note that it MUST NOT return null values. + */ + Class<T> getJavaType(); + + final class Simple<T> implements ColumnAdapter<T> { + private final Class<T> javaType; + private final SQLBiFunction<ResultSet, Integer, T> fetchValue; + + Simple(final Class<T> targetType, SQLBiFunction<ResultSet, Integer, T> fetchValue) { + ensureNonNull("Target type", targetType); + ensureNonNull("Function for value retrieval", fetchValue); + javaType = targetType; + this.fetchValue = fetchValue; + } + + @Override + public SQLBiFunction<ResultSet, Integer, T> prepare(Connection target) { + return fetchValue; + } + + @Override + public Class<T> getJavaType() { + return javaType; + } } } diff --git a/storage/sis-sqlstore/src/main/java/org/apache/sis/internal/sql/feature/EWKBReader.java b/storage/sis-sqlstore/src/main/java/org/apache/sis/internal/sql/feature/EWKBReader.java index 926dbc5..8f4bc1f 100644 --- a/storage/sis-sqlstore/src/main/java/org/apache/sis/internal/sql/feature/EWKBReader.java +++ b/storage/sis-sqlstore/src/main/java/org/apache/sis/internal/sql/feature/EWKBReader.java @@ -3,6 +3,10 @@ package org.apache.sis.internal.sql.feature; import java.nio.ByteBuffer; import java.nio.ByteOrder; import java.util.Arrays; +import java.util.Iterator; +import java.util.function.Function; +import java.util.function.IntFunction; +import java.util.stream.IntStream; import org.opengis.referencing.crs.CoordinateReferenceSystem; @@ -20,6 +24,10 @@ import static org.apache.sis.util.ArgumentChecks.ensureNonEmpty; * This format is the natural form returned by a query selection a geometry field * whithout using any ST_X method. * + * TODO: This format is almost equivalent to standard WKB, except that it includes SRID information. If needed, adding + * minor tweaks and flags should suffice to make it compatible with both formats. See {@link #MASK_SRID } usage for + * details. + * * @author Johann Sorel (Geomatys) * @author Alexis Manin (Geomatys) */ @@ -39,9 +47,9 @@ class EWKBReader { private static final int MULTIPOLYGON = 6; private static final int GEOMETRYCOLLECTION = 7; - private final Geometries factory; + final Geometries factory; - private CoordinateReferenceSystem crs; + private final Function<ByteBuffer, ?> decoder; EWKBReader() { this((GeometryLibrary) null); @@ -52,12 +60,38 @@ class EWKBReader { } EWKBReader(Geometries geometryFactory) { - this.factory = geometryFactory; + this(geometryFactory, bytes -> new Reader(geometryFactory, bytes).read()); + } + + private EWKBReader(final Geometries factory, Function<ByteBuffer, ?> decoder) { + this.factory = factory; + this.decoder = decoder; } - public EWKBReader setCrs(CoordinateReferenceSystem defaultCrs) { - this.crs = defaultCrs; - return this; + /** + * + * @param defaultCrs The coordinate reference system to associate to each geometry. + * @return A NEW instance of reader, with a fixed CRS resolution, applying constant value to all read geometries. + */ + public EWKBReader forCrs(CoordinateReferenceSystem defaultCrs) { + if (defaultCrs == null) return new EWKBReader(factory); + else return new EWKBReader(factory, bytes -> { + final Object geom = new Reader(factory, bytes).read(); + if (geom != null) factory.setCRS(geom, defaultCrs); + return geom; + }); + } + + public EWKBReader withResolver(final IntFunction<CoordinateReferenceSystem> fromPgSridToCrs) { + return new EWKBReader(factory, bytes -> { + final Reader reader = new Reader(factory, bytes); + final Object geom = reader.read(); + if (reader.srid > 0) { + final CoordinateReferenceSystem crs = fromPgSridToCrs.apply(reader.srid); + if (crs != null) factory.setCRS(geom, crs); + } + return geom; + }); } Object readHexa(final String hexaEWkb) { @@ -65,16 +99,22 @@ class EWKBReader { } Object read(final byte[] eWkb) { - return new Reader(ByteBuffer.wrap(eWkb)).read(); + return decoder.apply(ByteBuffer.wrap(eWkb)); + } + + Object read(final ByteBuffer eWkb) { + return decoder.apply(eWkb); } - private final class Reader { + private static final class Reader { + final Geometries factory; final ByteBuffer buffer; final int geomType; final int dimension; final int srid; - private Reader(ByteBuffer buffer) { + private Reader(Geometries factory, ByteBuffer buffer) { + this.factory = factory; final byte endianess = buffer.get(); if (isLittleEndian(endianess)) { this.buffer = buffer.order(ByteOrder.LITTLE_ENDIAN); @@ -89,17 +129,6 @@ class EWKBReader { } Object read() { - final Object geom = decodeGeometry(); - - // TODO: set CRS - if (crs != null) { - } else if (srid > 0) { - } - - return geom; - } - - Object decodeGeometry() { switch (geomType) { case POINT: return readPoint(); case LINESTRING: return readLineString(); @@ -114,11 +143,28 @@ class EWKBReader { } private Object readMultiLineString() { - throw new UnsupportedOperationException(); + final Iterator<Object> it = IntStream.range(0, readCount()) + .mapToObj(i -> new Reader(factory, buffer).read()) + .iterator(); + if (it.hasNext()) { + final Object first = it.next(); + if (it.hasNext()) return factory.tryMergePolylines(first, it); + else return first; + } + throw new IllegalStateException("No geometry decoded"); } private Object readMultiPolygon() { - throw new UnsupportedOperationException(); + final int count = readCount(); + final Object[] polygons = new Object[count]; + for (int i = 0 ; i < count ; i++) { + polygons[i] = new Reader(factory, buffer).read(); + } + + return factory.createMultiPolygon( + IntStream.range(0, readCount()) + .mapToObj(i -> new Reader(factory, buffer).read()) + ); } private Object readCollection() { @@ -143,7 +189,7 @@ class EWKBReader { final double[] nans = new double[dimension]; Arrays.fill(nans, Double.NaN); final Vector separator = Vector.create(nans); - final Vector[] allShells = new Vector[nbRings + nbRings -1]; // include ring separators + final Vector[] allShells = new Vector[Math.addExact(nbRings, nbRings -1)]; // include ring separators allShells[0] = outerShell; for (int i = 1 ; i < nbRings ;) { allShells[i++] = separator; @@ -158,15 +204,33 @@ class EWKBReader { } private double[] readCoordinateSequence() { - return readCoordinateSequence(buffer.getInt()); + return readCoordinateSequence(readCount()); } private double[] readCoordinateSequence(int nbPts) { - final double[] brutPoint = new double[dimension*nbPts]; + final double[] brutPoint = new double[Math.multiplyExact(dimension, nbPts)]; for (int i = 0 ; i < brutPoint.length ; i++) brutPoint[i] = buffer.getDouble(); return brutPoint; } + /** + * @implNote WKB specification declares lengths as uint32. However, the way to handle it in Java would be to + * return a long value, which is not possible anyway, because current implementation needs to put all geometry + * data in memory, in an array whose number of elements is limited to {@link Integer#MAX_VALUE}. So for now, + * we will just ensure that read value is compatible with our limitations. + * + * For details, see <a href="https://www.ibm.com/support/knowledgecenter/SSGU8G_14.1.0/com.ibm.spatial.doc/ids_spat_285.htm">IBM</a> + * or <a href="https://trac.osgeo.org/postgis/browser/trunk/doc/ZMSgeoms.txt">OSGEO</a> documentation. + * + * @return read count. + * @throws IllegalStateException If read count is 0 or above {@link Integer#MAX_VALUE}. + */ + private int readCount() { + final int count = buffer.getInt(); + if (count == 0) throw new IllegalStateException("Read a 0 point/geometry count in WKB."); + else if (count < 0) throw new IllegalStateException("Read a count overflowing Java integer max value: "+Integer.toUnsignedLong(count)); + return count; + } } private static boolean isLittleEndian(byte endianess) { diff --git a/storage/sis-sqlstore/src/main/java/org/apache/sis/internal/sql/feature/FeatureAdapter.java b/storage/sis-sqlstore/src/main/java/org/apache/sis/internal/sql/feature/FeatureAdapter.java index 88c7546..d9a2752 100644 --- a/storage/sis-sqlstore/src/main/java/org/apache/sis/internal/sql/feature/FeatureAdapter.java +++ b/storage/sis-sqlstore/src/main/java/org/apache/sis/internal/sql/feature/FeatureAdapter.java @@ -6,6 +6,7 @@ import java.sql.SQLException; import java.util.ArrayList; import java.util.Collections; import java.util.List; +import java.util.stream.Collectors; import org.opengis.feature.Feature; import org.opengis.feature.FeatureType; @@ -25,40 +26,57 @@ class FeatureAdapter { this.attributeMappers = Collections.unmodifiableList(new ArrayList<>(attributeMappers)); } - Feature read(final ResultSet cursor, final Connection origin) throws SQLException { - final Feature result = readAttributes(cursor); - addImports(result, cursor); - addExports(result); - return result; + ResultSetAdapter prepare(final Connection target) { + final List<ReadyMapper> rtu = attributeMappers.stream() + .map(mapper -> mapper.prepare(target)) + .collect(Collectors.toList()); + return new ResultSetAdapter(rtu); } - private void addImports(final Feature target, final ResultSet cursor) { - // TODO: see Features class - } + final class ResultSetAdapter { + final List<ReadyMapper> mappers; - private void addExports(final Feature target) { - // TODO: see Features class - } + ResultSetAdapter(List<ReadyMapper> mappers) { + this.mappers = mappers; + } - private Feature readAttributes(final ResultSet cursor) throws SQLException { - final Feature result = type.newInstance(); - for (PropertyMapper mapper : attributeMappers) mapper.read(cursor, result); - return result; - } + Feature read(final ResultSet cursor) throws SQLException { + final Feature result = readAttributes(cursor); + addImports(result, cursor); + addExports(result); + return result; + } - List<Feature> prefetch(final int size, final ResultSet cursor, final Connection origin) throws SQLException { - // TODO: optimize by resolving import associations by batch import fetching. - final ArrayList<Feature> features = new ArrayList<>(size); - for (int i = 0 ; i < size && cursor.next() ; i++) { - features.add(read(cursor, origin)); + private Feature readAttributes(final ResultSet cursor) throws SQLException { + final Feature result = type.newInstance(); + for (ReadyMapper mapper : mappers) mapper.read(cursor, result); + return result; } - return features; + //final SQLBiFunction<ResultSet, Integer, ?>[] adapters; + List<Feature> prefetch(final int size, final ResultSet cursor) throws SQLException { + // TODO: optimize by resolving import associations by batch import fetching. + final ArrayList<Feature> features = new ArrayList<>(size); + for (int i = 0 ; i < size && cursor.next() ; i++) { + features.add(read(cursor)); + } + + return features; + } + + private void addImports(final Feature target, final ResultSet cursor) { + // TODO: see Features class + } + + private void addExports(final Feature target) { + // TODO: see Features class + } } static final class PropertyMapper { // TODO: by using a indexed implementation of Feature, we could avoid the name mapping. However, a JMH benchmark - // would be required in order to be sure it's impacting performance positively. + // would be required in order to be sure it's impacting performance positively. also, features are sparse by + // nature, and an indexed implementation could (to verify, still) be bad on memory footprint. final String propertyName; final int columnIndex; final ColumnAdapter fetchValue; @@ -69,9 +87,23 @@ class FeatureAdapter { this.fetchValue = fetchValue; } + ReadyMapper prepare(final Connection target) { + return new ReadyMapper(this, fetchValue.prepare(target)); + } + } + + private static class ReadyMapper { + final SQLBiFunction<ResultSet, Integer, ?> reader; + final PropertyMapper parent; + + public ReadyMapper(PropertyMapper parent, SQLBiFunction<ResultSet, Integer, ?> reader) { + this.reader = reader; + this.parent = parent; + } + private void read(ResultSet cursor, Feature target) throws SQLException { - final Object value = fetchValue.apply(cursor, columnIndex); - if (value != null) target.setPropertyValue(propertyName, value); + final Object value = reader.apply(cursor, parent.columnIndex); + if (value != null) target.setPropertyValue(parent.propertyName, value); } } } diff --git a/storage/sis-sqlstore/src/main/java/org/apache/sis/internal/sql/feature/Features.java b/storage/sis-sqlstore/src/main/java/org/apache/sis/internal/sql/feature/Features.java index dd97df9..1bbc3f0 100644 --- a/storage/sis-sqlstore/src/main/java/org/apache/sis/internal/sql/feature/Features.java +++ b/storage/sis-sqlstore/src/main/java/org/apache/sis/internal/sql/feature/Features.java @@ -151,7 +151,7 @@ final class Features implements Spliterator<Feature> { */ private final long estimatedSize; - private final FeatureAdapter adapter; + private final FeatureAdapter.ResultSetAdapter adapter; /** * Creates a new iterator over the feature instances. @@ -182,7 +182,7 @@ final class Features implements Spliterator<Feature> { attributeNames[i++] = column.getAttributeName(); } this.featureType = table.featureType; - this.adapter = table.adapter; + this.adapter = table.adapter.prepare(connection); final DatabaseMetaData metadata = connection.getMetaData(); estimatedSize = following.isEmpty() ? table.countRows(metadata, true) : 0; /* @@ -418,7 +418,7 @@ final class Features implements Spliterator<Feature> { private boolean fetch(final Consumer<? super Feature> action, final boolean all) throws SQLException { while (result.next()) { // TODO: give connection to adapter. - final Feature feature = adapter.read(result, null); + final Feature feature = adapter.read(result); for (int i=0; i < dependencies.length; i++) { final Features dependency = dependencies[i]; final int[] columnIndices = foreignerKeyIndices[i]; diff --git a/storage/sis-sqlstore/src/main/java/org/apache/sis/internal/sql/feature/GeometryIdentification.java b/storage/sis-sqlstore/src/main/java/org/apache/sis/internal/sql/feature/GeometryIdentification.java index a10932c..bb5c67e 100644 --- a/storage/sis-sqlstore/src/main/java/org/apache/sis/internal/sql/feature/GeometryIdentification.java +++ b/storage/sis-sqlstore/src/main/java/org/apache/sis/internal/sql/feature/GeometryIdentification.java @@ -12,11 +12,8 @@ import java.util.Optional; import java.util.Set; import org.opengis.referencing.crs.CoordinateReferenceSystem; -import org.opengis.util.FactoryException; -import org.apache.sis.io.wkt.Convention; -import org.apache.sis.io.wkt.WKTFormat; -import org.apache.sis.referencing.CRS; +import org.apache.sis.util.collection.Cache; import static org.apache.sis.util.ArgumentChecks.ensureNonEmpty; @@ -27,7 +24,6 @@ import static org.apache.sis.util.ArgumentChecks.ensureNonEmpty; */ class GeometryIdentification implements SQLCloseable { - private final Connection c; final PreparedStatement identifySchemaQuery; /** * A statement serving two purposes: @@ -39,9 +35,7 @@ class GeometryIdentification implements SQLCloseable { * @implNote The statement definition is able to serve both purposes by changing geometry column filter to no-op. */ final PreparedStatement columnQuery; - final PreparedStatement wktFromSrid; - - private final WKTFormat wktReader; + final CRSIdentification crsIdent; /** * Describes if geometry column registry include a column for geometry types, according that one can apparently omit @@ -50,11 +44,11 @@ class GeometryIdentification implements SQLCloseable { */ final boolean typeIncluded; - public GeometryIdentification(Connection c) throws SQLException { - this(c, "geometry_columns", "f_geometry_column", "geometry_type"); + public GeometryIdentification(Connection c, Cache<Integer, CoordinateReferenceSystem> sessionCache) throws SQLException { + this(c, "geometry_columns", "f_geometry_column", "geometry_type", sessionCache); } - public GeometryIdentification(Connection c, String identificationTable, String geometryColumnName, String typeColumnName) throws SQLException { - this.c = c; + + public GeometryIdentification(Connection c, String identificationTable, String geometryColumnName, String typeColumnName, Cache<Integer, CoordinateReferenceSystem> sessionCache) throws SQLException { typeIncluded = typeColumnName != null && !(typeColumnName=typeColumnName.trim()).isEmpty(); identifySchemaQuery = c.prepareStatement("SELECT DISTINCT(f_table_schema) FROM "+identificationTable+" WHERE f_table_name = ?"); columnQuery = c.prepareStatement( @@ -64,9 +58,7 @@ class GeometryIdentification implements SQLCloseable { "AND f_table_name = ? " + "AND "+geometryColumnName+" LIKE ?" ); - wktFromSrid = c.prepareStatement("SELECT auth_name, auth_srid, srtext FROM spatial_ref_sys WHERE srid=?"); - wktReader = new WKTFormat(null, null); - wktReader.setConvention(Convention.WKT1_COMMON_UNITS); + crsIdent = new CRSIdentification(c, sessionCache); } Set<GeometryColumn> fetchGeometricColumns(String schema, final String table) throws SQLException, ParseException { @@ -120,61 +112,16 @@ class GeometryIdentification implements SQLCloseable { // Note: we make a query per entry, which could impact performance. However, 99% of defined tables // will have only one geometry column. Moreover, even with more than one, with prepared statement, the // performance impact should stay low. - final CoordinateReferenceSystem crs = fetchCrs(pgSrid); + final CoordinateReferenceSystem crs = crsIdent.fetchCrs(pgSrid); return new GeometryColumn(name, dimension, pgSrid, type, crs); } - /** - * Try to fetch spatial system relative to given SRID. - * @param pgSrid The SRID as defined by the database (see - * <a href="http://postgis.refractions.net/documentation/manual-1.3/ch04.html#id2571265">Official PostGIS documentation</a> for details). - * @return If input was 0 or less, a null value is returned. Otherwise, return the CRS decoded from database WKT. - * @throws IllegalArgumentException If given SRID is above 0, but no coordinate system definition can be found for - * it in the database, or found object is not a database, or no WKT is available, but authority code is not - * supported by SIS. - * @throws IllegalStateException If more than one match is found for given SRID. - */ - private CoordinateReferenceSystem fetchCrs(int pgSrid) throws SQLException, IllegalArgumentException, ParseException { - if (pgSrid <= 0) return null; - - wktFromSrid.setInt(1, pgSrid); - try (ResultSet result = wktFromSrid.executeQuery()) { - if (!result.next()) throw new IllegalArgumentException("No entry found for SRID "+pgSrid); - final String authority = result.getString(1); - final int authorityCode = result.getInt(2); - final String pgWkt = result.getString(3); - - // That should never happen, but if it does, there's a serious problem ! - if (result.next()) throw new IllegalStateException("More than one definition available for SRID "+pgSrid); - - if (pgWkt == null || pgWkt.trim().isEmpty()) { - try { - return CRS.getAuthorityFactory(authority).createCoordinateReferenceSystem(Integer.toString(authorityCode)); - } catch (FactoryException e) { - throw new IllegalArgumentException(String.format( - "Input SRID (%d) does not provide any WKT, but its authority code (%s:%d) is not supported by SIS", - pgSrid, authority, authorityCode - ), e); - } - } - final Object parsedWkt = wktReader.parseObject(pgWkt); - if (parsedWkt instanceof CoordinateReferenceSystem) { - return (CoordinateReferenceSystem) parsedWkt; - } else throw new ParseException(String.format( - "WKT of given SRID cannot be interprated as a CRS.%nInput SRID: %d%nOutput type: %s", - pgSrid, parsedWkt.getClass().getCanonicalName() - ), 0); - } finally { - wktFromSrid.clearParameters(); - } - } - @Override public void close() throws SQLException { try ( SQLCloseable c1 = columnQuery::close; SQLCloseable c2 = identifySchemaQuery::close; - SQLCloseable c3 = wktFromSrid::close; + SQLCloseable c3 = crsIdent ) {} } diff --git a/storage/sis-sqlstore/src/main/java/org/apache/sis/internal/sql/feature/PostGISMapping.java b/storage/sis-sqlstore/src/main/java/org/apache/sis/internal/sql/feature/PostGISMapping.java index 84144c8..eb3915a 100644 --- a/storage/sis-sqlstore/src/main/java/org/apache/sis/internal/sql/feature/PostGISMapping.java +++ b/storage/sis-sqlstore/src/main/java/org/apache/sis/internal/sql/feature/PostGISMapping.java @@ -3,15 +3,20 @@ package org.apache.sis.internal.sql.feature; import java.sql.Connection; import java.sql.ResultSet; import java.sql.SQLException; +import java.sql.Statement; import java.sql.Types; import java.text.ParseException; import java.util.Optional; +import java.util.logging.Level; +import java.util.logging.Logger; import org.opengis.referencing.crs.CoordinateReferenceSystem; import org.apache.sis.internal.feature.Geometries; import org.apache.sis.internal.metadata.sql.Dialect; import org.apache.sis.util.collection.BackingStoreException; +import org.apache.sis.util.collection.Cache; +import org.apache.sis.util.logging.Logging; public final class PostGISMapping implements DialectMapping { @@ -21,13 +26,23 @@ public final class PostGISMapping implements DialectMapping { final Connection connection; - final Geometries<?> library; + final Geometries library; + + /** + * A cache valid ONLY FOR A DATASOURCE. IT'S IMPORTANT ! Why ? Because : + * <ul> + * <li>CRS definition could differ between databases (PostGIS version, user alterations, etc.)</li> + * <li>Avoid inter-database locking</li> + * </ul> + */ + final Cache<Integer, CoordinateReferenceSystem> sessionCache; private PostGISMapping(final PostGISMapping.Spi spi, Connection c) throws SQLException { connection = c; this.spi = spi; - this.identifyGeometries = new GeometryIdentification(c, "geometry_columns", "f_geometry_column", "type"); - this.identifyGeographies = new GeometryIdentification(c, "geography_columns", "f_geography_column", "type"); + sessionCache = new Cache<>(7, 0, true); + this.identifyGeometries = new GeometryIdentification(c, "geometry_columns", "f_geometry_column", "type", sessionCache); + this.identifyGeographies = new GeometryIdentification(c, "geography_columns", "f_geography_column", "type", sessionCache); this.library = Geometries.implementation(null); } @@ -65,16 +80,14 @@ public final class PostGISMapping implements DialectMapping { } String geometryType = geomDef == null ? null : geomDef.type; final Class geomClass = getGeometricClass(geometryType); - SQLBiFunction<ResultSet, Integer, Object> geometryDecoder; - // TODO: activate optimisation : WKB is lighter, but we need to modify user query, and to know CRS in advance. -// if (geomDef.crs == null) { - geometryDecoder = new HexEWKBReader(); -// } else { -// // For optimisation purpose, we'll return directly a WKB reader if CRS is known in advance. -// geometryDecoder = new WKBReader(geomDef.crs); -// } - - return new ColumnAdapter<>(geomClass, geometryDecoder, geomDef == null ? null : geomDef.crs); + + if (geomDef == null || geomDef.crs == null) { + return new HexEWKBDynamicCrs(geomClass); + } else { + // TODO: activate optimisation : WKB is lighter, but we need to modify user query, and to know CRS in advance. + //geometryDecoder = new WKBReader(geomDef.crs); + return new HexEWKBFixedCrs(geomClass, geomDef.crs); + } } private Class getGeometricClass(String geometryType) { @@ -109,20 +122,54 @@ public final class PostGISMapping implements DialectMapping { @Override public Optional<DialectMapping> create(Connection c) throws SQLException { + try { + checkPostGISVersion(c); + } catch (SQLException e) { + final Logger logger = Logging.getLogger("org.apache.sis.internal.sql"); + logger.warning("No compatible PostGIS version found. Binding deactivated. See debug logs for more information"); + logger.log(Level.FINE, "Cannot determine PostGIS version", e); + return Optional.empty(); + } return Optional.of(new PostGISMapping(this, c)); } + private void checkPostGISVersion(final Connection c) throws SQLException { + try ( + Statement st = c.createStatement(); + ResultSet result = st.executeQuery("SELECT PostGIS_version();"); + ) { + result.next(); + final String pgisVersion = result.getString(1); + if (!pgisVersion.startsWith("2.")) throw new SQLException("Incompatible PostGIS version. Only 2.x is supported for now, but database declares: "); + } + } + @Override public Dialect getDialect() { return Dialect.POSTGRESQL; } } - private final class WKBReader implements SQLBiFunction<ResultSet, Integer, Object> { + private abstract class Reader implements ColumnAdapter { + + final Class geomClass; + + public Reader(Class geomClass) { + this.geomClass = geomClass; + } + + @Override + public Class getJavaType() { + return geomClass; + } + } + + private final class WKBReader extends Reader implements SQLBiFunction<ResultSet, Integer, Object> { final CoordinateReferenceSystem crsToApply; - private WKBReader(CoordinateReferenceSystem crsToApply) { + private WKBReader(Class geomClass, CoordinateReferenceSystem crsToApply) { + super(geomClass); this.crsToApply = crsToApply; } @@ -131,24 +178,72 @@ public final class PostGISMapping implements DialectMapping { final byte[] bytes = resultSet.getBytes(integer); if (bytes == null) return null; final Object value = library.parseWKB(bytes); - if (value != null) { - // TODO: set CRS + if (value != null && crsToApply != null) { + library.setCRS(value, crsToApply); } return value; } + + @Override + public SQLBiFunction prepare(Connection target) { + return this; + } + + @Override + public Optional<CoordinateReferenceSystem> getCrs() { + return Optional.ofNullable(crsToApply); + } } - private final class HexEWKBReader implements SQLBiFunction<ResultSet, Integer, Object> { + private final class HexEWKBFixedCrs extends Reader { + final CoordinateReferenceSystem crsToApply; - final EWKBReader reader; + public HexEWKBFixedCrs(Class geomClass, CoordinateReferenceSystem crsToApply) { + super(geomClass); + this.crsToApply = crsToApply; + } - private HexEWKBReader() { - this(null); + @Override + public SQLBiFunction prepare(Connection target) { + return new HexEWKBReader(new EWKBReader(library).forCrs(crsToApply)); } - private HexEWKBReader(CoordinateReferenceSystem crsToApply) { - reader = new EWKBReader(library).setCrs(crsToApply); + @Override + public Optional<CoordinateReferenceSystem> getCrs() { + return Optional.ofNullable(crsToApply); + } + } + + private final class HexEWKBDynamicCrs extends Reader { + + public HexEWKBDynamicCrs(Class geomClass) { + super(geomClass); + } + + @Override + public SQLBiFunction prepare(Connection target) { + // TODO: this component is not properly closed. As connection closing should also close this component + // statement, it should be Ok.However, a proper management would be better. + final CRSIdentification crsIdent; + try { + crsIdent = new CRSIdentification(target, sessionCache); + } catch (SQLException e) { + throw new BackingStoreException(e); + } + return new HexEWKBReader( + new EWKBReader(library) + .withResolver(crsIdent::fetchCrs) + ); + } + } + + private static final class HexEWKBReader implements SQLBiFunction<ResultSet, Integer, Object> { + + final EWKBReader reader; + + private HexEWKBReader(EWKBReader reader) { + this.reader = reader; } @Override diff --git a/storage/sis-sqlstore/src/main/java/org/apache/sis/internal/sql/feature/QueryFeatureSet.java b/storage/sis-sqlstore/src/main/java/org/apache/sis/internal/sql/feature/QueryFeatureSet.java index 7b94f2f..af9bde8 100644 --- a/storage/sis-sqlstore/src/main/java/org/apache/sis/internal/sql/feature/QueryFeatureSet.java +++ b/storage/sis-sqlstore/src/main/java/org/apache/sis/internal/sql/feature/QueryFeatureSet.java @@ -351,11 +351,11 @@ public class QueryFeatureSet extends AbstractFeatureSet { private abstract class QuerySpliterator implements java.util.Spliterator<Feature> { final ResultSet result; - final Connection origin; + final FeatureAdapter.ResultSetAdapter adapter; private QuerySpliterator(ResultSet result, Connection origin) { this.result = result; - this.origin = origin; + this.adapter = QueryFeatureSet.this.adapter.prepare(origin); } @Override @@ -380,7 +380,7 @@ public class QueryFeatureSet extends AbstractFeatureSet { public boolean tryAdvance(Consumer<? super Feature> action) { try { if (result.next()) { - final Feature f = adapter.read(result, origin); + final Feature f = adapter.read(result); action.accept(f); return true; } else return false; @@ -405,7 +405,7 @@ public class QueryFeatureSet extends AbstractFeatureSet { * there's not much improvement regarding to naive streaming approach. IMHO, two improvements would really impact * performance positively if done: * <ul> - * <li>Optimisation of batch loading through {@link FeatureAdapter#prefetch(int, ResultSet, Connection)}</li> + * <li>Optimisation of batch loading through {@link FeatureAdapter.ResultSetAdapter#prefetch(int, ResultSet)}</li> * <li>Better splitting balance, as stated by {@link Spliterator#trySplit()}</li> * </ul> */ @@ -465,7 +465,7 @@ public class QueryFeatureSet extends AbstractFeatureSet { if (chunk == null || idx >= chunk.size()) { idx = 0; try { - chunk = adapter.prefetch(fetchSize, result, origin); + chunk = adapter.prefetch(fetchSize, result); } catch (SQLException e) { throw new BackingStoreException(e); }
