blob: c566b572bb4ba73b97fc148f189bb77ef15f3881 [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.gui.dataset;
import java.util.Spliterator;
import java.util.stream.Stream;
import java.util.function.Consumer;
import java.util.concurrent.CancellationException;
import java.util.concurrent.ExecutionException;
import javafx.application.Platform;
import javafx.concurrent.Task;
import org.apache.sis.storage.FeatureSet;
import org.apache.sis.storage.DataStoreException;
import org.apache.sis.util.collection.BackingStoreException;
import org.apache.sis.gui.internal.Resources;
import org.apache.sis.system.Configuration;
// Specific to the main branch:
import org.apache.sis.feature.AbstractFeature;
import org.apache.sis.feature.DefaultFeatureType;
/**
* A task to execute in background thread for fetching feature instances.
* This task does not load all features; only {@value #PAGE_SIZE} of them are loaded.
* The boolean value returned by this task tells whether there is more features to load.
*
* @author Martin Desruisseaux (Geomatys)
*/
final class FeatureLoader extends Task<Boolean> implements Consumer<AbstractFeature> {
/**
* Maximum number of features to load in a background task.
* If there is more features to load, we will use many tasks.
*
* @see FeatureList#nextPageLoader
*/
@Configuration
private static final int PAGE_SIZE = 100;
/**
* The table where to add the features loaded by this task.
* All methods on this object shall be invoked from JavaFX thread.
*/
private final FeatureTable table;
/**
* The feature set from which to get the initial configuration.
* This is non-null only for the task loading the first {@value #PAGE_SIZE} instances,
* then become null for all subsequent tasks.
*/
private final FeatureSet initializer;
/**
* The stream to close after we finished to iterate over features.
* This stream should not be used for any other purpose.
*/
private Stream<AbstractFeature> toClose;
/**
* If the reading process is not finished, the iterator for reading more feature instances.
*/
private Spliterator<AbstractFeature> iterator;
/**
* The features loaded by this task. This array is created in a background thread,
* then added to {@link #table} in the JavaFX thread.
*/
private AbstractFeature[] loaded;
/**
* Number of features loaded by this task.
* This is the number of valid elements in the {@link #loaded} array.
*/
private int count;
/**
* Creates a new loader for the given set of features.
*/
FeatureLoader(final FeatureTable table, final FeatureSet features) {
this.table = table;
initializer = features;
}
/**
* Creates a new task for continuing the work of a previous task.
* The new task will load the next {@value #PAGE_SIZE} features.
*/
private FeatureLoader(final FeatureLoader previous) {
table = previous.table;
toClose = previous.toClose;
iterator = previous.iterator;
initializer = null;
}
/**
* Callback method for {@link Spliterator#tryAdvance(Consumer)},
* defined for {@link #call()} internal purpose only.
*/
@Override
public void accept(final AbstractFeature feature) {
loaded[count++] = feature;
}
/**
* Invoked in a background thread for loading up to {@value #PAGE_SIZE} features.
* If this method completed successfully but there is still more feature to read,
* then {@link #iterator} will keep a non-null value and a new {@link FeatureLoader}
* well be prepared by {@link #succeeded()} for reading of another page of features.
* In other cases, {@link #iterator} is null and the stream has been closed.
*
* @return whether there is more features to load.
*/
@Override
protected Boolean call() throws DataStoreException {
final boolean isTypeKnown;
if (initializer != null) {
isTypeKnown = setType(initializer.getType());
toClose = initializer.features(false);
iterator = toClose.spliterator();
} else {
isTypeKnown = true;
}
/*
* iterator.estimateSize() is a count or remaining elements (not the total number).
* If the number of remaining elements is equal to smaller than the page size, try
* to read one more element in order to check if we really reached the stream end.
* We do that because the estimated count is only approximate.
*/
final long remaining = iterator.estimateSize();
final int stopAt = (remaining > PAGE_SIZE) ? PAGE_SIZE : 1 + (int) remaining;
loaded = new AbstractFeature[stopAt];
try {
while (iterator.tryAdvance(this)) {
if (count >= stopAt) {
setMissingType(isTypeKnown);
return Boolean.TRUE; // Intentionally skip the call to close().
}
if (isCancelled()) {
close();
return Boolean.FALSE;
}
}
} catch (BackingStoreException e) {
try {
close();
} catch (DataStoreException s) {
e.addSuppressed(s);
}
throw e.unwrapOrRethrow(DataStoreException.class);
}
close(); // Loading completed successfully.
setMissingType(isTypeKnown);
return Boolean.FALSE;
}
/**
* Closes the feature stream. This method can be invoked in worker thread or in JavaFX thread,
* but only when {@link #call()} finished its work (if unsure, see {@link #waitAndClose()}).
* It is safe to invoke this method again even if this loader has already been closed.
*/
private void close() throws DataStoreException {
iterator = null;
final Stream<AbstractFeature> c = toClose;
if (c != null) try {
toClose = null; // Clear now in case an exception happens below.
c.close();
} catch (BackingStoreException e) {
throw e.unwrapOrRethrow(DataStoreException.class);
}
}
/**
* Waits for {@link #call()} to finish its work either successfully or as a result of cancellation,
* then closes the stream. This method should be invoked in a background thread when we don't know
* if the task is still running or not.
*
* @see FeatureTable#interrupt()
*/
final void waitAndClose() {
Throwable error = null;
try {
get(); // Wait for the task to stop before to close the stream.
} catch (InterruptedException | CancellationException e) {
/*
* Someone does not want to let us wait before closing. Log the exception so that
* if a ClosedChannelException happens in another thread, we can understand why.
*/
FeatureTable.recoverableException("interrupt", e);
} catch (ExecutionException e) {
error = e.getCause();
}
try {
close();
} catch (DataStoreException e) {
if (error != null) {
error.addSuppressed(e);
} else {
error = e;
}
}
if (error != null) {
// FeatureTable.interrupt() is the public API calling this method.
FeatureTable.unexpectedException("interrupt", error);
}
}
/**
* Invoked in JavaFX thread after new feature instances are ready.
* This method adds the new rows in the table and prepares another
* task for loading the next batch of features when needed.
*/
@Override
protected void succeeded() {
final FeatureList addTo = table.getFeatureList();
if (addTo.isCurrentLoader(this)) {
final boolean hasMore = getValue();
if (initializer != null) {
final long remainingCount;
final int characteristics;
if (hasMore) {
remainingCount = iterator.estimateSize();
characteristics = iterator.characteristics();
} else {
remainingCount = 0;
characteristics = Spliterator.SIZED;
}
addTo.setFeatures(remainingCount, characteristics, loaded, count, hasMore);
} else {
addTo.addFeatures(loaded, count, hasMore);
}
addTo.setNextPage(hasMore ? new FeatureLoader(this) : null);
} else try {
close();
} catch (DataStoreException e) {
FeatureTable.unexpectedException("setFeatures", e);
}
}
/**
* Invoked in JavaFX thread when a loading process has been cancelled or failed.
*
* @see FeatureTable#interrupt()
*/
@Override
protected void cancelled() {
stop("cancelled");
}
/**
* Invoked in JavaFX thread when a loading process failed.
*/
@Override
protected void failed() {
stop("failed");
}
/**
* Closes the {@link FeatureLoader} if it did not closed itself,
* then eventually shows the error in the table area.
*/
private void stop(final String caller) {
final FeatureList addTo = table.getFeatureList();
final boolean isCurrentLoader = addTo.isCurrentLoader(this);
if (isCurrentLoader) {
addTo.setNextPage(null);
}
/*
* Loader should be already closed if error or cancellation happened during the reading process.
* But it may not be closed if the task was cancelled before it started, or maybe because of some
* other holes we missed. So close again as a double-check.
*/
Throwable exception = getException();
try {
close();
} catch (DataStoreException e) {
if (exception == null) {
exception = e;
} else {
exception.addSuppressed(e);
}
}
if (exception != null) {
if (isCurrentLoader) {
table.setException(exception);
} else {
// Since we moved to other data, not appropriate anymore for current widget.
FeatureTable.unexpectedException(caller, exception);
}
}
}
/**
* Invoked when the feature type may have been found. If the given type is non-null,
* then this method delegates to {@link FeatureTable#setFeatureType(DefaultFeatureType)} in
* the JavaFX thread. This will erase the previous content and prepare new columns.
*
* <p>This method is invoked, directly or indirectly, only from the {@link #call()}
* method with non-null {@link #initializer}. Consequently, the new rows have not yet
* been added at this time.</p>
*
* @param type the feature type, or {@code null}.
* @return whether the given type was non-null.
*/
private boolean setType(final DefaultFeatureType type) {
if (type != null) {
Platform.runLater(() -> table.setFeatureType(type));
return true;
} else {
return false;
}
}
/**
* Safety for data stores that do not implement the {@link FeatureSet#getType()} method.
* That method is mandatory and implementations should not be allowed to return null, but
* incomplete implementations exist so we are better to be safe. If we cannot get the type
* from the first feature instances, we will give up.
*/
private void setMissingType(final boolean isTypeKnown) throws DataStoreException {
if (!isTypeKnown) {
for (int i=0; i<count; i++) {
final AbstractFeature f = loaded[i];
if (f != null && setType(f.getType())) {
return;
}
}
throw new DataStoreException(Resources.forLocale(table.textLocale).getString(Resources.Keys.NoFeatureTypeInfo));
}
}
}