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