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