View Javadoc
1   /*
2    * Licensed to the Apache Software Foundation (ASF) under one or more
3    * contributor license agreements. See the NOTICE file distributed with
4    * this work for additional information regarding copyright ownership.
5    * The ASF licenses this file to You under the Apache license, Version 2.0
6    * (the "License"); you may not use this file except in compliance with
7    * the License. You may obtain a copy of the License at
8    *
9    *      http://www.apache.org/licenses/LICENSE-2.0
10   *
11   * Unless required by applicable law or agreed to in writing, software
12   * distributed under the License is distributed on an "AS IS" BASIS,
13   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14   * See the license for the specific language governing permissions and
15   * limitations under the license.
16   */
17  package org.apache.logging.log4j.core.pattern;
18  
19  import java.lang.reflect.Method;
20  import java.lang.reflect.Modifier;
21  import java.util.ArrayList;
22  import java.util.Iterator;
23  import java.util.LinkedHashMap;
24  import java.util.List;
25  import java.util.Map;
26  import java.util.Objects;
27  
28  import org.apache.logging.log4j.Logger;
29  import org.apache.logging.log4j.core.config.Configuration;
30  import org.apache.logging.log4j.core.config.plugins.util.PluginManager;
31  import org.apache.logging.log4j.core.config.plugins.util.PluginType;
32  import org.apache.logging.log4j.core.util.SystemNanoClock;
33  import org.apache.logging.log4j.status.StatusLogger;
34  import org.apache.logging.log4j.util.Strings;
35  
36  /**
37   * Most of the work of the {@link org.apache.logging.log4j.core.layout.PatternLayout} class is delegated to the
38   * PatternParser class.
39   * <p>
40   * It is this class that parses conversion patterns and creates a chained list of {@link PatternConverter
41   * PatternConverters}.
42   */
43  public final class PatternParser {
44      static final String DISABLE_ANSI = "disableAnsi";
45      static final String NO_CONSOLE_NO_ANSI = "noConsoleNoAnsi";
46  
47      /**
48       * Escape character for format specifier.
49       */
50      private static final char ESCAPE_CHAR = '%';
51  
52      /**
53       * The states the parser can be in while parsing the pattern.
54       */
55      private enum ParserState {
56          /**
57           * Literal state.
58           */
59          LITERAL_STATE,
60  
61          /**
62           * In converter name state.
63           */
64          CONVERTER_STATE,
65  
66          /**
67           * Dot state.
68           */
69          DOT_STATE,
70  
71          /**
72           * Min state.
73           */
74          MIN_STATE,
75  
76          /**
77           * Max state.
78           */
79          MAX_STATE;
80      }
81  
82      private static final Logger LOGGER = StatusLogger.getLogger();
83  
84      private static final int BUF_SIZE = 32;
85  
86      private static final int DECIMAL = 10;
87  
88      private final Configuration config;
89  
90      private final Map<String, Class<PatternConverter>> converterRules;
91  
92      /**
93       * Constructor.
94       *
95       * @param converterKey
96       *            The type of converters that will be used.
97       */
98      public PatternParser(final String converterKey) {
99          this(null, converterKey, null, null);
100     }
101 
102     /**
103      * Constructor.
104      *
105      * @param config
106      *            The current Configuration.
107      * @param converterKey
108      *            The key to lookup the converters.
109      * @param expected
110      *            The expected base Class of each Converter.
111      */
112     public PatternParser(final Configuration config, final String converterKey, final Class<?> expected) {
113         this(config, converterKey, expected, null);
114     }
115 
116     /**
117      * Constructor.
118      *
119      * @param config
120      *            The current Configuration.
121      * @param converterKey
122      *            The key to lookup the converters.
123      * @param expectedClass
124      *            The expected base Class of each Converter.
125      * @param filterClass
126      *            Filter the returned plugins after calling the plugin manager.
127      */
128     public PatternParser(final Configuration config, final String converterKey, final Class<?> expectedClass,
129             final Class<?> filterClass) {
130         this.config = config;
131         final PluginManager manager = new PluginManager(converterKey);
132         manager.collectPlugins(config == null ? null : config.getPluginPackages());
133         final Map<String, PluginType<?>> plugins = manager.getPlugins();
134         final Map<String, Class<PatternConverter>> converters = new LinkedHashMap<>();
135 
136         for (final PluginType<?> type : plugins.values()) {
137             try {
138                 @SuppressWarnings("unchecked")
139                 final Class<PatternConverter> clazz = (Class<PatternConverter>) type.getPluginClass();
140                 if (filterClass != null && !filterClass.isAssignableFrom(clazz)) {
141                     continue;
142                 }
143                 final ConverterKeys keys = clazz.getAnnotation(ConverterKeys.class);
144                 if (keys != null) {
145                     for (final String key : keys.value()) {
146                         if (converters.containsKey(key)) {
147                             LOGGER.warn("Converter key '{}' is already mapped to '{}'. " +
148                                     "Sorry, Dave, I can't let you do that! Ignoring plugin [{}].",
149                                 key, converters.get(key), clazz);
150                         } else {
151                             converters.put(key, clazz);
152                         }
153                     }
154                 }
155             } catch (final Exception ex) {
156                 LOGGER.error("Error processing plugin " + type.getElementName(), ex);
157             }
158         }
159         converterRules = converters;
160     }
161 
162     public List<PatternFormatter> parse(final String pattern) {
163         return parse(pattern, false, false, false);
164     }
165 
166     public List<PatternFormatter> parse(final String pattern, final boolean alwaysWriteExceptions,
167                                         final boolean noConsoleNoAnsi) {
168         return parse(pattern, alwaysWriteExceptions, false, noConsoleNoAnsi);
169     }
170 
171     public List<PatternFormatter> parse(final String pattern, final boolean alwaysWriteExceptions,
172            final boolean disableAnsi, final boolean noConsoleNoAnsi) {
173         final List<PatternFormatter> list = new ArrayList<>();
174         final List<PatternConverter> converters = new ArrayList<>();
175         final List<FormattingInfo> fields = new ArrayList<>();
176 
177         parse(pattern, converters, fields, disableAnsi, noConsoleNoAnsi, true);
178 
179         final Iterator<FormattingInfo> fieldIter = fields.iterator();
180         boolean handlesThrowable = false;
181 
182         for (final PatternConverter converter : converters) {
183             if (converter instanceof NanoTimePatternConverter) {
184                 // LOG4J2-1074 Switch to actual clock if nanosecond timestamps are required in config.
185                 // LOG4J2-1248 set config nanoclock
186                 if (config != null) {
187                     config.setNanoClock(new SystemNanoClock());
188                 }
189             }
190             LogEventPatternConverter pc;
191             if (converter instanceof LogEventPatternConverter) {
192                 pc = (LogEventPatternConverter) converter;
193                 handlesThrowable |= pc.handlesThrowable();
194             } else {
195                 pc = new LiteralPatternConverter(config, Strings.EMPTY, true);
196             }
197 
198             FormattingInfo field;
199             if (fieldIter.hasNext()) {
200                 field = fieldIter.next();
201             } else {
202                 field = FormattingInfo.getDefault();
203             }
204             list.add(new PatternFormatter(pc, field));
205         }
206         if (alwaysWriteExceptions && !handlesThrowable) {
207             final LogEventPatternConverter pc = ExtendedThrowablePatternConverter.newInstance(config, null);
208             list.add(new PatternFormatter(pc, FormattingInfo.getDefault()));
209         }
210         return list;
211     }
212 
213     /**
214      * Extracts the converter identifier found at the given start position.
215      * <p>
216      * After this function returns, the variable i will point to the first char after the end of the converter
217      * identifier.
218      * </p>
219      * <p>
220      * If i points to a char which is not a character acceptable at the start of a unicode identifier, the value null is
221      * returned.
222      * </p>
223      *
224      * @param lastChar
225      *        last processed character.
226      * @param pattern
227      *        format string.
228      * @param start
229      *        current index into pattern format.
230      * @param convBuf
231      *        buffer to receive conversion specifier.
232      * @param currentLiteral
233      *        literal to be output in case format specifier in unrecognized.
234      * @return position in pattern after converter.
235      */
236     private static int extractConverter(final char lastChar, final String pattern, final int start,
237             final StringBuilder convBuf, final StringBuilder currentLiteral) {
238         int i = start;
239         convBuf.setLength(0);
240 
241         // When this method is called, lastChar points to the first character of the
242         // conversion word. For example:
243         // For "%hello" lastChar = 'h'
244         // For "%-5hello" lastChar = 'h'
245         // System.out.println("lastchar is "+lastChar);
246         if (!Character.isUnicodeIdentifierStart(lastChar)) {
247             return i;
248         }
249 
250         convBuf.append(lastChar);
251 
252         while (i < pattern.length() && Character.isUnicodeIdentifierPart(pattern.charAt(i))) {
253             convBuf.append(pattern.charAt(i));
254             currentLiteral.append(pattern.charAt(i));
255             i++;
256         }
257 
258         return i;
259     }
260 
261     /**
262      * Extract options.
263      *
264      * @param pattern
265      *            conversion pattern.
266      * @param start
267      *            start of options.
268      * @param options
269      *            array to receive extracted options
270      * @return position in pattern after options.
271      */
272     private static int extractOptions(final String pattern, final int start, final List<String> options) {
273         int i = start;
274         while (i < pattern.length() && pattern.charAt(i) == '{') {
275             i++; // skip opening "{"
276             final int begin = i; // position of first real char
277             int depth = 1; // already inside one level
278             while (depth > 0 && i < pattern.length()) {
279                 final char c = pattern.charAt(i);
280                 if (c == '{') {
281                     depth++;
282                 } else if (c == '}') {
283                     depth--;
284                     // TODO(?) maybe escaping of { and } with \ or %
285                 }
286                 i++;
287             } // while
288 
289             if (depth > 0) { // option not closed, continue with pattern after closing bracket
290                 i = pattern.lastIndexOf('}');
291                 if (i == -1 || i < start) {
292                     // if no closing bracket could be found or there is no closing bracket behind the starting
293                     // character of our parsing process continue parsing after the first opening bracket
294                     return begin;
295                 }
296                 return i + 1;
297             }
298 
299             options.add(pattern.substring(begin, i - 1));
300         } // while
301 
302         return i;
303     }
304 
305     /**
306      * Parse a format specifier.
307      *
308      * @param pattern
309      *            pattern to parse.
310      * @param patternConverters
311      *            list to receive pattern converters.
312      * @param formattingInfos
313      *            list to receive field specifiers corresponding to pattern converters.
314      * @param noConsoleNoAnsi
315      *            do not do not output ANSI escape codes if {@link System#console()}
316      * @param convertBackslashes if {@code true}, backslash characters are treated as escape characters and character
317      *            sequences like "\" followed by "t" (backslash+t) are converted to special characters like '\t' (tab).
318      */
319     public void parse(final String pattern, final List<PatternConverter> patternConverters,
320                       final List<FormattingInfo> formattingInfos, final boolean noConsoleNoAnsi,
321                       final boolean convertBackslashes) {
322         parse(pattern, patternConverters, formattingInfos, false, noConsoleNoAnsi, convertBackslashes);
323     }
324 
325     /**
326      * Parse a format specifier.
327      *
328      * @param pattern
329      *            pattern to parse.
330      * @param patternConverters
331      *            list to receive pattern converters.
332      * @param formattingInfos
333      *            list to receive field specifiers corresponding to pattern converters.
334      * @param disableAnsi
335      *            do not output ANSI escape codes
336      * @param noConsoleNoAnsi
337      *            do not do not output ANSI escape codes if {@link System#console()}
338      * @param convertBackslashes if {@code true}, backslash characters are treated as escape characters and character
339      *            sequences like "\" followed by "t" (backslash+t) are converted to special characters like '\t' (tab).
340      */
341     public void parse(final String pattern, final List<PatternConverter> patternConverters,
342             final List<FormattingInfo> formattingInfos, final boolean disableAnsi,
343             final boolean noConsoleNoAnsi, final boolean convertBackslashes) {
344         Objects.requireNonNull(pattern, "pattern");
345 
346         final StringBuilder currentLiteral = new StringBuilder(BUF_SIZE);
347 
348         final int patternLength = pattern.length();
349         ParserState state = ParserState.LITERAL_STATE;
350         char c;
351         int i = 0;
352         FormattingInfo formattingInfo = FormattingInfo.getDefault();
353 
354         while (i < patternLength) {
355             c = pattern.charAt(i++);
356 
357             switch (state) {
358             case LITERAL_STATE:
359 
360                 // In literal state, the last char is always a literal.
361                 if (i == patternLength) {
362                     currentLiteral.append(c);
363 
364                     continue;
365                 }
366 
367                 if (c == ESCAPE_CHAR) {
368                     // peek at the next char.
369                     switch (pattern.charAt(i)) {
370                     case ESCAPE_CHAR:
371                         currentLiteral.append(c);
372                         i++; // move pointer
373 
374                         break;
375 
376                     default:
377 
378                         if (currentLiteral.length() != 0) {
379                             patternConverters.add(new LiteralPatternConverter(config, currentLiteral.toString(),
380                                     convertBackslashes));
381                             formattingInfos.add(FormattingInfo.getDefault());
382                         }
383 
384                         currentLiteral.setLength(0);
385                         currentLiteral.append(c); // append %
386                         state = ParserState.CONVERTER_STATE;
387                         formattingInfo = FormattingInfo.getDefault();
388                     }
389                 } else {
390                     currentLiteral.append(c);
391                 }
392 
393                 break;
394 
395             case CONVERTER_STATE:
396                 currentLiteral.append(c);
397 
398                 switch (c) {
399                 case '0':
400                     // a '0' directly after the % sign indicates zero-padding
401                     formattingInfo = new FormattingInfo(formattingInfo.isLeftAligned(), formattingInfo.getMinLength(),
402                             formattingInfo.getMaxLength(), formattingInfo.isLeftTruncate(), true);
403                     break;
404 
405                 case '-':
406                     formattingInfo = new FormattingInfo(true, formattingInfo.getMinLength(),
407                             formattingInfo.getMaxLength(), formattingInfo.isLeftTruncate(), formattingInfo.isZeroPad());
408                     break;
409 
410                 case '.':
411                     state = ParserState.DOT_STATE;
412                     break;
413 
414                 default:
415 
416                     if (c >= '0' && c <= '9') {
417                         formattingInfo = new FormattingInfo(formattingInfo.isLeftAligned(), c - '0',
418                                 formattingInfo.getMaxLength(), formattingInfo.isLeftTruncate(), formattingInfo.isZeroPad());
419                         state = ParserState.MIN_STATE;
420                     } else {
421                         i = finalizeConverter(c, pattern, i, currentLiteral, formattingInfo, converterRules,
422                                 patternConverters, formattingInfos, disableAnsi, noConsoleNoAnsi, convertBackslashes);
423 
424                         // Next pattern is assumed to be a literal.
425                         state = ParserState.LITERAL_STATE;
426                         formattingInfo = FormattingInfo.getDefault();
427                         currentLiteral.setLength(0);
428                     }
429                 } // switch
430 
431                 break;
432 
433             case MIN_STATE:
434                 currentLiteral.append(c);
435 
436                 if (c >= '0' && c <= '9') {
437                     // Multiply the existing value and add the value of the number just encountered.
438                     formattingInfo = new FormattingInfo(formattingInfo.isLeftAligned(), formattingInfo.getMinLength()
439                             * DECIMAL + c - '0', formattingInfo.getMaxLength(), formattingInfo.isLeftTruncate(), formattingInfo.isZeroPad());
440                 } else if (c == '.') {
441                     state = ParserState.DOT_STATE;
442                 } else {
443                     i = finalizeConverter(c, pattern, i, currentLiteral, formattingInfo, converterRules,
444                             patternConverters, formattingInfos, disableAnsi, noConsoleNoAnsi, convertBackslashes);
445                     state = ParserState.LITERAL_STATE;
446                     formattingInfo = FormattingInfo.getDefault();
447                     currentLiteral.setLength(0);
448                 }
449 
450                 break;
451 
452             case DOT_STATE:
453                 currentLiteral.append(c);
454                 switch (c) {
455                 case '-':
456                     formattingInfo = new FormattingInfo(formattingInfo.isLeftAligned(), formattingInfo.getMinLength(),
457                             formattingInfo.getMaxLength(),false, formattingInfo.isZeroPad());
458                     break;
459 
460                 default:
461 
462 	                if (c >= '0' && c <= '9') {
463 	                    formattingInfo = new FormattingInfo(formattingInfo.isLeftAligned(), formattingInfo.getMinLength(),
464 	                            c - '0', formattingInfo.isLeftTruncate(), formattingInfo.isZeroPad());
465 	                    state = ParserState.MAX_STATE;
466 	                } else {
467 	                    LOGGER.error("Error occurred in position " + i + ".\n Was expecting digit, instead got char \"" + c
468 	                            + "\".");
469 
470 	                    state = ParserState.LITERAL_STATE;
471 	                }
472                 }
473 
474                 break;
475 
476             case MAX_STATE:
477                 currentLiteral.append(c);
478 
479                 if (c >= '0' && c <= '9') {
480                     // Multiply the existing value and add the value of the number just encountered.
481                     formattingInfo = new FormattingInfo(formattingInfo.isLeftAligned(), formattingInfo.getMinLength(),
482                             formattingInfo.getMaxLength() * DECIMAL + c - '0', formattingInfo.isLeftTruncate(), formattingInfo.isZeroPad());
483                 } else {
484                     i = finalizeConverter(c, pattern, i, currentLiteral, formattingInfo, converterRules,
485                             patternConverters, formattingInfos, disableAnsi, noConsoleNoAnsi, convertBackslashes);
486                     state = ParserState.LITERAL_STATE;
487                     formattingInfo = FormattingInfo.getDefault();
488                     currentLiteral.setLength(0);
489                 }
490 
491                 break;
492             } // switch
493         }
494 
495         // while
496         if (currentLiteral.length() != 0) {
497             patternConverters.add(new LiteralPatternConverter(config, currentLiteral.toString(), convertBackslashes));
498             formattingInfos.add(FormattingInfo.getDefault());
499         }
500     }
501 
502     /**
503      * Creates a new PatternConverter.
504      *
505      * @param converterId
506      *            converterId.
507      * @param currentLiteral
508      *            literal to be used if converter is unrecognized or following converter if converterId contains extra
509      *            characters.
510      * @param rules
511      *            map of stock pattern converters keyed by format specifier.
512      * @param options
513      *            converter options.
514      * @param disableAnsi
515      *            do not output ANSI escape codes
516      * @param noConsoleNoAnsi
517      *            do not do not output ANSI escape codes if {@link System#console()}
518      * @return converter or null.
519      */
520     private PatternConverter createConverter(final String converterId, final StringBuilder currentLiteral,
521             final Map<String, Class<PatternConverter>> rules, final List<String> options, final boolean disableAnsi,
522             final boolean noConsoleNoAnsi) {
523         String converterName = converterId;
524         Class<PatternConverter> converterClass = null;
525 
526         if (rules == null) {
527             LOGGER.error("Null rules for [" + converterId + ']');
528             return null;
529         }
530         for (int i = converterId.length(); i > 0 && converterClass == null; i--) {
531             converterName = converterName.substring(0, i);
532             converterClass = rules.get(converterName);
533         }
534 
535         if (converterClass == null) {
536             LOGGER.error("Unrecognized format specifier [" + converterId + ']');
537             return null;
538         }
539 
540         if (AnsiConverter.class.isAssignableFrom(converterClass)) {
541             options.add(DISABLE_ANSI + '=' + disableAnsi);
542             options.add(NO_CONSOLE_NO_ANSI + '=' + noConsoleNoAnsi);
543         }
544         // Work around the regression bug in Class.getDeclaredMethods() in Oracle Java in version > 1.6.0_17:
545         // http://bugs.sun.com/bugdatabase/view_bug.do?bug_id=6815786
546         final Method[] methods = converterClass.getDeclaredMethods();
547         Method newInstanceMethod = null;
548         for (final Method method : methods) {
549             if (Modifier.isStatic(method.getModifiers())
550                     && method.getDeclaringClass().equals(converterClass)
551                     && method.getName().equals("newInstance")
552                     && areValidNewInstanceParameters(method.getParameterTypes())) {
553                 if (newInstanceMethod == null) {
554                     newInstanceMethod = method;
555                 } else if (method.getReturnType().equals(newInstanceMethod.getReturnType())) {
556                     LOGGER.error("Class " + converterClass + " cannot contain multiple static newInstance methods");
557                     return null;
558                 }
559             }
560         }
561         if (newInstanceMethod == null) {
562             LOGGER.error("Class " + converterClass + " does not contain a static newInstance method");
563             return null;
564         }
565 
566         final Class<?>[] parmTypes = newInstanceMethod.getParameterTypes();
567         final Object[] parms = parmTypes.length > 0 ? new Object[parmTypes.length] : null;
568 
569         if (parms != null) {
570             int i = 0;
571             boolean errors = false;
572             for (final Class<?> clazz : parmTypes) {
573                 if (clazz.isArray() && clazz.getName().equals("[Ljava.lang.String;")) {
574                     final String[] optionsArray = options.toArray(new String[options.size()]);
575                     parms[i] = optionsArray;
576                 } else if (clazz.isAssignableFrom(Configuration.class)) {
577                     parms[i] = config;
578                 } else {
579                     LOGGER.error("Unknown parameter type " + clazz.getName() + " for static newInstance method of "
580                             + converterClass.getName());
581                     errors = true;
582                 }
583                 ++i;
584             }
585             if (errors) {
586                 return null;
587             }
588         }
589 
590         try {
591             final Object newObj = newInstanceMethod.invoke(null, parms);
592 
593             if (newObj instanceof PatternConverter) {
594                 currentLiteral.delete(0, currentLiteral.length() - (converterId.length() - converterName.length()));
595 
596                 return (PatternConverter) newObj;
597             }
598             LOGGER.warn("Class {} does not extend PatternConverter.", converterClass.getName());
599         } catch (final Exception ex) {
600             LOGGER.error("Error creating converter for " + converterId, ex);
601         }
602 
603         return null;
604     }
605 
606     /** LOG4J2-2564: Returns true if all method parameters are valid for injection. */
607     private static boolean areValidNewInstanceParameters(Class<?>[] parameterTypes) {
608         for (Class<?> clazz : parameterTypes) {
609             if (!clazz.isAssignableFrom(Configuration.class)
610                     && !(clazz.isArray() && "[Ljava.lang.String;".equals(clazz.getName()))) {
611                 return false;
612             }
613         }
614         return true;
615     }
616 
617     /**
618      * Processes a format specifier sequence.
619      *
620      * @param c
621      *            initial character of format specifier.
622      * @param pattern
623      *            conversion pattern
624      * @param start
625      *            current position in conversion pattern.
626      * @param currentLiteral
627      *            current literal.
628      * @param formattingInfo
629      *            current field specifier.
630      * @param rules
631      *            map of stock pattern converters keyed by format specifier.
632      * @param patternConverters
633      *            list to receive parsed pattern converter.
634      * @param formattingInfos
635      *            list to receive corresponding field specifier.
636      * @param disableAnsi
637      *            do not output ANSI escape codes
638      * @param noConsoleNoAnsi
639      *            do not do not output ANSI escape codes if {@link System#console()}
640      * @param convertBackslashes if {@code true}, backslash characters are treated as escape characters and character
641      *            sequences like "\" followed by "t" (backslash+t) are converted to special characters like '\t' (tab).
642      * @return position after format specifier sequence.
643      */
644     private int finalizeConverter(final char c, final String pattern, final int start,
645             final StringBuilder currentLiteral, final FormattingInfo formattingInfo,
646             final Map<String, Class<PatternConverter>> rules, final List<PatternConverter> patternConverters,
647             final List<FormattingInfo> formattingInfos, final boolean disableAnsi, final boolean noConsoleNoAnsi,
648             final boolean convertBackslashes) {
649         int i = start;
650         final StringBuilder convBuf = new StringBuilder();
651         i = extractConverter(c, pattern, i, convBuf, currentLiteral);
652 
653         final String converterId = convBuf.toString();
654 
655         final List<String> options = new ArrayList<>();
656         i = extractOptions(pattern, i, options);
657 
658         final PatternConverter pc = createConverter(converterId, currentLiteral, rules, options, disableAnsi,
659             noConsoleNoAnsi);
660 
661         if (pc == null) {
662             StringBuilder msg;
663 
664             if (Strings.isEmpty(converterId)) {
665                 msg = new StringBuilder("Empty conversion specifier starting at position ");
666             } else {
667                 msg = new StringBuilder("Unrecognized conversion specifier [");
668                 msg.append(converterId);
669                 msg.append("] starting at position ");
670             }
671 
672             msg.append(Integer.toString(i));
673             msg.append(" in conversion pattern.");
674 
675             LOGGER.error(msg.toString());
676 
677             patternConverters.add(new LiteralPatternConverter(config, currentLiteral.toString(), convertBackslashes));
678             formattingInfos.add(FormattingInfo.getDefault());
679         } else {
680             patternConverters.add(pc);
681             formattingInfos.add(formattingInfo);
682 
683             if (currentLiteral.length() > 0) {
684                 patternConverters
685                         .add(new LiteralPatternConverter(config, currentLiteral.toString(), convertBackslashes));
686                 formattingInfos.add(FormattingInfo.getDefault());
687             }
688         }
689 
690         currentLiteral.setLength(0);
691 
692         return i;
693     }
694 }