/*
 * Copyright 2006 Holger West, Ralf Joachim
 *
 * 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.castor.tools.log4j;

import org.apache.log4j.AppenderSkeleton;
import org.apache.log4j.spi.ErrorCode;
import org.apache.log4j.spi.LoggingEvent;
import org.exolab.castor.jdo.Database;
import org.exolab.castor.jdo.JDOManager;
import org.exolab.castor.jdo.Query;
import org.exolab.castor.jdo.QueryResults;

import java.util.ArrayList;
import java.util.Date;
import java.util.Iterator;
import java.util.List;

/**
 * The <code>CastorAppender</code> provides sending log events to a database.
 *
 * <p>Each append call adds the <code>LoggingEvent</code> to an <code>ArrayList</code>
 * buffer. When the buffer is filled each log event is saved to the database.
 *
 * <b>DatabaseName</b>, <b>BufferSize</b>, <b>ColumnWidthClass</b>,
 * <b>ColumnWidthThread</b>, <b>ColumnWidthMessage</b>, <b>ColumnWidthStackTrace</b> and
 * <b>DuplicateCount</b> are configurable options in the standard log4j ways.
 *
 * @version $Id: TNSAddress.java,v 1.2 2006/08/08 14:42:48 holger Exp $
 * @author  <a href="mailto:holger.west@syscon-informatics.de">Holger West</a>, 
 *          Syscon Ingenieurbüro für Mess- und Datentechnik GmbH.
 */
public final class CastorAppender extends AppenderSkeleton {
    // -----------------------------------------------------------------------------------
    
    /** Default column width for the class column. */
    private static final int COLUMNWIDTHCLASS = 100;
    
    /** Default column width for the thread column. */
    private static final int COLUMNWIDTHTHREAD = 100;
    
    /** Default column width for the message column. */
    private static final int COLUMNWIDTHMESSAGE = 1000;
    
    /** Default column width for the message column. If this value is greater than 4000
     *  and using an oracle database, as minimum the 10g driver is necessary. */
    private static final int COLUMNWIDTHSTACKTRACE = 20000;

    /** Should duplicate entries be replaced with the newest one and count the number of
     *  occurrence or should all records be saved independent. If set to false, all
     *  records are saved independent. */
    private static final boolean DUPLICATECOUNT = false;

    /** List holding all registered <code>CastorAppenders</code>. */
    private static List _elements = new ArrayList();

    /** Default size of LoggingEvent buffer before writting to the database. */
    private int _bufferSize = 1;

    /** ArrayList holding the buffer of Logging Events. */
    private ArrayList _buffer;

    /** Helper object for clearing out the buffer. */
    private ArrayList _removes;
  
    /** The database is opened the first time it is needed and then held open until the
     *  appender is closed. */
    private Database _database;
    
    /** A prepared statement to identify a possible existing entry with the same values.
     *  It is only used if 'duplicateCount' is enabled. */
    private Query _qry;

    /** The name of the database to be used by castor. This <b>must</b> be specified in
     *  the log4j configuration. */
    private String _databaseName;
    
    /** Column width for the class information. */
    private int _columnWidthClass = COLUMNWIDTHCLASS;
    
    /** Column width for the thread information. */
    private int _columnWidthThread = COLUMNWIDTHTHREAD;
    
    /** Column width for the message information. */
    private int _columnWidthMessage = COLUMNWIDTHMESSAGE;
  
    /** Column width for the stack trace information. */
    private int _columnWidthStackTrace = COLUMNWIDTHSTACKTRACE;
    
    /** Replace duplicate entries and count the occurrence? This can be very slow when
     *  saving to the database. */
    private boolean _duplicateCount = DUPLICATECOUNT;
    
    // -----------------------------------------------------------------------------------

    /**
     * Add a new <code>CastorAppender</code> to static list.
     * 
     * @param appender The <code>CastorAppender</code> to be added.
     */
    private static synchronized void addAppender(final CastorAppender appender) {
        _elements.add(appender);
    }
    
    /**
     * Remove a <code>CastorAppender</code> from static list.
     * 
     * @param appender The <code>CastorAppender</code> to be removed.
     */
    private static synchronized void removeAppender(final CastorAppender appender) {
        _elements.remove(appender);
    }
    
    /**
     * Get an array holding all registered <code>CastorAppender</code>.
     * 
     * @return An array holding all registered <code>CastorAppender</code>.
     */
    private static synchronized CastorAppender[] getAppenders() {
        CastorAppender[] appenders = new CastorAppender[_elements.size()];
        return (CastorAppender[]) _elements.toArray(appenders);
    }
    
    /**
     * When the program has ended all logger instances are destroyed. To save all data
     * which are still in the buffer, this method must be called. It saves all data from
     * all registered <code>CastorAppender</code>.
     * <br/>
     * As an alternative <code>org.apache.log4j.LogManager.shutdown()</code> can be
     * called.
     */
    public static void flush() {
        CastorAppender[] appenders = getAppenders();
        if (appenders.length > 0) {
            for (int i = 0; i < appenders.length; i++) {
                appenders[i].flushBuffer();
            }
        }
    }

    // -----------------------------------------------------------------------------------

    /**
     * Default constructor.
     */
    public CastorAppender() {
        super();
        addAppender(this);
        _database = null;
        _buffer = new ArrayList(_bufferSize);
        _removes = new ArrayList(_bufferSize);
    }
    
    /** Closes the appender before disposal. */
    public void finalize() {
        close();
    }

    /**
     * Closes the appender, flushing the buffer first then closing the query and database
     * if it is still open.
     */
    public void close() {
        flushBuffer();
      
        if (_database != null) {
            try {
                _qry.close();
                _database.close();
            } catch (Exception e) {
                errorHandler.error("Error closing database.", e, ErrorCode.CLOSE_FAILURE);
            }
        }
        this.closed = true;
        removeAppender(this);
    }

    // -----------------------------------------------------------------------------------

    /**
     * Adds the event to the buffer.  When full the buffer is flushed.
     * 
     * @param event The event to be logged.
     */
    public synchronized void append(final LoggingEvent event) {
        _buffer.add(event);
        
        if (_buffer.size() >= _bufferSize) {
            flushBuffer();
        }
    }

    /**
     * Loops through the buffer of <code>LoggingEvents</code> and store them into the
     * database. If a statement fails the <code>LoggingEvent</code> stays in the buffer!
     */
    private synchronized void flushBuffer() {
        _removes.ensureCapacity(_buffer.size());
    
        Database db = getDatabase();
        try {
            for (Iterator i = _buffer.iterator(); i.hasNext();) {
                LoggingEvent logEvent = (LoggingEvent) i.next();
                execute(logEvent);
                _removes.add(logEvent);
            }

            db.commit();
            
            _buffer.removeAll(_removes);
            _removes.clear();
        } catch (Exception e) {
            errorHandler.error("Error flush buffer.", e, ErrorCode.GENERIC_FAILURE);
        }
    }

    /**
     * Initialize the database and create the query. If the database is already
     * initialized, only return the database. In both cases a transaction is started. 
     * 
     * @return The initialized database.
     */
    private Database getDatabase() {
        if (_database == null) {
            try {
                _database = JDOManager.createInstance(_databaseName).getDatabase();
                _database.begin();
                String oql = "select o from " + LogEntry.class.getName() 
                + " o where o.className = $1 and"
                + " o.level = $2 and"
                + " o.message = $3";
                _qry = _database.getOQLQuery(oql);
            } catch (Exception e) {
                errorHandler.error("Error get database.", e, ErrorCode.GENERIC_FAILURE);
            }
        } else {
            try {
                _database.begin();
            } catch (Exception e) {
                errorHandler.error(
                        "Cannot begin a transaction.", e, ErrorCode.GENERIC_FAILURE);
            }
        }
        return _database;
    }
  
    /**
     * Save the given <code>LoggingEvent</code> to the database. If 'duplicateCount' is
     * enabled, a possible earlier entry is updated. Events with exceptions are stored
     * ever.
     * 
     * @param event The <code>LoggingEvent</code> to be saved.
     */
    private void execute(final LoggingEvent event) {
        LogEntry entry;
        if (event.getMessage() instanceof LogEntry) {
            entry = (LogEntry) event.getMessage();
        } else if (event.getMessage() != null) {
            String message = event.getMessage().toString();
            message = clipLength(message, _columnWidthMessage);
            entry = new LogEntry(message);
        } else {
            entry = new LogEntry();
        }
        
        String clazz = event.getLoggerName();
        clazz = clipLength(clazz, _columnWidthClass);
        entry.setClassName(clazz);

        String thread = event.getThreadName();
        thread = clipLength(thread, _columnWidthThread);
        entry.setThread(thread);

        entry.setLevel(event.getLevel().toString());
        entry.setTimestamp(new Date(event.timeStamp));
        
        //-----------------------------------------------------------------
        
        boolean hasException = (event.getThrowableInformation() != null);
        
        if (hasException) {
            if (_columnWidthStackTrace > 0) {
                LogExceptionEntry exceptionEntry = new LogExceptionEntry();
                
                String temp = "";
                String[] stackTrace = event.getThrowableStrRep();
                int stackSize = stackTrace.length;
                for (int i = 0; i < stackSize; i++) {
                    temp = temp.concat(stackTrace[i] + "\n");
                }
                
                temp = clipLength(temp, _columnWidthStackTrace);
                
                exceptionEntry.setStackTrace(temp);
                exceptionEntry.setEntry(entry);
                entry.setException(exceptionEntry);
            }
        }
        
        //-----------------------------------------------------------------

        try {
            if (!hasException && _duplicateCount) {
                _qry.bind(entry.getClassName());
                _qry.bind(entry.getLevel());
                _qry.bind(entry.getMessage());
                
                    QueryResults rst = _qry.execute();
                    if (rst.hasMore()) {
                        LogEntry x = (LogEntry) rst.next();
                        x.setTimestamp(entry.getTimestamp());
                        x.setThread(entry.getThread());
                        x.setCount(new Integer(x.getCount().intValue() + 1));
                    } else {
                        entry.setCount(new Integer(1));
                        _database.create(entry);
                    }
                    rst.close();
            } else {
                entry.setCount(new Integer(1));
                _database.create(entry);
            }
        } catch (Exception e) {
            errorHandler.error("Cannot save the object.", e, ErrorCode.FLUSH_FAILURE);
        }
    }
    
    /**
     * Clip a string to ensure the length. If the string is longer, the rear part is
     * clipped.
     * 
     * @param value The string to cut.
     * @param maxLength The maximum length of this value.
     * @return The clipped String.
     */
    private String clipLength(final String value, final int maxLength) {
        if (value.length() > maxLength) {
            return value.substring(0, maxLength);
        } 
        return value;
    }

    /**
     * CastorAppender don't requires a layout.
     * 
     * @return <code>true</code> if this appender require a layout, otherwise
     *         <code>false</code>.
     * */
    public boolean requiresLayout() {
        return false;
    }

    // -----------------------------------------------------------------------------------

    /**
     * Set the size of the buffer.
     * 
     * @param newBufferSize New size of the buffer.
     */
    public void setBufferSize(final int newBufferSize) {
        _bufferSize = newBufferSize;
        _buffer.ensureCapacity(_bufferSize);
        _removes.ensureCapacity(_bufferSize);
    }

    /**
     * Get the size of the buffer.
     * 
     * @return The size of the buffer.
     */
    public int getBufferSize() {
        return _bufferSize;
    }
    
    /**
     * Set the name of the database.
     * 
     * @param name Name of the database.
     */
    public void setDatabaseName(final String name) {
        _databaseName = name;
    }
    
    /**
     * Get the name of the database.
     * 
     * @return Name of the database.
     */
    public String getDatabaseName() {
        return _databaseName;
    }
    
    /**
     * Set the column width for class information.
     * 
     * @param columWidth The column width for class information.
     */
    public void setColumnWidthClass(final int columWidth) {
        _columnWidthClass = columWidth;
    }
    
    /**
     * Get the column width for class information.
     * 
     * @return The column width for class information.
     */
    public int getColumnWidthClass() {
        return _columnWidthClass;
    }

    /**
     * Set the column width for thread information.
     * 
     * @param columWidth The column width for thread information.
     */
    public void setColumnWidthThread(final int columWidth) {
        _columnWidthThread = columWidth;
    }
    
    /**
     * Get the column width for tread information.
     * 
     * @return The column width for thread information.
     */
    public int getColumnWidthThread() {
        return _columnWidthThread;
    }
    
    /**
     * Set the column width for message information.
     * 
     * @param columWidth The column width for message information.
     */
    public void setColumnWidthMessage(final int columWidth) {
        _columnWidthMessage = columWidth;
    }
    
    /**
     * Get the column width for message information.
     * 
     * @return The column width for message information.
     */
    public int getColumnWidthMessage() {
        return _columnWidthMessage;
    }

    /**
     * Set the column width for stack trace information.
     * 
     * @param columWidth The column width for stack trace information.
     */
    public void setColumnWidthStackTrace(final int columWidth) {
        _columnWidthStackTrace = columWidth;
    }
    
    /**
     * Get the column width for stack trace information.
     * 
     * @return The column width for stack trace information.
     */
    public int getColumnWidthStackTrace() {
        return _columnWidthStackTrace;
    }

    /**
     * Set duplicate count.
     * 
     * @param duplicateCount Should duplicate count be enabled?
     */
    public void setDuplicateCount(final String duplicateCount) {
        String temp = duplicateCount.toLowerCase();
        if ("true".equals(temp)) {
            _duplicateCount = true;
        } else {
            _duplicateCount = false;
        }
    }
    
    /**
     * Is duplicate count enabled?
     * 
     * @return <code>true</code> if duplicate count is enabled, otherwise
     *         <code>false</code>.
     */
    public String getDuplicateCount() {
        return new Boolean(_duplicateCount).toString();
    }

    // -----------------------------------------------------------------------------------
}
