001package biweekly.io.json;
002
003import static biweekly.util.StringUtils.NEWLINE;
004
005import java.io.Closeable;
006import java.io.Flushable;
007import java.io.IOException;
008import java.io.Writer;
009import java.util.LinkedList;
010import java.util.List;
011import java.util.Map;
012
013import biweekly.ICalDataType;
014import biweekly.parameter.ICalParameters;
015
016import com.fasterxml.jackson.core.JsonFactory;
017import com.fasterxml.jackson.core.JsonGenerator;
018import com.fasterxml.jackson.core.JsonGenerator.Feature;
019
020/*
021 Copyright (c) 2013-2015, Michael Angstadt
022 All rights reserved.
023
024 Redistribution and use in source and binary forms, with or without
025 modification, are permitted provided that the following conditions are met: 
026
027 1. Redistributions of source code must retain the above copyright notice, this
028 list of conditions and the following disclaimer. 
029 2. Redistributions in binary form must reproduce the above copyright notice,
030 this list of conditions and the following disclaimer in the documentation
031 and/or other materials provided with the distribution. 
032
033 THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
034 ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
035 WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
036 DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR
037 ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
038 (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
039 LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
040 ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
041 (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
042 SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
043 */
044
045/**
046 * Writes data to an iCalendar JSON data stream (jCal).
047 * @author Michael Angstadt
048 * @see <a href="http://tools.ietf.org/html/rfc7265">RFC 7265</a>
049 */
050public class JCalRawWriter implements Closeable, Flushable {
051        private final Writer writer;
052        private final boolean wrapInArray;
053        private final LinkedList<Info> stack = new LinkedList<Info>();
054        private JsonGenerator generator;
055        private boolean indent = false;
056        private boolean componentEnded = false;
057
058        /**
059         * @param writer the writer to wrap
060         * @param wrapInArray true to wrap everything in an array, false not to
061         * (useful when writing more than one iCalendar object)
062         */
063        public JCalRawWriter(Writer writer, boolean wrapInArray) {
064                this.writer = writer;
065                this.wrapInArray = wrapInArray;
066        }
067
068        /**
069         * Gets whether or not the JSON will be pretty-printed.
070         * @return true if it will be pretty-printed, false if not (defaults to
071         * false)
072         */
073        public boolean isIndent() {
074                return indent;
075        }
076
077        /**
078         * Sets whether or not to pretty-print the JSON.
079         * @param indent true to pretty-print it, false not to (defaults to false)
080         */
081        public void setIndent(boolean indent) {
082                this.indent = indent;
083        }
084
085        /**
086         * Writes the beginning of a new component array.
087         * @param componentName the component name (e.g. "vevent")
088         * @throws IOException if there's an I/O problem
089         */
090        public void writeStartComponent(String componentName) throws IOException {
091                if (generator == null) {
092                        init();
093                }
094
095                componentEnded = false;
096
097                if (!stack.isEmpty()) {
098                        Info parent = stack.getLast();
099                        if (!parent.wroteEndPropertiesArray) {
100                                generator.writeEndArray();
101                                parent.wroteEndPropertiesArray = true;
102                        }
103                        if (!parent.wroteStartSubComponentsArray) {
104                                generator.writeStartArray();
105                                parent.wroteStartSubComponentsArray = true;
106                        }
107                }
108
109                generator.writeStartArray();
110                indent(stack.size() * 2);
111                generator.writeString(componentName);
112                generator.writeStartArray(); //start properties array
113
114                stack.add(new Info());
115        }
116
117        /**
118         * Closes the current component array.
119         * @throws IllegalStateException if there are no open components (
120         * {@link #writeStartComponent(String)} must be called first)
121         * @throws IOException if there's an I/O problem
122         */
123        public void writeEndComponent() throws IOException {
124                if (stack.isEmpty()) {
125                        throw new IllegalStateException("Call \"writeStartComponent\" first.");
126                }
127                Info cur = stack.removeLast();
128
129                if (!cur.wroteEndPropertiesArray) {
130                        generator.writeEndArray();
131                }
132                if (!cur.wroteStartSubComponentsArray) {
133                        generator.writeStartArray();
134                }
135
136                generator.writeEndArray(); //end sub-components array
137                generator.writeEndArray(); //end the array of this component
138
139                componentEnded = true;
140        }
141
142        /**
143         * Writes a property to the current component.
144         * @param propertyName the property name (e.g. "version")
145         * @param dataType the property's data type (e.g. "text")
146         * @param value the property value
147         * @throws IllegalStateException if there are no open components (
148         * {@link #writeStartComponent(String)} must be called first) or if the last
149         * method called was {@link #writeEndComponent()}.
150         * @throws IOException if there's an I/O problem
151         */
152        public void writeProperty(String propertyName, ICalDataType dataType, JCalValue value) throws IOException {
153                writeProperty(propertyName, new ICalParameters(), dataType, value);
154        }
155
156        /**
157         * Writes a property to the current component.
158         * @param propertyName the property name (e.g. "version")
159         * @param parameters the parameters
160         * @param dataType the property's data type (e.g. "text")
161         * @param value the property value
162         * @throws IllegalStateException if there are no open components (
163         * {@link #writeStartComponent(String)} must be called first) or if the last
164         * method called was {@link #writeEndComponent()}.
165         * @throws IOException if there's an I/O problem
166         */
167        public void writeProperty(String propertyName, ICalParameters parameters, ICalDataType dataType, JCalValue value) throws IOException {
168                if (stack.isEmpty()) {
169                        throw new IllegalStateException("Call \"writeStartComponent\" first.");
170                }
171                if (componentEnded) {
172                        throw new IllegalStateException("Cannot write a property after calling \"writeEndComponent\".");
173                }
174
175                generator.writeStartArray();
176                indent(stack.size() * 2);
177
178                //write the property name
179                generator.writeString(propertyName);
180
181                //write parameters
182                generator.writeStartObject();
183                for (Map.Entry<String, List<String>> entry : parameters) {
184                        String name = entry.getKey().toLowerCase();
185                        List<String> values = entry.getValue();
186                        if (values.isEmpty()) {
187                                continue;
188                        }
189
190                        if (values.size() == 1) {
191                                generator.writeStringField(name, values.get(0));
192                        } else {
193                                generator.writeArrayFieldStart(name);
194                                for (String paramValue : values) {
195                                        generator.writeString(paramValue);
196                                }
197                                generator.writeEndArray();
198                        }
199                }
200                generator.writeEndObject();
201
202                //write data type
203                generator.writeString((dataType == null) ? "unknown" : dataType.getName().toLowerCase());
204
205                //write value
206                for (JsonValue jsonValue : value.getValues()) {
207                        writeValue(jsonValue);
208                }
209
210                generator.writeEndArray();
211        }
212
213        private void writeValue(JsonValue jsonValue) throws IOException {
214                if (jsonValue.isNull()) {
215                        generator.writeNull();
216                        return;
217                }
218
219                Object val = jsonValue.getValue();
220                if (val != null) {
221                        if (val instanceof Byte) {
222                                generator.writeNumber((Byte) val);
223                        } else if (val instanceof Short) {
224                                generator.writeNumber((Short) val);
225                        } else if (val instanceof Integer) {
226                                generator.writeNumber((Integer) val);
227                        } else if (val instanceof Long) {
228                                generator.writeNumber((Long) val);
229                        } else if (val instanceof Float) {
230                                generator.writeNumber((Float) val);
231                        } else if (val instanceof Double) {
232                                generator.writeNumber((Double) val);
233                        } else if (val instanceof Boolean) {
234                                generator.writeBoolean((Boolean) val);
235                        } else {
236                                generator.writeString(val.toString());
237                        }
238                        return;
239                }
240
241                List<JsonValue> array = jsonValue.getArray();
242                if (array != null) {
243                        generator.writeStartArray();
244                        for (JsonValue element : array) {
245                                writeValue(element);
246                        }
247                        generator.writeEndArray();
248                        return;
249                }
250
251                Map<String, JsonValue> object = jsonValue.getObject();
252                if (object != null) {
253                        generator.writeStartObject();
254                        for (Map.Entry<String, JsonValue> entry : object.entrySet()) {
255                                generator.writeFieldName(entry.getKey());
256                                writeValue(entry.getValue());
257                        }
258                        generator.writeEndObject();
259                        return;
260                }
261        }
262
263        /**
264         * Checks to see if pretty-printing is enabled, and adds indentation
265         * whitespace if it is.
266         * @param spaces the number of spaces to indent with
267         * @throws IOException
268         */
269        private void indent(int spaces) throws IOException {
270                if (!indent) {
271                        return;
272                }
273
274                generator.writeRaw(NEWLINE);
275                for (int i = 0; i < spaces; i++) {
276                        generator.writeRaw(' ');
277                }
278        }
279
280        /**
281         * Flushes the JSON stream.
282         * @throws IOException if there's a problem flushing the stream
283         */
284        public void flush() throws IOException {
285                if (generator == null) {
286                        return;
287                }
288
289                generator.flush();
290        }
291
292        /**
293         * Finishes writing the JSON document so that it is syntactically correct.
294         * No more data can be written once this method is called.
295         * @throws IOException if there's a problem closing the stream
296         */
297        public void closeJsonStream() throws IOException {
298                if (generator == null) {
299                        return;
300                }
301
302                while (!stack.isEmpty()) {
303                        writeEndComponent();
304                }
305
306                if (wrapInArray) {
307                        indent(0);
308                        generator.writeEndArray();
309                }
310
311                generator.close();
312        }
313
314        /**
315         * Finishes writing the JSON document and closes the underlying
316         * {@link Writer}.
317         * @throws IOException if there's a problem closing the stream
318         */
319        public void close() throws IOException {
320                if (generator == null) {
321                        return;
322                }
323
324                closeJsonStream();
325                writer.close();
326        }
327
328        private void init() throws IOException {
329                JsonFactory factory = new JsonFactory();
330                factory.configure(Feature.AUTO_CLOSE_TARGET, false);
331                generator = factory.createGenerator(writer);
332
333                if (wrapInArray) {
334                        generator.writeStartArray();
335                        indent(0);
336                }
337        }
338
339        private static class Info {
340                public boolean wroteEndPropertiesArray = false;
341                public boolean wroteStartSubComponentsArray = false;
342        }
343}