com.chilmers.configbootstrapper.ConfigServletContextListener.java Source code

Java tutorial

Introduction

Here is the source code for com.chilmers.configbootstrapper.ConfigServletContextListener.java

Source

/**
 *   Copyright 2013 Christian Hilmersson
 *
 *   This file is part of config-bootstrapper 
 *   http://www.github.com/chilmers/config-bootstrapper
 *
 *   config-bootstrapper is free software; you can redistribute it and/or modify
 *   it under the terms of version 2.1 of the GNU Lesser General Public
 *   License as published by the Free Software Foundation.
 *
 *   config-bootstrapper 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 Lesser General Public License for more details.
 *
 *   You should have received a copy of the GNU Lesser General Public
 *   License along with config-bootstrapper; if not, write to the
 *   Free Software Foundation, Inc., 59 Temple Place, Suite 330,
 *   Boston, MA 02111-1307  USA
 */

package com.chilmers.configbootstrapper;

import java.io.InputStream;
import java.util.Enumeration;
import java.util.MissingResourceException;
import java.util.PropertyResourceBundle;

import javax.servlet.ServletContext;
import javax.servlet.ServletContextEvent;
import javax.servlet.ServletContextListener;

import org.apache.commons.lang.StringUtils;
import org.apache.log4j.LogManager;
import org.apache.log4j.Logger;
import org.apache.log4j.PropertyConfigurator;
import org.apache.log4j.xml.DOMConfigurator;

/**
 *  Config Bootstrapper - https://github.com/chilmers/config-bootstrapper/<br/>
 *    Helps initializing logging and application configuration for a system.<br/>
 *  This class implements a ServletContextListener and initializes 
 *  config-bootstrapper upon servlet context initialization.<br/>
 * <br/>
 * 
 * <h2>Quick start</h2> 
 * A more comprehensive usage guide is found further down.<br/>
 * <br/>
 * Add this to your web.xml before any other listeners that need to use the configuration or that needs to log.<br/>
 *<pre>
 *&lt;listener&gt;
 *    &lt;listener-class&gt;com.chilmers.configbootstrapper.ConfigServletContextListener&lt;/listener-class&gt;
 *&lt;/listener&gt;
 *</pre>
 * You can now use the system property "application.config.location" to read your config location, for example to inject your config in Spring.
 *<pre>&lt;context:property-placeholder location="${application.config.location}"/&gt;</pre>
 * Or use readApplicationConfiguration in ConfigHelper:<br/>
 *<pre>See {@link com.chilmers.configbootstrapper.ConfigHelper#readApplicationConfiguration()}</pre>
 * 
 * The application will now read <i>application.properties from the classpath</i>, if you need to read from another location
 * you might specify this with a system property (or environment variable or servlet context parameter) at startup.<br/>
 * <br/>For example:
 *<pre>
 *mvn jetty:run -Dapplication.config.location=file:/Users/chilmers/myApp/app-config.properties
 *</pre>
 * or if you want to read config from classpath (other than application.properties)
 *<pre>
 *mvn jetty:run -Dapplication.config.location=classpath:app-config.properties
 *</pre>
 *
 * At this stage the application will try to find a default log4j configuration file i.e. <i>log4j.xml or log4j.properties on the classpath</i>
 * If you need to change this, add an entry like this to your application configuration file:
 *<pre>application.log4j.config.location=file:/Users/chilmers/myApp/app-log4j.xml</pre>
 * or if you want to read logging config from the classpath (other than log4j.xml/log4j.properties)
 *<pre>application.log4j.config.location=classpath:app-log4j.xml</pre>
 * <h2>Main functionalities:</h2>
 * <ul>
 *  <li><strong>Determines which configuration file to use</strong><br/>
 *      Looks in the given order in system properties, environment variables, and servlet context parameters for the location of a 
 *      properties file to use for configuration.<br/>
 *      By default it looks for an entry named <strong>"application.config.location"</strong>. <br/>
 *      If no such entry was found the location <strong>defaults to classpath:application.properties</strong><br/>
 *      The location that was determined will be written to the system property (by default "application.config.location").<br/>
 *      The obvious benefit of doing this is in when no location has been specified, you will still be able to read the configuration
 *      location from this system property. In other environments where you want to add a configuration on the file system, you can do
 *      this and add the location as a system property, environment variable or servlet context parameter and still read the location
 *      from the system property.<br/>
 *      This makes it easy to use separate configurations for separate environments.
 *      Use classpath: for files on the classpath and file: to read from an external location.
 *  </li>
 *  <li><strong>Loads logging (log4J) configuration</strong><br/>
 *      Uses the given application configuration to specify a location of a log4j configuration file.
 *      By default it looks for an entry named <strong>"application.log4j.config.location"</strong> in the application configuration.<br/>
 *      In this way it is easy to provide different logging configurations for different environments.<br/>
 *      If no specific log4j-configuration is configured, it falls back to log4j's default configuration handling (e.g. log4j.xml or log4j.properties on the classpath)<br/>
 *  </li>
 *  <li><strong>Possibility to set system properties from application configuration</strong><br/>
 *      Entries in the configuration file starting with "system.property." will automatically be written to
 *      the system properties.<br/>
 *      Example:
 *      <br/>
 *      <pre>system.property.foo=bar</pre>
 *      Will write <tt>foo=bar</tt> as a system property, which is handy in some circumstances.<br/>
 *      Don't use this feature if you don't understand what it is, since it might clutter your system properties.<br/>
 *  </li>
 * </ul>
 * <br/>
 * 
 * <h2>Finding correct application configuration</h2>
 * This context listener will in the given order look in the system properties, environment variables<br/>
 * or the servlet context parameters for the location of a configuration file to use in the application.<br/>
 * By default it will look for an entry with the key "application.config.location". (This key name can be overridden, see Overriding defaults below)<br/>
 * If no such entry is found it will by default fall back to using "classpath:application.properties" as configuration location. (The fallback location can also be overridden if necessary, see below)<br/>
 * <br/>
 * Whichever configuration location string is decided, will be set in the system properties using the same key as above, 
 * i.e by default it will be set in system property "application.config.location"<br/> 
 * This makes it possible to locate the configuration from within the application, for example by reading it into a 
 * PropertyResourceBundle or by using Spring's PropertyPlaceholderConfigurer like this:
 * <br/>
 *<pre>  
 *&lt;bean id="placeholderConfig" class="org.springframework.beans.factory.config.PropertyPlaceholderConfigurer"&gt;
 *      &lt;property name="location" value="${application.config.location}" /&gt;
 *&lt;/bean&gt;
 *</pre>
 *  Or, if you have the context namespace defined, simply:<br/>
 *<pre>&lt;context:property-placeholder location="${application.config.location}"/&gt;</pre>
 *  Or use readApplicationConfiguration in ConfigHelper for non-Spring applications:<br/>
 *  See {@link com.chilmers.configbootstrapper.ConfigHelper#readApplicationConfiguration()}
 * <br/>
 * <br/> 
 * <h2>Logging configuration (i.e. Log4j)</h2>
 * This mechanism configures Log4j using a given file whose location is stated in the application configuration, 
 * or if no such file is available falls back to Log4j's default configuration behavior, <br/>
 * i.e. looks for log4j.xml or log4j.properties on the classpath.<br/>
 * Use this format classpath:my-log4j-config.xml to point out a file on the classpath.<br/>
 * If you want to point out a file on the file system you can either prefix with file: or just write the location as it is. 
 * <br/>
 * To use an external log4j file you will have to state the location of the file in the application configuration as 
 * a property with the key given by "application.log4j.config.location". (This key name can be overriden, see Overriding defaults below)<br/>
 * <br/>
 * <br/> 
 * <h2>Usage</h2>
 * Add this to your web.xml and make sure it is located before any application specific listeners that need
 * to use the configuration location property or that needs to log. E.g. before Spring's ContextLoaderListener<br/>
 *<pre>
 *&lt;listener&gt;
 *      &lt;listener-class&gt;com.chilmers.configbootstrapper.ConfigServletContextListener&lt;/listener-class&gt;
 *&lt;/listener&gt;
 *</pre>
 * 
 * If you want to specify an external configuration file (instead of the default "classpath:application.properties"), <br/>
 * add a context-param or more likely a system property or environment variable stating the location of your application configuration. <br/> 
 * For example:<br/>
 * <br/>
 *  As context param:<br/>
 *<pre>
 *&lt;context-param&gt;
 *      &lt;description&gt;The location of the application configuration. 
 *          If not set it defaults to classpath:application.properties&lt;/description&gt;
 *      &lt;param-name&gt;application.config.location&lt;/param-name&gt;
 *      &lt;param-value&gt;file:/Users/myusername/my-app-config/app.properties&lt;/param-value&gt;
 *&lt;/context-param&gt;
 *</pre>
 * <br/>
 *  As environment variable in a bash shell:<br/>
 *<pre>export application.config.location=file:/Users/myusername/my-app-config/app.properties</pre>
 *  <br/>
 *  As a system property upon starting your container:<br/>
 *<pre>java [your application] -Dapplication.config.location=file:/Users/myusername/my-app-config/app.properties</pre>
 * <h2>Overriding defaults</h2>
 * The following context-parameters can be set to configure the listener.<br/>
 * All of them have default values so they don't have to be set if not needed<br/>
 *<pre>
 *&lt;context-param&gt;
 *      &lt;description&gt;Sets the key for the entry that holds the application configuration location. 
 *          If not set it defaults to application.config.location&lt;/description&gt;
 *      &lt;param-name&gt;configServletContextListener.configLocationPropertyKey&lt;/param-name&gt;
 *      &lt;param-value&gt;myown.config.location&lt;/param-value&gt;
 *&lt;/context-param&gt;
 *</pre>
 *<pre>
 *&lt;context-param&gt;
 *      &lt;description&gt;Sets the key for where in the application configuration file to look for a log4j
 *          configuration file location.
 *          If not set it defaults to application.log4j.config.location&lt;/description&gt;
 *      &lt;param-name&gt;configServletContextListener.log4jConfigLocationPropertyKey&lt;/param-name&gt;
 *      &lt;param-value&gt;myown.log4j.config.location&lt;/param-value&gt;
 *&lt;/context-param&gt;
 *</pre>
 *<pre>
 *&lt;context-param&gt;
 *      &lt;description&gt;Sets the location of the application configuration to fall back to if no other configuration
 *          file location was set. E.g. a bundled configuration on the classpath. 
 *          If not set it defaults to classpath:application.properties&lt;/description&gt;
 *      &lt;param-name&gt;configServletContextListener.fallbackConfigLocation&lt;/param-name&gt;
 *      &lt;param-value&gt;classpath:myown.properties&lt;/param-value&gt;
 *&lt;/context-param&gt;
 *</pre>
 *<pre>
 *&lt;context-param&gt;
 *      &lt;description&gt;
 *      Application name that is printed when using System.out logging when no logging manager is available.
 *      Defaults to the display-name of the web.xml or if no display-name exists it will be ConfigServletContextListener
 *      &lt;/description&gt;
 *      &lt;param-name&gt;configServletContextListener.applicationName&lt;/param-name&gt;
 *      &lt;param-value&gt;My Application&lt;/param-value&gt;
 *&lt;/context-param&gt;
 *</pre>
 *  
 * @author Christian Hilmersson (https://github.com/chilmers/)
 */
public class ConfigServletContextListener implements ServletContextListener {

    private static final Logger log = Logger.getLogger(ConfigServletContextListener.class);

    /**
     * Default value for {@link ConfigServletContextListener#applicationName}
     */
    private static final String DEFAULT_APPLICATION_NAME = "ConfigServletContextListener";

    /**
     * The name of the context param to use for overriding {@link ConfigServletContextListener#applicationName}
     */
    private static final String OVERRIDE_DEFAULT_APPLICATION_NAME_PARAM = "configServletContextListener.applicationName";

    /**
     * Default value for {@link ConfigServletContextListener#configLocationPropertyKey}
     */
    protected static final String DEFAULT_CONFIG_LOCATION_PROPERTY_KEY = "application.config.location";

    /**
     * The name of the context param to use for overriding {@link ConfigServletContextListener#configLocationPropertyKey}
     */
    private static final String OVERRIDE_DEFAULT_CONFIG_LOCATION_PROPERTY_KEY_PARAM = "configServletContextListener.configLocationPropertyKey";

    /**
     * Default value for {@link ConfigServletContextListener#fallbackConfigLocation}
     */
    private static final String DEFAULT_FALLBACK_CONFIG_LOCATION = "classpath:application.properties";

    /**
     * The name of the context param to use for overriding {@link ConfigServletContextListener#fallbackConfigLocation}
     */
    private static final String OVERRIDE_DEFAULT_FALLBACK_CONFIG_LOCATION_PARAM = "configServletContextListener.fallbackConfigLocation";

    /**
     * Default value for {@link ConfigServletContextListener#log4jConfigLocationPropertyKey}
     */
    private static final String DEFAULT_LOG4J_CONFIG_LOCATION_PROPERTY_KEY = "application.log4j.config.location";

    /**
     * The name of the context param to use for overriding {@link ConfigServletContextListener#log4jConfigLocationPropertyKey}
     */
    private static final String OVERRIDE_DEFAULT_LOG4J_CONFIG_LOCATION_PROPERTY_KEY_PARAM = "configServletContextListener.log4jConfigLocationPropertyKey";

    /**
     * Prefix for system properties found in the application configuration file
     */
    private static final String CONFIG_SYSTEM_PROPERTY_PREFIX = "system.property.";

    /**
     * The location of the application configuration, to fall back on if the application configuration location
     * was not set by other means. 
     * 
     * Defaults to the value of {@link ConfigServletContextListener#DEFAULT_FALLBACK_CONFIG_LOCATION}
     * Can be overriden in web.xml by stating the following context-param
     *<pre>
     *&lt;context-param&gt;
     *      &lt;description&gt;Sets the location of the application configuration to fall back to if no other configuration
     *          file location was set. E.g. a bundled configuration on the classpath. 
     *          If not set it defaults to classpath:application.properties&lt;/description&gt;
     *      &lt;param-name&gt;configServletContextListener.fallbackConfigLocation&lt;/param-name&gt;
     *      &lt;param-value&gt;classpath:myown.properties&lt;/param-value&gt;
     *&lt;/context-param&gt;
     *</pre>
     */
    private String fallbackConfigLocation;

    /**
     * The key used to find the application configuration location.
     * The system will in order use this key to look up the config location value in the system properties, 
     * the environment variables and in the servlet context params.
     * It will use the first one that is defined, or default to "application.config.location" 
     * will be used to locate the config location.
     */
    private String configLocationPropertyKey;

    /**
     * This value is the key of a property in the application configuration which holds the log4j configuration file location.
     * It defaults to "application.log4j.config.location" and can be overridden in by stating a 
     * context-param in web.xml like this:
     * 
     *<pre>
     *&lt;context-param&gt;
     *      &lt;description&gt;Sets the key for where in the configuration file to look for a log4j
     *          configuration file location.
     *          If not set it defaults to application.log4j.config.location&lt;/description&gt;
     *      &lt;param-name&gt;configServletContextListener.log4jConfigLocationPropertyKey&lt;/param-name&gt;
     *      &lt;param-value&gt;myown.log4j.config.location&lt;/param-value&gt;
     *&lt;/context-param&gt;
     *</pre>
     * 
     * The entry that this key points to shall hold a value that ends with .xml or .properties and the 
     * which is the location of the log4j configuration.
     * If no configuration entry with this key exists or if it is empty then the default Log4j configuration
     * mechanism will be utilized, e.g. Log4j will look for log4j.xml or log4j.properties on the classpath.
     */
    private String log4jConfigLocationPropertyKey;

    /**
     * Application name that is printed when using System.out logging when no 
     * logging manager is available.
     * Defaults to the display-name of the web.xml or if no display-name exists, the value of
     * {@link ConfigServletContextListener#DEFAULT_APPLICATION_NAME}
     */
    private String applicationName;

    private ConfigHelper configHelper;

    /**
     * Configures the when the servlet context is initialized.
     * {@inheritDoc} 
     */
    public void contextInitialized(ServletContextEvent sce) {
        overrideDefaults(sce.getServletContext());
        String configLocation = getApplicationConfigurationLocation(sce.getServletContext());
        if (!configLocation.startsWith("classpath:") && !configLocation.startsWith("file:")) {
            configLocation = "file:" + configLocation;
            logToSystemOut("The application config location neither starts with classpath: nor file:, "
                    + "assuming " + configLocation);
        }
        setSystemProperty(this.configLocationPropertyKey, configLocation);

        PropertyResourceBundle config = configHelper.getApplicationConfiguration(configLocation);
        if (config != null) {
            loadApplicationConfigurationSystemProperties(config);
            loadLoggingConfiguration(config);
        }
    }

    private void overrideDefaults(ServletContext ctx) {
        this.configLocationPropertyKey = ctx.getInitParameter(OVERRIDE_DEFAULT_CONFIG_LOCATION_PROPERTY_KEY_PARAM);
        if (StringUtils.isBlank(this.configLocationPropertyKey)) {
            this.configLocationPropertyKey = DEFAULT_CONFIG_LOCATION_PROPERTY_KEY;
        }
        this.log4jConfigLocationPropertyKey = ctx
                .getInitParameter(OVERRIDE_DEFAULT_LOG4J_CONFIG_LOCATION_PROPERTY_KEY_PARAM);
        if (StringUtils.isBlank(this.log4jConfigLocationPropertyKey)) {
            this.log4jConfigLocationPropertyKey = DEFAULT_LOG4J_CONFIG_LOCATION_PROPERTY_KEY;
        }
        this.fallbackConfigLocation = ctx.getInitParameter(OVERRIDE_DEFAULT_FALLBACK_CONFIG_LOCATION_PARAM);
        if (StringUtils.isBlank(this.fallbackConfigLocation)) {
            this.fallbackConfigLocation = DEFAULT_FALLBACK_CONFIG_LOCATION;
        }
        this.applicationName = ctx.getInitParameter(OVERRIDE_DEFAULT_APPLICATION_NAME_PARAM);
        if (StringUtils.isBlank(this.applicationName)) {
            this.applicationName = ctx.getServletContextName();
        }
        if (StringUtils.isBlank(this.applicationName)) {
            this.applicationName = DEFAULT_APPLICATION_NAME;
        }
        configHelper = new ConfigHelper(applicationName);
    }

    /**
     * Decides which application configuration file to use and sets the location in the system properties.
     * By default the location will be set in the property with key "application.config.location" but this 
     * can be overridden by configuration
     */
    private String getApplicationConfigurationLocation(ServletContext ctx) {
        logToSystemOut("Servlet context initialized, checking for configuration location parameters...");
        logToSystemOut("Checking for system property " + this.configLocationPropertyKey);
        String configLocation = System.getProperty(this.configLocationPropertyKey);
        if (configLocation == null) {
            logToSystemOut("Didn't find system property " + this.configLocationPropertyKey
                    + " holding application configuration location, checking environment variable "
                    + this.configLocationPropertyKey);
            configLocation = System.getenv(this.configLocationPropertyKey);
        }
        if (configLocation == null) {
            logToSystemOut("Didn't find environment variable " + this.configLocationPropertyKey
                    + " holding application configuration location, checking servlet context-param "
                    + this.configLocationPropertyKey);
            configLocation = ctx.getInitParameter(this.configLocationPropertyKey);
        }
        if (configLocation == null) {
            logToSystemOut("Didn't find servlet-context variable " + this.configLocationPropertyKey
                    + " holding application configuration location, " + "using fallback configuration location: "
                    + this.fallbackConfigLocation);
            configLocation = this.fallbackConfigLocation;
        }

        return configLocation;
    }

    /**
     * Short hand for ConfigHelper#logToSystemOut
     * @see ConfigHelper#logToSystemOut(String) 
     * @param text the text to log to System.out
     */
    private void logToSystemOut(String text) {
        configHelper.logToSystemOut(text);
    }

    protected void setSystemProperty(String key, String value) {
        logToSystemOut("Setting system property " + key);
        System.setProperty(key, value);
    }

    /**
     * Loads system properties from the application configuration
     *  
     * @param configBundle The application configuration
     */
    protected void loadApplicationConfigurationSystemProperties(PropertyResourceBundle configBundle) {
        logToSystemOut("Checking for system properties in application configuration");
        Enumeration<String> keys = configBundle.getKeys();

        while (keys.hasMoreElements()) {

            String key = keys.nextElement();

            if (key.startsWith(CONFIG_SYSTEM_PROPERTY_PREFIX)) {
                String systemPropertyKey = key.substring(CONFIG_SYSTEM_PROPERTY_PREFIX.length());

                if (systemPropertyKey.length() > 0) {
                    setSystemProperty(systemPropertyKey, configBundle.getString(key));
                }
            }
        }
    }

    /**
     * Loads the log4j configuration from a file whose location is given in the application configuration. 
     * 
     * The location of the log4j configuration file shall by default be specified in 
     * an entry in the application configuration file with key "application.log4j.config.location"
     * This key can be overridden by configuration if you need to. 
     * 
     * If no such property is found in the application configuration, log4j's default 
     * configuration mechanism will be used, e.g. it will look for log4j.xml 
     * or log4j.properties on the classpath.
     */
    private void loadLoggingConfiguration(PropertyResourceBundle configBundle) {
        logToSystemOut("Finding log4j configuration location in application configuration...");

        String log4jConfigLocation = null;

        try {
            log4jConfigLocation = configBundle.getString(this.log4jConfigLocationPropertyKey);
        } catch (MissingResourceException e) {
            logToSystemOut("No log4j configuration location was found for property "
                    + this.log4jConfigLocationPropertyKey + " in the application configuration. ");
        }

        if (StringUtils.isNotBlank(log4jConfigLocation)) {
            LogManager.resetConfiguration();
            logToSystemOut("Found log4j configuration location in the application configuration. "
                    + "Configuring logger using file: " + log4jConfigLocation);
            if (log4jConfigLocation.startsWith("file:")) {
                log4jConfigLocation = log4jConfigLocation.replaceFirst("file:", "");
            }
            if (log4jConfigLocation.endsWith(".xml")) {
                if (log4jConfigLocation.startsWith("classpath:")) {
                    log4jConfigLocation = log4jConfigLocation.replaceFirst("classpath:", "");
                    InputStream is = Thread.currentThread().getContextClassLoader()
                            .getResourceAsStream(log4jConfigLocation);
                    new DOMConfigurator().doConfigure(is, LogManager.getLoggerRepository());
                } else {
                    DOMConfigurator.configureAndWatch(log4jConfigLocation);
                }
            } else if (log4jConfigLocation.endsWith(".properties")) {
                if (log4jConfigLocation.startsWith("classpath:")) {
                    log4jConfigLocation = log4jConfigLocation.replaceFirst("classpath:", "");
                    InputStream is = Thread.currentThread().getContextClassLoader()
                            .getResourceAsStream(log4jConfigLocation);
                    PropertyConfigurator.configure(is);
                } else {
                    PropertyConfigurator.configureAndWatch(log4jConfigLocation);
                }
            } else {
                logToSystemOut("The log4j configuration file location must end with .xml or .properties. "
                        + "\nFalling back to the default log4j configuration mechanism.");
            }
        } else {
            logToSystemOut("Didn't find log4j configuration location in application configuration. "
                    + "Falling back to the default log4j configuration mechanism.");
        }
        log.info("Log4j was configured, see System.out log for initialization information.");
    }

    /**
     * Destroys the servlet context and shutting down the Log Manager 
     * which in turn is stopping Log4j's watch dog thread.
     * {@inheritDoc}
     */
    public void contextDestroyed(ServletContextEvent sce) {
        log.debug("Servlet context destroyed");
        log.debug("Shutting down log manager...");
        logToSystemOut("Destroying servlet context...");
        logToSystemOut("Shutting down log manager...");
        LogManager.shutdown();
        logToSystemOut("The log manager has been shut down.");
        logToSystemOut("The servlet context has been destroyed.");
    }

}