001package ezvcard.io.json; 002 003import java.io.Closeable; 004import java.io.IOException; 005import java.io.Reader; 006import java.util.ArrayList; 007import java.util.HashMap; 008import java.util.List; 009import java.util.Map; 010 011import com.fasterxml.jackson.core.JsonFactory; 012import com.fasterxml.jackson.core.JsonParseException; 013import com.fasterxml.jackson.core.JsonParser; 014import com.fasterxml.jackson.core.JsonToken; 015 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 * Parses an vCard JSON data stream (jCard). 046 * @author Michael Angstadt 047 * @see <a href="http://tools.ietf.org/html/rfc7095">RFC 7095</a> 048 */ 049public class JCardRawReader implements Closeable { 050 private final Reader reader; 051 private JsonParser parser; 052 private boolean eof = false; 053 private JCardDataStreamListener listener; 054 private boolean strict = false; 055 056 /** 057 * @param reader the reader to wrap 058 */ 059 public JCardRawReader(Reader reader) { 060 this.reader = reader; 061 } 062 063 /** 064 * @param parser the parser to read from 065 * @param strict true if the parser's current token is expected to be 066 * positioned at the start of a jCard, false if not. If this is true, and 067 * the parser is not positioned at the beginning of a jCard, a 068 * {@link JCardParseException} will be thrown. If this if false, the parser 069 * will consume input until it reaches the beginning of a jCard. 070 */ 071 public JCardRawReader(JsonParser parser, boolean strict) { 072 reader = null; 073 this.parser = parser; 074 this.strict = strict; 075 } 076 077 /** 078 * Gets the current line number. 079 * @return the line number 080 */ 081 public int getLineNum() { 082 return (parser == null) ? 0 : parser.getCurrentLocation().getLineNr(); 083 } 084 085 /** 086 * Reads the next vCard from the jCard data stream. 087 * @param listener handles the vCard data as it is read off the wire 088 * @throws JCardParseException if the jCard syntax is incorrect (the JSON 089 * syntax may be valid, but it is not in the correct jCard format). 090 * @throws JsonParseException if the JSON syntax is incorrect 091 * @throws IOException if there is a problem reading from the input stream 092 */ 093 public void readNext(JCardDataStreamListener listener) throws IOException { 094 if (parser == null) { 095 JsonFactory factory = new JsonFactory(); 096 parser = factory.createParser(reader); 097 } else if (parser.isClosed()) { 098 return; 099 } 100 101 this.listener = listener; 102 103 //find the next vCard object 104 JsonToken prev = parser.getCurrentToken(); 105 JsonToken cur; 106 while ((cur = parser.nextToken()) != null) { 107 if (prev == JsonToken.START_ARRAY && cur == JsonToken.VALUE_STRING && "vcard".equals(parser.getValueAsString())) { 108 //found 109 break; 110 } 111 112 if (strict) { 113 //the parser was expecting the jCard to be there 114 if (prev != JsonToken.START_ARRAY) { 115 throw new JCardParseException(JsonToken.START_ARRAY, prev); 116 } 117 118 if (cur != JsonToken.VALUE_STRING) { 119 throw new JCardParseException(JsonToken.VALUE_STRING, cur); 120 } 121 122 throw new JCardParseException("Invalid value for first token: expected \"vcard\" , was \"" + parser.getValueAsString() + "\"", JsonToken.VALUE_STRING, cur); 123 } 124 125 prev = cur; 126 } 127 128 if (cur == null) { 129 //EOF 130 eof = true; 131 return; 132 } 133 134 listener.beginVCard(); 135 parseProperties(); 136 137 check(JsonToken.END_ARRAY, parser.nextToken()); 138 } 139 140 private void parseProperties() throws IOException { 141 //start properties array 142 checkNext(JsonToken.START_ARRAY); 143 144 //read properties 145 while (parser.nextToken() != JsonToken.END_ARRAY) { //until we reach the end properties array 146 checkCurrent(JsonToken.START_ARRAY); 147 parser.nextToken(); 148 parseProperty(); 149 } 150 } 151 152 private void parseProperty() throws IOException { 153 //get property name 154 checkCurrent(JsonToken.VALUE_STRING); 155 String propertyName = parser.getValueAsString().toLowerCase(); 156 157 //get parameters 158 VCardParameters parameters = parseParameters(); 159 160 //get group 161 List<String> removed = parameters.removeAll("group"); 162 String group = removed.isEmpty() ? null : removed.get(0); 163 164 //get data type 165 checkNext(JsonToken.VALUE_STRING); 166 String dataTypeStr = parser.getText().toLowerCase(); 167 VCardDataType dataType = "unknown".equals(dataTypeStr) ? null : VCardDataType.get(dataTypeStr); 168 169 //get property value(s) 170 List<JsonValue> values = parseValues(); 171 172 JCardValue value = new JCardValue(values); 173 listener.readProperty(group, propertyName, parameters, dataType, value); 174 } 175 176 private VCardParameters parseParameters() throws IOException { 177 checkNext(JsonToken.START_OBJECT); 178 179 VCardParameters parameters = new VCardParameters(); 180 while (parser.nextToken() != JsonToken.END_OBJECT) { 181 String parameterName = parser.getText(); 182 183 if (parser.nextToken() == JsonToken.START_ARRAY) { 184 //multi-valued parameter 185 while (parser.nextToken() != JsonToken.END_ARRAY) { 186 parameters.put(parameterName, parser.getText()); 187 } 188 } else { 189 parameters.put(parameterName, parser.getValueAsString()); 190 } 191 } 192 193 return parameters; 194 } 195 196 private List<JsonValue> parseValues() throws IOException { 197 List<JsonValue> values = new ArrayList<>(); 198 while (parser.nextToken() != JsonToken.END_ARRAY) { //until we reach the end of the property array 199 JsonValue value = parseValue(); 200 values.add(value); 201 } 202 return values; 203 } 204 205 private Object parseValueElement() throws IOException { 206 switch (parser.getCurrentToken()) { 207 case VALUE_FALSE: 208 case VALUE_TRUE: 209 return parser.getBooleanValue(); 210 case VALUE_NUMBER_FLOAT: 211 return parser.getDoubleValue(); 212 case VALUE_NUMBER_INT: 213 return parser.getLongValue(); 214 case VALUE_NULL: 215 return null; 216 default: 217 return parser.getText(); 218 } 219 } 220 221 private List<JsonValue> parseValueArray() throws IOException { 222 List<JsonValue> array = new ArrayList<>(); 223 224 while (parser.nextToken() != JsonToken.END_ARRAY) { 225 JsonValue value = parseValue(); 226 array.add(value); 227 } 228 229 return array; 230 } 231 232 private Map<String, JsonValue> parseValueObject() throws IOException { 233 Map<String, JsonValue> object = new HashMap<>(); 234 235 while (parser.nextToken() != JsonToken.END_OBJECT) { 236 checkCurrent(JsonToken.FIELD_NAME); 237 238 String key = parser.getText(); 239 parser.nextToken(); 240 JsonValue value = parseValue(); 241 object.put(key, value); 242 } 243 244 return object; 245 } 246 247 private JsonValue parseValue() throws IOException { 248 switch (parser.getCurrentToken()) { 249 case START_ARRAY: 250 return new JsonValue(parseValueArray()); 251 case START_OBJECT: 252 return new JsonValue(parseValueObject()); 253 default: 254 return new JsonValue(parseValueElement()); 255 } 256 } 257 258 private void checkNext(JsonToken expected) throws IOException { 259 JsonToken actual = parser.nextToken(); 260 check(expected, actual); 261 } 262 263 private void checkCurrent(JsonToken expected) throws JCardParseException { 264 JsonToken actual = parser.getCurrentToken(); 265 check(expected, actual); 266 } 267 268 private void check(JsonToken expected, JsonToken actual) throws JCardParseException { 269 if (actual != expected) { 270 throw new JCardParseException(expected, actual); 271 } 272 } 273 274 /** 275 * Determines whether the end of the data stream has been reached. 276 * @return true if the end has been reached, false if not 277 */ 278 public boolean eof() { 279 return eof; 280 } 281 282 /** 283 * Handles the vCard data as it is read off the data stream. 284 * @author Michael Angstadt 285 */ 286 public interface JCardDataStreamListener { 287 /** 288 * Called when a vCard has been found in the stream. 289 */ 290 void beginVCard(); 291 292 /** 293 * Called when a property is read. 294 * @param group the group or null if there is not group 295 * @param propertyName the property name (e.g. "summary") 296 * @param parameters the parameters 297 * @param dataType the data type or null for "unknown" 298 * @param value the property value 299 */ 300 void readProperty(String group, String propertyName, VCardParameters parameters, VCardDataType dataType, JCardValue value); 301 } 302 303 /** 304 * Closes the underlying {@link Reader} object. 305 */ 306 public void close() throws IOException { 307 if (parser != null) { 308 parser.close(); 309 } 310 if (reader != null) { 311 reader.close(); 312 } 313 } 314}