blob: 57e9c1fd1ba6b52c282f359b3989973f5c32bc03 [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.List;
import java.util.ArrayList;
import java.util.Map;
import java.util.HashMap;
import java.util.Collection;
import java.util.Spliterator;
import java.util.function.Consumer;
import java.sql.Connection;
import java.sql.DatabaseMetaData;
import java.sql.Statement;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.lang.reflect.Array;
import org.apache.sis.internal.metadata.sql.SQLBuilder;
import org.apache.sis.storage.InternalDataStoreException;
import org.apache.sis.util.collection.BackingStoreException;
import org.apache.sis.util.collection.WeakValueHashMap;
import org.apache.sis.util.ArraysExt;
// Branch-dependent imports
import org.opengis.feature.Feature;
import org.opengis.feature.FeatureType;
/**
* Iterator over feature instances.
*
* @author Martin Desruisseaux (Geomatys)
* @version 1.0
* @since 1.0
* @module
*/
final class Features implements Spliterator<Feature>, Runnable {
/**
* An empty array of iterators, used when there is no dependency.
*/
private static final Features[] EMPTY = new Features[0];
/**
* The type of features to create.
*/
private final FeatureType featureType;
/**
* Name of attributes in feature instances, excluding operations and associations to other tables.
* Those names are in the order of columns declared in the {@code SELECT <columns} statement.
* This array is a shared instance and shall not be modified.
*/
private final String[] attributeNames;
/**
* 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]}.
*/
private final String[] associationNames;
/**
* Name of the property where to store the association that we can not handle with other {@link #dependencies}.
* This deferred association may exist because of circular dependency.
*/
private 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.
*/
private final Features[] 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}.
*/
private final int importCount;
/**
* One-based indices of the columns to query for each {@link #dependencies} entry.
*/
private final int[][] foreignerKeyIndices;
/**
* If this iterator returns only the feature matching some condition (typically a primary key value),
* the statement for performing that filtering. Otherwise if this iterator returns all features, then
* this field is {@code null}.
*/
private final PreparedStatement statement;
/**
* The result of executing the SQL query for a {@link Table}. If {@link #statement} is null,
* then a single {@code ResultSet} is used for all the lifetime of this {@code Features} instance.
* Otherwise an arbitrary amount of {@code ResultSet}s may be created from the statement.
*/
private ResultSet result;
/**
* 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>This {@code Features} 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>
*/
private 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;
/**
* Estimated number of rows, or {@literal <= 0} if unknown.
*/
private final long estimatedSize;
/**
* Creates a new iterator over the feature instances.
*
* @param table the table for which we are creating an iterator.
* @param connection connection to the database.
* @param attributeNames value of {@link Table#attributeNames}: where to store simple values.
* @param attributeColumns value of {@link Table#attributeColumns}: often the same as attribute names.
* @param importedKeys value of {@link Table#importedKeys}: targets of this table foreign keys.
* @param exportedKeys value of {@link Table#exportedKeys}: foreigner keys of other tables.
* @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.
*/
Features(final Table table, final Connection connection, final String[] attributeNames, final String[] attributeColumns,
final Relation[] importedKeys, final Relation[] exportedKeys, final List<Relation> following, final Relation noFollow)
throws SQLException, InternalDataStoreException
{
this.featureType = table.featureType;
this.attributeNames = attributeNames;
final DatabaseMetaData metadata = connection.getMetaData();
estimatedSize = following.isEmpty() ? table.countRows(metadata, true) : 0;
final SQLBuilder sql = new SQLBuilder(metadata, true).append("SELECT");
final Map<String,Integer> columnIndices = new HashMap<>();
/*
* Create a SELECT clause with all columns that are ordinary attributes.
* Order matter, since 'Features' iterator will map the columns to the
* attributes listed in the 'attributeNames' array in that order.
*/
for (String column : attributeColumns) {
appendColumn(sql, column, columnIndices);
}
/*
* Collect information about associations in local arrays before to assign
* them to the final fields, because some array lengths may be adjusted.
*/
int importCount = (importedKeys != null) ? importedKeys.length : 0;
int exportCount = (exportedKeys != null) ? exportedKeys.length : 0;
int totalCount = importCount + exportCount;
if (totalCount == 0) {
dependencies = EMPTY;
associationNames = null;
foreignerKeyIndices = null;
deferredAssociation = null;
} else {
String deferredAssociation = null;
final Features[] dependencies = new Features[totalCount];
final String[] associationNames = new String [totalCount];
final int[][] foreignerKeyIndices = new int [totalCount][];
/*
* 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.
*/
if (importCount != 0) {
importCount = 0; // We will recount.
for (final Relation dependency : importedKeys) {
if (dependency != noFollow) {
dependency.startFollowing(following); // Safety against never-ending recursivity.
associationNames [importCount] = dependency.propertyName;
foreignerKeyIndices[importCount] = getColumnIndices(sql, dependency.getForeignerKeys(), columnIndices);
dependencies [importCount] = dependency.getSearchTable().features(connection, following, noFollow);
dependency.endFollowing(following);
importCount++;
} else {
deferredAssociation = dependency.propertyName;
}
}
}
/*
* Create iterators 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.
*/
if (exportCount != 0) {
int i = importCount;
for (final Relation dependency : exportedKeys) {
dependency.startFollowing(following); // Safety against never-ending recursivity.
final Table foreigner = dependency.getSearchTable();
final Relation inverse = foreigner.getInverseOf(dependency, table.name);
associationNames [i] = dependency.propertyName;
foreignerKeyIndices[i] = getColumnIndices(sql, dependency.getForeignerKeys(), columnIndices);
dependencies [i] = foreigner.features(connection, following, inverse);
dependency.endFollowing(following);
i++;
}
}
totalCount = importCount + exportCount;
this.dependencies = ArraysExt.resize(dependencies, totalCount);
this.associationNames = ArraysExt.resize(associationNames, totalCount);
this.foreignerKeyIndices = ArraysExt.resize(foreignerKeyIndices, totalCount);
this.deferredAssociation = deferredAssociation;
}
this.importCount = importCount;
/*
* Create a Statement if we don't need any condition, or a PreparedStatement if we need to add
* a "WHERE" clause. In the later case, we will cache the features already created if there is
* a possibility that many rows reference the same feature instance.
*/
sql.append(" FROM ").appendIdentifier(table.name.catalog, table.name.schema, table.name.table);
if (following.isEmpty()) {
statement = null;
instances = null; // A future SIS version could use the map opportunistically if it exists.
keyComponentClass = null;
result = connection.createStatement().executeQuery(sql.toString());
} else {
final Relation componentOf = following.get(following.size() - 1);
String separator = " WHERE ";
for (String primaryKey : componentOf.getSearchColumns()) {
sql.append(separator).appendIdentifier(primaryKey).append("=?");
separator = " AND ";
}
statement = connection.prepareStatement(sql.toString());
/*
* Following assumes that the foreigner key references the primary key of this table,
* in which case 'table.primaryKeyClass' 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();
keyComponentClass = table.primaryKeyClass.getComponentType();
} else {
instances = new WeakValueHashMap<>(Object.class); // Can not share the table cache.
keyComponentClass = Object.class;
}
}
}
/**
* 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).
*/
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.append(' ').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 given columns, adding the columns in the given builder if necessary.
*/
private static int[] getColumnIndices(final SQLBuilder sql, final Collection<String> columns,
final Map<String,Integer> columnIndices) throws InternalDataStoreException
{
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;
}
/**
* Returns an array of the given length capable to hold the identifier,
* or {@code null} if there is no need for an array.
*/
private Object identifierArray(final int columnCount) {
return (columnCount > 1) ? Array.newInstance(keyComponentClass, columnCount) : null;
}
/**
* Declares that this iterator never returns {@code null} elements.
*/
@Override
public int characteristics() {
return NONNULL;
}
/**
* Returns the estimated number of features, or {@link Long#MAX_VALUE} if unknown.
*/
@Override
public long estimateSize() {
return (estimatedSize > 0) ? estimatedSize : Long.MAX_VALUE;
}
/**
* Current version does not support split.
*
* @return always {@code null}.
*/
@Override
public Spliterator<Feature> trySplit() {
return null;
}
/**
* Gives the next feature to the given consumer.
*/
@Override
public boolean tryAdvance(final Consumer<? super Feature> action) {
try {
return fetch(action, false);
} catch (SQLException e) {
throw new BackingStoreException(e);
}
}
/**
* Gives all remaining features to the given consumer.
*/
@Override
public void forEachRemaining(final Consumer<? super Feature> action) {
try {
fetch(action, true);
} catch (SQLException e) {
throw new BackingStoreException(e);
}
}
/**
* Gives at least the next feature to the given consumer.
* Gives all remaining features if {@code all} is {@code true}.
*
* @param action the action to execute for each {@link Feature} instances fetched by this method.
* @param all {@code true} for reading all remaining feature instances, or {@code false} for only the next one.
* @return {@code true} if we have read an instance and {@code all} is {@code false} (so there is maybe other instances).
*/
private boolean fetch(final Consumer<? super Feature> action, final boolean all) throws SQLException {
while (result.next()) {
final Feature feature = featureType.newInstance();
for (int i=0; i < attributeNames.length; i++) {
final Object value = result.getObject(i+1);
if (!result.wasNull()) {
feature.setPropertyValue(attributeNames[i], value);
}
}
for (int i=0; i < dependencies.length; i++) {
final Features dependency = dependencies[i];
final int[] columnIndices = foreignerKeyIndices[i];
final Object value;
if (i < importCount) {
/*
* Relation.Direction.IMPORT: this table contains the foreigner keys.
*
* If the foreigner key uses only one column, we will store the foreigner key value
* in the 'key' variable without creating array. But if the foreigner key uses more
* than one column, then we need to create an array holding all values.
*/
Object key = null;
final Object keys = dependency.identifierArray(columnIndices.length);
for (int p=0; p < columnIndices.length;) {
key = result.getObject(columnIndices[p]);
if (keys != null) Array.set(keys, p, key);
dependency.statement.setObject(++p, key);
}
if (keys != null) key = keys;
value = dependency.fetchReferenced(key, null);
} else {
/*
* Relation.Direction.EXPORT: another table references this table.
*
* 'key' must stay null because we do not cache those dependencies.
* The reason is that this direction can return a lot of instances,
* contrarily to Direction.IMPORT which return only one instance.
* Furthermore instances fetched from Direction.EXPORT can not be
* shared by feature instances, so caching would be useless here.
*/
for (int p=0; p < columnIndices.length;) {
final Object k = result.getObject(columnIndices[p]);
dependency.statement.setObject(++p, k);
}
value = dependency.fetchReferenced(null, feature);
}
feature.setPropertyValue(associationNames[i], value);
}
action.accept(feature);
if (!all) return true;
}
return false;
}
/**
* Executes the current {@link #statement} and stores all features in a list.
* Returns {@code null} if there is no feature, or returns the feature instance
* if there is only one such instance, or returns a list of features otherwise.
*
* @param key the key to use for referencing the feature in the cache, or {@code null} for no caching.
* @param owner if the features to fetch are components of another feature, that container feature instance.
* @return the feature as a singleton {@code Feature} or as a {@code Collection<Feature>}.
*/
private Object fetchReferenced(final Object key, final Feature owner) throws SQLException {
if (key != null) {
Object existing = instances.get(key);
if (existing != null) {
return existing;
}
}
final List<Feature> features = new ArrayList<>();
try (ResultSet r = statement.executeQuery()) {
result = r;
fetch(features::add, true);
} finally {
result = null;
}
if (owner != null && deferredAssociation != null) {
for (final Feature feature : features) {
feature.setPropertyValue(deferredAssociation, owner);
}
}
Object feature;
switch (features.size()) {
case 0: feature = null; break;
case 1: feature = features.get(0); break;
default: feature = features; break;
}
if (key != null) {
@SuppressWarnings("unchecked") // Check is performed by putIfAbsent(…).
final Object previous = ((WeakValueHashMap) instances).putIfAbsent(key, feature);
if (previous != null) {
feature = previous;
}
}
return feature;
}
/**
* Closes the (pooled) connection, including the statements of all dependencies.
*/
private void close() throws SQLException {
/*
* Only one of 'statement' and 'result' should be non-null. The connection should be closed
* by the 'Features' instance having a non-null 'result' because it is the main one created
* by 'Table.features(boolean)' method. The other 'Features' instances are dependencies.
*/
if (statement != null) {
statement.close();
}
final ResultSet r = result;
if (r != null) {
result = null;
final Statement s = r.getStatement();
try (Connection c = s.getConnection()) {
r.close(); // Implied by s.close() according JDBC javadoc, but we are paranoiac.
s.close();
for (final Features dependency : dependencies) {
dependency.close();
}
}
}
}
/**
* Closes the (pooled) connection, including the statements of all dependencies.
* This is a handler to be invoked by {@link java.util.stream.Stream#close()}.
*/
@Override
public void run() {
try {
close();
} catch (SQLException e) {
throw new BackingStoreException(e);
}
}
}