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&lt;String&gt; 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&lt;String&gt; 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&lt;String&gt; 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&lt;String&gt; 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&lt;String&gt; 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&lt;List&lt;String&gt;&gt; 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&lt;List&lt;?&gt;&gt; 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&lt;String, List&lt;String&gt;&gt; multimap = VObjectPropertyValues.parseMultimap(value);
552         * Map&lt;String, List&lt;String&gt;&gt; expected = new HashMap&lt;String, List&lt;String&gt;&gt;();
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&lt;String, List&lt;?&gt;&gt; input = new LinkedHashMap&lt;String, List&lt;?&gt;&gt;();
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}