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.