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 org.apache.logging.log4j.Logger;
020import org.apache.logging.log4j.core.config.Configuration;
021import org.apache.logging.log4j.core.config.plugins.util.PluginManager;
022import org.apache.logging.log4j.core.config.plugins.util.PluginType;
023import org.apache.logging.log4j.status.StatusLogger;
024import org.apache.logging.log4j.util.Strings;
025
026import java.lang.reflect.Method;
027import java.lang.reflect.Modifier;
028import java.util.ArrayList;
029import java.util.Iterator;
030import java.util.LinkedHashMap;
031import java.util.List;
032import 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 */
041public 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}