sis-commits mailing list archives

Site index · List index
Message view « Date » · « Thread »
Top « Date » · « Thread »
From desruisse...@apache.org
Subject [sis] 02/02: Move in a separated class (LocalDataSource) the code from Initializer.getDataSource() which was managing a local installation of the database. With this move, it is easier to manage an alternative database (HSQLDB) in addition to Derby for local storage.
Date Sun, 08 Mar 2020 23:43:13 GMT
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 6ff3fd24cdd16c83fd33c9316abfa9311d060811
Author: Martin Desruisseaux <martin.desruisseaux@geomatys.com>
AuthorDate: Mon Mar 9 00:41:43 2020 +0100

    Move in a separated class (LocalDataSource) the code from Initializer.getDataSource()
which was managing a local installation of the database.
    With this move, it is easier to manage an alternative database (HSQLDB) in addition to
Derby for local storage.
---
 .../sis/internal/metadata/sql/Initializer.java     | 249 +++-------
 .../sis/internal/metadata/sql/LocalDataSource.java | 510 +++++++++++++++++++++
 .../java/org/apache/sis/test/sql/TestDatabase.java |   4 +-
 .../apache/sis/internal/system/DataDirectory.java  |   2 +-
 .../org/apache/sis/internal/system/Shutdown.java   |   5 +-
 .../sql/feature/QuerySpliteratorsBench.java        |   5 +-
 6 files changed, 576 insertions(+), 199 deletions(-)

diff --git a/core/sis-metadata/src/main/java/org/apache/sis/internal/metadata/sql/Initializer.java
b/core/sis-metadata/src/main/java/org/apache/sis/internal/metadata/sql/Initializer.java
index b191d7c..2e50271 100644
--- a/core/sis-metadata/src/main/java/org/apache/sis/internal/metadata/sql/Initializer.java
+++ b/core/sis-metadata/src/main/java/org/apache/sis/internal/metadata/sql/Initializer.java
@@ -19,17 +19,11 @@ package org.apache.sis.internal.metadata.sql;
 import java.util.Locale;
 import java.util.function.Supplier;
 import java.util.concurrent.Callable;
-import java.net.URL;
-import java.net.URLClassLoader;
-import java.nio.file.Files;
-import java.nio.file.Path;
-import java.nio.file.Paths;
 import java.io.IOException;
 import java.util.logging.Level;
 import java.util.logging.LogRecord;
 import java.security.AccessController;
 import java.security.PrivilegedAction;
-import java.lang.reflect.Method;
 import javax.sql.DataSource;
 import java.sql.Connection;
 import java.sql.DatabaseMetaData;
@@ -72,7 +66,7 @@ import org.apache.sis.util.Configuration;
  * All other methods are related to getting the {@code DataSource} instance, through JNDI
or otherwise.
  *
  * @author  Martin Desruisseaux (Geomatys)
- * @version 1.0
+ * @version 1.1
  * @since   0.7
  * @module
  */
@@ -87,11 +81,6 @@ public abstract class Initializer {
     public static final String DATABASE = "SpatialMetadata";
 
     /**
-     * The property name for the home of Derby databases.
-     */
-    private static final String DERBY_HOME_KEY = "derby.system.home";
-
-    /**
      * Name of the JNDI resource to lookup in the {@code "java:comp/env"} context.
      */
     public static final String JNDI = "jdbc/" + DATABASE;
@@ -102,18 +91,6 @@ public abstract class Initializer {
     public static final String EMBEDDED = "Embedded";
 
     /**
-     * The class loader for JavaDB (i.e. the Derby database distributed with the JDK), created
when first needed.
-     * This field is never reset to {@code null} even if the classpath changed because this
class loader is for
-     * a JAR file the JDK installation directory, and we presume that the JDK installation
do not change.
-     *
-     * @see #forJavaDB(String)
-     *
-     * @deprecated to be removed after we migrate to Java 9, since Derby is no longer distributed
with the JDK.
-     */
-    @Deprecated
-    private static URLClassLoader javadbLoader;
-
-    /**
      * Data source specified by the user, to be used if no data source is specified by JNDI.
      *
      * @see #setDefault(Supplier)
@@ -184,12 +161,18 @@ public abstract class Initializer {
 
         /**
          * Invoked when the JVM is shutting down, or when the Servlet or OSGi bundle is uninstalled.
-         * This method unregisters the listener from the JNDI context.
+         * This method forgets the data source and unregisters the listener from the JNDI
context.
+         * Note that there is no need to shutdown a Derby or HDQLDB engine since this shutdown
is
+         * only for {@link DataSource} obtained from JNDI context, in which case shuting
down the
+         * database engine should be container job.
+         *
+         * @see Initializer#shutdown()
          */
         @Override
         public Object call() throws NamingException {
             synchronized (Initializer.class) {
-                // Do not clear the DataSource - the shutdown hook for Derby needs it.
+                source = null;
+                connected = false;
                 context.removeNamingListener(this);
             }
             return null;
@@ -197,25 +180,21 @@ public abstract class Initializer {
 
         /**
          * Invoked when the data source associated to {@code "jdbc/SpatialMetadata"} changed.
-         * This method clears the {@link Initializer#source}, unregisters this listener
-         * and notifies other SIS modules.
+         * This method clears the {@link Initializer#source}, unregisters this listener (a
new one
+         * will be registered later if a new data source is created) and notifies other SIS
modules.
          *
          * @param  event  ignored. Can be null.
          */
         @Override
         public void objectChanged(NamingEvent event) {
+            Shutdown.unregister(this);
             try {
-                synchronized (Initializer.class) {
-                    source = null;
-                    connected = false;
-                    Shutdown.unregister(this);
-                    context.removeNamingListener(this);
-                }
+                call();
             } catch (NamingException e) {
                 /*
                  * Not a fatal error since the listener may be unregistered anyway, or may
be unregistered
                  * automatically by other kinds of JNDI events. Even if the listener is not
unregistered,
-                 * it will hurt to badly: the DataSource would only be fetched more often
than necessary.
+                 * it will not hurt too badly: the DataSource would only be fetched more
often than necessary.
                  */
                 Logging.recoverableException(Logging.getLogger(Loggers.SYSTEM), Listener.class,
"objectChanged", e);
             }
@@ -227,7 +206,7 @@ public abstract class Initializer {
         /**
          * Invoked if JNDI lost connection to the server while preparing the {@code NamingEvent}.
          * Clears the data source anyway. In the worst case scenario, the application will
fetch
-         * it again from a the JNDI context.
+         * it again from the JNDI context.
          */
         @Override
         public void namingExceptionThrown(NamingExceptionEvent event) {
@@ -243,6 +222,9 @@ public abstract class Initializer {
      * in order to let users control their data source. This method does nothing if the data
source has
      * already been initialized.
      *
+     * <p>{@code Initializer} will not register any shutdown process for user-supplied
data source.
+     * We presume that database life cycle is managed by the caller.</p>
+     *
      * @param  ds  supplier of data source to set, or {@code null} for removing previous
supplier.
      *             This supplier may return {@code null}, in which case it will be ignored.
      * @return whether the given data source supplier has been successfully set.
@@ -280,6 +262,8 @@ public abstract class Initializer {
      *   <li>Otherwise (no JNDI, no environment variable, no Derby property set), {@code
null}.</li>
      * </ol>
      *
+     * The Derby database may be replaced by a HSQLDB database in above steps.
+     *
      * @return the data source for the {@code $SIS_DATA/Databases/SpatialMetadata} or equivalent
database, or {@code null} if none.
      * @throws javax.naming.NamingException     if an error occurred while fetching the data
source from a JNDI context.
      * @throws java.net.MalformedURLException   if an error occurred while converting the
{@code derby.jar} file to URL.
@@ -306,7 +290,7 @@ public abstract class Initializer {
                     /*
                      * No Derby shutdown hook for DataSource fetched from JNDI.
                      * We presume that shutdowns are handled by the container.
-                     * We do not clear the 'supplier' field in case 'source'
+                     * We do not clear the `supplier` field in case `source`
                      * is cleaned by the listener.
                      */
                 }
@@ -319,7 +303,8 @@ public abstract class Initializer {
             /*
              * At this point we determined that there is no JNDI context or no object binded
to "jdbc/SpatialMetadata".
              * Check for programmatically supplied data source. We verify only after JNDI
in order to let users control
-             * their data source if desired.
+             * their data source if desired. We do not provide shutdown hook for user-supplied
data source; we presume
+             * that users manage themselves their database life cycle.
              */
             if (supplier != null) {
                 source = supplier.get();
@@ -330,85 +315,47 @@ public abstract class Initializer {
             }
             /*
              * As a fallback, try to open the Derby database located in $SIS_DATA/Databases/SpatialMetadata
directory.
-             * Only if the SIS_DATA environment variable is not set, verify first if the
'sis-embedded-data' module is
+             * Only if the SIS_DATA environment variable is not set, verify first if the
`sis-embedded-data` module is
              * on the classpath. Note that if SIS_DATA is defined and valid, it has precedence.
              */
-            boolean create = false;
-            final boolean isEnvClear = DataDirectory.isEnvClear();
-            if (!isEnvClear || (source = embedded()) == null) {
-                final String home = AccessController.doPrivileged((PrivilegedAction<String>)
() -> System.getProperty(DERBY_HOME_KEY));
-                final Path dir = DataDirectory.DATABASES.getDirectory();
-                final String dbURL;
-                if (dir != null) {
-                    Path path = dir.resolve(DATABASE);
-                    if (home != null) try {
-                        /*
-                         * If a "derby.system.home" property is set, we may be able to get
a shorter path by making it
-                         * relative to Derby home. The intent is to have a nicer URL like
"jdbc:derby:SpatialMetadata"
-                         * instead than "jdbc:derby:/a/long/path/to/SIS/Data/Databases/SpatialMetadata".
In addition
-                         * to making loggings and EPSGDataAccess.getAuthority() output nicer,
it also reduces the risk
-                         * of encoding issues if the path contains spaces or non-ASCII characters.
-                         */
-                        path = Paths.get(home).relativize(path);
-                    } catch (IllegalArgumentException | SecurityException e) {
-                        // The path can not be relativized. This is okay.
-                        Logging.recoverableException(Logging.getLogger(Loggers.SQL), Initializer.class,
"getDataSource", e);
-                    }
-                    path   = path.normalize();
-                    create = !Files.exists(path);
-                    dbURL  = path.toString().replace(path.getFileSystem().getSeparator(),
"/");
-                } else if (home != null) {
-                    final Path path = Paths.get(home);
-                    create = !Files.exists(path.resolve(DATABASE)) && Files.isDirectory(path);
-                    dbURL  = DATABASE;
-                } else {
-                    create = true;
-                    dbURL  = null;
-                }
-                /*
-                 * If we need to create the database, verify if an embedded database is available
instead.
-                 * We perform this check only if we have not already checked for embedded
database at the
-                 * beginning of this block.
-                 */
-                if (create & !isEnvClear) {
-                    source = embedded();
-                    create = (source == null);
-                }
-                if (source == null) {
-                    if (dbURL == null) {
-                        return null;
-                    }
-                    /*
-                     * Create the Derby data source using the context class loader if possible,
-                     * or otherwise a URL class loader to the JavaDB distributed with the
JDK.
-                     */
-                    source = forJavaDB(dbURL);
+            DataSource        embedded   = null;
+            LocalDataSource[] candidates = null;
+            final boolean     isEnvClear = DataDirectory.isEnvClear();
+            if (isEnvClear) {
+                embedded = embedded();                  // Check embedded data first only
if SIS_DATA is not defined.
+            }
+            if (embedded == null) {
+                candidates = LocalDataSource.create(DATABASE, Dialect.DERBY, Dialect.HSQL);
    // Null or non-empty.
+                if (!isEnvClear && (candidates == null || candidates[0].create))
{
+                    // Check for embedded data only if not already checked and if no local
database already exists.
+                    embedded = embedded();
                 }
             }
+            if (embedded != null) {
+                source = LocalDataSource.wrap(embedded);
+            } else if (candidates != null) {
+                source = LocalDataSource.findDriver(candidates);
+            } else {
+                return null;
+            }
             supplier = null;        // Not needed anymore.
             /*
              * Register the shutdown hook before to attempt any operation on the database
in order to close
              * it properly if the schemas creation below fail.
              */
-            Shutdown.register(() -> {
-                shutdown();
-                return null;
-            });
+            if (source.isWrapperFor(LocalDataSource.class)) {
+                Shutdown.register(() -> {
+                    shutdown();
+                    return null;
+                });
+            }
             /*
              * If the database does not exist, create it. We allow creation only if we are
inside
              * the $SIS_DATA directory. The Java code creating the schemas is provided in
other
              * SIS modules. For example sis-referencing may create the EPSG dataset.
              */
-            if (create) {
-                final Method m = source.getClass().getMethod("setCreateDatabase", String.class);
-                m.invoke(source, "create");
-                try (Connection c = source.getConnection()) {
-                    for (Initializer init : DefaultFactories.createServiceLoader(Initializer.class))
{
-                        init.createSchema(c);
-                    }
-                } finally {
-                    m.invoke(source, "no");     // Any value other than "create".
-                }
+            if (source instanceof LocalDataSource) {
+                ((LocalDataSource) source).createDatabase();
             }
         }
         return source;
@@ -499,100 +446,20 @@ public abstract class Initializer {
     }
 
     /**
-     * Creates a data source for a Derby database at the given location. The location may
be either the
-     * {@code $SIS_DATA/Databases/SpatialMetadata} directory, or the {@code SpatialMetadata}
database
-     * in the directory given by the {@code derby.system.home} property.
-     *
-     * <p>This method does <strong>not</strong> create the database if
it does not exist, because this
-     * method does not know if we are inside the {@code $SIS_DATA} directory.</p>
-     *
-     * <p>It is caller's responsibility to shutdown the Derby database after usage.</p>
-     *
-     * @param  path  relative or absolute path to the database.
-     * @return the data source.
-     * @throws Exception if the data source can not be created.
-     */
-    private static DataSource forJavaDB(final String path) throws Exception {
-        try {
-            return forJavaDB(path, Thread.currentThread().getContextClassLoader());
-        } catch (ClassNotFoundException e) {
-            URLClassLoader loader;
-            synchronized (Initializer.class) {
-                loader = javadbLoader;
-                if (loader == null) {
-                    final String home = System.getProperty("java.home");
-                    if (home != null) {
-                        final Path file = Paths.get(home).resolveSibling("db/lib/derby.jar");
-                        if (Files.isRegularFile(file)) {
-                            javadbLoader = loader = new URLClassLoader(new URL[] {
-                                file.toUri().toURL(),
-                                file.resolveSibling("derbynet.jar").toUri().toURL()
-                            });
-                        }
-                    }
-                }
-            }
-            if (loader == null) {
-                throw e;
-            }
-            return forJavaDB(path, loader);
-        }
-    }
-
-    /**
-     * Creates a Derby data source for the given path using the given class loader.
-     * It is caller's responsibility to shutdown the Derby database after usage.
-     *
-     * @throws ClassNotFoundException if Derby is not on the classpath.
-     */
-    private static DataSource forJavaDB(final String path, final ClassLoader loader) throws
Exception {
-        final Class<?> c = Class.forName("org.apache.derby.jdbc.EmbeddedDataSource",
true, loader);
-        final DataSource ds = (DataSource) c.getConstructor().newInstance();
-        final Class<?>[] args = {String.class};
-        c.getMethod("setDatabaseName", args).invoke(ds, path);
-        c.getMethod("setDataSourceName", args).invoke(ds, "Apache SIS spatial metadata");
-        return ds;
-    }
-
-    /**
      * Invoked when the JVM is shutting down, or when the Servlet or OSGi bundle is uninstalled.
      * This method shutdowns the Derby database.
      *
      * @throws ReflectiveOperationException if an error occurred while
      *         setting the shutdown property on the Derby data source.
+     * @throws SQLException if call to {@link DataSource#unwrap(Class)} failed.
+     *         This exception should never happen since {@link #source} should always be
an instance of
+     *         {@link LocalDataSource} when this method is invoked, and {@link SQLException}
thrown by
+     *         the database are not propagated here.
      */
-    private static synchronized void shutdown() throws ReflectiveOperationException {
+    private static synchronized void shutdown() throws ReflectiveOperationException, SQLException
{
         final DataSource ds = source;
-        if (ds != null) {                       // Should never be null, but let be safe.
-            source = null;                      // Clear now in case of failure in remaining
code.
-            connected = false;
-            ds.getClass().getMethod("setShutdownDatabase", String.class).invoke(ds, "shutdown");
-            try {
-                ds.getConnection().close();     // Does the actual shutdown.
-            } catch (SQLException e) {          // This is the expected exception.
-                final LogRecord record = new LogRecord(Level.FINE, e.getMessage());
-                if (!isSuccessfulShutdown(e)) {
-                    record.setLevel(Level.WARNING);
-                    record.setThrown(e);
-                }
-                record.setLoggerName(Loggers.SQL);
-                Logging.log(Initializer.class, "shutdown", record);
-            }
-        }
-    }
-
-    /**
-     * Returns {@code true} if the given exception is the one that we expect in successful
shutdown of a Derby database.
-     *
-     * <div class="note"><b>Note:</b>
-     * this method is public for the needs of {@code non-free:sis-embedded-data} module.</div>
-     *
-     * @param  e  the exception thrown by Derby.
-     * @return {@code true} if the exception indicates a successful shutdown.
-     */
-    public static boolean isSuccessfulShutdown(final SQLException e) {
-        final String state = e.getSQLState();
-        return "08006".equals(state) ||     // Database 'SpatialMetadata' shutdown.
-               "XJ004".equals(state);       // Database 'SpatialMetadata' not found (may
happen if we failed to open it in the first place).
+        source    = null;                       // Clear now in case of failure in remaining
code.
+        connected = false;
+        ds.unwrap(LocalDataSource.class).shutdown();
     }
 }
diff --git a/core/sis-metadata/src/main/java/org/apache/sis/internal/metadata/sql/LocalDataSource.java
b/core/sis-metadata/src/main/java/org/apache/sis/internal/metadata/sql/LocalDataSource.java
new file mode 100644
index 0000000..81ac54c
--- /dev/null
+++ b/core/sis-metadata/src/main/java/org/apache/sis/internal/metadata/sql/LocalDataSource.java
@@ -0,0 +1,510 @@
+/*
+ * 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.internal.metadata.sql;
+
+import java.util.Arrays;
+import javax.sql.DataSource;
+import java.sql.Connection;
+import java.sql.Statement;
+import java.sql.SQLException;
+import java.sql.SQLFeatureNotSupportedException;
+import java.io.PrintWriter;
+import java.lang.reflect.Method;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.nio.file.Paths;
+import java.net.URL;
+import java.net.URLClassLoader;
+import java.util.logging.Level;
+import java.util.logging.Logger;
+import java.util.logging.LogRecord;
+import java.security.AccessController;
+import java.security.PrivilegedAction;
+import org.apache.sis.util.ArraysExt;
+import org.apache.sis.util.logging.Logging;
+import org.apache.sis.internal.system.Loggers;
+import org.apache.sis.internal.system.DataDirectory;
+import org.apache.sis.internal.system.DefaultFactories;
+import org.apache.sis.internal.util.Strings;
+
+
+/**
+ * A data source for a database stored locally in the {@code $SIS_DATA} directory.
+ * This class wraps the database-provided {@link DataSource} with the addition of a shutdown
method.
+ * It provides our {@linkplain #initialize() starting point} for initiating the system-wide
connection.
+ *
+ * @author  Martin Desruisseaux (Geomatys)
+ * @version 1.1
+ * @since   1.1
+ * @module
+ */
+public final class LocalDataSource implements DataSource, Comparable<LocalDataSource>
{
+    /**
+     * The property name for the home of Derby databases.
+     */
+    private static final String DERBY_HOME_KEY = "derby.system.home";
+
+    /**
+     * The class loader for JavaDB (i.e. the Derby database distributed with the JDK), created
when first needed.
+     * This field is never reset to {@code null} even if the classpath changed because this
class loader is for
+     * a JAR file the JDK installation directory, and we presume that the JDK installation
do not change.
+     *
+     * @see #initialize()
+     *
+     * @deprecated to be removed after we migrate to Java 9, since Derby is no longer distributed
with the JDK.
+     */
+    @Deprecated
+    private static URLClassLoader javadbLoader;
+
+    /**
+     * The database product to use. Currently supported values are
+     * {@link Dialect#DERBY} and {@link Dialect#HSQL}.
+     */
+    private final Dialect dialect;
+
+    /**
+     * Path to the database to open on the local file system. This is the argument to give
to the
+     * {@code DataSource.setDatabaseName(String)} method. This value is set to {@code null}
after
+     * {@link #source} creation because not needed anymore.
+     */
+    private String dbFile;
+
+    /**
+     * The database-provided data source.
+     */
+    private DataSource source;
+
+    /**
+     * Whether the database needs to be created.
+     */
+    final boolean create;
+
+    /**
+     * Prepares a new data source for the given database file. This construction is incomplete;
after return
+     * either {@link #initialize(ClassLoader)} shall be invoked or this {@code LocalDataSource}
is discarded.
+     *
+     * @param  dialect  {@link Dialect#DERBY} or {@link Dialect#HSQL}.
+     * @param  dbFile   path to the database to open on the local file system.
+     * @param  create   whether the database needs to be created.
+     */
+    private LocalDataSource(final Dialect dialect, final String dbFile, final boolean create)
{
+        this.dialect = dialect;
+        this.dbFile  = dbFile;
+        this.create  = create;
+    }
+
+    /**
+     * Prepares potential data source for the spatial metadata database. This constructor
prepares
+     * the path to local file(s), but actual {@link DataSource} construction must be done
by a call
+     * to {@link #initialize()} after construction.
+     *
+     * @param  database  database name (usually {@value Initializer#DATABASE}).
+     * @param  dialects  {@link Dialect#DERBY} and/or {@link Dialect#HSQL}.
+     * @return the local data sources (not yet initialized), or {@code null} if none.
+     *         If non-null, then the array is guaranteed to contain at least one element.
+     */
+    static LocalDataSource[] create(final String database, final Dialect... dialects) {
+        LocalDataSource[] sources = new LocalDataSource[dialects.length];
+        int count = 0;
+        for (final Dialect dialect : dialects) {
+            final String home;
+            switch (dialect) {
+                // More cases may be added in the future.
+                case DERBY: home = AccessController.doPrivileged((PrivilegedAction<String>)
() -> System.getProperty(DERBY_HOME_KEY)); break;
+                default:    home = null; break;
+            }
+            final String  dbFile;
+            final boolean create;
+            final Path dir = DataDirectory.DATABASES.getDirectory();
+            if (dir != null) {
+                /*
+                 * SIS_DATA directory defined: will search only there (no search in the Derby
home directory).
+                 * If a "derby.system.home" property is set, we may be able to get a shorter
path by making it
+                 * relative to Derby home. The intent is to have a nicer URL like "jdbc:derby:SpatialMetadata"
+                 * instead than "jdbc:derby:/a/long/path/to/SIS/Data/Databases/SpatialMetadata".
  In addition
+                 * to making loggings and EPSGDataAccess.getAuthority() output nicer, it
also reduces the risk
+                 * of encoding issues if the path contains spaces or non-ASCII characters.
+                 */
+                Path path = dir.resolve(database);
+                if (home != null) try {
+                    path = Paths.get(home).relativize(path);
+                } catch (IllegalArgumentException | SecurityException e) {
+                    // The path can not be relativized. This is okay.
+                    Logging.recoverableException(Logging.getLogger(Loggers.SQL), LocalDataSource.class,
"<init>", e);
+                }
+                path   = path.normalize();
+                dbFile = path.toString().replace(path.getFileSystem().getSeparator(), "/");
+                switch (dialect) {
+                    case HSQL: path = Paths.get(path.toString() + ".data"); break;
+                    // More cases may be added in the future.
+                }
+                create = !Files.exists(path);
+            } else if (home != null) {
+                /*
+                 * SIS_DATA not defined, but we may be able to fallback on "derby.system.home"
property.
+                 * This fallback is never executed if the SIS_DATA environment variable is
defined, even
+                 * if the database does not exist in that directory, because otherwise users
could define
+                 * SIS_DATA and get the impression that their setting is ignored.
+                 */
+                final Path path = Paths.get(home);
+                create = !Files.exists(path.resolve(database)) && Files.isDirectory(path);
+                dbFile = database;
+            } else {
+                continue;
+            }
+            sources[count++] = new LocalDataSource(dialect, dbFile, create);
+        }
+        /*
+         * Sort the data source in preference order, with already existing databases first.
+         * If there is no data source then we must return null, not an empty array.
+         */
+        if (count == 0) return null;
+        sources = ArraysExt.resize(sources, count);
+        Arrays.sort(sources);
+        return sources;
+    }
+
+    /**
+     * Wraps an existing data source for adding a shutdown method to it.
+     * This method is used for source of data embedded in a separated JAR file.
+     *
+     * @param  ds  the data source, usually given by {@link Initializer#embedded()}.
+     * @return the data source wrapped with a shutdown method, or {@code ds}.
+     */
+    static DataSource wrap(final DataSource ds) {
+        final Dialect dialect;
+        final String cn = ds.getClass().getName();
+        if (cn.startsWith("org.apache.derby.")) {
+            dialect = Dialect.DERBY;
+        } else if (cn.startsWith("org.hsqldb.")) {
+            dialect = Dialect.HSQL;
+        } else {
+            return ds;
+        }
+        final LocalDataSource local = new LocalDataSource(dialect, null, false);
+        local.source = ds;
+        return local;
+    }
+
+    /**
+     * Creates the data source using the given class loader.
+     * It is caller's responsibility to {@linkplain #shutdown() shutdown} the database after
usage.
+     *
+     * @param  loader  the loader to use for loading the data source class.
+     * @throws ClassNotFoundException if the database driver is not on the classpath.
+     * @throws ReflectiveOperationException if an error occurred
+     *         while setting the properties on the data source.
+     */
+    @SuppressWarnings("fallthrough")
+    private void initialize(final ClassLoader loader) throws ReflectiveOperationException
{
+        final String classname;
+        switch (dialect) {
+            case DERBY: classname = "org.apache.derby.jdbc.EmbeddedDataSource"; break;
+            case HSQL:  classname = "org.hsqldb.jdbc.JDBCDataSource"; break;
+            default:    throw new IllegalArgumentException(dialect.toString());
+        }
+        final Class<?> c = Class.forName(classname, true, loader);
+        source = (DataSource) c.getConstructor().newInstance();
+        final Class<?>[] args = {String.class};
+        switch (dialect) {
+            case DERBY: c.getMethod("setDataSourceName", args).invoke(source, "Apache SIS
spatial metadata");   // Fall through
+            case HSQL:  c.getMethod("setDatabaseName",   args).invoke(source, dbFile); break;
+        }
+        dbFile = null;          // Not needed anymore (let GC do its work).
+    }
+
+    /**
+     * Creates the data source using the context class loader if possible,
+     * or otherwise a URL class loader to the JavaDB distributed with the JDK.
+     *
+     * @throws ClassNotFoundException if the database driver is not on the classpath.
+     * @throws ReflectiveOperationException if an error occurred
+     *         while setting the properties on the data source.
+     * @throws java.net.MalformedURLException if the class loader for JavaDB can not be created.
+     */
+    private void initialize() throws Exception {
+        try {
+            initialize(Thread.currentThread().getContextClassLoader());
+        } catch (ClassNotFoundException e) {
+            if (dialect != Dialect.DERBY) {
+                throw e;
+            }
+            URLClassLoader loader;
+            synchronized (LocalDataSource.class) {
+                loader = javadbLoader;
+                if (loader == null) {
+                    final String home = System.getProperty("java.home");
+                    if (home != null) {
+                        final Path file = Paths.get(home).resolveSibling("db/lib/derby.jar");
+                        if (Files.isRegularFile(file)) {
+                            javadbLoader = loader = new URLClassLoader(new URL[] {
+                                file.toUri().toURL(),
+                                file.resolveSibling("derbynet.jar").toUri().toURL()
+                            });
+                        }
+                    }
+                }
+            }
+            if (loader == null) {
+                throw e;
+            }
+            initialize(loader);
+        }
+    }
+
+    /**
+     * Returns the first data source from the given array that can be initialized.
+     * The database may be located in the {@code $SIS_DATA/Databases/SpatialMetadata} directory,
+     * or in the {@code SpatialMetadata} sub-directory of the path given by the {@code derby.system.home}
property.
+     *
+     * <p>This method does <strong>not</strong> create the database if
it does not exist,
+     * because this method does not know if we are inside the {@code $SIS_DATA} directory.</p>
+     *
+     * <p>It is caller's responsibility to {@linkplain #shutdown() shutdown} the database
after usage.</p>
+     *
+     * @param  sources  the data sources to try.
+     * @return the first data source for which a driver is available.
+     * @throws ClassNotFoundException if no database driver is not on the classpath.
+     * @throws Exception if the operation failed for another reason.
+     */
+    static LocalDataSource findDriver(final LocalDataSource[] sources) throws Exception {
+        ClassNotFoundException fail = null;
+        for (final LocalDataSource local : sources) try {
+            local.initialize();
+            return local;
+        } catch (ClassNotFoundException e) {
+            if (fail == null) fail = e;
+            else fail.addSuppressed(e);
+        }
+        throw fail;
+    }
+
+    /**
+     * Creates the database if needed. For Derby we need to explicitly allows creation.
+     * For HSQLDB the creation is enabled by default.
+     */
+    final void createDatabase() throws Exception {
+        if (create) {
+            final Method enabler;
+            switch (dialect) {
+                case DERBY: {
+                    enabler = source.getClass().getMethod("setCreateDatabase", String.class);
+                    enabler.invoke(source, "create");
+                    break;
+                }
+                // More cases may be added in future versions.
+                default: enabler = null; break;
+            }
+            try (Connection c = source.getConnection()) {
+                for (Initializer init : DefaultFactories.createServiceLoader(Initializer.class))
{
+                    init.createSchema(c);
+                }
+            } finally {
+                switch (dialect) {
+                    // More cases may be added in future versions.
+                    case DERBY: enabler.invoke(source, "no"); break;        // Any value
other than "create".
+                }
+            }
+        }
+    }
+
+    /**
+     * Shutdowns the database used by this data source.
+     *
+     * @throws ReflectiveOperationException if an error occurred while
+     *         setting the shutdown property on the Derby data source.
+     */
+    final void shutdown() throws ReflectiveOperationException {
+        try {
+            switch (dialect) {
+                case HSQL: {
+                    try (Connection c = source.getConnection(); Statement stmt = c.createStatement())
{
+                        stmt.execute(create ? "SHUTDOWN COMPACT" : "SHUTDOWN");
+                    }
+                    break;
+                }
+                case DERBY: {
+                    source.getClass().getMethod("setShutdownDatabase", String.class).invoke(source,
"shutdown");
+                    source.getConnection().close();             // Does the actual shutdown.
+                    break;
+                }
+            }
+        } catch (SQLException e) {                              // This is the expected exception.
+            final LogRecord record = new LogRecord(Level.FINE, e.getMessage());
+            if (dialect != Dialect.DERBY || !isSuccessfulShutdown(e)) {
+                record.setLevel(Level.WARNING);
+                record.setThrown(e);
+            }
+            record.setLoggerName(Loggers.SQL);
+            Logging.log(LocalDataSource.class, "shutdown", record);
+        }
+    }
+
+    /**
+     * Returns {@code true} if the given exception is the one that we expect in successful
shutdown of a Derby database.
+     * While this method is primarily used for Derby shutdown, the error code tested may
be applicable to other systems.
+     *
+     * <div class="note"><b>Note:</b>
+     * this method is public for the needs of {@code non-free:sis-embedded-data} module.</div>
+     *
+     * @param  e  the exception thrown by Derby.
+     * @return {@code true} if the exception indicates a successful shutdown.
+     */
+    public static boolean isSuccessfulShutdown(final SQLException e) {
+        final String state = e.getSQLState();
+        return "08006".equals(state) ||     // Database "SpatialMetadata" shutdown.
+               "XJ004".equals(state);       // Database "SpatialMetadata" not found (may
happen if we failed to open it in the first place).
+    }
+
+    /**
+     * Compares this data source with the given one for preference order.
+     * The preferred data sources are the ones for a database that already exists.
+     *
+     * @param  other  the other data source to compare with this one.
+     * @return -1 if this data source is preferred to {@code other},
+     *         +1 if {@code other} is preferred to {@code this}, or
+     *          0 if no preference.
+     */
+    @Override
+    public int compareTo(final LocalDataSource other) {
+        return Boolean.compare(create, other.create);
+    }
+
+    /**
+     * Returns whether {@link #unwrap(Class)} can be invoked for the given type.
+     *
+     * @param  type   the interface or implementation type of desired wrapped object.
+     * @return whether {@link #unwrap(Class)} can be invoked for the given type.
+     * @throws SQLException if an error occurs while checking wrappers.
+     */
+    @Override
+    public boolean isWrapperFor(final Class<?> type) throws SQLException {
+        return (type == LocalDataSource.class) || source.isWrapperFor(type);
+    }
+
+    /**
+     * Returns an object of the given type to allow access to non-standard methods.
+     * The type can be either {@link LocalDataSource} or any type supported by the
+     * wrapped data source.
+     *
+     * @param  <T>   compile-time value of {@code type}.
+     * @param  type  the interface or implementation type of desired wrapped object.
+     * @return an object of the given type.
+     * @throws SQLException if there is no object of the given type.
+     */
+    @Override
+    @SuppressWarnings("unchecked")
+    public <T> T unwrap(final Class<T> type) throws SQLException {
+        if (type == LocalDataSource.class) {
+            return (T) this;
+        }
+        return source.unwrap(type);
+    }
+
+    /**
+     * Attempts to establish a connection.
+     *
+     * @return a connection to the locally installed database.
+     * @throws SQLException if a database access error occurs.
+     */
+    @Override
+    public Connection getConnection() throws SQLException {
+        return source.getConnection();
+    }
+
+    /**
+     * Attempts to establish a connection.
+     *
+     * @param  username  the database user.
+     * @param  password  the user's password.
+     * @return a connection to the locally installed database.
+     * @throws SQLException if a database access error occurs.
+     */
+    @Override
+    public Connection getConnection(String username, String password) throws SQLException
{
+        return source.getConnection(username, password);
+    }
+
+    /**
+     * Returns the maximum time in seconds that this data source will wait while attempting
to connect to a database.
+     * Initial value is 0, meaning default timeout or no timeout.
+     *
+     * @return the data source login time limit, or 0 for the default.
+     * @throws SQLException if a database access error occurs.
+     */
+    @Override
+    public int getLoginTimeout() throws SQLException {
+        return source.getLoginTimeout();
+    }
+
+    /**
+     * Sets the maximum time in seconds that this data source will wait while attempting
to connect to a database.
+     *
+     * @param  seconds   the data source login time limit, or 0 for the default.
+     * @throws SQLException if a database access error occurs.
+     */
+    @Override
+    public void setLoginTimeout(int seconds) throws SQLException {
+        source.setLoginTimeout(seconds);
+    }
+
+    /**
+     * Return the parent of all loggers used by this data source.
+     * Can be used for configuring log messages.
+     *
+     * @return the parent of all loggers used by this data source.
+     * @throws SQLFeatureNotSupportedException if the data source does not use logging.
+     */
+    @Override
+    public Logger getParentLogger() throws SQLFeatureNotSupportedException {
+        return source.getParentLogger();
+    }
+
+    /**
+     * Returns the output stream to which all logging and tracing messages for this data
source will be printed.
+     * The default writer is {@code null} (logging disabled).
+     *
+     * @return the log writer, or null if logging is disabled.
+     * @throws SQLException if a database access error occurs.
+     */
+    @Override
+    public PrintWriter getLogWriter() throws SQLException {
+        return source.getLogWriter();
+    }
+
+    /**
+     * Sets the output stream to which all logging and tracing messages for this data source
will be printed.
+     * This method needs to be invoked for enabling logging.
+     *
+     * @param out the log writer, or null if logging is disabled.
+     * @throws SQLException if a database access error occurs.
+     */
+    @Override
+    public void setLogWriter(PrintWriter out) throws SQLException {
+        source.setLogWriter(out);
+    }
+
+    /**
+     * Returns a string representation for debugging purpose.
+     *
+     * @return an arbitrary string representation.
+     */
+    @Override
+    public String toString() {
+        return Strings.toString(getClass(), null, dialect, "dbFile", dbFile, "source", source);
+    }
+}
diff --git a/core/sis-metadata/src/test/java/org/apache/sis/test/sql/TestDatabase.java b/core/sis-metadata/src/test/java/org/apache/sis/test/sql/TestDatabase.java
index 625817f..a0f78e8 100644
--- a/core/sis-metadata/src/test/java/org/apache/sis/test/sql/TestDatabase.java
+++ b/core/sis-metadata/src/test/java/org/apache/sis/test/sql/TestDatabase.java
@@ -28,7 +28,7 @@ import org.postgresql.ds.PGSimpleDataSource;
 import org.hsqldb.jdbc.JDBCDataSource;
 import org.hsqldb.jdbc.JDBCPool;
 import org.apache.derby.jdbc.EmbeddedDataSource;
-import org.apache.sis.internal.metadata.sql.Initializer;
+import org.apache.sis.internal.metadata.sql.LocalDataSource;
 import org.apache.sis.internal.metadata.sql.ScriptRunner;
 import org.apache.sis.test.TestCase;
 import org.apache.sis.util.Debug;
@@ -126,7 +126,7 @@ public strictfp class TestDatabase implements AutoCloseable {
                 try {
                     ds.getConnection().close();
                 } catch (SQLException e) {                          // This is the expected
exception.
-                    if (!Initializer.isSuccessfulShutdown(e)) {
+                    if (!LocalDataSource.isSuccessfulShutdown(e)) {
                         throw e;
                     }
                 }
diff --git a/core/sis-utility/src/main/java/org/apache/sis/internal/system/DataDirectory.java
b/core/sis-utility/src/main/java/org/apache/sis/internal/system/DataDirectory.java
index ef8c7f0..0dccacf 100644
--- a/core/sis-utility/src/main/java/org/apache/sis/internal/system/DataDirectory.java
+++ b/core/sis-utility/src/main/java/org/apache/sis/internal/system/DataDirectory.java
@@ -147,7 +147,7 @@ public enum DataDirectory {
 
     /**
      * Returns {@code true} if the {@value #ENV} environment variable is unset. In case of
doubt, this method
-     * returns {@code false}. This method is used for avoiding or at leat delaying the log
messages emitted by
+     * returns {@code false}. This method is used for avoiding or at least delaying the log
messages emitted by
      * {@link #getRootDirectory()} when a fallback exists in absence of any user attempt
to configure the system.
      *
      * @return {@code true} if the {@value #ENV} environment variable is unset.
diff --git a/core/sis-utility/src/main/java/org/apache/sis/internal/system/Shutdown.java b/core/sis-utility/src/main/java/org/apache/sis/internal/system/Shutdown.java
index 004754c..088b9fc 100644
--- a/core/sis-utility/src/main/java/org/apache/sis/internal/system/Shutdown.java
+++ b/core/sis-utility/src/main/java/org/apache/sis/internal/system/Shutdown.java
@@ -101,8 +101,8 @@ public final class Shutdown extends Thread {
     }
 
     /**
-     * Registers a code to execute at JVM shutdown time. The resources will be disposed at
-     * shutdown time in reverse order (most recently added resources will be disposed first).
+     * Registers a code to execute at JVM shutdown time, or if SIS library is unloaded from
a container such as OSGi.
+     * The resources will be disposed at shutdown time in reverse order (most recently added
resources disposed first).
      *
      * <p>The same resource shall not be added twice.</p>
      *
@@ -149,6 +149,7 @@ public final class Shutdown extends Thread {
 
     /**
      * Unregisters the supervisor MBean, executes the disposal tasks and shutdowns the {@code
sis-utility} threads.
+     * This method may be invoked at JVM shutdown, or if a container like OSGi is unloaded
the SIS library.
      *
      * @param  caller  the class invoking this method, to be used only for logging purpose,
or {@code null}
      *         if the logging system is not available anymore (i.e. the JVM itself is shutting
down).
diff --git a/storage/sis-sqlstore/src/test/java/org/apache/sis/internal/sql/feature/QuerySpliteratorsBench.java
b/storage/sis-sqlstore/src/test/java/org/apache/sis/internal/sql/feature/QuerySpliteratorsBench.java
index 7c1e60b..8abc26f 100644
--- a/storage/sis-sqlstore/src/test/java/org/apache/sis/internal/sql/feature/QuerySpliteratorsBench.java
+++ b/storage/sis-sqlstore/src/test/java/org/apache/sis/internal/sql/feature/QuerySpliteratorsBench.java
@@ -23,9 +23,8 @@ import java.sql.SQLException;
 import java.util.Random;
 import java.util.concurrent.TimeUnit;
 
-import org.apache.sis.internal.metadata.sql.Initializer;
-
 import org.apache.derby.jdbc.EmbeddedDataSource;
+import org.apache.sis.internal.metadata.sql.LocalDataSource;
 
 import org.openjdk.jmh.annotations.Benchmark;
 import org.openjdk.jmh.annotations.Fork;
@@ -119,7 +118,7 @@ public class QuerySpliteratorsBench {
             try {
                 db.getConnection().close();
             } catch (SQLException e) {                          // This is the expected exception.
-                if (!Initializer.isSuccessfulShutdown(e)) {
+                if (!LocalDataSource.isSuccessfulShutdown(e)) {
                     throw e;
                 }
             }


Mime
View raw message