001/*
002 * This file is part of the JDrupes non-blocking HTTP Codec
003 * Copyright (C) 2017 Michael N. Lipp
004 *
005 * This program is free software; you can redistribute it and/or modify it 
006 * under the terms of the GNU Lesser General Public License as published
007 * by the Free Software Foundation; either version 3 of the License, or 
008 * (at your option) any later version.
009 *
010 * This program is distributed in the hope that it will be useful, but 
011 * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
012 * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public 
013 * License for more details.
014 *
015 * You should have received a copy of the GNU Lesser General Public License along 
016 * with this program; if not, see <http://www.gnu.org/licenses/>.
017 */
018
019package org.jdrupes.httpcodec.types;
020
021import java.net.HttpCookie;
022import java.net.URI;
023import java.text.ParseException;
024import java.time.Instant;
025import java.util.ArrayList;
026import java.util.List;
027import java.util.Locale;
028import java.util.Map;
029
030import org.jdrupes.httpcodec.protocols.http.HttpConstants;
031import org.jdrupes.httpcodec.types.CommentedValue.CommentedValueConverter;
032import org.jdrupes.httpcodec.types.Directive.DirectiveConverter;
033import org.jdrupes.httpcodec.types.Etag.EtagConverter;
034import org.jdrupes.httpcodec.types.MediaBase.MediaTypePair;
035import org.jdrupes.httpcodec.types.MediaBase.MediaTypePairConverter;
036import org.jdrupes.httpcodec.types.MediaRange.MediaRangeConverter;
037import org.jdrupes.httpcodec.types.MediaType.MediaTypeConverter;
038import org.jdrupes.httpcodec.types.ParameterizedValue.ParamValueConverterBase;
039import org.jdrupes.httpcodec.util.ListItemizer;
040
041/**
042 * Utility methods and singletons for converters.
043 */
044public final class Converters {
045
046        /*
047         * Note that the initialization sequence is important.
048         * Converters used by others must be defined first.
049         */
050
051        /**
052         * A noop converter, except that text is trimmed when converted to
053         * a value.
054         */
055        public static final Converter<String> UNQUOTED_STRING 
056                = new Converter<String>() {
057        
058                @Override
059                public String asFieldValue(String value) {
060                        return value;
061                }
062        
063                @Override
064                public String fromFieldValue(String text) throws ParseException {
065                        return text.trim();
066                }
067        };
068        
069        /**
070         * A noop converter, except that text is trimmed and unquoted
071         * when converted to a value.
072         */
073        public static final Converter<String> UNQUOTE_ONLY 
074                = new Converter<String>() {
075        
076                @Override
077                public String asFieldValue(String value) {
078                        return value;
079                }
080        
081                @Override
082                public String fromFieldValue(String text) throws ParseException {
083                        return unquoteString(text.trim());
084                }
085        };
086        
087        /**
088         * A converter that quotes and unquoted strings as necessary.
089         */
090        public static final Converter<String> STRING 
091                = new Converter<String>() {
092        
093                @Override
094                public String asFieldValue(String value) {
095                        return quoteIfNecessary(value);
096                }
097        
098                @Override
099                public String fromFieldValue(String text) throws ParseException {
100                        return unquoteString(text.trim());
101                }
102        };
103        
104        public static final Converter<StringList> STRING_LIST 
105                = new DefaultMultiValueConverter<>(StringList::new, STRING);
106
107        /**
108         * A converter that quotes strings.
109         */
110        public static final Converter<String> QUOTED_STRING 
111                = new Converter<String>() {
112        
113                @Override
114                public String asFieldValue(String value) {
115                        return quoteString(value);
116                }
117        
118                @Override
119                public String fromFieldValue(String text) throws ParseException {
120                        return unquoteString(text.trim());
121                }
122        };
123        
124        public static final Converter<StringList> QUOTED_STRING_LIST 
125                = new DefaultMultiValueConverter<>(StringList::new, QUOTED_STRING);
126
127        /**
128         * An integer converter.
129         */
130        public static final Converter<Long> LONG = new Converter<Long>() {
131
132                @Override
133                public String asFieldValue(Long value) {
134                        return value.toString();
135                }
136
137                @Override
138                public Long fromFieldValue(String text) throws ParseException {
139                        try {
140                                return Long.parseLong(unquoteString(text));
141                        } catch (NumberFormatException e) {
142                                throw new ParseException(text, 0);
143                        }
144                }
145        };
146
147        /**
148         * An integer list converter.
149         */
150        public static final MultiValueConverter<List<Long>, Long> LONG_LIST 
151                = new DefaultMultiValueConverter<>(ArrayList<Long>::new, LONG);
152
153        /**
154         * A date/time converter.
155         */
156        public static final Converter<Instant> DATE_TIME 
157                = new InstantConverter();
158
159        /**
160         * A converter for set cookies.
161         */
162        public static final Converter<CookieList> SET_COOKIE 
163                = new DefaultMultiValueConverter<CookieList, HttpCookie>(
164                                CookieList::new, CookieList::add, new Converter<HttpCookie>() {
165                                        
166                                        @Override
167                                        public String asFieldValue(HttpCookie value) {
168                                                StringBuilder result = new StringBuilder();
169                                                result.append(value.toString());
170                                                if (value.getMaxAge() > 0) {
171                                                        result.append("; Max-Age=");
172                                                        result.append(Long.toString(value.getMaxAge()));
173                                                }
174                                                if (value.getDomain() != null) {
175                                                        result.append("; Domain=");
176                                                        result.append(value.getDomain());
177                                                }
178                                                if (value.getPath() != null) {
179                                                        result.append("; Path=");
180                                                        result.append(value.getPath());
181                                                }
182                                                if (value.getSecure()) {
183                                                        result.append("; Secure");
184                                                }
185                                                if (value.isHttpOnly()) {
186                                                        result.append("; HttpOnly");
187                                                }
188                                                return result.toString();
189                                        }
190
191                                        @Override
192                                        public HttpCookie fromFieldValue(String text)
193                                                        throws ParseException {
194                                                throw new UnsupportedOperationException();
195                                        }
196                                }, ",", true) {
197
198                @Override
199                public CookieList fromFieldValue(String text)
200                                throws ParseException {
201                        try {
202                                return new CookieList(HttpCookie.parse(text));
203                        } catch (IllegalArgumentException e) {
204                                throw new ParseException(text, 0);
205                        }
206                }
207
208                @Override
209                public String asFieldValue(CookieList value) {
210                        throw new UnsupportedOperationException();
211                }
212        };
213
214        /**
215         * A converter for a list of cookies.
216         */
217        public static final MultiValueConverter<CookieList, HttpCookie> COOKIE_LIST 
218                = new DefaultMultiValueConverter<CookieList, HttpCookie>(CookieList::new,
219                                CookieList::add, new Converter<HttpCookie>() {
220        
221                        @Override
222                        public String asFieldValue(HttpCookie value) {
223                                return value.toString();
224                        }
225
226                        @Override
227                        public HttpCookie fromFieldValue(String text)
228                                        throws ParseException {
229                                try {
230                                        return HttpCookie.parse(text).get(0);
231                                } catch (IllegalArgumentException e) {
232                                        throw new ParseException(text, 0);
233                                }
234                        }
235                }, ";", false);
236
237        /**
238         * A converter for a language or language range. 
239         * Language range "`*`" is converted to a Locale with an empty language.
240         */
241        public static final Converter<Locale> LANGUAGE 
242                = new Converter<Locale>() {
243                
244                @Override
245                public String asFieldValue(Locale value) {
246                        return value.getCountry().length() == 0
247                                        ? value.getLanguage()
248                                        : (value.getLanguage() + "-" + value.getCountry());
249                }
250        
251                @Override
252                public Locale fromFieldValue(String text) throws ParseException {
253                        return Locale.forLanguageTag(text);
254                }
255        };
256        
257        /**
258         * A converter for a weighted list of languages.
259         */
260        public static final MultiValueConverter<List<ParameterizedValue<Locale>>,
261                ParameterizedValue<Locale>> LANGUAGE_LIST 
262                        = new DefaultMultiValueConverter<List<ParameterizedValue<Locale>>,
263                                        ParameterizedValue<Locale>>(ArrayList::new,
264                                                        new ParamValueConverterBase
265                                                                <ParameterizedValue<Locale>, Locale>(
266                                                                                LANGUAGE, ParameterizedValue<Locale>::new) {
267                                        });
268
269        /**
270         * A converter for a weighted list of strings.
271         */
272        public static final MultiValueConverter<List<ParameterizedValue<String>>,
273                ParameterizedValue<String>> WEIGHTED_STRINGS 
274                        = new DefaultMultiValueConverter<List<ParameterizedValue<String>>,
275                                        ParameterizedValue<String>>(ArrayList::new,
276                                                        new ParamValueConverterBase
277                                                                <ParameterizedValue<String>, String>(
278                                                                                STRING, ParameterizedValue<String>::new) {
279                                        });
280
281        /**
282         * A converter for the media "topLevelType/Subtype" pair.
283         */
284        public static final Converter<MediaTypePair> MEDIA_TYPE_PAIR
285                = new MediaTypePairConverter();
286
287        /**
288         * A converter for a media type pair with parameters.
289         */
290        public static final Converter<MediaRange> MEDIA_RANGE 
291                = new MediaRangeConverter();
292
293        /**
294         * A converter for a list of media ranges.
295         */
296        public static final MultiValueConverter<List<MediaRange>, MediaRange> MEDIA_RANGE_LIST 
297                        = new DefaultMultiValueConverter<List<MediaRange>,
298                                MediaRange>(ArrayList::new, MEDIA_RANGE);
299
300        /**
301         * A converter for a media type pair with parameters.
302         */
303        public static final Converter<MediaType> MEDIA_TYPE 
304                = new MediaTypeConverter();
305
306        /**
307         * A converter for a directive.
308         */
309        public static final DirectiveConverter DIRECTIVE
310                = new DirectiveConverter();
311
312        /**
313         * A converter for a list of directives.
314         */
315        public static final MultiValueConverter<List<Directive>, Directive>
316                DIRECTIVE_LIST = new DefaultMultiValueConverter<List<Directive>, Directive>(
317                                ArrayList::new, DIRECTIVE);
318        
319        /**
320         * A converter for cache control directives.
321         */
322        public static final MultiValueConverter<CacheControlDirectives, Directive>
323                CACHE_CONTROL_LIST = new DefaultMultiValueConverter
324                        <CacheControlDirectives, Directive>(
325                                        CacheControlDirectives::new, DIRECTIVE);
326        
327        /**
328         * A converter for a URI.
329         */
330        public static final Converter<URI> URI_CONV 
331                = new Converter<URI>() {
332                
333                @Override
334                public String asFieldValue(URI value) {
335                        return value.toString();
336                }
337        
338                @Override
339                public URI fromFieldValue(String text) throws ParseException {
340                        try {
341                                return URI.create(text);
342                        } catch (IllegalArgumentException e) {
343                                throw new ParseException(e.getMessage(), 0);
344                        }
345                }
346        };
347
348        /**
349         * A converter for product descriptions as used in the `User-Agent`
350         * and `Server` header fields.
351         */
352        public static final ProductDescriptionConverter PRODUCT_DESCRIPTIONS 
353                = new ProductDescriptionConverter(); 
354                
355        /**
356         * Used by the {@link EtagConverter} to unambiguously denote
357         * a decoded wildcard. If the result of `fromFieldValue` == 
358         * `WILDCARD`, the field value was an unquoted asterisk.
359         * If the result `equals("*")`, it may also have been
360         * a quoted asterisk.
361         */
362        public static final String WILDCARD = "*";
363        
364        /**
365         * A converter for an ETag header.
366         */
367        public static final EtagConverter ETAG = new EtagConverter();
368        
369        public static final Converter<List<Etag>> ETAG_LIST 
370                = new DefaultMultiValueConverter<>(ArrayList::new, ETAG);
371
372        /**
373         * A converter for a list of challenges.
374         */
375        public static final Converter<List<ParameterizedValue<String>>>
376                CHALLENGE_LIST  = new AuthInfoConverter();
377
378        public static final Converter<ParameterizedValue<String>>
379                CREDENTIALS = new Converter<ParameterizedValue<String>>() {
380
381                        @Override
382                        public String asFieldValue(ParameterizedValue<String> value) {
383                                List<ParameterizedValue<String>> tmp = new ArrayList<>();
384                                tmp.add(value);
385                                return CHALLENGE_LIST.asFieldValue(tmp);
386                        }
387
388                        @Override
389                        public ParameterizedValue<String> fromFieldValue(String text)
390                                throws ParseException {
391                                return CHALLENGE_LIST.fromFieldValue(text).get(0);
392                        }
393                
394        };
395        
396        private Converters() {
397        }
398
399        /**
400         * If the string contains a char with a backslash before it,
401         * remove the backslash.
402         * 
403         * @param value the value to unquote
404         * @return the unquoted value
405         * @throws ParseException if the input violates the field format
406         * @see "[Field value components](https://tools.ietf.org/html/rfc7230#section-3.2.6)"
407         */
408        public static String unquote(String value) {
409                StringBuilder result = new StringBuilder();
410                boolean pendingBackslash = false;
411                for(char ch: value.toCharArray()) {
412                        switch (ch) {
413                        case '\\':
414                                if (pendingBackslash) {
415                                        result.append(ch);
416                                } else {
417                                        pendingBackslash = true;
418                                        continue;
419                                }
420                        break;
421                        
422                        default:
423                                result.append(ch);
424                                break;
425                        }
426                        pendingBackslash = false;
427                }
428                return result.toString();
429        }
430
431        /**
432         * If the value is double quoted, remove the quotes and escape
433         * characters.
434         * 
435         * @param value the value to unquote
436         * @return the unquoted value
437         * @throws ParseException if the input violates the field format
438         * @see "[Field value components](https://tools.ietf.org/html/rfc7230#section-3.2.6)"
439         */
440        public static String unquoteString(String value) throws ParseException {
441                if (value.length() == 0 || value.charAt(0) != '\"') {
442                        return value;
443                }
444                String unquoted = unquote(value);
445                if (!unquoted.endsWith("\"")) {
446                        throw new ParseException(value, value.length() - 1);
447                }
448                return unquoted.substring(1, unquoted.length() - 1);
449        }
450
451        /**
452         * Returns the given string as double quoted string if necessary.
453         * 
454         * @param value the value to quote if necessary
455         * @return the result
456         * @see "[Field value components](https://tools.ietf.org/html/rfc7230#section-3.2.6)"
457         */
458        public static String quoteIfNecessary(String value) {
459                StringBuilder result = new StringBuilder();
460                boolean needsQuoting = false;
461                result.append('"');
462                for (char ch: value.toCharArray()) {
463                        if (!needsQuoting && HttpConstants.TOKEN_CHARS.indexOf(ch) < 0) {
464                                needsQuoting = true;
465                        }
466                        switch(ch) {
467                        case '"':
468                                // fall through
469                        case '\\':
470                                result.append('\\');
471                                // fall through
472                        default:
473                                result.append(ch);
474                                break;
475                        }
476                }
477                result.append('\"');
478                if (needsQuoting) {
479                        return result.toString();
480                }
481                return value;
482        }
483        
484        /**
485         * Returns the given string as double quoted string.
486         * 
487         * @param value the value to quote
488         * @return the result
489         */
490        public static String quoteString(String value) {
491                StringBuilder result = new StringBuilder();
492                result.append('"');
493                for (char ch: value.toCharArray()) {
494                        switch(ch) {
495                        case '"':
496                                // fall through
497                        case '\\':
498                                result.append('\\');
499                                // fall through
500                        default:
501                                result.append(ch);
502                                break;
503                        }
504                }
505                result.append('\"');
506                return result.toString();
507        }
508        
509        /**
510         * Return a new string in which all characters from `toBeQuoted`
511         * are prefixed with a backslash. 
512         * 
513         * @param value the string
514         * @param toBeQuoted the characters to be quoted
515         * @return the result
516         * @see "[Field value components](https://tools.ietf.org/html/rfc7230#section-3.2.6)"
517         */
518        public static String quote(String value, String toBeQuoted) {
519                StringBuilder result = new StringBuilder();
520                for (char ch: value.toCharArray()) {
521                        if (toBeQuoted.indexOf(ch) >= 0) {
522                                result.append('\\');
523                        }
524                        result.append(ch);
525                }
526                return result.toString();
527        }
528        
529        /**
530         * Determines the length of a token in a header field
531         * 
532         * @param text the text to parse
533         * @param startPos the start position
534         * @return the length of the token
535         * @see "[RFC 7230, Section 3.2.6](https://tools.ietf.org/html/rfc7230#section-3.2.6)"
536         */
537        public static int tokenLength(String text, int startPos) {
538                int pos = startPos;
539                while (pos < text.length()
540                                && HttpConstants.TOKEN_CHARS.indexOf(text.charAt(pos)) >= 0) {
541                        pos += 1;
542                }
543                return pos - startPos;
544        }
545
546        /**
547         * Determines the length of a token68 in a header field
548         * 
549         * @param text the text to parse
550         * @param startPos the start position
551         * @return the length of the token
552         * @see "[RFC 7235, Section 2.1](https://tools.ietf.org/html/rfc7235#section-2.1)"
553         */
554        public static int token68Length(String text, int startPos) {
555                int pos = startPos;
556                while (pos < text.length()
557                                && HttpConstants.TOKEN68_CHARS.indexOf(text.charAt(pos)) >= 0) {
558                        pos += 1;
559                }
560                return pos - startPos;
561        }
562
563        /**
564         * Determines the length of a white space sequence in a header field. 
565         * 
566         * @param text the test to parse 
567         * @param startPos the start position
568         * @return the length of the white space sequence
569         * @see "[RFC 7230, Section 3.2.3](https://tools.ietf.org/html/rfc7230#section-3.2.3)"
570         */
571        public static int whiteSpaceLength(String text, int startPos) {
572                int pos = startPos;
573                while (pos < text.length()) {
574                        switch (text.charAt(pos)) {
575                        case ' ':
576                                // fall through
577                        case '\t':
578                                pos += 1;
579                                continue;
580                                
581                        default:
582                                break;
583                        }
584                        break;
585                }
586                return pos - startPos;
587        }
588
589        /**
590         * Determines the length of a comment in a header field.
591         * 
592         * @param text the text to parse
593         * @param startPos the starting position (must be the position of the
594         * opening brace)
595         * @return the length of the comment
596         * @see "[RFC 7230, Section 3.2.6](https://tools.ietf.org/html/rfc7230#section-3.2.6)"
597         */
598        public static int commentLength(String text, int startPos) {
599                int pos = startPos + 1;
600                while (pos < text.length()) {
601                        switch(text.charAt(pos)) {
602                        case ')':
603                                return pos - startPos + 1;
604                                
605                        case '(':
606                                pos += commentLength(text, pos);
607                                break;
608                                
609                        case '\\':
610                                pos = Math.min(pos + 2, text.length());
611                                break;
612                                
613                        default:
614                                pos += 1;
615                                break;
616                        }
617                }
618                return pos - startPos;
619        }
620
621        /**
622         * Returns the length up to one of the match chars or end of string.
623         * 
624         * @param text the text
625         * @param startPos the start position
626         * @param matches the chars to match
627         * @return the length
628         */
629        public int unmatchedLength(String text, int startPos, String matches) {
630                int pos = startPos;
631                while (pos < text.length()) {
632                        if (matches.indexOf(text.charAt(pos)) >= 0) {
633                                return pos - startPos;
634                        }
635                        pos += 1;
636                }
637                return pos - startPos;
638        }
639        
640        private static class ProductDescriptionConverter 
641                extends DefaultMultiValueConverter<List<CommentedValue<String>>, CommentedValue<String>> {
642        
643                public ProductDescriptionConverter() {
644                        super(ArrayList<CommentedValue<String>>::new,
645                                new CommentedValueConverter<>(Converters.STRING));
646                }
647
648                /*
649                 * (non-Javadoc)
650                 * 
651                 * @see Converter#fromFieldValue(java.lang.String)
652                 */
653                @Override
654                public List<CommentedValue<String>> fromFieldValue(String text)
655                        throws ParseException {
656                        List<CommentedValue<String>> result = new ArrayList<>();
657                        int pos = 0;
658                        while (pos < text.length()) {
659                                int length = Converters.tokenLength(text, pos);
660                                if (length == 0) {
661                                        throw new ParseException(
662                                                "Must start with token: " + text, pos);
663                                }
664                                String product = text.substring(pos, pos + length);
665                                pos += length;
666                                if (pos < text.length() && text.charAt(pos) == '/') {
667                                        pos += 1;
668                                        length = Converters.tokenLength(text, pos);
669                                        if (length == 0) {
670                                                throw new ParseException(
671                                                        "Token expected: " + text, pos);
672                                        }
673                                        product = product + text.substring(pos - 1, pos + length);
674                                        pos += length;
675                                }
676                                List<String> comments = new ArrayList<>();
677                                while (pos < text.length()) {
678                                        length = Converters.whiteSpaceLength(text, pos);
679                                        if (length == 0) {
680                                                throw new ParseException(
681                                                        "Whitespace expected: " + text, pos);
682                                        }
683                                        pos += length;
684                                        if (text.charAt(pos) != '(') {
685                                                break;
686                                        }
687                                        length = Converters.commentLength(text, pos);
688                                        if (text.charAt(pos + length - 1) != ')') {
689                                                throw new ParseException(
690                                                        "Comment end expected: " + text,
691                                                        pos + length - 1);
692                                        }
693                                        comments.add(text.substring(pos + 1, pos + length - 1));
694                                        pos += length;
695                                }
696                                result.add(new CommentedValue<String>(product,
697                                        comments.size() == 0 ? null
698                                                : comments
699                                                        .toArray(new String[comments.size()])));
700                        }
701                        return result;
702                }
703
704        }
705
706        private static class AuthInfoConverter extends
707                DefaultMultiValueConverter<List<ParameterizedValue<String>>, 
708                ParameterizedValue<String>> {
709
710                public AuthInfoConverter() {
711                        super(ArrayList<ParameterizedValue<String>>::new, 
712                                new Converter<ParameterizedValue<String>>() {
713
714                                @Override
715                                public String asFieldValue(ParameterizedValue<String> value) {
716                                        StringBuilder result = new StringBuilder();
717                                        result.append(value.value());
718                                        boolean first = true;
719                                        for (Map.Entry<String, String> e: value.parameters().entrySet()) {
720                                                if (first) {
721                                                        first = false;
722                                                } else {
723                                                        result.append(',');
724                                                }
725                                                result.append(' ');
726                                                if (e.getKey() == null) {
727                                                        result.append(e.getValue());
728                                                } else {
729                                                        result.append(e.getKey());
730                                                        result.append("=");
731                                                        result.append(quoteIfNecessary(e.getValue()));
732                                                }
733                                        }
734                                        return result.toString();
735                                }
736
737                                @Override
738                                public ParameterizedValue<String> fromFieldValue(
739                                                String text) throws ParseException {
740                                        throw new UnsupportedOperationException();
741                                }
742                        }, ",");
743                }
744
745                @Override
746                public List<ParameterizedValue<String>> fromFieldValue(String text)
747                        throws ParseException {
748                        List<ParameterizedValue<String>> result = new ArrayList<>();
749                        ListItemizer itemizer = new ListItemizer(text, ",");
750                        ParameterizedValue.Builder<ParameterizedValue<String>, String>
751                                builder = null;
752                        String itemRepr = null;
753                        while (true) {
754                                // New auth scheme may have left over the parameter part as itemRepr
755                                if (itemRepr == null) {
756                                        if (!itemizer.hasNext()) {
757                                                if (builder != null) {
758                                                        result.add(builder.build());
759                                                }
760                                                break;
761                                        } 
762                                        itemRepr = itemizer.next();
763                                }
764                                if (builder != null) {
765                                        // itemRepr may be new auth scheme or parameter
766                                        ListItemizer paramItemizer = new ListItemizer(itemRepr, "=");
767                                        String name = paramItemizer.next();
768                                        if (paramItemizer.hasNext() && name.indexOf(" ") < 0) {
769                                                // Really parameter
770                                                builder.setParameter(name, unquoteString(
771                                                                paramItemizer.next()));
772                                                itemRepr = null;
773                                                continue;
774                                        }
775                                        // new challenge or credentials
776                                        result.add(builder.build());
777                                        builder = null;
778                                        // fall through
779                                }
780                                // New challenge or credentials, space used as separator
781                                ListItemizer schemeItemizer = new ListItemizer(itemRepr, " ");
782                                String authScheme = schemeItemizer.next();
783                                if (authScheme == null) {
784                                        throw new ParseException(itemRepr, 0);
785                                }
786                                builder = ParameterizedValue.builder();
787                                builder.setValue(authScheme);
788                                itemRepr = schemeItemizer.next();
789                                if (itemRepr == null
790                                        || (token68Length(itemRepr, 0) == itemRepr.length())) {
791                                        if (itemRepr != null) {
792                                                builder.setParameter(null, itemRepr);
793                                        }
794                                        result.add(builder.build());
795                                        builder = null;
796                                        // Fully processed
797                                        itemRepr = null;
798                                        continue;
799                                }
800                        }
801                        return result;
802                }
803
804        }
805
806}