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 org.apache.logging.log4j.util.StringBuilders;
20  
21  import java.text.SimpleDateFormat;
22  import java.util.Arrays;
23  import java.util.Collection;
24  import java.util.Collections;
25  import java.util.Date;
26  import java.util.IdentityHashMap;
27  import java.util.Map;
28  import java.util.Set;
29  
30  /**
31   * Supports parameter formatting as used in ParameterizedMessage and ReusableParameterizedMessage.
32   */
33  final class ParameterFormatter {
34      /**
35       * Prefix for recursion.
36       */
37      static final String RECURSION_PREFIX = "[...";
38      /**
39       * Suffix for recursion.
40       */
41      static final String RECURSION_SUFFIX = "...]";
42  
43      /**
44       * Prefix for errors.
45       */
46      static final String ERROR_PREFIX = "[!!!";
47      /**
48       * Separator for errors.
49       */
50      static final String ERROR_SEPARATOR = "=>";
51      /**
52       * Separator for error messages.
53       */
54      static final String ERROR_MSG_SEPARATOR = ":";
55      /**
56       * Suffix for errors.
57       */
58      static final String ERROR_SUFFIX = "!!!]";
59  
60      private static final char DELIM_START = '{';
61      private static final char DELIM_STOP = '}';
62      private static final char ESCAPE_CHAR = '\\';
63  
64      private static final ThreadLocal<SimpleDateFormat> SIMPLE_DATE_FORMAT_REF =
65              ThreadLocal.withInitial(() -> new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSSZ"));
66  
67      private ParameterFormatter() {
68      }
69  
70      /**
71       * Counts the number of unescaped placeholders in the given messagePattern.
72       *
73       * @param messagePattern the message pattern to be analyzed.
74       * @return the number of unescaped placeholders.
75       */
76      static int countArgumentPlaceholders(final String messagePattern) {
77          if (messagePattern == null) {
78              return 0;
79          }
80          final int length = messagePattern.length();
81          int result = 0;
82          boolean isEscaped = false;
83          for (int i = 0; i < length - 1; i++) {
84              final char curChar = messagePattern.charAt(i);
85              if (curChar == ESCAPE_CHAR) {
86                  isEscaped = !isEscaped;
87              } else if (curChar == DELIM_START) {
88                  if (!isEscaped && messagePattern.charAt(i + 1) == DELIM_STOP) {
89                      result++;
90                      i++;
91                  }
92                  isEscaped = false;
93              } else {
94                  isEscaped = false;
95              }
96          }
97          return result;
98      }
99  
100     /**
101      * Counts the number of unescaped placeholders in the given messagePattern.
102      *
103      * @param messagePattern the message pattern to be analyzed.
104      * @return the number of unescaped placeholders.
105      */
106     static int countArgumentPlaceholders2(final String messagePattern, final int[] indices) {
107         if (messagePattern == null) {
108             return 0;
109         }
110         final int length = messagePattern.length();
111         int result = 0;
112         boolean isEscaped = false;
113         for (int i = 0; i < length - 1; i++) {
114             final char curChar = messagePattern.charAt(i);
115             if (curChar == ESCAPE_CHAR) {
116                 isEscaped = !isEscaped;
117                 indices[0] = -1; // escaping means fast path is not available...
118                 result++;
119             } else if (curChar == DELIM_START) {
120                 if (!isEscaped && messagePattern.charAt(i + 1) == DELIM_STOP) {
121                     indices[result] = i;
122                     result++;
123                     i++;
124                 }
125                 isEscaped = false;
126             } else {
127                 isEscaped = false;
128             }
129         }
130         return result;
131     }
132 
133     /**
134      * Counts the number of unescaped placeholders in the given messagePattern.
135      *
136      * @param messagePattern the message pattern to be analyzed.
137      * @return the number of unescaped placeholders.
138      */
139     static int countArgumentPlaceholders3(final char[] messagePattern, final int length, final int[] indices) {
140         int result = 0;
141         boolean isEscaped = false;
142         for (int i = 0; i < length - 1; i++) {
143             final char curChar = messagePattern[i];
144             if (curChar == ESCAPE_CHAR) {
145                 isEscaped = !isEscaped;
146             } else if (curChar == DELIM_START) {
147                 if (!isEscaped && messagePattern[i + 1] == DELIM_STOP) {
148                     indices[result] = i;
149                     result++;
150                     i++;
151                 }
152                 isEscaped = false;
153             } else {
154                 isEscaped = false;
155             }
156         }
157         return result;
158     }
159 
160     /**
161      * Replace placeholders in the given messagePattern with arguments.
162      *
163      * @param messagePattern the message pattern containing placeholders.
164      * @param arguments      the arguments to be used to replace placeholders.
165      * @return the formatted message.
166      */
167     static String format(final String messagePattern, final Object[] arguments) {
168         final StringBuilder result = new StringBuilder();
169         final int argCount = arguments == null ? 0 : arguments.length;
170         formatMessage(result, messagePattern, arguments, argCount);
171         return result.toString();
172     }
173 
174     /**
175      * Replace placeholders in the given messagePattern with arguments.
176      *
177      * @param buffer the buffer to write the formatted message into
178      * @param messagePattern the message pattern containing placeholders.
179      * @param arguments      the arguments to be used to replace placeholders.
180      */
181     static void formatMessage2(final StringBuilder buffer, final String messagePattern,
182             final Object[] arguments, final int argCount, final int[] indices) {
183         if (messagePattern == null || arguments == null || argCount == 0) {
184             buffer.append(messagePattern);
185             return;
186         }
187         int previous = 0;
188         for (int i = 0; i < argCount; i++) {
189             buffer.append(messagePattern, previous, indices[i]);
190             previous = indices[i] + 2;
191             recursiveDeepToString(arguments[i], buffer);
192         }
193         buffer.append(messagePattern, previous, messagePattern.length());
194     }
195 
196     /**
197      * Replace placeholders in the given messagePattern with arguments.
198      *
199      * @param buffer the buffer to write the formatted message into
200      * @param messagePattern the message pattern containing placeholders.
201      * @param arguments      the arguments to be used to replace placeholders.
202      */
203     static void formatMessage3(final StringBuilder buffer, final char[] messagePattern, final int patternLength,
204             final Object[] arguments, final int argCount, final int[] indices) {
205         if (messagePattern == null) {
206             return;
207         }
208         if (arguments == null || argCount == 0) {
209             buffer.append(messagePattern);
210             return;
211         }
212         int previous = 0;
213         for (int i = 0; i < argCount; i++) {
214             buffer.append(messagePattern, previous, indices[i]);
215             previous = indices[i] + 2;
216             recursiveDeepToString(arguments[i], buffer);
217         }
218         buffer.append(messagePattern, previous, patternLength);
219     }
220 
221     /**
222      * Replace placeholders in the given messagePattern with arguments.
223      *
224      * @param buffer the buffer to write the formatted message into
225      * @param messagePattern the message pattern containing placeholders.
226      * @param arguments      the arguments to be used to replace placeholders.
227      */
228     static void formatMessage(final StringBuilder buffer, final String messagePattern,
229             final Object[] arguments, final int argCount) {
230         if (messagePattern == null || arguments == null || argCount == 0) {
231             buffer.append(messagePattern);
232             return;
233         }
234         int escapeCounter = 0;
235         int currentArgument = 0;
236         int i = 0;
237         final int len = messagePattern.length();
238         for (; i < len - 1; i++) { // last char is excluded from the loop
239             final char curChar = messagePattern.charAt(i);
240             if (curChar == ESCAPE_CHAR) {
241                 escapeCounter++;
242             } else {
243                 if (isDelimPair(curChar, messagePattern, i)) { // looks ahead one char
244                     i++;
245 
246                     // write escaped escape chars
247                     writeEscapedEscapeChars(escapeCounter, buffer);
248 
249                     if (isOdd(escapeCounter)) {
250                         // i.e. escaped: write escaped escape chars
251                         writeDelimPair(buffer);
252                     } else {
253                         // unescaped
254                         writeArgOrDelimPair(arguments, argCount, currentArgument, buffer);
255                         currentArgument++;
256                     }
257                 } else {
258                     handleLiteralChar(buffer, escapeCounter, curChar);
259                 }
260                 escapeCounter = 0;
261             }
262         }
263         handleRemainingCharIfAny(messagePattern, len, buffer, escapeCounter, i);
264     }
265 
266     /**
267      * Returns {@code true} if the specified char and the char at {@code curCharIndex + 1} in the specified message
268      * pattern together form a "{}" delimiter pair, returns {@code false} otherwise.
269      */
270     // Profiling showed this method is important to log4j performance. Modify with care!
271     // 22 bytes (allows immediate JVM inlining: < 35 bytes) LOG4J2-1096
272     private static boolean isDelimPair(final char curChar, final String messagePattern, final int curCharIndex) {
273         return curChar == DELIM_START && messagePattern.charAt(curCharIndex + 1) == DELIM_STOP;
274     }
275 
276     /**
277      * Detects whether the message pattern has been fully processed or if an unprocessed character remains and processes
278      * it if necessary, returning the resulting position in the result char array.
279      */
280     // Profiling showed this method is important to log4j performance. Modify with care!
281     // 28 bytes (allows immediate JVM inlining: < 35 bytes) LOG4J2-1096
282     private static void handleRemainingCharIfAny(final String messagePattern, final int len,
283             final StringBuilder buffer, final int escapeCounter, final int i) {
284         if (i == len - 1) {
285             final char curChar = messagePattern.charAt(i);
286             handleLastChar(buffer, escapeCounter, curChar);
287         }
288     }
289 
290     /**
291      * Processes the last unprocessed character and returns the resulting position in the result char array.
292      */
293     // Profiling showed this method is important to log4j performance. Modify with care!
294     // 28 bytes (allows immediate JVM inlining: < 35 bytes) LOG4J2-1096
295     private static void handleLastChar(final StringBuilder buffer, final int escapeCounter, final char curChar) {
296         if (curChar == ESCAPE_CHAR) {
297             writeUnescapedEscapeChars(escapeCounter + 1, buffer);
298         } else {
299             handleLiteralChar(buffer, escapeCounter, curChar);
300         }
301     }
302 
303     /**
304      * Processes a literal char (neither an '\' escape char nor a "{}" delimiter pair) and returns the resulting
305      * position.
306      */
307     // Profiling showed this method is important to log4j performance. Modify with care!
308     // 16 bytes (allows immediate JVM inlining: < 35 bytes) LOG4J2-1096
309     private static void handleLiteralChar(final StringBuilder buffer, final int escapeCounter, final char curChar) {
310         // any other char beside ESCAPE or DELIM_START/STOP-combo
311         // write unescaped escape chars
312         writeUnescapedEscapeChars(escapeCounter, buffer);
313         buffer.append(curChar);
314     }
315 
316     /**
317      * Writes "{}" to the specified result array at the specified position and returns the resulting position.
318      */
319     // Profiling showed this method is important to log4j performance. Modify with care!
320     // 18 bytes (allows immediate JVM inlining: < 35 bytes) LOG4J2-1096
321     private static void writeDelimPair(final StringBuilder buffer) {
322         buffer.append(DELIM_START);
323         buffer.append(DELIM_STOP);
324     }
325 
326     /**
327      * Returns {@code true} if the specified parameter is odd.
328      */
329     // Profiling showed this method is important to log4j performance. Modify with care!
330     // 11 bytes (allows immediate JVM inlining: < 35 bytes) LOG4J2-1096
331     private static boolean isOdd(final int number) {
332         return (number & 1) == 1;
333     }
334 
335     /**
336      * Writes a '\' char to the specified result array (starting at the specified position) for each <em>pair</em> of
337      * '\' escape chars encountered in the message format and returns the resulting position.
338      */
339     // Profiling showed this method is important to log4j performance. Modify with care!
340     // 11 bytes (allows immediate JVM inlining: < 35 bytes) LOG4J2-1096
341     private static void writeEscapedEscapeChars(final int escapeCounter, final StringBuilder buffer) {
342         final int escapedEscapes = escapeCounter >> 1; // divide by two
343         writeUnescapedEscapeChars(escapedEscapes, buffer);
344     }
345 
346     /**
347      * Writes the specified number of '\' chars to the specified result array (starting at the specified position) and
348      * returns the resulting position.
349      */
350     // Profiling showed this method is important to log4j performance. Modify with care!
351     // 20 bytes (allows immediate JVM inlining: < 35 bytes) LOG4J2-1096
352     private static void writeUnescapedEscapeChars(int escapeCounter, final StringBuilder buffer) {
353         while (escapeCounter > 0) {
354             buffer.append(ESCAPE_CHAR);
355             escapeCounter--;
356         }
357     }
358 
359     /**
360      * Appends the argument at the specified argument index (or, if no such argument exists, the "{}" delimiter pair) to
361      * the specified result char array at the specified position and returns the resulting position.
362      */
363     // Profiling showed this method is important to log4j performance. Modify with care!
364     // 25 bytes (allows immediate JVM inlining: < 35 bytes) LOG4J2-1096
365     private static void writeArgOrDelimPair(final Object[] arguments, final int argCount, final int currentArgument,
366             final StringBuilder buffer) {
367         if (currentArgument < argCount) {
368             recursiveDeepToString(arguments[currentArgument], buffer);
369         } else {
370             writeDelimPair(buffer);
371         }
372     }
373 
374     /**
375      * This method performs a deep toString of the given Object.
376      * Primitive arrays are converted using their respective Arrays.toString methods while
377      * special handling is implemented for "container types", i.e. Object[], Map and Collection because those could
378      * contain themselves.
379      * <p>
380      * It should be noted that neither AbstractMap.toString() nor AbstractCollection.toString() implement such a
381      * behavior. They only check if the container is directly contained in itself, but not if a contained container
382      * contains the original one. Because of that, Arrays.toString(Object[]) isn't safe either.
383      * Confusing? Just read the last paragraph again and check the respective toString() implementation.
384      * </p>
385      * <p>
386      * This means, in effect, that logging would produce a usable output even if an ordinary System.out.println(o)
387      * would produce a relatively hard-to-debug StackOverflowError.
388      * </p>
389      * @param o The object.
390      * @return The String representation.
391      */
392     static String deepToString(final Object o) {
393         if (o == null) {
394             return null;
395         }
396         // Check special types to avoid unnecessary StringBuilder usage
397         if (o instanceof String) {
398             return (String) o;
399         }
400         if (o instanceof Integer) {
401             return Integer.toString((Integer) o);
402         }
403         if (o instanceof Long) {
404             return Long.toString((Long) o);
405         }
406         if (o instanceof Double) {
407             return Double.toString((Double) o);
408         }
409         if (o instanceof Boolean) {
410             return Boolean.toString((Boolean) o);
411         }
412         if (o instanceof Character) {
413             return Character.toString((Character) o);
414         }
415         if (o instanceof Short) {
416             return Short.toString((Short) o);
417         }
418         if (o instanceof Float) {
419             return Float.toString((Float) o);
420         }
421         if (o instanceof Byte) {
422             return Byte.toString((Byte) o);
423         }
424         final StringBuilder str = new StringBuilder();
425         recursiveDeepToString(o, str);
426         return str.toString();
427     }
428 
429     /**
430      * This method performs a deep {@code toString()} of the given {@code Object}.
431      * <p>
432      * Primitive arrays are converted using their respective {@code Arrays.toString()} methods, while
433      * special handling is implemented for <i>container types</i>, i.e. {@code Object[]}, {@code Map} and {@code Collection},
434      * because those could contain themselves.
435      * <p>
436      * It should be noted that neither {@code AbstractMap.toString()} nor {@code AbstractCollection.toString()} implement such a behavior.
437      * They only check if the container is directly contained in itself, but not if a contained container contains the original one.
438      * Because of that, {@code Arrays.toString(Object[])} isn't safe either.
439      * Confusing? Just read the last paragraph again and check the respective {@code toString()} implementation.
440      * <p>
441      * This means, in effect, that logging would produce a usable output even if an ordinary {@code System.out.println(o)}
442      * would produce a relatively hard-to-debug {@code StackOverflowError}.
443      *
444      * @param o      the {@code Object} to convert into a {@code String}
445      * @param str    the {@code StringBuilder} that {@code o} will be appended to
446      */
447     static void recursiveDeepToString(final Object o, final StringBuilder str) {
448         recursiveDeepToString(o, str, null);
449     }
450 
451     /**
452      * This method performs a deep {@code toString()} of the given {@code Object}.
453      * <p>
454      * Primitive arrays are converted using their respective {@code Arrays.toString()} methods, while
455      * special handling is implemented for <i>container types</i>, i.e. {@code Object[]}, {@code Map} and {@code Collection},
456      * because those could contain themselves.
457      * <p>
458      * {@code dejaVu} is used in case of those container types to prevent an endless recursion.
459      * <p>
460      * It should be noted that neither {@code AbstractMap.toString()} nor {@code AbstractCollection.toString()} implement such a behavior.
461      * They only check if the container is directly contained in itself, but not if a contained container contains the original one.
462      * Because of that, {@code Arrays.toString(Object[])} isn't safe either.
463      * Confusing? Just read the last paragraph again and check the respective {@code toString()} implementation.
464      * <p>
465      * This means, in effect, that logging would produce a usable output even if an ordinary {@code System.out.println(o)}
466      * would produce a relatively hard-to-debug {@code StackOverflowError}.
467      *
468      * @param o      the {@code Object} to convert into a {@code String}
469      * @param str    the {@code StringBuilder} that {@code o} will be appended to
470      * @param dejaVu a set of container objects directly or transitively containing {@code o}
471      */
472     private static void recursiveDeepToString(final Object o, final StringBuilder str, final Set<Object> dejaVu) {
473         if (appendSpecialTypes(o, str)) {
474             return;
475         }
476         if (isMaybeRecursive(o)) {
477             appendPotentiallyRecursiveValue(o, str, dejaVu);
478         } else {
479             tryObjectToString(o, str);
480         }
481     }
482 
483     private static boolean appendSpecialTypes(final Object o, final StringBuilder str) {
484         return StringBuilders.appendSpecificTypes(str, o) || appendDate(o, str);
485     }
486 
487     private static boolean appendDate(final Object o, final StringBuilder str) {
488         if (!(o instanceof Date)) {
489             return false;
490         }
491         final Date date = (Date) o;
492         final SimpleDateFormat format = SIMPLE_DATE_FORMAT_REF.get();
493         str.append(format.format(date));
494         return true;
495     }
496 
497     /**
498      * Returns {@code true} if the specified object is an array, a Map or a Collection.
499      */
500     private static boolean isMaybeRecursive(final Object o) {
501         return o.getClass().isArray() || o instanceof Map || o instanceof Collection;
502     }
503 
504     private static void appendPotentiallyRecursiveValue(
505             final Object o,
506             final StringBuilder str,
507             final Set<Object> dejaVu) {
508         final Class<?> oClass = o.getClass();
509         if (oClass.isArray()) {
510             appendArray(o, str, dejaVu, oClass);
511         } else if (o instanceof Map) {
512             appendMap(o, str, dejaVu);
513         } else if (o instanceof Collection) {
514             appendCollection(o, str, dejaVu);
515         } else {
516             throw new IllegalArgumentException("was expecting a container, found " + oClass);
517         }
518     }
519 
520     private static void appendArray(
521             final Object o,
522             final StringBuilder str,
523             final Set<Object> dejaVu,
524             final Class<?> oClass) {
525         if (oClass == byte[].class) {
526             str.append(Arrays.toString((byte[]) o));
527         } else if (oClass == short[].class) {
528             str.append(Arrays.toString((short[]) o));
529         } else if (oClass == int[].class) {
530             str.append(Arrays.toString((int[]) o));
531         } else if (oClass == long[].class) {
532             str.append(Arrays.toString((long[]) o));
533         } else if (oClass == float[].class) {
534             str.append(Arrays.toString((float[]) o));
535         } else if (oClass == double[].class) {
536             str.append(Arrays.toString((double[]) o));
537         } else if (oClass == boolean[].class) {
538             str.append(Arrays.toString((boolean[]) o));
539         } else if (oClass == char[].class) {
540             str.append(Arrays.toString((char[]) o));
541         } else {
542             // special handling of container Object[]
543             final Set<Object> effectiveDejaVu = getOrCreateDejaVu(dejaVu);
544             final boolean seen = !effectiveDejaVu.add(o);
545             if (seen) {
546                 final String id = identityToString(o);
547                 str.append(RECURSION_PREFIX).append(id).append(RECURSION_SUFFIX);
548             } else {
549                 final Object[] oArray = (Object[]) o;
550                 str.append('[');
551                 boolean first = true;
552                 for (final Object current : oArray) {
553                     if (first) {
554                         first = false;
555                     } else {
556                         str.append(", ");
557                     }
558                     recursiveDeepToString(current, str, cloneDejaVu(effectiveDejaVu));
559                 }
560                 str.append(']');
561             }
562         }
563     }
564 
565     /**
566      * Specialized handler for {@link Map}s.
567      */
568     private static void appendMap(
569             final Object o,
570             final StringBuilder str,
571             final Set<Object> dejaVu) {
572         final Set<Object> effectiveDejaVu = getOrCreateDejaVu(dejaVu);
573         final boolean seen = !effectiveDejaVu.add(o);
574         if (seen) {
575             final String id = identityToString(o);
576             str.append(RECURSION_PREFIX).append(id).append(RECURSION_SUFFIX);
577         } else {
578             final Map<?, ?> oMap = (Map<?, ?>) o;
579             str.append('{');
580             boolean isFirst = true;
581             for (final Object o1 : oMap.entrySet()) {
582                 final Map.Entry<?, ?> current = (Map.Entry<?, ?>) o1;
583                 if (isFirst) {
584                     isFirst = false;
585                 } else {
586                     str.append(", ");
587                 }
588                 final Object key = current.getKey();
589                 final Object value = current.getValue();
590                 recursiveDeepToString(key, str, cloneDejaVu(effectiveDejaVu));
591                 str.append('=');
592                 recursiveDeepToString(value, str, cloneDejaVu(effectiveDejaVu));
593             }
594             str.append('}');
595         }
596     }
597 
598     /**
599      * Specialized handler for {@link Collection}s.
600      */
601     private static void appendCollection(
602             final Object o,
603             final StringBuilder str,
604             final Set<Object> dejaVu) {
605         final Set<Object> effectiveDejaVu = getOrCreateDejaVu(dejaVu);
606         final boolean seen = !effectiveDejaVu.add(o);
607         if (seen) {
608             final String id = identityToString(o);
609             str.append(RECURSION_PREFIX).append(id).append(RECURSION_SUFFIX);
610         } else {
611             final Collection<?> oCol = (Collection<?>) o;
612             str.append('[');
613             boolean isFirst = true;
614             for (final Object anOCol : oCol) {
615                 if (isFirst) {
616                     isFirst = false;
617                 } else {
618                     str.append(", ");
619                 }
620                 recursiveDeepToString(anOCol, str, cloneDejaVu(effectiveDejaVu));
621             }
622             str.append(']');
623         }
624     }
625 
626     private static Set<Object> getOrCreateDejaVu(Set<Object> dejaVu) {
627         return dejaVu == null
628                 ? createDejaVu()
629                 : dejaVu;
630     }
631 
632     private static Set<Object> createDejaVu() {
633         return Collections.newSetFromMap(new IdentityHashMap<>());
634     }
635 
636     private static Set<Object> cloneDejaVu(Set<Object> dejaVu) {
637         Set<Object> clonedDejaVu = createDejaVu();
638         clonedDejaVu.addAll(dejaVu);
639         return clonedDejaVu;
640     }
641 
642     private static void tryObjectToString(final Object o, final StringBuilder str) {
643         // it's just some other Object, we can only use toString().
644         try {
645             str.append(o.toString());
646         } catch (final Throwable t) {
647             handleErrorInObjectToString(o, str, t);
648         }
649     }
650 
651     private static void handleErrorInObjectToString(final Object o, final StringBuilder str, final Throwable t) {
652         str.append(ERROR_PREFIX);
653         str.append(identityToString(o));
654         str.append(ERROR_SEPARATOR);
655         final String msg = t.getMessage();
656         final String className = t.getClass().getName();
657         str.append(className);
658         if (!className.equals(msg)) {
659             str.append(ERROR_MSG_SEPARATOR);
660             str.append(msg);
661         }
662         str.append(ERROR_SUFFIX);
663     }
664 
665     /**
666      * This method returns the same as if Object.toString() would not have been
667      * overridden in obj.
668      * <p>
669      * Note that this isn't 100% secure as collisions can always happen with hash codes.
670      * </p>
671      * <p>
672      * Copied from Object.hashCode():
673      * </p>
674      * <blockquote>
675      * As much as is reasonably practical, the hashCode method defined by
676      * class {@code Object} does return distinct integers for distinct
677      * objects. (This is typically implemented by converting the internal
678      * address of the object into an integer, but this implementation
679      * technique is not required by the Java&#8482; programming language.)
680      * </blockquote>
681      *
682      * @param obj the Object that is to be converted into an identity string.
683      * @return the identity string as also defined in Object.toString()
684      */
685     static String identityToString(final Object obj) {
686         if (obj == null) {
687             return null;
688         }
689         return obj.getClass().getName() + '@' + Integer.toHexString(System.identityHashCode(obj));
690     }
691 
692 }