You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@sis.apache.org by de...@apache.org on 2026/10/02 10:10:05 UTC

(sis) branch geoapi-4.0 updated (e242306664 -> b625838d76)

This is an automated email from the ASF dual-hosted git repository.

asf-gitbox-commits pushed a change to branch geoapi-4.0
in repository https://gitbox.apache.org/repos/asf/sis.git


    from e242306664 Post-merge automatic reorganization of imports order.
     new 8ff703b6c6 Security recommendation for MySQL/MariaDB. Fix an error message in netCDF reader.
     new 8bdb0c9430 When opening a folder as a DataStore, exclude auxiliary files from the list of resources.
     new b625838d76 Inflate the content of `sis-embedded-data` in a directory as a workaround for HDSQLDB slowness with embedded JAR.

The 3 revisions listed above as "new" are entirely new to this
repository and will be described in separate emails.  The revisions
listed as "add" were already present in the repository and have only
been added to this reference.


Summary of changes:
 .../metadata/sql/internal/shared/SQLBuilder.java   |  26 ++--
 .../org/apache/sis/storage/netcdf/NetcdfStore.java |   2 +-
 .../sis/storage/netcdf/NetcdfStoreProvider.java    |   6 +-
 .../sis/storage/netcdf/classic/ChannelDecoder.java |   7 +-
 .../apache/sis/storage/netcdf/package-info.java    |   2 +-
 .../main/module-info.java                          |  13 +-
 .../org/apache/sis/storage/sql/package-info.java   |   7 +-
 .../apache/sis/storage/esri/RawRasterStore.java    |   2 +
 .../main/org/apache/sis/storage/folder/Store.java  | 139 +++++++++++++--------
 .../sis/resources/embedded/EmbeddedResources.java  | 124 +++++++++++++++++-
 .../resources/embedded/EmbeddedResourcesTest.java  |  42 ++++++-
 .../apache/sis/resources/embedded/Generator.java   |   8 +-
 12 files changed, 298 insertions(+), 80 deletions(-)


(sis) 02/03: When opening a folder as a DataStore, exclude auxiliary files from the list of resources.

Posted by de...@apache.org.
This is an automated email from the ASF dual-hosted git repository.

asf-gitbox-commits pushed a commit to branch geoapi-4.0
in repository https://gitbox.apache.org/repos/asf/sis.git

commit 8bdb0c9430847594b355f0d2dac42ad76f19adf9
Author: Martin Desruisseaux <ma...@geomatys.com>
AuthorDate: Wed Sep 30 16:53:27 2026 +0900

    When opening a folder as a DataStore, exclude auxiliary files from the list of resources.
---
 .../apache/sis/storage/esri/RawRasterStore.java    |   2 +
 .../main/org/apache/sis/storage/folder/Store.java  | 139 +++++++++++++--------
 2 files changed, 91 insertions(+), 50 deletions(-)

diff --git a/endorsed/src/org.apache.sis.storage/main/org/apache/sis/storage/esri/RawRasterStore.java b/endorsed/src/org.apache.sis.storage/main/org/apache/sis/storage/esri/RawRasterStore.java
index a165413074..6bc1e08763 100644
--- a/endorsed/src/org.apache.sis.storage/main/org/apache/sis/storage/esri/RawRasterStore.java
+++ b/endorsed/src/org.apache.sis.storage/main/org/apache/sis/storage/esri/RawRasterStore.java
@@ -550,9 +550,11 @@ final class RawRasterStore extends RasterStore {
      * @throws DataStoreException if an error occurred while closing this data store.
      */
     @Override
+    @SuppressWarnings("ConvertToTryWithResources")
     public void close() throws DataStoreException {
         try {
             listeners.close();                      // Should never fail.
+            @SuppressWarnings("LocalVariableHidesMemberVariable")
             final ChannelDataInput input = this.input;
             if (input != null) try {
                 input.channel.close();
diff --git a/endorsed/src/org.apache.sis.storage/main/org/apache/sis/storage/folder/Store.java b/endorsed/src/org.apache.sis.storage/main/org/apache/sis/storage/folder/Store.java
index 6d377432cd..d30871166d 100644
--- a/endorsed/src/org.apache.sis.storage/main/org/apache/sis/storage/folder/Store.java
+++ b/endorsed/src/org.apache.sis.storage/main/org/apache/sis/storage/folder/Store.java
@@ -18,7 +18,7 @@ package org.apache.sis.storage.folder;
 
 import java.util.Map;
 import java.util.List;
-import java.util.ArrayList;
+import java.util.LinkedHashMap;
 import java.util.Collection;
 import java.util.Optional;
 import java.util.logging.Level;
@@ -109,14 +109,14 @@ class Store extends URIDataStore implements StoreResource, UnstructuredAggregate
      *
      * @see #getMetadata()
      */
-    private transient Metadata metadata;
+    private Metadata metadata;
 
     /**
      * Resources in the folder given at construction time, created when first needed.
      *
      * @see #components()
      */
-    transient Collection<Resource> components;
+    Collection<Resource> components;
 
     /**
      * The provider to use for probing the directory content, opening files and creating new files.
@@ -126,10 +126,10 @@ class Store extends URIDataStore implements StoreResource, UnstructuredAggregate
     protected final DataStoreProvider componentProvider;
 
     /**
-     * {@code true} if {@link #sharedRepository(Path)} has already been invoked for {@link #locationAsPath}.
+     * {@code true} if {@link #sharedDirectoryReported()} has already been invoked for {@link #locationAsPath}.
      * This is used for avoiding to report the same message many times.
      */
-    private transient boolean sharedRepositoryReported;
+    private boolean sharedDirectoryReported;
 
     /**
      * A structured view of this aggregate, or {@code null} if not net computed.
@@ -137,7 +137,7 @@ class Store extends URIDataStore implements StoreResource, UnstructuredAggregate
      *
      * @see #getStructuredView()
      */
-    private transient Resource structuredView;
+    private Resource structuredView;
 
     /**
      * Creates a new folder store from the given file, path or URI.
@@ -283,51 +283,50 @@ class Store extends URIDataStore implements StoreResource, UnstructuredAggregate
     @SuppressWarnings("ReturnOfCollectionOrArrayField")
     public synchronized Collection<Resource> components() throws DataStoreException {
         if (components == null) {
-            final var resources = new ArrayList<DataStore>();
+            final var resources = new LinkedHashMap<Path, DataStore>();  // May contain null values for auxiliary files.
             final var nameFactory = DefaultNameFactory.provider();
+            DataStoreException closeErrors = null;
             try (DirectoryStream<Path> stream = Files.newDirectoryStream(locationAsPath, this)) {
-                for (final Path candidate : stream) {
+                for (final Path childPath : stream) {
+                    final Path realPath = childPath.toRealPath();
+                    if (resources.containsKey(realPath)) {
+                        // Resource already added as an auxiliary file of another resource (for example, a ".prj" file).
+                        continue;
+                    }
                     /*
-                     * The candidate path may be a symbolic link to a file that we have previously read.
-                     * In such case, use the existing data store.   A use case is a directory containing
-                     * hundred of GeoTIFF files all accompanied by ".prj" files having identical content.
-                     * (Note: those ".prj" files should be invisible since they should be identified as
-                     * GeoTIFF auxiliary files, but current Store implementation does not know that).
+                     * The path of the child may be a symbolic link to a file that we have already read
+                     * in another directory. In such case, use (actually share) the existing data store.
                      */
-                    final Path real = candidate.toRealPath();
-                    DataStore next = children.get(real);
-                    if (next instanceof Store) {
-                        ((Store) next).sharedRepository(real);          // Warn about directories only.
-                    }
-                    if (next == null) {
+                    DataStore child = children.get(realPath);
+                    if (child == null) {
                         /*
                          * The candidate file has never been read before. Try to read it now.
-                         * If the file format is unknown (UnsupportedStorageException), we will
-                         * check if we can open it as a child folder store before to skip it.
+                         * If the file format is unknown (UnsupportedStorageException),
+                         * try to open the path as a child folder store.
                          */
-                        final StorageConnector connector = new StorageConnector(configuration, candidate);
+                        final var connector = new StorageConnector(configuration, childPath);
                         connector.setOption(OptionKey.PARENT_LISTENERS, listeners);
                         connector.setOption(OptionKey.OPEN_OPTIONS, new StandardOpenOption[] {
                             StandardOpenOption.READ         // Restrict to read-only mode.
                         });
                         try {
                             if (componentProvider == null) {
-                                next = DataStores.open(connector);          // May throw UnsupportedStorageException.
+                                child = DataStores.open(connector);          // May throw UnsupportedStorageException.
                             } else if (componentProvider.probeContent(connector).isSupported()) {
-                                next = componentProvider.open(connector);   // Open a file of specified format.
-                            } else if (Files.isDirectory(candidate)) {
-                                next = new Store(this, connector, nameFactory);        // Open a sub-directory.
+                                child = componentProvider.open(connector);   // Open a file of specified format.
+                            } else if (Files.isDirectory(childPath)) {
+                                child = new Store(this, connector, nameFactory);    // Open a sub-directory.
                             } else {
                                 connector.closeAllExcept(null);             // Not the format specified at construction time.
                                 continue;
                             }
                         } catch (UnsupportedStorageException ex) {
-                            if (!Files.isDirectory(candidate)) {
+                            if (!Files.isDirectory(childPath)) {
                                 connector.closeAllExcept(null);
                                 listeners.warning(Level.FINE, null, ex);
                                 continue;
                             }
-                            next = new Store(this, connector, nameFactory);
+                            child = new Store(this, connector, nameFactory);
                         } catch (DataStoreException ex) {
                             try {
                                 connector.closeAllExcept(null);
@@ -336,20 +335,26 @@ class Store extends URIDataStore implements StoreResource, UnstructuredAggregate
                             }
                             throw ex;
                         }
-                        /*
-                         * At this point we got the data store. It could happen that a store for
-                         * the same file has been added concurrently, so we need to check again.
-                         */
-                        final DataStore existing = children.putIfAbsent(real, next);
-                        if (existing != null) {
-                            next.close();
-                            next = existing;
-                            if (next instanceof Store) {
-                                ((Store) next).sharedRepository(real);      // Warn about directories only.
+                    }
+                    /*
+                     * If the resource has auxiliary files and if we opened some of them as standalone resources,
+                     * close and exclude the latter. The null value is for remembering to not open that file again.
+                     */
+                    resources.put(realPath, child);
+                    for (final Path auxiliary : child.getFileSet().map(FileSet::getPaths).orElse(List.of())) {
+                        if (!realPath.equals(auxiliary)) {
+                            final DataStore existing = resources.put(auxiliary.toRealPath(), null);
+                            if (existing != null && !children.containsKey(realPath)) try {
+                                existing.close();
+                            } catch (DataStoreException ex) {
+                                if (closeErrors == null) {
+                                    closeErrors = ex;
+                                } else {
+                                    closeErrors.addSuppressed(ex);
+                                }
                             }
                         }
                     }
-                    resources.add(next);
                 }
             } catch (DirectoryIteratorException | UncheckedIOException ex) {
                 // The cause is an IOException (no other type allowed).
@@ -359,7 +364,45 @@ class Store extends URIDataStore implements StoreResource, UnstructuredAggregate
             } catch (BackingStoreException ex) {
                 throw ex.unwrapOrRethrow(DataStoreException.class);
             }
-            components = Containers.copyToImmutableList(resources, Resource.class);
+            /*
+             * Finished the iteration over all directory entries. Remove auxiliary files (null values).
+             * For non-null values, check if the store has already been opened in another directory.
+             * It may happen if the directory tree contains symbolic links.
+             */
+            for (var it = resources.entrySet().iterator(); it.hasNext();) {
+                final Map.Entry<Path, DataStore> entry = it.next();
+                final DataStore next = entry.getValue();
+                if (next == null) {
+                    it.remove();
+                    continue;
+                }
+                final Path realPath = entry.getKey();
+                final DataStore existing = children.putIfAbsent(realPath, next);
+                if (existing != null) {
+                    entry.setValue(existing);
+                    /*
+                     * Logs a warning if the path is a directory which has already been read.
+                     * It may happen if the directory tree contains symbolic links.
+                     * The warning will be reported one time per directory.
+                     */
+                    if (existing instanceof Store && ((Store) existing).sharedDirectoryReported()) {
+                        listeners.warning(message(Resources.Keys.SharedDirectory_1, realPath));
+                    }
+                    try {
+                        next.close();
+                    } catch (DataStoreException ex) {
+                        if (closeErrors == null) {
+                            closeErrors = ex;
+                        } else {
+                            closeErrors.addSuppressed(ex);
+                        }
+                    }
+                }
+            }
+            components = Containers.copyToImmutableList(resources.values(), Resource.class);
+            if (closeErrors != null) {
+                listeners.warning(Level.FINE, null, closeErrors);
+            }
         }
         return components;              // Safe because unmodifiable list.
     }
@@ -372,17 +415,13 @@ class Store extends URIDataStore implements StoreResource, UnstructuredAggregate
     }
 
     /**
-     * Logs a warning about a file that could be read, but happen to be a directory that we have read previously.
-     * We could add the existing {@link Aggregate} instance in the parent {@code Aggregate} that we are building,
-     * but doing so may create a cycle. Current version logs a warning instead because users may not be prepared
-     * to handle cycles. Note that we have no guarantee that a cycle really exists at this stage, only that it may
-     * exist.
+     * Whether to logs a warning about a file that could be read,
+     * but happens to be a directory that we have read before.
      */
-    private void sharedRepository(final Path candidate) {
-        if (!sharedRepositoryReported) {
-            sharedRepositoryReported = true;
-            listeners.warning(message(Resources.Keys.SharedDirectory_1, candidate));
-        }
+    private synchronized boolean sharedDirectoryReported() {
+        final boolean r = sharedDirectoryReported;
+        sharedDirectoryReported = true;
+        return r;
     }
 
     /**


(sis) 03/03: Inflate the content of `sis-embedded-data` in a directory as a workaround for HDSQLDB slowness with embedded JAR.

Posted by de...@apache.org.
This is an automated email from the ASF dual-hosted git repository.

asf-gitbox-commits pushed a commit to branch geoapi-4.0
in repository https://gitbox.apache.org/repos/asf/sis.git

commit b625838d766223c9cba17d0ec2fdbd959efa5972
Author: Martin Desruisseaux <ma...@geomatys.com>
AuthorDate: Wed Sep 30 19:58:42 2026 +0900

    Inflate the content of `sis-embedded-data` in a directory
    as a workaround for HDSQLDB slowness with embedded JAR.
---
 .../sis/resources/embedded/EmbeddedResources.java  | 124 ++++++++++++++++++++-
 .../resources/embedded/EmbeddedResourcesTest.java  |  42 ++++++-
 .../apache/sis/resources/embedded/Generator.java   |   8 +-
 3 files changed, 164 insertions(+), 10 deletions(-)

diff --git a/optional/src/org.apache.sis.referencing.database/main/org/apache/sis/resources/embedded/EmbeddedResources.java b/optional/src/org.apache.sis.referencing.database/main/org/apache/sis/resources/embedded/EmbeddedResources.java
index 51b9becd2a..31dd1da8e6 100644
--- a/optional/src/org.apache.sis.referencing.database/main/org/apache/sis/resources/embedded/EmbeddedResources.java
+++ b/optional/src/org.apache.sis.referencing.database/main/org/apache/sis/resources/embedded/EmbeddedResources.java
@@ -16,17 +16,29 @@
  */
 package org.apache.sis.resources.embedded;
 
+import java.util.ArrayList;
 import java.util.Set;
 import java.util.Locale;
 import java.util.StringJoiner;
+import java.util.logging.Logger;
 import java.io.BufferedReader;
+import java.io.FileNotFoundException;
 import java.io.InputStreamReader;
 import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.nio.file.DirectoryStream;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.nio.file.StandardOpenOption;
 import javax.sql.DataSource;
 import org.hsqldb.jdbc.JDBCDataSource;
 import org.apache.sis.util.internal.shared.MetadataServices;
 import org.apache.sis.metadata.sql.internal.shared.Initializer;
 import org.apache.sis.setup.InstallationResources;
+import org.apache.sis.system.DataDirectory;
+import org.apache.sis.util.Workaround;
+import org.apache.sis.util.logging.Logging;
 import org.apache.sis.util.resources.Errors;
 
 
@@ -50,6 +62,28 @@ public class EmbeddedResources extends InstallationResources {
      */
     static final String DIRECTORY = "SIS-DATA";
 
+    /**
+     * The directory of embedded databases.
+     * This is a sub-directory of {@link #DIRECTORY}.
+     */
+    static final String DATABASES = "Databases";
+
+    /**
+     * Suffixes of all files generated by HSQLDB in the database directory.
+     */
+    static String[] databaseSuffixes() {
+        return new String[] {"properties", "script", "data"};
+    }
+
+    /**
+     * Whether to copy the HSQLDB files to a temporary directory.
+     * This is a workaround for HSQLDB slowness as embedded database.
+     *
+     * @see <a href="https://sourceforge.net/p/hsqldb/bugs/1758/">HSQLDB issue 1758</a>
+     */
+    @Workaround(library = "HSQLDB", version = "2.7.4")
+    private static final boolean COPY_TO_TMP = true;
+
     /**
      * The data source, created when first needed.
      * Also set to a different value during tests.
@@ -128,6 +162,10 @@ public class EmbeddedResources extends InstallationResources {
 
     /**
      * Returns the data source for embedded database.
+     * If the {@code SIS_DATA} environment variable is defined and does not contain {@code SpatialMetadata} files,
+     * the embedded data will be copied in that directory and the returned {@code DataSource} will use these files.
+     * This copy is done because database operations are faster on ordinary files compared to entries embedded in a
+     * <abbr>JAR</abbr> file.
      *
      * @param  authority  shall be {@code "Embedded"}.
      * @param  index      shall be 0.
@@ -143,15 +181,35 @@ public class EmbeddedResources extends InstallationResources {
              * be the case when using a local build.
              */
             if (dataSource == null && EmbeddedResources.class.getResourceAsStream("LICENSE.txt") != null) {
-                final var ds = new JDBCDataSource();
-                ds.setDatabaseName(Initializer.DATABASE);
-                ds.setURL("jdbc:hsqldb:res:" + DIRECTORY + "/Databases/" + Initializer.DATABASE);
-                dataSource = ds;
+                if (!tryInstall(DataDirectory.DATABASES.getDirectory(), false)) {
+                    if (COPY_TO_TMP) try {
+                        if (tryInstall(Files.createTempDirectory(DATABASES), true)) {
+                            return dataSource;
+                        }
+                    } catch (IOException e) {
+                        Logging.recoverableException(
+                                Logger.getLogger("org.apache.sis.referencing.database"),
+                                EmbeddedResources.class, "getResource", e);
+                    }
+                    createDataSource("jdbc:hsqldb:res:" + DIRECTORY + '/' + DATABASES + '/' + Initializer.DATABASE);
+                }
             }
             return dataSource;
         }
     }
 
+    /**
+     * Creates the data source.
+     *
+     * @param  path  path to the database, starting with the {@code "jdbc:hsqldb:"} prefix.
+     */
+    private static void createDataSource(final String path) {
+        final var ds = new JDBCDataSource();
+        ds.setDatabaseName(Initializer.DATABASE);
+        ds.setURL(path);
+        dataSource = ds;
+    }
+
     /**
      * Unconditionally throws an exception since the embedded database is not provided as <abbr>SQL</abbr> scripts.
      *
@@ -165,4 +223,62 @@ public class EmbeddedResources extends InstallationResources {
         verifyAuthority(authority);
         throw new IOException(Errors.format(Errors.Keys.CanNotConvertFromType_2, DataSource.class, BufferedReader.class));
     }
+
+    /**
+     * Copies the database files in the given directory if not already present.
+     *
+     * @param  directory     the directory where to copy database files.
+     * @param  deleteOnExit  whether to delete the file when the JVM exit.
+     * @return whether the data have been copied as a data source created.
+     */
+    @SuppressWarnings("UseSpecificCatch")
+    private static boolean tryInstall(final Path directory, final boolean deleteOnExit) {
+        if (directory != null) try {
+            final boolean exists;
+            try (DirectoryStream<Path> content = Files.newDirectoryStream(directory,
+                    (file) -> file.getFileName().toString().startsWith(Initializer.DATABASE)))
+            {
+                exists = content.iterator().hasNext();
+            }
+            if (!exists) {
+                final ClassLoader loader = EmbeddedResources.class.getClassLoader();
+                final var delete = new ArrayList<Path>(4);
+                try {
+                    // The array shall contain the suffix of all files generated by HSQLDB.
+                    for (final String suffix : databaseSuffixes()) {
+                        final String name = Initializer.DATABASE + '.' + suffix;
+                        final String path = DIRECTORY + '/' + DATABASES + '/' + name;
+                        try (InputStream in = loader.getResourceAsStream(path)) {
+                            if (in == null) {
+                                throw new FileNotFoundException(path);
+                            }
+                            final Path target = directory.resolve(name);
+                            try (OutputStream out = Files.newOutputStream(target, StandardOpenOption.CREATE_NEW)) {
+                                delete.add(target);     // File have been created, delete on failure.
+                                in.transferTo(out);
+                            }
+                        }
+                    }
+                } catch (Throwable ex) {
+                    for (Path path : delete) try {
+                        Files.delete(path);
+                    } catch (IOException e) {
+                        ex.addSuppressed(e);
+                    }
+                    throw ex;
+                }
+                if (COPY_TO_TMP && deleteOnExit) {  // This parameter is useful only for the workaround.
+                    delete.add(0, directory);
+                    delete.forEach((path) -> path.toFile().deleteOnExit());
+                }
+                createDataSource("jdbc:hsqldb:file:" + directory.resolve(Initializer.DATABASE));
+                return true;
+            }
+        } catch (IOException e) {
+            Logging.unexpectedException(
+                    Logger.getLogger("org.apache.sis.referencing.database"),
+                    EmbeddedResources.class, "getResource", e);
+        }
+        return false;
+    }
 }
diff --git a/optional/src/org.apache.sis.referencing.database/test/org/apache/sis/resources/embedded/EmbeddedResourcesTest.java b/optional/src/org.apache.sis.referencing.database/test/org/apache/sis/resources/embedded/EmbeddedResourcesTest.java
index e5948f847e..602e23c177 100644
--- a/optional/src/org.apache.sis.referencing.database/test/org/apache/sis/resources/embedded/EmbeddedResourcesTest.java
+++ b/optional/src/org.apache.sis.referencing.database/test/org/apache/sis/resources/embedded/EmbeddedResourcesTest.java
@@ -16,12 +16,17 @@
  */
 package org.apache.sis.resources.embedded;
 
+import java.net.URL;
 import java.io.IOException;
+import java.nio.file.DirectoryStream;
+import java.nio.file.Files;
+import java.nio.file.Path;
 import java.sql.Connection;
 import java.sql.ResultSet;
 import java.sql.Statement;
 import javax.sql.DataSource;
 import java.util.Map;
+import java.util.HashSet;
 import java.util.ServiceLoader;
 import org.opengis.referencing.crs.CoordinateReferenceSystem;
 import org.apache.sis.setup.InstallationResources;
@@ -41,7 +46,7 @@ import static org.apache.sis.test.Assertions.assertSingleton;
 
 /**
  * Tests {@link EmbeddedResources}.
- * This test has the side-effect of creating the database if it does not already exists.
+ * This test has the side-effect of creating the database if it does not already exist.
  *
  * @author  Martin Desruisseaux (Geomatys)
  */
@@ -111,6 +116,38 @@ public final class EmbeddedResourcesTest {
         assertTrue(provider.getLicense("Embedded", null, "text/html" ).contains("IOGP"));
     }
 
+    /**
+     * Verifies that the database directories contains the file expected by {@code tryInstall(…)}.
+     * Verifies also that there is no extra files. If extra files are found, maybe they should be
+     * declared in the {@code tryInstall(…)} method.
+     *
+     * @throws Exception if an error occurred while verifying the directory content.
+     */
+    @Test
+    public void testDatabaseFiles() throws Exception {
+        assumeContainsEPSG();
+        getInstance();
+        URL resource = null;
+        final var expected = new HashSet<String>();
+        final ClassLoader loader = EmbeddedResources.class.getClassLoader();
+        for (String suffix : EmbeddedResources.databaseSuffixes()) {
+            final String name = Initializer.DATABASE + '.' + suffix;
+            final String path = EmbeddedResources.DIRECTORY + '/'
+                              + EmbeddedResources.DATABASES + '/'
+                              + name;
+            resource = loader.getResource(path);
+            assertNotNull(resource, path);
+            assertTrue(expected.add(name));
+        }
+        try (DirectoryStream<Path> list = Files.newDirectoryStream(Path.of(resource.toURI()).getParent())) {
+            for (Path file : list) {
+                assertTrue(expected.remove(file.getFileName().toString()),
+                        () -> "File not declared in `databaseSuffixes()`:" + file);
+            }
+        }
+        assertTrue(expected.isEmpty());
+    }
+
     /**
      * Tests connecting to the database.
      *
@@ -127,7 +164,8 @@ public final class EmbeddedResourcesTest {
         final DataSource ds = Initializer.getDataSource();
         assertNotNull(ds, "Cannot find the data source.");
         try (Connection c = ds.getConnection()) {
-            assertEquals("jdbc:hsqldb:res:" + EmbeddedResources.DIRECTORY + "/Databases/" + Initializer.DATABASE, c.getMetaData().getURL(), "URL");
+            assertEquals("jdbc:hsqldb:res:" + EmbeddedResources.DIRECTORY + '/' + EmbeddedResources.DATABASES + '/' + Initializer.DATABASE,
+                         c.getMetaData().getURL(), "URL");
             try (Statement s = c.createStatement()) {
                 try (ResultSet r = s.executeQuery("SELECT COORD_REF_SYS_NAME FROM EPSG.\"Coordinate Reference System\" WHERE COORD_REF_SYS_CODE = 4326")) {
                     assertTrue(r.next(), "ResultSet.next()");
diff --git a/optional/src/org.apache.sis.referencing.database/test/org/apache/sis/resources/embedded/Generator.java b/optional/src/org.apache.sis.referencing.database/test/org/apache/sis/resources/embedded/Generator.java
index 09166f172b..a505b5148a 100644
--- a/optional/src/org.apache.sis.referencing.database/test/org/apache/sis/resources/embedded/Generator.java
+++ b/optional/src/org.apache.sis.referencing.database/test/org/apache/sis/resources/embedded/Generator.java
@@ -64,7 +64,7 @@ final class Generator extends ScriptProvider {
     /**
      * Provides a connection to the "SpatialMetadata" database.
      * The connection <abbr>URL</abbr> references the following directory in the compilation output directory:
-     * <code>{@value EmbeddedResources#DIRECTORY}/Databases/{@value Initializer#DATABASE}</code>
+     * <code>{@value EmbeddedResources#DIRECTORY}/{@value EmbeddedResources#DATABASES}/{@value Initializer#DATABASE}</code>
      */
     private final JDBCDataSource dataSource;
 
@@ -80,15 +80,15 @@ final class Generator extends ScriptProvider {
         while (!target.getFileName().toString().startsWith("org.apache.sis."));
         target = target.resolve(EmbeddedResources.DIRECTORY);
         if (Files.isDirectory(target)) {
-            target = target.resolve("Databases");
+            target = target.resolve(EmbeddedResources.DATABASES);
             sourceEPSG = null;
         } else {
             // We don't use `Files.createDirectories(…)` for safety against creation of undesirable directories.
             target = Files.createDirectory(target);
-            target = Files.createDirectory(target.resolve("Databases"));
+            target = Files.createDirectory(target.resolve(EmbeddedResources.DATABASES));
             sourceEPSG = directoryOf(ScriptProvider.class);
         }
-        dataSource.setURL("jdbc:hsqldb:file:" + target.resolve(Initializer.DATABASE).toString());
+        dataSource.setURL("jdbc:hsqldb:file:" + target.resolve(Initializer.DATABASE));
     }
 
     /**


(sis) 01/03: Security recommendation for MySQL/MariaDB. Fix an error message in netCDF reader.

Posted by de...@apache.org.
This is an automated email from the ASF dual-hosted git repository.

asf-gitbox-commits pushed a commit to branch geoapi-4.0
in repository https://gitbox.apache.org/repos/asf/sis.git

commit 8ff703b6c65fd6e95214639400e01548c4ca79c4
Author: Martin Desruisseaux <ma...@geomatys.com>
AuthorDate: Fri Oct 2 12:06:19 2026 +0200

    Security recommendation for MySQL/MariaDB.
    Fix an error message in netCDF reader.
---
 .../metadata/sql/internal/shared/SQLBuilder.java   | 26 ++++++++++++++++------
 .../org/apache/sis/storage/netcdf/NetcdfStore.java |  2 +-
 .../sis/storage/netcdf/NetcdfStoreProvider.java    |  6 ++---
 .../sis/storage/netcdf/classic/ChannelDecoder.java |  7 +++---
 .../apache/sis/storage/netcdf/package-info.java    |  2 +-
 .../main/module-info.java                          | 13 ++++++++---
 .../org/apache/sis/storage/sql/package-info.java   |  7 +++++-
 7 files changed, 43 insertions(+), 20 deletions(-)

diff --git a/endorsed/src/org.apache.sis.metadata/main/org/apache/sis/metadata/sql/internal/shared/SQLBuilder.java b/endorsed/src/org.apache.sis.metadata/main/org/apache/sis/metadata/sql/internal/shared/SQLBuilder.java
index 01e4015e7f..2455bff4f5 100644
--- a/endorsed/src/org.apache.sis.metadata/main/org/apache/sis/metadata/sql/internal/shared/SQLBuilder.java
+++ b/endorsed/src/org.apache.sis.metadata/main/org/apache/sis/metadata/sql/internal/shared/SQLBuilder.java
@@ -167,18 +167,25 @@ public class SQLBuilder extends Syntax {
      * Appends an identifier between quote characters.
      * If the identifier contains quotes, the quotes will be doubled.
      *
+     * @todo Replace by {@link Connection#enquoteIdentifier(String, boolean)}
+     * when we will be allowed to compile for JDK26.
+     *
      * @param  name  the identifier to append.
      * @return this builder, for method call chaining.
      */
     public final SQLBuilder appendIdentifier(final String name) {
-        int i = buffer.append(identifierQuote).length();
-        buffer.append(name);
-        while ((i = buffer.indexOf(identifierQuote, i)) >= 0) {
-            final int n = identifierQuote.length();
-            buffer.insert(i += n, identifierQuote);
-            i += n;
+        if (identifierQuote.isEmpty()) {
+            buffer.append(name);
+        } else {
+            int i = buffer.append(identifierQuote).length();
+            buffer.append(name);
+            while ((i = buffer.indexOf(identifierQuote, i)) >= 0) {
+                final int n = identifierQuote.length();
+                buffer.insert(i += n, identifierQuote);
+                i += n;
+            }
+            buffer.append(identifierQuote);
         }
-        buffer.append(identifierQuote);
         return this;
     }
 
@@ -205,6 +212,8 @@ public class SQLBuilder extends Syntax {
      * The name part is quoted only if {@code quoteName} is {@code true}.
      * Unquoted names are useful when the name is for built-in functions,
      * which often use the lower/upper case convention of the database.
+     * In the latter case, the caller must ensure that the {@code name}
+     * does not contain the quote character.
      *
      * <h4>Simplification</h4>
      * If the given catalog is equal to the {@linkplain Connection#getCatalog() catalog which was current} when
@@ -262,6 +271,9 @@ public class SQLBuilder extends Syntax {
      * Appends a value in a {@code SELECT} or {@code INSERT} statement.
      * The value is written between quotes.
      *
+     * @todo Replace by {@link Connection#enquoteLiteral(String)} when we will be allowed to compile for JDK26.
+     * This is important for MySQL/MariaDB, which may use the backslash as an escaping character.
+     *
      * @param  value  the value to append, or {@code null}.
      * @return this builder, for method call chaining.
      */
diff --git a/endorsed/src/org.apache.sis.storage.netcdf/main/org/apache/sis/storage/netcdf/NetcdfStore.java b/endorsed/src/org.apache.sis.storage.netcdf/main/org/apache/sis/storage/netcdf/NetcdfStore.java
index 48793ae1ae..eee38a13fa 100644
--- a/endorsed/src/org.apache.sis.storage.netcdf/main/org/apache/sis/storage/netcdf/NetcdfStore.java
+++ b/endorsed/src/org.apache.sis.storage.netcdf/main/org/apache/sis/storage/netcdf/NetcdfStore.java
@@ -60,7 +60,7 @@ import org.apache.sis.util.internal.shared.Constants;
  * Instances of this data store are created by {@link NetcdfStoreProvider#open(StorageConnector)}.
  *
  * @author  Martin Desruisseaux (Geomatys)
- * @version 1.6
+ * @version 1.7
  *
  * @see NetcdfStoreProvider
  *
diff --git a/endorsed/src/org.apache.sis.storage.netcdf/main/org/apache/sis/storage/netcdf/NetcdfStoreProvider.java b/endorsed/src/org.apache.sis.storage.netcdf/main/org/apache/sis/storage/netcdf/NetcdfStoreProvider.java
index 2af0c516bb..32cdbce872 100644
--- a/endorsed/src/org.apache.sis.storage.netcdf/main/org/apache/sis/storage/netcdf/NetcdfStoreProvider.java
+++ b/endorsed/src/org.apache.sis.storage.netcdf/main/org/apache/sis/storage/netcdf/NetcdfStoreProvider.java
@@ -67,7 +67,7 @@ import org.apache.sis.util.internal.shared.Constants;
  * the part of the caller. However, the {@link NetcdfStore} instances created by this factory are not thread-safe.
  *
  * @author  Martin Desruisseaux (Geomatys)
- * @version 1.4
+ * @version 1.7
  *
  * @see NetcdfStore
  *
@@ -315,10 +315,8 @@ public class NetcdfStoreProvider extends DataStoreProvider {
                 keepOpen = path;
             } catch (IOException | DataStoreException s) {
                 e.addSuppressed(s);
-                throw e;
-            } else {
-                throw e;
             }
+            throw e;
         } else {
             keepOpen = connector.getStorage();
             decoder = createByReflection(keepOpen, true, geomlib, listeners);
diff --git a/endorsed/src/org.apache.sis.storage.netcdf/main/org/apache/sis/storage/netcdf/classic/ChannelDecoder.java b/endorsed/src/org.apache.sis.storage.netcdf/main/org/apache/sis/storage/netcdf/classic/ChannelDecoder.java
index 0eaba588bf..82a4e7d459 100644
--- a/endorsed/src/org.apache.sis.storage.netcdf/main/org/apache/sis/storage/netcdf/classic/ChannelDecoder.java
+++ b/endorsed/src/org.apache.sis.storage.netcdf/main/org/apache/sis/storage/netcdf/classic/ChannelDecoder.java
@@ -44,6 +44,7 @@ import org.opengis.parameter.InvalidParameterCardinalityException;
 import org.apache.sis.storage.DataStore;
 import org.apache.sis.storage.DataStoreException;
 import org.apache.sis.storage.DataStoreContentException;
+import org.apache.sis.storage.UnsupportedEncodingException;
 import org.apache.sis.storage.metadata.MetadataBuilder;
 import org.apache.sis.storage.netcdf.base.DataType;
 import org.apache.sis.storage.netcdf.base.Decoder;
@@ -244,7 +245,7 @@ public final class ChannelDecoder extends Decoder {
          */
         int version = input.readInt();
         if ((version & 0xFFFFFF00) != MAGIC_NUMBER) {
-            throw new DataStoreContentException(errors().getString(Errors.Keys.UnexpectedFileFormat_2, FORMAT_NAME, getFilename()));
+            throw new UnsupportedEncodingException(errors().getString(Errors.Keys.UnexpectedFileFormat_2, FORMAT_NAME, getFilename()));
         }
         /*
          * Check the version number.
@@ -253,7 +254,7 @@ public final class ChannelDecoder extends Decoder {
         switch (version) {
             case 1:  is64bits = false; break;
             case 2:  is64bits = true;  break;
-            default: throw new DataStoreContentException(errors().getString(Errors.Keys.UnsupportedFormatVersion_2, FORMAT_NAME, version));
+            default: throw new UnsupportedEncodingException(errors().getString(Errors.Keys.UnsupportedFormatVersion_2, FORMAT_NAME, version));
             // If more cases are added, remember to increment the MAX_VERSION constant.
         }
         numrecs = input.readInt();
@@ -432,7 +433,7 @@ public final class ChannelDecoder extends Decoder {
             return;
         }
         args[0] = tagPath(tagName(tag));
-        throw new DataStoreContentException(errors().getString(key, tagPath(tagName(tag))));
+        throw new DataStoreContentException(errors().getString(key, args));
     }
 
     /**
diff --git a/endorsed/src/org.apache.sis.storage.netcdf/main/org/apache/sis/storage/netcdf/package-info.java b/endorsed/src/org.apache.sis.storage.netcdf/main/org/apache/sis/storage/netcdf/package-info.java
index b0885b3270..f7dff8776b 100644
--- a/endorsed/src/org.apache.sis.storage.netcdf/main/org/apache/sis/storage/netcdf/package-info.java
+++ b/endorsed/src/org.apache.sis.storage.netcdf/main/org/apache/sis/storage/netcdf/package-info.java
@@ -40,7 +40,7 @@
  * Care must be taken for avoiding confusion when using SIS and UCAR libraries together.
  *
  * @author  Martin Desruisseaux (IRD, Geomatys)
- * @version 1.6
+ * @version 1.7
  * @since   0.3
  */
 package org.apache.sis.storage.netcdf;
diff --git a/endorsed/src/org.apache.sis.storage.sql/main/module-info.java b/endorsed/src/org.apache.sis.storage.sql/main/module-info.java
index bb0104a6d9..416941c47d 100644
--- a/endorsed/src/org.apache.sis.storage.sql/main/module-info.java
+++ b/endorsed/src/org.apache.sis.storage.sql/main/module-info.java
@@ -21,12 +21,12 @@
  * <a href="https://www.ogc.org/standards/sfs">OGC Simple feature access - Part 2: SQL option</a>
  * international standard, also known as <abbr>ISO</abbr> 19125-2.
  *
- * <h2>Difference with Geopackage</h2>
+ * <h2>Differences with Geopackage</h2>
  * Compared to the <abbr>OGC</abbr> Geopackage standard,
  * this <abbr>SQL</abbr> module has the following differences:
  *
  * <ul>
- *   <li>There is no discovery mechanism (e.g., no {@code "gpkg_contents"} table).
+ *   <li>There is no {@code "gpkg_contents"} table.
  *       The tables to use as {@linkplain org.apache.sis.storage.sql.ResourceDefinition resource definitions}
  *       must be specified explicitly.</li>
  *   <li>Each feature table can contain an arbitrary number of geometry columns, including zero.
@@ -38,11 +38,18 @@
  *       (made of many columns). By contrast, Geopackage mandates primary keys made of exactly one column of integers.</li>
  * </ul>
  *
+ * <h2>Recommended database configuration</h2>
+ * <p><b>PostgreSQL</b> databases should have the PostGIS extension installed (optional but recommended).</p>
+ *
+ * <p><b>MySQL/MariaDB</b> databases should set the <abbr>SQL</abbr> mode to at least {@code NO_BACKSLASH_ESCAPES}.
+ * See <a href="https://mariadb.com/docs/server/server-management/variables-and-modes/sql_mode#no_backslash_escapes">
+ * MariaDB documentation</a>.</p>
+ *
  * @author  Johann Sorel (Geomatys)
  * @author  Martin Desruisseaux (Geomatys)
  * @author  Alexis Manin (Geomatys)
  * @author  Guilhem Legal (Geomatys)
- * @version 1.6
+ * @version 1.7
  * @since   1.0
  */
 module org.apache.sis.storage.sql {
diff --git a/endorsed/src/org.apache.sis.storage.sql/main/org/apache/sis/storage/sql/package-info.java b/endorsed/src/org.apache.sis.storage.sql/main/org/apache/sis/storage/sql/package-info.java
index c764390369..4d00813196 100644
--- a/endorsed/src/org.apache.sis.storage.sql/main/org/apache/sis/storage/sql/package-info.java
+++ b/endorsed/src/org.apache.sis.storage.sql/main/org/apache/sis/storage/sql/package-info.java
@@ -53,10 +53,15 @@
  * those other features are created at the same time as the parent feature. There is no lazy instantiation yet.
  * Performances should be okay if each parent feature references only a small amount of children.</p>
  *
+ * <h2>Security</h2>
+ * MySQL/MariaDB databases should set the <abbr>SQL</abbr> mode to at least {@code NO_BACKSLASH_ESCAPES}.
+ * See <a href="https://mariadb.com/docs/server/server-management/variables-and-modes/sql_mode#no_backslash_escapes">
+ * MariaDB documentation</a>.
+ *
  * @author  Johann Sorel (Geomatys)
  * @author  Martin Desruisseaux (Geomatys)
  * @author  Alexis Manin (Geomatys)
- * @version 1.6
+ * @version 1.7
  * @since   1.0
  */
 package org.apache.sis.storage.sql;