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