View Javadoc

    1   /*
    2    * Licensed to the Apache Software Foundation (ASF) under one or more
    3    * contributor license agreements. See the NOTICE file distributed with
    4    * this work for additional information regarding copyright ownership.
    5    * The ASF licenses this file to You under the Apache license, Version 2.0
    6    * (the "License"); you may not use this file except in compliance with
    7    * the License. You may obtain a copy of the License at
    8    *
    9    *      http://www.apache.org/licenses/LICENSE-2.0
    10   *
    11   * Unless required by applicable law or agreed to in writing, software
    12   * distributed under the License is distributed on an "AS IS" BASIS,
    13   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    14   * See the license for the specific language governing permissions and
    15   * limitations under the license.
    16   */
    17  package org.apache.logging.log4j.message;
    18
    19  import java.text.SimpleDateFormat;
    20  import java.util.Arrays;
    21  import java.util.Collection;
    22  import java.util.Date;
    23  import java.util.HashSet;
    24  import java.util.Map;
    25  import java.util.Set;
    26
    27  /**
    28   * Handles messages that consist of a format string containing '{}' to represent each replaceable token, and
    29   * the parameters.
    30   * <p>
    31   * This class was originally written for <a href="http://lilithapp.com/">Lilith</a> by Joern Huxhorn where it is
    32   * licensed under the LGPL. It has been relicensed here with his permission providing that this attribution remain.
    33   * </p>
    34   */
    35  public class ParameterizedMessage implements Message {
    36
    37      /**
    38       * Prefix for recursion.
    39       */
    40      public static final String RECURSION_PREFIX = "[...";
    41      /**
    42       * Suffix for recursion.
    43       */
    44      public static final String RECURSION_SUFFIX = "...]";
    45
    46      /**
    47       * Prefix for errors.
    48       */
    49      public static final String ERROR_PREFIX = "[!!!";
    50      /**
    51       * Separator for errors.
    52       */
    53      public static final String ERROR_SEPARATOR = "=>";
    54      /**
    55       * Separator for error messages.
    56       */
    57      public static final String ERROR_MSG_SEPARATOR = ":";
    58      /**
    59       * Suffix for errors.
    60       */
    61      public static final String ERROR_SUFFIX = "!!!]";
    62
    63      private static final long serialVersionUID = -665975803997290697L;
    64
    65      private static final int HASHVAL = 31;
    66
    67      private static final char DELIM_START = '{';
    68      private static final char DELIM_STOP = '}';
    69      private static final char ESCAPE_CHAR = '\\';
    70
    71      private final String messagePattern;
    72      private final String[] stringArgs;
    73      private transient Object[] argArray;
    74      private transient String formattedMessage;
    75      private transient Throwable throwable;
    76
    77      /**
    78       * Creates a parameterized message.
    79       * @param messagePattern The message "format" string. This will be a String containing "{}" placeholders
    80       * where parameters should be substituted.
    81       * @param stringArgs The arguments for substitution.
    82       * @param throwable A Throwable.
    83       */
    84      public ParameterizedMessage(final String messagePattern, final String[] stringArgs, final Throwable throwable) {
    85          this.messagePattern = messagePattern;
    86          this.stringArgs = stringArgs;
    87          this.throwable = throwable;
    88      }
    89
    90      /**
    91       * Creates a parameterized message.
    92       * @param messagePattern The message "format" string. This will be a String containing "{}" placeholders
    93       * where parameters should be substituted.
    94       * @param objectArgs The arguments for substitution.
    95       * @param throwable A Throwable.
    96       */
    97      public ParameterizedMessage(final String messagePattern, final Object[] objectArgs, final Throwable throwable) {
    98          this.messagePattern = messagePattern;
    99          this.throwable = throwable;
    100         this.stringArgs = argumentsToStrings(objectArgs);
    101     }
    102
    103     /**
    104      * Constructs a ParameterizedMessage which contains the arguments converted to String as well as an optional
    105      * Throwable.
    106      *
    107      * <p>If the last argument is a Throwable and is NOT used up by a placeholder in the message pattern it is returned
    108      * in {@link #getThrowable()} and won't be contained in the created String[].
    109      * If it is used up {@link #getThrowable()} will return null even if the last argument was a Throwable!</p>
    110      *
    111      * @param messagePattern the message pattern that to be checked for placeholders.
    112      * @param arguments      the argument array to be converted.
    113      */
    114     public ParameterizedMessage(final String messagePattern, final Object[] arguments) {
    115         this.messagePattern = messagePattern;
    116         this.stringArgs = argumentsToStrings(arguments);
    117     }
    118
    119     /**
    120      * Constructor with a pattern and a single parameter.
    121      * @param messagePattern The message pattern.
    122      * @param arg The parameter.
    123      */
    124     public ParameterizedMessage(final String messagePattern, final Object arg) {
    125         this(messagePattern, new Object[]{arg});
    126     }
    127
    128     /**
    129      * Constructor with a pattern and two parameters.
    130      * @param messagePattern The message pattern.
    131      * @param arg1 The first parameter.
    132      * @param arg2 The second parameter.
    133      */
    134     public ParameterizedMessage(final String messagePattern, final Object arg1, final Object arg2) {
    135         this(messagePattern, new Object[]{arg1, arg2});
    136     }
    137
    138     private String[] argumentsToStrings(final Object[] arguments) {
    139         if (arguments == null) {
    140             return null;
    141         }
    142         final int argsCount = countArgumentPlaceholders(messagePattern);
    143         int resultArgCount = arguments.length;
    144         if (argsCount < arguments.length && throwable == null && arguments[arguments.length - 1] instanceof Throwable) {
    145             throwable = (Throwable) arguments[arguments.length - 1];
    146             resultArgCount--;
    147         }
    148         argArray = new Object[resultArgCount];
    149         System.arraycopy(arguments, 0, argArray, 0, resultArgCount);
    150
    151         String[] strArgs;
    152         if (argsCount == 1 && throwable == null && arguments.length > 1) {
    153             // special case
    154             strArgs = new String[1];
    155             strArgs[0] = deepToString(arguments);
    156         } else {
    157             strArgs = new String[resultArgCount];
    158             for (int i = 0; i < strArgs.length; i++) {
    159                 strArgs[i] = deepToString(arguments[i]);
    160             }
    161         }
    162         return strArgs;
    163     }
    164
    165     /**
    166      * Returns the formatted message.
    167      * @return the formatted message.
    168      */
    169     @Override
    170     public String getFormattedMessage() {
    171         if (formattedMessage == null) {
    172             formattedMessage = formatMessage(messagePattern, stringArgs);
    173         }
    174         return formattedMessage;
    175     }
    176
    177     /**
    178      * Returns the message pattern.
    179      * @return the message pattern.
    180      */
    181     @Override
    182     public String getFormat() {
    183         return messagePattern;
    184     }
    185
    186     /**
    187      * Returns the message parameters.
    188      * @return the message parameters.
    189      */
    190     @Override
    191     public Object[] getParameters() {
    192         if (argArray != null) {
    193             return argArray;
    194         }
    195         return stringArgs;
    196     }
    197
    198     /**
    199      * Returns the Throwable that was given as the last argument, if any.
    200      * It will not survive serialization. The Throwable exists as part of the message
    201      * primarily so that it can be extracted from the end of the list of parameters
    202      * and then be added to the LogEvent. As such, the Throwable in the event should
    203      * not be used once the LogEvent has been constructed.
    204      *
    205      * @return the Throwable, if any.
    206      */
    207     @Override
    208     public Throwable getThrowable() {
    209         return throwable;
    210     }
    211
    212     protected String formatMessage(final String msgPattern, final String[] sArgs) {
    213         return 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 }