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&lt;ICalendar&gt; 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&lt;List&lt;String&gt;&gt; warnings = new ArrayList&lt;List&lt;String&gt;&gt;();
196 * List&lt;ICalendar&gt; icals = Biweekly.parse(icalStr).warnings(warnings).all();
197 * int i = 0;
198 * for (List&lt;String&gt; 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}