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