001package ezvcard.io.json; 002 003import java.io.Closeable; 004import java.io.Flushable; 005import java.io.IOException; 006import java.io.Writer; 007import java.util.List; 008import java.util.Map; 009 010import com.fasterxml.jackson.core.JsonFactory; 011import com.fasterxml.jackson.core.JsonGenerator; 012import com.fasterxml.jackson.core.JsonGenerator.Feature; 013import com.fasterxml.jackson.core.PrettyPrinter; 014 015import ezvcard.Messages; 016import ezvcard.VCardDataType; 017import ezvcard.parameter.VCardParameters; 018 019/* 020 Copyright (c) 2012-2026, 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 * 047 * @author Michael Angstadt 048 * @author Buddy Gorven 049 * @see <a href="http://tools.ietf.org/html/rfc7095">RFC 7095</a> 050 */ 051public class JCardRawWriter implements Closeable, Flushable { 052 private final Writer writer; 053 private final boolean wrapInArray; 054 private JsonGenerator generator; 055 private boolean prettyPrint = false; 056 private boolean open = false; 057 private boolean closeGenerator = true; 058 private PrettyPrinter prettyPrinter; 059 060 /** 061 * @param writer the writer to wrap 062 * @param wrapInArray true to wrap everything in an array, false not to 063 * (useful when writing more than one vCard) 064 */ 065 public JCardRawWriter(Writer writer, boolean wrapInArray) { 066 this.writer = writer; 067 this.wrapInArray = wrapInArray; 068 } 069 070 /** 071 * @param generator the generator to write to 072 */ 073 public JCardRawWriter(JsonGenerator generator) { 074 this.writer = null; 075 this.generator = generator; 076 this.closeGenerator = false; 077 this.wrapInArray = false; 078 } 079 080 /** 081 * Gets whether or not the JSON will be pretty-printed. 082 * @return true if it will be pretty-printed, false if not (defaults to 083 * false) 084 */ 085 public boolean isPrettyPrint() { 086 return prettyPrint; 087 } 088 089 /** 090 * Sets whether or not to pretty-print the JSON. 091 * @param prettyPrint true to pretty-print it, false not to (defaults to 092 * false) 093 */ 094 public void setPrettyPrint(boolean prettyPrint) { 095 this.prettyPrint = prettyPrint; 096 } 097 098 /** 099 * Sets the pretty printer to pretty-print the JSON with. Note that this 100 * method implicitly enables indenting, so {@code setPrettyPrint(true)} does 101 * not also need to be called. 102 * @param prettyPrinter the custom pretty printer (defaults to an instance 103 * of {@link JCardPrettyPrinter}, if {@code setPrettyPrint(true)} has been 104 * called) 105 */ 106 public void setPrettyPrinter(PrettyPrinter prettyPrinter) { 107 prettyPrint = true; 108 this.prettyPrinter = prettyPrinter; 109 } 110 111 /** 112 * Writes the beginning of a new "vcard" component. 113 * @throws IOException if there's a problem writing to the output stream 114 */ 115 public void writeStartVCard() throws IOException { 116 if (generator == null) { 117 init(); 118 } 119 120 if (open) { 121 writeEndVCard(); 122 } 123 124 generator.writeStartArray(); 125 generator.writeString("vcard"); 126 generator.writeStartArray(); //start properties array 127 128 open = true; 129 } 130 131 /** 132 * Closes the "vcard" component array. 133 * @throws IllegalStateException if the component was never opened ( 134 * {@link #writeStartVCard} must be called first) 135 * @throws IOException if there's a problem writing to the output stream 136 */ 137 public void writeEndVCard() throws IOException { 138 if (!open) { 139 throw new IllegalStateException(Messages.INSTANCE.getExceptionMessage(1)); 140 } 141 142 generator.writeEndArray(); //end the properties array 143 generator.writeEndArray(); //end the "vcard" component array 144 145 open = false; 146 } 147 148 /** 149 * Writes a property to the current component. 150 * @param propertyName the property name (e.g. "version") 151 * @param dataType the data type or null for "unknown" 152 * @param value the property value 153 * @throws IllegalStateException if the "vcard" component was never opened 154 * or was just closed ({@link #writeStartVCard} must be called first) 155 * @throws IOException if there's a problem writing to the output stream 156 */ 157 public void writeProperty(String propertyName, VCardDataType dataType, JCardValue value) throws IOException { 158 writeProperty(null, propertyName, new VCardParameters(), dataType, value); 159 } 160 161 /** 162 * Writes a property to the current vCard. 163 * @param group the group or null if there is no group 164 * @param propertyName the property name (e.g. "version") 165 * @param parameters the parameters 166 * @param dataType the data type or null for "unknown" 167 * @param value the property value 168 * @throws IllegalStateException if the "vcard" component was never opened 169 * or was just closed ({@link #writeStartVCard} must be called first) 170 * @throws IOException if there's a problem writing to the output stream 171 */ 172 public void writeProperty(String group, String propertyName, VCardParameters parameters, VCardDataType dataType, JCardValue value) throws IOException { 173 if (!open) { 174 throw new IllegalStateException(Messages.INSTANCE.getExceptionMessage(1)); 175 } 176 177 generator.assignCurrentValue(JCardPrettyPrinter.PROPERTY_VALUE); 178 179 generator.writeStartArray(); 180 181 //write the property name 182 generator.writeString(propertyName); 183 184 //write parameters and group 185 writeParametersAndGroup(parameters, group); 186 187 //write data type 188 generator.writeString((dataType == null) ? "unknown" : dataType.getName().toLowerCase()); 189 190 //write value 191 if (value.getValues().isEmpty()) { 192 generator.writeString(""); 193 } else { 194 for (JsonValue jsonValue : value.getValues()) { 195 writeJsonValue(jsonValue); 196 } 197 } 198 199 generator.writeEndArray(); 200 201 generator.assignCurrentValue(null); 202 } 203 204 private void writeParametersAndGroup(VCardParameters parameters, String group) throws IOException { 205 generator.writeStartObject(); 206 207 for (Map.Entry<String, List<String>> entry : parameters) { 208 String name = entry.getKey().toLowerCase(); 209 List<String> values = entry.getValue(); 210 if (values.isEmpty()) { 211 continue; 212 } 213 214 if (values.size() == 1) { 215 generator.writeStringField(name, values.get(0)); 216 } else { 217 generator.writeArrayFieldStart(name); 218 for (String value : values) { 219 generator.writeString(value); 220 } 221 generator.writeEndArray(); 222 } 223 } 224 225 //write group 226 if (group != null) { 227 generator.writeStringField("group", group); 228 } 229 230 generator.writeEndObject(); 231 } 232 233 private void writeJsonValue(JsonValue jsonValue) throws IOException { 234 if (jsonValue.isNull()) { 235 generator.writeNull(); 236 return; 237 } 238 239 Object val = jsonValue.getValue(); 240 if (val != null) { 241 writeValue(val); 242 return; 243 } 244 245 List<JsonValue> array = jsonValue.getArray(); 246 if (array != null) { 247 writeArray(array); 248 return; 249 } 250 251 Map<String, JsonValue> object = jsonValue.getObject(); 252 if (object != null) { 253 writeObject(object); 254 return; 255 } 256 } 257 258 private void writeValue(Object val) throws IOException { 259 if (val instanceof Byte) { 260 generator.writeNumber((Byte) val); 261 } else if (val instanceof Short) { 262 generator.writeNumber((Short) val); 263 } else if (val instanceof Integer) { 264 generator.writeNumber((Integer) val); 265 } else if (val instanceof Long) { 266 generator.writeNumber((Long) val); 267 } else if (val instanceof Float) { 268 generator.writeNumber((Float) val); 269 } else if (val instanceof Double) { 270 generator.writeNumber((Double) val); 271 } else if (val instanceof Boolean) { 272 generator.writeBoolean((Boolean) val); 273 } else { 274 generator.writeString(val.toString()); 275 } 276 } 277 278 private void writeArray(List<JsonValue> array) throws IOException { 279 generator.writeStartArray(); 280 for (JsonValue element : array) { 281 writeJsonValue(element); 282 } 283 generator.writeEndArray(); 284 } 285 286 private void writeObject(Map<String, JsonValue> object) throws IOException { 287 generator.writeStartObject(); 288 for (Map.Entry<String, JsonValue> entry : object.entrySet()) { 289 generator.writeFieldName(entry.getKey()); 290 writeJsonValue(entry.getValue()); 291 } 292 generator.writeEndObject(); 293 } 294 295 /** 296 * Flushes the JSON stream. 297 * @throws IOException if there's a problem writing to the output stream 298 */ 299 public void flush() throws IOException { 300 if (generator == null) { 301 return; 302 } 303 304 generator.flush(); 305 } 306 307 /** 308 * Finishes writing the JSON document so that it is syntactically correct. 309 * No more data can be written once this method is called. 310 * @throws IOException if there's a problem closing the output stream 311 */ 312 public void closeJsonStream() throws IOException { 313 if (generator == null) { 314 return; 315 } 316 317 while (open) { 318 writeEndVCard(); 319 } 320 321 if (wrapInArray) { 322 generator.writeEndArray(); 323 } 324 325 if (closeGenerator) { 326 generator.close(); 327 } 328 } 329 330 /** 331 * Finishes writing the JSON document and closes the underlying 332 * {@link Writer}. 333 * @throws IOException if there's a problem closing the output stream 334 */ 335 public void close() throws IOException { 336 if (generator == null) { 337 return; 338 } 339 340 closeJsonStream(); 341 if (writer != null) { 342 writer.close(); 343 } 344 } 345 346 private void init() throws IOException { 347 JsonFactory factory = new JsonFactory(); 348 factory.configure(Feature.AUTO_CLOSE_TARGET, false); 349 generator = factory.createGenerator(writer); 350 351 if (prettyPrint) { 352 if (prettyPrinter == null) { 353 prettyPrinter = new JCardPrettyPrinter(); 354 } 355 generator.setPrettyPrinter(prettyPrinter); 356 } 357 358 if (wrapInArray) { 359 generator.writeStartArray(); 360 } 361 } 362}