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         * Creates a new raw writer.
060         * @param writer the writer to the data stream
061         * @param wrapInArray true to wrap everything in an array, false not to
062         * (useful when writing more than one iCalendar object)
063         */
064        public JCalRawWriter(Writer writer, boolean wrapInArray) {
065                this.writer = writer;
066                this.wrapInArray = wrapInArray;
067        }
068
069        /**
070         * Gets whether or not the JSON will be pretty-printed.
071         * @return true if it will be pretty-printed, false if not (defaults to
072         * false)
073         */
074        public boolean isIndent() {
075                return indent;
076        }
077
078        /**
079         * Sets whether or not to pretty-print the JSON.
080         * @param indent true to pretty-print it, false not to (defaults to false)
081         */
082        public void setIndent(boolean indent) {
083                this.indent = indent;
084        }
085
086        /**
087         * Writes the beginning of a new component array.
088         * @param componentName the component name (e.g. "vevent")
089         * @throws IOException if there's an I/O problem
090         */
091        public void writeStartComponent(String componentName) throws IOException {
092                if (generator == null) {
093                        init();
094                }
095
096                componentEnded = false;
097
098                if (!stack.isEmpty()) {
099                        Info parent = stack.getLast();
100                        if (!parent.wroteEndPropertiesArray) {
101                                generator.writeEndArray();
102                                parent.wroteEndPropertiesArray = true;
103                        }
104                        if (!parent.wroteStartSubComponentsArray) {
105                                generator.writeStartArray();
106                                parent.wroteStartSubComponentsArray = true;
107                        }
108                }
109
110                generator.writeStartArray();
111                indent(stack.size() * 2);
112                generator.writeString(componentName);
113                generator.writeStartArray(); //start properties array
114
115                stack.add(new Info());
116        }
117
118        /**
119         * Closes the current component array.
120         * @throws IllegalStateException if there are no open components (
121         * {@link #writeStartComponent(String)} must be called first)
122         * @throws IOException if there's an I/O problem
123         */
124        public void writeEndComponent() throws IOException {
125                if (stack.isEmpty()) {
126                        throw new IllegalStateException("Call \"writeStartComponent\" first.");
127                }
128                Info cur = stack.removeLast();
129
130                if (!cur.wroteEndPropertiesArray) {
131                        generator.writeEndArray();
132                }
133                if (!cur.wroteStartSubComponentsArray) {
134                        generator.writeStartArray();
135                }
136
137                generator.writeEndArray(); //end sub-components array
138                generator.writeEndArray(); //end the array of this component
139
140                componentEnded = true;
141        }
142
143        /**
144         * Writes a property to the current component.
145         * @param propertyName the property name (e.g. "version")
146         * @param dataType the property's data type (e.g. "text")
147         * @param value the property value
148         * @throws IllegalStateException if there are no open components (
149         * {@link #writeStartComponent(String)} must be called first) or if the last
150         * method called was {@link #writeEndComponent()}.
151         * @throws IOException if there's an I/O problem
152         */
153        public void writeProperty(String propertyName, ICalDataType dataType, JCalValue value) throws IOException {
154                writeProperty(propertyName, new ICalParameters(), dataType, value);
155        }
156
157        /**
158         * Writes a property to the current component.
159         * @param propertyName the property name (e.g. "version")
160         * @param parameters the parameters
161         * @param dataType the property's data type (e.g. "text")
162         * @param value the property value
163         * @throws IllegalStateException if there are no open components (
164         * {@link #writeStartComponent(String)} must be called first) or if the last
165         * method called was {@link #writeEndComponent()}.
166         * @throws IOException if there's an I/O problem
167         */
168        public void writeProperty(String propertyName, ICalParameters parameters, ICalDataType dataType, JCalValue value) throws IOException {
169                if (stack.isEmpty()) {
170                        throw new IllegalStateException("Call \"writeStartComponent\" first.");
171                }
172                if (componentEnded) {
173                        throw new IllegalStateException("Cannot write a property after calling \"writeEndComponent\".");
174                }
175
176                generator.writeStartArray();
177                indent(stack.size() * 2);
178
179                //write the property name
180                generator.writeString(propertyName);
181
182                //write parameters
183                generator.writeStartObject();
184                for (Map.Entry<String, List<String>> entry : parameters) {
185                        String name = entry.getKey().toLowerCase();
186                        List<String> values = entry.getValue();
187                        if (values.isEmpty()) {
188                                continue;
189                        }
190
191                        if (values.size() == 1) {
192                                generator.writeStringField(name, values.get(0));
193                        } else {
194                                generator.writeArrayFieldStart(name);
195                                for (String paramValue : values) {
196                                        generator.writeString(paramValue);
197                                }
198                                generator.writeEndArray();
199                        }
200                }
201                generator.writeEndObject();
202
203                //write data type
204                generator.writeString((dataType == null) ? "unknown" : dataType.getName().toLowerCase());
205
206                //write value
207                for (JsonValue jsonValue : value.getValues()) {
208                        writeValue(jsonValue);
209                }
210
211                generator.writeEndArray();
212        }
213
214        private void writeValue(JsonValue jsonValue) throws IOException {
215                if (jsonValue.isNull()) {
216                        generator.writeNull();
217                        return;
218                }
219
220                Object val = jsonValue.getValue();
221                if (val != null) {
222                        if (val instanceof Byte) {
223                                generator.writeNumber((Byte) val);
224                        } else if (val instanceof Short) {
225                                generator.writeNumber((Short) val);
226                        } else if (val instanceof Integer) {
227                                generator.writeNumber((Integer) val);
228                        } else if (val instanceof Long) {
229                                generator.writeNumber((Long) val);
230                        } else if (val instanceof Float) {
231                                generator.writeNumber((Float) val);
232                        } else if (val instanceof Double) {
233                                generator.writeNumber((Double) val);
234                        } else if (val instanceof Boolean) {
235                                generator.writeBoolean((Boolean) val);
236                        } else {
237                                generator.writeString(val.toString());
238                        }
239                        return;
240                }
241
242                List<JsonValue> array = jsonValue.getArray();
243                if (array != null) {
244                        generator.writeStartArray();
245                        for (JsonValue element : array) {
246                                writeValue(element);
247                        }
248                        generator.writeEndArray();
249                        return;
250                }
251
252                Map<String, JsonValue> object = jsonValue.getObject();
253                if (object != null) {
254                        generator.writeStartObject();
255                        for (Map.Entry<String, JsonValue> entry : object.entrySet()) {
256                                generator.writeFieldName(entry.getKey());
257                                writeValue(entry.getValue());
258                        }
259                        generator.writeEndObject();
260                        return;
261                }
262        }
263
264        /**
265         * Checks to see if pretty-printing is enabled, and adds indentation
266         * whitespace if it is.
267         * @param spaces the number of spaces to indent with
268         * @throws IOException
269         */
270        private void indent(int spaces) throws IOException {
271                if (!indent) {
272                        return;
273                }
274
275                generator.writeRaw(NEWLINE);
276                for (int i = 0; i < spaces; i++) {
277                        generator.writeRaw(' ');
278                }
279        }
280
281        /**
282         * Flushes the JSON stream.
283         * @throws IOException if there's a problem flushing the stream
284         */
285        public void flush() throws IOException {
286                if (generator == null) {
287                        return;
288                }
289
290                generator.flush();
291        }
292
293        /**
294         * Finishes writing the JSON document so that it is syntactically correct.
295         * No more data can be written once this method is called.
296         * @throws IOException if there's a problem closing the stream
297         */
298        public void closeJsonStream() throws IOException {
299                if (generator == null) {
300                        return;
301                }
302
303                while (!stack.isEmpty()) {
304                        writeEndComponent();
305                }
306
307                if (wrapInArray) {
308                        indent(0);
309                        generator.writeEndArray();
310                }
311
312                generator.close();
313        }
314
315        /**
316         * Finishes writing the JSON document and closes the underlying
317         * {@link Writer}.
318         * @throws IOException if there's a problem closing the stream
319         */
320        public void close() throws IOException {
321                if (generator == null) {
322                        return;
323                }
324
325                closeJsonStream();
326                writer.close();
327        }
328
329        private void init() throws IOException {
330                JsonFactory factory = new JsonFactory();
331                factory.configure(Feature.AUTO_CLOSE_TARGET, false);
332                generator = factory.createGenerator(writer);
333
334                if (wrapInArray) {
335                        generator.writeStartArray();
336                        indent(0);
337                }
338        }
339
340        private static class Info {
341                public boolean wroteEndPropertiesArray = false;
342                public boolean wroteStartSubComponentsArray = false;
343        }
344}