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-2023, 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.setCurrentValue(JCardPrettyPrinter.PROPERTY_VALUE); 178 179 generator.writeStartArray(); 180 181 //write the property name 182 generator.writeString(propertyName); 183 184 //write parameters 185 generator.writeStartObject(); 186 for (Map.Entry<String, List<String>> entry : parameters) { 187 String name = entry.getKey().toLowerCase(); 188 List<String> values = entry.getValue(); 189 if (values.isEmpty()) { 190 continue; 191 } 192 193 if (values.size() == 1) { 194 generator.writeStringField(name, values.get(0)); 195 } else { 196 generator.writeArrayFieldStart(name); 197 for (String paramValue : values) { 198 generator.writeString(paramValue); 199 } 200 generator.writeEndArray(); 201 } 202 } 203 204 //write group 205 if (group != null) { 206 generator.writeStringField("group", group); 207 } 208 209 //end parameters object 210 generator.writeEndObject(); 211 212 //write data type 213 generator.writeString((dataType == null) ? "unknown" : dataType.getName().toLowerCase()); 214 215 //write value 216 if (value.getValues().isEmpty()) { 217 generator.writeString(""); 218 } else { 219 for (JsonValue jsonValue : value.getValues()) { 220 writeValue(jsonValue); 221 } 222 } 223 224 generator.writeEndArray(); 225 226 generator.setCurrentValue(null); 227 } 228 229 private void writeValue(JsonValue jsonValue) throws IOException { 230 if (jsonValue.isNull()) { 231 generator.writeNull(); 232 return; 233 } 234 235 Object val = jsonValue.getValue(); 236 if (val != null) { 237 if (val instanceof Byte) { 238 generator.writeNumber((Byte) val); 239 } else if (val instanceof Short) { 240 generator.writeNumber((Short) val); 241 } else if (val instanceof Integer) { 242 generator.writeNumber((Integer) val); 243 } else if (val instanceof Long) { 244 generator.writeNumber((Long) val); 245 } else if (val instanceof Float) { 246 generator.writeNumber((Float) val); 247 } else if (val instanceof Double) { 248 generator.writeNumber((Double) val); 249 } else if (val instanceof Boolean) { 250 generator.writeBoolean((Boolean) val); 251 } else { 252 generator.writeString(val.toString()); 253 } 254 return; 255 } 256 257 List<JsonValue> array = jsonValue.getArray(); 258 if (array != null) { 259 generator.writeStartArray(); 260 for (JsonValue element : array) { 261 writeValue(element); 262 } 263 generator.writeEndArray(); 264 return; 265 } 266 267 Map<String, JsonValue> object = jsonValue.getObject(); 268 if (object != null) { 269 generator.writeStartObject(); 270 for (Map.Entry<String, JsonValue> entry : object.entrySet()) { 271 generator.writeFieldName(entry.getKey()); 272 writeValue(entry.getValue()); 273 } 274 generator.writeEndObject(); 275 return; 276 } 277 } 278 279 /** 280 * Flushes the JSON stream. 281 * @throws IOException if there's a problem writing to the output stream 282 */ 283 public void flush() throws IOException { 284 if (generator == null) { 285 return; 286 } 287 288 generator.flush(); 289 } 290 291 /** 292 * Finishes writing the JSON document so that it is syntactically correct. 293 * No more data can be written once this method is called. 294 * @throws IOException if there's a problem closing the output stream 295 */ 296 public void closeJsonStream() throws IOException { 297 if (generator == null) { 298 return; 299 } 300 301 while (open) { 302 writeEndVCard(); 303 } 304 305 if (wrapInArray) { 306 generator.writeEndArray(); 307 } 308 309 if (closeGenerator) { 310 generator.close(); 311 } 312 } 313 314 /** 315 * Finishes writing the JSON document and closes the underlying 316 * {@link Writer}. 317 * @throws IOException if there's a problem closing the output stream 318 */ 319 public void close() throws IOException { 320 if (generator == null) { 321 return; 322 } 323 324 closeJsonStream(); 325 if (writer != null) { 326 writer.close(); 327 } 328 } 329 330 private void init() throws IOException { 331 JsonFactory factory = new JsonFactory(); 332 factory.configure(Feature.AUTO_CLOSE_TARGET, false); 333 generator = factory.createGenerator(writer); 334 335 if (prettyPrint) { 336 if (prettyPrinter == null) { 337 prettyPrinter = new JCardPrettyPrinter(); 338 } 339 generator.setPrettyPrinter(prettyPrinter); 340 } 341 342 if (wrapInArray) { 343 generator.writeStartArray(); 344 } 345 } 346}