| /* |
| * 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)); |
| } |
| } |
| } |