This is an automated email from the ASF dual-hosted git repository. desruisseaux pushed a commit to branch geoapi-4.0 in repository https://gitbox.apache.org/repos/asf/sis.git
commit 1cc76fd6399bb33a7eaba97ef3b63a909dc595cc Author: Martin Desruisseaux <[email protected]> AuthorDate: Fri Jul 6 16:45:37 2018 +0200 Move the analysis of primary/foreigner keys in the Relation table. --- .../sis/internal/metadata/sql/Reflection.java | 6 + .../apache/sis/internal/sql/feature/Column.java | 7 - .../apache/sis/internal/sql/feature/Database.java | 196 +++++++++------------ .../apache/sis/internal/sql/feature/MetaModel.java | 27 +-- .../apache/sis/internal/sql/feature/Relation.java | 172 +++++++++++++++--- .../apache/sis/internal/sql/feature/Resources.java | 140 +++++++++++++++ .../sis/internal/sql/feature/Resources.properties | 22 +++ .../internal/sql/feature/Resources_fr.properties | 27 +++ .../apache/sis/internal/sql/feature/Schema.java | 44 +++-- .../sis/internal/sql/feature/SpatialFunctions.java | 6 +- .../org/apache/sis/internal/sql/feature/Table.java | 63 +++---- .../sis/internal/sql/feature/package-info.java | 2 +- 12 files changed, 504 insertions(+), 208 deletions(-) diff --git a/core/sis-metadata/src/main/java/org/apache/sis/internal/metadata/sql/Reflection.java b/core/sis-metadata/src/main/java/org/apache/sis/internal/metadata/sql/Reflection.java index 71ea48a..cfd27ca 100644 --- a/core/sis-metadata/src/main/java/org/apache/sis/internal/metadata/sql/Reflection.java +++ b/core/sis-metadata/src/main/java/org/apache/sis/internal/metadata/sql/Reflection.java @@ -147,6 +147,12 @@ public final class Reflection { public static final String FK_NAME = "FK_NAME"; /** + * The {@value} key for the foreigner key table catalog. + * Values in this column may be null. + */ + public static final String FKTABLE_CAT = "FKTABLE_CAT"; + + /** * The {@value} key for the foreign key table schema. * Values in this column may be null. */ diff --git a/storage/sis-sql/src/main/java/org/apache/sis/internal/sql/feature/Column.java b/storage/sis-sql/src/main/java/org/apache/sis/internal/sql/feature/Column.java index 85afb3a..6916bde 100644 --- a/storage/sis-sql/src/main/java/org/apache/sis/internal/sql/feature/Column.java +++ b/storage/sis-sql/src/main/java/org/apache/sis/internal/sql/feature/Column.java @@ -45,13 +45,6 @@ final class Column { Boolean.class, 1, 1, null); /** - * Property information, if the field is a relation. - */ - static final AttributeType<Relation> JDBC_PROPERTY_RELATION = new DefaultAttributeType<>( - Collections.singletonMap(DefaultAttributeType.NAME_KEY, "relation"), - Relation.class, 1, 1, null); - - /** * Whether values in a column are generated by the database, computed from a sequence of supplied. */ enum Type { diff --git a/storage/sis-sql/src/main/java/org/apache/sis/internal/sql/feature/Database.java b/storage/sis-sql/src/main/java/org/apache/sis/internal/sql/feature/Database.java index bfdd113..4ed2102 100644 --- a/storage/sis-sql/src/main/java/org/apache/sis/internal/sql/feature/Database.java +++ b/storage/sis-sql/src/main/java/org/apache/sis/internal/sql/feature/Database.java @@ -25,11 +25,9 @@ import java.util.ArrayList; import java.util.Collection; import java.util.Collections; import java.util.HashMap; -import java.util.HashSet; +import java.util.Iterator; import java.util.List; -import java.util.Map.Entry; import java.util.Map; -import java.util.Set; import org.opengis.util.GenericName; import org.opengis.coverage.Coverage; import org.opengis.feature.AttributeType; @@ -69,23 +67,21 @@ public final class Database { private static final String TABLE = "TABLE", VIEW = "VIEW"; /** - * Feature type used to mark types which are sub-types of others. + * Abstract type used to mark features that are components of other features. + * + * @deprecated replace by scoped name (TODO). */ - private static final FeatureType SUBTYPE; - static { - final FeatureTypeBuilder ftb = new FeatureTypeBuilder() - .setName("SubType") - .setAbstract(true); - SUBTYPE = ftb.build(); - } + @Deprecated + private static final FeatureType COMPONENT = new FeatureTypeBuilder().setName("Component").setAbstract(true).build(); private final SpatialFunctions functions; private final FeatureNaming<PrimaryKey> pkIndex; private final FeatureNaming<FeatureType> typeIndex; private final Map<String,Schema> schemas; - public Database(final SQLStore store, final SpatialFunctions functions, final String schema, final String table, - final List<String> addWarningsTo) throws SQLException, IllegalNameException + public Database(final SQLStore store, final SpatialFunctions functions, final String catalog, + final String schema, final String table, final List<String> addWarningsTo) + throws SQLException, DataStoreException { if (table != null) { ArgumentChecks.ensureNonEmpty("table", table); @@ -94,19 +90,7 @@ public final class Database { pkIndex = new FeatureNaming<>(); typeIndex = new FeatureNaming<>(); schemas = new HashMap<>(); - analyze(store, schema, table, addWarningsTo); - } - - private Collection<Schema> getSchemaMetaModels() { - return schemas.values(); - } - - private Schema getSchemaMetaModel(String name) { - return schemas.get(name); - } - - private PrimaryKey getPrimaryKey(final SQLStore store, final String featureTypeName) throws IllegalNameException { - return pkIndex.get(store, featureTypeName); + analyze(store, catalog, schema, table, addWarningsTo); } public FeatureType getFeatureType(final SQLStore store, final String typeName) throws IllegalNameException { @@ -116,35 +100,45 @@ public final class Database { /** * Explores all tables and views then recreate a complex feature model from relations. */ - private synchronized void analyze(final SQLStore store, final String schemaName, final String tableName, final List<String> addWarningsTo) - throws SQLException, IllegalNameException + private synchronized void analyze(final SQLStore store, final String catalog, final String schemaName, + final String tableName, final List<String> addWarningsTo) + throws SQLException, DataStoreException { try (Connection cx = store.getDataSource().getConnection()) { final DatabaseMetaData metadata = cx.getMetaData(); - final Set<String> requieredSchemas = new HashSet<>(); - final Set<String> visitedSchemas = new HashSet<>(); + /* + * Keep trace of the schemas that we need to visit, and the schema already visited. + * The boolean value tells whether the schema has already been visited or not. + * New schemas to visit may be added when following the relation established by foreigner keys. + */ + final Map<String,Boolean> requiredSchemas = new HashMap<>(); /* * Schema names available in the database: * 1. TABLE_SCHEM : String => schema name * 2. TABLE_CATALOG : String => catalog name (may be null) */ if (schemaName != null) { - requieredSchemas.add(schemaName); + requiredSchemas.put(schemaName, Boolean.FALSE); } else try (ResultSet reflect = metadata.getSchemas()) { + // TODO: use schemas in getTables instead. while (reflect.next()) { - requieredSchemas.add(reflect.getString(Reflection.TABLE_SCHEM)); // TODO: use schemas in getTables instead. + requiredSchemas.put(reflect.getString(Reflection.TABLE_SCHEM), Boolean.FALSE); } } /* - * We need to analyze requiered schema references. + * Iterate over all schemas that we need to process. We may need to stop iteration and recreate + * a new iterator because the methods invoked in this loop may alter the map content. + * + * TODO: use a boolean return value telling us if we need to recreate the iterator. */ - while (!requieredSchemas.isEmpty()) { - final String sn = requieredSchemas.iterator().next(); - visitedSchemas.add(sn); - requieredSchemas.remove(sn); - // TODO: escape with metadata.getSearchStringEscape(). - final Schema schema = analyzeSchema(metadata, sn, tableName, requieredSchemas, visitedSchemas, addWarningsTo); - schemas.put(schema.name, schema); + Iterator<Map.Entry<String,Boolean>> it; + while ((it = requiredSchemas.entrySet().iterator()).hasNext()) { + final Map.Entry<String,Boolean> sn = it.next(); + if (!sn.setValue(Boolean.TRUE)) { + // TODO: escape with metadata.getSearchStringEscape(). + final Schema schema = analyzeSchema(metadata, catalog, sn.getKey(), tableName, requiredSchemas, addWarningsTo); + schemas.put(schema.name, schema); + } } reverseSimpleFeatureTypes(metadata); } @@ -153,35 +147,33 @@ public final class Database { */ final Collection<Schema> candidates; if (schemaName == null) { - candidates = getSchemaMetaModels(); // Take all schemas. + candidates = schemas.values(); // Take all schemas. } else { - candidates = Collections.singleton(getSchemaMetaModel(schemaName)); + candidates = Collections.singleton(schemas.get(schemaName)); } for (Schema schema : candidates) { - if (schema != null) { - for (Table table : schema.getTables()) { - - final FeatureTypeBuilder ft = table.getType(Table.View.SIMPLE_FEATURE_TYPE); - final GenericName name = ft.getName(); - pkIndex.add(store, name, table.key); - if (table.isSubType()) { - // We don't show subtype, they are part of other feature types, add a flag to identify then - ft.setSuperTypes(SUBTYPE); - } - typeIndex.add(store, name, ft.build()); - } - } else { + if (schema == null) { throw new SQLException("Specifed schema " + schemaName + " does not exist."); } + for (Table table : schema.getTables()) { + final FeatureTypeBuilder ft = table.featureType; + final GenericName name = ft.getName(); + pkIndex.add(store, name, table.key); + if (table.isComponent()) { + // We don't show subtype, they are part of other feature types, add a flag to identify then + ft.setSuperTypes(COMPONENT); + } + typeIndex.add(store, name, ft.build()); + } } } /** * @param schemaPattern schema name with "%" and "_" interpreted as wildcards, or {@code null} for all schemas. */ - private Schema analyzeSchema(final DatabaseMetaData metadata, final String schemaPattern, final String tableNamePattern, - final Set<String> requieredSchemas, final Set<String> visitedSchemas, final List<String> addWarningsTo) - throws SQLException, IllegalNameException + private Schema analyzeSchema(final DatabaseMetaData metadata, final String catalog, final String schemaPattern, + final String tableNamePattern, final Map<String,Boolean> requiredSchemas, + final List<String> addWarningsTo) throws SQLException, DataStoreException { final Schema schema = new Schema(schemaPattern); /* @@ -190,28 +182,28 @@ public final class Database { * 2. TABLE_NAME : String => table name * 3. TABLE_TYPE : String => table type (typically "TABLE" or "VIEW"). */ - try (ResultSet reflect = metadata.getTables(null, schemaPattern, tableNamePattern, new String[] {TABLE, VIEW})) { // TODO: use metadata.getTableTypes() + try (ResultSet reflect = metadata.getTables(catalog, schemaPattern, tableNamePattern, new String[] {TABLE, VIEW})) { // TODO: use metadata.getTableTypes() while (reflect.next()) { - final Table table = analyzeTable(metadata, reflect, requieredSchemas, visitedSchemas, addWarningsTo); - schema.tables.put(table.name, table); + schema.addTable(analyzeTable(metadata, reflect, requiredSchemas, addWarningsTo)); } } return schema; } private Table analyzeTable(final DatabaseMetaData metadata, final ResultSet tableSet, - final Set<String> requieredSchemas, final Set<String> visitedSchemas, final List<String> addWarningsTo) - throws SQLException, IllegalNameException + final Map<String,Boolean> requiredSchemas, final List<String> addWarningsTo) + throws SQLException, DataStoreException { + final String catalog = tableSet.getString(Reflection.TABLE_CAT); final String schemaName = tableSet.getString(Reflection.TABLE_SCHEM); final String tableName = tableSet.getString(Reflection.TABLE_NAME); final String tableType = tableSet.getString(Reflection.TABLE_TYPE); - final Table table = new Table(tableName, tableType); + final Table table = new Table(tableName); final FeatureTypeBuilder ftb = new FeatureTypeBuilder(); /* * Explore all columns. */ - try (ResultSet reflect = metadata.getColumns(null, schemaName, tableName, null)) { + try (ResultSet reflect = metadata.getColumns(catalog, schemaName, tableName, null)) { while (reflect.next()) { analyzeColumn(metadata, reflect, ftb.addAttribute(Object.class)); } @@ -220,11 +212,11 @@ public final class Database { * Find primary keys. */ final List<Column> cols = new ArrayList<>(); - try (ResultSet rp = metadata.getPrimaryKeys(null, schemaName, tableName)) { + try (ResultSet rp = metadata.getPrimaryKeys(catalog, schemaName, tableName)) { while (rp.next()) { final String columnNamePattern = rp.getString(Reflection.COLUMN_NAME); // TODO: escape columnNamePattern with metadata.getSearchStringEscape(). - try (ResultSet reflect = metadata.getColumns(null, schemaName, tableName, columnNamePattern)) { + try (ResultSet reflect = metadata.getColumns(catalog, schemaName, tableName, columnNamePattern)) { while (reflect.next()) { // Should loop exactly once. final int sqlType = reflect.getInt(Reflection.DATA_TYPE); final String sqlTypeName = reflect.getString(Reflection.TYPE_NAME); @@ -263,7 +255,7 @@ public final class Database { final Map<String,List<String>> uniqueIndexes = new HashMap<>(); String indexname = null; // We can't cache this one, seems to be a bug in the driver, it won't find anything for table name like '%' - try (ResultSet reflect = metadata.getIndexInfo(null, schemaName, tableName, true, false)) { + try (ResultSet reflect = metadata.getIndexInfo(catalog, schemaName, tableName, true, false)) { while (reflect.next()) { final String columnName = reflect.getString(Reflection.COLUMN_NAME); final String idxName = reflect.getString(Reflection.INDEX_NAME); @@ -290,7 +282,7 @@ public final class Database { /* * For each unique index composed of one column add a flag on the property descriptor. */ - for (Entry<String,List<String>> entry : uniqueIndexes.entrySet()) { + for (Map.Entry<String,List<String>> entry : uniqueIndexes.entrySet()) { final List<String> columns = entry.getValue(); if (columns.size() == 1) { String columnName = columns.get(0); @@ -306,7 +298,7 @@ public final class Database { /* * Build a primary key from unique index. */ - try (ResultSet reflect = metadata.getColumns(null, schemaName, tableName, null)) { + try (ResultSet reflect = metadata.getColumns(catalog, schemaName, tableName, null)) { while (reflect.next()) { final String columnName = reflect.getString(Reflection.COLUMN_NAME); if (names.contains(columnName)) { @@ -349,49 +341,31 @@ public final class Database { } } /* - * Find imported keys. + * Creates a list of associations between the table read by this method and other tables. + * The associations are defined by the foreigner keys referencing primary keys. Note that + * the table relations can be defined in both ways: the foreigner keys of this table may + * be referencing the primary keys of other tables (Direction.IMPORT) or the primary keys + * of this table may be referenced by the foreigner keys of other tables (Direction.EXPORT). + * However in both case, we will translate that into associations from this table to the + * other tables. We can not rely on IMPORT versus EXPORT for determining the association + * navigability because the database designer's choice may be driven by the need to support + * multi-occurrences. */ - try (ResultSet reflect = metadata.getImportedKeys(null, schemaName, tableName)) { - while (reflect.next()) { - String relationName = reflect.getString(Reflection.PK_NAME); - if (relationName == null) relationName = reflect.getString(Reflection.FK_NAME); - final String localColumn = reflect.getString(Reflection.FKCOLUMN_NAME); - final String refSchemaName = reflect.getString(Reflection.PKTABLE_SCHEM); - final String refTableName = reflect.getString(Reflection.PKTABLE_NAME); - final String refColumnName = reflect.getString(Reflection.PKCOLUMN_NAME); - final int deleteRule = reflect.getInt(Reflection.DELETE_RULE); - final boolean deleteCascade = DatabaseMetaData.importedKeyCascade == deleteRule; - final Relation relation = new Relation(relationName,localColumn, - refSchemaName, refTableName, refColumnName, true, deleteCascade); + try (ResultSet reflect = metadata.getImportedKeys(catalog, schemaName, tableName)) { + while (!reflect.isClosed()) { + final Relation relation = new Relation(Relation.Direction.IMPORT, reflect); table.importedKeys.add(relation); - if (refSchemaName!=null && !visitedSchemas.contains(refSchemaName)) requieredSchemas.add(refSchemaName); - for (PropertyTypeBuilder desc : ftb.properties()) { - if (desc.getName().tip().toString().equals(localColumn)) { - final AttributeTypeBuilder<?> atb = (AttributeTypeBuilder) desc; - atb.addCharacteristic(Column.JDBC_PROPERTY_RELATION).setDefaultValue(relation); - break; - } + if (relation.schema != null) { + requiredSchemas.putIfAbsent(relation.schema, Boolean.FALSE); } } } - /* - * Find exported keys. - */ - try (ResultSet reflect = metadata.getExportedKeys(null, schemaName, tableName)) { - while (reflect.next()) { - String relationName = reflect.getString(Reflection.FKCOLUMN_NAME); - if (relationName == null) relationName = reflect.getString(Reflection.FK_NAME); - final String localColumn = reflect.getString(Reflection.PKCOLUMN_NAME); - final String refSchemaName = reflect.getString(Reflection.FKTABLE_SCHEM); - final String refTableName = reflect.getString(Reflection.FKTABLE_NAME); - final String refColumnName = reflect.getString(Reflection.FKCOLUMN_NAME); - final int deleteRule = reflect.getInt(Reflection.DELETE_RULE); - final boolean deleteCascade = DatabaseMetaData.importedKeyCascade == deleteRule; - table.exportedKeys.add(new Relation(relationName, localColumn, - refSchemaName, refTableName, refColumnName, false, deleteCascade)); - - if (refSchemaName != null && !visitedSchemas.contains(refSchemaName)) { - requieredSchemas.add(refSchemaName); + try (ResultSet reflect = metadata.getExportedKeys(catalog, schemaName, tableName)) { + while (!reflect.isClosed()) { + final Relation relation = new Relation(Relation.Direction.IMPORT, reflect); + table.exportedKeys.add(relation); + if (relation.schema != null) { + requiredSchemas.putIfAbsent(relation.schema, Boolean.FALSE); } } } @@ -436,12 +410,12 @@ public final class Database { // Search if we already have this property PropertyType desc = null; - final Schema schema = getSchemaMetaModel(schemaName); + final Schema schema = schemas.get(schemaName); if (schema != null) { Table table = schema.getTable(tableName); if (table != null) { try { - desc = table.getType(Table.View.SIMPLE_FEATURE_TYPE).build().getProperty(columnName); + desc = table.featureType.build().getProperty(columnName); } catch (PropertyNotFoundException ex) { // ok } @@ -498,7 +472,7 @@ public final class Database { } } } - table.simpleFeatureType = ftb; + table.featureType = ftb; } } } diff --git a/storage/sis-sql/src/main/java/org/apache/sis/internal/sql/feature/MetaModel.java b/storage/sis-sql/src/main/java/org/apache/sis/internal/sql/feature/MetaModel.java index fd7975d..675ea71 100644 --- a/storage/sis-sql/src/main/java/org/apache/sis/internal/sql/feature/MetaModel.java +++ b/storage/sis-sql/src/main/java/org/apache/sis/internal/sql/feature/MetaModel.java @@ -19,6 +19,7 @@ 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; @@ -26,24 +27,26 @@ import org.apache.sis.util.collection.DefaultTreeTable; /** * Description about a database entity (schema, table, relation, <i>etc</i>). - * The information provided by subclasses are inferred from {@link DatabaseMetaData} - * and stored as structures from the {@link org.apache.sis.feature} package. + * 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 (schema, table, <i>etc</i>) name. + * 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. + * @param name the database entity name, or {@code null} if unspecified. */ MetaModel(final String name) { this.name = name; @@ -52,7 +55,8 @@ abstract class MetaModel { /** * 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 for appending additional information. + * 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. @@ -63,22 +67,23 @@ abstract class MetaModel { } /** - * Add a child of the given name to the given node. + * 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 node. + * @return the child added to the parent. */ @Debug - private static TreeTable.Node newChild(final TreeTable.Node parent, final String name) { + static TreeTable.Node newChild(final TreeTable.Node parent, final String name) { final TreeTable.Node child = parent.newChild(); - child.setValue(TableColumn.NAME, name); + 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 collection of children is empty, then no node of the given {@code name} is inserted. + * 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. @@ -102,7 +107,7 @@ abstract class MetaModel { * uses a monospaced font and supports Unicode. */ @Override - public String toString() { + public final String toString() { final DefaultTreeTable table = new DefaultTreeTable(TableColumn.NAME); appendTo(table.getRoot()); return table.toString(); diff --git a/storage/sis-sql/src/main/java/org/apache/sis/internal/sql/feature/Relation.java b/storage/sis-sql/src/main/java/org/apache/sis/internal/sql/feature/Relation.java index a395f63..8076ef0 100644 --- a/storage/sis-sql/src/main/java/org/apache/sis/internal/sql/feature/Relation.java +++ b/storage/sis-sql/src/main/java/org/apache/sis/internal/sql/feature/Relation.java @@ -16,43 +16,169 @@ */ package org.apache.sis.internal.sql.feature; +import java.util.Map; +import java.util.LinkedHashMap; +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; + /** - * Description of a relation between two tables. + * Description of a relation between two tables, as defined by foreigner keys. + * Each {@link Table} may contain an arbitrary amount of relations. + * Relations are defined in two directions: + * + * <ul> + * <li>{@link Direction#IMPORT}: primary keys of <em>other</em> tables are referenced by the foreigner keys + * of the table containing this {@code Relation}.</li> + * <li>{@link Direction#EXPORT}: foreigner keys of <em>other</em> tables are referencing the primary keys + * of the table containing this {@code Relation}.</li> + * </ul> + * + * Instances of this class are created from the results of {@link DatabaseMetaData#getImportedKeys getImportedKeys} + * or {@link DatabaseMetaData#getExportedKeys getExportedKeys} with {@code (catalog, schema, table)} parameters. * * @author Johann Sorel (Geomatys) + * @author Martin Desruisseaux (Geomatys) * @version 1.0 * @since 1.0 * @module */ final class Relation extends MetaModel { + /** + * Whether another table is <em>using</em> or is <em>used by</em> the table containing the {@link Relation}. + */ + enum Direction { + /** + * Primary keys of other tables are referenced by the foreigner keys of the table containing the {@code Relation}. + * In other words, the table containing {@code Relation} is <em>using</em> the {@link Relation#table}. + * + * @see DatabaseMetaData#getImportedKeys(String, String, String) + */ + IMPORT(Reflection.PK_NAME, Reflection.PKTABLE_CAT, Reflection.PKTABLE_SCHEM, + Reflection.PKTABLE_NAME, Reflection.PKCOLUMN_NAME, Reflection.FKCOLUMN_NAME), + + /** + * Foreigner keys of other tables are referencing the primary keys of the table containing the {@code Relation}. + * In other words, the table containing {@code Relation} is <em>used by</em> {@link Relation#table}. + * + * @see DatabaseMetaData#getExportedKeys(String, String, String) + */ + EXPORT(Reflection.FK_NAME, Reflection.FKTABLE_CAT, Reflection.FKTABLE_SCHEM, + Reflection.FKTABLE_NAME, Reflection.FKCOLUMN_NAME, Reflection.PKCOLUMN_NAME); + + /** + * The database {@link Reflection} key to use for fetching the name of a relation. + * The name is used only for informative purpose and may be {@code null}. + */ + final String name; + + /** + * The database {@link Reflection} key to use for fetching the name of other table column. + * That column is part of a primary key if the direction is {@link #IMPORT}, or part of a + * foreigner key if the direction is {@link #EXPORT}. + */ + final String catalog, schema, table, column; - final String currentColumn; - final String foreignSchema; - final String foreignTable; - final String foreignColumn; - final boolean isImported; + /** + * The database {@link Reflection} key to use for fetching the name of the column in the table + * containing the {@code Relation}. That column is part of a foreigner key if the direction is + * {@link #IMPORT}, or part of a primary key if the direction is {@link #EXPORT}. + */ + final String containerColumn; + + /** + * Creates a new {@code Direction} enumeration value. + */ + private Direction(final String name, final String catalog, final String schema, + final String table, final String column, final String containerColumn) + { + this.name = name; + this.catalog = catalog; + this.schema = schema; + this.table = table; + this.column = column; + this.containerColumn = containerColumn; + } + } + + /** + * The catalog, schema and table name of the other table. + */ + final String catalog, schema, table; + + /** + * The columns of the other table that constitute a primary or foreigner key. Keys are the columns of the + * other table and values are columns of the table containing this {@code Relation}. + */ + private final Map<String,String> columns; + + /** + * Whether entries in foreigner table will be deleted if the primary keys in the referenced table is deleted. + * This is used as a hint for detecting what may be the "main" table in a relation. + */ final boolean cascadeOnDelete; - Relation(final String name, final String currentColumn, final String foreignSchema, - final String foreignTable, final String foreignColumn, - final boolean isImported, final boolean cascadeOnDelete) - { - super(name); - this.currentColumn = currentColumn; - this.foreignSchema = foreignSchema; - this.foreignTable = foreignTable; - this.foreignColumn = foreignColumn; - this.isImported = isImported; - this.cascadeOnDelete = cascadeOnDelete; + /** + * Creates a new relation for an imported key. The given {@code ResultSet} must be positioned + * on the first row of {@code DatabaseMetaData.getImportedKeys(catalog, schema, table)} result, + * and the result must be sorted in the order of the given keys: + * + * <ol> + * <li>{@link Direction#catalog}</li> + * <li>{@link Direction#schema}</li> + * <li>{@link Direction#table}</li> + * </ol> + * + * Note that JDBC specification ensures this order if {@link Direction#IMPORT} is used with the result of + * {@code getImportedKeys} and {@link Direction#EXPORT} is used with the result of {@code getExportedKeys}. + * + * <p>After construction, the {@code ResultSet} will be positioned on the first row of the next relation, + * or be closed if the last row has been reached. This constructor always moves the given result set by at + * least one row, unless an exception occurs.</p> + */ + Relation(final Direction dir, final ResultSet reflect) throws SQLException, DataStoreContentException { + super(reflect.getString(dir.name)); + catalog = reflect.getString(dir.catalog); + schema = reflect.getString(dir.schema); + table = reflect.getString(dir.table); + final Map<String,String> m = new LinkedHashMap<>(); + boolean cascade = false; + do { + final String column = reflect.getString(dir.column); + if (m.put(column, reflect.getString(dir.containerColumn)) != null) { + throw new DataStoreContentException(Resources.format(Resources.Keys.DuplicatedEntity_2, "Column", column)); + } + if (!cascade) { + cascade = reflect.getInt(Reflection.DELETE_RULE) == DatabaseMetaData.importedKeyCascade; + } + if (!reflect.next()) { + reflect.close(); + break; + } + } while (reflect.getString(dir.table) == table && + reflect.getString(dir.schema) == schema && + reflect.getString(dir.catalog) == catalog); + + columns = CollectionsExt.compact(m); + cascadeOnDelete = cascade; } + /** + * Creates a tree representation of this relation for debugging purpose. + */ + @Debug @Override - public String toString() { - return new StringBuilder(currentColumn) - .append(isImported ? " → " : " ← ") - .append(foreignSchema).append('.') - .append(foreignTable).append('.').append(foreignColumn) - .toString(); + TreeTable.Node appendTo(final TreeTable.Node parent) { + final TreeTable.Node node = super.appendTo(parent); + for (final Map.Entry<String,String> e : columns.entrySet()) { + newChild(node, e.getValue() + " → " + e.getKey()); + } + return node; } } diff --git a/storage/sis-sql/src/main/java/org/apache/sis/internal/sql/feature/Resources.java b/storage/sis-sql/src/main/java/org/apache/sis/internal/sql/feature/Resources.java new file mode 100644 index 0000000..5c4a910 --- /dev/null +++ b/storage/sis-sql/src/main/java/org/apache/sis/internal/sql/feature/Resources.java @@ -0,0 +1,140 @@ +/* + * 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.net.URL; +import java.util.Locale; +import java.util.MissingResourceException; +import javax.annotation.Generated; +import org.apache.sis.util.resources.KeyConstants; +import org.apache.sis.util.resources.IndexedResourceBundle; + + +/** + * Warning and error messages that are specific to the {@code sis-sql} module. + * Resources in this file should not be used by any other module. For resources shared by + * all modules in the Apache SIS project, see {@link org.apache.sis.util.resources} package. + * + * @author Martin Desruisseaux (Geomatys) + * @version 1.0 + * @since 1.0 + * @module + */ +public final class Resources extends IndexedResourceBundle { + /** + * Resource keys. This class is used when compiling sources, but no dependencies to + * {@code Keys} should appear in any resulting class files. Since the Java compiler + * inlines final integer values, using long identifiers will not bloat the constant + * pools of compiled classes. + * + * @author Martin Desruisseaux (IRD, Geomatys) + * @since 1.0 + * @module + */ + @Generated("org.apache.sis.util.resources.IndexedResourceCompiler") + public static final class Keys extends KeyConstants { + /** + * The unique instance of key constants handler. + */ + static final Keys INSTANCE = new Keys(); + + /** + * For {@link #INSTANCE} creation only. + */ + private Keys() { + } + + /** + * Unexpected duplication of “{0}” entity named “{1}”. + */ + public static final short DuplicatedEntity_2 = 1; + } + + /** + * Constructs a new resource bundle loading data from the given UTF file. + * + * @param resources the path of the binary file containing resources, or {@code null} if + * there is no resources. The resources may be a file or an entry in a JAR file. + */ + public Resources(final URL resources) { + super(resources); + } + + /** + * Returns the handle for the {@code Keys} constants. + * + * @return a handler for the constants declared in the inner {@code Keys} class. + */ + @Override + protected KeyConstants getKeyConstants() { + return Keys.INSTANCE; + } + + /** + * Returns resources in the given locale. + * + * @param locale the locale, or {@code null} for the default locale. + * @return resources in the given locale. + * @throws MissingResourceException if resources can not be found. + */ + public static Resources forLocale(final Locale locale) throws MissingResourceException { + return getBundle(Resources.class, locale); + } + + /** + * Gets a string for the given key from this resource bundle or one of its parents. + * + * @param key the key for the desired string. + * @return the string for the given key. + * @throws MissingResourceException if no object for the given key can be found. + */ + public static String format(final short key) throws MissingResourceException { + return forLocale(null).getString(key); + } + + /** + * Gets a string for the given key are replace all occurrence of "{0}" + * with values of {@code arg0}. + * + * @param key the key for the desired string. + * @param arg0 value to substitute to "{0}". + * @return the formatted string for the given key. + * @throws MissingResourceException if no object for the given key can be found. + */ + public static String format(final short key, + final Object arg0) throws MissingResourceException + { + return forLocale(null).getString(key, arg0); + } + + /** + * Gets a string for the given key are replace all occurrence of "{0}", + * "{1}", with values of {@code arg0}, {@code arg1}. + * + * @param key the key for the desired string. + * @param arg0 value to substitute to "{0}". + * @param arg1 value to substitute to "{1}". + * @return the formatted string for the given key. + * @throws MissingResourceException if no object for the given key can be found. + */ + public static String format(final short key, + final Object arg0, + final Object arg1) throws MissingResourceException + { + return forLocale(null).getString(key, arg0, arg1); + } +} diff --git a/storage/sis-sql/src/main/java/org/apache/sis/internal/sql/feature/Resources.properties b/storage/sis-sql/src/main/java/org/apache/sis/internal/sql/feature/Resources.properties new file mode 100644 index 0000000..3e2e217 --- /dev/null +++ b/storage/sis-sql/src/main/java/org/apache/sis/internal/sql/feature/Resources.properties @@ -0,0 +1,22 @@ +# +# 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. +# + +# +# Resources in this file are for "sis-sql" 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. +# +DuplicatedEntity_2 = Unexpected duplication of \u201c{0}\u201d entity named \u201c{1}\u201d. diff --git a/storage/sis-sql/src/main/java/org/apache/sis/internal/sql/feature/Resources_fr.properties b/storage/sis-sql/src/main/java/org/apache/sis/internal/sql/feature/Resources_fr.properties new file mode 100644 index 0000000..cd0f2b9 --- /dev/null +++ b/storage/sis-sql/src/main/java/org/apache/sis/internal/sql/feature/Resources_fr.properties @@ -0,0 +1,27 @@ +# +# 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. +# + +# +# Resources in this file are for "sis-sql" 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. +# +# Punctuation rules in French (source: http://unicode.org/udhr/n/notes_fra.html) +# +# U+202F NARROW NO-BREAK SPACE before ; ! and ? +# U+00A0 NO-BREAK SPACE before : +# +DuplicatedEntity_2 = Doublon inattendu d\u2019une entit\u00e9 \u00ab\u202f{0}\u202f\u00bb nomm\u00e9e \u00ab\u202f{1}\u202f\u00bb. diff --git a/storage/sis-sql/src/main/java/org/apache/sis/internal/sql/feature/Schema.java b/storage/sis-sql/src/main/java/org/apache/sis/internal/sql/feature/Schema.java index 0d0d7a2..cc6001b 100644 --- a/storage/sis-sql/src/main/java/org/apache/sis/internal/sql/feature/Schema.java +++ b/storage/sis-sql/src/main/java/org/apache/sis/internal/sql/feature/Schema.java @@ -17,53 +17,67 @@ package org.apache.sis.internal.sql.feature; import java.util.Map; -import java.util.HashMap; +import java.util.LinkedHashMap; import java.util.Collection; import org.apache.sis.util.Debug; import org.apache.sis.util.collection.TreeTable; +import org.apache.sis.storage.DataStoreContentException; /** * Description of a database schema. + * Each schema contains a collection of {@link Table}s. * * @author Johann Sorel (Geomatys) + * @author Martin Desruisseaux (Geomatys) * @version 1.0 * @since 1.0 * @module */ final class Schema extends MetaModel { /** - * The tables in the schema. + * The tables in this schema. */ - final Map<String,Table> tables; + private final Map<String,Table> tables; /** - * Creates a new schema of the given name. - * It is caller responsibility to populate the {@link #tables} map. + * Creates a new, initially empty, schema of the given name. + * + * @param schemaName name of this schema. */ - Schema(final String name) { - super(name); - tables = new HashMap<>(); + Schema(final String schemaName) { + super(schemaName); + tables = new LinkedHashMap<>(); } /** - * Returns all tables in this schema. + * Adds a table in this schema. + * + * @param table the table to add. + * @throws DataStoreContentException if a table of the same name has already been added. */ - Collection<Table> getTables() { - return tables.values(); + void addTable(final Table table) throws DataStoreContentException { + if (tables.putIfAbsent(table.name, table) != null) { + throw new DataStoreContentException(Resources.format(Resources.Keys.DuplicatedEntity_2, "Table", table.name)); + } } /** * Returns the table of the given name, or {@code null} if none. */ - Table getTable(final String name){ + Table getTable(final String name) { return tables.get(name); } /** - * Creates a tree representation of this object for debugging purpose. - * - * @param parent the parent node where to add the tree representation. + * Returns all tables in this schema. + */ + Collection<Table> getTables() { + return tables.values(); + } + + /** + * Creates a tree representation of this schema with the list of all tables. */ @Debug @Override diff --git a/storage/sis-sql/src/main/java/org/apache/sis/internal/sql/feature/SpatialFunctions.java b/storage/sis-sql/src/main/java/org/apache/sis/internal/sql/feature/SpatialFunctions.java index e53966b..af3fbb5 100644 --- a/storage/sis-sql/src/main/java/org/apache/sis/internal/sql/feature/SpatialFunctions.java +++ b/storage/sis-sql/src/main/java/org/apache/sis/internal/sql/feature/SpatialFunctions.java @@ -41,8 +41,8 @@ import org.apache.sis.storage.DataStoreException; */ abstract class SpatialFunctions { /** - * The tables to be ignored when inspecting the tables in a database schema. - * Those tables are used for database (e.g. PostGIS) internal working. + * Names of tables to ignore when inspecting a database schema. + * Those tables are used for database internal working (for example by PostGIS). */ private final Set<String> ignoredTables; @@ -74,7 +74,7 @@ abstract class SpatialFunctions { } /** - * Indicates whether a table is reserved for the database internal working. + * Returns whether a table is reserved for database internal working. * If this method returns {@code false}, then the given table is a candidate * for use as a {@code FeatureType}. * diff --git a/storage/sis-sql/src/main/java/org/apache/sis/internal/sql/feature/Table.java b/storage/sis-sql/src/main/java/org/apache/sis/internal/sql/feature/Table.java index ecdf264..d451f48 100644 --- a/storage/sis-sql/src/main/java/org/apache/sis/internal/sql/feature/Table.java +++ b/storage/sis-sql/src/main/java/org/apache/sis/internal/sql/feature/Table.java @@ -24,7 +24,7 @@ import org.apache.sis.feature.builder.FeatureTypeBuilder; /** - * Description of a database table. + * Description of a table in the database. The description is provided as a {@code FeatureType}. * * @author Johann Sorel (Geomatys) * @version 1.0 @@ -32,54 +32,53 @@ import org.apache.sis.feature.builder.FeatureTypeBuilder; * @module */ final class Table extends MetaModel { - enum View { - TABLE, - SIMPLE_FEATURE_TYPE, - COMPLEX_FEATURE_TYPE, - ALL_COMPLEX - } - - String type; - + /** + * @deprecated to be replaced by {@link #featureType} only (TODO). + */ + @Deprecated FeatureTypeBuilder tableType; - FeatureTypeBuilder simpleFeatureType; - FeatureTypeBuilder complexFeatureType; - FeatureTypeBuilder allTypes; - PrimaryKey key; + /** + * A temporary object used for building the {@code FeatureType}. + */ + FeatureTypeBuilder featureType; /** - * those are 0:1 relations + * The primary key of this table. */ - final Collection<Relation> importedKeys = new ArrayList<>(); + PrimaryKey key; /** - * those are 0:N relations + * The primary keys of other tables that are referenced by this table foreign key columns. + * They are 0:1 relations. */ - final Collection<Relation> exportedKeys = new ArrayList<>(); + final Collection<Relation> importedKeys; /** - * inherited tables + * The foreign keys of other tables that reference this table primary key columns. + * They are 0:N relations */ - final Collection<String> parents = new ArrayList<>(); + final Collection<Relation> exportedKeys; - Table(final String name, final String type) { + /** + * Creates a new table of the given name. + */ + Table(final String name) { super(name); - this.type = type; + importedKeys = new ArrayList<>(); + exportedKeys = new ArrayList<>(); } /** - * Determines if given type is a subtype. Conditions are: + * Determines if this table is a component of another table. Conditions are: * <ul> * <li>having a relation toward another type</li> * <li>relation must be cascading.</li> * </ul> * - * @return true is type is a subtype. - * - * @todo a subtype of what? + * @return whether this table is a component of another table. */ - boolean isSubType() { + boolean isComponent() { for (Relation relation : importedKeys) { if (relation.cascadeOnDelete) { return true; @@ -88,16 +87,6 @@ final class Table extends MetaModel { return false; } - FeatureTypeBuilder getType(final View view) { - switch (view) { - case TABLE: return tableType; - case SIMPLE_FEATURE_TYPE: return simpleFeatureType; - case COMPLEX_FEATURE_TYPE: return complexFeatureType; - case ALL_COMPLEX: return allTypes; - default: throw new IllegalArgumentException("Unknown view type: " + view); - } - } - /** * Creates a tree representation of this object for debugging purpose. * diff --git a/storage/sis-sql/src/main/java/org/apache/sis/internal/sql/feature/package-info.java b/storage/sis-sql/src/main/java/org/apache/sis/internal/sql/feature/package-info.java index 80a2211..e123306 100644 --- a/storage/sis-sql/src/main/java/org/apache/sis/internal/sql/feature/package-info.java +++ b/storage/sis-sql/src/main/java/org/apache/sis/internal/sql/feature/package-info.java @@ -17,7 +17,7 @@ /** - * Build {@link org.opengis.feature.FeatureType}s by inspection of a database schema. + * Build {@link org.opengis.feature.FeatureType}s by inspection of database schemas. * The work done here is similar to reverse engineering. * * <STRONG>Do not use!</STRONG>
