You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@sis.apache.org by de...@apache.org on 2019/11/06 00:05:44 UTC

[sis] branch geoapi-4.0 updated (20e2995 -> cb86b7f)

This is an automated email from the ASF dual-hosted git repository.

desruisseaux pushed a change to branch geoapi-4.0
in repository https://gitbox.apache.org/repos/asf/sis.git.


    from 20e2995  Filter : add spatial filter classes, not implemented yet, this allows more CQL tests to be activated
     new 41e610d  First revision of FeatureTable. Data are loaded in a background thread by pages of 100 features. The work is not complete: the loading of next page is not yet delayed until first needed.
     new cb86b7f  Avoid a broken link for now (for avoiding a compilation error).

The 2 revisions listed above as "new" are entirely new to this
repository and will be described in separate emails.  The revisions
listed as "add" were already present in the repository and have only
been added to this reference.


Summary of changes:
 .../org/apache/sis/gui/dataset/FeatureTable.java   | 557 +++++++++++++++++----
 .../apache/sis/gui/dataset/ResourceExplorer.java   |  29 +-
 .../apache/sis/gui/metadata/MetadataSummary.java   |  16 +-
 .../org/apache/sis/gui/metadata/MetadataTree.java  |  24 +-
 .../java/org/apache/sis/gui/metadata/Section.java  |   4 +-
 .../org/apache/sis/internal/gui/Resources.java     |  10 +
 .../apache/sis/internal/gui/Resources.properties   |   2 +
 .../sis/internal/gui/Resources_fr.properties       |   2 +
 .../org/apache/sis/filter/LikeFunctionTest.java    |   2 +-
 .../sis/util/collection/BackingStoreException.java |  22 +-
 10 files changed, 536 insertions(+), 132 deletions(-)


[sis] 01/02: First revision of FeatureTable. Data are loaded in a background thread by pages of 100 features. The work is not complete: the loading of next page is not yet delayed until first needed.

Posted by de...@apache.org.
This is an automated email from the ASF dual-hosted git repository.

desruisseaux pushed a commit to branch geoapi-4.0
in repository https://gitbox.apache.org/repos/asf/sis.git

commit 41e610d77731dbc7161af4cf3ddd72bcf18bd14e
Author: Martin Desruisseaux <ma...@geomatys.com>
AuthorDate: Wed Nov 6 00:57:26 2019 +0100

    First revision of FeatureTable. Data are loaded in a background thread by pages of 100 features.
    The work is not complete: the loading of next page is not yet delayed until first needed.
---
 .../org/apache/sis/gui/dataset/FeatureTable.java   | 557 +++++++++++++++++----
 .../apache/sis/gui/dataset/ResourceExplorer.java   |  29 +-
 .../apache/sis/gui/metadata/MetadataSummary.java   |  16 +-
 .../org/apache/sis/gui/metadata/MetadataTree.java  |  24 +-
 .../java/org/apache/sis/gui/metadata/Section.java  |   4 +-
 .../org/apache/sis/internal/gui/Resources.java     |  10 +
 .../apache/sis/internal/gui/Resources.properties   |   2 +
 .../sis/internal/gui/Resources_fr.properties       |   2 +
 .../sis/util/collection/BackingStoreException.java |  22 +-
 9 files changed, 535 insertions(+), 131 deletions(-)

diff --git a/application/sis-javafx/src/main/java/org/apache/sis/gui/dataset/FeatureTable.java b/application/sis-javafx/src/main/java/org/apache/sis/gui/dataset/FeatureTable.java
index a151e25..10a9da3 100644
--- a/application/sis-javafx/src/main/java/org/apache/sis/gui/dataset/FeatureTable.java
+++ b/application/sis-javafx/src/main/java/org/apache/sis/gui/dataset/FeatureTable.java
@@ -16,154 +16,495 @@
  */
 package org.apache.sis.gui.dataset;
 
-import java.util.Map;
-import java.util.HashMap;
+import java.util.Locale;
 import java.util.List;
-import java.util.Iterator;
-import java.util.ResourceBundle;
-import java.util.MissingResourceException;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Spliterator;
 import java.util.stream.Stream;
-import java.util.stream.Collectors;
-import javafx.collections.FXCollections;
-import javafx.scene.control.Label;
-import javafx.scene.control.ScrollPane;
+import java.util.concurrent.ExecutionException;
+import java.util.concurrent.CancellationException;
+import javafx.application.Platform;
 import javafx.scene.control.TableColumn;
 import javafx.scene.control.TableView;
-import javafx.scene.control.TreeItem;
-import javafx.scene.control.TreeView;
-import javafx.scene.layout.BorderPane;
-import javafx.beans.property.SimpleObjectProperty;
+import javafx.beans.property.ReadOnlyObjectWrapper;
+import javafx.beans.value.ObservableValue;
+import javafx.concurrent.WorkerStateEvent;
+import javafx.concurrent.Task;
+import javafx.util.Callback;
 import org.opengis.feature.Feature;
+import org.opengis.feature.FeatureType;
 import org.opengis.feature.PropertyType;
-import org.opengis.geometry.Geometry;
-import org.apache.sis.internal.util.CheckedArrayList;
-import org.apache.sis.storage.DataStoreException;
+import org.opengis.util.InternationalString;
+import org.apache.sis.internal.util.Strings;
 import org.apache.sis.storage.FeatureSet;
-import org.apache.sis.storage.Resource;
+import org.apache.sis.internal.gui.Resources;
+import org.apache.sis.internal.gui.BackgroundThreads;
+import org.apache.sis.internal.system.Modules;
+import org.apache.sis.util.logging.Logging;
+import org.apache.sis.util.collection.BackingStoreException;
+import org.apache.sis.storage.DataStoreException;
 
 
 /**
+ * A view of {@link FeatureSet} data organized as a table. The features are specified by a call
+ * to {@link #setFeatures(FeatureSet)}, which will load the features in a background thread.
+ * At first only {@value #PAGE_SIZE} features are loaded.
+ * More features will be loaded only when the user scroll down.
+ *
+ * <p>If this view is removed from scene graph, then {@link #interrupt()} should be called
+ * for stopping any loading process that may be under progress.</p>
  *
  * @author  Johann Sorel (Geomatys)
  * @author  Smaniotto Enzo (GSoC)
- * @version 1.0
- * @since   1.0
+ * @author  Martin Desruisseaux (Geomatys)
+ * @version 1.1
+ * @since   1.1
  * @module
  */
-public class FeatureTable extends BorderPane {
+public class FeatureTable extends TableView<Feature> {
+    /**
+     * Maximum number of features to load in a background task.
+     * If there is more features to load, we will use many tasks.
+     *
+     * @see #nextPageLoader
+     */
+    private static final int PAGE_SIZE = 100;
+
+    /**
+     * The locale to use for texts.
+     */
+    private final Locale textLocale;
+
+    /**
+     * The locale to use for dates/numbers.
+     * This is often the same than {@link #textLocale}.
+     */
+    private final Locale dataLocale;
+
+    /**
+     * The type of features, or {@code null} if not yet determined.
+     * This type determines the columns that will be shown.
+     *
+     * @see #setFeatureType(FeatureType)
+     */
+    private FeatureType featureType;
+
+    /**
+     * If not all features have been read, the task for loading the next batch of {@value #PAGE_SIZE} features.
+     * This task will be executed only if there is a need to see new features.
+     *
+     * <p>If a loading is in progress, then this field is the loader doing the work.
+     * But this field will be updated with next loader as soon as the loading is completed.</p>
+     */
+    private Loader nextPageLoader;
+
+    /**
+     * Creates an initially empty table.
+     */
+    public FeatureTable() {
+        textLocale = Locale.getDefault(Locale.Category.DISPLAY);
+        dataLocale = Locale.getDefault(Locale.Category.FORMAT);
+        setColumnResizePolicy(TableView.UNCONSTRAINED_RESIZE_POLICY);
+        setTableMenuButtonVisible(true);
+    }
+
+    /**
+     * Sets the features to show in this table. This method loads an arbitrary amount of
+     * features in a background thread. It does not load all features if the feature set
+     * is large, unless the user scroll down.
+     *
+     * <p>If the loading of another {@code FeatureSet} was in progress at the time this method is invoked,
+     * then that previous loading process is cancelled.</p>
+     *
+     * <p><b>Note:</b> the table content may appear unmodified after this method returns.
+     * The modifications will appear at an undetermined amount of time later.</p>
+     *
+     * @param  features  the features, or {@code null} if none.
+     */
+    public void setFeatures(final FeatureSet features) {
+        assert Platform.isFxApplicationThread();
+        final Loader previous = nextPageLoader;
+        if (previous != null) {
+            nextPageLoader = null;
+            previous.cancel();
+        }
+        if (features != null) {
+            setLoader(new InitialLoader(features));
+            BackgroundThreads.execute(nextPageLoader);
+        } else {
+            featureType = null;
+            getItems().clear();
+            getColumns().clear();
+        }
+    }
+
+    /**
+     * Sets {@link #nextPageLoader} to the given values and sets the listeners, but without starting the task yet.
+     *
+     * @param  loader  the loader for next {@value #PAGE_SIZE} features,
+     *                 or {@code null} if there is no more features to load.
+     */
+    private void setLoader(final Loader loader) {
+        if (loader != null) {
+            loader.setOnSucceeded(this::addFeatures);
+            loader.setOnCancelled(this::cancelled);
+            loader.setOnFailed(this::cancelled);
+        }
+        nextPageLoader = loader;
+    }
+
     /**
-     * Contains ResourceBundles indexed by table names.
+     * Invoked in JavaFX thread after new feature instances are ready.
      */
-    private final Map<String, ResourceBundle> bundles = new HashMap<>();
+    private void addFeatures(final WorkerStateEvent event) {
+        assert Platform.isFxApplicationThread();
+        final Loader loader = (Loader) event.getSource();
+        if (loader == nextPageLoader) {
+            getItems().addAll((List<Feature>) event.getSource().getValue());
+            setLoader(nextPageLoader.next());
 
-    private String bundlePrefix;
+            // TODO: temporary hack: we should not start the job now, but wait until we need it.
+            if (nextPageLoader != null) {
+                BackgroundThreads.execute(nextPageLoader);
+            }
+        } else try {
+            loader.close();
+        } catch (DataStoreException e) {
+            unexpectedException("addFeatures", e);
+        }
+    }
 
-    private String generateFinalColumnName(final PropertyType prop) {
-        Map<String, Map.Entry<String, String>> labelInfo = (Map) prop.getDesignation();
-        final String labelName = prop.getName().toString();
-        String columnName = labelName;
-        String tableName = null;
+    /**
+     * Invoked in JavaFX thread when a loading process has been cancelled or failed.
+     *
+     * @see #interrupt()
+     */
+    private void cancelled(final WorkerStateEvent event) {
+        assert Platform.isFxApplicationThread();
+        final Loader loader = (Loader) event.getSource();
+        final boolean isCurrentLoader = (loader == nextPageLoader);
+        if (isCurrentLoader) {
+            nextPageLoader = null;
+        }
         /*
-         * If exists, explore labelInfo to retrive table and column respect to this label.
+         * 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.
          */
-        if (labelInfo != null) {
-            final Map.Entry<String, String> entry = labelInfo.get(labelName);
-            if (entry != null) {
-                if (entry.getKey() != null) {
-                    tableName = entry.getKey();
-                } else {
-                    tableName = null;
+        Throwable exception = loader.getException();
+        try {
+            loader.close();
+        } catch (DataStoreException e) {
+            if (exception == null) {
+                exception = e;
+            } else {
+                exception.addSuppressed(e);
+            }
+        }
+        if (exception != null) {
+            if (isCurrentLoader) {
+                exception.printStackTrace();        // TODO: write somewhere in the widget.
+            } else {
+                // Since we moved to other data, not appropriate anymore for current widget.
+                unexpectedException("cancelled", exception);
+            }
+        }
+    }
+
+    /**
+     * 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.
+     *
+     * <p>Loading processes are started by {@link InitialLoader}.
+     * Only additional pages are loaded by ordinary {@code Loader}.</p>
+     */
+    private static class Loader extends Task<List<Feature>> {
+        /**
+         * The stream to close after we finished to iterate over features.
+         * This stream should not be used for any other purpose.
+         */
+        private Stream<Feature> toClose;
+
+        /**
+         * If the reading process is not finished, the iterator for reading more feature instances.
+         */
+        private Spliterator<Feature> iterator;
+
+        /**
+         * An estimation of the number of features, or {@link Long#MAX_VALUE} if unknown.
+         */
+        private long estimatedCount;
+
+        /**
+         * Creates a new loader. This constructor is for {@link InitialLoader} usage only.
+         */
+        Loader() {
+            estimatedCount = Long.MAX_VALUE;
+        }
+
+        /**
+         * Creates a new task for continuing the work of a previous task.
+         * The new task will load the next {@value #PAGE_SIZE} features.
+         */
+        private Loader(final Loader previous) {
+            toClose        = previous.toClose;
+            iterator       = previous.iterator;
+            estimatedCount = previous.estimatedCount;
+        }
+
+        /**
+         * Initializes this task for reading features from the specified set.
+         * This method shall be invoked by {@link InitialLoader} only.
+         */
+        final void initialize(final FeatureSet features) throws DataStoreException {
+            toClose        = features.features(false);
+            iterator       = toClose .spliterator();
+            estimatedCount = iterator.estimateSize();
+        }
+
+        /**
+         * If there is more features to load, returns a new task for loading the next
+         * {@value #PAGE_SIZE} features. Otherwise returns {@code null}.
+         */
+        final Loader next() {
+            return (iterator != null) ? new Loader(this) : null;
+        }
+
+        /**
+         * Loads up to {@value #PAGE_SIZE} features.
+         */
+        @Override
+        protected List<Feature> call() throws DataStoreException {
+            final Spliterator<Feature> it = iterator;
+            iterator = null;                                // Clear now in case an exception happens below.
+            final List<Feature> instances = new ArrayList<>((int) Math.min(estimatedCount, PAGE_SIZE));
+            if (it != null) try {
+                while (it.tryAdvance(instances::add)) {
+                    if (instances.size() >= PAGE_SIZE) {
+                        iterator = it;                      // Remember that there is more instances to read.
+                        return instances;                   // Intentionally skip the call to close().
+                    }
+                    if (isCancelled()) {
+                        break;
+                    }
+                }
+            } catch (BackingStoreException e) {
+                try {
+                    close();
+                } catch (DataStoreException s) {
+                    e.addSuppressed(s);
                 }
-                if (entry.getValue() != null) {
-                    columnName = entry.getValue();
+                throw e.unwrapOrRethrow(DataStoreException.class);
+            }
+            close();                                        // Loading has been cancelled.
+            return instances;
+        }
+
+        /**
+         * Closes the feature stream. This method can be invoked only when {@link #call()} finished its work.
+         * It is safe to invoke this method again even if this loader has already been closed.
+         */
+        final void close() throws DataStoreException {
+            iterator = null;
+            final Stream<Feature> 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);
+            }
+        }
+
+        /**
+         * Wait for {@link #call()} to finish its work either successfully or as a result of cancellation,
+         * then close 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) {
+                // Ignore, we will try to close the stream right now.
+                recoverableException("interrupt", e);
+            } catch (ExecutionException e) {
+                error = e.getCause();
+            }
+            try {
+                close();
+            } catch (DataStoreException e) {
+                if (error != null) {
+                    error.addSuppressed(e);
                 } else {
-                    columnName = labelName;
+                    error = e;
                 }
             }
+            if (error != null) {
+                // FeatureTable.interrupt is the public API calling this method.
+                unexpectedException("interrupt", error);
+            }
         }
-        /*
-         * If table name is not null, try to found resourcebundle for this table.
+    }
+
+    /**
+     * The task to execute in background thread for initiating the loading process.
+     * This tasks is created only for the first {@value #PAGE_SIZE} features.
+     * For all additional features, an ordinary {@link Loader} will be used.
+     */
+    private final class InitialLoader extends Loader {
+        /**
+         * The set of features to read.
+         */
+        private final FeatureSet features;
+
+        /**
+         * Initializes a new task for loading features from the given set.
+         */
+        InitialLoader(final FeatureSet features) {
+            this.features = features;
+        }
+
+        /**
+         * Gets the feature type, initialize the iterator and gets the first {@value #PAGE_SIZE} features.
+         * The {@link FeatureType} should be given by {@link FeatureSet#getType()}, but this method is
+         * robust to incomplete implementations where {@code getType()} returns {@code null}.
          */
-        if (tableName != null) {
+        @Override
+        protected List<Feature> call() throws DataStoreException {
+            final boolean isTypeKnown = setType(features.getType());
+            initialize(features);
+            final List<Feature> instances = super.call();
+            if (isTypeKnown) {
+                return instances;
+            }
             /*
-             * If there isn't resource bundles (or not for the curruen table), try to generate.
+             * Following code is a safety for FeatureSet that do not implement the `getType()` method.
+             * This method is mandatory and implementation should not be allowed to return null, but
+             * incomplete implementations exist so we are better to be safe. If we can not get the type
+             * from the first feature instances, we will give up.
              */
-            if (bundles.get(tableName) == null) {
-                if (bundlePrefix != null) {
-                    bundles.put(tableName, ResourceBundle.getBundle(bundlePrefix + tableName));
+            for (final Feature f : instances) {
+                if (f != null && setType(f.getType())) {
+                    return instances;
                 }
             }
+            throw new DataStoreException(Resources.forLocale(textLocale).getString(Resources.Keys.NoFeatureTypeInfo));
         }
-        final ResourceBundle bundle = bundles.get(tableName);
-        String finalColumnName;
-        if (labelName == null) {
-            finalColumnName = "";
-        } else if (bundle == null) {
-            if (!labelName.equals(columnName)) {
-                finalColumnName = columnName + " as " + labelName;
+
+        /**
+         * Invoked when the feature type may have been found. If the given type is non-null,
+         * then this method delegates to {@link FeatureTable#setFeatureType(FeatureType)} in
+         * the JavaFX thread. This will erase the previous content and prepare new columns.
+         *
+         * @param  type  the feature type, or {@code null}.
+         * @return whether the given type was non-null.
+         */
+        private boolean setType(final FeatureType type) {
+            if (type != null) {
+                Platform.runLater(() -> setFeatureType(type));
+                return true;
             } else {
-                finalColumnName = columnName;
+                return false;
             }
-        } else {
-            try {
-                if (!labelName.equals(columnName)) {
-                    finalColumnName = bundle.getString(columnName) + " as " + labelName;
-                } else {
-                    finalColumnName = bundle.getString(columnName);
-                }
-            } catch (MissingResourceException ex) {
-                if (!labelName.equals(columnName)) {
-                    finalColumnName = columnName + " as " + labelName;
-                } else {
-                    finalColumnName = columnName;
+        }
+    }
+
+    /**
+     * Invoked in JavaFX thread after the feature type has been determined.
+     * This method clears all rows and replaces all columns by new columns
+     * determined from the given type.
+     */
+    private void setFeatureType(final FeatureType type) {
+        assert Platform.isFxApplicationThread();
+        getItems().clear();
+        if (type != null && !type.equals(featureType)) {
+            final Collection<? extends PropertyType> properties = type.getProperties(true);
+            final List<TableColumn<Feature,?>> columns = new ArrayList<>(properties.size());
+            for (final PropertyType pt : properties) {
+                final String name = pt.getName().toString();
+                String title = string(pt.getDesignation());
+                if (title == null) {
+                    title = string(pt.getName().toInternationalString());
+                    if (title == null) title = name;
                 }
+                final TableColumn<Feature, Object> column = new TableColumn<>(title);
+                column.setCellValueFactory(new ValueGetter(name));
+                columns.add(column);
             }
+            getColumns().setAll(columns);       // Change columns in an all or nothing operation.
         }
-        return finalColumnName;
+        featureType = type;
     }
 
-    public FeatureTable(Resource res, int i) throws DataStoreException {
-        TableView<Feature> ttv = new TableView<>();
-        final ScrollPane scroll = new ScrollPane(ttv);
-        scroll.setFitToHeight(true);
-        scroll.setFitToWidth(true);
-        ttv.setColumnResizePolicy(TableView.UNCONSTRAINED_RESIZE_POLICY);
-        ttv.setTableMenuButtonVisible(true);
-        ttv.setFixedCellSize(100);
-        scroll.setPrefSize(600, 400);
-        scroll.setMaxSize(Double.MAX_VALUE, Double.MAX_VALUE);
-        setCenter(scroll);
-        final List<Feature> list;
-        if (res instanceof FeatureSet) {
-            try (Stream<Feature> stream = ((FeatureSet) res).features(false)) {
-                list = stream.collect(Collectors.toList());
-                ttv.setItems(FXCollections.observableArrayList(list));
-                for (PropertyType pt : list.get(0).getType().getProperties(false)) {
-                    final TableColumn<Feature, BorderPane> column = new TableColumn<>(generateFinalColumnName(pt));
-                    column.setCellValueFactory((TableColumn.CellDataFeatures<Feature, BorderPane> param) -> {
-                        final Object val = param.getValue().getPropertyValue(pt.getName().toString());
-                        if (val instanceof Geometry) {
-                            return new SimpleObjectProperty<>(new BorderPane(new Label("{geometry}")));
-                        } else {
-                            SimpleObjectProperty<BorderPane> sop = new SimpleObjectProperty<>();
-                            if (val instanceof CheckedArrayList<?>) {
-                                Iterator<String> it = ((CheckedArrayList<String>) val).iterator();
-                                TreeItem<String> ti = new TreeItem<>(it.next());
-                                while (it.hasNext()) {
-                                    ti.getChildren().add(new TreeItem<>(it.next()));
-                                }
-                                BorderPane bp = new BorderPane(new TreeView<>(ti));
-                                sop.setValue(bp);
-                                return sop;
-                            } else {
-                                sop.setValue(new BorderPane(new Label(String.valueOf(val))));
-                                return sop;
-                            }
-                        }
-                    });
-                    ttv.getColumns().add(column);
-                }
+    /**
+     * Fetch values to show in the table cells.
+     */
+    private static final class ValueGetter implements Callback<TableColumn.CellDataFeatures<Feature,Object>, ObservableValue<Object>> {
+        /**
+         * The name of the feature property for which to fetch values.
+         */
+        final String name;
+
+        /**
+         * Creates a new getter of property values.
+         *
+         * @param  name  name of the feature property for which to fetch values.
+         */
+        ValueGetter(final String name) {
+            this.name = name;
+        }
+
+        /**
+         * Returns the value of the feature property wrapped by the given argument.
+         * This method is invoked by JavaFX when a new cell needs to be rendered.
+         */
+        @Override
+        public ObservableValue<Object> call(final TableColumn.CellDataFeatures<Feature, Object> cell) {
+            Object value = cell.getValue().getPropertyValue(name);
+            if (value instanceof Collection<?>) {
+                value = "collection";               // TODO
             }
+            return new ReadOnlyObjectWrapper<>(value);
         }
     }
+
+    /**
+     * Returns the given international string as a non-empty localized string, or {@code null} if none.
+     */
+    private String string(final InternationalString i18n) {
+        return (i18n != null) ? Strings.trimOrNull(i18n.toString(textLocale)) : null;
+    }
+
+    /**
+     * If a loading process was under way, interrupts it and close the feature stream.
+     * This method returns immediately; the release of resources happens in a background thread.
+     */
+    public void interrupt() {
+        assert Platform.isFxApplicationThread();
+        final Loader loader = nextPageLoader;
+        nextPageLoader = null;
+        if (loader != null) {
+            loader.cancel();
+            BackgroundThreads.execute(loader::waitAndClose);
+        }
+    }
+
+    /**
+     * Reports an exception that we can not display in this widget, for example because it applies
+     * to different data than the one currently viewed. The {@code method} argument should be the
+     * public API (if possible) invoking the method where the exception is caught.
+     */
+    private static void unexpectedException(final String method, final Throwable exception) {
+        Logging.unexpectedException(Logging.getLogger(Modules.APPLICATION), FeatureTable.class, method, exception);
+    }
+
+    /**
+     * Reports an exception that we choose to ignore.
+     */
+    private static void recoverableException(final String method, final Exception exception) {
+        Logging.recoverableException(Logging.getLogger(Modules.APPLICATION), FeatureTable.class, method, exception);
+    }
 }
diff --git a/application/sis-javafx/src/main/java/org/apache/sis/gui/dataset/ResourceExplorer.java b/application/sis-javafx/src/main/java/org/apache/sis/gui/dataset/ResourceExplorer.java
index 22a6b63..253ac94 100644
--- a/application/sis-javafx/src/main/java/org/apache/sis/gui/dataset/ResourceExplorer.java
+++ b/application/sis-javafx/src/main/java/org/apache/sis/gui/dataset/ResourceExplorer.java
@@ -27,6 +27,7 @@ import org.apache.sis.storage.Resource;
 import org.apache.sis.gui.metadata.MetadataSummary;
 import org.apache.sis.gui.metadata.MetadataTree;
 import org.apache.sis.internal.gui.Resources;
+import org.apache.sis.storage.FeatureSet;
 
 
 /**
@@ -45,6 +46,11 @@ public class ResourceExplorer {
     private final ResourceTree resources;
 
     /**
+     * The data as a table.
+     */
+    private final FeatureTable features;
+
+    /**
      * The widget showing metadata about a selected resource.
      */
     private final MetadataSummary metadata;
@@ -61,21 +67,21 @@ public class ResourceExplorer {
     public ResourceExplorer() {
         resources = new ResourceTree();
         metadata  = new MetadataSummary();
+        features  = new FeatureTable();
         pane      = new SplitPane();
 
-        final MetadataTree metadataTree = new MetadataTree();
-        metadata.metadataProperty.addListener((p,o,n) -> metadataTree.setContent(n));
+        final TabPane tabs = new TabPane(
+            new Tab(resources.localized.getString(Resources.Keys.Summary),  metadata.getView()),
+            new Tab(resources.localized.getString(Resources.Keys.Data),     features),
+            new Tab(resources.localized.getString(Resources.Keys.Metadata), new MetadataTree(metadata)));
 
-        final Tab summaryTab = new Tab(resources.localized.getString(Resources.Keys.Summary),  metadata.getView());
-        final Tab metadatTab = new Tab(resources.localized.getString(Resources.Keys.Metadata), metadataTree);
-        final TabPane tabs = new TabPane(summaryTab, metadatTab);
         tabs.setTabClosingPolicy(TabPane.TabClosingPolicy.UNAVAILABLE);
         tabs.setTabDragPolicy(TabPane.TabDragPolicy.REORDER);
 
         pane.getItems().setAll(resources, tabs);
         resources.getSelectionModel().getSelectedItems().addListener(this::selectResource);
         SplitPane.setResizableWithParent(resources, Boolean.FALSE);
-        SplitPane.setResizableWithParent(metadata.getView(), Boolean.TRUE);
+        SplitPane.setResizableWithParent(tabs, Boolean.TRUE);
         pane.setDividerPosition(0, 300);
     }
 
@@ -91,8 +97,9 @@ public class ResourceExplorer {
     }
 
     /**
-     * Adds all the given resources to the resource tree. The given collection typically contains
-     * files to load, but may also contain {@link Resource} instances to add directly.
+     * Loads all given sources in background threads and add them to the resource tree.
+     * The given collection typically contains files to load,
+     * but may also contain {@link Resource} instances to add directly.
      * This method forwards the files to {@link ResourceTree#loadResource(Object)},
      * which will allocate a background thread for each resource to load.
      *
@@ -108,8 +115,9 @@ public class ResourceExplorer {
     }
 
     /**
-     * Invoked when a new item is selected in the resource tree.
-     * This method takes the first non-null resource and forward to the children.
+     * Invoked in JavaFX thread when a new item is selected in the resource tree.
+     * Normally, only one resource is selected since we use a single selection model.
+     * We nevertheless loop over the items as a paranoiac check and take the first non-null resource.
      *
      * @param  change  a change event with the new resource to show.
      */
@@ -122,5 +130,6 @@ public class ResourceExplorer {
             }
         }
         metadata.setMetadata(resource);
+        features.setFeatures((resource instanceof FeatureSet) ? (FeatureSet) resource : null);
     }
 }
diff --git a/application/sis-javafx/src/main/java/org/apache/sis/gui/metadata/MetadataSummary.java b/application/sis-javafx/src/main/java/org/apache/sis/gui/metadata/MetadataSummary.java
index 1f6d473..2838eb3 100644
--- a/application/sis-javafx/src/main/java/org/apache/sis/gui/metadata/MetadataSummary.java
+++ b/application/sis-javafx/src/main/java/org/apache/sis/gui/metadata/MetadataSummary.java
@@ -79,7 +79,7 @@ public class MetadataSummary {
      * The locale to use for date/number formatters.
      * This is often the same than {@link #localized}.
      */
-    private final Locale formatLocale;
+    final Locale dataLocale;
 
     /**
      * The format to use for writing numbers, created when first needed.
@@ -140,9 +140,9 @@ public class MetadataSummary {
      * Creates an initially empty metadata overview.
      */
     public MetadataSummary() {
-        localized    = Resources.forLocale(Locale.getDefault(Locale.Category.DISPLAY));
-        formatLocale = Locale.getDefault(Locale.Category.FORMAT);
-        information  = new TitledPane[] {
+        localized   = Resources.forLocale(Locale.getDefault(Locale.Category.DISPLAY));
+        dataLocale  = Locale.getDefault(Locale.Category.FORMAT);
+        information = new TitledPane[] {
             new TitledPane(localized.getString(Resources.Keys.ResourceIdentification), new IdentificationInfo(this)),
             new TitledPane(localized.getString(Resources.Keys.SpatialRepresentation),  new RepresentationInfo(this))
         };
@@ -174,7 +174,7 @@ public class MetadataSummary {
      */
     final NumberFormat getNumberFormat() {
         if (numberFormat == null) {
-            numberFormat = NumberFormat.getInstance(formatLocale);
+            numberFormat = NumberFormat.getInstance(dataLocale);
         }
         return numberFormat;
     }
@@ -184,7 +184,7 @@ public class MetadataSummary {
      */
     final DateFormat getDateFormat() {
         if (dateFormat == null) {
-            dateFormat = DateFormat.getDateTimeInstance(DateFormat.LONG, DateFormat.MEDIUM, formatLocale);
+            dateFormat = DateFormat.getDateTimeInstance(DateFormat.LONG, DateFormat.MEDIUM, dataLocale);
         }
         return dateFormat;
     }
@@ -216,8 +216,8 @@ public class MetadataSummary {
                 /**
                  * Shows the result, unless another {@link #setMetadata(Resource)} has been invoked.
                  */
-                @Override protected void succeeded() {if (!isCancelled()) setMetadata(getValue());}
-                @Override protected void failed()    {if (!isCancelled()) setError(getException());}
+                @Override protected void succeeded() {super.succeeded(); setMetadata(getValue());}
+                @Override protected void failed()    {super.failed();    setError(getException());}
             }
             BackgroundThreads.execute(new Getter());
         }
diff --git a/application/sis-javafx/src/main/java/org/apache/sis/gui/metadata/MetadataTree.java b/application/sis-javafx/src/main/java/org/apache/sis/gui/metadata/MetadataTree.java
index 70ce16f..e925dc3 100644
--- a/application/sis-javafx/src/main/java/org/apache/sis/gui/metadata/MetadataTree.java
+++ b/application/sis-javafx/src/main/java/org/apache/sis/gui/metadata/MetadataTree.java
@@ -127,8 +127,25 @@ public class MetadataTree extends TreeTableView<TreeTable.Node> {
      * Creates a new initially empty metadata tree.
      */
     public MetadataTree() {
-        textLocale      = Locale.getDefault(Locale.Category.DISPLAY);
-        dataLocale      = Locale.getDefault(Locale.Category.FORMAT);
+        this(null);
+    }
+
+    /**
+     * Creates a new initially empty metadata tree which will be automatically updated
+     * when the given widget shows new metadata. This constructor registers a listener
+     * to {@link MetadataSummary#metadataProperty} which forwards the metadata changes
+     * to {@link #setContent(Metadata)}.
+     *
+     * @param  controller  the widget to watch, or {@code null} if none.
+     */
+    public MetadataTree(final MetadataSummary controller) {
+        if (controller != null) {
+            textLocale = controller.localized.getLocale();
+            dataLocale = controller.dataLocale;
+        } else {
+            textLocale = Locale.getDefault(Locale.Category.DISPLAY);
+            dataLocale = Locale.getDefault(Locale.Category.FORMAT);
+        }
         contentProperty = new ContentProperty(this);
         nameColumn      = new TreeTableColumn<>(TableColumn.NAME .getHeader().toString(textLocale));
         valueColumn     = new TreeTableColumn<>(TableColumn.VALUE.getHeader().toString(textLocale));
@@ -138,6 +155,9 @@ public class MetadataTree extends TreeTableView<TreeTable.Node> {
         setColumnResizePolicy(CONSTRAINED_RESIZE_POLICY);
         getColumns().setAll(nameColumn, valueColumn);
         contentProperty.addListener(MetadataTree::applyChange);
+        if (controller != null) {
+            controller.metadataProperty.addListener((p,o,n) -> setContent(n));
+        }
     }
 
     /**
diff --git a/application/sis-javafx/src/main/java/org/apache/sis/gui/metadata/Section.java b/application/sis-javafx/src/main/java/org/apache/sis/gui/metadata/Section.java
index 277b0c4..10f701b 100644
--- a/application/sis-javafx/src/main/java/org/apache/sis/gui/metadata/Section.java
+++ b/application/sis-javafx/src/main/java/org/apache/sis/gui/metadata/Section.java
@@ -185,7 +185,7 @@ abstract class Section<T> extends GridPane implements EventHandler<ActionEvent>
         /*
          * Update the pane content with the first information.
          */
-        setVisible(n != 0);
+        linesEndIndex = linesStartIndex;
         if (n != 0) {
             pageGroup.selectToggle((ToggleButton) pagination.getChildren().get(0));
             update(0);
@@ -284,6 +284,6 @@ abstract class Section<T> extends GridPane implements EventHandler<ActionEvent>
      * Returns {@code true} if this section contains no data.
      */
     boolean isEmpty() {
-        return linesStartIndex == linesEndIndex;
+        return linesStartIndex >= linesEndIndex;
     }
 }
diff --git a/application/sis-javafx/src/main/java/org/apache/sis/internal/gui/Resources.java b/application/sis-javafx/src/main/java/org/apache/sis/internal/gui/Resources.java
index 8273092..7240f4d 100644
--- a/application/sis-javafx/src/main/java/org/apache/sis/internal/gui/Resources.java
+++ b/application/sis-javafx/src/main/java/org/apache/sis/internal/gui/Resources.java
@@ -106,6 +106,11 @@ public final class Resources extends IndexedResourceBundle {
         public static final short Credit = 17;
 
         /**
+         * Data
+         */
+        public static final short Data = 32;
+
+        /**
          * Date:
          */
         public static final short Date = 18;
@@ -156,6 +161,11 @@ public final class Resources extends IndexedResourceBundle {
         public static final short Metadata = 30;
 
         /**
+         * No feature type information.
+         */
+        public static final short NoFeatureTypeInfo = 33;
+
+        /**
          * Number of dimensions:
          */
         public static final short NumberOfDimensions = 27;
diff --git a/application/sis-javafx/src/main/java/org/apache/sis/internal/gui/Resources.properties b/application/sis-javafx/src/main/java/org/apache/sis/internal/gui/Resources.properties
index 2f88906..425bf5d 100644
--- a/application/sis-javafx/src/main/java/org/apache/sis/internal/gui/Resources.properties
+++ b/application/sis-javafx/src/main/java/org/apache/sis/internal/gui/Resources.properties
@@ -30,6 +30,7 @@ Copy                   = Copy
 CreationDate           = Creation date:
 Credit                 = Credit:
 CRSs                   = Coordinate Reference Systems
+Data                   = Data
 Date                   = Date:
 Dimensions             = Dimensions:
 ErrorClosingFile       = Error closing file
@@ -40,6 +41,7 @@ File                   = File
 GeospatialFiles        = Geospatial data files
 Loading                = Loading\u2026
 Metadata               = Metadata
+NoFeatureTypeInfo      = No feature type information.
 NumberOfDimensions     = Number of dimensions:
 Open                   = Open\u2026
 OpenDataFile           = Open data file
diff --git a/application/sis-javafx/src/main/java/org/apache/sis/internal/gui/Resources_fr.properties b/application/sis-javafx/src/main/java/org/apache/sis/internal/gui/Resources_fr.properties
index 4aab7f8..7be1eb0 100644
--- a/application/sis-javafx/src/main/java/org/apache/sis/internal/gui/Resources_fr.properties
+++ b/application/sis-javafx/src/main/java/org/apache/sis/internal/gui/Resources_fr.properties
@@ -35,6 +35,7 @@ Copy                   = Copier
 CreationDate           = Date de cr\u00e9ation\u00a0:
 Credit                 = Cr\u00e9dit\u00a0:
 CRSs                   = Syst\u00e8mes de r\u00e9f\u00e9rence des coordonn\u00e9es
+Data                   = Donn\u00e9es
 Date                   = Date\u00a0:
 Dimensions             = Dimensions\u00a0:
 ErrorClosingFile       = Erreur \u00e0 la fermeture du fichier
@@ -45,6 +46,7 @@ File                   = Fichier
 GeospatialFiles        = Fichiers de donn\u00e9es g\u00e9ospatiales
 Loading                = Chargement\u2026
 Metadata               = Metadonn\u00e9es
+NoFeatureTypeInfo      = Pas d\u2019information sur le type d\u2019entit\u00e9.
 NumberOfDimensions     = Nombre de dimensions\u00a0:
 Open                   = Ouvrir\u2026
 OpenDataFile           = Ouvrir un fichier de donn\u00e9es
diff --git a/core/sis-utility/src/main/java/org/apache/sis/util/collection/BackingStoreException.java b/core/sis-utility/src/main/java/org/apache/sis/util/collection/BackingStoreException.java
index e453faf..7ecefbb 100644
--- a/core/sis-utility/src/main/java/org/apache/sis/util/collection/BackingStoreException.java
+++ b/core/sis-utility/src/main/java/org/apache/sis/util/collection/BackingStoreException.java
@@ -48,7 +48,7 @@ import java.sql.SQLException;
  * client code would be well advised to catch both kind of exceptions for robustness.
  *
  * @author  Martin Desruisseaux (IRD, Geomatys)
- * @version 0.3
+ * @version 1.1
  * @since   0.3
  * @module
  */
@@ -115,6 +115,10 @@ public class BackingStoreException extends RuntimeException {
      *     }
      * }
      *
+     * If this exception has {@linkplain #getSuppressed() suppressed exceptions} and this method decided
+     * that this exception should be discarded in favor of {@code <E>} or {@link RuntimeException} cause,
+     * then this method copies the suppressed exceptions into the cause before to throw the cause.
+     *
      * @param  <E>   the type of the exception to unwrap.
      * @param  type  the type of the exception to unwrap.
      * @return the cause as an exception of the given type (never {@code null}).
@@ -129,11 +133,27 @@ public class BackingStoreException extends RuntimeException {
     {
         final Throwable cause = getCause();
         if (type.isInstance(cause)) {
+            copySuppressed(cause);
             return (E) cause;
         } else if (cause instanceof RuntimeException) {
+            copySuppressed(cause);
             throw (RuntimeException) cause;
         } else {
             throw this;
         }
     }
+
+    /**
+     * Copies suppressed exceptions to the given target. This method is invoked before the cause is re-thrown.
+     * Current version does not verify that this copy operation does not create duplicated values.
+     * Most of the time, this exception has no suppressed exceptions and this method does nothing.
+     *
+     * <p>This copy operation is useful if a {@link BackingStoreException} was thrown inside a try-with-resource
+     * block, especially when the {@link AutoCloseable} is a {@link java.util.stream.Stream}.</p>
+     */
+    private void copySuppressed(final Throwable cause) {
+        for (final Throwable s : getSuppressed()) {
+            cause.addSuppressed(s);
+        }
+    }
 }


[sis] 02/02: Avoid a broken link for now (for avoiding a compilation error).

Posted by de...@apache.org.
This is an automated email from the ASF dual-hosted git repository.

desruisseaux pushed a commit to branch geoapi-4.0
in repository https://gitbox.apache.org/repos/asf/sis.git

commit cb86b7f37d27d9be2211bd4c691580173e3676c0
Author: Martin Desruisseaux <ma...@geomatys.com>
AuthorDate: Wed Nov 6 01:05:13 2019 +0100

    Avoid a broken link for now (for avoiding a compilation error).
---
 .../src/test/java/org/apache/sis/filter/LikeFunctionTest.java           | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/core/sis-feature/src/test/java/org/apache/sis/filter/LikeFunctionTest.java b/core/sis-feature/src/test/java/org/apache/sis/filter/LikeFunctionTest.java
index 7374ff5..00a1efe 100644
--- a/core/sis-feature/src/test/java/org/apache/sis/filter/LikeFunctionTest.java
+++ b/core/sis-feature/src/test/java/org/apache/sis/filter/LikeFunctionTest.java
@@ -23,7 +23,7 @@ import org.junit.Ignore;
 
 
 /**
- * Tests {@link ComparisonFunction.Like} implementations.
+ * Tests {@code ComparisonFunction.Like} implementations.
  *
  * @author  Johann Sorel (Geomatys)
  * @version 2.0