/*
 * 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.
 */

package org.apache.log4j.xml;

import org.apache.log4j.Appender;
import org.apache.log4j.Layout;
import org.apache.log4j.Level;
import org.apache.log4j.LogManager;
import org.apache.log4j.Logger;
import org.apache.log4j.config.PropertySetter;
import org.apache.log4j.helpers.FileWatchdog;
import org.apache.log4j.helpers.Loader;
import org.apache.log4j.helpers.LogLog;
import org.apache.log4j.helpers.OptionConverter;
import org.apache.log4j.or.RendererMap;
import org.apache.log4j.spi.AppenderAttachable;
import org.apache.log4j.spi.Configurator;
import org.apache.log4j.spi.ErrorHandler;
import org.apache.log4j.spi.Filter;
import org.apache.log4j.spi.LoggerFactory;
import org.apache.log4j.spi.LoggerRepository;
import org.apache.log4j.spi.RendererSupport;
import org.apache.log4j.spi.ThrowableRenderer;
import org.apache.log4j.spi.ThrowableRendererSupport;
import org.w3c.dom.Document;
import org.w3c.dom.Element;
import org.w3c.dom.NamedNodeMap;
import org.w3c.dom.Node;
import org.w3c.dom.NodeList;
import org.xml.sax.InputSource;
import org.xml.sax.SAXException;

import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.parsers.FactoryConfigurationError;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.io.InterruptedIOException;
import java.io.Reader;
import java.lang.reflect.Method;
import java.lang.reflect.InvocationTargetException;
import java.net.URL;
import java.net.URLConnection;
import java.util.Hashtable;
import java.util.Properties;

// Contributors:   Mark Womack
//                 Arun Katkere 

/**
 * Use this class to initialize the log4j environment using a DOM tree.
 * 
 * <p>
 * The DTD is specified in <a href="doc-files/log4j.dtd"><b>log4j.dtd</b></a>.
 * 
 * <p>
 * Sometimes it is useful to see how log4j is reading configuration files. You
 * can enable log4j internal logging by defining the <b>log4j.debug</b> variable
 * on the java command line. Alternatively, set the <code>debug</code> attribute
 * in the <code>log4j:configuration</code> element. As in
 * 
 * <pre>
   &lt;log4j:configuration <b>debug="true"</b> xmlns:log4j="http://jakarta.apache.org/log4j/"&gt;
   ...
   &lt;/log4j:configuration&gt;
 * </pre>
 * 
 * <p>
 * There are sample XML files included in the package.
 * 
 * @author Christopher Taylor
 * @author Ceki G&uuml;lc&uuml;
 * @author Anders Kristensen
 * 
 * @since 0.8.3
 */
public class DOMConfigurator implements Configurator {

    static final String CONFIGURATION_TAG = "log4j:configuration";
    static final String OLD_CONFIGURATION_TAG = "configuration";
    static final String RENDERER_TAG = "renderer";
    private static final String THROWABLE_RENDERER_TAG = "throwableRenderer";
    static final String APPENDER_TAG = "appender";
    static final String APPENDER_REF_TAG = "appender-ref";
    static final String PARAM_TAG = "param";
    static final String LAYOUT_TAG = "layout";
    static final String CATEGORY = "category";
    static final String LOGGER = "logger";
    static final String LOGGER_REF = "logger-ref";
    static final String CATEGORY_FACTORY_TAG = "categoryFactory";
    static final String LOGGER_FACTORY_TAG = "loggerFactory";
    static final String NAME_ATTR = "name";
    static final String CLASS_ATTR = "class";
    static final String VALUE_ATTR = "value";
    static final String ROOT_TAG = "root";
    static final String ROOT_REF = "root-ref";
    static final String LEVEL_TAG = "level";
    static final String PRIORITY_TAG = "priority";
    static final String FILTER_TAG = "filter";
    static final String ERROR_HANDLER_TAG = "errorHandler";
    static final String REF_ATTR = "ref";
    static final String ADDITIVITY_ATTR = "additivity";
    static final String THRESHOLD_ATTR = "threshold";
    static final String CONFIG_DEBUG_ATTR = "configDebug";
    static final String INTERNAL_DEBUG_ATTR = "debug";
    private static final String RESET_ATTR = "reset";
    static final String RENDERING_CLASS_ATTR = "renderingClass";
    static final String RENDERED_CLASS_ATTR = "renderedClass";

    static final String EMPTY_STR = "";
    static final Class[] ONE_STRING_PARAM = new Class[] { String.class };

    final static String dbfKey = "javax.xml.parsers.DocumentBuilderFactory";

    // key: appenderName, value: appender
    Hashtable appenderBag;

    Properties props;
    LoggerRepository repository;

    protected LoggerFactory catFactory = null;

    /**
     * No argument constructor.
     */
    public DOMConfigurator() {
	appenderBag = new Hashtable();
    }

    /**
     * Used internally to parse appenders by IDREF name.
     */
    protected Appender findAppenderByName(Document doc, String appenderName) {
	Appender appender = (Appender) appenderBag.get(appenderName);

	if (appender != null) {
	    return appender;
	} else {
	    // Doesn't work on DOM Level 1 :
	    // Element element = doc.getElementById(appenderName);

	    // Endre's hack:
	    Element element = null;
	    NodeList list = doc.getElementsByTagName("appender");
	    for (int t = 0; t < list.getLength(); t++) {
		Node node = list.item(t);
		NamedNodeMap map = node.getAttributes();
		Node attrNode = map.getNamedItem("name");
		if (appenderName.equals(attrNode.getNodeValue())) {
		    element = (Element) node;
		    break;
		}
	    }
	    // Hack finished.

	    if (element == null) {
		LogLog.error("No appender named [" + appenderName + "] could be found.");
		return null;
	    } else {
		appender = parseAppender(element);
		if (appender != null) {
		    appenderBag.put(appenderName, appender);
		}
		return appender;
	    }
	}
    }

    /**
     * Used internally to parse appenders by IDREF element.
     */
    protected Appender findAppenderByReference(Element appenderRef) {
	String appenderName = subst(appenderRef.getAttribute(REF_ATTR));
	Document doc = appenderRef.getOwnerDocument();
	return findAppenderByName(doc, appenderName);
    }

    /**
     * Delegates unrecognized content to created instance if it supports
     * UnrecognizedElementParser.
     * 
     * @since 1.2.15
     * @param instance instance, may be null.
     * @param element  element, may not be null.
     * @param props    properties
     * @throws IOException thrown if configuration of owner object should be
     *                     abandoned.
     */
    private static void parseUnrecognizedElement(final Object instance, final Element element, final Properties props)
	    throws Exception {
	boolean recognized = false;
	if (instance instanceof UnrecognizedElementHandler) {
	    recognized = ((UnrecognizedElementHandler) instance).parseUnrecognizedElement(element, props);
	}
	if (!recognized) {
	    LogLog.warn("Unrecognized element " + element.getNodeName());
	}
    }

    /**
     * Delegates unrecognized content to created instance if it supports
     * UnrecognizedElementParser and catches and logs any exception.
     * 
     * @since 1.2.15
     * @param instance instance, may be null.
     * @param element  element, may not be null.
     * @param props    properties
     */
    private static void quietParseUnrecognizedElement(final Object instance, final Element element,
	    final Properties props) {
	try {
	    parseUnrecognizedElement(instance, element, props);
	} catch (Exception ex) {
	    if (ex instanceof InterruptedException || ex instanceof InterruptedIOException) {
		Thread.currentThread().interrupt();
	    }
	    LogLog.error("Error in extension content: ", ex);
	}
    }

    /**
     * Used internally to parse an appender element.
     */
    protected Appender parseAppender(Element appenderElement) {
	String className = subst(appenderElement.getAttribute(CLASS_ATTR));
	LogLog.debug("Class name: [" + className + ']');
	try {
	    Object instance = Loader.loadClass(className).newInstance();
	    Appender appender = (Appender) instance;
	    PropertySetter propSetter = new PropertySetter(appender);

	    appender.setName(subst(appenderElement.getAttribute(NAME_ATTR)));

	    NodeList children = appenderElement.getChildNodes();
	    final int length = children.getLength();

	    for (int loop = 0; loop < length; loop++) {
		Node currentNode = children.item(loop);

		/* We're only interested in Elements */
		if (currentNode.getNodeType() == Node.ELEMENT_NODE) {
		    Element currentElement = (Element) currentNode;

		    // Parse appender parameters
		    if (currentElement.getTagName().equals(PARAM_TAG)) {
			setParameter(currentElement, propSetter);
		    }
		    // Set appender layout
		    else if (currentElement.getTagName().equals(LAYOUT_TAG)) {
			appender.setLayout(parseLayout(currentElement));
		    }
		    // Add filters
		    else if (currentElement.getTagName().equals(FILTER_TAG)) {
			parseFilters(currentElement, appender);
		    } else if (currentElement.getTagName().equals(ERROR_HANDLER_TAG)) {
			parseErrorHandler(currentElement, appender);
		    } else if (currentElement.getTagName().equals(APPENDER_REF_TAG)) {
			String refName = subst(currentElement.getAttribute(REF_ATTR));
			if (appender instanceof AppenderAttachable) {
			    AppenderAttachable aa = (AppenderAttachable) appender;
			    LogLog.debug("Attaching appender named [" + refName + "] to appender named ["
				    + appender.getName() + "].");
			    aa.addAppender(findAppenderByReference(currentElement));
			} else {
			    LogLog.error("Requesting attachment of appender named [" + refName + "] to appender named ["
				    + appender.getName()
				    + "] which does not implement org.apache.log4j.spi.AppenderAttachable.");
			}
		    } else {
			parseUnrecognizedElement(instance, currentElement, props);
		    }
		}
	    }
	    propSetter.activate();
	    return appender;
	}
	/*
	 * Yes, it's ugly. But all of these exceptions point to the same problem: we
	 * can't create an Appender
	 */
	catch (Exception oops) {
	    if (oops instanceof InterruptedException || oops instanceof InterruptedIOException) {
		Thread.currentThread().interrupt();
	    }
	    LogLog.error("Could not create an Appender. Reported error follows.", oops);
	    return null;
	}
    }

    /**
     * Used internally to parse an {@link ErrorHandler} element.
     */
    protected void parseErrorHandler(Element element, Appender appender) {
	ErrorHandler eh = (ErrorHandler) OptionConverter.instantiateByClassName(subst(element.getAttribute(CLASS_ATTR)),
		org.apache.log4j.spi.ErrorHandler.class, null);

	if (eh != null) {
	    eh.setAppender(appender);

	    PropertySetter propSetter = new PropertySetter(eh);
	    NodeList children = element.getChildNodes();
	    final int length = children.getLength();

	    for (int loop = 0; loop < length; loop++) {
		Node currentNode = children.item(loop);
		if (currentNode.getNodeType() == Node.ELEMENT_NODE) {
		    Element currentElement = (Element) currentNode;
		    String tagName = currentElement.getTagName();
		    if (tagName.equals(PARAM_TAG)) {
			setParameter(currentElement, propSetter);
		    } else if (tagName.equals(APPENDER_REF_TAG)) {
			eh.setBackupAppender(findAppenderByReference(currentElement));
		    } else if (tagName.equals(LOGGER_REF)) {
			String loggerName = currentElement.getAttribute(REF_ATTR);
			Logger logger = (catFactory == null) ? repository.getLogger(loggerName)
				: repository.getLogger(loggerName, catFactory);
			eh.setLogger(logger);
		    } else if (tagName.equals(ROOT_REF)) {
			Logger root = repository.getRootLogger();
			eh.setLogger(root);
		    } else {
			quietParseUnrecognizedElement(eh, currentElement, props);
		    }
		}
	    }
	    propSetter.activate();
	    appender.setErrorHandler(eh);
	}
    }

    /**
     * Used internally to parse a filter element.
     */
    protected void parseFilters(Element element, Appender appender) {
	String clazz = subst(element.getAttribute(CLASS_ATTR));
	Filter filter = (Filter) OptionConverter.instantiateByClassName(clazz, Filter.class, null);

	if (filter != null) {
	    PropertySetter propSetter = new PropertySetter(filter);
	    NodeList children = element.getChildNodes();
	    final int length = children.getLength();

	    for (int loop = 0; loop < length; loop++) {
		Node currentNode = children.item(loop);
		if (currentNode.getNodeType() == Node.ELEMENT_NODE) {
		    Element currentElement = (Element) currentNode;
		    String tagName = currentElement.getTagName();
		    if (tagName.equals(PARAM_TAG)) {
			setParameter(currentElement, propSetter);
		    } else {
			quietParseUnrecognizedElement(filter, currentElement, props);
		    }
		}
	    }
	    propSetter.activate();
	    LogLog.debug("Adding filter of type [" + filter.getClass() + "] to appender named [" + appender.getName()
		    + "].");
	    appender.addFilter(filter);
	}
    }

    /**
     * Used internally to parse an category element.
     */
    protected void parseCategory(Element loggerElement) {
	// Create a new org.apache.log4j.Category object from the <category> element.
	String catName = subst(loggerElement.getAttribute(NAME_ATTR));

	Logger cat;

	String className = subst(loggerElement.getAttribute(CLASS_ATTR));

	if (EMPTY_STR.equals(className)) {
	    LogLog.debug("Retreiving an instance of org.apache.log4j.Logger.");
	    cat = (catFactory == null) ? repository.getLogger(catName) : repository.getLogger(catName, catFactory);
	} else {
	    LogLog.debug("Desired logger sub-class: [" + className + ']');
	    try {
		Class clazz = Loader.loadClass(className);
		Method getInstanceMethod = clazz.getMethod("getLogger", ONE_STRING_PARAM);
		cat = (Logger) getInstanceMethod.invoke(null, new Object[] { catName });
	    } catch (InvocationTargetException oops) {
		if (oops.getTargetException() instanceof InterruptedException
			|| oops.getTargetException() instanceof InterruptedIOException) {
		    Thread.currentThread().interrupt();
		}
		LogLog.error("Could not retrieve category [" + catName + "]. Reported error follows.", oops);
		return;
	    } catch (Exception oops) {
		LogLog.error("Could not retrieve category [" + catName + "]. Reported error follows.", oops);
		return;
	    }
	}

	// Setting up a category needs to be an atomic operation, in order
	// to protect potential log operations while category
	// configuration is in progress.
	synchronized (cat) {
	    boolean additivity = OptionConverter.toBoolean(subst(loggerElement.getAttribute(ADDITIVITY_ATTR)), true);

	    LogLog.debug("Setting [" + cat.getName() + "] additivity to [" + additivity + "].");
	    cat.setAdditivity(additivity);
	    parseChildrenOfLoggerElement(loggerElement, cat, false);
	}
    }

    /**
     * Used internally to parse the category factory element.
     */
    protected void parseCategoryFactory(Element factoryElement) {
	String className = subst(factoryElement.getAttribute(CLASS_ATTR));

	if (EMPTY_STR.equals(className)) {
	    LogLog.error("Category Factory tag " + CLASS_ATTR + " attribute not found.");
	    LogLog.debug("No Category Factory configured.");
	} else {
	    LogLog.debug("Desired category factory: [" + className + ']');
	    Object factory = OptionConverter.instantiateByClassName(className, LoggerFactory.class, null);
	    if (factory instanceof LoggerFactory) {
		catFactory = (LoggerFactory) factory;
	    } else {
		LogLog.error(
			"Category Factory class " + className + " does not implement org.apache.log4j.LoggerFactory");
	    }
	    PropertySetter propSetter = new PropertySetter(factory);

	    Element currentElement = null;
	    Node currentNode = null;
	    NodeList children = factoryElement.getChildNodes();
	    final int length = children.getLength();

	    for (int loop = 0; loop < length; loop++) {
		currentNode = children.item(loop);
		if (currentNode.getNodeType() == Node.ELEMENT_NODE) {
		    currentElement = (Element) currentNode;
		    if (currentElement.getTagName().equals(PARAM_TAG)) {
			setParameter(currentElement, propSetter);
		    } else {
			quietParseUnrecognizedElement(factory, currentElement, props);
		    }
		}
	    }
	}
    }

    /**
     * Used internally to parse the roor category element.
     */
    protected void parseRoot(Element rootElement) {
	Logger root = repository.getRootLogger();
	// category configuration needs to be atomic
	synchronized (root) {
	    parseChildrenOfLoggerElement(rootElement, root, true);
	}
    }

    /**
     * Used internally to parse the children of a category element.
     */
    protected void parseChildrenOfLoggerElement(Element catElement, Logger cat, boolean isRoot) {

	PropertySetter propSetter = new PropertySetter(cat);

	// Remove all existing appenders from cat. They will be
	// reconstructed if need be.
	cat.removeAllAppenders();

	NodeList children = catElement.getChildNodes();
	final int length = children.getLength();

	for (int loop = 0; loop < length; loop++) {
	    Node currentNode = children.item(loop);

	    if (currentNode.getNodeType() == Node.ELEMENT_NODE) {
		Element currentElement = (Element) currentNode;
		String tagName = currentElement.getTagName();

		if (tagName.equals(APPENDER_REF_TAG)) {
		    Element appenderRef = (Element) currentNode;
		    Appender appender = findAppenderByReference(appenderRef);
		    String refName = subst(appenderRef.getAttribute(REF_ATTR));
		    if (appender != null)
			LogLog.debug("Adding appender named [" + refName + "] to category [" + cat.getName() + "].");
		    else
			LogLog.debug("Appender named [" + refName + "] not found.");

		    cat.addAppender(appender);

		} else if (tagName.equals(LEVEL_TAG)) {
		    parseLevel(currentElement, cat, isRoot);
		} else if (tagName.equals(PRIORITY_TAG)) {
		    parseLevel(currentElement, cat, isRoot);
		} else if (tagName.equals(PARAM_TAG)) {
		    setParameter(currentElement, propSetter);
		} else {
		    quietParseUnrecognizedElement(cat, currentElement, props);
		}
	    }
	}
	propSetter.activate();
    }

    /**
     * Used internally to parse a layout element.
     */
    protected Layout parseLayout(Element layout_element) {
	String className = subst(layout_element.getAttribute(CLASS_ATTR));
	LogLog.debug("Parsing layout of class: \"" + className + "\"");
	try {
	    Object instance = Loader.loadClass(className).newInstance();
	    Layout layout = (Layout) instance;
	    PropertySetter propSetter = new PropertySetter(layout);

	    NodeList params = layout_element.getChildNodes();
	    final int length = params.getLength();

	    for (int loop = 0; loop < length; loop++) {
		Node currentNode = (Node) params.item(loop);
		if (currentNode.getNodeType() == Node.ELEMENT_NODE) {
		    Element currentElement = (Element) currentNode;
		    String tagName = currentElement.getTagName();
		    if (tagName.equals(PARAM_TAG)) {
			setParameter(currentElement, propSetter);
		    } else {
			parseUnrecognizedElement(instance, currentElement, props);
		    }
		}
	    }

	    propSetter.activate();
	    return layout;
	} catch (Exception oops) {
	    if (oops instanceof InterruptedException || oops instanceof InterruptedIOException) {
		Thread.currentThread().interrupt();
	    }
	    LogLog.error("Could not create the Layout. Reported error follows.", oops);
	    return null;
	}
    }

    protected void parseRenderer(Element element) {
	String renderingClass = subst(element.getAttribute(RENDERING_CLASS_ATTR));
	String renderedClass = subst(element.getAttribute(RENDERED_CLASS_ATTR));
	if (repository instanceof RendererSupport) {
	    RendererMap.addRenderer((RendererSupport) repository, renderedClass, renderingClass);
	}
    }

    /**
     * Parses throwable renderer.
     * 
     * @param element throwableRenderer element.
     * @return configured throwable renderer.
     * @since 1.2.16.
     */
    protected ThrowableRenderer parseThrowableRenderer(final Element element) {
	String className = subst(element.getAttribute(CLASS_ATTR));
	LogLog.debug("Parsing throwableRenderer of class: \"" + className + "\"");
	try {
	    Object instance = Loader.loadClass(className).newInstance();
	    ThrowableRenderer tr = (ThrowableRenderer) instance;
	    PropertySetter propSetter = new PropertySetter(tr);

	    NodeList params = element.getChildNodes();
	    final int length = params.getLength();

	    for (int loop = 0; loop < length; loop++) {
		Node currentNode = (Node) params.item(loop);
		if (currentNode.getNodeType() == Node.ELEMENT_NODE) {
		    Element currentElement = (Element) currentNode;
		    String tagName = currentElement.getTagName();
		    if (tagName.equals(PARAM_TAG)) {
			setParameter(currentElement, propSetter);
		    } else {
			parseUnrecognizedElement(instance, currentElement, props);
		    }
		}
	    }

	    propSetter.activate();
	    return tr;
	} catch (Exception oops) {
	    if (oops instanceof InterruptedException || oops instanceof InterruptedIOException) {
		Thread.currentThread().interrupt();
	    }
	    LogLog.error("Could not create the ThrowableRenderer. Reported error follows.", oops);
	    return null;
	}
    }

    /**
     * Used internally to parse a level element.
     */
    protected void parseLevel(Element element, Logger logger, boolean isRoot) {
	String catName = logger.getName();
	if (isRoot) {
	    catName = "root";
	}

	String priStr = subst(element.getAttribute(VALUE_ATTR));
	LogLog.debug("Level value for " + catName + " is  [" + priStr + "].");

	if (INHERITED.equalsIgnoreCase(priStr) || NULL.equalsIgnoreCase(priStr)) {
	    if (isRoot) {
		LogLog.error("Root level cannot be inherited. Ignoring directive.");
	    } else {
		logger.setLevel(null);
	    }
	} else {
	    String className = subst(element.getAttribute(CLASS_ATTR));
	    if (EMPTY_STR.equals(className)) {
		logger.setLevel(OptionConverter.toLevel(priStr, Level.DEBUG));
	    } else {
		LogLog.debug("Desired Level sub-class: [" + className + ']');
		try {
		    Class clazz = Loader.loadClass(className);
		    Method toLevelMethod = clazz.getMethod("toLevel", ONE_STRING_PARAM);
		    Level pri = (Level) toLevelMethod.invoke(null, new Object[] { priStr });
		    logger.setLevel(pri);
		} catch (Exception oops) {
		    if (oops instanceof InterruptedException || oops instanceof InterruptedIOException) {
			Thread.currentThread().interrupt();
		    }
		    LogLog.error("Could not create level [" + priStr + "]. Reported error follows.", oops);
		    return;
		}
	    }
	}
	LogLog.debug(catName + " level set to " + logger.getLevel());
    }

    protected void setParameter(Element elem, PropertySetter propSetter) {
	String name = subst(elem.getAttribute(NAME_ATTR));
	String value = (elem.getAttribute(VALUE_ATTR));
	value = subst(OptionConverter.convertSpecialChars(value));
	propSetter.setProperty(name, value);
    }

    /**
     * Configure log4j using a <code>configuration</code> element as defined in the
     * log4j.dtd.
     * 
     */
    static public void configure(Element element) {
	DOMConfigurator configurator = new DOMConfigurator();
	configurator.doConfigure(element, LogManager.getLoggerRepository());
    }

    /**
     * Like {@link #configureAndWatch(String, long)} except that the default delay
     * as defined by {@link FileWatchdog#DEFAULT_DELAY} is used.
     * 
     * @param configFilename A log4j configuration file in XML 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 log4j configuration file in XML format.
     * @param delay          The delay in milliseconds to wait between each check.
     */
    static public void configureAndWatch(String configFilename, long delay) {
	XMLWatchdog xdog = new XMLWatchdog(configFilename);
	xdog.setDelay(delay);
	xdog.start();
    }

    private interface ParseAction {
	Document parse(final DocumentBuilder parser) throws SAXException, IOException;
    }

    public void doConfigure(final String filename, LoggerRepository repository) {
	ParseAction action = new ParseAction() {
	    public Document parse(final DocumentBuilder parser) throws SAXException, IOException {
		return parser.parse(new File(filename));
	    }

	    public String toString() {
		return "file [" + filename + "]";
	    }
	};
	doConfigure(action, repository);
    }

    public void doConfigure(final URL url, LoggerRepository repository) {
	ParseAction action = new ParseAction() {
	    public Document parse(final DocumentBuilder parser) throws SAXException, IOException {
		URLConnection uConn = url.openConnection();
		uConn.setUseCaches(false);
		InputStream stream = uConn.getInputStream();
		try {
		    InputSource src = new InputSource(stream);
		    src.setSystemId(url.toString());
		    return parser.parse(src);
		} finally {
		    stream.close();
		}
	    }

	    public String toString() {
		return "url [" + url.toString() + "]";
	    }
	};
	doConfigure(action, repository);
    }

    /**
     * Configure log4j by reading in a log4j.dtd compliant XML configuration file.
     * 
     */
    public void doConfigure(final InputStream inputStream, LoggerRepository repository)
	    throws FactoryConfigurationError {
	ParseAction action = new ParseAction() {
	    public Document parse(final DocumentBuilder parser) throws SAXException, IOException {
		InputSource inputSource = new InputSource(inputStream);
		inputSource.setSystemId("dummy://log4j.dtd");
		return parser.parse(inputSource);
	    }

	    public String toString() {
		return "input stream [" + inputStream.toString() + "]";
	    }
	};
	doConfigure(action, repository);
    }

    /**
     * Configure log4j by reading in a log4j.dtd compliant XML configuration file.
     * 
     */
    public void doConfigure(final Reader reader, LoggerRepository repository) throws FactoryConfigurationError {
	ParseAction action = new ParseAction() {
	    public Document parse(final DocumentBuilder parser) throws SAXException, IOException {
		InputSource inputSource = new InputSource(reader);
		inputSource.setSystemId("dummy://log4j.dtd");
		return parser.parse(inputSource);
	    }

	    public String toString() {
		return "reader [" + reader.toString() + "]";
	    }
	};
	doConfigure(action, repository);
    }

    /**
     * Configure log4j by reading in a log4j.dtd compliant XML configuration file.
     * 
     */
    protected void doConfigure(final InputSource inputSource, LoggerRepository repository)
	    throws FactoryConfigurationError {
	if (inputSource.getSystemId() == null) {
	    inputSource.setSystemId("dummy://log4j.dtd");
	}
	ParseAction action = new ParseAction() {
	    public Document parse(final DocumentBuilder parser) throws SAXException, IOException {
		return parser.parse(inputSource);
	    }

	    public String toString() {
		return "input source [" + inputSource.toString() + "]";
	    }
	};
	doConfigure(action, repository);
    }

    private final void doConfigure(final ParseAction action, final LoggerRepository repository)
	    throws FactoryConfigurationError {
	DocumentBuilderFactory dbf = null;
	this.repository = repository;
	try {
	    LogLog.debug("System property is :" + OptionConverter.getSystemProperty(dbfKey, null));
	    dbf = DocumentBuilderFactory.newInstance();
	    LogLog.debug("Standard DocumentBuilderFactory search succeded.");
	    LogLog.debug("DocumentBuilderFactory is: " + dbf.getClass().getName());
	} catch (FactoryConfigurationError fce) {
	    Exception e = fce.getException();
	    LogLog.debug("Could not instantiate a DocumentBuilderFactory.", e);
	    throw fce;
	}

	try {
	    dbf.setValidating(true);
	    // prevent XXE attacks
	    dbf.setFeature("http://xml.org/sax/features/external-general-entities", false);
	    dbf.setFeature("http://xml.org/sax/features/external-parameter-entities", false);
	            
	    DocumentBuilder docBuilder = dbf.newDocumentBuilder();

	    docBuilder.setErrorHandler(new SAXErrorHandler());
	    docBuilder.setEntityResolver(new Log4jEntityResolver());

	    Document doc = action.parse(docBuilder);
	    parse(doc.getDocumentElement());
	} catch (Exception e) {
	    if (e instanceof InterruptedException || e instanceof InterruptedIOException) {
		Thread.currentThread().interrupt();
	    }
	    LogLog.error("Could not parse " + action.toString() + ".", e);
	} catch (AbstractMethodError e) {
	    LogLog.error("Failed to parse XML file. Missing DocumentBuilderFactory.setFeature() method?", e);
	    throw e;
	}
    }

    /**
     * Configure by taking in an DOM element.
     */
    public void doConfigure(Element element, LoggerRepository repository) {
	this.repository = repository;
	parse(element);
    }

    /**
     * A static version of {@link #doConfigure(String, LoggerRepository)}.
     */
    static public void configure(String filename) throws FactoryConfigurationError {
	new DOMConfigurator().doConfigure(filename, LogManager.getLoggerRepository());
    }

    /**
     * A static version of {@link #doConfigure(URL, LoggerRepository)}.
     */
    static public void configure(URL url) throws FactoryConfigurationError {
	new DOMConfigurator().doConfigure(url, LogManager.getLoggerRepository());
    }

    /**
     * Used internally to configure the log4j framework by parsing a DOM tree of XML
     * elements based on <a href="doc-files/log4j.dtd">log4j.dtd</a>.
     * 
     */
    protected void parse(Element element) {

	String rootElementName = element.getTagName();

	if (!rootElementName.equals(CONFIGURATION_TAG)) {
	    if (rootElementName.equals(OLD_CONFIGURATION_TAG)) {
		LogLog.warn("The <" + OLD_CONFIGURATION_TAG + "> element has been deprecated.");
		LogLog.warn("Use the <" + CONFIGURATION_TAG + "> element instead.");
	    } else {
		LogLog.error("DOM element is - not a <" + CONFIGURATION_TAG + "> element.");
		return;
	    }
	}

	String debugAttrib = subst(element.getAttribute(INTERNAL_DEBUG_ATTR));

	LogLog.debug("debug attribute= \"" + debugAttrib + "\".");
	// if the log4j.dtd is not specified in the XML file, then the
	// "debug" attribute is returned as the empty string.
	if (!debugAttrib.equals("") && !debugAttrib.equals("null")) {
	    LogLog.setInternalDebugging(OptionConverter.toBoolean(debugAttrib, true));
	} else {
	    LogLog.debug("Ignoring " + INTERNAL_DEBUG_ATTR + " attribute.");
	}

	//
	// reset repository before configuration if reset="true"
	// on configuration element.
	//
	String resetAttrib = subst(element.getAttribute(RESET_ATTR));
	LogLog.debug("reset attribute= \"" + resetAttrib + "\".");
	if (!("".equals(resetAttrib))) {
	    if (OptionConverter.toBoolean(resetAttrib, false)) {
		repository.resetConfiguration();
	    }
	}

	String confDebug = subst(element.getAttribute(CONFIG_DEBUG_ATTR));
	if (!confDebug.equals("") && !confDebug.equals("null")) {
	    LogLog.warn("The \"" + CONFIG_DEBUG_ATTR + "\" attribute is deprecated.");
	    LogLog.warn("Use the \"" + INTERNAL_DEBUG_ATTR + "\" attribute instead.");
	    LogLog.setInternalDebugging(OptionConverter.toBoolean(confDebug, true));
	}

	String thresholdStr = subst(element.getAttribute(THRESHOLD_ATTR));
	LogLog.debug("Threshold =\"" + thresholdStr + "\".");
	if (!"".equals(thresholdStr) && !"null".equals(thresholdStr)) {
	    repository.setThreshold(thresholdStr);
	}

	// Hashtable appenderBag = new Hashtable(11);

	/*
	 * Building Appender objects, placing them in a local namespace for future
	 * reference
	 */

	// First configure each category factory under the root element.
	// Category factories need to be configured before any of
	// categories they support.
	//
	String tagName = null;
	Element currentElement = null;
	Node currentNode = null;
	NodeList children = element.getChildNodes();
	final int length = children.getLength();

	for (int loop = 0; loop < length; loop++) {
	    currentNode = children.item(loop);
	    if (currentNode.getNodeType() == Node.ELEMENT_NODE) {
		currentElement = (Element) currentNode;
		tagName = currentElement.getTagName();

		if (tagName.equals(CATEGORY_FACTORY_TAG) || tagName.equals(LOGGER_FACTORY_TAG)) {
		    parseCategoryFactory(currentElement);
		}
	    }
	}

	for (int loop = 0; loop < length; loop++) {
	    currentNode = children.item(loop);
	    if (currentNode.getNodeType() == Node.ELEMENT_NODE) {
		currentElement = (Element) currentNode;
		tagName = currentElement.getTagName();

		if (tagName.equals(CATEGORY) || tagName.equals(LOGGER)) {
		    parseCategory(currentElement);
		} else if (tagName.equals(ROOT_TAG)) {
		    parseRoot(currentElement);
		} else if (tagName.equals(RENDERER_TAG)) {
		    parseRenderer(currentElement);
		} else if (tagName.equals(THROWABLE_RENDERER_TAG)) {
		    if (repository instanceof ThrowableRendererSupport) {
			ThrowableRenderer tr = parseThrowableRenderer(currentElement);
			if (tr != null) {
			    ((ThrowableRendererSupport) repository).setThrowableRenderer(tr);
			}
		    }
		} else if (!(tagName.equals(APPENDER_TAG) || tagName.equals(CATEGORY_FACTORY_TAG)
			|| tagName.equals(LOGGER_FACTORY_TAG))) {
		    quietParseUnrecognizedElement(repository, currentElement, props);
		}
	    }
	}
    }

    protected String subst(final String value) {
	return subst(value, props);
    }

    /**
     * Substitutes property value for any references in expression.
     *
     * @param value value from configuration file, may contain literal text,
     *              property references or both
     * @param props properties.
     * @return evaluated expression, may still contain expressions if unable to
     *         expand.
     * @since 1.2.15
     */
    public static String subst(final String value, final Properties props) {
	try {
	    return OptionConverter.substVars(value, props);
	} catch (IllegalArgumentException e) {
	    LogLog.warn("Could not perform variable substitution.", e);
	    return value;
	}
    }

    /**
     * Sets a parameter based from configuration file content.
     *
     * @param elem       param element, may not be null.
     * @param propSetter property setter, may not be null.
     * @param props      properties
     * @since 1.2.15
     */
    public static void setParameter(final Element elem, final PropertySetter propSetter, final Properties props) {
	String name = subst(elem.getAttribute("name"), props);
	String value = (elem.getAttribute("value"));
	value = subst(OptionConverter.convertSpecialChars(value), props);
	propSetter.setProperty(name, value);
    }

    /**
     * Creates an object and processes any nested param elements but does not call
     * activateOptions. If the class also supports UnrecognizedElementParser, the
     * parseUnrecognizedElement method will be call for any child elements other
     * than param.
     *
     * @param element       element, may not be null.
     * @param props         properties
     * @param expectedClass interface or class expected to be implemented by created
     *                      class
     * @return created class or null.
     * @throws Exception thrown if the contain object should be abandoned.
     * @since 1.2.15
     */
    public static Object parseElement(final Element element, final Properties props, final Class expectedClass)
	    throws Exception {
	String clazz = subst(element.getAttribute("class"), props);
	Object instance = OptionConverter.instantiateByClassName(clazz, expectedClass, null);

	if (instance != null) {
	    PropertySetter propSetter = new PropertySetter(instance);
	    NodeList children = element.getChildNodes();
	    final int length = children.getLength();

	    for (int loop = 0; loop < length; loop++) {
		Node currentNode = children.item(loop);
		if (currentNode.getNodeType() == Node.ELEMENT_NODE) {
		    Element currentElement = (Element) currentNode;
		    String tagName = currentElement.getTagName();
		    if (tagName.equals("param")) {
			setParameter(currentElement, propSetter, props);
		    } else {
			parseUnrecognizedElement(instance, currentElement, props);
		    }
		}
	    }
	    return instance;
	}
	return null;
    }

}

class XMLWatchdog extends FileWatchdog {

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

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