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