PropertyConfigurator.java

/*
 * Licensed to the Apache Software Foundation (ASF) under one or more
 * contributor license agreements.  See the NOTICE file distributed with
 * this work for additional information regarding copyright ownership.
 * The ASF licenses this file to You 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.
 */

// Contibutors: "Luke Blanshard" <Luke@quiq.com>
//              "Mark DONSZELMANN" <Mark.Donszelmann@cern.ch>
//               Anders Kristensen <akristensen@dynamicsoft.com>

package org.apache.log4j;

import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.InterruptedIOException;
import java.net.URLConnection;
import java.util.Enumeration;
import java.util.Hashtable;
import java.util.Properties;
import java.util.StringTokenizer;
import java.util.Vector;
import java.util.Iterator;
import java.util.Map;

import org.apache.log4j.config.PropertySetter;
import org.apache.log4j.helpers.FileWatchdog;
import org.apache.log4j.helpers.LogLog;
import org.apache.log4j.helpers.OptionConverter;
import org.apache.log4j.or.RendererMap;
import org.apache.log4j.spi.Configurator;
import org.apache.log4j.spi.Filter;
import org.apache.log4j.spi.LoggerFactory;
import org.apache.log4j.spi.LoggerRepository;
import org.apache.log4j.spi.OptionHandler;
import org.apache.log4j.spi.RendererSupport;
import org.apache.log4j.spi.ThrowableRenderer;
import org.apache.log4j.spi.ThrowableRendererSupport;
import org.apache.log4j.spi.ErrorHandler;

/**
 * Allows the configuration of log4j from an external file. See
 * <b>{@link #doConfigure(String, LoggerRepository)}</b> for the expected
 * format.
 *
 * <p>
 * It is sometimes useful to see how log4j is reading configuration files. You can enable log4j internal logging by
 * defining the <b>log4j.debug</b> variable.
 *
 * <p>
 * As of log4j version 0.8.5, at class initialization time class, the file
 * <b>log4j.properties</b> will be searched from the search path used to load
 * classes. If the file can be found, then it will be fed to the {@link PropertyConfigurator#configure(java.net.URL)}
 * method.
 *
 * <p>
 * The <code>PropertyConfigurator</code> does not handle the advanced configuration features supported by the
 * {@link org.apache.log4j.xml.DOMConfigurator DOMConfigurator} such as support custom
 * {@link org.apache.log4j.spi.ErrorHandler ErrorHandlers}, nested appenders such as the
 * {@link org.apache.log4j.AsyncAppender AsyncAppender}, etc.
 *
 * <p>
 * All option <em>values</em> admit variable substitution. The syntax of variable substitution is similar to that of
 * Unix shells. The string between an opening <b>&quot;${&quot;</b> and closing <b>&quot;}&quot;</b> is interpreted as a
 * key. The value of the substituted variable can be defined as a system property or in the configuration file itself.
 * The value of the key is first searched in the system properties, and if not found there, it is then searched in the
 * configuration file being parsed. The corresponding value replaces the ${variableName} sequence. For example, if
 * <code>java.home</code> system property is set to <code>/home/xyz</code>, then every occurrence of the sequence
 * <code>${java.home}</code> will be interpreted as
 * <code>/home/xyz</code>.
 *
 * @author Ceki G&uuml;lc&uuml;
 * @author Anders Kristensen
 * @since 0.8.1
 */
public class PropertyConfigurator implements Configurator {

    /**
     * Used internally to keep track of configured appenders.
     */
    protected Hashtable registry = new Hashtable(11);
    private LoggerRepository repository;
    protected LoggerFactory loggerFactory = new DefaultCategoryFactory();

    static final String CATEGORY_PREFIX = "log4j.category.";
    static final String LOGGER_PREFIX = "log4j.logger.";
    static final String FACTORY_PREFIX = "log4j.factory";
    static final String ADDITIVITY_PREFIX = "log4j.additivity.";
    static final String ROOT_CATEGORY_PREFIX = "log4j.rootCategory";
    static final String ROOT_LOGGER_PREFIX = "log4j.rootLogger";
    static final String APPENDER_PREFIX = "log4j.appender.";
    static final String RENDERER_PREFIX = "log4j.renderer.";
    static final String THRESHOLD_PREFIX = "log4j.threshold";
    private static final String THROWABLE_RENDERER_PREFIX = "log4j.throwableRenderer";
    private static final String LOGGER_REF = "logger-ref";
    private static final String ROOT_REF = "root-ref";
    private static final String APPENDER_REF_TAG = "appender-ref";

    /**
     * Key for specifying the {@link org.apache.log4j.spi.LoggerFactory LoggerFactory}. Currently set to
     * "<code>log4j.loggerFactory</code>".
     */
    public static final String LOGGER_FACTORY_KEY = "log4j.loggerFactory";

    /**
     * If property set to true, then hierarchy will be reset before configuration.
     */
    private static final String RESET_KEY = "log4j.reset";

    static final private String INTERNAL_ROOT_NAME = "root";

    /**
     * Read configuration from a file. <b>The existing configuration is not cleared nor reset.</b> If you require a
     * different behavior, then call {@link LogManager#resetConfiguration resetConfiguration} method before calling
     * <code>doConfigure</code>.
     *
     * <p>
     * The configuration file consists of statements in the format
     * <code>key=value</code>. The syntax of different configuration elements are
     * discussed below.
     *
     * <h3>Repository-wide threshold</h3>
     *
     * <p>
     * The repository-wide threshold filters logging requests by level regardless of logger. The syntax is:
     *
     * <pre>
     * log4j.threshold=[level]
     * </pre>
     *
     * <p>
     * The level value can consist of the string values OFF, FATAL, ERROR, WARN, INFO, DEBUG, ALL or a <em>custom
     * level</em> value. A custom level value can be specified in the form level#classname. By default the
     * repository-wide threshold is set to the lowest possible value, namely the level
     * <code>ALL</code>.
     * </p>
     *
     *
     * <h3>Appender configuration</h3>
     *
     * <p>
     * Appender configuration syntax is:
     *
     * <pre>
     * # For appender named <i>appenderName</i>, set its class.
     * # Note: The appender name can contain dots.
     * log4j.appender.appenderName=fully.qualified.name.of.appender.class
     *
     * # Set appender specific options.
     * log4j.appender.appenderName.option1=value1
     * ...
     * log4j.appender.appenderName.optionN=valueN
     * </pre>
     * <p>
     * For each named appender you can configure its {@link Layout}. The syntax for configuring an appender's layout
     * is:
     *
     * <pre>
     * log4j.appender.appenderName.layout=fully.qualified.name.of.layout.class
     * log4j.appender.appenderName.layout.option1=value1
     * ....
     * log4j.appender.appenderName.layout.optionN=valueN
     * </pre>
     * <p>
     * The syntax for adding {@link Filter}s to an appender is:
     *
     * <pre>
     * log4j.appender.appenderName.filter.ID=fully.qualified.name.of.filter.class
     * log4j.appender.appenderName.filter.ID.option1=value1
     * ...
     * log4j.appender.appenderName.filter.ID.optionN=valueN
     * </pre>
     * <p>
     * The first line defines the class name of the filter identified by ID; subsequent lines with the same ID specify
     * filter option - value paris. Multiple filters are added to the appender in the lexicographic order of IDs.
     * <p>
     * The syntax for adding an {@link ErrorHandler} to an appender is:
     *
     * <pre>
     * log4j.appender.appenderName.errorhandler=fully.qualified.name.of.filter.class
     * log4j.appender.appenderName.errorhandler.root-ref={true|false}
     * log4j.appender.appenderName.errorhandler.logger-ref=loggerName
     * log4j.appender.appenderName.errorhandler.appender-ref=appenderName
     * log4j.appender.appenderName.errorhandler.option1=value1
     * ...
     * log4j.appender.appenderName.errorhandler.optionN=valueN
     * </pre>
     *
     * <h3>Configuring loggers</h3>
     *
     * <p>
     * The syntax for configuring the root logger is:
     *
     * <pre>
     * log4j.rootLogger=[level], appenderName, appenderName, ...
     * </pre>
     *
     * <p>
     * This syntax means that an optional <em>level</em> can be supplied followed by appender names separated by
     * commas.
     *
     * <p>
     * The level value can consist of the string values OFF, FATAL, ERROR, WARN, INFO, DEBUG, ALL or a <em>custom
     * level</em> value. A custom level value can be specified in the form <code>level#classname</code>.
     *
     * <p>
     * If a level value is specified, then the root level is set to the corresponding level. If no level value is
     * specified, then the root level remains untouched.
     *
     * <p>
     * The root logger can be assigned multiple appenders.
     *
     * <p>
     * Each <i>appenderName</i> (separated by commas) will be added to the root logger. The named appender is defined
     * using the appender syntax defined above.
     *
     * <p>
     * For non-root categories the syntax is almost the same:
     *
     * <pre>
     * log4j.logger.logger_name=[level|INHERITED|NULL], appenderName, appenderName, ...
     * </pre>
     *
     * <p>
     * The meaning of the optional level value is discussed above in relation to the root logger. In addition however,
     * the value INHERITED can be specified meaning that the named logger should inherit its level from the logger
     * hierarchy.
     *
     * <p>
     * If no level value is supplied, then the level of the named logger remains untouched.
     *
     * <p>
     * By default categories inherit their level from the hierarchy. However, if you set the level of a logger and later
     * decide that that logger should inherit its level, then you should specify INHERITED as the value for the level
     * value. NULL is a synonym for INHERITED.
     *
     * <p>
     * Similar to the root logger syntax, each <i>appenderName</i> (separated by commas) will be attached to the named
     * logger.
     *
     * <p>
     * See the <a href="../../../../manual.html#additivity">appender additivity rule</a> in the user manual for the
     * meaning of the <code>additivity</code> flag.
     *
     * <h3>ObjectRenderers</h3>
     * <p>
     * You can customize the way message objects of a given type are converted to String before being logged. This is
     * done by specifying an {@link org.apache.log4j.or.ObjectRenderer ObjectRenderer} for the object type would like to
     * customize.
     *
     * <p>
     * The syntax is:
     *
     * <pre>
     * log4j.renderer.fully.qualified.name.of.rendered.class = fully.qualified.name.of.rendering.class
     * </pre>
     * <p>
     * As in,
     *
     * <pre>
     * log4j.renderer.my.Fruit = my.FruitRenderer
     * </pre>
     *
     * <h3>ThrowableRenderer</h3>
     * <p>
     * You can customize the way an instance of Throwable is converted to String before being logged. This is done by
     * specifying an {@link org.apache.log4j.spi.ThrowableRenderer ThrowableRenderer}.
     *
     * <p>
     * The syntax is:
     *
     * <pre>
     * log4j.throwableRenderer=fully.qualified.name.of.rendering.class
     * log4j.throwableRenderer.paramName=paramValue
     * </pre>
     * <p>
     * As in,
     *
     * <pre>
     * log4j.throwableRenderer = org.apache.log4j.EnhancedThrowableRenderer
     * </pre>
     *
     * <h3>Logger Factories</h3>
     * <p>
     * The usage of custom logger factories is discouraged and no longer documented.
     *
     * <h3>Resetting Hierarchy</h3>
     * <p>
     * The hierarchy will be reset before configuration when log4j.reset=true is present in the properties file.
     *
     * <h3>Example</h3>
     *
     * <p>
     * An example configuration is given below. Other configuration file examples are given in the <code>examples</code>
     * folder.
     *
     * <pre>
     *
     * # Set options for appender named "A1".
     * # Appender "A1" will be a SyslogAppender
     * log4j.appender.A1=org.apache.log4j.net.SyslogAppender
     *
     * # The syslog daemon resides on www.abc.net
     * log4j.appender.A1.SyslogHost=www.abc.net
     *
     * # A1's layout is a PatternLayout, using the conversion pattern
     * # <b>%r %-5p %c{2} %M.%L %x - %m\n</b>. Thus, the log output will
     * # include # the relative time since the start of the application in
     * # milliseconds, followed by the level of the log request,
     * # followed by the two rightmost components of the logger name,
     * # followed by the callers method name, followed by the line number,
     * # the nested disgnostic context and finally the message itself.
     * # Refer to the documentation of {@link PatternLayout} for further information
     * # on the syntax of the ConversionPattern key.
     * log4j.appender.A1.layout=org.apache.log4j.PatternLayout
     * log4j.appender.A1.layout.ConversionPattern=%-4r %-5p %c{2} %M.%L %x - %m\n
     *
     * # Set options for appender named "A2"
     * # A2 should be a RollingFileAppender, with maximum file size of 10 MB
     * # using at most one backup file. A2's layout is TTCC, using the
     * # ISO8061 date format with context printing enabled.
     * log4j.appender.A2=org.apache.log4j.RollingFileAppender
     * log4j.appender.A2.MaxFileSize=10MB
     * log4j.appender.A2.MaxBackupIndex=1
     * log4j.appender.A2.layout=org.apache.log4j.TTCCLayout
     * log4j.appender.A2.layout.ContextPrinting=enabled
     * log4j.appender.A2.layout.DateFormat=ISO8601
     *
     * # Root logger set to DEBUG using the A2 appender defined above.
     * log4j.rootLogger=DEBUG, A2
     *
     * # Logger definitions:
     * # The SECURITY logger inherits is level from root. However, it's output
     * # will go to A1 appender defined above. It's additivity is non-cumulative.
     * log4j.logger.SECURITY=INHERIT, A1
     * log4j.additivity.SECURITY=false
     *
     * # Only warnings or above will be logged for the logger "SECURITY.access".
     * # Output will go to A1.
     * log4j.logger.SECURITY.access=WARN
     *
     *
     * # The logger "class.of.the.day" inherits its level from the
     * # logger hierarchy.  Output will go to the appender's of the root
     * # logger, A2 in this case.
     * log4j.logger.class.of.the.day=INHERIT
     * </pre>
     *
     * <p>
     * Refer to the <b>setOption</b> method in each Appender and Layout for class specific options.
     *
     * <p>
     * Use the <code>#</code> or <code>!</code> characters at the beginning of a line for comments.
     *
     * <p>
     *
     * @param configFileName The name of the configuration file where the configuration information is stored.
     */
    public void doConfigure(String configFileName, LoggerRepository hierarchy) {
        Properties props = new Properties();
        FileInputStream istream = null;
        try {
            istream = new FileInputStream(configFileName);
            props.load(istream);
            istream.close();
        } catch (Exception e) {
            if (e instanceof InterruptedIOException || e instanceof InterruptedException) {
                Thread.currentThread().interrupt();
            }
            LogLog.error("Could not read configuration file [" + configFileName + "].", e);
            LogLog.error("Ignoring configuration file [" + configFileName + "].");
            return;
        } finally {
            if (istream != null) {
                try {
                    istream.close();
                } catch (InterruptedIOException ignore) {
                    Thread.currentThread().interrupt();
                } catch (Throwable ignore) {
                }

            }
        }
        // If we reach here, then the config file is alright.
        doConfigure(props, hierarchy);
    }

    /**
     *
     */
    static public void configure(String configFilename) {
        new PropertyConfigurator().doConfigure(configFilename, LogManager.getLoggerRepository());
    }

    /**
     * Read configuration options from url <code>configURL</code>.
     *
     * @since 0.8.2
     */
    public static void configure(java.net.URL configURL) {
        new PropertyConfigurator().doConfigure(configURL, LogManager.getLoggerRepository());
    }

    /**
     * Reads configuration options from an InputStream.
     *
     * @since 1.2.17
     */
    public static void configure(InputStream inputStream) {
        new PropertyConfigurator().doConfigure(inputStream, LogManager.getLoggerRepository());
    }

    /**
     * Read configuration options from <code>properties</code>.
     * <p>
     * See {@link #doConfigure(String, LoggerRepository)} for the expected format.
     */
    static public void configure(Properties properties) {
        new PropertyConfigurator().doConfigure(properties, LogManager.getLoggerRepository());
    }

    /**
     * Like {@link #configureAndWatch(String, long)} except that the default delay as defined by
     * {@link FileWatchdog#DEFAULT_DELAY} is used.
     *
     * @param configFilename A file in key=value format.
     */
    static public void configureAndWatch(String configFilename) {
        configureAndWatch(configFilename, FileWatchdog.DEFAULT_DELAY);
    }

    /**
     * Read the configuration file <code>configFilename</code> if it exists. Moreover, a thread will be created that
     * will periodically check if
     * <code>configFilename</code> has been created or modified. The period is
     * determined by the <code>delay</code> argument. If a change or file creation is detected, then
     * <code>configFilename</code> is read to configure log4j.
     *
     * @param configFilename A file in key=value format.
     * @param delay          The delay in milliseconds to wait between each check.
     */
    static public void configureAndWatch(String configFilename, long delay) {
        PropertyWatchdog pdog = new PropertyWatchdog(configFilename);
        pdog.setDelay(delay);
        pdog.start();
    }

    /**
     * Read configuration options from <code>properties</code>.
     * <p>
     * See {@link #doConfigure(String, LoggerRepository)} for the expected format.
     */
    public void doConfigure(Properties properties, LoggerRepository hierarchy) {
        repository = hierarchy;
        String value = properties.getProperty(LogLog.DEBUG_KEY);
        if (value == null) {
            value = properties.getProperty("log4j.configDebug");
            if (value != null)
                LogLog.warn("[log4j.configDebug] is deprecated. Use [log4j.debug] instead.");
        }

        if (value != null) {
            LogLog.setInternalDebugging(OptionConverter.toBoolean(value, true));
        }

        //
        // if log4j.reset=true then
        // reset hierarchy
        String reset = properties.getProperty(RESET_KEY);
        if (reset != null && OptionConverter.toBoolean(reset, false)) {
            hierarchy.resetConfiguration();
        }

        String thresholdStr = OptionConverter.findAndSubst(THRESHOLD_PREFIX, properties);
        if (thresholdStr != null) {
            hierarchy.setThreshold(OptionConverter.toLevel(thresholdStr, (Level) Level.ALL));
            LogLog.debug("Hierarchy threshold set to [" + hierarchy.getThreshold() + "].");
        }

        configureRootCategory(properties, hierarchy);
        configureLoggerFactory(properties);
        parseCatsAndRenderers(properties, hierarchy);

        LogLog.debug("Finished configuring.");
        // We don't want to hold references to appenders preventing their
        // garbage collection.
        registry.clear();
    }

    /**
     * Read configuration options from url <code>configURL</code>.
     *
     * @since 1.2.17
     */
    public void doConfigure(InputStream inputStream, LoggerRepository hierarchy) {
        Properties props = new Properties();
        try {
            props.load(inputStream);
        } catch (IOException e) {
            if (e instanceof InterruptedIOException) {
                Thread.currentThread().interrupt();
            }
            LogLog.error("Could not read configuration file from InputStream [" + inputStream + "].", e);
            LogLog.error("Ignoring configuration InputStream [" + inputStream + "].");
            return;
        }
        this.doConfigure(props, hierarchy);
    }

    /**
     * Read configuration options from url <code>configURL</code>.
     */
    public void doConfigure(java.net.URL configURL, LoggerRepository hierarchy) {
        Properties props = new Properties();
        LogLog.debug("Reading configuration from URL " + configURL);
        InputStream istream = null;
        URLConnection uConn = null;
        try {
            uConn = configURL.openConnection();
            uConn.setUseCaches(false);
            istream = uConn.getInputStream();
            props.load(istream);
        } catch (Exception e) {
            if (e instanceof InterruptedIOException || e instanceof InterruptedException) {
                Thread.currentThread().interrupt();
            }
            LogLog.error("Could not read configuration file from URL [" + configURL + "].", e);
            LogLog.error("Ignoring configuration file [" + configURL + "].");
            return;
        } finally {
            if (istream != null) {
                try {
                    istream.close();
                } catch (InterruptedIOException ignore) {
                    Thread.currentThread().interrupt();
                } catch (IOException ignore) {
                } catch (RuntimeException ignore) {
                }
            }
        }
        doConfigure(props, hierarchy);
    }

    // --------------------------------------------------------------------------
    // Internal stuff
    // --------------------------------------------------------------------------

    /**
     * Check the provided <code>Properties</code> object for a {@link org.apache.log4j.spi.LoggerFactory LoggerFactory}
     * entry specified by {@link #LOGGER_FACTORY_KEY}. If such an entry exists, an attempt is made to create an instance
     * using the default constructor. This instance is used for subsequent Category creations within this configurator.
     *
     * @see #parseCatsAndRenderers
     */
    protected void configureLoggerFactory(Properties props) {
        String factoryClassName = OptionConverter.findAndSubst(LOGGER_FACTORY_KEY, props);
        if (factoryClassName != null) {
            LogLog.debug("Setting category factory to [" + factoryClassName + "].");
            loggerFactory = (LoggerFactory) OptionConverter.instantiateByClassName(factoryClassName,
                    LoggerFactory.class, loggerFactory);
            PropertySetter.setProperties(loggerFactory, props, FACTORY_PREFIX + ".");
        }
    }

    /*
     * void configureOptionHandler(OptionHandler oh, String prefix, Properties
     * props) { String[] options = oh.getOptionStrings(); if(options == null)
     * return;
     *
     * String value; for(int i = 0; i < options.length; i++) { value =
     * OptionConverter.findAndSubst(prefix + options[i], props); LogLog.debug(
     * "Option " + options[i] + "=[" + (value == null? "N/A" : value)+"]."); // Some
     * option handlers assume that null value are not passed to them. // So don't
     * remove this check if(value != null) { oh.setOption(options[i], value); } }
     * oh.activateOptions(); }
     */

    void configureRootCategory(Properties props, LoggerRepository hierarchy) {
        String effectiveFrefix = ROOT_LOGGER_PREFIX;
        String value = OptionConverter.findAndSubst(ROOT_LOGGER_PREFIX, props);

        if (value == null) {
            value = OptionConverter.findAndSubst(ROOT_CATEGORY_PREFIX, props);
            effectiveFrefix = ROOT_CATEGORY_PREFIX;
        }

        if (value == null)
            LogLog.debug("Could not find root logger information. Is this OK?");
        else {
            Logger root = hierarchy.getRootLogger();
            synchronized (root) {
                parseCategory(props, root, effectiveFrefix, INTERNAL_ROOT_NAME, value);
            }
        }
    }

    /**
     * Parse non-root elements, such non-root categories and renderers.
     */
    protected void parseCatsAndRenderers(Properties props, LoggerRepository hierarchy) {
        Enumeration enumeration = props.propertyNames();
        while (enumeration.hasMoreElements()) {
            String key = (String) enumeration.nextElement();
            if (key.startsWith(CATEGORY_PREFIX) || key.startsWith(LOGGER_PREFIX)) {
                String loggerName = null;
                if (key.startsWith(CATEGORY_PREFIX)) {
                    loggerName = key.substring(CATEGORY_PREFIX.length());
                } else if (key.startsWith(LOGGER_PREFIX)) {
                    loggerName = key.substring(LOGGER_PREFIX.length());
                }
                String value = OptionConverter.findAndSubst(key, props);
                Logger logger = hierarchy.getLogger(loggerName, loggerFactory);
                synchronized (logger) {
                    parseCategory(props, logger, key, loggerName, value);
                    parseAdditivityForLogger(props, logger, loggerName);
                }
            } else if (key.startsWith(RENDERER_PREFIX)) {
                String renderedClass = key.substring(RENDERER_PREFIX.length());
                String renderingClass = OptionConverter.findAndSubst(key, props);
                if (hierarchy instanceof RendererSupport) {
                    RendererMap.addRenderer((RendererSupport) hierarchy, renderedClass, renderingClass);
                }
            } else if (key.equals(THROWABLE_RENDERER_PREFIX)) {
                if (hierarchy instanceof ThrowableRendererSupport) {
                    ThrowableRenderer tr = (ThrowableRenderer) OptionConverter.instantiateByKey(props,
                            THROWABLE_RENDERER_PREFIX, org.apache.log4j.spi.ThrowableRenderer.class, null);
                    if (tr == null) {
                        LogLog.error("Could not instantiate throwableRenderer.");
                    } else {
                        PropertySetter setter = new PropertySetter(tr);
                        setter.setProperties(props, THROWABLE_RENDERER_PREFIX + ".");
                        ((ThrowableRendererSupport) hierarchy).setThrowableRenderer(tr);

                    }
                }
            }
        }
    }

    /**
     * Parse the additivity option for a non-root category.
     */
    void parseAdditivityForLogger(Properties props, Logger cat, String loggerName) {
        String value = OptionConverter.findAndSubst(ADDITIVITY_PREFIX + loggerName, props);
        LogLog.debug("Handling " + ADDITIVITY_PREFIX + loggerName + "=[" + value + "]");
        // touch additivity only if necessary
        if ((value != null) && (!value.equals(""))) {
            boolean additivity = OptionConverter.toBoolean(value, true);
            LogLog.debug("Setting additivity for \"" + loggerName + "\" to " + additivity);
            cat.setAdditivity(additivity);
        }
    }

    /**
     * This method must work for the root category as well.
     */
    void parseCategory(Properties props, Logger logger, String optionKey, String loggerName, String value) {

        LogLog.debug("Parsing for [" + loggerName + "] with value=[" + value + "].");
        // We must skip over ',' but not white space
        StringTokenizer st = new StringTokenizer(value, ",");

        // If value is not in the form ", appender.." or "", then we should set
        // the level of the loggeregory.

        if (!(value.startsWith(",") || value.equals(""))) {

            // just to be on the safe side...
            if (!st.hasMoreTokens())
                return;

            String levelStr = st.nextToken();
            LogLog.debug("Level token is [" + levelStr + "].");

            // If the level value is inherited, set category level value to
            // null. We also check that the user has not specified inherited for the
            // root category.
            if (INHERITED.equalsIgnoreCase(levelStr) || NULL.equalsIgnoreCase(levelStr)) {
                if (loggerName.equals(INTERNAL_ROOT_NAME)) {
                    LogLog.warn("The root logger cannot be set to null.");
                } else {
                    logger.setLevel(null);
                }
            } else {
                logger.setLevel(OptionConverter.toLevel(levelStr, (Level) Level.DEBUG));
            }
            LogLog.debug("Category " + loggerName + " set to " + logger.getLevel());
        }

        // Begin by removing all existing appenders.
        logger.removeAllAppenders();

        Appender appender;
        String appenderName;
        while (st.hasMoreTokens()) {
            appenderName = st.nextToken().trim();
            if (appenderName == null || appenderName.equals(","))
                continue;
            LogLog.debug("Parsing appender named \"" + appenderName + "\".");
            appender = parseAppender(props, appenderName);
            if (appender != null) {
                logger.addAppender(appender);
            }
        }
    }

    Appender parseAppender(Properties props, String appenderName) {
        Appender appender = registryGet(appenderName);
        if ((appender != null)) {
            LogLog.debug("Appender \"" + appenderName + "\" was already parsed.");
            return appender;
        }
        // Appender was not previously initialized.
        String prefix = APPENDER_PREFIX + appenderName;
        String layoutPrefix = prefix + ".layout";

        appender = (Appender) OptionConverter.instantiateByKey(props, prefix, org.apache.log4j.Appender.class, null);
        if (appender == null) {
            LogLog.error("Could not instantiate appender named \"" + appenderName + "\".");
            return null;
        }
        appender.setName(appenderName);

        if (appender instanceof OptionHandler) {
            if (appender.requiresLayout()) {
                Layout layout = (Layout) OptionConverter.instantiateByKey(props, layoutPrefix, Layout.class, null);
                if (layout != null) {
                    appender.setLayout(layout);
                    LogLog.debug("Parsing layout options for \"" + appenderName + "\".");
                    // configureOptionHandler(layout, layoutPrefix + ".", props);
                    PropertySetter.setProperties(layout, props, layoutPrefix + ".");
                    LogLog.debug("End of parsing for \"" + appenderName + "\".");
                }
            }
            final String errorHandlerPrefix = prefix + ".errorhandler";
            String errorHandlerClass = OptionConverter.findAndSubst(errorHandlerPrefix, props);
            if (errorHandlerClass != null) {
                ErrorHandler eh = (ErrorHandler) OptionConverter.instantiateByKey(props, errorHandlerPrefix,
                        ErrorHandler.class, null);
                if (eh != null) {
                    appender.setErrorHandler(eh);
                    LogLog.debug("Parsing errorhandler options for \"" + appenderName + "\".");
                    parseErrorHandler(eh, errorHandlerPrefix, props, repository);
                    final Properties edited = new Properties();
                    final String[] keys = new String[] { errorHandlerPrefix + "." + ROOT_REF,
                            errorHandlerPrefix + "." + LOGGER_REF, errorHandlerPrefix + "." + APPENDER_REF_TAG };
                    for (Iterator iter = props.entrySet().iterator(); iter.hasNext(); ) {
                        Map.Entry entry = (Map.Entry) iter.next();
                        int i = 0;
                        for (; i < keys.length; i++) {
                            if (keys[i].equals(entry.getKey()))
                                break;
                        }
                        if (i == keys.length) {
                            edited.put(entry.getKey(), entry.getValue());
                        }
                    }
                    PropertySetter.setProperties(eh, edited, errorHandlerPrefix + ".");
                    LogLog.debug("End of errorhandler parsing for \"" + appenderName + "\".");
                }

            }
            // configureOptionHandler((OptionHandler) appender, prefix + ".", props);
            PropertySetter.setProperties(appender, props, prefix + ".");
            LogLog.debug("Parsed \"" + appenderName + "\" options.");
        }
        parseAppenderFilters(props, appenderName, appender);
        registryPut(appender);
        return appender;
    }

    private void parseErrorHandler(final ErrorHandler eh, final String errorHandlerPrefix, final Properties props,
            final LoggerRepository hierarchy) {
        boolean rootRef = OptionConverter.toBoolean(OptionConverter.findAndSubst(errorHandlerPrefix + ROOT_REF, props),
                false);
        if (rootRef) {
            eh.setLogger(hierarchy.getRootLogger());
        }
        String loggerName = OptionConverter.findAndSubst(errorHandlerPrefix + LOGGER_REF, props);
        if (loggerName != null) {
            Logger logger = (loggerFactory == null)
                    ? hierarchy.getLogger(loggerName)
                    : hierarchy.getLogger(loggerName, loggerFactory);
            eh.setLogger(logger);
        }
        String appenderName = OptionConverter.findAndSubst(errorHandlerPrefix + APPENDER_REF_TAG, props);
        if (appenderName != null) {
            Appender backup = parseAppender(props, appenderName);
            if (backup != null) {
                eh.setBackupAppender(backup);
            }
        }
    }

    void parseAppenderFilters(Properties props, String appenderName, Appender appender) {
        // extract filters and filter options from props into a hashtable mapping
        // the property name defining the filter class to a list of pre-parsed
        // name-value pairs associated to that filter
        final String filterPrefix = APPENDER_PREFIX + appenderName + ".filter.";
        int fIdx = filterPrefix.length();
        Hashtable filters = new Hashtable();
        Enumeration e = props.keys();
        String name = "";
        while (e.hasMoreElements()) {
            String key = (String) e.nextElement();
            if (key.startsWith(filterPrefix)) {
                int dotIdx = key.indexOf('.', fIdx);
                String filterKey = key;
                if (dotIdx != -1) {
                    filterKey = key.substring(0, dotIdx);
                    name = key.substring(dotIdx + 1);
                }
                Vector filterOpts = (Vector) filters.get(filterKey);
                if (filterOpts == null) {
                    filterOpts = new Vector();
                    filters.put(filterKey, filterOpts);
                }
                if (dotIdx != -1) {
                    String value = OptionConverter.findAndSubst(key, props);
                    filterOpts.add(new NameValue(name, value));
                }
            }
        }

        // sort filters by IDs, insantiate filters, set filter options,
        // add filters to the appender
        Enumeration g = new SortedKeyEnumeration(filters);
        while (g.hasMoreElements()) {
            String key = (String) g.nextElement();
            String clazz = props.getProperty(key);
            if (clazz != null) {
                LogLog.debug(
                        "Filter key: [" + key + "] class: [" + props.getProperty(key) + "] props: " + filters.get(key));
                Filter filter = (Filter) OptionConverter.instantiateByClassName(clazz, Filter.class, null);
                if (filter != null) {
                    PropertySetter propSetter = new PropertySetter(filter);
                    Vector v = (Vector) filters.get(key);
                    Enumeration filterProps = v.elements();
                    while (filterProps.hasMoreElements()) {
                        NameValue kv = (NameValue) filterProps.nextElement();
                        propSetter.setProperty(kv.key, kv.value);
                    }
                    propSetter.activate();
                    LogLog.debug(
                            "Adding filter of type [" + filter.getClass() + "] to appender named [" + appender.getName()
                                    + "].");
                    appender.addFilter(filter);
                }
            } else {
                LogLog.warn("Missing class definition for filter: [" + key + "]");
            }
        }
    }

    void registryPut(Appender appender) {
        registry.put(appender.getName(), appender);
    }

    Appender registryGet(String name) {
        return (Appender) registry.get(name);
    }
}

class PropertyWatchdog extends FileWatchdog {

    PropertyWatchdog(String filename) {
        super(filename);
    }

    /**
     * Call {@link PropertyConfigurator#configure(String)} with the
     * <code>filename</code> to reconfigure log4j.
     */
    public void doOnChange() {
        new PropertyConfigurator().doConfigure(filename, LogManager.getLoggerRepository());
    }
}

class NameValue {
    String key, value;

    public NameValue(String key, String value) {
        this.key = key;
        this.value = value;
    }

    public String toString() {
        return key + "=" + value;
    }
}

class SortedKeyEnumeration implements Enumeration {

    private Enumeration e;

    public SortedKeyEnumeration(Hashtable ht) {
        Enumeration f = ht.keys();
        Vector keys = new Vector(ht.size());
        for (int i, last = 0; f.hasMoreElements(); ++last) {
            String key = (String) f.nextElement();
            for (i = 0; i < last; ++i) {
                String s = (String) keys.get(i);
                if (key.compareTo(s) <= 0)
                    break;
            }
            keys.add(i, key);
        }
        e = keys.elements();
    }

    public boolean hasMoreElements() {
        return e.hasMoreElements();
    }

    public Object nextElement() {
        return e.nextElement();
    }
}