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