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