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 * List&lt;ICalendar&gt; icals = ... 
060 * OutputStream out = ...
061 * JCalWriter jcalWriter = new JCalWriter(out);
062 * for (ICalendar ical : icals){
063 *   jcalWriter.write(ical);
064 * }
065 * jcalWriter.close();
066 * </pre>
067 * 
068 * </p>
069 * 
070 * <p>
071 * <b>Changing the timezone settings:</b>
072 * 
073 * <pre class="brush:java">
074 * JCalWriter writer = new JCalWriter(...);
075 * 
076 * //format all date/time values in a specific timezone instead of UTC
077 * //note: this makes an HTTP call to the "tzurl.org" website
078 * writer.getTimezoneInfo().setDefaultTimeZone(TimeZone.getDefault());
079 * 
080 * //format the value of a particular date/time property in a specific timezone instead of UTC
081 * //note: this makes an HTTP call to the "tzurl.org" website
082 * DateStart dtstart = ...
083 * writer.getTimezoneInfo().setTimeZone(dtstart, TimeZone.getDefault());
084 * 
085 * //generate Outlook-friendly VTIMEZONE components:
086 * writer.getTimezoneInfo().setGenerator(new TzUrlDotOrgGenerator(true));
087 * </pre>
088 * 
089 * </p>
090 * @author Michael Angstadt
091 * @see <a href="http://tools.ietf.org/html/rfc7265">RFC 7265</a>
092 */
093public class JCalWriter extends StreamWriter implements Flushable {
094        private final JCalRawWriter writer;
095        private final ICalVersion targetVersion = ICalVersion.V2_0;
096
097        /**
098         * Creates a jCal writer that writes to an output stream.
099         * @param outputStream the output stream to write to
100         */
101        public JCalWriter(OutputStream outputStream) {
102                this(utf8Writer(outputStream));
103        }
104
105        /**
106         * Creates a jCal writer that writes to an output stream.
107         * @param outputStream the output stream to write to
108         * @param wrapInArray true to wrap all iCalendar objects in a parent array,
109         * false not to (useful when writing more than one iCalendar object)
110         */
111        public JCalWriter(OutputStream outputStream, boolean wrapInArray) {
112                this(utf8Writer(outputStream), wrapInArray);
113        }
114
115        /**
116         * Creates a jCal writer that writes to a file.
117         * @param file the file to write to
118         * @throws IOException if the file cannot be written to
119         */
120        public JCalWriter(File file) throws IOException {
121                this(utf8Writer(file));
122        }
123
124        /**
125         * Creates a jCal writer that writes to a file.
126         * @param file the file to write to
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         * Creates a jCal writer that writes to a writer.
137         * @param writer the writer to the data stream
138         */
139        public JCalWriter(Writer writer) {
140                this(writer, false);
141        }
142
143        /**
144         * Creates a jCal writer that writes to a writer.
145         * @param writer the writer to the data stream
146         * @param wrapInArray true to wrap all iCalendar objects in a parent array,
147         * false not to (useful when writing more than one iCalendar object)
148         */
149        public JCalWriter(Writer writer, boolean wrapInArray) {
150                this.writer = new JCalRawWriter(writer, wrapInArray);
151        }
152
153        /**
154         * Gets whether or not the JSON will be pretty-printed.
155         * @return true if it will be pretty-printed, false if not (defaults to
156         * false)
157         */
158        public boolean isIndent() {
159                return writer.isIndent();
160        }
161
162        /**
163         * Sets whether or not to pretty-print the JSON.
164         * @param indent true to pretty-print it, false not to (defaults to false)
165         */
166        public void setIndent(boolean indent) {
167                writer.setIndent(indent);
168        }
169
170        @Override
171        protected void _write(ICalendar ical) throws IOException {
172                writeComponent(ical);
173        }
174
175        @Override
176        protected ICalVersion getTargetVersion() {
177                return targetVersion;
178        }
179
180        /**
181         * Writes a component to the data stream.
182         * @param component the component to write
183         * @throws IllegalArgumentException if the scribe class for a component or
184         * property object cannot be found (only happens when an experimental
185         * property/component scribe is not registered with the
186         * {@code registerScribe} method.)
187         * @throws IOException if there's a problem writing to the data stream
188         */
189        @SuppressWarnings({ "rawtypes", "unchecked" })
190        private void writeComponent(ICalComponent component) throws IOException {
191                ICalComponentScribe componentScribe = index.getComponentScribe(component);
192                writer.writeStartComponent(componentScribe.getComponentName().toLowerCase());
193
194                List propertyObjs = componentScribe.getProperties(component);
195                if (component instanceof ICalendar && component.getProperty(Version.class) == null) {
196                        propertyObjs.add(0, new Version(targetVersion));
197                }
198
199                //write properties
200                for (Object propertyObj : propertyObjs) {
201                        context.setParent(component); //set parent here incase a scribe resets the parent
202                        ICalProperty property = (ICalProperty) propertyObj;
203                        ICalPropertyScribe propertyScribe = index.getPropertyScribe(property);
204
205                        //marshal property
206                        ICalParameters parameters;
207                        JCalValue value;
208                        try {
209                                parameters = propertyScribe.prepareParameters(property, context);
210                                value = propertyScribe.writeJson(property, context);
211                        } catch (SkipMeException e) {
212                                continue;
213                        }
214
215                        //write property
216                        String propertyName = propertyScribe.getPropertyName().toLowerCase();
217                        ICalDataType dataType = propertyScribe.dataType(property, targetVersion);
218                        writer.writeProperty(propertyName, parameters, dataType, value);
219                }
220
221                //write sub-components
222                Collection subComponents = componentScribe.getComponents(component);
223                if (component instanceof ICalendar) {
224                        //add the VTIMEZONE components that were auto-generated by TimezoneOptions
225                        Collection<VTimezone> tzs = tzinfo.getComponents();
226                        for (VTimezone tz : tzs) {
227                                if (!subComponents.contains(tz)) {
228                                        subComponents.add(tz);
229                                }
230                        }
231                }
232                for (Object subComponentObj : subComponents) {
233                        ICalComponent subComponent = (ICalComponent) subComponentObj;
234                        writeComponent(subComponent);
235                }
236
237                writer.writeEndComponent();
238        }
239
240        /**
241         * Flushes the stream.
242         * @throws IOException if there's a problem flushing the stream
243         */
244        public void flush() throws IOException {
245                writer.flush();
246        }
247
248        /**
249         * Finishes writing the JSON document and closes the underlying
250         * {@link Writer} object.
251         * @throws IOException if there's a problem closing the stream
252         */
253        public void close() throws IOException {
254                writer.close();
255        }
256
257        /**
258         * Finishes writing the JSON document so that it is syntactically correct.
259         * No more iCalendar objects can be written once this method is called.
260         * @throws IOException if there's a problem writing to the data stream
261         */
262        public void closeJsonStream() throws IOException {
263                writer.closeJsonStream();
264        }
265}