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  */
9 package com.vladium.logging;
10 
11 import java.io.PrintWriter;
12 import java.io.StringWriter;
13 import java.util.HashSet;
14 import java.util.LinkedList;
15 import java.util.NoSuchElementException;
16 import java.util.Properties;
17 import java.util.Set;
18 import java.util.StringTokenizer;
19 
20 import com.vladium.emma.AppLoggers;
21 import com.vladium.emma.IAppConstants;
22 import com.vladium.util.ClassLoaderResolver;
23 import com.vladium.util.Property;
24 import 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  */
60 public
61 final class Logger implements ILogLevels
62 {
63     // public: ................................................................
64 
65     // TODO: update javadoc for 'logCaller'
66     // TODO: need isLoggable (Class)
67 
create(final int level, final PrintWriter out, final String prefix, final Set classMask)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      */
create(final int level, final PrintWriter out, final String prefix, final Set classMask, final Logger base)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      */
isLoggable(final int level)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      */
atINFO()138     public final boolean atINFO ()
139     {
140         return (INFO <= m_level);
141     }
142 
143     /**
144      * A convenience method equivalent to isLoggable(VERBOSE).
145      */
atVERBOSE()146     public final boolean atVERBOSE ()
147     {
148         return (VERBOSE <= m_level);
149     }
150 
151     /**
152      * A convenience method equivalent to isLoggable(TRACE1).
153      */
atTRACE1()154     public final boolean atTRACE1 ()
155     {
156         return (TRACE1 <= m_level);
157     }
158 
159     /**
160      * A convenience method equivalent to isLoggable(TRACE2).
161      */
atTRACE2()162     public final boolean atTRACE2 ()
163     {
164         return (TRACE2 <= m_level);
165     }
166 
167     /**
168      * A convenience method equivalent to isLoggable(TRACE3).
169      */
atTRACE3()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      */
warning(final String msg)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      */
info(final String msg)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      */
verbose(final String msg)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      */
trace1(final String method, final String msg)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      */
trace2(final String method, final String msg)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      */
trace3(final String method, final String msg)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      */
log(final int level, final String msg, final boolean logCaller)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      */
log(final int level, final String method, final String msg, final boolean logCaller)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      */
log(final int level, final String msg, final Throwable throwable)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      */
log(final int level, final String method, final String msg, final Throwable throwable)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      */
getWriter()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      */
getLogger()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      */
push(final Logger ctx)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      */
pop(final Logger ctx)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 
stringToLevel(final String level)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     {
initialValue()418         protected Object initialValue ()
419         {
420             return new LinkedList ();
421         }
422 
423     } // end of nested class
424 
425 
Logger(final int level, final PrintWriter out, final String prefix, final Set classMask)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 
cleanup()434     private void cleanup ()
435     {
436         m_out.flush ();
437     }
438 
_log(final int level, final String method, final String msg, final boolean logCaller)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 
_log(final int level, final String method, final String msg, final Throwable throwable)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                     {
tokenizer.nextToken()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 // ----------------------------------------------------------------------------