EmbeddedDerbyResource.java
/*
* Copyright 2015 Development Entropy (deventropy.org) Contributors
*
* Licensed 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.deventropy.junithelper.derby;
import java.io.Closeable;
import java.io.File;
import java.io.FileWriter;
import java.io.IOException;
import java.nio.charset.Charset;
import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.SQLException;
import java.util.Properties;
import org.apache.commons.io.FileUtils;
import org.apache.commons.io.IOUtils;
import org.apache.logging.log4j.Level;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.deventropy.junithelper.derby.util.DerbyBackupOperationsHelper;
import org.deventropy.junithelper.derby.util.DerbyScriptRunner;
import org.deventropy.junithelper.derby.util.DerbyUtils;
import org.deventropy.shared.utils.ArgumentCheck;
import org.junit.rules.ExternalResource;
import org.junit.rules.TemporaryFolder;
/**
* Provides an in-memory Derby resource. An instance of this class is initialized with the
* {@link DerbyResourceConfig configuration} and a {@link #getDerbySystemHome() Derby System Home}.
*
* <p>The class can be used either as a JUnit {@link org.junit.Rule Rule} / {@link org.junit.ClassRule ClassRule} OR
* directly by the user. Initialization and de-initialization of an instance of this class (and the embedded Derby
* instance) is handed by the {@link #start()} and {@link #close()} methods respectively. When used as a
* <code>Rule</code> or <code>ClassRule</code>, the {@link #before()} and {@link #after()} mdthods from those interfaces
* handle the initialization and de-initialization for the user (internally using the <code>#start()</code> and
* <code>#close()</code> methods.
*
* <p>Derby does not allow running multiple instances in the same JVM, so external protection should be provided to
* protect against that.
*
* <p>Example of usage:
* <pre>
* public class SimpleDerbyTest {
*
* private TemporaryFolder tempFolder = new TemporaryFolder();
* private EmbeddedDerbyResource embeddedDerbyResource =
* new EmbeddedDerbyResource(DerbyResourceConfig.buildDefault().useDevNullErrorLogging(),
* tempFolder);
*
* @Rule
* public RuleChain derbyRuleChain = RuleChain.outerRule(tempFolder).around(embeddedDerbyResource);
*
* @Test
* public void test () throws SQLException {
* final String jdbcUrl = embeddedDerbyResource.getJdbcUrl();
* Connection connection = null;
* Statement stmt = null;
* ResultSet rs = null;
*
* try {
* connection = DriverManager.getConnection(jdbcUrl);
*
* // Check a value
* stmt = connection.createStatement();
* rs = stmt.executeQuery("SELECT 1 FROM SYSIBM.SYSDUMMY1");
*
* assertTrue(rs.next());
* } finally {
* // Close resources
* }
* }
* }
* </pre>
*
* <p>For further information and examples, see
* <a href="http://www.deventropy.org/junit-helper/junit-helper-derby/manual/">User Manual on the Project Website</a>.
*
* @see DerbyResourceConfig
*
* @author Bindul Bhowmik
*/
public class EmbeddedDerbyResource extends ExternalResource implements Closeable {
private final Logger log = LogManager.getLogger();
private final Charset defaultCharset = Charset.defaultCharset();
private final DerbyResourceConfig config;
private File derbySystemHome;
private TemporaryFolder derbySystemHomeParent;
private final String jdbcUrl;
private boolean isActive = false;
private String oldDerbySystemHomeValue;
private final DerbyBackupOperationsHelper backupOperationsHelper = new DerbyBackupOperationsHelper(this);
/**
* Creates a new Derby resource. All configurable parameters for this resource come from the config object
* passed in.
*
* @param dbResourceConfig Configurations to setup this resource
* @param derbySystemHomeDir A folder to use as the derby system home
*/
public EmbeddedDerbyResource (final DerbyResourceConfig dbResourceConfig, final File derbySystemHomeDir) {
ArgumentCheck.notNull(dbResourceConfig, "Embedded derby config");
this.config = dbResourceConfig;
ArgumentCheck.notNull(derbySystemHomeDir, "Derby System Home Directory");
this.derbySystemHome = derbySystemHomeDir;
this.jdbcUrl = buildJdbcUrl();
}
/**
* Creates a new Derby resource. All configurable parameters for this resource come from the config object
* passed in.
*
* @param dbResourceConfig Configurations to setup this resource
* @param derbySystemHomeParentTmpFolder A temporary folder to use as the derby system home
*/
public EmbeddedDerbyResource (final DerbyResourceConfig dbResourceConfig,
final TemporaryFolder derbySystemHomeParentTmpFolder) {
ArgumentCheck.notNull(dbResourceConfig, "Embedded derby config");
this.config = dbResourceConfig;
ArgumentCheck.notNull(derbySystemHomeParentTmpFolder, "Derby System Home Parent Directory");
// This can be a TemporaryFolder, so make sure it is not touched before #before()
this.derbySystemHomeParent = derbySystemHomeParentTmpFolder;
this.jdbcUrl = buildJdbcUrl();
}
/**
* Allows extending classes to use the config.
* @return the config The resource config
*/
protected DerbyResourceConfig getConfig () {
return config;
}
private String buildJdbcUrl () {
final StringBuilder jdbcUrlBldr = new StringBuilder().append(config.getSubSubProtocol().jdbcConnectionPrefix());
appendDbLocNameToUrl(jdbcUrlBldr);
return jdbcUrlBldr.toString();
}
/**
* Adds the database name to the Derby JDBC Url being built, taking into consideration sub-protocol specific
* variations.
*
* @param jdbcUrlBldr The String builder to which the database name is being added
*/
protected void appendDbLocNameToUrl (final StringBuilder jdbcUrlBldr) {
if (JdbcDerbySubSubProtocol.Jar == config.getSubSubProtocol()) {
// for :jar: protocol, see http://db.apache.org/derby/docs/10.12/devguide/cdevdeploy11201.html
jdbcUrlBldr.append('(').append(config.getJarDatabaseJarFile()).append(')');
}
jdbcUrlBldr.append(config.getDatabasePath());
}
/* (non-Javadoc)
* @see org.junit.rules.ExternalResource#before()
*/
@Override
protected void before () throws Throwable {
super.before();
this.start();
}
/**
* Starts the Embedded derby instance.
*
* <p><em>Note:</em> If using this instance as a JUnit {@linkplain org.junit.Rule}, do not call this method;
* initialization is already handled from the {@linkplain org.junit.rules.ExternalResource#before()}.
*
* <p>On successful completion (with the exception of failures in
* {@linkplain DerbyResourceConfig#getPostInitScripts()}, the {@link #isActive()} state of this resource is set to
* <code>true</code>. Further calls to this method while the resource is active has no effect.
*
* @throws IOException IO exception creating or setting derby home
* @throws SQLException SQL exception starting derby or running the init scripts
*/
public void start () throws IOException, SQLException {
if (isActive) {
return; // already started
}
Connection conn = null;
try {
// Validate and setup
if (null != derbySystemHomeParent) {
this.derbySystemHome = derbySystemHomeParent.newFolder();
}
FileUtils.forceMkdir(derbySystemHome);
// Save it to reset later
oldDerbySystemHomeValue = System.getProperty(DerbyConstants.PROP_DERBY_SYSTEM_HOME);
System.setProperty(DerbyConstants.PROP_DERBY_SYSTEM_HOME, derbySystemHome.getAbsolutePath());
setupDerbyProperties();
// Start the database
// Recommended Derby startup process,
// see https://db.apache.org/derby/docs/10.12/publishedapi/org/apache/derby/jdbc/EmbeddedDriver.html
try {
Class.forName(DerbyConstants.DERBY_EMBEDDED_DRIVER_CLASS).newInstance();
} catch (InstantiationException | IllegalAccessException | ClassNotFoundException e) {
throw new SQLException(
"Unable to initialize Derby driver class: " + DerbyConstants.DERBY_EMBEDDED_DRIVER_CLASS, e);
}
// Create / Connect to the database
conn = DriverManager.getConnection(buildCreateJDBCUrl());
isActive = true;
} catch (IOException | SQLException e) {
// Reset the Derby System Home property
resetDerbyHome();
throw e;
} finally {
DerbyUtils.closeQuietly(conn);
}
// Post init scripts
executePostInitScripts();
}
private void executePostInitScripts () throws IOException, SQLException {
Connection conn = null;
try {
conn = createConnection();
final DerbyScriptRunner scriptRunner = new DerbyScriptRunner(conn);
for (String postInitScript : config.getPostInitScripts()) {
final File scriptLogFile = new File(derbySystemHome, "post-init-"
+ postInitScript.replaceAll("/", "_") + ".log");
try {
final int result = scriptRunner.executeScript(postInitScript, scriptLogFile);
if (result != 0) {
log.warn(FileUtils.readFileToString(scriptLogFile, defaultCharset));
throw new IOException("Exceptions exist in script. See output for details");
}
} catch (IOException e) {
log.warn(FileUtils.readFileToString(scriptLogFile, defaultCharset));
throw new IOException("Exceptions exist in script. See output for details");
}
}
} finally {
DerbyUtils.closeQuietly(conn);
}
}
private String buildCreateJDBCUrl () {
final StringBuilder createDbJdbcUrl = new StringBuilder(jdbcUrl);
final JdbcDerbySubSubProtocol subSubProtocol = config.getSubSubProtocol();
if (null != config.getDbCreateFromRestoreMode()) {
// Database from a backup
final DbCreateFromRestroreMode dbCreateFromRestroreMode = config.getDbCreateFromRestoreMode();
createDbJdbcUrl.append(DerbyConstants.URLPROP_DERBY_SEPARATOR)
.append(dbCreateFromRestroreMode.urlAttribute()).append(DerbyConstants.URLPROP_DERBY_EQUAL)
.append(config.getDbCreateFromRestoreFrom().getAbsolutePath());
if (dbCreateFromRestroreMode.requiresLogDevice()) {
createDbJdbcUrl.append(DerbyConstants.URLPROP_DERBY_SEPARATOR)
.append(DbCreateFromRestroreMode.URLPROP_DERBY_LOGDEVICE)
.append(DerbyConstants.URLPROP_DERBY_EQUAL)
.append(config.getDbRecoveryLogDevice().getAbsolutePath());
}
} else if (JdbcDerbySubSubProtocol.Memory == subSubProtocol
|| JdbcDerbySubSubProtocol.Directory == subSubProtocol && !config.isDirectoryDatabaseSkipCreate()) {
// Only :memory: and :directory: databases need the 'create' flag.
createDbJdbcUrl.append(DerbyConstants.URLPROP_DERBY_CREATE);
}
log.debug("Will use JDBC URL {} to start Derby", createDbJdbcUrl);
return createDbJdbcUrl.toString();
}
private void setupDerbyProperties () throws IOException {
final Properties derbyProps = new Properties();
// Logging
switch (config.getErrorLoggingMode()) {
case Null:
derbyProps.setProperty(DerbyConstants.PROP_DERBY_STREAM_ERROR_FIELD, DerbyUtils.DEV_NULL_FIELD_ID);
break;
case Default:
default:
derbyProps.setProperty(DerbyConstants.PROP_DERBY_STREAM_ERROR_FILE, "derby.log");
break;
}
// Write it
final File derbyPropertyFile = new File(derbySystemHome, DerbyConstants.PROP_FILE_DERBY_PROPERTIES);
final FileWriter derbyPropertyFileWriter = new FileWriter(derbyPropertyFile);
derbyProps.store(derbyPropertyFileWriter, null);
IOUtils.closeQuietly(derbyPropertyFileWriter);
}
/* (non-Javadoc)
* @see org.junit.rules.ExternalResource#after()
*/
@Override
protected void after () {
super.after();
try {
this.close();
} catch (IOException e) {
// Ignore
log.catching(Level.TRACE, e);
}
}
/**
* Shuts down and closes the Derby Instance. It also restores any previously set <code>derby.system.home</code>
* system property. However, should another derby instance need to be created in the same JVM after this is shut
* down, ensure the Derby system is properly shut down (see {@link DerbyUtils#shutdownDerbySystemQuitely(boolean)}.
*
* <p>This method sets the {@link #isActive()} state of the resouce to false, and calls to this method when the
* resource is not active are ignored.
*
* {@inheritDoc}
*/
@Override
public void close () throws IOException {
if (!isActive) {
return;
}
Connection conn = null;
try {
final StringBuilder shutdownUrl = new StringBuilder(jdbcUrl);
if (JdbcDerbySubSubProtocol.Memory == config.getSubSubProtocol()) {
shutdownUrl.append(DerbyConstants.URLPROP_DERBY_DROP);
} else {
shutdownUrl.append(DerbyConstants.URLPROP_DERBY_SHUTDOWN);
}
conn = DriverManager.getConnection(shutdownUrl.toString());
} catch (SQLException e) {
// Ignore - there will always be an exception
log.catching(Level.TRACE, e);
} finally {
DerbyUtils.closeQuietly(conn);
}
// Reset the Derby System Home property
resetDerbyHome();
isActive = false;
}
private void resetDerbyHome () {
// Reset the Derby System Home property
if (null != oldDerbySystemHomeValue && !oldDerbySystemHomeValue.isEmpty()) {
System.setProperty(DerbyConstants.PROP_DERBY_SYSTEM_HOME, oldDerbySystemHomeValue);
oldDerbySystemHomeValue = null;
} else {
System.clearProperty(DerbyConstants.PROP_DERBY_SYSTEM_HOME);
}
}
/**
* Returns the file reference to the Derby system home.
*
* @return the derbySystemHome
* @throws IllegalStateException if the method is invoked when the database is not {@link #isActive()}.
*/
public File getDerbySystemHome () {
ensureActive();
return derbySystemHome;
}
/**
* Returns a URL that can be used to create a connection to this database instance.
*
* @return the jdbcUrl
* @throws IllegalStateException if the method is invoked when the database is not {@link #isActive()}.
*/
public String getJdbcUrl () {
ensureActive();
return jdbcUrl;
}
/**
* Returns the database path of the JDBC URL.
* @see DerbyResourceConfig#getDatabasePath()
* @return The database path
*/
public String getDatabasePath () {
return config.getDatabasePath();
}
/**
* Create and return a new connection for this resource. If the resource is a pooled datasource, it will be a pooled
* connection.
*
* @return A new basic or pooled connection for this database.
* @throws SQLException If there is an error creating the connection
* @throws IllegalStateException if the method is invoked when the database is not {@link #isActive()}.
*/
public Connection createConnection () throws SQLException {
ensureActive();
return DriverManager.getConnection(getJdbcUrl());
}
/**
* Returns true if the resource is started and not closed.
* @return The current status of the resource.
*/
public boolean isActive () {
return isActive;
}
/**
* Checks if the Embedded Derby Resource is active, if not throws an {@link IllegalStateException}.
*/
protected void ensureActive () {
if (!isActive) {
throw new IllegalStateException("Derby resource is not active");
}
}
/**
* Perform an online backup of the running instance. The online backup uses either the
* <code>SYSCS_UTIL.SYSCS_BACKUP_DATABASE</code> if <code>enableArchiveLogging</code> is set to <code>false</code>
* or <code>SYSCS_UTIL.SYSCS_BACKUP_DATABASE_AND_ENABLE_LOG_ARCHIVE_MODE</code> otherwise. If the
* <code>waitForTransactions</code> parameter is set to <code>false</code> the <code>_NOWAIT</code> versions of the
* procedures are used.
*
* <p>For more information on backing up Derby database, see
* <a href="http://db.apache.org/derby/docs/10.12/adminguide/cadminhubbkup01.html">Using the backup procedures to
* perform an online backup</a> in the Derby Administrators guide.
*
* @param backupDir The directory to which the database should be backed up.
* @param waitForTransactions Wait for running transactions to complete.
* @param enableArchiveLogging If archive logging should be enabled for the database.
* @param deleteArchivedLogs Ask Derby to delete the old archive logs after the backup is successful.
* @throws SQLException Exception from Derby when the backup fails.
* @throws IllegalStateException if the method is invoked when the database is not {@link #isActive()}.
*/
public void backupLiveDatabase (final File backupDir, final boolean waitForTransactions,
final boolean enableArchiveLogging, final boolean deleteArchivedLogs) throws SQLException {
ensureActive();
backupOperationsHelper.backupLiveDatabase(backupDir, waitForTransactions, enableArchiveLogging,
deleteArchivedLogs);
}
}