    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     */
    017    package org.apache.logging.log4j.message;
    018
    019    import java.text.SimpleDateFormat;
    020    import java.util.Arrays;
    021    import java.util.Collection;
    022    import java.util.Date;
    023    import java.util.HashSet;
    024    import java.util.Map;
    025    import 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     */
    035    public 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    }