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 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 // ----------------------------------------------------------------------------