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.core.pattern;
18  
19  import java.lang.reflect.Method;
20  import java.lang.reflect.Modifier;
21  import java.util.ArrayList;
22  import java.util.Iterator;
23  import java.util.LinkedHashMap;
24  import java.util.List;
25  import java.util.Map;
26  import java.util.Objects;
27  
28  import org.apache.logging.log4j.Logger;
29  import org.apache.logging.log4j.core.config.Configuration;
30  import org.apache.logging.log4j.core.config.plugins.util.PluginManager;
31  import org.apache.logging.log4j.core.config.plugins.util.PluginType;
32  import org.apache.logging.log4j.core.util.NanoClockFactory;
33  import org.apache.logging.log4j.status.StatusLogger;
34  import org.apache.logging.log4j.util.Strings;
35  
36  /**
37   * Most of the work of the {@link org.apache.logging.log4j.core.layout.PatternLayout} class is delegated to the
38   * PatternParser class.
39   * <p>
40   * It is this class that parses conversion patterns and creates a chained list of {@link PatternConverter
41   * PatternConverters}.
42   */
43  public final class PatternParser {
44      static final String NO_CONSOLE_NO_ANSI = "noConsoleNoAnsi";
45  
46      /**
47       * Escape character for format specifier.
48       */
49      private static final char ESCAPE_CHAR = '%';
50  
51      /**
52       * The states the parser can be in while parsing the pattern.
53       */
54      private enum ParserState {
55          /**
56           * Literal state.
57           */
58          LITERAL_STATE,
59  
60          /**
61           * In converter name state.
62           */
63          CONVERTER_STATE,
64  
65          /**
66           * Dot state.
67           */
68          DOT_STATE,
69  
70          /**
71           * Min state.
72           */
73          MIN_STATE,
74  
75          /**
76           * Max state.
77           */
78          MAX_STATE;
79      }
80  
81      private static final Logger LOGGER = StatusLogger.getLogger();
82  
83      private static final int BUF_SIZE = 32;
84  
85      private static final int DECIMAL = 10;
86  
87      private final Configuration config;
88  
89      private final Map<String, Class<PatternConverter>> converterRules;
90  
91      /**
92       * Constructor.
93       *
94       * @param converterKey
95       *            The type of converters that will be used.
96       */
97      public PatternParser(final String converterKey) {
98          this(null, converterKey, null, null);
99      }
100 
101     /**
102      * Constructor.
103      *
104      * @param config
105      *            The current Configuration.
106      * @param converterKey
107      *            The key to lookup the converters.
108      * @param expected
109      *            The expected base Class of each Converter.
110      */
111     public PatternParser(final Configuration config, final String converterKey, final Class<?> expected) {
112         this(config, converterKey, expected, null);
113     }
114 
115     /**
116      * Constructor.
117      *
118      * @param config
119      *            The current Configuration.
120      * @param converterKey
121      *            The key to lookup the converters.
122      * @param expectedClass
123      *            The expected base Class of each Converter.
124      * @param filterClass
125      *            Filter the returned plugins after calling the plugin manager.
126      */
127     public PatternParser(final Configuration config, final String converterKey, final Class<?> expectedClass,
128             final Class<?> filterClass) {
129         this.config = config;
130         final PluginManager manager = new PluginManager(converterKey);
131         manager.collectPlugins(config == null ? null : config.getPluginPackages());
132         final Map<String, PluginType<?>> plugins = manager.getPlugins();
133         final Map<String, Class<PatternConverter>> converters = new LinkedHashMap<>();
134 
135         for (final PluginType<?> type : plugins.values()) {
136             try {
137                 @SuppressWarnings("unchecked")
138                 final Class<PatternConverter> clazz = (Class<PatternConverter>) type.getPluginClass();
139                 if (filterClass != null && !filterClass.isAssignableFrom(clazz)) {
140                     continue;
141                 }
142                 final ConverterKeys keys = clazz.getAnnotation(ConverterKeys.class);
143                 if (keys != null) {
144                     for (final String key : keys.value()) {
145                         if (converters.containsKey(key)) {
146                             LOGGER.warn("Converter key '{}' is already mapped to '{}'. " +
147                                     "Sorry, Dave, I can't let you do that! Ignoring plugin [{}].",
148                                 key, converters.get(key), clazz);
149                         } else {
150                             converters.put(key, clazz);
151                         }
152                     }
153                 }
154             } catch (final Exception ex) {
155                 LOGGER.error("Error processing plugin " + type.getElementName(), ex);
156             }
157         }
158         converterRules = converters;
159     }
160 
161     public List<PatternFormatter> parse(final String pattern) {
162         return parse(pattern, false, false);
163     }
164 
165     public List<PatternFormatter> parse(final String pattern, final boolean alwaysWriteExceptions,
166             final boolean noConsoleNoAnsi) {
167         final List<PatternFormatter> list = new ArrayList<>();
168         final List<PatternConverter> converters = new ArrayList<>();
169         final List<FormattingInfo> fields = new ArrayList<>();
170 
171         parse(pattern, converters, fields, noConsoleNoAnsi, true);
172 
173         final Iterator<FormattingInfo> fieldIter = fields.iterator();
174         boolean handlesThrowable = false;
175 
176         NanoClockFactory.setMode(NanoClockFactory.Mode.Dummy); // LOG4J2-1074 use dummy clock by default
177         for (final PatternConverter converter : converters) {
178             if (converter instanceof NanoTimePatternConverter) {
179                 // LOG4J2-1074 Switch to actual clock if nanosecond timestamps are required in config.
180                 // LoggerContext will notify known NanoClockFactory users that the configuration has changed.
181                 NanoClockFactory.setMode(NanoClockFactory.Mode.System);
182             }
183             LogEventPatternConverter pc;
184             if (converter instanceof LogEventPatternConverter) {
185                 pc = (LogEventPatternConverter) converter;
186                 handlesThrowable |= pc.handlesThrowable();
187             } else {
188                 pc = new LiteralPatternConverter(config, Strings.EMPTY, true);
189             }
190 
191             FormattingInfo field;
192             if (fieldIter.hasNext()) {
193                 field = fieldIter.next();
194             } else {
195                 field = FormattingInfo.getDefault();
196             }
197             list.add(new PatternFormatter(pc, field));
198         }
199         if (alwaysWriteExceptions && !handlesThrowable) {
200             final LogEventPatternConverter pc = ExtendedThrowablePatternConverter.newInstance(null);
201             list.add(new PatternFormatter(pc, FormattingInfo.getDefault()));
202         }
203         return list;
204     }
205 
206     /**
207      * Extracts the converter identifier found at the given start position.
208      * <p>
209      * After this function returns, the variable i will point to the first char after the end of the converter
210      * identifier.
211      * </p>
212      * <p>
213      * If i points to a char which is not a character acceptable at the start of a unicode identifier, the value null is
214      * returned.
215      * </p>
216      *
217      * @param lastChar
218      *        last processed character.
219      * @param pattern
220      *        format string.
221      * @param i
222      *        current index into pattern format.
223      * @param convBuf
224      *        buffer to receive conversion specifier.
225      * @param currentLiteral
226      *        literal to be output in case format specifier in unrecognized.
227      * @return position in pattern after converter.
228      */
229     private static int extractConverter(final char lastChar, final String pattern, final int start,
230             final StringBuilder convBuf, final StringBuilder currentLiteral) {
231         int i = start;
232         convBuf.setLength(0);
233 
234         // When this method is called, lastChar points to the first character of the
235         // conversion word. For example:
236         // For "%hello" lastChar = 'h'
237         // For "%-5hello" lastChar = 'h'
238         // System.out.println("lastchar is "+lastChar);
239         if (!Character.isUnicodeIdentifierStart(lastChar)) {
240             return i;
241         }
242 
243         convBuf.append(lastChar);
244 
245         while (i < pattern.length() && Character.isUnicodeIdentifierPart(pattern.charAt(i))) {
246             convBuf.append(pattern.charAt(i));
247             currentLiteral.append(pattern.charAt(i));
248             i++;
249         }
250 
251         return i;
252     }
253 
254     /**
255      * Extract options.
256      *
257      * @param pattern
258      *            conversion pattern.
259      * @param i
260      *            start of options.
261      * @param options
262      *            array to receive extracted options
263      * @return position in pattern after options.
264      */
265     private static int extractOptions(final String pattern, final int start, final List<String> options) {
266         int i = start;
267         while (i < pattern.length() && pattern.charAt(i) == '{') {
268             final int begin = i++;
269             int end;
270             int depth = 0;
271             do {
272                 end = pattern.indexOf('}', i);
273                 if (end == -1) {
274                     break;
275                 }
276                 final int next = pattern.indexOf("{", i);
277                 if (next != -1 && next < end) {
278                     i = end + 1;
279                     ++depth;
280                 } else if (depth > 0) {
281                     --depth;
282                 }
283             } while (depth > 0);
284 
285             if (end == -1) {
286                 break;
287             }
288 
289             final String r = pattern.substring(begin + 1, end);
290             options.add(r);
291             i = end + 1;
292         }
293 
294         return i;
295     }
296 
297     /**
298      * Parse a format specifier.
299      *
300      * @param pattern
301      *            pattern to parse.
302      * @param patternConverters
303      *            list to receive pattern converters.
304      * @param formattingInfos
305      *            list to receive field specifiers corresponding to pattern converters.
306      * @param noConsoleNoAnsi
307      *            TODO
308      * @param convertBackslashes if {@code true}, backslash characters are treated as escape characters and character
309      *            sequences like "\" followed by "t" (backslash+t) are converted to special characters like '\t' (tab).
310      */
311     public void parse(final String pattern, final List<PatternConverter> patternConverters,
312             final List<FormattingInfo> formattingInfos, final boolean noConsoleNoAnsi,
313             final boolean convertBackslashes) {
314         Objects.requireNonNull(pattern, "pattern");
315 
316         final StringBuilder currentLiteral = new StringBuilder(BUF_SIZE);
317 
318         final int patternLength = pattern.length();
319         ParserState state = ParserState.LITERAL_STATE;
320         char c;
321         int i = 0;
322         FormattingInfo formattingInfo = FormattingInfo.getDefault();
323 
324         while (i < patternLength) {
325             c = pattern.charAt(i++);
326 
327             switch (state) {
328             case LITERAL_STATE:
329 
330                 // In literal state, the last char is always a literal.
331                 if (i == patternLength) {
332                     currentLiteral.append(c);
333 
334                     continue;
335                 }
336 
337                 if (c == ESCAPE_CHAR) {
338                     // peek at the next char.
339                     switch (pattern.charAt(i)) {
340                     case ESCAPE_CHAR:
341                         currentLiteral.append(c);
342                         i++; // move pointer
343 
344                         break;
345 
346                     default:
347 
348                         if (currentLiteral.length() != 0) {
349                             patternConverters.add(new LiteralPatternConverter(config, currentLiteral.toString(),
350                                     convertBackslashes));
351                             formattingInfos.add(FormattingInfo.getDefault());
352                         }
353 
354                         currentLiteral.setLength(0);
355                         currentLiteral.append(c); // append %
356                         state = ParserState.CONVERTER_STATE;
357                         formattingInfo = FormattingInfo.getDefault();
358                     }
359                 } else {
360                     currentLiteral.append(c);
361                 }
362 
363                 break;
364 
365             case CONVERTER_STATE:
366                 currentLiteral.append(c);
367 
368                 switch (c) {
369                 case '-':
370                     formattingInfo = new FormattingInfo(true, formattingInfo.getMinLength(),
371                             formattingInfo.getMaxLength(), formattingInfo.isLeftTruncate());
372                     break;
373 
374                 case '.':
375                     state = ParserState.DOT_STATE;
376                     break;
377 
378                 default:
379 
380                     if (c >= '0' && c <= '9') {
381                         formattingInfo = new FormattingInfo(formattingInfo.isLeftAligned(), c - '0',
382                                 formattingInfo.getMaxLength(), formattingInfo.isLeftTruncate());
383                         state = ParserState.MIN_STATE;
384                     } else {
385                         i = finalizeConverter(c, pattern, i, currentLiteral, formattingInfo, converterRules,
386                                 patternConverters, formattingInfos, noConsoleNoAnsi, convertBackslashes);
387 
388                         // Next pattern is assumed to be a literal.
389                         state = ParserState.LITERAL_STATE;
390                         formattingInfo = FormattingInfo.getDefault();
391                         currentLiteral.setLength(0);
392                     }
393                 } // switch
394 
395                 break;
396 
397             case MIN_STATE:
398                 currentLiteral.append(c);
399 
400                 if (c >= '0' && c <= '9') {
401                     // Multiply the existing value and add the value of the number just encountered.
402                     formattingInfo = new FormattingInfo(formattingInfo.isLeftAligned(), formattingInfo.getMinLength()
403                             * DECIMAL + c - '0', formattingInfo.getMaxLength(), formattingInfo.isLeftTruncate());
404                 } else if (c == '.') {
405                     state = ParserState.DOT_STATE;
406                 } else {
407                     i = finalizeConverter(c, pattern, i, currentLiteral, formattingInfo, converterRules,
408                             patternConverters, formattingInfos, noConsoleNoAnsi, convertBackslashes);
409                     state = ParserState.LITERAL_STATE;
410                     formattingInfo = FormattingInfo.getDefault();
411                     currentLiteral.setLength(0);
412                 }
413 
414                 break;
415 
416             case DOT_STATE:
417                 currentLiteral.append(c);
418                 switch (c) {
419                 case '-':
420                     formattingInfo = new FormattingInfo(formattingInfo.isLeftAligned(), formattingInfo.getMinLength(),
421                             formattingInfo.getMaxLength(),false);
422                     break;
423 
424                 default:
425 
426 	                if (c >= '0' && c <= '9') {
427 	                    formattingInfo = new FormattingInfo(formattingInfo.isLeftAligned(), formattingInfo.getMinLength(),
428 	                            c - '0', formattingInfo.isLeftTruncate());
429 	                    state = ParserState.MAX_STATE;
430 	                } else {
431 	                    LOGGER.error("Error occurred in position " + i + ".\n Was expecting digit, instead got char \"" + c
432 	                            + "\".");
433 
434 	                    state = ParserState.LITERAL_STATE;
435 	                }
436                 }
437 
438                 break;
439 
440             case MAX_STATE:
441                 currentLiteral.append(c);
442 
443                 if (c >= '0' && c <= '9') {
444                     // Multiply the existing value and add the value of the number just encountered.
445                     formattingInfo = new FormattingInfo(formattingInfo.isLeftAligned(), formattingInfo.getMinLength(),
446                             formattingInfo.getMaxLength() * DECIMAL + c - '0', formattingInfo.isLeftTruncate());
447                 } else {
448                     i = finalizeConverter(c, pattern, i, currentLiteral, formattingInfo, converterRules,
449                             patternConverters, formattingInfos, noConsoleNoAnsi, convertBackslashes);
450                     state = ParserState.LITERAL_STATE;
451                     formattingInfo = FormattingInfo.getDefault();
452                     currentLiteral.setLength(0);
453                 }
454 
455                 break;
456             } // switch
457         }
458 
459         // while
460         if (currentLiteral.length() != 0) {
461             patternConverters.add(new LiteralPatternConverter(config, currentLiteral.toString(), convertBackslashes));
462             formattingInfos.add(FormattingInfo.getDefault());
463         }
464     }
465 
466     /**
467      * Creates a new PatternConverter.
468      *
469      * @param converterId
470      *            converterId.
471      * @param currentLiteral
472      *            literal to be used if converter is unrecognized or following converter if converterId contains extra
473      *            characters.
474      * @param rules
475      *            map of stock pattern converters keyed by format specifier.
476      * @param options
477      *            converter options.
478      * @param noConsoleNoAnsi TODO
479      * @return converter or null.
480      */
481     private PatternConverter createConverter(final String converterId, final StringBuilder currentLiteral,
482             final Map<String, Class<PatternConverter>> rules, final List<String> options, final boolean noConsoleNoAnsi) {
483         String converterName = converterId;
484         Class<PatternConverter> converterClass = null;
485 
486         if (rules == null) {
487             LOGGER.error("Null rules for [" + converterId + ']');
488             return null;
489         }
490         for (int i = converterId.length(); i > 0 && converterClass == null; i--) {
491             converterName = converterName.substring(0, i);
492             converterClass = rules.get(converterName);
493         }
494 
495         if (converterClass == null) {
496             LOGGER.error("Unrecognized format specifier [" + converterId + ']');
497             return null;
498         }
499 
500         if (AnsiConverter.class.isAssignableFrom(converterClass)) {
501             options.add(NO_CONSOLE_NO_ANSI + '=' + noConsoleNoAnsi);
502         }
503         // Work around the regression bug in Class.getDeclaredMethods() in Oracle Java in version > 1.6.0_17:
504         // http://bugs.sun.com/bugdatabase/view_bug.do?bug_id=6815786
505         final Method[] methods = converterClass.getDeclaredMethods();
506         Method newInstanceMethod = null;
507         for (final Method method : methods) {
508             if (Modifier.isStatic(method.getModifiers()) && method.getDeclaringClass().equals(converterClass)
509                     && method.getName().equals("newInstance")) {
510                 if (newInstanceMethod == null) {
511                     newInstanceMethod = method;
512                 } else if (method.getReturnType().equals(newInstanceMethod.getReturnType())) {
513                     LOGGER.error("Class " + converterClass + " cannot contain multiple static newInstance methods");
514                     return null;
515                 }
516             }
517         }
518         if (newInstanceMethod == null) {
519             LOGGER.error("Class " + converterClass + " does not contain a static newInstance method");
520             return null;
521         }
522 
523         final Class<?>[] parmTypes = newInstanceMethod.getParameterTypes();
524         final Object[] parms = parmTypes.length > 0 ? new Object[parmTypes.length] : null;
525 
526         if (parms != null) {
527             int i = 0;
528             boolean errors = false;
529             for (final Class<?> clazz : parmTypes) {
530                 if (clazz.isArray() && clazz.getName().equals("[Ljava.lang.String;")) {
531                     final String[] optionsArray = options.toArray(new String[options.size()]);
532                     parms[i] = optionsArray;
533                 } else if (clazz.isAssignableFrom(Configuration.class)) {
534                     parms[i] = config;
535                 } else {
536                     LOGGER.error("Unknown parameter type " + clazz.getName() + " for static newInstance method of "
537                             + converterClass.getName());
538                     errors = true;
539                 }
540                 ++i;
541             }
542             if (errors) {
543                 return null;
544             }
545         }
546 
547         try {
548             final Object newObj = newInstanceMethod.invoke(null, parms);
549 
550             if (newObj instanceof PatternConverter) {
551                 currentLiteral.delete(0, currentLiteral.length() - (converterId.length() - converterName.length()));
552 
553                 return (PatternConverter) newObj;
554             }
555             LOGGER.warn("Class {} does not extend PatternConverter.", converterClass.getName());
556         } catch (final Exception ex) {
557             LOGGER.error("Error creating converter for " + converterId, ex);
558         }
559 
560         return null;
561     }
562 
563     /**
564      * Processes a format specifier sequence.
565      *
566      * @param c
567      *            initial character of format specifier.
568      * @param pattern
569      *            conversion pattern
570      * @param i
571      *            current position in conversion pattern.
572      * @param currentLiteral
573      *            current literal.
574      * @param formattingInfo
575      *            current field specifier.
576      * @param rules
577      *            map of stock pattern converters keyed by format specifier.
578      * @param patternConverters
579      *            list to receive parsed pattern converter.
580      * @param formattingInfos
581      *            list to receive corresponding field specifier.
582      * @param noConsoleNoAnsi
583      *            TODO
584      * @param convertBackslashes if {@code true}, backslash characters are treated as escape characters and character
585      *            sequences like "\" followed by "t" (backslash+t) are converted to special characters like '\t' (tab).
586      * @return position after format specifier sequence.
587      */
588     private int finalizeConverter(final char c, final String pattern, final int start,
589             final StringBuilder currentLiteral, final FormattingInfo formattingInfo,
590             final Map<String, Class<PatternConverter>> rules, final List<PatternConverter> patternConverters,
591             final List<FormattingInfo> formattingInfos, final boolean noConsoleNoAnsi, final boolean convertBackslashes) {
592         int i = start;
593         final StringBuilder convBuf = new StringBuilder();
594         i = extractConverter(c, pattern, i, convBuf, currentLiteral);
595 
596         final String converterId = convBuf.toString();
597 
598         final List<String> options = new ArrayList<>();
599         i = extractOptions(pattern, i, options);
600 
601         final PatternConverter pc = createConverter(converterId, currentLiteral, rules, options, noConsoleNoAnsi);
602 
603         if (pc == null) {
604             StringBuilder msg;
605 
606             if (Strings.isEmpty(converterId)) {
607                 msg = new StringBuilder("Empty conversion specifier starting at position ");
608             } else {
609                 msg = new StringBuilder("Unrecognized conversion specifier [");
610                 msg.append(converterId);
611                 msg.append("] starting at position ");
612             }
613 
614             msg.append(Integer.toString(i));
615             msg.append(" in conversion pattern.");
616 
617             LOGGER.error(msg.toString());
618 
619             patternConverters.add(new LiteralPatternConverter(config, currentLiteral.toString(), convertBackslashes));
620             formattingInfos.add(FormattingInfo.getDefault());
621         } else {
622             patternConverters.add(pc);
623             formattingInfos.add(formattingInfo);
624 
625             if (currentLiteral.length() > 0) {
626                 patternConverters
627                         .add(new LiteralPatternConverter(config, currentLiteral.toString(), convertBackslashes));
628                 formattingInfos.add(FormattingInfo.getDefault());
629             }
630         }
631 
632         currentLiteral.setLength(0);
633 
634         return i;
635     }
636 }