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));

Reply via email to