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