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 c6d42d4e86a097fa2d11e669c730cfa3ba4829eb
Author: Martin Desruisseaux <martin.desruisse...@geomatys.com>
AuthorDate: Thu Jan 13 07:11:53 2022 +0100

    Provide a way to select the filter log messages by logger.
---
 application/sis-javafx/pom.xml                     |   4 +-
 .../java/org/apache/sis/gui/dataset/LogViewer.java | 158 ++++++++++++++----
 .../apache/sis/gui/dataset/ResourceExplorer.java   |   2 +-
 .../org/apache/sis/internal/gui/GUIUtilities.java  |  64 ++++++++
 .../org/apache/sis/internal/gui/LogHandler.java    | 177 ++++++++++++++++-----
 .../org/apache/sis/internal/gui/Resources.java     |   5 +
 .../apache/sis/internal/gui/Resources.properties   |   1 +
 .../sis/internal/gui/Resources_fr.properties       |   1 +
 .../apache/sis/internal/gui/GUIUtilitiesTest.java  |  81 +++++++++-
 .../apache/sis/util/logging/PerformanceLevel.java  |  25 ++-
 .../org/apache/sis/util/resources/Vocabulary.java  |  10 ++
 .../sis/util/resources/Vocabulary.properties       |   2 +
 .../sis/util/resources/Vocabulary_fr.properties    |   2 +
 13 files changed, 456 insertions(+), 76 deletions(-)

diff --git a/application/sis-javafx/pom.xml b/application/sis-javafx/pom.xml
index 44ad128..c928a37 100644
--- a/application/sis-javafx/pom.xml
+++ b/application/sis-javafx/pom.xml
@@ -92,13 +92,13 @@
       <plugin>
         <artifactId>maven-compiler-plugin</artifactId>
         <configuration>
-          <release>11</release>     <!-- Minimal version required by JavaFX. 
-->
+          <release>16</release>     <!-- Minimal version required by JavaFX. 
-->
         </configuration>
       </plugin>
       <plugin>
         <artifactId>maven-javadoc-plugin</artifactId>
         <configuration>
-          <release>11</release>
+          <release>16</release>
         </configuration>
       </plugin>
 
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
index 7fa4769..b0fcaf7 100644
--- 
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
@@ -21,6 +21,8 @@ import java.util.Date;
 import java.util.Locale;
 import java.util.Map;
 import java.util.HashMap;
+import java.util.ArrayList;
+import java.util.StringJoiner;
 import java.util.function.Predicate;
 import java.util.logging.Level;
 import java.util.logging.LogRecord;
@@ -37,10 +39,16 @@ import javafx.scene.layout.Priority;
 import javafx.scene.control.Label;
 import javafx.scene.control.TextArea;
 import javafx.scene.control.ChoiceBox;
+import javafx.scene.control.TreeView;
+import javafx.scene.control.TreeItem;
 import javafx.scene.control.TableView;
 import javafx.scene.control.TableColumn;
 import javafx.scene.control.TableColumn.CellDataFeatures;
 import javafx.scene.control.TitledPane;
+import javafx.scene.control.Dialog;
+import javafx.scene.control.DialogPane;
+import javafx.scene.control.Button;
+import javafx.scene.control.ButtonType;
 import javafx.scene.control.ToggleButton;
 import javafx.scene.control.ToggleGroup;
 import javafx.beans.property.ObjectProperty;
@@ -63,6 +71,7 @@ import org.apache.sis.internal.gui.Styles;
 import org.apache.sis.internal.gui.LogHandler;
 import org.apache.sis.internal.gui.ExceptionReporter;
 import org.apache.sis.internal.gui.ImmutableObjectProperty;
+import org.apache.sis.internal.gui.Resources;
 import org.apache.sis.util.logging.PerformanceLevel;
 import org.apache.sis.util.CharSequences;
 
@@ -92,15 +101,17 @@ public class LogViewer extends Widget {
     private static final int SPACE = 6;
 
     /**
-     * Space between {@link #message} and the log record identification
-     * (the lines ending with {@link #method}).
+     * Space between the (label, button) pairs on the filter bar.
+     *
+     * @see #filteredLevel
+     * @see #filteredLogger
      */
-    private static final Insets MARGIN = new Insets(SPACE, 0, 0, 0);
+    private static final Insets FILTER_MARGIN = new Insets(0, 0, 0, SPACE*4);
 
     /**
-     * Space around the button bar.
+     * Space around the buttons on the filter bar.
      */
-    private static final Insets BAR_INSETS = new Insets(SPACE);
+    private static final Insets BUTTON_MARGIN = new Insets(SPACE);
 
     /**
      * Localized string representations of {@link Level}.
@@ -111,6 +122,16 @@ public class LogViewer extends Widget {
     private static final Map<Level,String> LEVEL_NAMES = new HashMap<>(12);
 
     /**
+     * The current minimal level of log to show, or {@link Level#ALL} if no 
filtering.
+     */
+    private Level filteredLevel = Level.FINE;
+
+    /**
+     * The current prefix of loggers to show, or an empty string if no 
filtering.
+     */
+    private String filteredLogger = "";
+
+    /**
      * The table of log records.
      */
     private final TableView<LogRecord> table;
@@ -123,6 +144,11 @@ public class LogViewer extends Widget {
     private final VBox view;
 
     /**
+     * Space between the region containing {@link #level} … {@link #method} 
and the {@link #message}.
+     */
+    private static final Insets DETAILS_MARGIN = new Insets(SPACE, 0, 0, 0);
+
+    /**
      * Details about selected record.
      */
     private final Label level, time, logger, classe, method;
@@ -156,6 +182,13 @@ public class LogViewer extends Widget {
     private final IsEmpty isEmpty;
 
     /**
+     * The source of the list of logs. This is determined by {@link #source} 
or {@link #systemLogs}.
+     *
+     * @see #setItems(LogHandler.Destination)
+     */
+    private LogHandler.Destination sourceOfLogs;
+
+    /**
      * Whether {@link #source} is modified in reaction to a {@link 
#systemLogs} change, or conversely.
      */
     private boolean isAdjusting;
@@ -239,8 +272,8 @@ public class LogViewer extends Widget {
             message.setMinHeight(100);
             GridPane.setConstraints(textSelector, 0, 5);
             GridPane.setConstraints(message, 1, 5);
-            GridPane.setMargin(textSelector, MARGIN);
-            GridPane.setMargin(message, MARGIN);
+            GridPane.setMargin(textSelector, DETAILS_MARGIN);
+            GridPane.setMargin(message, DETAILS_MARGIN);
             details.getChildren().addAll(textSelector, message);
             details.setVgap(0);
         }
@@ -249,19 +282,27 @@ public class LogViewer extends Widget {
          */
         final HBox bar;
         {
-            final Label label = new 
Label(vocabulary.getLabel(Vocabulary.Keys.Level));
+            final Label levelLabel = new 
Label(vocabulary.getLabel(Vocabulary.Keys.Level));
             final ChoiceBox<Level> levels = new ChoiceBox<>();
-            label.setLabelFor(levels);
-            bar = new HBox(SPACE, label, levels);
-            bar.setAlignment(Pos.CENTER_LEFT);
-            bar.setPadding(BAR_INSETS);
-            VBox.setVgrow(table, Priority.ALWAYS);
-
+            levelLabel.setLabelFor(levels);
             levels.getItems().setAll(Level.SEVERE, Level.WARNING, Level.INFO, 
Level.CONFIG,
                                      PerformanceLevel.SLOW, Level.FINE, 
Level.FINER, Level.ALL);
             levels.setConverter(Converter.INSTANCE);
-            levels.getSelectionModel().select(Level.ALL);
-            
levels.getSelectionModel().selectedItemProperty().addListener((p,o,n) -> 
setFilter(n));
+            
levels.getSelectionModel().selectedItemProperty().addListener((p,o,n) -> 
setFilter(n, filteredLogger));
+            levels.getSelectionModel().select(filteredLevel);
+
+            final Label loggerLabel = new 
Label(vocabulary.getLabel(Vocabulary.Keys.Logger));
+            final Button loggers = new Button();
+            loggerLabel.setPadding(FILTER_MARGIN);
+            loggerLabel.setLabelFor(loggers);
+            loggers.setMinWidth(160);
+            loggers.setAlignment(Pos.CENTER_LEFT);
+            loggers.setOnAction((e) -> 
loggers.setText(showLoggerTreeDialog()));
+
+            bar = new HBox(SPACE, levelLabel, levels, loggerLabel, loggers);
+            bar.setAlignment(Pos.CENTER_LEFT);
+            bar.setPadding(BUTTON_MARGIN);
+            VBox.setVgrow(table, Priority.ALWAYS);
         }
         /*
          * Put all view components together.
@@ -320,14 +361,21 @@ public class LogViewer extends Widget {
      *
      * @param  records  the new list of records, or {@code null} if none.
      */
-    private void setItems(final ObservableList<LogRecord> records) {
-        if (records == null) {
+    private void setItems(final LogHandler.Destination target) {
+        sourceOfLogs = target;
+        if (target == null) {
             table.setItems(FXCollections.emptyObservableList());
         } else {
-            final boolean e = records.isEmpty();
+            final ObservableList<LogRecord> records = target.records;
             table.setItems(new FilteredList<>(records, filter));
+            final boolean e = records.isEmpty();
             isEmpty.set(e);
             if (e) {
+                /*
+                 * Clear the `isEmpty` flag when the list gets its first log 
record.
+                 * Note that the list will never become empty after that point,
+                 * so we do not need listener for setting the flag to `true`.
+                 */
                 records.addListener(isEmpty);
             }
         }
@@ -503,23 +551,71 @@ public class LogViewer extends Widget {
     }
 
     /**
-     * Sets the filter to the given setting. Currently sets only the logging 
level,
+     * Sets the filter to the given setting. Currently sets only the logging 
level of name,
      * but more configuration may be added in the future.
      *
-     * @param  level  the new level, or {@code null} if unchanged/
+     * @param  level   the new level.
+     * @param  logger  prefix of logger name.
      */
-    private void setFilter(final Level level) {
-        if (level != null) {
-            if (Level.ALL.equals(level)) {
-                filter = null;
-            } else {
-                filter = (log) -> log != null && log.getLevel().intValue() >= 
level.intValue();
+    private void setFilter(final Level level, final String logger) {
+        filteredLevel  = level;
+        filteredLogger = logger;
+        if (Level.ALL.equals(level) && logger.isEmpty()) {
+            filter = null;
+        } else {
+            filter = (log) -> {
+                if (log != null && log.getLevel().intValue() >= 
level.intValue()) {
+                    final String name = log.getLoggerName();
+                    return (name == null) || name.startsWith(logger);
+                }
+                return false;
+            };
+        }
+        final ObservableList<LogRecord> items = table.getItems();
+        if (items instanceof FilteredList<?>) {
+            ((FilteredList<LogRecord>) items).setPredicate(filter);
+        }
+    }
+
+    /**
+     * Shows the dialog box asking to the user to select a logger name.
+     * The loggers are shown in a tree which is dynamically updated as log 
records are received.
+     * The selected logger will be used for filtering the logs.
+     *
+     * <h4>Limitations</h4>
+     * Current implementation allows to select only one logger.
+     * A future version may use a tree table with check-boxes.
+     *
+     * @return a display label for the selected logger.
+     */
+    private String showLoggerTreeDialog() {
+        final var loggers = new TreeView<String>(sourceOfLogs.loggerNames());
+        final var dialog  = new Dialog<String>();
+        dialog.initOwner(view.getScene().getWindow());
+        
dialog.setTitle(Resources.forLocale(getLocale()).getString(Resources.Keys.SelectParentLogger));
+        dialog.setResultConverter((button) -> {
+            if (!ButtonType.OK.equals(button)) {
+                return null;
             }
-            final ObservableList<LogRecord> items = table.getItems();
-            if (items instanceof FilteredList<?>) {
-                ((FilteredList<LogRecord>) items).setPredicate(filter);
+            final var path = new ArrayList<String>();
+            TreeItem<String> root = loggers.getRoot();
+            TreeItem<String> item = 
loggers.getSelectionModel().getSelectedItem();
+            while (item != null && item != root) {
+                path.add(item.getValue());
+                item = item.getParent();
             }
-        }
+            final var joiner = new StringJoiner(".");
+            for (int i = path.size(); --i >= 0;) {
+                joiner.add(path.get(i));
+            }
+            return joiner.toString();
+        });
+        dialog.setResizable(true);
+        final DialogPane pane = dialog.getDialogPane();
+        pane.setContent(loggers);
+        pane.getButtonTypes().setAll(ButtonType.OK, ButtonType.CANCEL);
+        dialog.showAndWait().ifPresent((name) -> setFilter(filteredLevel, 
name));
+        return filteredLogger.substring(filteredLogger.lastIndexOf('.') + 1);
     }
 
     /**
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 8768e41..1628327 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
@@ -630,7 +630,7 @@ public class ResourceExplorer extends WindowManager {
      * @param error     the exception to log.
      */
     private static void warning(final String caller, final Resource resource, 
final Throwable error) {
-        final ObservableList<LogRecord> records = 
LogHandler.getRecords(resource);
+        final LogHandler.Destination records = LogHandler.getRecords(resource);
         if (records != null) {
             final LogRecord record = new LogRecord(Level.WARNING, 
error.getLocalizedMessage());
             record.setSourceClassName(ResourceExplorer.class.getName());
diff --git 
a/application/sis-javafx/src/main/java/org/apache/sis/internal/gui/GUIUtilities.java
 
b/application/sis-javafx/src/main/java/org/apache/sis/internal/gui/GUIUtilities.java
index f8c4bc4..4b5fd4e 100644
--- 
a/application/sis-javafx/src/main/java/org/apache/sis/internal/gui/GUIUtilities.java
+++ 
b/application/sis-javafx/src/main/java/org/apache/sis/internal/gui/GUIUtilities.java
@@ -99,6 +99,70 @@ public final class GUIUtilities extends Static {
     }
 
     /**
+     * Appends a path in a tree where all children lists are sorted. This 
method inserts all parents as needed.
+     * If the leaf at the given path already exists, then this method does 
nothing.
+     *
+     * @param  <T>   type of values in tree nodes.
+     * @param  item  root of the tree where to append a path.
+     * @param  path  path to the leaf to insert, together with all needed 
parents.
+     */
+    @SafeVarargs
+    public static <T extends Comparable<? super T>> void 
appendPathSorted(TreeItem<T> item, final T... path) {
+walk:   for (final T search : path) {
+            final ObservableList<TreeItem<T>> children = item.getChildren();
+            int lo = 0, hi = children.size() - 1;
+            while (lo <= hi) {
+                final int m = (lo + hi) >>> 1;      // Safe against overflow.
+                final TreeItem<T> child = children.get(m);
+                final int c = child.getValue().compareTo(search);
+                if (c < 0) {lo = m + 1; continue;}
+                if (c > 0) {hi = m - 1; continue;}
+                item = child;                       // Found existing item, 
continue down the path.
+                continue walk;
+            }
+            item = new TreeItem<>(search);          // Item not found, insert 
it where it should be.
+            children.add(lo, item);
+        }
+    }
+
+    /**
+     * Removes a path in a tree where all children lists are sorted.
+     * This method prunes all parents that become empty as a result of this 
removal.
+     *
+     * @param  <T>   type of values in tree nodes.
+     * @param  item  root of the tree from where to remove a path.
+     * @param  path  path to the leaf to remove, together with its parents if 
they become empty.
+     */
+    @SafeVarargs
+    public static <T extends Comparable<? super T>> void 
removePathSorted(TreeItem<T> item, final T... path) {
+        ObservableList<TreeItem<T>> removeFrom = null;
+        int removeIndex = 0;
+walk:   for (final T search : path) {
+            final ObservableList<TreeItem<T>> children = item.getChildren();
+            final int sm = children.size() - 1;
+            int hi = sm, lo = 0;
+            while (lo <= hi) {
+                final int m = (lo + hi) >>> 1;
+                final TreeItem<T> child = children.get(m);
+                final int c = child.getValue().compareTo(search);
+                if (c < 0) {lo = m + 1; continue;}
+                if (c > 0) {hi = m - 1; continue;}
+                if (sm != 0 || removeFrom == null) {    // Found item. 
Remember to delete it or its parent.
+                    removeFrom  = children;
+                    removeIndex = m;
+                }
+                item = child;
+                continue walk;
+            }
+            if (sm < 0) break;                          // Item not found. 
Stop the search.
+            else return;
+        }
+        if (removeFrom != null) {
+            removeFrom.remove(removeIndex);
+        }
+    }
+
+    /**
      * Forces a {@link TreeItem} to update the {@code TreeView} when its value 
has been externally modified.
      * This is a workaround for situations where the item's value is 
unchanged, but some state of the value
      * has been modified.
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
index 71c1389..a6e7c2f 100644
--- 
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
@@ -16,6 +16,7 @@
  */
 package org.apache.sis.internal.gui;
 
+import java.util.TreeMap;
 import java.util.WeakHashMap;
 import java.util.concurrent.ConcurrentMap;
 import java.util.concurrent.ConcurrentHashMap;
@@ -25,10 +26,13 @@ import java.util.logging.LogRecord;
 import javafx.application.Platform;
 import javafx.collections.FXCollections;
 import javafx.collections.ObservableList;
+import javafx.scene.control.TreeItem;
 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;
+import org.apache.sis.util.resources.Vocabulary;
+import org.apache.sis.util.CharSequences;
 
 
 /**
@@ -56,14 +60,117 @@ public final class LogHandler extends Handler implements 
StoreListener<WarningEv
      * 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;
+    private final Destination systemLogs;
+
+    /**
+     * Destination where to write log records.
+     */
+    public static final class Destination {
+        /**
+         * The list where to add and remove log records. Logger names shall be 
unmodified.
+         */
+        private final ObservableList<LogRecord> queue;
+
+        /**
+         * The read-only list of log records. Elements in this list shall not 
be modified.
+         * This list shall be read in JavaFX thread only.
+         */
+        public final ObservableList<LogRecord> records;
+
+        /**
+         * Names of all logger in the {@link #queue} list, associated to a 
count of occurrences.
+         * The occurrence count is used for detecting when to remove an entry 
from the map.
+         */
+        private TreeMap<String,Integer> nameCount;
+
+        /**
+         * Root of a tree of logger names. Created when first needed.
+         *
+         * @see #loggerNames()
+         */
+        private TreeItem<String> loggers;
+
+        /**
+         * Creates a new list of records.
+         */
+        Destination() {
+            queue   = FXCollections.observableArrayList();
+            records = FXCollections.unmodifiableObservableList(queue);
+        }
+
+        /**
+         * Returns the components of the logger name, or an empty array if the 
logger name is null.
+         */
+        private static String[] path(final LogRecord record) {
+            return (String[]) CharSequences.split(record.getLoggerName(), '.');
+        }
+
+        /**
+         * Adds the given log record. If the number of records exceeds {@value 
#LIMIT},
+         * then the oldest records are removed. This method shall be invoked 
in JavaFX thread.
+         *
+         * @param  record  the record to add.
+         */
+        public final void add(final LogRecord record) {
+            if (queue.add(record)) {
+                if (nameCount != null) {
+                    updateTree(record);
+                }
+                while (queue.size() > LIMIT) {
+                    final LogRecord first = queue.remove(0);
+                    if (nameCount != null) {
+                        final String name = first.getLoggerName();
+                        if (name != null) {
+                            final Integer remaining = 
nameCount.computeIfPresent(name, (k,o) -> {
+                                final int v = o - 1;
+                                return (v > 0) ? v : null;
+                            });
+                            if (remaining == null) {
+                                GUIUtilities.removePathSorted(loggers, 
path(first));
+                            }
+                        }
+                    }
+                }
+            }
+        }
+
+        /**
+         * Adds the given record to the {@link #nameCount} map,
+         * then update the {@link #loggers} tree if needed.
+         */
+        private void updateTree(final LogRecord record) {
+            final String name = record.getLoggerName();
+            if (name != null) {
+                if (nameCount.merge(name, 1, (o,n) -> o+1) == 1) {
+                    GUIUtilities.appendPathSorted(loggers, path(record));
+                }
+            }
+        }
+
+        /**
+         * Returns the root of a tree of logger names. This method shall be 
invoked in JavaFX thread
+         * and the tree should not be modified by the caller. The tree is 
created when first needed,
+         * then cached. Its content will be updated automatically when log 
records are added or removed.
+         *
+         * @return root of a tree of logger names.
+         */
+        public TreeItem<String> loggerNames() {
+            if (loggers == null) {
+                nameCount = new TreeMap<>();
+                loggers   = new 
TreeItem<>(Vocabulary.format(Vocabulary.Keys.Root));
+                queue.forEach(this::updateTree);
+                loggers.setExpanded(true);
+            }
+            return loggers;
+        }
+    }
 
     /**
      * 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;
+    private final WeakHashMap<Resource, Destination> resourceLogs;
 
     /**
      * The list of log records for which loading are in progress. Keys are 
thread identifiers
@@ -71,19 +178,20 @@ public final class LogHandler extends Handler implements 
StoreListener<WarningEv
      * 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;
+    private final ConcurrentMap<Long, Destination> inProgress;
 
     /**
      * Creates an initially empty collector.
      */
     private LogHandler() {
-        systemLogs   = FXCollections.observableArrayList();
+        systemLogs   = new Destination();
         resourceLogs = new WeakHashMap<>();
         inProgress   = new ConcurrentHashMap<>();
     }
 
     /**
      * Registers or unregisters the unique handler instance on the root logger.
+     * This method should be invoked only at application start and shutdown.
      *
      * @param  enabled  {@code true} for registering or {@code false} for 
unregistering.
      */
@@ -142,7 +250,7 @@ public final class LogHandler extends Handler implements 
StoreListener<WarningEv
      * @return loggings related to the SIS library as a whole, not specific to 
any particular resources.
      */
     @SuppressWarnings("ReturnOfCollectionOrArrayField")
-    public static ObservableList<LogRecord> getSystemRecords() {
+    public static Destination getSystemRecords() {
         return INSTANCE.systemLogs;
     }
 
@@ -152,20 +260,20 @@ public final class LogHandler extends Handler implements 
StoreListener<WarningEv
      * @param  source  the resource for which to get the list of log records, 
or {@code null}.
      * @return the records for the given resource, or {@code null} if the 
given source is null.
      */
-    public static ObservableList<LogRecord> getRecords(final Resource source) {
+    public static Destination getRecords(final Resource source) {
         return (source != null) ? INSTANCE.getRecordsNonNull(source) : null;
     }
 
     /**
      * Returns the list of log records for the given resource.
-     * The given resource shall not be null (the check is done by {@link 
ConcurrentHashMap}).
+     * The given resource shall not be null.
      *
      * @param  source  the resource for which to get the list of log records.
      * @return the records for the given resource.
      */
-    private ObservableList<LogRecord> getRecordsNonNull(final Resource source) 
{
+    private Destination getRecordsNonNull(final Resource source) {
         synchronized (resourceLogs) {
-            return resourceLogs.computeIfAbsent(source, (k) -> 
FXCollections.observableArrayList());
+            return resourceLogs.computeIfAbsent(source, (k) -> new 
Destination());
         }
     }
 
@@ -177,13 +285,16 @@ public final class LogHandler extends Handler implements 
StoreListener<WarningEv
      */
     @Override
     public void eventOccured(final WarningEvent event) {
-        final LogRecord log = event.getDescription();
-        if (isLoggable(log)) {
-            final ObservableList<LogRecord> records = 
getRecordsNonNull(event.getSource());
-            if (Platform.isFxApplicationThread()) {
-                records.add(log);
-            } else {
-                Platform.runLater(() -> records.add(log));
+        final Resource source = event.getSource();
+        if (source != null) {
+            final LogRecord log = event.getDescription();
+            if (isLoggable(log)) {
+                final Destination records = getRecordsNonNull(source);
+                if (Platform.isFxApplicationThread()) {
+                    records.add(log);
+                } else {
+                    Platform.runLater(() -> records.add(log));
+                }
             }
         }
     }
@@ -199,35 +310,25 @@ public final class LogHandler extends Handler implements 
StoreListener<WarningEv
     @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);
+            final Long id = log.getLongThreadID();
+            final Destination records = inProgress.get(id);
             if (Platform.isFxApplicationThread()) {
-                add(log, records);
+                systemLogs.add(log);
+                if (records != null) {
+                    records.add(log);
+                }
             } else {
-                Platform.runLater(() -> add(log, records));
+                Platform.runLater(() -> {
+                    systemLogs.add(log);
+                    if (records != null) {
+                        records.add(log);
+                    }
+                });
             }
         }
     }
 
     /**
-     * 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
diff --git 
a/application/sis-javafx/src/main/java/org/apache/sis/internal/gui/Resources.java
 
b/application/sis-javafx/src/main/java/org/apache/sis/internal/gui/Resources.java
index 6011b18..4bacb51 100644
--- 
a/application/sis-javafx/src/main/java/org/apache/sis/internal/gui/Resources.java
+++ 
b/application/sis-javafx/src/main/java/org/apache/sis/internal/gui/Resources.java
@@ -343,6 +343,11 @@ public final class Resources extends IndexedResourceBundle 
{
         public static final short SelectCrsByContextMenu = 49;
 
         /**
+         * Select parent logger
+         */
+        public static final short SelectParentLogger = 69;
+
+        /**
          * Send to
          */
         public static final short SendTo = 31;
diff --git 
a/application/sis-javafx/src/main/java/org/apache/sis/internal/gui/Resources.properties
 
b/application/sis-javafx/src/main/java/org/apache/sis/internal/gui/Resources.properties
index 9444b62..23f0bce 100644
--- 
a/application/sis-javafx/src/main/java/org/apache/sis/internal/gui/Resources.properties
+++ 
b/application/sis-javafx/src/main/java/org/apache/sis/internal/gui/Resources.properties
@@ -77,6 +77,7 @@ PropertyValue          = Property value
 RangeOfValues          = Range of values\u2026
 SelectCRS              = Select a coordinate reference system
 SelectCrsByContextMenu = For changing the projection, use contextual menu on 
the map.
+SelectParentLogger     = Select parent logger
 SendTo                 = Send to
 SizeOrPosition         = Size or position
 StandardErrorStream    = Standard error stream
diff --git 
a/application/sis-javafx/src/main/java/org/apache/sis/internal/gui/Resources_fr.properties
 
b/application/sis-javafx/src/main/java/org/apache/sis/internal/gui/Resources_fr.properties
index eece6be..a31f6bc 100644
--- 
a/application/sis-javafx/src/main/java/org/apache/sis/internal/gui/Resources_fr.properties
+++ 
b/application/sis-javafx/src/main/java/org/apache/sis/internal/gui/Resources_fr.properties
@@ -82,6 +82,7 @@ PropertyValue          = Valeur de la propri\u00e9t\u00e9
 RangeOfValues          = Plage de valeurs\u2026
 SelectCRS              = Choisir un syst\u00e8me de r\u00e9f\u00e9rence des 
coordonn\u00e9es
 SelectCrsByContextMenu = Pour changer la projection, utilisez le menu 
contextuel sur la carte.
+SelectParentLogger     = Choisir le journal parent
 SendTo                 = Envoyer vers
 SizeOrPosition         = Taille ou position
 StandardErrorStream    = Flux d\u2019erreur standard
diff --git 
a/application/sis-javafx/src/test/java/org/apache/sis/internal/gui/GUIUtilitiesTest.java
 
b/application/sis-javafx/src/test/java/org/apache/sis/internal/gui/GUIUtilitiesTest.java
index 61b4219..a83c56a 100644
--- 
a/application/sis-javafx/src/test/java/org/apache/sis/internal/gui/GUIUtilitiesTest.java
+++ 
b/application/sis-javafx/src/test/java/org/apache/sis/internal/gui/GUIUtilitiesTest.java
@@ -18,8 +18,10 @@ package org.apache.sis.internal.gui;
 
 import java.util.Arrays;
 import java.util.List;
+import javafx.scene.control.TreeItem;
 import javafx.scene.paint.Color;
 import org.apache.sis.test.TestCase;
+import org.apache.sis.test.TestUtilities;
 import org.junit.Test;
 
 import static org.junit.Assert.*;
@@ -29,12 +31,89 @@ import static org.junit.Assert.*;
  * Tests {@link GUIUtilities}.
  *
  * @author  Martin Desruisseaux (Geomatys)
- * @version 1.1
+ * @version 1.2
  * @since   1.1
  * @module
  */
 public final strictfp class GUIUtilitiesTest extends TestCase {
     /**
+     * Tests {@link GUIUtilities#appendPathSorted(TreeItem, Comparable...)}
+     * and   {@link GUIUtilities#removePathSorted(TreeItem, Comparable...)}.
+     */
+    @Test
+    public void testPathSorted() {
+        final TreeItem<Integer> root = new TreeItem<>();
+        GUIUtilities.appendPathSorted(root, 5, 2, 6);
+        GUIUtilities.appendPathSorted(root, 5, 1, 7);
+        GUIUtilities.appendPathSorted(root, 5, 2, 4);
+        GUIUtilities.appendPathSorted(root, 5, 1, 7);       // Should be a 
no-operation.
+        /*
+         * root
+         *  └─5
+         *    ├─1
+         *    │ └─7
+         *    └─2
+         *      ├─4
+         *      └─6
+         */
+        {
+            TreeItem<Integer> item = 
TestUtilities.getSingleton(root.getChildren());
+            assertEquals(Integer.valueOf(5), item.getValue());
+
+            List<TreeItem<Integer>> list = item.getChildren();
+            assertEquals(2, list.size());
+            assertEquals(Integer.valueOf(1), list.get(0).getValue());
+            assertEquals(Integer.valueOf(2), list.get(1).getValue());
+            assertEquals(Integer.valueOf(7), 
TestUtilities.getSingleton(list.get(0).getChildren()).getValue());
+
+            list = list.get(1).getChildren();
+            assertEquals(2, list.size());
+            assertEquals(Integer.valueOf(4), list.get(0).getValue());
+            assertEquals(Integer.valueOf(6), list.get(1).getValue());
+        }
+        GUIUtilities.removePathSorted(root, 5, 2, 7);       // Should be a 
no-operation.
+        GUIUtilities.removePathSorted(root, 5, 1, 7);
+        /*
+         * root
+         *  └─5
+         *    └─2
+         *      ├─4
+         *      └─6
+         */
+        {
+            TreeItem<Integer> item = 
TestUtilities.getSingleton(root.getChildren());
+            assertEquals(Integer.valueOf(5), item.getValue());
+
+            item = TestUtilities.getSingleton(item.getChildren());
+            assertEquals(Integer.valueOf(2), item.getValue());
+
+            List<TreeItem<Integer>> list = item.getChildren();
+            assertEquals(2, list.size());
+            assertEquals(Integer.valueOf(4), list.get(0).getValue());
+            assertEquals(Integer.valueOf(6), list.get(1).getValue());
+        }
+        GUIUtilities.removePathSorted(root, 5, 2, 4);
+        /*
+         * root
+         *  └─5
+         *    └─2
+         *      └─6
+         */
+        {
+            TreeItem<Integer> item = 
TestUtilities.getSingleton(root.getChildren());
+            assertEquals(Integer.valueOf(5), item.getValue());
+
+            item = TestUtilities.getSingleton(item.getChildren());
+            assertEquals(Integer.valueOf(2), item.getValue());
+
+            item = TestUtilities.getSingleton(item.getChildren());
+            assertEquals(Integer.valueOf(6), item.getValue());
+        }
+        GUIUtilities.removePathSorted(root, 5, 2, 6);
+        assertTrue(root.getChildren().isEmpty());
+    }
+
+    /**
      * Tests {@link GUIUtilities#longestCommonSubsequence(List, List)}.
      */
     @Test
diff --git 
a/core/sis-utility/src/main/java/org/apache/sis/util/logging/PerformanceLevel.java
 
b/core/sis-utility/src/main/java/org/apache/sis/util/logging/PerformanceLevel.java
index 4e2de91..015e0ee 100644
--- 
a/core/sis-utility/src/main/java/org/apache/sis/util/logging/PerformanceLevel.java
+++ 
b/core/sis-utility/src/main/java/org/apache/sis/util/logging/PerformanceLevel.java
@@ -21,6 +21,7 @@ import java.util.logging.Logger;
 import java.util.concurrent.TimeUnit;
 import org.apache.sis.util.Configuration;
 import org.apache.sis.util.ArgumentChecks;
+import org.apache.sis.util.resources.Vocabulary;
 
 
 /**
@@ -65,7 +66,7 @@ public final class PerformanceLevel extends Level {
      * time equals or greater than 1 second are logged at this level. However 
this threshold can
      * be changed by a call to <code>SLOW.{@linkplain #setMinDuration(long, 
TimeUnit)}</code>.
      */
-    public static final PerformanceLevel SLOW = new PerformanceLevel("SLOW", 
620, 1000_000_000L);
+    public static final PerformanceLevel SLOW = new PerformanceLevel("SLOW", 
Vocabulary.Keys.Slow, 620, 1000_000_000L);
 
     /**
      * The level for logging only events slower than the ones logged at the 
{@link #SLOW} level.
@@ -73,7 +74,7 @@ public final class PerformanceLevel extends Level {
      * logged at this level. However this threshold can be changed by a call to
      * <code>SLOWER.{@linkplain #setMinDuration(long, TimeUnit)}</code>.
      */
-    public static final PerformanceLevel SLOWER = new 
PerformanceLevel("SLOWER", 630, 10_000_000_000L);
+    public static final PerformanceLevel SLOWER = new 
PerformanceLevel("SLOWER", Vocabulary.Keys.Slower, 630, 10_000_000_000L);
 
     /**
      * The minimal duration (in nanoseconds) for logging the record.
@@ -81,14 +82,20 @@ public final class PerformanceLevel extends Level {
     private volatile long minDuration;
 
     /**
+     * The key for producing a localized name of this level.
+     */
+    private final short localization;
+
+    /**
      * Constructs a new logging level for monitoring performance.
      *
      * @param name      the logging level name.
      * @param value     the level value.
      * @param duration  the minimal duration (in nanoseconds) for logging a 
record.
      */
-    private PerformanceLevel(final String name, final int value, final long 
duration) {
+    private PerformanceLevel(final String name, final short key, final int 
value, final long duration) {
         super(name, value);
+        localization = key;
         minDuration = duration;
     }
 
@@ -150,4 +157,16 @@ public final class PerformanceLevel extends Level {
             }
         }
     }
+
+    /**
+     * Return the name of this level for the current default locale.
+     *
+     * @return name of this level for the current locale.
+     *
+     * @since 1.2
+     */
+    @Override
+    public String getLocalizedName() {
+        return Vocabulary.format(localization);
+    }
 }
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 0f7d994..b43a037 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
@@ -1125,6 +1125,16 @@ public final class Vocabulary extends 
IndexedResourceBundle {
         public static final short SlashSeparatedList_2 = 181;
 
         /**
+         * Slow
+         */
+        public static final short Slow = 267;
+
+        /**
+         * Slower
+         */
+        public static final short Slower = 268;
+
+        /**
          * Source
          */
         public static final short Source = 182;
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 62e15b9..b5cb4e3 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
@@ -228,6 +228,8 @@ SampleDimensions        = Sample dimensions
 Scale                   = Scale
 Simplified              = Simplified
 SlashSeparatedList_2    = {0}/{1}
+Slow                    = Slow
+Slower                  = Slower
 Source                  = Source
 SouthBound              = South bound
 SpatialRepresentation   = Spatial representation
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 8440b29..348a8da 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
@@ -235,6 +235,8 @@ SampleDimensions        = Dimensions 
d\u2019\u00e9chantillonnage
 Scale                   = \u00c9chelle
 Simplified              = Simplifi\u00e9
 SlashSeparatedList_2    = {0}/{1}
+Slow                    = Lent
+Slower                  = Plus lent
 Source                  = Source
 SouthBound              = Limite sud
 SpatialRepresentation   = Repr\u00e9sentation spatiale

Reply via email to