// ========================================================================
// Copyright (c) 1997 MortBay Consulting, Sydney
// $Id: OutputStreamLogSink.java,v 1.4 2004/09/19 08:04:57 gregwilkins Exp $
// ========================================================================

package org.mortbay.log;

import java.io.IOException;
import java.io.OutputStream;
import java.io.PrintWriter;
import java.lang.reflect.Array;
import java.lang.reflect.Method;
import java.util.TimeZone;

import org.mortbay.util.ByteArrayISO8859Writer;
import org.mortbay.util.DateCache;
import org.mortbay.util.MultiException;
import org.mortbay.util.RolloverFileOutputStream;
import org.mortbay.util.StringBufferWriter;
import org.mortbay.util.StringUtil;


/* ------------------------------------------------------------ */
/** A Log sink.
 * This class represents both a concrete or abstract sink of
 * Log data.  The default implementation logs to System.err, but
 * other output stream or files may be specified.
 *
 * Currently this Stream only writes in ISO8859_1 encoding.  For
 * Other encodings use the less efficient WriterLogSink.
 *
 * If a logFilename is specified, output is sent to that file.
 * If the filename contains "yyyy_mm_dd", the log file date format
 * is used to create the actual filename and the log file is rolled
 * over at local midnight.
 * If append is set, existing logfiles are appended to, otherwise
 * a backup is created with a timestamp.
 * Dated log files are deleted after retain days.
 * 
 * <p> If the property LOG_DATE_FORMAT is set, then it is interpreted
 * as a format string for java.text.SimpleDateFormat and used to
 * format the log timestamps. Default value: HH:mm:ss.SSS
 *
 * <p> If LOG_TIMEZONE is set, it is used to set the timezone of the log date
 * format, otherwise GMT is used.
 *
 * @see org.mortbay.util.Log
 * @version $Id: OutputStreamLogSink.java,v 1.4 2004/09/19 08:04:57 gregwilkins Exp $
 * @author Greg Wilkins (gregw)
 */
public class OutputStreamLogSink
    implements LogSink
{
    /* ------------------------------------------------------------ */
    private final static String __lineSeparator = System.getProperty("line.separator");

    /*-------------------------------------------------------------------*/
    private int _retainDays=31;
    protected DateCache _dateFormat= new DateCache("HH:mm:ss.SSS");
    protected String _logTimezone;

    /* ------------------------------------------------------------ */
    protected boolean _logTimeStamps=true;
    protected boolean _logLabels=true;
    protected boolean _logTags=true;
    protected boolean _logStackSize=true;
    protected boolean _logStackTrace=false;
    protected boolean _logOneLine=false;
    protected boolean _suppressStack=false;
    
    /*-------------------------------------------------------------------*/
    private String _filename;
    private boolean _append=true;
    protected boolean _flushOn=true;
    protected int _bufferSize=2048;
    protected boolean _reopen=false;
    

    protected transient LogImpl _logImpl=null;
    protected transient boolean _started;
    protected transient OutputStream _out;
    protected transient ByteArrayISO8859Writer _buffer;

    /* ------------------------------------------------------------ */
    /** Constructor. 
     */
    public OutputStreamLogSink()
        throws IOException
    {
        _filename=System.getProperty("LOG_FILE");
        if (_filename==null)
            _out=LogStream.STDERR_STREAM;
    }
        
    /* ------------------------------------------------------------ */
    public OutputStreamLogSink(String filename)
    {
        _filename=filename;
    }
    
    /* ------------------------------------------------------------ */
    public String getLogDateFormat()
    {
        return _dateFormat.getFormatString();
    }
    
    /* ------------------------------------------------------------ */
    public void setLogDateFormat(String logDateFormat)
    {
        _dateFormat = new DateCache(logDateFormat);
        if (_logTimezone!=null)
            _dateFormat.getFormat().setTimeZone(TimeZone.getTimeZone(_logTimezone));
    }

    
    /* ------------------------------------------------------------ */
    /** 
     * @deprecated Use getLogTimeZone() 
     */
    public String getLogTimezone()
    {
        return _logTimezone;
    }
    
    /* ------------------------------------------------------------ */
    /** 
     * @deprecated Use setLogTimeZone(String) 
     */
    public void setLogTimezone(String logTimezone)
    {
        _logTimezone=logTimezone;
        if (_dateFormat!=null && _logTimezone!=null)
            _dateFormat.getFormat().setTimeZone(TimeZone.getTimeZone(_logTimezone));
    }
    
    /* ------------------------------------------------------------ */
    public String getLogTimeZone()
    {
        return _logTimezone;
    }
    
    /* ------------------------------------------------------------ */
    public void setLogTimeZone(String logTimezone)
    {
        _logTimezone=logTimezone;
        if (_dateFormat!=null && _logTimezone!=null)
            _dateFormat.getFormat().setTimeZone(TimeZone.getTimeZone(_logTimezone));
    }
    
    /* ------------------------------------------------------------ */
    public boolean isLogTimeStamps()
    {
        return _logTimeStamps;
    }
    /* ------------------------------------------------------------ */
    public void setLogTimeStamps(boolean logTimeStamps)
    {
        _logTimeStamps = logTimeStamps;
    }
    /* ------------------------------------------------------------ */
    public boolean isLogLabels()
    {
        return _logLabels;
    }
    /* ------------------------------------------------------------ */
    public void setLogLabels(boolean logLabels)
    {
        _logLabels = logLabels;
    }
    /* ------------------------------------------------------------ */
    public boolean isLogTags()
    {
        return _logTags;
    }
    /* ------------------------------------------------------------ */
    public void setLogTags(boolean logTags)
    {
        _logTags = logTags;
    }
    /* ------------------------------------------------------------ */
    public boolean isLogStackSize()
    {
        return _logStackSize;
    }
    /* ------------------------------------------------------------ */
    public void setLogStackSize(boolean logStackSize)
    {
        _logStackSize = logStackSize;
    }
    /* ------------------------------------------------------------ */
    public boolean isLogStackTrace()
    {
        return _logStackTrace;
    }
    /* ------------------------------------------------------------ */
    public void setLogStackTrace(boolean logStackTrace)
    {
        _logStackTrace = logStackTrace;
    }
    /* ------------------------------------------------------------ */
    public boolean isLogOneLine()
    {
        return _logOneLine;
    }
    /* ------------------------------------------------------------ */
    public void setLogOneLine(boolean logOneLine)
    {
        _logOneLine = logOneLine;
    }

    /* ------------------------------------------------------------ */
    public boolean isAppend()
    {
        return _append;
    }

    /* ------------------------------------------------------------ */
    public void setAppend(boolean a)
    {
        _append=a;
    }

    /* ------------------------------------------------------------ */
    public boolean isSuppressStack()
    {
        return _suppressStack;
    }

    /* ------------------------------------------------------------ */
    public void setSuppressStack(boolean suppressStack)
    {
        _suppressStack = suppressStack;
    }
    
    /* ------------------------------------------------------------ */
    public synchronized void setOutputStream(OutputStream out)
    {
        _reopen=isStarted() && out!=out;
        _filename=null;
        if (_buffer!=null)
            _buffer.resetWriter();
        _out=out;
    }

    /* ------------------------------------------------------------ */
    public OutputStream getOutputStream()
    {
        return _out;
    }

    /* ------------------------------------------------------------ */
    public synchronized void setFilename(String filename)
    {
        if (filename!=null)
        {
            filename=filename.trim();
            if (filename.length()==0)
                filename=null;
        }
        
        if (isStarted() && _filename!=null && filename==null)
            _out=null;
        
        _reopen=isStarted() &&
            ((_filename==null && filename!=null)||
             (_filename!=null && !_filename.equals(filename)));
        _filename=filename;

        if (!isStarted() && _filename!=null)
            _out=null;
    }

    /* ------------------------------------------------------------ */
    public String getFilename()
    {
        return _filename;
    }
    
    /* ------------------------------------------------------------ */
    public String getDatedFilename()
    {
        if (_filename==null)
            return null;

        if (_out==null || ! (_out instanceof RolloverFileOutputStream))
            return null;

        return ((RolloverFileOutputStream)_out).getDatedFilename();
    }
    
    /* ------------------------------------------------------------ */
    public int getRetainDays()
    {
        return _retainDays;
    }

    /* ------------------------------------------------------------ */
    public void setRetainDays(int retainDays)
    {
        _reopen=isStarted() && _retainDays!=retainDays;
        _retainDays = retainDays;
    }
    
    /* ------------------------------------------------------------ */
    /** 
     * @param on If true, log is flushed on every log. 
     */
    public void setFlushOn(boolean on)
    {
        _flushOn=on;
        if (on && _out!=null)
        {
            try{_out.flush();}
            catch(IOException e){e.printStackTrace();}
        }
    }
    
    /* ------------------------------------------------------------ */
    /** 
     * @return true, log is flushed on every log. 
     */
    public boolean getFlushOn()
    {
        return _flushOn;
    }
    
    /* ------------------------------------------------------------ */
    /** Log a message.
     * This method formats the log information as a string and calls
     * log(String).  It should only be specialized by a derived
     * implementation if the format of the logged messages is to be changed.
     *
     * @param tag Tag for type of log
     * @param msg The message
     * @param frame The frame that generated the message.
     * @param time The time stamp of the message.
     */
    public  synchronized void log(String tag,
                                  Object o,
                                  Frame frame,
                                  long time)
    {
        StringBuffer buf = new StringBuffer(160);
        
        // Log the time stamp
        if (_logTimeStamps)
        {
            buf.append(_dateFormat.format(time));
            buf.append(' ');
        }
            
            
        // Log the tag
        if (_logTags)
            buf.append(tag);
        
        // Log the label
        if (_logLabels && frame != null)
        {
            buf.append(frame.toString());
        }
        
        // Log the stack depth.
        if (_logStackSize && frame != null)
        {
            buf.append(" >");
            if (frame.getDepth()<10)    
                buf.append('0');
            buf.append(Integer.toString(frame.getDepth()));  
            buf.append("> ");
        }
        
        // Determine the indent string for the message and append it
        // to the buffer. Only put a newline in the buffer if the first
        // line is not blank
        String nl=__lineSeparator;
        
        if (_logLabels && !_logOneLine && _buffer.size() > 0)
            buf.append(nl);
        
        // Log message
        formatObject(buf,o);
        
        // Add stack frame to message
        if (_logStackTrace && frame != null)
        {
            buf.append(nl);
            buf.append(frame.getStack());
        }
        
        log(buf.toString());
    }
    
    /* ------------------------------------------------------------ */
    /** Log a message.
     * The formatted log string is written to the log sink. The default
     * implementation writes the message to an outputstream.
     * @param formattedLog 
     */
    public synchronized void log(String formattedLog)
    {
        if (_reopen || _out==null)
        {
            stop();
            start();
        }
        try
        {
            _buffer.write(formattedLog);
            _buffer.write(StringUtil.__LINE_SEPARATOR);
            if (_flushOn || _buffer.size()>_bufferSize)
            {
                _buffer.writeTo(_out);
                _buffer.resetWriter();
                _out.flush();
            }
        }
        catch(IOException e){e.printStackTrace();}
    }

    
    /* ------------------------------------------------------------ */
    /** Start a log sink.
     * The default implementation does nothing 
     */
    public synchronized void start()
    {
        _buffer=new ByteArrayISO8859Writer(_bufferSize);
        _reopen=false;
        if (_started)
            return;
        
        if (_out==null && _filename!=null)
        {
            try
            {
                RolloverFileOutputStream rfos=
                    new RolloverFileOutputStream(_filename,_append,_retainDays);
                _out=rfos;
            }
            catch(IOException e){e.printStackTrace();}   
        }

        if (_out==null)
            _out=LogStream.STDERR_STREAM;
        
        _started=true;
    }
    
    
    /* ------------------------------------------------------------ */
    /** Stop a log sink.
     * An opportunity for subclasses to clean up. The default
     * implementation does nothing 
     */
    public synchronized void stop()
    {
        _started=false;

        if (_out!=null)
        {
            try
            {
                if (_buffer.size()>0)
                {
                    _buffer.writeTo(_out);
                }
                _out.flush();
                _buffer=null;
            }
            catch(Exception e){if (_logImpl!=null && _logImpl.getDebug())e.printStackTrace();}
            Thread.yield();
        }
        
        if (_out!=null && _out!=LogStream.STDERR_STREAM)
        {
            try{_out.close();}
            catch(Exception e){if (_logImpl!=null && _logImpl.getDebug())e.printStackTrace();}
        }

        if (_filename!=null)
            _out=null;
    }

    /* ------------------------------------------------------------ */
    public boolean isStarted()
    {
        return _started;
    }

    /* (non-Javadoc)
     * @see org.mortbay.log.LogSink#setLogImpl(org.mortbay.log.LogImpl)
     */
    public void setLogImpl(LogImpl impl)
    {
        _logImpl=impl;
    }    

    /*-------------------------------------------------------------------*/
    private static final Class[] __noArgs=new Class[0];
    private static final String[] __nestedEx =
        {"getTargetException","getTargetError","getException","getRootCause"};

    /*-------------------------------------------------------------------*/
    /** Shared static instances, reduces object creation at expense
     * of lock contention in multi threaded debugging */
    private static StringBufferWriter __stringBufferWriter = new StringBufferWriter();
    private static PrintWriter __printWriter = new PrintWriter(__stringBufferWriter);

    /*-------------------------------------------------------------------*/
    void formatObject(StringBuffer buf,Object o)
    {
        int init_size=buf.length();
        
        if (o==null)
            buf.append("null");
        else if (o.getClass().isArray())
        {
            int l=Array.getLength(o);
            for (int i=0;i<l;i++)
                formatObject(buf,Array.get(o,i));
        }
        else if (o instanceof Throwable)
        {
            Throwable ex = (Throwable) o;
            buf.append('\n');
                
            if (_suppressStack)
            {
                buf.append(ex.toString());
                buf.append("\nNo stack available\n--");
            }
            else
            {
                synchronized(__printWriter)
                {
                    __stringBufferWriter.setStringBuffer(buf);
                    expandThrowable(ex);
                    __printWriter.flush();
                }
            }
        }
        else
            buf.append(o.toString()); 
        
        int end_size=buf.length();
        if (_logOneLine)
        {
            for (int i=init_size;i<end_size;i++)
            {
                char c=buf.charAt(i);
                if (c=='\n')
                    buf.setCharAt(i,'|');
                else if (c=='\r')
                    buf.setCharAt(i,'<');
            }
        }
    }

    /* ------------------------------------------------------------ */
    private static void expandThrowable(Throwable ex)
    {
        ex.printStackTrace(__printWriter);

        if (ex instanceof MultiException)
        {
            MultiException mx = (MultiException)ex;
            
            for (int i=0;i<mx.size();i++)
            {
                __printWriter.print("["+i+"]=");
                Throwable ex2=mx.getException(i);
                expandThrowable(ex2);
            }
        }
        else
        {
            for (int i=0;i<__nestedEx.length;i++)
            {
                try
                {
                    Method getTargetException =
                        ex.getClass().getMethod(__nestedEx[i],__noArgs);
                    Throwable ex2=(Throwable)getTargetException.invoke(ex,(java.lang.Object[])null);
                    if (ex2!=null)
                    {
                        __printWriter.println(__nestedEx[i]+"():");
                        expandThrowable(ex2);
                    }
                }
                catch(Exception ignore){}
            }
        }
    }
    
};
