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 */
017package org.apache.logging.log4j.core.pattern;
018
019import java.lang.reflect.Method;
020import java.lang.reflect.Modifier;
021import java.util.ArrayList;
022import java.util.Iterator;
023import java.util.LinkedHashMap;
024import java.util.List;
025import java.util.Map;
026import java.util.Objects;
027
028import org.apache.logging.log4j.Logger;
029import org.apache.logging.log4j.core.config.Configuration;
030import org.apache.logging.log4j.core.config.plugins.util.PluginManager;
031import org.apache.logging.log4j.core.config.plugins.util.PluginType;
032import org.apache.logging.log4j.core.util.SystemNanoClock;
033import org.apache.logging.log4j.status.StatusLogger;
034import org.apache.logging.log4j.util.Strings;
035
036/**
037 * Most of the work of the {@link org.apache.logging.log4j.core.layout.PatternLayout} class is delegated to the
038 * PatternParser class.
039 * <p>
040 * It is this class that parses conversion patterns and creates a chained list of {@link PatternConverter
041 * PatternConverters}.
042 */
043public final class PatternParser {
044    static final String DISABLE_ANSI = "disableAnsi";
045    static final String NO_CONSOLE_NO_ANSI = "noConsoleNoAnsi";
046
047    /**
048     * Escape character for format specifier.
049     */
050    private static final char ESCAPE_CHAR = '%';
051
052    /**
053     * The states the parser can be in while parsing the pattern.
054     */
055    private enum ParserState {
056        /**
057         * Literal state.
058         */
059        LITERAL_STATE,
060
061        /**
062         * In converter name state.
063         */
064        CONVERTER_STATE,
065
066        /**
067         * Dot state.
068         */
069        DOT_STATE,
070
071        /**
072         * Min state.
073         */
074        MIN_STATE,
075
076        /**
077         * Max state.
078         */
079        MAX_STATE;
080    }
081
082    private static final Logger LOGGER = StatusLogger.getLogger();
083
084    private static final int BUF_SIZE = 32;
085
086    private static final int DECIMAL = 10;
087
088    private final Configuration config;
089
090    private final Map<String, Class<PatternConverter>> converterRules;
091
092    /**
093     * Constructor.
094     *
095     * @param converterKey
096     *            The type of converters that will be used.
097     */
098    public PatternParser(final String converterKey) {
099        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}