001package ezvcard.io.text; 002 003import java.io.IOException; 004import java.io.InputStream; 005import java.io.InputStreamReader; 006import java.io.Reader; 007import java.io.StringReader; 008import java.nio.charset.Charset; 009import java.nio.file.Files; 010import java.nio.file.Path; 011import java.util.ArrayList; 012import java.util.List; 013 014import com.github.mangstadt.vinnie.VObjectProperty; 015import com.github.mangstadt.vinnie.io.Context; 016import com.github.mangstadt.vinnie.io.SyntaxRules; 017import com.github.mangstadt.vinnie.io.VObjectDataListener; 018import com.github.mangstadt.vinnie.io.VObjectPropertyValues; 019import com.github.mangstadt.vinnie.io.VObjectReader; 020import com.github.mangstadt.vinnie.io.Warning; 021 022import ezvcard.VCard; 023import ezvcard.VCardDataType; 024import ezvcard.VCardVersion; 025import ezvcard.io.CannotParseException; 026import ezvcard.io.EmbeddedVCardException; 027import ezvcard.io.ParseWarning; 028import ezvcard.io.SkipMeException; 029import ezvcard.io.StreamReader; 030import ezvcard.io.scribe.RawPropertyScribe; 031import ezvcard.io.scribe.VCardPropertyScribe; 032import ezvcard.parameter.Encoding; 033import ezvcard.parameter.VCardParameters; 034import ezvcard.property.Address; 035import ezvcard.property.Label; 036import ezvcard.property.VCardProperty; 037import ezvcard.util.IOUtils; 038import ezvcard.util.StringUtils; 039 040/* 041 Copyright (c) 2012-2023, Michael Angstadt 042 All rights reserved. 043 044 Redistribution and use in source and binary forms, with or without 045 modification, are permitted provided that the following conditions are met: 046 047 1. Redistributions of source code must retain the above copyright notice, this 048 list of conditions and the following disclaimer. 049 2. Redistributions in binary form must reproduce the above copyright notice, 050 this list of conditions and the following disclaimer in the documentation 051 and/or other materials provided with the distribution. 052 053 THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND 054 ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 055 WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 056 DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR 057 ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES 058 (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; 059 LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND 060 ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 061 (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS 062 SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 063 064 The views and conclusions contained in the software and documentation are those 065 of the authors and should not be interpreted as representing official policies, 066 either expressed or implied, of the FreeBSD Project. 067 */ 068 069/** 070 * <p> 071 * Parses {@link VCard} objects from a plain-text vCard data stream. 072 * </p> 073 * <p> 074 * <b>Example:</b> 075 * </p> 076 * 077 * <pre class="brush:java"> 078 * Path file = Paths.get("vcards.vcf"); 079 * try (VCardReader reader = new VCardReader(file)) { 080 * VCard vcard; 081 * while ((vcard = reader.readNext()) != null) { 082 * //... 083 * } 084 * } 085 * </pre> 086 * @author Michael Angstadt 087 * @see <a href="http://www.imc.org/pdi/vcard-21.rtf">vCard 2.1</a> 088 * @see <a href="http://tools.ietf.org/html/rfc2426">RFC 2426 (3.0)</a> 089 * @see <a href="http://tools.ietf.org/html/rfc6350">RFC 6350 (4.0)</a> 090 */ 091public class VCardReader extends StreamReader { 092 private final VObjectReader reader; 093 private final VCardVersion defaultVersion; 094 095 /** 096 * Creates a new vCard reader. 097 * @param str the string to read from 098 */ 099 public VCardReader(String str) { 100 this(str, VCardVersion.V2_1); 101 } 102 103 /** 104 * Creates a new vCard reader. 105 * @param str the string to read from 106 * @param defaultVersion the version to assume the vCard is in until a 107 * VERSION property is encountered (defaults to 2.1) 108 */ 109 public VCardReader(String str, VCardVersion defaultVersion) { 110 this(new StringReader(str), defaultVersion); 111 } 112 113 /** 114 * Creates a new vCard reader. 115 * @param in the input stream to read from 116 */ 117 public VCardReader(InputStream in) { 118 this(in, VCardVersion.V2_1); 119 } 120 121 /** 122 * Creates a new vCard reader. 123 * @param in the input stream to read from 124 * @param defaultVersion the version to assume the vCard is in until a 125 * VERSION property is encountered (defaults to 2.1) 126 */ 127 public VCardReader(InputStream in, VCardVersion defaultVersion) { 128 this(new InputStreamReader(in), defaultVersion); 129 } 130 131 /** 132 * Creates a new vCard reader. 133 * @param file the file to read from 134 * @throws IOException if there is a problem opening the file 135 */ 136 public VCardReader(Path file) throws IOException { 137 this(file, VCardVersion.V2_1); 138 } 139 140 /** 141 * Creates a new vCard reader. 142 * @param file the file to read from 143 * @param defaultVersion the version to assume the vCard is in until a 144 * VERSION property is encountered (defaults to 2.1) 145 * @throws IOException if there is a problem opening the file 146 */ 147 public VCardReader(Path file, VCardVersion defaultVersion) throws IOException { 148 this(Files.newBufferedReader(file), defaultVersion); 149 } 150 151 /** 152 * Creates a new vCard reader. 153 * @param reader the reader to read from 154 */ 155 public VCardReader(Reader reader) { 156 this(reader, VCardVersion.V2_1); 157 } 158 159 /** 160 * Creates a new vCard reader. 161 * @param reader the reader to read from 162 * @param defaultVersion the version to assume the vCard is in until a 163 * VERSION property is encountered (defaults to 2.1) 164 */ 165 public VCardReader(Reader reader, VCardVersion defaultVersion) { 166 SyntaxRules rules = SyntaxRules.vcard(); 167 rules.setDefaultSyntaxStyle(defaultVersion.getSyntaxStyle()); 168 this.reader = new VObjectReader(reader, rules); 169 this.defaultVersion = defaultVersion; 170 } 171 172 /** 173 * Gets whether the reader will decode parameter values that use circumflex 174 * accent encoding (enabled by default). This escaping mechanism allows 175 * newlines and double quotes to be included in parameter values. 176 * @return true if circumflex accent decoding is enabled, false if not 177 * @see VObjectReader#isCaretDecodingEnabled() 178 */ 179 public boolean isCaretDecodingEnabled() { 180 return reader.isCaretDecodingEnabled(); 181 } 182 183 /** 184 * Sets whether the reader will decode parameter values that use circumflex 185 * accent encoding (enabled by default). This escaping mechanism allows 186 * newlines and double quotes to be included in parameter values. 187 * @param enable true to use circumflex accent decoding, false not to 188 * @see VObjectReader#setCaretDecodingEnabled(boolean) 189 */ 190 public void setCaretDecodingEnabled(boolean enable) { 191 reader.setCaretDecodingEnabled(enable); 192 } 193 194 /** 195 * Gets the character set to use when the parser cannot determine what 196 * character set to use to decode a quoted-printable property value. 197 * @return the character set 198 * @see VObjectReader#getDefaultQuotedPrintableCharset() 199 */ 200 public Charset getDefaultQuotedPrintableCharset() { 201 return reader.getDefaultQuotedPrintableCharset(); 202 } 203 204 /** 205 * Sets the character set to use when the parser cannot determine what 206 * character set to use to decode a quoted-printable property value. 207 * @param charset the character set (cannot be null) 208 * @see VObjectReader#setDefaultQuotedPrintableCharset 209 */ 210 public void setDefaultQuotedPrintableCharset(Charset charset) { 211 reader.setDefaultQuotedPrintableCharset(charset); 212 } 213 214 @Override 215 protected VCard _readNext() throws IOException { 216 VObjectDataListenerImpl listener = new VObjectDataListenerImpl(); 217 reader.parse(listener); 218 return listener.root; 219 } 220 221 private class VObjectDataListenerImpl implements VObjectDataListener { 222 private VCard root; 223 private final VCardStack stack = new VCardStack(); 224 private EmbeddedVCardException embeddedVCardException; 225 226 public void onComponentBegin(String name, Context context) { 227 if (!isVCardComponent(name)) { 228 //ignore non-VCARD components 229 return; 230 } 231 232 VCard vcard = new VCard(defaultVersion); 233 if (stack.isEmpty()) { 234 root = vcard; 235 } 236 stack.push(vcard); 237 238 if (embeddedVCardException != null) { 239 embeddedVCardException.injectVCard(vcard); 240 embeddedVCardException = null; 241 } 242 } 243 244 public void onComponentEnd(String name, Context context) { 245 if (!isVCardComponent(name)) { 246 //ignore non-VCARD components 247 return; 248 } 249 250 VCardStack.Item item = stack.pop(); 251 assignLabels(item.vcard, item.labels); 252 253 if (stack.isEmpty()) { 254 context.stop(); 255 } 256 } 257 258 public void onProperty(VObjectProperty vobjectProperty, Context vobjectContext) { 259 if (!inVCardComponent(vobjectContext.getParentComponents())) { 260 //ignore properties that are not directly inside a VCARD component 261 return; 262 } 263 264 if (embeddedVCardException != null) { 265 //the next property was supposed to be the start of a nested vCard, but it wasn't 266 embeddedVCardException.injectVCard(null); 267 embeddedVCardException = null; 268 } 269 270 VCard curVCard = stack.peek().vcard; 271 VCardVersion version = curVCard.getVersion(); 272 273 VCardProperty property = parseProperty(vobjectProperty, version, vobjectContext.getLineNumber()); 274 if (property != null) { 275 curVCard.addProperty(property); 276 } 277 } 278 279 private VCardProperty parseProperty(VObjectProperty vobjectProperty, VCardVersion version, int lineNumber) { 280 String group = vobjectProperty.getGroup(); 281 String name = vobjectProperty.getName(); 282 VCardParameters parameters = new VCardParameters(vobjectProperty.getParameters().getMap()); 283 String value = vobjectProperty.getValue(); 284 285 context.getWarnings().clear(); 286 context.setVersion(version); 287 context.setLineNumber(lineNumber); 288 context.setPropertyName(name); 289 290 //sanitize the parameters 291 processNamelessParameters(parameters); 292 processQuotedMultivaluedTypeParams(parameters, version); 293 294 //get the scribe 295 VCardPropertyScribe<? extends VCardProperty> scribe = index.getPropertyScribe(name); 296 if (scribe == null) { 297 scribe = new RawPropertyScribe(name); 298 } 299 300 //get the data type (VALUE parameter) 301 VCardDataType dataType = parameters.getValue(); 302 parameters.setValue(null); 303 if (dataType == null) { 304 //use the default data type if there is no VALUE parameter 305 dataType = scribe.defaultDataType(version); 306 } 307 308 VCardProperty property; 309 try { 310 property = scribe.parseText(value, dataType, parameters, context); 311 warnings.addAll(context.getWarnings()); 312 } catch (SkipMeException e) { 313 handleSkippedProperty(name, lineNumber, e); 314 return null; 315 } catch (CannotParseException e) { 316 property = handleUnparseableProperty(name, parameters, value, dataType, lineNumber, version, e); 317 } catch (EmbeddedVCardException e) { 318 handledEmbeddedVCard(name, value, lineNumber, e); 319 property = e.getProperty(); 320 } 321 322 property.setGroup(group); 323 324 /* 325 * LABEL properties must be treated specially so they can be matched 326 * up with the ADR properties that they belong to. LABELs are not 327 * added to the vCard as properties, they are added to the ADR 328 * properties they belong to (unless they cannot be matched up with 329 * an ADR). 330 */ 331 if (property instanceof Label) { 332 Label label = (Label) property; 333 stack.peek().labels.add(label); 334 return null; 335 } 336 337 handleLabelParameter(property); 338 339 return property; 340 } 341 342 private void handleSkippedProperty(String propertyName, int lineNumber, SkipMeException e) { 343 //@formatter:off 344 warnings.add(new ParseWarning.Builder(context) 345 .message(22, e.getMessage()) 346 .build() 347 ); 348 //@formatter:on 349 } 350 351 private VCardProperty handleUnparseableProperty(String name, VCardParameters parameters, String value, VCardDataType dataType, int lineNumber, VCardVersion version, CannotParseException e) { 352 //@formatter:off 353 warnings.add(new ParseWarning.Builder(context) 354 .message(e) 355 .build() 356 ); 357 //@formatter:on 358 359 RawPropertyScribe scribe = new RawPropertyScribe(name); 360 return scribe.parseText(value, dataType, parameters, null); 361 } 362 363 private void handledEmbeddedVCard(String name, String value, int lineNumber, EmbeddedVCardException exception) { 364 /* 365 * If the property does not have a value, a nested vCard is expected 366 * to be next (2.1 style). 367 */ 368 if (value.trim().isEmpty()) { 369 embeddedVCardException = exception; 370 return; 371 } 372 373 /* 374 * If the property does have a value, the property value should be 375 * an embedded vCard (3.0 style). 376 */ 377 value = VObjectPropertyValues.unescape(value); 378 379 VCardReader agentReader = new VCardReader(value); 380 agentReader.setCaretDecodingEnabled(isCaretDecodingEnabled()); 381 agentReader.setDefaultQuotedPrintableCharset(getDefaultQuotedPrintableCharset()); 382 agentReader.setScribeIndex(index); 383 384 try { 385 VCard nestedVCard = agentReader.readNext(); 386 if (nestedVCard != null) { 387 exception.injectVCard(nestedVCard); 388 } 389 } catch (IOException ignore) { 390 //shouldn't be thrown because we're reading from a string 391 } finally { 392 warnings.addAll(agentReader.getWarnings()); 393 IOUtils.closeQuietly(agentReader); 394 } 395 } 396 397 /** 398 * <p> 399 * Unescapes newline sequences in the LABEL parameter of {@link Address} 400 * properties. Newlines cannot normally be escaped in parameter values. 401 * </p> 402 * <p> 403 * Only version 4.0 allows this (and only version 4.0 defines a LABEL 404 * parameter), but do this for all versions for compatibility. 405 * </p> 406 * @param property the property 407 */ 408 private void handleLabelParameter(VCardProperty property) { 409 if (!(property instanceof Address)) { 410 return; 411 } 412 413 Address adr = (Address) property; 414 String label = adr.getLabel(); 415 if (label == null) { 416 return; 417 } 418 419 label = label.replace("\\n", StringUtils.NEWLINE); 420 adr.setLabel(label); 421 } 422 423 public void onVersion(String value, Context vobjectContext) { 424 VCardVersion version = VCardVersion.valueOfByStr(value); 425 context.setVersion(version); 426 stack.peek().vcard.setVersion(version); 427 } 428 429 public void onWarning(Warning warning, VObjectProperty property, Exception thrown, Context vobjectContext) { 430 if (!inVCardComponent(vobjectContext.getParentComponents())) { 431 //ignore warnings that are not directly inside a VCARD component 432 return; 433 } 434 435 //@formatter:off 436 warnings.add(new ParseWarning.Builder(context) 437 .lineNumber(vobjectContext.getLineNumber()) 438 .propertyName((property == null) ? null : property.getName()) 439 .message(27, warning.getMessage(), vobjectContext.getUnfoldedLine()) 440 .build() 441 ); 442 //@formatter:on 443 } 444 445 private boolean inVCardComponent(List<String> parentComponents) { 446 if (parentComponents.isEmpty()) { 447 return false; 448 } 449 String last = parentComponents.get(parentComponents.size() - 1); 450 return isVCardComponent(last); 451 } 452 453 private boolean isVCardComponent(String componentName) { 454 return "VCARD".equals(componentName); 455 } 456 457 /** 458 * Assigns names to all nameless parameters. v3.0 and v4.0 require all 459 * parameters to have names, but v2.1 does not. 460 * @param parameters the parameters 461 */ 462 private void processNamelessParameters(VCardParameters parameters) { 463 List<String> namelessValues = parameters.removeAll(null); 464 for (String value : namelessValues) { 465 String name = guessParameterName(value); 466 parameters.put(name, value); 467 } 468 } 469 470 /** 471 * Makes a guess as to what a parameter value's name should be. 472 * @param value the parameter value (e.g. "HOME") 473 * @return the guessed name (e.g. "TYPE") 474 */ 475 private String guessParameterName(String value) { 476 if (VCardDataType.find(value) != null) { 477 return VCardParameters.VALUE; 478 } 479 480 if (Encoding.find(value) != null) { 481 return VCardParameters.ENCODING; 482 } 483 484 //otherwise, assume it's a TYPE 485 return VCardParameters.TYPE; 486 } 487 488 /** 489 * <p> 490 * Accounts for multi-valued TYPE parameters being enclosed entirely in 491 * double quotes (for example: ADR;TYPE="home,work"). 492 * </p> 493 * <p> 494 * Many examples throughout the 4.0 specs show TYPE parameters being 495 * encoded in this way. This conflicts with the ABNF and is noted in the 496 * errata. This method will parse these incorrectly-formatted TYPE 497 * parameters as if they were multi-valued, even though, technically, 498 * they are not. 499 * </p> 500 * @param parameters the parameters 501 * @param version the version 502 */ 503 private void processQuotedMultivaluedTypeParams(VCardParameters parameters, VCardVersion version) { 504 if (version == VCardVersion.V2_1) { 505 return; 506 } 507 508 List<String> types = parameters.getTypes(); 509 if (types.isEmpty()) { 510 return; 511 } 512 513 String valueWithComma = null; 514 for (String value : types) { 515 if (value.indexOf(',') >= 0) { 516 valueWithComma = value; 517 break; 518 } 519 } 520 if (valueWithComma == null) { 521 return; 522 } 523 524 types.clear(); 525 int prev = -1, cur; 526 while ((cur = valueWithComma.indexOf(',', prev + 1)) >= 0) { 527 types.add(valueWithComma.substring(prev + 1, cur)); 528 prev = cur; 529 } 530 types.add(valueWithComma.substring(prev + 1)); 531 } 532 } 533 534 /** 535 * Keeps track of the hierarchy of nested vCards. 536 */ 537 private static class VCardStack { 538 private final List<Item> stack = new ArrayList<>(); 539 540 /** 541 * Adds a vCard to the stack. 542 * @param vcard the vcard to add 543 */ 544 public void push(VCard vcard) { 545 stack.add(new Item(vcard, new ArrayList<>())); 546 } 547 548 /** 549 * Removes the top item from the stack and returns it. 550 * @return the last item or null if the stack is empty 551 */ 552 public Item pop() { 553 return isEmpty() ? null : stack.remove(stack.size() - 1); 554 } 555 556 /** 557 * Gets the top item of the stack. 558 * @return the top item 559 */ 560 public Item peek() { 561 return isEmpty() ? null : stack.get(stack.size() - 1); 562 } 563 564 /** 565 * Determines if the stack is empty. 566 * @return true if it's empty, false if not 567 */ 568 public boolean isEmpty() { 569 return stack.isEmpty(); 570 } 571 572 private static class Item { 573 public final VCard vcard; 574 public final List<Label> labels; 575 576 public Item(VCard vcard, List<Label> labels) { 577 this.vcard = vcard; 578 this.labels = labels; 579 } 580 } 581 } 582 583 /** 584 * Closes the input stream. 585 * @throws IOException if there's a problem closing the input stream 586 */ 587 public void close() throws IOException { 588 reader.close(); 589 } 590}