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.SystemNanoClock;
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         for (final PatternConverter converter : converters) {
177             if (converter instanceof NanoTimePatternConverter) {
178                 // LOG4J2-1074 Switch to actual clock if nanosecond timestamps are required in config.
179                 // LOG4J2-1248 set config nanoclock
180                 if (config != null) {
181                     config.setNanoClock(new SystemNanoClock());
182                 }
183             }
184             LogEventPatternConverter pc;
185             if (converter instanceof LogEventPatternConverter) {
186                 pc = (LogEventPatternConverter) converter;
187                 handlesThrowable |= pc.handlesThrowable();
188             } else {
189                 pc = new LiteralPatternConverter(config, Strings.EMPTY, true);
190             }
191 
192             FormattingInfo field;
193             if (fieldIter.hasNext()) {
194                 field = fieldIter.next();
195             } else {
196                 field = FormattingInfo.getDefault();
197             }
198             list.add(new PatternFormatter(pc, field));
199         }
200         if (alwaysWriteExceptions && !handlesThrowable) {
201             final LogEventPatternConverter pc = ExtendedThrowablePatternConverter.newInstance(null);
202             list.add(new PatternFormatter(pc, FormattingInfo.getDefault()));
203         }
204         return list;
205     }
206 
207     /**
208      * Extracts the converter identifier found at the given start position.
209      * <p>
210      * After this function returns, the variable i will point to the first char after the end of the converter
211      * identifier.
212      * </p>
213      * <p>
214      * If i points to a char which is not a character acceptable at the start of a unicode identifier, the value null is
215      * returned.
216      * </p>
217      *
218      * @param lastChar
219      *        last processed character.
220      * @param pattern
221      *        format string.
222      * @param start
223      *        current index into pattern format.
224      * @param convBuf
225      *        buffer to receive conversion specifier.
226      * @param currentLiteral
227      *        literal to be output in case format specifier in unrecognized.
228      * @return position in pattern after converter.
229      */
230     private static int extractConverter(final char lastChar, final String pattern, final int start,
231             final StringBuilder convBuf, final StringBuilder currentLiteral) {
232         int i = start;
233         convBuf.setLength(0);
234 
235         // When this method is called, lastChar points to the first character of the
236         // conversion word. For example:
237         // For "%hello" lastChar = 'h'
238         // For "%-5hello" lastChar = 'h'
239         // System.out.println("lastchar is "+lastChar);
240         if (!Character.isUnicodeIdentifierStart(lastChar)) {
241             return i;
242         }
243 
244         convBuf.append(lastChar);
245 
246         while (i < pattern.length() && Character.isUnicodeIdentifierPart(pattern.charAt(i))) {
247             convBuf.append(pattern.charAt(i));
248             currentLiteral.append(pattern.charAt(i));
249             i++;
250         }
251 
252         return i;
253     }
254 
255     /**
256      * Extract options.
257      *
258      * @param pattern
259      *            conversion pattern.
260      * @param start
261      *            start of options.
262      * @param options
263      *            array to receive extracted options
264      * @return position in pattern after options.
265      */
266     private static int extractOptions(final String pattern, final int start, final List<String> options) {
267         int i = start;
268         while (i < pattern.length() && pattern.charAt(i) == '{') {
269             final int begin = i++;
270             int end;
271             int depth = 0;
272             do {
273                 end = pattern.indexOf('}', i);
274                 if (end == -1) {
275                     break;
276                 }
277                 final int next = pattern.indexOf("{", i);
278                 if (next != -1 && next < end) {
279                     i = end + 1;
280                     ++depth;
281                 } else if (depth > 0) {
282                     --depth;
283                 }
284             } while (depth > 0);
285 
286             if (end == -1) {
287                 break;
288             }
289 
290             final String r = pattern.substring(begin + 1, end);
291             options.add(r);
292             i = end + 1;
293         }
294 
295         return i;
296     }
297 
298     /**
299      * Parse a format specifier.
300      *
301      * @param pattern
302      *            pattern to parse.
303      * @param patternConverters
304      *            list to receive pattern converters.
305      * @param formattingInfos
306      *            list to receive field specifiers corresponding to pattern converters.
307      * @param noConsoleNoAnsi
308      *            TODO
309      * @param convertBackslashes if {@code true}, backslash characters are treated as escape characters and character
310      *            sequences like "\" followed by "t" (backslash+t) are converted to special characters like '\t' (tab).
311      */
312     public void parse(final String pattern, final List<PatternConverter> patternConverters,
313             final List<FormattingInfo> formattingInfos, final boolean noConsoleNoAnsi,
314             final boolean convertBackslashes) {
315         Objects.requireNonNull(pattern, "pattern");
316 
317         final StringBuilder currentLiteral = new StringBuilder(BUF_SIZE);
318 
319         final int patternLength = pattern.length();
320         ParserState state = ParserState.LITERAL_STATE;
321         char c;
322         int i = 0;
323         FormattingInfo formattingInfo = FormattingInfo.getDefault();
324 
325         while (i < patternLength) {
326             c = pattern.charAt(i++);
327 
328             switch (state) {
329             case LITERAL_STATE:
330 
331                 // In literal state, the last char is always a literal.
332                 if (i == patternLength) {
333                     currentLiteral.append(c);
334 
335                     continue;
336                 }
337 
338                 if (c == ESCAPE_CHAR) {
339                     // peek at the next char.
340                     switch (pattern.charAt(i)) {
341                     case ESCAPE_CHAR:
342                         currentLiteral.append(c);
343                         i++; // move pointer
344 
345                         break;
346 
347                     default:
348 
349                         if (currentLiteral.length() != 0) {
350                             patternConverters.add(new LiteralPatternConverter(config, currentLiteral.toString(),
351                                     convertBackslashes));
352                             formattingInfos.add(FormattingInfo.getDefault());
353                         }
354 
355                         currentLiteral.setLength(0);
356                         currentLiteral.append(c); // append %
357                         state = ParserState.CONVERTER_STATE;
358                         formattingInfo = FormattingInfo.getDefault();
359                     }
360                 } else {
361                     currentLiteral.append(c);
362                 }
363 
364                 break;
365 
366             case CONVERTER_STATE:
367                 currentLiteral.append(c);
368 
369                 switch (c) {
370                 case '-':
371                     formattingInfo = new FormattingInfo(true, formattingInfo.getMinLength(),
372                             formattingInfo.getMaxLength(), formattingInfo.isLeftTruncate());
373                     break;
374 
375                 case '.':
376                     state = ParserState.DOT_STATE;
377                     break;
378 
379                 default:
380 
381                     if (c >= '0' && c <= '9') {
382                         formattingInfo = new FormattingInfo(formattingInfo.isLeftAligned(), c - '0',
383                                 formattingInfo.getMaxLength(), formattingInfo.isLeftTruncate());
384                         state = ParserState.MIN_STATE;
385                     } else {
386                         i = finalizeConverter(c, pattern, i, currentLiteral, formattingInfo, converterRules,
387                                 patternConverters, formattingInfos, noConsoleNoAnsi, convertBackslashes);
388 
389                         // Next pattern is assumed to be a literal.
390                         state = ParserState.LITERAL_STATE;
391                         formattingInfo = FormattingInfo.getDefault();
392                         currentLiteral.setLength(0);
393                     }
394                 } // switch
395 
396                 break;
397 
398             case MIN_STATE:
399                 currentLiteral.append(c);
400 
401                 if (c >= '0' && c <= '9') {
402                     // Multiply the existing value and add the value of the number just encountered.
403                     formattingInfo = new FormattingInfo(formattingInfo.isLeftAligned(), formattingInfo.getMinLength()
404                             * DECIMAL + c - '0', formattingInfo.getMaxLength(), formattingInfo.isLeftTruncate());
405                 } else if (c == '.') {
406                     state = ParserState.DOT_STATE;
407                 } else {
408                     i = finalizeConverter(c, pattern, i, currentLiteral, formattingInfo, converterRules,
409                             patternConverters, formattingInfos, noConsoleNoAnsi, convertBackslashes);
410                     state = ParserState.LITERAL_STATE;
411                     formattingInfo = FormattingInfo.getDefault();
412                     currentLiteral.setLength(0);
413                 }
414 
415                 break;
416 
417             case DOT_STATE:
418                 currentLiteral.append(c);
419                 switch (c) {
420                 case '-':
421                     formattingInfo = new FormattingInfo(formattingInfo.isLeftAligned(), formattingInfo.getMinLength(),
422                             formattingInfo.getMaxLength(),false);
423                     break;
424 
425                 default:
426 
427 	                if (c >= '0' && c <= '9') {
428 	                    formattingInfo = new FormattingInfo(formattingInfo.isLeftAligned(), formattingInfo.getMinLength(),
429 	                            c - '0', formattingInfo.isLeftTruncate());
430 	                    state = ParserState.MAX_STATE;
431 	                } else {
432 	                    LOGGER.error("Error occurred in position " + i + ".\n Was expecting digit, instead got char \"" + c
433 	                            + "\".");
434 
435 	                    state = ParserState.LITERAL_STATE;
436 	                }
437                 }
438 
439                 break;
440 
441             case MAX_STATE:
442                 currentLiteral.append(c);
443 
444                 if (c >= '0' && c <= '9') {
445                     // Multiply the existing value and add the value of the number just encountered.
446                     formattingInfo = new FormattingInfo(formattingInfo.isLeftAligned(), formattingInfo.getMinLength(),
447                             formattingInfo.getMaxLength() * DECIMAL + c - '0', formattingInfo.isLeftTruncate());
448                 } else {
449                     i = finalizeConverter(c, pattern, i, currentLiteral, formattingInfo, converterRules,
450                             patternConverters, formattingInfos, noConsoleNoAnsi, convertBackslashes);
451                     state = ParserState.LITERAL_STATE;
452                     formattingInfo = FormattingInfo.getDefault();
453                     currentLiteral.setLength(0);
454                 }
455 
456                 break;
457             } // switch
458         }
459 
460         // while
461         if (currentLiteral.length() != 0) {
462             patternConverters.add(new LiteralPatternConverter(config, currentLiteral.toString(), convertBackslashes));
463             formattingInfos.add(FormattingInfo.getDefault());
464         }
465     }
466 
467     /**
468      * Creates a new PatternConverter.
469      *
470      * @param converterId
471      *            converterId.
472      * @param currentLiteral
473      *            literal to be used if converter is unrecognized or following converter if converterId contains extra
474      *            characters.
475      * @param rules
476      *            map of stock pattern converters keyed by format specifier.
477      * @param options
478      *            converter options.
479      * @param noConsoleNoAnsi TODO
480      * @return converter or null.
481      */
482     private PatternConverter createConverter(final String converterId, final StringBuilder currentLiteral,
483             final Map<String, Class<PatternConverter>> rules, final List<String> options, final boolean noConsoleNoAnsi) {
484         String converterName = converterId;
485         Class<PatternConverter> converterClass = null;
486 
487         if (rules == null) {
488             LOGGER.error("Null rules for [" + converterId + ']');
489             return null;
490         }
491         for (int i = converterId.length(); i > 0 && converterClass == null; i--) {
492             converterName = converterName.substring(0, i);
493             converterClass = rules.get(converterName);
494         }
495 
496         if (converterClass == null) {
497             LOGGER.error("Unrecognized format specifier [" + converterId + ']');
498             return null;
499         }
500 
501         if (AnsiConverter.class.isAssignableFrom(converterClass)) {
502             options.add(NO_CONSOLE_NO_ANSI + '=' + noConsoleNoAnsi);
503         }
504         // Work around the regression bug in Class.getDeclaredMethods() in Oracle Java in version > 1.6.0_17:
505         // http://bugs.sun.com/bugdatabase/view_bug.do?bug_id=6815786
506         final Method[] methods = converterClass.getDeclaredMethods();
507         Method newInstanceMethod = null;
508         for (final Method method : methods) {
509             if (Modifier.isStatic(method.getModifiers()) && method.getDeclaringClass().equals(converterClass)
510                     && method.getName().equals("newInstance")) {
511                 if (newInstanceMethod == null) {
512                     newInstanceMethod = method;
513                 } else if (method.getReturnType().equals(newInstanceMethod.getReturnType())) {
514                     LOGGER.error("Class " + converterClass + " cannot contain multiple static newInstance methods");
515                     return null;
516                 }
517             }
518         }
519         if (newInstanceMethod == null) {
520             LOGGER.error("Class " + converterClass + " does not contain a static newInstance method");
521             return null;
522         }
523 
524         final Class<?>[] parmTypes = newInstanceMethod.getParameterTypes();
525         final Object[] parms = parmTypes.length > 0 ? new Object[parmTypes.length] : null;
526 
527         if (parms != null) {
528             int i = 0;
529             boolean errors = false;
530             for (final Class<?> clazz : parmTypes) {
531                 if (clazz.isArray() && clazz.getName().equals("[Ljava.lang.String;")) {
532                     final String[] optionsArray = options.toArray(new String[options.size()]);
533                     parms[i] = optionsArray;
534                 } else if (clazz.isAssignableFrom(Configuration.class)) {
535                     parms[i] = config;
536                 } else {
537                     LOGGER.error("Unknown parameter type " + clazz.getName() + " for static newInstance method of "
538                             + converterClass.getName());
539                     errors = true;
540                 }
541                 ++i;
542             }
543             if (errors) {
544                 return null;
545             }
546         }
547 
548         try {
549             final Object newObj = newInstanceMethod.invoke(null, parms);
550 
551             if (newObj instanceof PatternConverter) {
552                 currentLiteral.delete(0, currentLiteral.length() - (converterId.length() - converterName.length()));
553 
554                 return (PatternConverter) newObj;
555             }
556             LOGGER.warn("Class {} does not extend PatternConverter.", converterClass.getName());
557         } catch (final Exception ex) {
558             LOGGER.error("Error creating converter for " + converterId, ex);
559         }
560 
561         return null;
562     }
563 
564     /**
565      * Processes a format specifier sequence.
566      *
567      * @param c
568      *            initial character of format specifier.
569      * @param pattern
570      *            conversion pattern
571      * @param start
572      *            current position in conversion pattern.
573      * @param currentLiteral
574      *            current literal.
575      * @param formattingInfo
576      *            current field specifier.
577      * @param rules
578      *            map of stock pattern converters keyed by format specifier.
579      * @param patternConverters
580      *            list to receive parsed pattern converter.
581      * @param formattingInfos
582      *            list to receive corresponding field specifier.
583      * @param noConsoleNoAnsi
584      *            TODO
585      * @param convertBackslashes if {@code true}, backslash characters are treated as escape characters and character
586      *            sequences like "\" followed by "t" (backslash+t) are converted to special characters like '\t' (tab).
587      * @return position after format specifier sequence.
588      */
589     private int finalizeConverter(final char c, final String pattern, final int start,
590             final StringBuilder currentLiteral, final FormattingInfo formattingInfo,
591             final Map<String, Class<PatternConverter>> rules, final List<PatternConverter> patternConverters,
592             final List<FormattingInfo> formattingInfos, final boolean noConsoleNoAnsi, final boolean convertBackslashes) {
593         int i = start;
594         final StringBuilder convBuf = new StringBuilder();
595         i = extractConverter(c, pattern, i, convBuf, currentLiteral);
596 
597         final String converterId = convBuf.toString();
598 
599         final List<String> options = new ArrayList<>();
600         i = extractOptions(pattern, i, options);
601 
602         final PatternConverter pc = createConverter(converterId, currentLiteral, rules, options, noConsoleNoAnsi);
603 
604         if (pc == null) {
605             StringBuilder msg;
606 
607             if (Strings.isEmpty(converterId)) {
608                 msg = new StringBuilder("Empty conversion specifier starting at position ");
609             } else {
610                 msg = new StringBuilder("Unrecognized conversion specifier [");
611                 msg.append(converterId);
612                 msg.append("] starting at position ");
613             }
614 
615             msg.append(Integer.toString(i));
616             msg.append(" in conversion pattern.");
617 
618             LOGGER.error(msg.toString());
619 
620             patternConverters.add(new LiteralPatternConverter(config, currentLiteral.toString(), convertBackslashes));
621             formattingInfos.add(FormattingInfo.getDefault());
622         } else {
623             patternConverters.add(pc);
624             formattingInfos.add(formattingInfo);
625 
626             if (currentLiteral.length() > 0) {
627                 patternConverters
628                         .add(new LiteralPatternConverter(config, currentLiteral.toString(), convertBackslashes));
629                 formattingInfos.add(FormattingInfo.getDefault());
630             }
631         }
632 
633         currentLiteral.setLength(0);
634 
635         return i;
636     }
637 }