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


The following commit(s) were added to refs/heads/geoapi-4.0 by this push:
     new f0a2456d6f Move the static nested classes of `ResourceTree` as 
package-private top-level classes. Those classes are becoming bigger as new 
actions (e.g. context menu items) are added.
f0a2456d6f is described below

commit f0a2456d6fa1807240878133fea5e77345fb13f0
Author: Martin Desruisseaux <martin.desruisse...@geomatys.com>
AuthorDate: Sat Sep 10 23:59:47 2022 +0200

    Move the static nested classes of `ResourceTree` as package-private 
top-level classes.
    Those classes are becoming bigger as new actions (e.g. context menu items) 
are added.
---
 .../org/apache/sis/gui/dataset/ResourceCell.java   | 183 ++++++++
 .../org/apache/sis/gui/dataset/ResourceItem.java   | 242 ++++++++++
 .../org/apache/sis/gui/dataset/ResourceTree.java   | 516 ++-------------------
 .../org/apache/sis/gui/dataset/RootResource.java   | 132 ++++++
 4 files changed, 585 insertions(+), 488 deletions(-)

diff --git 
a/application/sis-javafx/src/main/java/org/apache/sis/gui/dataset/ResourceCell.java
 
b/application/sis-javafx/src/main/java/org/apache/sis/gui/dataset/ResourceCell.java
new file mode 100644
index 0000000000..109bd88397
--- /dev/null
+++ 
b/application/sis-javafx/src/main/java/org/apache/sis/gui/dataset/ResourceCell.java
@@ -0,0 +1,183 @@
+/*
+ * 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.Locale;
+import javafx.collections.ObservableList;
+import javafx.scene.control.Button;
+import javafx.scene.control.ContextMenu;
+import javafx.scene.control.MenuItem;
+import javafx.scene.control.TreeCell;
+import javafx.scene.control.TreeItem;
+import javafx.scene.paint.Color;
+import org.apache.sis.storage.Resource;
+import org.apache.sis.storage.DataStoreException;
+import org.apache.sis.internal.storage.URIDataStore;
+import org.apache.sis.internal.storage.io.IOUtilities;
+import org.apache.sis.internal.gui.ExceptionReporter;
+import org.apache.sis.internal.gui.DataStoreOpener;
+import org.apache.sis.internal.gui.Resources;
+import org.apache.sis.internal.gui.Styles;
+import org.apache.sis.internal.util.Strings;
+import org.apache.sis.util.Classes;
+import org.apache.sis.util.Exceptions;
+import org.apache.sis.util.resources.Vocabulary;
+
+
+/**
+ * The visual appearance of an {@link ResourceItem} in a tree.
+ * Cells are initially empty; their content will be specified by {@link 
TreeView} after construction.
+ * This class gets the cell text from a resource by a call to
+ * {@link DataStoreOpener#findLabel(Resource, Locale, boolean)} in a 
background thread.
+ * The same call may be recycled many times for different {@link ResourceItem} 
data.
+ *
+ * @author  Martin Desruisseaux (Geomatys)
+ * @version 1.3
+ *
+ * @see ResourceItem
+ *
+ * @since 1.3
+ * @module
+ */
+final class ResourceCell extends TreeCell<Resource> {
+    /**
+     * Creates a new cell with initially no data.
+     */
+    ResourceCell() {
+    }
+
+    /**
+     * Returns a localized (if possible) string representation of the given 
exception.
+     * This method returns the message if one exists, or the exception class 
name otherwise.
+     */
+    private static String string(final Throwable failure, final Locale locale) 
{
+        String text = 
Strings.trimOrNull(Exceptions.getLocalizedMessage(failure, locale));
+        if (text == null) {
+            text = Classes.getShortClassName(failure);
+        }
+        return text;
+    }
+
+    /**
+     * Invoked when a new resource needs to be shown in the tree view.
+     * This method sets the text to a label that describe the resource
+     * (possibly starting a background thread for fetching that label)
+     * and set a contextual menu.
+     *
+     * @param resource  the resource to show.
+     * @param empty     whether this cell is used to fill out space.
+     */
+    @Override
+    protected void updateItem(final Resource resource, boolean empty) {
+        super.updateItem(resource, empty);          // Mandatory according 
JavaFX documentation.
+        Color       color = Styles.NORMAL_TEXT;
+        String      text  = null;
+        Button      more  = null;
+        ContextMenu menu  = null;
+        final TreeItem<Resource> t;
+        if (!empty && (t = getTreeItem()) instanceof ResourceItem) {
+            final ResourceTree tree = (ResourceTree) getTreeView();
+            final ResourceItem item = (ResourceItem) t;
+            final Throwable error;
+            text = item.label;
+            if (item.isLoading) {
+                /*
+                 * If the resource is in process of being loaded in a 
background thread, show "Loading…"
+                 * with a different color. Item with null resource will be 
replaced by a collection of new
+                 * items by a call to `CellItem.getChildren().setAll(…)` after 
loading process finished.
+                 * Item with non-null resource only need to have their name 
updated.
+                 */
+                color = Styles.LOADING_TEXT;
+                if (text == null) {
+                    text = item.label = 
tree.localized().getString(Resources.Keys.Loading);
+                    if (resource != null) {
+                        tree.fetchLabel(item.new Completer(resource));      // 
Start a background thread.
+                    }
+                }
+            } else if ((error = item.error) != null) {
+                /*
+                 * If an error occurred, show the exception message with a 
button for more details.
+                 * The list of resource children may or may not be available, 
depending if the error
+                 * occurred while fetching the children list or only their 
labels.
+                 */
+                color = Styles.ERROR_TEXT;
+                if (text == null) {
+                    if (resource != null) {
+                        // We have the resource, we only failed to fetch its 
name.
+                        text = 
Vocabulary.getResources(tree.locale).getString(Vocabulary.Keys.Unnamed);
+                    } else {
+                        // More serious error (no resource), show exception 
message.
+                        text = string(error, tree.locale);
+                    }
+                    item.label = text;
+                }
+                more = (Button) getGraphic();
+                if (more == null) {
+                    more = new Button(Styles.ERROR_DETAILS_ICON);
+                }
+                more.setOnAction((e) -> {
+                    final Resources localized = tree.localized();
+                    ExceptionReporter.show(tree,
+                            localized.getString(Resources.Keys.ErrorDetails),
+                            
localized.getString(Resources.Keys.CanNotReadResource), error);
+                });
+            }
+            /*
+             * If the resource is one of the "root" resources, add a menu for 
removing it.
+             * If we find that the cell already has a menu, we do not need to 
build it again.
+             */
+            if (tree.findOrRemove(resource, false) != null) {
+                menu = getContextMenu();
+                if (menu == null) {
+                    menu = new ContextMenu();
+                    final Resources localized = tree.localized();
+                    final MenuItem[] items = new MenuItem[CLOSE + 1];
+                    items[COPY_PATH]   = 
localized.menu(Resources.Keys.CopyFilePath, new PathAction(this, false));
+                    items[OPEN_FOLDER] = 
localized.menu(Resources.Keys.OpenContainingFolder, new PathAction(this, true));
+                    items[CLOSE]       = localized.menu(Resources.Keys.Close, 
(e) -> {
+                        ((ResourceTree) 
getTreeView()).removeAndClose(getItem());
+                    });
+                    menu.getItems().setAll(items);
+                }
+                /*
+                 * "Copy file path" menu item should be enabled only if we can
+                 * get some kind of file path or URI from the specified 
resource.
+                 */
+                Object path;
+                try {
+                    path = URIDataStore.location(resource);
+                } catch (DataStoreException e) {
+                    path = null;
+                    ResourceTree.unexpectedException("updateItem", e);
+                }
+                final ObservableList<MenuItem> items = menu.getItems();
+                
items.get(COPY_PATH).setDisable(!IOUtilities.isKindOfPath(path));
+                items.get(OPEN_FOLDER).setDisable(PathAction.isBrowseDisabled 
|| IOUtilities.toFile(path) == null);
+            }
+        }
+        setText(text);
+        setTextFill(isSelected() ? Styles.SELECTED_TEXT : color);
+        setGraphic(more);
+        setContextMenu(menu);
+    }
+
+    /**
+     * Position of menu items in the contextual menu built by {@link 
#updateItem(Resource, boolean)}.
+     * Above method assumes that {@link #CLOSE} is the last menu item.
+     */
+    private static final int COPY_PATH = 0, OPEN_FOLDER = 1, CLOSE = 2;
+}
diff --git 
a/application/sis-javafx/src/main/java/org/apache/sis/gui/dataset/ResourceItem.java
 
b/application/sis-javafx/src/main/java/org/apache/sis/gui/dataset/ResourceItem.java
new file mode 100644
index 0000000000..1b6151ff46
--- /dev/null
+++ 
b/application/sis-javafx/src/main/java/org/apache/sis/gui/dataset/ResourceItem.java
@@ -0,0 +1,242 @@
+/*
+ * 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.nio.file.Path;
+import java.util.Locale;
+import java.util.List;
+import java.util.ArrayList;
+import javafx.application.Platform;
+import javafx.concurrent.Task;
+import javafx.collections.ObservableList;
+import javafx.scene.control.TreeItem;
+import org.apache.sis.storage.Resource;
+import org.apache.sis.storage.Aggregate;
+import org.apache.sis.storage.DataStoreException;
+import org.apache.sis.internal.gui.DataStoreOpener;
+import org.apache.sis.internal.gui.BackgroundThreads;
+import org.apache.sis.internal.gui.GUIUtilities;
+import org.apache.sis.internal.gui.LogHandler;
+
+
+/**
+ * An item of the {@link Resource} tree completed with additional information.
+ * The list of children is fetched in a background thread when first needed.
+ * This node contains only the data; for visual appearance, see {@link Cell}.
+ *
+ * @author  Martin Desruisseaux (Geomatys)
+ * @version 1.3
+ *
+ * @see Cell
+ *
+ * @since 1.3
+ * @module
+ */
+final class ResourceItem extends TreeItem<Resource> {
+    /**
+     * The path to the resource, or {@code null} if none or unknown. This is 
used for notifications only;
+     * this information does not play an important role for {@link 
ResourceTree} itself.
+     */
+    Path path;
+
+    /**
+     * The text of this node, computed and cached when first needed. 
Computation is done by invoking
+     * {@link DataStoreOpener#findLabel(Resource, Locale, boolean)} in a 
background thread.
+     *
+     * @see ResourceTree#fetchLabel(ResourceItem.Completer)
+     */
+    String label;
+
+    /**
+     * Whether this node is in process of loading data. There is two kinds of 
loading:
+     * <ul>
+     *   <li>The {@link Resource} itself, in which case {@link #getValue()} is 
null.</li>
+     *   <li>The resource {@link #title}, in which case {@link #getValue()} 
has a valid value.</li>
+     * </ul>
+     */
+    boolean isLoading;
+
+    /**
+     * If an error occurred while loading the resource, the cause. The {@link 
#getValue()} property may
+     * be null or non-null, depending if the error occurred while loading the 
resource or only its title.
+     */
+    Throwable error;
+
+    /**
+     * Whether the resource is a leaf. A resource is a leaf if it is not an
+     * instance of {@link Aggregate}, in which case it can not have children.
+     * This information is cached because requested often.
+     */
+    private final boolean isLeaf;
+
+    /**
+     * Whether the list of children has been determined. We use this flag in 
order
+     * to fetch children only when first requested, since this process is 
costly.
+     *
+     * @todo Register {@link org.apache.sis.storage.event.StoreListener} and 
reset
+     *       this flag to {@code false} if the resource content or structure 
changed.
+     */
+    private boolean isChildrenKnown;
+
+    /**
+     * Creates a temporary item with null value for a resource in process of 
being loaded.
+     * This item will be replaced (not updated) by a fresh {@code 
ResourceItem} instance
+     * when the resource will become available.
+     */
+    ResourceItem() {
+        isLeaf    = true;
+        isLoading = true;
+    }
+
+    /**
+     * Creates an item for a resource that we failed to load.
+     */
+    ResourceItem(final Throwable exception) {
+        isLeaf = true;
+        error  = exception;
+    }
+
+    /**
+     * Creates a new node for the given resource.
+     *
+     * @param resource  the resource to show in the tree.
+     */
+    ResourceItem(final Resource resource) {
+        super(resource);
+        isLoading = true;       // Means that the label still need to be 
fetched.
+        isLeaf = !(resource instanceof Aggregate);
+        LogHandler.installListener(resource);
+    }
+
+    /**
+     * Update {@link #label} with the resource label fetched in background 
thread.
+     * Caller should invoke this method only if {@link #isLoading} is {@code 
true}.
+     */
+    final class Completer implements Runnable {
+        /** The resource for which to fetch a label. */
+        private final Resource resource;
+
+        /** Result of fetching the label of a resource. */
+        private String result;
+
+        /** Error that occurred while fetching the label. */
+        private Throwable failure;
+
+        /** Creates a new container for the label of a resource. */
+        Completer(final Resource resource) {
+            this.resource = resource;
+        }
+
+        /** Invoked in a background thread for fetching the label. */
+        final void fetch(final Locale locale) {
+            try {
+                result = DataStoreOpener.findLabel(resource, locale, false);
+            } catch (Throwable e) {
+                failure = e;
+            }
+            Platform.runLater(this);
+        }
+
+        /** Invoked in JavaFX thread after the label has been fetched. */
+        public void run() {
+            isLoading = false;
+            label     = result;
+            error     = failure;
+            GUIUtilities.forceCellUpdate(ResourceItem.this);
+        }
+    }
+
+    /**
+     * Returns whether the resource can not have children.
+     */
+    @Override
+    public boolean isLeaf() {
+        return isLeaf;
+    }
+
+    /**
+     * Returns the items for all sub-resources contained in this resource.
+     * The list is empty if the resource is not an aggregate.
+     */
+    @Override
+    public ObservableList<TreeItem<Resource>> getChildren() {
+        final ObservableList<TreeItem<Resource>> children = 
super.getChildren();
+        if (!isChildrenKnown) {
+            isChildrenKnown = true;                 // Set first for avoiding 
to repeat in case of failure.
+            final Resource resource = getValue();
+            if (resource instanceof Aggregate) {
+                BackgroundThreads.execute(new GetChildren((Aggregate) 
resource));
+                children.add(new ResourceItem());
+            }
+        }
+        return children;
+    }
+
+    /**
+     * The task to execute in a background thread for fetching the children.
+     */
+    private final class GetChildren extends Task<List<TreeItem<Resource>>> {
+        /**
+         * The aggregate from which to get the children.
+         */
+        private final Aggregate resource;
+
+        /**
+         * Creates a new background task for fetching the children from the 
given resource.
+         */
+        GetChildren(final Aggregate resource) {
+            this.resource = resource;
+        }
+
+        /**
+         * Invoked in a background thread for fetching the children of the 
resource
+         * specified at construction time.
+         */
+        @Override
+        protected List<TreeItem<Resource>> call() throws DataStoreException {
+            final List<TreeItem<Resource>> items = new ArrayList<>();
+            final Long id = LogHandler.loadingStart(resource);
+            try {
+                for (final Resource component : resource.components()) {
+                    items.add(new ResourceItem(component));
+                }
+            } finally {
+                LogHandler.loadingStop(id);
+            }
+            return items;
+        }
+
+        /**
+         * Invoked in JavaFX thread if children have been loaded successfully.
+         * The previous node, which was showing "Loading…", is replaced by all
+         * nodes loaded in the background thread.
+         */
+        @Override
+        protected void succeeded() {
+            ResourceItem.super.getChildren().setAll(getValue());
+        }
+
+        /**
+         * Invoked in JavaFX thread if children can not be loaded.
+         */
+        @Override
+        @SuppressWarnings("unchecked")
+        protected void failed() {
+            ResourceItem.super.getChildren().setAll(new 
ResourceItem(getException()));
+        }
+    }
+}
diff --git 
a/application/sis-javafx/src/main/java/org/apache/sis/gui/dataset/ResourceTree.java
 
b/application/sis-javafx/src/main/java/org/apache/sis/gui/dataset/ResourceTree.java
index bab1d75692..3cb14838ec 100644
--- 
a/application/sis-javafx/src/main/java/org/apache/sis/gui/dataset/ResourceTree.java
+++ 
b/application/sis-javafx/src/main/java/org/apache/sis/gui/dataset/ResourceTree.java
@@ -21,52 +21,31 @@ import java.nio.file.Path;
 import java.net.URL;
 import java.net.MalformedURLException;
 import java.nio.file.FileSystemNotFoundException;
-import java.util.AbstractList;
 import java.util.Locale;
 import java.util.Queue;
 import java.util.List;
-import java.util.ArrayList;
 import java.util.LinkedList;
 import java.util.Collection;
-import java.util.Optional;
 import javafx.application.Platform;
 import javafx.concurrent.Task;
 import javafx.collections.ObservableList;
 import javafx.beans.property.ObjectProperty;
 import javafx.beans.property.SimpleObjectProperty;
 import javafx.event.EventHandler;
-import javafx.scene.control.Button;
-import javafx.scene.control.ContextMenu;
-import javafx.scene.control.MenuItem;
-import javafx.scene.control.TreeCell;
 import javafx.scene.control.TreeItem;
 import javafx.scene.control.TreeView;
 import javafx.scene.input.DragEvent;
 import javafx.scene.input.Dragboard;
 import javafx.scene.input.TransferMode;
-import javafx.scene.paint.Color;
-import org.opengis.util.GenericName;
-import org.opengis.metadata.Metadata;
-import org.apache.sis.util.resources.Vocabulary;
-import org.apache.sis.util.Exceptions;
-import org.apache.sis.util.Classes;
 import org.apache.sis.storage.Resource;
 import org.apache.sis.storage.Aggregate;
 import org.apache.sis.storage.DataStore;
-import org.apache.sis.storage.DataStoreException;
-import org.apache.sis.storage.event.StoreEvent;
-import org.apache.sis.storage.event.StoreListener;
-import org.apache.sis.internal.storage.URIDataStore;
 import org.apache.sis.internal.storage.io.IOUtilities;
 import org.apache.sis.internal.gui.DataStoreOpener;
 import org.apache.sis.internal.gui.BackgroundThreads;
 import org.apache.sis.internal.gui.ExceptionReporter;
-import org.apache.sis.internal.gui.GUIUtilities;
-import org.apache.sis.internal.gui.LogHandler;
 import org.apache.sis.internal.gui.Resources;
-import org.apache.sis.internal.gui.Styles;
 import org.apache.sis.internal.system.Modules;
-import org.apache.sis.internal.util.Strings;
 import org.apache.sis.storage.DataStoreProvider;
 import org.apache.sis.util.logging.Logging;
 
@@ -93,7 +72,7 @@ import static java.util.logging.Logger.getLogger;
  *
  * @author  Johann Sorel (Geomatys)
  * @author  Martin Desruisseaux (Geomatys)
- * @version 1.2
+ * @version 1.3
  * @since   1.1
  * @module
  */
@@ -133,9 +112,9 @@ public class ResourceTree extends TreeView<Resource> {
      * of threads, which are likely to be blocked anyway because of {@link 
DataStore} synchronization.
      * Furthermore those threads of overkill in the common case where labels 
are very quick to fetch.
      *
-     * @see #fetchLabel(Item.Completer)
+     * @see #fetchLabel(ResourceItem.Completer)
      */
-    private final Queue<Item.Completer> pendingItems;
+    private final Queue<ResourceItem.Completer> pendingItems;
 
     /**
      * Creates a new tree of resources with initially no resource to show.
@@ -144,7 +123,7 @@ public class ResourceTree extends TreeView<Resource> {
     public ResourceTree() {
         locale = Locale.getDefault();
         pendingItems = new LinkedList<>();
-        setCellFactory((v) -> new Cell());
+        setCellFactory((v) -> new ResourceCell());
         setOnDragOver(ResourceTree::onDragOver);
         setOnDragDropped(this::onDragDropped);
         onResourceLoaded = new SimpleObjectProperty<>(this, 
"onResourceLoaded");
@@ -167,7 +146,7 @@ public class ResourceTree extends TreeView<Resource> {
      */
     public Resource getResource() {
         final TreeItem<Resource> item = getRoot();
-        return item == null ? null : item.getValue();
+        return (item == null) ? null : item.getValue();
     }
 
     /**
@@ -186,8 +165,8 @@ public class ResourceTree extends TreeView<Resource> {
      * @see #removeAndClose(Resource)
      */
     public void setResource(final Resource resource) {
-        setRoot(resource == null ? null : new Item(resource));
-        setShowRoot(!(resource instanceof Root));
+        setRoot(resource == null ? null : new ResourceItem(resource));
+        setShowRoot(!(resource instanceof RootResource));
     }
 
     /**
@@ -210,27 +189,27 @@ public class ResourceTree extends TreeView<Resource> {
         if (resource == null) {
             return false;
         }
-        Root addTo = null;
+        RootResource addTo = null;
         final TreeItem<Resource> item = getRoot();
         if (item != null) {
             final Resource root = item.getValue();
             if (root == resource) {
                 return false;
             }
-            if (root instanceof Root) {
-                addTo = (Root) root;
+            if (root instanceof RootResource) {
+                addTo = (RootResource) root;
             }
         }
         /*
-         * We create the `Root` pseudo-resource even if there is only one 
resource.
-         * A previous version created `Root` only if there was two or more 
ressources,
+         * We create the `RootResource` pseudo-resource even if there is only 
one resource.
+         * A previous version created `RootResource` only if there was two or 
more ressources,
          * but it was causing confusing events when the second resource was 
added.
          */
         if (addTo == null) {
             final TreeItem<Resource> group = new TreeItem<>();
             setShowRoot(false);
             setRoot(group);                                 // Also detach 
`item` from the TreeView root.
-            addTo = new Root(group, item);                  // Pseudo-resource 
for a group of data stores.
+            addTo = new RootResource(group, item);          // Pseudo-resource 
for a group of data stores.
             group.setValue(addTo);
         }
         return addTo.add(resource);
@@ -296,7 +275,7 @@ public class ResourceTree extends TreeView<Resource> {
                  * wrapping of resources.
                  */
                 if (added) {
-                    ((Item) findOrRemove(store, false)).path = path;
+                    ((ResourceItem) findOrRemove(store, false)).path = path;
                 }
                 handler.handle(new ResourceEvent(this, path, 
ResourceEvent.LOADED));
             }
@@ -358,7 +337,7 @@ public class ResourceTree extends TreeView<Resource> {
      *
      * <p>Only the "root" resources (such as the resources given to {@link 
#setResource(Resource)} or
      * {@link #addResource(Resource)} methods) can be removed.
-     * Children of {@link Aggregate} resource and not scanned.
+     * Children of {@link Aggregate} resource are not scanned.
      * If the given resource can not be removed, then this method does 
nothing.</p>
      *
      * <h4>Notifications</h4>
@@ -380,8 +359,8 @@ public class ResourceTree extends TreeView<Resource> {
             final EventHandler<ResourceEvent> handler = onResourceClosed.get();
             if (handler != null) {
                 Path path = null;
-                if (item instanceof Item) {
-                    path = ((Item) item).path;
+                if (item instanceof ResourceItem) {
+                    path = ((ResourceItem) item).path;
                 }
                 if (path == null) try {
                     path = store.getOpenParameters()
@@ -400,12 +379,14 @@ public class ResourceTree extends TreeView<Resource> {
 
     /**
      * Verifies if the given resource is one of the roots, and optionally 
removes it.
+     * If {@code remove} is {@code true}, then it is caller's responsibility 
to close
+     * the resource.
      *
      * @param  resource  the resource to search of remove, or {@code null}.
      * @param  remove    {@code true} for removing the resource, or {@code 
false} for checking only.
      * @return the item wrapping the resource, or {@code null} if the resource 
has not been found in the roots.
      */
-    private TreeItem<Resource> findOrRemove(final Resource resource, final 
boolean remove) {
+    final TreeItem<Resource> findOrRemove(final Resource resource, final 
boolean remove) {
         assert Platform.isFxApplicationThread();
         if (resource != null) {
             /*
@@ -422,6 +403,8 @@ public class ResourceTree extends TreeView<Resource> {
             }
             /*
              * Search for the resource from the root, and optionally remove it.
+             * Intentionally use identity comparison, not `Object.equals(…)`
+             * (should be consistent in whole `ResourceTree` implementation).
              */
             final TreeItem<Resource> item = getRoot();
             if (item != null) {
@@ -433,8 +416,8 @@ public class ResourceTree extends TreeView<Resource> {
                         }
                         return item;
                     }
-                    if (root instanceof Root) {
-                        return ((Root) root).contains(resource, remove);
+                    if (root instanceof RootResource) {
+                        return ((RootResource) root).contains(resource, 
remove);
                     }
                 }
             }
@@ -443,10 +426,10 @@ public class ResourceTree extends TreeView<Resource> {
     }
 
     /**
-     * Updates {@link Item#label} with the resource label fetched in 
background thread.
-     * Caller should invoke this method only if {@link Item#isLoading} is 
{@code true}.
+     * Updates {@link ResourceItem#label} with the resource label fetched in 
background thread.
+     * Caller should invoke this method only if {@link ResourceItem#isLoading} 
is {@code true}.
      */
-    private void fetchLabel(final Item.Completer item) {
+    final void fetchLabel(final ResourceItem.Completer item) {
         final boolean isEmpty;
         synchronized (pendingItems) {
             // The two operations below must be atomic (this is why we do not 
use ConcurrentLinkedQueue).
@@ -457,7 +440,7 @@ public class ResourceTree extends TreeView<Resource> {
             // Not a problem if 2 tasks are launched in parallel.
             BackgroundThreads.execute(() -> {
                 for (;;) {
-                    final Item.Completer c;
+                    final ResourceItem.Completer c;
                     synchronized (pendingItems) {
                         c = pendingItems.poll();
                     }
@@ -475,18 +458,6 @@ public class ResourceTree extends TreeView<Resource> {
         return Resources.forLocale(locale);
     }
 
-    /**
-     * Returns a localized (if possible) string representation of the given 
exception.
-     * This method returns the message if one exist, or the exception class 
name otherwise.
-     */
-    private static String string(final Throwable failure, final Locale locale) 
{
-        String text = 
Strings.trimOrNull(Exceptions.getLocalizedMessage(failure, locale));
-        if (text == null) {
-            text = Classes.getShortClassName(failure);
-        }
-        return text;
-    }
-
     /**
      * Reports an ignorable exception in the given method.
      */
@@ -500,435 +471,4 @@ public class ResourceTree extends TreeView<Resource> {
     static void unexpectedException(final String method, final Exception e) {
         Logging.unexpectedException(getLogger(Modules.APPLICATION), 
ResourceTree.class, method, e);
     }
-
-
-
-
-    /**
-     * The visual appearance of an {@link Item} in a tree. Cells are initially 
empty;
-     * their content will be specified by {@link TreeView} after construction.
-     * This class gets the cell text from a resource by a call to
-     * {@link DataStoreOpener#findLabel(Resource, Locale, boolean)} in a 
background thread.
-     * The same call may be recycled many times for different {@link Item} 
data.
-     *
-     * @see Item
-     */
-    private static final class Cell extends TreeCell<Resource> {
-        /**
-         * Creates a new cell with initially no data.
-         */
-        Cell() {
-        }
-
-        /**
-         * Invoked when a new resource needs to be shown in the tree view.
-         * This method sets the text to a label that describe the resource.
-         *
-         * @param resource  the resource to show.
-         * @param empty     whether this cell is used to fill out space.
-         */
-        @Override
-        protected void updateItem(final Resource resource, boolean empty) {
-            super.updateItem(resource, empty);          // Mandatory according 
JavaFX documentation.
-            Color       color = Styles.NORMAL_TEXT;
-            String      text  = null;
-            Button      more  = null;
-            ContextMenu menu  = null;
-            final TreeItem<Resource> t;
-            if (!empty && (t = getTreeItem()) instanceof Item) {
-                final ResourceTree tree = (ResourceTree) getTreeView();
-                final Item item = (Item) t;
-                final Throwable error;
-                text = item.label;
-                if (item.isLoading) {
-                    /*
-                     * If the resource is in process of being loaded in a 
background thread, show "Loading…"
-                     * with a different color. Item with null resource will be 
replaced by a collection of new
-                     * items by a call to `CellItem.getChildren().setAll(…)` 
after loading process finished.
-                     * Item with non-null resource only need to have their 
name updated.
-                     */
-                    color = Styles.LOADING_TEXT;
-                    if (text == null) {
-                        text = item.label = 
tree.localized().getString(Resources.Keys.Loading);
-                        if (resource != null) {
-                            tree.fetchLabel(item.new Completer(resource));     
 // Start a background thread.
-                        }
-                    }
-                } else if ((error = item.error) != null) {
-                    /*
-                     * If an error occurred, show the exception message with a 
button for more details.
-                     * The list of resource children may or may not be 
available, depending if the error
-                     * occurred while fetching the children list or only their 
labels.
-                     */
-                    color = Styles.ERROR_TEXT;
-                    if (text == null) {
-                        if (resource != null) {
-                            // We have the resource, we only failed to fetch 
its name.
-                            text = 
Vocabulary.getResources(tree.locale).getString(Vocabulary.Keys.Unnamed);
-                        } else {
-                            // More serious error (no resource), show 
exception message.
-                            text = string(error, tree.locale);
-                        }
-                        item.label = text;
-                    }
-                    more = (Button) getGraphic();
-                    if (more == null) {
-                        more = new Button(Styles.ERROR_DETAILS_ICON);
-                    }
-                    more.setOnAction((e) -> {
-                        final Resources localized = tree.localized();
-                        ExceptionReporter.show(tree,
-                                
localized.getString(Resources.Keys.ErrorDetails),
-                                
localized.getString(Resources.Keys.CanNotReadResource), error);
-                    });
-                }
-                /*
-                 * If the resource is one of the "root" resources, add a menu 
for removing it.
-                 * If we find that the cell already has a menu, we do not need 
to build it again.
-                 */
-                if (tree.findOrRemove(resource, false) != null) {
-                    menu = getContextMenu();
-                    if (menu == null) {
-                        menu = new ContextMenu();
-                        final Resources localized = tree.localized();
-                        final MenuItem[] items = new MenuItem[CLOSE + 1];
-                        items[COPY_PATH]   = 
localized.menu(Resources.Keys.CopyFilePath, new PathAction(this, false));
-                        items[OPEN_FOLDER] = 
localized.menu(Resources.Keys.OpenContainingFolder, new PathAction(this, true));
-                        items[CLOSE]       = 
localized.menu(Resources.Keys.Close, (e) -> {
-                            ((ResourceTree) 
getTreeView()).removeAndClose(getItem());
-                        });
-                        menu.getItems().setAll(items);
-                    }
-                    /*
-                     * "Copy file path" menu item should be enabled only if we 
can
-                     * get some kind of file path or URI from the specified 
resource.
-                     */
-                    Object path;
-                    try {
-                        path = URIDataStore.location(resource);
-                    } catch (DataStoreException e) {
-                        path = null;
-                        unexpectedException("updateItem", e);
-                    }
-                    
menu.getItems().get(COPY_PATH).setDisable(!IOUtilities.isKindOfPath(path));
-                    
menu.getItems().get(OPEN_FOLDER).setDisable(PathAction.isBrowseDisabled || 
IOUtilities.toFile(path) == null);
-                }
-            }
-            setText(text);
-            setTextFill(isSelected() ? Styles.SELECTED_TEXT : color);
-            setGraphic(more);
-            setContextMenu(menu);
-        }
-
-        /**
-         * Position of menu items in the contextual menu built by {@link 
#updateItem(Resource, boolean)}.
-         * Above method assumes that {@link #CLOSE} is the last menu item.
-         */
-        private static final int COPY_PATH = 0, OPEN_FOLDER = 1, CLOSE = 2;
-    }
-
-
-
-
-    /**
-     * An item of the {@link Resource} tree completed with additional 
information.
-     * The list of children is fetched in a background thread when first 
needed.
-     * This node contains only the data; for visual appearance, see {@link 
Cell}.
-     *
-     * @see Cell
-     */
-    private static final class Item extends TreeItem<Resource> {
-        /**
-         * The path to the resource, or {@code null} if none or unknown. This 
is used for notifications only;
-         * this information does not play an important role for {@link 
ResourceTree} itself.
-         */
-        Path path;
-
-        /**
-         * The text of this node, computed and cached when first needed. 
Computation is done by invoking
-         * {@link DataStoreOpener#findLabel(Resource, Locale, boolean)} in a 
background thread.
-         *
-         * @see #fetchLabel(Item.Completer)
-         */
-        String label;
-
-        /**
-         * Whether this node is in process of loading data. There is two kinds 
of loading:
-         * <ul>
-         *   <li>The {@link Resource} itself, in which case {@link 
#getValue()} is null.</li>
-         *   <li>The resource {@link #title}, in which case {@link 
#getValue()} has a valid value.</li>
-         * </ul>
-         */
-        boolean isLoading;
-
-        /**
-         * If an error occurred while loading the resource, the cause. The 
{@link #getValue()} property may
-         * be null or non-null, depending if the error occurred while loading 
the resource or only its title.
-         */
-        Throwable error;
-
-        /**
-         * Whether the resource is a leaf. A resource is a leaf if it is not an
-         * instance of {@link Aggregate}, in which case it can not have 
children.
-         * This information is cached because requested often.
-         */
-        private final boolean isLeaf;
-
-        /**
-         * Whether the list of children has been determined. We use this flag 
in order
-         * to fetch children only when first requested, since this process is 
costly.
-         *
-         * @todo Register {@link org.apache.sis.storage.event.StoreListener} 
and reset
-         *       this flag to {@code false} if the resource content or 
structure changed.
-         */
-        private boolean isChildrenKnown;
-
-        /**
-         * Creates a temporary item with null value for a resource in process 
of being loaded.
-         * This item will be replaced (not updated) by a fresh {@code Item} 
instance when the
-         * resource will become available.
-         */
-        Item() {
-            isLeaf    = true;
-            isLoading = true;
-        }
-
-        /**
-         * Creates an item for a resource that we failed to load.
-         */
-        Item(final Throwable exception) {
-            isLeaf = true;
-            error  = exception;
-        }
-
-        /**
-         * Creates a new node for the given resource.
-         *
-         * @param resource  the resource to show in the tree.
-         */
-        Item(final Resource resource) {
-            super(resource);
-            isLoading = true;       // Means that the label still need to be 
fetched.
-            isLeaf = !(resource instanceof Aggregate);
-            LogHandler.installListener(resource);
-        }
-
-        /**
-         * Update {@link #label} with the resource label fetched in background 
thread.
-         * Caller should invoke this method only if {@link #isLoading} is 
{@code true}.
-         */
-        final class Completer implements Runnable {
-            /** The resource for which to fetch a label. */
-            private final Resource resource;
-
-            /** Result of fetching the label of a resource. */
-            private String result;
-
-            /** Error that occurred while fetching the label. */
-            private Throwable failure;
-
-            /** Creates a new container for the label of a resource. */
-            Completer(final Resource resource) {
-                this.resource = resource;
-            }
-
-            /** Invoked in a background thread for fetching the label. */
-            final void fetch(final Locale locale) {
-                try {
-                    result = DataStoreOpener.findLabel(resource, locale, 
false);
-                } catch (Throwable e) {
-                    failure = e;
-                }
-                Platform.runLater(this);
-            }
-
-            /** Invoked in JavaFX thread after the label has been fetched. */
-            public void run() {
-                isLoading = false;
-                label     = result;
-                error     = failure;
-                GUIUtilities.forceCellUpdate(Item.this);
-            }
-        }
-
-        /**
-         * Returns whether the resource can not have children.
-         */
-        @Override
-        public boolean isLeaf() {
-            return isLeaf;
-        }
-
-        /**
-         * Returns the items for all sub-resources contained in this resource.
-         * The list is empty if the resource is not an aggregate.
-         */
-        @Override
-        public ObservableList<TreeItem<Resource>> getChildren() {
-            final ObservableList<TreeItem<Resource>> children = 
super.getChildren();
-            if (!isChildrenKnown) {
-                isChildrenKnown = true;                 // Set first for 
avoiding to repeat in case of failure.
-                final Resource resource = getValue();
-                if (resource instanceof Aggregate) {
-                    BackgroundThreads.execute(new GetChildren((Aggregate) 
resource));
-                    children.add(new Item());
-                }
-            }
-            return children;
-        }
-
-        /**
-         * The task to execute in a background thread for fetching the 
children.
-         */
-        private final class GetChildren extends Task<List<TreeItem<Resource>>> 
{
-            /**
-             * The aggregate from which to get the children.
-             */
-            private final Aggregate resource;
-
-            /**
-             * Creates a new background task for fetching the children from 
the given resource.
-             */
-            GetChildren(final Aggregate resource) {
-                this.resource = resource;
-            }
-
-            /**
-             * Invoked in a background thread for fetching the children of the 
resource
-             * specified at construction time.
-             */
-            @Override
-            protected List<TreeItem<Resource>> call() throws 
DataStoreException {
-                final List<TreeItem<Resource>> items = new ArrayList<>();
-                final Long id = LogHandler.loadingStart(resource);
-                try {
-                    for (final Resource component : resource.components()) {
-                        items.add(new Item(component));
-                    }
-                } finally {
-                    LogHandler.loadingStop(id);
-                }
-                return items;
-            }
-
-            /**
-             * Invoked in JavaFX thread if children have been loaded 
successfully.
-             * The previous node, which was showing "Loading…", is replaced by 
all
-             * nodes loaded in the background thread.
-             */
-            @Override
-            protected void succeeded() {
-                Item.super.getChildren().setAll(getValue());
-            }
-
-            /**
-             * Invoked in JavaFX thread if children can not be loaded.
-             */
-            @Override
-            @SuppressWarnings("unchecked")
-            protected void failed() {
-                Item.super.getChildren().setAll(new Item(getException()));
-            }
-        }
-    }
-
-
-
-
-    /**
-     * The root pseudo-resource for allowing the tree to contain more than one 
resource.
-     * This root node should be hidden in the {@link ResourceTree}.
-     */
-    private static final class Root implements Aggregate {
-        /**
-         * The children to expose as an unmodifiable list of components.
-         */
-        private final List<TreeItem<Resource>> components;
-
-        /**
-         * Creates a new aggregate which is going to be wrapped in the given 
node.
-         * Caller shall invoke {@code group.setValue(root)} after this 
constructor.
-         *
-         * @param  group     the new tree root which will contain "real" 
resources.
-         * @param  previous  the previous root, to be added in the new group.
-         */
-        Root(final TreeItem<Resource> group, final TreeItem<Resource> 
previous) {
-            components = group.getChildren();
-            if (previous != null) {
-                components.add(previous);
-            }
-        }
-
-        /**
-         * Checks whether this root contains the given resource as a direct 
child.
-         * This method does not search recursively in sub-trees.
-         *
-         * @param  resource  the resource to search.
-         * @param  remove    whether to remove the resource if found.
-         * @return the resource wrapper, or {@code null} if not found.
-         */
-        TreeItem<Resource> contains(final Resource resource, final boolean 
remove) {
-            for (int i=components.size(); --i >= 0;) {
-                final TreeItem<Resource> item = components.get(i);
-                if (item.getValue() == resource) {
-                    return remove ? components.remove(i) : item;
-                }
-            }
-            return null;
-        }
-
-        /**
-         * Adds the given resource if not already present.
-         *
-         * @param  resource  the resource to add.
-         * @return whether the given resource has been added.
-         */
-        boolean add(final Resource resource) {
-            for (int i = components.size(); --i >= 0;) {
-                if (components.get(i).getValue() == resource) {
-                    return false;
-                }
-            }
-            return components.add(new Item(resource));
-        }
-
-        /**
-         * Returns a read-only view of the components. This method is not used 
directly by {@link ResourceTree}
-         * but is defined in case a user invoke {@link 
ResourceTree#getResource()}. For this reason, it is not
-         * worth to cache the list created in this method.
-         */
-        @Override
-        public Collection<Resource> components() {
-            return new AbstractList<Resource>() {
-                @Override public int size() {
-                    return components.size();
-                }
-
-                @Override public Resource get(final int index) {
-                    return components.get(index).getValue();
-                }
-            };
-        }
-
-        /**
-         * Returns empty optional since this resource has no identifier.
-         */
-        @Override
-        public Optional<GenericName> getIdentifier() {
-            return Optional.empty();
-        }
-
-        /**
-         * Returns null since this resource has no metadata. Returning null is 
normally
-         * not allowed for this method, but {@link ResourceTree} is robust to 
this case.
-         */
-        @Override
-        public Metadata getMetadata() {
-            return null;
-        }
-
-        /** Ignored since this class does not emit any event. */
-        @Override public <T extends StoreEvent> void    addListener(Class<T> 
eventType, StoreListener<? super T> listener) {}
-        @Override public <T extends StoreEvent> void removeListener(Class<T> 
eventType, StoreListener<? super T> listener) {}
-    }
 }
diff --git 
a/application/sis-javafx/src/main/java/org/apache/sis/gui/dataset/RootResource.java
 
b/application/sis-javafx/src/main/java/org/apache/sis/gui/dataset/RootResource.java
new file mode 100644
index 0000000000..86942977e5
--- /dev/null
+++ 
b/application/sis-javafx/src/main/java/org/apache/sis/gui/dataset/RootResource.java
@@ -0,0 +1,132 @@
+/*
+ * 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.AbstractList;
+import java.util.Collection;
+import java.util.List;
+import java.util.Optional;
+import javafx.scene.control.TreeItem;
+import org.apache.sis.storage.Aggregate;
+import org.apache.sis.storage.Resource;
+import org.apache.sis.storage.event.StoreEvent;
+import org.apache.sis.storage.event.StoreListener;
+import org.opengis.metadata.Metadata;
+import org.opengis.util.GenericName;
+
+
+/**
+ * The root pseudo-resource for allowing the tree to contain more than one 
resource.
+ * This root node should be hidden in the {@link ResourceTree}.
+ *
+ * @author  Martin Desruisseaux (Geomatys)
+ * @version 1.3
+ * @since   1.3
+ * @module
+ */
+final class RootResource implements Aggregate {
+    /**
+     * The children to expose as an unmodifiable list of components.
+     */
+    private final List<TreeItem<Resource>> components;
+
+    /**
+     * Creates a new aggregate which is going to be wrapped in the given node.
+     * Caller shall invoke {@code group.setValue(root)} after this constructor.
+     *
+     * @param  group     the new tree root which will contain "real" resources.
+     * @param  previous  the previous root, to be added in the new group.
+     */
+    RootResource(final TreeItem<Resource> group, final TreeItem<Resource> 
previous) {
+        components = group.getChildren();
+        if (previous != null) {
+            components.add(previous);
+        }
+    }
+
+    /**
+     * Checks whether this root contains the given resource as a direct child.
+     * This method does not search recursively in sub-trees.
+     *
+     * @param  resource  the resource to search.
+     * @param  remove    whether to remove the resource if found.
+     * @return the resource wrapper, or {@code null} if not found.
+     */
+    TreeItem<Resource> contains(final Resource resource, final boolean remove) 
{
+        for (int i=components.size(); --i >= 0;) {
+            final TreeItem<Resource> item = components.get(i);
+            if (item.getValue() == resource) {
+                return remove ? components.remove(i) : item;
+            }
+        }
+        return null;
+    }
+
+    /**
+     * Adds the given resource if not already present.
+     *
+     * @param  resource  the resource to add.
+     * @return whether the given resource has been added.
+     */
+    boolean add(final Resource resource) {
+        for (int i = components.size(); --i >= 0;) {
+            if (components.get(i).getValue() == resource) {
+                return false;
+            }
+        }
+        return components.add(new ResourceItem(resource));
+    }
+
+    /**
+     * Returns a read-only view of the components. This method is not used 
directly by {@link ResourceTree}
+     * but is defined in case a user invoke {@link 
ResourceTree#getResource()}. For this reason, it is not
+     * worth to cache the list created in this method.
+     */
+    @Override
+    public Collection<Resource> components() {
+        return new AbstractList<Resource>() {
+            @Override public int size() {
+                return components.size();
+            }
+
+            @Override public Resource get(final int index) {
+                return components.get(index).getValue();
+            }
+        };
+    }
+
+    /**
+     * Returns empty optional since this resource has no identifier.
+     */
+    @Override
+    public Optional<GenericName> getIdentifier() {
+        return Optional.empty();
+    }
+
+    /**
+     * Returns null since this resource has no metadata. Returning null is 
normally
+     * not allowed for this method, but {@link ResourceTree} is robust to this 
case.
+     */
+    @Override
+    public Metadata getMetadata() {
+        return null;
+    }
+
+    /** Ignored since this class does not emit any event. */
+    @Override public <T extends StoreEvent> void    addListener(Class<T> 
eventType, StoreListener<? super T> listener) {}
+    @Override public <T extends StoreEvent> void removeListener(Class<T> 
eventType, StoreListener<? super T> listener) {}
+}

Reply via email to