001 package biweekly;
002
003 import java.io.File;
004 import java.io.IOException;
005 import java.io.InputStream;
006 import java.io.OutputStream;
007 import java.io.Reader;
008 import java.io.StringWriter;
009 import java.io.Writer;
010 import java.util.ArrayList;
011 import java.util.Arrays;
012 import java.util.Collection;
013 import java.util.HashMap;
014 import java.util.List;
015 import java.util.Map;
016 import java.util.Properties;
017
018 import javax.xml.transform.TransformerException;
019
020 import org.w3c.dom.Document;
021 import org.xml.sax.SAXException;
022
023 import biweekly.component.ICalComponent;
024 import biweekly.component.marshaller.ICalComponentMarshaller;
025 import biweekly.io.ICalMarshallerRegistrar;
026 import biweekly.io.json.JCalParseException;
027 import biweekly.io.json.JCalReader;
028 import biweekly.io.json.JCalWriter;
029 import biweekly.io.text.ICalRawReader;
030 import biweekly.io.text.ICalRawWriter;
031 import biweekly.io.text.ICalReader;
032 import biweekly.io.text.ICalWriter;
033 import biweekly.io.xml.XCalDocument;
034 import biweekly.property.ICalProperty;
035 import biweekly.property.marshaller.ICalPropertyMarshaller;
036 import biweekly.util.IOUtils;
037
038 import 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 */
239 public 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 ICalMarshallerRegistrar registrar = new ICalMarshallerRegistrar();
451
452 @SuppressWarnings("unchecked")
453 final T this_ = (T) this;
454
455 List<List<String>> warnings;
456
457 /**
458 * Registers a property marshaller.
459 * @param marshaller the marshaller
460 * @return this
461 */
462 public T register(ICalPropertyMarshaller<? extends ICalProperty> marshaller) {
463 registrar.register(marshaller);
464 return this_;
465 }
466
467 /**
468 * Registers a component marshaller.
469 * @param marshaller the marshaller
470 * @return this
471 */
472 public T register(ICalComponentMarshaller<? extends ICalComponent> marshaller) {
473 registrar.register(marshaller);
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.setRegistrar(registrar);
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(ICalPropertyMarshaller<? extends ICalProperty> marshaller) {
614 return super.register(marshaller);
615 }
616
617 @Override
618 public ParserChainTextReader register(ICalComponentMarshaller<? extends ICalComponent> marshaller) {
619 return super.register(marshaller);
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(ICalPropertyMarshaller<? extends ICalProperty> marshaller) {
658 return super.register(marshaller);
659 }
660
661 @Override
662 public ParserChainTextString register(ICalComponentMarshaller<? extends ICalComponent> marshaller) {
663 return super.register(marshaller);
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.setRegistrar(registrar);
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(ICalPropertyMarshaller<? extends ICalProperty> marshaller) {
749 return super.register(marshaller);
750 }
751
752 @Override
753 public ParserChainXmlString register(ICalComponentMarshaller<? extends ICalComponent> marshaller) {
754 return super.register(marshaller);
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(ICalPropertyMarshaller<? extends ICalProperty> marshaller) {
819 return super.register(marshaller);
820 }
821
822 @Override
823 public ParserChainXmlReader register(ICalComponentMarshaller<? extends ICalComponent> marshaller) {
824 return super.register(marshaller);
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(ICalPropertyMarshaller<? extends ICalProperty> marshaller) {
857 return super.register(marshaller);
858 }
859
860 @Override
861 public ParserChainXmlDocument register(ICalComponentMarshaller<? extends ICalComponent> marshaller) {
862 return super.register(marshaller);
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.setRegistrar(registrar);
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(ICalPropertyMarshaller<? extends ICalProperty> marshaller) {
1004 return super.register(marshaller);
1005 }
1006
1007 @Override
1008 public ParserChainJsonReader register(ICalComponentMarshaller<? extends ICalComponent> marshaller) {
1009 return super.register(marshaller);
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(ICalPropertyMarshaller<? extends ICalProperty> marshaller) {
1043 return super.register(marshaller);
1044 }
1045
1046 @Override
1047 public ParserChainJsonString register(ICalComponentMarshaller<? extends ICalComponent> marshaller) {
1048 return super.register(marshaller);
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 ICalMarshallerRegistrar registrar = new ICalMarshallerRegistrar();
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 marshaller.
1095 * @param marshaller the marshaller
1096 * @return this
1097 */
1098 public T register(ICalPropertyMarshaller<? extends ICalProperty> marshaller) {
1099 registrar.register(marshaller);
1100 return this_;
1101 }
1102
1103 /**
1104 * Registers a component marshaller.
1105 * @param marshaller the marshaller
1106 * @return this
1107 */
1108 public T register(ICalComponentMarshaller<? extends ICalComponent> marshaller) {
1109 registrar.register(marshaller);
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 marshaller class for a
1155 * component or property object cannot be found (only happens when an
1156 * experimental property/component marshaller is not registered with the
1157 * {@code register} 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 marshaller class for a
1173 * component or property object cannot be found (only happens when an
1174 * experimental property/component marshaller is not registered with the
1175 * {@code register} 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 marshaller class for a
1186 * component or property object cannot be found (only happens when an
1187 * experimental property/component marshaller is not registered with the
1188 * {@code register} 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 marshaller class for a
1201 * component or property object cannot be found (only happens when an
1202 * experimental property/component marshaller is not registered with the
1203 * {@code register} 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 marshaller class for a
1219 * component or property object cannot be found (only happens when an
1220 * experimental property/component marshaller is not registered with the
1221 * {@code register} 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.setRegistrar(registrar);
1230 icalWriter.setCaretEncodingEnabled(caretEncoding);
1231
1232 for (ICalendar ical : icals) {
1233 icalWriter.write(ical);
1234 }
1235 }
1236 }
1237
1238 ///////////////////////////////////////////////////////
1239 // XML
1240 ///////////////////////////////////////////////////////
1241
1242 /**
1243 * Chainer class for writing xCal documents (XML-encoded iCalendar objects).
1244 * @see Biweekly#writeXml(Collection)
1245 * @see Biweekly#writeXml(ICalendar...)
1246 */
1247 public static class WriterChainXml extends WriterChain<WriterChainXml> {
1248 int indent = -1;
1249 final Map<String, ICalDataType> parameterDataTypes = new HashMap<String, ICalDataType>(0);
1250
1251 WriterChainXml(Collection<ICalendar> icals) {
1252 super(icals);
1253 }
1254
1255 @Override
1256 public WriterChainXml register(ICalPropertyMarshaller<? extends ICalProperty> marshaller) {
1257 return super.register(marshaller);
1258 }
1259
1260 @Override
1261 public WriterChainXml register(ICalComponentMarshaller<? extends ICalComponent> marshaller) {
1262 return super.register(marshaller);
1263 }
1264
1265 /**
1266 * Registers the data type of an experimental parameter. Experimental
1267 * parameters use the "unknown" xCal data type by default.
1268 * @param parameterName the parameter name (e.g. "x-foo")
1269 * @param dataType the data type
1270 * @return this
1271 */
1272 public WriterChainXml register(String parameterName, ICalDataType dataType) {
1273 parameterDataTypes.put(parameterName, dataType);
1274 return this_;
1275 }
1276
1277 /**
1278 * Sets the number of indent spaces to use for pretty-printing. If not
1279 * set, then the XML will not be pretty-printed.
1280 * @param indent the number of spaces
1281 * @return this
1282 */
1283 public WriterChainXml indent(int indent) {
1284 this.indent = indent;
1285 return this_;
1286 }
1287
1288 /**
1289 * Writes the xCal document to a string.
1290 * @return the XML string
1291 * @throws IllegalArgumentException if the marshaller class for a
1292 * component or property object cannot be found (only happens when an
1293 * experimental property/component marshaller is not registered with the
1294 * {@code register} method.)
1295 */
1296 public String go() {
1297 StringWriter sw = new StringWriter();
1298 try {
1299 go(sw);
1300 } catch (TransformerException e) {
1301 //writing to a string
1302 }
1303 return sw.toString();
1304 }
1305
1306 /**
1307 * Writes the xCal document to an output stream.
1308 * @param out the output stream to write to
1309 * @throws IllegalArgumentException if the marshaller class for a
1310 * component or property object cannot be found (only happens when an
1311 * experimental property/component marshaller is not registered with the
1312 * {@code register} method.)
1313 * @throws TransformerException if there's a problem writing the XML
1314 */
1315 public void go(OutputStream out) throws TransformerException {
1316 XCalDocument document = constructDocument();
1317 document.write(out, indent);
1318 }
1319
1320 /**
1321 * Writes the xCal document to a file.
1322 * @param file the file to write to
1323 * @throws IllegalArgumentException if the marshaller class for a
1324 * component or property object cannot be found (only happens when an
1325 * experimental property/component marshaller is not registered with the
1326 * {@code register} method.)
1327 * @throws TransformerException if there's a problem writing the XML
1328 * @throws IOException if there's a problem writing to the file
1329 */
1330 public void go(File file) throws TransformerException, IOException {
1331 XCalDocument document = constructDocument();
1332 document.write(file, indent);
1333 }
1334
1335 /**
1336 * Writes the xCal document to a writer.
1337 * @param writer the writer to write to
1338 * @throws IllegalArgumentException if the marshaller class for a
1339 * component or property object cannot be found (only happens when an
1340 * experimental property/component marshaller is not registered with the
1341 * {@code register} method.)
1342 * @throws TransformerException if there's a problem writing the XML
1343 */
1344 public void go(Writer writer) throws TransformerException {
1345 XCalDocument document = constructDocument();
1346 document.write(writer, indent);
1347 }
1348
1349 /**
1350 * Writes the xCal document to an XML DOM.
1351 * @return the XML DOM
1352 */
1353 public Document dom() {
1354 XCalDocument document = constructDocument();
1355 return document.getDocument();
1356 }
1357
1358 private XCalDocument constructDocument() {
1359 XCalDocument document = new XCalDocument();
1360 document.setRegistrar(registrar);
1361 for (Map.Entry<String, ICalDataType> entry : parameterDataTypes.entrySet()) {
1362 document.registerParameterDataType(entry.getKey(), entry.getValue());
1363 }
1364
1365 for (ICalendar ical : icals) {
1366 document.add(ical);
1367 }
1368
1369 return document;
1370 }
1371 }
1372
1373 ///////////////////////////////////////////////////////
1374 // JSON
1375 ///////////////////////////////////////////////////////
1376
1377 /**
1378 * Chainer class for writing to JSON-encoded iCalendar data streams (jCal).
1379 * @see Biweekly#writeJson(Collection)
1380 * @see Biweekly#writeJson(ICalendar...)
1381 */
1382 public static class WriterChainJson extends WriterChain<WriterChainJson> {
1383 private boolean indent = false;
1384
1385 private WriterChainJson(Collection<ICalendar> icals) {
1386 super(icals);
1387 }
1388
1389 /**
1390 * Sets whether or not to pretty-print the JSON.
1391 * @param indent true to pretty-print it, false not to (defaults to
1392 * false)
1393 * @return this
1394 */
1395 public WriterChainJson indent(boolean indent) {
1396 this.indent = indent;
1397 return this_;
1398 }
1399
1400 /**
1401 * Writes the iCalendar objects to a string.
1402 * @return the iCalendar string
1403 * @throws IllegalArgumentException if the marshaller class for a
1404 * component or property object cannot be found (only happens when an
1405 * experimental property/component marshaller is not registered with the
1406 * {@code register} method.)
1407 */
1408 public String go() {
1409 StringWriter sw = new StringWriter();
1410 try {
1411 go(sw);
1412 } catch (IOException e) {
1413 //writing to a string
1414 }
1415 return sw.toString();
1416 }
1417
1418 /**
1419 * Writes the iCalendar objects to a data stream.
1420 * @param out the output stream to write to
1421 * @throws IllegalArgumentException if the marshaller class for a
1422 * component or property object cannot be found (only happens when an
1423 * experimental property/component marshaller is not registered with the
1424 * {@code register} method.)
1425 * @throws IOException if there's a problem writing to the output stream
1426 */
1427 public void go(OutputStream out) throws IOException {
1428 go(new JCalWriter(out, icals.size() > 1));
1429 }
1430
1431 /**
1432 * Writes the iCalendar objects to a file.
1433 * @param file the file to write to
1434 * @throws IllegalArgumentException if the marshaller class for a
1435 * component or property object cannot be found (only happens when an
1436 * experimental property/component marshaller is not registered with the
1437 * {@code register} method.)
1438 * @throws IOException if there's a problem writing to the file
1439 */
1440 public void go(File file) throws IOException {
1441 JCalWriter jcalWriter = new JCalWriter(file, icals.size() > 1);
1442 try {
1443 go(jcalWriter);
1444 } finally {
1445 IOUtils.closeQuietly(jcalWriter);
1446 }
1447 }
1448
1449 /**
1450 * Writes the iCalendar objects to a data stream.
1451 * @param writer the writer to write to
1452 * @throws IllegalArgumentException if the marshaller class for a
1453 * component or property object cannot be found (only happens when an
1454 * experimental property/component marshaller is not registered with the
1455 * {@code register} method.)
1456 * @throws IOException if there's a problem writing to the writer
1457 */
1458 public void go(Writer writer) throws IOException {
1459 go(new JCalWriter(writer, icals.size() > 1));
1460 }
1461
1462 private void go(JCalWriter jcalWriter) throws IOException {
1463 jcalWriter.setRegistrar(registrar);
1464 jcalWriter.setIndent(indent);
1465
1466 for (ICalendar ical : icals) {
1467 jcalWriter.write(ical);
1468 }
1469 jcalWriter.closeJsonStream();
1470 }
1471 }
1472
1473 private Biweekly() {
1474 //hide
1475 }
1476 }