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

Reply via email to