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