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