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 Lillith (http://mac.freshmeat.net/projects/lilith-viewer) by
032     * Joern Huxhorn where it is licensed under the LGPL. It has been relicensed here with his permission
033     * providing that this attribution remain.
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 String[] stringArgs;
073        private transient Object[] argArray;
074        private transient String formattedMessage;
075        private transient Throwable throwable;
076    
077        /**
078         * Create the parameterizedMessage.
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        public ParameterizedMessage(final String messagePattern, final Object[] arguments, final Throwable throwable) {
091            this.messagePattern = messagePattern;
092            this.throwable = throwable;
093            if (arguments != null) {
094                parseArguments(arguments);
095            }
096        }
097    
098        /**
099         * <p>This method returns a ParameterizedMessage which contains the arguments converted to String
100         * as well as an optional Throwable.</p>
101         * <p/>
102         * <p>If the last argument is a Throwable and is NOT used up by a placeholder in the message pattern it is returned
103         * in ParameterizedMessage.getThrowable() and won't be contained in the created String[].<br/>
104         * If it is used up ParameterizedMessage.getThrowable() will return null even if the last argument was a
105         * Throwable!</p>
106         *
107         * @param messagePattern the message pattern that to be checked for placeholders.
108         * @param arguments      the argument array to be converted.
109         */
110        public ParameterizedMessage(final String messagePattern, final Object[] arguments) {
111            this.messagePattern = messagePattern;
112            if (arguments != null) {
113                parseArguments(arguments);
114            }
115        }
116    
117        /**
118         * Constructor with a pattern and a single parameter.
119         * @param messagePattern The message pattern.
120         * @param arg The parameter.
121         */
122        public ParameterizedMessage(final String messagePattern, final Object arg) {
123            this(messagePattern, new Object[]{arg});
124        }
125    
126        /**
127         * Constructor with a pattern and two parameters.
128         * @param messagePattern The message pattern.
129         * @param arg1 The first parameter.
130         * @param arg2 The second parameter.
131         */
132        public ParameterizedMessage(final String messagePattern, final Object arg1, final Object arg2) {
133            this(messagePattern, new Object[]{arg1, arg2});
134        }
135    
136        private void parseArguments(final Object[] arguments) {
137            final int argsCount = countArgumentPlaceholders(messagePattern);
138            int resultArgCount = arguments.length;
139            if (argsCount < arguments.length) {
140                if (throwable == null && arguments[arguments.length - 1] instanceof Throwable) {
141                    throwable = (Throwable) arguments[arguments.length - 1];
142                    resultArgCount--;
143                }
144            }
145            argArray = new Object[resultArgCount];
146            for (int i = 0; i < resultArgCount; ++i) {
147                argArray[i] = arguments[i];
148            }
149    
150            if (argsCount == 1 && throwable == null && arguments.length > 1) {
151                // special case
152                stringArgs = new String[1];
153                stringArgs[0] = deepToString(arguments);
154            } else {
155                stringArgs = new String[resultArgCount];
156                for (int i = 0; i < stringArgs.length; i++) {
157                    stringArgs[i] = deepToString(arguments[i]);
158                }
159            }
160        }
161    
162        /**
163         * Returns the formatted message.
164         * @return the formatted message.
165         */
166        @Override
167        public String getFormattedMessage() {
168            if (formattedMessage == null) {
169                formattedMessage = formatMessage(messagePattern, stringArgs);
170            }
171            return formattedMessage;
172        }
173    
174        /**
175         * Returns the message pattern.
176         * @return the message pattern.
177         */
178        @Override
179        public String getFormat() {
180            return messagePattern;
181        }
182    
183        /**
184         * Returns the message parameters.
185         * @return the message parameters.
186         */
187        @Override
188        public Object[] getParameters() {
189            if (argArray != null) {
190                return argArray;
191            }
192            return stringArgs;
193        }
194    
195        /**
196         * Returns the Throwable that was given as the last argument, if any.
197         * It will not survive serialization. The Throwable exists as part of the message
198         * primarily so that it can be extracted from the end of the list of parameters
199         * and then be added to the LogEvent. As such, the Throwable in the event should
200         * not be used once the LogEvent has been constructed.
201         *
202         * @return the Throwable, if any.
203         */
204        @Override
205        public Throwable getThrowable() {
206            return throwable;
207        }
208    
209        protected String formatMessage(final String msgPattern, final String[] sArgs) {
210            return format(msgPattern, sArgs);
211        }
212    
213        @Override
214        public boolean equals(final Object o) {
215            if (this == o) {
216                return true;
217            }
218            if (o == null || getClass() != o.getClass()) {
219                return false;
220            }
221    
222            final ParameterizedMessage that = (ParameterizedMessage) o;
223    
224            if (messagePattern != null ? !messagePattern.equals(that.messagePattern) : that.messagePattern != null) {
225                return false;
226            }
227            if (!Arrays.equals(stringArgs, that.stringArgs)) {
228                return false;
229            }
230            //if (throwable != null ? !throwable.equals(that.throwable) : that.throwable != null) return false;
231    
232            return true;
233        }
234    
235        @Override
236        public int hashCode() {
237            int result = messagePattern != null ? messagePattern.hashCode() : 0;
238            result = HASHVAL * result + (stringArgs != null ? Arrays.hashCode(stringArgs) : 0);
239            return result;
240        }
241    
242        /**
243         * Replace placeholders in the given messagePattern with arguments.
244         *
245         * @param messagePattern the message pattern containing placeholders.
246         * @param arguments      the arguments to be used to replace placeholders.
247         * @return the formatted message.
248         */
249        public static String format(final String messagePattern, final Object[] arguments) {
250            if (messagePattern == null || arguments == null || arguments.length == 0) {
251                return messagePattern;
252            }
253    
254            final StringBuilder result = new StringBuilder();
255            int escapeCounter = 0;
256            int currentArgument = 0;
257            for (int i = 0; i < messagePattern.length(); i++) {
258                final char curChar = messagePattern.charAt(i);
259                if (curChar == ESCAPE_CHAR) {
260                    escapeCounter++;
261                } else {
262                    if (curChar == DELIM_START) {
263                        if (i < messagePattern.length() - 1) {
264                            if (messagePattern.charAt(i + 1) == DELIM_STOP) {
265                                // write escaped escape chars
266                                final int escapedEscapes = escapeCounter / 2;
267                                for (int j = 0; j < escapedEscapes; j++) {
268                                    result.append(ESCAPE_CHAR);
269                                }
270    
271                                if (escapeCounter % 2 == 1) {
272                                    // i.e. escaped
273                                    // write escaped escape chars
274                                    result.append(DELIM_START);
275                                    result.append(DELIM_STOP);
276                                } else {
277                                    // unescaped
278                                    if (currentArgument < arguments.length) {
279                                        result.append(arguments[currentArgument]);
280                                    } else {
281                                        result.append(DELIM_START).append(DELIM_STOP);
282                                    }
283                                    currentArgument++;
284                                }
285                                i++;
286                                escapeCounter = 0;
287                                continue;
288                            }
289                        }
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) {
330                        if (i < messagePattern.length() - 1) {
331                            if (messagePattern.charAt(i + 1) == DELIM_STOP) {
332                                result++;
333                                i++;
334                            }
335                        }
336                    }
337                    isEscaped = false;
338                } else {
339                    isEscaped = false;
340                }
341            }
342            return result;
343        }
344    
345        /**
346         * This method performs a deep toString of the given Object.
347         * Primitive arrays are converted using their respective Arrays.toString methods while
348         * special handling is implemented for "container types", i.e. Object[], Map and Collection because those could
349         * contain themselves.
350         * <p/>
351         * It should be noted that neither AbstractMap.toString() nor AbstractCollection.toString() implement such a
352         * behavior. They only check if the container is directly contained in itself, but not if a contained container
353         * contains the original one. Because of that, Arrays.toString(Object[]) isn't safe either.
354         * Confusing? Just read the last paragraph again and check the respective toString() implementation.
355         * <p/>
356         * This means, in effect, that logging would produce a usable output even if an ordinary System.out.println(o)
357         * would produce a relatively hard-to-debug StackOverflowError.
358         * @param o The object.
359         * @return The String representation.
360         */
361        public static String deepToString(final Object o) {
362            if (o == null) {
363                return null;
364            }
365            if (o instanceof String) {
366                return (String) o;
367            }
368            final StringBuilder str = new StringBuilder();
369            final Set<String> dejaVu = new HashSet<String>(); // that's actually a neat name ;)
370            recursiveDeepToString(o, str, dejaVu);
371            return str.toString();
372        }
373    
374        /**
375         * This method performs a deep toString of the given Object.
376         * Primitive arrays are converted using their respective Arrays.toString methods while
377         * special handling is implemented for "container types", i.e. Object[], Map and Collection because those could
378         * contain themselves.
379         * <p/>
380         * dejaVu is used in case of those container types to prevent an endless recursion.
381         * <p/>
382         * It should be noted that neither AbstractMap.toString() nor AbstractCollection.toString() implement such a
383         * behavior.
384         * They only check if the container is directly contained in itself, but not if a contained container contains the
385         * original one. Because of that, Arrays.toString(Object[]) isn't safe either.
386         * Confusing? Just read the last paragraph again and check the respective toString() implementation.
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         *
391         * @param o      the Object to convert into a String
392         * @param str    the StringBuilder that o will be appended to
393         * @param dejaVu a list of container identities that were already used.
394         */
395        private static void recursiveDeepToString(final Object o, final StringBuilder str, final Set<String> dejaVu) {
396            if (o == null) {
397                str.append("null");
398                return;
399            }
400            if (o instanceof String) {
401                str.append(o);
402                return;
403            }
404    
405            final Class<?> oClass = o.getClass();
406            if (oClass.isArray()) {
407                if (oClass == byte[].class) {
408                    str.append(Arrays.toString((byte[]) o));
409                } else if (oClass == short[].class) {
410                    str.append(Arrays.toString((short[]) o));
411                } else if (oClass == int[].class) {
412                    str.append(Arrays.toString((int[]) o));
413                } else if (oClass == long[].class) {
414                    str.append(Arrays.toString((long[]) o));
415                } else if (oClass == float[].class) {
416                    str.append(Arrays.toString((float[]) o));
417                } else if (oClass == double[].class) {
418                    str.append(Arrays.toString((double[]) o));
419                } else if (oClass == boolean[].class) {
420                    str.append(Arrays.toString((boolean[]) o));
421                } else if (oClass == char[].class) {
422                    str.append(Arrays.toString((char[]) o));
423                } else {
424                    // special handling of container Object[]
425                    final String id = identityToString(o);
426                    if (dejaVu.contains(id)) {
427                        str.append(RECURSION_PREFIX).append(id).append(RECURSION_SUFFIX);
428                    } else {
429                        dejaVu.add(id);
430                        final Object[] oArray = (Object[]) o;
431                        str.append("[");
432                        boolean first = true;
433                        for (final Object current : oArray) {
434                            if (first) {
435                                first = false;
436                            } else {
437                                str.append(", ");
438                            }
439                            recursiveDeepToString(current, str, new HashSet<String>(dejaVu));
440                        }
441                        str.append("]");
442                    }
443                    //str.append(Arrays.deepToString((Object[]) o));
444                }
445            } else if (o instanceof Map) {
446                // special handling of container Map
447                final String id = identityToString(o);
448                if (dejaVu.contains(id)) {
449                    str.append(RECURSION_PREFIX).append(id).append(RECURSION_SUFFIX);
450                } else {
451                    dejaVu.add(id);
452                    final Map<?, ?> oMap = (Map<?, ?>) o;
453                    str.append("{");
454                    boolean isFirst = true;
455                    for (final Object o1 : oMap.entrySet()) {
456                        final Map.Entry<?, ?> current = (Map.Entry<?, ?>) o1;
457                        if (isFirst) {
458                            isFirst = false;
459                        } else {
460                            str.append(", ");
461                        }
462                        final Object key = current.getKey();
463                        final Object value = current.getValue();
464                        recursiveDeepToString(key, str, new HashSet<String>(dejaVu));
465                        str.append("=");
466                        recursiveDeepToString(value, str, new HashSet<String>(dejaVu));
467                    }
468                    str.append("}");
469                }
470            } else if (o instanceof Collection) {
471                // special handling of container Collection
472                final String id = identityToString(o);
473                if (dejaVu.contains(id)) {
474                    str.append(RECURSION_PREFIX).append(id).append(RECURSION_SUFFIX);
475                } else {
476                    dejaVu.add(id);
477                    final Collection<?> oCol = (Collection<?>) o;
478                    str.append("[");
479                    boolean isFirst = true;
480                    for (final Object anOCol : oCol) {
481                        if (isFirst) {
482                            isFirst = false;
483                        } else {
484                            str.append(", ");
485                        }
486                        recursiveDeepToString(anOCol, str, new HashSet<String>(dejaVu));
487                    }
488                    str.append("]");
489                }
490            } else if (o instanceof Date) {
491                final Date date = (Date) o;
492                final SimpleDateFormat format = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSSZ");
493                // I'll leave it like this for the moment... this could probably be optimized using ThreadLocal...
494                str.append(format.format(date));
495            } else {
496                // it's just some other Object, we can only use toString().
497                try {
498                    str.append(o.toString());
499                } catch (final Throwable t) {
500                    str.append(ERROR_PREFIX);
501                    str.append(identityToString(o));
502                    str.append(ERROR_SEPARATOR);
503                    final String msg = t.getMessage();
504                    final String className = t.getClass().getName();
505                    str.append(className);
506                    if (!className.equals(msg)) {
507                        str.append(ERROR_MSG_SEPARATOR);
508                        str.append(msg);
509                    }
510                    str.append(ERROR_SUFFIX);
511                }
512            }
513        }
514    
515        /**
516         * This method returns the same as if Object.toString() would not have been
517         * overridden in obj.
518         * <p/>
519         * Note that this isn't 100% secure as collisions can always happen with hash codes.
520         * <p/>
521         * Copied from Object.hashCode():
522         * As much as is reasonably practical, the hashCode method defined by
523         * class <tt>Object</tt> does return distinct integers for distinct
524         * objects. (This is typically implemented by converting the internal
525         * address of the object into an integer, but this implementation
526         * technique is not required by the
527         * Java<font size="-2"><sup>TM</sup></font>
528         * programming language.)
529         *
530         * @param obj the Object that is to be converted into an identity string.
531         * @return the identity string as also defined in Object.toString()
532         */
533        public static String identityToString(final Object obj) {
534            if (obj == null) {
535                return null;
536            }
537            return obj.getClass().getName() + "@" + Integer.toHexString(System.identityHashCode(obj));
538        }
539    
540        @Override
541        public String toString() {
542            return "ParameterizedMessage[messagePattern=" + messagePattern + ", stringArgs=" +
543                Arrays.toString(stringArgs) + ", throwable=" + throwable + "]";
544        }
545    }