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