001    package biweekly;
002    
003    import java.io.File;
004    import java.io.FileNotFoundException;
005    import java.io.FileReader;
006    import java.io.FileWriter;
007    import java.io.IOException;
008    import java.io.InputStream;
009    import java.io.InputStreamReader;
010    import java.io.OutputStream;
011    import java.io.OutputStreamWriter;
012    import java.io.Reader;
013    import java.io.StringWriter;
014    import java.io.Writer;
015    import java.util.ArrayList;
016    import java.util.Arrays;
017    import java.util.Collection;
018    import java.util.HashMap;
019    import java.util.List;
020    import java.util.Map;
021    import java.util.Properties;
022    
023    import javax.xml.transform.TransformerException;
024    
025    import org.w3c.dom.Document;
026    import org.xml.sax.SAXException;
027    
028    import biweekly.component.ICalComponent;
029    import biweekly.component.marshaller.ICalComponentMarshaller;
030    import biweekly.io.text.ICalRawReader;
031    import biweekly.io.text.ICalRawWriter;
032    import biweekly.io.text.ICalReader;
033    import biweekly.io.text.ICalWriter;
034    import biweekly.io.xml.XCalDocument;
035    import biweekly.parameter.Value;
036    import biweekly.property.ICalProperty;
037    import biweekly.property.marshaller.ICalPropertyMarshaller;
038    import biweekly.util.IOUtils;
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>
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     * //writer
085     * Writer writer = ...
086     * Biweekly.write(ical).go(writer);
087     * writer.close();
088     * </pre>
089     * 
090     * </p>
091     * 
092     * <p>
093     * <b>Writing multiple iCalendar objects</b>
094     * 
095     * <pre>
096     * ICalendar ical1 = new ICalendar();
097     * ICalendar ical2 = new ICalendar();
098     * 
099     * String icalString = Biweekly.write(ical1, ical2).go();
100     * </pre>
101     * 
102     * </p>
103     * 
104     * <p>
105     * <b>Writing an XML-encoded iCalendar object (xCal)</b><br>
106     * 
107     * <pre>
108     * //Call writeXml() instead of write()
109     * ICalendar ical = new ICalendar();
110     * String xml = Biweekly.writeXml(ical).indent(2).go();
111     * </pre>
112     * 
113     * </p>
114     * 
115     * <p>
116     * <b>Reading an iCalendar object</b>
117     * 
118     * <pre>
119     * ICalendar ical;
120     * 
121     * //string
122     * String icalStr = ...
123     * ical = Biweekly.parse(icalStr).first();
124     * 
125     * //file
126     * File file = new File("meeting.ics");
127     * ical = Biweekly.parse(file).first();
128     * 
129     * //reader
130     * Reader reader = ...
131     * ical = Biweekly.parse(reader).first();
132     * reader.close();
133     * </pre>
134     * 
135     * </p>
136     * 
137     * <p>
138     * <b>Reading multiple iCalendar objects</b>
139     * 
140     * <pre>
141     * String icalStr = ...
142     * List&lt;ICalendar&gt; icals = Biweekly.parse(icalStr).all();
143     * </pre>
144     * 
145     * </p>
146     * 
147     * <p>
148     * <b>Reading an XML-encoded iCalendar object (xCal)</b><br>
149     * 
150     * <pre>
151     * //Call parseXml() instead of parse()
152     * String xml = ...
153     * ICalendar ical = Biweekly.parseXml(xml).first();
154     * </pre>
155     * 
156     * </p>
157     * 
158     * <p>
159     * <b>Retrieving parser warnings</b>
160     * 
161     * <pre>
162     * String icalStr = ...
163     * List&lt;List&lt;String&gt;&gt; warnings = new ArrayList&lt;List&lt;String&gt;&gt;();
164     * List&lt;ICalendar&gt; icals = Biweekly.parse(icalStr).warnings(warnings).all();
165     * int i = 0;
166     * for (List&lt;String&gt; icalWarnings : warnings){
167     *   System.out.println("iCal #" + (i++) + " warnings:");
168     *   for (String warning : icalWarnings){
169     *     System.out.println(warning);
170     *   }
171     * }
172     * </pre>
173     * 
174     * </p>
175     * 
176     * <p>
177     * The methods in this class make use of the following classes. These classes
178     * can be used if greater control over the read/write operation is required:
179     * </p>
180     * 
181     * <style> table.t td, table.t th {border:1px solid #000;} </style>
182     * <table class="t" cellpadding="5" style="border-collapse:collapse;">
183     * <tr>
184     * <th></th>
185     * <th>Classes</th>
186     * <th>Supports<br>
187     * streaming?</th>
188     * </tr>
189     * <tr>
190     * <th>Text</th>
191     * <td>{@link ICalReader} / {@link ICalWriter}</td>
192     * <td>yes</td>
193     * </tr>
194     * <tr>
195     * <th>XML</th>
196     * <td>{@link XCalDocument}</td>
197     * <td>no</td>
198     * </tr>
199     * </table>
200     * @author Michael Angstadt
201     */
202    public class Biweekly {
203            /**
204             * The version of the library.
205             */
206            public static final String VERSION;
207    
208            /**
209             * The project webpage.
210             */
211            public static final String URL;
212    
213            static {
214                    InputStream in = null;
215                    try {
216                            in = Biweekly.class.getResourceAsStream("/biweekly.properties");
217                            Properties props = new Properties();
218                            props.load(in);
219    
220                            VERSION = props.getProperty("version");
221                            URL = props.getProperty("url");
222                    } catch (IOException e) {
223                            throw new RuntimeException(e);
224                    } finally {
225                            IOUtils.closeQuietly(in);
226                    }
227            }
228    
229            /**
230             * Parses an iCalendar object string.
231             * @param ical the iCalendar data
232             * @return chainer object for completing the parse operation
233             */
234            public static ParserChainTextString parse(String ical) {
235                    return new ParserChainTextString(ical);
236            }
237    
238            /**
239             * Parses an iCalendar file.
240             * @return chainer object for completing the parse operation
241             * @throws FileNotFoundException if the file does not exist or cannot be
242             * accessed
243             */
244            public static ParserChainTextReader parse(File file) throws FileNotFoundException {
245                    return new ParserChainTextReader(new FileReader(file), true); //close the FileReader, since we created it
246            }
247    
248            /**
249             * Parses an iCalendar data stream.
250             * @param in the input stream
251             * @return chainer object for completing the parse operation
252             */
253            public static ParserChainTextReader parse(InputStream in) {
254                    return parse(new InputStreamReader(in));
255            }
256    
257            /**
258             * Parses an iCalendar data stream.
259             * @param reader the reader
260             * @return chainer object for completing the parse operation
261             */
262            public static ParserChainTextReader parse(Reader reader) {
263                    return new ParserChainTextReader(reader, false); //do not close the Reader, since we didn't create it
264            }
265    
266            /**
267             * Writes an iCalendar object to a data stream.
268             * @param ical the iCalendar object to write
269             * @return chainer object for completing the write operation
270             */
271            public static WriterChainTextSingle write(ICalendar ical) {
272                    return new WriterChainTextSingle(ical);
273            }
274    
275            /**
276             * Writes multiple iCalendar objects to a data stream.
277             * @param icals the iCalendar objects to write
278             * @return chainer object for completing the write operation
279             */
280            public static WriterChainTextMulti write(ICalendar... icals) {
281                    return write(Arrays.asList(icals));
282            }
283    
284            /**
285             * Writes multiple iCalendar objects to a data stream.
286             * @param icals the iCalendar objects to write
287             * @return chainer object for completing the write operation
288             */
289            public static WriterChainTextMulti write(Collection<ICalendar> icals) {
290                    return new WriterChainTextMulti(icals);
291            }
292    
293            /**
294             * Parses an xCal document (XML-encoded iCalendar objects).
295             * @param xml the XML string
296             * @return chainer object for completing the parse operation
297             */
298            public static ParserChainXmlString parseXml(String xml) {
299                    return new ParserChainXmlString(xml);
300            }
301    
302            /**
303             * Parses an xCal document (XML-encoded iCalendar objects).
304             * @param file the XML file
305             * @return chainer object for completing the parse operation
306             * @throws FileNotFoundException if the file does not exist or cannot be
307             * accessed
308             */
309            public static ParserChainXmlReader parseXml(File file) throws FileNotFoundException {
310                    return new ParserChainXmlReader(file); //close the FileReader, since we created it
311            }
312    
313            /**
314             * Parses an xCal document (XML-encoded iCalendar objects).
315             * @param in the input stream
316             * @return chainer object for completing the parse operation
317             */
318            public static ParserChainXmlReader parseXml(InputStream in) {
319                    return parseXml(new InputStreamReader(in));
320            }
321    
322            /**
323             * Parses an xCal document (XML-encoded iCalendar objects).
324             * @param reader the reader
325             * @return chainer object for completing the parse operation
326             */
327            public static ParserChainXmlReader parseXml(Reader reader) {
328                    return new ParserChainXmlReader(reader);
329            }
330    
331            /**
332             * Parses an xCal document (XML-encoded iCalendar objects).
333             * @param document the XML document
334             * @return chainer object for completing the parse operation
335             */
336            public static ParserChainXmlDocument parseXml(Document document) {
337                    return new ParserChainXmlDocument(document);
338            }
339    
340            /**
341             * Writes an xCal document (XML-encoded iCalendar objects).
342             * @param icals the iCalendar object(s) to write
343             * @return chainer object for completing the write operation
344             */
345            public static WriterChainXml writeXml(ICalendar... icals) {
346                    return writeXml(Arrays.asList(icals));
347            }
348    
349            /**
350             * Writes an xCal document (XML-encoded iCalendar objects).
351             * @param icals the iCalendar objects to write
352             * @return chainer object for completing the write operation
353             */
354            public static WriterChainXml writeXml(Collection<ICalendar> icals) {
355                    return new WriterChainXml(icals);
356            }
357    
358            static abstract class ParserChain<T> {
359                    //Note: "package" level is used so various fields/methods don't show up in the Javadocs, but are still visible to child classes
360                    final List<ICalPropertyMarshaller<? extends ICalProperty>> propertyMarshallers = new ArrayList<ICalPropertyMarshaller<? extends ICalProperty>>(0);
361                    final List<ICalComponentMarshaller<? extends ICalComponent>> componentMarshallers = new ArrayList<ICalComponentMarshaller<? extends ICalComponent>>(0);
362    
363                    @SuppressWarnings("unchecked")
364                    final T this_ = (T) this;
365    
366                    List<List<String>> warnings;
367    
368                    /**
369                     * Registers a property marshaller.
370                     * @param marshaller the marshaller
371                     * @return this
372                     */
373                    public T register(ICalPropertyMarshaller<? extends ICalProperty> marshaller) {
374                            propertyMarshallers.add(marshaller);
375                            return this_;
376                    }
377    
378                    /**
379                     * Registers a component marshaller.
380                     * @param marshaller the marshaller
381                     * @return this
382                     */
383                    public T register(ICalComponentMarshaller<? extends ICalComponent> marshaller) {
384                            componentMarshallers.add(marshaller);
385                            return this_;
386                    }
387    
388                    /**
389                     * Provides a list for putting the parser warnings into.
390                     * @param warnings the list object to populate (it is a
391                     * "list of lists"--each parsed {@link ICalendar} object has its own
392                     * warnings list)
393                     * @return this
394                     */
395                    public T warnings(List<List<String>> warnings) {
396                            this.warnings = warnings;
397                            return this_;
398                    }
399    
400                    /**
401                     * Reads the first iCalendar object from the data stream.
402                     * @return the first iCalendar object or null if there are none
403                     * @throws IOException if there a problem reading from the data stream
404                     * @throws SAXException if there's a problem parsing the XML
405                     */
406                    public abstract ICalendar first() throws IOException, SAXException;
407    
408                    /**
409                     * Reads all iCalendar objects from the data stream.
410                     * @return the parsed iCalendar objects
411                     * @throws IOException if there's a problem reading from the data stream
412                     * @throws SAXException if there's a problem parsing the XML
413                     */
414                    public abstract List<ICalendar> all() throws IOException, SAXException;
415            }
416    
417            ///////////////////////////////////////////////////////
418            // plain-text
419            ///////////////////////////////////////////////////////
420    
421            static abstract class ParserChainText<T> extends ParserChain<T> {
422                    boolean caretDecoding = true;
423                    final boolean closeWhenDone;
424    
425                    private ParserChainText(boolean closeWhenDone) {
426                            this.closeWhenDone = closeWhenDone;
427                    }
428    
429                    /**
430                     * Sets whether the reader will decode parameter values that use
431                     * circumflex accent encoding (enabled by default). This escaping
432                     * mechanism allows newlines and double quotes to be included in
433                     * parameter values.
434                     * @param enable true to use circumflex accent decoding, false not to
435                     * @see ICalRawReader#setCaretDecodingEnabled(boolean)
436                     */
437                    public T caretDecoding(boolean enable) {
438                            caretDecoding = enable;
439                            return this_;
440                    }
441    
442                    @Override
443                    public ICalendar first() throws IOException {
444                            ICalReader parser = constructReader();
445    
446                            try {
447                                    ICalendar ical = parser.readNext();
448                                    if (warnings != null) {
449                                            warnings.add(parser.getWarnings());
450                                    }
451                                    return ical;
452                            } finally {
453                                    if (closeWhenDone) {
454                                            IOUtils.closeQuietly(parser);
455                                    }
456                            }
457                    }
458    
459                    @Override
460                    public List<ICalendar> all() throws IOException {
461                            ICalReader parser = constructReader();
462    
463                            try {
464                                    List<ICalendar> icals = new ArrayList<ICalendar>();
465                                    ICalendar ical;
466                                    while ((ical = parser.readNext()) != null) {
467                                            if (warnings != null) {
468                                                    warnings.add(parser.getWarnings());
469                                            }
470                                            icals.add(ical);
471                                    }
472                                    return icals;
473                            } finally {
474                                    if (closeWhenDone) {
475                                            IOUtils.closeQuietly(parser);
476                                    }
477                            }
478                    }
479    
480                    private ICalReader constructReader() {
481                            ICalReader parser = _constructReader();
482                            for (ICalPropertyMarshaller<? extends ICalProperty> marshaller : propertyMarshallers) {
483                                    parser.registerMarshaller(marshaller);
484                            }
485                            for (ICalComponentMarshaller<? extends ICalComponent> marshaller : componentMarshallers) {
486                                    parser.registerMarshaller(marshaller);
487                            }
488                            parser.setCaretDecodingEnabled(caretDecoding);
489                            return parser;
490                    }
491    
492                    abstract ICalReader _constructReader();
493            }
494    
495            /**
496             * Chainer class for parsing plain text iCalendar data streams.
497             * @see Biweekly#parse(InputStream)
498             * @see Biweekly#parse(File)
499             * @see Biweekly#parse(Reader)
500             */
501            public static class ParserChainTextReader extends ParserChainText<ParserChainTextReader> {
502                    private final Reader reader;
503    
504                    private ParserChainTextReader(Reader reader, boolean closeWhenDone) {
505                            super(closeWhenDone);
506                            this.reader = reader;
507                    }
508    
509                    @Override
510                    public ParserChainTextReader register(ICalPropertyMarshaller<? extends ICalProperty> marshaller) {
511                            return super.register(marshaller);
512                    }
513    
514                    @Override
515                    public ParserChainTextReader register(ICalComponentMarshaller<? extends ICalComponent> marshaller) {
516                            return super.register(marshaller);
517                    }
518    
519                    @Override
520                    public ParserChainTextReader warnings(List<List<String>> warnings) {
521                            return super.warnings(warnings);
522                    }
523    
524                    @Override
525                    public ParserChainTextReader caretDecoding(boolean enable) {
526                            return super.caretDecoding(enable);
527                    }
528    
529                    @Override
530                    ICalReader _constructReader() {
531                            return new ICalReader(reader);
532                    }
533            }
534    
535            /**
536             * Chainer class for parsing plain text iCalendar strings.
537             * @see Biweekly#parse(String)
538             */
539            public static class ParserChainTextString extends ParserChainText<ParserChainTextString> {
540                    private final String text;
541    
542                    private ParserChainTextString(String text) {
543                            super(false);
544                            this.text = text;
545                    }
546    
547                    @Override
548                    public ParserChainTextString register(ICalPropertyMarshaller<? extends ICalProperty> marshaller) {
549                            return super.register(marshaller);
550                    }
551    
552                    @Override
553                    public ParserChainTextString register(ICalComponentMarshaller<? extends ICalComponent> marshaller) {
554                            return super.register(marshaller);
555                    }
556    
557                    @Override
558                    public ParserChainTextString caretDecoding(boolean enable) {
559                            return super.caretDecoding(enable);
560                    }
561    
562                    @Override
563                    ICalReader _constructReader() {
564                            return new ICalReader(text);
565                    }
566    
567                    @Override
568                    public ICalendar first() {
569                            try {
570                                    return super.first();
571                            } catch (IOException e) {
572                                    //reading from string
573                            }
574                            return null;
575                    }
576    
577                    @Override
578                    public List<ICalendar> all() {
579                            try {
580                                    return super.all();
581                            } catch (IOException e) {
582                                    //reading from string
583                            }
584                            return null;
585                    }
586            }
587    
588            ///////////////////////////////////////////////////////
589            // XML
590            ///////////////////////////////////////////////////////
591    
592            static abstract class ParserChainXml<T> extends ParserChain<T> {
593                    @Override
594                    public ICalendar first() throws IOException, SAXException {
595                            XCalDocument document = constructDocument();
596                            ICalendar ical = document.parseFirst();
597                            if (warnings != null) {
598                                    warnings.addAll(document.getParseWarnings());
599                            }
600                            return ical;
601                    }
602    
603                    @Override
604                    public List<ICalendar> all() throws IOException, SAXException {
605                            XCalDocument document = constructDocument();
606                            List<ICalendar> icals = document.parseAll();
607                            if (warnings != null) {
608                                    warnings.addAll(document.getParseWarnings());
609                            }
610                            return icals;
611                    }
612    
613                    private XCalDocument constructDocument() throws SAXException, IOException {
614                            XCalDocument parser = _constructDocument();
615                            for (ICalPropertyMarshaller<? extends ICalProperty> marshaller : propertyMarshallers) {
616                                    parser.registerMarshaller(marshaller);
617                            }
618                            for (ICalComponentMarshaller<? extends ICalComponent> marshaller : componentMarshallers) {
619                                    parser.registerMarshaller(marshaller);
620                            }
621                            return parser;
622                    }
623    
624                    abstract XCalDocument _constructDocument() throws IOException, SAXException;
625            }
626    
627            /**
628             * Chainer class for parsing XML-encoded iCalendar objects (xCal).
629             * @see Biweekly#parseXml(String)
630             */
631            public static class ParserChainXmlString extends ParserChainXml<ParserChainXmlString> {
632                    private final String xml;
633    
634                    public ParserChainXmlString(String xml) {
635                            this.xml = xml;
636                    }
637    
638                    @Override
639                    public ParserChainXmlString register(ICalPropertyMarshaller<? extends ICalProperty> marshaller) {
640                            return super.register(marshaller);
641                    }
642    
643                    @Override
644                    public ParserChainXmlString register(ICalComponentMarshaller<? extends ICalComponent> marshaller) {
645                            return super.register(marshaller);
646                    }
647    
648                    @Override
649                    XCalDocument _constructDocument() throws SAXException {
650                            return new XCalDocument(xml);
651                    }
652    
653                    @Override
654                    public ICalendar first() throws SAXException {
655                            try {
656                                    return super.first();
657                            } catch (IOException e) {
658                                    //reading from string
659                            }
660                            return null;
661                    }
662    
663                    @Override
664                    public List<ICalendar> all() throws SAXException {
665                            try {
666                                    return super.all();
667                            } catch (IOException e) {
668                                    //reading from string
669                            }
670                            return null;
671                    }
672            }
673    
674            /**
675             * Chainer class for parsing XML-encoded iCalendar objects (xCal).
676             * @see Biweekly#parseXml(InputStream)
677             * @see Biweekly#parseXml(File)
678             * @see Biweekly#parseXml(Reader)
679             */
680            public static class ParserChainXmlReader extends ParserChainXml<ParserChainXmlReader> {
681                    private final Reader reader;
682                    private final File file;
683    
684                    public ParserChainXmlReader(Reader reader) {
685                            this.reader = reader;
686                            this.file = null;
687                    }
688    
689                    public ParserChainXmlReader(File file) {
690                            this.reader = null;
691                            this.file = file;
692                    }
693    
694                    @Override
695                    public ParserChainXmlReader register(ICalPropertyMarshaller<? extends ICalProperty> marshaller) {
696                            return super.register(marshaller);
697                    }
698    
699                    @Override
700                    public ParserChainXmlReader register(ICalComponentMarshaller<? extends ICalComponent> marshaller) {
701                            return super.register(marshaller);
702                    }
703    
704                    @Override
705                    XCalDocument _constructDocument() throws IOException, SAXException {
706                            return (reader == null) ? new XCalDocument(file) : new XCalDocument(reader);
707                    }
708            }
709    
710            /**
711             * Chainer class for parsing XML-encoded iCalendar objects (xCal).
712             * @see Biweekly#parseXml(Document)
713             */
714            public static class ParserChainXmlDocument extends ParserChainXml<ParserChainXmlDocument> {
715                    private final Document document;
716    
717                    public ParserChainXmlDocument(Document document) {
718                            this.document = document;
719                    }
720    
721                    @Override
722                    public ParserChainXmlDocument register(ICalPropertyMarshaller<? extends ICalProperty> marshaller) {
723                            return super.register(marshaller);
724                    }
725    
726                    @Override
727                    public ParserChainXmlDocument register(ICalComponentMarshaller<? extends ICalComponent> marshaller) {
728                            return super.register(marshaller);
729                    }
730    
731                    @Override
732                    XCalDocument _constructDocument() {
733                            return new XCalDocument(document);
734                    }
735    
736                    @Override
737                    public ICalendar first() {
738                            try {
739                                    return super.first();
740                            } catch (IOException e) {
741                                    //reading from string
742                            } catch (SAXException e) {
743                                    //reading from Document
744                            }
745                            return null;
746                    }
747    
748                    @Override
749                    public List<ICalendar> all() {
750                            try {
751                                    return super.all();
752                            } catch (IOException e) {
753                                    //reading from string
754                            } catch (SAXException e) {
755                                    //reading from Document
756                            }
757                            return null;
758                    }
759            }
760    
761            static abstract class WriterChain<T> {
762                    final Collection<ICalendar> icals;
763                    final List<ICalPropertyMarshaller<? extends ICalProperty>> propertyMarshallers = new ArrayList<ICalPropertyMarshaller<? extends ICalProperty>>(0);
764                    final List<ICalComponentMarshaller<? extends ICalComponent>> componentMarshallers = new ArrayList<ICalComponentMarshaller<? extends ICalComponent>>(0);
765    
766                    @SuppressWarnings("unchecked")
767                    final T this_ = (T) this;
768    
769                    WriterChain(Collection<ICalendar> icals) {
770                            this.icals = icals;
771                    }
772    
773                    /**
774                     * Registers a property marshaller.
775                     * @param marshaller the marshaller
776                     * @return this
777                     */
778                    public T register(ICalPropertyMarshaller<? extends ICalProperty> marshaller) {
779                            propertyMarshallers.add(marshaller);
780                            return this_;
781                    }
782    
783                    /**
784                     * Registers a component marshaller.
785                     * @param marshaller the marshaller
786                     * @return this
787                     */
788                    public T register(ICalComponentMarshaller<? extends ICalComponent> marshaller) {
789                            componentMarshallers.add(marshaller);
790                            return this_;
791                    }
792            }
793    
794            ///////////////////////////////////////////////////////
795            // plain-text
796            ///////////////////////////////////////////////////////
797    
798            static abstract class WriterChainText<T> extends WriterChain<T> {
799                    boolean caretEncoding = false;
800    
801                    WriterChainText(Collection<ICalendar> icals) {
802                            super(icals);
803                    }
804    
805                    /**
806                     * <p>
807                     * Sets whether the writer will apply circumflex accent encoding on
808                     * parameter values (disabled by default). This escaping mechanism
809                     * allows for newlines and double quotes to be included in parameter
810                     * values.
811                     * </p>
812                     * 
813                     * <p>
814                     * When disabled, the writer will replace newlines with spaces and
815                     * double quotes with single quotes.
816                     * </p>
817                     * @param enable true to use circumflex accent encoding, false not to
818                     * @see ICalRawWriter#setCaretEncodingEnabled(boolean)
819                     */
820                    public T caretEncoding(boolean enable) {
821                            this.caretEncoding = enable;
822                            return this_;
823                    }
824    
825                    /**
826                     * Writes the iCalendar objects to a string.
827                     * @return the iCalendar string
828                     */
829                    public String go() {
830                            StringWriter sw = new StringWriter();
831                            try {
832                                    go(sw);
833                            } catch (IOException e) {
834                                    //writing to a string
835                            }
836                            return sw.toString();
837                    }
838    
839                    /**
840                     * Writes the iCalendar objects to a data stream.
841                     * @param out the output stream to write to
842                     * @throws IOException if there's a problem writing to the output stream
843                     */
844                    public void go(OutputStream out) throws IOException {
845                            go(new OutputStreamWriter(out));
846                    }
847    
848                    /**
849                     * Writes the iCalendar objects to a file.
850                     * @param file the file to write to
851                     * @throws IOException if there's a problem writing to the file
852                     */
853                    public void go(File file) throws IOException {
854                            FileWriter writer = null;
855                            try {
856                                    writer = new FileWriter(file);
857                                    go(writer);
858                            } finally {
859                                    IOUtils.closeQuietly(writer);
860                            }
861                    }
862    
863                    /**
864                     * Writes the iCalendar objects to a data stream.
865                     * @param writer the writer to write to
866                     * @throws IOException if there's a problem writing to the writer
867                     */
868                    public void go(Writer writer) throws IOException {
869                            ICalWriter icalWriter = new ICalWriter(writer);
870                            for (ICalPropertyMarshaller<? extends ICalProperty> marshaller : propertyMarshallers) {
871                                    icalWriter.registerMarshaller(marshaller);
872                            }
873                            for (ICalComponentMarshaller<? extends ICalComponent> marshaller : componentMarshallers) {
874                                    icalWriter.registerMarshaller(marshaller);
875                            }
876                            icalWriter.setCaretEncodingEnabled(caretEncoding);
877    
878                            for (ICalendar ical : icals) {
879                                    icalWriter.write(ical);
880                                    addWarnings(icalWriter.getWarnings());
881                            }
882                    }
883    
884                    abstract void addWarnings(List<String> warnings);
885            }
886    
887            /**
888             * Chainer class for writing to plain text iCalendar data streams.
889             * @see Biweekly#write(Collection)
890             * @see Biweekly#write(ICalendar...)
891             */
892            public static class WriterChainTextMulti extends WriterChainText<WriterChainTextMulti> {
893                    private List<List<String>> warnings;
894    
895                    private WriterChainTextMulti(Collection<ICalendar> icals) {
896                            super(icals);
897                    }
898    
899                    @Override
900                    public WriterChainTextMulti caretEncoding(boolean enable) {
901                            return super.caretEncoding(enable);
902                    }
903    
904                    /**
905                     * Provides a list for putting the marshal warnings into.
906                     * @param warnings the list object to populate (it is a
907                     * "list of lists"--each {@link ICalendar} object has its own warnings
908                     * list)
909                     * @return this
910                     */
911                    public WriterChainTextMulti warnings(List<List<String>> warnings) {
912                            this.warnings = warnings;
913                            return this;
914                    }
915    
916                    @Override
917                    public WriterChainTextMulti register(ICalPropertyMarshaller<? extends ICalProperty> marshaller) {
918                            return super.register(marshaller);
919                    }
920    
921                    @Override
922                    public WriterChainTextMulti register(ICalComponentMarshaller<? extends ICalComponent> marshaller) {
923                            return super.register(marshaller);
924                    }
925    
926                    @Override
927                    void addWarnings(List<String> warnings) {
928                            if (this.warnings != null) {
929                                    this.warnings.add(warnings);
930                            }
931                    }
932            }
933    
934            /**
935             * Chainer class for writing to plain text iCalendar data streams.
936             * @see Biweekly#write(ICalendar)
937             */
938            public static class WriterChainTextSingle extends WriterChainText<WriterChainTextSingle> {
939                    private List<String> warnings;
940    
941                    private WriterChainTextSingle(ICalendar ical) {
942                            super(Arrays.asList(ical));
943                    }
944    
945                    @Override
946                    public WriterChainTextSingle caretEncoding(boolean enable) {
947                            return super.caretEncoding(enable);
948                    }
949    
950                    /**
951                     * Provides a list for putting the parser warnings into.
952                     * @param warnings the list object to populate
953                     * @return this
954                     */
955                    public WriterChainTextSingle warnings(List<String> warnings) {
956                            this.warnings = warnings;
957                            return this;
958                    }
959    
960                    @Override
961                    public WriterChainTextSingle register(ICalPropertyMarshaller<? extends ICalProperty> marshaller) {
962                            return super.register(marshaller);
963                    }
964    
965                    @Override
966                    public WriterChainTextSingle register(ICalComponentMarshaller<? extends ICalComponent> marshaller) {
967                            return super.register(marshaller);
968                    }
969    
970                    @Override
971                    void addWarnings(List<String> warnings) {
972                            if (this.warnings != null) {
973                                    this.warnings.addAll(warnings);
974                            }
975                    }
976            }
977    
978            ///////////////////////////////////////////////////////
979            // XML
980            ///////////////////////////////////////////////////////
981    
982            /**
983             * Chainer class for writing xCal documents (XML-encoded iCalendar objects).
984             * @see Biweekly#writeXml(Collection)
985             * @see Biweekly#writeXml(ICalendar...)
986             */
987            public static class WriterChainXml extends WriterChain<WriterChainXml> {
988                    int indent = -1;
989                    final Map<String, Value> parameterDataTypes = new HashMap<String, Value>(0);
990    
991                    WriterChainXml(Collection<ICalendar> icals) {
992                            super(icals);
993                    }
994    
995                    @Override
996                    public WriterChainXml register(ICalPropertyMarshaller<? extends ICalProperty> marshaller) {
997                            return super.register(marshaller);
998                    }
999    
1000                    @Override
1001                    public WriterChainXml register(ICalComponentMarshaller<? extends ICalComponent> marshaller) {
1002                            return super.register(marshaller);
1003                    }
1004    
1005                    /**
1006                     * Registers the data type of an experimental parameter. Experimental
1007                     * parameters use the "unknown" xCal data type by default.
1008                     * @param parameterName the parameter name (e.g. "x-foo")
1009                     * @param dataType the data type
1010                     */
1011                    public WriterChainXml register(String parameterName, Value dataType) {
1012                            parameterDataTypes.put(parameterName, dataType);
1013                            return this_;
1014                    }
1015    
1016                    /**
1017                     * Sets the number of indent spaces to use for pretty-printing. If not
1018                     * set, then the XML will not be pretty-printed.
1019                     * @param indent the number of spaces
1020                     * @return this
1021                     */
1022                    public WriterChainXml indent(int indent) {
1023                            this.indent = indent;
1024                            return this_;
1025                    }
1026    
1027                    /**
1028                     * Writes the xCal document to a string.
1029                     * @return the XML string
1030                     */
1031                    public String go() {
1032                            StringWriter sw = new StringWriter();
1033                            try {
1034                                    go(sw);
1035                            } catch (TransformerException e) {
1036                                    //writing to a string
1037                            }
1038                            return sw.toString();
1039                    }
1040    
1041                    /**
1042                     * Writes the xCal document to an output stream.
1043                     * @param out the output stream to write to
1044                     * @throws TransformerException if there's a problem writing the XML
1045                     */
1046                    public void go(OutputStream out) throws TransformerException {
1047                            go(new OutputStreamWriter(out));
1048                    }
1049    
1050                    /**
1051                     * Writes the xCal document to a file..
1052                     * @param file the file to write to
1053                     * @throws TransformerException if there's a problem writing the XML
1054                     * @throws IOException if there's a problem writing to the file
1055                     */
1056                    public void go(File file) throws TransformerException, IOException {
1057                            XCalDocument document = constructDocument();
1058                            document.write(file, indent);
1059                    }
1060    
1061                    /**
1062                     * Writes the xCal document to a writer.
1063                     * @param writer the writer to write to
1064                     * @throws TransformerException if there's a problem writing the XML
1065                     */
1066                    public void go(Writer writer) throws TransformerException {
1067                            XCalDocument document = constructDocument();
1068                            document.write(writer, indent);
1069                    }
1070    
1071                    /**
1072                     * Writes the xCal document to an XML DOM.
1073                     * @return the XML DOM
1074                     */
1075                    public Document dom() {
1076                            XCalDocument document = constructDocument();
1077                            return document.getDocument();
1078                    }
1079    
1080                    private XCalDocument constructDocument() {
1081                            XCalDocument document = new XCalDocument();
1082    
1083                            for (ICalPropertyMarshaller<? extends ICalProperty> marshaller : propertyMarshallers) {
1084                                    document.registerMarshaller(marshaller);
1085                            }
1086                            for (ICalComponentMarshaller<? extends ICalComponent> marshaller : componentMarshallers) {
1087                                    document.registerMarshaller(marshaller);
1088                            }
1089                            for (Map.Entry<String, Value> entry : parameterDataTypes.entrySet()) {
1090                                    document.registerParameterDataType(entry.getKey(), entry.getValue());
1091                            }
1092    
1093                            for (ICalendar ical : icals) {
1094                                    document.add(ical);
1095                            }
1096    
1097                            return document;
1098                    }
1099            }
1100    
1101            private Biweekly() {
1102                    //hide
1103            }
1104    }