package winterwell.utils.reporting;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.logging.Level;
import winterwell.utils.Environment;
import winterwell.utils.Key;
import winterwell.utils.Printer;
import winterwell.utils.Utils;
public final class Log {
public static enum KErrorPolicy {
/** Keep the error as is (throw an exception if this is not possible) */
ACCEPT,
/** Remove the file or whatever was to blame */
DELETE_CAUSE,
/**
* There is no point in going on.
*
* @deprecated You rarely want this.
*/
DIE,
/** There is no error here. This never happened. */
IGNORE,
/** Report to log or user. */
REPORT,
/** Return null _or a default value_ in place of whatever. */
RETURN_NULL,
/** Raise hell. */
THROW_EXCEPTION
}
/**
* An extra string which can be accessed by log listeners. Added to
* {@link LogFile}s. Usage: e.g. a high-level process wishes to include info
* in low-level reports.
*/
private static final Key<String> ENV_CONTEXT_MESSAGE = new Key<String>(
"Log.context");
private static final Collection<String> ignoredTags = new ArrayList();
private static ILogListener[] listeners = new ILogListener[0];
/**
* Maximum length (in chars) of a single log report: 5k
*/
private static final int MAX_LENGTH = 1024 * 5;
private static final Key<Level> MIN_LEVEL = new Key<Level>("Log.minLevel");
// private static final Set<Thread> blacklist = new ArraySet<Thread>();
// just an idea...
// public static void setBlacklist(Thread thread, boolean isIgnored) {
// if (isIgnored) blacklist.add(thread);
// else blacklist.remove(thread);
// }
// Add a simple console output listener
static {
addListener(new ILogListener() {
@Override
public void listen(Report report) {
Printer.out(Environment.get().get(Printer.INDENT)
+ report.getMessage());
}
});
}
/**
* Listen for log reports
*
* @param listener
*/
public static synchronized void addListener(ILogListener listener) {
assert listener != null;
for (ILogListener l : listeners) {
if (l.equals(listener))
return;
}
listeners = Arrays.copyOf(listeners, listeners.length + 1);
listeners[listeners.length - 1] = listener;
}
/**
* @return extra contextual message, or "" if unset
*/
public static String getContextMessage() {
String cm = Environment.get().get(Log.ENV_CONTEXT_MESSAGE);
return cm == null ? "" : cm;
}
/**
* get the *default* minimum level to report events. Applies across all
* threads.
*/
public static Level getDefaultLevel() {
// FIXME this is only the default if no-one sets the thread-local!
return Environment.get().get(MIN_LEVEL);
}
public static void info(Object message) {
info(null, message);
}
/**
* Provide an informational message
*
* @param message
*/
public static void info(String tag, Object message) {
assert !(message instanceof Throwable);
report(tag, message, Level.INFO);
}
public static synchronized void removeListener(ILogListener listener) {
ArrayList<ILogListener> ls = new ArrayList(Arrays.asList(listeners));
ls.remove(listener);
listeners = ls.toArray(new ILogListener[0]);
}
public static void report(Object msg) {
if (!(msg instanceof Throwable)) {
report(msg, Level.WARNING);
} else {
report((Throwable) msg);
}
}
public static void report(Object msg, Level error) {
report(null, msg, error);
}
/**
* This is the "master" version of this method (to which the others delegate
* - so perhaps that makes it more the servant method?).
* <p>
* It should never throw an exception. Any exceptions will be swallowed.
*
* @param tag
* Inspired by Android's LogCat. The tag is a rough
* classification on the report, which allows for
* simple-but-effective filtering. Can be null
* @param msg
* @param error
*/
public static void report(String tag, Object msg, Level error) {
// Ignore?
Level minLevel = Environment.get().get(MIN_LEVEL);
if (minLevel != null && minLevel.intValue() > error.intValue())
return;
if (ignoredTags.contains(tag))
return;
String msgText = Printer.toString(msg);
// Guard against giant objects getting put into log, which is almost
// certainly a careless error
if (msgText.length() > MAX_LENGTH) {
msgText = msgText.substring(0, MAX_LENGTH / 2)
+ "... (message is too long for Log!)";
System.err.println(new IllegalArgumentException(
"Log message too long: " + msgText));
}
Report report = new Report(tag, msgText, error);
// We were getting ConcurrentModificationExceptions on startup for Small
// Town.
// This hack should avoid that.
// Commented out by JH -- can't see why this would make any difference.
// List<ILogListener> listeners2 = new
// ArrayList<ILogListener>(listeners);
for (ILogListener listener : listeners) {
try {
listener.listen(report);
} catch (Exception ex) {
// swallow if something goes wrong
ex.printStackTrace();
} catch (AssertionError ex) {
ex.printStackTrace();
}
}
}
public static void report(Throwable ex) {
report(Printer.toString(ex, true), Level.SEVERE);
}
public static void setContextMessage(String message) {
Environment.get().put(Log.ENV_CONTEXT_MESSAGE, message);
}
/**
* Set *default* minimum level to report events. Applies across all threads.
*
* @param level
*/
public static void setDefaultLevel(Level level) {
Environment.putDefault(MIN_LEVEL, level);
}
/**
*
* @param tag
* @param ignore
* true => ignore
*/
public static void setIgnoreTag(String tag, boolean ignore) {
if (ignore) {
ignoredTags.add(tag);
} else {
while (ignoredTags.contains(tag)) {
ignoredTags.remove(tag);
}
}
}
/**
* Minimum level to report events. Only sets for this thread! TODO should
* this be on a per-listener basis?
*
* @param level
*/
public static void setLevel(Level level) {
Environment.get().put(MIN_LEVEL, level);
}
/**
* For pain-level debugging.
* <p>
* This prints out (via .report()):<br>
* class.method(file:linenumber): objects<br>
* It does so in a format which can be copied-and-pasted into Eclipse's Java
* Stack Trace Console, where it will gain a link to the line of code.
*
* @param objects
* These will be printed out.
*/
public static void trace(Object... objects) {
StackTraceElement caller = Utils.getCaller();
Log.report(caller + ": " + Printer.toString(objects), Level.FINEST);
}
/**
* You cannot pass Throwable objects to this method
*
* @param string
*/
public static void warn(Object message) {
assert !(message instanceof Throwable);
report(message, Level.WARNING);
}
/**
* Does nothing. Provides an object if you need one - but all the methods
* are static.
*/
public Log() {
// does nothing
}
}