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.PluginManager;
030import org.apache.logging.log4j.core.config.plugins.PluginType;
031import org.apache.logging.log4j.core.helpers.Strings;
032import org.apache.logging.log4j.status.StatusLogger;
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, expectedClass);
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            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, "");
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                    final int next = pattern.indexOf("{", i);
257                    if (next != -1 && next < end) {
258                        i = end + 1;
259                        ++depth;
260                    } else if (depth > 0) {
261                        --depth;
262                    }
263                }
264            } while (depth > 0);
265
266            if (end == -1) {
267                break;
268            }
269
270            final String r = pattern.substring(begin + 1, end);
271            options.add(r);
272            i = end + 1;
273        }
274
275        return i;
276    }
277
278    /**
279     * Parse a format specifier.
280     * 
281     * @param pattern
282     *            pattern to parse.
283     * @param patternConverters
284     *            list to receive pattern converters.
285     * @param formattingInfos
286     *            list to receive field specifiers corresponding to pattern converters.
287     * @param noConsoleNoAnsi
288     *            TODO
289     */
290    public void parse(final String pattern, final List<PatternConverter> patternConverters,
291            final List<FormattingInfo> formattingInfos, final boolean noConsoleNoAnsi) {
292        if (pattern == null) {
293            throw new NullPointerException("pattern");
294        }
295
296        final StringBuilder currentLiteral = new StringBuilder(BUF_SIZE);
297
298        final int patternLength = pattern.length();
299        ParserState state = ParserState.LITERAL_STATE;
300        char c;
301        int i = 0;
302        FormattingInfo formattingInfo = FormattingInfo.getDefault();
303
304        while (i < patternLength) {
305            c = pattern.charAt(i++);
306
307            switch (state) {
308            case LITERAL_STATE:
309
310                // In literal state, the last char is always a literal.
311                if (i == patternLength) {
312                    currentLiteral.append(c);
313
314                    continue;
315                }
316
317                if (c == ESCAPE_CHAR) {
318                    // peek at the next char.
319                    switch (pattern.charAt(i)) {
320                    case ESCAPE_CHAR:
321                        currentLiteral.append(c);
322                        i++; // move pointer
323
324                        break;
325
326                    default:
327
328                        if (currentLiteral.length() != 0) {
329                            patternConverters.add(new LiteralPatternConverter(config, currentLiteral.toString()));
330                            formattingInfos.add(FormattingInfo.getDefault());
331                        }
332
333                        currentLiteral.setLength(0);
334                        currentLiteral.append(c); // append %
335                        state = ParserState.CONVERTER_STATE;
336                        formattingInfo = FormattingInfo.getDefault();
337                    }
338                } else {
339                    currentLiteral.append(c);
340                }
341
342                break;
343
344            case CONVERTER_STATE:
345                currentLiteral.append(c);
346
347                switch (c) {
348                case '-':
349                    formattingInfo = new FormattingInfo(true, formattingInfo.getMinLength(),
350                            formattingInfo.getMaxLength());
351                    break;
352
353                case '.':
354                    state = ParserState.DOT_STATE;
355                    break;
356
357                default:
358
359                    if (c >= '0' && c <= '9') {
360                        formattingInfo = new FormattingInfo(formattingInfo.isLeftAligned(), c - '0',
361                                formattingInfo.getMaxLength());
362                        state = ParserState.MIN_STATE;
363                    } else {
364                        i = finalizeConverter(c, pattern, i, currentLiteral, formattingInfo, converterRules,
365                                patternConverters, formattingInfos, noConsoleNoAnsi);
366
367                        // Next pattern is assumed to be a literal.
368                        state = ParserState.LITERAL_STATE;
369                        formattingInfo = FormattingInfo.getDefault();
370                        currentLiteral.setLength(0);
371                    }
372                } // switch
373
374                break;
375
376            case MIN_STATE:
377                currentLiteral.append(c);
378
379                if (c >= '0' && c <= '9') {
380                    // Multiply the existing value and add the value of the number just encountered.
381                    formattingInfo = new FormattingInfo(formattingInfo.isLeftAligned(), formattingInfo.getMinLength()
382                            * DECIMAL + c - '0', formattingInfo.getMaxLength());
383                } else if (c == '.') {
384                    state = ParserState.DOT_STATE;
385                } else {
386                    i = finalizeConverter(c, pattern, i, currentLiteral, formattingInfo, converterRules,
387                            patternConverters, formattingInfos, noConsoleNoAnsi);
388                    state = ParserState.LITERAL_STATE;
389                    formattingInfo = FormattingInfo.getDefault();
390                    currentLiteral.setLength(0);
391                }
392
393                break;
394
395            case DOT_STATE:
396                currentLiteral.append(c);
397
398                if (c >= '0' && c <= '9') {
399                    formattingInfo = new FormattingInfo(formattingInfo.isLeftAligned(), formattingInfo.getMinLength(),
400                            c - '0');
401                    state = ParserState.MAX_STATE;
402                } else {
403                    LOGGER.error("Error occurred in position " + i + ".\n Was expecting digit, instead got char \"" + c
404                            + "\".");
405
406                    state = ParserState.LITERAL_STATE;
407                }
408
409                break;
410
411            case MAX_STATE:
412                currentLiteral.append(c);
413
414                if (c >= '0' && c <= '9') {
415                    // Multiply the existing value and add the value of the number just encountered.
416                    formattingInfo = new FormattingInfo(formattingInfo.isLeftAligned(), formattingInfo.getMinLength(),
417                            formattingInfo.getMaxLength() * DECIMAL + c - '0');
418                } else {
419                    i = finalizeConverter(c, pattern, i, currentLiteral, formattingInfo, converterRules,
420                            patternConverters, formattingInfos, noConsoleNoAnsi);
421                    state = ParserState.LITERAL_STATE;
422                    formattingInfo = FormattingInfo.getDefault();
423                    currentLiteral.setLength(0);
424                }
425
426                break;
427            } // switch
428        }
429
430        // while
431        if (currentLiteral.length() != 0) {
432            patternConverters.add(new LiteralPatternConverter(config, currentLiteral.toString()));
433            formattingInfos.add(FormattingInfo.getDefault());
434        }
435    }
436
437    /**
438     * Creates a new PatternConverter.
439     * 
440     * @param converterId
441     *            converterId.
442     * @param currentLiteral
443     *            literal to be used if converter is unrecognized or following converter if converterId contains extra
444     *            characters.
445     * @param rules
446     *            map of stock pattern converters keyed by format specifier.
447     * @param options
448     *            converter options.
449     * @param noConsoleNoAnsi TODO
450     * @return converter or null.
451     */
452    private PatternConverter createConverter(final String converterId, final StringBuilder currentLiteral,
453            final Map<String, Class<PatternConverter>> rules, final List<String> options, boolean noConsoleNoAnsi) {
454        String converterName = converterId;
455        Class<PatternConverter> converterClass = null;
456
457        if (rules == null) {
458            LOGGER.error("Null rules for [" + converterId + "]");
459            return null;
460        }
461        for (int i = converterId.length(); i > 0 && converterClass == null; i--) {
462            converterName = converterName.substring(0, i);
463            converterClass = rules.get(converterName);
464        }
465
466        if (converterClass == null) {
467            LOGGER.error("Unrecognized format specifier [" + converterId + "]");
468            return null;
469        }
470
471        if (AnsiConverter.class.isAssignableFrom(converterClass)) {
472            options.add(NO_CONSOLE_NO_ANSI + "=" + noConsoleNoAnsi);
473        }
474        // Work around the regression bug in Class.getDeclaredMethods() in Oracle Java in version > 1.6.0_17:
475        // http://bugs.sun.com/bugdatabase/view_bug.do?bug_id=6815786
476        final Method[] methods = converterClass.getDeclaredMethods();
477        Method newInstanceMethod = null;
478        for (final Method method : methods) {
479            if (Modifier.isStatic(method.getModifiers()) && method.getDeclaringClass().equals(converterClass)
480                    && method.getName().equals("newInstance")) {
481                if (newInstanceMethod == null) {
482                    newInstanceMethod = method;
483                } else if (method.getReturnType().equals(newInstanceMethod.getReturnType())) {
484                    LOGGER.error("Class " + converterClass + " cannot contain multiple static newInstance methods");
485                    return null;
486                }
487            }
488        }
489        if (newInstanceMethod == null) {
490            LOGGER.error("Class " + converterClass + " does not contain a static newInstance method");
491            return null;
492        }
493
494        final Class<?>[] parmTypes = newInstanceMethod.getParameterTypes();
495        final Object[] parms = parmTypes.length > 0 ? new Object[parmTypes.length] : null;
496
497        if (parms != null) {
498            int i = 0;
499            boolean errors = false;
500            for (final Class<?> clazz : parmTypes) {
501                if (clazz.isArray() && clazz.getName().equals("[Ljava.lang.String;")) {
502                    final String[] optionsArray = options.toArray(new String[options.size()]);
503                    parms[i] = optionsArray;
504                } else if (clazz.isAssignableFrom(Configuration.class)) {
505                    parms[i] = config;
506                } else {
507                    LOGGER.error("Unknown parameter type " + clazz.getName() + " for static newInstance method of "
508                            + converterClass.getName());
509                    errors = true;
510                }
511                ++i;
512            }
513            if (errors) {
514                return null;
515            }
516        }
517
518        try {
519            final Object newObj = newInstanceMethod.invoke(null, parms);
520
521            if (newObj instanceof PatternConverter) {
522                currentLiteral.delete(0, currentLiteral.length() - (converterId.length() - converterName.length()));
523
524                return (PatternConverter) newObj;
525            } else {
526                LOGGER.warn("Class " + converterClass.getName() + " does not extend PatternConverter.");
527            }
528        } catch (final Exception ex) {
529            LOGGER.error("Error creating converter for " + converterId, ex);
530        }
531
532        return null;
533    }
534
535    /**
536     * Processes a format specifier sequence.
537     * 
538     * @param c
539     *            initial character of format specifier.
540     * @param pattern
541     *            conversion pattern
542     * @param i
543     *            current position in conversion pattern.
544     * @param currentLiteral
545     *            current literal.
546     * @param formattingInfo
547     *            current field specifier.
548     * @param rules
549     *            map of stock pattern converters keyed by format specifier.
550     * @param patternConverters
551     *            list to receive parsed pattern converter.
552     * @param formattingInfos
553     *            list to receive corresponding field specifier.
554     * @param noConsoleNoAnsi
555     *            TODO
556     * @return position after format specifier sequence.
557     */
558    private int finalizeConverter(final char c, final String pattern, int i, final StringBuilder currentLiteral,
559            final FormattingInfo formattingInfo, final Map<String, Class<PatternConverter>> rules,
560            final List<PatternConverter> patternConverters, final List<FormattingInfo> formattingInfos,
561            final boolean noConsoleNoAnsi) {
562        final StringBuilder convBuf = new StringBuilder();
563        i = extractConverter(c, pattern, i, convBuf, currentLiteral);
564
565        final String converterId = convBuf.toString();
566
567        final List<String> options = new ArrayList<String>();
568        i = extractOptions(pattern, i, options);
569
570        final PatternConverter pc = createConverter(converterId, currentLiteral, rules, options, noConsoleNoAnsi);
571
572        if (pc == null) {
573            StringBuilder msg;
574
575            if (Strings.isEmpty(converterId)) {
576                msg = new StringBuilder("Empty conversion specifier starting at position ");
577            } else {
578                msg = new StringBuilder("Unrecognized conversion specifier [");
579                msg.append(converterId);
580                msg.append("] starting at position ");
581            }
582
583            msg.append(Integer.toString(i));
584            msg.append(" in conversion pattern.");
585
586            LOGGER.error(msg.toString());
587
588            patternConverters.add(new LiteralPatternConverter(config, currentLiteral.toString()));
589            formattingInfos.add(FormattingInfo.getDefault());
590        } else {
591            patternConverters.add(pc);
592            formattingInfos.add(formattingInfo);
593
594            if (currentLiteral.length() > 0) {
595                patternConverters.add(new LiteralPatternConverter(config, currentLiteral.toString()));
596                formattingInfos.add(FormattingInfo.getDefault());
597            }
598        }
599
600        currentLiteral.setLength(0);
601
602        return i;
603    }
604}