/* 
 * Copyright 1999,2005 The Apache Software Foundation.
 * 
 * Licensed 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;

 
import java.util.Date;
import java.util.Enumeration;
import java.util.HashSet;
import java.util.Iterator;
import java.util.Set;

import org.apache.log4j.helpers.CyclicBuffer;
import org.apache.log4j.spi.LoggingEvent;


/**
 * An appender which caches the last n events in memory and outputs them to defined appenders only in the 
 * case of an event with level {@link #dumpCacheLevel} or above.
 * <P>
 * Configuration:
 * <PRE>
 *    log4j.rootLogger=INFO, CachingApp
 *    log4j.logger.CachingApp=DEBUG, CA
 *
 *    log4j.appender.CachingApp=org.apache.log4j.CachingAppender
 *    log4j.appender.CachingApp.threshold=WARN
 *    log4j.appender.CachingApp.maxCacheSize=60
 *    log4j.appender.CachingApp.dumpCacheLevel=ERROR
 *    log4j.appender.CachingApp.dumpHeader=>>>>>>>>>>>>>>>>>> Start of dump of cached events (%r) >>>>>>>>>>>>>>>
 *    log4j.appender.CachingApp.dumpFooter=<<<<<<<<<<<<<<<<<<< End of dump of cached events (%r) <<<<<<<<<<<<<<<<
 *    
 *    log4j.appender.CA=org.apache.log4j.ConsoleAppender
 *    log4j.appender.CA.layout=org.apache.log4j.PatternLayout
 *    log4j.appender.CA.layout.ConversionPattern=%p [%d{ISO8601}] "%t" %c{2} - %m%n
 * </PRE>
 * This configuration installs this appender ("CachingApp") to store all events of level <code>INFO</code> or above
 * in a cache of max. 60 events. If an event of level <code>ERROR</code> or above is logged, the whole cache 
 * is given to the decorated appenders.
 * <P>
 * As decorated appenders all appenders configured for the category named equal to the 
 * {@link Appender#getName name of this appender} are used.
 * <P>
 * In this example the caching appender is called "CachingApp" (<code>log4j.appender.<B>CachingApp</B>=org.apache.log4j.CachingAppender</code>). 
 * Hence all appenders (here: <code>CA</code>)configured in the second line for category "CachingApp" 
 * (<code>log4j.logger.CachingApp=DEBUG, CA</code>) are used to output the contents of the cache.
 * <P>
 * If present the text defined by <B>dumpHeader</B> will be given to the decorated 
 * appenders before the whole cache will be dumped and <B>dumpFooter</B>
 * will mark the end of the dump of the cache. Both lines are logged using level {@link #dumpCacheLevel}.
 * <BR>
 * In both lines the patterns defined in {@link PatternLayout} may be used. The timestamp of both lines of one 
 * cache dump contain the same value (the timestamp of the start of the dump).
 * <P>
 * If a <B>threshold</B> is defined (<code>log4j.appender.CachingApp.threshold=WARN</code>) an event with a level >=
 * the defined threshold will be given to the decorated appenders immediately without dumping the whole cache.
 * Additionally the event is processed like any other event. Thus maybe the event is written twice: 
 * once because of the threshold and once because of the immediately after that output starting dump of the
 * whole cache.
 * 
 * @author Alex Gadenz
 */
public class CachingAppender extends AppenderSkeleton
{
	/** Default size of the cache. */
	private static final int DEFAULT_MAX_CACHE_SIZE = 100;

	/** 
	 * The logger used to output internal messages to decorated appenders.
	 * 
	 * @see #appendInternalMessage
	 */
	private static final Logger internalMessageLogger = Logger.getLogger( CachingAppender.class );
			
	/** 
     * If an event with that level or above is logged the whole cache will be dumped
	 * to all {@link #appenders decorated appenders}. 
	 */
	private Level dumpCacheLevel = Level.INFO;

	/** Header line of a cache dump. */
	private String dumpHeader;
    
    /** The layout of the header line of a cache dump. */
    private PatternLayout dumpHeaderLayout;

	/** Footer line of a cache dump. */
	private String dumpFooter;
    
    /** The layout of the footer line of a cache dump. */
    private PatternLayout dumpFooterLayout;
	
	/** The required level to determine if the decorated appenders should be called. */
	private Level appenderLevel = Level.INFO;
	
	/** Set of decorated appenders. */
	private Set appenders = null;

	/** The cache to store events in. */
	private CyclicBuffer cache = new CyclicBuffer( DEFAULT_MAX_CACHE_SIZE );

	

    public CachingAppender()
    {
        this( false );
    }
    
    public CachingAppender( boolean isActive )
    {
        super( isActive );
    }
    
    /**
	 * Nothing to close.
	 */
	public void close()	{
	}
	
	/**
	 * Caches the passed <code>event</code>. 
	 * If the whole cache should be dumped because the level of <code>event</code> is >= 
	 * {@link #dumpCacheLevel} the cache will be dumped.
	 * 
	 * @param event the event to add to the cache
	 */
	public void doAppend( LoggingEvent event )	{
		// get the appenders here, 'cause here we know they are present
		initAppenders();

		// add the event to the cache
		addToCache( event );
		
		// should we log this event because of the threshold?
		if( getThreshold() != null && isAsSevereAsThreshold( event.getLevel() ) ) {
			append( event );
		}
		
		// should we dump the cache?
		if( event.getLevel().isGreaterOrEqual( dumpCacheLevel ) ) {
			dumpCache();
		}
	}
	
	/**
	 * Forwards <code>event</code> to all decorated appenders.
	 * 
	 * @param event the event to log
	 */
	protected void append( LoggingEvent event )	{
		// get all decorated appenders and forward the event
		Iterator iter = getAppenderIterator();
		while( iter.hasNext() )	{
			Appender app = (Appender)iter.next();
			app.doAppend( event );
		}
	}

	/**
	 * Initializes the decorated appenders.
	 * <P>
	 * Gets the appenders from the {@link Logger} for the category named like
	 * this appenders name and stores them in {@link #appenders}.
	 */
	protected void initAppenders() {
		if( appenders == null ) {
            synchronized( cache ) {
                if( appenders == null ) {
        			appenders = new HashSet();
        			
        			// get logger for category with name equal to name of this appender
        			Logger dummyLogger = Logger.getLogger( getName() );
        			// init level of the appenders
        			appenderLevel = dummyLogger.getEffectiveLevel();
        			
        			// get reference to all appenders of that category
        			Enumeration apps = dummyLogger.getAllAppenders();
        			while( apps.hasMoreElements() ) {
        				appenders.add( apps.nextElement() );
        			}
                }
            }
		}
	}

	/**
	 * Outputs all cached {@link LoggingEvent}s to the decorated
	 * appenders stored in {@link #appenders} and removes the events from the cache.
	 * 
	 * @see #initAppenders
	 */
	protected void dumpCache() {
		synchronized( cache ) {
            // timestamp of dump
            long timestamp = new Date().getTime();
            
			// output the header line
			appendInternalMessage( getDumpHeaderLayout(), timestamp );
			
			// output all cached events and clears the cache (implicit)
			while( cache.length() > 0 ) {
				LoggingEvent cachedEvent = cache.get();
	
				if( cachedEvent.getLevel().isGreaterOrEqual( appenderLevel ) ) {
					append( cachedEvent );
				}
			}
	
			// output the footer line
			appendInternalMessage( getDumpFooterLayout(), timestamp );
		}
	}
	
	/**
	 * Forwards an event containing <code>msgLayout</code> and <code>timestamp</code> to {@link #append} if
	 * <code>msgLayout</code> is not <code>null</code>.
	 * 
     * @param msgLayout the layout of the message
	 * @param timestamp the time to use in the generated message
	 */
	protected void appendInternalMessage( PatternLayout msgLayout, long timestamp ) {
		if( msgLayout != null ) {
			// create event to format message (with pattern substitution)
			LoggingEvent unformattedEvent = new LoggingEvent(  
										Category.class.getName(),
										internalMessageLogger,
										dumpCacheLevel,
										null,
										null );
            unformattedEvent.setTimeStamp( timestamp );

			// create event to output the formatted message
			LoggingEvent formattedEvent = new LoggingEvent(
										Category.class.getName(),
										internalMessageLogger,
										dumpCacheLevel,
                                        msgLayout.format( unformattedEvent ),
										null );

			append( formattedEvent );
		}
	}
	
	/**
	 * Adds <code>event</code> to the cache.
	 * 
	 * @param event the event to add to the cache
	 */
	protected void addToCache( LoggingEvent event ) {
		synchronized( cache ) {
            // fix the internal attributes of the event to current values
            event.prepareForDeferredProcessing();
            
			cache.add( event );
		}
	}

	/**
	 * Returns an iterator over the list of decorated appenders.
	 * 
	 * @return iterator over the list of decorated appenders
	 */
	protected Iterator getAppenderIterator() {
		return appenders.iterator();
	}

	/**
	 * Returns {@link Level} from which on the cache should be dumped.
	 * 
	 * @return level to dump the cache
	 */
	protected Level getDumpCacheLevel() {
		return dumpCacheLevel;
	}

	/**
	 * Sets the {@link Level} from which on the cache should be dumped.
	 * 
	 * @param dumpCacheLevel the new level
	 */
	public void setDumpCacheLevel(Level dumpCacheLevel) {
		this.dumpCacheLevel = dumpCacheLevel;
	}

	/**
	 * Returns the maximal size of the cache.
	 * 
	 * @return maximal size of the cache
	 */
	protected int getMaxCacheSize() {
		return cache.getMaxSize();
	}

	/**
	 * Sets the maximal size of the cache.
	 * 
	 * @param maxCacheSize maximal size of the cache
	 */
	public void setMaxCacheSize(int maxCacheSize) {
		synchronized( cache ) {
			cache.resize( maxCacheSize );
		}
	}

	/**
	 * Returns the line to start a dump with.
	 * 
	 * @return line to start a dump with
	 */
	protected String getDumpHeader() {
		return dumpHeader;
	}

	/**
	 * Sets the line to start a dump with.
	 * 
	 * @param dumpHeader line to start a dump with
	 */
	public void setDumpHeader(String dumpHeader) {
		this.dumpHeader = dumpHeader;
        this.dumpHeaderLayout = null;
	}
    
    /**
     * Returns the header message of the dump as layout.
     *
     * @return the header message as layout; <code>null</code> if no header message is available
     */
    protected PatternLayout getDumpHeaderLayout() {
        String msg = getDumpHeader();
        
        if( dumpHeaderLayout == null && msg != null ) {
            dumpHeaderLayout = new PatternLayout( msg );
        }
        
        return dumpHeaderLayout;
    }
	
	/**
	 * Returns the line to end a dump with.
	 * 
	 * @return line to end a dump with
	 */
	protected String getDumpFooter() {
		return dumpFooter;
	}

	/**
	 * Sets the line to end a dump with.
	 * 
	 * @param dumpFooter line to end a dump with
	 */
	public void setDumpFooter(String dumpFooter) {
		this.dumpFooter = dumpFooter;
        this.dumpFooterLayout = null;
	}
    
    /**
     * Returns the footer message of the dump as layout.
     *
     * @return the footer message as layout; <code>null</code> if no footer message is available
     */
    protected PatternLayout getDumpFooterLayout() {
        String msg = getDumpFooter();
        
        if( dumpFooterLayout == null && msg != null ) {
            dumpFooterLayout = new PatternLayout( msg );
        }
        
        return dumpFooterLayout;
    }
}