This is an automated email from the ASF dual-hosted git repository. desruisseaux pushed a commit to branch geoapi-4.0 in repository https://gitbox.apache.org/repos/asf/sis.git
commit a6b1bfaeca683f0da95b83e88f2452a5efe9381a Author: Martin Desruisseaux <martin.desruisse...@geomatys.com> AuthorDate: Fri Aug 16 13:32:21 2024 +0200 Add `findCRS(int)` and `findSRID(CoordinateReferenceSystem)` public methods in `SQLStore`. This is in replacemen for equivalent methods in the geopackage module. --- .../main/org/apache/sis/storage/sql/SQLStore.java | 230 ++++++++++++++++--- .../apache/sis/storage/sql/feature/Database.java | 145 ++++++------ .../sis/storage/sql/feature/InfoStatements.java | 244 +++++++++++++++------ .../sis/storage/sql/feature/SchemaModifier.java | 2 +- .../sis/storage/sql/feature/ValueGetter.java | 6 +- .../sis/storage/sql/postgis/ExtentEstimator.java | 2 +- .../apache/sis/storage/sql/postgis/Postgres.java | 11 +- .../storage/sql/feature/InfoStatementsTest.java | 2 +- 8 files changed, 475 insertions(+), 167 deletions(-) diff --git a/endorsed/src/org.apache.sis.storage.sql/main/org/apache/sis/storage/sql/SQLStore.java b/endorsed/src/org.apache.sis.storage.sql/main/org/apache/sis/storage/sql/SQLStore.java index 2dec07e23d..44b7d958c6 100644 --- a/endorsed/src/org.apache.sis.storage.sql/main/org/apache/sis/storage/sql/SQLStore.java +++ b/endorsed/src/org.apache.sis.storage.sql/main/org/apache/sis/storage/sql/SQLStore.java @@ -20,17 +20,23 @@ import java.util.Map; import java.util.LinkedHashMap; import java.util.Optional; import java.util.Collection; +import java.util.concurrent.locks.Lock; +import java.util.concurrent.locks.ReadWriteLock; +import java.util.concurrent.locks.ReentrantReadWriteLock; import javax.sql.DataSource; import java.sql.Connection; import java.lang.reflect.Method; import org.opengis.util.GenericName; import org.opengis.metadata.Metadata; import org.opengis.parameter.ParameterValueGroup; +import org.opengis.referencing.crs.CoordinateReferenceSystem; import org.opengis.metadata.spatial.SpatialRepresentationType; import org.apache.sis.storage.Aggregate; import org.apache.sis.storage.FeatureSet; import org.apache.sis.storage.DataStore; import org.apache.sis.storage.DataStoreException; +import org.apache.sis.storage.DataStoreContentException; +import org.apache.sis.storage.NoSuchDataException; import org.apache.sis.storage.IllegalNameException; import org.apache.sis.storage.StorageConnector; import org.apache.sis.storage.event.StoreEvent; @@ -38,6 +44,7 @@ import org.apache.sis.storage.event.StoreListener; import org.apache.sis.storage.event.WarningEvent; import org.apache.sis.storage.sql.feature.Database; import org.apache.sis.storage.sql.feature.Resources; +import org.apache.sis.storage.sql.feature.InfoStatements; import org.apache.sis.storage.sql.feature.SchemaModifier; import org.apache.sis.storage.base.MetadataBuilder; import org.apache.sis.util.ArgumentChecks; @@ -56,7 +63,7 @@ import org.apache.sis.setup.OptionKey; * * @author Johann Sorel (Geomatys) * @author Martin Desruisseaux (Geomatys) - * @version 1.3 + * @version 1.5 * @since 1.0 */ public class SQLStore extends DataStore implements Aggregate { @@ -73,6 +80,8 @@ public class SQLStore extends DataStore implements Aggregate { /** * The data source to use for obtaining connections to the database. + * + * @see #getDataSource() */ private final DataSource source; @@ -84,8 +93,11 @@ public class SQLStore extends DataStore implements Aggregate { /** * The result of inspecting database schema for deriving {@link org.opengis.feature.FeatureType}s. * Created when first needed. May be discarded and recreated if the store needs a refresh. + * + * @see #model() + * @see #model(Connection) */ - private Database<?> model; + private volatile Database<?> model; /** * Fully qualified names (including catalog and schema) of the tables to include in this store. @@ -120,6 +132,23 @@ public class SQLStore extends DataStore implements Aggregate { */ private final SchemaModifier customizer; + /** + * The lock for read or write operations in the SQL database, or {@code null} if none. + * The read or write lock should be obtained before to get a connection for executing + * a statement, and released after closing the connection. Locking is assumed unneeded + * for obtaining database metadata. + * + * <p>This field should be null if the database manages concurrent transactions by itself. + * It is non-null only as a workaround for databases that do not support concurrency.</p> + * + * <p>This lock is not used for cache integrity. The {@code SQLStore} caches are protected + * by classical synchronized statements.</p> + * + * @see #lock(boolean) + * @see Database#transactionLocks + */ + private final ReadWriteLock transactionLocks; + /** * Creates a new {@code SQLStore} for the given data source and tables, views or queries. * The given {@code connector} shall contain a {@link DataSource} instance. @@ -166,9 +195,18 @@ public class SQLStore extends DataStore implements Aggregate { } this.tableNames = ArraysExt.resize(tableNames, tableCount); this.queries = ArraysExt.resize(queries, queryCount); - if (getClass() == SQLStore.class) { - listeners.useReadOnlyEvents(); - } + transactionLocks = new ReentrantReadWriteLock(); // TODO: make optional. + } + + /** + * The data source to use for obtaining connections to the database. + * This is the data source specified at construction time. + * + * @return the data source to use for obtaining connections to the database. + * @since 1.5 + */ + public final DataSource getDataSource() { + return source; } /** @@ -210,18 +248,22 @@ public class SQLStore extends DataStore implements Aggregate { /** * Returns the database model, analyzing the database schema when first needed. + * This method is thread-safe. */ - private synchronized Database<?> model() throws DataStoreException { - if (model == null) { - try (Connection c = source.getConnection()) { - model = Database.create(this, source, c, geomLibrary, tableNames, queries, customizer, listeners); - } catch (DataStoreException e) { - throw e; - } catch (Exception e) { - throw new DataStoreException(Exceptions.unwrap(e)); + private Database<?> model() throws DataStoreException { + Database<?> current = model; + if (current == null) { + synchronized (this) { + try (Connection c = source.getConnection()) { + current = model(c); + } catch (DataStoreException e) { + throw e; + } catch (Exception e) { + throw new DataStoreException(Exceptions.unwrap(e)); + } } } - return model; + return current; } /** @@ -232,10 +274,13 @@ public class SQLStore extends DataStore implements Aggregate { * @param c connection to the database. */ private Database<?> model(final Connection c) throws Exception { - if (model == null) { - model = Database.create(this, source, c, geomLibrary, tableNames, queries, customizer, listeners); + assert Thread.holdsLock(this); + Database<?> current = model; + if (current == null) { + current = Database.create(this, source, c, geomLibrary, tableNames, queries, customizer, listeners, transactionLocks); + model = current; } - return model; + return current; } /** @@ -248,18 +293,12 @@ public class SQLStore extends DataStore implements Aggregate { @Override public synchronized Metadata getMetadata() throws DataStoreException { if (metadata == null) { - final MetadataBuilder builder = new MetadataBuilder(); + final var builder = new MetadataBuilder(); builder.addSpatialRepresentation(SpatialRepresentationType.TEXT_TABLE); try (Connection c = source.getConnection()) { @SuppressWarnings("LocalVariableHidesMemberVariable") final Database<?> model = model(c); - if (model.hasGeometry()) { - builder.addSpatialRepresentation(SpatialRepresentationType.VECTOR); - } - if (model.hasRaster()) { - builder.addSpatialRepresentation(SpatialRepresentationType.GRID); - } - model.listTables(c.getMetaData(), builder); + model.metadata(c.getMetaData(), builder); } catch (DataStoreException e) { throw e; } catch (Exception e) { @@ -317,6 +356,132 @@ public class SQLStore extends DataStore implements Aggregate { return model().findTable(this, identifier); } + /** + * Returns the coordinate reference system associated to the given identifier. The spatial reference + * system identifiers (<abbr>SRID</abbr>) are the primary keys of the {@code "SPATIAL_REF_SYS"} table + * (the name of that table may vary depending on which spatial schema standard is used). + * Those identifiers are specific to each database and are not necessarily related to EPSG codes. + * They should be considered as opaque identifiers. + * + * <h4>Undefined <abbr>CRS</abbr></h4> + * Some standards such as Geopackage define 0 as "undefined geographic <abbr>CRS</abbr>" and -1 as + * "undefined Cartesian <abbr>CRS</abbr>". This method returns {@code null} for all undefined <abbr>CRS</abbr>, + * regardless their type. No default value is returned because this class cannot guess the datum and units of + * measurement of an undefined <abbr>CRS</abbr>. All <abbr>SRID</abbr> equal or less than zero are considered + * undefined. + * + * <h4>Axis order</h4> + * Some standards such as Geopackage mandate (east, north) axis order. {@code SQLStore} uses the axis order + * as defined in the <abbr>WKT</abbr> descriptions of the {@code "SPATIAL_REF_SYS"} table. No reordering is + * applied. It is data producer's responsibility to provide definitions with the expected axis order. + * + * @param srid a primary key value of the {@code "SPATIAL_REF_SYS"} table. + * @return the <abbr>CRS</abbr> associated to the given <abbr>SRID</abbr>, or {@code null} if the given + * <abbr>SRID</abbr> is a code explicitly associated to an undefined <abbr>CRS</abbr>. + * @throws NoSuchDataException if no <abbr>CRS</abbr> is associated to the given <abbr>SRID</abbr>. + * @throws DataStoreException if the query failed for another reason. + * + * @since 1.5 + */ + public CoordinateReferenceSystem findCRS(final int srid) throws DataStoreException { + if (srid <= 0) { + return null; + } + Database<?> database = model; + CoordinateReferenceSystem crs; + if (database == null || (crs = database.getCachedCRS(srid)) == null) { + final Lock lock = lock(false); + try { + try (Connection c = source.getConnection()) { + if (database == null) { + synchronized (this) { + database = model(c); + } + } + try (InfoStatements info = database.createInfoStatements(c)) { + crs = info.fetchCRS(srid); + } + } catch (DataStoreContentException e) { + throw new NoSuchDataException(e.getMessage(), e.getCause()); + } catch (DataStoreException e) { + throw e; + } catch (Exception e) { + throw new DataStoreException(Exceptions.unwrap(e)); + } + } finally { + if (lock != null) { + lock.unlock(); + } + } + } + return crs; + } + + /** + * Returns the <abbr>SRID</abbr> associated to the given spatial reference system. + * This method is the converse of {@link #findCRS(int)}. + * + * @param crs the CRS for which to find a SRID, or {@code null}. + * @param allowAdd whether to allow the addition of a new row in the {@code "SPATIAL_REF_SYS"} table + * if the given <abbr>CRS</abbr> is not found. + * @return SRID for the given <abbr>CRS</abbr>, or 0 if the given <abbr>CRS</abbr> was null. + * @throws DataStoreException if an <abbr>SQL</abbr> error, parsing error or other error occurred. + * + * @since 1.5 + */ + public int findSRID(final CoordinateReferenceSystem crs, final boolean allowAdd) throws DataStoreException { + if (crs == null) { + return 0; + } + Database<?> database = model; + if (database != null) { + Integer srid = database.getCachedSRID(crs); + if (srid != null) return srid; + } + final int srid; + final Lock lock = lock(allowAdd); + try { + try (Connection c = source.getConnection()) { + if (database == null) { + synchronized (this) { + database = model(c); + } + } + if (!allowAdd && database.dialect.supportsReadOnlyUpdate()) { + c.setReadOnly(true); + } + try (InfoStatements info = database.createInfoStatements(c)) { + srid = info.findSRID(crs); + } + } catch (DataStoreException e) { + throw e; + } catch (Exception e) { + throw new DataStoreException(Exceptions.unwrap(e)); + } + } finally { + if (lock != null) { + lock.unlock(); + } + } + return srid; + } + + /** + * Returns a read or write lock, or {@code null} if none. + * The call to {@link Lock#lock()} will be already performed. + * + * @param write {@code true} for the write lock, or {@code false} for the read lock. + * @return the requested lock, or {@code null} if none. + */ + private Lock lock(final boolean write) { + if (transactionLocks == null) { + return null; + } + final Lock lock = write ? transactionLocks.writeLock() : transactionLocks.readLock(); + lock.lock(); + return lock; + } + /** * Registers a listener to notify when the specified kind of event occurs in this data store. * The current implementation of this data store can emit only {@link WarningEvent}s; @@ -330,6 +495,18 @@ public class SQLStore extends DataStore implements Aggregate { } } + /** + * Clears the cache so that next operations will reload all needed information from the database. + * This method can be invoked when the database content has been modified by a process other than + * the methods in this class. + * + * @since 1.5 + */ + public synchronized void refresh() { + model = null; + metadata = null; + } + /** * Closes this SQL store and releases any underlying resources. * @@ -339,6 +516,7 @@ public class SQLStore extends DataStore implements Aggregate { public synchronized void close() throws DataStoreException { listeners.close(); // Should never fail. // There is no JDBC connection to close here. - model = null; + model = null; + metadata = null; } } diff --git a/endorsed/src/org.apache.sis.storage.sql/main/org/apache/sis/storage/sql/feature/Database.java b/endorsed/src/org.apache.sis.storage.sql/main/org/apache/sis/storage/sql/feature/Database.java index 66010d1a6b..23cf211df8 100644 --- a/endorsed/src/org.apache.sis.storage.sql/main/org/apache/sis/storage/sql/feature/Database.java +++ b/endorsed/src/org.apache.sis.storage.sql/main/org/apache/sis/storage/sql/feature/Database.java @@ -29,7 +29,6 @@ import java.util.Locale; import java.util.Optional; import java.util.logging.LogRecord; import java.util.concurrent.locks.ReadWriteLock; -import java.util.concurrent.locks.ReentrantReadWriteLock; import java.sql.Array; import java.sql.Connection; import java.sql.DatabaseMetaData; @@ -41,6 +40,7 @@ import javax.sql.DataSource; import org.opengis.util.GenericName; import org.opengis.geometry.Envelope; import org.opengis.referencing.crs.CoordinateReferenceSystem; +import org.opengis.metadata.spatial.SpatialRepresentationType; import org.apache.sis.setup.GeometryLibrary; import org.apache.sis.metadata.sql.privy.Syntax; import org.apache.sis.metadata.sql.privy.Dialect; @@ -161,16 +161,12 @@ public class Database<G> extends Syntax { /** * {@code true} if this database contains at least one geometry column. * This field is initialized by {@link #analyze analyze(…)} and shall not be modified after that point. - * - * @see #hasGeometry() */ private boolean hasGeometry; /** * {@code true} if this database contains at least one raster column. * This field is initialized by {@link #analyze analyze(…)} and shall not be modified after that point. - * - * @see #hasRaster() */ private boolean hasRaster; @@ -246,10 +242,11 @@ public class Database<G> extends Syntax { * @param dialect additional information not provided by {@code metadata}. * @param geomLibrary the factory to use for creating geometric objects. * @param listeners where to send warnings. + * @param locks the read/write locks, or {@code null} if none. * @throws SQLException if an error occurred while reading database metadata. */ protected Database(final DataSource source, final DatabaseMetaData metadata, final Dialect dialect, - final Geometries<G> geomLibrary, final StoreListeners listeners) + final Geometries<G> geomLibrary, final StoreListeners listeners, final ReadWriteLock locks) throws SQLException { super(metadata, true); @@ -292,7 +289,7 @@ public class Database<G> extends Syntax { supportsSchemas = metadata.supportsSchemasInDataManipulation(); supportsJavaTime = dialect.supportsJavaTime(); crsEncodings = EnumSet.noneOf(CRSEncoding.class); - transactionLocks = dialect.supportsConcurrency() ? null : new ReentrantReadWriteLock(); + transactionLocks = dialect.supportsConcurrency() ? null : locks; } /** @@ -306,13 +303,14 @@ public class Database<G> extends Syntax { * @param queries additional resources associated to SQL queries. Specified by users at construction time. * @param customizer user-specified modification to the features, or {@code null} if none. * @param listeners where to send warnings. + * @param locks the read/write locks, or {@code null} if none. * @return handler for the spatial database. * @throws SQLException if a database error occurred while reading metadata. * @throws DataStoreException if a logical error occurred while analyzing the database structure. */ public static Database<?> create(final SQLStore store, final DataSource source, final Connection connection, final GeometryLibrary geomLibrary, final GenericName[] tableNames, final ResourceDefinition[] queries, - final SchemaModifier customizer, final StoreListeners listeners) + final SchemaModifier customizer, final StoreListeners listeners, final ReadWriteLock locks) throws Exception { final DatabaseMetaData metadata = connection.getMetaData(); @@ -320,11 +318,8 @@ public class Database<G> extends Syntax { final Dialect dialect = Dialect.guess(metadata); final Database<?> db; switch (dialect) { - case POSTGRESQL: db = new Postgres<>(source, connection, metadata, dialect, g, listeners); break; - default: { - db = new Database<>(source, metadata, dialect, g, listeners); - break; - } + case POSTGRESQL: db = new Postgres<>(source, connection, metadata, dialect, g, listeners, locks); break; + default: db = new Database<>(source, metadata, dialect, g, listeners, locks); break; } db.analyze(store, connection, tableNames, queries, customizer); return db; @@ -521,7 +516,13 @@ public class Database<G> extends Syntax { * @param builder where to add information about the tables. * @throws SQLException if an error occurred while fetching table information. */ - public final void listTables(final DatabaseMetaData metadata, final MetadataBuilder builder) throws SQLException { + public final void metadata(final DatabaseMetaData metadata, final MetadataBuilder builder) throws SQLException { + if (hasGeometry) { + builder.addSpatialRepresentation(SpatialRepresentationType.VECTOR); + } + if (hasRaster) { + builder.addSpatialRepresentation(SpatialRepresentationType.GRID); + } for (final Table table : tables) { builder.addFeatureType(table.featureType, table.countRows(metadata, false, false)); } @@ -551,14 +552,14 @@ public class Database<G> extends Syntax { } /** - * Appends a call to a function or table defined in the spatial schema. + * Appends a table or a call to a function defined in the spatial schema. * The name will be prefixed by catalog and schema name if applicable. * The name will not be quoted. * - * @param sql the SQL builder where to add the spatial function or table name. - * @param name the function or table to append. + * @param sql the SQL builder where to add the spatial table or function. + * @param name name of the table or function to append. */ - public final void appendSpatialSchema(final SQLBuilder sql, final String name) { + public final void formatTableName(final SQLBuilder sql, final String name) { final String schema = schemaOfSpatialTables; if (schema != null && !schema.isEmpty()) { final String catalog = catalogOfSpatialTables; @@ -581,23 +582,46 @@ public class Database<G> extends Syntax { } /** - * Returns {@code true} if this database contains at least one geometry column. - * This information can be used for metadata purpose. + * If a <abbr>CRS</abbr> is available in the cache for the given SRID, returns that <abbr>CRS</abbr>. + * Otherwise returns {@code null}. This method does not query the database. + * + * @param srid identifier of the <abbr>CRS</abbr> to get. + * @return the requested <abbr>CRS</abbr>, or {@code null} if not in the cache. + */ + public final CoordinateReferenceSystem getCachedCRS(final int srid) { + return cacheOfCRS.get(srid); + } + + /** + * If a <abbr>SRID</abbr> is available in the cache for the given <abbr>CRS</abbr>, returns that SRID. + * Otherwise returns {@code null}. This method does not query the database. * - * @return whether at least one geometry column has been found. + * @param crs the <abbr>CRS</abbr> for which to get the <abbr>SRID</abbr>. + * @return the <abbr>SRID</abbr> for the given <abbr>CRS</abbr>, or {@code null} if not in the cache. */ - public final boolean hasGeometry() { - return hasGeometry; + public final int getCachedSRID(final CoordinateReferenceSystem crs) { + synchronized (cacheOfSRID) { + return cacheOfSRID.get(crs); + } } /** - * Returns {@code true} if this database contains at least one raster column. - * This information can be used for metadata purpose. + * Returns a function for getting values from a geometry or geography column. + * This is a helper method for {@link #getMapping(Column)} implementations. * - * @return whether at least one raster column has been found. + * @param columnDefinition information about the column to extract values from and expose through Java API. + * @return converter to the corresponding java type, or {@code null} if this class cannot find a mapping, */ - public final boolean hasRaster() { - return hasRaster; + protected final ValueGetter<?> forGeometry(final Column columnDefinition) { + /* + * The geometry type should not be empty. But it may still happen if the "GEOMETRY_COLUMNS" + * table does not contain a line for the specified column. It is a server issue, but seems + * to happen sometimes. + */ + final GeometryType type = columnDefinition.getGeometryType().orElse(GeometryType.GEOMETRY); + final Class<? extends G> geometryClass = geomLibrary.getGeometryClass(type).asSubclass(geomLibrary.rootClass); + return new GeometryGetter<>(geomLibrary, geometryClass, columnDefinition.getDefaultCRS().orElse(null), + getBinaryEncoding(columnDefinition)); } /** @@ -663,6 +687,18 @@ public class Database<G> extends Syntax { } } + /** + * Returns a mapping for {@link Types#JAVA_OBJECT} or unrecognized types. Some JDBC drivers wrap + * objects in implementation-specific classes, for example {@link org.postgresql.util.PGobject}. + * This method should be overwritten in database-specific subclasses for returning a value getter + * capable to unwrap the value. + * + * @return the default mapping for unknown or unrecognized types. + */ + protected ValueGetter<Object> getDefaultMapping() { + return ValueGetter.AsObject.INSTANCE; + } + /** * Returns the type of components in SQL arrays stored in a column. * This method is invoked when {@link Column#type} = {@link Types#ARRAY}. @@ -680,18 +716,6 @@ public class Database<G> extends Syntax { return Types.OTHER; } - /** - * Returns a mapping for {@link Types#JAVA_OBJECT} or unrecognized types. Some JDBC drivers wrap - * objects in implementation-specific classes, for example {@link org.postgresql.util.PGobject}. - * This method should be overwritten in database-specific subclasses for returning a value getter - * capable to unwrap the value. - * - * @return the default mapping for unknown or unrecognized types. - */ - protected ValueGetter<Object> getDefaultMapping() { - return ValueGetter.AsObject.INSTANCE; - } - /** * Returns an identifier of the way binary data are encoded by the JDBC driver. * @@ -719,36 +743,6 @@ public class Database<G> extends Syntax { return null; } - /** - * Returns a function for getting values from a geometry or geography column. - * This is a helper method for {@link #getMapping(Column)} implementations. - * - * @param columnDefinition information about the column to extract values from and expose through Java API. - * @return converter to the corresponding java type, or {@code null} if this class cannot find a mapping, - */ - protected final ValueGetter<?> forGeometry(final Column columnDefinition) { - /* - * The geometry type should not be empty. But it may still happen if the "GEOMETRY_COLUMNS" - * table does not contain a line for the specified column. It is a server issue, but seems - * to happen sometimes. - */ - final GeometryType type = columnDefinition.getGeometryType().orElse(GeometryType.GEOMETRY); - final Class<? extends G> geometryClass = geomLibrary.getGeometryClass(type).asSubclass(geomLibrary.rootClass); - return new GeometryGetter<>(geomLibrary, geometryClass, columnDefinition.getDefaultCRS().orElse(null), - getBinaryEncoding(columnDefinition)); - } - - /** - * Prepares a cache of statements about spatial information using the given connection. - * Statements will be created only when first needed. - * - * @param connection the connection to use for creating statements. - * @return a cache of prepared statements about spatial information. - */ - protected InfoStatements createInfoStatements(final Connection connection) { - return new InfoStatements(this, connection); - } - /** * Returns the spatial schema conventions that may possibly be supported by this database. * The default implementation returns all {@link SpatialSchema} enumeration values. @@ -802,6 +796,17 @@ public class Database<G> extends Syntax { return filterToSQL; } + /** + * Prepares a cache of statements about spatial information using the given connection. + * Each statement in the returned object will be created only when first needed. + * + * @param connection the connection to use for creating statements. + * @return a cache of prepared statements about spatial information. + */ + public InfoStatements createInfoStatements(final Connection connection) { + return new InfoStatements(this, connection); + } + /** * Sets the logger, class and method names of the given record, then logs it. * This method declares {@link SQLStore#components()} as the public source of the log. diff --git a/endorsed/src/org.apache.sis.storage.sql/main/org/apache/sis/storage/sql/feature/InfoStatements.java b/endorsed/src/org.apache.sis.storage.sql/main/org/apache/sis/storage/sql/feature/InfoStatements.java index 19adc0d1f7..8df07b0a93 100644 --- a/endorsed/src/org.apache.sis.storage.sql/main/org/apache/sis/storage/sql/feature/InfoStatements.java +++ b/endorsed/src/org.apache.sis.storage.sql/main/org/apache/sis/storage/sql/feature/InfoStatements.java @@ -20,19 +20,19 @@ import java.util.Map; import java.util.Set; import java.util.HashSet; import java.util.LinkedHashMap; -import java.util.AbstractMap.SimpleImmutableEntry; import java.util.ArrayList; import java.util.Iterator; import java.util.Locale; import java.util.logging.Level; import java.util.logging.LogRecord; +import java.util.concurrent.Callable; import java.text.ParseException; -import java.sql.Array; import java.sql.Connection; import java.sql.PreparedStatement; import java.sql.ResultSet; import java.sql.Statement; import java.sql.SQLException; +import org.opengis.util.FactoryException; import org.opengis.referencing.IdentifiedObject; import org.opengis.referencing.NoSuchAuthorityCodeException; import org.opengis.referencing.crs.CRSAuthorityFactory; @@ -41,12 +41,15 @@ import org.apache.sis.storage.DataStoreContentException; import org.apache.sis.storage.DataStoreReferencingException; import org.apache.sis.referencing.CRS; import org.apache.sis.referencing.IdentifiedObjects; +import org.apache.sis.referencing.crs.AbstractCRS; +import org.apache.sis.referencing.cs.AxesConvention; import org.apache.sis.referencing.factory.IdentifiedObjectFinder; import org.apache.sis.referencing.privy.DefinitionVerifier; import org.apache.sis.referencing.privy.ReferencingUtilities; import org.apache.sis.metadata.sql.privy.SQLUtilities; import org.apache.sis.metadata.sql.privy.SQLBuilder; import org.apache.sis.geometry.wrapper.GeometryType; +import org.apache.sis.system.CommonExecutor; import org.apache.sis.system.Modules; import org.apache.sis.util.Localized; import org.apache.sis.util.Utilities; @@ -95,7 +98,7 @@ public class InfoStatements implements Localized, AutoCloseable { * cache of CRS created from SRID codes and the listeners where to send warnings. * A {@code Database} object does <strong>not</strong> contain live JDBC {@link Connection}. */ - private final Database<?> database; + protected final Database<?> database; /** * Connection to use for creating the prepared statements. @@ -147,24 +150,12 @@ public class InfoStatements implements Localized, AutoCloseable { return database.listeners.getLocale(); } - /** - * Returns a function for getting values of components in the given array. - * If no match is found, then this method returns {@code null}. - * - * @param array the array from which to get the mapping of component values. - * @return converter to the corresponding java type, or {@code null} if this class cannot find a mapping. - * @throws SQLException if the mapping cannot be obtained. - */ - public final ValueGetter<?> getComponentMapping(final Array array) throws SQLException { - return database.getMapping(new Column(array.getBaseType(), array.getBaseTypeName())); - } - /** * Appends a {@code " FROM <table> WHERE "} text to the given builder. * The table name will be prefixed by catalog and schema name if applicable. */ private void appendFrom(final SQLBuilder sql, final String table) { - database.appendSpatialSchema(sql.append(" FROM "), table); + database.formatTableName(sql.append(" FROM "), table); sql.append(" WHERE "); } @@ -293,6 +284,8 @@ public class InfoStatements implements Localized, AutoCloseable { * or a single entry exists but has no WKT definition and its authority code is unsupported by SIS. * @throws ParseException if the WKT cannot be parsed. * @throws SQLException if a SQL error occurred. + * + * @see org.apache.sis.storage.sql.SQLStore#findCRS(int) */ public final CoordinateReferenceSystem fetchCRS(final int srid) throws Exception { /* @@ -340,6 +333,9 @@ public class InfoStatements implements Localized, AutoCloseable { * {@link SpatialSchema#refSysTable} then gets the CRS from its authority code if possible, * or fallback on the WKT otherwise. * + * <p>This method does not cache the returned <abbr>CRS</abbr>. + * The caching is done by {@code Cache.getOrCreate(…)}.</p> + * * @param srid the Spatial Reference Identifier (SRID) of the CRS to create from the database content. * @return the CRS created from database content. * @throws Exception if an SQL error, parsing error or other error occurred. @@ -476,17 +472,148 @@ public class InfoStatements implements Localized, AutoCloseable { * @param crs the CRS for which to find a SRID, or {@code null}. * @return SRID for the given CRS, or 0 if the given CRS was null. * @throws Exception if an SQL error, parsing error or other error occurred. + * + * @see org.apache.sis.storage.sql.SQLStore#findSRID(CoordinateReferenceSystem) */ public final int findSRID(final CoordinateReferenceSystem crs) throws Exception { if (crs == null) { return 0; } + final SRID result; + final Integer srid; synchronized (database.cacheOfSRID) { - final Integer srid = database.cacheOfSRID.get(crs); - if (srid != null) { - return srid; + final Integer cached = database.cacheOfSRID.get(crs); + if (cached != null) { + return cached; + } + result = findOrAddCRS(crs); + database.cacheOfSRID.put(crs, srid = result.srid); + } + CommonExecutor.instance().submit((Runnable) result); + return result.srid; + } + + /** + * The result of a search for the <abbr>SRID</abbr> from a <abbr>CRS</abbr>. + * This result is stored in the {@link #srid} field, but is accompanied by information allowing to + * opportunistically cache the result in the map for the opposite search (fetching <abbr>CRS</abbr> + * from a <abbr>SRID</abbr>). A difficulty is that the opposite operation would query the geodetic + * registry for more metadata. So in order to have more consistent values in the cache, we need to + * do the same query in {@link #call()} as what would have been done with {@code fetchCRS(srid)}. + */ + private final class SRID implements Callable<CoordinateReferenceSystem>, Runnable { + /** + * The <abbr>CRS</abbr> for which to provide an "authority:code pair". + */ + final CoordinateReferenceSystem crs; + + /** + * The authority managing the code value. + * This is part of the key in hash map. + */ + final String authority; + + /** + * Code managed by the authority. + * This is part of the key in hash map. + */ + final int code; + + /** + * Primary key used in the {@code SPATIAL_REF_SYS} table. + * This is initially zero, then set to a value after the <abbr>SRID</abbr> has been computed. + */ + int srid; + + /** + * Creates a new "authority:code" pair for holding the <abbr>SRID</abbr> result. + */ + SRID(final CoordinateReferenceSystem crs, final String authority, final int code) { + this.crs = crs; + this.authority = authority; + this.code = code; + } + + /** + * Invoked in a background thread for opportunistically caching the <abbr>SRID</abbr> → <abbr>CRS</abbr> + * mapping. This is done in a background thread because this is not the information asked by the user, + * by the information for the reverse operation. + */ + @Override + public void run() { + try { + database.cacheOfCRS.getOrCreate(srid, this); + } catch (Exception e) { + log("findSRID", new LogRecord(Level.FINER, e.toString())); } } + + /** + * Simulates a call to {@code fetchCRS(srid)} for cache consistency. + */ + @Override + public CoordinateReferenceSystem call() throws FactoryException { + final CoordinateReferenceSystem fromAuthority; + try { + final CRSAuthorityFactory factory = CRS.getAuthorityFactory(authority); + fromAuthority = factory.createCoordinateReferenceSystem(Integer.toString(code)); + } catch (NoSuchAuthorityCodeException e) { + return crs; + } + for (int i=0; ; i++) { + final CoordinateReferenceSystem candidate; + switch (i) { + case 0: candidate = fromAuthority; break; + case 1: candidate = AbstractCRS.castOrCopy(fromAuthority).forConvention(AxesConvention.RIGHT_HANDED); break; + case 2: candidate = AbstractCRS.castOrCopy(fromAuthority).forConvention(AxesConvention.DISPLAY_ORIENTED); break; + default: return crs; + } + if (Utilities.equalsApproximately(crs, candidate)) { + return candidate; + } + } + } + + /** + * Compares the "authority:code" tuples, intentionally omitting other properties. + * Only the "authority:code" pair is compared. Needed for use in hash map, + * in order to avoid processing the same "authority:code" pair many times. + */ + @Override + public boolean equals(final Object obj) { + if (obj instanceof SRID) { + final var other = (SRID) obj; + return other.code == code && authority.equals(other.authority); + } + return false; + } + + /** + * Returns a hash code compatible with {@link #equals'Object)} contract. + */ + @Override + public int hashCode() { + return authority.hashCode() + code; + } + + /** + * Returns a string representation for debugging purposes. + */ + @Override + public String toString() { + return authority + ':' + code + " → " + srid; + } + } + + /** + * Invoked when a <abbr>CRS</abbr> is not in the cache. + * This method does not cache the result. Caching should be done by the caller. + * + * @param crs the <abbr>CRS</abbr> to search. + * @return <abbr>SRID</abbr> associated to the given <abbr>CRS</abbr>. + * @throws Exception if an SQL error, parsing error or other error occurred. + */ + private SRID findOrAddCRS(final CoordinateReferenceSystem crs) throws Exception { /* * Search in the database. In the `done` map, keys are "authority:code" pairs that we * already tried and values tell whether we can use that pair if we need to add new CRS. @@ -494,7 +621,7 @@ public class InfoStatements implements Localized, AutoCloseable { Exception error = null; boolean tryWithGivenCRS = true; final var sridFounInUse = new HashSet<Integer>(); - final var done = new LinkedHashMap<SimpleImmutableEntry<String,Object>, Boolean>(); + final var done = new LinkedHashMap<SRID, Boolean>(); // Value tells whether the SRID may be valid. for (Iterator<IdentifiedObject> it = Set.<IdentifiedObject>of(crs).iterator(); it.hasNext();) { final IdentifiedObject candidate = it.next(); /* @@ -505,21 +632,19 @@ public class InfoStatements implements Localized, AutoCloseable { for (final Identifier id : candidate.getIdentifiers()) { final String authority = id.getCodeSpace(); if (authority == null) continue; - final String code = id.getCode(); - if (done.putIfAbsent(new SimpleImmutableEntry<>(authority, code), Boolean.FALSE) != null) { - continue; // Skip "authority:code" that we already tried. - } - final int codeValue; + final int code; try { - codeValue = Integer.parseInt(code); + code = Integer.parseInt(id.getCode()); } catch (NumberFormatException e) { - if (error == null) error = e; - else error.addSuppressed(e); + if (tryWithGivenCRS) { + if (error == null) error = e; + else error.addSuppressed(e); + } continue; // Ignore codes that are not integers. } - final var key = new SimpleImmutableEntry<String,Object>(authority, codeValue); - if (done.putIfAbsent(key, codeValue > 0) != null) { - continue; + final SRID search = new SRID(crs, authority, code); + if (done.putIfAbsent(search, code > 0) != null) { + continue; // Skip "authority:code" that we already tried. } /* * Found an "authority:code" pair that we did not tested before. @@ -528,7 +653,7 @@ public class InfoStatements implements Localized, AutoCloseable { if (sridFromCRS == null) { sridFromCRS = prepareSearchCRS(true); } - sridFromCRS.setInt(1, codeValue); + sridFromCRS.setInt(1, code); sridFromCRS.setString(2, SQLUtilities.toLikePattern(authority, true)); try (ResultSet result = sridFromCRS.executeQuery()) { while (result.next()) { @@ -537,16 +662,14 @@ public class InfoStatements implements Localized, AutoCloseable { if (sridFounInUse.add(srid)) try { final Object parsed = parseDefinition(result, 3); if (Utilities.equalsApproximately(parsed, crs)) { - synchronized (database.cacheOfSRID) { - database.cacheOfSRID.put(crs, srid); - } - return srid; + search.srid = srid; + return search; } } catch (ParseException e) { if (error == null) error = e; else error.addSuppressed(e); } - done.put(key, Boolean.FALSE); // Declare this "authority:code" pair as not available. + done.put(search, Boolean.FALSE); // Declare this "authority:code" pair as not available. } } } @@ -569,24 +692,22 @@ public class InfoStatements implements Localized, AutoCloseable { * It is caller's responsibility to hold a write lock if needed. */ if (!connection.isReadOnly()) { - String fallback = null; - int fallbackCode = 0; - for (final Map.Entry<SimpleImmutableEntry<String,Object>, Boolean> entry : done.entrySet()) { + SRID fallback = null; + for (final Map.Entry<SRID, Boolean> entry : done.entrySet()) { if (entry.getValue()) { - final SimpleImmutableEntry<String,Object> identifier = entry.getKey(); - final int code = (Integer) identifier.getValue(); // Safe cast when `entry.getValue()` is true. - final String authority = identifier.getKey(); - if (Constants.EPSG.equalsIgnoreCase(authority)) { - return addCRS(authority, code, crs, sridFounInUse); + final SRID search = entry.getKey(); + if (Constants.EPSG.equalsIgnoreCase(search.authority)) { + search.srid = addCRS(search, sridFounInUse); + return search; } if (fallback == null) { - fallback = authority; - fallbackCode = code; + fallback = search; } } } if (fallback != null) { - return addCRS(fallback, fallbackCode, crs, sridFounInUse); + fallback.srid = addCRS(fallback, sridFounInUse); + return fallback; } // In the current version, we don't encode a CRS without an "authority:code" pair. } @@ -599,22 +720,19 @@ public class InfoStatements implements Localized, AutoCloseable { * Adds a new entry in the {@code SPATIAL_REF_SYS} table for the given <abbr>CRS</abbr>. * The {@code authority} and {@code code} arguments are the values to store in the {@code "AUTH_NAME"} and * {@code "AUTH_SRID"} columns respectively. A common usage is to prefer EPSG codes, but this is not mandatory. + * This method does not cache the added <abbr>CRS</abbr>. Caching should be done by the caller. * - * @param authority authority providing the CRS definition. - * @param code authority code. - * @param crs the coordinate reference system to encode. + * @param search the <abbr>CRS</abbr> to search together with its "authority:code" pair. * @param sridFounInUse SRIDs which have been found in use, for avoiding SRID that would be certain to fail. * This is only a hint. A SRID not in this set is not a guarantee that it is available. * @return SRID of the CRS added by this method. * @throws DataStoreReferencingException if the given CRS cannot be encoded in at least one supported format. * @throws SQLException if an error occurred while executing the SQL statement. */ - private int addCRS(final String authority, final int code, final CoordinateReferenceSystem crs, - final Set<Integer> sridFounInUse) throws Exception - { + private int addCRS(final SRID search, final Set<Integer> sridFounInUse) throws Exception { final SpatialSchema schema = database.getSpatialSchema().orElseThrow(); final var sql = new SQLBuilder(database).append(SQLBuilder.INSERT); - database.appendSpatialSchema(sql, schema.crsTable); + database.formatTableName(sql, schema.crsTable); sql.append(" (").append(schema.crsIdentifierColumn) .append(", ").append(schema.crsAuthorityNameColumn) .append(", ").append(schema.crsAuthorityCodeColumn); @@ -633,11 +751,11 @@ public class InfoStatements implements Localized, AutoCloseable { final String def; switch (encoding) { default: continue; // Skip unknown formats (none at this time). - case WKT1: def = wktFormat.format(crs); break; + case WKT1: def = wktFormat.format(search.crs); break; case WKT2: { try { wktFormat.setConvention(Convention.WKT2); - def = wktFormat.format(crs); + def = wktFormat.format(search.crs); } finally { wktFormat.setConvention(Convention.WKT1_COMMON_UNITS); } @@ -671,8 +789,8 @@ public class InfoStatements implements Localized, AutoCloseable { do { final String column = description ? schema.crsDescriptionColumn : schema.crsNameColumn; if (column != null) { - String name = description ? IdentifiedObjects.getDisplayName(crs, getLocale()) - : IdentifiedObjects.getName(crs, null); + String name = description ? IdentifiedObjects.getDisplayName(search.crs, getLocale()) + : IdentifiedObjects.getName(search.crs, null); if (name != null) { definitions[numDefinitions++] = name; sql.append(", ").append(column); @@ -689,10 +807,10 @@ public class InfoStatements implements Localized, AutoCloseable { sql.append(separator).append('?'); separator = ", "; } - int srid = code; + int srid = search.code; try (PreparedStatement stmt = connection.prepareStatement(sql.append(')').toString())) { - stmt.setString(2, authority); - stmt.setInt(3, code); + stmt.setString(2, search.authority); + stmt.setInt(3, search.code); for (int i=0; i<numDefinitions; i++) { stmt.setString(4+i, definitions[i]); } @@ -715,7 +833,7 @@ public class InfoStatements implements Localized, AutoCloseable { */ try { final CoordinateReferenceSystem candidate = fetchCRS(srid); - if (Utilities.equalsIgnoreMetadata(crs, candidate)) { + if (Utilities.equalsIgnoreMetadata(search.crs, candidate)) { return srid; } } catch (Exception f) { diff --git a/endorsed/src/org.apache.sis.storage.sql/main/org/apache/sis/storage/sql/feature/SchemaModifier.java b/endorsed/src/org.apache.sis.storage.sql/main/org/apache/sis/storage/sql/feature/SchemaModifier.java index 46a1b40865..fe204d2705 100644 --- a/endorsed/src/org.apache.sis.storage.sql/main/org/apache/sis/storage/sql/feature/SchemaModifier.java +++ b/endorsed/src/org.apache.sis.storage.sql/main/org/apache/sis/storage/sql/feature/SchemaModifier.java @@ -71,5 +71,5 @@ public interface SchemaModifier { * @todo if we move this key in public API in the future, then it would be a * value in existing {@link org.apache.sis.storage.DataOptionKey} class. */ - OptionKey<SchemaModifier> OPTION = new InternalOptionKey<SchemaModifier>("SCHEMA_MODIFIER", SchemaModifier.class); + OptionKey<SchemaModifier> OPTION = new InternalOptionKey<>("SCHEMA_MODIFIER", SchemaModifier.class); } diff --git a/endorsed/src/org.apache.sis.storage.sql/main/org/apache/sis/storage/sql/feature/ValueGetter.java b/endorsed/src/org.apache.sis.storage.sql/main/org/apache/sis/storage/sql/feature/ValueGetter.java index b1158d119d..dd1253b117 100644 --- a/endorsed/src/org.apache.sis.storage.sql/main/org/apache/sis/storage/sql/feature/ValueGetter.java +++ b/endorsed/src/org.apache.sis.storage.sql/main/org/apache/sis/storage/sql/feature/ValueGetter.java @@ -487,7 +487,11 @@ public class ValueGetter<T> { } Object result = array.getArray(); if (cmget == null && stmts != null) { - cmget = stmts.getComponentMapping(array); + /* + * Get a function for getting values of components in the array. + * If no match is found, then `cmget` stay null. + */ + cmget = stmts.database.getMapping(new Column(array.getBaseType(), array.getBaseTypeName())); } Class<?> componentType = Numbers.primitiveToWrapper(result.getClass().getComponentType()); if (cmget != null && !cmget.valueType.isAssignableFrom(componentType)) { diff --git a/endorsed/src/org.apache.sis.storage.sql/main/org/apache/sis/storage/sql/postgis/ExtentEstimator.java b/endorsed/src/org.apache.sis.storage.sql/main/org/apache/sis/storage/sql/postgis/ExtentEstimator.java index d9680f2620..5865e933c1 100644 --- a/endorsed/src/org.apache.sis.storage.sql/main/org/apache/sis/storage/sql/postgis/ExtentEstimator.java +++ b/endorsed/src/org.apache.sis.storage.sql/main/org/apache/sis/storage/sql/postgis/ExtentEstimator.java @@ -120,7 +120,7 @@ final class ExtentEstimator { private void query(final Statement statement) throws SQLException { for (final Column column : columns) { if (column.getGeometryType().isPresent()) { - database.appendSpatialSchema(builder.append(SQLBuilder.SELECT), "ST_EstimatedExtent"); + database.formatTableName(builder.append(SQLBuilder.SELECT), "ST_EstimatedExtent"); builder.append('('); if (table.schema != null) { builder.appendValue(table.schema).append(", "); diff --git a/endorsed/src/org.apache.sis.storage.sql/main/org/apache/sis/storage/sql/postgis/Postgres.java b/endorsed/src/org.apache.sis.storage.sql/main/org/apache/sis/storage/sql/postgis/Postgres.java index 20a4a43d1a..d8ad170df1 100644 --- a/endorsed/src/org.apache.sis.storage.sql/main/org/apache/sis/storage/sql/postgis/Postgres.java +++ b/endorsed/src/org.apache.sis.storage.sql/main/org/apache/sis/storage/sql/postgis/Postgres.java @@ -18,6 +18,8 @@ package org.apache.sis.storage.sql.postgis; import java.util.Map; import java.util.Locale; +import java.util.logging.Level; +import java.util.concurrent.locks.ReadWriteLock; import java.sql.Types; import java.sql.JDBCType; import java.sql.Connection; @@ -26,7 +28,6 @@ import java.sql.ResultSet; import java.sql.DatabaseMetaData; import java.sql.SQLException; import javax.sql.DataSource; -import java.util.logging.Level; import org.opengis.geometry.Envelope; import org.apache.sis.geometry.wrapper.Geometries; import org.apache.sis.storage.sql.feature.BinaryEncoding; @@ -72,13 +73,15 @@ public final class Postgres<G> extends Database<G> { * @param dialect additional information not provided by {@code metadata}. * @param geomLibrary the factory to use for creating geometric objects. * @param listeners where to send warnings. + * @param locks the read/write locks, or {@code null} if none. * @throws SQLException if an error occurred while reading database metadata. */ public Postgres(final DataSource source, final Connection connection, final DatabaseMetaData metadata, - final Dialect dialect, final Geometries<G> geomLibrary, final StoreListeners listeners) + final Dialect dialect, final Geometries<G> geomLibrary, final StoreListeners listeners, + final ReadWriteLock locks) throws SQLException { - super(source, metadata, dialect, geomLibrary, listeners); + super(source, metadata, dialect, geomLibrary, listeners, locks); Version version = null; try (Statement st = connection.createStatement(); ResultSet result = st.executeQuery("SELECT public.PostGIS_version();")) @@ -181,7 +184,7 @@ public final class Postgres<G> extends Database<G> { * @return a cache of prepared statements about spatial information. */ @Override - protected InfoStatements createInfoStatements(final Connection connection) { + public InfoStatements createInfoStatements(final Connection connection) { return new ExtendedInfo(this, connection); } diff --git a/endorsed/src/org.apache.sis.storage.sql/test/org/apache/sis/storage/sql/feature/InfoStatementsTest.java b/endorsed/src/org.apache.sis.storage.sql/test/org/apache/sis/storage/sql/feature/InfoStatementsTest.java index b93ac7b67a..403bdee386 100644 --- a/endorsed/src/org.apache.sis.storage.sql/test/org/apache/sis/storage/sql/feature/InfoStatementsTest.java +++ b/endorsed/src/org.apache.sis.storage.sql/test/org/apache/sis/storage/sql/feature/InfoStatementsTest.java @@ -95,7 +95,7 @@ public final class InfoStatementsTest extends TestCase { connection = test.source.getConnection(); database = new Database<>(test.source, connection.getMetaData(), Dialect.DERBY, Geometries.factory(GeometryLibrary.JAVA2D), - new StoreListeners(null, new DataStoreMock("Unused"))); + new StoreListeners(null, new DataStoreMock("Unused")), null); /* * The `spatialSchema` is private, so we need to use reflection for setting its value. * Normally that field would be set by `Database.analyze(…)`, bur we want to avoid that