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 <[email protected]> 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);
