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