com.buisonje.tools.xmldbloader.XmlDataSetDBLoader.java Source code

Java tutorial

Introduction

Here is the source code for com.buisonje.tools.xmldbloader.XmlDataSetDBLoader.java

Source

/*
 *  2016 Volkert de Buisonj, released under the Apache License 2.0. License terms: https://www.apache.org/licenses/LICENSE-2.0
 */
package com.buisonje.tools.xmldbloader;

import java.io.File;
import java.io.FileFilter;
import java.net.URL;
import java.net.URLClassLoader;
import java.sql.Connection;
import java.sql.Driver;
import java.sql.DriverManager;
import java.sql.SQLException;
import java.util.List;
import java.util.Properties;
import java.util.Set;

import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;

import org.apache.commons.io.IOCase;
import org.apache.commons.io.filefilter.WildcardFileFilter;
import org.dbunit.DatabaseUnitException;
import org.dbunit.database.DatabaseConfig;
import org.dbunit.database.DatabaseConnection;
import org.dbunit.database.IDatabaseConnection;
import org.dbunit.dataset.DataSetException;
import org.dbunit.operation.DatabaseOperation;
import org.dbunit.util.fileloader.DataFileLoader;
import org.dbunit.util.fileloader.FlatXmlDataFileLoader;
import org.reflections.Reflections;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.w3c.dom.Document;

import com.kfu.www.nsayer.DriverShim;

/**
 * Enables the loading of XML DataSets (as supported by {@link FlatXmlDataFileLoader}) directly into databases using the JDBC interface.
 *
 * @author Volkert de Buisonjé
 */
public class XmlDataSetDBLoader implements DataLoader {

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

    private static final String ARG_ALLOW_EMPTY_FIELDS = "allowemptyfields";
    private static final String ARG_JDBC_DRIVER_JAR_FILE_PATH = "jdbcdriverclassfile=";
    private static final String ARG_DB_URL = "dburl=";
    private static final String ARG_DB_USERNAME = "dbusername=";
    private static final String ARG_DB_PASSWORD = "dbpassword=";

    private static final String JDBC_SCHEME_PREFIX = "jdbc:";
    private static final String DATASET_XML_ROOT_ELEMENT_NAME = "dataset";

    /**
     * {@inheritDoc}
     * <p>
     * This implementation expects XML DataSet files (as supported by {@link FlatXmlDataFileLoader}) as input files and loads them into a database.
     * </p>
     */
    @Override
    public void loadDataFiles(List<String> params, Properties properties, List<String> inputFilePaths) {
        if (inputFilePaths.size() < 1) {
            throw new IllegalArgumentException(
                    "You need to specify at least one DataSet XML file (or at least one directory containing at least one DataSet XML file) to load into the"
                            + " database.");
        }

        IDatabaseConnection dbConnection = null;
        try {

            dbConnection = processParams(params, properties);
            processInputFilePaths(inputFilePaths, dbConnection);

        } catch (Exception e) {
            throw new IllegalStateException(
                    "An error occurred while trying to load one of the specified DataSet XML files into the database.",
                    e);
        } finally {
            closeDBConnectionQuietly(dbConnection);
        }
    }

    /**
     * @param dbConnection the {@link IDatabaseConnection} to close.
     * @deprecated <b>TODO</b>: Upgrade this project beyond Java 6 and then replace this entire finally-block with a solution based on try-with-resources.
     */
    @Deprecated
    void closeDBConnectionQuietly(IDatabaseConnection dbConnection) {
        // TODO:
        try {
            if (dbConnection != null && dbConnection.getConnection() != null
                    && !dbConnection.getConnection().isClosed()) {
                dbConnection.close();
                dbConnection = null;
            }
        } catch (SQLException e) {
            LOGGER.error(String.format("Could not clean up %s after failure.", dbConnection.getClass()), e);
        }
    }

    private void processInputFilePaths(final List<String> inputFilePaths, IDatabaseConnection connection)
            throws DataSetException {
        DataFileLoader loader = new FlatXmlDataFileLoader();
        for (final String currentInputFilePath : inputFilePaths) {
            loadDataSetXMLFiles(connection, loader, new File(currentInputFilePath));
        }
    }

    private void loadDataSetXMLFiles(IDatabaseConnection connection, DataFileLoader loader, File currentFileOrDir)
            throws DataSetException {
        final String currentFileOrDirPath = currentFileOrDir.getAbsolutePath();
        if (!currentFileOrDir.exists() || !currentFileOrDir.canRead()) {
            throw new IllegalArgumentException(String
                    .format("Specified path \"%s\" does not exist or is not accessible.", currentFileOrDirPath));
        }
        if (currentFileOrDir.isDirectory()) {
            FileFilter fileFilter = new WildcardFileFilter("*.xml", IOCase.INSENSITIVE);
            File[] files = currentFileOrDir.listFiles(fileFilter);
            if (files == null) {
                LOGGER.error("Skipping invalid directory \"{}\".", currentFileOrDirPath);
                return;
            }
            if (files.length < 1) {
                LOGGER.warn("Directory \"{}\" contains no XML files. Skipping this directory.",
                        currentFileOrDirPath);
                return;
            }
            for (final File xmlFile : files) {
                if (xmlFile.isDirectory()) {
                    LOGGER.warn(
                            "Specified directory \"{}\" contains a subdirectory called \"{}\". Will *not* recurse.",
                            currentFileOrDir.getAbsolutePath(), xmlFile.getAbsolutePath());
                } else {
                    loadDataSetXMLFile(connection, loader, xmlFile);
                }
            }
        } else {
            loadDataSetXMLFile(connection, loader, currentFileOrDir);
        }

    }

    private void loadDataSetXMLFile(IDatabaseConnection connection, DataFileLoader loader, File currentInputFile)
            throws DataSetException {
        final String currentInputFilePath = currentInputFile.getAbsolutePath();
        try {
            if (!isValidDataSetXML(currentInputFile)) {
                LOGGER.warn("Skipping unsupported XML file {}.", currentInputFilePath);
                return;
            }
            LOGGER.info("Loading DataSet XML file {}...", currentInputFilePath);
            DatabaseOperation.CLEAN_INSERT.execute(connection,
                    loader.loadDataSet(currentInputFile.toURI().toURL()));
        } catch (Exception e) {
            throw new DataSetException(
                    String.format("Error while trying to load DataSet XML file \"%s\" into the database.",
                            currentInputFilePath),
                    e);
        }
    }

    private boolean isValidDataSetXML(File currentInputFile) throws Exception {
        DocumentBuilderFactory dbFactory = DocumentBuilderFactory.newInstance();
        DocumentBuilder dBuilder = dbFactory.newDocumentBuilder();
        Document document = dBuilder.parse(currentInputFile);
        if (!DATASET_XML_ROOT_ELEMENT_NAME.equalsIgnoreCase(document.getDocumentElement().getTagName())) {
            LOGGER.error(
                    "The file \"{}\" is not a valid XML DataSet, since it has a root element other than \"<{}>\".",
                    currentInputFile.getAbsolutePath(), DATASET_XML_ROOT_ELEMENT_NAME);
            return false;
        }
        return true;
    }

    IDatabaseConnection processParams(List<String> params, Properties properties)
            throws SQLException, DatabaseUnitException {

        boolean allowEmptyFields = false;
        final String allowEmptyFieldsProperty = getRequiredProperty(properties, "allowEmptyFields");
        if ("true".equalsIgnoreCase(allowEmptyFieldsProperty) || "yes".equalsIgnoreCase(allowEmptyFieldsProperty)) {
            LOGGER.info("Setting allowEmptyFields to \"true\" as configured in {}.", properties);
            allowEmptyFields = true;
        }

        String dbURL = getRequiredProperty(properties, "jdbc.url");
        String dbUsername = getRequiredProperty(properties, "jdbc.username");
        String dbPassword = getRequiredProperty(properties, "jdbc.password");

        boolean dbJarAlreadyLoaded = false;

        for (String param : params) {
            param = param.replaceAll("--", "");
            if (ARG_ALLOW_EMPTY_FIELDS.equalsIgnoreCase(param)) {
                LOGGER.info(
                        "Argument \"{}\" was specified. Empty fields will be allowed in the specified source files.",
                        ARG_ALLOW_EMPTY_FIELDS);
                allowEmptyFields = true;
                continue;
            }

            final String currentParamValue = param.substring(param.indexOf('=') + 1);

            if (param.toLowerCase().startsWith(ARG_JDBC_DRIVER_JAR_FILE_PATH)) {
                LOGGER.info("Loading specified JDBC driver class file \"{}\"...", currentParamValue);
                loadJdbcDriverClassFromJar(currentParamValue);
                dbJarAlreadyLoaded = true;
                continue;
            }

            if (param.toLowerCase().startsWith(ARG_DB_URL)) {
                dbURL = currentParamValue;
                LOGGER.info("Specified target database URL: \"{}\"", dbURL);
                continue;
            }
            if (param.toLowerCase().startsWith(ARG_DB_USERNAME)) {
                dbUsername = currentParamValue;
                LOGGER.info("Specified target database username: \"{}\"", dbUsername);
                continue;
            }
            if (param.toLowerCase().startsWith(ARG_DB_PASSWORD)) {
                dbPassword = currentParamValue;
                LOGGER.info("Specified target database password: \"{}\"", dbPassword);
                continue;
            }

            throw new IllegalArgumentException(
                    "Unsupported parameter \"%s\" specified. If you are trying to specify an XML file or a directory with a name that happens to start with a"
                            + " dash or a hyphen, try either specifying its absolute path, or prefixing it with \"."
                            + File.separatorChar + "\".");

        }

        final String dbJarPath = properties.getProperty("jdbc.jarpath");
        if (!dbJarAlreadyLoaded && dbJarPath != null) {
            LOGGER.info("Loading JDBC Driver JAR from location \"{}\" as specified in {}...", dbJarPath,
                    properties);
            loadJdbcDriverClassFromJar(dbJarPath);
        }

        if (!dbURL.toLowerCase().startsWith(JDBC_SCHEME_PREFIX)) {
            dbURL = JDBC_SCHEME_PREFIX + dbURL;
        }

        Connection jdbcConnection = DriverManager.getConnection(dbURL, dbUsername, dbPassword);
        IDatabaseConnection dbConnection = new DatabaseConnection(jdbcConnection);

        if (allowEmptyFields) {
            dbConnection.getConfig().setProperty(DatabaseConfig.FEATURE_ALLOW_EMPTY_FIELDS, true);
        }

        return dbConnection;
    }

    private String getRequiredProperty(Properties properties, String property) {
        final String propValue = properties.getProperty(property);
        if (propValue == null || propValue.trim().isEmpty()) {
            throw new IllegalArgumentException(String.format(
                    "Required property \"%s\" empty or missing in properties file \"%s\".", property, properties));
        }
        return propValue;
    }

    /**
     * Instructs the {@link System}'s {@link ClassLoader} to load a JDBC driver class file from an external JAR.
     * @param jarFilePath the path to the class file to be loaded.
     * @throws IllegalArgumentException if no class could successfully loaded from the specified path.
     */
    private void loadJdbcDriverClassFromJar(final String jarFilePath) throws IllegalArgumentException {

        try {

            URL u = new URL("jar:file:///" + jarFilePath + "!/");
            URLClassLoader ucl = new URLClassLoader(new URL[] { u });

            Reflections reflections = new Reflections(ucl);

            final Set<Class<? extends Driver>> detectedJdbcDrivers = reflections.getSubTypesOf(Driver.class);

            if (detectedJdbcDrivers.isEmpty()) {
                throw new IllegalArgumentException(String.format(
                        "The supplied JAR file at \"%s\" contains no JDBC drivers (which should implement the interface \"%s\").",
                        jarFilePath, Driver.class.getName()));
            }

            final int numberOfDetectedJdbcDrivers = detectedJdbcDrivers.size();
            if (numberOfDetectedJdbcDrivers > 1) {
                LOGGER.warn(
                        "Detected more than one ({}) JDBC drivers in the supplied JAR file at \"{}\". Choosing the first one...",
                        numberOfDetectedJdbcDrivers, jarFilePath);
            }

            Driver driver = detectedJdbcDrivers.iterator().next().newInstance();
            LOGGER.info("Loaded JDBC driver \"{}\".", driver.getClass().getName());
            DriverManager.registerDriver(new DriverShim(driver));

        } catch (InstantiationException e) {
            throw new IllegalArgumentException(String.format(
                    "JAR file \"%s\" apparently contains a JDBC Driver that is incompatible with the Java version "
                            + "on which the application is currently running (version %s). To solve this problem, either upgrade the Java version (recommended) or "
                            + "downgrade the JDBC driver.",
                    jarFilePath, System.getProperty("java.version")), e);
        } catch (Exception e) {
            throw new IllegalArgumentException(
                    String.format("Unable to load JDBC Driver class from JAR \"%s\".", jarFilePath), e);
        }
    }

}