blob: 736bb44444a5af0a54e1b07f952e90bb441f8bcb [file] [log] [blame]
/*
* 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.Map;
import java.util.List;
import java.util.ArrayList;
import java.util.LinkedHashMap;
import java.sql.SQLException;
import java.sql.ResultSet;
import org.apache.sis.storage.DataStoreException;
import org.apache.sis.internal.metadata.sql.Reflection;
import org.apache.sis.internal.metadata.sql.SQLUtilities;
import org.apache.sis.internal.util.Strings;
/**
* Constructor for a {@link Table} based on a "physical" table.
* The table is identified by {@link #id}, which contains a (catalog, schema, name) tuple.
*
* @author Johann Sorel (Geomatys)
* @author Martin Desruisseaux (Geomatys)
* @author Alexis Manin (Geomatys)
* @version 1.1
* @since 1.1
* @module
*/
final class TableAnalyzer extends FeatureAnalyzer {
/**
* If the analyzed table is imported by the foreigner keys of another table, the parent table.
* Otherwise {@code null}. This is relevant only for {@link Relation.Direction#EXPORT}.
*/
private final TableReference importedBy;
/**
* The table/schema name width {@code '_'} and {@code '%'} characters escaped.
* These names are intended for use in arguments expecting a {@code LIKE} pattern.
*/
private final String tableEsc, schemaEsc;
/**
* Creates an analyzer for 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.
*
* @param id the catalog, schema and table name of the table to analyze.
* @param importedBy if the analyzed table is imported by the foreigner keys of another table,
* the parent table. Otherwise {@code null}.
* @throws SQLException if an error occurred while fetching information from the database.
*/
TableAnalyzer(final Analyzer analyzer, final TableReference id, final TableReference importedBy) throws SQLException {
super(analyzer, id);
this.importedBy = importedBy;
this.tableEsc = escape(id.table);
this.schemaEsc = escape(id.schema);
try (ResultSet reflect = analyzer.metadata.getPrimaryKeys(id.catalog, id.schema, id.table)) {
while (reflect.next()) {
primaryKey.add(analyzer.getUniqueString(reflect, Reflection.COLUMN_NAME));
}
}
/*
* Note: when a table contains no primary keys, we could still look for index columns
* with unique constraint using `metadata.getIndexInfo(catalog, schema, table, true)`.
* We don't do that for now because of uncertainties (which index to use if there is many?
* If they are suitable as identifiers why they are not already defined as primary keys?)
*/
}
/**
* Returns the given pattern with {@code '_'} and {@code '%'} characters escaped by the database-specific
* escape characters. This method should be invoked for escaping the values of all {@link DatabaseMetaData}
* method arguments with a name ending by {@code "Pattern"}. Note that not all arguments are pattern; please
* checks carefully {@link DatabaseMetaData} javadoc for each method.
*
* <div class="note"><b>Example:</b> if a method expects an argument named {@code tableNamePattern},
* then that argument value should be escaped. But if the argument name is only {@code tableName},
* then the value should not be escaped.</div>
*/
private String escape(final String pattern) {
return SQLUtilities.escape(pattern, analyzer.escape);
}
/**
* Returns 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.
*
* <h4>Side effects</h4>
* This method shall be invoked exactly once for each direction.
* <p><b>Required by this method:</b> none.</p>
* <p><b>Computed by this method:</b> {@link #foreignerKeys}.</p>
*
* @param direction direction of the foreigner key for which to return components.
* @return components of the foreigner key in the requested direction.
*/
@Override
final Relation[] getForeignerKeys(final Relation.Direction direction) throws SQLException, DataStoreException {
final List<Relation> relations = new ArrayList<>();
final boolean isImport = (direction == Relation.Direction.IMPORT);
try (ResultSet reflect = isImport ? analyzer.metadata.getImportedKeys(id.catalog, id.schema, id.table)
: analyzer.metadata.getExportedKeys(id.catalog, id.schema, id.table))
{
if (reflect.next()) do {
final Relation relation = new Relation(analyzer, direction, reflect);
if (isImport) {
addForeignerKeys(relation);
} else if (relation.equals(importedBy)) {
continue;
}
relations.add(relation);
} while (!reflect.isClosed());
}
final int size = relations.size();
return (size != 0) ? relations.toArray(new Relation[size]) : Relation.EMPTY;
}
/**
* Configures the feature builder with attributes and associations inferred from the analyzed table.
* The ordinary attributes and the associations (inferred from foreigner keys) are handled together
* in order to have properties listed in the same order as the columns in the database table.
*
* <h4>Side effects</h4>
* <p><b>Required by this method:</b> {@link #foreignerKeys}.</p>
* <p><b>Computed by this method:</b> {@link #primaryKey}, {@link #primaryKeyClass}.</p>
*
* @param feature the builder where to add attributes and associations.
* @return the columns for attribute values (not including associations).
*/
@Override
final Column[] createAttributes() throws Exception {
/*
* Get all columns in advance because `completeGeometryColumns(…)`
* needs to be invoked before to invoke `database.getMapping(column)`.
*/
final Map<String,Column> columns = new LinkedHashMap<>();
try (ResultSet reflect = analyzer.metadata.getColumns(id.catalog, schemaEsc, tableEsc, null)) {
while (reflect.next()) {
final Column column = new Column(analyzer, reflect);
if (columns.put(column.name, column) != null) {
throw duplicatedColumn(column);
}
}
}
final InfoStatements spatialInformation = analyzer.spatialInformation;
if (spatialInformation != null) {
spatialInformation.completeGeometryColumns(id, columns);
}
/*
* Analyze the type of each column, which may be geometric as a consequence of above call.
*/
final List<Column> attributes = new ArrayList<>();
for (final Column column : columns.values()) {
if (createAttribute(column)) {
attributes.add(column);
}
}
return attributes.toArray(new Column[attributes.size()]);
}
/**
* Returns an optional description of the application schema.
*/
@Override
public String getRemarks() throws SQLException {
if (id instanceof Relation) {
try (ResultSet reflect = analyzer.metadata.getTables(id.catalog, schemaEsc, tableEsc, null)) {
while (reflect.next()) {
final String remarks = Strings.trimOrNull(analyzer.getUniqueString(reflect, Reflection.REMARKS));
if (remarks != null) {
return remarks;
}
}
}
}
return super.getRemarks();
}
}