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