001    package biweekly.io.json;
002    
003    import static biweekly.util.StringUtils.NEWLINE;
004    
005    import java.io.Closeable;
006    import java.io.Flushable;
007    import java.io.IOException;
008    import java.io.Writer;
009    import java.util.LinkedList;
010    import java.util.List;
011    import java.util.Map;
012    
013    import biweekly.ICalDataType;
014    import biweekly.parameter.ICalParameters;
015    
016    import com.fasterxml.jackson.core.JsonFactory;
017    import com.fasterxml.jackson.core.JsonGenerator;
018    import com.fasterxml.jackson.core.JsonGenerator.Feature;
019    
020    /*
021     Copyright (c) 2013, 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/draft-ietf-jcardcal-jcal-05">jCal
049     * draft</a>
050     */
051    public class JCalRawWriter implements Closeable, Flushable {
052            private final Writer writer;
053            private final boolean wrapInArray;
054            private final LinkedList<Info> stack = new LinkedList<Info>();
055            private JsonGenerator jg;
056            private boolean indent = false;
057            private boolean componentEnded = false;
058    
059            /**
060             * Creates a new raw writer.
061             * @param writer the writer to the data stream
062             * @param wrapInArray true to wrap everything in an array, false not to
063             * (useful when writing more than one iCalendar object)
064             */
065            public JCalRawWriter(Writer writer, boolean wrapInArray) {
066                    this.writer = writer;
067                    this.wrapInArray = wrapInArray;
068            }
069    
070            /**
071             * Gets whether or not the JSON will be pretty-printed.
072             * @return true if it will be pretty-printed, false if not (defaults to
073             * false)
074             */
075            public boolean isIndent() {
076                    return indent;
077            }
078    
079            /**
080             * Sets whether or not to pretty-print the JSON.
081             * @param indent true to pretty-print it, false not to (defaults to false)
082             */
083            public void setIndent(boolean indent) {
084                    this.indent = indent;
085            }
086    
087            /**
088             * Writes the beginning of a new component array.
089             * @param componentName the component name (e.g. "vevent")
090             * @throws IOException if there's an I/O problem
091             */
092            public void writeStartComponent(String componentName) throws IOException {
093                    if (jg == null) {
094                            init();
095                    }
096    
097                    componentEnded = false;
098    
099                    if (!stack.isEmpty()) {
100                            Info parent = stack.getLast();
101                            if (!parent.wroteEndPropertiesArray) {
102                                    jg.writeEndArray();
103                                    parent.wroteEndPropertiesArray = true;
104                            }
105                            if (!parent.wroteStartSubComponentsArray) {
106                                    jg.writeStartArray();
107                                    parent.wroteStartSubComponentsArray = true;
108                            }
109                    }
110    
111                    jg.writeStartArray();
112                    indent(stack.size() * 2);
113                    jg.writeString(componentName);
114                    jg.writeStartArray(); //start properties array
115    
116                    stack.add(new Info());
117            }
118    
119            /**
120             * Closes the current component array.
121             * @throws IllegalStateException if there are no open components (
122             * {@link #writeStartComponent(String)} must be called first)
123             * @throws IOException if there's an I/O problem
124             */
125            public void writeEndComponent() throws IOException {
126                    if (stack.isEmpty()) {
127                            throw new IllegalStateException("Call \"writeStartComponent\" first.");
128                    }
129                    Info cur = stack.removeLast();
130    
131                    if (!cur.wroteEndPropertiesArray) {
132                            jg.writeEndArray();
133                    }
134                    if (!cur.wroteStartSubComponentsArray) {
135                            jg.writeStartArray();
136                    }
137    
138                    jg.writeEndArray(); //end sub-components array
139                    jg.writeEndArray(); //end the array of this component
140    
141                    componentEnded = true;
142            }
143    
144            /**
145             * Writes a property to the current component.
146             * @param propertyName the property name (e.g. "version")
147             * @param dataType the property's data type (e.g. "text")
148             * @param value the property value
149             * @throws IllegalStateException if there are no open components (
150             * {@link #writeStartComponent(String)} must be called first) or if the last
151             * method called was {@link #writeEndComponent()}.
152             * @throws IOException if there's an I/O problem
153             */
154            public void writeProperty(String propertyName, ICalDataType dataType, JCalValue value) throws IOException {
155                    writeProperty(propertyName, new ICalParameters(), dataType, value);
156            }
157    
158            /**
159             * Writes a property to the current component.
160             * @param propertyName the property name (e.g. "version")
161             * @param parameters the parameters
162             * @param dataType the property's data type (e.g. "text")
163             * @param value the property value
164             * @throws IllegalStateException if there are no open components (
165             * {@link #writeStartComponent(String)} must be called first) or if the last
166             * method called was {@link #writeEndComponent()}.
167             * @throws IOException if there's an I/O problem
168             */
169            public void writeProperty(String propertyName, ICalParameters parameters, ICalDataType dataType, JCalValue value) throws IOException {
170                    if (stack.isEmpty()) {
171                            throw new IllegalStateException("Call \"writeStartComponent\" first.");
172                    }
173                    if (componentEnded) {
174                            throw new IllegalStateException("Cannot write a property after calling \"writeEndComponent\".");
175                    }
176    
177                    jg.writeStartArray();
178                    indent(stack.size() * 2);
179    
180                    //write the property name
181                    jg.writeString(propertyName);
182    
183                    //write parameters
184                    jg.writeStartObject();
185                    for (Map.Entry<String, List<String>> entry : parameters) {
186                            String name = entry.getKey().toLowerCase();
187                            List<String> values = entry.getValue();
188                            if (values.isEmpty()) {
189                                    continue;
190                            }
191    
192                            if (values.size() == 1) {
193                                    jg.writeStringField(name, values.get(0));
194                            } else {
195                                    jg.writeArrayFieldStart(name);
196                                    for (String paramValue : values) {
197                                            jg.writeString(paramValue);
198                                    }
199                                    jg.writeEndArray();
200                            }
201                    }
202                    jg.writeEndObject();
203    
204                    //write data type
205                    jg.writeString((dataType == null) ? "unknown" : dataType.getName().toLowerCase());
206    
207                    //write value
208                    for (JsonValue jsonValue : value.getValues()) {
209                            writeValue(jsonValue);
210                    }
211    
212                    jg.writeEndArray();
213            }
214    
215            private void writeValue(JsonValue jsonValue) throws IOException {
216                    if (jsonValue.isNull()) {
217                            jg.writeNull();
218                            return;
219                    }
220    
221                    Object val = jsonValue.getValue();
222                    if (val != null) {
223                            if (val instanceof Byte) {
224                                    jg.writeNumber((Byte) val);
225                            } else if (val instanceof Short) {
226                                    jg.writeNumber((Short) val);
227                            } else if (val instanceof Integer) {
228                                    jg.writeNumber((Integer) val);
229                            } else if (val instanceof Long) {
230                                    jg.writeNumber((Long) val);
231                            } else if (val instanceof Float) {
232                                    jg.writeNumber((Float) val);
233                            } else if (val instanceof Double) {
234                                    jg.writeNumber((Double) val);
235                            } else if (val instanceof Boolean) {
236                                    jg.writeBoolean((Boolean) val);
237                            } else {
238                                    jg.writeString(val.toString());
239                            }
240                            return;
241                    }
242    
243                    List<JsonValue> array = jsonValue.getArray();
244                    if (array != null) {
245                            jg.writeStartArray();
246                            for (JsonValue element : array) {
247                                    writeValue(element);
248                            }
249                            jg.writeEndArray();
250                            return;
251                    }
252    
253                    Map<String, JsonValue> object = jsonValue.getObject();
254                    if (object != null) {
255                            jg.writeStartObject();
256                            for (Map.Entry<String, JsonValue> entry : object.entrySet()) {
257                                    jg.writeFieldName(entry.getKey());
258                                    writeValue(entry.getValue());
259                            }
260                            jg.writeEndObject();
261                            return;
262                    }
263            }
264    
265            /**
266             * Checks to see if pretty-printing is enabled, and adds indentation
267             * whitespace if it is.
268             * @param spaces the number of spaces to indent with
269             * @throws IOException
270             */
271            private void indent(int spaces) throws IOException {
272                    if (indent) {
273                            jg.writeRaw(NEWLINE);
274                            for (int i = 0; i < spaces; i++) {
275                                    jg.writeRaw(' ');
276                            }
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 (jg == null) {
286                            return;
287                    }
288    
289                    jg.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 (jg == null) {
299                            return;
300                    }
301    
302                    while (!stack.isEmpty()) {
303                            writeEndComponent();
304                    }
305    
306                    if (wrapInArray) {
307                            indent(0);
308                            jg.writeEndArray();
309                    }
310    
311                    jg.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 (jg == 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                    jg = factory.createJsonGenerator(writer);
332    
333                    if (wrapInArray) {
334                            jg.writeStartArray();
335                            indent(0);
336                    }
337            }
338    
339            private static class Info {
340                    public boolean wroteEndPropertiesArray = false;
341                    public boolean wroteStartSubComponentsArray = false;
342            }
343    }