001package biweekly.io.text;
002
003import static biweekly.io.DataModelConverter.convert;
004import static biweekly.util.IOUtils.utf8Writer;
005
006import java.io.File;
007import java.io.FileWriter;
008import java.io.Flushable;
009import java.io.IOException;
010import java.io.OutputStream;
011import java.io.OutputStreamWriter;
012import java.io.Writer;
013import java.util.Collection;
014import java.util.List;
015
016import biweekly.ICalDataType;
017import biweekly.ICalVersion;
018import biweekly.ICalendar;
019import biweekly.component.ICalComponent;
020import biweekly.component.VAlarm;
021import biweekly.component.VTimezone;
022import biweekly.io.DataModelConverter.VCalTimezoneProperties;
023import biweekly.io.SkipMeException;
024import biweekly.io.StreamWriter;
025import biweekly.io.scribe.component.ICalComponentScribe;
026import biweekly.io.scribe.property.ICalPropertyScribe;
027import biweekly.parameter.ICalParameters;
028import biweekly.property.Attendee;
029import biweekly.property.Created;
030import biweekly.property.DateTimeStamp;
031import biweekly.property.Daylight;
032import biweekly.property.ICalProperty;
033import biweekly.property.Organizer;
034import biweekly.property.Timezone;
035import biweekly.property.VCalAlarmProperty;
036import biweekly.property.Version;
037
038/*
039 Copyright (c) 2013-2015, Michael Angstadt
040 All rights reserved.
041
042 Redistribution and use in source and binary forms, with or without
043 modification, are permitted provided that the following conditions are met: 
044
045 1. Redistributions of source code must retain the above copyright notice, this
046 list of conditions and the following disclaimer. 
047 2. Redistributions in binary form must reproduce the above copyright notice,
048 this list of conditions and the following disclaimer in the documentation
049 and/or other materials provided with the distribution. 
050
051 THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
052 ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
053 WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
054 DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR
055 ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
056 (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
057 LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
058 ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
059 (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
060 SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
061 */
062
063/**
064 * <p>
065 * Writes {@link ICalendar} objects to an iCalendar data stream.
066 * </p>
067 * <p>
068 * <b>Example:</b>
069 * 
070 * <pre class="brush:java">
071 * List&lt;ICalendar&gt; icals = ... 
072 * OutputStream out = ...
073 * ICalWriter icalWriter = new ICalWriter(out, ICalVersion.V2_0);
074 * for (ICalendar ical : icals){
075 *   icalWriter.write(ical);
076 * }
077 * icalWriter.close();
078 * </pre>
079 * 
080 * </p>
081 * 
082 * <p>
083 * <b>Changing the timezone settings:</b>
084 * 
085 * <pre class="brush:java">
086 * ICalWriter writer = new ICalWriter(...);
087 * 
088 * //format all date/time values in a specific timezone instead of UTC
089 * //note: this makes an HTTP call to "http://tzurl.org"
090 * writer.getTimezoneInfo().setDefaultTimeZone(TimeZone.getDefault());
091 * 
092 * //format the value of a single date/time property in a specific timezone instead of UTC
093 * //note: this makes an HTTP call to "http://tzurl.org"
094 * DateStart dtstart = ...
095 * writer.getTimezoneInfo().setTimeZone(dtstart, TimeZone.getDefault());
096 * 
097 * //generate Outlook-friendly VTIMEZONE components:
098 * writer.getTimezoneInfo().setGenerator(new TzUrlDotOrgGenerator(true));
099 * </pre>
100 * 
101 * </p>
102 * 
103 * <p>
104 * <b>Changing the line folding settings:</b>
105 * 
106 * <pre class="brush:java">
107 * ICalWriter writer = new ICalWriter(...);
108 * 
109 * //disable line folding
110 * writer.getRawWriter().getFoldedLineWriter().setLineLength(null);
111 * 
112 * //set line length (defaults to 75)
113 * writer.getRawWriter().getFoldedLineWriter().setLineLength(50);
114 * 
115 * //change folded line indent string (defaults to one space character)
116 * writer.getRawWriter().getFoldedLineWriter().setIndent("\t");
117 * 
118 * //change newline character (defaults to CRLF)
119 * writer.getRawWriter().getFoldedLineWriter().setNewline("**");
120 * </pre>
121 * 
122 * </p>
123 * @author Michael Angstadt
124 * @see <a href="http://tools.ietf.org/html/rfc5545">RFC 5545</a>
125 */
126public class ICalWriter extends StreamWriter implements Flushable {
127        private final ICalRawWriter writer;
128
129        /**
130         * Creates an iCalendar writer that writes to an output stream.
131         * @param outputStream the output stream to write to
132         * @param version the iCalendar version to adhere to
133         */
134        public ICalWriter(OutputStream outputStream, ICalVersion version) {
135                this((version == ICalVersion.V1_0) ? new OutputStreamWriter(outputStream) : utf8Writer(outputStream), version);
136        }
137
138        /**
139         * Creates an iCalendar writer that writes to a file.
140         * @param file the file to write to
141         * @param version the iCalendar version to adhere to
142         * @throws IOException if the file cannot be written to
143         */
144        public ICalWriter(File file, ICalVersion version) throws IOException {
145                this(file, false, version);
146        }
147
148        /**
149         * Creates an iCalendar writer that writes to a file.
150         * @param file the file to write to
151         * @param version the iCalendar version to adhere to
152         * @param append true to append to the end of the file, false to overwrite
153         * it
154         * @throws IOException if the file cannot be written to
155         */
156        public ICalWriter(File file, boolean append, ICalVersion version) throws IOException {
157                this((version == ICalVersion.V1_0) ? new FileWriter(file, append) : utf8Writer(file, append), version);
158        }
159
160        /**
161         * Creates an iCalendar writer that writes to a writer.
162         * @param writer the output stream to write to
163         * @param version the iCalendar version to adhere to
164         */
165        public ICalWriter(Writer writer, ICalVersion version) {
166                this.writer = new ICalRawWriter(writer, version);
167        }
168
169        /**
170         * Gets the writer object that is used internally to write to the output
171         * stream.
172         * @return the raw writer
173         */
174        public ICalRawWriter getRawWriter() {
175                return writer;
176        }
177
178        /**
179         * Gets the version that the written iCalendar objects will adhere to.
180         * @return the iCalendar version
181         */
182        @Override
183        public ICalVersion getTargetVersion() {
184                return writer.getVersion();
185        }
186
187        /**
188         * Sets the version that the written iCalendar objects will adhere to.
189         * @param targetVersion the iCalendar version
190         */
191        public void setTargetVersion(ICalVersion targetVersion) {
192                writer.setVersion(targetVersion);
193        }
194
195        /**
196         * <p>
197         * Gets whether the writer will apply circumflex accent encoding on
198         * parameter values (disabled by default). This escaping mechanism allows
199         * for newlines and double quotes to be included in parameter values.
200         * </p>
201         * 
202         * <p>
203         * When disabled, the writer will replace newlines with spaces and double
204         * quotes with single quotes.
205         * </p>
206         * @return true if circumflex accent encoding is enabled, false if not
207         * @see ICalRawWriter#isCaretEncodingEnabled()
208         */
209        public boolean isCaretEncodingEnabled() {
210                return writer.isCaretEncodingEnabled();
211        }
212
213        /**
214         * <p>
215         * Sets whether the writer will apply circumflex accent encoding on
216         * parameter values (disabled by default). This escaping mechanism allows
217         * for newlines and double quotes to be included in parameter values.
218         * </p>
219         * 
220         * <p>
221         * When disabled, the writer will replace newlines with spaces and double
222         * quotes with single quotes.
223         * </p>
224         * @param enable true to use circumflex accent encoding, false not to
225         * @see ICalRawWriter#setCaretEncodingEnabled(boolean)
226         */
227        public void setCaretEncodingEnabled(boolean enable) {
228                writer.setCaretEncodingEnabled(enable);
229        }
230
231        @Override
232        protected void _write(ICalendar ical) throws IOException {
233                writeComponent(ical, null);
234        }
235
236        /**
237         * Writes a component to the data stream.
238         * @param component the component to write
239         * @param parent the parent component
240         * @throws IOException if there's a problem writing to the data stream
241         */
242        @SuppressWarnings({ "rawtypes", "unchecked" })
243        private void writeComponent(ICalComponent component, ICalComponent parent) throws IOException {
244                switch (writer.getVersion()) {
245                case V1_0:
246                        //VALARM component => vCal alarm property
247                        if (component instanceof VAlarm) {
248                                VAlarm valarm = (VAlarm) component;
249                                VCalAlarmProperty vcalAlarm = convert(valarm, component);
250                                if (vcalAlarm != null) {
251                                        writeProperty(vcalAlarm);
252                                        return;
253                                }
254                        }
255
256                        break;
257
258                default:
259                        //empty
260                        break;
261                }
262
263                boolean inICalendar = component instanceof ICalendar;
264                boolean inVCalRoot = inICalendar && getTargetVersion() == ICalVersion.V1_0;
265                boolean inICalRoot = inICalendar && getTargetVersion() != ICalVersion.V1_0;
266
267                ICalComponentScribe componentScribe = index.getComponentScribe(component);
268                writer.writeBeginComponent(componentScribe.getComponentName());
269
270                List propertyObjs = componentScribe.getProperties(component);
271                if (inICalendar && component.getProperty(Version.class) == null) {
272                        propertyObjs.add(0, new Version(getTargetVersion()));
273                }
274
275                for (Object propertyObj : propertyObjs) {
276                        context.setParent(component); //set parent here incase a scribe resets the parent
277                        ICalProperty property = (ICalProperty) propertyObj;
278                        writeProperty(property);
279                }
280
281                Collection subComponents = componentScribe.getComponents(component);
282                if (inICalRoot) {
283                        //add the VTIMEZONE components
284                        Collection<VTimezone> timezones = tzinfo.getComponents();
285                        for (VTimezone timezone : timezones) {
286                                if (!subComponents.contains(timezone)) {
287                                        subComponents.add(timezone);
288                                }
289                        }
290                }
291
292                for (Object subComponentObj : subComponents) {
293                        ICalComponent subComponent = (ICalComponent) subComponentObj;
294                        writeComponent(subComponent, component);
295                }
296
297                if (inVCalRoot) {
298                        Collection<VTimezone> timezones = tzinfo.getComponents();
299                        if (!timezones.isEmpty()) {
300                                VTimezone timezone = timezones.iterator().next();
301                                VCalTimezoneProperties props = convert(timezone, context.getDates());
302
303                                Timezone tz = props.getTz();
304                                if (tz != null) {
305                                        writeProperty(tz);
306                                }
307                                for (Daylight daylight : props.getDaylights()) {
308                                        writeProperty(daylight);
309                                }
310                        }
311                }
312
313                writer.writeEndComponent(componentScribe.getComponentName());
314        }
315
316        @SuppressWarnings({ "rawtypes", "unchecked" })
317        private void writeProperty(ICalProperty property) throws IOException {
318                switch (writer.getVersion()) {
319                case V1_0:
320                        //ORGANIZER property => ATTENDEE with role of "organizer" 
321                        if (property instanceof Organizer) {
322                                Organizer organizer = (Organizer) property;
323                                Attendee attendee = convert(organizer);
324                                writeProperty(attendee);
325                                return;
326                        }
327                        if (property instanceof DateTimeStamp) {
328                                //do not write this property
329                                return;
330                        }
331                        break;
332
333                default:
334                        //empty
335                        break;
336                }
337
338                ICalPropertyScribe scribe = index.getPropertyScribe(property);
339
340                //marshal property
341                String value;
342                try {
343                        value = scribe.writeText(property, context);
344                } catch (SkipMeException e) {
345                        return;
346                }
347
348                //get parameters
349                ICalParameters parameters = scribe.prepareParameters(property, context);
350
351                //set the data type
352                ICalDataType dataType = scribe.dataType(property, writer.getVersion());
353                if (dataType != null && dataType != scribe.defaultDataType(writer.getVersion())) {
354                        //only add a VALUE parameter if the data type is (1) not "unknown" and (2) different from the property's default data type
355                        parameters = new ICalParameters(parameters);
356                        parameters.setValue(dataType);
357                }
358
359                //get the property name
360                String propertyName;
361                if (writer.getVersion() == ICalVersion.V1_0 && property instanceof Created) {
362                        //the vCal DCREATED property is the same as the iCal CREATED property
363                        propertyName = "DCREATED";
364                } else {
365                        propertyName = scribe.getPropertyName();
366                }
367
368                //write property to data stream
369                writer.writeProperty(propertyName, parameters, value);
370        }
371
372        /**
373         * Flushes the stream.
374         * @throws IOException if there's a problem flushing the stream
375         */
376        public void flush() throws IOException {
377                writer.flush();
378        }
379
380        /**
381         * Closes the underlying {@link Writer} object.
382         */
383        public void close() throws IOException {
384                writer.close();
385        }
386}