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