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