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 }