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&#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}