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"] --> "the\;value"</code><br> 486 * <code>["x-foo", {}, "text", 2] --> "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"] --> 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 * --> "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}] --> ""}</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}