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™ 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 }
693