org.deventropy.junithelper.derby.EmbeddedDerbyResource.java Source code

Java tutorial

Introduction

Here is the source code for org.deventropy.junithelper.derby.EmbeddedDerbyResource.java

Source

/* 
 * 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.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.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);
 * 
 *    &#064;Rule
 *    public RuleChain derbyRuleChain = RuleChain.outerRule(tempFolder).around(embeddedDerbyResource);
 * 
 *    &#064;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>.
 * 
 * @author Bindul Bhowmik
 */
public class EmbeddedDerbyResource extends ExternalResource implements Closeable {

    private static final String PROP_FILE_DERBY_PROPERTIES = "derby.properties";
    private static final String PROP_DERBY_SYSTEM_HOME = "derby.system.home";
    private static final String PROP_DERBY_STREAM_ERROR_FILE = "derby.stream.error.file";
    private static final String PROP_DERBY_STREAM_ERROR_FIELD = "derby.stream.error.field";

    private static final String URLPROP_DERBY_CREATE = ";create=true";
    private static final String URLPROP_DERBY_SHUTDOWN = ";shutdown=true";
    private static final String URLPROP_DERBY_DROP = ";drop=true";

    private static final String DERBY_EMBEDDED_DRIVER_CLASS = "org.apache.derby.jdbc.EmbeddedDriver";

    private final Logger log = LogManager.getLogger();

    private final DerbyResourceConfig config;
    private File derbySystemHome;
    private TemporaryFolder derbySystemHomeParent;

    private final String jdbcUrl;

    private String oldDerbySystemHomeValue;

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

    private String buildJdbcUrl() {
        final StringBuilder jdbcUrlBldr = new StringBuilder()
                .append(config.getSubSubProtocol().jdbcConnectionPrefix());
        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());
        return jdbcUrlBldr.toString();
    }

    /* (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()}.
     * 
     * @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 {
        // Validate and setup
        if (null != derbySystemHomeParent) {
            this.derbySystemHome = derbySystemHomeParent.newFolder();
        }
        FileUtils.forceMkdir(derbySystemHome);
        oldDerbySystemHomeValue = System.getProperty(PROP_DERBY_SYSTEM_HOME); // Saving it to reset it later
        System.setProperty(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(DERBY_EMBEDDED_DRIVER_CLASS).newInstance();
        } catch (InstantiationException | IllegalAccessException | ClassNotFoundException e) {
            // Reset the Derby System Home property
            resetDerbyHome();
            throw new SQLException("Unable to initialize Derby driver class: " + DERBY_EMBEDDED_DRIVER_CLASS, e);
        }
        // Create / Connect to the database
        final Connection conn = DriverManager.getConnection(buildCreateJDBCUrl());
        try {
            // Post init scripts
            executePostInitScripts(conn);
        } finally {
            DerbyUtils.closeQuietly(conn);
        }
    }

    private void executePostInitScripts(final Connection conn) throws IOException {
        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));
                    throw new IOException("Exceptions exist in script. See output for details");
                }
            } catch (IOException e) {
                log.warn(FileUtils.readFileToString(scriptLogFile));
                throw new IOException("Exceptions exist in script. See output for details");
            }
        }
    }

    private String buildCreateJDBCUrl() {
        // TODO Will handle things here to restore from a backup, etc.
        return new StringBuilder().append(jdbcUrl).append(URLPROP_DERBY_CREATE).toString();
    }

    private void setupDerbyProperties() throws IOException {
        final Properties derbyProps = new Properties();

        // Logging
        switch (config.getErrorLoggingMode()) {
        case Null:
            derbyProps.setProperty(PROP_DERBY_STREAM_ERROR_FIELD, DerbyUtils.DEV_NULL_FIELD_ID);
            break;
        case Default:
        default:
            derbyProps.setProperty(PROP_DERBY_STREAM_ERROR_FILE, "derby.log");
            break;
        }

        // Write it
        final File derbyPropertyFile = new File(derbySystemHome, 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);
        }
    }

    /* (non-Javadoc)
     * @see java.io.Closeable#close()
     */
    @Override
    public void close() throws IOException {
        Connection conn = null;
        try {
            final StringBuilder shutdownUrl = new StringBuilder(jdbcUrl);
            if (JdbcDerbySubSubProtocol.Memory == config.getSubSubProtocol()) {
                shutdownUrl.append(URLPROP_DERBY_DROP);
            } else {
                shutdownUrl.append(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();
    }

    private void resetDerbyHome() {
        // Reset the Derby System Home property
        if (null != oldDerbySystemHomeValue && !oldDerbySystemHomeValue.isEmpty()) {
            System.setProperty(PROP_DERBY_SYSTEM_HOME, oldDerbySystemHomeValue);
            oldDerbySystemHomeValue = null;
        } else {
            System.clearProperty(PROP_DERBY_SYSTEM_HOME);
        }
    }

    /**
     * @return the derbySystemHome
     */
    public File getDerbySystemHome() {
        return derbySystemHome;
    }

    /**
     * @return the jdbcUrl
     */
    public String getJdbcUrl() {
        return jdbcUrl;
    }

    /**
     * Returns the database path of the JDBC URL.
     * @see DerbyResourceConfig#getDatabasePath()
     * @return The database path
     */
    public String getDatabasePath() {
        return config.getDatabasePath();
    }
}