/*
* 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 <code>System.out.println()</code> 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:
* <pre>Log.log(Log.DEBUG,this,"counter = " + counter);</pre>
* The corresponding activity log entry might read as follows:
* <pre>[debug] JavaParser: counter = 15</pre>
*
* @param urgency The urgency; can be one of
* <code>Log.DEBUG</code>, <code>Log.MESSAGE</code>,
* <code>Log.NOTICE</code>, <code>Log.WARNING</code>, or
* <code>Log.ERROR</code>.
* @param source The source of the message, either an object or a
* class instance. When writing log messages from macros, set
* this parameter to <code>BeanShell.class</code> 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<Throwable> 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<Throwable>(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<ListDataListener> listeners = new ArrayList<ListDataListener>();
//{{{ 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);
} //}}}
} //}}}
}