sis-commits mailing list archives

Site index · List index
Message view « Date » · « Thread »
Top « Date » · « Thread »
From desruisse...@apache.org
Subject [sis] branch geoapi-4.0 updated: First SQLStore version capable to return FeatureTypes (not yet complete).
Date Wed, 11 Jul 2018 17:03:08 GMT
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


The following commit(s) were added to refs/heads/geoapi-4.0 by this push:
     new 3e356b0  First SQLStore version capable to return FeatureTypes (not yet complete).
3e356b0 is described below

commit 3e356b0dd4ad91212c5ed677b7c0457250699e9c
Author: Martin Desruisseaux <martin.desruisseaux@geomatys.com>
AuthorDate: Wed Jul 11 19:01:23 2018 +0200

    First SQLStore version capable to return FeatureTypes (not yet complete).
---
 .../main/java/org/apache/sis/util/iso/Names.java   |  25 ++-
 .../java/org/apache/sis/parameter/Parameters.java  |   2 +
 .../org/apache/sis/storage/netcdf/NetcdfStore.java |  48 ++---
 .../apache/sis/internal/sql/feature/Analyzer.java  |  20 ++-
 .../apache/sis/internal/sql/feature/Database.java  |  76 ++++----
 .../apache/sis/internal/sql/feature/MetaModel.java | 115 ------------
 .../apache/sis/internal/sql/feature/Relation.java  |  47 ++++-
 .../apache/sis/internal/sql/feature/Resources.java |  19 +-
 .../sis/internal/sql/feature/Resources.properties  |   3 +
 .../internal/sql/feature/Resources_fr.properties   |   3 +
 .../org/apache/sis/internal/sql/feature/Table.java |  81 +++++++--
 .../{TableName.java => TableReference.java}        |  37 ++--
 .../sis/internal/sql/feature/package-info.java     |   1 +
 .../java/org/apache/sis/storage/sql/SQLStore.java  | 156 +++++++++++++++-
 .../apache/sis/storage/sql/SQLStoreProvider.java   | 199 ++++++++++++++++++++-
 .../org/apache/sis/storage/sql/package-info.java   |   1 +
 .../org/apache/sis/storage/sql/SQLStoreTest.java   |  13 +-
 17 files changed, 613 insertions(+), 233 deletions(-)

diff --git a/core/sis-metadata/src/main/java/org/apache/sis/util/iso/Names.java b/core/sis-metadata/src/main/java/org/apache/sis/util/iso/Names.java
index e8e921f..f381c38 100644
--- a/core/sis-metadata/src/main/java/org/apache/sis/util/iso/Names.java
+++ b/core/sis-metadata/src/main/java/org/apache/sis/util/iso/Names.java
@@ -72,7 +72,7 @@ import static org.apache.sis.util.ArgumentChecks.ensureNonNull;
  * </table></blockquote>
  *
  * @author  Martin Desruisseaux (Geomatys)
- * @version 0.8
+ * @version 1.0
  *
  * @see DefaultNameFactory
  * @see DefaultNameSpace
@@ -114,7 +114,7 @@ public final class Names extends Static {
      * around the {@linkplain DefaultNameSpace#DEFAULT_SEPARATOR default separator}, which is {@code ":"}.
      *
      * @param  namespace   the namespace, or {@code null} for the global namespace.
-     * @param  separator   the separator between the namespace and the scoped name, or {@code null}
+     * @param  separator   the separator between the namespace and the generic name, or {@code null}
      *                     for the {@linkplain DefaultNameSpace#DEFAULT_SEPARATOR default separator}.
      * @param  scopedName  the name to parse using {@code ':'} as the separator between components.
      * @return a local or scoped name in the given namespace.
@@ -128,7 +128,26 @@ public final class Names extends Static {
     }
 
     /**
-     * Constructs a scoped name as the concatenation of the given generic name with a single character sequence.
+     * Creates a local or scoped name from an array of parsed names. This method returns a local name if the
+     * length of the {@code parsedNames} array is 1, or a scoped named if the length of the array is 2 or more.
+     *
+     * @param  namespace    the namespace, or {@code null} for the global namespace.
+     * @param  separator    the separator between the namespace and the generic name, or {@code null}
+     *                      for the {@linkplain DefaultNameSpace#DEFAULT_SEPARATOR default separator}.
+     * @param  parsedNames  the local names as an array of {@link String} or {@link InternationalString} instances.
+     *                      This array shall contain at least one element.
+     * @return the generic name for the given parsed names.
+     *
+     * @since 1.0
+     */
+    public static GenericName createGenericName(final CharSequence namespace, final String separator, final CharSequence... parsedNames) {
+        ensureNonNull("parsedNames", parsedNames);
+        final NameFactory factory = DefaultFactories.forBuildin(NameFactory.class);
+        return factory.createGenericName(createNameSpace(factory, namespace, separator), parsedNames);
+    }
+
+    /**
+     * Creates a scoped name as the concatenation of the given generic name with a single character sequence.
      * The scope of the new name will be the scope of the {@code path} argument.
      * The tail is a local name created from the given character sequence.
      *
diff --git a/core/sis-referencing/src/main/java/org/apache/sis/parameter/Parameters.java b/core/sis-referencing/src/main/java/org/apache/sis/parameter/Parameters.java
index 495fddd..fdf1f3b 100644
--- a/core/sis-referencing/src/main/java/org/apache/sis/parameter/Parameters.java
+++ b/core/sis-referencing/src/main/java/org/apache/sis/parameter/Parameters.java
@@ -32,6 +32,7 @@ import org.apache.sis.measure.Range;
 import org.apache.sis.measure.NumberRange;
 import org.apache.sis.measure.MeasurementRange;
 import org.apache.sis.referencing.IdentifiedObjects;
+import org.apache.sis.util.UnconvertibleObjectException;
 import org.apache.sis.util.ArgumentChecks;
 import org.apache.sis.util.ObjectConverters;
 import org.apache.sis.util.resources.Errors;
@@ -487,6 +488,7 @@ public abstract class Parameters implements ParameterValueGroup, Cloneable {
      * @return the requested parameter value if it exists, or the {@linkplain DefaultParameterDescriptor#getDefaultValue()
      *         default value} otherwise (which may be {@code null}).
      * @throws ParameterNotFoundException if the given {@code parameter} name or alias is not legal for this group.
+     * @throws UnconvertibleObjectException if the parameter value can not be converted to the expected type.
      *
      * @see #getMandatoryValue(ParameterDescriptor)
      * @see #getOrCreate(ParameterDescriptor)
diff --git a/storage/sis-netcdf/src/main/java/org/apache/sis/storage/netcdf/NetcdfStore.java b/storage/sis-netcdf/src/main/java/org/apache/sis/storage/netcdf/NetcdfStore.java
index 077307c..b9c5597 100644
--- a/storage/sis-netcdf/src/main/java/org/apache/sis/storage/netcdf/NetcdfStore.java
+++ b/storage/sis-netcdf/src/main/java/org/apache/sis/storage/netcdf/NetcdfStore.java
@@ -102,28 +102,6 @@ public class NetcdfStore extends DataStore implements Aggregate {
     }
 
     /**
-     * Returns information about the dataset as a whole. The returned metadata object can contain information
-     * such as the spatiotemporal extent of the dataset, contact information about the creator or distributor,
-     * data quality, usage constraints and more.
-     *
-     * @return information about the dataset.
-     * @throws DataStoreException if an error occurred while reading the data.
-     */
-    @Override
-    public synchronized Metadata getMetadata() throws DataStoreException {
-        if (metadata == null) try {
-            final MetadataReader reader = new MetadataReader(decoder);
-            metadata = reader.read();
-            if (metadata instanceof ModifiableMetadata) {
-                ((ModifiableMetadata) metadata).apply(ModifiableMetadata.State.FINAL);
-            }
-        } catch (IOException e) {
-            throw new DataStoreException(e);
-        }
-        return metadata;
-    }
-
-    /**
      * Returns the parameters used to open this netCDF data store.
      * If non-null, the parameters are described by {@link NetcdfStoreProvider#getOpenParameters()} and contains at
      * least a parameter named {@value org.apache.sis.storage.DataStoreProvider#LOCATION} with a {@link URI} value.
@@ -159,9 +137,31 @@ public class NetcdfStore extends DataStore implements Aggregate {
     }
 
     /**
-     * Returns the resources (features or coverages) in this netCDF file.
+     * Returns information about the dataset as a whole. The returned metadata object can contain information
+     * such as the spatiotemporal extent of the dataset, contact information about the creator or distributor,
+     * data quality, usage constraints and more.
+     *
+     * @return information about the dataset.
+     * @throws DataStoreException if an error occurred while reading the data.
+     */
+    @Override
+    public synchronized Metadata getMetadata() throws DataStoreException {
+        if (metadata == null) try {
+            final MetadataReader reader = new MetadataReader(decoder);
+            metadata = reader.read();
+            if (metadata instanceof ModifiableMetadata) {
+                ((ModifiableMetadata) metadata).apply(ModifiableMetadata.State.FINAL);
+            }
+        } catch (IOException e) {
+            throw new DataStoreException(e);
+        }
+        return metadata;
+    }
+
+    /**
+     * Returns the resources (features or coverages) in this netCDF store.
      *
-     * @return children resources that are components of this netCDF.
+     * @return children resources that are components of this netCDF store.
      * @throws DataStoreException if an error occurred while fetching the components.
      *
      * @since 0.8
diff --git a/storage/sis-sqlstore/src/main/java/org/apache/sis/internal/sql/feature/Analyzer.java b/storage/sis-sqlstore/src/main/java/org/apache/sis/internal/sql/feature/Analyzer.java
index 2a86ff0..bf4b8ad 100644
--- a/storage/sis-sqlstore/src/main/java/org/apache/sis/internal/sql/feature/Analyzer.java
+++ b/storage/sis-sqlstore/src/main/java/org/apache/sis/internal/sql/feature/Analyzer.java
@@ -27,6 +27,8 @@ import java.sql.SQLException;
 import java.sql.DatabaseMetaData;
 import org.opengis.util.InternationalString;
 import org.apache.sis.internal.metadata.sql.Dialect;
+import org.apache.sis.util.logging.WarningListeners;
+import org.apache.sis.storage.DataStore;
 
 
 /**
@@ -70,14 +72,14 @@ final class Analyzer {
      * The value tells whether the table in the relation has already been analyzed.
      * Only the catalog, schema and table names are taken in account for the keys in this map.
      */
-    private final Map<TableName,Boolean> dependencies;
+    private final Map<TableReference,Boolean> dependencies;
 
     /**
      * Iterator over {@link #dependencies} entries, or {@code null} if none.
      * This field may be set to {@code null} in the middle of an iteration if
      * the {@link #dependencies} map is modified concurrently.
      */
-    private Iterator<Map.Entry<TableName,Boolean>> depIter;
+    private Iterator<Map.Entry<TableReference,Boolean>> depIter;
 
     /**
      * Warnings found while analyzing a database structure. Duplicated warnings are omitted.
@@ -85,10 +87,16 @@ final class Analyzer {
     private final Set<InternationalString> warnings;
 
     /**
+     * Where to send warnings after we finished to collect them, or when reading the feature instances.
+     */
+    final WarningListeners<DataStore> listeners;
+
+    /**
      * Creates a new analyzer for the database described by given metadata.
      */
-    Analyzer(final DatabaseMetaData metadata) throws SQLException {
+    Analyzer(final DatabaseMetaData metadata, final WarningListeners<DataStore> listeners) throws SQLException {
         this.metadata  = metadata;
+        this.listeners = listeners;
         this.escape    = metadata.getSearchStringEscape();
         this.functions = new SpatialFunctions(metadata);
         /*
@@ -166,7 +174,7 @@ final class Analyzer {
      * table has already been analyzed, then this method does nothing. Otherwise if the table has not yet
      * been analyzed, then this method remembers that the foreigner table will need to be analyzed later.
      */
-    final void addDependency(final TableName foreigner) {
+    final void addDependency(final TableReference foreigner) {
         if (dependencies.putIfAbsent(foreigner, Boolean.FALSE) == null) {
             depIter = null;         // Will need to fetch a new iterator.
         }
@@ -175,12 +183,12 @@ final class Analyzer {
     /**
      * Returns the next table to visit, or {@code null} if there is no more.
      */
-    final TableName nextDependency() {
+    final TableReference nextDependency() {
         if (depIter == null) {
             depIter = dependencies.entrySet().iterator();
         }
         while (depIter.hasNext()) {
-            final Map.Entry<TableName,Boolean> e = depIter.next();
+            final Map.Entry<TableReference,Boolean> e = depIter.next();
             if (!e.setValue(Boolean.TRUE)) {
                 return e.getKey();
             }
diff --git a/storage/sis-sqlstore/src/main/java/org/apache/sis/internal/sql/feature/Database.java b/storage/sis-sqlstore/src/main/java/org/apache/sis/internal/sql/feature/Database.java
index 5599554..6f04381 100644
--- a/storage/sis-sqlstore/src/main/java/org/apache/sis/internal/sql/feature/Database.java
+++ b/storage/sis-sqlstore/src/main/java/org/apache/sis/internal/sql/feature/Database.java
@@ -22,19 +22,23 @@ import java.sql.Connection;
 import java.sql.DatabaseMetaData;
 import java.sql.ResultSet;
 import java.sql.SQLException;
+import org.opengis.util.LocalName;
+import org.opengis.util.GenericName;
+import org.apache.sis.util.ArraysExt;
 import org.apache.sis.internal.metadata.sql.Reflection;
+import org.apache.sis.storage.DataStore;
 import org.apache.sis.storage.sql.SQLStore;
+import org.apache.sis.storage.FeatureSet;
 import org.apache.sis.storage.FeatureNaming;
 import org.apache.sis.storage.DataStoreException;
-import org.apache.sis.storage.IllegalNameException;
-
-// Branch-dependent imports
-import org.opengis.feature.FeatureType;
+import org.apache.sis.util.logging.WarningListeners;
 
 
 /**
  * Represent the structure of features in the database.
  * The work done here is similar to reverse engineering.
+ * Instances of this class are thread-safe after construction;
+ * if the database schema changes, then a new {@code Database} instance shall be created.
  *
  * @author  Johann Sorel (Geomatys)
  * @author  Martin Desruisseaux (Geomatys)
@@ -44,9 +48,15 @@ import org.opengis.feature.FeatureType;
  */
 public final class Database {
     /**
-     * All tables known to this {@code Database}. Each table contains a {@link Table#featureType}.
+     * The SQL wildcard for any characters. A string containing only this wildcard
+     * means "any value" and can sometime be replaced by {@code null}.
+     */
+    public static final String WILDCARD = "%";
+
+    /**
+     * All tables known to this {@code Database}.
      */
-    private final FeatureNaming<Table> tables;
+    public final FeatureNaming<FeatureSet> tables;
 
     /**
      * Functions that may be specific to the geospatial database in use.
@@ -59,23 +69,35 @@ public final class Database {
      * but this list should not include the dependencies; this constructor
      * will follow foreigner keys automatically.
      *
-     * @param  store          the data store for which we are creating a model. Used only in case of error.
-     * @param  connection     connection to the database.
-     * @param  catalog        name of a catalog as it is stored in the database, or {@code null} for any catalog.
-     * @param  schemaPattern  pattern (with {@code '_'} and {@code '%'} wildcards) of a schema, or {@code null} for any.
-     * @param  tablePatterns  pattern (with {@code '_'} and {@code '%'} wildcards) of tables to include in the model.
+     * <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  connection   connection to the database.
+     * @param  tableNames   qualified name of the tables.
+     * @param  listeners    where to send the warnings.
      * @throws SQLException if a database error occurred while reading metadata.
      * @throws DataStoreException if a logical error occurred while analyzing the database structure.
      */
-    public Database(final SQLStore store, final Connection connection, final String catalog,
-            final String schemaPattern, final String[] tablePatterns)
-            throws SQLException, DataStoreException
+    public Database(final SQLStore store, final Connection connection, final GenericName[] tableNames,
+            final WarningListeners<DataStore> listeners) throws SQLException, DataStoreException
     {
         tables = new FeatureNaming<>();
-        final Analyzer analyzer = new Analyzer(connection.getMetaData());
+        final Analyzer analyzer = new Analyzer(connection.getMetaData(), listeners);
         final String[] tableTypes = getTableTypes(analyzer.metadata);
-        for (final String tablePattern : tablePatterns) {
-            try (ResultSet reflect = analyzer.metadata.getTables(catalog, schemaPattern, tablePattern, tableTypes)) {
+        for (final GenericName tableName : tableNames) {
+            String[] names = tableName.getParsedNames().stream().map(LocalName::toString).toArray(String[]::new);
+            ArraysExt.reverse(names);               // Reorganize in (catalog, schemaPattern, tablePattern) order.
+            names = ArraysExt.resize(names, 3);     // Pad with null values if necessary.
+            try (ResultSet reflect = analyzer.metadata.getTables(names[2], names[1], names[0], tableTypes)) {
                 while (reflect.next()) {
                     final String table = reflect.getString(Reflection.TABLE_NAME);
                     if (analyzer.isIgnoredTable(table)) {
@@ -83,17 +105,17 @@ public final class Database {
                     }
                     String remarks = reflect.getString(Reflection.REMARKS);
                     remarks = (remarks != null) ? remarks.trim() : "";      // Empty string means that we verified that there is no remarks.
-                    analyzer.addDependency(new TableName(remarks,           // Opportunistically use the 'name' field for storing remarks.
+                    analyzer.addDependency(new TableReference(
                             reflect.getString(Reflection.TABLE_CAT),
                             reflect.getString(Reflection.TABLE_SCHEM),
-                            table));
+                            table, remarks));
                 }
             }
         }
-        TableName dependency;
+        TableReference dependency;
         while ((dependency = analyzer.nextDependency()) != null) {
             final Table table = new Table(analyzer, dependency);
-            tables.add(store, table.featureType.getName(), table);
+            tables.add(store, table.getType().getName(), table);
         }
         functions = analyzer.functions;
     }
@@ -113,16 +135,4 @@ public final class Database {
         }
         return types.toArray(new String[types.size()]);
     }
-
-    /**
-     * Returns the feature type of the given name.
-     *
-     * @param  store  the data store for which we are created the model. Used only in case of error.
-     * @param  name   name of the feature type to fetch.
-     * @return the feature type of the given name.
-     * @throws IllegalNameException if the given name is unknown or ambiguous.
-     */
-    public FeatureType getFeatureType(final SQLStore store, final String name) throws IllegalNameException {
-        return tables.get(store, name).featureType;
-    }
 }
diff --git a/storage/sis-sqlstore/src/main/java/org/apache/sis/internal/sql/feature/MetaModel.java b/storage/sis-sqlstore/src/main/java/org/apache/sis/internal/sql/feature/MetaModel.java
deleted file mode 100644
index 675ea71..0000000
--- a/storage/sis-sqlstore/src/main/java/org/apache/sis/internal/sql/feature/MetaModel.java
+++ /dev/null
@@ -1,115 +0,0 @@
-/*
- * Licensed to the Apache Software Foundation (ASF) under one or more
- * contributor license agreements.  See the NOTICE file distributed with
- * this work for additional information regarding copyright ownership.
- * The ASF licenses this file to You under the Apache License, Version 2.0
- * (the "License"); you may not use this file except in compliance with
- * the License.  You may obtain a copy of the License at
- *
- *     http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-package org.apache.sis.internal.sql.feature;
-
-import java.util.Collection;
-import java.sql.DatabaseMetaData;
-import org.apache.sis.util.Debug;
-import org.apache.sis.util.resources.Vocabulary;
-import org.apache.sis.util.collection.TreeTable;
-import org.apache.sis.util.collection.TableColumn;
-import org.apache.sis.util.collection.DefaultTreeTable;
-
-
-/**
- * Description about a database entity (schema, table, relation, <i>etc</i>).
- * Information provided by subclasses are inferred from {@link DatabaseMetaData}
- * and stored as {@link org.apache.sis.feature} classes.
- *
- * @author  Johann Sorel (Geomatys)
- * @author  Martin Desruisseaux (Geomatys)
- * @version 1.0
- * @since   1.0
- * @module
- */
-abstract class MetaModel {
-    /**
-     * The entity name (schema, table, <i>etc</i>).
-     * May be null, for example if this is the primary key name and that name is unspecified.
-     */
-    final String name;
-
-    /**
-     * Creates a new object describing a database entity (schema, table, <i>etc</i>).
-     *
-     * @param  name  the database entity name, or {@code null} if unspecified.
-     */
-    MetaModel(final String name) {
-        this.name = name;
-    }
-
-    /**
-     * Creates a tree representation of this object for debugging purpose.
-     * The default implementation adds a single node with the {@link #name} of this entity
-     * and returns that node. Subclasses can override this method for appending additional
-     * information.
-     *
-     * @param  parent  the parent node where to add the tree representation.
-     * @return the node added by this method.
-     */
-    @Debug
-    TreeTable.Node appendTo(final TreeTable.Node parent) {
-        return newChild(parent, name);
-    }
-
-    /**
-     * Adds a child of the given name to the given parent node.
-     * This is a convenience method for {@link #appendTo(TreeTable.Node)} implementations.
-     *
-     * @param  parent  the node where to add a child.
-     * @param  name    the name to assign to the child.
-     * @return the child added to the parent.
-     */
-    @Debug
-    static TreeTable.Node newChild(final TreeTable.Node parent, final String name) {
-        final TreeTable.Node child = parent.newChild();
-        child.setValue(TableColumn.NAME, (name != null) ? name : Vocabulary.format(Vocabulary.Keys.Unnamed));
-        return child;
-    }
-
-    /**
-     * Appends all children to the given parent. The children are added under a node of the given name.
-     * If the children collection is empty, then this method does nothing.
-     *
-     * @param  parent    the node where to add children.
-     * @param  name      the name of a node to insert between the parent and the children, or {@code null} if none.
-     * @param  children  the children to add, or an empty collection if none.
-     */
-    @Debug
-    static void appendAll(TreeTable.Node parent, final String name, final Collection<? extends MetaModel> children) {
-        if (!children.isEmpty()) {
-            if (name != null) {
-                parent = newChild(parent, name);
-            }
-            for (final MetaModel child : children) {
-                child.appendTo(parent);
-            }
-        }
-    }
-
-    /**
-     * Formats a graphical representation of this object for debugging purpose. This representation can
-     * be printed to the {@linkplain System#out standard output stream} (for example) if the output device
-     * uses a monospaced font and supports Unicode.
-     */
-    @Override
-    public final String toString() {
-        final DefaultTreeTable table = new DefaultTreeTable(TableColumn.NAME);
-        appendTo(table.getRoot());
-        return table.toString();
-    }
-}
diff --git a/storage/sis-sqlstore/src/main/java/org/apache/sis/internal/sql/feature/Relation.java b/storage/sis-sqlstore/src/main/java/org/apache/sis/internal/sql/feature/Relation.java
index 8f45ef7..bf2dfe8 100644
--- a/storage/sis-sqlstore/src/main/java/org/apache/sis/internal/sql/feature/Relation.java
+++ b/storage/sis-sqlstore/src/main/java/org/apache/sis/internal/sql/feature/Relation.java
@@ -23,11 +23,13 @@ import java.util.Objects;
 import java.sql.ResultSet;
 import java.sql.SQLException;
 import java.sql.DatabaseMetaData;
-import org.apache.sis.util.Debug;
-import org.apache.sis.util.collection.TreeTable;
 import org.apache.sis.internal.util.CollectionsExt;
 import org.apache.sis.internal.metadata.sql.Reflection;
 import org.apache.sis.storage.DataStoreContentException;
+import org.apache.sis.util.collection.DefaultTreeTable;
+import org.apache.sis.util.collection.TableColumn;
+import org.apache.sis.util.collection.TreeTable;
+import org.apache.sis.util.Debug;
 
 
 /**
@@ -51,7 +53,7 @@ import org.apache.sis.storage.DataStoreContentException;
  * @since   1.0
  * @module
  */
-final class Relation extends TableName {
+final class Relation extends TableReference {
     /**
      * Whether another table is <em>using</em> or is <em>used by</em> the table containing the {@link Relation}.
      */
@@ -147,10 +149,10 @@ final class Relation extends TableName {
      * least one row, unless an exception occurs.</p>
      */
     Relation(final Direction dir, final ResultSet reflect) throws SQLException, DataStoreContentException {
-        super(reflect.getString(dir.name),
-              reflect.getString(dir.catalog),
+        super(reflect.getString(dir.catalog),
               reflect.getString(dir.schema),
-              reflect.getString(dir.table));
+              reflect.getString(dir.table),
+              reflect.getString(dir.name));
 
         final Map<String,String> m = new LinkedHashMap<>();
         boolean cascade = false;
@@ -184,15 +186,44 @@ final class Relation extends TableName {
     }
 
     /**
+     * Adds a child of the given name to the given parent node.
+     * This is a convenience method for {@code toString()} implementations.
+     *
+     * @param  parent  the node where to add a child.
+     * @param  name    the name to assign to the child.
+     * @return the child added to the parent.
+     */
+    @Debug
+    static TreeTable.Node newChild(final TreeTable.Node parent, final String name) {
+        final TreeTable.Node child = parent.newChild();
+        child.setValue(TableColumn.NAME, name);
+        return child;
+    }
+
+    /**
      * Creates a tree representation of this relation for debugging purpose.
+     *
+     * @param  parent  the parent node where to add the tree representation.
+     * @return the node added by this method.
      */
     @Debug
-    @Override
     TreeTable.Node appendTo(final TreeTable.Node parent) {
-        final TreeTable.Node node = super.appendTo(parent);
+        final TreeTable.Node node = newChild(parent, remarks);
         for (final Map.Entry<String,String> e : columns.entrySet()) {
             newChild(node, e.getValue() + " → " + e.getKey());
         }
         return node;
     }
+
+    /**
+     * Formats a graphical representation of this object for debugging purpose. This representation can
+     * be printed to the {@linkplain System#out standard output stream} (for example) if the output device
+     * uses a monospaced font and supports Unicode.
+     */
+    @Override
+    public String toString() {
+        final DefaultTreeTable table = new DefaultTreeTable(TableColumn.NAME);
+        appendTo(table.getRoot());
+        return table.toString();
+    }
 }
diff --git a/storage/sis-sqlstore/src/main/java/org/apache/sis/internal/sql/feature/Resources.java b/storage/sis-sqlstore/src/main/java/org/apache/sis/internal/sql/feature/Resources.java
index 0785e03..a56b2a9 100644
--- a/storage/sis-sqlstore/src/main/java/org/apache/sis/internal/sql/feature/Resources.java
+++ b/storage/sis-sqlstore/src/main/java/org/apache/sis/internal/sql/feature/Resources.java
@@ -61,14 +61,29 @@ public final class Resources extends IndexedResourceBundle {
         }
 
         /**
+         * Provider of connections to the database.
+         */
+        public static final short DataSource = 1;
+
+        /**
          * Unexpected duplication of “{0}” entity named “{1}”.
          */
-        public static final short DuplicatedEntity_2 = 1;
+        public static final short DuplicatedEntity_2 = 5;
+
+        /**
+         * “{0}” is not a valid qualified name for a table.
+         */
+        public static final short IllegalQualifiedName_1 = 3;
+
+        /**
+         * Table names, optionally with their schemas and catalogs.
+         */
+        public static final short QualifiedTableNames = 2;
 
         /**
          * No mapping from SQL type “{0}” to a Java class.
          */
-        public static final short UnknownType_1 = 2;
+        public static final short UnknownType_1 = 4;
     }
 
     /**
diff --git a/storage/sis-sqlstore/src/main/java/org/apache/sis/internal/sql/feature/Resources.properties b/storage/sis-sqlstore/src/main/java/org/apache/sis/internal/sql/feature/Resources.properties
index b694616..4ca1b07 100644
--- a/storage/sis-sqlstore/src/main/java/org/apache/sis/internal/sql/feature/Resources.properties
+++ b/storage/sis-sqlstore/src/main/java/org/apache/sis/internal/sql/feature/Resources.properties
@@ -19,5 +19,8 @@
 # Resources in this file are for "sis-sqlstore" usage only and should not be used by any other module.
 # For resources shared by all modules in the Apache SIS project, see "org.apache.sis.util.resources" package.
 #
+DataSource                        = Provider of connections to the database.
 DuplicatedEntity_2                = Unexpected duplication of \u201c{0}\u201d entity named \u201c{1}\u201d.
+IllegalQualifiedName_1            = \u201c{0}\u201d is not a valid qualified name for a table.
+QualifiedTableNames               = Table names, optionally with their schemas and catalogs.
 UnknownType_1                     = No mapping from SQL type \u201c{0}\u201d to a Java class.
diff --git a/storage/sis-sqlstore/src/main/java/org/apache/sis/internal/sql/feature/Resources_fr.properties b/storage/sis-sqlstore/src/main/java/org/apache/sis/internal/sql/feature/Resources_fr.properties
index 274e752..d0f55bd 100644
--- a/storage/sis-sqlstore/src/main/java/org/apache/sis/internal/sql/feature/Resources_fr.properties
+++ b/storage/sis-sqlstore/src/main/java/org/apache/sis/internal/sql/feature/Resources_fr.properties
@@ -24,5 +24,8 @@
 #   U+202F NARROW NO-BREAK SPACE  before  ; ! and ?
 #   U+00A0 NO-BREAK SPACE         before  :
 #
+DataSource                        = Fournisseur de connexions \u00e0 la base de donn\u00e9es.
 DuplicatedEntity_2                = Doublon inattendu d\u2019une entit\u00e9 \u00ab\u202f{0}\u202f\u00bb nomm\u00e9e \u00ab\u202f{1}\u202f\u00bb.
+IllegalQualifiedName_1            = \u00ab\u202f{0}\u202f\u00bb n\u2019est pas un nom qualifi\u00e9 de table valide.
+QualifiedTableNames               = Noms de tables, optionnellemment avec leurs noms de sch\u00e9mas et catalogues.
 UnknownType_1                     = Pas de correspondance entre le type SQL \u00ab\u202f{0}\u202f\u00bb et une classe Java.
diff --git a/storage/sis-sqlstore/src/main/java/org/apache/sis/internal/sql/feature/Table.java b/storage/sis-sqlstore/src/main/java/org/apache/sis/internal/sql/feature/Table.java
index 8a66477..464efb7 100644
--- a/storage/sis-sqlstore/src/main/java/org/apache/sis/internal/sql/feature/Table.java
+++ b/storage/sis-sqlstore/src/main/java/org/apache/sis/internal/sql/feature/Table.java
@@ -22,11 +22,11 @@ import java.util.Set;
 import java.util.HashSet;
 import java.util.List;
 import java.util.ArrayList;
+import java.util.Collection;
 import java.sql.ResultSet;
 import java.sql.SQLException;
+import java.util.stream.Stream;
 import org.opengis.referencing.crs.CoordinateReferenceSystem;
-import org.apache.sis.util.Debug;
-import org.apache.sis.util.collection.TreeTable;
 import org.apache.sis.feature.builder.AttributeRole;
 import org.apache.sis.feature.builder.AttributeTypeBuilder;
 import org.apache.sis.feature.builder.FeatureTypeBuilder;
@@ -34,12 +34,19 @@ import org.apache.sis.storage.DataStoreContentException;
 import org.apache.sis.internal.feature.Geometries;
 import org.apache.sis.internal.metadata.sql.Reflection;
 import org.apache.sis.internal.metadata.sql.SQLUtilities;
+import org.apache.sis.internal.storage.AbstractFeatureSet;
 import org.apache.sis.internal.util.CollectionsExt;
+import org.apache.sis.storage.DataStoreException;
+import org.apache.sis.util.collection.DefaultTreeTable;
+import org.apache.sis.util.collection.TableColumn;
+import org.apache.sis.util.collection.TreeTable;
+import org.apache.sis.util.Debug;
 
 // Branch-dependent imports
 import org.opengis.feature.FeatureType;
 import org.opengis.feature.AttributeType;
 import org.opengis.feature.FeatureAssociationRole;
+import org.opengis.feature.Feature;
 
 
 /**
@@ -54,13 +61,13 @@ import org.opengis.feature.FeatureAssociationRole;
  * @since   1.0
  * @module
  */
-final class Table extends MetaModel {
+final class Table extends AbstractFeatureSet {
     /**
      * The structure of this table represented as a feature. Each feature attribute is a table column,
      * except synthetic attributes like "sis:identifier". The feature may also contain associations
      * inferred from foreigner keys that are not immediately apparent in the table.
      */
-    final FeatureType featureType;
+    private final FeatureType featureType;
 
     /**
      * The primary key of this table. The boolean values tells whether the column
@@ -85,14 +92,11 @@ final class Table extends MetaModel {
      * The table is identified by {@code id}, which contains a (catalog, schema, name) tuple.
      * The catalog and schema parts are optional and can be null, but the table is mandatory.
      *
-     * <p>The {@link TableName#name} field is opportunistically used for storing optional remarks
-     * (this may change in any future version).</p>
-     *
      * @param  analyzer  helper functions, e.g. for converting SQL types to Java types.
      * @param  id        the catalog, schema and table name of the table to analyze.
      */
-    Table(final Analyzer analyzer, final TableName id) throws SQLException, DataStoreContentException {
-        super(id.table);
+    Table(final Analyzer analyzer, final TableReference id) throws SQLException, DataStoreContentException {
+        super(analyzer.listeners);
         final String tableEsc  = analyzer.escape(id.table);
         final String schemaEsc = analyzer.escape(id.schema);
         /*
@@ -203,7 +207,7 @@ final class Table extends MetaModel {
             ftb.setNameSpace(id.schema);
         }
         ftb.setName(id.table);
-        String remarks = id.name;
+        String remarks = id.remarks;
         if (remarks == null) {
             try (ResultSet reflect = analyzer.metadata.getTables(id.catalog, schemaEsc, tableEsc, null)) {
                 while (reflect.next()) {
@@ -227,16 +231,65 @@ final class Table extends MetaModel {
     }
 
     /**
-     * Creates a tree representation of this object for debugging purpose.
+     * Appends all children to the given parent. The children are added under a node of the given name.
+     * If the children collection is empty, then this method does nothing.
+     *
+     * @param  parent    the node where to add children.
+     * @param  name      the name of a node to insert between the parent and the children.
+     * @param  children  the children to add, or an empty collection if none.
+     */
+    @Debug
+    private static void appendAll(TreeTable.Node parent, final String name, final Collection<Relation> children) {
+        if (!children.isEmpty()) {
+            parent = Relation.newChild(parent, name);
+            for (final Relation child : children) {
+                child.appendTo(parent);
+            }
+        }
+    }
+
+    /**
+     * Creates a tree representation of this table for debugging purpose.
      *
      * @param  parent  the parent node where to add the tree representation.
      */
     @Debug
-    @Override
-    TreeTable.Node appendTo(final TreeTable.Node parent) {
-        final TreeTable.Node node = super.appendTo(parent);
+    final TreeTable.Node appendTo(final TreeTable.Node parent) {
+        final TreeTable.Node node = Relation.newChild(parent, featureType.getName().toString());
         appendAll(parent, "Imported Keys", importedKeys);
         appendAll(parent, "Exported Keys", exportedKeys);
         return node;
     }
+
+    /**
+     * Formats a graphical representation of this object for debugging purpose. This representation can
+     * be printed to the {@linkplain System#out standard output stream} (for example) if the output device
+     * uses a monospaced font and supports Unicode.
+     */
+    @Override
+    public String toString() {
+        final DefaultTreeTable table = new DefaultTreeTable(TableColumn.NAME);
+        appendTo(table.getRoot());
+        return table.toString();
+    }
+
+    /**
+     * Returns the feature type inferred from the database structure analysis.
+     */
+    @Override
+    public FeatureType getType() {
+        return featureType;
+    }
+
+    /**
+     * Returns a stream of all features contained in this dataset.
+     *
+     * @param  parallel  {@code true} for a parallel stream (if supported), or {@code false} for a sequential stream.
+     * @return all features contained in this dataset.
+     * @throws DataStoreException if an error occurred while creating the stream.
+     */
+    @Override
+    public Stream<Feature> features(boolean parallel) throws DataStoreException {
+        throw new UnsupportedOperationException("Not supported yet.");  // TODO
+    }
 }
diff --git a/storage/sis-sqlstore/src/main/java/org/apache/sis/internal/sql/feature/TableName.java b/storage/sis-sqlstore/src/main/java/org/apache/sis/internal/sql/feature/TableReference.java
similarity index 60%
rename from storage/sis-sqlstore/src/main/java/org/apache/sis/internal/sql/feature/TableName.java
rename to storage/sis-sqlstore/src/main/java/org/apache/sis/internal/sql/feature/TableReference.java
index 41dc99a..15065fd 100644
--- a/storage/sis-sqlstore/src/main/java/org/apache/sis/internal/sql/feature/TableName.java
+++ b/storage/sis-sqlstore/src/main/java/org/apache/sis/internal/sql/feature/TableReference.java
@@ -17,11 +17,11 @@
 package org.apache.sis.internal.sql.feature;
 
 import java.util.Objects;
+import org.apache.sis.storage.sql.SQLStoreProvider;
 
 
 /**
  * A (catalog, schema, table) name tuple, which can be used as keys in hash map.
- * The {@link #name} field is for informative purpose only and ignored by this class.
  *
  * @author  Johann Sorel (Geomatys)
  * @author  Martin Desruisseaux (Geomatys)
@@ -29,7 +29,7 @@ import java.util.Objects;
  * @since   1.0
  * @module
  */
-class TableName extends MetaModel {
+class TableReference {
     /**
      * The catalog, schema and table name of a table.
      * The table name is mandatory, but the schema and catalog names may be null.
@@ -37,28 +37,33 @@ class TableName extends MetaModel {
     final String catalog, schema, table;
 
     /**
+     * Ignored by this class; reserved for caller and subclasses usage.
+     */
+    final String remarks;
+
+    /**
      * Creates a new tuple with the give names.
      */
-    TableName(final String name, final String catalog, final String schema, final String table) {
-        super(name);
+    TableReference(final String catalog, final String schema, final String table, final String remarks) {
         this.catalog = catalog;
         this.schema  = schema;
         this.table   = table;
+        this.remarks = remarks;
     }
 
     /**
-     * Returns {@code true} if the given object is a {@code Relation} with equal table, schema and catalog names.
-     * All other properties (column names, action on delete…) are ignored; this method is <strong>not</strong> for
-     * testing if two {@code Relation} are fully equal. The purpose of this method is only to use {@code Relation}
-     * as keys in {@link Analyzer#dependencies} map for remembering full coordinates of tables that may need to be
-     * analyzed later.
+     * Returns {@code true} if the given object is a {@code TableReference} with equal table, schema and catalog names.
+     * All other properties that may be defined in subclasses (column names, action on delete, etc.) are ignored; this
+     * method is <strong>not</strong> for testing if two {@link Relation} are fully equal. The purpose of this method
+     * is only to use {@code TableReference} as keys in {@link Analyzer#dependencies} map for remembering full
+     * coordinates of tables that may need to be analyzed later.
      */
     @Override
     public final boolean equals(final Object obj) {
-        if (obj instanceof TableName) {
-            final TableName other = (TableName) obj;
+        if (obj instanceof TableReference) {
+            final TableReference other = (TableReference) obj;
             return table.equals(other.table) && Objects.equals(schema, other.schema) && Objects.equals(catalog, other.catalog);
-            // Other properties (columns, cascadeOnDelete) intentionally omitted.
+            // Other properties (remarks, columns, cascadeOnDelete) intentionally omitted.
         }
         return false;
     }
@@ -71,4 +76,12 @@ class TableName extends MetaModel {
     public final int hashCode() {
         return table.hashCode() + 31*Objects.hashCode(schema) + 37*Objects.hashCode(catalog);
     }
+
+    /**
+     * Formats a string representation of this object for debugging purpose.
+     */
+    @Override
+    public String toString() {
+        return SQLStoreProvider.createTableName(catalog, schema, table).toString();
+    }
 }
diff --git a/storage/sis-sqlstore/src/main/java/org/apache/sis/internal/sql/feature/package-info.java b/storage/sis-sqlstore/src/main/java/org/apache/sis/internal/sql/feature/package-info.java
index e123306..2c198a8 100644
--- a/storage/sis-sqlstore/src/main/java/org/apache/sis/internal/sql/feature/package-info.java
+++ b/storage/sis-sqlstore/src/main/java/org/apache/sis/internal/sql/feature/package-info.java
@@ -26,6 +26,7 @@
  * may change in incompatible ways in any future version without notice.
  *
  * @author  Johann Sorel (Geomatys)
+ * @author  Martin Desruisseaux (Geomatys)
  * @version 1.0
  * @since   1.0
  * @module
diff --git a/storage/sis-sqlstore/src/main/java/org/apache/sis/storage/sql/SQLStore.java b/storage/sis-sqlstore/src/main/java/org/apache/sis/storage/sql/SQLStore.java
index ba7fbd9..698ea22 100644
--- a/storage/sis-sqlstore/src/main/java/org/apache/sis/storage/sql/SQLStore.java
+++ b/storage/sis-sqlstore/src/main/java/org/apache/sis/storage/sql/SQLStore.java
@@ -16,12 +16,24 @@
  */
 package org.apache.sis.storage.sql;
 
+import java.util.Collection;
 import javax.sql.DataSource;
-import org.apache.sis.internal.sql.feature.Database;
+import java.sql.Connection;
+import java.sql.SQLException;
+import org.opengis.util.GenericName;
+import org.opengis.metadata.Metadata;
+import org.opengis.parameter.ParameterValueGroup;
+import org.apache.sis.storage.Resource;
 import org.apache.sis.storage.Aggregate;
 import org.apache.sis.storage.DataStore;
 import org.apache.sis.storage.DataStoreException;
+import org.apache.sis.storage.IllegalNameException;
 import org.apache.sis.storage.StorageConnector;
+import org.apache.sis.storage.event.ChangeEvent;
+import org.apache.sis.storage.event.ChangeListener;
+import org.apache.sis.internal.sql.feature.Database;
+import org.apache.sis.internal.sql.feature.Resources;
+import org.apache.sis.util.ArgumentChecks;
 
 
 /**
@@ -29,11 +41,12 @@ import org.apache.sis.storage.StorageConnector;
  * An example of spatial database is PostGIS.
  *
  * @author  Johann Sorel (Geomatys)
+ * @author  Martin Desruisseaux (Geomatys)
  * @version 1.0
  * @since   1.0
  * @module
  */
-public abstract class SQLStore extends DataStore implements Aggregate {
+public class SQLStore extends DataStore implements Aggregate {
     /**
      * The data source to use for obtaining connections to the database.
      */
@@ -46,24 +59,149 @@ public abstract class SQLStore extends DataStore implements Aggregate {
     private Database model;
 
     /**
+     * Fully qualified names (including catalog and schema) of the tables to include in this store.
+     */
+    private final GenericName[] tableNames;
+
+    /**
      * Creates a new instance for the given storage.
      * The given {@code connector} shall contain a {@link DataSource}.
+     * The given table names shall be qualified names of 1, 2 or 3 components.
+     * The name components are {@code <catalog>.<schema pattern>.<table pattern>} where:
+     *
+     * <ul>
+     *   <li>{@code <catalog>}, if present, is the name of a catalog as stored in the database.</li>
+     *   <li>{@code <schema pattern>}, if present, is the pattern of a schema.
+     *       The pattern can use {@code '_'} and {@code '%'} wildcards characters.</li>
+     *   <li>{@code <table pattern>} (mandatory) is the pattern of a table.
+     *       The pattern can use {@code '_'} and {@code '%'} wildcards characters.</li>
+     * </ul>
      *
-     * @param  provider   the factory that created this {@code DataStore} instance, or {@code null} if unspecified.
-     * @param  connector  information about the storage (JDBC data source, <i>etc</i>).
+     * Qualified table names can be created by the {@link SQLStoreProvider#createTableName(String, String, String)}
+     * convenience method. Only the main tables need to be specified; dependencies will be followed automatically.
+     *
+     * @param  provider    the factory that created this {@code DataStore} instance, or {@code null} if unspecified.
+     * @param  connector   information about the storage (JDBC data source, <i>etc</i>).
+     * @param  tableNames  fully qualified names (including catalog and schema) of the tables to include in this store.
      * @throws DataStoreException if an error occurred while creating the data store for the given storage.
      */
-    protected SQLStore(final SQLStoreProvider provider, final StorageConnector connector) throws DataStoreException {
+    protected SQLStore(final SQLStoreProvider provider, final StorageConnector connector, GenericName... tableNames)
+            throws DataStoreException
+    {
         super(provider, connector);
         source = connector.getStorageAs(DataSource.class);
+        ArgumentChecks.ensureNonNull("tableNames", tableNames);
+        tableNames = tableNames.clone();
+        for (int i=0; i<tableNames.length; i++) {
+            final GenericName name = tableNames[i];
+            ArgumentChecks.ensureNonNullElement("tableNames", i, tableNames);
+            final int depth = name.depth();
+            if (depth < 1 || depth > 3) {
+                throw new IllegalNameException(Resources.format(Resources.Keys.IllegalQualifiedName_1, name));
+            }
+        }
+        this.tableNames = tableNames;
+    }
+
+    /**
+     * Returns the parameters used to open this netCDF data store.
+     * The parameters are described by {@link SQLStoreProvider#getOpenParameters()} and contains
+     * at least a parameter named {@value SQLStoreProvider#LOCATION} with a {@link DataSource} value.
+     *
+     * @return parameters used for opening this data store.
+     */
+    @Override
+    public ParameterValueGroup getOpenParameters() {
+        if (provider == null) {
+            return null;
+        }
+        final ParameterValueGroup pg = provider.getOpenParameters().createValue();
+        pg.parameter(SQLStoreProvider.LOCATION).setValue(source);
+        pg.parameter(SQLStoreProvider.TABLES).setValue(tableNames);
+        return pg;
+    }
+
+    /**
+     * Returns the database model, analyzing the database schema when first needed.
+     */
+    private synchronized Database model() throws DataStoreException, SQLException {
+        if (model == null) {
+            try (Connection c = source.getConnection()) {
+                model = new Database(this, c, tableNames, listeners);
+            }
+        }
+        return model;
+    }
+
+    /**
+     * Returns information about the dataset as a whole. The returned metadata object can contain information
+     * such as the spatiotemporal extent of the dataset.
+     *
+     * @return information about the dataset.
+     * @throws DataStoreException if an error occurred while reading the data.
+     */
+    @Override
+    public Metadata getMetadata() throws DataStoreException {
+        throw new UnsupportedOperationException("Not supported yet."); // TODO
+    }
+
+    /**
+     * Returns the resources (features or coverages) in this SQL store.
+     *
+     * @return children resources that are components of this SQL store.
+     * @throws DataStoreException if an error occurred while fetching the components.
+     */
+    @Override
+    public Collection<Resource> components() throws DataStoreException {
+        throw new UnsupportedOperationException("Not supported yet."); // TODO
+    }
+
+    /**
+     * Searches for a resource identified by the given identifier.
+     * The given identifier should match one of the table name.
+     *
+     * @param  identifier  identifier of the resource to fetch. Must be non-null.
+     * @return resource associated to the given identifier (never {@code null}).
+     * @throws IllegalNameException if no resource is found for the given identifier, or if more than one resource is found.
+     * @throws DataStoreException if another kind of error occurred while searching resources.
+     */
+    @Override
+    public Resource findResource(final String identifier) throws DataStoreException {
+        try {
+            return model().tables.get(this, identifier);
+        } catch (SQLException e) {
+            throw new DataStoreException(e);
+        }
+    }
+
+    /**
+     * Ignored in current implementation, since this resource produces no events.
+     *
+     * @param  <T>        {@inheritDoc}
+     * @param  listener   {@inheritDoc}
+     * @param  eventType  {@inheritDoc}
+     */
+    @Override
+    public <T extends ChangeEvent> void addListener(ChangeListener<? super T> listener, Class<T> eventType) {
+    }
+
+    /**
+     * Ignored in current implementation, since this resource produces no events.
+     *
+     * @param  <T>        {@inheritDoc}
+     * @param  listener   {@inheritDoc}
+     * @param  eventType  {@inheritDoc}
+     */
+    @Override
+    public <T extends ChangeEvent> void removeListener(ChangeListener<? super T> listener, Class<T> eventType) {
     }
 
     /**
-     * Returns the data source used for obtaining connections to the database.
+     * Closes this SQL store and releases any underlying resources.
      *
-     * @return the data source for obtaining connections to the database.
+     * @throws DataStoreException if an error occurred while closing the SQL store.
      */
-    public DataSource getDataSource() {
-        return source;
+    @Override
+    public void close() throws DataStoreException {
     }
 }
diff --git a/storage/sis-sqlstore/src/main/java/org/apache/sis/storage/sql/SQLStoreProvider.java b/storage/sis-sqlstore/src/main/java/org/apache/sis/storage/sql/SQLStoreProvider.java
index 3fb4467..9fd55ec 100644
--- a/storage/sis-sqlstore/src/main/java/org/apache/sis/storage/sql/SQLStoreProvider.java
+++ b/storage/sis-sqlstore/src/main/java/org/apache/sis/storage/sql/SQLStoreProvider.java
@@ -16,21 +16,216 @@
  */
 package org.apache.sis.storage.sql;
 
+import java.util.Map;
+import java.util.HashMap;
+import java.sql.Connection;
+import java.sql.SQLException;
+import javax.sql.DataSource;
+import org.opengis.util.GenericName;
+import org.opengis.util.NameFactory;
+import org.opengis.util.NameSpace;
+import org.opengis.parameter.ParameterValueGroup;
+import org.opengis.parameter.ParameterDescriptor;
+import org.opengis.parameter.ParameterDescriptorGroup;
+import org.opengis.parameter.ParameterNotFoundException;
+import org.apache.sis.internal.system.DefaultFactories;
+import org.apache.sis.internal.sql.feature.Resources;
+import org.apache.sis.storage.DataStore;
 import org.apache.sis.storage.DataStoreProvider;
+import org.apache.sis.storage.DataStoreException;
+import org.apache.sis.storage.IllegalOpenParameterException;
+import org.apache.sis.storage.StorageConnector;
+import org.apache.sis.storage.ProbeResult;
+import org.apache.sis.parameter.Parameters;
+import org.apache.sis.parameter.ParameterBuilder;
+import org.apache.sis.util.UnconvertibleObjectException;
+import org.apache.sis.util.ArgumentChecks;
+
+import static org.apache.sis.internal.sql.feature.Database.WILDCARD;
 
 
 /**
  * Provider of {@code SQLStore} instances.
  *
  * @author  Johann Sorel (Geomatys)
+ * @author  Martin Desruisseaux (Geomatys)
  * @version 1.0
  * @since   1.0
  * @module
  */
-public abstract class SQLStoreProvider extends DataStoreProvider {
+public class SQLStoreProvider extends DataStoreProvider {
+    /**
+     * The format name.
+     */
+    private static final String NAME = "SQL";
+
+    /**
+     * Name of the parameter for the list of qualified table names.
+     * Values of this parameter are {@code GenericName[]}.
+     */
+    static final String TABLES = "tables";
+
+    /**
+     * Description of the {@value #LOCATION} parameter.
+     */
+    private static final ParameterDescriptor<DataSource> SOURCE_PARAM;
+
+    /**
+     * Description of the {@code "tables"} parameter.
+     */
+    private static final ParameterDescriptor<GenericName[]> TABLES_PARAM;
+
+    /**
+     * The parameter descriptor to be returned by {@link #getOpenParameters()}.
+     */
+    private static final ParameterDescriptorGroup OPEN_DESCRIPTOR;
+    static {
+        final ParameterBuilder builder = new ParameterBuilder();
+        SOURCE_PARAM = builder.addName(LOCATION).setRequired(true)
+                              .setDescription(Resources.formatInternational(Resources.Keys.DataSource))
+                              .create(DataSource.class, null);
+        TABLES_PARAM = builder.addName(TABLES).setRequired(true)
+                              .setDescription(Resources.formatInternational(Resources.Keys.QualifiedTableNames))
+                              .create(GenericName[].class, null);
+        OPEN_DESCRIPTOR = builder.addName(NAME).createGroup(SOURCE_PARAM, TABLES_PARAM);
+    }
+
+    /**
+     * The namespace for table names, created when first needed.
+     * Used for specifying the name separator, which is {@code '.'}.
+     */
+    private static volatile NameSpace tableNS;
+
     /**
      * Creates a new provider.
      */
-    protected SQLStoreProvider() {
+    public SQLStoreProvider() {
+    }
+
+    /**
+     * Create a qualified table name. The returned {@code GenericName} can be any of the following:
+     *
+     * <ul>
+     *   <li>{@code catalog.schemaPattern.tablePattern}</li>
+     *   <li>{@code schemaPattern.tablePattern}</li>
+     *   <li>{@code tablePattern}</li>
+     * </ul>
+     *
+     * The schema and table names (but not the catalog) can contain SQL wildcard characters:
+     * {@code '_'} matches any single character and {@code '%'} matches any sequence of characters.
+     *
+     * @param  catalog        name of a catalog as it is stored in the database, or {@code null} for any catalog.
+     * @param  schemaPattern  pattern (with {@code '_'} and {@code '%'} wildcards) of a schema, or {@code null} for any.
+     * @param  tablePattern   pattern (with {@code '_'} and {@code '%'} wildcards) of a table.
+     * @return the fully qualified name.
+     */
+    @SuppressWarnings("fallthrough")
+    public static GenericName createTableName(final String catalog, String schemaPattern, final String tablePattern) {
+        ArgumentChecks.ensureNonNull("tablePattern", tablePattern);
+        final int numParts;
+        if (catalog != null) {
+            numParts = 3;
+            if (schemaPattern == null) {
+                schemaPattern = WILDCARD;
+            }
+        } else if (schemaPattern != null && !schemaPattern.equals(WILDCARD)) {
+            numParts = 2;
+        } else {
+            numParts = 1;
+        }
+        final String[] names = new String[numParts];
+        int i = 0;
+        switch (numParts) {
+            default: throw new AssertionError(numParts);
+            case 3: names[i++] = catalog;           // Fall through
+            case 2: names[i++] = schemaPattern;     // Fall through
+            case 1: names[i]   = tablePattern;
+        }
+        final NameFactory factory = DefaultFactories.forBuildin(NameFactory.class);
+        NameSpace ns = tableNS;
+        if (ns == null) {
+            final Map<String,String> properties = new HashMap<>(4);     // TODO: use Map.of with JDK9.
+            properties.put("separator",      ".");
+            properties.put("separator.head", ":");
+            tableNS = ns = factory.createNameSpace(factory.createLocalName(null, "JDBC"), properties);
+        }
+        return factory.createGenericName(ns, names);
+    }
+
+    /**
+     * Returns a generic name for this data store, used mostly in warnings or error messages.
+     *
+     * @return a short name or abbreviation for the data format.
+     */
+    @Override
+    public String getShortName() {
+        return NAME;
+    }
+
+    /**
+     * Returns a description of all parameters accepted by this provider for opening a connection to the database.
+     *
+     * @return description of available parameters for opening a connection to a database.
+     */
+    @Override
+    public ParameterDescriptorGroup getOpenParameters() {
+        return OPEN_DESCRIPTOR;
+    }
+
+    /**
+     * Returns {@link ProbeResult#SUPPORTED} if the given storage appears to be supported by {@link SQLStore}.
+     * Returning {@code SUPPORTED} from this method does not guarantee that reading or writing will succeed,
+     * only that there appears to be a reasonable chance of success based on a brief inspection of the connection.
+     *
+     * @param  connector  information about the storage (data source).
+     * @return {@code SUPPORTED} if the given storage seems to be usable by {@code SQLStore} instances.
+     * @throws DataStoreException if an I/O error occurred.
+     */
+    @Override
+    public ProbeResult probeContent(final StorageConnector connector) throws DataStoreException {
+        final DataSource ds = connector.getStorageAs(DataSource.class);
+        if (ds != null) {
+            try (Connection c = ds.getConnection()) {
+                return ProbeResult.SUPPORTED;
+            } catch (SQLException e) {
+                final String state = e.getSQLState();
+                if (!"08001".equals(state) || !"3D000".equals(state)) {
+                    throw new DataStoreException(e);
+                }
+            }
+        }
+        return ProbeResult.UNSUPPORTED_STORAGE;
+    }
+
+    /**
+     * Returns a {@link SQLStore} implementation associated with this provider.
+     *
+     * @param  connector  information about the storage (data source).
+     * @return a data store implementation associated with this provider for the given storage.
+     * @throws DataStoreException if an error occurred while creating the data store instance.
+     */
+    @Override
+    public DataStore open(final StorageConnector connector) throws DataStoreException {
+        return new SQLStore(this, connector, createTableName(null, null, WILDCARD));
+    }
+
+    /**
+     * Returns a data store implementation associated with this provider for the given parameters.
+     *
+     * @param  parameters  opening parameters as defined by {@link #getOpenParameters()}.
+     * @return a data store implementation associated with this provider for the given parameters.
+     * @throws DataStoreException if an error occurred while creating the data store instance.
+     */
+    @Override
+    public DataStore open(final ParameterValueGroup parameters) throws DataStoreException {
+        ArgumentChecks.ensureNonNull("parameters", parameters);
+        try {
+            final Parameters p = Parameters.castOrWrap(parameters);
+            final StorageConnector connector = new StorageConnector(p.getValue(SOURCE_PARAM));
+            final GenericName[] tableNames = p.getValue(TABLES_PARAM);
+            return new SQLStore(this, connector, tableNames);
+        } catch (ParameterNotFoundException | UnconvertibleObjectException e) {
+            throw new IllegalOpenParameterException(e.getMessage(), e);
+        }
     }
 }
diff --git a/storage/sis-sqlstore/src/main/java/org/apache/sis/storage/sql/package-info.java b/storage/sis-sqlstore/src/main/java/org/apache/sis/storage/sql/package-info.java
index 795bc0c..2238777 100644
--- a/storage/sis-sqlstore/src/main/java/org/apache/sis/storage/sql/package-info.java
+++ b/storage/sis-sqlstore/src/main/java/org/apache/sis/storage/sql/package-info.java
@@ -20,6 +20,7 @@
  * Data store capable to read and create features from a JDBC connection to a database.
  *
  * @author  Johann Sorel (Geomatys)
+ * @author  Martin Desruisseaux (Geomatys)
  * @version 1.0
  * @since   1.0
  * @module
diff --git a/storage/sis-sqlstore/src/test/java/org/apache/sis/storage/sql/SQLStoreTest.java b/storage/sis-sqlstore/src/test/java/org/apache/sis/storage/sql/SQLStoreTest.java
index a9edca5..89e0765 100644
--- a/storage/sis-sqlstore/src/test/java/org/apache/sis/storage/sql/SQLStoreTest.java
+++ b/storage/sis-sqlstore/src/test/java/org/apache/sis/storage/sql/SQLStoreTest.java
@@ -16,10 +16,10 @@
  */
 package org.apache.sis.storage.sql;
 
-import java.sql.Connection;
-import org.apache.sis.internal.sql.feature.Database;
-import org.apache.sis.test.TestCase;
+import org.apache.sis.storage.FeatureSet;
+import org.apache.sis.storage.StorageConnector;
 import org.apache.sis.test.sql.TestDatabase;
+import org.apache.sis.test.TestCase;
 import org.junit.Test;
 
 
@@ -41,8 +41,11 @@ public final strictfp class SQLStoreTest extends TestCase {
     public void testReadStructure() throws Exception {
         try (TestDatabase tmp = TestDatabase.createOnPostgreSQL("features", true)) {
             tmp.executeSQL(SQLStoreTest.class, "Features.sql");
-            try (Connection c = tmp.source.getConnection()) {
-                final Database db = new Database(null, c, null, "features", new String[] {"Cities"});
+            try (SQLStore store = new SQLStore(new SQLStoreProvider(), new StorageConnector(tmp.source),
+                    SQLStoreProvider.createTableName(null, "features", "Cities")))
+            {
+                final FeatureSet cities = (FeatureSet) store.findResource("Cities");
+                System.out.println(cities.getType());
             }
         }
     }


Mime
View raw message