1/* Copyright (C) 2003 Vladimir Roubtsov. All rights reserved.
2 *
3 * This program and the accompanying materials are made available under
4 * the terms of the Common Public License v1.0 which accompanies this distribution,
5 * and is available at http://www.eclipse.org/legal/cpl-v10.html
6 *
7 * $Id: Logger.java,v 1.1.1.1.2.2 2004/07/16 23:32:29 vlad_r Exp $
8 */
9package com.vladium.logging;
10
11import java.io.PrintWriter;
12import java.io.StringWriter;
13import java.util.HashSet;
14import java.util.LinkedList;
15import java.util.NoSuchElementException;
16import java.util.Properties;
17import java.util.Set;
18import java.util.StringTokenizer;
19
20import com.vladium.emma.AppLoggers;
21import com.vladium.emma.IAppConstants;
22import com.vladium.util.ClassLoaderResolver;
23import com.vladium.util.Property;
24import com.vladium.util.Strings;
25
26// ----------------------------------------------------------------------------
27/**
28 * A simple Java version-independent logging framework. Each Logger is also
29 * an immutable context that encapsulates configuration elements like the
30 * logging verbosity level etc. In general, a Logger is looked up as an
31 * inheritable thread-local piece of data. This decouples classes and
32 * logging configurations in a way that seems difficult with log4j.<P>
33 *
34 * Note that a given class is free to cache its context in an instance field
35 * if the class is instantiated and used only on a single thread (or a set of
36 * threads that are guaranteed to share the same logging context). [This is
37 * different from the usual log4j pattern of caching a logger in a class static
38 * field]. In other cases (e.g., the instrumentation runtime), it makes more
39 * sense to scope a context to a single method invocation.<P>
40 *
41 * Every log message is structured as follows:
42 * <OL>
43 *  <LI> message is prefixed with the prefix string set in the Logger if that is
44 * not null;
45 *  <LI> if the calling class could be identified and it supplied the calling
46 * method name, the calling method is identified with all name components that
47 * are not null;
48 *  <LI> caller-supplied message is logged, if not null;
49 *  <LI> caller-supplied Throwable is dumped starting with a new line, if not null.
50 * </OL>
51 *
52 * MT-safety: a given Logger instance will not get corrupted by concurrent
53 * usage from multiple threads and guarantees that data written to the underlying
54 * PrintWriter in a single log call will be done in one atomic print() step.
55 *
56 * @see ILogLevels
57 *
58 * @author (C) 2001, Vlad Roubtsov
59 */
60public
61final class Logger implements ILogLevels
62{
63    // public: ................................................................
64
65    // TODO: update javadoc for 'logCaller'
66    // TODO: need isLoggable (Class)
67
68    public static Logger create (final int level, final PrintWriter out, final String prefix, final Set classMask)
69    {
70        if ((level < NONE) || (level > ALL))
71            throw new IllegalArgumentException ("invalid log level: " + level);
72
73        if ((out == null) || out.checkError ())
74            throw new IllegalArgumentException ("null or corrupt input: out");
75
76        return new Logger (level, out, prefix, classMask);
77    }
78
79    /**
80     * This works as a cloning creator of sorts.
81     *
82     * @param level
83     * @param out
84     * @param prefix
85     * @param classMask
86     * @param base
87     * @return
88     */
89    public static Logger create (final int level, final PrintWriter out, final String prefix, final Set classMask,
90                                 final Logger base)
91    {
92        if (base == null)
93        {
94            return create (level, out, prefix, classMask);
95        }
96        else
97        {
98            final int _level = level >= NONE
99                ? level
100                : base.m_level;
101
102            final PrintWriter _out = (out != null) && ! out.checkError ()
103                ? out
104                : base.m_out;
105
106            // TODO: do a better job of logger cloning
107            final String _prefix = prefix;
108//            final String _prefix = prefix != null
109//                ? prefix
110//                : base.m_prefix;
111
112            final Set _classMask = classMask != null
113                ? classMask
114                : base.m_classMask;
115
116
117            return new Logger (_level, _out, _prefix, _classMask);
118        }
119    }
120
121
122    /**
123     * A quick method to determine if logging is enabled at a given level.
124     * This method acquires no monitors and should be used when calling one of
125     * log() or convenience logging methods directly incurs significant
126     * parameter construction overhead.
127     *
128     * @see ILogLevels
129     */
130    public final boolean isLoggable (final int level)
131    {
132        return (level <= m_level);
133    }
134
135    /**
136     * A convenience method equivalent to isLoggable(INFO).
137     */
138    public final boolean atINFO ()
139    {
140        return (INFO <= m_level);
141    }
142
143    /**
144     * A convenience method equivalent to isLoggable(VERBOSE).
145     */
146    public final boolean atVERBOSE ()
147    {
148        return (VERBOSE <= m_level);
149    }
150
151    /**
152     * A convenience method equivalent to isLoggable(TRACE1).
153     */
154    public final boolean atTRACE1 ()
155    {
156        return (TRACE1 <= m_level);
157    }
158
159    /**
160     * A convenience method equivalent to isLoggable(TRACE2).
161     */
162    public final boolean atTRACE2 ()
163    {
164        return (TRACE2 <= m_level);
165    }
166
167    /**
168     * A convenience method equivalent to isLoggable(TRACE3).
169     */
170    public final boolean atTRACE3 ()
171    {
172        return (TRACE3 <= m_level);
173    }
174
175
176    /**
177     * A convenience method to log 'msg' from an anonymous calling method
178     * at WARNING level.
179     *
180     * @param msg log message [ignored if null]
181     */
182    public final void warning (final String msg)
183    {
184        _log (WARNING, null, msg, false);
185    }
186
187    /**
188     * A convenience method to log 'msg' from an anonymous calling method
189     * at INFO level.
190     *
191     * @param msg log message [ignored if null]
192     */
193    public final void info (final String msg)
194    {
195        _log (INFO, null, msg, false);
196    }
197
198    /**
199     * A convenience method to log 'msg' from an anonymous calling method
200     * at VERBOSE level.
201     *
202     * @param msg log message [ignored if null]
203     */
204    public final void verbose (final String msg)
205    {
206        _log (VERBOSE, null, msg, false);
207    }
208
209
210    /**
211     * A convenience method equivalent to log(TRACE1, method, msg).
212     *
213     * @param method calling method name [ignored if null]
214     * @param msg log message [ignored if null]
215     */
216    public final void trace1 (final String method, final String msg)
217    {
218        _log (TRACE1, method, msg, true);
219    }
220
221    /**
222     * A convenience method equivalent to log(TRACE2, method, msg).
223     *
224     * @param method calling method name [ignored if null]
225     * @param msg log message [ignored if null]
226     */
227    public final void trace2 (final String method, final String msg)
228    {
229        _log (TRACE2, method, msg, true);
230    }
231
232    /**
233     * A convenience method equivalent to log(TRACE3, method, msg).
234     *
235     * @param method calling method name [ignored if null]
236     * @param msg log message [ignored if null]
237     */
238    public final void trace3 (final String method, final String msg)
239    {
240        _log (TRACE3, method, msg, true);
241    }
242
243    /**
244     * Logs 'msg' from an unnamed calling method.
245     *
246     * @param level level to log at [the method does nothing if this is less
247     * than the set level].
248     * @param msg log message [ignored if null]
249     */
250    public final void log (final int level, final String msg, final boolean logCaller)
251    {
252        _log (level, null, msg, logCaller);
253    }
254
255    /**
256     * Logs 'msg' from a given calling method.
257     *
258     * @param level level to log at [the method does nothing if this is less
259     * than the set level].
260     * @param method calling method name [ignored if null]
261     * @param msg log message [ignored if null]
262     */
263    public final void log (final int level, final String method, final String msg, final boolean logCaller)
264    {
265        _log (level, method, msg, logCaller);
266    }
267
268    /**
269     * Logs 'msg' from an unnamed calling method followed by the 'throwable' stack
270     * trace dump.
271     *
272     * @param level level to log at [the method does nothing if this is less
273     * than the set level].
274     * @param msg log message [ignored if null]
275     * @param throwable to dump after message [ignored if null]
276     */
277    public final void log (final int level, final String msg, final Throwable throwable)
278    {
279        _log (level, null, msg, throwable);
280    }
281
282    /**
283     * Logs 'msg' from a given calling method followed by the 'throwable' stack
284     * trace dump.
285     *
286     * @param level level to log at [the method does nothing if this is less
287     * than the set level].
288     * @param method calling method name [ignored if null]
289     * @param msg log message [ignored if null]
290     * @param throwable to dump after message [ignored if null]
291     */
292    public final void log (final int level, final String method, final String msg, final Throwable throwable)
293    {
294        _log (level, method, msg, throwable);
295    }
296
297
298    /**
299     * Provides direct access to the PrintWriter used by this Logger.
300     *
301     * @return print writer used by this logger [never null]
302     */
303    public PrintWriter getWriter ()
304    {
305        return m_out;
306    }
307
308
309    /**
310     * Returns the current top of the thread-local logger stack or the static
311     * Logger instance scoped to Logger.class if the stack is empty.
312     *
313     * @return current logger [never null]
314     */
315    public static Logger getLogger ()
316    {
317        final LinkedList stack = (LinkedList) THREAD_LOCAL_STACK.get ();
318
319        // [assertion: stack != null]
320
321        if (stack.isEmpty ())
322        {
323            return STATIC_LOGGER;
324        }
325        else
326        {
327            return (Logger) stack.getLast ();
328        }
329    }
330
331    /**
332     *
333     * @param ctx [may not be null]
334     */
335    public static void push (final Logger ctx)
336    {
337        if (ctx == null)
338            throw new IllegalArgumentException ("null input: ctx");
339
340        final LinkedList stack = (LinkedList) THREAD_LOCAL_STACK.get ();
341        stack.addLast (ctx);
342    }
343
344    /**
345     * Requiring a context parameter here helps enforce correct push/pop
346     * nesting in the caller code.
347     *
348     * @param ctx [may not be null]
349     */
350    public static void pop (final Logger ctx)
351    {
352        // TODO: add guards for making sure only the pushing thread is allowed to
353        // execute this
354
355        final LinkedList stack = (LinkedList) THREAD_LOCAL_STACK.get ();
356
357        try
358        {
359            final Logger current = (Logger) stack.getLast ();
360            if (current != ctx)
361                throw new IllegalStateException ("invalid context being popped: " + ctx);
362
363            stack.removeLast ();
364            current.cleanup ();
365        }
366        catch (NoSuchElementException nsee)
367        {
368            throw new IllegalStateException ("empty logger context stack on thread [" + Thread.currentThread () + "]: " + nsee);
369        }
370    }
371
372
373    public static int stringToLevel (final String level)
374    {
375        if (ILogLevels.SEVERE_STRING.equalsIgnoreCase (level) || ILogLevels.SILENT_STRING.equalsIgnoreCase (level))
376            return ILogLevels.SEVERE;
377        else if (ILogLevels.WARNING_STRING.equalsIgnoreCase (level) || ILogLevels.QUIET_STRING.equalsIgnoreCase (level))
378            return ILogLevels.WARNING;
379        else if (ILogLevels.INFO_STRING.equalsIgnoreCase (level))
380            return ILogLevels.INFO;
381        else if (ILogLevels.VERBOSE_STRING.equalsIgnoreCase (level))
382            return ILogLevels.VERBOSE;
383        else if (ILogLevels.TRACE1_STRING.equalsIgnoreCase (level))
384            return ILogLevels.TRACE1;
385        else if (ILogLevels.TRACE2_STRING.equalsIgnoreCase (level))
386            return ILogLevels.TRACE2;
387        else if (ILogLevels.TRACE3_STRING.equalsIgnoreCase (level))
388            return ILogLevels.TRACE3;
389        else if (ILogLevels.NONE_STRING.equalsIgnoreCase (level))
390            return ILogLevels.NONE;
391        else if (ILogLevels.ALL_STRING.equalsIgnoreCase (level))
392            return ILogLevels.ALL;
393        else
394        {
395            int _level = Integer.MIN_VALUE;
396            try
397            {
398                _level = Integer.parseInt (level);
399            }
400            catch (Exception ignore) {}
401
402            if ((_level >= ILogLevels.NONE) && (_level <= ILogLevels.ALL))
403                return _level;
404            else
405                return ILogLevels.INFO; // default to something middle of the ground
406        }
407    }
408
409    // protected: .............................................................
410
411    // package: ...............................................................
412
413    // private: ...............................................................
414
415
416    private static final class ThreadLocalStack extends InheritableThreadLocal
417    {
418        protected Object initialValue ()
419        {
420            return new LinkedList ();
421        }
422
423    } // end of nested class
424
425
426    private Logger (final int level, final PrintWriter out, final String prefix, final Set classMask)
427    {
428        m_level = level;
429        m_out = out;
430        m_prefix = prefix;
431        m_classMask = classMask; // no defensive clone
432    }
433
434    private void cleanup ()
435    {
436        m_out.flush ();
437    }
438
439    private void _log (final int level, final String method,
440                       final String msg, final boolean logCaller)
441    {
442        if ((level <= m_level) && (level >= SEVERE))
443        {
444            final Class caller = logCaller ? ClassLoaderResolver.getCallerClass (2) : null;
445            final StringBuffer buf = new StringBuffer (m_prefix != null ? m_prefix + ": " : "");
446
447            if ((caller != null) || (method != null))
448            {
449                buf.append ("[");
450
451                if (caller != null) // if the caller could not be determined, s_classMask is ignored
452                {
453                    String callerName = caller.getName ();
454
455                    if (callerName.startsWith (PREFIX_TO_STRIP))
456                        callerName = callerName.substring (PREFIX_TO_STRIP_LENGTH);
457
458                    String parentName = callerName;
459
460                    final int firstDollar = callerName.indexOf ('$');
461                    if (firstDollar > 0) parentName = callerName.substring (0, firstDollar);
462
463                    if ((m_classMask == null) || m_classMask.contains (parentName))
464                        buf.append (callerName);
465                    else
466                        return;
467                }
468
469                if (method != null)
470                {
471                    buf.append ("::");
472                    buf.append (method);
473                }
474
475                buf.append ("] ");
476            }
477
478            final PrintWriter out = m_out;
479
480            if (msg != null) buf.append (msg);
481
482            out.println (buf);
483            if (FLUSH_LOG) out.flush ();
484        }
485    }
486
487    private void _log (final int level, final String method,
488                       final String msg, final Throwable throwable)
489    {
490        if ((level <= m_level) && (level >= SEVERE))
491        {
492            final Class caller = ClassLoaderResolver.getCallerClass (2);
493            final StringBuffer buf = new StringBuffer (m_prefix != null ? m_prefix + ": " : "");
494
495            if ((caller != null) || (method != null))
496            {
497                buf.append ("[");
498
499                if (caller != null) // if the caller could not be determined, s_classMask is ignored
500                {
501                    String callerName = caller.getName ();
502
503                    if (callerName.startsWith (PREFIX_TO_STRIP))
504                        callerName = callerName.substring (PREFIX_TO_STRIP_LENGTH);
505
506                    String parentName = callerName;
507
508                    final int firstDollar = callerName.indexOf ('$');
509                    if (firstDollar > 0) parentName = callerName.substring (0, firstDollar);
510
511                    if ((m_classMask == null) || m_classMask.contains (parentName))
512                        buf.append (callerName);
513                    else
514                        return;
515                }
516
517                if (method != null)
518                {
519                    buf.append ("::");
520                    buf.append (method);
521                }
522
523                buf.append ("] ");
524            }
525
526            final PrintWriter out = m_out;
527
528            if (msg != null) buf.append (msg);
529
530            if (throwable != null)
531            {
532                final StringWriter sw = new StringWriter ();
533                final PrintWriter pw = new PrintWriter (sw);
534
535                throwable.printStackTrace (pw);
536                pw.flush ();
537
538                buf.append (sw.toString ());
539            }
540
541            out.println (buf);
542            if (FLUSH_LOG) out.flush ();
543        }
544    }
545
546
547
548    private final int m_level; // always in [NONE, ALL] range
549    private final PrintWriter m_out; // never null
550    private final String m_prefix; // null is equivalent to no prefix
551    private final Set /* String */ m_classMask; // null is equivalent to no class filtering
552
553    private static final String PREFIX_TO_STRIP = "com.vladium."; // TODO: can this be set programmatically ?
554    private static final int PREFIX_TO_STRIP_LENGTH = PREFIX_TO_STRIP.length ();
555    private static final boolean FLUSH_LOG = true;
556    private static final String COMMA_DELIMITERS    = "," + Strings.WHITE_SPACE;
557
558    private static final Logger STATIC_LOGGER; // set in <clinit>
559    private static final ThreadLocalStack THREAD_LOCAL_STACK; // set in <clinit>
560
561    static
562    {
563        THREAD_LOCAL_STACK = new ThreadLocalStack ();
564
565        // TODO: unfortunately, this init code makes Logger coupled to the app classes
566        // (via the app namespace string constants)
567        // I don't quite see an elegant solution to this design problem yet
568
569        final Properties properties = Property.getAppProperties (IAppConstants.APP_NAME_LC, Logger.class.getClassLoader ());
570
571        // verbosity level:
572
573        final int level;
574        {
575            final String _level = properties.getProperty (AppLoggers.PROPERTY_VERBOSITY_LEVEL,
576                                                          AppLoggers.DEFAULT_VERBOSITY_LEVEL);
577            level = stringToLevel (_level);
578        }
579
580        // verbosity filter:
581
582        final Set filter;
583        {
584            final String _filter = properties.getProperty (AppLoggers.PROPERTY_VERBOSITY_FILTER);
585            Set temp = null;
586
587            if (_filter != null)
588            {
589                final StringTokenizer tokenizer = new StringTokenizer (_filter, COMMA_DELIMITERS);
590                if (tokenizer.countTokens () > 0)
591                {
592                    temp = new HashSet (tokenizer.countTokens ());
593                    while (tokenizer.hasMoreTokens ())
594                    {
595                        temp.add (tokenizer.nextToken ());
596                    }
597                }
598            }
599
600            filter = temp;
601        }
602
603
604        STATIC_LOGGER = create (level,
605                                new PrintWriter (System.out, false),
606                                IAppConstants.APP_NAME,
607                                filter);
608    }
609
610} // end of class
611// ----------------------------------------------------------------------------