001package com.identityworksllc.iiq.common.logging; 002 003import bsh.This; 004import org.apache.commons.collections.map.ListOrderedMap; 005import org.apache.commons.logging.Log; 006import org.apache.commons.logging.LogFactory; 007import org.w3c.dom.Document; 008import sailpoint.api.SailPointContext; 009import sailpoint.object.Application; 010import sailpoint.object.Bundle; 011import sailpoint.object.Filter; 012import sailpoint.object.Identity; 013import sailpoint.object.Link; 014import sailpoint.object.ManagedAttribute; 015import sailpoint.object.ProvisioningPlan; 016import sailpoint.object.SailPointObject; 017import sailpoint.tools.GeneralException; 018import sailpoint.tools.xml.AbstractXmlObject; 019 020import javax.xml.transform.OutputKeys; 021import javax.xml.transform.Transformer; 022import javax.xml.transform.TransformerFactory; 023import javax.xml.transform.dom.DOMSource; 024import javax.xml.transform.stream.StreamResult; 025import java.io.ByteArrayOutputStream; 026import java.io.IOException; 027import java.io.PrintStream; 028import java.io.PrintWriter; 029import java.io.StringWriter; 030import java.lang.reflect.Array; 031import java.text.DateFormat; 032import java.text.MessageFormat; 033import java.text.SimpleDateFormat; 034import java.util.Calendar; 035import java.util.Collection; 036import java.util.Date; 037import java.util.Map; 038import java.util.TreeMap; 039import java.util.function.Supplier; 040 041/** 042 * A wrapper around the Commons Logging {@link Log} class that supplements its 043 * features with some extras available in other logging libraries. Since this 044 * class itself implements {@link Log}, Beanshell should not care if you simply 045 * overwrite the 'log' variable in your code. 046 * 047 * Log strings are interpreted as Java {@link MessageFormat} objects and have 048 * the various features of that class in your JDK version. 049 * 050 * Values passed as arguments are only evaluated when the appropriate log 051 * level is active. If the log level is not active, the operation becomes a 052 * quick no-op. This prevents a lot of isDebugEnabled() type checks. 053 * 054 * The 'S' stands for Super. Super Logger. 055 */ 056public class SLogger implements org.apache.commons.logging.Log { 057 058 /** 059 * Helper class to format an object for logging. The format is only derived 060 * when the {@link #toString()} is called, meaning that if you log one of these 061 * and the log level is not enabled, a slow string conversion will never occur. 062 * 063 * Null values are transformed into the special string '(null)'. 064 * 065 * Formatted values are cached after the first format operation, even if the 066 * underlying object is modified. 067 * 068 * The following types are handled by the Formatter: 069 * 070 * - null 071 * - Strings 072 * - Arrays of Objects 073 * - Arrays of StackTraceElements 074 * - Collections of Objects 075 * - Maps 076 * - Dates and Calendars 077 * - XML {@link Document}s 078 * - Various SailPointObjects 079 * 080 * Nested objects are also passed through a Formatter. 081 */ 082 public static class Formatter implements Supplier<String> { 083 084 /** 085 * The cached formatted value 086 */ 087 private String formattedValue; 088 /** 089 * The object to format. 090 */ 091 private final Object item; 092 093 /** 094 * Creates a new formatter. 095 * 096 * @param Item The item to format. 097 */ 098 public Formatter(Object Item) { 099 this.item = Item; 100 this.formattedValue = null; 101 } 102 103 @SuppressWarnings("unchecked") 104 private Map<String, Object> createLogMap(SailPointObject value) { 105 Map<String, Object> map = new ListOrderedMap(); 106 map.put("class", value.getClass().getName()); 107 map.put("id", value.getId()); 108 map.put("name", value.getName()); 109 return map; 110 } 111 112 /** 113 * Formats an object. 114 * 115 * @param valueToFormat The object to format. 116 * @return The formatted version of that object. 117 */ 118 private String format(Object valueToFormat) { 119 return format(valueToFormat, false); 120 } 121 122 /** 123 * Formats an object according to multiple type-specific format rules. 124 * 125 * @param valueToFormat The object to format. 126 * @return The formatted version of that object. 127 */ 128 private String format(Object valueToFormat, boolean indent) { 129 StringBuilder value = new StringBuilder(); 130 131 if (valueToFormat == null) { 132 value.append("(null)"); 133 } else if (valueToFormat instanceof String) { 134 value.append(valueToFormat); 135 } else if (valueToFormat instanceof bsh.This) { 136 String namespaceName = "???"; 137 if (((This) valueToFormat).getNameSpace() != null) { 138 namespaceName = ((This) valueToFormat).getNameSpace().getName(); 139 } 140 value.append("bsh.This[namespace=").append(namespaceName).append("]"); 141 } else if (valueToFormat instanceof StackTraceElement[]) { 142 for (StackTraceElement element : (StackTraceElement[]) valueToFormat) { 143 value.append("\n at "); 144 value.append(element); 145 } 146 } else if (valueToFormat instanceof Throwable) { 147 Throwable t = (Throwable)valueToFormat; 148 try (StringWriter target = new StringWriter()) { 149 try (PrintWriter printWriter = new PrintWriter(target)) { 150 t.printStackTrace(printWriter); 151 } 152 value.append(target); 153 } catch(IOException e) { 154 return "Exception printing object of type Throwable: " + e; 155 } 156 } else if (valueToFormat.getClass().isArray()) { 157 value.append("[\n"); 158 boolean first = true; 159 int length = Array.getLength(valueToFormat); 160 for (int i = 0; i < length; i++) { 161 if (!first) { 162 value.append(",\n"); 163 } 164 value.append(" "); 165 value.append(format(Array.get(valueToFormat, i), true)); 166 first = false; 167 } 168 value.append("\n]"); 169 } else if (valueToFormat instanceof Collection) { 170 value.append("[\n"); 171 boolean first = true; 172 for (Object arg : (Collection<?>) valueToFormat) { 173 if (!first) { 174 value.append(",\n"); 175 } 176 value.append(" ").append(format(arg, true)); 177 first = false; 178 } 179 value.append("\n]"); 180 } else if (valueToFormat instanceof Map) { 181 value.append("{\n"); 182 boolean first = true; 183 for (Map.Entry<?, ?> entry : new TreeMap<Object, Object>((Map<?, ?>) valueToFormat).entrySet()) { 184 if (!first) { 185 value.append(",\n"); 186 } 187 value.append(" "); 188 value.append(format(entry.getKey())); 189 value.append("="); 190 value.append(format(entry.getValue(), true)); 191 first = false; 192 } 193 value.append("\n}"); 194 } else if (valueToFormat instanceof Date) { 195 DateFormat format = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss z"); 196 value.append(format.format((Date) valueToFormat)); 197 } else if (valueToFormat instanceof Calendar) { 198 value.append(format(((Calendar) valueToFormat).getTime())); 199 } else if (valueToFormat instanceof Document) { 200 try { 201 Transformer transformer = TransformerFactory.newInstance().newTransformer(); 202 transformer.setOutputProperty(OutputKeys.INDENT, "yes"); 203 transformer.setOutputProperty("{http://xml.apache.org/xslt}indent-amount", "2"); 204 try (ByteArrayOutputStream output = new ByteArrayOutputStream()) { 205 transformer.transform(new DOMSource((Document) valueToFormat), new StreamResult(output)); 206 value.append(output); 207 } 208 } catch (Exception e) { 209 return "Exception transforming object of type Document " + e; 210 } 211 } else if (valueToFormat instanceof Identity) { 212 value.append("Identity").append(format(toLogMap((Identity) valueToFormat))); 213 } else if (valueToFormat instanceof Bundle) { 214 value.append("Bundle").append(format(toLogMap((Bundle)valueToFormat))); 215 } else if (valueToFormat instanceof ManagedAttribute) { 216 value.append("ManagedAttribute").append(format(toLogMap((ManagedAttribute) valueToFormat))); 217 } else if (valueToFormat instanceof Link) { 218 value.append("Link").append(format(toLogMap((Link) valueToFormat))); 219 } else if (valueToFormat instanceof Application) { 220 value.append("Application").append(format(toLogMap((Application) valueToFormat))); 221 } else if (valueToFormat instanceof SailPointContext) { 222 value.append("SailPointContext[").append(valueToFormat.hashCode()).append(", username = ").append(((SailPointContext) valueToFormat).getUserName()).append("]"); 223 } else if (valueToFormat instanceof ProvisioningPlan) { 224 try { 225 value.append(ProvisioningPlan.getLoggingPlan((ProvisioningPlan) valueToFormat).toXml()); 226 } catch (GeneralException e) { 227 return "Exception transforming object of type " + valueToFormat.getClass().getName() + " to XML: " + e; 228 } 229 } else if (valueToFormat instanceof Filter) { 230 value.append("Filter[").append(((Filter) valueToFormat).getExpression(true)).append("]"); 231 } else if (valueToFormat instanceof AbstractXmlObject) { 232 try { 233 value.append(((AbstractXmlObject)valueToFormat).toXml()); 234 } catch (GeneralException e) { 235 return "Exception transforming object of type " + valueToFormat.getClass().getName() + " to XML: " + e; 236 } 237 } else { 238 value.append(valueToFormat); 239 } 240 241 String result = value.toString(); 242 if (indent) { 243 result = result.replace("\n", "\n ").trim(); 244 } 245 246 return result; 247 } 248 249 /** 250 * Returns the formatted string of the item when invoked via the {@link Supplier} interface 251 * @return The formatted string 252 * @see Supplier#get() 253 */ 254 @Override 255 public String get() { 256 return toString(); 257 } 258 259 /** 260 * Converts the Identity to a Map for logging purposes 261 * @param value The Identity convert 262 * @return A Map containing some basic identity details 263 */ 264 private Map<String, Object> toLogMap(Identity value) { 265 Map<String, Object> map = createLogMap(value); 266 map.put("type", value.getType()); 267 map.put("displayName", value.getDisplayName()); 268 map.put("disabled", value.isDisabled() || value.isInactive()); 269 map.put("attributes", format(value.getAttributes())); 270 return map; 271 } 272 273 /** 274 * Converts the Link to a Map for logging purposes 275 * @param value The Link to convert 276 * @return A Map containing some basic Link details 277 */ 278 private Map<String, Object> toLogMap(Link value) { 279 Map<String, Object> map = createLogMap(value); 280 map.put("application", value.getApplicationName()); 281 map.put("nativeIdentity", value.getNativeIdentity()); 282 map.put("displayName", value.getDisplayName()); 283 map.put("disabled", value.isDisabled()); 284 return map; 285 } 286 287 /** 288 * Converts the Application to a Map for logging purposes 289 * @param value The Application to convert 290 * @return A Map containing some basic Application details 291 */ 292 private Map<String, Object> toLogMap(Application value) { 293 Map<String, Object> map = createLogMap(value); 294 map.put("authoritative", value.isAuthoritative()); 295 map.put("connector", value.getConnector()); 296 if (value.isInMaintenance()) { 297 map.put("maintenance", true); 298 } 299 return map; 300 } 301 302 /** 303 * Converts the Bundle / Role to a Map for logging purposes 304 * @param value The Bundle to convert 305 * @return A Map containing some basic Bundle details 306 */ 307 private Map<String, Object> toLogMap(Bundle value) { 308 Map<String, Object> map = createLogMap(value); 309 map.put("type", value.getType()); 310 map.put("displayName", value.getDisplayName()); 311 return map; 312 } 313 314 /** 315 * Converts the ManagedAttribute / Entitlement object to a Map for logging purposes 316 * @param value The MA to convert 317 * @return A Map containing some basic MA details 318 */ 319 private Map<String, Object> toLogMap(ManagedAttribute value) { 320 Map<String, Object> map = createLogMap(value); 321 map.put("application", value.getApplication().getName()); 322 map.put("attribute", value.getAttribute()); 323 map.put("value", value.getValue()); 324 map.put("displayName", value.getDisplayName()); 325 return map; 326 } 327 328 /** 329 * If the formatted value exists, the cached version will be returned. 330 * Otherwise, the format string will be calculated at this time, cached, 331 * and then returned. 332 * 333 * @see java.lang.Object#toString() 334 */ 335 @Override 336 public String toString() { 337 if (this.formattedValue == null) { 338 this.formattedValue = format(item); 339 } 340 return this.formattedValue; 341 } 342 } 343 344 /** 345 * An enumeration of log levels to replace the one in log4j 346 */ 347 public enum Level { 348 TRACE, 349 DEBUG, 350 INFO, 351 WARN, 352 ERROR, 353 FATAL 354 } 355 356 /** 357 * Wraps the arguments for future formatting. The format string is not resolved 358 * at this time. 359 * 360 * NOTE: In newer versions of logging APIs, this would be accomplished 361 * by passing a {@link Supplier} to the API. However, in Commons Logging 1.x, 362 * this is not available. It is also not available in Beanshell, as it requires 363 * lambda syntax. If that ever becomes available, this class will become 364 * obsolete. 365 * 366 * @param args The arguments for any place-holders in the message template. 367 * @return The formatted arguments. 368 */ 369 public static SLogger.Formatter[] format(Object[] args) { 370 if (args == null) { 371 return null; 372 } 373 Formatter[] argsCopy = new Formatter[args.length]; 374 for (int i = 0; i < args.length; i++) { 375 argsCopy[i] = new Formatter(args[i]); 376 } 377 return argsCopy; 378 } 379 380 /** 381 * Renders the MessageTemplate using the given arguments 382 * @param messageTemplate The message template 383 * @param args The arguments 384 * @return The resolved message template 385 */ 386 public static String renderMessage(String messageTemplate, Object[] args) { 387 if (args != null && args.length > 0) { 388 MessageFormat template = new MessageFormat(messageTemplate); 389 return template.format(args); 390 } else { 391 return messageTemplate; 392 } 393 } 394 395 /** 396 * The underlying logger to use. 397 */ 398 private final Log logger; 399 /** 400 * The underlying output stream to use. 401 */ 402 private final PrintStream out; 403 404 /** 405 * Creates a new logger. 406 * 407 * @param Owner The class to log messages for. 408 */ 409 public SLogger(Class<?> Owner) { 410 logger = LogFactory.getLog(Owner); 411 out = null; 412 } 413 414 /** 415 * Wraps the given log4j logger with this logger 416 * 417 * @param WrapLog The logger to wrap 418 */ 419 public SLogger(Log WrapLog) { 420 logger = WrapLog; 421 out = null; 422 } 423 424 /** 425 * Creates a new logger. 426 * 427 * @param Out The output stream to 428 */ 429 public SLogger(PrintStream Out) { 430 logger = null; 431 out = Out; 432 } 433 434 @Override 435 public void debug(Object arg0) { 436 debug("{0}", arg0); 437 } 438 439 @Override 440 public void debug(Object arg0, Throwable arg1) { 441 debug("{0} {1}", arg0, arg1); 442 } 443 444 /** 445 * Logs an debugging message. 446 * 447 * @param MessageTemplate A message template, which can either be a plain string or contain place-holders like {0} and {1}. 448 * @param Args The arguments for any place-holders in the message template. 449 */ 450 public void debug(String MessageTemplate, Object... Args) { 451 log(Level.DEBUG, MessageTemplate, format(Args)); 452 } 453 454 /** 455 * @see Log#error(Object) 456 */ 457 @Override 458 public void error(Object arg0) { 459 error("{0}", arg0); 460 } 461 462 /** 463 * @see Log#error(Object, Throwable) 464 */ 465 @Override 466 public void error(Object arg0, Throwable arg1) { 467 error("{0}", arg0); 468 handleException(arg1); 469 } 470 471 /** 472 * Logs an error message. 473 * 474 * @param MessageTemplate A message template, which can either be a plain string or contain place-holders like {0} and {1}. 475 * @param Args The arguments for any place-holders in the message template. 476 */ 477 public void error(String MessageTemplate, Object... Args) { 478 log(Level.ERROR, MessageTemplate, format(Args)); 479 } 480 481 @Override 482 public void fatal(Object arg0) { 483 fatal("{0}", arg0); 484 } 485 486 @Override 487 public void fatal(Object arg0, Throwable arg1) { 488 fatal("{0}", arg0); 489 handleException(arg1); 490 } 491 492 /** 493 * Logs a fatal error message. 494 * 495 * @param MessageTemplate A message template, which can either be a plain string or contain place-holders like {0} and {1}. 496 * @param Args The arguments for any place-holders in the message template. 497 */ 498 public void fatal(String MessageTemplate, Object... Args) { 499 log(Level.FATAL, MessageTemplate, format(Args)); 500 } 501 502 /** 503 * Gets the internal Log object wrapped by this class 504 * @return The internal log object 505 */ 506 /*package*/ Log getLogger() { 507 return logger; 508 } 509 510 /** 511 * Handles an exception. 512 * 513 * @param Error The exception to handle. 514 */ 515 public synchronized void handleException(Throwable Error) { 516 save(Error); 517 if (logger != null) { 518 logger.error(Error.toString(), Error); 519 } else if (out != null) { 520 Error.printStackTrace(out); 521 } 522 } 523 524 @Override 525 public void info(Object arg0) { 526 info("{0}", arg0); 527 } 528 529 @Override 530 public void info(Object arg0, Throwable arg1) { 531 info("{0} {1}", arg0, arg1); 532 } 533 534 /** 535 * Logs an informational message. 536 * 537 * @param MessageTemplate A message template, which can either be a plain string or contain place-holders like {0} and {1}. 538 * @param Args The arguments for any place-holders in the message template. 539 */ 540 public void info(String MessageTemplate, Object... Args) { 541 log(Level.INFO, MessageTemplate, format(Args)); 542 } 543 544 /** 545 * @see Log#isDebugEnabled() 546 */ 547 @Override 548 public boolean isDebugEnabled() { 549 if (logger != null) { 550 return logger.isDebugEnabled(); 551 } else { 552 return true; 553 } 554 } 555 556 /** 557 * Returns true if the logger is enabled for the given level. Unfortunately, Commons Logging doesn't have a friendly isEnabledFor(Level) type API, since some of its downstream loggers may not either. 558 * 559 * @param log The logger to check 560 * @param logLevel The level to check 561 * @return true if the logger is enabled 562 */ 563 private boolean isEnabledFor(Log log, Level logLevel) { 564 switch(logLevel) { 565 case TRACE: 566 return log.isTraceEnabled(); 567 case DEBUG: 568 return log.isDebugEnabled(); 569 case INFO: 570 return log.isInfoEnabled(); 571 case WARN: 572 return log.isWarnEnabled(); 573 case ERROR: 574 return log.isErrorEnabled(); 575 case FATAL: 576 return log.isFatalEnabled(); 577 } 578 return false; 579 } 580 581 /** 582 * @see Log#isErrorEnabled() 583 */ 584 @Override 585 public boolean isErrorEnabled() { 586 if (logger != null) { 587 return logger.isErrorEnabled(); 588 } else { 589 return true; 590 } 591 } 592 593 /** 594 * @see Log#isFatalEnabled() 595 */ 596 @Override 597 public boolean isFatalEnabled() { 598 if (logger != null) { 599 return logger.isFatalEnabled(); 600 } else { 601 return true; 602 } 603 } 604 605 /** 606 * @see Log#isInfoEnabled() 607 */ 608 @Override 609 public boolean isInfoEnabled() { 610 if (logger != null) { 611 return logger.isInfoEnabled(); 612 } else { 613 return true; 614 } 615 } 616 617 /** 618 * @see Log#isTraceEnabled() 619 */ 620 @Override 621 public boolean isTraceEnabled() { 622 if (logger != null) { 623 return logger.isTraceEnabled(); 624 } else { 625 return true; 626 } 627 } 628 629 /** 630 * @see Log#isWarnEnabled() 631 */ 632 @Override 633 public boolean isWarnEnabled() { 634 if (logger != null) { 635 return logger.isWarnEnabled(); 636 } else { 637 return true; 638 } 639 } 640 641 /** 642 * Logs the message at the appropriate level according to the Commons Logging API 643 * @param logLevel The log level to log at 644 * @param message The message to log 645 */ 646 public void log(Level logLevel, String message) { 647 switch(logLevel) { 648 case TRACE: 649 logger.trace(message); 650 break; 651 case DEBUG: 652 logger.debug(message); 653 break; 654 case INFO: 655 logger.info(message); 656 break; 657 case WARN: 658 logger.warn(message); 659 break; 660 case ERROR: 661 logger.error(message); 662 break; 663 case FATAL: 664 logger.fatal(message); 665 break; 666 } 667 } 668 669 /** 670 * Logs a message. 671 * 672 * @param logLevel The level to log the message at. 673 * @param messageTemplate A message template, which can either be a plain string or contain place-holders like {0} and {1}. 674 * @param args The arguments for any place-holders in the message template. 675 */ 676 private void log(Level logLevel, String messageTemplate, Object... args) { 677 save(logLevel, messageTemplate, args); 678 if (logger != null) { 679 if (isEnabledFor(logger, logLevel)) { 680 String message = renderMessage(messageTemplate, args); 681 log(logLevel, message); 682 } 683 } else if (out != null) { 684 String message = renderMessage(messageTemplate, args); 685 out.println(message); 686 } 687 } 688 689 /** 690 * Hook to allow log messages to be intercepted and saved. 691 * 692 * @param LogLevel The level to log the message at. 693 * @param MessageTemplate A message template, which can either be a plain string or contain place-holders like {0} and {1}. 694 * @param Args The arguments for any place-holders in the message template. 695 */ 696 protected void save(@SuppressWarnings("unused") Level LogLevel, @SuppressWarnings("unused") String MessageTemplate, @SuppressWarnings("unused") Object[] Args) { 697 /* Does Nothing */ 698 } 699 700 /** 701 * Hook to allow log messages to be intercepted and saved. In this version of this 702 * class, this is a no-op. 703 * 704 * @param Error The exception to handle. 705 */ 706 protected void save(@SuppressWarnings("unused") Throwable Error) { 707 /* Does Nothing */ 708 } 709 710 @Override 711 public void trace(Object arg0) { 712 trace("{0}", arg0); 713 } 714 715 @Override 716 public void trace(Object arg0, Throwable arg1) { 717 trace("{0} {1}", arg0, arg1); 718 } 719 720 /** 721 * Logs a trace message. 722 * 723 * @param MessageTemplate A message template, which can either be a plain string or contain place-holders like {0} and {1}. 724 * @param Args The arguments for any place-holders in the message template. 725 */ 726 public void trace(String MessageTemplate, Object... Args) { 727 log(Level.TRACE, MessageTemplate, format(Args)); 728 } 729 730 /** 731 * @see Log#warn(Object) 732 */ 733 @Override 734 public void warn(Object arg0) { 735 warn("{0}", arg0); 736 } 737 738 /** 739 * @see Log#warn(Object, Throwable) 740 */ 741 @Override 742 public void warn(Object arg0, Throwable arg1) { 743 warn("{0} {1}", arg0, arg1); 744 } 745 746 /** 747 * Logs a warning message. 748 * 749 * @param MessageTemplate A message template, which can either be a plain string or contain place-holders like {0} and {1}. 750 * @param Args The arguments for any place-holders in the message template. 751 */ 752 public void warn(String MessageTemplate, Object... Args) { 753 log(Level.WARN, MessageTemplate, format(Args)); 754 } 755}