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