Monday, July 16, 2012

ReverseEngineer for Log4J

I love Log4J. Long before the Java logging API, Log4J version 1.2 had better features and gave me everything I wanted. Well, almost everything. There were a few things missing that I desired to have. Thankfully, Log4J is open source and I was free to add to it. Perhaps version Log4J 2.0 will be able to satisfy me, but until then I can work with my own modifications.

One shortcoming was that I could not look at the current configuration of Log4J for a running process. I could look at the configuration files -- if I could figure out which configuration file was actually being used -- and if the configuration had not been changed in memory.

My solution was to write a class that would examine the Log4J configuration in memory and derive the appropriate configuration text or object for either PropertyConfigurator or DOMConfigurator that could be used to recreate the current Log4J configuration. This code would have to be executed in the running JVM, so the running program would have to provide access in some fashion. I have found this to be most useful as a Java Servlet, but more on that later.

Here is the source code for ReverseEngineer. Below is also a JUnit 3.8 test case that tests it.



package log4j.util;

import java.io.*;
import java.lang.reflect.*;
import java.util.*;
import java.util.regex.*;
import javax.xml.parsers.*;
import javax.xml.transform.*;
import javax.xml.transform.dom.DOMSource;
import javax.xml.transform.stream.StreamResult;
import javax.xml.transform.stream.StreamSource;
import org.w3c.dom.*;
import org.apache.log4j.*;
import org.apache.log4j.spi.*;

/**
   No support for ObjectRenderers yet.  Should be really easy to add if wanted.
 */

public class ReverseEngineer
{
    public static final Logger LOGGER = Logger.getLogger(ReverseEngineer.class);

    public static void writeDOMConfiguration (LoggerRepository repository, Writer target) throws IOException
    {
        writeDOMConfiguration(repository, target, false);
    }

    public static void writeDOMConfiguration (LoggerRepository repository, Writer target, boolean debug) throws IOException
    {
        String stylesheet = "<xsl:stylesheet xmlns:xsl=\"http://www.w3.org/1999/XSL/Transform\" version=\"1.0\">\n" +
            "\n" +
            "<xsl:output method=\"xml\" doctype-system=\"log4j.dtd\" indent=\"yes\" />\n" +
            "\n" +
            "<xsl:template match=\"@*|node()\">\n" +
            "  <xsl:copy>\n" +
            "    <xsl:apply-templates select=\"@*|node()\"/>\n" +
            "  </xsl:copy>\n" +
            "</xsl:template>\n" +
            "\n" +
            "</xsl:stylesheet>";

        StreamSource xsl = new StreamSource(new StringReader(stylesheet));
        StreamResult out = new StreamResult(target);
        DOMSource in = new DOMSource(getDOMConfiguration(repository, debug));
        try
        {
            TransformerFactory.newInstance().newTransformer(xsl).transform(in, out);
        }
        catch (TransformerException shouldNotHappen)
        {
            IOException ioe = new IOException();
            ioe.initCause(shouldNotHappen);
            throw ioe;
        }
    }

    public static Document getDOMConfiguration (LoggerRepository repository) throws IllegalStateException
    {
        return getDOMConfiguration(repository, false);
    }

    public static Document getDOMConfiguration (LoggerRepository repository, boolean debug) throws IllegalStateException
    {
        IllegalStateException illEx = new IllegalStateException();
        try
        {
            DOMImplementation domImpl = DocumentBuilderFactory.newInstance().newDocumentBuilder().getDOMImplementation();
            Document dom = domImpl.createDocument("http://jakarta.apache.org/log4j/", "log4j:configuration",
                                                  domImpl.createDocumentType("log4j", null, "log4j.dtd"));

            Element docElement = dom.getDocumentElement();
            if (debug)
            {
                docElement.setAttribute("debug", "true");
            }

            for (Enumeration loggers = repository.getCurrentLoggers();
                 loggers.hasMoreElements();
                 )
            {
                Logger nextLogger = (Logger)loggers.nextElement();
                if (nextLogger != repository.getRootLogger())
                {
                    addLogger(dom, docElement, nextLogger);
                }
            }

            addRootLogger(dom, docElement, repository.getRootLogger());
            return dom;
        }
        catch (ParserConfigurationException ex1)
        {
            illEx.initCause(ex1);
            throw illEx;
        }
    }

    private static void addRootLogger (Document dom, Element element, Logger rootLogger)
    {
        Element rootElement = dom.createElement("root");
        element.appendChild(rootElement);
        addLoggerInfo(dom, element, rootElement, rootLogger);
        addParams(dom, rootElement, rootLogger, Logger.class);
    }

    private static void addLogger (Document dom, Element element, Logger logger)
    {
        Element loggerElement = dom.createElement("logger");
        element.appendChild(loggerElement);
        loggerElement.setAttribute("name", logger.getName());
        loggerElement.setAttribute("additivity", String.valueOf(logger.getAdditivity()));
        addLoggerInfo(dom, element, loggerElement, logger);
    }

    private static void addLoggerInfo (Document dom, Element parent, Element element, Logger logger)
    {
        if (logger.getLevel() != null)
        {
            Element levelElement = dom.createElement("level");
            levelElement.setAttribute("value", logger.getLevel().toString());
            element.appendChild(levelElement);
        }

        for (Enumeration appenders = logger.getAllAppenders();
             appenders.hasMoreElements();
             )
        {
            Appender nextAppender = (Appender)appenders.nextElement();
            Element appenderRefElement = dom.createElement("appender-ref");
            element.appendChild(appenderRefElement);
            appenderRefElement.setAttribute("ref", nextAppender.getName());
            addAppender(dom, parent, nextAppender);
        }
    }

    private static void addAppender (Document dom, Element element, Appender appender)
    {
        Element appenderElement = dom.createElement("appender");
        NodeList loggerElements = dom.getElementsByTagName("logger");
        if (loggerElements.getLength() > 0)
        {
            element.insertBefore(appenderElement, loggerElements.item(0));
        }
        else
        {
            element.insertBefore(appenderElement, dom.getElementsByTagName("root").item(0));
        }
        appenderElement.setAttribute("class", appender.getClass().getName());
        appenderElement.setAttribute("name", appender.getName());

        addParams(dom, appenderElement, appender, Appender.class);

        Layout layout = appender.getLayout();
        if (layout != null)
        {
            addLayout(dom, appenderElement, layout);
        }

        ErrorHandler errorHandler = appender.getErrorHandler();
        if (errorHandler != null &&
            errorHandler.getClass() != org.apache.log4j.helpers.OnlyOnceErrorHandler.class)
        {
            addErrorHandler(dom, appenderElement, appender, errorHandler);
        }

        for (Filter filter = appender.getFilter(); filter != null; filter = filter.next)
        {
            addFilter(dom, appenderElement, filter);
        }
    }

    private static void addLayout (Document dom, Element element, Layout layout)
    {
        Element layoutElement = dom.createElement("layout");
        element.appendChild(layoutElement);
        layoutElement.setAttribute("class", layout.getClass().getName());
        addParams(dom, layoutElement, layout, Layout.class);
    }

    private static void addErrorHandler (Document dom, Element appenderElement, Appender appender, ErrorHandler errorHandler)
    {
        Element errorHandlerElement = dom.createElement("errorHandler");
        appenderElement.appendChild(errorHandlerElement);
        errorHandlerElement.setAttribute("class", errorHandler.getClass().getName());
        addParams(dom, errorHandlerElement, errorHandler, ErrorHandler.class);
    }

    private static void addFilter (Document dom, Element element, Filter filter)
    {
        Element filterElement = dom.createElement("filter");
        element.appendChild(filterElement);
        filterElement.setAttribute("class", filter.getClass().getName());
        addParams(dom, filterElement, filter, Filter.class);
    }

    private static HashMap getOptionParams (Object parameterized, Class base)
    {
        HashMap optionParams = new HashMap();

        HashMap getters = new HashMap();
        HashMap setters = new HashMap();
        for (Iterator methods = Arrays.asList(parameterized.getClass().getMethods()).iterator();
             methods.hasNext();
             )
        {
            Method nextMethod = (Method)methods.next();
            String name = nextMethod.getName();
            if (name.startsWith("get"))
            {
                getters.put(name, nextMethod);
            }
            else if (name.startsWith("set") && nextMethod.getParameterTypes().length == 1)
            {
                setters.put(name, nextMethod);
            }
        }

        for (Iterator methods = Arrays.asList(base.getMethods()).iterator();
             methods.hasNext();
             )
        {
            Method nextMethod = (Method)methods.next();
            String name = nextMethod.getName();
            if (name.startsWith("get"))
            {
                getters.remove(name);
            }
            else if (name.startsWith("set"))
            {
                setters.remove(name);
            }
        }

        for (Iterator getterIter = getters.entrySet().iterator();
             getterIter.hasNext();
             )
        {
            Map.Entry entry = (Map.Entry)getterIter.next();
            String getterName = (String)entry.getKey();
            String setterName =  "s" + getterName.substring(1);
            Method setter = (Method)setters.get(setterName);
            if (setter != null)
            {
                Class [] paramTypes = setter.getParameterTypes();
                if (paramTypes != null && paramTypes.length == 1)
                {
                    Method getter = (Method)entry.getValue();
                    if (paramTypes[0] == getter.getReturnType())
                    {
                        String paramName = getterName.substring(3);
                        try
                        {
                            Object paramValue = getter.invoke(parameterized, new Object [0]);
                            if (paramValue != null)
                            {
                                optionParams.put(paramName, paramValue);
                            }
                        }
                        catch (IllegalAccessException justSkip) {}
                        catch (InvocationTargetException whoops)
                        {
                            LOGGER.warn("Could not get param value", whoops.getTargetException());
                        }
                    }
                }
            }
        }
        return optionParams;
    }

    private static void addParams (Document dom, Element element, Object parameterized, Class base)
    {
        for (Iterator i = getOptionParams(parameterized, base).entrySet().iterator();
             i.hasNext();
             )
        {
            Map.Entry nextEntry = (Map.Entry)i.next();
            addParam(dom, element, String.valueOf(nextEntry.getKey()), String.valueOf(nextEntry.getValue()));
        }
    }

    private static void addParam (Document dom, Element element, String name, String value)
    {
        Element paramElement = dom.createElement("param");
        element.appendChild(paramElement);
        paramElement.setAttribute("name", name);
        paramElement.setAttribute("value", escapeXml(value));
    }

    public static String escapeXml (String unescaped)
    {
        String escaped = Pattern.compile("&").matcher(unescaped).replaceAll("&amp;");
        escaped = Pattern.compile("<").matcher(escaped).replaceAll("&lt;");
        escaped = Pattern.compile(">").matcher(escaped).replaceAll("&gt;");
        escaped = Pattern.compile("\\\\").matcher(escaped).replaceAll("\\\\\\\\");
        escaped = Pattern.compile("'").matcher(escaped).replaceAll("&#39;");
        escaped = Pattern.compile("\"").matcher(escaped).replaceAll("&quot;");
        return escaped;
    }

    public static void writePropertyConfiguration (LoggerRepository repository, OutputStream target) throws IOException
    {
        getPropertyConfiguration(repository).store(target, null);
    }

    public static Properties getPropertyConfiguration (LoggerRepository repository)
    {
        Properties config = new Properties();

        if (repository.getThreshold() != null)
        {
            config.setProperty("log4j.threshold", String.valueOf(repository.getThreshold()));
        }

        addRootLogger(config, repository.getRootLogger());

        for (Enumeration loggers = repository.getCurrentLoggers();
             loggers.hasMoreElements();
             )
        {
            Logger nextLogger = (Logger)loggers.nextElement();
            if (nextLogger != repository.getRootLogger())
            {
                    addLogger(config, nextLogger);
            }
        }

        return config;
    }

    private static void addRootLogger (Properties config, Logger rootLogger)
    {
        StringBuffer buf = new StringBuffer();

        if (rootLogger.getLevel() != null)
        {
            buf.append(rootLogger.getLevel().toString());
        }
        buf.append(", ");

        addAppenders(config, buf, rootLogger);
        buf.setLength(buf.length() - 2);
        config.put("log4j.rootLogger", buf.toString());
    }

    private static void addLogger (Properties config, Logger logger)
    {
        StringBuffer buf = new StringBuffer();

        if (logger.getLevel() != null)
        {
            buf.append(logger.getLevel().toString());
        }
        buf.append(", ");

        addAppenders(config, buf, logger);
        buf.setLength(buf.length() - 2);
        config.put("log4j.logger." + logger.getName(), buf.toString());
    }

    private static void addAppenders (Properties config, StringBuffer buf, Logger logger)
    {
        for (Enumeration appenders = logger.getAllAppenders();
             appenders.hasMoreElements();
             )
        {
            Appender nextAppender = (Appender)appenders.nextElement();
            buf.append(nextAppender.getName());
            buf.append(", ");

            config.setProperty("log4j.appender." + nextAppender.getName(), nextAppender.getClass().getName());
            addParams(config, "log4j.appender." + nextAppender.getName() + ".", nextAppender, Appender.class);
            if (nextAppender.getLayout() != null)
            {
                addLayout(config, "log4j.appender." + nextAppender.getName() + ".layout", nextAppender.getLayout());
            }
        }
    }

    private static void addParams (Properties config, String prefix, Object obj, Class base)
    {
        for (Iterator i = getOptionParams(obj, base).entrySet().iterator();
             i.hasNext();
             )
        {
            Map.Entry nextEntry = (Map.Entry)i.next();
            config.setProperty(prefix + nextEntry.getKey(), String.valueOf(nextEntry.getValue()));
        }
    }

    private static void addLayout (Properties config, String key, Layout layout)
    {
        config.setProperty(key, layout.getClass().getName());
        addParams(config, key + ".", layout, Layout.class);
    }
}



package unit.logging;

import java.io.*;
import java.util.*;

import org.w3c.dom.*;
import org.apache.log4j.*;
import org.apache.log4j.spi.*;
import org.apache.log4j.xml.*;
import log4j.util.ReverseEngineer;
import junit.framework.*;

public class ReverseEngineeringTest extends TestCase
{
    public static final Logger LOGGER = Logger.getLogger(ReverseEngineeringTest.class);

    public ReverseEngineeringTest (String name)
    {
        super(name);
    }

    public void testGetDOMConfiguration () throws Exception
    {
        Logger logger = Logger.getLogger("test.ReverseEngineer.DOM");
        logger.setLevel(Level.INFO);

        LoggerRepository repository = logger.getLoggerRepository();
        Document domConfig = ReverseEngineer.getDOMConfiguration(repository);

        StringWriter sw = new StringWriter();
        ReverseEngineer.writeDOMConfiguration(logger.getLoggerRepository(), sw);
        String config = sw.toString();

        repository.resetConfiguration();
        DOMConfigurator.configure(domConfig.getDocumentElement());

        sw = new StringWriter();
        ReverseEngineer.writeDOMConfiguration(repository, sw);
        String config2 = sw.toString();

        assertEquals("Reconfiguration produced different results", config, config2);
    }

    public void testEscapeXml () throws Exception
    {
        String unescaped = "<hello & good morning/>";
        assertEquals("Failed to escape ampersand/less-than/greater-than", "&lt;hello &amp; good morning/&gt;", ReverseEngineer.escapeXml(unescaped));

        unescaped = "\"Now,\" he says!  t' the \\backslash";
        String escaped = ReverseEngineer.escapeXml(unescaped);

        assertEquals("Failed to escape quotation marks/apostrophe/backslash", "&quot;Now,&quot; he says!  t&#39; the \\\\backslash", escaped);
    }

    public void testGetPropertyConfiguration () throws Exception
    {
        Logger logger = Logger.getLogger("test.ReverseEngineer.DOM");
        logger.setLevel(Level.INFO);

        LoggerRepository repository = logger.getLoggerRepository();
        Properties propConfig = ReverseEngineer.getPropertyConfiguration(repository);

        ByteArrayOutputStream bout = new ByteArrayOutputStream();
        propConfig.store(bout, null);
        String config = new String(bout.toByteArray());

        repository.resetConfiguration();
        Properties resetConfig = ReverseEngineer.getPropertyConfiguration(repository);
        assertFalse("Repository failed to reset", propConfig.equals(resetConfig));

        PropertyConfigurator.configure(propConfig);

        Properties propConfig2 = ReverseEngineer.getPropertyConfiguration(repository);
        bout = new ByteArrayOutputStream();
        propConfig2.store(bout, null);
        String config2 = new String(bout.toByteArray());

        assertEquals("Reconfiguration produced different results", config, config2);
    }
}

No comments:

Post a Comment