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 }