Development Java Tutorial

/*
 * Log.java - A class for logging events
 * :tabSize=8:indentSize=8:noTabs=false:
 * :folding=explicit:collapseFolds=1:
 *
 * Copyright (C) 1999, 2003 Slava Pestov
 *
 * This program is free software; you can redistribute it and/or
 * modify it under the terms of the GNU General Public License
 * as published by the Free Software Foundation; either version 2
 * of the License, or any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program; if not, write to the Free Software
 * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA  02111-1307, USA.
 */
//{{{ Imports
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.OutputStream;
import java.io.PrintStream;
import java.io.Writer;
import java.text.DateFormat;
import java.util.*;
import javax.swing.event.ListDataEvent;
import javax.swing.event.ListDataListener;
import javax.swing.ListModel;
import javax.swing.SwingUtilities;
import static java.text.DateFormat.MEDIUM;
//}}}
/**
 * This class provides methods for logging events. In terms of functionality,
 * it is somewhere in between System.out.println() and
 * full-blown logging packages such as log4j.
 *
 * All events are logged to an in-memory buffer and optionally a stream,
 * and those with a high urgency (warnings and errors) are also printed
 * to standard output.
 *
 * Logging of exception tracebacks is supported.
 *
 * This class can also optionally redirect standard output and error to the log.
 *
 * @author Slava Pestov
 * @version $Id: Log.java 12789 2008-06-04 21:23:10Z kpouer $
 */
public class Log
{
  //{{{ Constants
  /**
   * The maximum number of log messages that will be kept in memory.
   * @since jEdit 2.6pre5
   */
  public static final int MAXLINES = 500;
  /**
   * Debugging message urgency. Should be used for messages only
   * useful when debugging a problem.
   * @since jEdit 2.2pre2
   */
  public static final int DEBUG = 1;
  /**
   * Message urgency. Should be used for messages which give more
   * detail than notices.
   * @since jEdit 2.2pre2
   */
  public static final int MESSAGE = 3;
  /**
   * Notice urgency. Should be used for messages that directly
   * affect the user.
   * @since jEdit 2.2pre2
   */
  public static final int NOTICE = 5;
  /**
   * Warning urgency. Should be used for messages that warrant
   * attention.
   * @since jEdit 2.2pre2
   */
  public static final int WARNING = 7;
  /**
   * Error urgency. Should be used for messages that signal a
   * failure.
   * @since jEdit 2.2pre2
   */
  public static final int ERROR = 9;
  //}}}
  //{{{ init() method
  /**
   * Initializes the log.
   * @param stdio If true, standard output and error will be
   * sent to the log
   * @param level Messages with this log level or higher will
   * be printed to the system console
   * @since jEdit 3.2pre4
   */
  public static void init(boolean stdio, int level)
  {
    if(stdio)
    {
      if(System.out == realOut && System.err == realErr)
      {
        System.setOut(createPrintStream(NOTICE,null));
        System.setErr(createPrintStream(ERROR,null));
      }
    }
    Log.level = level;
    // Log some stuff
    log(MESSAGE,Log.class,"When reporting bugs, please"
      + " include the following information:");
    String[] props = {
      "java.version", "java.vm.version", "java.runtime.version",
      "java.vendor", "java.compiler", "os.name", "os.version",
      "os.arch", "user.home", "java.home",
      "java.class.path",
      };
    for(int i = 0; i < props.length; i++)
    {
      log(MESSAGE,Log.class,
        props[i] + '=' + System.getProperty(props[i]));
    }
  } //}}}
  //{{{ setLogWriter() method
  /**
   * Writes all currently logged messages to this stream if there was no
   * stream set previously, and sets the stream to write future log
   * messages to.
   * @param stream The writer
   * @since jEdit 3.2pre4
   */
  public static void setLogWriter(Writer stream)
  {
    if(Log.stream == null && stream != null)
    {
      try
      {
        if(wrap)
        {
          for(int i = logLineCount; i < log.length; i++)
          {
            stream.write(log[i]);
            stream.write(lineSep);
          }
        }
        for(int i = 0; i < logLineCount; i++)
        {
          stream.write(log[i]);
          stream.write(lineSep);
        }
        stream.flush();
      }
      catch(Exception e)
      {
        // do nothing, who cares
      }
    }
    Log.stream = stream;
  } //}}}
  //{{{ flushStream() method
  /**
   * Flushes the log stream.
   * @since jEdit 2.6pre5
   */
  public static void flushStream()
  {
    if(stream != null)
    {
      try
      {
        stream.flush();
      }
      catch(IOException io)
      {
        io.printStackTrace(realErr);
      }
    }
  } //}}}
  //{{{ closeStream() method
  /**
   * Closes the log stream. Should be done before your program exits.
   * @since jEdit 2.6pre5
   */
  public static void closeStream()
  {
    if(stream != null)
    {
      try
      {
        stream.close();
        stream = null;
      }
      catch(IOException io)
      {
        io.printStackTrace(realErr);
      }
    }
  } //}}}
  //{{{ getLogListModel() method
  /**
   * Returns the list model for viewing the log contents.
   * @since jEdit 4.2pre1
   */
  public static ListModel getLogListModel()
  {
    return listModel;
  } //}}}
  //{{{ log() method
  /**
   * Logs an exception with a message.
   *
   * If an exception is the cause of a call to {@link #log}, then
   * the exception should be explicitly provided so that it can
   * be presented to the (debugging) user in a useful manner
   * (not just the exception message, but also the exception stack trace)
   *
   * @since jEdit 4.3pre5
   */
  public static void log(int urgency, Object source, Object message,
    Throwable exception)
  {
    // We can do nicer here, but this is a start...
    log(urgency,source,message);
    log(urgency,source,exception);
  } //}}}
  //{{{ log() method
  /**
   * Logs a message. This method is thread-safe.
   *
   * The following code sends a typical debugging message to the activity
   * log:
   * 
Log.log(Log.DEBUG,this,"counter = " + counter);

   * The corresponding activity log entry might read as follows:
   * 
[debug] JavaParser: counter = 15

   *
   * @param urgency The urgency; can be one of
   * Log.DEBUGLog.MESSAGE,
   * Log.NOTICELog.WARNING, or
   * Log.ERROR.
   * @param source The source of the message, either an object or a
   * class instance. When writing log messages from macros, set
   * this parameter to BeanShell.class to make macro
   * errors easier to spot in the activity log.
   * @param message The message. This can either be a string or
   * an exception
   *
   * @since jEdit 2.2pre2
   */
  public static void log(int urgency, Object source, Object message)
  {
    String _source;
    if(source == null)
    {
      _source = Thread.currentThread().getName();
      if(_source == null)
      {
        _source = Thread.currentThread().getClass().getName();
      }
    }
    else if(source instanceof Class)
      _source = ((Class)source).getName();
    else
      _source = source.getClass().getName();
    int index = _source.lastIndexOf('.');
    if(index != -1)
      _source = _source.substring(index+1);
    if(message instanceof Throwable)
    {
      _logException(urgency,source,(Throwable)message);
    }
    else
    {
      String _message = String.valueOf(message);
      // If multiple threads log stuff, we don't want
      // the output to get mixed up
      synchronized(LOCK)
      {
        StringTokenizer st = new StringTokenizer(
          _message,"\r\n");
        int lineCount = 0;
        boolean oldWrap = wrap;
        while(st.hasMoreTokens())
        {
          lineCount++;
          _log(urgency,_source,st.nextToken()
            .replace('\t',' '));
        }
        listModel.update(lineCount,oldWrap);
      }
    }
  } //}}}
  //{{{ Private members
  //{{{ Instance variables
  private static final Object LOCK;
  private static final String[] log;
  private static int logLineCount;
  private static boolean wrap;
  private static int level;
  private static Writer stream;
  private static final String lineSep;
  private static final PrintStream realOut;
  private static final PrintStream realErr;
  private static final LogListModel listModel;
  private static final DateFormat timeFormat;
  private static final int MAX_THROWABLES = 10;
  public static final List throwables;
  //}}}
  //{{{ Class initializer
  static
  {
    LOCK = new Object();
    level = WARNING;
    realOut = System.out;
    realErr = System.err;
    log = new String[MAXLINES];
    lineSep = System.getProperty("line.separator");
    listModel = new LogListModel();
    
    timeFormat = DateFormat.getTimeInstance(MEDIUM);
    throwables = Collections.synchronizedList(new ArrayList(MAX_THROWABLES));
  } //}}}
  //{{{ createPrintStream() method
  private static PrintStream createPrintStream(final int urgency,
    final Object source)
  {
    return new LogPrintStream(urgency, source);
  } //}}}
  //{{{ _logException() method
  private static void _logException(final int urgency,
    final Object source,
    final Throwable message)
  {
    PrintStream out = createPrintStream(urgency,source);
    if (urgency >= level)
    {
      synchronized (throwables)
      {
        if (throwables.size() == MAX_THROWABLES)
        {
          throwables.remove(0);
        }
        throwables.add(message);
      }
    }
    synchronized(LOCK)
    {
      message.printStackTrace(out);
    }
  } //}}}
  //{{{ _log() method
  private static void _log(int urgency, String source, String message)
  {
    String fullMessage = timeFormat.format(new Date()) + " ["+Thread.currentThread().getName()+"] [" + urgencyToString(urgency) + "] " + source
      + ": " + message;
    try
    {
      log[logLineCount] = fullMessage;
      if(++logLineCount >= log.length)
      {
        wrap = true;
        logLineCount = 0;
      }
      if(stream != null)
      {
        stream.write(fullMessage);
        stream.write(lineSep);
      }
    }
    catch(Exception e)
    {
      e.printStackTrace(realErr);
    }
    if(urgency >= level)
    {
      if(urgency == ERROR)
        realErr.println(fullMessage);
      else
        realOut.println(fullMessage);
    }
  } //}}}
  //{{{ urgencyToString() method
  private static String urgencyToString(int urgency)
  {
    switch(urgency)
    {
    case DEBUG:
      return "debug";
    case MESSAGE:
      return "message";
    case NOTICE:
      return "notice";
    case WARNING:
      return "warning";
    case ERROR:
      return "error";
    }
    throw new IllegalArgumentException("Invalid urgency: " + urgency);
  } //}}}
  //}}}
  //{{{ LogListModel class
  static class LogListModel implements ListModel
  {
    final List listeners = new ArrayList();
    //{{{ fireIntervalAdded() method
    private void fireIntervalAdded(int index1, int index2)
    {
      for(int i = 0; i < listeners.size(); i++)
      {
        ListDataListener listener = listeners.get(i);
        listener.intervalAdded(new ListDataEvent(this,
          ListDataEvent.INTERVAL_ADDED,
          index1,index2));
      }
    } //}}}
    //{{{ fireIntervalRemoved() method
    private void fireIntervalRemoved(int index1, int index2)
    {
      for(int i = 0; i < listeners.size(); i++)
      {
        ListDataListener listener = listeners.get(i);
        listener.intervalRemoved(new ListDataEvent(this,
          ListDataEvent.INTERVAL_REMOVED,
          index1,index2));
      }
    } //}}}
    //{{{ addListDataListener() method
    public void addListDataListener(ListDataListener listener)
    {
      listeners.add(listener);
    } //}}}
    //{{{ removeListDataListener() method
    public void removeListDataListener(ListDataListener listener)
    {
      listeners.remove(listener);
    } //}}}
    //{{{ getElementAt() method
    public Object getElementAt(int index)
    {
      if(wrap)
      {
        if(index < MAXLINES - logLineCount)
          return log[index + logLineCount];
        else
          return log[index - MAXLINES + logLineCount];
      }
      else
        return log[index];
    } //}}}
    //{{{ getSize() method
    public int getSize()
    {
      if(wrap)
        return MAXLINES;
      else
        return logLineCount;
    } //}}}
    //{{{ update() method
    void update(final int lineCount, final boolean oldWrap)
    {
      if(lineCount == 0 || listeners.isEmpty())
        return;
      SwingUtilities.invokeLater(new Runnable()
      {
        public void run()
        {
          if(wrap)
          {
            if(oldWrap)
              fireIntervalRemoved(0,lineCount - 1);
            else
            {
              fireIntervalRemoved(0,
                logLineCount);
            }
            fireIntervalAdded(
              MAXLINES - lineCount + 1,
              MAXLINES);
          }
          else
          {
            fireIntervalAdded(
              logLineCount - lineCount + 1,
              logLineCount);
          }
        }
      });
    } //}}}
  } //}}}
  //{{{ LogPrintStream class
  /**
   * A print stream that uses the "Log" class to output the messages,
   * and has special treatment for the printf() function. Using this
   * stream has one caveat: printing messages that don't have a line
   * break at the end will have one added automatically...
   */
  private static class LogPrintStream extends PrintStream {
    private final ByteArrayOutputStream buffer;
    private final OutputStream orig;
    //{{{ LogPrintStream constructor
    LogPrintStream(int urgency, Object source)
    {
      super(new LogOutputStream(urgency, source));
      buffer = new ByteArrayOutputStream();
      orig = out;
    } //}}}
    //{{{ printf() method
    /**
     * This is a hack to allow "printf" to not print weird
     * stuff to the output. Since "printf" doesn't seem to
     * print the whole message in one shot, our output
     * stream above would break a line of log into several
     * lines; so we buffer the result of the printf call and
     * print the whole thing in one shot. A similar hack
     * would be needed for the "other" printf method, but
     * I'll settle for the common case only.
     */
    public PrintStream printf(String format, Object... args)
    {
      synchronized (orig)
      {
        buffer.reset();
        out = buffer;
        super.printf(format, args);
        try
        {
          byte[] data = buffer.toByteArray();
          orig.write(data, 0, data.length);
          out = orig;
        }
        catch (IOException ioe)
        {
          // don't do anything?
        }
        finally
        {
          buffer.reset();
        }
      }
      return this;
    } //}}}
  } //}}}
  //{{{ LogOutputStream class
  private static class LogOutputStream extends OutputStream
  {
    private final int   urgency;
    private final Object  source;
    //{{{ LogOutputStream constructor
    LogOutputStream(int urgency, Object source)
    {
      this.urgency  = urgency;
      this.source   = source;
    } //}}}
    //{{{ write() method
    public synchronized void write(int b)
    {
      byte[] barray = { (byte)b };
      write(barray,0,1);
    } //}}}
    //{{{ write() method
    public synchronized void write(byte[] b, int off, int len)
    {
      String str = new String(b,off,len);
      log(urgency,source,str);
    } //}}}
  } //}}}
}