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 <[email protected]>
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);
+        }
+    }
 }

Reply via email to