001package biweekly.io.text;
002
003import static biweekly.util.IOUtils.utf8Writer;
004
005import java.io.Closeable;
006import java.io.File;
007import java.io.FileNotFoundException;
008import java.io.Flushable;
009import java.io.IOException;
010import java.io.OutputStream;
011import java.io.Writer;
012
013import biweekly.ICalDataType;
014import biweekly.ICalendar;
015import biweekly.component.ICalComponent;
016import biweekly.io.SkipMeException;
017import biweekly.io.scribe.ScribeIndex;
018import biweekly.io.scribe.component.ICalComponentScribe;
019import biweekly.io.scribe.property.ICalPropertyScribe;
020import biweekly.parameter.ICalParameters;
021import biweekly.property.ICalProperty;
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 * @see <a href="http://tools.ietf.org/html/rfc5545">RFC 5545</a>
068 */
069public class ICalWriter implements Closeable, Flushable {
070        private ScribeIndex index = new ScribeIndex();
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 scribe. Can also be used to override
234         * the scribe of a standard property (such as DTSTART). Calling this method
235         * is the same as calling:
236         * </p>
237         * <p>
238         * {@code getScribeIndex().register(scribe)}.
239         * </p>
240         * @param scribe the scribe to register
241         */
242        public void registerScribe(ICalPropertyScribe<? extends ICalProperty> scribe) {
243                index.register(scribe);
244        }
245
246        /**
247         * <p>
248         * Registers an experimental component scribe. Can also be used to override
249         * the scribe of a standard component (such as VEVENT). Calling this method
250         * is the same as calling:
251         * </p>
252         * <p>
253         * {@code getScribeIndex().register(scribe)}.
254         * </p>
255         * @param scribe the scribe to register
256         */
257        public void registerScribe(ICalComponentScribe<? extends ICalComponent> scribe) {
258                index.register(scribe);
259        }
260
261        /**
262         * Gets the object that manages the component/property scribes.
263         * @return the scribe index
264         */
265        public ScribeIndex getScribeIndex() {
266                return index;
267        }
268
269        /**
270         * Sets the object that manages the component/property scribes.
271         * @param scribe the scribe index
272         */
273        public void setScribeIndex(ScribeIndex scribe) {
274                this.index = scribe;
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 scribe class for a component or
281         * property object cannot be found (only happens when an experimental
282         * property/component scribe is not registered with the
283         * {@code registerScribe} method.)
284         * @throws IOException if there's a problem writing to the data stream
285         */
286        public void write(ICalendar ical) throws IOException {
287                index.hasScribesFor(ical);
288                writeComponent(ical);
289        }
290
291        /**
292         * Writes a component to the data stream.
293         * @param component the component to write
294         * @throws IOException if there's a problem writing to the data stream
295         */
296        @SuppressWarnings({ "rawtypes", "unchecked" })
297        private void writeComponent(ICalComponent component) throws IOException {
298                ICalComponentScribe componentScribe = index.getComponentScribe(component);
299                writer.writeBeginComponent(componentScribe.getComponentName());
300
301                for (Object propertyObj : componentScribe.getProperties(component)) {
302                        ICalProperty property = (ICalProperty) propertyObj;
303                        ICalPropertyScribe propertyScribe = index.getPropertyScribe(property);
304
305                        //marshal property
306                        ICalParameters parameters;
307                        String value;
308                        try {
309                                parameters = propertyScribe.prepareParameters(property);
310                                value = propertyScribe.writeText(property);
311                        } catch (SkipMeException e) {
312                                continue;
313                        }
314
315                        //set the data type
316                        ICalDataType dataType = propertyScribe.dataType(property);
317                        if (dataType != null && dataType != propertyScribe.getDefaultDataType()) {
318                                //only add a VALUE parameter if the data type is (1) not "unknown" and (2) different from the property's default data type
319                                parameters.setValue(dataType);
320                        }
321
322                        //write property to data stream
323                        writer.writeProperty(propertyScribe.getPropertyName(), parameters, value);
324                }
325
326                for (Object subComponentObj : componentScribe.getComponents(component)) {
327                        ICalComponent subComponent = (ICalComponent) subComponentObj;
328                        writeComponent(subComponent);
329                }
330
331                writer.writeEndComponent(componentScribe.getComponentName());
332        }
333
334        /**
335         * Flushes the stream.
336         * @throws IOException if there's a problem flushing the stream
337         */
338        public void flush() throws IOException {
339                writer.flush();
340        }
341
342        /**
343         * Closes the underlying {@link Writer} object.
344         */
345        public void close() throws IOException {
346                writer.close();
347        }
348}