sis-commits mailing list archives

Site index · List index
Message view « Date » · « Thread »
Top « Date » · « Thread »
From desruisse...@apache.org
Subject [sis] 02/02: Replace `Consumer<LogRecord>` by a new `ErrorHandler` interface. It allows us to provide more information about failures, in particular a list of tiles that failed.
Date Wed, 03 Feb 2021 22:46:41 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 c40cdfbe2d1d39eac4419cb14ae8cf1134dbbbd6
Author: Martin Desruisseaux <martin.desruisseaux@geomatys.com>
AuthorDate: Wed Feb 3 23:16:47 2021 +0100

    Replace `Consumer<LogRecord>` by a new `ErrorHandler` interface.
    It allows us to provide more information about failures, in particular a list of tiles that failed.
---
 .../org/apache/sis/gui/coverage/RenderingData.java |   3 +-
 .../java/org/apache/sis/image/AnnotatedImage.java  |  59 ++++---
 .../java/org/apache/sis/image/ErrorAction.java     |  80 +++++++++
 .../java/org/apache/sis/image/ErrorHandler.java    | 179 +++++++++++++++++++++
 .../java/org/apache/sis/image/ImageProcessor.java  | 129 +++++----------
 .../java/org/apache/sis/image/PlanarImage.java     |   2 +-
 .../sis/internal/coverage/j2d/TileOpExecutor.java  |  90 +++++------
 .../apache/sis/image/StatisticsCalculatorTest.java |   2 +-
 8 files changed, 377 insertions(+), 167 deletions(-)

diff --git a/application/sis-javafx/src/main/java/org/apache/sis/gui/coverage/RenderingData.java b/application/sis-javafx/src/main/java/org/apache/sis/gui/coverage/RenderingData.java
index c6f768f..2ba2b33 100644
--- a/application/sis-javafx/src/main/java/org/apache/sis/gui/coverage/RenderingData.java
+++ b/application/sis-javafx/src/main/java/org/apache/sis/gui/coverage/RenderingData.java
@@ -45,6 +45,7 @@ import org.apache.sis.coverage.SampleDimension;
 import org.apache.sis.geometry.AbstractEnvelope;
 import org.apache.sis.geometry.Envelope2D;
 import org.apache.sis.geometry.Shapes2D;
+import org.apache.sis.image.ErrorHandler;
 import org.apache.sis.image.ImageProcessor;
 import org.apache.sis.internal.coverage.j2d.ColorModelType;
 import org.apache.sis.internal.coverage.j2d.ImageUtilities;
@@ -206,7 +207,7 @@ final class RenderingData implements Cloneable {
     RenderingData() {
         selectedDerivative = Stretching.NONE;
         processor = new ImageProcessor();
-        processor.setErrorAction(ImageProcessor.ErrorAction.LOG);
+        processor.setErrorHandler(ErrorHandler.LOG);
         processor.setImageResizingPolicy(ImageProcessor.Resizing.EXPAND);
     }
 
diff --git a/core/sis-feature/src/main/java/org/apache/sis/image/AnnotatedImage.java b/core/sis-feature/src/main/java/org/apache/sis/image/AnnotatedImage.java
index c0430c7..7d38278 100644
--- a/core/sis-feature/src/main/java/org/apache/sis/image/AnnotatedImage.java
+++ b/core/sis-feature/src/main/java/org/apache/sis/image/AnnotatedImage.java
@@ -16,12 +16,9 @@
  */
 package org.apache.sis.image;
 
-import java.util.Locale;
 import java.util.Objects;
 import java.util.WeakHashMap;
-import java.util.logging.Level;
 import java.util.logging.LogRecord;
-import java.util.logging.Filter;
 import java.util.stream.Collector;
 import java.awt.Image;
 import java.awt.Shape;
@@ -30,7 +27,6 @@ import java.awt.image.RenderedImage;
 import java.awt.image.Raster;
 import java.awt.image.ImagingOpException;
 import org.apache.sis.util.ArraysExt;
-import org.apache.sis.util.logging.Logging;
 import org.apache.sis.util.resources.Errors;
 import org.apache.sis.util.collection.Cache;
 import org.apache.sis.internal.system.Modules;
@@ -170,7 +166,7 @@ abstract class AnnotatedImage extends ImageAdapter {
      * The errors that occurred while computing the result, or {@code null} if none or not yet determined.
      * This field is never set if {@link #failOnException} is {@code true}.
      */
-    private volatile LogRecord errors;
+    private volatile ErrorHandler.Report errors;
 
     /**
      * Whether parallel execution is authorized for the {@linkplain #source} image.
@@ -287,9 +283,10 @@ abstract class AnnotatedImage extends ImageAdapter {
      */
     @Override
     public String[] getPropertyNames() {
-        final String[] names = new String[(errors != null) ? 2 : 1];
+        final boolean hasErrors = (errors != null);
+        final String[] names = new String[hasErrors ? 2 : 1];
         names[0] = getComputedPropertyName();
-        if (errors != null) {
+        if (hasErrors) {
             names[1] = names[0] + WARNINGS_SUFFIX;
         }
         return ArraysExt.concatenate(source.getPropertyNames(), names);
@@ -354,14 +351,13 @@ abstract class AnnotatedImage extends ImageAdapter {
                             Errors.format(Errors.Keys.CanNotCompute_1, property)).initCause(e);
                 }
                 synchronized (this) {
-                    LogRecord record = errors;
-                    if (record != null) {
-                        record.getThrown().addSuppressed(e);
-                    } else {
-                        record = Errors.getResources((Locale) null).getLogRecord(Level.WARNING, Errors.Keys.CanNotCompute_1, property);
-                        record.setThrown(e);
-                        setError(record);
+                    ErrorHandler.Report report = errors;
+                    final boolean create = (report == null);
+                    if (create) {
+                        report = new ErrorHandler.Report();
                     }
+                    report.addPropertyError(e, property);
+                    if (create) setError(report);
                 }
             }
         } else if (isErrorProperty(property, name)) {
@@ -377,23 +373,18 @@ abstract class AnnotatedImage extends ImageAdapter {
      * Can also be invoked by {@link #getProperty(String)} directly if the error occurred
      * outside {@link TileOpExecutor}. This method shall be invoked at most once.
      *
-     * @param  record  a description of the error that occurred.
+     * @param  report  a description of the error that occurred.
      */
-    private void setError(final LogRecord record) {
+    private void setError(final ErrorHandler.Report report) {
         /*
          * Complete record with source identification as if the error occurred from
          * above `getProperty(String)` method (this is always the case, indirectly).
          */
+        final LogRecord record = report.getDescription();
         record.setSourceClassName(AnnotatedImage.class.getCanonicalName());
         record.setSourceMethodName("getProperty");
         record.setLoggerName(Modules.RASTER);
-        synchronized (this) {
-            if (errors == null) {
-                errors = record;
-            } else {
-                throw new IllegalStateException();      // If it happens, this is a bug in thie AnnotatedImage class.
-            }
-        }
+        errors = report;
     }
 
     /**
@@ -402,17 +393,20 @@ abstract class AnnotatedImage extends ImageAdapter {
      *
      * @param  classe   the class to report as the source of the logging message.
      * @param  method   the method to report as the source of the logging message.
-     * @param  handler  where to send the log message, or {@code null} for the standard logger.
+     * @param  handler  where to send the log message.
      */
-    final void logAndClearError(final Class<?> classe, final String method, final Filter handler) {
-        final LogRecord record;
+    final void logAndClearError(final Class<?> classe, final String method, final ErrorHandler handler) {
+        final ErrorHandler.Report report;
         synchronized (this) {
-            record = errors;
+            report = errors;
             errors = null;
         }
-        if (record != null) {
-            if (handler == null || handler.isLoggable(record)) {
-                Logging.log(classe, method, record);
+        if (report != null) {
+            final LogRecord record = report.getDescription();
+            if (record != null) {
+                record.setSourceClassName(classe.getCanonicalName());
+                record.setSourceMethodName(method);
+                handler.handle(report);
             }
         }
     }
@@ -437,10 +431,13 @@ abstract class AnnotatedImage extends ImageAdapter {
     protected Object computeProperty() throws Exception {
         if (parallel) {
             final TileOpExecutor executor = new TileOpExecutor(source, boundsOfInterest);
+            if (!failOnException) {
+                executor.setErrorHandler(this::setError);
+            }
             if (executor.isMultiTiled()) {
                 final Collector<? super Raster,?,?> collector = collector();
                 if (collector != null) {
-                    return executor.executeOnReadable(source, collector(), failOnException ? null : this::setError);
+                    return executor.executeOnReadable(source, collector());
                 }
             }
         }
diff --git a/core/sis-feature/src/main/java/org/apache/sis/image/ErrorAction.java b/core/sis-feature/src/main/java/org/apache/sis/image/ErrorAction.java
new file mode 100644
index 0000000..b2abedd
--- /dev/null
+++ b/core/sis-feature/src/main/java/org/apache/sis/image/ErrorAction.java
@@ -0,0 +1,80 @@
+/*
+ * 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.image;
+
+import java.util.logging.LogRecord;
+import java.util.logging.SimpleFormatter;
+import java.awt.image.ImagingOpException;
+import org.apache.sis.util.logging.Logging;
+import org.apache.sis.internal.system.Modules;
+
+
+/**
+ * Some common ways to handle exceptions occurring during tile calculation.
+ * This class provides the implementations of {@link ErrorHandler} static constants.
+ *
+ * @author  Martin Desruisseaux (Geomatys)
+ * @version 1.1
+ * @since   1.1
+ * @module
+ */
+enum ErrorAction implements ErrorHandler {
+    /**
+     * Exceptions are wrapped in an {@link ImagingOpException} and thrown.
+     * In such case, no result is available. This is the default action.
+     */
+    THROW,
+
+    /**
+     * Exceptions are wrapped in a {@link LogRecord} and logged at {@link java.util.logging.Level#WARNING}.
+     * Only one log record is created for all tiles that failed for the same operation on the same image.
+     * A partial result may be available.
+     *
+     * <p>Users are encouraged to use {@link #THROW} or to specify their own {@link ErrorHandler}
+     * instead than using this error action, because not everyone read logging records.</p>
+     */
+    LOG;
+
+    /**
+     * Logs the given record or throws its exception, depending on {@code this} enumeration value.
+     * This method is implemented as a matter of principle but not invoked for {@code ErrorAction}
+     * enumeration values.
+     */
+    @Override
+    public void handle(final Report details) {
+        final LogRecord record = details.getDescription();
+        if (record != null) {
+            if (this == LOG) {
+                String logger = record.getLoggerName();
+                if (logger == null) {
+                    logger = Modules.RASTER;
+                }
+                Logging.getLogger(logger).log(record);
+            } else {
+                final Throwable ex = record.getThrown();
+                if (ex instanceof Error) {
+                    throw (Error) ex;
+                } else if (ex instanceof ImagingOpException) {
+                    throw (ImagingOpException) ex;
+                } else {
+                    final String message = new SimpleFormatter().formatMessage(record);
+                    throw (ImagingOpException) new ImagingOpException(message).initCause(ex);
+                }
+            }
+        }
+    }
+}
diff --git a/core/sis-feature/src/main/java/org/apache/sis/image/ErrorHandler.java b/core/sis-feature/src/main/java/org/apache/sis/image/ErrorHandler.java
new file mode 100644
index 0000000..ba20674
--- /dev/null
+++ b/core/sis-feature/src/main/java/org/apache/sis/image/ErrorHandler.java
@@ -0,0 +1,179 @@
+/*
+ * 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.image;
+
+import java.awt.Point;
+import java.util.Arrays;
+import java.util.Locale;
+import java.util.logging.Level;
+import java.util.logging.LogRecord;
+import java.awt.image.ImagingOpException;
+import org.apache.sis.util.ArgumentChecks;
+import org.apache.sis.util.resources.Errors;
+import org.apache.sis.internal.feature.Resources;
+
+
+/**
+ * Action to perform when errors occurred while reading or writing some tiles in an image.
+ * The most typical actions are {@linkplain #THROW throwing an exception} or {@linkplain #LOG logging a warning}.
+ *
+ * @author  Martin Desruisseaux (Geomatys)
+ * @version 1.1
+ * @since   1.1
+ * @module
+ */
+public interface ErrorHandler {
+    /**
+     * Exceptions are wrapped in an {@link ImagingOpException} and thrown.
+     * In such case, no result is available. This is the default handler.
+     */
+    ErrorHandler THROW = ErrorAction.THROW;
+
+    /**
+     * Exceptions are wrapped in a {@link LogRecord} and logged at {@link java.util.logging.Level#WARNING}.
+     * Only one log record is created for all tiles that failed for the same operation on the same image.
+     * A partial result may be available.
+     *
+     * <p>Users are encouraged to use {@link #THROW} or to specify their own {@link ErrorHandler}
+     * instead than using this error action, because not everyone read logging records.</p>
+     */
+    ErrorHandler LOG = ErrorAction.LOG;
+
+    /**
+     * Invoked after errors occurred in one or many tiles. This method may be invoked an arbitrary
+     * time after the error occurred, and may aggregate errors that occurred in more than one tile.
+     *
+     * @param  details  information about errors.
+     */
+    void handle(Report details);
+
+    /**
+     * Information about errors that occurred while reading or writing tiles in an image.
+     * A single {@code Report} may be generated for failures in more than one tiles.
+     */
+    class Report {
+        /**
+         * The tile indices as (x,y) tuples where errors occurred, or {@code null} if none.
+         */
+        private int[] indices;
+
+        /**
+         * Number of valid elements in {@link #indices}.
+         */
+        private int length;
+
+        /**
+         * Description of the error that occurred, or {@code null} if none.
+         */
+        private LogRecord description;
+
+        /**
+         * Creates an initially empty report.
+         * Error reports can be added by calls to {@code add(Throwable, …)} methods.
+         */
+        public Report() {
+        }
+
+        /**
+         * Returns {@code true} if no error has been reported.
+         * This is true only if no {@code add(Throwable, …)} method had been invoked.
+         *
+         * @return whether this report is empty.
+         */
+        public boolean isEmpty() {
+            return length == 0 && description == null;
+        }
+
+        /**
+         * Reports an error that occurred while computing an image property.
+         * This method can be invoked many times on the same {@code Report} instance.
+         *
+         * @param error     the error that occurred.
+         * @param property  name of the property which was computed, or {@code null} if none.
+         */
+        public void addPropertyError(final Throwable error, final String property) {
+            ArgumentChecks.ensureNonNull("error", error);
+            if (description == null) {
+                if (property != null) {
+                    description = Errors.getResources((Locale) null)
+                            .getLogRecord(Level.WARNING, Errors.Keys.CanNotCompute_1, property);
+                } else {
+                    description = new LogRecord(Level.WARNING, error.toString());
+                }
+                description.setThrown(error);
+            } else {
+                description.getThrown().addSuppressed(error);
+            }
+        }
+
+        /**
+         * Reports an error that occurred while computing an image tile.
+         * This method can be invoked many times on the same {@code Report} instance.
+         *
+         * @param error  the error that occurred.
+         * @param tx     column index of the tile where the error occurred.
+         * @param ty     row index of the tile where the error occurred.
+         */
+        public void addTileError(final Throwable error, final int tx, final int ty) {
+            ArgumentChecks.ensureNonNull("error", error);
+            if (indices == null) {
+                indices = new int[8];
+            } else if (length >= indices.length) {
+                indices = Arrays.copyOf(indices, indices.length * 2);
+            }
+            indices[length++] = tx;
+            indices[length++] = ty;
+            if (description == null) {
+                description = Resources.forLocale(null)
+                        .getLogRecord(Level.WARNING, Resources.Keys.CanNotProcessTile_2, tx, ty);
+                description.setThrown(error);
+            } else {
+                description.getThrown().addSuppressed(error);
+            }
+        }
+
+        /**
+         * Returns indices of all tiles where an error has been reported.
+         *
+         * @return indices of all tiles in error, or an empty array if none.
+         */
+        public Point[] getTileIndices() {
+            final Point[] p = new Point[length >>> 1];
+            for (int i=0; i<length;) {
+                p[i >>> 1] = new Point(indices[i++], indices[i++]);
+            }
+            return p;
+        }
+
+        /**
+         * Returns a description of errors as a log record. The exception can be obtained by
+         * {@link LogRecord#getThrown()}. In addition the {@code LogRecord} has the
+         * {@linkplain LogRecord#getLevel() level} and
+         * {@linkplain LogRecord#getMessage() message} properties set. But the
+         * {@linkplain LogRecord#getSourceClassName() source class name},
+         * {@linkplain LogRecord#getSourceMethodName() source method name} and
+         * {@linkplain LogRecord#getLoggerName() logger name} may be undefined;
+         * they should be set by the {@link ErrorHandler}.
+         * The return value is never null unless this report {@linkplain #isEmpty() is empty}.
+         *
+         * @return errors description, or {@code null} if this report is empty.
+         */
+        public LogRecord getDescription() {
+            return description;
+        }
+    }
+}
diff --git a/core/sis-feature/src/main/java/org/apache/sis/image/ImageProcessor.java b/core/sis-feature/src/main/java/org/apache/sis/image/ImageProcessor.java
index ac69507..b786f97 100644
--- a/core/sis-feature/src/main/java/org/apache/sis/image/ImageProcessor.java
+++ b/core/sis-feature/src/main/java/org/apache/sis/image/ImageProcessor.java
@@ -21,7 +21,6 @@ import java.util.List;
 import java.util.Arrays;
 import java.util.Objects;
 import java.util.function.Function;
-import java.util.logging.Filter;
 import java.util.logging.LogRecord;
 import java.awt.Color;
 import java.awt.Shape;
@@ -107,11 +106,11 @@ import org.apache.sis.measure.Units;
  *
  * <h2>Error handling</h2>
  * If an exception occurs during the computation of a tile, then the {@code ImageProcessor} behavior
- * is controlled by the {@link #getErrorAction() errorAction} property:
+ * is controlled by the {@link #getErrorHandler() errorHandler} property:
  *
  * <ul>
- *   <li>If {@link ErrorAction#THROW}, the exception is wrapped in an {@link ImagingOpException} and thrown.</li>
- *   <li>If {@link ErrorAction#LOG}, the exception is logged and a partial result is returned.</li>
+ *   <li>If {@link ErrorHandler#THROW}, the exception is wrapped in an {@link ImagingOpException} and thrown.</li>
+ *   <li>If {@link ErrorHandler#LOG}, the exception is logged and a partial result is returned.</li>
  *   <li>If any other value, the exception is wrapped in a {@link LogRecord} and sent to that filter.
  *     The filter can store the log record, for example for showing later in a graphical user interface (GUI).
  *     If the filter returns {@code true}, the log record is also logged, otherwise it is silently discarded.
@@ -265,58 +264,20 @@ public class ImageProcessor implements Cloneable {
      * If errors are wrapped in a {@link LogRecord}, this field specifies what to do with the record.
      * Only one log record is created for all tiles that failed for the same operation on the same image.
      *
-     * @see #getErrorAction()
-     * @see #setErrorAction(Filter)
+     * @see #getErrorHandler()
+     * @see #setErrorHandler(ErrorHandler)
      */
-    private Filter errorAction;
-
-    /**
-     * Specifies how exceptions occurring during calculation should be handled.
-     * This enumeration provides common actions, but the set of values that can
-     * be specified to {@link #setErrorAction(Filter)} is not limited to this enumeration.
-     *
-     * @see #getErrorAction()
-     * @see #setErrorAction(Filter)
-     */
-    public enum ErrorAction implements Filter {
-        /**
-         * Exceptions are wrapped in an {@link ImagingOpException} and thrown.
-         * In such case, no result is available. This is the default action.
-         */
-        THROW,
-
-        /**
-         * Exceptions are wrapped in a {@link LogRecord} and logged at {@link java.util.logging.Level#WARNING}.
-         * Only one log record is created for all tiles that failed for the same operation on the same image.
-         * A partial result may be available.
-         *
-         * <p>Users are encouraged to use {@link #THROW} or to specify their own {@link Filter}
-         * instead than using this error action, because not everyone read logging records.</p>
-         */
-        LOG;
-
-        /**
-         * Unconditionally returns {@code true} for allowing the given record to be logged.
-         * This method is not useful for this {@code ErrorAction} enumeration, but is useful
-         * for other instances given to {@link #setErrorAction(Filter)}.
-         *
-         * @param  record  the error that occurred during computation of a tile.
-         * @return always {@code true}.
-         */
-        @Override
-        public boolean isLoggable(final LogRecord record) {
-            return true;
-        }
-    }
+    private ErrorHandler errorHandler;
 
     /**
      * Creates a new processor with default configuration.
-     * The execution mode is initialized to {@link Mode#DEFAULT} and the error action to {@link ErrorAction#THROW}.
+     * The execution mode is initialized to {@link Mode#DEFAULT}
+     * and the error handler to {@link ErrorHandler#THROW}.
      */
     public ImageProcessor() {
         layout        = ImageLayout.DEFAULT;
         executionMode = Mode.DEFAULT;
-        errorAction   = ErrorAction.THROW;
+        errorHandler  = ErrorHandler.THROW;
         interpolation = Interpolation.BILINEAR;
     }
 
@@ -523,28 +484,33 @@ public class ImageProcessor implements Cloneable {
 
     /**
      * Returns whether exceptions occurring during computation are propagated or logged.
-     * If {@link ErrorAction#THROW} (the default), exceptions are wrapped in {@link ImagingOpException} and thrown.
-     * If any other value, exceptions are wrapped in a {@link LogRecord}, filtered then eventually logged.
+     * If {@link ErrorHandler#THROW} (the default), exceptions are wrapped in {@link ImagingOpException} and thrown.
+     * If {@link ErrorHandler#LOG}, exceptions are wrapped in a {@link LogRecord}, filtered then eventually logged.
      *
      * @return whether exceptions occurring during computation are propagated or logged.
      */
-    public synchronized Filter getErrorAction() {
-        return errorAction;
+    public synchronized ErrorHandler getErrorHandler() {
+        return errorHandler;
     }
 
     /**
      * Sets whether exceptions occurring during computation are propagated or logged.
      * The default behavior is to wrap exceptions in {@link ImagingOpException} and throw them.
-     * If this property is set to {@link ErrorAction#LOG} or any other value, then exceptions will
-     * be wrapped in {@link LogRecord} instead, in which case a partial result may be available.
+     * If this property is set to {@link ErrorHandler#LOG}, then exceptions will be wrapped in
+     * {@link LogRecord} instead, in which case a partial result may be available.
      * Only one log record is created for all tiles that failed for the same operation on the same image.
      *
-     * @param  action  filter to notify when an operation failed on one or more tiles,
-     *                 or {@link ErrorAction#THROW} for propagating the exception.
+     * <h4>Limitations</h4>
+     * In current {@code ImageProcessor} implementation, the error handler is not honored by all operations.
+     * Some operations may continue to throw an exception on failure (the behavior of default error handler)
+     * even if a different handler has been specified.
+     *
+     * @param  handler  handler to notify when an operation failed on one or more tiles,
+     *                  or {@link ErrorHandler#THROW} for propagating the exception.
      */
-    public synchronized void setErrorAction(final Filter action) {
-        ArgumentChecks.ensureNonNull("action", action);
-        errorAction = action;
+    public synchronized void setErrorHandler(final ErrorHandler handler) {
+        ArgumentChecks.ensureNonNull("handler", handler);
+        errorHandler = handler;
     }
 
     /**
@@ -553,25 +519,14 @@ public class ImageProcessor implements Cloneable {
      */
     private boolean failOnException() {
         assert Thread.holdsLock(this);
-        return errorAction == ErrorAction.THROW;
-    }
-
-    /**
-     * Where to send exceptions (wrapped in {@link LogRecord}) if an operation failed on one or more tiles.
-     * Only one log record is created for all tiles that failed for the same operation on the same image.
-     * This is always {@code null} if {@link #failOnException()} is {@code true}.
-     * This method shall be invoked in a method synchronized on {@code this}.
-     */
-    private Filter errorListener() {
-        assert Thread.holdsLock(this);
-        return (errorAction instanceof ErrorAction) ? null : errorAction;
+        return errorHandler == ErrorHandler.THROW;
     }
 
     /**
      * Returns statistics (minimum, maximum, mean, standard deviation) on each bands of the given image.
      * Invoking this method is equivalent to invoking {@link #statistics(RenderedImage, Shape)} and
      * extracting immediately the statistics property value, except that errors are handled by the
-     * {@linkplain #getErrorAction() error handler}.
+     * {@linkplain #getErrorHandler() error handler}.
      *
      * <p>If {@code areaOfInterest} is {@code null}, then the default is as below:</p>
      * <ul>
@@ -585,14 +540,14 @@ public class ImageProcessor implements Cloneable {
      * This operation uses the following properties in addition to method parameters:
      * <ul>
      *   <li>{@linkplain #getExecutionMode() Execution mode} (parallel or sequential).</li>
-     *   <li>{@linkplain #getErrorAction() Error action} (custom action executed if an exception is thrown).</li>
+     *   <li>{@linkplain #getErrorHandler() Error handler} (custom action executed if an exception is thrown).</li>
      * </ul>
      *
      * @param  source          the image for which to compute statistics.
      * @param  areaOfInterest  pixel coordinates of the area of interest, or {@code null} for the default.
      * @return the statistics of sample values in each band.
      * @throws ImagingOpException if an error occurred during calculation
-     *         and the error handler is {@link ErrorAction#THROW}.
+     *         and the error handler is {@link ErrorHandler#THROW}.
      *
      * @see #statistics(RenderedImage, Shape)
      * @see StatisticsCalculator#STATISTICS_KEY
@@ -606,11 +561,11 @@ public class ImageProcessor implements Cloneable {
             }
         }
         final boolean parallel, failOnException;
-        final Filter errorListener;
+        final ErrorHandler errorListener;
         synchronized (this) {
             parallel        = parallel(source);
             failOnException = failOnException();
-            errorListener   = errorListener();
+            errorListener   = errorHandler;
         }
         /*
          * No need to check if the given source is already an instance of StatisticsCalculator.
@@ -641,7 +596,7 @@ public class ImageProcessor implements Cloneable {
      * This operation uses the following properties in addition to method parameters:
      * <ul>
      *   <li>{@linkplain #getExecutionMode() Execution mode} (parallel or sequential).</li>
-     *   <li>{@linkplain #getErrorAction() Error action} (whether to fail if an exception is thrown).</li>
+     *   <li>{@linkplain #getErrorHandler() Error handler} (whether to fail if an exception is thrown).</li>
      * </ul>
      *
      * @param  source          the image for which to provide statistics.
@@ -906,7 +861,7 @@ public class ImageProcessor implements Cloneable {
      * Computations will use many threads if {@linkplain #getExecutionMode() execution mode} is parallel.
      *
      * <div class="note"><b>Note:</b>
-     * current implementation ignores the {@linkplain #getErrorAction() error action} because we do not yet
+     * current implementation ignores the {@linkplain #getErrorHandler() error handler} because we do not yet
      * have a mechanism for specifying which tile to produce in replacement of tiles that can not be computed.
      * This behavior may be changed in a future version.</div>
      *
@@ -1102,26 +1057,26 @@ public class ImageProcessor implements Cloneable {
         if (object != null && object.getClass() == getClass()) {
             final ImageProcessor other = (ImageProcessor) object;
             final Mode          executionMode;
-            final Filter        errorAction;
+            final ErrorHandler  errorHandler;
             final Interpolation interpolation;
             final Number[]      fillValues;
             final Function<Category,Color[]> colors;
             final Quantity<?>[] positionalAccuracyHints;
             synchronized (this) {
                 executionMode           = this.executionMode;
-                errorAction             = this.errorAction;
+                errorHandler            = this.errorHandler;
                 interpolation           = this.interpolation;
                 fillValues              = this.fillValues;
                 colors                  = this.colors;
                 positionalAccuracyHints = this.positionalAccuracyHints;
             }
             synchronized (other) {
-                return errorAction.equals(other.errorAction)     &&
-                     executionMode.equals(other.executionMode)   &&
-                     interpolation.equals(other.interpolation)   &&
-                     Objects.equals(colors, other.colors)        &&
-                     Arrays.equals(fillValues, other.fillValues) &&
-                     Arrays.equals(positionalAccuracyHints, other.positionalAccuracyHints);
+                return errorHandler.equals(other.errorHandler)    &&
+                      executionMode.equals(other.executionMode)   &&
+                      interpolation.equals(other.interpolation)   &&
+                      Objects.equals(colors, other.colors)        &&
+                      Arrays.equals(fillValues, other.fillValues) &&
+                      Arrays.equals(positionalAccuracyHints, other.positionalAccuracyHints);
             }
         }
         return false;
@@ -1134,7 +1089,7 @@ public class ImageProcessor implements Cloneable {
      */
     @Override
     public synchronized int hashCode() {
-        return Objects.hash(getClass(), errorAction, executionMode, interpolation)
+        return Objects.hash(getClass(), errorHandler, executionMode, interpolation)
                 + 37 * Arrays.hashCode(fillValues) + 31 * Objects.hashCode(colors)
                 + 39 * Arrays.hashCode(positionalAccuracyHints);
     }
diff --git a/core/sis-feature/src/main/java/org/apache/sis/image/PlanarImage.java b/core/sis-feature/src/main/java/org/apache/sis/image/PlanarImage.java
index c5483a4..88ed8f8 100644
--- a/core/sis-feature/src/main/java/org/apache/sis/image/PlanarImage.java
+++ b/core/sis-feature/src/main/java/org/apache/sis/image/PlanarImage.java
@@ -541,7 +541,7 @@ public abstract class PlanarImage implements RenderedImage {
         if (sm != null) {
             final ColorModel cm = getColorModel();
             if (cm != null) {
-                if (!cm.isCompatibleSampleModel(sm)) return "SampleModel";
+                if (!cm.isCompatibleSampleModel(sm)) return "colorModel";
             }
             /*
              * The SampleModel size represents the physical layout of pixels in the data buffer,
diff --git a/core/sis-feature/src/main/java/org/apache/sis/internal/coverage/j2d/TileOpExecutor.java b/core/sis-feature/src/main/java/org/apache/sis/internal/coverage/j2d/TileOpExecutor.java
index b1f7c2c..bed6e80 100644
--- a/core/sis-feature/src/main/java/org/apache/sis/internal/coverage/j2d/TileOpExecutor.java
+++ b/core/sis-feature/src/main/java/org/apache/sis/internal/coverage/j2d/TileOpExecutor.java
@@ -16,12 +16,8 @@
  */
 package org.apache.sis.internal.coverage.j2d;
 
-import java.util.Locale;
-import java.util.logging.Level;
 import java.util.logging.LogRecord;
-import java.util.logging.SimpleFormatter;
 import java.util.stream.Collector;
-import java.util.function.Consumer;
 import java.util.function.BiConsumer;
 import java.util.function.BinaryOperator;
 import java.util.concurrent.atomic.AtomicInteger;
@@ -36,6 +32,7 @@ import java.awt.image.ImagingOpException;
 import org.apache.sis.util.Classes;
 import org.apache.sis.util.Exceptions;
 import org.apache.sis.util.ArgumentChecks;
+import org.apache.sis.image.ErrorHandler;
 import org.apache.sis.internal.feature.Resources;
 import org.apache.sis.internal.system.CommonExecutor;
 import org.apache.sis.internal.util.Strings;
@@ -83,6 +80,13 @@ public class TileOpExecutor {
     private final int minTileX, minTileY, maxTileX, maxTileY;
 
     /**
+     * Where to report exceptions, or {@link ErrorHandler#THROW} for throwing them.
+     * In current implementation this is used only during parallel computation.
+     * A future version may need to use it for sequential computations as well for consistency.
+     */
+    private ErrorHandler errorHandler;
+
+    /**
      * Creates a new operation for tiles in the specified region of the specified image.
      * It is caller responsibility to ensure that {@code aoi} is contained inside {@code image} bounds
      * (caller can invoke {@link ImageUtilities#clipBounds(RenderedImage, Rectangle)} if needed).
@@ -92,6 +96,7 @@ public class TileOpExecutor {
      * @throws ArithmeticException if some tile indices are too large.
      */
     public TileOpExecutor(final RenderedImage image, final Rectangle aoi) {
+        errorHandler = ErrorHandler.THROW;
         if (aoi != null) {
             final int  tileWidth       = image.getTileWidth();
             final int  tileHeight      = image.getTileHeight();
@@ -110,6 +115,18 @@ public class TileOpExecutor {
     }
 
     /**
+     * Sets the handler where to report exceptions.
+     * In current implementation this is used only during parallel computation.
+     * A future version may need to use it for sequential computations as well for consistency.
+     *
+     * @param  errorHandler  where to report exceptions, or {@link ErrorHandler#THROW} for throwing them.
+     */
+    public final void setErrorHandler(final ErrorHandler errorHandler) {
+        ArgumentChecks.ensureNonNull("errorHandler", errorHandler);
+        this.errorHandler = errorHandler;
+    }
+
+    /**
      * Returns {@code true} if the region of interest covers at least two tiles.
      * Returns {@code false} if the region of interest covers a single tile or no tile at all.
      *
@@ -273,7 +290,7 @@ public class TileOpExecutor {
                 } catch (Exception ex) {
                     throw Worker.rethrowOrWrap(ex);             // Will be caught again by Worker.run().
                 }
-            }), null);
+            }));
         } else {
             readFrom(source);
         }
@@ -312,7 +329,7 @@ public class TileOpExecutor {
                 } catch (Exception ex) {
                     throw Worker.rethrowOrWrap(ex);             // Will be caught again by Worker.run().
                 }
-            }), null);
+            }));
         } else {
             writeTo(target);
         }
@@ -362,12 +379,12 @@ public class TileOpExecutor {
      *
      * <ul class="verbose">
      *   <li>
-     *     If the {@code errorHandler} is {@code null}, then all threads will finish the tiles they were
+     *     If the {@code errorHandler} is {@code THROW}, then all threads will finish the tiles they were
      *     processing at the time the error occurred, but will not take any other tile (i.e. remaining tiles
      *     will be left unprocessed). The exception that occurred is wrapped in an {@link ImagingOpException}
      *     and thrown.
      *   </li><li>
-     *     If the {@code errorHandler} is non-null, then the exception is wrapped in a {@link LogRecord} and
+     *     If the {@code errorHandler} is {@code LOG}, then the exception is wrapped in a {@link LogRecord} and
      *     the processing continues with other tiles. If more exceptions happen, those subsequent exceptions
      *     will be added to the first one by {@link Exception#addSuppressed(Throwable)}.
      *     After all tiles have been processed, the error handler will be invoked with that {@link LogRecord}.
@@ -385,15 +402,13 @@ public class TileOpExecutor {
      *                       but other images are okay if they share the same pixel and tile coordinate systems.
      * @param  collector     the action to execute on each {@link Raster}, together with supplier and combiner
      *                       of thread-local objects of type <var>A</var>. See above javadoc for more information.
-     * @param  errorHandler  where to report exceptions, or {@code null} for throwing them.
      * @return the final result computed by finisher (may be {@code null}).
      * @throws ImagingOpException if an exception occurred during {@link RenderedImage#getTile(int, int)}
      *         or {@link #readFrom(Raster)} execution, and {@code errorHandler} is {@code null}.
      * @throws RuntimeException if an exception occurred elsewhere (for example in the combiner or finisher).
      */
     public final <A,R> R executeOnReadable(final RenderedImage source,
-                                           final Collector<? super Raster, A, R> collector,
-                                           final Consumer<LogRecord> errorHandler)
+                                           final Collector<? super Raster, A, R> collector)
     {
         ArgumentChecks.ensureNonNull("source", source);
         ArgumentChecks.ensureNonNull("collector", collector);
@@ -435,8 +450,8 @@ public class TileOpExecutor {
      * there is a choice:
      *
      * <ul>
-     *   <li>If the {@code errorHandler} is {@code null}, the exception is wrapped in an {@link ImagingOpException} and thrown.</li>
-     *   <li>If the {@code errorHandler} is non-null, the exception is wrapped in a {@link LogRecord} and given to the handler.
+     *   <li>If the {@code errorHandler} is {@code THROW}, the exception is wrapped in an {@link ImagingOpException} and thrown.</li>
+     *   <li>If the {@code errorHandler} is {@code LOG}, the exception is wrapped in a {@link LogRecord} and given to the handler.
      *       That {@code LogRecord} has the level, message and exception properties set, but not the source class name,
      *       source method name or logger name. The error handler should set those properties itself.</li>
      * </ul>
@@ -452,7 +467,6 @@ public class TileOpExecutor {
      *                       but other images are okay if they share the same pixel and tile coordinate systems.
      * @param  collector     the action to execute on each {@link WritableRaster}, together with supplier and combiner
      *                       of thread-local objects of type <var>A</var>. See above javadoc for more information.
-     * @param  errorHandler  where to report exceptions, or {@code null} for throwing them.
      * @return the final result computed by finisher. This is often {@code null} because the purpose of calling
      *         {@code executeOnWritable(…)} is more often to update existing tiles instead than to compute a value.
      * @throws ImagingOpException if an exception occurred during {@link WritableRenderedImage#getWritableTile(int, int)},
@@ -461,8 +475,7 @@ public class TileOpExecutor {
      * @throws RuntimeException if an exception occurred elsewhere (for example in the combiner or finisher).
      */
     public final <A,R> R executeOnWritable(final WritableRenderedImage target,
-                                           final Collector<? super WritableRaster,A,R> collector,
-                                           final Consumer<LogRecord> errorHandler)
+                                           final Collector<? super WritableRaster,A,R> collector)
     {
         ArgumentChecks.ensureNonNull("target", target);
         ArgumentChecks.ensureNonNull("collector", collector);
@@ -523,12 +536,13 @@ public class TileOpExecutor {
         private A accumulator;
 
         /**
-         * The errors that occurred while computing a tile, or {@code null} if none.
+         * The errors that occurred while computing a tile.
+         * Will be ignored if {@linkplain ErrorHandler.Report#isEmpty() empty}.
          *
          * @see #stopOnError
          * @see #recordError(Worker, Throwable)
          */
-        private LogRecord errors;
+        private final ErrorHandler.Report errors;
 
         /**
          * Whether to stop of the first error. If {@code true} the error will be reported as soon as possible.
@@ -551,6 +565,7 @@ public class TileOpExecutor {
             this.combiner    = collector.combiner();
             this.numXTiles   = Math.incrementExact(Math.subtractExact(maxTileX, minTileX));
             this.stopOnError = stopOnError;
+            this.errors      = new ErrorHandler.Report();
         }
 
         /**
@@ -605,13 +620,13 @@ public class TileOpExecutor {
          * @param  workers       handlers of all worker threads other than the current threads.
          *                       Content of this array may be modified by this method.
          * @param  collector     provides the finisher to use for computing final result of type <var>R</var>.
-         * @param  errorHandler  where to report exceptions, or {@code null} for throwing them.
+         * @param  errorHandler  where to report exceptions, or {@link ErrorHandler#THROW} for throwing them.
          * @return the final result computed by finisher (may be {@code null}).
          * @throws ImagingOpException if an exception occurred during {@link Worker#executeOnCurrentTile()}
          *         and the {@code errorHandler} is {@code null}.
          * @throws RuntimeException if an exception occurred elsewhere (for example in the combiner or finisher).
          */
-        final <R> R finish(final Future<?>[] workers, final Collector<?,A,R> collector, final Consumer<LogRecord> errorHandler) {
+        final <R> R finish(final Future<?>[] workers, final Collector<?,A,R> collector, final ErrorHandler errorHandler) {
             /*
              * Before to wait for other threads to complete their work, we need to remove from executor queue all
              * workers that did not yet started their run. Those threads may be waiting for an executor thread to
@@ -629,7 +644,7 @@ public class TileOpExecutor {
                  * This is not an exception that occurred in Worker.executeOnCurrentTile(), RenderedImage.getTile(…)
                  * or similar methods, otherwise it would have been handled by Worker.run(). This is an error or an
                  * exception that occurred elsewhere, for example in the combiner, in which case we do not wrap it
-                 * in an ImagingOpException.
+                 * in an ImagingOpException (unless we have to because the cause is a checked exception).
                  */
                 throw Worker.rethrowOrWrap(ex.getCause());
             } catch (InterruptedException ex) {
@@ -638,12 +653,7 @@ public class TileOpExecutor {
                  * We will report that interruption as an error.
                  */
                 synchronized (this) {
-                    if (errors == null) {
-                        errors = new LogRecord(Level.WARNING, ex.toString());
-                        errors.setThrown(ex);
-                    } else {
-                        errors.getThrown().addSuppressed(ex);
-                    }
+                    errors.addPropertyError(ex, null);
                 }
                 break;
             }
@@ -655,14 +665,8 @@ public class TileOpExecutor {
             final R result;
             synchronized (this) {
                 result = collector.finisher().apply(accumulator);
-                if (errors != null) {
-                    if (errorHandler != null) {
-                        errorHandler.accept(errors);
-                    } else {
-                        final Throwable ex = trimImagingWrapper(errors.getThrown());
-                        final String message = new SimpleFormatter().formatMessage(errors);
-                        throw (ImagingOpException) new ImagingOpException(message).initCause(ex);
-                    }
+                if (!errors.isEmpty()) {
+                    errorHandler.handle(errors);
                 }
             }
             return result;
@@ -681,13 +685,7 @@ public class TileOpExecutor {
                 set(Integer.MIN_VALUE);         // Will cause other threads to stop fetching tiles.
             }
             synchronized (this) {
-                if (errors == null) {
-                    errors = Resources.forLocale((Locale) null).getLogRecord(Level.WARNING,
-                             Resources.Keys.CanNotProcessTile_2, indices.tx, indices.ty);
-                    errors.setThrown(ex);
-                } else {
-                    errors.getThrown().addSuppressed(ex);
-                }
+                errors.addTileError(ex, indices.tx, indices.ty);
             }
         }
 
@@ -867,11 +865,11 @@ public class TileOpExecutor {
         }
 
         /**
-         * Implementation of {@link #executeOnReadable(RenderedImage, Collector, Consumer)}.
+         * Implementation of {@link #executeOnReadable(RenderedImage, Collector)}.
          * See the Javadoc of that method for details.
          */
         static <A,R> R execute(final TileOpExecutor executor, final RenderedImage source,
-                final Collector<? super Raster, A, R> collector, final Consumer<LogRecord> errorHandler)
+                final Collector<? super Raster, A, R> collector, final ErrorHandler errorHandler)
         {
             final Cursor<RenderedImage,A> cursor = executor.new Cursor<>(source, collector, errorHandler == null);
             final Future<?>[] workers = new Future<?>[cursor.getNumWorkers()];
@@ -924,11 +922,11 @@ public class TileOpExecutor {
         }
 
         /**
-         * Implementation of {@link #executeOnWritable(WritableRenderedImage, Collector, Consumer)}.
+         * Implementation of {@link #executeOnWritable(WritableRenderedImage, Collector)}.
          * See the Javadoc of that method for details.
          */
         static <A,R> R execute(final TileOpExecutor executor, final WritableRenderedImage target,
-                final Collector<? super WritableRaster,A,R> collector, final Consumer<LogRecord> errorHandler)
+                final Collector<? super WritableRaster,A,R> collector, final ErrorHandler errorHandler)
         {
             final Cursor<WritableRenderedImage,A> cursor = executor.new Cursor<>(target, collector, false);
             final Future<?>[] workers = new Future<?>[cursor.getNumWorkers()];
diff --git a/core/sis-feature/src/test/java/org/apache/sis/image/StatisticsCalculatorTest.java b/core/sis-feature/src/test/java/org/apache/sis/image/StatisticsCalculatorTest.java
index 20c2c9b..9b881f9 100644
--- a/core/sis-feature/src/test/java/org/apache/sis/image/StatisticsCalculatorTest.java
+++ b/core/sis-feature/src/test/java/org/apache/sis/image/StatisticsCalculatorTest.java
@@ -134,7 +134,7 @@ public final strictfp class StatisticsCalculatorTest extends TestCase {
     public void testWithLoggings() {
         final ImageProcessor operations = new ImageProcessor();
         operations.setExecutionMode(ImageProcessor.Mode.PARALLEL);
-        operations.setErrorAction(ImageProcessor.ErrorAction.LOG);
+        operations.setErrorHandler(ErrorHandler.LOG);
         final TiledImageMock image = createImage();
         image.failRandomly(new Random(8004277484984714811L));
         final Statistics[] stats = operations.valueOfStatistics(image, null);


Mime
View raw message