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