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 format(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
257        final StringBuilder result = new StringBuilder();
258        int escapeCounter = 0;
259        int currentArgument = 0;
260        for (int i = 0; i < messagePattern.length(); i++) {
261            final char curChar = messagePattern.charAt(i);
262            if (curChar == ESCAPE_CHAR) {
263                escapeCounter++;
264            } else {
265                if (curChar == DELIM_START && i < messagePattern.length() - 1
266                        && messagePattern.charAt(i + 1) == DELIM_STOP) {
267                    // write escaped escape chars
268                    final int escapedEscapes = escapeCounter / 2;
269                    for (int j = 0; j < escapedEscapes; j++) {
270                        result.append(ESCAPE_CHAR);
271                    }
272
273                    if (escapeCounter % 2 == 1) {
274                        // i.e. escaped
275                        // write escaped escape chars
276                        result.append(DELIM_START);
277                        result.append(DELIM_STOP);
278                    } else {
279                        // unescaped
280                        if (currentArgument < arguments.length) {
281                            result.append(arguments[currentArgument]);
282                        } else {
283                            result.append(DELIM_START).append(DELIM_STOP);
284                        }
285                        currentArgument++;
286                    }
287                    i++;
288                    escapeCounter = 0;
289                    continue;
290                }
291                // any other char beside ESCAPE or DELIM_START/STOP-combo
292                // write unescaped escape chars
293                if (escapeCounter > 0) {
294                    for (int j = 0; j < escapeCounter; j++) {
295                        result.append(ESCAPE_CHAR);
296                    }
297                    escapeCounter = 0;
298                }
299                result.append(curChar);
300            }
301        }
302        return result.toString();
303    }
304
305    /**
306     * Counts the number of unescaped placeholders in the given messagePattern.
307     *
308     * @param messagePattern the message pattern to be analyzed.
309     * @return the number of unescaped placeholders.
310     */
311    public static int countArgumentPlaceholders(final String messagePattern) {
312        if (messagePattern == null) {
313            return 0;
314        }
315
316        final int delim = messagePattern.indexOf(DELIM_START);
317
318        if (delim == -1) {
319            // special case, no placeholders at all.
320            return 0;
321        }
322        int result = 0;
323        boolean isEscaped = false;
324        for (int i = 0; i < messagePattern.length(); i++) {
325            final char curChar = messagePattern.charAt(i);
326            if (curChar == ESCAPE_CHAR) {
327                isEscaped = !isEscaped;
328            } else if (curChar == DELIM_START) {
329                if (!isEscaped && i < messagePattern.length() - 1 && messagePattern.charAt(i + 1) == DELIM_STOP) {
330                    result++;
331                    i++;
332                }
333                isEscaped = false;
334            } else {
335                isEscaped = false;
336            }
337        }
338        return result;
339    }
340
341    /**
342     * This method performs a deep toString of the given Object.
343     * Primitive arrays are converted using their respective Arrays.toString methods while
344     * special handling is implemented for "container types", i.e. Object[], Map and Collection because those could
345     * contain themselves.
346     * <p>
347     * It should be noted that neither AbstractMap.toString() nor AbstractCollection.toString() implement such a
348     * behavior. They only check if the container is directly contained in itself, but not if a contained container
349     * contains the original one. Because of that, Arrays.toString(Object[]) isn't safe either.
350     * Confusing? Just read the last paragraph again and check the respective toString() implementation.
351     * </p>
352     * <p>
353     * This means, in effect, that logging would produce a usable output even if an ordinary System.out.println(o)
354     * would produce a relatively hard-to-debug StackOverflowError.
355     * </p>
356     * @param o The object.
357     * @return The String representation.
358     */
359    public static String deepToString(final Object o) {
360        if (o == null) {
361            return null;
362        }
363        if (o instanceof String) {
364            return (String) o;
365        }
366        final StringBuilder str = new StringBuilder();
367        final Set<String> dejaVu = new HashSet<String>(); // that's actually a neat name ;)
368        recursiveDeepToString(o, str, dejaVu);
369        return str.toString();
370    }
371
372    /**
373     * This method performs a deep toString of the given Object.
374     * Primitive arrays are converted using their respective Arrays.toString methods while
375     * special handling is implemented for "container types", i.e. Object[], Map and Collection because those could
376     * contain themselves.
377     * <p>
378     * dejaVu is used in case of those container types to prevent an endless recursion.
379     * </p>
380     * <p>
381     * It should be noted that neither AbstractMap.toString() nor AbstractCollection.toString() implement such a
382     * behavior.
383     * They only check if the container is directly contained in itself, but not if a contained container contains the
384     * original one. Because of that, Arrays.toString(Object[]) isn't safe either.
385     * Confusing? Just read the last paragraph again and check the respective toString() implementation.
386     * </p>
387     * <p>
388     * This means, in effect, that logging would produce a usable output even if an ordinary System.out.println(o)
389     * would produce a relatively hard-to-debug StackOverflowError.
390     * </p>
391     *
392     * @param o      the Object to convert into a String
393     * @param str    the StringBuilder that o will be appended to
394     * @param dejaVu a list of container identities that were already used.
395     */
396    private static void recursiveDeepToString(final Object o, final StringBuilder str, final Set<String> dejaVu) {
397        if (o == null) {
398            str.append("null");
399            return;
400        }
401        if (o instanceof String) {
402            str.append(o);
403            return;
404        }
405
406        final Class<?> oClass = o.getClass();
407        if (oClass.isArray()) {
408            if (oClass == byte[].class) {
409                str.append(Arrays.toString((byte[]) o));
410            } else if (oClass == short[].class) {
411                str.append(Arrays.toString((short[]) o));
412            } else if (oClass == int[].class) {
413                str.append(Arrays.toString((int[]) o));
414            } else if (oClass == long[].class) {
415                str.append(Arrays.toString((long[]) o));
416            } else if (oClass == float[].class) {
417                str.append(Arrays.toString((float[]) o));
418            } else if (oClass == double[].class) {
419                str.append(Arrays.toString((double[]) o));
420            } else if (oClass == boolean[].class) {
421                str.append(Arrays.toString((boolean[]) o));
422            } else if (oClass == char[].class) {
423                str.append(Arrays.toString((char[]) o));
424            } else {
425                // special handling of container Object[]
426                final String id = identityToString(o);
427                if (dejaVu.contains(id)) {
428                    str.append(RECURSION_PREFIX).append(id).append(RECURSION_SUFFIX);
429                } else {
430                    dejaVu.add(id);
431                    final Object[] oArray = (Object[]) o;
432                    str.append('[');
433                    boolean first = true;
434                    for (final Object current : oArray) {
435                        if (first) {
436                            first = false;
437                        } else {
438                            str.append(", ");
439                        }
440                        recursiveDeepToString(current, str, new HashSet<String>(dejaVu));
441                    }
442                    str.append(']');
443                }
444                //str.append(Arrays.deepToString((Object[]) o));
445            }
446        } else if (o instanceof Map) {
447            // special handling of container Map
448            final String id = identityToString(o);
449            if (dejaVu.contains(id)) {
450                str.append(RECURSION_PREFIX).append(id).append(RECURSION_SUFFIX);
451            } else {
452                dejaVu.add(id);
453                final Map<?, ?> oMap = (Map<?, ?>) o;
454                str.append('{');
455                boolean isFirst = true;
456                for (final Object o1 : oMap.entrySet()) {
457                    final Map.Entry<?, ?> current = (Map.Entry<?, ?>) o1;
458                    if (isFirst) {
459                        isFirst = false;
460                    } else {
461                        str.append(", ");
462                    }
463                    final Object key = current.getKey();
464                    final Object value = current.getValue();
465                    recursiveDeepToString(key, str, new HashSet<String>(dejaVu));
466                    str.append('=');
467                    recursiveDeepToString(value, str, new HashSet<String>(dejaVu));
468                }
469                str.append('}');
470            }
471        } else if (o instanceof Collection) {
472            // special handling of container Collection
473            final String id = identityToString(o);
474            if (dejaVu.contains(id)) {
475                str.append(RECURSION_PREFIX).append(id).append(RECURSION_SUFFIX);
476            } else {
477                dejaVu.add(id);
478                final Collection<?> oCol = (Collection<?>) o;
479                str.append('[');
480                boolean isFirst = true;
481                for (final Object anOCol : oCol) {
482                    if (isFirst) {
483                        isFirst = false;
484                    } else {
485                        str.append(", ");
486                    }
487                    recursiveDeepToString(anOCol, str, new HashSet<String>(dejaVu));
488                }
489                str.append(']');
490            }
491        } else if (o instanceof Date) {
492            final Date date = (Date) o;
493            final SimpleDateFormat format = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSSZ");
494            // I'll leave it like this for the moment... this could probably be optimized using ThreadLocal...
495            str.append(format.format(date));
496        } else {
497            // it's just some other Object, we can only use toString().
498            try {
499                str.append(o.toString());
500            } catch (final Throwable t) {
501                str.append(ERROR_PREFIX);
502                str.append(identityToString(o));
503                str.append(ERROR_SEPARATOR);
504                final String msg = t.getMessage();
505                final String className = t.getClass().getName();
506                str.append(className);
507                if (!className.equals(msg)) {
508                    str.append(ERROR_MSG_SEPARATOR);
509                    str.append(msg);
510                }
511                str.append(ERROR_SUFFIX);
512            }
513        }
514    }
515
516    /**
517     * This method returns the same as if Object.toString() would not have been
518     * overridden in obj.
519     * <p>
520     * Note that this isn't 100% secure as collisions can always happen with hash codes.
521     * </p>
522     * <p>
523     * Copied from Object.hashCode():
524     * </p>
525     * <blockquote>
526     * As much as is reasonably practical, the hashCode method defined by
527     * class {@code Object} does return distinct integers for distinct
528     * objects. (This is typically implemented by converting the internal
529     * address of the object into an integer, but this implementation
530     * technique is not required by the Java&#8482; programming language.)
531     * </blockquote>
532     *
533     * @param obj the Object that is to be converted into an identity string.
534     * @return the identity string as also defined in Object.toString()
535     */
536    public static String identityToString(final Object obj) {
537        if (obj == null) {
538            return null;
539        }
540        return obj.getClass().getName() + '@' + Integer.toHexString(System.identityHashCode(obj));
541    }
542
543    @Override
544    public String toString() {
545        return "ParameterizedMessage[messagePattern=" + messagePattern + ", stringArgs=" +
546            Arrays.toString(stringArgs) + ", throwable=" + throwable + ']';
547    }
548}