001/* 002 * MIT License 003 * 004 * Copyright (c) 2016 Michael Angstadt 005 * 006 * Permission is hereby granted, free of charge, to any person obtaining a copy 007 * of this software and associated documentation files (the "Software"), to deal 008 * in the Software without restriction, including without limitation the rights 009 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 010 * copies of the Software, and to permit persons to whom the Software is 011 * furnished to do so, subject to the following conditions: 012 * 013 * The above copyright notice and this permission notice shall be included in 014 * all copies or substantial portions of the Software. 015 * 016 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 017 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 018 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 019 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 020 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 021 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 022 * SOFTWARE. 023 */ 024 025package com.github.mangstadt.vinnie.io; 026 027import java.util.ArrayList; 028import java.util.Arrays; 029import java.util.Collection; 030import java.util.HashMap; 031import java.util.Iterator; 032import java.util.LinkedHashMap; 033import java.util.List; 034import java.util.Map; 035 036/** 037 * Contains utility methods for parsing and writing property values. 038 * @author Michael Angstadt 039 */ 040public final class VObjectPropertyValues { 041 /** 042 * The local computer's newline character sequence. 043 */ 044 private static final String NEWLINE = System.getProperty("line.separator"); 045 046 /** 047 * <p> 048 * Unescapes all escaped characters in a property value. Escaped newlines 049 * are replaced with the local system's newline character sequence. 050 * </p> 051 * <p> 052 * <b>Example:</b> 053 * </p> 054 * 055 * <pre class="brush:java"> 056 * String value = "one\\,two\\;three\\nfour"; 057 * String unescaped = VObjectPropertyValues.unescape(value); 058 * assertEquals("one,two;three\nfour", unescaped); 059 * </pre> 060 * 061 * @param value the value to unescape 062 * @return the unescaped value 063 */ 064 public static String unescape(String value) { 065 return unescape(value, 0, value.length()); 066 } 067 068 /** 069 * Unescapes all escaped characters in a substring. 070 * @param string the entire string 071 * @param start the start index of the substring to unescape 072 * @param end the end index (exclusive) of the substring to unescape 073 * @return the unescaped substring 074 */ 075 private static String unescape(String string, int start, int end) { 076 StringBuilder sb = null; 077 boolean escaped = false; 078 for (int i = start; i < end; i++) { 079 char c = string.charAt(i); 080 081 if (escaped) { 082 escaped = false; 083 084 if (sb == null) { 085 sb = new StringBuilder(end - start); 086 sb.append(string.substring(start, i - 1)); 087 } 088 089 switch (c) { 090 case 'n': 091 case 'N': 092 sb.append(NEWLINE); 093 continue; 094 } 095 096 sb.append(c); 097 continue; 098 } 099 100 switch (c) { 101 case '\\': 102 escaped = true; 103 continue; 104 } 105 106 if (sb != null) { 107 sb.append(c); 108 } 109 } 110 111 if (sb != null) { 112 return sb.toString(); 113 } 114 115 /* 116 * The "String#substring" method makes no guarantee that the same String 117 * object will be returned if the entire string length is passed into 118 * the method. 119 */ 120 if (start == 0 && end == string.length()) { 121 return string; 122 } 123 124 return string.substring(start, end); 125 } 126 127 /** 128 * <p> 129 * Escapes all special characters within a property value. These characters 130 * are: 131 * </p> 132 * <ul> 133 * <li>backslashes ({@code \})</li> 134 * <li>commas ({@code ,})</li> 135 * <li>semi-colons ({@code ;})</li> 136 * </ul> 137 * <p> 138 * Newlines are not escaped by this method. They are automatically escaped 139 * by {@link VObjectWriter} when the data is serialized. 140 * </p> 141 * <p> 142 * <b>Example:</b> 143 * </p> 144 * 145 * <pre class="brush:java"> 146 * String value = "one,two;three\nfour"; 147 * String escaped = VObjectPropertyValues.escape(value); 148 * assertEquals("one\\,two\\;three\nfour", escaped); 149 * </pre> 150 * 151 * @param value the value to escape 152 * @return the escaped value 153 */ 154 public static String escape(String value) { 155 StringBuilder sb = null; 156 for (int i = 0; i < value.length(); i++) { 157 char c = value.charAt(i); 158 switch (c) { 159 case '\\': 160 case ',': 161 case ';': 162 if (sb == null) { 163 sb = new StringBuilder(value.length() * 2); 164 sb.append(value.substring(0, i)); 165 } 166 sb.append('\\').append(c); 167 break; 168 default: 169 if (sb != null) { 170 sb.append(c); 171 } 172 break; 173 } 174 } 175 return (sb == null) ? value : sb.toString(); 176 } 177 178 /** 179 * Escapes all special characters within the given string. 180 * @param string the string to escape 181 * @param escapeCommas true to escape comma characters, false not to. 182 * Old-style syntax does not expect commas to be escaped in semi-structured 183 * values. 184 * @param sb the buffer on which to append the escaped string 185 */ 186 private static void escape(String string, boolean escapeCommas, StringBuilder sb) { 187 for (int i = 0; i < string.length(); i++) { 188 char c = string.charAt(i); 189 if (c == '\\' || c == ';' || (escapeCommas && c == ',')) { 190 sb.append('\\'); 191 } 192 sb.append(c); 193 } 194 } 195 196 /** 197 * <p> 198 * Parses a "list" property value. 199 * </p> 200 * <p> 201 * List values contain multiple values separated by commas. The order that 202 * the values are in usually doesn't matter. 203 * </p> 204 * <p> 205 * <b>Example:</b> 206 * </p> 207 * 208 * <pre class="brush:java"> 209 * String value = "one,two\\,three"; 210 * List<String> list = VObjectPropertyValues.parseList(value); 211 * assertEquals(Arrays.asList("one", "two,three"), list); 212 * </pre> 213 * 214 * @param value the value to parse 215 * @return the parsed list 216 */ 217 public static List<String> parseList(String value) { 218 return split(value, ',', -1); 219 } 220 221 /** 222 * <p> 223 * Generates a "list" property value. 224 * </p> 225 * <p> 226 * List values contain multiple values separated by commas. The order that 227 * the values are in usually doesn't matter. 228 * </p> 229 * <p> 230 * Each list item's {@code toString()} method is called to generate its 231 * string representation. If a list item is null, then "null" will be 232 * outputted. 233 * </p> 234 * <p> 235 * <b>Example:</b> 236 * </p> 237 * 238 * <pre class="brush:java"> 239 * List<String> list = Arrays.asList("one", "two", null, "three,four"); 240 * String value = VObjectPropertyValues.writeList(list); 241 * assertEquals("one,two,null,three\\,four", value); 242 * </pre> 243 * 244 * @param values the values to write 245 * @return the list value string 246 */ 247 public static String writeList(Collection<?> values) { 248 StringBuilder sb = new StringBuilder(); 249 250 boolean first = true; 251 for (Object value : values) { 252 if (!first) { 253 sb.append(','); 254 } 255 256 if (value == null) { 257 sb.append("null"); 258 } else { 259 escape(value.toString(), true, sb); 260 } 261 262 first = false; 263 } 264 265 return sb.toString(); 266 } 267 268 /** 269 * <p> 270 * Parses a "semi-structured" property value. 271 * </p> 272 * <p> 273 * Semi-structured values contain multiple values separate by semicolons. 274 * Unlike structured values, each value cannot have their own 275 * comma-delimited list of sub-values. The order that the values are in 276 * usually matters. 277 * </p> 278 * <p> 279 * <b>Example:</b> 280 * </p> 281 * 282 * <pre class="brush:java"> 283 * String value = "one;two\\;three,four"; 284 * List<String> values = VObjectPropertyValues.parseSemiStructured(value); 285 * assertEquals(Arrays.asList("one", "two;three,four"), values); 286 * </pre> 287 * 288 * @param value the value to parse 289 * @return the parsed values 290 */ 291 public static List<String> parseSemiStructured(String value) { 292 return parseSemiStructured(value, -1); 293 } 294 295 /** 296 * <p> 297 * Parses a "semi-structured" property value. 298 * </p> 299 * <p> 300 * Semi-structured values contain multiple values separate by semicolons. 301 * Unlike structured values, each value cannot have their own 302 * comma-delimited list of sub-values. The order that the values are in 303 * usually matters. 304 * </p> 305 * <p> 306 * <b>Example:</b> 307 * </p> 308 * 309 * <pre class="brush:java"> 310 * String value = "one;two;three"; 311 * List<String> values = VObjectPropertyValues.parseSemiStructured(value, 2); 312 * assertEquals(Arrays.asList("one", "two;three"), values); 313 * </pre> 314 * 315 * @param value the value to parse 316 * @param limit the max number of items to parse 317 * @return the parsed values 318 */ 319 public static List<String> parseSemiStructured(String value, int limit) { 320 return split(value, ';', limit); 321 } 322 323 /** 324 * <p> 325 * Writes a "semi-structured" property value. 326 * </p> 327 * <p> 328 * Semi-structured values contain multiple values separate by semicolons. 329 * Unlike structured values, each value cannot have their own 330 * comma-delimited list of sub-values. The order that the values are in 331 * usually matters. 332 * <p> 333 * <b>Example:</b> 334 * </p> 335 * 336 * <pre class="brush:java"> 337 * List<String> list = Arrays.asList("one", null, "two;three", ""); 338 * 339 * String value = VObjectPropertyValues.writeSemiStructured(list, false); 340 * assertEquals("one;null;two\\;three", value); 341 * 342 * value = VObjectPropertyValues.writeSemiStructured(list, true); 343 * assertEquals("one;null;two\\;three;", value); 344 * </pre> 345 * 346 * @param values the values to write 347 * @param escapeCommas true to escape comma characters, false not to. 348 * Old-style syntax does not expect commas to be escaped in semi-structured 349 * values. 350 * @param includeTrailingSemicolons true to include the semicolon delimiters 351 * for empty values at the end of the values list, false to trim them 352 * @return the semi-structured value string 353 */ 354 public static String writeSemiStructured(List<?> values, boolean escapeCommas, boolean includeTrailingSemicolons) { 355 StringBuilder sb = new StringBuilder(); 356 357 boolean first = true; 358 for (Object value : values) { 359 if (!first) { 360 sb.append(';'); 361 } 362 363 if (value == null) { 364 sb.append("null"); 365 } else { 366 escape(value.toString(), escapeCommas, sb); 367 } 368 369 first = false; 370 } 371 372 if (!includeTrailingSemicolons) { 373 trimTrailingSemicolons(sb); 374 } 375 376 return sb.toString(); 377 } 378 379 /** 380 * <p> 381 * Parses a "structured" property value. 382 * </p> 383 * <p> 384 * Structured values are essentially 2-D arrays. They contain multiple 385 * components separated by semicolons, and each component can have multiple 386 * values separated by commas. The order that the components are in matters, 387 * but the order that each component's list of values are in usually doesn't 388 * matter. 389 * </p> 390 * <p> 391 * <b>Example:</b> 392 * </p> 393 * 394 * <pre class="brush:java"> 395 * String value = "one;two,three;four\\,five\\;six"; 396 * List<List<String>> values = VObjectPropertyValues.parseStructured(value); 397 * assertEquals(Arrays.asList( 398 * Arrays.asList("one"), 399 * Arrays.asList("two", "three"), 400 * Arrays.asList("four,five;six") 401 * ), values); 402 * </pre> 403 * @param value the value to parse 404 * @return the parsed values 405 */ 406 public static List<List<String>> parseStructured(String value) { 407 if (value.length() == 0) { 408 return new ArrayList<List<String>>(0); //return a mutable list 409 } 410 411 List<List<String>> components = new ArrayList<List<String>>(); 412 List<String> curComponent = new ArrayList<String>(); 413 components.add(curComponent); 414 415 boolean escaped = false; 416 int cursor = 0; 417 for (int i = 0; i < value.length(); i++) { 418 if (escaped) { 419 escaped = false; 420 continue; 421 } 422 423 char c = value.charAt(i); 424 switch (c) { 425 case ';': 426 String v = unescape(value, cursor, i); 427 if (curComponent.isEmpty() && v.length() == 0) { 428 /* 429 * If the component is empty, do not add an empty string to 430 * the list. 431 */ 432 } else { 433 curComponent.add(v); 434 } 435 436 curComponent = new ArrayList<String>(); 437 components.add(curComponent); 438 cursor = i + 1; 439 continue; 440 case ',': 441 v = unescape(value, cursor, i); 442 curComponent.add(v); 443 cursor = i + 1; 444 continue; 445 case '\\': 446 escaped = true; 447 continue; 448 } 449 } 450 451 String v = unescape(value, cursor, value.length()); 452 if (curComponent.isEmpty() && v.length() == 0) { 453 /* 454 * If the component is empty, do not add an empty string to the 455 * list. 456 */ 457 } else { 458 curComponent.add(v); 459 } 460 461 return components; 462 } 463 464 /** 465 * <p> 466 * Writes a "structured" property value. 467 * </p> 468 * <p> 469 * Structured values are essentially 2-D arrays. They contain multiple 470 * components separated by semicolons, and each component can have multiple 471 * values separated by commas. The order that the components are in matters, 472 * but the order that each component's list of values are in usually doesn't 473 * matter. 474 * </p> 475 * <p> 476 * The {@code toString()} method of each component value is called to 477 * generate its string representation. If a value is null, then "null" will 478 * be outputted. 479 * </p> 480 * <p> 481 * <b>Example:</b> 482 * </p> 483 * 484 * <pre class="brush:java"> 485 * List<List<?>> values = Arrays.asList( 486 * Arrays.asList("one"), 487 * Arrays.asList("two", "three", null), 488 * Arrays.asList("four,five;six"), 489 * Arrays.asList() 490 * ); 491 * 492 * String value = VObjectPropertyValues.writeStructured(values, false); 493 * assertEquals("one;two,three,null;four\\,five\\;six", value); 494 * 495 * value = VObjectPropertyValues.writeStructured(values, true); 496 * assertEquals("one;two,three,null;four\\,five\\;six;", value); 497 * </pre> 498 * @param components the components to write 499 * @param includeTrailingSemicolons true to include the semicolon delimiters 500 * for empty components at the end of the written value, false to trim them 501 * @return the structured value string 502 */ 503 public static String writeStructured(List<? extends List<?>> components, boolean includeTrailingSemicolons) { 504 StringBuilder sb = new StringBuilder(); 505 boolean firstComponent = true; 506 for (List<?> component : components) { 507 if (!firstComponent) { 508 sb.append(';'); 509 } 510 511 boolean firstValue = true; 512 for (Object value : component) { 513 if (!firstValue) { 514 sb.append(','); 515 } 516 517 if (value == null) { 518 sb.append("null"); 519 } else { 520 escape(value.toString(), true, sb); 521 } 522 523 firstValue = false; 524 } 525 526 firstComponent = false; 527 } 528 529 if (!includeTrailingSemicolons) { 530 trimTrailingSemicolons(sb); 531 } 532 533 return sb.toString(); 534 } 535 536 /** 537 * <p> 538 * Parses a "multimap" property value. 539 * </p> 540 * <p> 541 * Multimap values are collections of key/value pairs whose keys can be 542 * multi-valued. Key/value pairs are separated by semicolons. Values are 543 * separated by commas. Keys are converted to uppercase. 544 * </p> 545 * <p> 546 * <b>Example:</b> 547 * </p> 548 * 549 * <pre class="brush:java"> 550 * String value = "one=two;THREE=four,five\\,six\\;seven"; 551 * Map<String, List<String>> multimap = VObjectPropertyValues.parseMultimap(value); 552 * Map<String, List<String>> expected = new HashMap<String, List<String>>(); 553 * expected.put("ONE", Arrays.asList("two")); 554 * expected.put("THREE", Arrays.asList("four", "five,six;seven")); 555 * assertEquals(expected, multimap); 556 * </pre> 557 * 558 * @param value the value to parse 559 * @return the parsed values 560 */ 561 public static Map<String, List<String>> parseMultimap(String value) { 562 if (value.length() == 0) { 563 return new HashMap<String, List<String>>(0); //return a mutable map 564 } 565 566 Map<String, List<String>> multimap = new LinkedHashMap<String, List<String>>(); 567 String curName = null; 568 List<String> curValues = new ArrayList<String>(); 569 570 boolean escaped = false; 571 int cursor = 0; 572 for (int i = 0; i < value.length(); i++) { 573 if (escaped) { 574 escaped = false; 575 continue; 576 } 577 578 char c = value.charAt(i); 579 580 switch (c) { 581 case ';': 582 if (curName == null) { 583 curName = unescape(value, cursor, i).toUpperCase(); 584 } else { 585 curValues.add(unescape(value, cursor, i)); 586 } 587 588 if (curName.length() > 0) { 589 if (curValues.isEmpty()) { 590 curValues.add(""); 591 } 592 List<String> existing = multimap.get(curName); 593 if (existing == null) { 594 multimap.put(curName, curValues); 595 } else { 596 existing.addAll(curValues); 597 } 598 } 599 600 curName = null; 601 curValues = new ArrayList<String>(); 602 cursor = i + 1; 603 break; 604 case '=': 605 if (curName == null) { 606 curName = unescape(value, cursor, i).toUpperCase(); 607 cursor = i + 1; 608 } 609 break; 610 case ',': 611 curValues.add(unescape(value, cursor, i)); 612 cursor = i + 1; 613 break; 614 case '\\': 615 escaped = true; 616 break; 617 } 618 } 619 620 if (curName == null) { 621 curName = unescape(value, cursor, value.length()).toUpperCase(); 622 } else { 623 curValues.add(unescape(value, cursor, value.length())); 624 } 625 626 if (curName.length() > 0) { 627 if (curValues.isEmpty()) { 628 curValues.add(""); 629 } 630 List<String> existing = multimap.get(curName); 631 if (existing == null) { 632 multimap.put(curName, curValues); 633 } else { 634 existing.addAll(curValues); 635 } 636 } 637 638 return multimap; 639 } 640 641 /** 642 * <p> 643 * Writes a "multimap" property value. 644 * </p> 645 * <p> 646 * Multimap values are collections of key/value pairs whose keys can be 647 * multi-valued. Key/value pairs are separated by semicolons. Values are 648 * separated by commas. Keys are converted to uppercase. 649 * </p> 650 * <p> 651 * Each value's {@code toString()} method is called to generate its string 652 * representation. If a value is null, then "null" will be outputted. 653 * </p> 654 * <p> 655 * <b>Example:</b> 656 * </p> 657 * 658 * <pre class="brush:java"> 659 * Map<String, List<?>> input = new LinkedHashMap<String, List<?>>(); 660 * input.put("one", Arrays.asList("two")); 661 * input.put("THREE", Arrays.asList("four", "five,six;seven")); 662 * 663 * String value = VObjectPropertyValues.writeMultimap(input); 664 * assertEquals("ONE=two;THREE=four,five\\,six\\;seven", value); 665 * </pre> 666 * 667 * @param multimap the multimap to write 668 * @return the multimap value string 669 */ 670 public static String writeMultimap(Map<String, ? extends List<?>> multimap) { 671 StringBuilder sb = new StringBuilder(); 672 boolean firstKey = true; 673 for (Map.Entry<String, ? extends List<?>> entry : multimap.entrySet()) { 674 if (!firstKey) { 675 sb.append(';'); 676 } 677 678 String key = entry.getKey().toUpperCase(); 679 escape(key, true, sb); 680 681 List<?> values = entry.getValue(); 682 if (values.isEmpty()) { 683 continue; 684 } 685 686 sb.append('='); 687 688 boolean firstValue = true; 689 for (Object value : values) { 690 if (!firstValue) { 691 sb.append(','); 692 } 693 694 if (value == null) { 695 sb.append("null"); 696 } else { 697 escape(value.toString(), true, sb); 698 } 699 700 firstValue = false; 701 } 702 703 firstKey = false; 704 } 705 706 return sb.toString(); 707 } 708 709 /** 710 * Removes trailing semicolon characters from the end of the given buffer. 711 * @param sb the buffer 712 */ 713 private static void trimTrailingSemicolons(StringBuilder sb) { 714 int index = -1; 715 for (int i = sb.length() - 1; i >= 0; i--) { 716 char c = sb.charAt(i); 717 if (c != ';') { 718 index = i; 719 break; 720 } 721 } 722 sb.setLength(index + 1); 723 } 724 725 /** 726 * Splits a string. 727 * @param string the string to split 728 * @param delimiter the delimiter to split by 729 * @param limit the number of split values to parse or -1 to parse them all 730 * @return the split values 731 */ 732 private static List<String> split(String string, char delimiter, int limit) { 733 if (string.length() == 0) { 734 return new ArrayList<String>(0); //return a mutable list 735 } 736 737 List<String> list = new ArrayList<String>(); 738 boolean escaped = false; 739 int cursor = 0; 740 for (int i = 0; i < string.length(); i++) { 741 char ch = string.charAt(i); 742 743 if (escaped) { 744 escaped = false; 745 continue; 746 } 747 748 if (ch == delimiter) { 749 String value = unescape(string, cursor, i); 750 list.add(value); 751 752 cursor = i + 1; 753 if (limit > 0 && list.size() == limit - 1) { 754 break; 755 } 756 757 continue; 758 } 759 760 switch (ch) { 761 case '\\': 762 escaped = true; 763 continue; 764 } 765 } 766 767 String value = unescape(string, cursor, string.length()); 768 list.add(value); 769 770 return list; 771 } 772 773 /** 774 * <p> 775 * Helper class for iterating over the values in a "semi-structured" 776 * property value. 777 * </p> 778 * <p> 779 * Semi-structured values contain multiple values separate by semicolons. 780 * Unlike structured values, each value cannot have their own 781 * comma-delimited list of sub-values. The order that the values are in 782 * usually matters. 783 * </p> 784 * <p> 785 * <b>Example:</b> 786 * </p> 787 * 788 * <pre class="brush:java"> 789 * String value = "one;two;;three"; 790 * 791 * SemiStructuredValueIterator it = new SemiStructuredValueIterator(value); 792 * assertEquals("one", it.next()); 793 * assertEquals("two", it.next()); 794 * assertNull(it.next()); 795 * assertEquals("three", it.next()); 796 * assertFalse(it.hasNext()); 797 * 798 * it = new SemiStructuredValueIterator(value, 2); 799 * assertEquals("one", it.next()); 800 * assertEquals("two;;three", it.next()); 801 * assertFalse(it.hasNext()); 802 * </pre> 803 */ 804 public static class SemiStructuredValueIterator { 805 private final Iterator<String> it; 806 807 /** 808 * Constructs a new semi-structured value iterator. 809 * @param value the value to parse 810 */ 811 public SemiStructuredValueIterator(String value) { 812 this(value, -1); 813 } 814 815 /** 816 * Constructs a new semi-structured value iterator. 817 * @param value the value to parse 818 * @param limit the number of values to parse, or -1 to parse all values 819 */ 820 public SemiStructuredValueIterator(String value, int limit) { 821 it = parseSemiStructured(value, limit).iterator(); 822 } 823 824 /** 825 * Gets the next value. 826 * @return the next value or null if the value is empty or null if there 827 * are no more values 828 */ 829 public String next() { 830 if (!hasNext()) { 831 return null; 832 } 833 834 String next = it.next(); 835 return (next.length() == 0) ? null : next; 836 } 837 838 /** 839 * Determines if there are any more values left. 840 * @return true if there are more values, false if not 841 */ 842 public boolean hasNext() { 843 return it.hasNext(); 844 } 845 } 846 847 /** 848 * <p> 849 * Helper class for building "semi-structured" property values. 850 * </p> 851 * <p> 852 * Semi-structured values contain multiple values separate by semicolons. 853 * Unlike structured values, each value cannot have their own 854 * comma-delimited list of sub-values. The order that the values are in 855 * usually matters. 856 * </p> 857 * <p> 858 * <b>Example:</b> 859 * </p> 860 * 861 * <pre class="brush:java"> 862 * SemiStructuredValueBuilder b = new SemiStructuredValueBuilder(); 863 * b.append("one").append(null).append("two").append(""); 864 * assertEquals("one;;two;", b.build()); 865 * assertEquals("one;;two", b.build(false)); 866 * </pre> 867 */ 868 public static class SemiStructuredValueBuilder { 869 private final List<Object> values = new ArrayList<Object>(); 870 871 /** 872 * Appends a value to the semi-structured value. The value's 873 * {@code toString()} method will be called to generate its string 874 * representation. If the value is null, then an empty string will be 875 * appended. 876 * @param value the value 877 * @return this 878 */ 879 public SemiStructuredValueBuilder append(Object value) { 880 if (value == null) { 881 value = ""; 882 } 883 values.add(value); 884 return this; 885 } 886 887 /** 888 * Builds the semi-structured value string. 889 * @param escapeCommas true to escape comma characters, false not to. 890 * Old-style syntax does not expect commas to be escaped in 891 * semi-structured values. 892 * @param includeTrailingSemicolons true to include the semicolon 893 * delimiters of empty values at the end of the value string, false to 894 * trim them 895 * @return the semi-structured value string 896 */ 897 public String build(boolean escapeCommas, boolean includeTrailingSemicolons) { 898 return writeSemiStructured(values, escapeCommas, includeTrailingSemicolons); 899 } 900 } 901 902 /** 903 * <p> 904 * Helper class for iterating over the values in a "structured" property 905 * value. 906 * </p> 907 * <p> 908 * Structured values are essentially 2-D arrays. They contain multiple 909 * components separated by semicolons, and each component can have multiple 910 * values separated by commas. The order that the components are in matters, 911 * but the order that each component's list of values are in usually doesn't 912 * matter. 913 * </p> 914 * <p> 915 * <b>Example:</b> 916 * </p> 917 * 918 * <pre class="brush:java"> 919 * String value = "one;two,three;;;four"; 920 * StructuredValueIterator it = new StructuredValueIterator(value); 921 * 922 * assertEquals(Arrays.asList("one"), it.nextComponent()); 923 * assertEquals(Arrays.asList("two", "three"), it.nextComponent()); 924 * assertEquals(Arrays.asList(), it.nextComponent()); 925 * assertNull(it.nextValue()); 926 * assertEquals("four", it.nextValue()); 927 * assertFalse(it.hasNext()); 928 * </pre> 929 */ 930 public static class StructuredValueIterator { 931 private final Iterator<List<String>> it; 932 933 /** 934 * Constructs a new structured value iterator. 935 * @param string the structured value to parse 936 */ 937 public StructuredValueIterator(String string) { 938 this(parseStructured(string)); 939 } 940 941 /** 942 * Constructs a new structured value iterator. 943 * @param components the components to iterator over 944 */ 945 public StructuredValueIterator(List<List<String>> components) { 946 it = components.iterator(); 947 } 948 949 /** 950 * Gets the first value of the next component. 951 * @return the value or null if the component is empty or null if there 952 * are no more components 953 */ 954 public String nextValue() { 955 if (!hasNext()) { 956 return null; 957 } 958 959 List<String> list = it.next(); 960 return list.isEmpty() ? null : list.get(0); 961 } 962 963 /** 964 * Gets the next component. 965 * @return the next component or an empty list if there are no more 966 * components 967 */ 968 public List<String> nextComponent() { 969 if (!hasNext()) { 970 return new ArrayList<String>(0); //should be mutable 971 } 972 973 return it.next(); 974 } 975 976 public boolean hasNext() { 977 return it.hasNext(); 978 } 979 } 980 981 /** 982 * <p> 983 * Helper class for building "structured" property values. 984 * </p> 985 * <p> 986 * Structured values are essentially 2-D arrays. They contain multiple 987 * components separated by semicolons, and each component can have multiple 988 * values separated by commas. The order that the components are in matters, 989 * but the order that each component's list of values are in usually doesn't 990 * matter. 991 * </p> 992 * <p> 993 * <b>Example:</b> 994 * </p> 995 * 996 * <pre class="brush:java"> 997 * StructuredValueBuilder b = new StructuredValueBuilder(); 998 * b.append("one").append(Arrays.asList("two", "three")).append(""); 999 * assertEquals("one;two,three;", b.build()); 1000 * assertEquals("one;two,three", b.build(false)); 1001 * </pre> 1002 */ 1003 public static class StructuredValueBuilder { 1004 private final List<List<?>> components = new ArrayList<List<?>>(); 1005 1006 /** 1007 * Appends a single-valued component. The value's {@code toString()} 1008 * method will be called to generate its string representation. If the 1009 * value is null, then an empty component will be appended. 1010 * @param value the value 1011 * @return this 1012 */ 1013 public StructuredValueBuilder append(Object value) { 1014 List<Object> component = (value == null) ? Arrays.<Object> asList() : Arrays.asList(value); 1015 return append(component); 1016 } 1017 1018 /** 1019 * Appends a component. The {@code toString()} method of each component 1020 * value will be called to generate its string representation. If a 1021 * value is null, then "null" will be outputted. 1022 * @param component the component 1023 * @return this 1024 */ 1025 public StructuredValueBuilder append(List<?> component) { 1026 if (component == null) { 1027 component = Arrays.<Object> asList(); 1028 } 1029 components.add(component); 1030 return this; 1031 } 1032 1033 /** 1034 * Builds the structured value string. Trailing semicolon delimiters 1035 * will not be trimmed. 1036 * @return the structured value string 1037 */ 1038 public String build() { 1039 return build(true); 1040 } 1041 1042 /** 1043 * Builds the structured value string. 1044 * @param includeTrailingSemicolons true to include the semicolon 1045 * delimiters for empty components at the end of the value string, false 1046 * to trim them 1047 * @return the structured value string 1048 */ 1049 public String build(boolean includeTrailingSemicolons) { 1050 return writeStructured(components, includeTrailingSemicolons); 1051 } 1052 } 1053 1054 private VObjectPropertyValues() { 1055 //hide 1056 } 1057}