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  import org.apache.logging.log4j.util.StringBuilderFormattable;
28  
29  /**
30   * Supports parameter formatting as used in ParameterizedMessage and ReusableParameterizedMessage.
31   */
32  final class ParameterFormatter {
33      /**
34       * Prefix for recursion.
35       */
36      static final String RECURSION_PREFIX = "[...";
37      /**
38       * Suffix for recursion.
39       */
40      static final String RECURSION_SUFFIX = "...]";
41  
42      /**
43       * Prefix for errors.
44       */
45      static final String ERROR_PREFIX = "[!!!";
46      /**
47       * Separator for errors.
48       */
49      static final String ERROR_SEPARATOR = "=>";
50      /**
51       * Separator for error messages.
52       */
53      static final String ERROR_MSG_SEPARATOR = ":";
54      /**
55       * Suffix for errors.
56       */
57      static final String ERROR_SUFFIX = "!!!]";
58  
59      private static final char DELIM_START = '{';
60      private static final char DELIM_STOP = '}';
61      private static final char ESCAPE_CHAR = '\\';
62  
63      private static ThreadLocal<SimpleDateFormat> threadLocalSimpleDateFormat = new ThreadLocal<>();
64  
65      private ParameterFormatter() {
66      }
67  
68      /**
69       * Counts the number of unescaped placeholders in the given messagePattern.
70       *
71       * @param messagePattern the message pattern to be analyzed.
72       * @return the number of unescaped placeholders.
73       */
74      static int countArgumentPlaceholders(final String messagePattern) {
75          if (messagePattern == null) {
76              return 0;
77          }
78          final int length = messagePattern.length();
79          int result = 0;
80          boolean isEscaped = false;
81          for (int i = 0; i < length - 1; i++) {
82              final char curChar = messagePattern.charAt(i);
83              if (curChar == ESCAPE_CHAR) {
84                  isEscaped = !isEscaped;
85              } else if (curChar == DELIM_START) {
86                  if (!isEscaped && messagePattern.charAt(i + 1) == DELIM_STOP) {
87                      result++;
88                      i++;
89                  }
90                  isEscaped = false;
91              } else {
92                  isEscaped = false;
93              }
94          }
95          return result;
96      }
97  
98      /**
99       * Counts the number of unescaped placeholders in the given messagePattern.
100      *
101      * @param messagePattern the message pattern to be analyzed.
102      * @return the number of unescaped placeholders.
103      */
104     static int countArgumentPlaceholders2(final String messagePattern, final int[] indices) {
105         if (messagePattern == null) {
106             return 0;
107         }
108         final int length = messagePattern.length();
109         int result = 0;
110         boolean isEscaped = false;
111         for (int i = 0; i < length - 1; i++) {
112             final char curChar = messagePattern.charAt(i);
113             if (curChar == ESCAPE_CHAR) {
114                 isEscaped = !isEscaped;
115                 indices[0] = -1; // escaping means fast path is not available...
116                 result++;
117             } else if (curChar == DELIM_START) {
118                 if (!isEscaped && messagePattern.charAt(i + 1) == DELIM_STOP) {
119                     indices[result] = i;
120                     result++;
121                     i++;
122                 }
123                 isEscaped = false;
124             } else {
125                 isEscaped = false;
126             }
127         }
128         return result;
129     }
130 
131     /**
132      * Counts the number of unescaped placeholders in the given messagePattern.
133      *
134      * @param messagePattern the message pattern to be analyzed.
135      * @return the number of unescaped placeholders.
136      */
137     static int countArgumentPlaceholders3(final char[] messagePattern, final int length, final int[] indices) {
138         int result = 0;
139         boolean isEscaped = false;
140         for (int i = 0; i < length - 1; i++) {
141             final char curChar = messagePattern[i];
142             if (curChar == ESCAPE_CHAR) {
143                 isEscaped = !isEscaped;
144             } else if (curChar == DELIM_START) {
145                 if (!isEscaped && messagePattern[i + 1] == DELIM_STOP) {
146                     indices[result] = i;
147                     result++;
148                     i++;
149                 }
150                 isEscaped = false;
151             } else {
152                 isEscaped = false;
153             }
154         }
155         return result;
156     }
157 
158     /**
159      * Replace placeholders in the given messagePattern with arguments.
160      *
161      * @param messagePattern the message pattern containing placeholders.
162      * @param arguments      the arguments to be used to replace placeholders.
163      * @return the formatted message.
164      */
165     static String format(final String messagePattern, final Object[] arguments) {
166         final StringBuilder result = new StringBuilder();
167         final int argCount = arguments == null ? 0 : arguments.length;
168         formatMessage(result, messagePattern, arguments, argCount);
169         return result.toString();
170     }
171 
172     /**
173      * Replace placeholders in the given messagePattern with arguments.
174      *
175      * @param buffer the buffer to write the formatted message into
176      * @param messagePattern the message pattern containing placeholders.
177      * @param arguments      the arguments to be used to replace placeholders.
178      */
179     static void formatMessage2(final StringBuilder buffer, final String messagePattern,
180             final Object[] arguments, final int argCount, final int[] indices) {
181         if (messagePattern == null || arguments == null || argCount == 0) {
182             buffer.append(messagePattern);
183             return;
184         }
185         int previous = 0;
186         for (int i = 0; i < argCount; i++) {
187             buffer.append(messagePattern, previous, indices[i]);
188             previous = indices[i] + 2;
189             recursiveDeepToString(arguments[i], buffer, null);
190         }
191         buffer.append(messagePattern, previous, messagePattern.length());
192     }
193 
194     /**
195      * Replace placeholders in the given messagePattern with arguments.
196      *
197      * @param buffer the buffer to write the formatted message into
198      * @param messagePattern the message pattern containing placeholders.
199      * @param arguments      the arguments to be used to replace placeholders.
200      */
201     static void formatMessage3(final StringBuilder buffer, final char[] messagePattern, final int patternLength,
202             final Object[] arguments, final int argCount, final int[] indices) {
203         if (messagePattern == null || arguments == null || argCount == 0) {
204             buffer.append(messagePattern);
205             return;
206         }
207         int previous = 0;
208         for (int i = 0; i < argCount; i++) {
209             buffer.append(messagePattern, previous, indices[i]);
210             previous = indices[i] + 2;
211             recursiveDeepToString(arguments[i], buffer, null);
212         }
213         buffer.append(messagePattern, previous, patternLength);
214     }
215 
216     /**
217      * Replace placeholders in the given messagePattern with arguments.
218      *
219      * @param buffer the buffer to write the formatted message into
220      * @param messagePattern the message pattern containing placeholders.
221      * @param arguments      the arguments to be used to replace placeholders.
222      */
223     static void formatMessage(final StringBuilder buffer, final String messagePattern,
224             final Object[] arguments, final int argCount) {
225         if (messagePattern == null || arguments == null || argCount == 0) {
226             buffer.append(messagePattern);
227             return;
228         }
229         int escapeCounter = 0;
230         int currentArgument = 0;
231         int i = 0;
232         final int len = messagePattern.length();
233         for (; i < len - 1; i++) { // last char is excluded from the loop
234             final char curChar = messagePattern.charAt(i);
235             if (curChar == ESCAPE_CHAR) {
236                 escapeCounter++;
237             } else {
238                 if (isDelimPair(curChar, messagePattern, i)) { // looks ahead one char
239                     i++;
240 
241                     // write escaped escape chars
242                     writeEscapedEscapeChars(escapeCounter, buffer);
243 
244                     if (isOdd(escapeCounter)) {
245                         // i.e. escaped: write escaped escape chars
246                         writeDelimPair(buffer);
247                     } else {
248                         // unescaped
249                         writeArgOrDelimPair(arguments, argCount, currentArgument, buffer);
250                         currentArgument++;
251                     }
252                 } else {
253                     handleLiteralChar(buffer, escapeCounter, curChar);
254                 }
255                 escapeCounter = 0;
256             }
257         }
258         handleRemainingCharIfAny(messagePattern, len, buffer, escapeCounter, i);
259     }
260 
261     /**
262      * Returns {@code true} if the specified char and the char at {@code curCharIndex + 1} in the specified message
263      * pattern together form a "{}" delimiter pair, returns {@code false} otherwise.
264      */
265     // Profiling showed this method is important to log4j performance. Modify with care!
266     // 22 bytes (allows immediate JVM inlining: < 35 bytes) LOG4J2-1096
267     private static boolean isDelimPair(final char curChar, final String messagePattern, final int curCharIndex) {
268         return curChar == DELIM_START && messagePattern.charAt(curCharIndex + 1) == DELIM_STOP;
269     }
270 
271     /**
272      * Detects whether the message pattern has been fully processed or if an unprocessed character remains and processes
273      * it if necessary, returning the resulting position in the result char array.
274      */
275     // Profiling showed this method is important to log4j performance. Modify with care!
276     // 28 bytes (allows immediate JVM inlining: < 35 bytes) LOG4J2-1096
277     private static void handleRemainingCharIfAny(final String messagePattern, final int len,
278             final StringBuilder buffer, final int escapeCounter, final int i) {
279         if (i == len - 1) {
280             final char curChar = messagePattern.charAt(i);
281             handleLastChar(buffer, escapeCounter, curChar);
282         }
283     }
284 
285     /**
286      * Processes the last unprocessed character and returns the resulting position in the result char array.
287      */
288     // Profiling showed this method is important to log4j performance. Modify with care!
289     // 28 bytes (allows immediate JVM inlining: < 35 bytes) LOG4J2-1096
290     private static void handleLastChar(final StringBuilder buffer, final int escapeCounter, final char curChar) {
291         if (curChar == ESCAPE_CHAR) {
292             writeUnescapedEscapeChars(escapeCounter + 1, buffer);
293         } else {
294             handleLiteralChar(buffer, escapeCounter, curChar);
295         }
296     }
297 
298     /**
299      * Processes a literal char (neither an '\' escape char nor a "{}" delimiter pair) and returns the resulting
300      * position.
301      */
302     // Profiling showed this method is important to log4j performance. Modify with care!
303     // 16 bytes (allows immediate JVM inlining: < 35 bytes) LOG4J2-1096
304     private static void handleLiteralChar(final StringBuilder buffer, final int escapeCounter, final char curChar) {
305         // any other char beside ESCAPE or DELIM_START/STOP-combo
306         // write unescaped escape chars
307         writeUnescapedEscapeChars(escapeCounter, buffer);
308         buffer.append(curChar);
309     }
310 
311     /**
312      * Writes "{}" to the specified result array at the specified position and returns the resulting position.
313      */
314     // Profiling showed this method is important to log4j performance. Modify with care!
315     // 18 bytes (allows immediate JVM inlining: < 35 bytes) LOG4J2-1096
316     private static void writeDelimPair(final StringBuilder buffer) {
317         buffer.append(DELIM_START);
318         buffer.append(DELIM_STOP);
319     }
320 
321     /**
322      * Returns {@code true} if the specified parameter is odd.
323      */
324     // Profiling showed this method is important to log4j performance. Modify with care!
325     // 11 bytes (allows immediate JVM inlining: < 35 bytes) LOG4J2-1096
326     private static boolean isOdd(final int number) {
327         return (number & 1) == 1;
328     }
329 
330     /**
331      * Writes a '\' char to the specified result array (starting at the specified position) for each <em>pair</em> of
332      * '\' escape chars encountered in the message format and returns the resulting position.
333      */
334     // Profiling showed this method is important to log4j performance. Modify with care!
335     // 11 bytes (allows immediate JVM inlining: < 35 bytes) LOG4J2-1096
336     private static void writeEscapedEscapeChars(final int escapeCounter, final StringBuilder buffer) {
337         final int escapedEscapes = escapeCounter >> 1; // divide by two
338         writeUnescapedEscapeChars(escapedEscapes, buffer);
339     }
340 
341     /**
342      * Writes the specified number of '\' chars to the specified result array (starting at the specified position) and
343      * returns the resulting position.
344      */
345     // Profiling showed this method is important to log4j performance. Modify with care!
346     // 20 bytes (allows immediate JVM inlining: < 35 bytes) LOG4J2-1096
347     private static void writeUnescapedEscapeChars(int escapeCounter, final StringBuilder buffer) {
348         while (escapeCounter > 0) {
349             buffer.append(ESCAPE_CHAR);
350             escapeCounter--;
351         }
352     }
353 
354     /**
355      * Appends the argument at the specified argument index (or, if no such argument exists, the "{}" delimiter pair) to
356      * the specified result char array at the specified position and returns the resulting position.
357      */
358     // Profiling showed this method is important to log4j performance. Modify with care!
359     // 25 bytes (allows immediate JVM inlining: < 35 bytes) LOG4J2-1096
360     private static void writeArgOrDelimPair(final Object[] arguments, final int argCount, final int currentArgument,
361             final StringBuilder buffer) {
362         if (currentArgument < argCount) {
363             recursiveDeepToString(arguments[currentArgument], buffer, null);
364         } else {
365             writeDelimPair(buffer);
366         }
367     }
368 
369     /**
370      * This method performs a deep toString of the given Object.
371      * Primitive arrays are converted using their respective Arrays.toString methods while
372      * special handling is implemented for "container types", i.e. Object[], Map and Collection because those could
373      * contain themselves.
374      * <p>
375      * It should be noted that neither AbstractMap.toString() nor AbstractCollection.toString() implement such a
376      * behavior. They only check if the container is directly contained in itself, but not if a contained container
377      * contains the original one. Because of that, Arrays.toString(Object[]) isn't safe either.
378      * Confusing? Just read the last paragraph again and check the respective toString() implementation.
379      * </p>
380      * <p>
381      * This means, in effect, that logging would produce a usable output even if an ordinary System.out.println(o)
382      * would produce a relatively hard-to-debug StackOverflowError.
383      * </p>
384      * @param o The object.
385      * @return The String representation.
386      */
387     static String deepToString(final Object o) {
388         if (o == null) {
389             return null;
390         }
391         if (o instanceof String) {
392             return (String) o;
393         }
394         final StringBuilder str = new StringBuilder();
395         final Set<String> dejaVu = new HashSet<>(); // that's actually a neat name ;)
396         recursiveDeepToString(o, str, dejaVu);
397         return str.toString();
398     }
399 
400     /**
401      * This method performs a deep toString of the given Object.
402      * Primitive arrays are converted using their respective Arrays.toString methods while
403      * special handling is implemented for "container types", i.e. Object[], Map and Collection because those could
404      * contain themselves.
405      * <p>
406      * dejaVu is used in case of those container types to prevent an endless recursion.
407      * </p>
408      * <p>
409      * It should be noted that neither AbstractMap.toString() nor AbstractCollection.toString() implement such a
410      * behavior.
411      * They only check if the container is directly contained in itself, but not if a contained container contains the
412      * original one. Because of that, Arrays.toString(Object[]) isn't safe either.
413      * Confusing? Just read the last paragraph again and check the respective toString() implementation.
414      * </p>
415      * <p>
416      * This means, in effect, that logging would produce a usable output even if an ordinary System.out.println(o)
417      * would produce a relatively hard-to-debug StackOverflowError.
418      * </p>
419      *
420      * @param o      the Object to convert into a String
421      * @param str    the StringBuilder that o will be appended to
422      * @param dejaVu a list of container identities that were already used.
423      */
424     private static void recursiveDeepToString(final Object o, final StringBuilder str, final Set<String> dejaVu) {
425         if (appendSpecialTypes(o, str)) {
426             return;
427         }
428         if (isMaybeRecursive(o)) {
429             appendPotentiallyRecursiveValue(o, str, dejaVu);
430         } else {
431             tryObjectToString(o, str);
432         }
433     }
434 
435     private static boolean appendSpecialTypes(final Object o, final StringBuilder str) {
436         if (o == null || o instanceof String) {
437             str.append((String) o);
438             return true;
439         } else if (o instanceof CharSequence) {
440             str.append((CharSequence) o);
441             return true;
442         } else if (o instanceof StringBuilderFormattable) {
443             ((StringBuilderFormattable) o).formatTo(str);
444             return true;
445         }
446         return appendDate(o, str);
447     }
448 
449     private static boolean appendDate(final Object o, final StringBuilder str) {
450         if (!(o instanceof Date)) {
451             return false;
452         }
453         final Date date = (Date) o;
454         final SimpleDateFormat format = getSimpleDateFormat();
455         str.append(format.format(date));
456         return true;
457     }
458 
459     private static SimpleDateFormat getSimpleDateFormat() {
460         SimpleDateFormat result = threadLocalSimpleDateFormat.get();
461         if (result == null) {
462             result = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSSZ");
463             threadLocalSimpleDateFormat.set(result);
464         }
465         return result;
466     }
467 
468     /**
469      * Returns {@code true} if the specified object is an array, a Map or a Collection.
470      */
471     private static boolean isMaybeRecursive(final Object o) {
472         return o.getClass().isArray() || o instanceof Map || o instanceof Collection;
473     }
474 
475     private static void appendPotentiallyRecursiveValue(final Object o, final StringBuilder str,
476             final Set<String> dejaVu) {
477         final Class<?> oClass = o.getClass();
478         if (oClass.isArray()) {
479             appendArray(o, str, dejaVu, oClass);
480         } else if (o instanceof Map) {
481             appendMap(o, str, dejaVu);
482         } else if (o instanceof Collection) {
483             appendCollection(o, str, dejaVu);
484         }
485     }
486 
487     private static void appendArray(final Object o, final StringBuilder str, Set<String> dejaVu,
488             final Class<?> oClass) {
489         if (oClass == byte[].class) {
490             str.append(Arrays.toString((byte[]) o));
491         } else if (oClass == short[].class) {
492             str.append(Arrays.toString((short[]) o));
493         } else if (oClass == int[].class) {
494             str.append(Arrays.toString((int[]) o));
495         } else if (oClass == long[].class) {
496             str.append(Arrays.toString((long[]) o));
497         } else if (oClass == float[].class) {
498             str.append(Arrays.toString((float[]) o));
499         } else if (oClass == double[].class) {
500             str.append(Arrays.toString((double[]) o));
501         } else if (oClass == boolean[].class) {
502             str.append(Arrays.toString((boolean[]) o));
503         } else if (oClass == char[].class) {
504             str.append(Arrays.toString((char[]) o));
505         } else {
506             if (dejaVu == null) {
507                 dejaVu = new HashSet<>();
508             }
509             // special handling of container Object[]
510             final 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                 final Object[] oArray = (Object[]) o;
516                 str.append('[');
517                 boolean first = true;
518                 for (final Object current : oArray) {
519                     if (first) {
520                         first = false;
521                     } else {
522                         str.append(", ");
523                     }
524                     recursiveDeepToString(current, str, new HashSet<>(dejaVu));
525                 }
526                 str.append(']');
527             }
528             //str.append(Arrays.deepToString((Object[]) o));
529         }
530     }
531 
532     private static void appendMap(final Object o, final StringBuilder str, Set<String> dejaVu) {
533         // special handling of container Map
534         if (dejaVu == null) {
535             dejaVu = new HashSet<>();
536         }
537         final String id = identityToString(o);
538         if (dejaVu.contains(id)) {
539             str.append(RECURSION_PREFIX).append(id).append(RECURSION_SUFFIX);
540         } else {
541             dejaVu.add(id);
542             final Map<?, ?> oMap = (Map<?, ?>) o;
543             str.append('{');
544             boolean isFirst = true;
545             for (final Object o1 : oMap.entrySet()) {
546                 final Map.Entry<?, ?> current = (Map.Entry<?, ?>) o1;
547                 if (isFirst) {
548                     isFirst = false;
549                 } else {
550                     str.append(", ");
551                 }
552                 final Object key = current.getKey();
553                 final Object value = current.getValue();
554                 recursiveDeepToString(key, str, new HashSet<>(dejaVu));
555                 str.append('=');
556                 recursiveDeepToString(value, str, new HashSet<>(dejaVu));
557             }
558             str.append('}');
559         }
560     }
561 
562     private static void appendCollection(final Object o, final StringBuilder str, Set<String> dejaVu) {
563         // special handling of container Collection
564         if (dejaVu == null) {
565             dejaVu = new HashSet<>();
566         }
567         final String id = identityToString(o);
568         if (dejaVu.contains(id)) {
569             str.append(RECURSION_PREFIX).append(id).append(RECURSION_SUFFIX);
570         } else {
571             dejaVu.add(id);
572             final Collection<?> oCol = (Collection<?>) o;
573             str.append('[');
574             boolean isFirst = true;
575             for (final Object anOCol : oCol) {
576                 if (isFirst) {
577                     isFirst = false;
578                 } else {
579                     str.append(", ");
580                 }
581                 recursiveDeepToString(anOCol, str, new HashSet<>(dejaVu));
582             }
583             str.append(']');
584         }
585     }
586 
587     private static void tryObjectToString(final Object o, final StringBuilder str) {
588         // it's just some other Object, we can only use toString().
589         try {
590             str.append(o.toString());
591         } catch (final Throwable t) {
592             handleErrorInObjectToString(o, str, t);
593         }
594     }
595 
596     private static void handleErrorInObjectToString(final Object o, final StringBuilder str, final Throwable t) {
597         str.append(ERROR_PREFIX);
598         str.append(identityToString(o));
599         str.append(ERROR_SEPARATOR);
600         final String msg = t.getMessage();
601         final String className = t.getClass().getName();
602         str.append(className);
603         if (!className.equals(msg)) {
604             str.append(ERROR_MSG_SEPARATOR);
605             str.append(msg);
606         }
607         str.append(ERROR_SUFFIX);
608     }
609 
610     /**
611      * This method returns the same as if Object.toString() would not have been
612      * overridden in obj.
613      * <p>
614      * Note that this isn't 100% secure as collisions can always happen with hash codes.
615      * </p>
616      * <p>
617      * Copied from Object.hashCode():
618      * </p>
619      * <blockquote>
620      * As much as is reasonably practical, the hashCode method defined by
621      * class {@code Object} does return distinct integers for distinct
622      * objects. (This is typically implemented by converting the internal
623      * address of the object into an integer, but this implementation
624      * technique is not required by the Java&#8482; programming language.)
625      * </blockquote>
626      *
627      * @param obj the Object that is to be converted into an identity string.
628      * @return the identity string as also defined in Object.toString()
629      */
630     static String identityToString(final Object obj) {
631         if (obj == null) {
632             return null;
633         }
634         return obj.getClass().getName() + '@' + Integer.toHexString(System.identityHashCode(obj));
635     }
636 
637 }