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 }