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