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