001package biweekly.io.json;
002
003import static biweekly.util.IOUtils.utf8Writer;
004
005import java.io.File;
006import java.io.Flushable;
007import java.io.IOException;
008import java.io.OutputStream;
009import java.io.Writer;
010import java.util.Collection;
011import java.util.List;
012
013import biweekly.ICalDataType;
014import biweekly.ICalVersion;
015import biweekly.ICalendar;
016import biweekly.component.ICalComponent;
017import biweekly.component.VTimezone;
018import biweekly.io.SkipMeException;
019import biweekly.io.StreamWriter;
020import biweekly.io.scribe.component.ICalComponentScribe;
021import biweekly.io.scribe.property.ICalPropertyScribe;
022import biweekly.parameter.ICalParameters;
023import biweekly.property.ICalProperty;
024import biweekly.property.Version;
025
026/*
027 Copyright (c) 2013-2015, Michael Angstadt
028 All rights reserved.
029
030 Redistribution and use in source and binary forms, with or without
031 modification, are permitted provided that the following conditions are met: 
032
033 1. Redistributions of source code must retain the above copyright notice, this
034 list of conditions and the following disclaimer. 
035 2. Redistributions in binary form must reproduce the above copyright notice,
036 this list of conditions and the following disclaimer in the documentation
037 and/or other materials provided with the distribution. 
038
039 THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
040 ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
041 WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
042 DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR
043 ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
044 (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
045 LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
046 ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
047 (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
048 SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
049 */
050
051/**
052 * <p>
053 * Writes {@link ICalendar} objects to a JSON data stream (jCal).
054 * </p>
055 * <p>
056 * <b>Example:</b>
057 * 
058 * <pre class="brush:java">
059 * ICalendar ical1 = ...
060 * ICalendar ical2 = ...
061 * File file = new File("icals.json");
062 * JCalWriter writer = null;
063 * try {
064 *   writer = new JCalWriter(file);
065 *   writer.write(ical1);
066 *   writer.write(ical2);
067 * } finally {
068 *   if (writer != null) writer.close();
069 * }
070 * </pre>
071 * 
072 * </p>
073 * 
074 * <p>
075 * <b>Changing the timezone settings:</b>
076 * 
077 * <pre class="brush:java">
078 * JCalWriter writer = new JCalWriter(...);
079 * 
080 * //format all date/time values in a specific timezone instead of UTC
081 * //note: this makes an HTTP call to the "tzurl.org" website
082 * writer.getTimezoneInfo().setDefaultTimeZone(TimeZone.getDefault());
083 * 
084 * //format the value of a particular date/time property in a specific timezone instead of UTC
085 * //note: this makes an HTTP call to the "tzurl.org" website
086 * DateStart dtstart = ...
087 * writer.getTimezoneInfo().setTimeZone(dtstart, TimeZone.getDefault());
088 * 
089 * //generate Outlook-friendly VTIMEZONE components:
090 * writer.getTimezoneInfo().setGenerator(new TzUrlDotOrgGenerator(true));
091 * </pre>
092 * 
093 * </p>
094 * @author Michael Angstadt
095 * @see <a href="http://tools.ietf.org/html/rfc7265">RFC 7265</a>
096 */
097public class JCalWriter extends StreamWriter implements Flushable {
098        private final JCalRawWriter writer;
099        private final ICalVersion targetVersion = ICalVersion.V2_0;
100
101        /**
102         * @param out the output stream to write to (UTF-8 encoding will be used)
103         */
104        public JCalWriter(OutputStream out) {
105                this(utf8Writer(out));
106        }
107
108        /**
109         * @param out the output stream to write to (UTF-8 encoding will be used)
110         * @param wrapInArray true to wrap all iCalendar objects in a parent array,
111         * false not to (useful when writing more than one iCalendar object)
112         */
113        public JCalWriter(OutputStream out, boolean wrapInArray) {
114                this(utf8Writer(out), wrapInArray);
115        }
116
117        /**
118         * @param file the file to write to (UTF-8 encoding will be used)
119         * @throws IOException if the file cannot be written to
120         */
121        public JCalWriter(File file) throws IOException {
122                this(utf8Writer(file));
123        }
124
125        /**
126         * @param file the file to write to (UTF-8 encoding will be used)
127         * @param wrapInArray true to wrap all iCalendar objects in a parent array,
128         * false not to (useful when writing more than one iCalendar object)
129         * @throws IOException if the file cannot be written to
130         */
131        public JCalWriter(File file, boolean wrapInArray) throws IOException {
132                this(utf8Writer(file), wrapInArray);
133        }
134
135        /**
136         * @param writer the writer to write to
137         */
138        public JCalWriter(Writer writer) {
139                this(writer, false);
140        }
141
142        /**
143         * @param writer the writer to write to
144         * @param wrapInArray true to wrap all iCalendar objects in a parent array,
145         * false not to (useful when writing more than one iCalendar object)
146         */
147        public JCalWriter(Writer writer, boolean wrapInArray) {
148                this.writer = new JCalRawWriter(writer, wrapInArray);
149        }
150
151        /**
152         * Gets whether or not the JSON will be pretty-printed.
153         * @return true if it will be pretty-printed, false if not (defaults to
154         * false)
155         */
156        public boolean isIndent() {
157                return writer.isIndent();
158        }
159
160        /**
161         * Sets whether or not to pretty-print the JSON.
162         * @param indent true to pretty-print it, false not to (defaults to false)
163         */
164        public void setIndent(boolean indent) {
165                writer.setIndent(indent);
166        }
167
168        @Override
169        protected void _write(ICalendar ical) throws IOException {
170                writeComponent(ical);
171        }
172
173        @Override
174        protected ICalVersion getTargetVersion() {
175                return targetVersion;
176        }
177
178        /**
179         * Writes a component to the data stream.
180         * @param component the component to write
181         * @throws IllegalArgumentException if the scribe class for a component or
182         * property object cannot be found (only happens when an experimental
183         * property/component scribe is not registered with the
184         * {@code registerScribe} method.)
185         * @throws IOException if there's a problem writing to the data stream
186         */
187        @SuppressWarnings({ "rawtypes", "unchecked" })
188        private void writeComponent(ICalComponent component) throws IOException {
189                ICalComponentScribe componentScribe = index.getComponentScribe(component);
190                writer.writeStartComponent(componentScribe.getComponentName().toLowerCase());
191
192                List propertyObjs = componentScribe.getProperties(component);
193                if (component instanceof ICalendar && component.getProperty(Version.class) == null) {
194                        propertyObjs.add(0, new Version(targetVersion));
195                }
196
197                //write properties
198                for (Object propertyObj : propertyObjs) {
199                        context.setParent(component); //set parent here incase a scribe resets the parent
200                        ICalProperty property = (ICalProperty) propertyObj;
201                        ICalPropertyScribe propertyScribe = index.getPropertyScribe(property);
202
203                        //marshal property
204                        ICalParameters parameters;
205                        JCalValue value;
206                        try {
207                                parameters = propertyScribe.prepareParameters(property, context);
208                                value = propertyScribe.writeJson(property, context);
209                        } catch (SkipMeException e) {
210                                continue;
211                        }
212
213                        //write property
214                        String propertyName = propertyScribe.getPropertyName().toLowerCase();
215                        ICalDataType dataType = propertyScribe.dataType(property, targetVersion);
216                        writer.writeProperty(propertyName, parameters, dataType, value);
217                }
218
219                //write sub-components
220                Collection subComponents = componentScribe.getComponents(component);
221                if (component instanceof ICalendar) {
222                        //add the VTIMEZONE components that were auto-generated by TimezoneOptions
223                        Collection<VTimezone> tzs = tzinfo.getComponents();
224                        for (VTimezone tz : tzs) {
225                                if (!subComponents.contains(tz)) {
226                                        subComponents.add(tz);
227                                }
228                        }
229                }
230                for (Object subComponentObj : subComponents) {
231                        ICalComponent subComponent = (ICalComponent) subComponentObj;
232                        writeComponent(subComponent);
233                }
234
235                writer.writeEndComponent();
236        }
237
238        /**
239         * Flushes the stream.
240         * @throws IOException if there's a problem flushing the stream
241         */
242        public void flush() throws IOException {
243                writer.flush();
244        }
245
246        /**
247         * Finishes writing the JSON document and closes the underlying
248         * {@link Writer} object.
249         * @throws IOException if there's a problem closing the stream
250         */
251        public void close() throws IOException {
252                writer.close();
253        }
254
255        /**
256         * Finishes writing the JSON document so that it is syntactically correct.
257         * No more iCalendar objects can be written once this method is called.
258         * @throws IOException if there's a problem writing to the data stream
259         */
260        public void closeJsonStream() throws IOException {
261                writer.closeJsonStream();
262        }
263}