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)); } /**
