blob: e07765449d6a6524c0d0f1715abe3fb0d7a386e3 [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.storage.sql.feature;
import java.util.Map;
import java.util.List;
import java.util.HashMap;
import java.util.ArrayList;
import java.util.Collection;
import java.lang.reflect.Array;
import java.sql.DatabaseMetaData;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import org.apache.sis.metadata.sql.privy.SQLBuilder;
import org.apache.sis.storage.InternalDataStoreException;
import org.apache.sis.util.ArraysExt;
import org.apache.sis.util.collection.WeakValueHashMap;
// Specific to the geoapi-3.1 and geoapi-4.0 branches:
import org.opengis.feature.Feature;
import org.opengis.feature.FeatureType;
/**
* Converter of {@link ResultSet} rows to {@link Feature} instances.
* Each {@code FeatureAdapter} instance is specific to the set of rows given by a SQL query,
* ignoring {@code DISTINCT}, {@code ORDER BY} and filter conditions in the {@code WHERE} clause.
* This class does not hold JDBC resources; {@link ResultSet} must be provided by the caller.
* This object can be prepared once and reused every time the query needs to be executed.
*
* <h2>Multi-threading</h2>
* This class is immutable (except for the cache) and safe for concurrent use by many threads.
* The content of arrays in this class shall not be modified in order to preserve immutability.
*
* @author Martin Desruisseaux (Geomatys)
* @author Alexis Manin (Geomatys)
*/
final class FeatureAdapter {
/**
* An empty array of adapters, used when there is no dependency.
*/
private static final FeatureAdapter[] EMPTY = new FeatureAdapter[0];
/**
* The type of features to create.
*
* @see Table#featureType
*/
private final FeatureType featureType;
/**
* Attributes in feature instances, excluding operations and associations to other tables.
* Elements are in the order of columns declared in the {@code SELECT <columns>} statement.
* This array is a shared instance and shall not be modified.
*
* @see Table#attributes
*/
private final Column[] attributes;
/**
* Name of the properties where are stored associations in feature instances.
* The length of this array shall be equal to the {@link #dependencies} array length.
* Imported or exported features read by {@code dependencies[i]} will be stored in
* the association named {@code associationNames[i]}.
*/
final String[] associationNames;
/**
* Name of the property where to store the association that we cannot handle with other {@link #dependencies}.
* This deferred association may exist because of circular dependency.
*/
final String deferredAssociation;
/**
* The feature sets referenced through foreigner keys, or {@link #EMPTY} if none.
* This includes the associations inferred from both the imported and exported keys.
* The first {@link #importCount} iterators are for imported keys, and the remaining
* iterators are for the exported keys.
*/
final FeatureAdapter[] dependencies;
/**
* Number of entries in {@link #dependencies} for {@link Relation.Direction#IMPORT}.
* The entries immediately following the first {@code importCount} entries are for
* {@link Relation.Direction#EXPORT}.
*/
final int importCount;
/**
* One-based indices of the columns to query for each {@link #dependencies} entry.
*/
private final int[][] foreignerKeyIndices;
/**
* Feature instances already created, or {@code null} if the features created by this iterator are not cached.
* This map is used when requesting a feature by identifier, not when iterating over all features (note: we
* could perform an opportunistic check in a future SIS version). The same map may be shared by all iterators
* on the same {@link Table}, but {@link WeakValueHashMap} already provides the required synchronizations.
*
* <p>The {@link FeatureIterator} class does not require the identifiers to be built from primary key columns.
* However if this map has been provided by {@link Table#instanceForPrimaryKeys()}, then the identifiers
* need to be primary keys with columns in the exact same order for allowing the same map to be shared.</p>
*/
final WeakValueHashMap<?,Object> instances;
/**
* The component class of the keys in the {@link #instances} map, or {@code null} if the keys are not array.
* For example if a primary key is made of two columns of type {@code String}, then this field may be set to
* {@code String}.
*/
private final Class<?> keyComponentClass;
/**
* The SQL statement to execute for creating features, without {@code DISTINCT} or {@code ORDER BY} clauses.
* May contain a {@code WHERE} clause for fetching a dependency, but not for user-specified filtering.
*/
final String sql;
/**
* Creates a new adapter for features in the given table.
*
* @param table the table for which we are creating an adapter.
* @param metadata metadata about the database.
*/
FeatureAdapter(final Table table, final DatabaseMetaData metadata) throws SQLException, InternalDataStoreException {
this(table, metadata, new ArrayList<>(), null);
}
/**
* Creates a new adapter for features in the given table.
* This constructor may be invoked recursively for creating adapters for dependencies.
*
* @param table the table for which we are creating an adapter.
* @param metadata metadata about the database.
* @param following the relations that we are following. Used for avoiding never ending loop.
* @param noFollow relation to not follow, or {@code null} if none.
*/
private FeatureAdapter(final Table table, final DatabaseMetaData metadata,
final List<Relation> following, final Relation noFollow)
throws SQLException, InternalDataStoreException
{
this.featureType = table.featureType;
this.attributes = table.attributes;
if (table.primaryKey != null) {
keyComponentClass = table.primaryKey.valueClass.getComponentType();
} else {
keyComponentClass = null;
}
final Map<String,Integer> columnIndices = new HashMap<>();
/*
* Create a SELECT clause with all columns that are ordinary attributes.
* Order matter, because `FeatureIterator` iterator will map the columns
* to the attributes listed in the `attributes` array in that order.
*/
final SQLBuilder sql = new SQLBuilder(table.database).append(SQLBuilder.SELECT);
for (final Column column : attributes) {
appendColumn(sql, column.label, columnIndices);
}
/*
* Collect information about associations in local arrays before to assign
* them to the final fields, because some array lengths may be adjusted.
*/
int count = table.importedKeys.length
+ table.exportedKeys.length;
if (count == 0) {
importCount = 0;
dependencies = EMPTY;
associationNames = null;
foreignerKeyIndices = null;
deferredAssociation = null;
} else {
String deferredAssociation = null;
final FeatureAdapter[] dependencies = new FeatureAdapter[count];
final String[] associationNames = new String[count];
final int[][] foreignerKeyIndices = new int[count][];
/*
* For each foreigner key to another table, append all columns of that foreigner key
* and the name of the single feature property where the association will be stored.
*/
count = 0; // We will recount.
for (final Relation dependency : table.importedKeys) {
if (dependency.excluded) continue;
if (dependency != noFollow) {
dependency.startFollowing(following); // Safety against never-ending recursivity.
associationNames [count] = dependency.propertyName;
foreignerKeyIndices[count] = getColumnIndices(sql, dependency, columnIndices);
dependencies [count] = new FeatureAdapter(dependency.getSearchTable(), metadata, following, noFollow);
dependency.endFollowing(following);
count++;
} else {
deferredAssociation = dependency.propertyName;
}
}
importCount = count;
/*
* Create adapters for other tables that reference the primary keys of this table. For example
* if we have a "City" feature with attributes for the city name, population, etc. and a "Parks"
* feature referencing the city where the park is located, in order to populate the "City.parks"
* associations we need to iterate over all "Parks" rows referencing the city.
*/
for (final Relation dependency : table.exportedKeys) {
if (dependency.excluded) continue;
dependency.startFollowing(following); // Safety against never-ending recursivity.
final Table foreigner = dependency.getSearchTable();
final Relation inverse = foreigner.getInverseOf(dependency, table.name);
associationNames [count] = dependency.propertyName;
foreignerKeyIndices[count] = getColumnIndices(sql, dependency, columnIndices);
dependencies [count] = new FeatureAdapter(foreigner, metadata, following, inverse);
dependency.endFollowing(following);
count++;
}
if (count != 0) {
this.dependencies = ArraysExt.resize(dependencies, count);
this.associationNames = ArraysExt.resize(associationNames, count);
this.foreignerKeyIndices = ArraysExt.resize(foreignerKeyIndices, count);
} else {
this.dependencies = EMPTY;
this.associationNames = null;
this.foreignerKeyIndices = null;
}
this.deferredAssociation = deferredAssociation;
}
/*
* Prepare SQL for a `Statement` if we do not need any condition, or for a `PreparedStatement`
* if we need to add a `WHERE` clause. In the latter case, we will cache the features already
* created if there is a possibility that many rows reference the same feature instance.
*/
table.appendFromClause(sql);
if (following.isEmpty()) {
instances = null; // A future SIS version could use the map opportunistically if it exists.
} else {
final Relation componentOf = following.get(following.size() - 1);
String separator = " WHERE ";
for (final String primaryKey : componentOf.getSearchColumns()) {
sql.append(separator).appendIdentifier(primaryKey).append("=?");
separator = " AND ";
}
/*
* Following assumes that the foreigner key references the primary key of this table,
* in which case `table.primaryKey` should never be null. This assumption may not hold
* if the relation has been defined by `DatabaseMetaData.getCrossReference(…)` instead.
*/
if (componentOf.useFullKey()) {
instances = table.instanceForPrimaryKeys();
} else {
instances = new WeakValueHashMap<>(Object.class); // Cannot share the table cache.
}
}
this.sql = sql.toString();
}
/**
* Appends a columns in the given builder and remember the column indices.
* An exception is thrown if the column has already been added (should never happen).
*
* @param sql the SQL statement where to add column identifiers after the {@code SELECT} clause.
* @param column name of the column to add.
* @param columnIndices map where to add the mapping from column name to 1-based column index.
*/
private static int appendColumn(final SQLBuilder sql, final String column,
final Map<String,Integer> columnIndices) throws InternalDataStoreException
{
int columnCount = columnIndices.size();
if (columnCount != 0) sql.append(", ");
sql.appendIdentifier(column);
if (columnIndices.put(column, ++columnCount) == null) return columnCount;
throw new InternalDataStoreException(Resources.format(Resources.Keys.DuplicatedColumn_1, column));
}
/**
* Computes the 1-based indices of columns of foreigner keys of given dependency.
* This method also ensure that the SQL statement contains all required columns,
* adding missing columns in the given SQL builder if necessary.
*
* @param sql the SQL statement to complete if there is missing columns.
* @param dependency the dependency for which to get column indices of foreigner keys.
* @param columnIndices the map containing existing column indices, or where to add missing column indices.
* @return indices of columns of foreigner keys of given dependency. Numbering starts at 1.
*/
private static int[] getColumnIndices(final SQLBuilder sql, final Relation dependency,
final Map<String,Integer> columnIndices) throws InternalDataStoreException
{
final Collection<String> columns = dependency.getOwnerColumns();
int i = 0;
final int[] indices = new int[columns.size()];
for (final String column : columns) {
final Integer pos = columnIndices.get(column);
indices[i++] = (pos != null) ? pos : appendColumn(sql, column, columnIndices);
}
return indices;
}
// ────────────────────────────────────────────────────────────────────────────────────────
// End of adapter construction. Next methods are helper methods for feature iterator.
// ────────────────────────────────────────────────────────────────────────────────────────
/**
* Creates a feature with attribute values initialized to values fetched from the given result set.
* This method does not follow associations.
*
* @param stmts prepared statements for fetching CRS from SRID, or {@code null} if none.
* @param result the result set from which to get attribute values.
* @return the feature with attribute values initialized.
* @throws Exception if an error occurred while reading the database or converting values.
*/
final Feature createFeature(final InfoStatements stmts, final ResultSet result) throws Exception {
final Feature feature = featureType.newInstance();
for (int i=0; i<attributes.length; i++) {
final Column column = attributes[i];
final Object value = column.valueGetter.getValue(stmts, result, i+1);
if (value != null) {
feature.setPropertyValue(column.propertyName, value);
}
}
return feature;
}
/**
* Returns the key to use for caching the feature of a dependency.
* If the foreigner key uses only one column, we will use the foreigner key value without creating array.
* But if the foreigner key uses more than one column, then we need to create an array holding all values.
*
* @param result the result set over rows expected by this feature adapter.
* @param dependency index of the dependency for which to create a cache key.
* @return key to use for accesses in the {@link #instances} map,
* or {@code null} if any component of the key is null.
*/
final Object getCacheKey(final ResultSet result, final int dependency) throws SQLException {
final int[] columnIndices = foreignerKeyIndices[dependency];
final int n = columnIndices.length;
final Object keys = (n > 1) ? Array.newInstance(dependencies[dependency].keyComponentClass, n) : null;
Object key = null;
for (int p=0; p<n; p++) {
key = result.getObject(columnIndices[p]);
if (keys != null) Array.set(keys, p, key);
if (key == null) return null;
}
return (keys != null) ? keys : key;
}
/**
* Sets the statement parameters for searching a dependency.
*
* @param result the result set over rows expected by this feature adapter.
* @param target the statement on which to set parameters.
* @param dependency index of the dependency for which to set the parameters.
*/
final void setForeignerKeys(final ResultSet source, final PreparedStatement target, final int dependency)
throws SQLException
{
final int[] columnIndices = foreignerKeyIndices[dependency];
for (int p=0; p < columnIndices.length;) {
final Object k = source.getObject(columnIndices[p]);
target.setObject(++p, k);
}
}
}