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