nl.strohalm.cyclos.setup.DataBaseConfiguration.java Source code

Java tutorial

Introduction

Here is the source code for nl.strohalm.cyclos.setup.DataBaseConfiguration.java

Source

/*
This file is part of Cyclos (www.cyclos.org).
A project of the Social Trade Organisation (www.socialtrade.org).
    
Cyclos is free software; you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation; either version 2 of the License, or
(at your option) any later version.
    
Cyclos is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
    
You should have received a copy of the GNU General Public License
along with Cyclos; if not, write to the Free Software
Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
    
 */
package nl.strohalm.cyclos.setup;

import java.sql.Connection;
import java.sql.DatabaseMetaData;
import java.sql.Driver;
import java.sql.DriverManager;
import java.sql.SQLException;
import java.util.Enumeration;
import java.util.List;
import java.util.Locale;
import java.util.Properties;

import nl.strohalm.cyclos.utils.JDBCWrapper;
import nl.strohalm.cyclos.utils.conversion.LocaleConverter;
import nl.strohalm.cyclos.utils.tasks.TaskRunner;

import org.apache.commons.collections.CollectionUtils;
import org.apache.commons.lang.StringUtils;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.hibernate.SessionFactory;
import org.hibernate.cfg.Configuration;
import org.hibernate.cfg.Environment;
import org.hibernate.connection.ConnectionProvider;
import org.hibernate.connection.DatasourceConnectionProvider;

/**
 * Class used to manage database configuration, validate the connection, generate the database when in embedded mode and apply automatic schema
 * updates.
 * @author luis
 */
public class DataBaseConfiguration {
    public static boolean SKIP = false;
    private static final Log LOG = LogFactory.getLog(DataBaseConfiguration.class);
    private final Configuration configuration;
    private SessionFactory sessionFactory;
    private final TaskRunner taskRunner;
    private Class<?> driverToUnregister;

    public DataBaseConfiguration(final Configuration configuration, final TaskRunner taskRunner) {
        this.configuration = configuration;
        this.taskRunner = taskRunner;
    }

    public Configuration getConfiguration() {
        return configuration;
    }

    public SessionFactory getSessionFactory() {
        return sessionFactory;
    }

    public void release() {
        if (driverToUnregister != null) {
            Enumeration<Driver> drivers = DriverManager.getDrivers();
            while (drivers.hasMoreElements()) {
                Driver driver = drivers.nextElement();
                if (driverToUnregister.isInstance(driver)) {
                    try {
                        DriverManager.deregisterDriver(driver);
                    } catch (Exception e) {
                        // Ignore
                    }
                }
            }
        }
    }

    public void run() {
        final Properties properties = configuration.getProperties();

        warnTrailingSpaces(properties);

        // Define some fixed Cyclos properties for Hibernate
        initCommonProperties(properties);

        // Check if all tests must be skipped
        final String skipTestsProperty = StringUtils
                .trimToNull(properties.getProperty("cyclos.database.skipTests"));
        final boolean skipTests = SKIP || skipTestsProperty == null || Boolean.valueOf(skipTestsProperty);
        if (skipTests) {
            if (!SKIP && skipTestsProperty == null) {
                LOG.info(
                        "Skipping Cyclos database check, as cyclos.properties doesn't set cyclos.database.skipTests=false");
            }
            return;
        }

        // Delegate the database initialization to the task runner
        taskRunner.handleDatabaseInitialization(new Runnable() {
            @Override
            public void run() {
                handleDatabase(properties);
            }
        });
    }

    private void handleDatabase(final Properties properties) {
        // Retrieve the connection
        final String dataSource = StringUtils.trimToNull(properties.getProperty(Environment.DATASOURCE));
        Connection connection;
        String connectionLocation;
        if (dataSource != null) {
            // Use Hibernate's own DatasourceConnectionProvider when using a JNDI datasource
            final ConnectionProvider provider = new DatasourceConnectionProvider();
            provider.configure(properties);
            try {
                connection = provider.getConnection();
            } catch (final SQLException e) {
                final String msg = "Error connecting to datasource at " + dataSource;
                LOG.error(msg);
                throw new RuntimeException(msg, e);
            }
            connectionLocation = dataSource;
        } else {
            // Manually verify the connection
            final String driverClass = StringUtils.trimToNull(properties.getProperty(Environment.DRIVER));
            validateDriver(driverClass);
            final String url = properties.getProperty(Environment.URL);
            final String username = properties.getProperty(Environment.USER);
            final String password = properties.getProperty(Environment.PASS);
            connection = validateConnection(url, username, password);
            connectionLocation = url;
        }

        final JDBCWrapper jdbc = new JDBCWrapper(connection);

        // Check whether we will create the database if necessary
        final boolean embedded = Boolean.valueOf(properties.getProperty("cyclos.embedded.enable", "false"));

        // Get the connection meta data
        boolean dataBaseExists = true;
        String dataBaseName;
        String dataBaseVersion;
        try {
            final DatabaseMetaData metaData = connection.getMetaData();
            dataBaseName = metaData.getDatabaseProductName();
            dataBaseVersion = metaData.getDatabaseProductVersion();
        } catch (final SQLException e) {
            throw new RuntimeException("Error reading database metadata", e);
        }

        // Check if the database exists, by reading the current version
        final String currentVersion;
        try {
            currentVersion = readCurrentVersion(jdbc);
            if (currentVersion != null) {
                LOG.info(String.format("Cyclos database version %s found on %s version %s", currentVersion,
                        dataBaseName, dataBaseVersion));
            }
            dataBaseExists = currentVersion != null;
            // Check for new versions schema upgrade
            if (dataBaseExists) {
                final boolean autoUpgrade = Boolean
                        .valueOf(properties.getProperty("cyclos.autoSchemaUpgrade.enable", "false"));
                if (autoUpgrade) {
                    // Run the schema upgrade
                    final String newVersion = upgradeSchema(currentVersion, jdbc);
                    if (!currentVersion.equals(newVersion)) {
                        // The version has changed. Add a custom property on the configuration properties for the cache
                        // manager to clear the current cache, because new entities may be incompatible with cached versions
                        properties.setProperty("cyclos.versionHasChanged", "true");
                    }
                }
            } else {
                // For MySQL connections, we should ensure that the database is set to utf8
                if (embedded && dataBaseName.toLowerCase().equals("mysql")) {
                    try {
                        jdbc.commit();
                        jdbc.execute("alter database character set utf8");
                    } catch (final SQLException e) {
                        e.printStackTrace();
                        // Ignore
                    }
                }
            }
        } finally {
            // Close the connection
            try {
                connection.close();
            } catch (final SQLException e) {
            }
        }

        // Run the setup if needed
        if (!dataBaseExists) {
            if (embedded) {
                final boolean smsEmbedded = Boolean
                        .valueOf(properties.getProperty("cyclos.embedded.sms.enable", "false"));
                LOG.info("Database is empty. Running setup to populate it");
                sessionFactory = configuration.buildSessionFactory();
                final Locale locale = LocaleConverter.instance()
                        .valueOf(properties.getProperty("cyclos.embedded.locale", "en_US"));
                final Setup setup = new Setup(configuration, sessionFactory);
                setup.setLocale(locale);
                setup.setCreateDataBase(true);
                setup.setCreateBasicData(true);
                setup.setCreateInitialData(true);
                setup.setCreateSmsData(smsEmbedded);
                setup.setForce(true);
                setup.execute();
            } else {
                throw new RuntimeException("Cyclos database not found at " + connectionLocation);
            }
        }
    }

    /**
     * Initialize some common properties for Hibernate
     */
    private void initCommonProperties(final Properties properties) {
        // The transaction isolation level
        if (StringUtils.isEmpty(properties.getProperty("hibernate.connection.isolation"))) {
            properties.setProperty("hibernate.connection.isolation", "" + Connection.TRANSACTION_READ_COMMITTED);
        }

        final boolean secondLevelCacheEnabled = Boolean
                .parseBoolean(properties.getProperty("hibernate.cache.use_second_level_cache", "false"));
        if (secondLevelCacheEnabled) {
            // The second level cache provider
            if (StringUtils.isEmpty(properties.getProperty("hibernate.cache.region.factory_class"))) {
                properties.setProperty("hibernate.cache.region.factory_class",
                        "net.sf.ehcache.hibernate.EhCacheRegionFactory");
            }
        }

        // For MySQL, set the character encoding to utf8
        String url = StringUtils.trimToNull(properties.getProperty("hibernate.connection.url"));
        if (url != null && url.toLowerCase().startsWith("jdbc:mysql:")) {
            if (!url.contains("useUnicode")) {
                url += (url.contains("?") ? "&" : "?") + "useUnicode=true";
            }
            if (!url.contains("characterEncoding")) {
                url += (url.contains("?") ? "&" : "?") + "characterEncoding=utf8";
            }
            properties.setProperty("hibernate.connection.url", url);
        }
    }

    /**
     * Read the current version from the database
     */
    private String readCurrentVersion(final JDBCWrapper jdbc) {
        try {
            return jdbc.readScalarAsString("select version from application");
        } catch (final SQLException e) {
            return null;
        }
    }

    /**
     * Apply automatic upgrades on the schema
     */
    @SuppressWarnings("deprecation")
    private String upgradeSchema(final String originalVersion, final JDBCWrapper jdbc) {
        String currentVersion = originalVersion;
        final VersionHistory history = new VersionHistoryReader().read();
        final List<Version> intermediateVersions = history.upgrade(originalVersion);
        if (intermediateVersions == null) {
            LOG.warn("Unknown version on database: " + originalVersion);
            return originalVersion;
        }
        String databaseName;
        try {
            databaseName = jdbc.getConnection().getMetaData().getDatabaseProductName();
        } catch (final SQLException e) {
            throw new RuntimeException("Error reading database name", e);
        }
        for (final Version version : intermediateVersions) {
            final String newVersion = version.getLabel();

            // Apply the statements
            final List<String> statements = version.getStatements(databaseName);
            LOG.info(String.format("Upgrading schema from version %s to version %s", currentVersion, newVersion));
            if (CollectionUtils.isNotEmpty(statements)) {
                int executedOk = 0;
                int totalUpdatedRows = 0;

                for (final String statement : statements) {
                    try {
                        final int updatedRows = jdbc.execute(statement);
                        LOG.info(String.format("Statement executed: %s", statement));
                        if (updatedRows > 0) {
                            LOG.info("Updated rows: " + updatedRows);
                            totalUpdatedRows += updatedRows;
                        }
                        executedOk++;
                    } catch (final SQLException e) {
                        LOG.warn(String.format(
                                "Error applying automatic schema upgrade on version %s when executing statement [%s]: %s",
                                newVersion, statement, e.getMessage()));
                    }
                }

                String msg = "Statements executed (ok / total): " + executedOk + " / " + statements.size() + ".";
                if (totalUpdatedRows == 0) {
                    msg += " None of the executed statements has modified data.";
                }
                LOG.info(msg);
            }

            LOG.info("Executing migrations...");

            // Apply the migrations
            try {
                final List<Class<Migration>> migrations = version.getMigrations(databaseName);
                if (CollectionUtils.isNotEmpty(migrations)) {
                    int executedOk = 0;
                    int totalUpdatedRows = 0;

                    for (final Class<Migration> clazz : migrations) {
                        LOG.info(String.format("Executing migration class %s", clazz.getName()));
                        Migration migration;
                        try {
                            migration = clazz.newInstance();
                        } catch (final Exception e) {
                            LOG.warn(String.format("Error instantiating the migration class %s", clazz.getName()),
                                    e);
                            continue;
                        }
                        try {
                            if (migration instanceof UntraceableMigration) {
                                ((UntraceableMigration) migration).execute(jdbc);
                            } else {// (TraceableMigration)
                                final int updatedRows = ((TraceableMigration) migration).execute(jdbc);
                                if (updatedRows > 0) {
                                    LOG.info("Updated rows: " + updatedRows);
                                    totalUpdatedRows += updatedRows;
                                }
                            }
                            executedOk++;
                        } catch (final Exception e) {
                            LOG.warn(String.format("Error upgrading to version %s when executing migration: %s",
                                    newVersion, e.getMessage()));
                            LOG.debug(e);
                        }
                    }

                    String msg = "Migrations executed (ok / total): " + executedOk + " / " + migrations.size()
                            + ".";
                    if (totalUpdatedRows == 0) {
                        msg += " None of the executed migrations has modified data.";
                    }
                    LOG.info(msg);
                }

                // Perform a commit
                try {
                    jdbc.commit();
                } catch (final SQLException e) {
                    LOG.warn("Error while committing", e);
                }
            } catch (final Exception e) {
                try {
                    jdbc.rollback();
                } catch (final SQLException e1) {
                    LOG.warn("Error while rolling back", e1);
                }
            }

            // Remove the unused translation messages
            final List<String> removedTranslationKeys = version.getRemovedTranslationKeys();
            if (CollectionUtils.isNotEmpty(removedTranslationKeys)) {
                for (String key : removedTranslationKeys) {
                    key = StringUtils.trimToEmpty(key);
                    try {
                        final int deletedRows = jdbc.execute("delete from translation_messages where msg_key = ?",
                                key);
                        if (deletedRows > 0) {
                            LOG.info(String.format("Removing unused translation message: %s", key));
                        } else {
                            LOG.info(String.format("Unused translation message: %s was not found in the database",
                                    key));
                        }
                    } catch (final Exception e) {
                        // Keeping an unused translation message is not that serious... Move on ;-)
                        LOG.warn(String.format("Error removing unused translation message: %s", key), e);
                    }
                }
                try {
                    jdbc.commit();
                } catch (final SQLException e) {
                    LOG.warn("Error while committing", e);
                }
            }

            // Update the version
            currentVersion = newVersion;
            try {
                jdbc.execute("update application set version = ?", currentVersion);
                jdbc.commit();
            } catch (final SQLException e) {
                LOG.warn(String.format("Error while updating the current version table to %s", currentVersion), e);
                try {
                    jdbc.rollback();
                } catch (final SQLException e1) {
                    LOG.warn("Error while rolling back", e1);
                }
            }

        }
        if (!originalVersion.equals(currentVersion)) {
            LOG.info(String.format("The database has been upgraded to version %s", currentVersion));
        }
        return currentVersion;
    }

    /**
     * Validates the connection parameters, returning an open connection
     */
    private Connection validateConnection(final String url, final String username, final String password) {
        Connection connection = null;
        try {
            connection = DriverManager.getConnection(url, username, password);
            connection.setAutoCommit(false);
        } catch (final SQLException e) {
            final String msg = "Error connecting to database at " + url;
            LOG.error(msg);
            throw new RuntimeException(msg, e);
        }

        return connection;
    }

    /**
     * Validates the given JDBC driver class
     */
    private void validateDriver(final String driverClass) {
        try {
            final Class<?> clazz = Class.forName(driverClass);
            if (!Driver.class.isAssignableFrom(clazz)) {
                throw new Exception();
            }
            driverToUnregister = clazz;
        } catch (final Exception e) {
            final String msg = "Illegal JDBC driver class on cyclos.properties: " + driverClass;
            LOG.error(msg);
            throw new RuntimeException(msg);
        }
    }

    private void warnTrailingSpaces(final Properties properties) {
        for (String key : properties.stringPropertyNames()) {
            String value = properties.getProperty(key);
            if (!key.equals("line.separator") && !value.trim().equals(value)) {
                LOG.warn("Property '" + key + "' has trailing spaces. Its value is : '" + value + "'");
            }
        }
    }
}