    001    /*
    002     * Licensed to the Apache Software Foundation (ASF) under one or more
    003     * contributor license agreements. See the NOTICE file distributed with
    004     * this work for additional information regarding copyright ownership.
    005     * The ASF licenses this file to You under the Apache license, Version 2.0
    006     * (the "License"); you may not use this file except in compliance with
    007     * the License. You may obtain a copy of the License at
    008     *
    009     *      http://www.apache.org/licenses/LICENSE-2.0
    010     *
    011     * Unless required by applicable law or agreed to in writing, software
    012     * distributed under the License is distributed on an "AS IS" BASIS,
    013     * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    014     * See the license for the specific language governing permissions and
    015     * limitations under the license.
    016     */
    017    package org.apache.logging.log4j.core.pattern;
    018
    019    import org.apache.logging.log4j.Logger;
    020    import org.apache.logging.log4j.core.config.Configuration;
    021    import org.apache.logging.log4j.core.config.plugins.util.PluginManager;
    022    import org.apache.logging.log4j.core.config.plugins.util.PluginType;
    023    import org.apache.logging.log4j.status.StatusLogger;
    024    import org.apache.logging.log4j.util.Strings;
    025
    026    import java.lang.reflect.Method;
    027    import java.lang.reflect.Modifier;
    028    import java.util.ArrayList;
    029    import java.util.Iterator;
    030    import java.util.LinkedHashMap;
    031    import java.util.List;
    032    import java.util.Map;
    033
    034    /**
    035     * Most of the work of the {@link org.apache.logging.log4j.core.layout.PatternLayout} class is delegated to the
    036     * PatternParser class.
    037     * <p>
    038     * It is this class that parses conversion patterns and creates a chained list of {@link PatternConverter
    039     * PatternConverters}.
    040     */
    041    public final class PatternParser {
    042        static final String NO_CONSOLE_NO_ANSI = "noConsoleNoAnsi";
    043
    044        /**
    045         * Escape character for format specifier.
    046         */
    047        private static final char ESCAPE_CHAR = '%';
    048
    049        /**
    050         * The states the parser can be in while parsing the pattern.
    051         */
    052        private enum ParserState {
    053            /**
    054             * Literal state.
    055             */
    056            LITERAL_STATE,
    057
    058            /**
    059             * In converter name state.
    060             */
    061            CONVERTER_STATE,
    062
    063            /**
    064             * Dot state.
    065             */
    066            DOT_STATE,
    067
    068            /**
    069             * Min state.
    070             */
    071            MIN_STATE,
    072
    073            /**
    074             * Max state.
    075             */
    076            MAX_STATE;
    077        }
    078
    079        private static final Logger LOGGER = StatusLogger.getLogger();
    080
    081        private static final int BUF_SIZE = 32;
    082
    083        private static final int DECIMAL = 10;
    084
    085        private final Configuration config;
    086
    087        private final Map<String, Class<PatternConverter>> converterRules;
    088
    089        /**
    090         * Constructor.
    091         *
    092         * @param converterKey
    093         *            The type of converters that will be used.
    094         */
    095        public PatternParser(final String converterKey) {
    096            this(null, converterKey, null, null);
    097        }
    098
    099        /**
    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    }