001package ezvcard.io.scribe;
002
003import java.time.temporal.Temporal;
004import java.util.Arrays;
005import java.util.List;
006import java.util.Optional;
007
008import javax.xml.namespace.QName;
009
010import org.w3c.dom.Element;
011
012import com.github.mangstadt.vinnie.io.VObjectPropertyValues;
013
014import ezvcard.VCard;
015import ezvcard.VCardDataType;
016import ezvcard.VCardVersion;
017import ezvcard.io.CannotParseException;
018import ezvcard.io.EmbeddedVCardException;
019import ezvcard.io.ParseContext;
020import ezvcard.io.SkipMeException;
021import ezvcard.io.html.HCardElement;
022import ezvcard.io.json.JCardValue;
023import ezvcard.io.json.JsonValue;
024import ezvcard.io.text.WriteContext;
025import ezvcard.io.xml.XCardElement;
026import ezvcard.io.xml.XCardElement.XCardValue;
027import ezvcard.parameter.VCardParameters;
028import ezvcard.property.VCardProperty;
029import ezvcard.util.VCardDateFormat;
030
031/*
032 Copyright (c) 2012-2026, Michael Angstadt
033 All rights reserved.
034
035 Redistribution and use in source and binary forms, with or without
036 modification, are permitted provided that the following conditions are met: 
037
038 1. Redistributions of source code must retain the above copyright notice, this
039 list of conditions and the following disclaimer. 
040 2. Redistributions in binary form must reproduce the above copyright notice,
041 this list of conditions and the following disclaimer in the documentation
042 and/or other materials provided with the distribution. 
043
044 THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
045 ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
046 WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
047 DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR
048 ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
049 (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
050 LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
051 ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
052 (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
053 SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
054 */
055
056/**
057 * Base class for vCard property scribes (aka "marshallers" or "serializers").
058 * @param <T> the property class
059 * @author Michael Angstadt
060 */
061public abstract class VCardPropertyScribe<T extends VCardProperty> {
062        protected final Class<T> clazz;
063        protected final String propertyName;
064        protected final QName qname;
065
066        /**
067         * Creates a new scribe.
068         * @param clazz the property class
069         * @param propertyName the property name (e.g. "FN")
070         */
071        protected VCardPropertyScribe(Class<T> clazz, String propertyName) {
072                this(clazz, propertyName, new QName(VCardVersion.V4_0.getXmlNamespace(), propertyName.toLowerCase()));
073        }
074
075        /**
076         * Creates a new scribe.
077         * @param clazz the property class
078         * @param propertyName the property name (e.g. "FN")
079         * @param qname the XML element name and namespace to use for xCard
080         * documents (by default, the XML element name is set to the lower-cased
081         * property name, and the element namespace is set to the xCard namespace)
082         */
083        protected VCardPropertyScribe(Class<T> clazz, String propertyName, QName qname) {
084                this.clazz = clazz;
085                this.propertyName = propertyName;
086                this.qname = qname;
087        }
088
089        /**
090         * Gets the property class.
091         * @return the property class
092         */
093        public Class<T> getPropertyClass() {
094                return clazz;
095        }
096
097        /**
098         * Gets the property name.
099         * @return the property name (e.g. "FN")
100         */
101        public String getPropertyName() {
102                return propertyName;
103        }
104
105        /**
106         * Gets this property's local name and namespace for xCard documents.
107         * @return the XML local name and namespace
108         */
109        public QName getQName() {
110                return qname;
111        }
112
113        /**
114         * Sanitizes a property's parameters (called before the property is
115         * written). Note that a copy of the parameters is returned so that the
116         * property object does not get modified.
117         * @param property the property
118         * @param version the version of the vCard that is being generated
119         * @param vcard the vCard that the property belongs to
120         * @return the sanitized parameters
121         */
122        public final VCardParameters prepareParameters(T property, VCardVersion version, VCard vcard) {
123                //make a copy because the property should not get modified when it is marshalled
124                VCardParameters copy = new VCardParameters(property.getParameters());
125                _prepareParameters(property, copy, version, vcard);
126                return copy;
127        }
128
129        /**
130         * <p>
131         * Determines the property's default data type.
132         * </p>
133         * <p>
134         * When writing a plain-text vCard, if the data type of a property instance
135         * (as determined by the {@link #dataType} method) matches the default data
136         * type, then a VALUE parameter will *not* be written.
137         * </p>
138         * <p>
139         * When parsing a plain-text vCard, if a property has no VALUE parameter,
140         * then the property's default data type will be passed into the
141         * {@link #parseText} method.
142         * </p>
143         * @param version the vCard version
144         * @return the default data type or null if unknown
145         */
146        public final VCardDataType defaultDataType(VCardVersion version) {
147                return _defaultDataType(version);
148        }
149
150        /**
151         * Determines the data type of a property instance.
152         * @param property the property
153         * @param version the version of the vCard that is being generated
154         * @return the data type or null if unknown
155         */
156        public final VCardDataType dataType(T property, VCardVersion version) {
157                return _dataType(property, version);
158        }
159
160        /**
161         * Marshals a property's value to a string.
162         * @param property the property
163         * @param context contains information about the vCard being written, such
164         * as the target version
165         * @return the string value
166         * @throws SkipMeException if the property should not be written to the data
167         * stream
168         */
169        public final String writeText(T property, WriteContext context) {
170                return _writeText(property, context);
171        }
172
173        /**
174         * Marshals a property's value to an XML element (xCard).
175         * @param property the property
176         * @param element the property's XML element.
177         * @throws SkipMeException if the property should not be written to the data
178         * stream
179         */
180        public final void writeXml(T property, Element element) {
181                XCardElement xCardElement = new XCardElement(element);
182                _writeXml(property, xCardElement);
183        }
184
185        /**
186         * Marshals a property's value to a JSON data stream (jCard).
187         * @param property the property
188         * @return the marshalled value
189         * @throws SkipMeException if the property should not be written to the data
190         * stream
191         */
192        public final JCardValue writeJson(T property) {
193                return _writeJson(property);
194        }
195
196        /**
197         * Unmarshals a property from a plain-text vCard.
198         * @param value the value as read off the wire
199         * @param dataType the data type of the property value. The property's VALUE
200         * parameter is used to determine the data type. If the property has no
201         * VALUE parameter, then this parameter will be set to the property's
202         * default datatype, as determined by the {@link #defaultDataType} method.
203         * Note that the VALUE parameter is removed from the property's parameter
204         * list after it has been read.
205         * @param parameters the parsed parameters
206         * @param context the parse context
207         * @return the unmarshalled property
208         * @throws CannotParseException if the marshaller could not parse the
209         * property's value
210         * @throws SkipMeException if the property should not be added to the final
211         * {@link VCard} object
212         * @throws EmbeddedVCardException if the property value is an embedded vCard
213         * (i.e. the AGENT property)
214         */
215        public final T parseText(String value, VCardDataType dataType, VCardParameters parameters, ParseContext context) {
216                T property = _parseText(value, dataType, parameters, context);
217                property.setParameters(parameters);
218                return property;
219        }
220
221        /**
222         * Unmarshals a property's value from an XML document (xCard).
223         * @param element the property's XML element
224         * @param parameters the parsed parameters
225         * @param context the parse context
226         * @return the unmarshalled property
227         * @throws CannotParseException if the marshaller could not parse the
228         * property's value
229         * @throws SkipMeException if the property should not be added to the final
230         * {@link VCard} object
231         */
232        public final T parseXml(Element element, VCardParameters parameters, ParseContext context) {
233                T property = _parseXml(new XCardElement(element), parameters, context);
234                property.setParameters(parameters);
235                return property;
236        }
237
238        /**
239         * Unmarshals the property from an HTML document (hCard).
240         * @param element the property's HTML element
241         * @param context the parse context
242         * @return the unmarshalled property
243         * @throws CannotParseException if the property value could not be parsed
244         * @throws SkipMeException if this type should NOT be added to the
245         * {@link VCard} object
246         * @throws EmbeddedVCardException if the property value is an embedded vCard
247         * (i.e. the AGENT property)
248         */
249        public final T parseHtml(HCardElement element, ParseContext context) {
250                return _parseHtml(element, context);
251        }
252
253        /**
254         * Unmarshals a property's value from a JSON data stream (jCard).
255         * @param value the property's JSON value
256         * @param dataType the data type
257         * @param parameters the parsed parameters
258         * @param context the parse context
259         * @return the unmarshalled property
260         * @throws CannotParseException if the marshaller could not parse the
261         * property's value
262         * @throws SkipMeException if the property should not be added to the final
263         * {@link VCard} object
264         */
265        public final T parseJson(JCardValue value, VCardDataType dataType, VCardParameters parameters, ParseContext context) {
266                T property = _parseJson(value, dataType, parameters, context);
267                property.setParameters(parameters);
268                return property;
269        }
270
271        /**
272         * <p>
273         * Sanitizes a property's parameters before the property is written.
274         * </p>
275         * <p>
276         * This method should be overridden by child classes that wish to tweak the
277         * property's parameters before the property is written. The default
278         * implementation of this method does nothing.
279         * </p>
280         * @param property the property
281         * @param copy the list of parameters to make modifications to (it is a copy
282         * of the property's parameters)
283         * @param version the version of the vCard that is being generated
284         * @param vcard the vCard that the property belongs to
285         */
286        protected void _prepareParameters(T property, VCardParameters copy, VCardVersion version, VCard vcard) {
287                //do nothing
288        }
289
290        /**
291         * <p>
292         * Determines the property's default data type.
293         * </p>
294         * <p>
295         * When writing a plain-text vCard, if the data type of a property instance
296         * (as determined by the {@link #dataType} method) matches the default data
297         * type, then a VALUE parameter will *not* be written.
298         * </p>
299         * <p>
300         * When parsing a plain-text vCard, if a property has no VALUE parameter,
301         * then the property's default data type will be passed into the
302         * {@link #parseText} method.
303         * </p>
304         * @param version the vCard version
305         * @return the default data type or null if unknown
306         */
307        protected abstract VCardDataType _defaultDataType(VCardVersion version);
308
309        /**
310         * <p>
311         * Determines the data type of a property instance.
312         * </p>
313         * <p>
314         * This method should be overridden by child classes if a property's data
315         * type changes depending on its value. The default implementation of this
316         * method calls {@link #_defaultDataType}.
317         * </p>
318         * @param property the property
319         * @param version the version of the vCard that is being generated
320         * @return the data type or null if unknown
321         */
322        protected VCardDataType _dataType(T property, VCardVersion version) {
323                return _defaultDataType(version);
324        }
325
326        /**
327         * Marshals a property's value to a string.
328         * @param property the property
329         * @param context contains information about the vCard being written, such
330         * as the target version
331         * @return the marshalled value
332         * @throws SkipMeException if the property should not be written to the data
333         * stream
334         */
335        protected abstract String _writeText(T property, WriteContext context);
336
337        /**
338         * <p>
339         * Marshals a property's value to an XML element (xCard).
340         * </p>
341         * <p>
342         * This method should be overridden by child classes that wish to support
343         * xCard. The default implementation of this method will append one child
344         * element to the property's XML element. The child element's name will be
345         * that of the property's data type (retrieved using the {@link #dataType}
346         * method), and the child element's text content will be set to the
347         * property's marshalled plain-text value (retrieved using the
348         * {@link #writeText} method).
349         * </p>
350         * @param property the property
351         * @param element the property's XML element
352         * @throws SkipMeException if the property should not be written to the data
353         * stream
354         */
355        protected void _writeXml(T property, XCardElement element) {
356                String value = writeText(property, new WriteContext(VCardVersion.V4_0, null, false));
357                VCardDataType dataType = dataType(property, VCardVersion.V4_0);
358                element.append(dataType, value);
359        }
360
361        /**
362         * <p>
363         * Marshals a property's value to a JSON data stream (jCard).
364         * </p>
365         * <p>
366         * This method should be overridden by child classes that wish to support
367         * jCard. The default implementation of this method will create a jCard
368         * property that has a single JSON string value (generated by the
369         * {@link #writeText} method).
370         * </p>
371         * @param property the property
372         * @return the marshalled value
373         * @throws SkipMeException if the property should not be written to the data
374         * stream
375         */
376        protected JCardValue _writeJson(T property) {
377                String value = writeText(property, new WriteContext(VCardVersion.V4_0, null, false));
378                return JCardValue.single(value);
379        }
380
381        /**
382         * Unmarshals a property from a plain-text vCard.
383         * @param value the value as read off the wire
384         * @param dataType the data type of the property value. The property's VALUE
385         * parameter is used to determine the data type. If the property has no
386         * VALUE parameter, then this parameter will be set to the property's
387         * default datatype, as determined by the {@link #defaultDataType} method.
388         * Note that the VALUE parameter is removed from the property's parameter
389         * list after it has been read.
390         * @param parameters the parsed parameters. These parameters will be
391         * assigned to the property object once this method returns. Therefore, do
392         * not assign any parameters to the property object itself whilst inside of
393         * this method, or else they will be overwritten.
394         * @param context the parse context
395         * @return the unmarshalled property object
396         * @throws CannotParseException if the marshaller could not parse the
397         * property's value
398         * @throws SkipMeException if the property should not be added to the final
399         * {@link VCard} object
400         */
401        protected abstract T _parseText(String value, VCardDataType dataType, VCardParameters parameters, ParseContext context);
402
403        /**
404         * <p>
405         * Unmarshals a property from an XML document (xCard).
406         * </p>
407         * <p>
408         * This method should be overridden by child classes that wish to support
409         * xCard. The default implementation of this method will find the first
410         * child element with the xCard namespace. The element's name will be used
411         * as the property's data type and its text content (escaped for inclusion
412         * in a text-based vCard, e.g. escaping comma characters) will be passed
413         * into the {@link #_parseText} method. If no such child element is found,
414         * then the parent element's text content will be passed into
415         * {@link #_parseText} and the data type will be {@code null}.
416         * </p>
417         * @param element the property's XML element
418         * @param parameters the parsed parameters. These parameters will be
419         * assigned to the property object once this method returns. Therefore, do
420         * not assign any parameters to the property object itself whilst inside of
421         * this method, or else they will be overwritten.
422         * @param context the parse context
423         * @return the unmarshalled property object
424         * @throws CannotParseException if the marshaller could not parse the
425         * property's value
426         * @throws SkipMeException if the property should not be added to the final
427         * {@link VCard} object
428         */
429        protected T _parseXml(XCardElement element, VCardParameters parameters, ParseContext context) {
430                XCardValue firstValue = element.firstValue();
431                VCardDataType dataType = firstValue.getDataType();
432                String value = VObjectPropertyValues.escape(firstValue.getValue());
433                return _parseText(value, dataType, parameters, context);
434        }
435
436        /**
437         * <p>
438         * Unmarshals the property from an hCard (HTML document).
439         * </p>
440         * <p>
441         * This method should be overridden by child classes that wish to support
442         * hCard. The default implementation of this method will retrieve the HTML
443         * element's hCard value (as described in {@link HCardElement#value()}), and
444         * pass it into the {@link #_parseText} method.
445         * </p>
446         * @param element the property's HTML element
447         * @param context the parse context
448         * @return the unmarshalled property object
449         * @throws CannotParseException if the property value could not be parsed
450         * @throws SkipMeException if this property should NOT be added to the
451         * {@link VCard} object
452         * @throws EmbeddedVCardException if the value of this property is an
453         * embedded vCard (i.e. the AGENT property)
454         */
455        protected T _parseHtml(HCardElement element, ParseContext context) {
456                String value = VObjectPropertyValues.escape(element.value());
457                VCardParameters parameters = new VCardParameters();
458                T property = _parseText(value, null, parameters, context);
459                property.setParameters(parameters);
460                return property;
461        }
462
463        /**
464         * <p>
465         * Unmarshals a property from a JSON data stream (jCard).
466         * </p>
467         * <p>
468         * This method should be overridden by child classes that wish to support
469         * jCard. The default implementation of this method will convert the jCard
470         * property value to a string and pass it into the {@link #_parseText}
471         * method.
472         * </p>
473         * 
474         * <hr>
475         * 
476         * <p>
477         * The following paragraphs describe the way in which this method's default
478         * implementation converts a jCard value to a string:
479         * </p>
480         * <p>
481         * If the jCard value consists of a single, non-array, non-object value,
482         * then the value is converted to a string. Special characters (backslashes,
483         * commas, and semicolons) are escaped in order to simulate what the value
484         * might look like in a plain-text vCard.<br>
485         * <code>["x-foo", {}, "text", "the;value"] --&gt; "the\;value"</code><br>
486         * <code>["x-foo", {}, "text", 2] --&gt; "2"</code>
487         * </p>
488         * <p>
489         * If the jCard value consists of multiple, non-array, non-object values,
490         * then all the values are appended together in a single string, separated
491         * by commas. Special characters (backslashes, commas, and semicolons) are
492         * escaped for each value in order to prevent commas from being treated as
493         * delimiters, and to simulate what the value might look like in a
494         * plain-text vCard.<br>
495         * <code>["x-foo", {}, "text", "one", "two,three"] --&gt;
496         * "one,two\,three"</code>
497         * </p>
498         * <p>
499         * If the jCard value is a single array, then this array is treated as a
500         * "structured value", and converted its plain-text representation. Special
501         * characters (backslashes, commas, and semicolons) are escaped for each
502         * value in order to prevent commas and semicolons from being treated as
503         * delimiters.<br>
504         * <code>["x-foo", {}, "text", ["one", ["two", "three"], "four;five"]]
505         * --&gt; "one;two,three;four\;five"</code>
506         * </p>
507         * <p>
508         * If the jCard value starts with a JSON object, then it is converted to an
509         * empty string (JSON objects are not supported by this method).<br>
510         * <code>["x-foo", , "text", {"one": 1}] --&gt; ""}</code>
511         * </p>
512         * <p>
513         * For all other cases, behavior is undefined.
514         * </p>
515         * @param value the property's JSON value
516         * @param dataType the data type
517         * @param parameters the parsed parameters. These parameters will be
518         * assigned to the property object once this method returns. Therefore, do
519         * not assign any parameters to the property object itself whilst inside of
520         * this method, or else they will be overwritten.
521         * @param context the parse context
522         * @return the unmarshalled property object
523         * @throws CannotParseException if the marshaller could not parse the
524         * property's value
525         * @throws SkipMeException if the property should not be added to the final
526         * {@link VCard} object
527         */
528        protected T _parseJson(JCardValue value, VCardDataType dataType, VCardParameters parameters, ParseContext context) {
529                String valueStr = jcardValueToString(value);
530                return _parseText(valueStr, dataType, parameters, context);
531        }
532
533        /**
534         * Converts a jCard value to its plain-text format representation.
535         * @param value the jCard value
536         * @return the plain-text format representation (for example, "1,2,3" for a
537         * list of values)
538         */
539        private static String jcardValueToString(JCardValue value) {
540                List<JsonValue> values = value.getValues();
541                if (values.size() > 1) {
542                        List<String> multi = value.asMulti();
543                        if (!multi.isEmpty()) {
544                                return VObjectPropertyValues.writeList(multi);
545                        }
546                }
547
548                if (!values.isEmpty() && values.get(0).getArray() != null) {
549                        List<List<String>> structured = value.asStructured();
550                        if (!structured.isEmpty()) {
551                                return VObjectPropertyValues.writeStructured(structured, true);
552                        }
553                }
554
555                return VObjectPropertyValues.escape(value.asSingle());
556        }
557
558        /**
559         * Parses a date string.
560         * @param value the date string
561         * @return the parsed date
562         * @throws IllegalArgumentException if the date cannot be parsed
563         */
564        protected static Temporal date(String value) {
565                return VCardDateFormat.parse(value);
566        }
567
568        /**
569         * Formats a {@link Temporal} object as a string.
570         * @param date the date
571         * @return a helper object for customizing the write operation
572         */
573        protected static DateWriter date(Temporal date) {
574                return new DateWriter(date);
575        }
576
577        /**
578         * A helper class for writing date values.
579         */
580        protected static class DateWriter {
581                private Temporal date;
582                private boolean extended = false;
583
584                /**
585                 * Creates a new date writer object.
586                 * @param date the date to format
587                 */
588                public DateWriter(Temporal date) {
589                        this.date = date;
590                }
591
592                /**
593                 * Sets whether to use extended format or basic.
594                 * @param extended true to use extended format, false to use basic
595                 * (defaults to "false")
596                 * @return this
597                 */
598                public DateWriter extended(boolean extended) {
599                        this.extended = extended;
600                        return this;
601                }
602
603                /**
604                 * Creates the date string.
605                 * @return the date string
606                 */
607                public String write() {
608                        VCardDateFormat format = extended ? VCardDateFormat.EXTENDED : VCardDateFormat.BASIC;
609                        return format.format(date);
610                }
611        }
612
613        /**
614         * Creates a {@link CannotParseException} to indicate that a scribe could
615         * not find the necessary XML elements required in order to successfully
616         * parse a property (xCards only).
617         * @param dataTypes the expected data types (null for "unknown")
618         * @return the exception object (note that the exception is NOT thrown!)
619         */
620        protected static CannotParseException missingXmlElements(VCardDataType... dataTypes) {
621                //@formatter:off
622                String[] elements = Arrays.stream(dataTypes)
623                        .map(dataType -> (dataType == null) ? "unknown" : dataType.getName().toLowerCase())
624                .toArray(len -> new String[len]);
625                //@formatter:on
626
627                return missingXmlElements(elements);
628        }
629
630        /**
631         * Creates a {@link CannotParseException} to indicate that a scribe could
632         * not find the necessary XML elements required in order to successfully
633         * parse a property (xCards only).
634         * @param elements the names of the expected XML elements.
635         * @return the exception object (note that the exception is NOT thrown!)
636         */
637        protected static CannotParseException missingXmlElements(String... elements) {
638                return new CannotParseException(0, Arrays.toString(elements));
639        }
640
641        /**
642         * A utility method for switching between the "PREF" and "TYPE=PREF"
643         * parameters when marshalling a property (version 4.0 vCards use "PREF=1",
644         * while version 3.0 vCards use "TYPE=PREF"). This method is meant to be
645         * called from a scribe's {@link #_prepareParameters} method.
646         * @param property the property that is being marshalled
647         * @param parameters the parameters that are being marshalled (this should
648         * be a copy of the property's parameters so that changes can be made to
649         * them without affecting the original object)
650         * @param version the vCard version that the vCard is being marshalled to
651         * @param vcard the vCard that's being marshalled
652         */
653        protected static void handlePrefParam(VCardProperty property, VCardParameters parameters, VCardVersion version, VCard vcard) {
654                switch (version) {
655                case V2_1:
656                case V3_0:
657                        parameters.setPref(null);
658
659                        VCardProperty mostPreferred = findPropertyWithLowestPref(property.getClass(), vcard);
660                        if (property == mostPreferred) {
661                                parameters.put(VCardParameters.TYPE, "pref");
662                        }
663
664                        break;
665                case V4_0:
666                        //@formatter:off
667                        Optional<String> prefType = property.getParameters().getTypes().stream()
668                                .filter("pref"::equalsIgnoreCase)
669                        .findFirst();
670                        //@formatter:on
671
672                        if (prefType.isPresent()) {
673                                parameters.remove(VCardParameters.TYPE, prefType.get());
674                                parameters.setPref(1);
675                        }
676                        break;
677                }
678        }
679
680        private static VCardProperty findPropertyWithLowestPref(Class<? extends VCardProperty> clazz, VCard vcard) {
681                VCardProperty mostPreferred = null;
682                Integer lowestPref = null;
683
684                for (VCardProperty property : vcard.getProperties(clazz)) {
685                        Integer pref;
686                        try {
687                                pref = property.getParameters().getPref();
688                        } catch (IllegalStateException e) {
689                                continue;
690                        }
691
692                        if (pref == null) {
693                                continue;
694                        }
695
696                        if (lowestPref == null || pref < lowestPref) {
697                                mostPreferred = property;
698                                lowestPref = pref;
699                        }
700                }
701
702                return mostPreferred;
703        }
704
705        /**
706         * <p>
707         * Escapes special characters in a property value for writing to a
708         * plain-text output stream.
709         * </p>
710         * <p>
711         * If the target version is 2.1, then the value will be returned unchanged.
712         * 2.1 only requires special characters to be escaped within properties that
713         * make use of those special characters.
714         * </p>
715         * @param value the value to escape
716         * @param context the write context
717         * @return the escaped value
718         */
719        protected static String escape(String value, WriteContext context) {
720                if (context.getVersion() == VCardVersion.V2_1) {
721                        return value;
722                }
723
724                return VObjectPropertyValues.escape(value);
725        }
726}