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