All Downloads are FREE. Search and download functionalities are using the official Maven repository.

org.zapodot.junit.db.EmbeddedDatabaseRule Maven / Gradle / Ivy

There is a newer version: 2.2.0
Show newest version
package org.zapodot.junit.db;

import org.h2.jdbc.JdbcSQLException;
import org.junit.rules.TestRule;
import org.junit.runner.Description;
import org.junit.runners.model.Statement;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.zapodot.junit.db.internal.CloseSuppressedConnectionFactory;
import org.zapodot.junit.db.internal.EmbeddedDataSource;
import org.zapodot.junit.db.internal.FilePathInitializationPlugin;
import org.zapodot.junit.db.internal.H2JdbcUrlFactory;
import org.zapodot.junit.db.internal.HyperSqlJdbcUrlFactory;
import org.zapodot.junit.db.internal.JdbcUrlFactory;
import org.zapodot.junit.db.internal.SQLInitializationPlugin;
import org.zapodot.junit.db.plugin.InitializationPlugin;

import javax.sql.DataSource;
import java.nio.charset.Charset;
import java.nio.charset.StandardCharsets;
import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.SQLException;
import java.util.Arrays;
import java.util.Collections;
import java.util.LinkedHashMap;
import java.util.Map;

/**
 * A JUnit Rule implementation that makes it easy to stub JDBC integrations from your tests
 *
 * @author zapodot
 */
public class EmbeddedDatabaseRule implements TestRule {

    private final boolean autoCommit;

    private final String predefinedName;

    private String testName;

    private static final Logger LOGGER = LoggerFactory.getLogger(EmbeddedDatabaseRule.class);

    private final Map jdbcUrlProperties;

    private final Map, InitializationPlugin> initializationPlugins;

    private final JdbcUrlFactory jdbcUrlFactory;

    private final CompatibilityMode compatibilityMode;

    private Connection connection;

    /**
     * Standard constructor that is suitable if you don't need to do anything special
     */
    public EmbeddedDatabaseRule() {
        this(true, null, null, null, null, CompatibilityMode.REGULAR);
    }


    private EmbeddedDatabaseRule(final boolean autoCommit,
                                 final String name,
                                 final Map jdbcUrlProperties,
                                 final Map, InitializationPlugin> initializationPlugins,
                                 final JdbcUrlFactory jdbcUrlFactory,
                                 final CompatibilityMode compatibilityMode) {
        this.autoCommit = autoCommit;
        this.predefinedName = name;
        this.jdbcUrlProperties = jdbcUrlProperties == null ? Collections.emptyMap() : jdbcUrlProperties;
        this.initializationPlugins = initializationPlugins == null ? Collections.emptyMap() : initializationPlugins;
        this.jdbcUrlFactory = jdbcUrlFactory == null ? new H2JdbcUrlFactory() : jdbcUrlFactory;
        this.compatibilityMode = compatibilityMode;
    }

    /**
     * Creates a builder that enables you to use the fluent API when construction an EmbeddedDatabaseRule instance
     *
     * @return a Builder
     */
    public static Builder builder() {
        return h2();
    }

    /**
     * Creates a builder that enables you to use the fluent API to construct an {@link EmbeddedDatabaseRule} using the H2 engine
     *
     * @return a {@link Builder} instance
     */
    public static Builder h2() {
        return Builder.h2();
    }

    /**
     * Creates a builder that enables you to use the fluent PAI to consturct an {@link EmbeddedDatabaseRule} using the HSQLDB engine
     *
     * @return a {@link Builder} instance
     */
    public static Builder hsqldb() {
        return Builder.hsqldb();
    }

    /**
     * Gives access to the current H2 JDBC connection. The connection returned by this method will suppress all "close" calls
     *
     * @return the current JDBC connection to be used internally in your test, or null if has not been set yet
     */
    public Connection getConnection() {
        return CloseSuppressedConnectionFactory.createProxy(connection);
    }

    /**
     * To be used when you actually need is a DataSource
     *
     * @return a DataSource instance wrapping a single connection
     */
    public DataSource getDataSource() {
        return EmbeddedDataSource.create(connection);
    }

    public boolean isAutoCommit() {
        return autoCommit;
    }


    /**
     * Returns a JDBC url for connecting to the in-memory database created by this rule with all INIT params stripped
     *
     * @return a JDBC url string
     */
    public String getConnectionJdbcUrl() {
        return jdbcUrlFactory.connectionUrl(getInMemoryDatabaseName(), getJdbcUrlProperties());
    }

    /**
     * Will generate a JDBC url for an in-memory H2 named database
     *
     * @return a JDBC URL string
     */
    private String generateJdbcUrl() {
        return jdbcUrlFactory.connectionUrlForInitialization(getInMemoryDatabaseName(), getJdbcUrlProperties());
    }

    private Map getJdbcUrlProperties() {
        final LinkedHashMap properties = new LinkedHashMap<>();
        properties.putAll(jdbcUrlFactory.compatibilityModeParam(compatibilityMode));
        properties.putAll(jdbcUrlProperties);
        return properties;
    }

    private String getInMemoryDatabaseName() {
        return predefinedName == null ? testName : predefinedName;
    }

    @Override
    public Statement apply(final Statement base, final Description description) {
        warnIfNameIsPredifinedAndTheRuleIsMethodBased(description);
        return statement(base, predefinedName != null ? predefinedName : extractNameFromDescription(description));
    }

    private void warnIfNameIsPredifinedAndTheRuleIsMethodBased(final Description description) {
        if (description.getMethodName() != null && predefinedName != null) {
            LOGGER.warn(
                    "You have set a name for your datasource and are running the EmbeddedDatabaseRule as a method @Rule. " +
                            "This may lead to the datasource not being reset between tests especially of your tests uses runs with " +
                            "multiple threads");
        }
    }

    private String extractNameFromDescription(Description description) {
        return description.getTestClass() == null ? description.getClassName() : description.getTestClass()
                                                                                            .getSimpleName();
    }


    private Statement statement(final Statement base, final String name) {
        return new Statement() {
            @Override
            public void evaluate() throws Throwable {
                setupConnection(name);
                try {
                    base.evaluate();
                } finally {
                    takeDownConnection();
                }
            }
        };
    }

    private void takeDownConnection() throws SQLException {
        this.connection.close();
    }

    private void setupConnection(final String name) throws SQLException {

        testName = name;
        final String url = generateJdbcUrl();
        try {
            connection = DriverManager.getConnection(url);
        } catch (JdbcSQLException e) {
            if (url.contains("RUNSCRIPT")) {
                LOGGER.error("Failed to initialize the H2 database. Please check your init script for errors", e);
            }
            throw e;
        }
        connection.setAutoCommit(isAutoCommit());
        for (final Map.Entry, InitializationPlugin> entry : initializationPlugins
                .entrySet()) {
            entry.getValue().connectionMade(name, getConnection());
        }
    }

    /**
     * A builder class that provides a fluent api for building DB rules
     */
    public static class Builder {

        private final Map properties = new LinkedHashMap<>();

        private final Map, InitializationPlugin> initializationPlugins = new LinkedHashMap<>();

        private String name;

        private boolean autoCommit = true;

        private final Engine engine;

        private CompatibilityMode compatibilityMode = CompatibilityMode.REGULAR;

        private Builder(final Engine engine) {
            if (engine == null) {
                throw new IllegalArgumentException("The \"engine\" argument can not be null");
            }
            this.engine = engine;
        }

        /**
         * Creates a builder for the H2 engine
         *
         * @return a builder for creating an {@link EmbeddedDatabaseRule} that will use the H2 engine
         * @deprecated use {@link Builder#h2()} instead. Will be removed in the 2.0 release
         */
        @Deprecated
        public static Builder instance() {
            return new Builder(Engine.H2);
        }

        /**
         * Creates a builder for the H2 engine
         *
         * @return a builder for creating an {@link EmbeddedDatabaseRule} that will use the H2 engine
         */
        public static Builder h2() {
            return new Builder(Engine.H2);
        }


        /**
         * Creates a builder for the H2 engine
         *
         * @return a builder for creating an {@link EmbeddedDatabaseRule} that will use the HSQLDB engine
         */
        public static Builder hsqldb() {
            return new Builder(Engine.HSQLDB);
        }

        public Builder withName(final String name) {
            this.name = name;
            return this;
        }

        private String normalizeString(final String input) {
            if (input == null) {
                return null;
            } else {
                return input.replaceAll("\n", "").replaceAll(";", "\\\\;").trim();
            }
        }

        public Builder withInitialSql(final String sql) {
            if (sql == null) {
                throw new IllegalArgumentException("The value of the \"sql\" argument can not be null");
            }
            return initializedByPlugin(new SQLInitializationPlugin(sql));
        }

        public Builder withInitialSqlFromResource(final String resource) {

            if (null == resource) {
                throw new IllegalArgumentException("The value of the \"resource\" argument can not be null");
            }
            return withInitialSqlFromResource(resource, StandardCharsets.UTF_8);
        }

        public Builder withInitialSqlFromResource(final String resource, final Charset charset) {
            if (null == resource) {
                throw new IllegalArgumentException("The value of the \"resource\" argument can not be null");
            }
            if (null == charset) {
                throw new IllegalArgumentException("The value of the \"charset\" argument can not be null");
            }
            return initializedByPlugin(new FilePathInitializationPlugin(resource, charset));
        }


        public Builder withMode(final String mode) {
            if (mode == null) {
                throw new IllegalArgumentException("The \"mode\" argument can not be null");
            }
            if (mode != "") {
                return withMode(mapToCompatibilityMode(mode));
            } else {
                return this;
            }
        }

        private CompatibilityMode mapToCompatibilityMode(final String mode) {
            if (mode == null) {
                throw new IllegalArgumentException("The \"mode\" argument can not be null");
            }
            return Arrays.stream(CompatibilityMode.values())
                         .filter(c -> c.name().equalsIgnoreCase(mode))
                         .findAny()
                         .orElseThrow(() -> new IllegalArgumentException("Could not map mode \"" + mode + "\" to a valid Compatibility mode"));
        }

        public Builder withMode(final CompatibilityMode compatibilityMode) {

            if (compatibilityMode == null) {
                throw new IllegalArgumentException("The \"compatibilityMode\" argument can not be null");
            }
            this.compatibilityMode = compatibilityMode;
            return this;
        }

        public 

Builder initializedByPlugin(final P plugin) { if (plugin != null) { initializationPlugins.put(plugin.getClass(), plugin); } return this; } public Builder withProperty(final String property, final String value) { if (property != null && value != null) { properties.put(property, normalizeString(value)); } return this; } public Builder withoutAutoCommit() { autoCommit = false; return this; } private Map propertiesMap() { return new LinkedHashMap<>(properties); } public EmbeddedDatabaseRule build() { return new EmbeddedDatabaseRule(autoCommit, name, propertiesMap(), initializationPlugins, createJdbcUrlFactory(), compatibilityMode); } private JdbcUrlFactory createJdbcUrlFactory() { if (engine == Engine.HSQLDB) { return new HyperSqlJdbcUrlFactory(); } else { return new H2JdbcUrlFactory(); } } } public enum Engine { H2, HSQLDB } }





© 2015 - 2025 Weber Informatics LLC | Privacy Policy