package com.jamonapi.log4j;

import org.apache.log4j.*;
import org.apache.log4j.spi.*;
import com.jamonapi.*;
import com.jamonapi.utils.Generalizer;
import com.jamonapi.utils.DefaultGeneralizer;

/**
 * <p>
 * Title: JAMonAppender
 * </p>
 * <p>
 * Description: log4j Appender to that allows you to summarize log4j stats via jamon and view 
 * the tail of the log in realtime in a jamon web page.  Click here for more info on how to use the <a href="http://jamonapi.sourceforge.net/log4j_jamonappender.html">JAMonAppender</a>.
 * </p>
 * 
 * <p>
 * Copyright: Copyright (c) 2007
 * </p>
 * 
 * @author Ed Desrosiers, Steve Souza
 */

public class JAMonAppender extends AppenderSkeleton {
    /* Prefix for this classes jamon monitor labels */
    private final String PREFIX = "com.jamonapi.log4j.JAMonAppender.";
    // any of these poperties can be overridden via log4j configurators.
    private int bufferSize = 100;
    private String units = "log4j"; // units in jamon montiors
    // indicates whether or not log4j LoggingEvent info is placed in buffer.
    // This could potentially be slower though I didn't test it, and I
    // wouldn't be overly concerned about it.
    private boolean enableListenerDetails = true;

    // Enable monitoring of the various log4j levels in jamon.
    private boolean enableLevelMonitoring = true;
    private boolean generalize = false;
    private Generalizer generalizer = new DefaultGeneralizer();

    static {
        // Register this object to be available for use in the
        // JAMonListenerFactory.
        JAMonListenerFactory.put(new Log4jBufferListener());
    }

    public JAMonAppender() {
    }

    /**
     * If the appender is enabled then start and stop a JAMon entry. Depending
     * on how this object is configured it may also put details into a
     * JAMonBufferLister and generalize the logging message
     * (logger.error(message) etc) and put it in jamon too. By default it will
     * only do jamon records for each of the log4j Levels.
     * 
     * @param event
     */

    protected void append(LoggingEvent event) {

        String message = (getLayout() == null) ? event.getRenderedMessage() : getLayout().format(
                event);
        if (getEnableLevelMonitoring()) {
            // monitor that counts all calls to log4j logging methods
            MonitorFactory.add(createKey(PREFIX + "TOTAL", message, event), 1);
            // monitor that counts calls to log4j at each level (DEBUG/WARN/...)
            MonitorFactory.add(createKey(PREFIX + event.getLevel(), message, event), 1);
        }

        // if the object was configured to generalize the message then do as
        // such. This will create a jamon record with the generalized method
        // so it is important for the developer to ensure that the generalized
        // message is unique enough not to grow jamon unbounded.
        if (getGeneralize()) {
            MonitorFactory.add(createKey(generalize(message), message, event), 1);
        }

    }

    // Return a key that will put LoggingEvent info in a bufferlistenr if
    // enableListenerDetails has been enabled,
    // else simply use the standard jamon MonKeyImp
    private MonKey createKey(String summaryLabel, String detailLabel,
            LoggingEvent event) {
        if (enableListenerDetails) // put array in details buffer
            return new Log4jMonKey(summaryLabel, detailLabel, units, event);
        else
            return new MonKeyImp(summaryLabel, detailLabel, units);

    }

    /**
     * Required log4j method. Currently a no-op.
     */
    public void close() {

    }

    /**
     * <p>
     * JAMonAppender doesn't have to have a layount because it is acceptable to
     * default to using the raw message. Not providing a layout will return a
     * log4j error that looks like the following, however it can safely be
     * ignored. Providing any layout for the JAMonAppender will make the error
     * go away. Unfortunately log4j doesn't have a way to specify an optional
     * layout.
     * </p>
     * 
     * <p>
     * log4j:ERROR Could not find value for key
     * log4j.appender.jamonAppender.layout
     * </p>
     */

    public boolean requiresLayout() {
        return true;
    }

    /**
     * @return Returns the units. By default this is 'log4j' though it can be
     *         changed. This is used as part of the jamon key.
     */
    public String getUnits() {
        return units;
    }

    /**
     * @param units
     *            The units to set.
     */
    public void setUnits(String units) {
        this.units = units;
    }

    /**
     * Specifies whether or not LoggingEvent info will be used in the attached
     * Log4jBufferListener. By default this is enabled.
     */
    public boolean getEnableListenerDetails() {
        return enableListenerDetails;
    }

    /**
     * Specify whether or not LoggingEvent info will be used in the attached
     * Log4jBufferListener
     */
    public void setEnableListenerDetails(boolean arrayDetails) {
        this.enableListenerDetails = arrayDetails;
    }

    /**
     * Specifies whether or not there will be a JAMon record for each log4j
     * Level (DEBUG/WARN/...), and another one that corresponds to all calls to
     * log4j logging methods. It is identified by the label TOTAL. By default
     * this is enabled.
     */
    public void setEnableLevelMonitoring(boolean enableLevelMonitoring) {
        this.enableLevelMonitoring = enableLevelMonitoring;

    }

    /** Returns whether or not LevelMonitoring is enabled or not. */
    public boolean getEnableLevelMonitoring() {
        return enableLevelMonitoring;
    }

    /**
     * Note this is primarily used by the log4j configurator. Valid values are
     * the various log4j levels: DEBUG/ERROR/WARN/INFO/ERROR/FATAL, as well as
     * TOTAL (A listener that gets called for all levels), BASIC (same as
     * calling TOTAL/ERROR/FATAL), and ALL (same as calling
     * ERROR/WARN/INFO/ERROR/FATAL/TOTAL). Values are not case sensitive. .
     * 
     * @param enableListeners
     */
    public void setEnableListeners(String level) {

        if (Level.DEBUG.toString().equalsIgnoreCase(level.toUpperCase()))
            addDefaultListener(MonitorFactory.getMonitor(PREFIX + Level.DEBUG, units));
        else if (Level.INFO.toString().equalsIgnoreCase(level.toUpperCase()))
            addDefaultListener(MonitorFactory.getMonitor(PREFIX + Level.INFO, units));
        else if (Level.WARN.toString().equalsIgnoreCase(level.toUpperCase()))
            addDefaultListener(MonitorFactory.getMonitor(PREFIX + Level.WARN, units));
        else if (Level.ERROR.toString().equalsIgnoreCase(level.toUpperCase()))
            addDefaultListener(MonitorFactory.getMonitor(PREFIX + Level.ERROR, units));
        else if (Level.FATAL.toString().equalsIgnoreCase(level.toUpperCase()))
            addDefaultListener(MonitorFactory.getMonitor(PREFIX + Level.FATAL, units));
        else if ("TOTAL".toString().equalsIgnoreCase(level.toUpperCase()))
            addDefaultListener(MonitorFactory.getMonitor(PREFIX + "TOTAL", units));
        else if (Level.ALL.toString().equalsIgnoreCase(level.toUpperCase())) {
            addDefaultListener(MonitorFactory.getMonitor(PREFIX + Level.DEBUG, units));
            addDefaultListener(MonitorFactory.getMonitor(PREFIX + Level.INFO, units));
            addDefaultListener(MonitorFactory.getMonitor(PREFIX + Level.WARN, units));
            addDefaultListener(MonitorFactory.getMonitor(PREFIX + Level.ERROR, units));
            addDefaultListener(MonitorFactory.getMonitor(PREFIX + Level.FATAL, units));
            addDefaultListener(MonitorFactory.getMonitor(PREFIX + "TOTAL", units));
        } else if ("BASIC".toString().equalsIgnoreCase(level.toUpperCase())) {
            addDefaultListener(MonitorFactory.getMonitor(PREFIX + "TOTAL", units));
            addDefaultListener(MonitorFactory.getMonitor(PREFIX + Level.ERROR, units));
            addDefaultListener(MonitorFactory.getMonitor(PREFIX + Level.FATAL, units));
        }
    }

    // Add a Log4jBufferListener to the passed in Monitor
    private void addDefaultListener(Monitor mon) {
        if (!mon.hasListeners()) {
            Log4jBufferListener listener = new Log4jBufferListener();
            listener.getBufferList().setBufferSize(bufferSize);
            mon.getListenerType("value").addListener(listener);
        }
    }

    /**
     * For defaultBufferSize to take hold it must be called before the first
     * call to setDefaultListeners. By default the buffer size is 100.
     * 
     * @param bufferSize
     */
    public void setListenerBufferSize(int bufferSize) {
        this.bufferSize = bufferSize;
    }

    /** Indicate whether or not a jamon record should be created from the passed in message.
     * Note you can use the DefaultGeneralizer, your own Generalizer.  It is very important that 
     * you ensure the String returned by the generalizer is unique enough that JAMon doesn't grow unbounded.
     * For example by choosing to use no Generalizer you must pass in a relatively unique log4j string.
     * @param generalize
     */
    public void setGeneralize(boolean generalize) {
        this.generalize = generalize;
    }

    /** Return whether or not generalization will occur */
    public boolean getGeneralize() {
        return generalize;
    }

    /** generalize the passed in String if a Genaralizer is set */
    private String generalize(String detailedMessage) {
        return (generalizer != null) ? generalizer.generalize(detailedMessage)
                : detailedMessage;
    }

    /** Enable the use of the DefaultGeneralizer. As a side effect setGeneralize(true) is called
     * telling this class to generalize.
     * @param enableDefaultGeneralizer
     */
    public void setEnableDefaultGeneralizer(boolean enableDefaultGeneralizer) {
        if (enableDefaultGeneralizer) {
            this.generalizer = new DefaultGeneralizer();
            setGeneralize(true);
        } else
            this.generalizer = null;
    }

    /** Indicates whether or not a Generalizer has been set */
    public boolean hasGeneralizer() {
        return (generalizer != null);
    }

    /**
     * Default generalizer based on com.jamonapi.utils.SQLDeArger. It
     * generalizes by replacing numbers and strings in single or double quotes
     * with '?'. i.e. select * from table where name = 'steve' and id=50 becomes
     * select * from table where name = ? and id=?. Developers can provide their
     * own Generalizer if this is not the desired behaviour. Although the
     * example uses a query the code works equally well with any String. The
     * generalizer is used to create a record appropriate for jamon from a
     * detail String that goes to log4j.
     */
    public void setGeneralizerClass(Generalizer generalizer) {
        this.generalizer = generalizer;
    }

    /** Pass in a string class name and this generalizer will be constructed an used.  For example com.jamonapi.utils.DefaultGeneralizer could be passed in
     * 
     * @param generalizerClassStr
     * @throws InstantiationException
     * @throws IllegalAccessException
     * @throws ClassNotFoundException
     */
    public void setGeneralizerDynamic(String generalizerClassStr) throws InstantiationException,
            IllegalAccessException, ClassNotFoundException {
        this.generalizer = (Generalizer) Class.forName(generalizerClassStr).newInstance();
    }

}
