sis-commits mailing list archives

Site index · List index
Message view « Date » · « Thread »
Top « Date » · « Thread »
From desruisse...@apache.org
Subject [sis] 02/02: Report warning that occurred while loading resources or data store.
Date Wed, 08 Jul 2020 18:28:55 GMT
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 8b3e75c165b5afe2817be72fd1da11c543021b29
Author: Martin Desruisseaux <martin.desruisseaux@geomatys.com>
AuthorDate: Wed Jul 8 20:27:35 2020 +0200

    Report warning that occurred while loading resources or data store.
---
 .../main/java/org/apache/sis/gui/DataViewer.java   |  33 +++
 .../org/apache/sis/gui/coverage/ImageRequest.java  |  88 ++++----
 .../java/org/apache/sis/gui/dataset/LogViewer.java | 250 +++++++++++++++++++++
 .../apache/sis/gui/dataset/ResourceExplorer.java   |   7 +-
 .../org/apache/sis/gui/dataset/ResourceTree.java   |  90 ++++----
 .../org/apache/sis/internal/gui/LogHandler.java    | 245 ++++++++++++++++++++
 .../org/apache/sis/util/resources/Vocabulary.java  |  30 +++
 .../sis/util/resources/Vocabulary.properties       |   6 +
 .../sis/util/resources/Vocabulary_fr.properties    |   6 +
 9 files changed, 677 insertions(+), 78 deletions(-)

diff --git a/application/sis-javafx/src/main/java/org/apache/sis/gui/DataViewer.java b/application/sis-javafx/src/main/java/org/apache/sis/gui/DataViewer.java
index ba5aca1..e0e2288 100644
--- a/application/sis-javafx/src/main/java/org/apache/sis/gui/DataViewer.java
+++ b/application/sis-javafx/src/main/java/org/apache/sis/gui/DataViewer.java
@@ -35,8 +35,10 @@ import javafx.stage.FileChooser;
 import javafx.scene.image.Image;
 import javafx.stage.Screen;
 import javafx.stage.Stage;
+import org.apache.sis.gui.dataset.LogViewer;
 import org.apache.sis.gui.dataset.ResourceExplorer;
 import org.apache.sis.internal.gui.BackgroundThreads;
+import org.apache.sis.internal.gui.LogHandler;
 import org.apache.sis.internal.gui.Resources;
 import org.apache.sis.internal.gui.RecentChoices;
 import org.apache.sis.internal.storage.Capability;
@@ -65,6 +67,7 @@ public class DataViewer extends Application {
      * @param args  ignored.
      */
     public static void main(final String[] args) {
+        LogHandler.register(true);
         launch(args);
     }
 
@@ -101,6 +104,13 @@ public class DataViewer extends Application {
     private FileChooser.ExtensionFilter lastFilter;
 
     /**
+     * The window showing system logs. Created when first requested.
+     *
+     * @see #showSystemLogsWindow()
+     */
+    private Stage systemLogsWindow;
+
+    /**
      * Creates a new Apache SIS application.
      */
     public DataViewer() {
@@ -135,8 +145,11 @@ public class DataViewer extends Application {
         }
         final Menu help = new Menu(localized.getString(Resources.Keys.Help));
         {   // For keeping variables locale.
+            final MenuItem logging = new MenuItem(vocabulary.getString(Vocabulary.Keys.Logs));
+            logging.setOnAction((e) -> showSystemLogsWindow());
             help.getItems().addAll(
                     localized.menu(Resources.Keys.WebSite, (e) -> getHostServices().showDocument("https://sis.apache.org/")),
+                    new SeparatorMenuItem(), logging,
                     localized.menu(Resources.Keys.About, (e) -> AboutDialog.show()));
         }
         final Menu windows = new Menu(localized.getString(Resources.Keys.Windows));
@@ -229,6 +242,25 @@ public class DataViewer extends Application {
     }
 
     /**
+     * Shows system logs in a separated window.
+     */
+    private void showSystemLogsWindow() {
+        if (systemLogsWindow == null) {
+            final LogViewer viewer = new LogViewer();
+            viewer.systemLogs.set(true);
+            final Stage w = new Stage();
+            w.setTitle(Vocabulary.format(Vocabulary.Keys.Logs) + " — Apache SIS");
+            w.getIcons().setAll(window.getIcons());
+            w.setScene(new Scene(viewer.getView()));
+            w.setWidth (800);
+            w.setHeight(600);
+            window.setOnHidden((e) -> w.hide());
+            systemLogsWindow = w;
+        }
+        systemLogsWindow.show();
+    }
+
+    /**
      * Invoked when the application should stop. No SIS application can be used after
      * this method has been invoked (i.e. the application can not be restarted).
      *
@@ -236,6 +268,7 @@ public class DataViewer extends Application {
      */
     @Override
     public void stop() throws Exception {
+        LogHandler.register(false);
         BackgroundThreads.stop();
         RecentChoices.saveReferenceSystems();
         super.stop();
diff --git a/application/sis-javafx/src/main/java/org/apache/sis/gui/coverage/ImageRequest.java
b/application/sis-javafx/src/main/java/org/apache/sis/gui/coverage/ImageRequest.java
index 85b4378..18a3383 100644
--- a/application/sis-javafx/src/main/java/org/apache/sis/gui/coverage/ImageRequest.java
+++ b/application/sis-javafx/src/main/java/org/apache/sis/gui/coverage/ImageRequest.java
@@ -26,6 +26,7 @@ import org.apache.sis.coverage.grid.GridGeometry;
 import org.apache.sis.coverage.grid.GridExtent;
 import org.apache.sis.util.ArgumentChecks;
 import org.apache.sis.gui.map.StatusBar;
+import org.apache.sis.internal.gui.LogHandler;
 import org.apache.sis.referencing.operation.transform.MathTransforms;
 import org.apache.sis.storage.DataStoreException;
 
@@ -48,6 +49,7 @@ public class ImageRequest {
 
     /**
      * The source from where to read the image, specified at construction time.
+     * Can not be {@code null}.
      */
     final GridCoverageResource resource;
 
@@ -274,32 +276,37 @@ public class ImageRequest {
      * @throws DataStoreException if an error occurred while loading the grid coverage.
      */
     final synchronized RenderedImage load(final FutureTask<?> task, final boolean converted)
throws DataStoreException {
-        if (coverage == null) {
-            GridGeometry domain = this.domain;
-            if (domain == null) {
-                domain = resource.getGridGeometry();
+        final Long id = LogHandler.loadingStart(resource);
+        try {
+            if (coverage == null) {
+                GridGeometry domain = this.domain;
+                if (domain == null) {
+                    domain = resource.getGridGeometry();
+                }
+                if (domain != null && domain.getDimension() > BIDIMENSIONAL) {
+                    domain = slice(domain).build();
+                }
+                /*
+                 * TODO: We restrict loading to a two-dimensional slice for now.
+                 * Future version will need to give user control over slices.
+                 */
+                coverage = resource.read(domain, range);                    // May be long
to execute.
+                coverage = coverage.forConvertedValues(converted);
             }
-            if (domain != null && domain.getDimension() > BIDIMENSIONAL) {
-                domain = slice(domain).build();
+            if (task.isCancelled()) {
+                return null;
             }
-            /*
-             * TODO: We restrict loading to a two-dimensional slice for now.
-             * Future version will need to give user control over slices.
-             */
-            coverage = resource.read(domain, range);                    // May be long to
execute.
-            coverage = coverage.forConvertedValues(converted);
-        }
-        if (task.isCancelled()) {
-            return null;
-        }
-        GridExtent se = sliceExtent;
-        if (se == null) {
-            final GridGeometry cd = coverage.getGridGeometry();
-            if (cd != null && cd.getDimension() > BIDIMENSIONAL) {      // Should
never be null but we are paranoiac.
-                se = slice(cd).getIntersection();
+            GridExtent se = sliceExtent;
+            if (se == null) {
+                final GridGeometry cd = coverage.getGridGeometry();
+                if (cd != null && cd.getDimension() > BIDIMENSIONAL) {      //
Should never be null but we are paranoiac.
+                    se = slice(cd).getIntersection();
+                }
             }
+            return coverage.render(se);
+        } finally {
+            LogHandler.loadingStop(id);
         }
-        return coverage.render(se);
     }
 
     /**
@@ -308,23 +315,28 @@ public class ImageRequest {
      * successfully loaded in background thread a new image.
      */
     final void configure(final StatusBar bar) {
-        final GridCoverage cv = coverage;
-        final GridExtent request = sliceExtent;
-        bar.applyCanvasGeometry(cv != null ? cv.getGridGeometry() : null);
-        /*
-         * By `GridCoverage.render(GridExtent)` contract, the `RenderedImage` pixel coordinates
are relative
-         * to the requested `GridExtent`. Consequently we need to translate the image coordinates
so that it
-         * become the coordinates of the original `GridGeometry` before to apply `gridToCRS`.
 It is okay to
-         * modify `StatusBar.localToObjectiveCRS` because we do not associate it to a `MapCanvas`,
so it will
-         * not be overwritten by gesture events (zoom, pan, etc).
-         */
-        if (request != null) {
-            final double[] origin = new double[request.getDimension()];
-            for (int i=0; i<origin.length; i++) {
-                origin[i] = request.getLow(i);
+        final Long id = LogHandler.loadingStart(resource);
+        try {
+            final GridCoverage cv = coverage;
+            final GridExtent request = sliceExtent;
+            bar.applyCanvasGeometry(cv != null ? cv.getGridGeometry() : null);
+            /*
+             * By `GridCoverage.render(GridExtent)` contract, the `RenderedImage` pixel coordinates
are relative
+             * to the requested `GridExtent`. Consequently we need to translate the image
coordinates so that it
+             * become the coordinates of the original `GridGeometry` before to apply `gridToCRS`.
 It is okay to
+             * modify `StatusBar.localToObjectiveCRS` because we do not associate it to a
`MapCanvas`, so it will
+             * not be overwritten by gesture events (zoom, pan, etc).
+             */
+            if (request != null) {
+                final double[] origin = new double[request.getDimension()];
+                for (int i=0; i<origin.length; i++) {
+                    origin[i] = request.getLow(i);
+                }
+                bar.localToObjectiveCRS.set(MathTransforms.concatenate(
+                        MathTransforms.translation(origin), bar.localToObjectiveCRS.get()));
             }
-            bar.localToObjectiveCRS.set(MathTransforms.concatenate(
-                    MathTransforms.translation(origin), bar.localToObjectiveCRS.get()));
+        } finally {
+            LogHandler.loadingStop(id);
         }
     }
 }
diff --git a/application/sis-javafx/src/main/java/org/apache/sis/gui/dataset/LogViewer.java
b/application/sis-javafx/src/main/java/org/apache/sis/gui/dataset/LogViewer.java
new file mode 100644
index 0000000..b28cc65
--- /dev/null
+++ b/application/sis-javafx/src/main/java/org/apache/sis/gui/dataset/LogViewer.java
@@ -0,0 +1,250 @@
+/*
+ * 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 java.util.logging.LogRecord;
+import java.util.logging.SimpleFormatter;
+import javafx.scene.layout.Region;
+import javafx.scene.control.TableView;
+import javafx.scene.control.TableColumn;
+import javafx.scene.control.TableColumn.CellDataFeatures;
+import javafx.beans.property.ObjectProperty;
+import javafx.beans.property.BooleanProperty;
+import javafx.beans.property.ReadOnlyBooleanProperty;
+import javafx.beans.property.ReadOnlyBooleanWrapper;
+import javafx.beans.property.SimpleObjectProperty;
+import javafx.beans.property.SimpleBooleanProperty;
+import javafx.beans.value.ObservableValue;
+import javafx.collections.FXCollections;
+import javafx.collections.ListChangeListener;
+import javafx.collections.ObservableList;
+import org.apache.sis.gui.Widget;
+import org.apache.sis.storage.Resource;
+import org.apache.sis.util.resources.Vocabulary;
+import org.apache.sis.internal.gui.LogHandler;
+import org.apache.sis.internal.gui.ImmutableObjectProperty;
+
+
+/**
+ * Shows a table of recent log records, optionally filtered to logs related to a specific
resource.
+ *
+ * @author  Martin Desruisseaux (Geomatys)
+ * @version 1.1
+ * @since   1.1
+ * @module
+ */
+public class LogViewer extends Widget {
+    /**
+     * The table of log records.
+     */
+    private final TableView<LogRecord> table;
+
+    /**
+     * The data store or resource for which to show log records.
+     * If this property value is {@code null}, then the system logs will be shown
+     * if {@link #systemLogs} is {@code true}, or no logs will be shown otherwise.
+     */
+    public final ObjectProperty<Resource> source;
+
+    /**
+     * Whether to show system logs instead then the logs related to a specific resource.
+     * If this property is set to {@code true}, then {@link #source} is automatically set
to {@code null}.
+     * Conversely if {@link #source} is set to a non-null value, then this property is set
to {@code false}.
+     */
+    public final BooleanProperty systemLogs;
+
+    /**
+     * Whether this viewer has no log record to show.
+     *
+     * @see #isEmptyProperty()
+     */
+    private final Listener isEmpty;
+
+    /**
+     * Whether {@link #source} is modified in reaction to a {@link #systemLogs} change, or
conversely.
+     */
+    private boolean isAdjusting;
+
+    /**
+     * The formatter for logging messages.
+     */
+    private final SimpleFormatter formatter;
+
+    /**
+     * Creates an initially empty viewer of log records. For viewing logs, {@link #source}
+     * must be set to a non-null value or {@link #systemLogs} must be set to {@code true}.
+     */
+    public LogViewer() {
+        this(Vocabulary.getResources((Locale) null));
+    }
+
+    /**
+     * Creates a new view of log records.
+     */
+    LogViewer(final Vocabulary vocabulary) {
+        formatter  = new SimpleFormatter();
+        source     = new SimpleObjectProperty<>(this, "source");
+        systemLogs = new SimpleBooleanProperty (this, "systemLogs");
+        isEmpty    = new Listener(this);
+        table      = new TableView<>(FXCollections.emptyObservableList());
+
+        final TableColumn<LogRecord, String> level   = new TableColumn<>(vocabulary.getString(Vocabulary.Keys.Level));
+        final TableColumn<LogRecord, String> time    = new TableColumn<>(vocabulary.getString(Vocabulary.Keys.DateAndTime));
+        final TableColumn<LogRecord, String> logger  = new TableColumn<>(vocabulary.getString(Vocabulary.Keys.Logger));
+        final TableColumn<LogRecord, String> classe  = new TableColumn<>(vocabulary.getString(Vocabulary.Keys.Class));
+        final TableColumn<LogRecord, String> method  = new TableColumn<>(vocabulary.getString(Vocabulary.Keys.Method));
+        final TableColumn<LogRecord, String> message = new TableColumn<>(vocabulary.getString(Vocabulary.Keys.Message));
+
+        level  .setCellValueFactory((cell) -> toString(cell, Vocabulary.Keys.Level));
+        time   .setCellValueFactory((cell) -> toString(cell, Vocabulary.Keys.DateAndTime));
+        logger .setCellValueFactory((cell) -> toString(cell, Vocabulary.Keys.Logger));
+        classe .setCellValueFactory((cell) -> toString(cell, Vocabulary.Keys.Class));
+        method .setCellValueFactory((cell) -> toString(cell, Vocabulary.Keys.Method));
+        message.setCellValueFactory((cell) -> toString(cell, Vocabulary.Keys.Message));
+
+        level .setVisible(false);
+        time  .setVisible(false);
+        logger.setVisible(false);
+        classe.setVisible(false);
+        method.setVisible(false);
+
+        table.getColumns().setAll(level, time, logger, classe, method, message);
+        table.setColumnResizePolicy(TableView.CONSTRAINED_RESIZE_POLICY);
+        table.setTableMenuButtonVisible(true);
+
+        source.addListener((p,o,n) -> {
+            if (!isAdjusting) try {
+                isAdjusting = true;
+                systemLogs.set(false);
+                setItems(LogHandler.getRecords(n));
+            } finally {
+                isAdjusting = false;
+            }
+        });
+        systemLogs.addListener((p,o,n) -> {
+            if (!isAdjusting) try {
+                isAdjusting = true;
+                source.set(null);
+                setItems(n ? LogHandler.getSystemRecords() : FXCollections.emptyObservableList());
+            } finally {
+                isAdjusting = false;
+            }
+        });
+    }
+
+    /**
+     * Sets a new list of log records.
+     */
+    private void setItems(final ObservableList<LogRecord> records) {
+        final boolean e = records.isEmpty();
+        table.setItems(records);
+        isEmpty.set(e);
+        if (e) {
+            records.addListener(isEmpty);
+        }
+    }
+
+    /**
+     * Implementation of {@link LogViewer#isEmpty} property.
+     * Also a listener for being notified when the property value needs to be changed.
+     */
+    private static final class Listener extends ReadOnlyBooleanWrapper implements ListChangeListener<LogRecord>
{
+        /**
+         * Creates the {@link LogViewer#isEmpty} property.
+         */
+        Listener(final LogViewer owner) {
+            super(owner, "isEmpty", true);
+        }
+
+        /**
+         * Invoked when the list of records changed.
+         */
+        @Override public void onChanged(final Change<? extends LogRecord> change) {
+            final ObservableList<? extends LogRecord> list = change.getList();
+            if (!list.isEmpty()) {
+                list.removeListener(this);
+            }
+            set(false);
+        }
+    }
+
+    /**
+     * Whether this viewer has no log record to show.
+     * This property is useful for disabling or enabling a tab.
+     *
+     * @return the property telling whether this viewer no log record to show.
+     */
+    public final ReadOnlyBooleanProperty isEmptyProperty() {
+        return isEmpty.getReadOnlyProperty();
+    }
+
+    /**
+     * Returns the string representation of a logger property for the given cell.
+     */
+    private ObservableValue<String> toString(final CellDataFeatures<LogRecord,String>
cell, final int type) {
+        if (cell != null) {
+            final LogRecord log = cell.getValue();
+            if (log != null) {
+                String text;
+                switch (type) {
+                    case Vocabulary.Keys.Level: {
+                        text = log.getLevel().getLocalizedName();
+                        break;
+                    }
+                    case Vocabulary.Keys.DateAndTime: {
+                        text = log.getInstant().toString();
+                        break;
+                    }
+                    case Vocabulary.Keys.Logger: {
+                        text = log.getLoggerName();
+                        break;
+                    }
+                    case Vocabulary.Keys.Class: {
+                        text = log.getSourceClassName();
+                        if (text != null) {
+                            text = text.substring(text.lastIndexOf('.') + 1);
+                        }
+                        break;
+                    }
+                    case Vocabulary.Keys.Method: {
+                        text = log.getSourceMethodName();
+                        break;
+                    }
+                    case Vocabulary.Keys.Message: {
+                        text = formatter.formatMessage(log);
+                        break;
+                    }
+                    default: throw new AssertionError(type);
+                }
+                if (text != null) {
+                    return new ImmutableObjectProperty<>(text);
+                }
+            }
+        }
+        return null;
+    }
+
+    /**
+     * Returns the control to show in the scene graph.
+     * The implementation class may change in any future version.
+     */
+    @Override
+    public Region getView() {
+        return table;
+    }
+}
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 ec7eb4a..e7763fa 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
@@ -152,6 +152,8 @@ public class ResourceExplorer extends WindowManager {
 
         tableTab = new Tab(vocabulary.getString(Vocabulary.Keys.Data));
         tableTab.setContextMenu(new ContextMenu(SelectedData.setTabularView(createNewWindowMenu())));
+        final LogViewer logging = new LogViewer(vocabulary);
+        logging.source.bind(selectedResource);
 
         final String nativeTabText = vocabulary.getString(Vocabulary.Keys.Format);
         final MetadataTree nativeMetadata = new MetadataTree(metadata);
@@ -163,10 +165,13 @@ public class ResourceExplorer extends WindowManager {
             nativeTab.setText(Objects.toString(label, nativeTabText));
         });
 
+        final Tab loggingTab = new Tab(vocabulary.getString(Vocabulary.Keys.Logs), logging.getView());
+        loggingTab.disableProperty().bind(logging.isEmptyProperty());
+
         final TabPane tabs = new TabPane(
             new Tab(vocabulary.getString(Vocabulary.Keys.Summary),  metadata.getView()),
viewTab, tableTab,
             new Tab(vocabulary.getString(Vocabulary.Keys.Metadata), new StandardMetadataTree(metadata)),
-            nativeTab);
+            nativeTab, loggingTab);
 
         tabs.setTabClosingPolicy(TabPane.TabClosingPolicy.UNAVAILABLE);
         tabs.setTabDragPolicy(TabPane.TabDragPolicy.REORDER);
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 3201e10..09159c8 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
@@ -56,6 +56,7 @@ import org.apache.sis.storage.event.StoreListener;
 import org.apache.sis.internal.gui.ResourceLoader;
 import org.apache.sis.internal.gui.BackgroundThreads;
 import org.apache.sis.internal.gui.ExceptionReporter;
+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.util.Strings;
@@ -318,48 +319,53 @@ public class ResourceTree extends TreeView<Resource> {
     final String getTitle(final Resource resource, final boolean showError) {
         Throwable failure = null;
         if (resource != null) try {
-            /*
-             * The data store display name is typically the file name. We give precedence
to that name
-             * instead than the citation title because the citation may be the same for many
files of
-             * the same product, while the display name have better chances to be distinct
for each file.
-             */
-            if (resource instanceof DataStore) {
-                final String name = Strings.trimOrNull(((DataStore) resource).getDisplayName());
-                if (name != null) return name;
-            }
-            /*
-             * Search for a title in metadata first because it has better chances
-             * to be human-readable compared to the resource identifier.
-             */
-            Collection<? extends Identification> identifications = null;
-            final Metadata metadata = resource.getMetadata();
-            if (metadata != null) {
-                identifications = metadata.getIdentificationInfo();
-                if (identifications != null) {
-                    for (final Identification identification : identifications) {
-                        final Citation citation = identification.getCitation();
-                        if (citation != null) {
-                            final String t = string(citation.getTitle());
-                            if (t != null) return t;
+            final Long logID = LogHandler.loadingStart(resource);
+            try {
+                /*
+                 * The data store display name is typically the file name. We give precedence
to that name
+                 * instead than the citation title because the citation may be the same for
many files of
+                 * the same product, while the display name have better chances to be distinct
for each file.
+                 */
+                if (resource instanceof DataStore) {
+                    final String name = Strings.trimOrNull(((DataStore) resource).getDisplayName());
+                    if (name != null) return name;
+                }
+                /*
+                 * Search for a title in metadata first because it has better chances
+                 * to be human-readable compared to the resource identifier.
+                 */
+                Collection<? extends Identification> identifications = null;
+                final Metadata metadata = resource.getMetadata();
+                if (metadata != null) {
+                    identifications = metadata.getIdentificationInfo();
+                    if (identifications != null) {
+                        for (final Identification identification : identifications) {
+                            final Citation citation = identification.getCitation();
+                            if (citation != null) {
+                                final String t = string(citation.getTitle());
+                                if (t != null) return t;
+                            }
                         }
                     }
                 }
-            }
-            /*
-             * If we find no title in the metadata, use the resource identifier.
-             * We search of explicitly declared identifier first before to fallback
-             * on metadata, because the later is more subject to interpretation.
-             */
-            final Optional<GenericName> id = resource.getIdentifier();
-            if (id.isPresent()) {
-                final String t = string(id.get().toInternationalString());
-                if (t != null) return t;
-            }
-            if (identifications != null) {
-                for (final Identification identification : identifications) {
-                    final String t = Citations.getIdentifier(identification.getCitation());
+                /*
+                 * If we find no title in the metadata, use the resource identifier.
+                 * We search of explicitly declared identifier first before to fallback
+                 * on metadata, because the later is more subject to interpretation.
+                 */
+                final Optional<GenericName> id = resource.getIdentifier();
+                if (id.isPresent()) {
+                    final String t = string(id.get().toInternationalString());
                     if (t != null) return t;
                 }
+                if (identifications != null) {
+                    for (final Identification identification : identifications) {
+                        final String t = Citations.getIdentifier(identification.getCitation());
+                        if (t != null) return t;
+                    }
+                }
+            } finally {
+                LogHandler.loadingStop(logID);
             }
         } catch (DataStoreException | RuntimeException e) {
             if (showError) {
@@ -517,6 +523,7 @@ public class ResourceTree extends TreeView<Resource> {
         Item(final Resource resource) {
             super(resource);
             isLeaf = !(resource instanceof Aggregate);
+            LogHandler.installListener(resource);
         }
 
         /**
@@ -568,8 +575,13 @@ public class ResourceTree extends TreeView<Resource> {
             @Override
             protected List<TreeItem<Resource>> call() throws DataStoreException
{
                 final List<TreeItem<Resource>> items = new ArrayList<>();
-                for (final Resource component : resource.components()) {
-                    items.add(new Item(component));
+                final Long id = LogHandler.loadingStart(resource);
+                try {
+                    for (final Resource component : resource.components()) {
+                        items.add(new Item(component));
+                    }
+                } finally {
+                    LogHandler.loadingStop(id);
                 }
                 return items;
             }
diff --git a/application/sis-javafx/src/main/java/org/apache/sis/internal/gui/LogHandler.java
b/application/sis-javafx/src/main/java/org/apache/sis/internal/gui/LogHandler.java
new file mode 100644
index 0000000..29fe9d3
--- /dev/null
+++ b/application/sis-javafx/src/main/java/org/apache/sis/internal/gui/LogHandler.java
@@ -0,0 +1,245 @@
+/*
+ * 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.internal.gui;
+
+import java.util.WeakHashMap;
+import java.util.concurrent.ConcurrentMap;
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.logging.Logger;
+import java.util.logging.Handler;
+import java.util.logging.LogRecord;
+import javafx.application.Platform;
+import javafx.collections.FXCollections;
+import javafx.collections.ObservableList;
+import org.apache.sis.storage.DataStore;
+import org.apache.sis.storage.Resource;
+import org.apache.sis.storage.event.StoreListener;
+import org.apache.sis.storage.event.WarningEvent;
+
+
+/**
+ * A collector of log records emitted either by the logging system or by {@link DataStore}
instances.
+ * This class maintains both a global (system) list and a list of log records specific to
each resource.
+ *
+ * @author  Martin Desruisseaux (Geomatys)
+ * @version 1.1
+ * @since   1.1
+ * @module
+ */
+public final class LogHandler extends Handler implements StoreListener<WarningEvent>
{
+    /**
+     * Maximal number of log records stored by this class.
+     */
+    private static final int LIMIT = 1000;
+
+    /**
+     * The unique instance of this handler. This handler is registered on the root logger.
+     */
+    private static final LogHandler INSTANCE = new LogHandler();
+
+    /**
+     * Loggings related to the SIS library as a whole, not specific to any particular resources.
+     * May also contain loggings from libraries other than SIS. The length of this list is
limited
+     * to {@value #LIMIT} elements. This list shall be read and written in JavaFX thread
only.
+     */
+    private final ObservableList<LogRecord> systemLogs;
+
+    /**
+     * The list of log records specific to each resource.
+     * Read and write operations on this map shall be synchronized on {@code resourceLogs}.
+     * Read and write operations on map values shall be done in JavaFX thread only.
+     */
+    private final WeakHashMap<Resource, ObservableList<LogRecord>> resourceLogs;
+
+    /**
+     * The list of log records for which loading are in progress. Keys are thread identifiers
+     * and values are values of {@link #resourceLogs}. Addition must be followed by a removal
+     * in a {@code try ... finally} block. Read and write operations on map values shall
be
+     * done in JavaFX thread only.
+     */
+    private final ConcurrentMap<Long, ObservableList<LogRecord>> inProgress;
+
+    /**
+     * Creates an initially empty collector.
+     */
+    private LogHandler() {
+        systemLogs   = FXCollections.observableArrayList();
+        resourceLogs = new WeakHashMap<>();
+        inProgress   = new ConcurrentHashMap<>();
+    }
+
+    /**
+     * Registers or unregisters the unique handler instance on the root logger.
+     *
+     * @param  enabled  {@code true} for registering or {@code false} for unregistering.
+     */
+    public static void register(final boolean enabled) {
+        final Logger root = Logger.getLogger("");
+        if (enabled) {
+            root.addHandler(INSTANCE);
+        } else {
+            root.removeHandler(INSTANCE);
+            INSTANCE.close();
+        }
+    }
+
+    /**
+     * Installs warning listener on the given resource. There is not uninstall method;
+     * it is okay to rely on the garbage collector when the resource is no longer used.
+     *
+     * @param  resource  the resource on which to install listener. May be {@code null}.
+     */
+    public static void installListener(final Resource resource) {
+        if (resource != null) {
+            resource.addListener(WarningEvent.class, INSTANCE);
+        }
+    }
+
+    /**
+     * Notifies this {@code LogHandler} that an operation is about to start on the given
resource.
+     * Call to this method must be followed by call to {@link #loadingStop(Long)} in a {@code
finally} block.
+     *
+     * @param  source  the resource on which an operation is about to start in current thread.
+     * @return key to use in call to {@link #loadingStop(Long)} when the operation is finished.
+     */
+    public static Long loadingStart(final Resource source) {
+        final Long id = Thread.currentThread().getId();
+        INSTANCE.inProgress.put(id, INSTANCE.getResourceRecords(source));
+        return id;
+    }
+
+    /**
+     * Notifies this {@code LogHandler} than an operation done on a resource is finished,
either successfully or
+     * with an exception thrown. Must be invoked in a {@code finally} block after {@link
#loadingStart(Resource)}.
+     *
+     * @param  id  the value returned by {@link #loadingStart(Resource)}.
+     */
+    public static void loadingStop(final Long id) {
+        INSTANCE.inProgress.remove(id);
+    }
+
+    /**
+     * Returns the loggings related to the SIS library as a whole, not specific to any particular
resources.
+     * The returned list shall be read in JavaFX thread only.
+     *
+     * @return loggings related to the SIS library as a whole, not specific to any particular
resources.
+     */
+    @SuppressWarnings("ReturnOfCollectionOrArrayField")
+    public static ObservableList<LogRecord> getSystemRecords() {
+        return INSTANCE.systemLogs;
+    }
+
+    /**
+     * Returns the list of log records for the given resource, or an empty list if the given
source is null.
+     *
+     * @param  source  the resource for which to get the list of log records, or {@code null}.
+     * @return the records for the given resource.
+     */
+    public static ObservableList<LogRecord> getRecords(final Resource source) {
+        return (source != null) ? INSTANCE.getResourceRecords(source) : FXCollections.emptyObservableList();
+    }
+
+    /**
+     * Returns the list of log records for the given resource.
+     *
+     * @param  source  the resource for which to get the list of log records.
+     * @return the records for the given resource.
+     */
+    private ObservableList<LogRecord> getResourceRecords(final Resource source) {
+        synchronized (resourceLogs) {
+            return resourceLogs.computeIfAbsent(source, (k) -> FXCollections.observableArrayList());
+        }
+    }
+
+    /**
+     * Invoked when a {@link DataStore} emitted a warning. This method adds the warning to
the list
+     * of log records specific to that resource. The record is not added to the global (system)
list.
+     *
+     * @param  event  the warning event.
+     */
+    @Override
+    public void eventOccured(final WarningEvent event) {
+        final LogRecord log = event.getDescription();
+        if (isLoggable(log)) {
+            final ObservableList<LogRecord> records = getResourceRecords(event.getSource());
+            if (Platform.isFxApplicationThread()) {
+                records.add(log);
+            } else {
+                Platform.runLater(() -> records.add(log));
+            }
+        }
+    }
+
+    /**
+     * Invoked when a log record is published by the {@link java.util.logging} system.
+     * The log is added to the global (system) list, with oldest record potentially discarded.
+     * In addition, if the log has been emitted in a thread monitored by {@link #inProgress},
+     * then the log is also added to resource-specific log list.
+     *
+     * @param  log  the record to publish (may be {@code null}).
+     */
+    @Override
+    public void publish(final LogRecord log) {
+        if (isLoggable(log)) {
+            // TODO: replace by log.getLongThreadId() with JDK16.
+            final Long id = Thread.currentThread().getId();
+            final ObservableList<LogRecord> records = inProgress.get(id);
+            if (Platform.isFxApplicationThread()) {
+                add(log, records);
+            } else {
+                Platform.runLater(() -> add(log, records));
+            }
+        }
+    }
+
+    /**
+     * Adds the given log record to the global (system) list of logs and to the resource-specific
+     * list of logs, if any.
+     *
+     * @param log      the log to add (must be non-null).
+     * @param records  list of resource-specific logs, or {@code null} if none.
+     */
+    private void add(final LogRecord log, final ObservableList<LogRecord> records)
{
+        if (systemLogs.size() >= LIMIT) {
+            systemLogs.remove(0);
+        }
+        systemLogs.add(log);
+        if (records != null) {
+            records.add(log);
+        }
+    }
+
+    /**
+     * No operation.
+     */
+    @Override
+    public void flush() {
+    }
+
+    /**
+     * Release resources. It is still possible to use this {@code LogHandler} after this
method call,
+     * but according {@link Handler#close()} documentation it should not be allowed.
+     */
+    @Override
+    public void close() {
+        synchronized (resourceLogs) {
+            resourceLogs.clear();
+        }
+        inProgress.clear();
+        // Do not clear `systemLogs` because it would need to be done in JavaFX thread.
+    }
+}
diff --git a/core/sis-utility/src/main/java/org/apache/sis/util/resources/Vocabulary.java
b/core/sis-utility/src/main/java/org/apache/sis/util/resources/Vocabulary.java
index 1c4c409..6a5b684 100644
--- a/core/sis-utility/src/main/java/org/apache/sis/util/resources/Vocabulary.java
+++ b/core/sis-utility/src/main/java/org/apache/sis/util/resources/Vocabulary.java
@@ -190,6 +190,11 @@ public final class Vocabulary extends IndexedResourceBundle {
         public static final short Characteristics = 26;
 
         /**
+         * Class
+         */
+        public static final short Class = 240;
+
+        /**
          * Classpath
          */
         public static final short Classpath = 27;
@@ -330,6 +335,11 @@ public final class Vocabulary extends IndexedResourceBundle {
         public static final short Date = 52;
 
         /**
+         * Date and time
+         */
+        public static final short DateAndTime = 243;
+
+        /**
          * Datum
          */
         public static final short Datum = 53;
@@ -685,11 +695,21 @@ public final class Vocabulary extends IndexedResourceBundle {
         public static final short LocationType = 120;
 
         /**
+         * Logger
+         */
+        public static final short Logger = 241;
+
+        /**
          * Logging
          */
         public static final short Logging = 121;
 
         /**
+         * Logs
+         */
+        public static final short Logs = 244;
+
+        /**
          * Longitude
          */
         public static final short Longitude = 122;
@@ -735,11 +755,21 @@ public final class Vocabulary extends IndexedResourceBundle {
         public static final short Measures = 130;
 
         /**
+         * Message
+         */
+        public static final short Message = 239;
+
+        /**
          * Metadata
          */
         public static final short Metadata = 131;
 
         /**
+         * Method
+         */
+        public static final short Method = 242;
+
+        /**
          * Methods
          */
         public static final short Methods = 132;
diff --git a/core/sis-utility/src/main/java/org/apache/sis/util/resources/Vocabulary.properties
b/core/sis-utility/src/main/java/org/apache/sis/util/resources/Vocabulary.properties
index be15b03..5bf1e4c 100644
--- a/core/sis-utility/src/main/java/org/apache/sis/util/resources/Vocabulary.properties
+++ b/core/sis-utility/src/main/java/org/apache/sis/util/resources/Vocabulary.properties
@@ -40,6 +40,7 @@ CellCount_1             = {0} cells
 CellGeometry            = Cell geometry
 CharacterEncoding       = Character encoding
 Characteristics         = Characteristics
+Class                   = Class
 Classpath               = Classpath
 Code                    = Code
 Code_1                  = {0} code
@@ -69,6 +70,7 @@ DataDirectory           = Data directory
 DataFormats             = Data formats
 DataType                = Data type
 Date                    = Date
+DateAndTime             = Date and time
 Datum                   = Datum
 DatumShift              = Datum shift
 DaylightTime            = Daylight time
@@ -141,6 +143,8 @@ LocalConfiguration      = Local configuration
 Locale                  = Locale
 Localization            = Localization
 LocationType            = Location type
+Logs                    = Logs
+Logger                  = Logger
 Logging                 = Logging
 LowerBound              = Lower bound
 Magenta                 = Magenta
@@ -150,7 +154,9 @@ Maximum                 = Maximum
 MaximumValue            = Maximum value
 MeanValue               = Mean value
 Measures                = Measures
+Message                 = Message
 Metadata                = Metadata
+Method                  = Method
 Methods                 = Methods
 Minimum                 = Minimum
 MinimumValue            = Minimum value
diff --git a/core/sis-utility/src/main/java/org/apache/sis/util/resources/Vocabulary_fr.properties
b/core/sis-utility/src/main/java/org/apache/sis/util/resources/Vocabulary_fr.properties
index ab11e59..29d24ea 100644
--- a/core/sis-utility/src/main/java/org/apache/sis/util/resources/Vocabulary_fr.properties
+++ b/core/sis-utility/src/main/java/org/apache/sis/util/resources/Vocabulary_fr.properties
@@ -47,6 +47,7 @@ CellCount_1             = {0} cellules
 CellGeometry            = G\u00e9om\u00e9trie des cellules
 CharacterEncoding       = Encodage des caract\u00e8res
 Characteristics         = Caract\u00e9ristiques
+Class                   = Classe
 Classpath               = Chemin de classes
 Code                    = Code
 Code_1                  = Code {0}
@@ -76,6 +77,7 @@ DataDirectory           = R\u00e9pertoire des donn\u00e9es
 DataFormats             = Formats de donn\u00e9es
 DataType                = Type de donn\u00e9es
 Date                    = Date
+DateAndTime             = Date et heure
 Datum                   = R\u00e9f\u00e9rentiel
 DatumShift              = Changement de r\u00e9f\u00e9rentiel
 DaylightTime            = Heure normale
@@ -148,6 +150,8 @@ LocalConfiguration      = Configuration locale
 Locale                  = Locale
 Localization            = R\u00e9gionalisation
 LocationType            = Type de location
+Logs                    = Journal
+Logger                  = Journal
 Logging                 = Journalisation
 LowerBound              = Limite basse
 Magenta                 = Magenta
@@ -157,7 +161,9 @@ Maximum                 = Maximum
 MaximumValue            = Valeur maximale
 MeanValue               = Valeur moyenne
 Measures                = Mesures
+Message                 = Message
 Metadata                = Metadonn\u00e9es
+Method                  = M\u00e9thode
 Methods                 = M\u00e9thodes
 Minimum                 = Minimum
 MinimumValue            = Valeur minimale


Mime
View raw message