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 = parseArguments(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 = parseArguments(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[] parseArguments(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 }