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