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