View Javadoc
1   /*
2    * Licensed to the Apache Software Foundation (ASF) under one or more
3    * contributor license agreements. See the NOTICE file distributed with
4    * this work for additional information regarding copyright ownership.
5    * The ASF licenses this file to You under the Apache license, Version 2.0
6    * (the "License"); you may not use this file except in compliance with
7    * the License. You may obtain a copy of the License at
8    *
9    *      http://www.apache.org/licenses/LICENSE-2.0
10   *
11   * Unless required by applicable law or agreed to in writing, software
12   * distributed under the License is distributed on an "AS IS" BASIS,
13   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14   * See the license for the specific language governing permissions and
15   * limitations under the license.
16   */
17  package org.apache.logging.log4j.message;
18  
19  import java.text.SimpleDateFormat;
20  import java.util.Arrays;
21  import java.util.Collection;
22  import java.util.Date;
23  import java.util.HashSet;
24  import java.util.Map;
25  import java.util.Set;
26  
27  /**
28   * Handles messages that consist of a format string containing '{}' to represent each replaceable token, and
29   * the parameters.
30   * <p>
31   * This class was originally written for <a href="http://lilithapp.com/">Lilith</a> by Joern Huxhorn where it is
32   * licensed under the LGPL. It has been relicensed here with his permission providing that this attribution remain.
33   * </p>
34   */
35  public class ParameterizedMessage implements Message {
36  
37      /**
38       * Prefix for recursion.
39       */
40      public static final String RECURSION_PREFIX = "[...";
41      /**
42       * Suffix for recursion.
43       */
44      public static final String RECURSION_SUFFIX = "...]";
45  
46      /**
47       * Prefix for errors.
48       */
49      public static final String ERROR_PREFIX = "[!!!";
50      /**
51       * Separator for errors.
52       */
53      public static final String ERROR_SEPARATOR = "=>";
54      /**
55       * Separator for error messages.
56       */
57      public static final String ERROR_MSG_SEPARATOR = ":";
58      /**
59       * Suffix for errors.
60       */
61      public static final String ERROR_SUFFIX = "!!!]";
62  
63      private static final long serialVersionUID = -665975803997290697L;
64  
65      private static final int HASHVAL = 31;
66  
67      private static final char DELIM_START = '{';
68      private static final char DELIM_STOP = '}';
69      private static final char ESCAPE_CHAR = '\\';
70  
71      private final String messagePattern;
72      private final String[] stringArgs;
73      private transient Object[] argArray;
74      private transient String formattedMessage;
75      private transient Throwable throwable;
76  
77      /**
78       * Creates a parameterized message.
79       * @param messagePattern The message "format" string. This will be a String containing "{}" placeholders
80       * where parameters should be substituted.
81       * @param stringArgs The arguments for substitution.
82       * @param throwable A Throwable.
83       */
84      public ParameterizedMessage(final String messagePattern, final String[] stringArgs, final Throwable throwable) {
85          this.messagePattern = messagePattern;
86          this.stringArgs = stringArgs;
87          this.throwable = throwable;
88      }
89  
90      /**
91       * Creates a parameterized message.
92       * @param messagePattern The message "format" string. This will be a String containing "{}" placeholders
93       * where parameters should be substituted.
94       * @param objectArgs The arguments for substitution.
95       * @param throwable A Throwable.
96       */
97      public ParameterizedMessage(final String messagePattern, final Object[] objectArgs, final Throwable throwable) {
98          this.messagePattern = messagePattern;
99          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&#8482; 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 }