001/* 002 * Licensed to the Apache Software Foundation (ASF) under one or more 003 * contributor license agreements. See the NOTICE file distributed with 004 * this work for additional information regarding copyright ownership. 005 * The ASF licenses this file to You under the Apache license, Version 2.0 006 * (the "License"); you may not use this file except in compliance with 007 * the License. You may obtain a copy of the License at 008 * 009 * http://www.apache.org/licenses/LICENSE-2.0 010 * 011 * Unless required by applicable law or agreed to in writing, software 012 * distributed under the License is distributed on an "AS IS" BASIS, 013 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 014 * See the license for the specific language governing permissions and 015 * limitations under the license. 016 */ 017package org.apache.logging.log4j.message; 018 019import java.text.SimpleDateFormat; 020import java.util.Arrays; 021import java.util.Collection; 022import java.util.Date; 023import java.util.HashSet; 024import java.util.Map; 025import java.util.Set; 026 027/** 028 * Handles messages that consist of a format string containing '{}' to represent each replaceable token, and 029 * the parameters. 030 * <p> 031 * This class was originally written for <a href="http://lilithapp.com/">Lilith</a> by Joern Huxhorn where it is 032 * licensed under the LGPL. It has been relicensed here with his permission providing that this attribution remain. 033 * </p> 034 */ 035public class ParameterizedMessage implements Message { 036 037 /** 038 * Prefix for recursion. 039 */ 040 public static final String RECURSION_PREFIX = "[..."; 041 /** 042 * Suffix for recursion. 043 */ 044 public static final String RECURSION_SUFFIX = "...]"; 045 046 /** 047 * Prefix for errors. 048 */ 049 public static final String ERROR_PREFIX = "[!!!"; 050 /** 051 * Separator for errors. 052 */ 053 public static final String ERROR_SEPARATOR = "=>"; 054 /** 055 * Separator for error messages. 056 */ 057 public static final String ERROR_MSG_SEPARATOR = ":"; 058 /** 059 * Suffix for errors. 060 */ 061 public static final String ERROR_SUFFIX = "!!!]"; 062 063 private static final long serialVersionUID = -665975803997290697L; 064 065 private static final int HASHVAL = 31; 066 067 private static final char DELIM_START = '{'; 068 private static final char DELIM_STOP = '}'; 069 private static final char ESCAPE_CHAR = '\\'; 070 071 private final String messagePattern; 072 private final String[] stringArgs; 073 private transient Object[] argArray; 074 private transient String formattedMessage; 075 private transient Throwable throwable; 076 077 /** 078 * Creates a parameterized message. 079 * @param messagePattern The message "format" string. This will be a String containing "{}" placeholders 080 * where parameters should be substituted. 081 * @param stringArgs The arguments for substitution. 082 * @param throwable A Throwable. 083 */ 084 public ParameterizedMessage(final String messagePattern, final String[] stringArgs, final Throwable throwable) { 085 this.messagePattern = messagePattern; 086 this.stringArgs = stringArgs; 087 this.throwable = throwable; 088 } 089 090 /** 091 * Creates a parameterized message. 092 * @param messagePattern The message "format" string. This will be a String containing "{}" placeholders 093 * where parameters should be substituted. 094 * @param objectArgs The arguments for substitution. 095 * @param throwable A Throwable. 096 */ 097 public ParameterizedMessage(final String messagePattern, final Object[] objectArgs, final Throwable throwable) { 098 this.messagePattern = messagePattern; 099 this.throwable = throwable; 100 this.stringArgs = argumentsToStrings(objectArgs); 101 } 102 103 /** 104 * Constructs a ParameterizedMessage which contains the arguments converted to String as well as an optional 105 * Throwable. 106 * 107 * <p>If the last argument is a Throwable and is NOT used up by a placeholder in the message pattern it is returned 108 * in {@link #getThrowable()} and won't be contained in the created String[]. 109 * If it is used up {@link #getThrowable()} will return null even if the last argument was a Throwable!</p> 110 * 111 * @param messagePattern the message pattern that to be checked for placeholders. 112 * @param arguments the argument array to be converted. 113 */ 114 public ParameterizedMessage(final String messagePattern, final Object[] arguments) { 115 this.messagePattern = messagePattern; 116 this.stringArgs = argumentsToStrings(arguments); 117 } 118 119 /** 120 * Constructor with a pattern and a single parameter. 121 * @param messagePattern The message pattern. 122 * @param arg The parameter. 123 */ 124 public ParameterizedMessage(final String messagePattern, final Object arg) { 125 this(messagePattern, new Object[]{arg}); 126 } 127 128 /** 129 * Constructor with a pattern and two parameters. 130 * @param messagePattern The message pattern. 131 * @param arg1 The first parameter. 132 * @param arg2 The second parameter. 133 */ 134 public ParameterizedMessage(final String messagePattern, final Object arg1, final Object arg2) { 135 this(messagePattern, new Object[]{arg1, arg2}); 136 } 137 138 private String[] argumentsToStrings(final Object[] arguments) { 139 if (arguments == null) { 140 return null; 141 } 142 final int argsCount = countArgumentPlaceholders(messagePattern); 143 int resultArgCount = arguments.length; 144 if (argsCount < arguments.length && throwable == null && arguments[arguments.length - 1] instanceof Throwable) { 145 throwable = (Throwable) arguments[arguments.length - 1]; 146 resultArgCount--; 147 } 148 argArray = new Object[resultArgCount]; 149 System.arraycopy(arguments, 0, argArray, 0, resultArgCount); 150 151 String[] strArgs; 152 if (argsCount == 1 && throwable == null && arguments.length > 1) { 153 // special case 154 strArgs = new String[1]; 155 strArgs[0] = deepToString(arguments); 156 } else { 157 strArgs = new String[resultArgCount]; 158 for (int i = 0; i < strArgs.length; i++) { 159 strArgs[i] = deepToString(arguments[i]); 160 } 161 } 162 return strArgs; 163 } 164 165 /** 166 * Returns the formatted message. 167 * @return the formatted message. 168 */ 169 @Override 170 public String getFormattedMessage() { 171 if (formattedMessage == null) { 172 formattedMessage = formatMessage(messagePattern, stringArgs); 173 } 174 return formattedMessage; 175 } 176 177 /** 178 * Returns the message pattern. 179 * @return the message pattern. 180 */ 181 @Override 182 public String getFormat() { 183 return messagePattern; 184 } 185 186 /** 187 * Returns the message parameters. 188 * @return the message parameters. 189 */ 190 @Override 191 public Object[] getParameters() { 192 if (argArray != null) { 193 return argArray; 194 } 195 return stringArgs; 196 } 197 198 /** 199 * Returns the Throwable that was given as the last argument, if any. 200 * It will not survive serialization. The Throwable exists as part of the message 201 * primarily so that it can be extracted from the end of the list of parameters 202 * and then be added to the LogEvent. As such, the Throwable in the event should 203 * not be used once the LogEvent has been constructed. 204 * 205 * @return the Throwable, if any. 206 */ 207 @Override 208 public Throwable getThrowable() { 209 return throwable; 210 } 211 212 protected String formatMessage(final String msgPattern, final String[] sArgs) { 213 return formatStringArgs(msgPattern, sArgs); 214 } 215 216 @Override 217 public boolean equals(final Object o) { 218 if (this == o) { 219 return true; 220 } 221 if (o == null || getClass() != o.getClass()) { 222 return false; 223 } 224 225 final ParameterizedMessage that = (ParameterizedMessage) o; 226 227 if (messagePattern != null ? !messagePattern.equals(that.messagePattern) : that.messagePattern != null) { 228 return false; 229 } 230 if (!Arrays.equals(stringArgs, that.stringArgs)) { 231 return false; 232 } 233 //if (throwable != null ? !throwable.equals(that.throwable) : that.throwable != null) return false; 234 235 return true; 236 } 237 238 @Override 239 public int hashCode() { 240 int result = messagePattern != null ? messagePattern.hashCode() : 0; 241 result = HASHVAL * result + (stringArgs != null ? Arrays.hashCode(stringArgs) : 0); 242 return result; 243 } 244 245 /** 246 * Replace placeholders in the given messagePattern with arguments. 247 * 248 * @param messagePattern the message pattern containing placeholders. 249 * @param arguments the arguments to be used to replace placeholders. 250 * @return the formatted message. 251 */ 252 public static String format(final String messagePattern, final Object[] arguments) { 253 if (messagePattern == null || arguments == null || arguments.length == 0) { 254 return messagePattern; 255 } 256 if (arguments instanceof String[]) { 257 return formatStringArgs(messagePattern, (String[]) arguments); 258 } 259 final String[] stringArgs = new String[arguments.length]; 260 for (int i = 0; i < arguments.length; i++) { 261 stringArgs[i] = String.valueOf(arguments[i]); 262 } 263 return formatStringArgs(messagePattern, stringArgs); 264 } 265 266 /** 267 * Replace placeholders in the given messagePattern with arguments. 268 * <p> 269 * Package protected for unit tests. 270 * 271 * @param messagePattern the message pattern containing placeholders. 272 * @param arguments the arguments to be used to replace placeholders. 273 * @return the formatted message. 274 */ 275 // Profiling showed this method is important to log4j performance. Modify with care! 276 // 33 bytes (allows immediate JVM inlining: < 35 bytes) LOG4J2-1096 277 static String formatStringArgs(final String messagePattern, final String[] arguments) { 278 int len = 0; 279 if (messagePattern == null || (len = messagePattern.length()) == 0 || arguments == null 280 || arguments.length == 0) { 281 return messagePattern; 282 } 283 284 return formatStringArgs0(messagePattern, len, arguments); 285 } 286 287 // Profiling showed this method is important to log4j performance. Modify with care! 288 // 157 bytes (will be inlined when hot enough: < 325 bytes) 289 private static String formatStringArgs0(final String messagePattern, final int len, final String[] arguments) { 290 final char[] result = new char[len + sumStringLengths(arguments)]; 291 int pos = 0; 292 int escapeCounter = 0; 293 int currentArgument = 0; 294 int i = 0; 295 for (; i < len - 1; i++) { // last char is excluded from the loop 296 final char curChar = messagePattern.charAt(i); 297 if (curChar == ESCAPE_CHAR) { 298 escapeCounter++; 299 } else { 300 if (isDelimPair(curChar, messagePattern, i)) { // looks ahead one char 301 i++; 302 303 // write escaped escape chars 304 pos = writeEscapedEscapeChars(escapeCounter, result, pos); 305 306 if (isOdd(escapeCounter)) { 307 // i.e. escaped 308 // write escaped escape chars 309 pos = writeDelimPair(result, pos); 310 } else { 311 // unescaped 312 pos = writeArgOrDelimPair(arguments, currentArgument, result, pos); 313 currentArgument++; 314 } 315 } else { 316 pos = handleLiteralChar(result, pos, escapeCounter, curChar); 317 } 318 escapeCounter = 0; 319 } 320 } 321 pos = handleRemainingCharIfAny(messagePattern, len, result, pos, escapeCounter, i); 322 return new String(result, 0, pos); 323 } 324 325 /** 326 * Returns the sum of the lengths of all Strings in the specified array. 327 */ 328 // Profiling showed this method is important to log4j performance. Modify with care! 329 // 30 bytes (allows immediate JVM inlining: < 35 bytes) LOG4J2-1096 330 private static int sumStringLengths(final String[] arguments) { 331 int result = 0; 332 for (int i = 0; i < arguments.length; i++) { 333 result += String.valueOf(arguments[i]).length(); 334 } 335 return result; 336 } 337 338 /** 339 * Returns {@code true} if the specified char and the char at {@code curCharIndex + 1} in the specified message 340 * pattern together form a "{}" delimiter pair, returns {@code false} otherwise. 341 */ 342 // Profiling showed this method is important to log4j performance. Modify with care! 343 // 22 bytes (allows immediate JVM inlining: < 35 bytes) LOG4J2-1096 344 private static boolean isDelimPair(final char curChar, final String messagePattern, final int curCharIndex) { 345 return curChar == DELIM_START && messagePattern.charAt(curCharIndex + 1) == DELIM_STOP; 346 } 347 348 /** 349 * Detects whether the message pattern has been fully processed or if an unprocessed character remains and processes 350 * it if necessary, returning the resulting position in the result char array. 351 */ 352 // Profiling showed this method is important to log4j performance. Modify with care! 353 // 28 bytes (allows immediate JVM inlining: < 35 bytes) LOG4J2-1096 354 private static int handleRemainingCharIfAny(final String messagePattern, final int len, final char[] result, 355 int pos, int escapeCounter, int i) { 356 if (i == len - 1) { 357 final char curChar = messagePattern.charAt(i); 358 pos = handleLastChar(result, pos, escapeCounter, curChar); 359 } 360 return pos; 361 } 362 363 /** 364 * Processes the last unprocessed character and returns the resulting position in the result char array. 365 */ 366 // Profiling showed this method is important to log4j performance. Modify with care! 367 // 28 bytes (allows immediate JVM inlining: < 35 bytes) LOG4J2-1096 368 private static int handleLastChar(final char[] result, int pos, final int escapeCounter, final char curChar) { 369 if (curChar == ESCAPE_CHAR) { 370 pos = writeUnescapedEscapeChars(escapeCounter + 1, result, pos); 371 } else { 372 pos = handleLiteralChar(result, pos, escapeCounter, curChar); 373 } 374 return pos; 375 } 376 377 /** 378 * Processes a literal char (neither an '\' escape char nor a "{}" delimiter pair) and returns the resulting 379 * position. 380 */ 381 // Profiling showed this method is important to log4j performance. Modify with care! 382 // 16 bytes (allows immediate JVM inlining: < 35 bytes) LOG4J2-1096 383 private static int handleLiteralChar(final char[] result, int pos, final int escapeCounter, final char curChar) { 384 // any other char beside ESCAPE or DELIM_START/STOP-combo 385 // write unescaped escape chars 386 pos = writeUnescapedEscapeChars(escapeCounter, result, pos); 387 result[pos++] = curChar; 388 return pos; 389 } 390 391 /** 392 * Writes "{}" to the specified result array at the specified position and returns the resulting position. 393 */ 394 // Profiling showed this method is important to log4j performance. Modify with care! 395 // 18 bytes (allows immediate JVM inlining: < 35 bytes) LOG4J2-1096 396 private static int writeDelimPair(final char[] result, int pos) { 397 result[pos++] = DELIM_START; 398 result[pos++] = DELIM_STOP; 399 return pos; 400 } 401 402 /** 403 * Returns {@code true} if the specified parameter is odd. 404 */ 405 // Profiling showed this method is important to log4j performance. Modify with care! 406 // 11 bytes (allows immediate JVM inlining: < 35 bytes) LOG4J2-1096 407 private static boolean isOdd(final int number) { 408 return (number & 1) == 1; 409 } 410 411 /** 412 * Writes a '\' char to the specified result array (starting at the specified position) for each <em>pair</em> of 413 * '\' escape chars encountered in the message format and returns the resulting position. 414 */ 415 // Profiling showed this method is important to log4j performance. Modify with care! 416 // 11 bytes (allows immediate JVM inlining: < 35 bytes) LOG4J2-1096 417 private static int writeEscapedEscapeChars(final int escapeCounter, final char[] result, final int pos) { 418 final int escapedEscapes = escapeCounter >> 1; // divide by two 419 return writeUnescapedEscapeChars(escapedEscapes, result, pos); 420 } 421 422 /** 423 * Writes the specified number of '\' chars to the specified result array (starting at the specified position) and 424 * returns the resulting position. 425 */ 426 // Profiling showed this method is important to log4j performance. Modify with care! 427 // 20 bytes (allows immediate JVM inlining: < 35 bytes) LOG4J2-1096 428 private static int writeUnescapedEscapeChars(int escapeCounter, char[] result, int pos) { 429 while (escapeCounter > 0) { 430 result[pos++] = ESCAPE_CHAR; 431 escapeCounter--; 432 } 433 return pos; 434 } 435 436 /** 437 * Appends the argument at the specified argument index (or, if no such argument exists, the "{}" delimiter pair) to 438 * the specified result char array at the specified position and returns the resulting position. 439 */ 440 // Profiling showed this method is important to log4j performance. Modify with care! 441 // 25 bytes (allows immediate JVM inlining: < 35 bytes) LOG4J2-1096 442 private static int writeArgOrDelimPair(final String[] arguments, final int currentArgument, final char[] result, 443 int pos) { 444 if (currentArgument < arguments.length) { 445 pos = writeArgAt0(arguments, currentArgument, result, pos); 446 } else { 447 pos = writeDelimPair(result, pos); 448 } 449 return pos; 450 } 451 452 /** 453 * Appends the argument at the specified argument index to the specified result char array at the specified position 454 * and returns the resulting position. 455 */ 456 // Profiling showed this method is important to log4j performance. Modify with care! 457 // 30 bytes (allows immediate JVM inlining: < 35 bytes) LOG4J2-1096 458 private static int writeArgAt0(final String[] arguments, final int currentArgument, final char[] result, 459 final int pos) { 460 final String arg = String.valueOf(arguments[currentArgument]); 461 int argLen = arg.length(); 462 arg.getChars(0, argLen, result, pos); 463 return pos + argLen; 464 } 465 466 /** 467 * Counts the number of unescaped placeholders in the given messagePattern. 468 * 469 * @param messagePattern the message pattern to be analyzed. 470 * @return the number of unescaped placeholders. 471 */ 472 public static int countArgumentPlaceholders(final String messagePattern) { 473 if (messagePattern == null) { 474 return 0; 475 } 476 477 final int delim = messagePattern.indexOf(DELIM_START); 478 479 if (delim == -1) { 480 // special case, no placeholders at all. 481 return 0; 482 } 483 int result = 0; 484 boolean isEscaped = false; 485 for (int i = 0; i < messagePattern.length(); i++) { 486 final char curChar = messagePattern.charAt(i); 487 if (curChar == ESCAPE_CHAR) { 488 isEscaped = !isEscaped; 489 } else if (curChar == DELIM_START) { 490 if (!isEscaped && i < messagePattern.length() - 1 && messagePattern.charAt(i + 1) == DELIM_STOP) { 491 result++; 492 i++; 493 } 494 isEscaped = false; 495 } else { 496 isEscaped = false; 497 } 498 } 499 return result; 500 } 501 502 /** 503 * This method performs a deep toString of the given Object. 504 * Primitive arrays are converted using their respective Arrays.toString methods while 505 * special handling is implemented for "container types", i.e. Object[], Map and Collection because those could 506 * contain themselves. 507 * <p> 508 * It should be noted that neither AbstractMap.toString() nor AbstractCollection.toString() implement such a 509 * behavior. They only check if the container is directly contained in itself, but not if a contained container 510 * contains the original one. Because of that, Arrays.toString(Object[]) isn't safe either. 511 * Confusing? Just read the last paragraph again and check the respective toString() implementation. 512 * </p> 513 * <p> 514 * This means, in effect, that logging would produce a usable output even if an ordinary System.out.println(o) 515 * would produce a relatively hard-to-debug StackOverflowError. 516 * </p> 517 * @param o The object. 518 * @return The String representation. 519 */ 520 public static String deepToString(final Object o) { 521 if (o == null) { 522 return null; 523 } 524 if (o instanceof String) { 525 return (String) o; 526 } 527 final StringBuilder str = new StringBuilder(); 528 final Set<String> dejaVu = new HashSet<>(); // that's actually a neat name ;) 529 recursiveDeepToString(o, str, dejaVu); 530 return str.toString(); 531 } 532 533 /** 534 * This method performs a deep toString of the given Object. 535 * Primitive arrays are converted using their respective Arrays.toString methods while 536 * special handling is implemented for "container types", i.e. Object[], Map and Collection because those could 537 * contain themselves. 538 * <p> 539 * dejaVu is used in case of those container types to prevent an endless recursion. 540 * </p> 541 * <p> 542 * It should be noted that neither AbstractMap.toString() nor AbstractCollection.toString() implement such a 543 * behavior. 544 * They only check if the container is directly contained in itself, but not if a contained container contains the 545 * original one. Because of that, Arrays.toString(Object[]) isn't safe either. 546 * Confusing? Just read the last paragraph again and check the respective toString() implementation. 547 * </p> 548 * <p> 549 * This means, in effect, that logging would produce a usable output even if an ordinary System.out.println(o) 550 * would produce a relatively hard-to-debug StackOverflowError. 551 * </p> 552 * 553 * @param o the Object to convert into a String 554 * @param str the StringBuilder that o will be appended to 555 * @param dejaVu a list of container identities that were already used. 556 */ 557 private static void recursiveDeepToString(final Object o, final StringBuilder str, final Set<String> dejaVu) { 558 if (appendStringDateOrNull(o, str)) { 559 return; 560 } 561 if (isMaybeRecursive(o)) { 562 appendPotentiallyRecursiveValue(o, str, dejaVu); 563 } else { 564 tryObjectToString(o, str); 565 } 566 } 567 568 private static boolean appendStringDateOrNull(final Object o, final StringBuilder str) { 569 if (o == null || o instanceof String) { 570 str.append(String.valueOf(o)); 571 return true; 572 } 573 return appendDate(o, str); 574 } 575 576 private static boolean appendDate(final Object o, final StringBuilder str) { 577 if (!(o instanceof Date)) { 578 return false; 579 } 580 final Date date = (Date) o; 581 final SimpleDateFormat format = getSimpleDateFormat(); 582 str.append(format.format(date)); 583 return true; 584 } 585 586 private static SimpleDateFormat getSimpleDateFormat() { 587 // I'll leave it like this for the moment... this could probably be optimized using ThreadLocal... 588 return new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSSZ"); 589 } 590 591 /** 592 * Returns {@code true} if the specified object is an array, a Map or a Collection. 593 */ 594 private static boolean isMaybeRecursive(final Object o) { 595 return o.getClass().isArray() || o instanceof Map || o instanceof Collection; 596 } 597 598 private static void appendPotentiallyRecursiveValue(final Object o, final StringBuilder str, 599 final Set<String> dejaVu) { 600 final Class<?> oClass = o.getClass(); 601 if (oClass.isArray()) { 602 appendArray(o, str, dejaVu, oClass); 603 } else if (o instanceof Map) { 604 appendMap(o, str, dejaVu); 605 } else if (o instanceof Collection) { 606 appendCollection(o, str, dejaVu); 607 } 608 } 609 610 private static void appendArray(final Object o, final StringBuilder str, final Set<String> dejaVu, 611 final Class<?> oClass) { 612 if (oClass == byte[].class) { 613 str.append(Arrays.toString((byte[]) o)); 614 } else if (oClass == short[].class) { 615 str.append(Arrays.toString((short[]) o)); 616 } else if (oClass == int[].class) { 617 str.append(Arrays.toString((int[]) o)); 618 } else if (oClass == long[].class) { 619 str.append(Arrays.toString((long[]) o)); 620 } else if (oClass == float[].class) { 621 str.append(Arrays.toString((float[]) o)); 622 } else if (oClass == double[].class) { 623 str.append(Arrays.toString((double[]) o)); 624 } else if (oClass == boolean[].class) { 625 str.append(Arrays.toString((boolean[]) o)); 626 } else if (oClass == char[].class) { 627 str.append(Arrays.toString((char[]) o)); 628 } else { 629 // special handling of container Object[] 630 final String id = identityToString(o); 631 if (dejaVu.contains(id)) { 632 str.append(RECURSION_PREFIX).append(id).append(RECURSION_SUFFIX); 633 } else { 634 dejaVu.add(id); 635 final Object[] oArray = (Object[]) o; 636 str.append('['); 637 boolean first = true; 638 for (final Object current : oArray) { 639 if (first) { 640 first = false; 641 } else { 642 str.append(", "); 643 } 644 recursiveDeepToString(current, str, new HashSet<>(dejaVu)); 645 } 646 str.append(']'); 647 } 648 //str.append(Arrays.deepToString((Object[]) o)); 649 } 650 } 651 652 private static void appendMap(final Object o, final StringBuilder str, final Set<String> dejaVu) { 653 // special handling of container Map 654 final String id = identityToString(o); 655 if (dejaVu.contains(id)) { 656 str.append(RECURSION_PREFIX).append(id).append(RECURSION_SUFFIX); 657 } else { 658 dejaVu.add(id); 659 final Map<?, ?> oMap = (Map<?, ?>) o; 660 str.append('{'); 661 boolean isFirst = true; 662 for (final Object o1 : oMap.entrySet()) { 663 final Map.Entry<?, ?> current = (Map.Entry<?, ?>) o1; 664 if (isFirst) { 665 isFirst = false; 666 } else { 667 str.append(", "); 668 } 669 final Object key = current.getKey(); 670 final Object value = current.getValue(); 671 recursiveDeepToString(key, str, new HashSet<>(dejaVu)); 672 str.append('='); 673 recursiveDeepToString(value, str, new HashSet<>(dejaVu)); 674 } 675 str.append('}'); 676 } 677 } 678 679 private static void appendCollection(final Object o, final StringBuilder str, final Set<String> dejaVu) { 680 // special handling of container Collection 681 final String id = identityToString(o); 682 if (dejaVu.contains(id)) { 683 str.append(RECURSION_PREFIX).append(id).append(RECURSION_SUFFIX); 684 } else { 685 dejaVu.add(id); 686 final Collection<?> oCol = (Collection<?>) o; 687 str.append('['); 688 boolean isFirst = true; 689 for (final Object anOCol : oCol) { 690 if (isFirst) { 691 isFirst = false; 692 } else { 693 str.append(", "); 694 } 695 recursiveDeepToString(anOCol, str, new HashSet<>(dejaVu)); 696 } 697 str.append(']'); 698 } 699 } 700 701 private static void tryObjectToString(final Object o, final StringBuilder str) { 702 // it's just some other Object, we can only use toString(). 703 try { 704 str.append(o.toString()); 705 } catch (final Throwable t) { 706 handleErrorInObjectToString(o, str, t); 707 } 708 } 709 710 private static void handleErrorInObjectToString(final Object o, final StringBuilder str, final Throwable t) { 711 str.append(ERROR_PREFIX); 712 str.append(identityToString(o)); 713 str.append(ERROR_SEPARATOR); 714 final String msg = t.getMessage(); 715 final String className = t.getClass().getName(); 716 str.append(className); 717 if (!className.equals(msg)) { 718 str.append(ERROR_MSG_SEPARATOR); 719 str.append(msg); 720 } 721 str.append(ERROR_SUFFIX); 722 } 723 724 /** 725 * This method returns the same as if Object.toString() would not have been 726 * overridden in obj. 727 * <p> 728 * Note that this isn't 100% secure as collisions can always happen with hash codes. 729 * </p> 730 * <p> 731 * Copied from Object.hashCode(): 732 * </p> 733 * <blockquote> 734 * As much as is reasonably practical, the hashCode method defined by 735 * class {@code Object} does return distinct integers for distinct 736 * objects. (This is typically implemented by converting the internal 737 * address of the object into an integer, but this implementation 738 * technique is not required by the Java™ programming language.) 739 * </blockquote> 740 * 741 * @param obj the Object that is to be converted into an identity string. 742 * @return the identity string as also defined in Object.toString() 743 */ 744 public static String identityToString(final Object obj) { 745 if (obj == null) { 746 return null; 747 } 748 return obj.getClass().getName() + '@' + Integer.toHexString(System.identityHashCode(obj)); 749 } 750 751 @Override 752 public String toString() { 753 return "ParameterizedMessage[messagePattern=" + messagePattern + ", stringArgs=" + 754 Arrays.toString(stringArgs) + ", throwable=" + throwable + ']'; 755 } 756}