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 905edce310fbe77d2647c17f8a8cca2d9b5a01a5
Author: Martin Desruisseaux <martin.desruisse...@geomatys.com>
AuthorDate: Sat Sep 14 15:06:52 2024 +0200

    Replace the error management of `GDALStore` by a more robust mechanism 
based on the registration of an error handler in GDAL.
---
 .../main/org/apache/sis/storage/gdal/Band.java     |  11 +-
 .../org/apache/sis/storage/gdal/ErrorHandler.java  | 247 +++++++++++++++++++++
 .../main/org/apache/sis/storage/gdal/GDAL.java     | 164 ++++----------
 .../org/apache/sis/storage/gdal/GDALStore.java     |  45 ++--
 .../main/org/apache/sis/storage/gdal/Opener.java   |  10 +-
 .../org/apache/sis/storage/gdal/TiledCoverage.java |  18 +-
 .../org/apache/sis/storage/gdal/TiledResource.java |  48 +++-
 .../apache/sis/storage/panama/NativeFunctions.java |   9 +
 8 files changed, 377 insertions(+), 175 deletions(-)

diff --git 
a/incubator/src/org.apache.sis.storage.gdal/main/org/apache/sis/storage/gdal/Band.java
 
b/incubator/src/org.apache.sis.storage.gdal/main/org/apache/sis/storage/gdal/Band.java
index f71907f4ea..ec1c0e1bf4 100644
--- 
a/incubator/src/org.apache.sis.storage.gdal/main/org/apache/sis/storage/gdal/Band.java
+++ 
b/incubator/src/org.apache.sis.storage.gdal/main/org/apache/sis/storage/gdal/Band.java
@@ -109,7 +109,6 @@ final class Band {
         boolean hasRange, hasNoData, convert;
         final MemorySegment names, uom;
         try {
-            gdal.errorReset();
             band    = (int)    gdal.getBandNumber       .invokeExact(handle);
             minimum = (double) gdal.getRasterMinimum    .invokeExact(handle, 
flag); hasRange  = isTrue(flag);
             maximum = (double) gdal.getRasterMaximum    .invokeExact(handle, 
flag); hasRange &= isTrue(flag);
@@ -121,7 +120,6 @@ final class Band {
         } catch (Throwable e) {
             throw GDAL.propagate(e);
         }
-        gdal.checkCPLErr(parent, "getSampleDimensions", false);
         /*
          * If a unit of measurement is given, we consider that the band is 
quantitative
          * even if the transfer function (scale and offset) was not specified. 
The GDAL
@@ -251,12 +249,13 @@ final class Band {
      * @param  aoi     region of the image to read or write. (0,0) is the 
upper-left pixel.
      * @param  raster  the Java2D raster where to store of fetch the values to 
read or write.
      * @param  band    band of sample values in the Java2D raster.
+     * @return whether the operation was successful according 
<abbr>GDAL</abbr>.
      * @throws ClassCastException if an above-documented prerequisite is not 
true.
      * @throws DataStoreException if <var>GDAL</var> reported a warning or 
fatal error.
      */
-    final void transfer(final GDAL gdal, final int rwFlag,
-                        final TiledResource image, final Rectangle aoi,     // 
GDAL model
-                        final Raster raster, final int band)                // 
Java2D model
+    final boolean transfer(final GDAL gdal, final int rwFlag,
+                           final TiledResource image, final Rectangle aoi,     
// GDAL model
+                           final Raster raster, final int band)                
// Java2D model
             throws DataStoreException
     {
         if (rwFlag == OpenFlag.READ && !(raster instanceof WritableRaster)) {
@@ -290,6 +289,6 @@ final class Band {
         } catch (Throwable e) {
             throw GDAL.propagate(e);
         }
-        gdal.checkCPLErr(image.parent, (rwFlag == OpenFlag.READ) ? "read" : 
"write", false, err);
+        return ErrorHandler.checkCPLErr(err);
     }
 }
diff --git 
a/incubator/src/org.apache.sis.storage.gdal/main/org/apache/sis/storage/gdal/ErrorHandler.java
 
b/incubator/src/org.apache.sis.storage.gdal/main/org/apache/sis/storage/gdal/ErrorHandler.java
new file mode 100644
index 0000000000..7432ccfdf5
--- /dev/null
+++ 
b/incubator/src/org.apache.sis.storage.gdal/main/org/apache/sis/storage/gdal/ErrorHandler.java
@@ -0,0 +1,247 @@
+/*
+ * 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.storage.gdal;
+
+import java.util.List;
+import java.util.ArrayList;
+import java.util.logging.Level;
+import java.util.logging.LogRecord;
+import java.lang.invoke.MethodType;
+import java.lang.invoke.MethodHandle;
+import java.lang.invoke.MethodHandles;
+import java.lang.foreign.MemorySegment;
+import org.apache.sis.util.logging.Logging;
+import org.apache.sis.storage.DataStoreException;
+import org.apache.sis.storage.InternalDataStoreException;
+import org.apache.sis.storage.panama.NativeFunctions;
+
+
+/**
+ * A callback which is invoked by <abbr>GDAL</abbr> when a warning or error 
occurred.
+ *
+ * @author  Martin Desruisseaux (Geomatys)
+ */
+final class ErrorHandler {
+    /**
+     * Context in which a <abbr>GDAL</abbr> method is invoked.
+     */
+    private static final ThreadLocal<List<ErrorHandler>> CURRENT = new 
ThreadLocal<>();
+
+    /**
+     * The {@code CE_Failure} value of the <abbr>GDAL</abbr> {@code CPLErr} 
codes.
+     */
+    private static final int FAILURE = 3;
+
+    /**
+     * <abbr>GDAL</abbr> Common Portability Library error code ({@code 
CPLErr}) of the message.
+     * This code will be mapped to a {@link Level} at logging time. The 
possible values are:
+     *
+     * <ul>
+     *   <li>{@code CE_None}    = 0: No error or warning occurred.</li>
+     *   <li>{@code CE_Debug}   = 1: The result contains debug information 
that users can ignore.</li>
+     *   <li>{@code CE_Warning} = 2: The result contains informational 
warning.</li>
+     *   <li>{@code CE_Failure} = 3: The action failed.</li>
+     *   <li>{@code CE_Fatal}   = 4: A fatal error has occurred and 
<abbr>GDAL</abbr> should not be used anymore.
+     *       The default GDAL behavior is to report errors to {@code stderr} 
and to abort the application.</li>
+     * </ul>
+     */
+    private final int err;
+
+    /**
+     * The error message, or {@code null} if none.
+     */
+    private final String message;
+
+    /**
+     * Creates a record for a <abbr>GDAL</abbr> error message. The use of this 
class as a record is an
+     * {@code ErrorHandler} implementation details. Code outside this class 
see only the static methods.
+     */
+    private ErrorHandler(final int err, final String message) {
+        this.err     = err;
+        this.message = message;
+    }
+
+    /**
+     * Returns the handle for the method to invoke from <abbr>GDAL</abbr>.
+     * Used only at {@linkplain GDAL#setErrorHandler initialization time}
+     * after the <abbr>GDAL</abbr> library has been loaded.
+     */
+    static MethodHandle getMethod() {
+        try {
+            return MethodHandles.lookup().findStatic(ErrorHandler.class, 
"errorOccurred",
+                    MethodType.methodType(Void.TYPE, Integer.TYPE, 
Integer.TYPE, MemorySegment.class));
+        } catch (ReflectiveOperationException e) {
+            throw new AssertionError(e);        // Should never happen.
+        }
+    }
+
+    /**
+     * Invoked by <abbr>GDAL</abbr> native code when a warning or an error 
occurred.
+     * This method merely adds the message in a queue for logging at a later 
stage.
+     * No logging is performed here for avoiding the risk that a user's code 
throws
+     * an exception.
+     *
+     * @param  err      the <abbr>GDAL</abbr> Common Portability Library error 
level ({@code CPLErr}).
+     * @param  code     the <abbr>GDAL</abbr> {@code CPLErrorNum} error code.
+     * @param  message  a message describing the error, or {@link 
MemorySegment#NULL} if none.
+     */
+    @SuppressWarnings("CallToPrintStackTrace")
+    public static void errorOccurred(final int err, final int code, final 
MemorySegment message) {
+        try {
+            List<ErrorHandler> messages = CURRENT.get();
+            if (messages == null) {
+                messages = new ArrayList<>();
+                CURRENT.set(messages);
+            }
+            String text = NativeFunctions.toString(message);
+            if (text == null || text.isBlank()) {
+                text = "GDAL error #" + code;
+            }
+            messages.add(new ErrorHandler(err, text));
+        } catch (Throwable e) {
+            /*
+             * Should never occurs. If it occurs anyway, we cannot let any 
exception to be thrown by this method,
+             * because this method is invoked from native code. Even logging 
may be dangerous, because logger may
+             * perform complex operations. The less risky is to only print the 
stack trace.
+             */
+            e.printStackTrace();
+        }
+    }
+
+    /**
+     * Remembers the {@code CPLError} value returned by a <abbr>GDAL</abbr> 
function in context where that value
+     * cannot be consumed immediately. This is a safety in case a warning with 
a more descriptive message has not
+     * been added to the {@link #CURRENT} list, and is used for remembering 
that an exception should be thrown.
+     */
+    static void errorOccurred(final int err) {
+        List<ErrorHandler> messages = CURRENT.get();
+        if (messages == null) {
+            messages = new ArrayList<>();
+            CURRENT.set(messages);
+        }
+        messages.add(new ErrorHandler(err, null));
+    }
+
+    /**
+     * Invoked after the execution of <abbr>GDAL</abbr> functions when failure 
are considered warnings.
+     * This method should be used as below.
+     *
+     * {@snippet lang="java" :
+     *     public void myPublicMethod() throws DataStoreException {
+     *         try {
+     *             handle.invokeExact(…);
+     *         } catch (Throwable e) {
+     *             throws GDAL.propagate(e);
+     *         } finally {
+     *             ErrorHandler.report(store, "myPublicMethod");
+     *         =
+     *     }
+     *     }
+     *
+     * @param store   the store where to redirect the <abbr>GDAL</abbr> 
warnings, or {@code null} if none.
+     * @param method  name of the {@code GDALStore} method to declare as the 
source of the warning, or {@code null}.
+     */
+    static void report(final GDALStore store, final String method) {
+        final List<ErrorHandler> messages = CURRENT.get();
+        if (messages != null) {
+            CURRENT.remove();
+            for (final ErrorHandler m : messages) {
+                final Level level;
+                switch (m.err) {
+                    default: {
+                        level = Level.SEVERE;
+                        if (store != null) {
+                            store.getProvider().fatalError();
+                        }
+                        break;
+                    }
+                    case FAILURE: // Fall through
+                    case 2: level = Level.WARNING; break;
+                    case 1: level = Level.FINE;    break;
+                    case 0: return;
+                }
+                /*
+                 * The message may be null only when the warning has been 
reported by `errorOccurred(int)`,
+                 * in which case a more descriptive message has probably been 
already reported in a prior
+                 * instance of `ErrorHandler`.
+                 */
+                if (m.message != null) {
+                    final var r = new LogRecord(level, m.message);
+                    if (store != null) {
+                        store.warning(method, r);
+                    } else {
+                        Class<?> src = (method != null) ? GDALStore.class : 
null;
+                        Logging.completeAndLog(GDALStoreProvider.LOGGER, src, 
method, r);
+                    }
+                }
+            }
+        }
+    }
+
+    /**
+     * Invoked after the execution of <abbr>GDAL</abbr> functions when 
failures are considered errors.
+     *
+     * @param store   the store where to redirect the <abbr>GDAL</abbr> 
warnings, or {@code null} if none.
+     * @param method  name of the {@code GDALStore} method to declare as the 
source of the warning, or {@code null}.
+     */
+    static void throwOnFailure(final GDALStore store, final String method) 
throws DataStoreException {
+        final List<ErrorHandler> messages = CURRENT.get();
+        if (messages != null) {
+            String error = null;
+            boolean hasError = false;
+            for (int i = messages.size(); --i >= 0;) {
+                final ErrorHandler m = messages.get(i);
+                if (m.err >= FAILURE) {
+                    error = m.message;          // Message may be null.
+                    if (m.err == FAILURE) {     // Keep logging of fatal 
errors.
+                        messages.remove(i);
+                    }
+                    hasError = true;
+                    if (error != null) break;
+                }
+            }
+            report(store, method);
+            if (hasError) {
+                throw new DataStoreException(error);
+            }
+        }
+    }
+
+    /**
+     * Handles a <abbr>GDAL</abbr> Common Portability Library error code 
({@code CPLErr}).
+     * The possible values are:
+     *
+     * <ul>
+     *   <li>{@code CE_None}    = 0: No error or warning occurred.</li>
+     *   <li>{@code CE_Debug}   = 1: The result contains debug information 
that users can ignore.</li>
+     *   <li>{@code CE_Warning} = 2: The result contains informational 
warning.</li>
+     *   <li>{@code CE_Failure} = 3: The action failed.</li>
+     *   <li>{@code CE_Fatal}   = 4: A fatal error has occurred and 
<abbr>GDAL</abbr> should not be used anymore.
+     *       The default GDAL behavior is to report errors to {@code stderr} 
and to abort the application.</li>
+     * </ul>
+     *
+     * @param  err  the {@code CPLErr} enumeration value returned by 
<abbr>GDAL</abbr>.
+     * @return whether the operation was successful, possibly with warnings.
+     * @throws DataStoreException if the error category is {@code CE_Fatal} or 
higher.
+     */
+    static boolean checkCPLErr(final int err) throws DataStoreException {
+        if (err > FAILURE) {
+            throw new InternalDataStoreException("GDAL fatal error.");
+        }
+        return err < FAILURE;
+    }
+}
diff --git 
a/incubator/src/org.apache.sis.storage.gdal/main/org/apache/sis/storage/gdal/GDAL.java
 
b/incubator/src/org.apache.sis.storage.gdal/main/org/apache/sis/storage/gdal/GDAL.java
index 349612edff..78f78d1c8c 100644
--- 
a/incubator/src/org.apache.sis.storage.gdal/main/org/apache/sis/storage/gdal/GDAL.java
+++ 
b/incubator/src/org.apache.sis.storage.gdal/main/org/apache/sis/storage/gdal/GDAL.java
@@ -227,31 +227,6 @@ final class GDAL extends NativeFunctions implements 
Runnable {
      */
     final MethodHandle rasterIO;
 
-    /**
-     * <abbr>GDAL</abbr> {@code void CPLErrorReset(void)}.
-     * Erase any traces of previous errors. Used to ensure that an error which 
has been
-     * recovered from  does not appear to be still in play with high level 
functions.
-     *
-     * @see #errorReset()
-     */
-    private final MethodHandle errorReset;
-
-    /**
-     * <abbr>GDAL</abbr> {@code CPLErr CPLGetLastErrorType(void)}.
-     * Fetches the last error type that hasn't been cleared by {@link 
#errorReset}.
-     *
-     * @see #checkCPLErr(GDALStore, String, boolean)
-     */
-    private final MethodHandle getLastErrorType;
-
-    /**
-     * <abbr>GDAL</abbr> {@code const char *CPLGetLastErrorMsg(void)}.
-     * Fetches the last error message that hasn't been cleared by {@link 
#errorReset}
-     *
-     * @todo Replace by a call to {@code CPLSetErrorHandler}.
-     */
-    final MethodHandle getLastErrorMsg;
-
     /**
      * Creates the handles for all <abbr>GDAL</abbr> functions which will be 
needed.
      *
@@ -350,15 +325,12 @@ final class GDAL extends NativeFunctions implements 
Runnable {
                 ValueLayout.JAVA_INT,       // int nPixelSpace
                 ValueLayout.JAVA_INT));     // int nLineSpace
 
-        // Error handling
-        errorReset       = lookup(linker, "CPLErrorReset",       
FunctionDescriptor.ofVoid());
-        getLastErrorType = lookup(linker, "CPLGetLastErrorType", 
FunctionDescriptor.of(ValueLayout.JAVA_INT));
-        getLastErrorMsg  = lookup(linker, "CPLGetLastErrorMsg",  
FunctionDescriptor.of(ValueLayout.ADDRESS));
+        // Set error handling first in order to redirect initialization 
warnings.
+        setErrorHandler(linker, null);
 
         // Initialize GDAL after we found all functions.
         if (!invoke("GDALAllRegister")) {
-            final LogRecord record = new LogRecord(Level.WARNING, "Could not 
initialize GDAL.");
-            log("open", record);
+            log("open", new LogRecord(Level.WARNING, "Could not initialize 
GDAL."));
         }
     }
 
@@ -438,6 +410,41 @@ final class GDAL extends NativeFunctions implements 
Runnable {
         return Optional.ofNullable(global);
     }
 
+    /**
+     * Installs a function for redirecting <abbr>GDAL</abbr> errors to Apache 
<abbr>SIS</abbr> loggers.
+     * The handler is set by a call to {@code CPLErrorHandler 
CPLSetErrorHandler(CPLErrorHandler)} where
+     * {@code CPLErrorHandler} is {@code void (*CPLErrorHandler)(CPLErr, 
CPLErrorNum, const char*)}.
+     *
+     * <p><b>The error handler is valid only during the lifetime of the 
{@linkplain #arena() arena}.</b>
+     * The error handle shall be uninstalled before the arena is closed.</p>
+     *
+     * @param  linker  the linker to use. Should be {@link 
Linker#nativeLinker()}.
+     * @param  target  the function to set as an error handler, or {@link 
MemorySegment#NULL} for the GDAL default.
+     *                 If {@code null}, the function handle will be created by 
this method.
+     * @return the previous error handler, or {@link MemorySegment#NULL} it it 
was the <abbr>GDAL</abbr> default.
+     *
+     * @see #run()
+     * @see GDALStoreProvider#fatalError()
+     */
+    @SuppressWarnings("restricted")
+    private MemorySegment setErrorHandler(final Linker linker, MemorySegment 
target) {
+        final MemorySegment setter = 
symbols.find("CPLSetErrorHandler").orElse(null);
+        if (setter == null) {
+            return MemorySegment.NULL;
+        }
+        if (target == null) {
+            target = linker.upcallStub(ErrorHandler.getMethod(),
+                    FunctionDescriptor.ofVoid(ValueLayout.JAVA_INT, 
ValueLayout.JAVA_INT, ValueLayout.ADDRESS), arena());
+        }
+        final MethodHandle handle = linker.downcallHandle(setter,
+                FunctionDescriptor.of(ValueLayout.ADDRESS, 
ValueLayout.ADDRESS));
+        try {
+            return (MemorySegment) handle.invokeExact(target);
+        } catch (Throwable e) {
+            throw propagate(e);
+        }
+    }
+
     /**
      * Logs the given record as if was produced by the {@link 
GDALStoreProvider}, which is the public class.
      *
@@ -489,99 +496,6 @@ final class GDAL extends NativeFunctions implements 
Runnable {
         return items;
     }
 
-    /**
-     * Resets the <abbr>GDAL</abbr> Common Portability Library 
(<abbr>CPL</abbr>) error code.
-     * This method should be invoked before functions returning a value other 
than {@code CPLErr}.
-     *
-     * @see #checkCPLErr(GDALStore, String, boolean, int)
-     */
-    final void errorReset() throws Throwable {
-        errorReset.invokeExact();
-    }
-
-    /**
-     * Handles the <abbr>GDAL</abbr> Common Portability Library error code of 
the last invoked function.
-     * This method assumes that no other <var>GDAL</var> function has been 
invoked since the last error.
-     *
-     * <h4>Race conditions</h4>
-     * <em>Despite synchronization, this method is not thread-safe.</em> If 
another {@code GDALStore}
-     * is performing an action concurrently, even on unrelated data, the GDAL 
message may be confusing.
-     *
-     * @param  caller     the store that performed a <abbr>GDAL</abbr> 
operation.
-     * @param  method     the {@code GDALStore} method executed, or null for 
throwing an exception instead of logging.
-     * @param  mandatory  whether a failure should throw an exception. If not, 
only a warning will be logged.
-     * @return whether the operation was successful, possibly with warnings.
-     * @throws DataStoreException if the error category is {@code CE_Failure} 
and {@code mandatory} is true,
-     *         or if the error category is {@code CE_Fatal} or higher.
-     *
-     * @see #errorReset()
-     */
-    final boolean checkCPLErr(final GDALStore caller, final String method, 
final boolean mandatory)
-            throws DataStoreException
-    {
-        final int err;
-        try {
-            err = (int) getLastErrorType.invokeExact();
-        } catch (Throwable e) {
-            throw propagate(e);
-        }
-        return checkCPLErr(caller, method, mandatory, err);
-    }
-
-    /**
-     * Handles a <abbr>GDAL</abbr> Common Portability Library error code 
({@code CPLErr}).
-     * The possible values are:
-     *
-     * <ul>
-     *   <li>{@code CE_None}    = 0: No error or warning occurred.</li>
-     *   <li>{@code CE_Debug}   = 1: The result contains debug information 
that users can ignore.</li>
-     *   <li>{@code CE_Warning} = 2: The result contains informational 
warning.</li>
-     *   <li>{@code CE_Failure} = 3: The action failed.</li>
-     *   <li>{@code CE_Fatal}   = 4: A fatal error has occurred and 
<abbr>GDAL</abbr> should not be used anymore.
-     *       The default GDAL behavior is to report errors to {@code stderr} 
and to abort the application.</li>
-     * </ul>
-     *
-     * If the error level is debug or warning, a message is sent to the 
{@linkplain #listeners}.
-     * If the error level is failure or fatal, a {@link DataStoreException} is 
thrown.
-     *
-     * @param  caller     the store that performed a <abbr>GDAL</abbr> 
operation.
-     * @param  method     the {@code GDALStore} method executed.c
-     * @param  mandatory  whether a failure should throw an exception. If not, 
only a warning will be logged.
-     * @param  err        the {@code CPLErr} enumeration value returned by 
<abbr>GDAL</abbr>.
-     * @return whether the operation was successful, possibly with warnings.
-     * @throws DataStoreException if the error category is {@code CE_Failure} 
and {@code mandatory} is true,
-     *         or if the error category is {@code CE_Fatal} or higher.
-     *
-     * @see #errorReset()
-     */
-    final boolean checkCPLErr(final GDALStore caller, final String method, 
final boolean mandatory, final int err)
-            throws DataStoreException
-    {
-        final Level level;
-        switch (err) {
-            case 0: return true;
-            case 1: level = Level.FINE; break;
-            case 2: level = Level.WARNING; break;
-            case 3: level = mandatory ? null : Level.WARNING; break;
-            default: {
-                caller.getProvider().fatalError();
-                throw new DataStoreException("GDAL fatal error. Cannot use 
GDAL anymore.");
-            }
-        }
-        MemorySegment msg = null;
-        try {
-            msg = (MemorySegment) getLastErrorMsg.invokeExact();
-        } catch (Throwable e) {
-            throw propagate(e);
-        }
-        final String message = toString(msg);
-        if (level == null) {
-            throw new DataStoreException(message);
-        }
-        caller.warning(method, new LogRecord(level, message));
-        return err < 3;
-    }
-
     /**
      * Unloads the <abbr>GDAL</abbr> library. If the arena is global,
      * then this method should not be invoked before <abbr>JVM</abbr> shutdown.
@@ -590,6 +504,8 @@ final class GDAL extends NativeFunctions implements 
Runnable {
     @Override
     public void run() {
         try {
+            // Clear the error handler because the arena will be closed.
+            setErrorHandler(Linker.nativeLinker(), MemorySegment.NULL);
             invoke("GDALDestroy");
         } finally {
             super.run();
diff --git 
a/incubator/src/org.apache.sis.storage.gdal/main/org/apache/sis/storage/gdal/GDALStore.java
 
b/incubator/src/org.apache.sis.storage.gdal/main/org/apache/sis/storage/gdal/GDALStore.java
index e43fa2ee45..ad92e76d58 100644
--- 
a/incubator/src/org.apache.sis.storage.gdal/main/org/apache/sis/storage/gdal/GDALStore.java
+++ 
b/incubator/src/org.apache.sis.storage.gdal/main/org/apache/sis/storage/gdal/GDALStore.java
@@ -220,6 +220,8 @@ public class GDALStore extends DataStore implements 
Aggregate, ResourceOnFileSys
             driver = (MemorySegment) 
gdal.getDatasetDriver.invokeExact(handle());
         } catch (Throwable e) {
             throw GDAL.propagate(e);
+        } finally {
+            ErrorHandler.throwOnFailure(this, "getDriver");
         }
         // Should never be null, but verify as a safety.
         return GDAL.isNull(driver) ? null : new Driver(getProvider(), driver);
@@ -305,19 +307,16 @@ public class GDALStore extends DataStore implements 
Aggregate, ResourceOnFileSys
     @Override
     @SuppressWarnings("ReturnOfCollectionOrArrayField")   // Because 
unmodifable.
     public synchronized Collection<? extends Resource> components() throws 
DataStoreException {
-        if (components == null) {
+        if (components == null) try {
             final GDAL gdal = getProvider().GDAL();
             final List<Subdataset> subdatasets = groupBySubset(gdal);
             if (subdatasets != null && !subdatasets.isEmpty()) {
                 components = subdatasets;
             } else {
-                final TiledResource[] rasters = 
TiledResource.groupBySizeAndType(this, gdal, handle);
-                if (gdal.checkCPLErr(this, "components", true)) {
-                    components = UnmodifiableArrayList.wrap(rasters);
-                } else {
-                    components = List.of();
-                }
+                components = 
UnmodifiableArrayList.wrap(TiledResource.groupBySizeAndType(this, gdal, 
handle));
             }
+        } finally {
+            ErrorHandler.throwOnFailure(this, "components");
         }
         return components;
     }
@@ -427,18 +426,15 @@ public class GDALStore extends DataStore implements 
Aggregate, ResourceOnFileSys
     final CoordinateReferenceSystem parseCRS(final GDAL gdal, final String 
caller) throws DataStoreException {
         MemorySegment result = null;
         try {
-            gdal.errorReset();
             result = (MemorySegment) gdal.getSpatialRef.invokeExact(handle());
         } catch (Throwable e) {
             throw GDAL.propagate(e);
         }
-        if (gdal.checkCPLErr(this, caller, false)) {
-            final String wkt = GDAL.toString(result);
-            if (wkt != null && !wkt.isBlank()) try {
-                return (CoordinateReferenceSystem) 
wktFormat().parseObject(wkt);
-            } catch (ParseException | ClassCastException e) {
-                warning(caller, "Cannot parse the CRS of " + getDisplayName(), 
e);
-            }
+        final String wkt = GDAL.toString(result);
+        if (wkt != null && !wkt.isBlank()) try {
+            return (CoordinateReferenceSystem) wktFormat().parseObject(wkt);
+        } catch (ParseException | ClassCastException e) {
+            warning(caller, "Cannot parse the CRS of " + getDisplayName(), e);
         }
         return null;
     }
@@ -487,12 +483,19 @@ public class GDALStore extends DataStore implements 
Aggregate, ResourceOnFileSys
      */
     @Override
     public void close() throws DataStoreException {
-        try {
-            listeners.close();      // Should never fail.
-        } finally {
-            synchronized (this) {
-                handle = null;
-                closer.clean();
+        final class Flush implements AutoCloseable {
+            @Override public void close() throws DataStoreException {
+                ErrorHandler.throwOnFailure(GDALStore.this, "close");
+            }
+        }
+        try (Flush _ = new Flush()) {
+            try {
+                listeners.close();
+            } finally {
+                synchronized (this) {
+                    handle = null;
+                    closer.clean();     // Important to always invoke, even if 
an exception occurred before.
+                }
             }
         }
     }
diff --git 
a/incubator/src/org.apache.sis.storage.gdal/main/org/apache/sis/storage/gdal/Opener.java
 
b/incubator/src/org.apache.sis.storage.gdal/main/org/apache/sis/storage/gdal/Opener.java
index d8cb47ae8a..297ab3c950 100644
--- 
a/incubator/src/org.apache.sis.storage.gdal/main/org/apache/sis/storage/gdal/Opener.java
+++ 
b/incubator/src/org.apache.sis.storage.gdal/main/org/apache/sis/storage/gdal/Opener.java
@@ -158,22 +158,20 @@ final class Opener implements Runnable {
     /**
      * Invoked by {@link java.lang.ref.Cleaner} when the native resource is 
ready to be closed.
      * This method shall not be invoked explicitly. The {@code Cleaner} 
<abbr>API</abbr> ensures
-     * that this method will be invoked exactly once (except maybe on 
application termination).
+     * that this method will be invoked exactly once.
      */
     @Override
     public void run() {
         owner.tryGDAL("close").ifPresent((gdal) -> {
             final int err;
             try {
-                // The constructor verified that `close` is non-null.
                 err = (int) gdal.close.invokeExact(handle);
             } catch (Throwable e) {
                 throw GDAL.propagate(e);
             }
-            /*
-             * TODO: report the error, maybe in a ThreadLocal. We may need a 
Threadlocal
-             * anyway if we provide a error handle with CPLSetErrorHandler.
-             */
+            if (err != 0) {
+                ErrorHandler.errorOccurred(err);
+            }
         });
     }
 }
diff --git 
a/incubator/src/org.apache.sis.storage.gdal/main/org/apache/sis/storage/gdal/TiledCoverage.java
 
b/incubator/src/org.apache.sis.storage.gdal/main/org/apache/sis/storage/gdal/TiledCoverage.java
index 4537f2a35d..b68297fff0 100644
--- 
a/incubator/src/org.apache.sis.storage.gdal/main/org/apache/sis/storage/gdal/TiledCoverage.java
+++ 
b/incubator/src/org.apache.sis.storage.gdal/main/org/apache/sis/storage/gdal/TiledCoverage.java
@@ -70,13 +70,17 @@ final class TiledCoverage extends TiledGridCoverage {
     protected Raster[] readTiles(final AOI iterator) throws IOException, 
DataStoreException {
         synchronized (owner.getSynchronizationLock()) {
             final var result = new Raster[iterator.tileCountInQuery];
-            do {
-                final WritableRaster tile = iterator.createRaster();
-                final Rectangle bounds = tile.getBounds();
-                toFullResolution(bounds);
-                owner.transfer(OpenFlag.READ, bounds, tile, includedBands);
-                result[iterator.getIndexInResultArray()] = tile;
-            } while (iterator.next());
+            try {
+                do {
+                    final WritableRaster tile = iterator.createRaster();
+                    final Rectangle bounds = tile.getBounds();
+                    toFullResolution(bounds);
+                    owner.transfer(OpenFlag.READ, bounds, tile, includedBands);
+                    result[iterator.getIndexInResultArray()] = tile;
+                } while (iterator.next());
+            } finally {
+                ErrorHandler.report(owner.parent, "read");      // Public 
caller of this method.
+            }
             return result;
         }
     }
diff --git 
a/incubator/src/org.apache.sis.storage.gdal/main/org/apache/sis/storage/gdal/TiledResource.java
 
b/incubator/src/org.apache.sis.storage.gdal/main/org/apache/sis/storage/gdal/TiledResource.java
index d8768ed759..3cfb673508 100644
--- 
a/incubator/src/org.apache.sis.storage.gdal/main/org/apache/sis/storage/gdal/TiledResource.java
+++ 
b/incubator/src/org.apache.sis.storage.gdal/main/org/apache/sis/storage/gdal/TiledResource.java
@@ -201,7 +201,6 @@ final class TiledResource extends TiledGridResource {
         try (Arena arena = Arena.ofConfined()) {
             final MemorySegment pnXSize = arena.allocate(ValueLayout.JAVA_INT, 
2);
             final MemorySegment pnYSize = pnXSize.asSlice(Integer.BYTES);
-            gdal.errorReset();
             final int count = (int) gdal.getRasterCount.invokeExact(dataset);
             for (int i=0; i<count; i++) {
                 final var band = (MemorySegment) 
gdal.getRasterBand.invokeExact(dataset, i+1);
@@ -273,7 +272,7 @@ final class TiledResource extends TiledGridResource {
     @Override
     public GridGeometry getGridGeometry() throws DataStoreException {
         synchronized (getSynchronizationLock()) {
-            if (geometry == null) {
+            if (geometry == null) try {
                 final MemorySegment handle = parent.handle();          // 
Handle to the GDAL dataset.
                 final GDAL gdal = parent.getProvider().GDAL();
                 final CoordinateReferenceSystem crs = parent.parseCRS(gdal, 
"getGridGeometry");
@@ -293,7 +292,7 @@ final class TiledResource extends TiledGridResource {
                     } catch (Throwable e) {
                         throw GDAL.propagate(e);
                     }
-                    if (gdal.checkCPLErr(parent, "getGridGeometry", false, 
err)) {
+                    if (ErrorHandler.checkCPLErr(err)) {
                         gridToCRS = new AffineTransform2D(
                                 m.get(layout, Double.BYTES * 1),
                                 m.get(layout, Double.BYTES * 4),
@@ -314,6 +313,8 @@ final class TiledResource extends TiledGridResource {
                 } catch (NullPointerException | IllegalArgumentException e) {
                     throw new DataStoreReferencingException(e);
                 }
+            } finally {
+                ErrorHandler.report(parent, "getGridGeometry");
             }
             return geometry;
         }
@@ -328,7 +329,7 @@ final class TiledResource extends TiledGridResource {
     @SuppressWarnings("ReturnOfCollectionOrArrayField")   // Because 
unmodifable.
     public List<SampleDimension> getSampleDimensions() throws 
DataStoreException {
         synchronized (getSynchronizationLock()) {
-            if (sampleDimensions == null) {
+            if (sampleDimensions == null) try {
                 final GDAL gdal = parent.getProvider().GDAL();
                 final var sd = new SampleDimension[bands.length];
                 try (Arena arena = Arena.ofConfined()) {
@@ -338,6 +339,8 @@ final class TiledResource extends TiledGridResource {
                     }
                 }
                 sampleDimensions = List.of(sd);
+            } finally {
+                ErrorHandler.report(parent, "getSampleDimensions");
             }
             return sampleDimensions;
         }
@@ -362,8 +365,13 @@ final class TiledResource extends TiledGridResource {
 
     /**
      * Creates the color model and sample model.
-     * This method uses cached values if the {@code bandIndices} argument is
-     * equal to the values given the last time that this method has been 
invoked.
+     * This method stores the results in {@link #sampleModel} and {@link 
#colorModel},
+     * which are used by the callers as a cache when the {@code bandIndices} 
argument
+     * is equal to the values given the last time that this method has been 
invoked.
+     *
+     * All calls to this method should be indirect calls from {@link #read 
read(…)}
+     * through the {@link Subset} constructor. Therefore, this method relies 
on the
+     * error handling setup by {@code read(…)}.
      *
      * @param  bandIndices  indices of the selected bands.
      */
@@ -422,6 +430,9 @@ final class TiledResource extends TiledGridResource {
 
     /**
      * Returns the Java2D color model for rendering images.
+     * All calls to this method should be indirect calls from {@link #read 
read(…)}
+     * through the {@link Subset} constructor. Therefore, this method relies 
on the
+     * error handling setup by {@code read(…)}.
      */
     @Override
     protected ColorModel getColorModel(final int[] bandIndices) throws 
DataStoreException {
@@ -436,6 +447,10 @@ final class TiledResource extends TiledGridResource {
     /**
      * Returns the sample model for tiles at full resolution with all their 
bands.
      * The raster size is the {@linkplain #getTileSize() tile size} as stored 
in the resource.
+     *
+     * All calls to this method should be indirect calls from {@link #read 
read(…)} through the
+     * {@link Subset} constructor. Therefore, this method relies on the error 
handling setup by
+     * {@code read(…)}.
      */
     @Override
     protected SampleModel getSampleModel(final int[] bandIndices) throws 
DataStoreException {
@@ -451,6 +466,10 @@ final class TiledResource extends TiledGridResource {
      * Returns the values to use for filling empty spaces in rasters, with one 
value per band.
      * The returned array can be {@code null} if the fill values are not 
different than zero.
      * The zero value is excluded because tiles are already initialized to 
zero by default.
+     *
+     * All calls to this method should be indirect calls from {@link #read 
read(…)} through
+     * the {@link Subset} constructor. Therefore, this method relies on the 
error handling
+     * setup by {@code read(…)}.
      */
     @Override
     @SuppressWarnings("ReturnOfCollectionOrArrayField")
@@ -503,18 +522,21 @@ final class TiledResource extends TiledGridResource {
      * @param  aoi          region of the image to read or write. (0,0) is the 
upper-left pixel.
      * @param  raster       the Java2D raster where to store of fetch the 
values to read or write.
      * @param  bandIndices  bands of sample values in the Java2D raster, or 
{@code null} for all.
+     * @return whether the operation was successful according 
<abbr>GDAL</abbr>.
      * @throws ClassCastException if an above-documented prerequisite is not 
true.
      * @throws DataStoreException if <var>GDAL</var> reported a warning or 
fatal error.
      */
-    final void transfer(final int rwFlag, final Rectangle aoi, final Raster 
raster, final int[] bandIndices)
+    final boolean transfer(final int rwFlag, final Rectangle aoi, final Raster 
raster, final int[] bandIndices)
             throws DataStoreException
     {
         final GDAL gdal = parent.getProvider().GDAL();
         final int n = (bandIndices != null) ? bandIndices.length : 
bands.length;
+        boolean success = true;
         for (int i=0; i<n; i++) {
             final Band band = bands[(bandIndices != null) ? bandIndices[i] : 
i];
-            band.transfer(gdal, rwFlag, this, aoi, raster, i);
+            success &= band.transfer(gdal, rwFlag, this, aoi, raster, i);
         }
+        return success;
     }
 
     /**
@@ -528,9 +550,13 @@ final class TiledResource extends TiledGridResource {
     @Override
     public GridCoverage read(final GridGeometry domain, final int... ranges) 
throws DataStoreException {
         synchronized (getSynchronizationLock()) {
-            final var subset = new Subset(domain, ranges);
-            final var result = new TiledCoverage(this, subset);
-            return preload(result);
+            try {
+                final var subset = new Subset(domain, ranges);
+                final var result = new TiledCoverage(this, subset);
+                return preload(result);
+            } finally {
+                ErrorHandler.report(parent, "read");
+            }
         }
     }
 }
diff --git 
a/incubator/src/org.apache.sis.storage.gdal/main/org/apache/sis/storage/panama/NativeFunctions.java
 
b/incubator/src/org.apache.sis.storage.gdal/main/org/apache/sis/storage/panama/NativeFunctions.java
index 0f8209980b..f92b831fd9 100644
--- 
a/incubator/src/org.apache.sis.storage.gdal/main/org/apache/sis/storage/panama/NativeFunctions.java
+++ 
b/incubator/src/org.apache.sis.storage.gdal/main/org/apache/sis/storage/panama/NativeFunctions.java
@@ -44,6 +44,8 @@ public abstract class NativeFunctions implements Runnable {
     /**
      * The arena used for loading the library, or {@code null} for the global 
arena.
      * This is the arena to close for unloading the library.
+     *
+     * @see #arena()
      */
     private final Arena arena;
 
@@ -63,6 +65,13 @@ public abstract class NativeFunctions implements Runnable {
         symbols     = loader.symbols;
     }
 
+    /**
+     * Returns the arena used for loading the library.
+     */
+    protected final Arena arena() {
+        return (arena != null) ? arena : Arena.global();
+    }
+
     /**
      * Returns the method handler for the <abbr>GDAL</abbr> function of given 
name and signature.
      * This is a convenience method for initialization of fields in subclasses.


Reply via email to