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}