001    package biweekly.io.text;
002    
003    import static biweekly.util.IOUtils.utf8Writer;
004    
005    import java.io.Closeable;
006    import java.io.File;
007    import java.io.FileNotFoundException;
008    import java.io.IOException;
009    import java.io.OutputStream;
010    import java.io.Writer;
011    
012    import biweekly.ICalDataType;
013    import biweekly.ICalendar;
014    import biweekly.component.ICalComponent;
015    import biweekly.component.marshaller.ICalComponentMarshaller;
016    import biweekly.io.ICalMarshallerRegistrar;
017    import biweekly.io.SkipMeException;
018    import biweekly.parameter.ICalParameters;
019    import biweekly.property.ICalProperty;
020    import biweekly.property.marshaller.ICalPropertyMarshaller;
021    
022    /*
023     Copyright (c) 2013, Michael Angstadt
024     All rights reserved.
025    
026     Redistribution and use in source and binary forms, with or without
027     modification, are permitted provided that the following conditions are met: 
028    
029     1. Redistributions of source code must retain the above copyright notice, this
030     list of conditions and the following disclaimer. 
031     2. Redistributions in binary form must reproduce the above copyright notice,
032     this list of conditions and the following disclaimer in the documentation
033     and/or other materials provided with the distribution. 
034    
035     THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
036     ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
037     WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
038     DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR
039     ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
040     (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
041     LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
042     ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
043     (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
044     SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
045     */
046    
047    /**
048     * <p>
049     * Writes {@link ICalendar} objects to an iCalendar data stream.
050     * </p>
051     * <p>
052     * <b>Example:</b>
053     * 
054     * <pre class="brush:java">
055     * List&lt;ICalendar&gt; icals = ... 
056     * OutputStream out = ...
057     * ICalWriter icalWriter = new ICalWriter(out);
058     * for (ICalendar ical : icals){
059     *   icalWriter.write(ical);
060     * }
061     * icalWriter.close();
062     * </pre>
063     * 
064     * </p>
065     * @author Michael Angstadt
066     * @rfc 5545
067     */
068    public class ICalWriter implements Closeable {
069            private ICalMarshallerRegistrar registrar = new ICalMarshallerRegistrar();
070            private final ICalRawWriter writer;
071    
072            /**
073             * Creates an iCalendar writer that writes to an output stream. Uses the
074             * standard folding scheme and newline sequence.
075             * @param outputStream the output stream to write to
076             */
077            public ICalWriter(OutputStream outputStream) {
078                    this(utf8Writer(outputStream));
079            }
080    
081            /**
082             * Creates an iCalendar writer that writes to an output stream. Uses the
083             * standard newline sequence.
084             * @param outputStream the output stream to write to
085             * @param foldingScheme the folding scheme to use or null not to fold at all
086             */
087            public ICalWriter(OutputStream outputStream, FoldingScheme foldingScheme) {
088                    this(utf8Writer(outputStream), foldingScheme);
089            }
090    
091            /**
092             * Creates an iCalendar writer that writes to an output stream.
093             * @param outputStream the output stream to write to
094             * @param foldingScheme the folding scheme to use or null not to fold at all
095             * @param newline the newline sequence to use
096             */
097            public ICalWriter(OutputStream outputStream, FoldingScheme foldingScheme, String newline) {
098                    this(utf8Writer(outputStream), foldingScheme, newline);
099            }
100    
101            /**
102             * Creates an iCalendar writer that writes to a file. Uses the standard
103             * folding scheme and newline sequence.
104             * @param file the file to write to
105             * @throws FileNotFoundException if the file cannot be written to
106             */
107            public ICalWriter(File file) throws FileNotFoundException {
108                    this(utf8Writer(file));
109            }
110    
111            /**
112             * Creates an iCalendar writer that writes to a file. Uses the standard
113             * folding scheme and newline sequence.
114             * @param file the file to write to
115             * @param append true to append to the end of the file, false to overwrite
116             * it
117             * @throws FileNotFoundException if the file cannot be written to
118             */
119            public ICalWriter(File file, boolean append) throws FileNotFoundException {
120                    this(utf8Writer(file, append));
121            }
122    
123            /**
124             * Creates an iCalendar writer that writes to a file. Uses the standard
125             * newline sequence.
126             * @param file the file to write to
127             * @param append true to append to the end of the file, false to overwrite
128             * it
129             * @param foldingScheme the folding scheme to use or null not to fold at all
130             * @throws FileNotFoundException if the file cannot be written to
131             */
132            public ICalWriter(File file, boolean append, FoldingScheme foldingScheme) throws FileNotFoundException {
133                    this(utf8Writer(file, append), foldingScheme);
134            }
135    
136            /**
137             * Creates an iCalendar writer that writes to a file.
138             * @param file the file to write to
139             * @param append true to append to the end of the file, false to overwrite
140             * it
141             * @param foldingScheme the folding scheme to use or null not to fold at all
142             * @param newline the newline sequence to use
143             * @throws FileNotFoundException if the file cannot be written to
144             */
145            public ICalWriter(File file, boolean append, FoldingScheme foldingScheme, String newline) throws FileNotFoundException {
146                    this(utf8Writer(file, append), foldingScheme, newline);
147            }
148    
149            /**
150             * Creates an iCalendar writer that writes to a writer. Uses the standard
151             * folding scheme and newline sequence.
152             * @param writer the writer to the data stream
153             */
154            public ICalWriter(Writer writer) {
155                    this(writer, FoldingScheme.DEFAULT);
156            }
157    
158            /**
159             * Creates an iCalendar writer that writes to a writer. Uses the standard
160             * newline sequence.
161             * @param writer the writer to the data stream
162             * @param foldingScheme the folding scheme to use or null not to fold at all
163             */
164            public ICalWriter(Writer writer, FoldingScheme foldingScheme) {
165                    this(writer, foldingScheme, "\r\n");
166            }
167    
168            /**
169             * Creates an iCalendar writer that writes to a writer.
170             * @param writer the writer to the data stream
171             * @param foldingScheme the folding scheme to use or null not to fold at all
172             * @param newline the newline sequence to use
173             */
174            public ICalWriter(Writer writer, FoldingScheme foldingScheme, String newline) {
175                    this.writer = new ICalRawWriter(writer, foldingScheme, newline);
176            }
177    
178            /**
179             * <p>
180             * Gets whether the writer will apply circumflex accent encoding on
181             * parameter values (disabled by default). This escaping mechanism allows
182             * for newlines and double quotes to be included in parameter values.
183             * </p>
184             * 
185             * <p>
186             * When disabled, the writer will replace newlines with spaces and double
187             * quotes with single quotes.
188             * </p>
189             * @return true if circumflex accent encoding is enabled, false if not
190             * @see ICalRawWriter#isCaretEncodingEnabled()
191             */
192            public boolean isCaretEncodingEnabled() {
193                    return writer.isCaretEncodingEnabled();
194            }
195    
196            /**
197             * <p>
198             * Sets whether the writer will apply circumflex accent encoding on
199             * parameter values (disabled by default). This escaping mechanism allows
200             * for newlines and double quotes to be included in parameter values.
201             * </p>
202             * 
203             * <p>
204             * When disabled, the writer will replace newlines with spaces and double
205             * quotes with single quotes.
206             * </p>
207             * @param enable true to use circumflex accent encoding, false not to
208             * @see ICalRawWriter#setCaretEncodingEnabled(boolean)
209             */
210            public void setCaretEncodingEnabled(boolean enable) {
211                    writer.setCaretEncodingEnabled(enable);
212            }
213    
214            /**
215             * Gets the newline sequence that is used to separate lines.
216             * @return the newline sequence
217             */
218            public String getNewline() {
219                    return writer.getNewline();
220            }
221    
222            /**
223             * Gets the rules for how each line is folded.
224             * @return the folding scheme or null if the lines are not folded
225             */
226            public FoldingScheme getFoldingScheme() {
227                    return writer.getFoldingScheme();
228            }
229    
230            /**
231             * <p>
232             * Registers an experimental property marshaller. Can also be used to
233             * override the marshaller of a standard property (such as DTSTART). Calling
234             * this method is the same as calling:
235             * </p>
236             * <p>
237             * {@code getRegistrar().register(marshaller)}.
238             * </p>
239             * @param marshaller the marshaller to register
240             */
241            public void registerMarshaller(ICalPropertyMarshaller<? extends ICalProperty> marshaller) {
242                    registrar.register(marshaller);
243            }
244    
245            /**
246             * <p>
247             * Registers an experimental component marshaller. Can also be used to
248             * override the marshaller of a standard component (such as VEVENT). Calling
249             * this method is the same as calling:
250             * </p>
251             * <p>
252             * {@code getRegistrar().register(marshaller)}.
253             * </p>
254             * @param marshaller the marshaller to register
255             */
256            public void registerMarshaller(ICalComponentMarshaller<? extends ICalComponent> marshaller) {
257                    registrar.register(marshaller);
258            }
259    
260            /**
261             * Gets the object that manages the component/property marshaller objects.
262             * @return the marshaller registrar
263             */
264            public ICalMarshallerRegistrar getRegistrar() {
265                    return registrar;
266            }
267    
268            /**
269             * Sets the object that manages the component/property marshaller objects.
270             * @param registrar the marshaller registrar
271             */
272            public void setRegistrar(ICalMarshallerRegistrar registrar) {
273                    this.registrar = registrar;
274            }
275    
276            /**
277             * Writes an iCalendar object to the data stream.
278             * @param ical the iCalendar object to write
279             * @throws IllegalArgumentException if the marshaller class for a component
280             * or property object cannot be found (only happens when an experimental
281             * property/component marshaller is not registered with the
282             * {@code registerMarshaller} method.)
283             * @throws IOException if there's a problem writing to the data stream
284             */
285            public void write(ICalendar ical) throws IOException {
286                    writeComponent(ical);
287            }
288    
289            /**
290             * Writes a component to the data stream.
291             * @param component the component to write
292             * @throws IOException if there's a problem writing to the data stream
293             */
294            @SuppressWarnings({ "rawtypes", "unchecked" })
295            private void writeComponent(ICalComponent component) throws IOException {
296                    ICalComponentMarshaller m = registrar.getComponentMarshaller(component);
297                    if (m == null) {
298                            throw new IllegalArgumentException("No marshaller found for component class \"" + component.getClass().getName() + "\".");
299                    }
300    
301                    writer.writeBeginComponent(m.getComponentName());
302    
303                    for (Object obj : m.getProperties(component)) {
304                            ICalProperty property = (ICalProperty) obj;
305                            ICalPropertyMarshaller pm = registrar.getPropertyMarshaller(property);
306                            if (pm == null) {
307                                    throw new IllegalArgumentException("No marshaller found for property class \"" + property.getClass().getName() + "\".");
308                            }
309    
310                            //marshal property
311                            ICalParameters parameters;
312                            String value;
313                            try {
314                                    parameters = pm.prepareParameters(property);
315                                    value = pm.writeText(property);
316                            } catch (SkipMeException e) {
317                                    continue;
318                            }
319    
320                            //set the data type
321                            ICalDataType dataType = pm.dataType(property);
322                            if (dataType != null && dataType != pm.getDefaultDataType()) {
323                                    //only add a VALUE parameter if the data type is (1) not "unknown" and (2) different from the property's default data type
324                                    parameters.setValue(dataType);
325                            }
326    
327                            //write property to data stream
328                            writer.writeProperty(pm.getPropertyName(), parameters, value);
329                    }
330    
331                    for (Object obj : m.getComponents(component)) {
332                            ICalComponent subComponent = (ICalComponent) obj;
333                            writeComponent(subComponent);
334                    }
335    
336                    writer.writeEndComponent(m.getComponentName());
337            }
338    
339            /**
340             * Closes the underlying {@link Writer} object.
341             */
342            public void close() throws IOException {
343                    writer.close();
344            }
345    }