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