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