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: Take in account the geometry library to be needed for creating geometric objects. Never omit primary key columns since they are needed for creating identifiers.
Date Thu, 12 Jul 2018 14:56:09 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 d39f176  Take in account the geometry library to be needed for creating geometric
objects. Never omit primary key columns since they are needed for creating identifiers.
d39f176 is described below

commit d39f1769ea101601da1ed45a4ee91a5fb0e4f751
Author: Martin Desruisseaux <martin.desruisseaux@geomatys.com>
AuthorDate: Thu Jul 12 16:54:59 2018 +0200

    Take in account the geometry library to be needed for creating geometric objects.
    Never omit primary key columns since they are needed for creating identifiers.
---
 .../apache/sis/internal/sql/feature/Analyzer.java  |  62 ++++++++++-
 .../apache/sis/internal/sql/feature/Database.java  |  85 +++++++++++++--
 .../apache/sis/internal/sql/feature/Relation.java  |  27 +----
 .../sis/internal/sql/feature/SpatialFunctions.java |  11 ++
 .../org/apache/sis/internal/sql/feature/Table.java | 117 +++++++++++----------
 .../sis/internal/sql/feature/TableReference.java   |  60 ++++++++++-
 .../java/org/apache/sis/storage/sql/SQLStore.java  |  50 ++++++---
 .../apache/sis/storage/sql/SQLStoreProvider.java   |   8 +-
 .../org/apache/sis/storage/sql/package-info.java   |   1 +
 .../org/apache/sis/storage/sql/SQLStoreTest.java   |   1 +
 .../org/apache/sis/storage/sql/Features.sql        |  32 +++---
 11 files changed, 331 insertions(+), 123 deletions(-)

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 bf4b8ad..28ea66e 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
@@ -23,10 +23,15 @@ import java.util.LinkedHashSet;
 import java.util.LinkedHashMap;
 import java.util.Iterator;
 import java.util.Locale;
+import java.util.Objects;
 import java.sql.SQLException;
 import java.sql.DatabaseMetaData;
+import org.opengis.util.NameSpace;
+import org.opengis.util.NameFactory;
+import org.opengis.util.GenericName;
 import org.opengis.util.InternationalString;
 import org.apache.sis.internal.metadata.sql.Dialect;
+import org.apache.sis.internal.system.DefaultFactories;
 import org.apache.sis.util.logging.WarningListeners;
 import org.apache.sis.storage.DataStore;
 
@@ -55,6 +60,11 @@ final class Analyzer {
     final SpatialFunctions functions;
 
     /**
+     * The factory for creating {@code FeatureType} names.
+     */
+    final NameFactory nameFactory;
+
+    /**
      * The string to insert before wildcard characters ({@code '_'} or {@code '%'}) to escape.
      * This is used by {@link #escape(String)} before to pass argument values (e.g. table
name)
      * to {@link DatabaseMetaData} methods expecting a pattern.
@@ -92,13 +102,33 @@ final class Analyzer {
     final WarningListeners<DataStore> listeners;
 
     /**
+     * The locale for warning messages.
+     */
+    final Locale locale;
+
+    /**
+     * The last catalog and schema used for creating {@link #namespace}.
+     * Used for determining if {@link #namespace} is still valid.
+     */
+    private transient String catalog, schema;
+
+    /**
+     * The namespace created with {@link #catalog} and {@link #schema}.
+     */
+    private transient NameSpace namespace;
+
+    /**
      * Creates a new analyzer for the database described by given metadata.
      */
-    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);
+    Analyzer(final DatabaseMetaData metadata, final WarningListeners<DataStore> listeners,
final Locale locale)
+            throws SQLException
+    {
+        this.metadata    = metadata;
+        this.listeners   = listeners;
+        this.locale      = locale;
+        this.escape      = metadata.getSearchStringEscape();
+        this.functions   = new SpatialFunctions(metadata);
+        this.nameFactory = DefaultFactories.forBuildin(NameFactory.class);
         /*
          * 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
@@ -169,6 +199,28 @@ final class Analyzer {
     }
 
     /**
+     * Returns a namespace for the given catalog and schema names.
+     */
+    final NameSpace namespace(final String catalog, final String schema) {
+        if (!Objects.equals(this.schema, schema) || !Objects.equals(this.catalog, catalog))
{
+            if (schema != null) {
+                final GenericName name;
+                if (catalog == null) {
+                    name = nameFactory.createLocalName(null, schema);
+                } else {
+                    name = nameFactory.createGenericName(null, catalog, schema);
+                }
+                namespace = nameFactory.createNameSpace(name, TableReference.NAMESPACE_PROPERTIES);
+            } else {
+                namespace = null;
+            }
+            this.catalog = catalog;
+            this.schema  = schema;
+        }
+        return namespace;
+    }
+
+    /**
      * Declares that a relation to a foreigner table has been found. Only the catalog, schema
and table names
      * are taken in account. If a dependency for the same table has already been declared
before or if that
      * table has already been analyzed, then this method does nothing. Otherwise if the table
has not yet
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 6f04381..59b0047 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
@@ -17,7 +17,9 @@
 package org.apache.sis.internal.sql.feature;
 
 import java.util.Set;
+import java.util.List;
 import java.util.HashSet;
+import java.util.ArrayList;
 import java.sql.Connection;
 import java.sql.DatabaseMetaData;
 import java.sql.ResultSet;
@@ -26,12 +28,17 @@ 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.internal.util.UnmodifiableArrayList;
 import org.apache.sis.storage.DataStore;
 import org.apache.sis.storage.sql.SQLStore;
+import org.apache.sis.storage.Resource;
 import org.apache.sis.storage.FeatureSet;
 import org.apache.sis.storage.FeatureNaming;
 import org.apache.sis.storage.DataStoreException;
+import org.apache.sis.storage.IllegalNameException;
 import org.apache.sis.util.logging.WarningListeners;
+import org.apache.sis.util.collection.TreeTable;
+import org.apache.sis.util.Debug;
 
 
 /**
@@ -54,9 +61,15 @@ public final class Database {
     public static final String WILDCARD = "%";
 
     /**
-     * All tables known to this {@code Database}.
+     * All tables known to this {@code Database}. Populated in the constructor,
+     * and shall not be modified after construction for preserving thread-safety.
      */
-    public final FeatureNaming<FeatureSet> tables;
+    private final FeatureNaming<Table> tablesByNames;
+
+    /**
+     * All tables known to this {@code Database} in declaration order.
+     */
+    private final Table[] tables;
 
     /**
      * Functions that may be specific to the geospatial database in use.
@@ -64,6 +77,11 @@ public final class Database {
     final SpatialFunctions functions;
 
     /**
+     * {@code true} if this database contains at least one geometry column.
+     */
+    public final boolean hasGeometry;
+
+    /**
      * Creates a new model about the specified tables in a database.
      * This constructor requires a list of tables to include in the model,
      * but this list should not include the dependencies; this constructor
@@ -90,8 +108,7 @@ public final class Database {
     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(), listeners);
+        final Analyzer analyzer = new Analyzer(connection.getMetaData(), listeners, store.getLocale());
         final String[] tableTypes = getTableTypes(analyzer.metadata);
         for (final GenericName tableName : tableNames) {
             String[] names = tableName.getParsedNames().stream().map(LocalName::toString).toArray(String[]::new);
@@ -112,12 +129,21 @@ public final class Database {
                 }
             }
         }
+        final List<Table> tableList = new ArrayList<>(tableNames.length);
+        tablesByNames = new FeatureNaming<>();
+        boolean hasGeometry = false;
         TableReference dependency;
         while ((dependency = analyzer.nextDependency()) != null) {
             final Table table = new Table(analyzer, dependency);
-            tables.add(store, table.getType().getName(), table);
+            hasGeometry |= table.hasGeometry;
+            tablesByNames.add(store, table.getType().getName(), table);
+            if (!(dependency instanceof Relation)) {
+                tableList.add(table);                   // Adds only the table explicitly
required by the user.
+            }
         }
-        functions = analyzer.functions;
+        this.tables = tableList.toArray(new Table[tableList.size()]);
+        this.functions = analyzer.functions;
+        this.hasGeometry = hasGeometry;
     }
 
     /**
@@ -135,4 +161,51 @@ public final class Database {
         }
         return types.toArray(new String[types.size()]);
     }
+
+    /**
+     * Returns all tables in declaration order.
+     * The list contains only the tables explicitly requested at construction time.
+     *
+     * @return all tables in an unmodifiable list.
+     */
+    public final List<Resource> tables() {
+        return UnmodifiableArrayList.wrap(tables);
+    }
+
+    /**
+     * Returns the table for the given name.
+     *
+     * @param  store  the data store for which we are fetching a table. Used only in case
of error.
+     * @param  name   name of the table to fetch.
+     * @return the table (never null).
+     * @throws IllegalNameException if no table of the given name is found or if the name
is ambiguous.
+     */
+    public final FeatureSet findTable(final SQLStore store, final String name) throws IllegalNameException
{
+        return tablesByNames.get(store, name);
+    }
+
+    /**
+     * Creates a tree representation of this table for debugging purpose.
+     *
+     * @param  parent  the parent node where to add the tree representation.
+     */
+    @Debug
+    final void appendTo(TreeTable.Node parent) {
+        parent = Relation.newChild(parent, "Database");
+        for (final Table child : tables) {
+            child.appendTo(parent);
+        }
+    }
+
+    /**
+     * Formats a graphical representation of this database 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.
+     *
+     * @return string representation of this database.
+     */
+    @Override
+    public String toString() {
+        return TableReference.toString((n) -> appendTo(n));
+    }
 }
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 bf2dfe8..b5f22ba 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
@@ -26,8 +26,6 @@ import java.sql.DatabaseMetaData;
 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;
 
@@ -186,44 +184,25 @@ final class Relation extends TableReference {
     }
 
     /**
-     * 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
-    TreeTable.Node appendTo(final TreeTable.Node parent) {
+    void appendTo(final TreeTable.Node 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
+     * Formats a graphical representation of this relation 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();
+        return toString((n) -> appendTo(n));
     }
 }
diff --git a/storage/sis-sqlstore/src/main/java/org/apache/sis/internal/sql/feature/SpatialFunctions.java
b/storage/sis-sqlstore/src/main/java/org/apache/sis/internal/sql/feature/SpatialFunctions.java
index ee4ad7f..7ef4e90 100644
--- a/storage/sis-sqlstore/src/main/java/org/apache/sis/internal/sql/feature/SpatialFunctions.java
+++ b/storage/sis-sqlstore/src/main/java/org/apache/sis/internal/sql/feature/SpatialFunctions.java
@@ -28,6 +28,7 @@ import java.sql.SQLException;
 import java.sql.DatabaseMetaData;
 import org.opengis.referencing.crs.CoordinateReferenceSystem;
 import org.apache.sis.internal.metadata.sql.Reflection;
+import org.apache.sis.setup.GeometryLibrary;
 
 
 /**
@@ -51,6 +52,11 @@ class SpatialFunctions {
     private final boolean isByteUnsigned;
 
     /**
+     * The library to use for creating geometric objects, or {@code null} for the default.
+     */
+    final GeometryLibrary library;
+
+    /**
      * Creates a new accessor to geospatial functions for the database described by given
metadata.
      */
     SpatialFunctions(final DatabaseMetaData metadata) throws SQLException {
@@ -69,6 +75,11 @@ class SpatialFunctions {
             }
         }
         isByteUnsigned = unsigned;
+        /*
+         * The library to use depends on the database implementation.
+         * For now use the default library.
+         */
+        library = null;
     }
 
     /**
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 464efb7..877b931 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
@@ -37,8 +37,6 @@ 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;
 
@@ -88,6 +86,11 @@ final class Table extends AbstractFeatureSet {
     private final List<Relation> exportedKeys;
 
     /**
+     * {@code true} if this table contains at least one geometry column.
+     */
+    final boolean hasGeometry;
+
+    /**
      * Creates a description of the table of the given name.
      * 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.
@@ -152,61 +155,71 @@ final class Table extends AbstractFeatureSet {
          * nullability.
          */
         boolean hasGeometry = false;
-        final FeatureTypeBuilder ftb = new FeatureTypeBuilder();
+        final FeatureTypeBuilder ftb = new FeatureTypeBuilder(analyzer.nameFactory, analyzer.functions.library,
analyzer.locale);
         try (ResultSet reflect = analyzer.metadata.getColumns(id.catalog, schemaEsc, tableEsc,
null)) {
             while (reflect.next()) {
                 final String column = reflect.getString(Reflection.COLUMN_NAME);
-                if (foreignerKeys.contains(column)) {
-                    // TODO: create association.
-                    continue;
-                }
-                final String typeName = reflect.getString(Reflection.TYPE_NAME);
-                Class<?> type = analyzer.functions.toJavaType(reflect.getInt(Reflection.DATA_TYPE),
typeName);
-                if (type == null) {
-                    analyzer.warning(Resources.Keys.UnknownType_1, typeName);
-                    type = Object.class;
-                }
-                final AttributeTypeBuilder<?> atb = ftb.addAttribute(type).setName(column);
-                final int size = reflect.getInt(Reflection.COLUMN_SIZE);
-                if (!reflect.wasNull()) {
-                    atb.setMaximalLength(size);
-                }
-                final Boolean nullable = SQLUtilities.parseBoolean(reflect.getString(Reflection.IS_NULLABLE));
-                if (nullable == null || nullable) {
-                    atb.setMinimumOccurs(0);
-                }
-                /*
-                 * Some columns have special purposes: components of primary keys will be
used for creating
-                 * identifiers, some columns may contain a geometric object. Adding a role
on those columns
-                 * may create synthetic columns, for example "sis:identifier".
-                 */
-                if (primaryKeys.containsKey(column)) {
-                    atb.addRole(AttributeRole.IDENTIFIER_COMPONENT);
-                    if (primaryKeys.put(column, SQLUtilities.parseBoolean(reflect.getString(Reflection.IS_AUTOINCREMENT)))
!= null) {
-                        throw new DataStoreContentException(Resources.format(Resources.Keys.DuplicatedEntity_2,
"Column", column));
+                final boolean isPrimaryKey = primaryKeys.containsKey(column);
+                final boolean isForeignKey = foreignerKeys.contains(column);
+                if (isPrimaryKey | !isForeignKey) {
+                    /*
+                     * Foreign keys are excluded (they will be replaced by association),
except if the column is
+                     * also a primary key. In the later case we need to keep that column
because it is needed for
+                     * building the feature identifier.
+                     */
+                    final String typeName = reflect.getString(Reflection.TYPE_NAME);
+                    Class<?> type = analyzer.functions.toJavaType(reflect.getInt(Reflection.DATA_TYPE),
typeName);
+                    if (type == null) {
+                        analyzer.warning(Resources.Keys.UnknownType_1, typeName);
+                        type = Object.class;
                     }
-                }
-                if (Geometries.isKnownType(type)) {
-                    final CoordinateReferenceSystem crs = analyzer.functions.createGeometryCRS(reflect);
-                    if (crs != null) {
-                        atb.setCRS(crs);
+                    final AttributeTypeBuilder<?> atb = ftb.addAttribute(type).setName(column);
+                    if (CharSequence.class.isAssignableFrom(type)) {
+                        final int size = reflect.getInt(Reflection.COLUMN_SIZE);
+                        if (!reflect.wasNull()) {
+                            atb.setMaximalLength(size);
+                        }
+                    }
+                    final Boolean nullable = SQLUtilities.parseBoolean(reflect.getString(Reflection.IS_NULLABLE));
+                    if (nullable == null || nullable) {
+                        atb.setMinimumOccurs(0);
+                    }
+                    /*
+                     * Some columns have special purposes: components of primary keys will
be used for creating
+                     * identifiers, some columns may contain a geometric object. Adding a
role on those columns
+                     * may create synthetic columns, for example "sis:identifier".
+                     */
+                    if (isPrimaryKey) {
+                        atb.addRole(AttributeRole.IDENTIFIER_COMPONENT);
+                        if (primaryKeys.put(column, SQLUtilities.parseBoolean(reflect.getString(Reflection.IS_AUTOINCREMENT)))
!= null) {
+                            throw new DataStoreContentException(Resources.format(Resources.Keys.DuplicatedEntity_2,
"Column", column));
+                        }
                     }
-                    if (!hasGeometry) {
-                        hasGeometry = true;
-                        atb.addRole(AttributeRole.DEFAULT_GEOMETRY);
+                    if (Geometries.isKnownType(type)) {
+                        final CoordinateReferenceSystem crs = analyzer.functions.createGeometryCRS(reflect);
+                        if (crs != null) {
+                            atb.setCRS(crs);
+                        }
+                        if (!hasGeometry) {
+                            hasGeometry = true;
+                            atb.addRole(AttributeRole.DEFAULT_GEOMETRY);
+                        }
                     }
                 }
+                /*
+                 * If the column is a foreigner key, insert an association to another feature
instead.
+                 */
+                if (isForeignKey) {
+                    // TODO
+                }
             }
         }
         /*
          * Global information on the feature type (name, remarks).
-         * The remarks are opportunistically stored in id.name if available by the caller.
+         * The remarks are opportunistically stored in id.remarks if available by the caller.
          * An empty string means that the caller has checked for remarks and found none.
          */
-        if (id.schema != null) {
-            ftb.setNameSpace(id.schema);
-        }
-        ftb.setName(id.table);
+        ftb.setName(id.getName(analyzer));
         String remarks = id.remarks;
         if (remarks == null) {
             try (ResultSet reflect = analyzer.metadata.getTables(id.catalog, schemaEsc, tableEsc,
null)) {
@@ -228,6 +241,7 @@ final class Table extends AbstractFeatureSet {
         this.primaryKeys  = CollectionsExt.compact(primaryKeys);
         this.importedKeys = CollectionsExt.compact(importedKeys);
         this.exportedKeys = CollectionsExt.compact(exportedKeys);
+        this.hasGeometry  = hasGeometry;
     }
 
     /**
@@ -254,23 +268,20 @@ final class Table extends AbstractFeatureSet {
      * @param  parent  the parent node where to add the tree representation.
      */
     @Debug
-    final TreeTable.Node appendTo(final TreeTable.Node parent) {
-        final TreeTable.Node node = Relation.newChild(parent, featureType.getName().toString());
+    final void appendTo(TreeTable.Node parent) {
+        parent = 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.
+     * Formats a graphical representation of this table 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();
+        return TableReference.toString((n) -> appendTo(n));
     }
 
     /**
diff --git a/storage/sis-sqlstore/src/main/java/org/apache/sis/internal/sql/feature/TableReference.java
b/storage/sis-sqlstore/src/main/java/org/apache/sis/internal/sql/feature/TableReference.java
index 15065fd..aa9d9ac 100644
--- a/storage/sis-sqlstore/src/main/java/org/apache/sis/internal/sql/feature/TableReference.java
+++ b/storage/sis-sqlstore/src/main/java/org/apache/sis/internal/sql/feature/TableReference.java
@@ -16,8 +16,16 @@
  */
 package org.apache.sis.internal.sql.feature;
 
+import java.util.Map;
+import java.util.HashMap;
 import java.util.Objects;
+import java.util.function.Consumer;
+import org.opengis.util.LocalName;
 import org.apache.sis.storage.sql.SQLStoreProvider;
+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;
 
 
 /**
@@ -29,7 +37,18 @@ import org.apache.sis.storage.sql.SQLStoreProvider;
  * @since   1.0
  * @module
  */
-class TableReference {
+public class TableReference {
+    /**
+     * Properties to give to {@code NameFactory.createNameSpace(…)} for specifying the
separators.
+     */
+    public static final Map<String,String> NAMESPACE_PROPERTIES;
+    static {
+        final Map<String,String> properties = new HashMap<>(4);     // TODO:
use Map.of with JDK9.
+        properties.put("separator",      ".");
+        properties.put("separator.head", ":");
+        NAMESPACE_PROPERTIES = properties;
+    }
+
     /**
      * The catalog, schema and table name of a table.
      * The table name is mandatory, but the schema and catalog names may be null.
@@ -52,11 +71,20 @@ class TableReference {
     }
 
     /**
+     * Creates a name for the feature type backed by this table.
+     */
+    final LocalName getName(final Analyzer analyzer) {
+        return analyzer.nameFactory.createLocalName(analyzer.namespace(catalog, schema),
table);
+    }
+
+    /**
      * 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.
+     *
+     * @return whether the given object is another {@code TableReference} for the same table.
      */
     @Override
     public final boolean equals(final Object obj) {
@@ -71,6 +99,8 @@ class TableReference {
     /**
      * Computes a hash code from the catalog, schema and table names.
      * See {@link #equals(Object)} for information about the purpose.
+     *
+     * @return a hash code value for this table reference.
      */
     @Override
     public final int hashCode() {
@@ -78,7 +108,35 @@ class TableReference {
     }
 
     /**
+     * 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;
+    }
+
+    /**
+     * Formats a graphical representation of an 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.
+     */
+    static String toString(final Consumer<TreeTable.Node> appender) {
+        final DefaultTreeTable table = new DefaultTreeTable(TableColumn.NAME);
+        appender.accept(table.getRoot());
+        return table.toString();
+    }
+
+    /**
      * Formats a string representation of this object for debugging purpose.
+     *
+     * @return a string representation of this table reference.
      */
     @Override
     public String toString() {
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 698ea22..b93fcd9 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
@@ -23,6 +23,8 @@ import java.sql.SQLException;
 import org.opengis.util.GenericName;
 import org.opengis.metadata.Metadata;
 import org.opengis.parameter.ParameterValueGroup;
+import org.opengis.metadata.spatial.SpatialRepresentationType;
+import org.apache.sis.storage.FeatureSet;
 import org.apache.sis.storage.Resource;
 import org.apache.sis.storage.Aggregate;
 import org.apache.sis.storage.DataStore;
@@ -33,12 +35,16 @@ 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.internal.storage.MetadataBuilder;
 import org.apache.sis.util.ArgumentChecks;
+import org.apache.sis.util.Exceptions;
 
 
 /**
  * A data store capable to read and create features from a spatial database.
- * An example of spatial database is PostGIS.
+ * {@code SQLStore} requires a {@link DataSource} to be specified (indirectly) at construction
time.
+ * The {@code DataSource} should provide pooled connections, since connections will be frequently
+ * opened and closed.
  *
  * @author  Johann Sorel (Geomatys)
  * @author  Martin Desruisseaux (Geomatys)
@@ -64,6 +70,11 @@ public class SQLStore extends DataStore implements Aggregate {
     private final GenericName[] tableNames;
 
     /**
+     * The metadata, created when first requested.
+     */
+    private Metadata metadata;
+
+    /**
      * 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.
@@ -124,10 +135,12 @@ public class SQLStore extends DataStore implements Aggregate {
     /**
      * Returns the database model, analyzing the database schema when first needed.
      */
-    private synchronized Database model() throws DataStoreException, SQLException {
+    private synchronized Database model() throws DataStoreException {
         if (model == null) {
             try (Connection c = source.getConnection()) {
                 model = new Database(this, c, tableNames, listeners);
+            } catch (SQLException e) {
+                throw new DataStoreException(Exceptions.unwrap(e));
             }
         }
         return model;
@@ -135,30 +148,47 @@ public class SQLStore 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.
+     * such as the list of feature types.
      *
      * @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
+    public synchronized Metadata getMetadata() throws DataStoreException {
+        if (metadata == null) {
+            final Database model = model();
+            final MetadataBuilder builder = new MetadataBuilder();
+            builder.addSpatialRepresentation(SpatialRepresentationType.TEXT_TABLE);
+            if (model.hasGeometry) {
+                builder.addSpatialRepresentation(SpatialRepresentationType.VECTOR);
+            }
+            for (final Resource r : model.tables()) {
+                if (r instanceof FeatureSet) {
+                    builder.addFeatureType(((FeatureSet) r).getType(), null);
+                }
+            }
+            metadata = builder.build(true);
+        }
+        return metadata;
     }
 
     /**
      * Returns the resources (features or coverages) in this SQL store.
+     * The list contains only the tables explicitly named at construction time.
      *
      * @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
+        return model().tables();
     }
 
     /**
      * Searches for a resource identified by the given identifier.
-     * The given identifier should match one of the table name.
+     * The given identifier should match one of the table names.
+     * It may be one of the tables named at construction time, or one of the dependencies.
+     * The given name may be qualified with the schema name, or may be only the table name
if there is no ambiguity.
      *
      * @param  identifier  identifier of the resource to fetch. Must be non-null.
      * @return resource associated to the given identifier (never {@code null}).
@@ -167,11 +197,7 @@ public class SQLStore extends DataStore implements Aggregate {
      */
     @Override
     public Resource findResource(final String identifier) throws DataStoreException {
-        try {
-            return model().tables.get(this, identifier);
-        } catch (SQLException e) {
-            throw new DataStoreException(e);
-        }
+        return model().findTable(this, identifier);
     }
 
     /**
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 9fd55ec..24ebe94 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,8 +16,6 @@
  */
 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;
@@ -30,6 +28,7 @@ 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.internal.sql.feature.TableReference;
 import org.apache.sis.storage.DataStore;
 import org.apache.sis.storage.DataStoreProvider;
 import org.apache.sis.storage.DataStoreException;
@@ -144,10 +143,7 @@ public class SQLStoreProvider extends DataStoreProvider {
         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);
+            tableNS = ns = factory.createNameSpace(factory.createLocalName(null, "JDBC"),
TableReference.NAMESPACE_PROPERTIES);
         }
         return factory.createGenericName(ns, names);
     }
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 2238777..4ab7e5f 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
@@ -18,6 +18,7 @@
 
 /**
  * Data store capable to read and create features from a JDBC connection to a database.
+ * An example of spatial database is PostGIS.
  *
  * @author  Johann Sorel (Geomatys)
  * @author  Martin Desruisseaux (Geomatys)
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 89e0765..834467b 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
@@ -44,6 +44,7 @@ public final strictfp class SQLStoreTest extends TestCase {
             try (SQLStore store = new SQLStore(new SQLStoreProvider(), new StorageConnector(tmp.source),
                     SQLStoreProvider.createTableName(null, "features", "Cities")))
             {
+                System.out.println(store.getMetadata());
                 final FeatureSet cities = (FeatureSet) store.findResource("Cities");
                 System.out.println(cities.getType());
             }
diff --git a/storage/sis-sqlstore/src/test/resources/org/apache/sis/storage/sql/Features.sql
b/storage/sis-sqlstore/src/test/resources/org/apache/sis/storage/sql/Features.sql
index 1c8adab..e33e61f 100644
--- a/storage/sis-sqlstore/src/test/resources/org/apache/sis/storage/sql/Features.sql
+++ b/storage/sis-sqlstore/src/test/resources/org/apache/sis/storage/sql/Features.sql
@@ -11,32 +11,32 @@
 --   "Parks" through exported keys ("Cities" is referenced by "Parks").
 
 CREATE TABLE features."Countries" (
-    code           CHARACTER(3)          NOT NULL,
-    "native name"  CHARACTER VARYING(20) NOT NULL,
+    code         CHARACTER(3)          NOT NULL,
+    native_name  CHARACTER VARYING(20) NOT NULL,
 
     CONSTRAINT "PK_Country" PRIMARY KEY (code)
 );
 
 
 CREATE TABLE features."Cities" (
-    country        CHARACTER(3)          NOT NULL,
-    "native name"  CHARACTER VARYING(20) NOT NULL,
-    "translation"  CHARACTER VARYING(20) NOT NULL,
-    population     INTEGER,
+    country      CHARACTER(3)          NOT NULL,
+    native_name  CHARACTER VARYING(20) NOT NULL,
+    translation  CHARACTER VARYING(20),
+    population   INTEGER,
 
-    CONSTRAINT "PK_City"    PRIMARY KEY (country, "native name"),
+    CONSTRAINT "PK_City"    PRIMARY KEY (country, native_name),
     CONSTRAINT "FK_Country" FOREIGN KEY (country) REFERENCES features."Countries"(code)
 );
 
 
 CREATE TABLE features."Parks" (
-    country        CHARACTER(3)          NOT NULL,
-    city           CHARACTER VARYING(20) NOT NULL,
-    "native name"  CHARACTER VARYING(20) NOT NULL,
-    "translation"  CHARACTER VARYING(20) NOT NULL,
+    country      CHARACTER(3)          NOT NULL,
+    city         CHARACTER VARYING(20) NOT NULL,
+    native_name  CHARACTER VARYING(20) NOT NULL,
+    translation  CHARACTER VARYING(20),
 
-    CONSTRAINT "PK_Park" PRIMARY KEY (country, city, "native name"),
-    CONSTRAINT "FK_City" FOREIGN KEY (country, city) REFERENCES features."Cities"(country,
"native name") ON DELETE CASCADE
+    CONSTRAINT "PK_Park" PRIMARY KEY (country, city, native_name),
+    CONSTRAINT "FK_City" FOREIGN KEY (country, city) REFERENCES features."Cities"(country,
native_name) ON DELETE CASCADE
 );
 
 
@@ -49,18 +49,18 @@ COMMENT ON TABLE features."Parks"     IS 'Parks in cities.';
 -- Add enough data for having at least two parks for a city.
 -- The data intentionally use ideograms for testing encoding.
 
-INSERT INTO features."Countries" (code, "native name") VALUES
+INSERT INTO features."Countries" (code, native_name) VALUES
     ('CAN', 'Canada'),
     ('FRA', 'France'),
     ('JPN', '日本');
 
-INSERT INTO features."Cities" (country, "native name", "translation", population) VALUES
+INSERT INTO features."Cities" (country, native_name, translation, population) VALUES
     ('CAN', 'Montréal', 'Montreal', 1704694),       -- Population in 2016
     ('CAN', 'Québec',   'Quebec',    531902),       -- Population in 2016
     ('FRA', 'Paris',    'Paris',    2206488),       -- Population in 2017
     ('JPN', '東京',     'Tōkyō',   13622267);       -- Population in 2016
 
-INSERT INTO features."Parks" (country, city, "native name", "translation") VALUES
+INSERT INTO features."Parks" (country, city, native_name, translation) VALUES
     ('CAN', 'Montréal', 'Mont Royal',           'Mount Royal'),
     ('FRA', 'Paris',    'Jardin des Tuileries', 'Tuileries Garden'),
     ('FRA', 'Paris',    'Jardin du Luxembourg', 'Luxembourg Garden'),


Mime
View raw message