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 a plain-text iCalendar data stream.
066 * </p>
067 * <p>
068 * <b>Example:</b>
069 * 
070 * <pre class="brush:java">
071 * ICalendar ical1 = ...
072 * ICalendar ical2 = ...
073 * File file = new File("icals.ics");
074 * ICalWriter writer = null;
075 * try {
076 *   writer = new ICalWriter(file, ICalVersion.V2_0);
077 *   writer.write(ical1);
078 *   writer.write(ical2);
079 * } finally {
080 *   if (writer != null) writer.close();
081 * }
082 * </pre>
083 * 
084 * </p>
085 * 
086 * <p>
087 * <b>Changing the timezone settings:</b>
088 * 
089 * <pre class="brush:java">
090 * ICalWriter writer = new ICalWriter(...);
091 * 
092 * //format all date/time values in a specific timezone instead of UTC
093 * //note: this makes an HTTP call to "http://tzurl.org"
094 * writer.getTimezoneInfo().setDefaultTimeZone(TimeZone.getDefault());
095 * 
096 * //format the value of a single date/time property in a specific timezone instead of UTC
097 * //note: this makes an HTTP call to "http://tzurl.org"
098 * DateStart dtstart = ...
099 * writer.getTimezoneInfo().setTimeZone(dtstart, TimeZone.getDefault());
100 * 
101 * //generate Outlook-friendly VTIMEZONE components:
102 * writer.getTimezoneInfo().setGenerator(new TzUrlDotOrgGenerator(true));
103 * </pre>
104 * 
105 * </p>
106 * 
107 * <p>
108 * <b>Changing the line folding settings:</b>
109 * 
110 * <pre class="brush:java">
111 * ICalWriter writer = new ICalWriter(...);
112 * 
113 * //disable line folding
114 * writer.getRawWriter().getFoldedLineWriter().setLineLength(null);
115 * 
116 * //set line length (defaults to 75)
117 * writer.getRawWriter().getFoldedLineWriter().setLineLength(50);
118 * 
119 * //change folded line indent string (defaults to one space character)
120 * writer.getRawWriter().getFoldedLineWriter().setIndent("\t");
121 * 
122 * //change newline character (defaults to CRLF)
123 * writer.getRawWriter().getFoldedLineWriter().setNewline("**");
124 * </pre>
125 * 
126 * </p>
127 * @author Michael Angstadt
128 * @see <a href="http://www.imc.org/pdi/pdiproddev.html">1.0 specs</a>
129 * @see <a href="https://tools.ietf.org/html/rfc2445">RFC 2445</a>
130 * @see <a href="http://tools.ietf.org/html/rfc5545">RFC 5545</a>
131 */
132public class ICalWriter extends StreamWriter implements Flushable {
133        private final ICalRawWriter writer;
134
135        /**
136         * @param out the output stream to write to
137         * @param version the iCalendar version to adhere to
138         */
139        public ICalWriter(OutputStream out, ICalVersion version) {
140                this((version == ICalVersion.V1_0) ? new OutputStreamWriter(out) : utf8Writer(out), version);
141        }
142
143        /**
144         * @param file the file to write to
145         * @param version the iCalendar version to adhere to
146         * @throws IOException if the file cannot be written to
147         */
148        public ICalWriter(File file, ICalVersion version) throws IOException {
149                this(file, false, version);
150        }
151
152        /**
153         * @param file the file to write to
154         * @param version the iCalendar version to adhere to
155         * @param append true to append to the end of the file, false to overwrite
156         * it
157         * @throws IOException if the file cannot be written to
158         */
159        public ICalWriter(File file, boolean append, ICalVersion version) throws IOException {
160                this((version == ICalVersion.V1_0) ? new FileWriter(file, append) : utf8Writer(file, append), version);
161        }
162
163        /**
164         * @param writer the writer to write to
165         * @param version the iCalendar version to adhere to
166         */
167        public ICalWriter(Writer writer, ICalVersion version) {
168                this.writer = new ICalRawWriter(writer, version);
169        }
170
171        /**
172         * Gets the writer object that is used internally to write to the output
173         * stream.
174         * @return the raw writer
175         */
176        public ICalRawWriter getRawWriter() {
177                return writer;
178        }
179
180        /**
181         * Gets the version that the written iCalendar objects will adhere to.
182         * @return the iCalendar version
183         */
184        @Override
185        public ICalVersion getTargetVersion() {
186                return writer.getVersion();
187        }
188
189        /**
190         * Sets the version that the written iCalendar objects will adhere to.
191         * @param targetVersion the iCalendar version
192         */
193        public void setTargetVersion(ICalVersion targetVersion) {
194                writer.setVersion(targetVersion);
195        }
196
197        /**
198         * <p>
199         * Gets 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         * @return true if circumflex accent encoding is enabled, false if not
209         * @see ICalRawWriter#isCaretEncodingEnabled()
210         */
211        public boolean isCaretEncodingEnabled() {
212                return writer.isCaretEncodingEnabled();
213        }
214
215        /**
216         * <p>
217         * Sets whether the writer will apply circumflex accent encoding on
218         * parameter values (disabled by default). This escaping mechanism allows
219         * for newlines and double quotes to be included in parameter values.
220         * </p>
221         * 
222         * <p>
223         * When disabled, the writer will replace newlines with spaces and double
224         * quotes with single quotes.
225         * </p>
226         * @param enable true to use circumflex accent encoding, false not to
227         * @see ICalRawWriter#setCaretEncodingEnabled(boolean)
228         */
229        public void setCaretEncodingEnabled(boolean enable) {
230                writer.setCaretEncodingEnabled(enable);
231        }
232
233        @Override
234        protected void _write(ICalendar ical) throws IOException {
235                writeComponent(ical, null);
236        }
237
238        /**
239         * Writes a component to the data stream.
240         * @param component the component to write
241         * @param parent the parent component
242         * @throws IOException if there's a problem writing to the data stream
243         */
244        @SuppressWarnings({ "rawtypes", "unchecked" })
245        private void writeComponent(ICalComponent component, ICalComponent parent) throws IOException {
246                switch (writer.getVersion()) {
247                case V1_0:
248                        //VALARM component => vCal alarm property
249                        if (component instanceof VAlarm) {
250                                VAlarm valarm = (VAlarm) component;
251                                VCalAlarmProperty vcalAlarm = convert(valarm, component);
252                                if (vcalAlarm != null) {
253                                        writeProperty(vcalAlarm);
254                                        return;
255                                }
256                        }
257
258                        break;
259
260                default:
261                        //empty
262                        break;
263                }
264
265                boolean inICalendar = component instanceof ICalendar;
266                boolean inVCalRoot = inICalendar && getTargetVersion() == ICalVersion.V1_0;
267                boolean inICalRoot = inICalendar && getTargetVersion() != ICalVersion.V1_0;
268
269                ICalComponentScribe componentScribe = index.getComponentScribe(component);
270                writer.writeBeginComponent(componentScribe.getComponentName());
271
272                List propertyObjs = componentScribe.getProperties(component);
273                if (inICalendar && component.getProperty(Version.class) == null) {
274                        propertyObjs.add(0, new Version(getTargetVersion()));
275                }
276
277                for (Object propertyObj : propertyObjs) {
278                        context.setParent(component); //set parent here incase a scribe resets the parent
279                        ICalProperty property = (ICalProperty) propertyObj;
280                        writeProperty(property);
281                }
282
283                Collection subComponents = componentScribe.getComponents(component);
284                if (inICalRoot) {
285                        //add the VTIMEZONE components
286                        Collection<VTimezone> timezones = tzinfo.getComponents();
287                        for (VTimezone timezone : timezones) {
288                                if (!subComponents.contains(timezone)) {
289                                        subComponents.add(timezone);
290                                }
291                        }
292                }
293
294                for (Object subComponentObj : subComponents) {
295                        ICalComponent subComponent = (ICalComponent) subComponentObj;
296                        writeComponent(subComponent, component);
297                }
298
299                if (inVCalRoot) {
300                        Collection<VTimezone> timezones = tzinfo.getComponents();
301                        if (!timezones.isEmpty()) {
302                                VTimezone timezone = timezones.iterator().next();
303                                VCalTimezoneProperties props = convert(timezone, context.getDates());
304
305                                Timezone tz = props.getTz();
306                                if (tz != null) {
307                                        writeProperty(tz);
308                                }
309                                for (Daylight daylight : props.getDaylights()) {
310                                        writeProperty(daylight);
311                                }
312                        }
313                }
314
315                writer.writeEndComponent(componentScribe.getComponentName());
316        }
317
318        @SuppressWarnings({ "rawtypes", "unchecked" })
319        private void writeProperty(ICalProperty property) throws IOException {
320                switch (writer.getVersion()) {
321                case V1_0:
322                        //ORGANIZER property => ATTENDEE with role of "organizer" 
323                        if (property instanceof Organizer) {
324                                Organizer organizer = (Organizer) property;
325                                Attendee attendee = convert(organizer);
326                                writeProperty(attendee);
327                                return;
328                        }
329                        if (property instanceof DateTimeStamp) {
330                                //do not write this property
331                                return;
332                        }
333                        break;
334
335                default:
336                        //empty
337                        break;
338                }
339
340                ICalPropertyScribe scribe = index.getPropertyScribe(property);
341
342                //marshal property
343                String value;
344                try {
345                        value = scribe.writeText(property, context);
346                } catch (SkipMeException e) {
347                        return;
348                }
349
350                //get parameters
351                ICalParameters parameters = scribe.prepareParameters(property, context);
352
353                /*
354                 * Set the property's data type.
355                 * 
356                 * Only add a VALUE parameter if the data type is:
357                 * (1) not "unknown"
358                 * (2) different from the property's default data type
359                 */
360                ICalDataType dataType = scribe.dataType(property, writer.getVersion());
361                if (dataType != null && dataType != scribe.defaultDataType(writer.getVersion())) {
362                        parameters = new ICalParameters(parameters);
363                        parameters.setValue(dataType);
364                }
365
366                //get the property name
367                String propertyName;
368                if (writer.getVersion() == ICalVersion.V1_0 && property instanceof Created) {
369                        //the vCal DCREATED property is the same as the iCal CREATED property
370                        propertyName = "DCREATED";
371                } else {
372                        propertyName = scribe.getPropertyName();
373                }
374
375                //write property to data stream
376                writer.writeProperty(propertyName, parameters, value);
377        }
378
379        /**
380         * Flushes the stream.
381         * @throws IOException if there's a problem flushing the stream
382         */
383        public void flush() throws IOException {
384                writer.flush();
385        }
386
387        /**
388         * Closes the underlying {@link Writer} object.
389         */
390        public void close() throws IOException {
391                writer.close();
392        }
393}