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.io.Serializable;
20  import java.text.SimpleDateFormat;
21  import java.util.Arrays;
22  import java.util.Collection;
23  import java.util.Date;
24  import java.util.HashSet;
25  import java.util.Map;
26  import java.util.Set;
27  
28  /**
29   * Handles messages that consist of a format string containing '{}' to represent each replaceable token, and
30   * the parameters.
31   * <p/>
32   * This class was originally written for Lillith (http://mac.freshmeat.net/projects/lilith-viewer) by
33   * Joern Huxhorn where it is licensed under the LGPL. It has been relicensed here with his permission
34   * providing that this attribution remain.
35   */
36  public class ParameterizedMessage implements Message, Serializable {
37  
38      /**
39       * Prefix for recursion.
40       */
41      public static final String RECURSION_PREFIX = "[...";
42      /**
43       * Suffix for recursion.
44       */
45      public static final String RECURSION_SUFFIX = "...]";
46  
47      /**
48       * Prefix for errors.
49       */
50      public static final String ERROR_PREFIX = "[!!!";
51      /**
52       * Separator for errors.
53       */
54      public static final String ERROR_SEPARATOR = "=>";
55      /**
56       * Separator for error messages.
57       */
58      public static final String ERROR_MSG_SEPARATOR = ":";
59      /**
60       * Suffix for errors.
61       */
62      public static final String ERROR_SUFFIX = "!!!]";
63  
64      private static final long serialVersionUID = -665975803997290697L;
65  
66      private static final int HASHVAL = 31;
67  
68      private static final char DELIM_START = '{';
69      private static final char DELIM_STOP = '}';
70      private static final char ESCAPE_CHAR = '\\';
71  
72      private String messagePattern;
73      private String[] stringArgs;
74      private transient Object[] argArray;
75      private transient String formattedMessage;
76      private transient Throwable throwable;
77  
78      /**
79       * Create the ParameterizedMessage.
80       */
81      public ParameterizedMessage() {
82          this(null, null, null);
83      }
84  
85      /**
86       * Create the parameterizedMessage.
87       * @param messagePattern The message "format" string. This will be a String containing "{}" placeholders
88       * where parameters should be substituted.
89       * @param stringArgs The arguments for substitution.
90       * @param throwable A Throwable.
91       */
92      public ParameterizedMessage(String messagePattern, String[] stringArgs, Throwable throwable) {
93          this.messagePattern = messagePattern;
94          this.stringArgs = stringArgs;
95          this.throwable = throwable;
96      }
97  
98      public ParameterizedMessage(String messagePattern, Object[] arguments, Throwable throwable) {
99          this.messagePattern = messagePattern;
100         this.throwable = throwable;
101         if (arguments != null) {
102             parseArguments(arguments);
103         }
104     }
105 
106     /**
107      * <p>This method returns a ParameterizedMessage which contains the arguments converted to String
108      * as well as an optional Throwable.</p>
109      * <p/>
110      * <p>If the last argument is a Throwable and is NOT used up by a placeholder in the message pattern it is returned
111      * in ParameterizedMessage.getThrowable() and won't be contained in the created String[].<br/>
112      * If it is used up ParameterizedMessage.getThrowable() will return null even if the last argument was a
113      * Throwable!</p>
114      *
115      * @param messagePattern the message pattern that to be checked for placeholders.
116      * @param arguments      the argument array to be converted.
117      */
118     public ParameterizedMessage(String messagePattern, Object[] arguments) {
119         this.messagePattern = messagePattern;
120         if (arguments == null) {
121             return;
122         }
123         parseArguments(arguments);
124     }
125 
126     /**
127      * Constructor with a pattern and a single parameter.
128      * @param messagePattern The message pattern.
129      * @param arg The parameter.
130      */
131     public ParameterizedMessage(String messagePattern, Object arg) {
132         this(messagePattern, new Object[]{arg});
133     }
134 
135     /**
136      * Constructor with a pattern and two parameters.
137      * @param messagePattern The message pattern.
138      * @param arg1 The first parameter.
139      * @param arg2 The second parameter.
140      */
141     public ParameterizedMessage(String messagePattern, Object arg1, Object arg2) {
142         this(messagePattern, new Object[]{arg1, arg2});
143     }
144 
145     private void parseArguments(Object[] arguments) {
146         int argsCount = countArgumentPlaceholders(messagePattern);
147         int resultArgCount = arguments.length;
148         if (argsCount < arguments.length) {
149             if (throwable == null && arguments[arguments.length - 1] instanceof Throwable) {
150                 throwable = (Throwable) arguments[arguments.length - 1];
151                 resultArgCount--;
152             }
153         }
154         argArray = new Object[resultArgCount];
155         for (int i = 0; i < resultArgCount; ++i) {
156             argArray[i] = arguments[i];
157         }
158 
159         if (argsCount == 1 && throwable == null && arguments.length > 1) {
160             // special case
161             stringArgs = new String[1];
162             stringArgs[0] = deepToString(arguments);
163         } else {
164             stringArgs = new String[resultArgCount];
165             for (int i = 0; i < stringArgs.length; i++) {
166                 stringArgs[i] = deepToString(arguments[i]);
167             }
168         }
169     }
170 
171     /**
172      * Return the formatted message.
173      * @return the formatted message.
174      */
175     public String getFormattedMessage() {
176         if (formattedMessage == null) {
177             formattedMessage = formatMessage(messagePattern, stringArgs);
178         }
179         return formattedMessage;
180     }
181 
182     /**
183      * Returns the message pattern.
184      * @return the message pattern.
185      */
186     public String getMessageFormat() {
187         return messagePattern;
188     }
189 
190     /**
191      * Set the message pattern.
192      * @param messagePattern The message pattern.
193      */
194     public void setMessageFormat(String messagePattern) {
195         this.messagePattern = messagePattern;
196         this.formattedMessage = null;
197     }
198 
199     /**
200      * Returns the message parameters.
201      * @return the message parameters.
202      */
203     public Object[] getParameters() {
204         if (argArray != null) {
205             return argArray;
206         }
207         return stringArgs;
208     }
209 
210     /**
211      * Sets the parameters for the message.
212      * @param parameters The parameters.
213      */
214     public void setParameters(String[] parameters) {
215         this.stringArgs = parameters;
216         this.formattedMessage = null;
217     }
218 
219     /**
220      * Sets the parameters for the message.
221      * @param parameters The parameters.
222      */
223     public void setParameters(Object[] parameters) {
224         parseArguments(parameters);
225         this.formattedMessage = null;
226     }
227 
228     /**
229      * Set the Throwable for the message.
230      * @param throwable The Throwable.
231      */
232     public void setThrowable(Throwable throwable) {
233         this.throwable = throwable;
234     }
235 
236     /**
237      * Returns the Throwable that was given as the last argument, if any.
238      * It will not survive serialization. The Throwable exists as part of the message
239      * primarily so that it can be extracted from the end of the list of parameters
240      * and then be added to the LogEvent. As such, the Throwable in the event should
241      * not be used once the LogEvent has been constructed.
242      *
243      * @return the Throwable, if any.
244      */
245     public Throwable getThrowable() {
246         return throwable;
247     }
248 
249     protected String formatMessage(String msgPattern, String[] sArgs) {
250         return format(msgPattern, sArgs);
251     }
252 
253     public boolean equals(Object o) {
254         if (this == o) {
255             return true;
256         }
257         if (o == null || getClass() != o.getClass()) {
258             return false;
259         }
260 
261         ParameterizedMessage that = (ParameterizedMessage) o;
262 
263         if (messagePattern != null ? !messagePattern.equals(that.messagePattern) : that.messagePattern != null) {
264             return false;
265         }
266         if (!Arrays.equals(stringArgs, that.stringArgs)) {
267             return false;
268         }
269         //if (throwable != null ? !throwable.equals(that.throwable) : that.throwable != null) return false;
270 
271         return true;
272     }
273 
274     public int hashCode() {
275         int result = messagePattern != null ? messagePattern.hashCode() : 0;
276         result = HASHVAL * result + (stringArgs != null ? Arrays.hashCode(stringArgs) : 0);
277         return result;
278     }
279 
280     /**
281      * Replace placeholders in the given messagePattern with arguments.
282      *
283      * @param messagePattern the message pattern containing placeholders.
284      * @param arguments      the arguments to be used to replace placeholders.
285      * @return the formatted message.
286      */
287     public static String format(String messagePattern, Object[] arguments) {
288         if (messagePattern == null || arguments == null || arguments.length == 0) {
289             return messagePattern;
290         }
291 
292         StringBuilder result = new StringBuilder();
293         int escapeCounter = 0;
294         int currentArgument = 0;
295         for (int i = 0; i < messagePattern.length(); i++) {
296             char curChar = messagePattern.charAt(i);
297             if (curChar == ESCAPE_CHAR) {
298                 escapeCounter++;
299             } else {
300                 if (curChar == DELIM_START) {
301                     if (i < messagePattern.length() - 1) {
302                         if (messagePattern.charAt(i + 1) == DELIM_STOP) {
303                             // write escaped escape chars
304                             int escapedEscapes = escapeCounter / 2;
305                             for (int j = 0; j < escapedEscapes; j++) {
306                                 result.append(ESCAPE_CHAR);
307                             }
308 
309                             if (escapeCounter % 2 == 1) {
310                                 // i.e. escaped
311                                 // write escaped escape chars
312                                 result.append(DELIM_START);
313                                 result.append(DELIM_STOP);
314                             } else {
315                                 // unescaped
316                                 if (currentArgument < arguments.length) {
317                                     result.append(arguments[currentArgument]);
318                                 } else {
319                                     result.append(DELIM_START).append(DELIM_STOP);
320                                 }
321                                 currentArgument++;
322                             }
323                             i++;
324                             escapeCounter = 0;
325                             continue;
326                         }
327                     }
328                 }
329                 // any other char beside ESCAPE or DELIM_START/STOP-combo
330                 // write unescaped escape chars
331                 if (escapeCounter > 0) {
332                     for (int j = 0; j < escapeCounter; j++) {
333                         result.append(ESCAPE_CHAR);
334                     }
335                     escapeCounter = 0;
336                 }
337                 result.append(curChar);
338             }
339         }
340         return result.toString();
341     }
342 
343     /**
344      * Counts the number of unescaped placeholders in the given messagePattern.
345      *
346      * @param messagePattern the message pattern to be analyzed.
347      * @return the number of unescaped placeholders.
348      */
349     public static int countArgumentPlaceholders(String messagePattern) {
350         if (messagePattern == null) {
351             return 0;
352         }
353 
354         int delim = messagePattern.indexOf(DELIM_START);
355 
356         if (delim == -1) {
357             // special case, no placeholders at all.
358             return 0;
359         }
360         int result = 0;
361         boolean isEscaped = false;
362         for (int i = 0; i < messagePattern.length(); i++) {
363             char curChar = messagePattern.charAt(i);
364             if (curChar == ESCAPE_CHAR) {
365                 isEscaped = !isEscaped;
366             } else if (curChar == DELIM_START) {
367                 if (!isEscaped) {
368                     if (i < messagePattern.length() - 1) {
369                         if (messagePattern.charAt(i + 1) == DELIM_STOP) {
370                             result++;
371                             i++;
372                         }
373                     }
374                 }
375                 isEscaped = false;
376             } else {
377                 isEscaped = false;
378             }
379         }
380         return result;
381     }
382 
383     /**
384      * This method performs a deep toString of the given Object.
385      * Primitive arrays are converted using their respective Arrays.toString methods while
386      * special handling is implemented for "container types", i.e. Object[], Map and Collection because those could
387      * contain themselves.
388      * <p/>
389      * It should be noted that neither AbstractMap.toString() nor AbstractCollection.toString() implement such a
390      * behavior. They only check if the container is directly contained in itself, but not if a contained container
391      * contains the original one. Because of that, Arrays.toString(Object[]) isn't safe either.
392      * Confusing? Just read the last paragraph again and check the respective toString() implementation.
393      * <p/>
394      * This means, in effect, that logging would produce a usable output even if an ordinary System.out.println(o)
395      * would produce a relatively hard-to-debug StackOverflowError.
396      * @param o The object.
397      * @return The String representation.
398      */
399     public static String deepToString(Object o) {
400         if (o == null) {
401             return null;
402         }
403         if (o instanceof String) {
404             return (String) o;
405         }
406         StringBuilder str = new StringBuilder();
407         Set<String> dejaVu = new HashSet<String>(); // that's actually a neat name ;)
408         recursiveDeepToString(o, str, dejaVu);
409         return str.toString();
410     }
411 
412     /**
413      * This method performs a deep toString of the given Object.
414      * Primitive arrays are converted using their respective Arrays.toString methods while
415      * special handling is implemented for "container types", i.e. Object[], Map and Collection because those could
416      * contain themselves.
417      * <p/>
418      * dejaVu is used in case of those container types to prevent an endless recursion.
419      * <p/>
420      * It should be noted that neither AbstractMap.toString() nor AbstractCollection.toString() implement such a
421      * behavior.
422      * They only check if the container is directly contained in itself, but not if a contained container contains the
423      * original one. Because of that, Arrays.toString(Object[]) isn't safe either.
424      * Confusing? Just read the last paragraph again and check the respective toString() implementation.
425      * <p/>
426      * This means, in effect, that logging would produce a usable output even if an ordinary System.out.println(o)
427      * would produce a relatively hard-to-debug StackOverflowError.
428      *
429      * @param o      the Object to convert into a String
430      * @param str    the StringBuilder that o will be appended to
431      * @param dejaVu a list of container identities that were already used.
432      */
433     private static void recursiveDeepToString(Object o, StringBuilder str, Set<String> dejaVu) {
434         if (o == null) {
435             str.append("null");
436             return;
437         }
438         if (o instanceof String) {
439             str.append(o);
440             return;
441         }
442 
443         Class oClass = o.getClass();
444         if (oClass.isArray()) {
445             if (oClass == byte[].class) {
446                 str.append(Arrays.toString((byte[]) o));
447             } else if (oClass == short[].class) {
448                 str.append(Arrays.toString((short[]) o));
449             } else if (oClass == int[].class) {
450                 str.append(Arrays.toString((int[]) o));
451             } else if (oClass == long[].class) {
452                 str.append(Arrays.toString((long[]) o));
453             } else if (oClass == float[].class) {
454                 str.append(Arrays.toString((float[]) o));
455             } else if (oClass == double[].class) {
456                 str.append(Arrays.toString((double[]) o));
457             } else if (oClass == boolean[].class) {
458                 str.append(Arrays.toString((boolean[]) o));
459             } else if (oClass == char[].class) {
460                 str.append(Arrays.toString((char[]) o));
461             } else {
462                 // special handling of container Object[]
463                 String id = identityToString(o);
464                 if (dejaVu.contains(id)) {
465                     str.append(RECURSION_PREFIX).append(id).append(RECURSION_SUFFIX);
466                 } else {
467                     dejaVu.add(id);
468                     Object[] oArray = (Object[]) o;
469                     str.append("[");
470                     boolean first = true;
471                     for (Object current : oArray) {
472                         if (first) {
473                             first = false;
474                         } else {
475                             str.append(", ");
476                         }
477                         recursiveDeepToString(current, str, new HashSet<String>(dejaVu));
478                     }
479                     str.append("]");
480                 }
481                 //str.append(Arrays.deepToString((Object[]) o));
482             }
483         } else if (o instanceof Map) {
484             // special handling of container Map
485             String id = identityToString(o);
486             if (dejaVu.contains(id)) {
487                 str.append(RECURSION_PREFIX).append(id).append(RECURSION_SUFFIX);
488             } else {
489                 dejaVu.add(id);
490                 Map oMap = (Map) o;
491                 str.append("{");
492                 boolean isFirst = true;
493                 for (Object o1 : oMap.entrySet()) {
494                     Map.Entry current = (Map.Entry) o1;
495                     if (isFirst) {
496                         isFirst = false;
497                     } else {
498                         str.append(", ");
499                     }
500                     Object key = current.getKey();
501                     Object value = current.getValue();
502                     recursiveDeepToString(key, str, new HashSet<String>(dejaVu));
503                     str.append("=");
504                     recursiveDeepToString(value, str, new HashSet<String>(dejaVu));
505                 }
506                 str.append("}");
507             }
508         } else if (o instanceof Collection) {
509             // special handling of container Collection
510             String id = identityToString(o);
511             if (dejaVu.contains(id)) {
512                 str.append(RECURSION_PREFIX).append(id).append(RECURSION_SUFFIX);
513             } else {
514                 dejaVu.add(id);
515                 Collection oCol = (Collection) o;
516                 str.append("[");
517                 boolean isFirst = true;
518                 for (Object anOCol : oCol) {
519                     if (isFirst) {
520                         isFirst = false;
521                     } else {
522                         str.append(", ");
523                     }
524                     recursiveDeepToString(anOCol, str, new HashSet<String>(dejaVu));
525                 }
526                 str.append("]");
527             }
528         } else if (o instanceof Date) {
529             Date date = (Date) o;
530             SimpleDateFormat format = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSSZ");
531             // I'll leave it like this for the moment... this could probably be optimized using ThreadLocal...
532             str.append(format.format(date));
533         } else {
534             // it's just some other Object, we can only use toString().
535             try {
536                 str.append(o.toString());
537             } catch (Throwable t) {
538                 str.append(ERROR_PREFIX);
539                 str.append(identityToString(o));
540                 str.append(ERROR_SEPARATOR);
541                 String msg = t.getMessage();
542                 String className = t.getClass().getName();
543                 str.append(className);
544                 if (!className.equals(msg)) {
545                     str.append(ERROR_MSG_SEPARATOR);
546                     str.append(msg);
547                 }
548                 str.append(ERROR_SUFFIX);
549             }
550         }
551     }
552 
553     /**
554      * This method returns the same as if Object.toString() would not have been
555      * overridden in obj.
556      * <p/>
557      * Note that this isn't 100% secure as collisions can always happen with hash codes.
558      * <p/>
559      * Copied from Object.hashCode():
560      * As much as is reasonably practical, the hashCode method defined by
561      * class <tt>Object</tt> does return distinct integers for distinct
562      * objects. (This is typically implemented by converting the internal
563      * address of the object into an integer, but this implementation
564      * technique is not required by the
565      * Java<font size="-2"><sup>TM</sup></font>
566      * programming language.)
567      *
568      * @param obj the Object that is to be converted into an identity string.
569      * @return the identity string as also defined in Object.toString()
570      */
571     public static String identityToString(Object obj) {
572         if (obj == null) {
573             return null;
574         }
575         return obj.getClass().getName() + "@" + Integer.toHexString(System.identityHashCode(obj));
576     }
577 
578     public String toString() {
579         return "ParameterizedMessage[messagePattern=" + messagePattern + ", stringArgs=" +
580             Arrays.toString(stringArgs) + ", throwable=" + throwable + "]";
581     }
582 }