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