001package biweekly; 002 003import java.io.File; 004import java.io.IOException; 005import java.io.InputStream; 006import java.io.OutputStream; 007import java.io.Reader; 008import java.io.StringWriter; 009import java.io.Writer; 010import java.util.ArrayList; 011import java.util.Arrays; 012import java.util.Collection; 013import java.util.HashMap; 014import java.util.List; 015import java.util.Map; 016import java.util.Properties; 017 018import javax.xml.transform.TransformerException; 019 020import org.w3c.dom.Document; 021import org.xml.sax.SAXException; 022 023import biweekly.component.ICalComponent; 024import biweekly.io.json.JCalParseException; 025import biweekly.io.json.JCalReader; 026import biweekly.io.json.JCalWriter; 027import biweekly.io.scribe.ScribeIndex; 028import biweekly.io.scribe.component.ICalComponentScribe; 029import biweekly.io.scribe.property.ICalPropertyScribe; 030import biweekly.io.text.ICalRawReader; 031import biweekly.io.text.ICalRawWriter; 032import biweekly.io.text.ICalReader; 033import biweekly.io.text.ICalWriter; 034import biweekly.io.xml.XCalDocument; 035import biweekly.property.ICalProperty; 036import biweekly.util.IOUtils; 037 038import com.fasterxml.jackson.core.JsonParseException; 039 040/* 041 Copyright (c) 2013, Michael Angstadt 042 All rights reserved. 043 044 Redistribution and use in source and binary forms, with or without 045 modification, are permitted provided that the following conditions are met: 046 047 1. Redistributions of source code must retain the above copyright notice, this 048 list of conditions and the following disclaimer. 049 2. Redistributions in binary form must reproduce the above copyright notice, 050 this list of conditions and the following disclaimer in the documentation 051 and/or other materials provided with the distribution. 052 053 THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND 054 ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 055 WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 056 DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR 057 ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES 058 (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; 059 LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND 060 ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 061 (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS 062 SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 063 */ 064 065/** 066 * <p> 067 * Contains static chaining factory methods for reading/writing iCalendar 068 * objects. 069 * </p> 070 * 071 * <p> 072 * <b>Writing an iCalendar object</b> 073 * 074 * <pre class="brush:java"> 075 * ICalendar ical = new ICalendar(); 076 * 077 * //string 078 * String icalString = Biweekly.write(ical).go(); 079 * 080 * //file 081 * File file = new File("meeting.ics"); 082 * Biweekly.write(ical).go(file); 083 * 084 * //output stream 085 * OutputStream out = ... 086 * Biweekly.write(ical).go(out); 087 * out.close(); 088 * 089 * //writer (should be configured to use UTF-8 encoding) 090 * Writer writer = ... 091 * Biweekly.write(ical).go(writer); 092 * writer.close(); 093 * </pre> 094 * 095 * </p> 096 * 097 * <p> 098 * <b>Writing multiple iCalendar objects</b> 099 * 100 * <pre class="brush:java"> 101 * ICalendar ical1 = new ICalendar(); 102 * ICalendar ical2 = new ICalendar(); 103 * 104 * String icalString = Biweekly.write(ical1, ical2).go(); 105 * </pre> 106 * 107 * </p> 108 * 109 * <p> 110 * <b>Writing an XML-encoded iCalendar object (xCal)</b><br> 111 * 112 * <pre class="brush:java"> 113 * //Call writeXml() instead of write() 114 * ICalendar ical = new ICalendar(); 115 * String xml = Biweekly.writeXml(ical).indent(2).go(); 116 * </pre> 117 * 118 * </p> 119 * 120 * <p> 121 * <b>Writing a JSON-encoded iCalendar object (jCal)</b><br> 122 * 123 * <pre class="brush:java"> 124 * //Call writeJson() instead of write() 125 * ICalendar ical = new ICalendar(); 126 * String json = Biweekly.writeJson(ical).go(); 127 * </pre> 128 * 129 * </p> 130 * 131 * <p> 132 * <b>Reading an iCalendar object</b> 133 * 134 * <pre class="brush:java"> 135 * ICalendar ical; 136 * 137 * //string 138 * String icalStr = ... 139 * ical = Biweekly.parse(icalStr).first(); 140 * 141 * //file 142 * File file = new File("meeting.ics"); 143 * ical = Biweekly.parse(file).first(); 144 * 145 * //input stream 146 * InputStream in = ... 147 * ical = Biweekly.parse(in).first(); 148 * in.close(); 149 * 150 * //reader (should be configured to read UTF-8) 151 * Reader reader = ... 152 * ical = Biweekly.parse(reader).first(); 153 * reader.close(); 154 * </pre> 155 * 156 * </p> 157 * 158 * <p> 159 * <b>Reading multiple iCalendar objects</b> 160 * 161 * <pre class="brush:java"> 162 * String icalStr = ... 163 * List<ICalendar> icals = Biweekly.parse(icalStr).all(); 164 * </pre> 165 * 166 * </p> 167 * 168 * <p> 169 * <b>Reading an XML-encoded iCalendar object (xCal)</b><br> 170 * 171 * <pre class="brush:java"> 172 * //Call parseXml() instead of parse() 173 * String xml = ... 174 * ICalendar ical = Biweekly.parseXml(xml).first(); 175 * </pre> 176 * 177 * </p> 178 * 179 * <p> 180 * <b>Reading a JSON-encoded iCalendar object (Cal)</b><br> 181 * 182 * <pre class="brush:java"> 183 * //Call parseJson() instead of parse() 184 * String json = ... 185 * ICalendar ical = Biweekly.parseJson(json).first(); 186 * </pre> 187 * 188 * </p> 189 * 190 * <p> 191 * <b>Retrieving parser warnings</b> 192 * 193 * <pre class="brush:java"> 194 * String icalStr = ... 195 * List<List<String>> warnings = new ArrayList<List<String>>(); 196 * List<ICalendar> icals = Biweekly.parse(icalStr).warnings(warnings).all(); 197 * int i = 0; 198 * for (List<String> icalWarnings : warnings){ 199 * System.out.println("iCal #" + (i++) + " warnings:"); 200 * for (String warning : icalWarnings){ 201 * System.out.println(warning); 202 * } 203 * } 204 * </pre> 205 * 206 * </p> 207 * 208 * <p> 209 * The methods in this class make use of the following classes. These classes 210 * can be used if greater control over the read/write operation is required: 211 * </p> 212 * 213 * <style> table.t td, table.t th {border:1px solid #000;} </style> 214 * <table class="t" cellpadding="5" style="border-collapse:collapse;"> 215 * <tr> 216 * <th></th> 217 * <th>Classes</th> 218 * <th>Supports<br> 219 * streaming?</th> 220 * </tr> 221 * <tr> 222 * <th>Text</th> 223 * <td>{@link ICalReader} / {@link ICalWriter}</td> 224 * <td>yes</td> 225 * </tr> 226 * <tr> 227 * <th>XML</th> 228 * <td>{@link XCalDocument}</td> 229 * <td>no</td> 230 * </tr> 231 * <tr> 232 * <th>JSON</th> 233 * <td>{@link JCalReader} / {@link JCalWriter}</td> 234 * <td>yes</td> 235 * </tr> 236 * </table> 237 * @author Michael Angstadt 238 */ 239public class Biweekly { 240 /** 241 * The version of the library. 242 */ 243 public static final String VERSION; 244 245 /** 246 * The project webpage. 247 */ 248 public static final String URL; 249 250 static { 251 InputStream in = null; 252 try { 253 in = Biweekly.class.getResourceAsStream("/biweekly.properties"); 254 Properties props = new Properties(); 255 props.load(in); 256 257 VERSION = props.getProperty("version"); 258 URL = props.getProperty("url"); 259 } catch (IOException e) { 260 throw new RuntimeException(e); 261 } finally { 262 IOUtils.closeQuietly(in); 263 } 264 } 265 266 /** 267 * Parses an iCalendar object string. 268 * @param ical the iCalendar data 269 * @return chainer object for completing the parse operation 270 */ 271 public static ParserChainTextString parse(String ical) { 272 return new ParserChainTextString(ical); 273 } 274 275 /** 276 * Parses an iCalendar file. 277 * @param file the iCalendar file 278 * @return chainer object for completing the parse operation 279 */ 280 public static ParserChainTextReader parse(File file) { 281 return new ParserChainTextReader(file); 282 } 283 284 /** 285 * Parses an iCalendar data stream. 286 * @param in the input stream 287 * @return chainer object for completing the parse operation 288 */ 289 public static ParserChainTextReader parse(InputStream in) { 290 return new ParserChainTextReader(in); 291 } 292 293 /** 294 * Parses an iCalendar data stream. 295 * @param reader the reader 296 * @return chainer object for completing the parse operation 297 */ 298 public static ParserChainTextReader parse(Reader reader) { 299 return new ParserChainTextReader(reader); 300 } 301 302 /** 303 * Writes multiple iCalendar objects to a data stream. 304 * @param icals the iCalendar objects to write 305 * @return chainer object for completing the write operation 306 */ 307 public static WriterChainText write(ICalendar... icals) { 308 return write(Arrays.asList(icals)); 309 } 310 311 /** 312 * Writes multiple iCalendar objects to a data stream. 313 * @param icals the iCalendar objects to write 314 * @return chainer object for completing the write operation 315 */ 316 public static WriterChainText write(Collection<ICalendar> icals) { 317 return new WriterChainText(icals); 318 } 319 320 /** 321 * Parses an xCal document (XML-encoded iCalendar objects) from a string. 322 * @param xml the XML string 323 * @return chainer object for completing the parse operation 324 */ 325 public static ParserChainXmlString parseXml(String xml) { 326 return new ParserChainXmlString(xml); 327 } 328 329 /** 330 * Parses an xCal document (XML-encoded iCalendar objects) from a file. 331 * @param file the XML file 332 * @return chainer object for completing the parse operation 333 */ 334 public static ParserChainXmlReader parseXml(File file) { 335 return new ParserChainXmlReader(file); 336 } 337 338 /** 339 * Parses an xCal document (XML-encoded iCalendar objects) from an input 340 * stream. 341 * @param in the input stream 342 * @return chainer object for completing the parse operation 343 */ 344 public static ParserChainXmlReader parseXml(InputStream in) { 345 return new ParserChainXmlReader(in); 346 } 347 348 /** 349 * <p> 350 * Parses an xCal document (XML-encoded iCalendar objects) from a reader. 351 * </p> 352 * <p> 353 * Note that use of this method is discouraged. It ignores the character 354 * encoding that is defined within the XML document itself, and should only 355 * be used if the encoding is undefined or if the encoding needs to be 356 * ignored for whatever reason. The {@link #parseXml(InputStream)} method 357 * should be used instead, since it takes the XML document's character 358 * encoding into account when parsing. 359 * </p> 360 * @param reader the reader 361 * @return chainer object for completing the parse operation 362 */ 363 public static ParserChainXmlReader parseXml(Reader reader) { 364 return new ParserChainXmlReader(reader); 365 } 366 367 /** 368 * Parses an xCal document (XML-encoded iCalendar objects). 369 * @param document the XML document 370 * @return chainer object for completing the parse operation 371 */ 372 public static ParserChainXmlDocument parseXml(Document document) { 373 return new ParserChainXmlDocument(document); 374 } 375 376 /** 377 * Writes an xCal document (XML-encoded iCalendar objects). 378 * @param icals the iCalendar object(s) to write 379 * @return chainer object for completing the write operation 380 */ 381 public static WriterChainXml writeXml(ICalendar... icals) { 382 return writeXml(Arrays.asList(icals)); 383 } 384 385 /** 386 * Writes an xCal document (XML-encoded iCalendar objects). 387 * @param icals the iCalendar objects to write 388 * @return chainer object for completing the write operation 389 */ 390 public static WriterChainXml writeXml(Collection<ICalendar> icals) { 391 return new WriterChainXml(icals); 392 } 393 394 /** 395 * Parses a jCal data stream (JSON-encoded iCalendar objects). 396 * @param json the JSON data 397 * @return chainer object for completing the parse operation 398 */ 399 public static ParserChainJsonString parseJson(String json) { 400 return new ParserChainJsonString(json); 401 } 402 403 /** 404 * Parses a jCal data stream (JSON-encoded iCalendar objects). 405 * @param file the JSON file 406 * @return chainer object for completing the parse operation 407 */ 408 public static ParserChainJsonReader parseJson(File file) { 409 return new ParserChainJsonReader(file); 410 } 411 412 /** 413 * Parses a jCal data stream (JSON-encoded iCalendar objects). 414 * @param in the input stream 415 * @return chainer object for completing the parse operation 416 */ 417 public static ParserChainJsonReader parseJson(InputStream in) { 418 return new ParserChainJsonReader(in); 419 } 420 421 /** 422 * Parses a jCal data stream (JSON-encoded iCalendar objects). 423 * @param reader the reader 424 * @return chainer object for completing the parse operation 425 */ 426 public static ParserChainJsonReader parseJson(Reader reader) { 427 return new ParserChainJsonReader(reader); 428 } 429 430 /** 431 * Writes an xCal document (XML-encoded iCalendar objects). 432 * @param icals the iCalendar object(s) to write 433 * @return chainer object for completing the write operation 434 */ 435 public static WriterChainJson writeJson(ICalendar... icals) { 436 return writeJson(Arrays.asList(icals)); 437 } 438 439 /** 440 * Writes an xCal document (XML-encoded iCalendar objects). 441 * @param icals the iCalendar objects to write 442 * @return chainer object for completing the write operation 443 */ 444 public static WriterChainJson writeJson(Collection<ICalendar> icals) { 445 return new WriterChainJson(icals); 446 } 447 448 static abstract class ParserChain<T> { 449 //Note: "package" level is used so various fields/methods don't show up in the Javadocs, but are still visible to child classes 450 final ScribeIndex index = new ScribeIndex(); 451 452 @SuppressWarnings("unchecked") 453 final T this_ = (T) this; 454 455 List<List<String>> warnings; 456 457 /** 458 * Registers a property scribe. 459 * @param scribe the scribe 460 * @return this 461 */ 462 public T register(ICalPropertyScribe<? extends ICalProperty> scribe) { 463 index.register(scribe); 464 return this_; 465 } 466 467 /** 468 * Registers a component scribe. 469 * @param scribe the scribe 470 * @return this 471 */ 472 public T register(ICalComponentScribe<? extends ICalComponent> scribe) { 473 index.register(scribe); 474 return this_; 475 } 476 477 /** 478 * Provides a list for putting the parser warnings into. 479 * @param warnings the list object to populate (it is a 480 * "list of lists"--each parsed {@link ICalendar} object has its own 481 * warnings list) 482 * @return this 483 */ 484 public T warnings(List<List<String>> warnings) { 485 this.warnings = warnings; 486 return this_; 487 } 488 489 /** 490 * Reads the first iCalendar object from the data stream. 491 * @return the first iCalendar object or null if there are none 492 * @throws IOException if there a problem reading from the data stream 493 * @throws SAXException if there's a problem parsing the XML 494 */ 495 public abstract ICalendar first() throws IOException, SAXException; 496 497 /** 498 * Reads all iCalendar objects from the data stream. 499 * @return the parsed iCalendar objects 500 * @throws IOException if there's a problem reading from the data stream 501 * @throws SAXException if there's a problem parsing the XML 502 */ 503 public abstract List<ICalendar> all() throws IOException, SAXException; 504 } 505 506 /////////////////////////////////////////////////////// 507 // plain-text 508 /////////////////////////////////////////////////////// 509 510 static abstract class ParserChainText<T> extends ParserChain<T> { 511 boolean caretDecoding = true; 512 final boolean closeWhenDone; 513 514 private ParserChainText(boolean closeWhenDone) { 515 this.closeWhenDone = closeWhenDone; 516 } 517 518 /** 519 * Sets whether the reader will decode parameter values that use 520 * circumflex accent encoding (enabled by default). This escaping 521 * mechanism allows newlines and double quotes to be included in 522 * parameter values. 523 * @param enable true to use circumflex accent decoding, false not to 524 * @return this 525 * @see ICalRawReader#setCaretDecodingEnabled(boolean) 526 */ 527 public T caretDecoding(boolean enable) { 528 caretDecoding = enable; 529 return this_; 530 } 531 532 @Override 533 public ICalendar first() throws IOException { 534 ICalReader parser = constructReader(); 535 536 try { 537 ICalendar ical = parser.readNext(); 538 if (warnings != null) { 539 warnings.add(parser.getWarnings()); 540 } 541 return ical; 542 } finally { 543 if (closeWhenDone) { 544 IOUtils.closeQuietly(parser); 545 } 546 } 547 } 548 549 @Override 550 public List<ICalendar> all() throws IOException { 551 ICalReader parser = constructReader(); 552 553 try { 554 List<ICalendar> icals = new ArrayList<ICalendar>(); 555 ICalendar ical; 556 while ((ical = parser.readNext()) != null) { 557 if (warnings != null) { 558 warnings.add(parser.getWarnings()); 559 } 560 icals.add(ical); 561 } 562 return icals; 563 } finally { 564 if (closeWhenDone) { 565 IOUtils.closeQuietly(parser); 566 } 567 } 568 } 569 570 private ICalReader constructReader() throws IOException { 571 ICalReader parser = _constructReader(); 572 parser.setScribeIndex(index); 573 parser.setCaretDecodingEnabled(caretDecoding); 574 return parser; 575 } 576 577 abstract ICalReader _constructReader() throws IOException; 578 } 579 580 /** 581 * Chainer class for parsing plain text iCalendar data streams. 582 * @see Biweekly#parse(InputStream) 583 * @see Biweekly#parse(File) 584 * @see Biweekly#parse(Reader) 585 */ 586 public static class ParserChainTextReader extends ParserChainText<ParserChainTextReader> { 587 private final InputStream in; 588 private final File file; 589 private final Reader reader; 590 591 private ParserChainTextReader(InputStream in) { 592 super(false); 593 this.in = in; 594 this.reader = null; 595 this.file = null; 596 } 597 598 private ParserChainTextReader(File file) { 599 super(true); 600 this.in = null; 601 this.reader = null; 602 this.file = file; 603 } 604 605 private ParserChainTextReader(Reader reader) { 606 super(false); 607 this.in = null; 608 this.reader = reader; 609 this.file = null; 610 } 611 612 @Override 613 public ParserChainTextReader register(ICalPropertyScribe<? extends ICalProperty> scribe) { 614 return super.register(scribe); 615 } 616 617 @Override 618 public ParserChainTextReader register(ICalComponentScribe<? extends ICalComponent> scribe) { 619 return super.register(scribe); 620 } 621 622 @Override 623 public ParserChainTextReader warnings(List<List<String>> warnings) { 624 return super.warnings(warnings); 625 } 626 627 @Override 628 public ParserChainTextReader caretDecoding(boolean enable) { 629 return super.caretDecoding(enable); 630 } 631 632 @Override 633 ICalReader _constructReader() throws IOException { 634 if (in != null) { 635 return new ICalReader(in); 636 } 637 if (file != null) { 638 return new ICalReader(file); 639 } 640 return new ICalReader(reader); 641 } 642 } 643 644 /** 645 * Chainer class for parsing plain text iCalendar strings. 646 * @see Biweekly#parse(String) 647 */ 648 public static class ParserChainTextString extends ParserChainText<ParserChainTextString> { 649 private final String text; 650 651 private ParserChainTextString(String text) { 652 super(false); 653 this.text = text; 654 } 655 656 @Override 657 public ParserChainTextString register(ICalPropertyScribe<? extends ICalProperty> scribe) { 658 return super.register(scribe); 659 } 660 661 @Override 662 public ParserChainTextString register(ICalComponentScribe<? extends ICalComponent> scribe) { 663 return super.register(scribe); 664 } 665 666 @Override 667 public ParserChainTextString warnings(List<List<String>> warnings) { 668 return super.warnings(warnings); 669 } 670 671 @Override 672 public ParserChainTextString caretDecoding(boolean enable) { 673 return super.caretDecoding(enable); 674 } 675 676 @Override 677 ICalReader _constructReader() { 678 return new ICalReader(text); 679 } 680 681 @Override 682 public ICalendar first() { 683 try { 684 return super.first(); 685 } catch (IOException e) { 686 //should never been thrown because we're reading from a string 687 throw new RuntimeException(e); 688 } 689 } 690 691 @Override 692 public List<ICalendar> all() { 693 try { 694 return super.all(); 695 } catch (IOException e) { 696 //should never been thrown because we're reading from a string 697 throw new RuntimeException(e); 698 } 699 } 700 } 701 702 /////////////////////////////////////////////////////// 703 // XML 704 /////////////////////////////////////////////////////// 705 706 static abstract class ParserChainXml<T> extends ParserChain<T> { 707 @Override 708 public ICalendar first() throws IOException, SAXException { 709 XCalDocument document = constructDocument(); 710 ICalendar ical = document.parseFirst(); 711 if (warnings != null) { 712 warnings.addAll(document.getParseWarnings()); 713 } 714 return ical; 715 } 716 717 @Override 718 public List<ICalendar> all() throws IOException, SAXException { 719 XCalDocument document = constructDocument(); 720 List<ICalendar> icals = document.parseAll(); 721 if (warnings != null) { 722 warnings.addAll(document.getParseWarnings()); 723 } 724 return icals; 725 } 726 727 private XCalDocument constructDocument() throws SAXException, IOException { 728 XCalDocument parser = _constructDocument(); 729 parser.setScribeIndex(index); 730 return parser; 731 } 732 733 abstract XCalDocument _constructDocument() throws IOException, SAXException; 734 } 735 736 /** 737 * Chainer class for parsing XML-encoded iCalendar objects (xCal). 738 * @see Biweekly#parseXml(String) 739 */ 740 public static class ParserChainXmlString extends ParserChainXml<ParserChainXmlString> { 741 private final String xml; 742 743 private ParserChainXmlString(String xml) { 744 this.xml = xml; 745 } 746 747 @Override 748 public ParserChainXmlString register(ICalPropertyScribe<? extends ICalProperty> scribe) { 749 return super.register(scribe); 750 } 751 752 @Override 753 public ParserChainXmlString register(ICalComponentScribe<? extends ICalComponent> scribe) { 754 return super.register(scribe); 755 } 756 757 @Override 758 public ParserChainXmlString warnings(List<List<String>> warnings) { 759 return super.warnings(warnings); 760 } 761 762 @Override 763 XCalDocument _constructDocument() throws SAXException { 764 return new XCalDocument(xml); 765 } 766 767 @Override 768 public ICalendar first() throws SAXException { 769 try { 770 return super.first(); 771 } catch (IOException e) { 772 //should never been thrown because we're reading from a string 773 throw new RuntimeException(e); 774 } 775 } 776 777 @Override 778 public List<ICalendar> all() throws SAXException { 779 try { 780 return super.all(); 781 } catch (IOException e) { 782 //should never been thrown because we're reading from a string 783 throw new RuntimeException(e); 784 } 785 } 786 } 787 788 /** 789 * Chainer class for parsing XML-encoded iCalendar objects (xCal). 790 * @see Biweekly#parseXml(InputStream) 791 * @see Biweekly#parseXml(File) 792 * @see Biweekly#parseXml(Reader) 793 */ 794 public static class ParserChainXmlReader extends ParserChainXml<ParserChainXmlReader> { 795 private final InputStream in; 796 private final File file; 797 private final Reader reader; 798 799 private ParserChainXmlReader(InputStream in) { 800 this.in = in; 801 this.reader = null; 802 this.file = null; 803 } 804 805 private ParserChainXmlReader(File file) { 806 this.in = null; 807 this.reader = null; 808 this.file = file; 809 } 810 811 private ParserChainXmlReader(Reader reader) { 812 this.in = null; 813 this.reader = reader; 814 this.file = null; 815 } 816 817 @Override 818 public ParserChainXmlReader register(ICalPropertyScribe<? extends ICalProperty> scribe) { 819 return super.register(scribe); 820 } 821 822 @Override 823 public ParserChainXmlReader register(ICalComponentScribe<? extends ICalComponent> scribe) { 824 return super.register(scribe); 825 } 826 827 @Override 828 public ParserChainXmlReader warnings(List<List<String>> warnings) { 829 return super.warnings(warnings); 830 } 831 832 @Override 833 XCalDocument _constructDocument() throws IOException, SAXException { 834 if (in != null) { 835 return new XCalDocument(in); 836 } 837 if (file != null) { 838 return new XCalDocument(file); 839 } 840 return new XCalDocument(reader); 841 } 842 } 843 844 /** 845 * Chainer class for parsing XML-encoded iCalendar objects (xCal). 846 * @see Biweekly#parseXml(Document) 847 */ 848 public static class ParserChainXmlDocument extends ParserChainXml<ParserChainXmlDocument> { 849 private final Document document; 850 851 private ParserChainXmlDocument(Document document) { 852 this.document = document; 853 } 854 855 @Override 856 public ParserChainXmlDocument register(ICalPropertyScribe<? extends ICalProperty> scribe) { 857 return super.register(scribe); 858 } 859 860 @Override 861 public ParserChainXmlDocument register(ICalComponentScribe<? extends ICalComponent> scribe) { 862 return super.register(scribe); 863 } 864 865 @Override 866 public ParserChainXmlDocument warnings(List<List<String>> warnings) { 867 return super.warnings(warnings); 868 } 869 870 @Override 871 XCalDocument _constructDocument() { 872 return new XCalDocument(document); 873 } 874 875 @Override 876 public ICalendar first() { 877 try { 878 return super.first(); 879 } catch (IOException e) { 880 //should never been thrown because we're reading from a DOM 881 throw new RuntimeException(e); 882 } catch (SAXException e) { 883 //should never been thrown because we're reading from a DOM 884 throw new RuntimeException(e); 885 } 886 } 887 888 @Override 889 public List<ICalendar> all() { 890 try { 891 return super.all(); 892 } catch (IOException e) { 893 //should never been thrown because we're reading from a DOM 894 throw new RuntimeException(e); 895 } catch (SAXException e) { 896 //should never been thrown because we're reading from a DOM 897 throw new RuntimeException(e); 898 } 899 } 900 } 901 902 /////////////////////////////////////////////////////// 903 // JSON 904 /////////////////////////////////////////////////////// 905 906 static abstract class ParserChainJson<T> extends ParserChain<T> { 907 final boolean closeWhenDone; 908 909 private ParserChainJson(boolean closeWhenDone) { 910 this.closeWhenDone = closeWhenDone; 911 } 912 913 /** 914 * @throws JCalParseException if the jCal syntax is incorrect (the JSON 915 * syntax may be valid, but it is not in the correct jCal format). 916 * @throws JsonParseException if the JSON syntax is incorrect 917 */ 918 @Override 919 public ICalendar first() throws IOException { 920 JCalReader parser = constructReader(); 921 922 try { 923 ICalendar ical = parser.readNext(); 924 if (warnings != null) { 925 warnings.add(parser.getWarnings()); 926 } 927 return ical; 928 } finally { 929 if (closeWhenDone) { 930 IOUtils.closeQuietly(parser); 931 } 932 } 933 } 934 935 /** 936 * @throws JCalParseException if the jCal syntax is incorrect (the JSON 937 * syntax may be valid, but it is not in the correct jCal format). 938 * @throws JsonParseException if the JSON syntax is incorrect 939 */ 940 @Override 941 public List<ICalendar> all() throws IOException { 942 JCalReader parser = constructReader(); 943 944 try { 945 List<ICalendar> icals = new ArrayList<ICalendar>(); 946 ICalendar ical; 947 while ((ical = parser.readNext()) != null) { 948 if (warnings != null) { 949 warnings.add(parser.getWarnings()); 950 } 951 icals.add(ical); 952 } 953 return icals; 954 } finally { 955 if (closeWhenDone) { 956 IOUtils.closeQuietly(parser); 957 } 958 } 959 } 960 961 private JCalReader constructReader() throws IOException { 962 JCalReader parser = _constructReader(); 963 parser.setScribeIndex(index); 964 return parser; 965 } 966 967 abstract JCalReader _constructReader() throws IOException; 968 } 969 970 /** 971 * Chainer class for parsing JSON-encoded iCalendar data streams (jCal). 972 * @see Biweekly#parseJson(InputStream) 973 * @see Biweekly#parseJson(File) 974 * @see Biweekly#parseJson(Reader) 975 */ 976 public static class ParserChainJsonReader extends ParserChainJson<ParserChainJsonReader> { 977 private final InputStream in; 978 private final File file; 979 private final Reader reader; 980 981 private ParserChainJsonReader(InputStream in) { 982 super(false); 983 this.in = in; 984 this.reader = null; 985 this.file = null; 986 } 987 988 private ParserChainJsonReader(File file) { 989 super(true); 990 this.in = null; 991 this.reader = null; 992 this.file = file; 993 } 994 995 private ParserChainJsonReader(Reader reader) { 996 super(false); 997 this.in = null; 998 this.reader = reader; 999 this.file = null; 1000 } 1001 1002 @Override 1003 public ParserChainJsonReader register(ICalPropertyScribe<? extends ICalProperty> scribe) { 1004 return super.register(scribe); 1005 } 1006 1007 @Override 1008 public ParserChainJsonReader register(ICalComponentScribe<? extends ICalComponent> scribe) { 1009 return super.register(scribe); 1010 } 1011 1012 @Override 1013 public ParserChainJsonReader warnings(List<List<String>> warnings) { 1014 return super.warnings(warnings); 1015 } 1016 1017 @Override 1018 JCalReader _constructReader() throws IOException { 1019 if (in != null) { 1020 return new JCalReader(in); 1021 } 1022 if (file != null) { 1023 return new JCalReader(file); 1024 } 1025 return new JCalReader(reader); 1026 } 1027 } 1028 1029 /** 1030 * Chainer class for parsing JSON-encoded iCalendar strings (jCal). 1031 * @see Biweekly#parseJson(String) 1032 */ 1033 public static class ParserChainJsonString extends ParserChainJson<ParserChainJsonString> { 1034 private final String text; 1035 1036 private ParserChainJsonString(String text) { 1037 super(false); 1038 this.text = text; 1039 } 1040 1041 @Override 1042 public ParserChainJsonString register(ICalPropertyScribe<? extends ICalProperty> scribe) { 1043 return super.register(scribe); 1044 } 1045 1046 @Override 1047 public ParserChainJsonString register(ICalComponentScribe<? extends ICalComponent> scribe) { 1048 return super.register(scribe); 1049 } 1050 1051 @Override 1052 public ParserChainJsonString warnings(List<List<String>> warnings) { 1053 return super.warnings(warnings); 1054 } 1055 1056 @Override 1057 JCalReader _constructReader() { 1058 return new JCalReader(text); 1059 } 1060 1061 @Override 1062 public ICalendar first() { 1063 try { 1064 return super.first(); 1065 } catch (IOException e) { 1066 //should never been thrown because we're reading from a string 1067 throw new RuntimeException(e); 1068 } 1069 } 1070 1071 @Override 1072 public List<ICalendar> all() { 1073 try { 1074 return super.all(); 1075 } catch (IOException e) { 1076 //should never been thrown because we're reading from a string 1077 throw new RuntimeException(e); 1078 } 1079 } 1080 } 1081 1082 static abstract class WriterChain<T> { 1083 final Collection<ICalendar> icals; 1084 final ScribeIndex index = new ScribeIndex(); 1085 1086 @SuppressWarnings("unchecked") 1087 final T this_ = (T) this; 1088 1089 WriterChain(Collection<ICalendar> icals) { 1090 this.icals = icals; 1091 } 1092 1093 /** 1094 * Registers a property scribe. 1095 * @param scribe the scribe 1096 * @return this 1097 */ 1098 public T register(ICalPropertyScribe<? extends ICalProperty> scribe) { 1099 index.register(scribe); 1100 return this_; 1101 } 1102 1103 /** 1104 * Registers a component scribe. 1105 * @param scribe the scribe 1106 * @return this 1107 */ 1108 public T register(ICalComponentScribe<? extends ICalComponent> scribe) { 1109 index.register(scribe); 1110 return this_; 1111 } 1112 } 1113 1114 /////////////////////////////////////////////////////// 1115 // plain-text 1116 /////////////////////////////////////////////////////// 1117 1118 /** 1119 * Chainer class for writing to plain text iCalendar data streams. 1120 * @see Biweekly#write(Collection) 1121 * @see Biweekly#write(ICalendar...) 1122 */ 1123 public static class WriterChainText extends WriterChain<WriterChainText> { 1124 boolean caretEncoding = false; 1125 1126 private WriterChainText(Collection<ICalendar> icals) { 1127 super(icals); 1128 } 1129 1130 /** 1131 * <p> 1132 * Sets whether the writer will apply circumflex accent encoding on 1133 * parameter values (disabled by default). This escaping mechanism 1134 * allows for newlines and double quotes to be included in parameter 1135 * values. 1136 * </p> 1137 * 1138 * <p> 1139 * When disabled, the writer will replace newlines with spaces and 1140 * double quotes with single quotes. 1141 * </p> 1142 * @param enable true to use circumflex accent encoding, false not to 1143 * @return this 1144 * @see ICalRawWriter#setCaretEncodingEnabled(boolean) 1145 */ 1146 public WriterChainText caretEncoding(boolean enable) { 1147 this.caretEncoding = enable; 1148 return this_; 1149 } 1150 1151 /** 1152 * Writes the iCalendar objects to a string. 1153 * @return the iCalendar string 1154 * @throws IllegalArgumentException if the scribe class for a component 1155 * or property object cannot be found (only happens when an experimental 1156 * property/component scribe is not registered with the {@code register} 1157 * method.) 1158 */ 1159 public String go() { 1160 StringWriter sw = new StringWriter(); 1161 try { 1162 go(sw); 1163 } catch (IOException e) { 1164 //writing to a string 1165 } 1166 return sw.toString(); 1167 } 1168 1169 /** 1170 * Writes the iCalendar objects to a data stream. 1171 * @param out the output stream to write to 1172 * @throws IllegalArgumentException if the scribe class for a component 1173 * or property object cannot be found (only happens when an experimental 1174 * property/component scribe is not registered with the {@code register} 1175 * method.) 1176 * @throws IOException if there's a problem writing to the output stream 1177 */ 1178 public void go(OutputStream out) throws IOException { 1179 go(new ICalWriter(out)); 1180 } 1181 1182 /** 1183 * Writes the iCalendar objects to a file. 1184 * @param file the file to write to 1185 * @throws IllegalArgumentException if the scribe class for a component 1186 * or property object cannot be found (only happens when an experimental 1187 * property/component scribe is not registered with the {@code register} 1188 * method.) 1189 * @throws IOException if there's a problem writing to the file 1190 */ 1191 public void go(File file) throws IOException { 1192 go(file, false); 1193 } 1194 1195 /** 1196 * Writes the iCalendar objects to a file. 1197 * @param file the file to write to 1198 * @param append true to append to the end of the file, false to 1199 * overwrite it 1200 * @throws IllegalArgumentException if the scribe class for a component 1201 * or property object cannot be found (only happens when an experimental 1202 * property/component scribe is not registered with the {@code register} 1203 * method.) 1204 * @throws IOException if there's a problem writing to the file 1205 */ 1206 public void go(File file, boolean append) throws IOException { 1207 ICalWriter icalWriter = new ICalWriter(file, append); 1208 try { 1209 go(icalWriter); 1210 } finally { 1211 IOUtils.closeQuietly(icalWriter); 1212 } 1213 } 1214 1215 /** 1216 * Writes the iCalendar objects to a data stream. 1217 * @param writer the writer to write to 1218 * @throws IllegalArgumentException if the scribe class for a component 1219 * or property object cannot be found (only happens when an experimental 1220 * property/component scribe is not registered with the {@code register} 1221 * method.) 1222 * @throws IOException if there's a problem writing to the writer 1223 */ 1224 public void go(Writer writer) throws IOException { 1225 go(new ICalWriter(writer)); 1226 } 1227 1228 private void go(ICalWriter icalWriter) throws IOException { 1229 icalWriter.setScribeIndex(index); 1230 icalWriter.setCaretEncodingEnabled(caretEncoding); 1231 1232 for (ICalendar ical : icals) { 1233 icalWriter.write(ical); 1234 icalWriter.flush(); 1235 } 1236 } 1237 } 1238 1239 /////////////////////////////////////////////////////// 1240 // XML 1241 /////////////////////////////////////////////////////// 1242 1243 /** 1244 * Chainer class for writing xCal documents (XML-encoded iCalendar objects). 1245 * @see Biweekly#writeXml(Collection) 1246 * @see Biweekly#writeXml(ICalendar...) 1247 */ 1248 public static class WriterChainXml extends WriterChain<WriterChainXml> { 1249 int indent = -1; 1250 final Map<String, ICalDataType> parameterDataTypes = new HashMap<String, ICalDataType>(0); 1251 1252 WriterChainXml(Collection<ICalendar> icals) { 1253 super(icals); 1254 } 1255 1256 @Override 1257 public WriterChainXml register(ICalPropertyScribe<? extends ICalProperty> scribe) { 1258 return super.register(scribe); 1259 } 1260 1261 @Override 1262 public WriterChainXml register(ICalComponentScribe<? extends ICalComponent> scribe) { 1263 return super.register(scribe); 1264 } 1265 1266 /** 1267 * Registers the data type of an experimental parameter. Experimental 1268 * parameters use the "unknown" xCal data type by default. 1269 * @param parameterName the parameter name (e.g. "x-foo") 1270 * @param dataType the data type 1271 * @return this 1272 */ 1273 public WriterChainXml register(String parameterName, ICalDataType dataType) { 1274 parameterDataTypes.put(parameterName, dataType); 1275 return this_; 1276 } 1277 1278 /** 1279 * Sets the number of indent spaces to use for pretty-printing. If not 1280 * set, then the XML will not be pretty-printed. 1281 * @param indent the number of spaces 1282 * @return this 1283 */ 1284 public WriterChainXml indent(int indent) { 1285 this.indent = indent; 1286 return this_; 1287 } 1288 1289 /** 1290 * Writes the xCal document to a string. 1291 * @return the XML string 1292 * @throws IllegalArgumentException if the scribe class for a component 1293 * or property object cannot be found (only happens when an experimental 1294 * property/component scribe is not registered with the {@code register} 1295 * method.) 1296 */ 1297 public String go() { 1298 StringWriter sw = new StringWriter(); 1299 try { 1300 go(sw); 1301 } catch (TransformerException e) { 1302 //writing to a string 1303 } 1304 return sw.toString(); 1305 } 1306 1307 /** 1308 * Writes the xCal document to an output stream. 1309 * @param out the output stream to write to 1310 * @throws IllegalArgumentException if the scribe class for a component 1311 * or property object cannot be found (only happens when an experimental 1312 * property/component scribe is not registered with the {@code register} 1313 * method.) 1314 * @throws TransformerException if there's a problem writing the XML 1315 */ 1316 public void go(OutputStream out) throws TransformerException { 1317 XCalDocument document = constructDocument(); 1318 document.write(out, indent); 1319 } 1320 1321 /** 1322 * Writes the xCal document to a file. 1323 * @param file the file to write to 1324 * @throws IllegalArgumentException if the scribe class for a component 1325 * or property object cannot be found (only happens when an experimental 1326 * property/component scribe is not registered with the {@code register} 1327 * method.) 1328 * @throws TransformerException if there's a problem writing the XML 1329 * @throws IOException if there's a problem writing to the file 1330 */ 1331 public void go(File file) throws TransformerException, IOException { 1332 XCalDocument document = constructDocument(); 1333 document.write(file, indent); 1334 } 1335 1336 /** 1337 * Writes the xCal document to a writer. 1338 * @param writer the writer to write to 1339 * @throws IllegalArgumentException if the scribe class for a component 1340 * or property object cannot be found (only happens when an experimental 1341 * property/component scribe is not registered with the {@code register} 1342 * method.) 1343 * @throws TransformerException if there's a problem writing the XML 1344 */ 1345 public void go(Writer writer) throws TransformerException { 1346 XCalDocument document = constructDocument(); 1347 document.write(writer, indent); 1348 } 1349 1350 /** 1351 * Writes the xCal document to an XML DOM. 1352 * @return the XML DOM 1353 */ 1354 public Document dom() { 1355 XCalDocument document = constructDocument(); 1356 return document.getDocument(); 1357 } 1358 1359 private XCalDocument constructDocument() { 1360 XCalDocument document = new XCalDocument(); 1361 document.setScribeIndex(index); 1362 for (Map.Entry<String, ICalDataType> entry : parameterDataTypes.entrySet()) { 1363 document.registerParameterDataType(entry.getKey(), entry.getValue()); 1364 } 1365 1366 for (ICalendar ical : icals) { 1367 document.add(ical); 1368 } 1369 1370 return document; 1371 } 1372 } 1373 1374 /////////////////////////////////////////////////////// 1375 // JSON 1376 /////////////////////////////////////////////////////// 1377 1378 /** 1379 * Chainer class for writing to JSON-encoded iCalendar data streams (jCal). 1380 * @see Biweekly#writeJson(Collection) 1381 * @see Biweekly#writeJson(ICalendar...) 1382 */ 1383 public static class WriterChainJson extends WriterChain<WriterChainJson> { 1384 private boolean indent = false; 1385 1386 private WriterChainJson(Collection<ICalendar> icals) { 1387 super(icals); 1388 } 1389 1390 /** 1391 * Sets whether or not to pretty-print the JSON. 1392 * @param indent true to pretty-print it, false not to (defaults to 1393 * false) 1394 * @return this 1395 */ 1396 public WriterChainJson indent(boolean indent) { 1397 this.indent = indent; 1398 return this_; 1399 } 1400 1401 /** 1402 * Writes the iCalendar objects to a string. 1403 * @return the iCalendar string 1404 * @throws IllegalArgumentException if the scribe class for a component 1405 * or property object cannot be found (only happens when an experimental 1406 * property/component scribe is not registered with the {@code register} 1407 * method.) 1408 */ 1409 public String go() { 1410 StringWriter sw = new StringWriter(); 1411 try { 1412 go(sw); 1413 } catch (IOException e) { 1414 //writing to a string 1415 } 1416 return sw.toString(); 1417 } 1418 1419 /** 1420 * Writes the iCalendar objects to a data stream. 1421 * @param out the output stream to write to 1422 * @throws IllegalArgumentException if the scribe class for a component 1423 * or property object cannot be found (only happens when an experimental 1424 * property/component scribe is not registered with the {@code register} 1425 * method.) 1426 * @throws IOException if there's a problem writing to the output stream 1427 */ 1428 public void go(OutputStream out) throws IOException { 1429 go(new JCalWriter(out, icals.size() > 1)); 1430 } 1431 1432 /** 1433 * Writes the iCalendar objects to a file. 1434 * @param file the file to write to 1435 * @throws IllegalArgumentException if the scribe class for a component 1436 * or property object cannot be found (only happens when an experimental 1437 * property/component scribe is not registered with the {@code register} 1438 * method.) 1439 * @throws IOException if there's a problem writing to the file 1440 */ 1441 public void go(File file) throws IOException { 1442 JCalWriter jcalWriter = new JCalWriter(file, icals.size() > 1); 1443 try { 1444 go(jcalWriter); 1445 } finally { 1446 IOUtils.closeQuietly(jcalWriter); 1447 } 1448 } 1449 1450 /** 1451 * Writes the iCalendar objects to a data stream. 1452 * @param writer the writer to write to 1453 * @throws IllegalArgumentException if the scribe class for a component 1454 * or property object cannot be found (only happens when an experimental 1455 * property/component scribe is not registered with the {@code register} 1456 * method.) 1457 * @throws IOException if there's a problem writing to the writer 1458 */ 1459 public void go(Writer writer) throws IOException { 1460 go(new JCalWriter(writer, icals.size() > 1)); 1461 } 1462 1463 private void go(JCalWriter jcalWriter) throws IOException { 1464 jcalWriter.setScribeIndex(index); 1465 jcalWriter.setIndent(indent); 1466 1467 for (ICalendar ical : icals) { 1468 jcalWriter.write(ical); 1469 jcalWriter.flush(); 1470 } 1471 jcalWriter.closeJsonStream(); 1472 } 1473 } 1474 1475 private Biweekly() { 1476 //hide 1477 } 1478}