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