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 Lillith (http://mac.freshmeat.net/projects/lilith-viewer) by
32   * Joern Huxhorn where it is licensed under the LGPL. It has been relicensed here with his permission
33   * providing that this attribution remain.
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 String[] stringArgs;
73      private transient Object[] argArray;
74      private transient String formattedMessage;
75      private transient Throwable throwable;
76  
77      /**
78       * Create the parameterizedMessage.
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      public ParameterizedMessage(final String messagePattern, final Object[] arguments, final Throwable throwable) {
91          this.messagePattern = messagePattern;
92          this.throwable = throwable;
93          if (arguments != null) {
94              parseArguments(arguments);
95          }
96      }
97  
98      /**
99       * <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 }