001package biweekly.io.scribe.property;
002
003import static biweekly.io.xml.XCalNamespaceContext.XCAL_NS;
004import static biweekly.util.StringUtils.join;
005
006import java.util.ArrayList;
007import java.util.Arrays;
008import java.util.Collection;
009import java.util.Date;
010import java.util.Iterator;
011import java.util.List;
012import java.util.Map;
013import java.util.TimeZone;
014
015import javax.xml.namespace.QName;
016
017import org.w3c.dom.Element;
018
019import biweekly.ICalDataType;
020import biweekly.ICalendar;
021import biweekly.Warning;
022import biweekly.io.CannotParseException;
023import biweekly.io.SkipMeException;
024import biweekly.io.json.JCalValue;
025import biweekly.io.text.ICalRawWriter;
026import biweekly.io.xml.XCalElement;
027import biweekly.parameter.ICalParameters;
028import biweekly.property.ICalProperty;
029import biweekly.util.ICalDateFormat;
030import biweekly.util.ListMultimap;
031import biweekly.util.StringUtils;
032import biweekly.util.StringUtils.JoinCallback;
033import biweekly.util.StringUtils.JoinMapCallback;
034import biweekly.util.XmlUtils;
035
036/*
037 Copyright (c) 2013, Michael Angstadt
038 All rights reserved.
039
040 Redistribution and use in source and binary forms, with or without
041 modification, are permitted provided that the following conditions are met: 
042
043 1. Redistributions of source code must retain the above copyright notice, this
044 list of conditions and the following disclaimer. 
045 2. Redistributions in binary form must reproduce the above copyright notice,
046 this list of conditions and the following disclaimer in the documentation
047 and/or other materials provided with the distribution. 
048
049 THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
050 ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
051 WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
052 DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR
053 ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
054 (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
055 LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
056 ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
057 (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
058 SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
059 */
060
061/**
062 * Base class for iCalendar property scribes.
063 * @param <T> the property class
064 * @author Michael Angstadt
065 */
066public abstract class ICalPropertyScribe<T extends ICalProperty> {
067        protected final Class<T> clazz;
068        protected final String propertyName;
069        protected final ICalDataType defaultDataType;
070        protected final QName qname;
071
072        /**
073         * Creates a new scribe.
074         * @param clazz the property class
075         * @param propertyName the property name (e.g. "VERSION")
076         * @param defaultDataType the property's default data type (e.g. "text") or
077         * null if unknown
078         */
079        public ICalPropertyScribe(Class<T> clazz, String propertyName, ICalDataType defaultDataType) {
080                this(clazz, propertyName, defaultDataType, new QName(XCAL_NS, propertyName.toLowerCase()));
081        }
082
083        /**
084         * Creates a new scribe.
085         * @param clazz the property class
086         * @param propertyName the property name (e.g. "VERSION")
087         * @param defaultDataType the property's default data type (e.g. "text") or
088         * null if unknown
089         * @param qname the XML element name and namespace to use for xCal documents
090         * (by default, the XML element name is set to the lower-cased property
091         * name, and the element namespace is set to the xCal namespace)
092         */
093        public ICalPropertyScribe(Class<T> clazz, String propertyName, ICalDataType defaultDataType, QName qname) {
094                this.clazz = clazz;
095                this.propertyName = propertyName;
096                this.defaultDataType = defaultDataType;
097                this.qname = qname;
098        }
099
100        /**
101         * Gets the property class.
102         * @return the property class
103         */
104        public Class<T> getPropertyClass() {
105                return clazz;
106        }
107
108        /**
109         * Gets the property name.
110         * @return the property name (e.g. "VERSION")
111         */
112        public String getPropertyName() {
113                return propertyName;
114        }
115
116        /**
117         * Gets the property's default data type.
118         * @return the default data type (e.g. "text") or null if unknown
119         */
120        public ICalDataType getDefaultDataType() {
121                return defaultDataType;
122        }
123
124        /**
125         * Gets this property's local name and namespace for xCal documents.
126         * @return the XML local name and namespace
127         */
128        public QName getQName() {
129                return qname;
130        }
131
132        /**
133         * Sanitizes a property's parameters (called before the property is
134         * written). Note that a copy of the parameters is returned so that the
135         * property object does not get modified.
136         * @param property the property
137         * @return the sanitized parameters
138         */
139        public final ICalParameters prepareParameters(T property) {
140                //make a copy because the property should not get modified when it is marshalled
141                ICalParameters copy = new ICalParameters(property.getParameters());
142                _prepareParameters(property, copy);
143                return copy;
144        }
145
146        /**
147         * Determines the data type of a property instance.
148         * @param property the property
149         * @return the data type or null if unknown
150         */
151        public final ICalDataType dataType(T property) {
152                return _dataType(property);
153        }
154
155        /**
156         * Marshals a property's value to a string.
157         * @param property the property
158         * @return the marshalled value
159         * @throws SkipMeException if the property should not be written to the data
160         * stream
161         */
162        public final String writeText(T property) {
163                return _writeText(property);
164        }
165
166        /**
167         * Marshals a property's value to an XML element (xCal).
168         * @param property the property
169         * @param element the property's XML element
170         * @throws SkipMeException if the property should not be written to the data
171         * stream
172         */
173        public final void writeXml(T property, Element element) {
174                XCalElement xcalElement = new XCalElement(element);
175                _writeXml(property, xcalElement);
176        }
177
178        /**
179         * Marshals a property's value to a JSON data stream (jCal).
180         * @param property the property
181         * @return the marshalled value
182         * @throws SkipMeException if the property should not be written to the data
183         * stream
184         */
185        public final JCalValue writeJson(T property) {
186                return _writeJson(property);
187        }
188
189        /**
190         * Unmarshals a property from a plain-text iCalendar data stream.
191         * @param value the value as read off the wire
192         * @param dataType the data type of the property value. The property's VALUE
193         * parameter is used to determine the data type. If the property has no
194         * VALUE parameter, then this parameter will be set to the property's
195         * default datatype. Note that the VALUE parameter is removed from the
196         * property's parameter list after it has been read.
197         * @param parameters the parsed parameters
198         * @return the unmarshalled property and its warnings
199         * @throws CannotParseException if the scribe could not parse the property's
200         * value
201         * @throws SkipMeException if the property should not be added to the final
202         * {@link ICalendar} object
203         */
204        public final Result<T> parseText(String value, ICalDataType dataType, ICalParameters parameters) {
205                List<Warning> warnings = new ArrayList<Warning>(0);
206                T property = _parseText(value, dataType, parameters, warnings);
207                property.setParameters(parameters);
208                return new Result<T>(property, warnings);
209        }
210
211        /**
212         * Unmarshals a property's value from an XML document (xCal).
213         * @param element the property's XML element
214         * @param parameters the property's parameters
215         * @return the unmarshalled property and its warnings
216         * @throws CannotParseException if the scribe could not parse the property's
217         * value
218         * @throws SkipMeException if the property should not be added to the final
219         * {@link ICalendar} object
220         */
221        public final Result<T> parseXml(Element element, ICalParameters parameters) {
222                List<Warning> warnings = new ArrayList<Warning>(0);
223                T property = _parseXml(new XCalElement(element), parameters, warnings);
224                property.setParameters(parameters);
225                return new Result<T>(property, warnings);
226        }
227
228        /**
229         * Unmarshals a property's value from a JSON data stream (jCal).
230         * @param value the property's JSON value
231         * @param dataType the data type
232         * @param parameters the parsed parameters
233         * @return the unmarshalled property and its warnings
234         * @throws CannotParseException if the scribe could not parse the property's
235         * value
236         * @throws SkipMeException if the property should not be added to the final
237         * {@link ICalendar} object
238         */
239        public final Result<T> parseJson(JCalValue value, ICalDataType dataType, ICalParameters parameters) {
240                List<Warning> warnings = new ArrayList<Warning>(0);
241                T property = _parseJson(value, dataType, parameters, warnings);
242                property.setParameters(parameters);
243                return new Result<T>(property, warnings);
244        }
245
246        /**
247         * <p>
248         * Sanitizes a property's parameters before the property is written.
249         * </p>
250         * <p>
251         * This method should be overridden by child classes that wish to tweak the
252         * property's parameters before the property is written. The default
253         * implementation of this method does nothing.
254         * </p>
255         * @param property the property
256         * @param copy the list of parameters to make modifications to (it is a copy
257         * of the property's parameters)
258         */
259        protected void _prepareParameters(T property, ICalParameters copy) {
260                //do nothing
261        }
262
263        /**
264         * <p>
265         * Determines the data type of a property instance.
266         * </p>
267         * <p>
268         * This method should be overridden by child classes if a property's data
269         * type changes depending on its value. The default implementation of this
270         * method returns the property's default data type.
271         * </p>
272         * @param property the property
273         * @return the data type or null if unknown
274         */
275        protected ICalDataType _dataType(T property) {
276                return defaultDataType;
277        }
278
279        /**
280         * Marshals a property's value to a string.
281         * @param property the property
282         * @return the marshalled value
283         * @throws SkipMeException if the property should not be written to the data
284         * stream
285         */
286        protected abstract String _writeText(T property);
287
288        /**
289         * <p>
290         * Marshals a property's value to an XML element (xCal).
291         * <p>
292         * <p>
293         * This method should be overridden by child classes that wish to support
294         * xCal. The default implementation of this method will append one child
295         * element to the property's XML element. The child element's name will be
296         * that of the property's data type (retrieved using the {@link #dataType}
297         * method), and the child element's text content will be set to the
298         * property's marshalled plain-text value (retrieved using the
299         * {@link #writeText} method).
300         * </p>
301         * @param property the property
302         * @param element the property's XML element
303         * @throws SkipMeException if the property should not be written to the data
304         * stream
305         */
306        protected void _writeXml(T property, XCalElement element) {
307                String value = writeText(property);
308                ICalDataType dataType = dataType(property);
309                element.append(dataType, value);
310        }
311
312        /**
313         * <p>
314         * Marshals a property's value to a JSON data stream (jCal).
315         * </p>
316         * <p>
317         * This method should be overridden by child classes that wish to support
318         * jCal. The default implementation of this method will create a jCard
319         * property that has a single JSON string value (generated by the
320         * {@link #writeText} method).
321         * </p>
322         * @param property the property
323         * @return the marshalled value
324         * @throws SkipMeException if the property should not be written to the data
325         * stream
326         */
327        protected JCalValue _writeJson(T property) {
328                String value = writeText(property);
329                return JCalValue.single(value);
330        }
331
332        /**
333         * Unmarshals a property from a plain-text iCalendar data stream.
334         * @param value the value as read off the wire
335         * @param dataType the data type of the property value. The property's VALUE
336         * parameter is used to determine the data type. If the property has no
337         * VALUE parameter, then this parameter will be set to the property's
338         * default datatype. Note that the VALUE parameter is removed from the
339         * property's parameter list after it has been read.
340         * @param parameters the parsed parameters. These parameters will be
341         * assigned to the property object once this method returns. Therefore, do
342         * not assign any parameters to the property object itself whilst inside of
343         * this method, or else they will be overwritten.
344         * @param warnings allows the programmer to alert the user to any
345         * note-worthy (but non-critical) issues that occurred during the
346         * unmarshalling process
347         * @return the unmarshalled property object
348         * @throws CannotParseException if the scribe could not parse the property's
349         * value
350         * @throws SkipMeException if the property should not be added to the final
351         * {@link ICalendar} object
352         */
353        protected abstract T _parseText(String value, ICalDataType dataType, ICalParameters parameters, List<Warning> warnings);
354
355        /**
356         * <p>
357         * Unmarshals a property from an XML document (xCal).
358         * </p>
359         * <p>
360         * This method should be overridden by child classes that wish to support
361         * xCal. The default implementation of this method will find the first child
362         * element with the xCal namespace. The element's name will be used as the
363         * property's data type and its text content will be passed into the
364         * {@link #_parseText} method. If no such child element is found, then the
365         * parent element's text content will be passed into {@link #_parseText} and
366         * the data type will be null.
367         * </p>
368         * @param element the property's XML element
369         * @param parameters the parsed parameters. These parameters will be
370         * assigned to the property object once this method returns. Therefore, do
371         * not assign any parameters to the property object itself whilst inside of
372         * this method, or else they will be overwritten.
373         * @param warnings allows the programmer to alert the user to any
374         * note-worthy (but non-critical) issues that occurred during the
375         * unmarshalling process
376         * @return the unmarshalled property object
377         * @throws CannotParseException if the scribe could not parse the property's
378         * value
379         * @throws SkipMeException if the property should not be added to the final
380         * {@link ICalendar} object
381         */
382        protected T _parseXml(XCalElement element, ICalParameters parameters, List<Warning> warnings) {
383                String value = null;
384                ICalDataType dataType = null;
385                Element rawElement = element.getElement();
386
387                //get the text content of the first child element with the xCard namespace
388                List<Element> children = XmlUtils.toElementList(rawElement.getChildNodes());
389                for (Element child : children) {
390                        if (!XCAL_NS.equals(child.getNamespaceURI())) {
391                                continue;
392                        }
393
394                        String dataTypeStr = child.getLocalName();
395                        dataType = "unknown".equals(dataTypeStr) ? null : ICalDataType.get(dataTypeStr);
396                        value = child.getTextContent();
397                        break;
398                }
399
400                if (dataType == null) {
401                        //get the text content of the property element
402                        value = rawElement.getTextContent();
403                }
404
405                value = escape(value);
406                return _parseText(value, dataType, parameters, warnings);
407        }
408
409        /**
410         * /**
411         * <p>
412         * Unmarshals a property from a JSON data stream (jCal).
413         * </p>
414         * <p>
415         * This method should be overridden by child classes that wish to support
416         * jCal. The default implementation of this method will convert the jCal
417         * property value to a string and pass it into the {@link #_parseText}
418         * method.
419         * </p>
420         * 
421         * <hr>
422         * 
423         * <p>
424         * The following paragraphs describe the way in which this method's default
425         * implementation converts a jCal value to a string:
426         * </p>
427         * <p>
428         * If the jCal value consists of a single, non-array, non-object value, then
429         * the value is converted to a string. Special characters (backslashes,
430         * commas, and semicolons) are escaped in order to simulate what the value
431         * might look like in a plain-text iCalendar object.<br>
432         * <code>["x-foo", {}, "text", "the;value"] --&gt; "the\;value"</code><br>
433         * <code>["x-foo", {}, "text", 2] --&gt; "2"</code>
434         * </p>
435         * <p>
436         * If the jCal value consists of multiple, non-array, non-object values,
437         * then all the values are appended together in a single string, separated
438         * by commas. Special characters (backslashes, commas, and semicolons) are
439         * escaped for each value in order to prevent commas from being treated as
440         * delimiters, and to simulate what the value might look like in a
441         * plain-text iCalendar object.<br>
442         * <code>["x-foo", {}, "text", "one", "two,three"] --&gt;
443         * "one,two\,three"</code>
444         * </p>
445         * <p>
446         * If the jCal value is a single array, then this array is treated as a
447         * "structured value", and converted its plain-text representation. Special
448         * characters (backslashes, commas, and semicolons) are escaped for each
449         * value in order to prevent commas and semicolons from being treated as
450         * delimiters.<br>
451         * <code>["x-foo", {}, "text", ["one", ["two", "three"], "four;five"]]
452         * --&gt; "one;two,three;four\;five"</code>
453         * </p>
454         * <p>
455         * If the jCal value starts with a JSON object, then the object is converted
456         * to a format identical to the one used in the RRULE and EXRULE properties.
457         * Special characters (backslashes, commas, semicolons, and equal signs) are
458         * escaped for each value in order to preserve the syntax of the string
459         * value.<br>
460         * <code>["x-foo", {}, "text", {"one": 1, "two": [2, 2.5]}] --&gt; "ONE=1;TWO=2,2.5"</code>
461         * </p>
462         * <p>
463         * For all other cases, behavior is undefined.
464         * </p>
465         * @param value the property's JSON value
466         * @param dataType the data type
467         * @param parameters the parsed parameters. These parameters will be
468         * assigned to the property object once this method returns. Therefore, do
469         * not assign any parameters to the property object itself whilst inside of
470         * this method, or else they will be overwritten.
471         * @param warnings allows the programmer to alert the user to any
472         * note-worthy (but non-critical) issues that occurred during the
473         * unmarshalling process
474         * @return the unmarshalled property object
475         * @throws CannotParseException if the scribe could not parse the property's
476         * value
477         * @throws SkipMeException if the property should not be added to the final
478         * {@link ICalendar} object
479         */
480        protected T _parseJson(JCalValue value, ICalDataType dataType, ICalParameters parameters, List<Warning> warnings) {
481                return _parseText(jcalValueToString(value), dataType, parameters, warnings);
482        }
483
484        private String jcalValueToString(JCalValue value) {
485                if (value.getValues().size() > 1) {
486                        List<String> multi = value.asMulti();
487                        if (!multi.isEmpty()) {
488                                return list(multi);
489                        }
490                }
491
492                if (!value.getValues().isEmpty() && value.getValues().get(0).getArray() != null) {
493                        List<List<String>> structured = value.asStructured();
494                        if (!structured.isEmpty()) {
495                                return structured(structured.toArray());
496                        }
497                }
498
499                if (value.getValues().get(0).getObject() != null) {
500                        ListMultimap<String, String> object = value.asObject();
501                        if (!object.isEmpty()) {
502                                return object(object.getMap());
503                        }
504                }
505
506                return escape(value.asSingle());
507        }
508
509        /**
510         * Unescapes all special characters that are escaped with a backslash, as
511         * well as escaped newlines.
512         * @param text the text to unescape
513         * @return the unescaped text
514         */
515        protected static String unescape(String text) {
516                if (text == null) {
517                        return text;
518                }
519
520                StringBuilder sb = null;
521                boolean escaped = false;
522                for (int i = 0; i < text.length(); i++) {
523                        char ch = text.charAt(i);
524
525                        if (escaped) {
526                                if (sb == null) {
527                                        sb = new StringBuilder(text.length());
528                                        sb.append(text.substring(0, i - 1));
529                                }
530
531                                escaped = false;
532
533                                if (ch == 'n' || ch == 'N') {
534                                        //newlines appear as "\n" or "\N" (see RFC 5545 p.46)
535                                        sb.append(StringUtils.NEWLINE);
536                                        continue;
537                                }
538
539                                sb.append(ch);
540                                continue;
541                        }
542
543                        if (ch == '\\') {
544                                escaped = true;
545                                continue;
546                        }
547
548                        if (sb != null) {
549                                sb.append(ch);
550                        }
551                }
552                return (sb == null) ? text : sb.toString();
553        }
554
555        /**
556         * <p>
557         * Escapes all special characters within a iCalendar value. These characters
558         * are:
559         * </p>
560         * <ul>
561         * <li>backslashes ({@code \})</li>
562         * <li>commas ({@code ,})</li>
563         * <li>semi-colons ({@code ;})</li>
564         * </ul>
565         * <p>
566         * Newlines are not escaped by this method. They are escaped when the
567         * iCalendar object is serialized (in the {@link ICalRawWriter} class).
568         * </p>
569         * @param text the text to escape
570         * @return the escaped text
571         */
572        protected static String escape(String text) {
573                if (text == null) {
574                        return text;
575                }
576
577                String chars = "\\,;";
578                StringBuilder sb = null;
579                for (int i = 0; i < text.length(); i++) {
580                        char ch = text.charAt(i);
581                        if (chars.indexOf(ch) >= 0) {
582                                if (sb == null) {
583                                        sb = new StringBuilder(text.length());
584                                        sb.append(text.substring(0, i));
585                                }
586                                sb.append('\\');
587                        }
588
589                        if (sb != null) {
590                                sb.append(ch);
591                        }
592                }
593                return (sb == null) ? text : sb.toString();
594        }
595
596        /**
597         * Splits a string by a delimiter, taking escaped characters into account.
598         * @param delimiter the delimiter (e.g. ',')
599         * @return the factory object
600         */
601        protected static Splitter splitter(char delimiter) {
602                return new Splitter(delimiter);
603        }
604
605        /**
606         * A helper class for splitting strings.
607         */
608        protected static class Splitter {
609                private char delimiter;
610                private boolean unescape = false;
611                private boolean nullEmpties = false;
612                private int limit = -1;
613
614                /**
615                 * Creates a new splitter object.
616                 * @param delimiter the delimiter character (e.g. ',')
617                 */
618                public Splitter(char delimiter) {
619                        this.delimiter = delimiter;
620                }
621
622                /**
623                 * Sets whether to unescape each split string.
624                 * @param unescape true to unescape, false not to (default is false)
625                 * @return this
626                 */
627                public Splitter unescape(boolean unescape) {
628                        this.unescape = unescape;
629                        return this;
630                }
631
632                /**
633                 * Sets whether to treat empty elements as null elements.
634                 * @param nullEmpties true to treat them as null elements, false to
635                 * treat them as empty strings (default is false)
636                 * @return this
637                 */
638                public Splitter nullEmpties(boolean nullEmpties) {
639                        this.nullEmpties = nullEmpties;
640                        return this;
641                }
642
643                /**
644                 * Sets the max number of split strings it should parse.
645                 * @param limit the max number of split strings
646                 * @return this
647                 */
648                public Splitter limit(int limit) {
649                        this.limit = limit;
650                        return this;
651                }
652
653                /**
654                 * Performs the split operation.
655                 * @param string the string to split (e.g. "one,two,three")
656                 * @return the split string
657                 */
658                public List<String> split(String string) {
659                        //doing it this way is 10x faster than a regex
660
661                        List<String> list = new ArrayList<String>();
662                        boolean escaped = false;
663                        int start = 0;
664                        for (int i = 0; i < string.length(); i++) {
665                                char ch = string.charAt(i);
666
667                                if (escaped) {
668                                        escaped = false;
669                                        continue;
670                                }
671
672                                if (ch == delimiter) {
673                                        add(string.substring(start, i), list);
674                                        start = i + 1;
675                                        if (limit > 0 && list.size() == limit - 1) {
676                                                break;
677                                        }
678
679                                        continue;
680                                }
681
682                                if (ch == '\\') {
683                                        escaped = true;
684                                        continue;
685                                }
686                        }
687
688                        add(string.substring(start), list);
689
690                        return list;
691                }
692
693                private void add(String str, List<String> list) {
694                        str = str.trim();
695
696                        if (nullEmpties && str.length() == 0) {
697                                str = null;
698                        } else if (unescape) {
699                                str = ICalPropertyScribe.unescape(str);
700                        }
701
702                        list.add(str);
703                }
704        }
705
706        /**
707         * Parses a comma-separated list of values.
708         * @param value the string to parse (e.g. "one,two,th\,ree")
709         * @return the parsed values
710         */
711        protected static List<String> list(String value) {
712                if (value.length() == 0) {
713                        return new ArrayList<String>(0);
714                }
715                return splitter(',').unescape(true).split(value);
716        }
717
718        /**
719         * Writes a comma-separated list of values.
720         * @param values the values to write
721         * @return the list
722         */
723        protected static String list(Object... values) {
724                return list(Arrays.asList(values));
725        }
726
727        /**
728         * Writes a comma-separated list of values.
729         * @param values the values to write
730         * @return the list
731         */
732        protected static <T> String list(Collection<T> values) {
733                return list(values, new ListCallback<T>() {
734                        public String asString(T value) {
735                                return value.toString();
736                        }
737                });
738        }
739
740        /**
741         * Writes a comma-separated list of values.
742         * @param values the values to write
743         * @param callback callback function used for converting each value to a
744         * string
745         * @return the list
746         */
747        protected static <T> String list(Collection<T> values, final ListCallback<T> callback) {
748                return join(values, ",", new JoinCallback<T>() {
749                        public void handle(StringBuilder sb, T value) {
750                                if (value == null) {
751                                        return;
752                                }
753
754                                String valueStr = callback.asString(value);
755                                sb.append(escape(valueStr));
756                        }
757                });
758        }
759
760        /**
761         * Callback function used in conjunction with the
762         * {@link ICalPropertyScribe#list(Collection, ListCallback) list} method
763         * @param <T> the value class
764         */
765        protected static interface ListCallback<T> {
766                /**
767                 * Converts a value to a string.
768                 * @param value the value (null values are not passed to this method, so
769                 * this parameter will never be null)
770                 * @return the string
771                 */
772                String asString(T value);
773        }
774
775        /**
776         * Parses a list of values that are delimited by semicolons. Unlike
777         * structured value components, semi-structured components cannot be
778         * multi-valued.
779         * @param value the string to parse (e.g. "one;two;three")
780         * @return the parsed values
781         */
782        protected static SemiStructuredIterator semistructured(String value) {
783                return semistructured(value, false);
784        }
785
786        /**
787         * Parses a list of values that are delimited by semicolons. Unlike
788         * structured value components, semi-structured components cannot be
789         * multi-valued.
790         * @param value the string to parse (e.g. "one;two;three")
791         * @param nullEmpties true to treat empty elements as null, false to treat
792         * them as empty strings
793         * @return the parsed values
794         */
795        protected static SemiStructuredIterator semistructured(String value, boolean nullEmpties) {
796                List<String> split = splitter(';').unescape(true).nullEmpties(nullEmpties).split(value);
797                return new SemiStructuredIterator(split.iterator());
798        }
799
800        /**
801         * Parses a structured value.
802         * @param value the string to parse (e.g. "one;two,three;four")
803         * @return the parsed values
804         */
805        protected static StructuredIterator structured(String value) {
806                List<String> split = splitter(';').split(value);
807                List<List<String>> components = new ArrayList<List<String>>(split.size());
808                for (String s : split) {
809                        components.add(list(s));
810                }
811                return new StructuredIterator(components.iterator());
812        }
813
814        /**
815         * Provides an iterator for a jCard structured value.
816         * @param value the jCard value
817         * @return the parsed values
818         */
819        protected static StructuredIterator structured(JCalValue value) {
820                return new StructuredIterator(value.asStructured().iterator());
821        }
822
823        /**
824         * <p>
825         * Writes a structured value.
826         * </p>
827         * <p>
828         * This method accepts a list of {@link Object} instances.
829         * {@link Collection} objects will be treated as multi-valued components.
830         * Null objects will be treated as empty components. All other objects will
831         * have their {@code toString()} method invoked to generate the string
832         * value.
833         * </p>
834         * @param values the values to write
835         * @return the structured value string
836         */
837        protected static String structured(Object... values) {
838                return join(Arrays.asList(values), ";", new JoinCallback<Object>() {
839                        public void handle(StringBuilder sb, Object value) {
840                                if (value == null) {
841                                        return;
842                                }
843
844                                if (value instanceof Collection) {
845                                        Collection<?> list = (Collection<?>) value;
846                                        sb.append(list(list));
847                                        return;
848                                }
849
850                                sb.append(escape(value.toString()));
851                        }
852                });
853        }
854
855        /**
856         * Iterates over the fields in a structured value.
857         */
858        protected static class StructuredIterator {
859                private final Iterator<List<String>> it;
860
861                /**
862                 * Constructs a new structured iterator.
863                 * @param it the iterator to wrap
864                 */
865                public StructuredIterator(Iterator<List<String>> it) {
866                        this.it = it;
867                }
868
869                /**
870                 * Gets the first value of the next component.
871                 * @return the first value, null if the value is an empty string, or
872                 * null if there are no more components
873                 */
874                public String nextString() {
875                        if (!hasNext()) {
876                                return null;
877                        }
878
879                        List<String> list = it.next();
880                        if (list.isEmpty()) {
881                                return null;
882                        }
883
884                        String value = list.get(0);
885                        return (value.length() == 0) ? null : value;
886                }
887
888                /**
889                 * Gets the next component.
890                 * @return the next component, an empty list if the component is empty,
891                 * or an empty list of there are no more components
892                 */
893                public List<String> nextComponent() {
894                        if (!hasNext()) {
895                                return new ArrayList<String>(0); //the lists should be mutable so they can be directly assigned to the property object's fields
896                        }
897
898                        List<String> list = it.next();
899                        if (list.size() == 1 && list.get(0).length() == 0) {
900                                return new ArrayList<String>(0);
901                        }
902
903                        return list;
904                }
905
906                /**
907                 * Determines if there are any elements left in the value.
908                 * @return true if there are elements left, false if not
909                 */
910                public boolean hasNext() {
911                        return it.hasNext();
912                }
913        }
914
915        /**
916         * Iterates over the fields in a semi-structured value (a structured value
917         * whose components cannot be multi-valued).
918         */
919        protected static class SemiStructuredIterator {
920                private final Iterator<String> it;
921
922                /**
923                 * Constructs a new structured iterator.
924                 * @param it the iterator to wrap
925                 */
926                public SemiStructuredIterator(Iterator<String> it) {
927                        this.it = it;
928                }
929
930                /**
931                 * Gets the next value.
932                 * @return the next value, null if the value is an empty string, or null
933                 * if there are no more values
934                 */
935                public String next() {
936                        return hasNext() ? it.next() : null;
937                }
938
939                /**
940                 * Determines if there are any elements left in the value.
941                 * @return true if there are elements left, false if not
942                 */
943                public boolean hasNext() {
944                        return it.hasNext();
945                }
946        }
947
948        /**
949         * Writes an object property value to a string.
950         * @param value the value
951         * @return the string
952         */
953        protected static <T> String object(Map<String, List<T>> value) {
954                return join(value, ";", new JoinMapCallback<String, List<T>>() {
955                        public void handle(StringBuilder sb, String key, List<T> value) {
956                                sb.append(key.toUpperCase()).append('=').append(list(value));
957                        }
958                });
959        }
960
961        /**
962         * Parses an object property value.
963         * @param value the value to parse
964         * @return the parsed value
965         */
966        protected static ListMultimap<String, String> object(String value) {
967                ListMultimap<String, String> map = new ListMultimap<String, String>();
968
969                for (String component : splitter(';').unescape(false).split(value)) {
970                        if (component.length() == 0) {
971                                continue;
972                        }
973
974                        String[] split = component.split("=", 2);
975
976                        String name = unescape(split[0].toUpperCase());
977                        List<String> values = (split.length > 1) ? list(split[1]) : Arrays.asList("");
978
979                        map.putAll(name, values);
980                }
981
982                return map;
983        }
984
985        /**
986         * Parses a date string.
987         * @param value the date string
988         * @return the factory object
989         */
990        protected static DateParser date(String value) {
991                return new DateParser(value);
992        }
993
994        /**
995         * Formats a {@link Date} object as a string.
996         * @param date the date
997         * @return the factory object
998         */
999        protected static DateWriter date(Date date) {
1000                return new DateWriter(date);
1001        }
1002
1003        /**
1004         * Factory class for parsing dates.
1005         */
1006        protected static class DateParser {
1007                private String value;
1008                private TimeZone timezone;
1009
1010                /**
1011                 * Creates a new date writer object.
1012                 * @param value the date string to parse
1013                 */
1014                public DateParser(String value) {
1015                        this.value = value;
1016                }
1017
1018                /**
1019                 * Sets the ID of the timezone to parse the date as (TZID parameter
1020                 * value). If the ID does not contain a "/" character, it will be
1021                 * ignored.
1022                 * @param timezoneId the timezone ID
1023                 * @return this
1024                 */
1025                public DateParser tzid(String timezoneId) {
1026                        return tzid(timezoneId, null);
1027                }
1028
1029                /**
1030                 * Sets the ID of the timezone to parse the date as (TZID parameter
1031                 * value).
1032                 * @param timezoneId the timezone ID. If the ID is global (contains a
1033                 * "/" character), it will attempt to look up the timezone in Java's
1034                 * timezone registry and parse the date according to that timezone. If
1035                 * the timezone is not found, the date will be parsed according to the
1036                 * JVM's default timezone and a warning message will be added to the
1037                 * provided warnings list. If the ID is not global, it will be parsed
1038                 * according to the JVM's default timezone. Whichever timezone is chosen
1039                 * here, it will be ignored if the date string is in UTC time or
1040                 * contains an offset.
1041                 * @param warnings if the ID is global and is not recognized, a warning
1042                 * message will be added to this list
1043                 * @return this
1044                 */
1045                public DateParser tzid(String timezoneId, List<Warning> warnings) {
1046                        if (timezoneId == null) {
1047                                return tz(null);
1048                        }
1049
1050                        if (timezoneId.contains("/")) {
1051                                TimeZone timezone = ICalDateFormat.parseTimeZoneId(timezoneId);
1052                                if (timezone == null) {
1053                                        timezone = TimeZone.getDefault();
1054                                        if (warnings != null) {
1055                                                warnings.add(Warning.parse(5, timezoneId, timezone.getID()));
1056                                        }
1057                                }
1058                                return tz(timezone);
1059                        }
1060
1061                        //TODO parse according to the associated VTIMEZONE component
1062                        return tz(TimeZone.getDefault());
1063                }
1064
1065                /**
1066                 * Sets the timezone to parse the date as.
1067                 * @param timezone the timezone
1068                 * @return this
1069                 */
1070                public DateParser tz(TimeZone timezone) {
1071                        this.timezone = timezone;
1072                        return this;
1073                }
1074
1075                /**
1076                 * Parses the date string.
1077                 * @return the parsed date
1078                 * @throws IllegalArgumentException if the date string is invalid
1079                 */
1080                public Date parse() {
1081                        return ICalDateFormat.parse(value, timezone);
1082                }
1083        }
1084
1085        /**
1086         * Factory class for writing dates.
1087         */
1088        protected static class DateWriter {
1089                private Date date;
1090                private boolean hasTime = true;
1091                private TimeZone timezone;
1092                private boolean extended = false;
1093
1094                /**
1095                 * Creates a new date writer object.
1096                 * @param date the date to format
1097                 */
1098                public DateWriter(Date date) {
1099                        this.date = date;
1100                }
1101
1102                /**
1103                 * Sets whether to output the date's time component.
1104                 * @param hasTime true include the time, false if it's strictly a date
1105                 * (defaults to "true")
1106                 * @return this
1107                 */
1108                public DateWriter time(boolean hasTime) {
1109                        this.hasTime = hasTime;
1110                        return this;
1111                }
1112
1113                /**
1114                 * Sets the ID of the timezone to format the date as (TZID parameter
1115                 * value).
1116                 * @param timezoneId the timezone ID. If the ID is global (contains a
1117                 * "/" character), it will attempt to look up the timezone in Java's
1118                 * timezone registry and format the date according to that timezone. If
1119                 * the timezone is not found, the date will be formatted in UTC. If the
1120                 * ID is not global, it will be formatted according to the JVM's default
1121                 * timezone. If no timezone preference is specified, the date will be
1122                 * formatted as UTC.
1123                 * @return this
1124                 */
1125                public DateWriter tzid(String timezoneId) {
1126                        if (timezoneId == null) {
1127                                return tz(null);
1128                        }
1129
1130                        if (timezoneId.contains("/")) {
1131                                return tz(ICalDateFormat.parseTimeZoneId(timezoneId));
1132                        }
1133
1134                        //TODO format according to the associated VTIMEZONE component
1135                        return tz(TimeZone.getDefault());
1136                }
1137
1138                /**
1139                 * Outputs the date in local time (without a timezone). If no timezone
1140                 * preference is specified, the date will be formatted as UTC.
1141                 * @param localTz true to use local time, false not to
1142                 * @return this
1143                 */
1144                public DateWriter localTz(boolean localTz) {
1145                        return localTz ? tz(TimeZone.getDefault()) : this;
1146                }
1147
1148                /**
1149                 * Convenience method that combines {@link #localTz(boolean)} and
1150                 * {@link #tzid(String)} into one method.
1151                 * @param localTz true to use local time, false not to
1152                 * @param timezoneId the timezone ID
1153                 * @return this
1154                 */
1155                public DateWriter tz(boolean localTz, String timezoneId) {
1156                        return localTz ? localTz(true) : tzid(timezoneId);
1157                }
1158
1159                /**
1160                 * Sets the timezone to format the date as. If no timezone preference is
1161                 * specified, the date will be formatted as UTC.
1162                 * @param timezone the timezone
1163                 * @return this
1164                 */
1165                public DateWriter tz(TimeZone timezone) {
1166                        this.timezone = timezone;
1167                        return this;
1168                }
1169
1170                /**
1171                 * Sets whether to use extended format or basic.
1172                 * @param extended true to use extended format, false to use basic
1173                 * (defaults to "false")
1174                 * @return this
1175                 */
1176                public DateWriter extended(boolean extended) {
1177                        this.extended = extended;
1178                        return this;
1179                }
1180
1181                /**
1182                 * Creates the date string.
1183                 * @return the date string
1184                 */
1185                public String write() {
1186                        ICalDateFormat format;
1187                        TimeZone timezone = this.timezone;
1188                        if (hasTime) {
1189                                if (timezone == null) {
1190                                        format = extended ? ICalDateFormat.UTC_TIME_EXTENDED : ICalDateFormat.UTC_TIME_BASIC;
1191                                } else {
1192                                        format = extended ? ICalDateFormat.DATE_TIME_EXTENDED_WITHOUT_TZ : ICalDateFormat.DATE_TIME_BASIC_WITHOUT_TZ;
1193                                }
1194                        } else {
1195                                format = extended ? ICalDateFormat.DATE_EXTENDED : ICalDateFormat.DATE_BASIC;
1196                                timezone = null;
1197                        }
1198
1199                        return format.format(date, timezone);
1200                }
1201        }
1202
1203        /**
1204         * Creates a {@link CannotParseException}, indicating that the XML elements
1205         * that the parser expected to find are missing from the property's XML
1206         * element.
1207         * @param dataTypes the expected data types (null for "unknown")
1208         */
1209        protected static CannotParseException missingXmlElements(ICalDataType... dataTypes) {
1210                String[] elements = new String[dataTypes.length];
1211                for (int i = 0; i < dataTypes.length; i++) {
1212                        ICalDataType dataType = dataTypes[i];
1213                        elements[i] = (dataType == null) ? "unknown" : dataType.getName().toLowerCase();
1214                }
1215                return missingXmlElements(elements);
1216        }
1217
1218        /**
1219         * Creates a {@link CannotParseException}, indicating that the XML elements
1220         * that the parser expected to find are missing from property's XML element.
1221         * @param elements the names of the expected XML elements.
1222         */
1223        protected static CannotParseException missingXmlElements(String... elements) {
1224                return new CannotParseException(23, Arrays.toString(elements));
1225        }
1226
1227        /**
1228         * Represents the result of an unmarshal operation.
1229         * @author Michael Angstadt
1230         * @param <T> the unmarshalled property class
1231         */
1232        public static class Result<T extends ICalProperty> {
1233                private final T property;
1234                private final List<Warning> warnings;
1235
1236                /**
1237                 * Creates a new result.
1238                 * @param property the property object
1239                 * @param warnings the warnings
1240                 */
1241                public Result(T property, List<Warning> warnings) {
1242                        this.property = property;
1243                        this.warnings = warnings;
1244                }
1245
1246                /**
1247                 * Gets the warnings.
1248                 * @return the warnings
1249                 */
1250                public List<Warning> getWarnings() {
1251                        return warnings;
1252                }
1253
1254                /**
1255                 * Gets the property object.
1256                 * @return the property object
1257                 */
1258                public T getProperty() {
1259                        return property;
1260                }
1261        }
1262}