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                                    icalWriter.flush();
1235                            }
1236                    }
1237            }
1238    
1239            ///////////////////////////////////////////////////////
1240            // XML
1241            ///////////////////////////////////////////////////////
1242    
1243            /**
1244             * Chainer class for writing xCal documents (XML-encoded iCalendar objects).
1245             * @see Biweekly#writeXml(Collection)
1246             * @see Biweekly#writeXml(ICalendar...)
1247             */
1248            public static class WriterChainXml extends WriterChain<WriterChainXml> {
1249                    int indent = -1;
1250                    final Map<String, ICalDataType> parameterDataTypes = new HashMap<String, ICalDataType>(0);
1251    
1252                    WriterChainXml(Collection<ICalendar> icals) {
1253                            super(icals);
1254                    }
1255    
1256                    @Override
1257                    public WriterChainXml register(ICalPropertyMarshaller<? extends ICalProperty> marshaller) {
1258                            return super.register(marshaller);
1259                    }
1260    
1261                    @Override
1262                    public WriterChainXml register(ICalComponentMarshaller<? extends ICalComponent> marshaller) {
1263                            return super.register(marshaller);
1264                    }
1265    
1266                    /**
1267                     * Registers the data type of an experimental parameter. Experimental
1268                     * parameters use the "unknown" xCal data type by default.
1269                     * @param parameterName the parameter name (e.g. "x-foo")
1270                     * @param dataType the data type
1271                     * @return this
1272                     */
1273                    public WriterChainXml register(String parameterName, ICalDataType dataType) {
1274                            parameterDataTypes.put(parameterName, dataType);
1275                            return this_;
1276                    }
1277    
1278                    /**
1279                     * Sets the number of indent spaces to use for pretty-printing. If not
1280                     * set, then the XML will not be pretty-printed.
1281                     * @param indent the number of spaces
1282                     * @return this
1283                     */
1284                    public WriterChainXml indent(int indent) {
1285                            this.indent = indent;
1286                            return this_;
1287                    }
1288    
1289                    /**
1290                     * Writes the xCal document to a string.
1291                     * @return the XML string
1292                     * @throws IllegalArgumentException if the marshaller class for a
1293                     * component or property object cannot be found (only happens when an
1294                     * experimental property/component marshaller is not registered with the
1295                     * {@code register} method.)
1296                     */
1297                    public String go() {
1298                            StringWriter sw = new StringWriter();
1299                            try {
1300                                    go(sw);
1301                            } catch (TransformerException e) {
1302                                    //writing to a string
1303                            }
1304                            return sw.toString();
1305                    }
1306    
1307                    /**
1308                     * Writes the xCal document to an output stream.
1309                     * @param out the output stream to write to
1310                     * @throws IllegalArgumentException if the marshaller class for a
1311                     * component or property object cannot be found (only happens when an
1312                     * experimental property/component marshaller is not registered with the
1313                     * {@code register} method.)
1314                     * @throws TransformerException if there's a problem writing the XML
1315                     */
1316                    public void go(OutputStream out) throws TransformerException {
1317                            XCalDocument document = constructDocument();
1318                            document.write(out, indent);
1319                    }
1320    
1321                    /**
1322                     * Writes the xCal document to a file.
1323                     * @param file the file to write to
1324                     * @throws IllegalArgumentException if the marshaller class for a
1325                     * component or property object cannot be found (only happens when an
1326                     * experimental property/component marshaller is not registered with the
1327                     * {@code register} method.)
1328                     * @throws TransformerException if there's a problem writing the XML
1329                     * @throws IOException if there's a problem writing to the file
1330                     */
1331                    public void go(File file) throws TransformerException, IOException {
1332                            XCalDocument document = constructDocument();
1333                            document.write(file, indent);
1334                    }
1335    
1336                    /**
1337                     * Writes the xCal document to a writer.
1338                     * @param writer the writer to write to
1339                     * @throws IllegalArgumentException if the marshaller class for a
1340                     * component or property object cannot be found (only happens when an
1341                     * experimental property/component marshaller is not registered with the
1342                     * {@code register} method.)
1343                     * @throws TransformerException if there's a problem writing the XML
1344                     */
1345                    public void go(Writer writer) throws TransformerException {
1346                            XCalDocument document = constructDocument();
1347                            document.write(writer, indent);
1348                    }
1349    
1350                    /**
1351                     * Writes the xCal document to an XML DOM.
1352                     * @return the XML DOM
1353                     */
1354                    public Document dom() {
1355                            XCalDocument document = constructDocument();
1356                            return document.getDocument();
1357                    }
1358    
1359                    private XCalDocument constructDocument() {
1360                            XCalDocument document = new XCalDocument();
1361                            document.setRegistrar(registrar);
1362                            for (Map.Entry<String, ICalDataType> entry : parameterDataTypes.entrySet()) {
1363                                    document.registerParameterDataType(entry.getKey(), entry.getValue());
1364                            }
1365    
1366                            for (ICalendar ical : icals) {
1367                                    document.add(ical);
1368                            }
1369    
1370                            return document;
1371                    }
1372            }
1373    
1374            ///////////////////////////////////////////////////////
1375            // JSON
1376            ///////////////////////////////////////////////////////
1377    
1378            /**
1379             * Chainer class for writing to JSON-encoded iCalendar data streams (jCal).
1380             * @see Biweekly#writeJson(Collection)
1381             * @see Biweekly#writeJson(ICalendar...)
1382             */
1383            public static class WriterChainJson extends WriterChain<WriterChainJson> {
1384                    private boolean indent = false;
1385    
1386                    private WriterChainJson(Collection<ICalendar> icals) {
1387                            super(icals);
1388                    }
1389    
1390                    /**
1391                     * Sets whether or not to pretty-print the JSON.
1392                     * @param indent true to pretty-print it, false not to (defaults to
1393                     * false)
1394                     * @return this
1395                     */
1396                    public WriterChainJson indent(boolean indent) {
1397                            this.indent = indent;
1398                            return this_;
1399                    }
1400    
1401                    /**
1402                     * Writes the iCalendar objects to a string.
1403                     * @return the iCalendar string
1404                     * @throws IllegalArgumentException if the marshaller class for a
1405                     * component or property object cannot be found (only happens when an
1406                     * experimental property/component marshaller is not registered with the
1407                     * {@code register} method.)
1408                     */
1409                    public String go() {
1410                            StringWriter sw = new StringWriter();
1411                            try {
1412                                    go(sw);
1413                            } catch (IOException e) {
1414                                    //writing to a string
1415                            }
1416                            return sw.toString();
1417                    }
1418    
1419                    /**
1420                     * Writes the iCalendar objects to a data stream.
1421                     * @param out the output stream to write to
1422                     * @throws IllegalArgumentException if the marshaller class for a
1423                     * component or property object cannot be found (only happens when an
1424                     * experimental property/component marshaller is not registered with the
1425                     * {@code register} method.)
1426                     * @throws IOException if there's a problem writing to the output stream
1427                     */
1428                    public void go(OutputStream out) throws IOException {
1429                            go(new JCalWriter(out, icals.size() > 1));
1430                    }
1431    
1432                    /**
1433                     * Writes the iCalendar objects to a file.
1434                     * @param file the file to write to
1435                     * @throws IllegalArgumentException if the marshaller class for a
1436                     * component or property object cannot be found (only happens when an
1437                     * experimental property/component marshaller is not registered with the
1438                     * {@code register} method.)
1439                     * @throws IOException if there's a problem writing to the file
1440                     */
1441                    public void go(File file) throws IOException {
1442                            JCalWriter jcalWriter = new JCalWriter(file, icals.size() > 1);
1443                            try {
1444                                    go(jcalWriter);
1445                            } finally {
1446                                    IOUtils.closeQuietly(jcalWriter);
1447                            }
1448                    }
1449    
1450                    /**
1451                     * Writes the iCalendar objects to a data stream.
1452                     * @param writer the writer to write to
1453                     * @throws IllegalArgumentException if the marshaller class for a
1454                     * component or property object cannot be found (only happens when an
1455                     * experimental property/component marshaller is not registered with the
1456                     * {@code register} method.)
1457                     * @throws IOException if there's a problem writing to the writer
1458                     */
1459                    public void go(Writer writer) throws IOException {
1460                            go(new JCalWriter(writer, icals.size() > 1));
1461                    }
1462    
1463                    private void go(JCalWriter jcalWriter) throws IOException {
1464                            jcalWriter.setRegistrar(registrar);
1465                            jcalWriter.setIndent(indent);
1466    
1467                            for (ICalendar ical : icals) {
1468                                    jcalWriter.write(ical);
1469                                    jcalWriter.flush();
1470                            }
1471                            jcalWriter.closeJsonStream();
1472                    }
1473            }
1474    
1475            private Biweekly() {
1476                    //hide
1477            }
1478    }