/* Copyright (C) 2003 Vladimir Roubtsov. All rights reserved. * * This program and the accompanying materials are made available under * the terms of the Common Public License v1.0 which accompanies this distribution, * and is available at http://www.eclipse.org/legal/cpl-v10.html * * $Id: Logger.java,v 1.1.1.1.2.2 2004/07/16 23:32:29 vlad_r Exp $ */ package com.vladium.logging; import java.io.PrintWriter; import java.io.StringWriter; import java.util.HashSet; import java.util.LinkedList; import java.util.NoSuchElementException; import java.util.Properties; import java.util.Set; import java.util.StringTokenizer; import com.vladium.emma.AppLoggers; import com.vladium.emma.IAppConstants; import com.vladium.util.ClassLoaderResolver; import com.vladium.util.Property; import com.vladium.util.Strings; // ---------------------------------------------------------------------------- /** * A simple Java version-independent logging framework. Each Logger is also * an immutable context that encapsulates configuration elements like the * logging verbosity level etc. In general, a Logger is looked up as an * inheritable thread-local piece of data. This decouples classes and * logging configurations in a way that seems difficult with log4j.<P> * * Note that a given class is free to cache its context in an instance field * if the class is instantiated and used only on a single thread (or a set of * threads that are guaranteed to share the same logging context). [This is * different from the usual log4j pattern of caching a logger in a class static * field]. In other cases (e.g., the instrumentation runtime), it makes more * sense to scope a context to a single method invocation.<P> * * Every log message is structured as follows: * <OL> * <LI> message is prefixed with the prefix string set in the Logger if that is * not null; * <LI> if the calling class could be identified and it supplied the calling * method name, the calling method is identified with all name components that * are not null; * <LI> caller-supplied message is logged, if not null; * <LI> caller-supplied Throwable is dumped starting with a new line, if not null. * </OL> * * MT-safety: a given Logger instance will not get corrupted by concurrent * usage from multiple threads and guarantees that data written to the underlying * PrintWriter in a single log call will be done in one atomic print() step. * * @see ILogLevels * * @author (C) 2001, Vlad Roubtsov */ public final class Logger implements ILogLevels { // public: ................................................................ // TODO: update javadoc for 'logCaller' // TODO: need isLoggable (Class) public static Logger create (final int level, final PrintWriter out, final String prefix, final Set classMask) { if ((level < NONE) || (level > ALL)) throw new IllegalArgumentException ("invalid log level: " + level); if ((out == null) || out.checkError ()) throw new IllegalArgumentException ("null or corrupt input: out"); return new Logger (level, out, prefix, classMask); } /** * This works as a cloning creator of sorts. * * @param level * @param out * @param prefix * @param classMask * @param base * @return */ public static Logger create (final int level, final PrintWriter out, final String prefix, final Set classMask, final Logger base) { if (base == null) { return create (level, out, prefix, classMask); } else { final int _level = level >= NONE ? level : base.m_level; final PrintWriter _out = (out != null) && ! out.checkError () ? out : base.m_out; // TODO: do a better job of logger cloning final String _prefix = prefix; // final String _prefix = prefix != null // ? prefix // : base.m_prefix; final Set _classMask = classMask != null ? classMask : base.m_classMask; return new Logger (_level, _out, _prefix, _classMask); } } /** * A quick method to determine if logging is enabled at a given level. * This method acquires no monitors and should be used when calling one of * log() or convenience logging methods directly incurs significant * parameter construction overhead. * * @see ILogLevels */ public final boolean isLoggable (final int level) { return (level <= m_level); } /** * A convenience method equivalent to isLoggable(INFO). */ public final boolean atINFO () { return (INFO <= m_level); } /** * A convenience method equivalent to isLoggable(VERBOSE). */ public final boolean atVERBOSE () { return (VERBOSE <= m_level); } /** * A convenience method equivalent to isLoggable(TRACE1). */ public final boolean atTRACE1 () { return (TRACE1 <= m_level); } /** * A convenience method equivalent to isLoggable(TRACE2). */ public final boolean atTRACE2 () { return (TRACE2 <= m_level); } /** * A convenience method equivalent to isLoggable(TRACE3). */ public final boolean atTRACE3 () { return (TRACE3 <= m_level); } /** * A convenience method to log 'msg' from an anonymous calling method * at WARNING level. * * @param msg log message [ignored if null] */ public final void warning (final String msg) { _log (WARNING, null, msg, false); } /** * A convenience method to log 'msg' from an anonymous calling method * at INFO level. * * @param msg log message [ignored if null] */ public final void info (final String msg) { _log (INFO, null, msg, false); } /** * A convenience method to log 'msg' from an anonymous calling method * at VERBOSE level. * * @param msg log message [ignored if null] */ public final void verbose (final String msg) { _log (VERBOSE, null, msg, false); } /** * A convenience method equivalent to log(TRACE1, method, msg). * * @param method calling method name [ignored if null] * @param msg log message [ignored if null] */ public final void trace1 (final String method, final String msg) { _log (TRACE1, method, msg, true); } /** * A convenience method equivalent to log(TRACE2, method, msg). * * @param method calling method name [ignored if null] * @param msg log message [ignored if null] */ public final void trace2 (final String method, final String msg) { _log (TRACE2, method, msg, true); } /** * A convenience method equivalent to log(TRACE3, method, msg). * * @param method calling method name [ignored if null] * @param msg log message [ignored if null] */ public final void trace3 (final String method, final String msg) { _log (TRACE3, method, msg, true); } /** * Logs 'msg' from an unnamed calling method. * * @param level level to log at [the method does nothing if this is less * than the set level]. * @param msg log message [ignored if null] */ public final void log (final int level, final String msg, final boolean logCaller) { _log (level, null, msg, logCaller); } /** * Logs 'msg' from a given calling method. * * @param level level to log at [the method does nothing if this is less * than the set level]. * @param method calling method name [ignored if null] * @param msg log message [ignored if null] */ public final void log (final int level, final String method, final String msg, final boolean logCaller) { _log (level, method, msg, logCaller); } /** * Logs 'msg' from an unnamed calling method followed by the 'throwable' stack * trace dump. * * @param level level to log at [the method does nothing if this is less * than the set level]. * @param msg log message [ignored if null] * @param throwable to dump after message [ignored if null] */ public final void log (final int level, final String msg, final Throwable throwable) { _log (level, null, msg, throwable); } /** * Logs 'msg' from a given calling method followed by the 'throwable' stack * trace dump. * * @param level level to log at [the method does nothing if this is less * than the set level]. * @param method calling method name [ignored if null] * @param msg log message [ignored if null] * @param throwable to dump after message [ignored if null] */ public final void log (final int level, final String method, final String msg, final Throwable throwable) { _log (level, method, msg, throwable); } /** * Provides direct access to the PrintWriter used by this Logger. * * @return print writer used by this logger [never null] */ public PrintWriter getWriter () { return m_out; } /** * Returns the current top of the thread-local logger stack or the static * Logger instance scoped to Logger.class if the stack is empty. * * @return current logger [never null] */ public static Logger getLogger () { final LinkedList stack = (LinkedList) THREAD_LOCAL_STACK.get (); // [assertion: stack != null] if (stack.isEmpty ()) { return STATIC_LOGGER; } else { return (Logger) stack.getLast (); } } /** * * @param ctx [may not be null] */ public static void push (final Logger ctx) { if (ctx == null) throw new IllegalArgumentException ("null input: ctx"); final LinkedList stack = (LinkedList) THREAD_LOCAL_STACK.get (); stack.addLast (ctx); } /** * Requiring a context parameter here helps enforce correct push/pop * nesting in the caller code. * * @param ctx [may not be null] */ public static void pop (final Logger ctx) { // TODO: add guards for making sure only the pushing thread is allowed to // execute this final LinkedList stack = (LinkedList) THREAD_LOCAL_STACK.get (); try { final Logger current = (Logger) stack.getLast (); if (current != ctx) throw new IllegalStateException ("invalid context being popped: " + ctx); stack.removeLast (); current.cleanup (); } catch (NoSuchElementException nsee) { throw new IllegalStateException ("empty logger context stack on thread [" + Thread.currentThread () + "]: " + nsee); } } public static int stringToLevel (final String level) { if (ILogLevels.SEVERE_STRING.equalsIgnoreCase (level) || ILogLevels.SILENT_STRING.equalsIgnoreCase (level)) return ILogLevels.SEVERE; else if (ILogLevels.WARNING_STRING.equalsIgnoreCase (level) || ILogLevels.QUIET_STRING.equalsIgnoreCase (level)) return ILogLevels.WARNING; else if (ILogLevels.INFO_STRING.equalsIgnoreCase (level)) return ILogLevels.INFO; else if (ILogLevels.VERBOSE_STRING.equalsIgnoreCase (level)) return ILogLevels.VERBOSE; else if (ILogLevels.TRACE1_STRING.equalsIgnoreCase (level)) return ILogLevels.TRACE1; else if (ILogLevels.TRACE2_STRING.equalsIgnoreCase (level)) return ILogLevels.TRACE2; else if (ILogLevels.TRACE3_STRING.equalsIgnoreCase (level)) return ILogLevels.TRACE3; else if (ILogLevels.NONE_STRING.equalsIgnoreCase (level)) return ILogLevels.NONE; else if (ILogLevels.ALL_STRING.equalsIgnoreCase (level)) return ILogLevels.ALL; else { int _level = Integer.MIN_VALUE; try { _level = Integer.parseInt (level); } catch (Exception ignore) {} if ((_level >= ILogLevels.NONE) && (_level <= ILogLevels.ALL)) return _level; else return ILogLevels.INFO; // default to something middle of the ground } } // protected: ............................................................. // package: ............................................................... // private: ............................................................... private static final class ThreadLocalStack extends InheritableThreadLocal { protected Object initialValue () { return new LinkedList (); } } // end of nested class private Logger (final int level, final PrintWriter out, final String prefix, final Set classMask) { m_level = level; m_out = out; m_prefix = prefix; m_classMask = classMask; // no defensive clone } private void cleanup () { m_out.flush (); } private void _log (final int level, final String method, final String msg, final boolean logCaller) { if ((level <= m_level) && (level >= SEVERE)) { final Class caller = logCaller ? ClassLoaderResolver.getCallerClass (2) : null; final StringBuffer buf = new StringBuffer (m_prefix != null ? m_prefix + ": " : ""); if ((caller != null) || (method != null)) { buf.append ("["); if (caller != null) // if the caller could not be determined, s_classMask is ignored { String callerName = caller.getName (); if (callerName.startsWith (PREFIX_TO_STRIP)) callerName = callerName.substring (PREFIX_TO_STRIP_LENGTH); String parentName = callerName; final int firstDollar = callerName.indexOf ('$'); if (firstDollar > 0) parentName = callerName.substring (0, firstDollar); if ((m_classMask == null) || m_classMask.contains (parentName)) buf.append (callerName); else return; } if (method != null) { buf.append ("::"); buf.append (method); } buf.append ("] "); } final PrintWriter out = m_out; if (msg != null) buf.append (msg); out.println (buf); if (FLUSH_LOG) out.flush (); } } private void _log (final int level, final String method, final String msg, final Throwable throwable) { if ((level <= m_level) && (level >= SEVERE)) { final Class caller = ClassLoaderResolver.getCallerClass (2); final StringBuffer buf = new StringBuffer (m_prefix != null ? m_prefix + ": " : ""); if ((caller != null) || (method != null)) { buf.append ("["); if (caller != null) // if the caller could not be determined, s_classMask is ignored { String callerName = caller.getName (); if (callerName.startsWith (PREFIX_TO_STRIP)) callerName = callerName.substring (PREFIX_TO_STRIP_LENGTH); String parentName = callerName; final int firstDollar = callerName.indexOf ('$'); if (firstDollar > 0) parentName = callerName.substring (0, firstDollar); if ((m_classMask == null) || m_classMask.contains (parentName)) buf.append (callerName); else return; } if (method != null) { buf.append ("::"); buf.append (method); } buf.append ("] "); } final PrintWriter out = m_out; if (msg != null) buf.append (msg); if (throwable != null) { final StringWriter sw = new StringWriter (); final PrintWriter pw = new PrintWriter (sw); throwable.printStackTrace (pw); pw.flush (); buf.append (sw.toString ()); } out.println (buf); if (FLUSH_LOG) out.flush (); } } private final int m_level; // always in [NONE, ALL] range private final PrintWriter m_out; // never null private final String m_prefix; // null is equivalent to no prefix private final Set /* String */ m_classMask; // null is equivalent to no class filtering private static final String PREFIX_TO_STRIP = "com.vladium."; // TODO: can this be set programmatically ? private static final int PREFIX_TO_STRIP_LENGTH = PREFIX_TO_STRIP.length (); private static final boolean FLUSH_LOG = true; private static final String COMMA_DELIMITERS = "," + Strings.WHITE_SPACE; private static final Logger STATIC_LOGGER; // set in <clinit> private static final ThreadLocalStack THREAD_LOCAL_STACK; // set in <clinit> static { THREAD_LOCAL_STACK = new ThreadLocalStack (); // TODO: unfortunately, this init code makes Logger coupled to the app classes // (via the app namespace string constants) // I don't quite see an elegant solution to this design problem yet final Properties properties = Property.getAppProperties (IAppConstants.APP_NAME_LC, Logger.class.getClassLoader ()); // verbosity level: final int level; { final String _level = properties.getProperty (AppLoggers.PROPERTY_VERBOSITY_LEVEL, AppLoggers.DEFAULT_VERBOSITY_LEVEL); level = stringToLevel (_level); } // verbosity filter: final Set filter; { final String _filter = properties.getProperty (AppLoggers.PROPERTY_VERBOSITY_FILTER); Set temp = null; if (_filter != null) { final StringTokenizer tokenizer = new StringTokenizer (_filter, COMMA_DELIMITERS); if (tokenizer.countTokens () > 0) { temp = new HashSet (tokenizer.countTokens ()); while (tokenizer.hasMoreTokens ()) { temp.add (tokenizer.nextToken ()); } } } filter = temp; } STATIC_LOGGER = create (level, new PrintWriter (System.out, false), IAppConstants.APP_NAME, filter); } } // end of class // ----------------------------------------------------------------------------