001    package ezvcard.io.json;
002    
003    import static ezvcard.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.List;
010    import java.util.Map;
011    
012    import com.fasterxml.jackson.core.JsonFactory;
013    import com.fasterxml.jackson.core.JsonGenerator;
014    import com.fasterxml.jackson.core.JsonGenerator.Feature;
015    
016    import ezvcard.VCardDataType;
017    import ezvcard.parameter.VCardParameters;
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 vCard JSON data stream (jCard).
046     * @author Michael Angstadt
047     * @see <a
048     * href="http://tools.ietf.org/html/draft-kewisch-vcard-in-json-04">jCard
049     * draft</a>
050     */
051    public class JCardRawWriter implements Closeable, Flushable {
052            private final Writer writer;
053            private final boolean wrapInArray;
054            private JsonGenerator jg;
055            private boolean indent = false;
056            private boolean open = 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 vCard)
063             */
064            public JCardRawWriter(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 "vcard" component.
088             * @throws IOException if there's an I/O problem
089             */
090            public void writeStartVCard() throws IOException {
091                    if (jg == null) {
092                            init();
093                    }
094    
095                    if (open) {
096                            writeEndVCard();
097                    }
098    
099                    jg.writeStartArray();
100                    indent(0);
101                    jg.writeString("vcard");
102                    jg.writeStartArray(); //start properties array
103    
104                    open = true;
105            }
106    
107            /**
108             * Closes the "vcard" component array.
109             * @throws IllegalStateException if the component was never opened (
110             * {@link #writeStartVCard} must be called first)
111             * @throws IOException if there's an I/O problem
112             */
113            public void writeEndVCard() throws IOException {
114                    if (!open) {
115                            throw new IllegalStateException("Call \"writeStartVCard\" first.");
116                    }
117    
118                    jg.writeEndArray(); //end the properties array
119                    jg.writeEndArray(); //end the "vcard" component array
120    
121                    open = false;
122            }
123    
124            /**
125             * Writes a property to the current component.
126             * @param propertyName the property name (e.g. "version")
127             * @param dataType the data type or null for "unknown"
128             * @param value the property value
129             * @throws IllegalStateException if the "vcard" component was never opened
130             * or was just closed ({@link #writeStartVCard} must be called first)
131             * @throws IOException if there's an I/O problem
132             */
133            public void writeProperty(String propertyName, VCardDataType dataType, JCardValue value) throws IOException {
134                    writeProperty(null, propertyName, new VCardParameters(), dataType, value);
135            }
136    
137            /**
138             * Writes a property to the current vCard.
139             * @param group the group or null if there is no group
140             * @param propertyName the property name (e.g. "version")
141             * @param parameters the parameters
142             * @param dataType the data type or null for "unknown"
143             * @param value the property value
144             * @throws IllegalStateException if the "vcard" component was never opened
145             * or was just closed ({@link #writeStartVCard} must be called first)
146             * @throws IOException if there's an I/O problem
147             */
148            public void writeProperty(String group, String propertyName, VCardParameters parameters, VCardDataType dataType, JCardValue value) throws IOException {
149                    if (!open) {
150                            throw new IllegalStateException("Call \"writeStartVCard\" first.");
151                    }
152    
153                    jg.writeStartArray();
154                    indent(2);
155    
156                    //write the property name
157                    jg.writeString(propertyName);
158    
159                    //write parameters
160                    jg.writeStartObject();
161                    for (Map.Entry<String, List<String>> entry : parameters) {
162                            String name = entry.getKey().toLowerCase();
163                            List<String> values = entry.getValue();
164                            if (values.isEmpty()) {
165                                    continue;
166                            }
167    
168                            if (values.size() == 1) {
169                                    jg.writeStringField(name, values.get(0));
170                            } else {
171                                    jg.writeArrayFieldStart(name);
172                                    for (String paramValue : values) {
173                                            jg.writeString(paramValue);
174                                    }
175                                    jg.writeEndArray();
176                            }
177                    }
178    
179                    //write group
180                    if (group != null) {
181                            jg.writeStringField("group", group);
182                    }
183    
184                    jg.writeEndObject(); //end parameters object
185    
186                    //write data type
187                    jg.writeString((dataType == null) ? "unknown" : dataType.getName().toLowerCase());
188    
189                    //write value
190                    if (value.getValues().isEmpty()) {
191                            jg.writeString("");
192                    } else {
193                            for (JsonValue jsonValue : value.getValues()) {
194                                    writeValue(jsonValue);
195                            }
196                    }
197    
198                    jg.writeEndArray();
199            }
200    
201            private void writeValue(JsonValue jsonValue) throws IOException {
202                    if (jsonValue.isNull()) {
203                            jg.writeNull();
204                            return;
205                    }
206    
207                    Object val = jsonValue.getValue();
208                    if (val != null) {
209                            if (val instanceof Byte) {
210                                    jg.writeNumber((Byte) val);
211                            } else if (val instanceof Short) {
212                                    jg.writeNumber((Short) val);
213                            } else if (val instanceof Integer) {
214                                    jg.writeNumber((Integer) val);
215                            } else if (val instanceof Long) {
216                                    jg.writeNumber((Long) val);
217                            } else if (val instanceof Float) {
218                                    jg.writeNumber((Float) val);
219                            } else if (val instanceof Double) {
220                                    jg.writeNumber((Double) val);
221                            } else if (val instanceof Boolean) {
222                                    jg.writeBoolean((Boolean) val);
223                            } else {
224                                    jg.writeString(val.toString());
225                            }
226                            return;
227                    }
228    
229                    List<JsonValue> array = jsonValue.getArray();
230                    if (array != null) {
231                            jg.writeStartArray();
232                            for (JsonValue element : array) {
233                                    writeValue(element);
234                            }
235                            jg.writeEndArray();
236                            return;
237                    }
238    
239                    Map<String, JsonValue> object = jsonValue.getObject();
240                    if (object != null) {
241                            jg.writeStartObject();
242                            for (Map.Entry<String, JsonValue> entry : object.entrySet()) {
243                                    jg.writeFieldName(entry.getKey());
244                                    writeValue(entry.getValue());
245                            }
246                            jg.writeEndObject();
247                            return;
248                    }
249            }
250    
251            /**
252             * Checks to see if pretty-printing is enabled, and adds indentation
253             * whitespace if it is.
254             * @param spaces the number of spaces to indent with
255             * @throws IOException
256             */
257            private void indent(int spaces) throws IOException {
258                    if (!indent) {
259                            return;
260                    }
261    
262                    jg.writeRaw(NEWLINE);
263                    for (int i = 0; i < spaces; i++) {
264                            jg.writeRaw(' ');
265                    }
266            }
267    
268            /**
269             * Flushes the JSON stream.
270             */
271            public void flush() throws IOException {
272                    if (jg == null) {
273                            return;
274                    }
275    
276                    jg.flush();
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 (open) {
290                            writeEndVCard();
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    }