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