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 81bd4b34e7b0d3becb7bbbb0774c0509138cc3de Author: Martin Desruisseaux <martin.desruisse...@geomatys.com> AuthorDate: Thu Aug 29 18:58:14 2024 +0200 Add a `SQLStore.initialize(Connection)` method that subclasses can override. Ensure that the initialization is done and the spatial schema is analyzed before the `readResourceDescriptors(DataAccess)` method is invoked. Opportunistic cleanup (documentation, checks). This commit completes (for now) https://issues.apache.org/jira/browse/SIS-603 --- .../main/org/apache/sis/storage/landsat/Band.java | 2 +- .../apache/sis/storage/geotiff/GeoTiffStore.java | 2 +- .../sis/storage/geotiff/spi/SchemaModifier.java | 2 +- .../org/apache/sis/storage/sql/DataAccess.java | 2 +- .../main/org/apache/sis/storage/sql/SQLStore.java | 32 +++- .../apache/sis/storage/sql/feature/Analyzer.java | 177 +++++++++++++++++---- .../apache/sis/storage/sql/feature/Database.java | 177 ++++++--------------- .../sis/storage/sql/feature/FeatureAnalyzer.java | 3 +- .../sis/storage/sql/feature/QueryAnalyzer.java | 2 +- .../sis/storage/sql/feature/SchemaModifier.java | 2 +- .../org/apache/sis/storage/sql/feature/Table.java | 2 +- .../sis/storage/sql/feature/TableAnalyzer.java | 1 - .../org/apache/sis/storage/sql/SQLStoreTest.java | 2 +- .../sql/feature/SelectionClauseWriterTest.java | 2 +- .../org/apache/sis/storage/StorageConnector.java | 17 +- 15 files changed, 249 insertions(+), 176 deletions(-) diff --git a/endorsed/src/org.apache.sis.storage.earthobservation/main/org/apache/sis/storage/landsat/Band.java b/endorsed/src/org.apache.sis.storage.earthobservation/main/org/apache/sis/storage/landsat/Band.java index c358371216..ac8d512304 100644 --- a/endorsed/src/org.apache.sis.storage.earthobservation/main/org/apache/sis/storage/landsat/Band.java +++ b/endorsed/src/org.apache.sis.storage.earthobservation/main/org/apache/sis/storage/landsat/Band.java @@ -121,7 +121,7 @@ final class Band extends GridResourceWrapper implements SchemaModifier { file = Path.of(filename); } final StorageConnector connector = new StorageConnector(file); - connector.setOption(SchemaModifier.OPTION, this); + connector.setOption(SchemaModifier.OPTION_KEY, this); return new GeoTiffStore(parent, parent.getProvider(), connector, true).components().get(0); } diff --git a/endorsed/src/org.apache.sis.storage.geotiff/main/org/apache/sis/storage/geotiff/GeoTiffStore.java b/endorsed/src/org.apache.sis.storage.geotiff/main/org/apache/sis/storage/geotiff/GeoTiffStore.java index 67a087e7ab..69bc362295 100644 --- a/endorsed/src/org.apache.sis.storage.geotiff/main/org/apache/sis/storage/geotiff/GeoTiffStore.java +++ b/endorsed/src/org.apache.sis.storage.geotiff/main/org/apache/sis/storage/geotiff/GeoTiffStore.java @@ -245,7 +245,7 @@ public class GeoTiffStore extends DataStore implements Aggregate { this.hidden = hidden; @SuppressWarnings("LocalVariableHidesMemberVariable") - final SchemaModifier customizer = connector.getOption(SchemaModifier.OPTION); + final SchemaModifier customizer = connector.getOption(SchemaModifier.OPTION_KEY); this.customizer = (customizer != null) ? customizer : SchemaModifier.DEFAULT; @SuppressWarnings("LocalVariableHidesMemberVariable") diff --git a/endorsed/src/org.apache.sis.storage.geotiff/main/org/apache/sis/storage/geotiff/spi/SchemaModifier.java b/endorsed/src/org.apache.sis.storage.geotiff/main/org/apache/sis/storage/geotiff/spi/SchemaModifier.java index 90714892b3..3d1e0d6689 100644 --- a/endorsed/src/org.apache.sis.storage.geotiff/main/org/apache/sis/storage/geotiff/spi/SchemaModifier.java +++ b/endorsed/src/org.apache.sis.storage.geotiff/main/org/apache/sis/storage/geotiff/spi/SchemaModifier.java @@ -296,7 +296,7 @@ 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<>("SCHEMA_MODIFIER", SchemaModifier.class); + OptionKey<SchemaModifier> OPTION_KEY = new InternalOptionKey<>("SCHEMA_MODIFIER", SchemaModifier.class); /** * The default instance which performs no modification. diff --git a/endorsed/src/org.apache.sis.storage.sql/main/org/apache/sis/storage/sql/DataAccess.java b/endorsed/src/org.apache.sis.storage.sql/main/org/apache/sis/storage/sql/DataAccess.java index e18f8be797..1b7aeee962 100644 --- a/endorsed/src/org.apache.sis.storage.sql/main/org/apache/sis/storage/sql/DataAccess.java +++ b/endorsed/src/org.apache.sis.storage.sql/main/org/apache/sis/storage/sql/DataAccess.java @@ -130,7 +130,7 @@ public class DataAccess implements AutoCloseable { * Sets the connections and spatial statements to instances that already exist. * This method is invoked during database model initialization only. */ - final void initialize(final Connection connection, final InfoStatements spatialInformation) { + final void setConnection(final Connection connection, final InfoStatements spatialInformation) { this.connection = connection; this.spatialInformation = spatialInformation; } 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 ca6889ffdb..468f211524 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 @@ -26,6 +26,7 @@ import java.sql.Connection; import java.sql.Statement; import java.sql.DatabaseMetaData; import java.lang.reflect.Method; +import java.sql.SQLException; import org.opengis.util.GenericName; import org.opengis.metadata.Metadata; import org.opengis.parameter.ParameterValueGroup; @@ -40,6 +41,7 @@ import org.apache.sis.storage.StorageConnector; import org.apache.sis.storage.event.StoreEvent; import org.apache.sis.storage.event.StoreListener; import org.apache.sis.storage.event.WarningEvent; +import org.apache.sis.storage.sql.feature.Analyzer; import org.apache.sis.storage.sql.feature.Database; import org.apache.sis.storage.sql.feature.Resources; import org.apache.sis.storage.sql.feature.SchemaModifier; @@ -221,7 +223,7 @@ public abstract class SQLStore extends DataStore implements Aggregate { source = connector.commit(DataSource.class, "SQL"); geomLibrary = connector.getOption(OptionKey.GEOMETRY_LIBRARY); contentLocale = connector.getOption(OptionKey.LOCALE); - customizer = connector.getOption(SchemaModifier.OPTION); + customizer = connector.getOption(SchemaModifier.OPTION_KEY); transactionLocks = connector.getOption(InternalOptionKey.LOCKS); identifier = getDataSourceProperty(IDENTIFIER_GETTERS) .map((id) -> Names.createLocalName(null, null, id)).orElse(null); @@ -260,6 +262,24 @@ public abstract class SQLStore extends DataStore implements Aggregate { this.queries = ArraysExt.resize(queries, queryCount); } + /** + * Invoked the first time that {@code SQLStore} opens a connection on the database, or after refresh. + * The default implementation does nothing. Subclasses can override this method if they need to create + * tables in an empty database, or for fetching more information. + * + * <p>If {@link #refresh()} has been invoked, then this initialization method will be invoked again + * the next time that a connection is needed. This method is invoked <em>before</em> + * {@link #readResourceDefinitions(DataAccess)}.</p> + * + * @param connection a connection to the database. + * @throws DataStoreException if an error occurred during the initialization. + * @throws SQLException if an error occurred during the execution of an <abbr>SQL</abbr> query. + * + * @since 1.5 + */ + protected void initialize(Connection connection) throws DataStoreException, SQLException { + } + /** * Returns the data source to use for obtaining connections to the database. * @@ -368,16 +388,18 @@ public abstract class SQLStore extends DataStore implements Aggregate { assert Thread.holdsLock(this); Database<?> current = model; if (current == null) { + initialize(c); final DatabaseMetaData md = c.getMetaData(); - current = Database.create(source, md, geomLibrary, contentLocale, listeners, transactionLocks); + final var analyzer = new Analyzer(source, md, geomLibrary, contentLocale, listeners, transactionLocks); + current = analyzer.database; try (final InfoStatements spatialInformation = current.createInfoStatements(c)) { if (tableNames == null) { final DataAccess dao = newDataAccess(false); - dao.initialize(c, spatialInformation); + dao.setConnection(c, spatialInformation); setModelSources(readResourceDefinitions(dao)); // Do not close the DAO, because we still use the connection and the info statements. } - current.analyze(this, md, tableNames, queries, customizer, spatialInformation); + current.analyze(this, analyzer, tableNames, queries, customizer, spatialInformation); } model = current; } @@ -491,6 +513,8 @@ public abstract class SQLStore extends DataStore implements Aggregate { * Dependencies (inferred from the foreigner keys) will be followed automatically. * @throws DataStoreException if an error occurred while fetching the resource definitions. * + * @see #initialize(Connection) + * * @since 1.5 */ protected abstract ResourceDefinition[] readResourceDefinitions(DataAccess dao) throws DataStoreException; diff --git a/endorsed/src/org.apache.sis.storage.sql/main/org/apache/sis/storage/sql/feature/Analyzer.java b/endorsed/src/org.apache.sis.storage.sql/main/org/apache/sis/storage/sql/feature/Analyzer.java index 6cbb945461..25c43fab78 100644 --- a/endorsed/src/org.apache.sis.storage.sql/main/org/apache/sis/storage/sql/feature/Analyzer.java +++ b/endorsed/src/org.apache.sis.storage.sql/main/org/apache/sis/storage/sql/feature/Analyzer.java @@ -16,22 +16,35 @@ */ package org.apache.sis.storage.sql.feature; +import java.util.Collection; +import java.util.List; +import java.util.ArrayList; import java.util.Set; +import java.util.HashSet; +import java.util.LinkedHashSet; import java.util.Map; import java.util.HashMap; -import java.util.LinkedHashSet; -import java.util.Collection; import java.util.Objects; +import java.util.Locale; import java.util.logging.Level; +import java.util.concurrent.locks.ReadWriteLock; import java.sql.SQLException; import java.sql.DatabaseMetaData; import java.sql.ResultSet; +import javax.sql.DataSource; import org.opengis.util.NameFactory; import org.opengis.util.NameSpace; import org.opengis.util.GenericName; +import org.apache.sis.setup.GeometryLibrary; +import org.apache.sis.geometry.wrapper.Geometries; import org.apache.sis.storage.DataStoreException; import org.apache.sis.storage.IllegalNameException; import org.apache.sis.storage.InternalDataStoreException; +import org.apache.sis.storage.event.StoreListeners; +import org.apache.sis.storage.sql.postgis.Postgres; +import org.apache.sis.metadata.sql.privy.Dialect; +import org.apache.sis.metadata.sql.privy.Reflection; +import org.apache.sis.storage.sql.ResourceDefinition; import org.apache.sis.util.iso.DefaultNameFactory; import org.apache.sis.util.resources.ResourceInternationalString; @@ -45,18 +58,18 @@ import org.apache.sis.util.resources.ResourceInternationalString; * @author Martin Desruisseaux (Geomatys) * @author Alexis Manin (Geomatys) */ -final class Analyzer { +public final class Analyzer { /** * Information about the spatial database to analyze. */ - final Database<?> database; + public final Database<?> database; /** * A cache of statements for fetching spatial information such as geometry columns or SRID. * May be {@code null} if the database is not a spatial database, e.g. because the geometry * table has not been found. */ - final InfoStatements spatialInformation; + InfoStatements spatialInformation; /** * Information about the database as a whole. @@ -76,7 +89,7 @@ final class Analyzer { * * @see #getUniqueString(ResultSet, String) */ - private final Map<String,String> strings; + private final Map<String,String> uniqueStrings; /** * The string to insert before wildcard characters ({@code '_'} or {@code '%'}) to escape. @@ -85,11 +98,22 @@ final class Analyzer { */ final String escape; + /** + * The "TABLE" and "VIEW" keywords for table types, with unsupported keywords omitted. + */ + private final String[] tableTypes; + + /** + * Names of tables to ignore. This set includes at least the tables defined by the spatial + * schema standard from storing geometry columns, spatial reference systems, <i>etc.</i> + */ + private final Set<String> ignoredTables; + /** * All tables created by analysis of the database structure. A {@code null} value means that the table * is in process of being created. This may happen if there is cyclic dependencies between tables. */ - private final Map<GenericName,Table> tables; + private final Map<GenericName,Table> featureTables; /** * Warnings found while analyzing a database structure. Duplicated warnings are omitted. @@ -112,28 +136,111 @@ final class Analyzer { /** * User-specified modification to the features, or {@code null} if none. */ - final SchemaModifier customizer; + SchemaModifier customizer; /** - * Creates a new analyzer for the database described by given metadata. + * Creates a new analyzer for a spatial database. + * Callers shall invoke {@link #analyze analyze(…)} after this method. * - * @param database information about the spatial database. - * @param metadata value of {@code connection.getMetaData()} (provided because already known by caller). - * @param customizer user-specified modification to the features, or {@code null} if none. - * @param spatialInformation statements for fetching SRID, geometry types, <i>etc.</i> + * @param source provider of (pooled) connections to the database. + * @param metadata value of {@code connection.getMetaData()} (provided because already known by caller). + * @param geomLibrary the factory to use for creating geometric objects. + * @param contentLocale the locale to use for international texts to write in the database, or {@code null} for default. + * @param listeners where to send warnings. + * @param locks the read/write locks, or {@code null} if none. + * @throws SQLException if a database error occurred while reading metadata. + * @throws DataStoreException if a logical error occurred while analyzing the database structure. */ - Analyzer(final Database<?> database, final DatabaseMetaData metadata, final SchemaModifier customizer, - final InfoStatements spatialInformation) throws SQLException + public Analyzer(final DataSource source, final DatabaseMetaData metadata, final GeometryLibrary geomLibrary, + final Locale contentLocale, final StoreListeners listeners, final ReadWriteLock locks) + throws Exception { - this.database = database; - this.tables = new HashMap<>(); - this.strings = new HashMap<>(); - this.warnings = new LinkedHashSet<>(); - this.customizer = customizer; - this.metadata = metadata; - this.escape = metadata.getSearchStringEscape(); - this.nameFactory = DefaultNameFactory.provider(); - this.spatialInformation = database.getSpatialSchema().isPresent() ? spatialInformation : null; + this.metadata = metadata; + this.escape = metadata.getSearchStringEscape(); + this.nameFactory = DefaultNameFactory.provider(); + this.featureTables = new HashMap<>(); + this.uniqueStrings = new HashMap<>(); + this.warnings = new LinkedHashSet<>(); + /* + * Finds the keyword used for identifying tables and views. + * Derby, HSQLDB and PostgreSQL uses the "TABLE" type, but H2 uses "BASE TABLE". + */ + final Set<String> types = new HashSet<>(4); + try (ResultSet reflect = metadata.getTableTypes()) { + while (reflect.next()) { + final String type = reflect.getString(Reflection.TABLE_TYPE); + if ("TABLE".equalsIgnoreCase(type) || "VIEW".equalsIgnoreCase(type) || "BASE TABLE".equalsIgnoreCase(type)) { + types.add(type); + } + } + } + tableTypes = types.toArray(String[]::new); + /* + * Creates the database, then detect the spatial schema. Detects also the catalog and schema names. + * It needs to be done before to search for feature tables, because the latter may require accesses + * to the SPATIAL_REF_SYS table. For example, if the search consists in reading the "gpkg_contents" + * table. + */ + final Geometries<?> g = Geometries.factory(geomLibrary); + final Dialect dialect = Dialect.guess(metadata); + switch (dialect) { + case POSTGRESQL: database = new Postgres<>(source, metadata, dialect, g, contentLocale, listeners, locks); break; + default: database = new Database<>(source, metadata, dialect, g, contentLocale, listeners, locks); break; + } + ignoredTables = database.detectSpatialSchema(metadata, tableTypes); + } + + /** + * Collects the names of all tables specified by user, ignoring standard tables such as {@code SPATIAL_REF_SYS}. + * This method requires a list of tables to include in the model, but this list should not include dependencies. + * This method will follow foreigner keys automatically. + * + * <h4>Prerequisites</h4> + * The {@link #spatialInformation} (if available) and {@link #customizer} (if any) fields should + * be set before to invoke this method. Those fields are optional, they may be {@code null}. + * + * @param tableNames qualified name of the tables. Specified by users at construction time. + * @param queries additional resources associated to SQL queries. Specified by users at construction time. + * @throws Exception if a <abbr>SQL</abbr> error or logical error occurred. + * + * @see #finish() + */ + final List<Table> findFeatureTables(final GenericName[] tableNames, final ResourceDefinition[] queries) throws Exception { + final var declared = new LinkedHashSet<TableReference>(); + for (final GenericName tableName : tableNames) { + final String[] names = TableReference.splitName(tableName); + try (ResultSet reflect = metadata.getTables(names[2], names[1], names[0], tableTypes)) { + while (reflect.next()) { + final String table = getUniqueString(reflect, Reflection.TABLE_NAME); + if (ignoredTables.contains(table)) { + continue; + } + declared.add(new TableReference( + getUniqueString(reflect, Reflection.TABLE_CAT), + getUniqueString(reflect, Reflection.TABLE_SCHEM), table, + getUniqueString(reflect, Reflection.REMARKS))); + } + } + } + /* + * At this point we got the list of tables requested by the user. Now create the Table objects for each + * specified name. During this iteration, we may discover new tables to analyze because of dependencies + * (foreigner keys). + */ + final List<Table> tableList; + tableList = new ArrayList<>(tableNames.length); + for (final TableReference reference : declared) { + // Adds only the table explicitly required by the user. + tableList.add(table(reference, reference.getName(this), null)); + } + /* + * Add queries if any. + */ + for (final ResourceDefinition resource : queries) { + // Optional value should always be present in this context. + tableList.add(query(resource.getName(), resource.getQuery().get())); + } + return tableList; } /** @@ -149,7 +256,7 @@ final class Analyzer { final String getUniqueString(final ResultSet reflect, final String column) throws SQLException { String value = reflect.getString(column); if (value != null) { - final String p = strings.putIfAbsent(value, value); + final String p = uniqueStrings.putIfAbsent(value, value); if (p != null) value = p; } return value; @@ -192,12 +299,12 @@ final class Analyzer { * @return the table, or {@code null} if there is a cyclic dependency and * the table of the given name is already in process of being created. */ - public final Table table(final TableReference id, final GenericName name, final TableReference dependencyOf) throws Exception { - Table table = tables.get(name); - if (table == null && !tables.containsKey(name)) { - tables.put(name, null); // Mark the feature as in process of being created. + final Table table(final TableReference id, final GenericName name, final TableReference dependencyOf) throws Exception { + Table table = featureTables.get(name); + if (table == null && !featureTables.containsKey(name)) { + featureTables.put(name, null); // Mark the feature as in process of being created. table = new Table(database, new TableAnalyzer(this, id, dependencyOf), null); - if (tables.put(name, table) != null) { + if (featureTables.put(name, table) != null) { // Should never happen. If thrown, we have a bug (e.g. synchronization) in this package. throw new InternalDataStoreException(internalError()); } @@ -213,9 +320,9 @@ final class Analyzer { * @param query the query to execute. * @return the virtual table for the given query. */ - public final Table query(final GenericName name, final String query) throws Exception { + private Table query(final GenericName name, final String query) throws Exception { final Table table = new Table(database, new QueryAnalyzer(this, name, query, null), query); - if (!tables.containsKey(name) && tables.put(name, table) == null) { + if (!featureTables.containsKey(name) && featureTables.put(name, table) == null) { return table; } throw new IllegalNameException(resources().getString(Resources.Keys.NameAlreadyUsed_1, name)); @@ -251,13 +358,13 @@ final class Analyzer { * (omitting duplicated warnings), then returns all tables including dependencies. */ final Collection<Table> finish() throws DataStoreException { - for (final Table table : tables.values()) { - table.setDeferredSearchTables(this, tables); + for (final Table table : featureTables.values()) { + table.setDeferredSearchTables(this, featureTables); } for (final ResourceInternationalString warning : warnings) { database.log(warning.toLogRecord(Level.WARNING)); } - return tables.values(); + return featureTables.values(); } /** 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 64dd97c6db..7fa2a694ac 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 @@ -20,11 +20,8 @@ import java.util.Set; import java.util.Map; import java.util.List; import java.util.EnumSet; -import java.util.HashSet; import java.util.HashMap; -import java.util.LinkedHashSet; import java.util.WeakHashMap; -import java.util.ArrayList; import java.util.Locale; import java.util.Optional; import java.util.logging.LogRecord; @@ -41,7 +38,6 @@ 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; import org.apache.sis.metadata.sql.privy.Reflection; @@ -57,7 +53,6 @@ import org.apache.sis.geometry.wrapper.GeometryType; import org.apache.sis.system.Modules; import org.apache.sis.storage.sql.SQLStore; import org.apache.sis.storage.sql.ResourceDefinition; -import org.apache.sis.storage.sql.postgis.Postgres; import org.apache.sis.storage.event.StoreListeners; import org.apache.sis.util.collection.TreeTable; import org.apache.sis.util.collection.Cache; @@ -301,63 +296,15 @@ public class Database<G> extends Syntax { } /** - * Creates a new handler for a spatial database. - * Callers shall invoke {@link #analyze analyze(…)} after this method. - * - * @param source provider of (pooled) connections to the database. - * @param metadata value of {@code connection.getMetaData()} (provided because already known by caller). - * @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. - * @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 DataSource source, final DatabaseMetaData metadata, - final GeometryLibrary geomLibrary, final Locale contentLocale, final StoreListeners listeners, - final ReadWriteLock locks) throws Exception - { - final Geometries<?> g = Geometries.factory(geomLibrary); - final Dialect dialect = Dialect.guess(metadata); - final Database<?> db; - switch (dialect) { - case POSTGRESQL: db = new Postgres<>(source, metadata, dialect, g, contentLocale, listeners, locks); break; - default: db = new Database<>(source, metadata, dialect, g, contentLocale, listeners, locks); break; - } - return db; - } - - /** - * Creates a model about the specified tables in the database. - * This method shall be invoked exactly once after {@code Database} construction. - * It requires a list of tables to include in the model, but this list should not - * include the dependencies; this method will follow foreigner keys automatically. - * - * <p>The table names shall be qualified names of 1, 2 or 3 components. - * The components are {@code <catalog>.<schema pattern>.<table pattern>} where:</p> + * Detects automatically which spatial schema is in use. Detects also the catalog name and schema name. + * This method is invoked exactly once after construction and before the analysis of feature tables. * - * <ul> - * <li>{@code <catalog>}, if present, shall be the name of a catalog as it is stored in the database.</li> - * <li>{@code <schema pattern>}, if present, shall be the pattern of a schema. - * The pattern can use {@code '_'} and {@code '%'} wildcards characters.</li> - * <li>{@code <table pattern>} (mandatory) shall be the pattern of a table. - * The pattern can use {@code '_'} and {@code '%'} wildcards characters.</li> - * </ul> - * - * @param store the data store for which we are creating a model. Used only in case of error. - * @param metadata value of {@code connection.getMetaData()} (provided because already known by caller). - * @param tableNames qualified name of the tables. Specified by users at construction time. - * @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 spatialInformation statements for fetching SRID, geometry types, <i>etc.</i> - * @throws SQLException if a database error occurred while reading metadata. - * @throws DataStoreException if a logical error occurred while analyzing the database structure. + * @param metadata metadata to use for verifying which tables are present. + * @param tableTypes the "TABLE" and "VIEW" keywords for table types, with unsupported keywords omitted. + * @return names of the standard tables defined by the spatial schema. */ - public final void analyze(final SQLStore store, final DatabaseMetaData metadata, final GenericName[] tableNames, - final ResourceDefinition[] queries, final SchemaModifier customizer, - final InfoStatements spatialInformation) throws Exception - { - final String[] tableTypes = getTableTypes(metadata); + final Set<String> detectSpatialSchema(final DatabaseMetaData metadata, final String[] tableTypes) throws SQLException { + final String escape = metadata.getSearchStringEscape(); /* * The following tables are defined by ISO 19125 / OGC Simple feature access part 2. * Note that the standard specified those names in upper-case letters, which is also @@ -390,7 +337,7 @@ public class Database<G> extends Syntax { String catalog = null, schema = null; for (final Map.Entry<String,Boolean> entry : ignoredTables.entrySet()) { if (entry.getValue()) { - String table = SQLUtilities.escape(entry.getKey(), metadata.getSearchStringEscape()); + String table = SQLUtilities.escape(entry.getKey(), escape); try (ResultSet reflect = metadata.getTables(null, null, table, tableTypes)) { while (reflect.next()) { consistent &= consistent(catalog, catalog = reflect.getString(Reflection.TABLE_CAT)); @@ -416,16 +363,15 @@ public class Database<G> extends Syntax { * Some schemas have additional columns for optional encodings, for example a separated column for WKT2. * The preference order will be defined by the `CRSEncoding` enumeration order. */ - final Analyzer analyzer = new Analyzer(this, metadata, customizer, spatialInformation); if (spatialSchema != null) { - final String schema = SQLUtilities.escape(schemaOfSpatialTables, analyzer.escape); - final String table = SQLUtilities.escape(crsTable, analyzer.escape); + final String schema = SQLUtilities.escape(schemaOfSpatialTables, escape); + final String table = SQLUtilities.escape(crsTable, escape); for (Map.Entry<CRSEncoding, String> entry : spatialSchema.crsDefinitionColumn.entrySet()) { String column = entry.getValue(); if (metadata.storesLowerCaseIdentifiers()) { column = column.toLowerCase(Locale.US); } - column = SQLUtilities.escape(column, analyzer.escape); + column = SQLUtilities.escape(column, escape); try (ResultSet reflect = metadata.getColumns(catalogOfSpatialTables, schema, table, column)) { if (reflect.next()) { crsEncodings.add(entry.getKey()); @@ -433,48 +379,48 @@ public class Database<G> extends Syntax { } } } - /* - * Collect the names of all tables specified by user, ignoring the tables - * used for database internal working (for example by PostGIS). - */ - final var declared = new LinkedHashSet<TableReference>(); - for (final GenericName tableName : tableNames) { - final String[] names = TableReference.splitName(tableName); - try (ResultSet reflect = metadata.getTables(names[2], names[1], names[0], tableTypes)) { - while (reflect.next()) { - final String table = analyzer.getUniqueString(reflect, Reflection.TABLE_NAME); - if (ignoredTables.containsKey(table)) { - continue; - } - declared.add(new TableReference( - analyzer.getUniqueString(reflect, Reflection.TABLE_CAT), - analyzer.getUniqueString(reflect, Reflection.TABLE_SCHEM), table, - analyzer.getUniqueString(reflect, Reflection.REMARKS))); - } - } - } - /* - * At this point we got the list of tables requested by the user. Now create the Table objects for each - * specified name. During this iteration, we may discover new tables to analyze because of dependencies - * (foreigner keys). - */ - final List<Table> tableList; - tableList = new ArrayList<>(tableNames.length); - for (final TableReference reference : declared) { - // Adds only the table explicitly required by the user. - tableList.add(analyzer.table(reference, reference.getName(analyzer), null)); - } - /* - * Add queries if any. - */ - for (final ResourceDefinition resource : queries) { - // Optional value should always be present in this context. - tableList.add(analyzer.query(resource.getName(), resource.getQuery().get())); + return ignoredTables.keySet(); + } + + /** + * Creates a model about the specified tables in the database. + * This method shall be invoked exactly once after {@code Database} construction. + * It requires a list of tables to include in the model, but this list should not + * include the dependencies; this method will follow foreigner keys automatically. + * + * <p>The table names shall be qualified names of 1, 2 or 3 components. + * The components are {@code <catalog>.<schema pattern>.<table pattern>} where:</p> + * + * <ul> + * <li>{@code <catalog>}, if present, shall be the name of a catalog as it is stored in the database.</li> + * <li>{@code <schema pattern>}, if present, shall be the pattern of a schema. + * The pattern can use {@code '_'} and {@code '%'} wildcards characters.</li> + * <li>{@code <table pattern>} (mandatory) shall be the pattern of a table. + * The pattern can use {@code '_'} and {@code '%'} wildcards characters.</li> + * </ul> + * + * @param store the data store for which we are creating a model. Used only in case of error. + * @param analyzer the opaque temporary object used for analyzing the database schema. + * @param tableNames qualified name of the tables. Specified by users at construction time. + * @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 spatialInformation statements for fetching SRID, geometry types, <i>etc.</i> + * @throws SQLException if a database error occurred while reading metadata. + * @throws DataStoreException if a logical error occurred while analyzing the database structure. + */ + public final void analyze(final SQLStore store, final Analyzer analyzer, final GenericName[] tableNames, + final ResourceDefinition[] queries, final SchemaModifier customizer, + final InfoStatements spatialInformation) throws Exception + { + if (spatialSchema != null) { + analyzer.spatialInformation = spatialInformation; } + analyzer.customizer = customizer; + final List<Table> tableList = analyzer.findFeatureTables(tableNames, queries); /* * At this point we finished to create the tables explicitly requested by the user. * Register all tables only at this point, because other tables (dependencies) may - * have been analyzed as a side-effect of above loop. + * have been analyzed as a side-effect of above method call. */ for (final Table table : analyzer.finish()) { tablesByNames.add(store, table.featureType.getName(), table); @@ -484,25 +430,6 @@ public class Database<G> extends Syntax { tables = tableList.toArray(Table[]::new); } - /** - * Returns the "TABLE" and "VIEW" keywords for table types, with unsupported keywords omitted. - */ - private static String[] getTableTypes(final DatabaseMetaData metadata) throws SQLException { - final Set<String> types = new HashSet<>(4); - try (ResultSet reflect = metadata.getTableTypes()) { - while (reflect.next()) { - /* - * Derby, HSQLDB and PostgreSQL uses the "TABLE" type, but H2 uses "BASE TABLE". - */ - final String type = reflect.getString(Reflection.TABLE_TYPE); - if ("TABLE".equalsIgnoreCase(type) || "VIEW".equalsIgnoreCase(type) || "BASE TABLE".equalsIgnoreCase(type)) { - types.add(type); - } - } - } - return types.toArray(String[]::new); - } - /** * Helper method for checking if catalog or schema names are consistent. * If an previous (old) name existed, the new name should be the same. @@ -830,8 +757,10 @@ public class Database<G> extends Syntax { */ @Debug final void appendTo(final TreeTable.Node parent) { - for (final Table child : tables) { - child.appendTo(parent); + if (tables != null) { + for (final Table child : tables) { + child.appendTo(parent); + } } } diff --git a/endorsed/src/org.apache.sis.storage.sql/main/org/apache/sis/storage/sql/feature/FeatureAnalyzer.java b/endorsed/src/org.apache.sis.storage.sql/main/org/apache/sis/storage/sql/feature/FeatureAnalyzer.java index 189e1379e6..d817e1940f 100644 --- a/endorsed/src/org.apache.sis.storage.sql/main/org/apache/sis/storage/sql/feature/FeatureAnalyzer.java +++ b/endorsed/src/org.apache.sis.storage.sql/main/org/apache/sis/storage/sql/feature/FeatureAnalyzer.java @@ -80,7 +80,7 @@ abstract class FeatureAnalyzer { * If the primary key use more than one column, then is the class of an array; * it may be an array of primitive type. * - * <p>This field is computed as a side-effect of {@link #createAttributes(FeatureTypeBuilder)}.</p> + * <p>This field is computed as a side-effect of {@link #createAttributes()}.</p> * * @see PrimaryKey#valueClass */ @@ -190,7 +190,6 @@ abstract class FeatureAnalyzer { * The values of those properties are singletons. By contrast, the associations in {@code EXPORT} direction * are multi-valued.</p> * - * @param feature the builder where to add attributes and associations. * @return the columns for attribute values (not including associations). * @throws SQLException if an error occurred while fetching information from the database. * @throws DataStoreException if a logical error occurred while analyzing the relations. diff --git a/endorsed/src/org.apache.sis.storage.sql/main/org/apache/sis/storage/sql/feature/QueryAnalyzer.java b/endorsed/src/org.apache.sis.storage.sql/main/org/apache/sis/storage/sql/feature/QueryAnalyzer.java index 964f2187a7..73bf3dd419 100644 --- a/endorsed/src/org.apache.sis.storage.sql/main/org/apache/sis/storage/sql/feature/QueryAnalyzer.java +++ b/endorsed/src/org.apache.sis.storage.sql/main/org/apache/sis/storage/sql/feature/QueryAnalyzer.java @@ -132,7 +132,7 @@ final class QueryAnalyzer extends FeatureAnalyzer { } /* * Opportunistically search for primary keys. They are not needed by this method, - * but will be needed later by `createAttributes(…)` and other methods. + * but will be needed later by `createAttributes()` and other methods. * This is a "all or nothing" operations: if some primary key columns are missing * from the query, then we cannot have primary key at all for this query. */ 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 fe204d2705..49b1c822e3 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<>("SCHEMA_MODIFIER", SchemaModifier.class); + OptionKey<SchemaModifier> OPTION_KEY = new InternalOptionKey<>("SCHEMA_MODIFIER", SchemaModifier.class); } diff --git a/endorsed/src/org.apache.sis.storage.sql/main/org/apache/sis/storage/sql/feature/Table.java b/endorsed/src/org.apache.sis.storage.sql/main/org/apache/sis/storage/sql/feature/Table.java index 15ec5d6bbe..ff13364df8 100644 --- a/endorsed/src/org.apache.sis.storage.sql/main/org/apache/sis/storage/sql/feature/Table.java +++ b/endorsed/src/org.apache.sis.storage.sql/main/org/apache/sis/storage/sql/feature/Table.java @@ -177,7 +177,7 @@ final class Table extends AbstractFeatureSet { importedKeys = analyzer.getForeignerKeys(Relation.Direction.IMPORT); exportedKeys = analyzer.getForeignerKeys(Relation.Direction.EXPORT); attributes = analyzer.createAttributes(); // Must be after `spec.getForeignerKeys(IMPORT)`. - primaryKey = analyzer.createAssociations(exportedKeys); // Must be after `spec.createAttributes(…)`. + primaryKey = analyzer.createAssociations(exportedKeys); // Must be after `spec.createAttributes()`. featureType = analyzer.buildFeatureType(); hasGeometry = analyzer.hasGeometry; hasRaster = analyzer.hasRaster; diff --git a/endorsed/src/org.apache.sis.storage.sql/main/org/apache/sis/storage/sql/feature/TableAnalyzer.java b/endorsed/src/org.apache.sis.storage.sql/main/org/apache/sis/storage/sql/feature/TableAnalyzer.java index d0ff452623..d31ceddfc2 100644 --- a/endorsed/src/org.apache.sis.storage.sql/main/org/apache/sis/storage/sql/feature/TableAnalyzer.java +++ b/endorsed/src/org.apache.sis.storage.sql/main/org/apache/sis/storage/sql/feature/TableAnalyzer.java @@ -145,7 +145,6 @@ final class TableAnalyzer extends FeatureAnalyzer { * <p><b>Required by this method:</b> {@link #foreignerKeys}.</p> * <p><b>Computed by this method:</b> {@link #primaryKey}, {@link #primaryKeyClass}.</p> * - * @param feature the builder where to add attributes and associations. * @return the columns for attribute values (not including associations). */ @Override diff --git a/endorsed/src/org.apache.sis.storage.sql/test/org/apache/sis/storage/sql/SQLStoreTest.java b/endorsed/src/org.apache.sis.storage.sql/test/org/apache/sis/storage/sql/SQLStoreTest.java index ff57652574..80a484c799 100644 --- a/endorsed/src/org.apache.sis.storage.sql/test/org/apache/sis/storage/sql/SQLStoreTest.java +++ b/endorsed/src/org.apache.sis.storage.sql/test/org/apache/sis/storage/sql/SQLStoreTest.java @@ -161,7 +161,7 @@ public final class SQLStoreTest extends TestOnAllDatabases { * Test on the table again, but with cyclic associations enabled. */ final StorageConnector connector = connector(database); - connector.setOption(SchemaModifier.OPTION, new SchemaModifier() { + connector.setOption(SchemaModifier.OPTION_KEY, new SchemaModifier() { @Override public boolean isCyclicAssociationAllowed(TableReference dependency) { return true; } diff --git a/endorsed/src/org.apache.sis.storage.sql/test/org/apache/sis/storage/sql/feature/SelectionClauseWriterTest.java b/endorsed/src/org.apache.sis.storage.sql/test/org/apache/sis/storage/sql/feature/SelectionClauseWriterTest.java index 457f7d0ff2..d4e304c81b 100644 --- a/endorsed/src/org.apache.sis.storage.sql/test/org/apache/sis/storage/sql/feature/SelectionClauseWriterTest.java +++ b/endorsed/src/org.apache.sis.storage.sql/test/org/apache/sis/storage/sql/feature/SelectionClauseWriterTest.java @@ -75,7 +75,7 @@ public final class SelectionClauseWriterTest extends TestCase implements SchemaM try (TestDatabase db = TestDatabase.create("SelectionClause")) { db.executeSQL(List.of("CREATE TABLE TEST (ALPHA INTEGER, BETA INTEGER, GAMMA INTEGER, PI FLOAT);")); final StorageConnector connector = new StorageConnector(db.source); - connector.setOption(SchemaModifier.OPTION, this); + connector.setOption(SchemaModifier.OPTION_KEY, this); try (DataStore store = new SQLStoreProvider().open(connector)) { table = (Table) store.findResource("TEST"); testSimpleFilter(); diff --git a/endorsed/src/org.apache.sis.storage/main/org/apache/sis/storage/StorageConnector.java b/endorsed/src/org.apache.sis.storage/main/org/apache/sis/storage/StorageConnector.java index d563aca6bb..1b64ee68cf 100644 --- a/endorsed/src/org.apache.sis.storage/main/org/apache/sis/storage/StorageConnector.java +++ b/endorsed/src/org.apache.sis.storage/main/org/apache/sis/storage/StorageConnector.java @@ -899,7 +899,7 @@ public class StorageConnector implements Serializable { * @see DataStoreProvider#probeContent(StorageConnector, Class, Prober) */ public <S> S getStorageAs(final Class<S> type) throws IllegalArgumentException, DataStoreException { - if (views != null && views.isEmpty()) { + if (isClosed()) { throw new IllegalStateException(Resources.format(Resources.Keys.ClosedStorageConnector)); } /* @@ -1836,6 +1836,18 @@ public class StorageConnector implements Serializable { } } + /** + * Returns whether this storage connector has been closed. If this method returns {@code true}, + * then any call to {@link #getStorageAs(Class)} will throw {@link IllegalStateException}. + * + * @return {@code true} if this storage connector is closed. + * + * @since 1.5 + */ + public final boolean isClosed() { + return views != null && views.isEmpty(); + } + /** * Returns the cause of given exception if it exists, or the exception itself otherwise. * This method is invoked in the {@code catch} block of a {@code try} block invoking @@ -1866,6 +1878,9 @@ public class StorageConnector implements Serializable { */ @Override public String toString() { + if (isClosed()) { + return Resources.format(Resources.Keys.ClosedStorageConnector); + } final TreeTable table = new DefaultTreeTable(TableColumn.NAME, TableColumn.VALUE); final TreeTable.Node root = table.getRoot(); root.setValue(TableColumn.NAME, Classes.getShortClassName(this));