001    package biweekly.io.json;
002    
003    import static biweekly.util.StringUtils.NEWLINE;
004    
005    import java.io.Closeable;
006    import java.io.IOException;
007    import java.io.Writer;
008    import java.util.LinkedList;
009    import java.util.List;
010    import java.util.Map;
011    
012    import biweekly.ICalDataType;
013    import biweekly.parameter.ICalParameters;
014    
015    import com.fasterxml.jackson.core.JsonFactory;
016    import com.fasterxml.jackson.core.JsonGenerator;
017    import com.fasterxml.jackson.core.JsonGenerator.Feature;
018    
019    /*
020     Copyright (c) 2013, Michael Angstadt
021     All rights reserved.
022    
023     Redistribution and use in source and binary forms, with or without
024     modification, are permitted provided that the following conditions are met: 
025    
026     1. Redistributions of source code must retain the above copyright notice, this
027     list of conditions and the following disclaimer. 
028     2. Redistributions in binary form must reproduce the above copyright notice,
029     this list of conditions and the following disclaimer in the documentation
030     and/or other materials provided with the distribution. 
031    
032     THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
033     ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
034     WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
035     DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR
036     ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
037     (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
038     LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
039     ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
040     (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
041     SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
042     */
043    
044    /**
045     * Writes data to an iCalendar JSON data stream (jCal).
046     * @author Michael Angstadt
047     * @see <a href="http://tools.ietf.org/html/draft-ietf-jcardcal-jcal-05">jCal
048     * draft</a>
049     */
050    public class JCalRawWriter implements Closeable {
051            private final Writer writer;
052            private final boolean wrapInArray;
053            private final LinkedList<Info> stack = new LinkedList<Info>();
054            private JsonGenerator jg;
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 (jg == null) {
093                            init();
094                    }
095    
096                    componentEnded = false;
097    
098                    if (!stack.isEmpty()) {
099                            Info parent = stack.getLast();
100                            if (!parent.wroteEndPropertiesArray) {
101                                    jg.writeEndArray();
102                                    parent.wroteEndPropertiesArray = true;
103                            }
104                            if (!parent.wroteStartSubComponentsArray) {
105                                    jg.writeStartArray();
106                                    parent.wroteStartSubComponentsArray = true;
107                            }
108                    }
109    
110                    jg.writeStartArray();
111                    indent(stack.size() * 2);
112                    jg.writeString(componentName);
113                    jg.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                            jg.writeEndArray();
132                    }
133                    if (!cur.wroteStartSubComponentsArray) {
134                            jg.writeStartArray();
135                    }
136    
137                    jg.writeEndArray(); //end sub-components array
138                    jg.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                    jg.writeStartArray();
177                    indent(stack.size() * 2);
178    
179                    //write the property name
180                    jg.writeString(propertyName);
181    
182                    //write parameters
183                    jg.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                                    jg.writeStringField(name, values.get(0));
193                            } else {
194                                    jg.writeArrayFieldStart(name);
195                                    for (String paramValue : values) {
196                                            jg.writeString(paramValue);
197                                    }
198                                    jg.writeEndArray();
199                            }
200                    }
201                    jg.writeEndObject();
202    
203                    //write data type
204                    jg.writeString((dataType == null) ? "unknown" : dataType.getName().toLowerCase());
205    
206                    //write value
207                    for (JsonValue jsonValue : value.getValues()) {
208                            writeValue(jsonValue);
209                    }
210    
211                    jg.writeEndArray();
212            }
213    
214            private void writeValue(JsonValue jsonValue) throws IOException {
215                    if (jsonValue.isNull()) {
216                            jg.writeNull();
217                            return;
218                    }
219    
220                    Object val = jsonValue.getValue();
221                    if (val != null) {
222                            if (val instanceof Byte) {
223                                    jg.writeNumber((Byte) val);
224                            } else if (val instanceof Short) {
225                                    jg.writeNumber((Short) val);
226                            } else if (val instanceof Integer) {
227                                    jg.writeNumber((Integer) val);
228                            } else if (val instanceof Long) {
229                                    jg.writeNumber((Long) val);
230                            } else if (val instanceof Float) {
231                                    jg.writeNumber((Float) val);
232                            } else if (val instanceof Double) {
233                                    jg.writeNumber((Double) val);
234                            } else if (val instanceof Boolean) {
235                                    jg.writeBoolean((Boolean) val);
236                            } else {
237                                    jg.writeString(val.toString());
238                            }
239                            return;
240                    }
241    
242                    List<JsonValue> array = jsonValue.getArray();
243                    if (array != null) {
244                            jg.writeStartArray();
245                            for (JsonValue element : array) {
246                                    writeValue(element);
247                            }
248                            jg.writeEndArray();
249                            return;
250                    }
251    
252                    Map<String, JsonValue> object = jsonValue.getObject();
253                    if (object != null) {
254                            jg.writeStartObject();
255                            for (Map.Entry<String, JsonValue> entry : object.entrySet()) {
256                                    jg.writeFieldName(entry.getKey());
257                                    writeValue(entry.getValue());
258                            }
259                            jg.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                            jg.writeRaw(NEWLINE);
273                            for (int i = 0; i < spaces; i++) {
274                                    jg.writeRaw(' ');
275                            }
276                    }
277            }
278    
279            /**
280             * Finishes writing the JSON document so that it is syntactically correct.
281             * No more data can be written once this method is called.
282             * @throws IOException if there's a problem closing the stream
283             */
284            public void closeJsonStream() throws IOException {
285                    if (jg == null) {
286                            return;
287                    }
288    
289                    while (!stack.isEmpty()) {
290                            writeEndComponent();
291                    }
292    
293                    if (wrapInArray) {
294                            indent(0);
295                            jg.writeEndArray();
296                    }
297    
298                    jg.close();
299            }
300    
301            /**
302             * Finishes writing the JSON document and closes the underlying
303             * {@link Writer}.
304             * @throws IOException if there's a problem closing the stream
305             */
306            public void close() throws IOException {
307                    if (jg == null) {
308                            return;
309                    }
310    
311                    closeJsonStream();
312                    writer.close();
313            }
314    
315            private void init() throws IOException {
316                    JsonFactory factory = new JsonFactory();
317                    factory.configure(Feature.AUTO_CLOSE_TARGET, false);
318                    jg = factory.createJsonGenerator(writer);
319    
320                    if (wrapInArray) {
321                            jg.writeStartArray();
322                            indent(0);
323                    }
324            }
325    
326            private static class Info {
327                    public boolean wroteEndPropertiesArray = false;
328                    public boolean wroteStartSubComponentsArray = false;
329            }
330    }