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