org.finra.herd.dao.Log4jOverridableConfigurer.java Source code

Java tutorial

Introduction

Here is the source code for org.finra.herd.dao.Log4jOverridableConfigurer.java

Source

/*
* Copyright 2015 herd 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.finra.herd.dao;

import java.io.ByteArrayInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.sql.Clob;
import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.sql.Statement;

import javax.sql.DataSource;

import edu.umd.cs.findbugs.annotations.SuppressFBWarnings;
import org.apache.commons.io.IOUtils;
import org.apache.commons.lang3.StringUtils;
import org.apache.logging.log4j.core.LoggerContext;
import org.apache.logging.log4j.core.config.ConfigurationSource;
import org.apache.logging.log4j.core.config.Configurator;
import org.apache.logging.log4j.core.config.xml.XmlConfiguration;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.BeansException;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.config.BeanPostProcessor;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationContextAware;
import org.springframework.core.PriorityOrdered;
import org.springframework.core.io.Resource;

/**
 * Configure Log4J based on a possible resource override location, a possible database override location, and a default resource location, in that order. The
 * Log4J configuration itself can include an optional "monitorInterval" attribute that allows the Log4J configuration to be monitored for changes and
 * re-initialized if changes are found.
 * <p/>
 * This class will write to System.out and System.err for output before the logging is successfully initialized. Once the logging is initialized, logging will
 * go to where the logger is configured. Note that it is important to use valid Log4J configuration files for Log4J to work properly. If an invalid
 * configuration file is used, Log4J will output the error to System.err, but will not throw an exception so this class will not know that logging hasn't been
 * configured properly. That means that application logging messages could be lost. If logging was initialized successfully and a new override file is found
 * that is invalid, the previous valid configuration will remain in effect until a new valid configuration is found.
 * <p/>
 * This class contains nested named inner-classes. The reason for this was to keep everything required for the Log4JOverridableConfigurer functionality fully
 * encapsulated within one file so it could be easily ported if needed.
 * <p/>
 * This bean is a bean post processor and is priority ordered. By being a bean post processor, this class will get called early in the bean creation lifecycle:
 * specifically after the beans are instantiated, but before they are used. By being priority ordered (as opposed to standard ordering), our bean will get
 * called before other post processors. This is important to ensure logging is initialized before other things are initialized that could have problems. If
 * other beans have problems and perform error logging (e.g. Hibernate session manager, Ehcache, JGroups), we will loose that logging if we haven't initialized
 * our logging first.
 * <p/>
 * To use a Log4J configuration specified in the database, configure these properties:
 * <pre>
 *     dataSource: the data source for the database to connect to.
 *     tableName: the table name that contains the Log4J configuration.
 *     selectColumn: the column to select that contains the Log4J configuration.
 *     whereColumn and whereValue: an optional column name and value to filter on if multiple rows would be returned where a single row is required.
 * </pre>
 * <p/>
 * To use an externally specified Log4J override configuration, configure the overrideResourceLocation (e.g. file:///tmp/myOverride.xml).
 * <p/>
 * To use the default Log4J configuration, specify an internally bundled defaultResourceLocation (e.g. classpath:herd-log4j.xml).
 * <p/>
 * Given the difficulty of initializing Log4J from a non-URI such as the database, the approach taken is to read the configuration from the database and copy it
 * to a temporary file on the file system and initializes Log4J from there. Then the database watchdog thread will re-read the configuration as specified in the
 * configuration itself and will update the temporary file. Since the interval for the local file being updated and the interval when Log4J monitors the file
 * for changes are different, the actual changes may be slightly delayed, but no longer than double the configured interval.
 */
@SuppressFBWarnings(value = "SQL_PREPARED_STATEMENT_GENERATED_FROM_NONCONSTANT_STRING", justification = "The SQL used within this class is self-contained and only varies based on the Spring configuration so there is no risk of SQL injection.")
public class Log4jOverridableConfigurer implements BeanPostProcessor, PriorityOrdered, ApplicationContextAware {
    // Only use this once logging has been initialized.
    private static final Logger LOGGER = LoggerFactory.getLogger(Log4jOverridableConfigurer.class);

    @Autowired
    private ApplicationContext applicationContext;

    // Configurable properties
    private String overrideResourceLocation;

    private String defaultResourceLocation;

    private String tableName;

    private String selectColumn;

    private String whereColumn;

    private String whereValue;

    private DataSource dataSource;

    private boolean loggingInitialized;

    private String existingDbLog4JConfiguration;

    private Path tempFile = null;

    private Log4jDbWatchdog watchdog;

    private LoggerContext loggerContext;

    @Override
    public Object postProcessBeforeInitialization(Object bean, String beanName) throws BeansException {
        // Perform our initialization one time even though this method will be called once for each bean being initialized.
        // The bean properties will have already been set by the time this method is called which is required for our initialization.
        if (!loggingInitialized) {
            initLogging();
            loggingInitialized = true;
        }

        // Perform the standard processing by returning the original bean.
        return bean;
    }

    @Override
    public Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException {
        // Don't do anything here but return the original bean.
        return bean;
    }

    /**
     * Perform the Log4J initialization (after the properties have been set).
     */
    // Using System.out and System.err is okay here because we need a place to output information before logging is initialized.
    @SuppressWarnings("PMD.SystemPrintln")
    private void initLogging() {
        // First try the override resource location. This gives the local machine a chance to override the configuration. This could be useful
        // for local developers who want to create a local override configuration file.
        boolean initialized = initializeLog4jFromResourceLocation(overrideResourceLocation);

        // If the override resource location wasn't used, then try the database configuration. This is useful when you have a cluster of application
        // servers that all share a common configuration.
        if (!initialized) {
            initialized = initializeLog4jFromDatabase();
        }

        // If no override location was found, try the default resource location which is typically one bundled within the WAR.
        if (!initialized) {
            initialized = initializeLog4jFromResourceLocation(defaultResourceLocation);
        }

        // If we didn't find any Log4J configuration, then display an error.
        if (!initialized) {
            // We shouldn't typically get here if a valid Log4J configuration file was bundled with the WAR.
            System.err.println("Unable to find a valid Log4J configuration using database query \""
                    + getLog4JConfigurationRetrievalQuery(true) + "\" or the override resource location \""
                    + overrideResourceLocation + "\" or the default resource location \"" + defaultResourceLocation
                    + "\".");
        }
    }

    /**
     * Initializes Log4J from a resource location.
     *
     * @param resourceLocation the resource location.
     *
     * @return true if Log4J was initialized or false if not.
     */
    // Using System.out and System.err is okay here because we need a place to output information before logging is initialized.
    @SuppressWarnings("PMD.SystemPrintln")
    private boolean initializeLog4jFromResourceLocation(String resourceLocation) {
        // Default the return boolean to false (i.e. we didn't initialize Log4J).
        boolean isInitSuccessful = false;

        // See if an override resource location is configured.
        if (StringUtils.isNotBlank(resourceLocation)) {
            // Trim the resource location and get a handle to the resource.
            String resourceLocationTrimmed = resourceLocation.trim();
            Resource resource = applicationContext.getResource(resourceLocationTrimmed);

            // If the resource exists, then initialize Log4J with the resource.
            if (resource.exists()) {
                // Initialize Log4J from the resource location.
                // Write the "good" parameters to System.out since logging hasn't been initialized yet.
                System.out.println("Using Log4J configuration location \"" + resourceLocationTrimmed + "\".");

                // Initialize Log4J with the resource. The configuration itself can use "monitorInterval" to have it refresh if it came from a file.
                loggerContext = Configurator.initialize(null, resourceLocationTrimmed);

                // For some initialization errors, a null context will be returned.
                if (loggerContext == null) {
                    // We shouldn't get here since we already checked if the location existed previously.
                    throw new IllegalArgumentException("Invalid configuration found at resource location: \""
                            + resourceLocationTrimmed + "\".");
                }

                // Now that Logging has been initialized, log something so we know it's working.
                // Note that it is possible that Log4J didn't initialize properly and didn't let us know. In this case, an error will be displayed on the
                // console by Log4J and the default logging level will be "error". As such, the below logging message won't be displayed.
                LOGGER.info("Logging successfully initialized.");

                // Mark that we successfully initialized Log4J - as much as we're able to.
                isInitSuccessful = true;
            }
        }

        // Return if we successfully initialized Log4J or not.
        return isInitSuccessful;
    }

    /**
     * Initializes Log4J from the database.
     *
     * @return true if Log4J was initialized or false if not.
     */
    // Using System.out and System.err is okay here because we need a place to output information before logging is initialized.
    @SuppressWarnings("PMD.SystemPrintln")
    private boolean initializeLog4jFromDatabase() {
        // Default the return boolean to false (i.e. we didn't initialize Log4J).
        boolean isInitSuccessful = false;

        // Try the database override location if all the necessary DB parameters were specified.
        if (isDbConfigurationPresent()) {
            existingDbLog4JConfiguration = getLog4JConfigurationFromDatabase();
        }

        // If a database configuration was found, try to initialize with it.
        if (StringUtils.isNotBlank(existingDbLog4JConfiguration)) {
            // Write the "good" parameters to System.out since logging hasn't been initialized yet.
            System.out.println("Using Log4J configuration from the database using query \""
                    + getLog4JConfigurationRetrievalQuery(true) + "\".");

            // Proceed with the initial Log4J initialization based on the retrieved database configuration.
            initializeConfiguration(existingDbLog4JConfiguration);

            // Now that Logging has been initialized, log something so we know it's working.
            LOGGER.info("Logging successfully initialized.");

            // Mark that we successfully initialized Log4J.
            isInitSuccessful = true;
        }

        // Return if we successfully initialized Log4J or not.
        return isInitSuccessful;
    }

    /**
     * Returns whether the database configuration is present or not (i.e. the select column and the tableName have both been specified).
     *
     * @return true if specified or false if not.
     */
    private boolean isDbConfigurationPresent() {
        return StringUtils.isNotBlank(selectColumn) && StringUtils.isNotBlank(tableName);
    }

    /**
     * Gets the Log4J configuration retrieval query.
     *
     * @param isForLogging If true, the statement will be returned for logging purposes (i.e. the where clause value will be present instead of a "?"
     * character.
     *
     * @return the query.
     */
    private String getLog4JConfigurationRetrievalQuery(boolean isForLogging) {
        StringBuilder stringBuilder = new StringBuilder();
        stringBuilder.append("SELECT ").append(selectColumn).append(" FROM ").append(tableName);
        if (StringUtils.isNotBlank(whereColumn) && StringUtils.isNotBlank(whereValue)) {
            stringBuilder.append(" WHERE ").append(whereColumn).append(" = ");
            if (isForLogging) {
                stringBuilder.append('\'').append(whereValue).append('\'');
            } else {
                stringBuilder.append('?');
            }
        }
        return stringBuilder.toString();
    }

    /**
     * Gets the Log4J override configuration from the database.
     *
     * @return the Log4J configuration or null if no configuration was found.
     */
    protected String getLog4JConfigurationFromDatabase() {
        // Create a JDBC operation that can execute our query and return the Log4J configuration.
        JdbcOperation<String> jdbcOperation = new JdbcOperation<String>(dataSource) {
            @Override
            protected String performOperation() throws SQLException {
                try (ResultSet resultSet = openResultSet(getLog4JConfigurationRetrievalQuery(false), whereValue)) {
                    if (resultSet.next()) {
                        return getConfigurationFromResultSet(resultSet, selectColumn);
                    }

                    // No results were found so return null.
                    return null;
                }
            }
        };

        // Execute the JDBC operation.
        try {
            return jdbcOperation.execute();
        } catch (SQLException ex) {
            // In the case of a SQL exception, just return null indicating there is no override configuration available.
            LOGGER.error("SQL Exception encountered while getting Log4J override configuration.", ex);
            return null;
        }
    }

    /**
     * Extracts the configuration from the given result set. The passed in {@code ResultSet} was created by a SELECT statement on the underlying database table.
     * The selectColumn is the column that will be retrieved from the result set. Normally the contained value is directly returned. However, if it is of type
     * {@code CLOB}, text is extracted as string. This method was mostly obtained from the DatabaseConfiguration class.
     *
     * @param resultSet the current {@code ResultSet}
     * @param selectColumn the selected column to obtain from the result set.
     *
     * @return the Log4J configuration.
     * @throws SQLException if an error occurs.
     */
    protected String getConfigurationFromResultSet(ResultSet resultSet, String selectColumn) throws SQLException {
        // Default to a return value of null.
        String returnString = null;

        // Get the value from the result set for the select column.
        Object resultSetValue = resultSet.getObject(selectColumn);
        if (resultSetValue != null) {
            if (resultSetValue instanceof Clob) {
                // If we have a CLOB, we need to extract the contents of the CLOB to a string.
                returnString = convertClob((Clob) resultSetValue);
            } else {
                // If we have any other value (should typically be a string), perform toString on it to ensure we have a string to return.
                returnString = resultSetValue.toString();
            }
        }

        // Return the return value as a string.
        return returnString;
    }

    /**
     * Converts a CLOB to a string. This method was mostly obtained from DatabaseConfiguration.
     *
     * @param clob the CLOB to be converted.
     *
     * @return the extracted string value.
     * @throws SQLException if an error occurs.
     */
    protected String convertClob(Clob clob) throws SQLException {
        int len = (int) clob.length();
        return (len > 0) ? clob.getSubString(1, len) : ""; // Note getSubString has the first character at position 1 (not 0).
    }

    @Override
    public int getOrder() {
        // We want our logging to come very early in the bean creation process so we are setting the priority very high.
        // (0 is the highest and MAX_INT is the lowest). We're using 100 to make it high, but not 0 to give other beans we write a chance to
        // use a higher priority if desired.
        return 100;
    }

    @Override
    public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
        this.applicationContext = applicationContext;
    }

    /**
     * Set the Log4J override configuration resource location to use. If the override location doesn't exist, the default resource location will be attempted.
     *
     * @param overrideResourceLocation the location.
     */
    public void setOverrideResourceLocation(String overrideResourceLocation) {
        this.overrideResourceLocation = overrideResourceLocation;
    }

    /**
     * The default Log4J configuration location to use if the override location isn't found.
     *
     * @param defaultResourceLocation the default location.
     */
    public void setDefaultResourceLocation(String defaultResourceLocation) {
        this.defaultResourceLocation = defaultResourceLocation;
    }

    /**
     * Sets the table name that contains the Log4J database override configuration.
     *
     * @param tableName the table name.
     */
    public void setTableName(String tableName) {
        this.tableName = tableName;
    }

    /**
     * Sets the column to be selected (from the specified "tableName") that contains the Log4J database override configuration.
     *
     * @param selectColumn the select column.
     */
    public void setSelectColumn(String selectColumn) {
        this.selectColumn = selectColumn;
    }

    /**
     * Sets the optional where column to filter on to determine which row to retrieve that contains the Log4J database override configuration. If no where
     * column or value is specified, then only one row should exist in the specified table.
     *
     * @param whereColumn the where column to filter on.
     */
    public void setWhereColumn(String whereColumn) {
        this.whereColumn = whereColumn;
    }

    /**
     * Sets the optional where clause value to filter on to determine which row to retrieve that contains the Log4J database override configuration. If no where
     * column or value is specified, then only one row should exist in the specified table.
     *
     * @param whereValue the where value to filter on.
     */
    public void setWhereValue(String whereValue) {
        this.whereValue = whereValue;
    }

    /**
     * Sets the data source to use when connecting to the database to get the Log4J override configuration.
     *
     * @param dataSource the data source.
     */
    public void setDataSource(DataSource dataSource) {
        this.dataSource = dataSource;
    }

    /**
     * Initializes a Log4J configuration based on the specified configuration string.
     *
     * @param xmlConfigurationString the configuration string.
     */
    // Using System.out and System.err is okay here because we need a place to output information before logging is initialized.
    @SuppressWarnings("PMD.SystemPrintln")
    private void initializeConfiguration(String xmlConfigurationString) {
        try {
            // Note whether this is the first time we are creating the configuration based on whether we already created a temp configuration file or not.
            boolean initialConfiguration = tempFile == null;

            // If this is the initial configuration, create a temp file and add a shutdown hook so it will get cleaned up upon JVM exit.
            if (initialConfiguration) {
                tempFile = Files.createTempFile("log4jTempConfig", ".xml");
                System.out
                        .println("Created temporary logging configuration file: \"" + tempFile.toString() + "\".");
                Runtime.getRuntime().addShutdownHook(new ShutdownHook(tempFile));
            }

            // Write the Log4J configuration to the temporary file every time.
            try (FileOutputStream fileOutputStream = new FileOutputStream(tempFile.toAbsolutePath().toString())) {
                IOUtils.write(xmlConfigurationString, fileOutputStream);
            }

            // Get the refresh interval from the configuration.
            int refreshIntervalSeconds = getRefreshIntervalSeconds(xmlConfigurationString);

            if (initialConfiguration) {
                System.out.println("Initial logging refresh interval: " + refreshIntervalSeconds + " second(s).");

                // This is the initial configuration so tell Log4J to do the initialization based on the temporary file we just created.
                // The configuration file itself can use "monitorInterval" to have it refresh if it came from a file. We will let
                // Log4J do it's auto-refresh from the temporary file, but we will update that file ourselves using our watchdog.
                loggerContext = Configurator.initialize(tempFile.toString(), null, tempFile.toUri());

                // Create and start a watchdog thread to monitor the DB configuration and when a change is found, it will call this method again
                // to overwrite the temporary file with the new configuration.
                watchdog = new Log4jDbWatchdog(refreshIntervalSeconds);
                watchdog.start();
            } else {
                // If this isn't the initial configuration, update the refresh interval (or set it the same value). That way, if a new refresh interval
                // was specified in the configuration file, we will grab it and adjust the watchdog interval.
                watchdog.setRefreshIntervalSeconds(refreshIntervalSeconds);
            }
        } catch (IOException ex) {
            throw new IllegalStateException(
                    "Unable to initialize Log4J with configuration: \"" + xmlConfigurationString + "\".", ex);
        }
    }

    /**
     * Gets the refresh interval in seconds from the Log4J XML configuration.
     *
     * @param xmlConfigurationString the XML configuration.
     *
     * @return the refresh interval in seconds.
     * @throws IOException if there were any errors parsing the configuration file.
     */
    private int getRefreshIntervalSeconds(String xmlConfigurationString) throws IOException {
        // Create the Log4J configuration object from the configuration string and get the refresh interval in seconds from the configuration.
        XmlConfiguration xmlConfiguration = new XmlConfiguration(loggerContext, new ConfigurationSource(
                new ByteArrayInputStream(xmlConfigurationString.getBytes(StandardCharsets.UTF_8))));
        return xmlConfiguration.getWatchManager().getIntervalSeconds();
    }

    /**
     * An internally used helper class for simplifying database access through plain JDBC. This class provides a simple framework for creating and executing a
     * JDBC statement. It especially takes care of proper handling of JDBC resources even in case of an error. This code was mostly taken from the Commons
     * Configuration DatabaseConfiguration class:
     * <pre>
     * https://apache.googlesource.com/commons-configuration/+/trunk/src/main/java/org/apache/commons/configuration2/DatabaseConfiguration.java
     * </pre>
     * Using a simple low level JDBC class such as this as opposed to a higher level abstraction such as JPA/Hibernate is preferred since we want logging
     * initialized before as many other technologies as possible. That way, we will be able to capture higher level technology logging in our Log4J logs and not
     * lose it.
     *
     * @param <T> the type of the results produced by a JDBC operation
     */
    protected abstract class JdbcOperation<T> {
        /**
         * Stores the connection.
         */
        private Connection connection;

        /**
         * Stores the statement.
         */
        private PreparedStatement preparedStatement;

        /**
         * Stores the result set.
         */
        private ResultSet resultSet;

        /**
         * Stores the data source used to connect to the database.
         */
        private DataSource dataSource;

        /**
         * Creates a new instance of {@code JdbcOperation}.
         *
         * @param dataSource the data source to the database.
         */
        protected JdbcOperation(DataSource dataSource) {
            this.dataSource = dataSource;
        }

        /**
         * Performs the JDBC operation. This method is called by {@code execute()} after this object has been fully initialized. Here the actual JDBC logic has
         * to be placed.
         *
         * @return the result of the operation
         * @throws SQLException if an SQL error occurs
         */
        protected abstract T performOperation() throws SQLException;

        /**
         * Executes this operation. This method obtains a database connection and then delegates to {@code performOperation()}. Afterwards it performs the
         * necessary clean up.
         *
         * @return the result of the operation.
         * @throws SQLException if there was a problem executing the operation.
         */
        public T execute() throws SQLException {
            T result = null;
            try {
                connection = dataSource.getConnection();
                result = performOperation();
            } finally {
                close(connection, preparedStatement, resultSet);
            }
            return result;
        }

        /**
         * Creates a {@code PreparedStatement} for a query, initializes it and executes it. The resulting {@code ResultSet} is returned.
         *
         * @param sql the statement to be executed.
         * @param params the parameters for the statement.
         *
         * @return the {@code ResultSet} produced by the query.
         * @throws SQLException if an SQL error occurs.
         */
        protected ResultSet openResultSet(String sql, Object... params) throws SQLException {
            resultSet = initStatement(sql, params).executeQuery();
            return resultSet;
        }

        /**
         * Creates an initializes a {@code PreparedStatement} object for executing an SQL statement.
         *
         * @param sql the statement to be executed
         * @param parameters the parameters for the statement
         *
         * @return the initialized statement object
         * @throws SQLException if an SQL error occurs
         */
        protected PreparedStatement initStatement(String sql, Object... parameters) throws SQLException {
            // Create the prepared statement.
            preparedStatement = getConnection().prepareStatement(sql);

            // Loop through all the parameters and set them on the prepared statement.
            for (int i = 0; i < parameters.length; i++) {
                preparedStatement.setObject(i + 1, parameters[i]);
            }

            // Return the prepared statement.
            return preparedStatement;
        }

        /**
         * Returns the current connection. This method can be called while {@code execute()} is running. It returns <b>null</b> otherwise.
         *
         * @return the current connection
         */
        protected Connection getConnection() {
            return connection;
        }

        /**
         * Close the specified database objects. Avoid closing if null and hide any SQLExceptions that occur.
         *
         * @param connection The database connection to close
         * @param statement The statement to close
         * @param resultSet the result set to close
         */
        protected void close(Connection connection, Statement statement, ResultSet resultSet) {
            // Close the result set if it exists.
            try {
                if (resultSet != null) {
                    resultSet.close();
                }
            } catch (SQLException ex) {
                LOGGER.error("An error occurred on closing the result set.", ex);
            }

            // Close the statement if it exists.
            try {
                if (statement != null) {
                    statement.close();
                }
            } catch (SQLException ex) {
                LOGGER.error("An error occurred on closing the statement.", ex);
            }

            // Close the connection if it exists.
            try {
                if (connection != null) {
                    connection.close();
                }
            } catch (SQLException ex) {
                LOGGER.error("An error occurred on closing the connection.", ex);
            }
        }
    }

    /**
     * A shutdown hook that will delete the temporary logging configuration file when the JVM exits.
     */
    // Using System.out and System.err is okay here because we need a place to output information before logging is initialized.
    // We are printing a stack trace because logging may not have been initialized.
    @SuppressWarnings({ "PMD.SystemPrintln", "PMD.AvoidPrintStackTrace" })
    private static class ShutdownHook extends Thread {
        private Path tempFile = null;

        public ShutdownHook(Path tempFile) {
            this.tempFile = tempFile;
        }

        @Override
        public void run() {
            try {
                System.out
                        .println("Deleting temporary logging configuration file: \"" + tempFile.toString() + "\".");
                Files.delete(tempFile);
            } catch (IOException e) {
                // Print a stack trace since logging may not have been initialized.
                System.err.println(
                        "Unable to delete temporary Log4J configuration file: \"" + tempFile.toString() + "\".");
                e.printStackTrace();
            }
        }
    }

    /**
     * An internally used watchdog thread that keeps querying the database to retrieve the Log4J configuration each time it wakes up. Each time it queries the
     * configuration, it compares it to the previously read configuration and if any changes are present, the Log4J configuration is re-initialized. This is
     * similar to what Log4J provides out of the box for configurations in files. Note that this thread will first sleep before it does anything so it is
     * important that the caller first initializes Log4J before this thread is started.
     */
    protected class Log4jDbWatchdog extends Thread {
        private long refreshIntervalSeconds;

        public Log4jDbWatchdog(long refreshIntervalSeconds) {
            this.refreshIntervalSeconds = refreshIntervalSeconds;
        }

        /**
         * Sets the refresh interval in seconds. Note that this is the time we will monitor for DB changes and not the time that Log4J will check for temporary
         * file updates so an additional delay will be present before the DB changes will be applied.
         *
         * @param refreshIntervalSeconds the refresh interval.
         */
        public void setRefreshIntervalSeconds(long refreshIntervalSeconds) {
            if (refreshIntervalSeconds != this.refreshIntervalSeconds) {
                LOGGER.info("Logging refresh interval changed from " + this.refreshIntervalSeconds + " to "
                        + refreshIntervalSeconds + " second(s).");
            }
            this.refreshIntervalSeconds = refreshIntervalSeconds;
        }

        /**
         * The main "run" method which gets invoked when this thread gets started.
         */
        @Override
        public void run() {
            // Keep running until the JVM exits or until we explicitly break out of the loop.
            while (true) {
                // Once the refresh is turned off, exit the loop which will stop the watchdog.
                if (refreshIntervalSeconds <= 0) {
                    LOGGER.info("Logging refresh interval is <= 0 so no more monitoring will occur.");
                    break;
                }

                try {
                    // Sleep for the refresh interval.
                    Thread.sleep(refreshIntervalSeconds * 1000);
                } catch (InterruptedException ex) {
                    // We don't really expect to get interrupted, but log a warning message so we're aware of it just in case.
                    LOGGER.warn("An attempt was made to interrupt a sleeping thread.", ex);
                }

                // Each time we wake up, read the latest configuration from the database and re-configure Log4J if a change is found from the previously
                // read configuration.
                String latestLog4JConfiguration = getLog4JConfigurationFromDatabase();
                if ((StringUtils.isNotBlank(latestLog4JConfiguration))
                        && (!(latestLog4JConfiguration.equals(existingDbLog4JConfiguration)))) {
                    LOGGER.info("Log4J configuration change found in database so Log4J will be re-initialized.");
                    existingDbLog4JConfiguration = latestLog4JConfiguration;
                    initializeConfiguration(existingDbLog4JConfiguration);
                }
            }
        }
    }
}