001 package ezvcard.io.text; 002 003 import java.io.Closeable; 004 import java.io.File; 005 import java.io.FileNotFoundException; 006 import java.io.FileReader; 007 import java.io.IOException; 008 import java.io.InputStream; 009 import java.io.InputStreamReader; 010 import java.io.Reader; 011 import java.io.StringReader; 012 import java.nio.charset.Charset; 013 import java.nio.charset.IllegalCharsetNameException; 014 import java.nio.charset.UnsupportedCharsetException; 015 import java.util.ArrayList; 016 import java.util.LinkedList; 017 import java.util.List; 018 019 import ezvcard.Messages; 020 import ezvcard.VCard; 021 import ezvcard.VCardDataType; 022 import ezvcard.VCardVersion; 023 import ezvcard.io.CannotParseException; 024 import ezvcard.io.EmbeddedVCardException; 025 import ezvcard.io.SkipMeException; 026 import ezvcard.io.scribe.RawPropertyScribe; 027 import ezvcard.io.scribe.ScribeIndex; 028 import ezvcard.io.scribe.VCardPropertyScribe; 029 import ezvcard.io.scribe.VCardPropertyScribe.Result; 030 import ezvcard.parameter.Encoding; 031 import ezvcard.parameter.VCardParameters; 032 import ezvcard.property.Address; 033 import ezvcard.property.Label; 034 import ezvcard.property.RawProperty; 035 import ezvcard.property.VCardProperty; 036 import ezvcard.util.IOUtils; 037 import ezvcard.util.org.apache.commons.codec.DecoderException; 038 import ezvcard.util.org.apache.commons.codec.net.QuotedPrintableCodec; 039 040 /* 041 Copyright (c) 2013, 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 * 076 * <pre class="brush:java"> 077 * File file = new File("vcards.vcf"); 078 * VCardReader vcardReader = new VCardReader(file); 079 * VCard vcard; 080 * while ((vcard = vcardReader.readNext()) != null){ 081 * ... 082 * } 083 * vcardReader.close(); 084 * </pre> 085 * 086 * </p> 087 * @author Michael Angstadt 088 */ 089 public class VCardReader implements Closeable { 090 private final List<String> warnings = new ArrayList<String>(); 091 private ScribeIndex index = new ScribeIndex(); 092 private final VCardRawReader reader; 093 094 /** 095 * Creates a reader that parses vCards from a string. 096 * @param str the string to read the vCards from 097 */ 098 public VCardReader(String str) { 099 this(new StringReader(str)); 100 } 101 102 /** 103 * Creates a reader that parses vCards from an input stream. 104 * @param in the input stream to read the vCards from 105 */ 106 public VCardReader(InputStream in) { 107 this(new InputStreamReader(in)); 108 } 109 110 /** 111 * Creates a reader that parses vCards from a file. 112 * @param file the file to read the vCards from 113 * @throws FileNotFoundException if the file doesn't exist 114 */ 115 public VCardReader(File file) throws FileNotFoundException { 116 this(new FileReader(file)); 117 } 118 119 /** 120 * Creates a reader that parses vCards from a reader. 121 * @param reader the reader to read the vCards from 122 */ 123 public VCardReader(Reader reader) { 124 this.reader = new VCardRawReader(reader); 125 } 126 127 /** 128 * Gets whether the reader will decode parameter values that use circumflex 129 * accent encoding (enabled by default). This escaping mechanism allows 130 * newlines and double quotes to be included in parameter values. 131 * @return true if circumflex accent decoding is enabled, false if not 132 * @see VCardRawReader#isCaretDecodingEnabled() 133 */ 134 public boolean isCaretDecodingEnabled() { 135 return reader.isCaretDecodingEnabled(); 136 } 137 138 /** 139 * Sets whether the reader will decode parameter values that use circumflex 140 * accent encoding (enabled by default). This escaping mechanism allows 141 * newlines and double quotes to be included in parameter values. 142 * @param enable true to use circumflex accent decoding, false not to 143 * @see VCardRawReader#setCaretDecodingEnabled(boolean) 144 */ 145 public void setCaretDecodingEnabled(boolean enable) { 146 reader.setCaretDecodingEnabled(enable); 147 } 148 149 /** 150 * <p> 151 * Registers a property scribe. This is the same as calling: 152 * </p> 153 * <p> 154 * {@code getScribeIndex().register(scribe)} 155 * </p> 156 * @param scribe the scribe to register 157 */ 158 public void registerScribe(VCardPropertyScribe<? extends VCardProperty> scribe) { 159 index.register(scribe); 160 } 161 162 /** 163 * Gets the scribe index. 164 * @return the scribe index 165 */ 166 public ScribeIndex getScribeIndex() { 167 return index; 168 } 169 170 /** 171 * Sets the scribe index. 172 * @param index the scribe index 173 */ 174 public void setScribeIndex(ScribeIndex index) { 175 this.index = index; 176 } 177 178 /** 179 * Gets the warnings from the last vCard that was unmarshalled. This list is 180 * reset every time a new vCard is read. 181 * @return the warnings or empty list if there were no warnings 182 */ 183 public List<String> getWarnings() { 184 return new ArrayList<String>(warnings); 185 } 186 187 /** 188 * Reads the next vCard from the data stream. 189 * @return the next vCard or null if there are no more 190 * @throws IOException if there's a problem reading from the stream 191 */ 192 public VCard readNext() throws IOException { 193 if (reader.eof()) { 194 return null; 195 } 196 197 warnings.clear(); 198 199 VCardDataStreamListenerImpl listener = new VCardDataStreamListenerImpl(); 200 reader.start(listener); 201 202 return listener.root; 203 } 204 205 /** 206 * Assigns names to all nameless parameters. v3.0 and v4.0 requires all 207 * parameters to have names, but v2.1 does not. 208 * @param parameters the parameters 209 */ 210 private void handleNamelessParameters(VCardParameters parameters) { 211 List<String> namelessParamValues = parameters.get(null); 212 for (String paramValue : namelessParamValues) { 213 String paramName; 214 if (VCardDataType.find(paramValue) != null) { 215 paramName = VCardParameters.VALUE; 216 } else if (Encoding.find(paramValue) != null) { 217 paramName = VCardParameters.ENCODING; 218 } else { 219 //otherwise, assume it's a TYPE 220 paramName = VCardParameters.TYPE; 221 } 222 parameters.put(paramName, paramValue); 223 } 224 parameters.removeAll(null); 225 } 226 227 /** 228 * <p> 229 * Accounts for multi-valued TYPE parameters being enclosed entirely in 230 * double quotes (for example: ADR;TYPE="home,work"). 231 * </p> 232 * <p> 233 * Many examples throughout the 4.0 specs show TYPE parameters being encoded 234 * in this way. This conflicts with the ABNF and is noted in the errata. 235 * This method will split the value by comma incase the vendor implemented 236 * it this way. 237 * </p> 238 * @param parameters the parameters 239 */ 240 private void handleQuotedMultivaluedTypeParams(VCardParameters parameters) { 241 //account for multi-valued TYPE parameters being enclosed entirely in double quotes 242 //e.g. ADR;TYPE="home,work" 243 for (String typeParam : parameters.getTypes()) { 244 if (!typeParam.contains(",")) { 245 continue; 246 } 247 248 parameters.removeTypes(); 249 for (String splitValue : typeParam.split(",")) { 250 parameters.addType(splitValue); 251 } 252 } 253 } 254 255 /** 256 * Decodes the property value if it's encoded in quoted-printable encoding. 257 * Quoted-printable encoding is only supported in v2.1. 258 * @param name the property name 259 * @param parameters the parameters 260 * @param value the property value 261 * @return the decoded property value 262 */ 263 private String decodeQuotedPrintable(String name, VCardParameters parameters, String value) { 264 if (parameters.getEncoding() != Encoding.QUOTED_PRINTABLE) { 265 return value; 266 } 267 268 //remove encoding parameter 269 parameters.setEncoding(null); 270 271 //determine the character set 272 Charset charset = null; 273 String charsetStr = parameters.getCharset(); 274 if (charsetStr != null) { 275 try { 276 charset = Charset.forName(charsetStr); 277 } catch (IllegalCharsetNameException e) { 278 charset = null; 279 } catch (UnsupportedCharsetException e) { 280 charset = null; 281 } 282 } 283 if (charset == null) { 284 charset = reader.getEncoding(); 285 if (charset == null) { 286 charset = Charset.defaultCharset(); 287 } 288 if (charsetStr != null) { 289 //the given charset was invalid, so add a warning 290 addWarning(name, 23, charsetStr, charset); 291 } 292 } 293 294 QuotedPrintableCodec codec = new QuotedPrintableCodec(charset.name()); 295 try { 296 return codec.decode(value); 297 } catch (DecoderException e) { 298 //only thrown if the charset is invalid, which we know will never happen because we're using a Charset object 299 throw new RuntimeException(e); 300 } 301 } 302 303 /** 304 * Closes the underlying {@link Reader} object. 305 */ 306 public void close() throws IOException { 307 reader.close(); 308 } 309 310 private void addWarning(String propertyName, int code, Object... args) { 311 String message = Messages.INSTANCE.getParseMessage(code, args); 312 addWarning(propertyName, message); 313 } 314 315 private void addWarning(String propertyName, String message) { 316 int code = (propertyName == null) ? 37 : 36; 317 int line = reader.getLineNum(); 318 319 String warning = Messages.INSTANCE.getParseMessage(code, line, propertyName, message); 320 warnings.add(warning); 321 } 322 323 private class VCardDataStreamListenerImpl implements VCardRawReader.VCardDataStreamListener { 324 private VCard root; 325 private final List<Label> labels = new ArrayList<Label>(); 326 private final LinkedList<VCard> vcardStack = new LinkedList<VCard>(); 327 private EmbeddedVCardException embeddedVCardException; 328 329 public void beginComponent(String name) { 330 if (!"VCARD".equalsIgnoreCase(name)) { 331 return; 332 } 333 334 VCard vcard = new VCard(); 335 336 //initialize version to 2.1, since the VERSION property can exist anywhere in a 2.1 vCard 337 vcard.setVersion(VCardVersion.V2_1); 338 339 vcardStack.add(vcard); 340 341 if (root == null) { 342 root = vcard; 343 } 344 345 if (embeddedVCardException != null) { 346 embeddedVCardException.injectVCard(vcard); 347 embeddedVCardException = null; 348 } 349 } 350 351 public void readVersion(VCardVersion version) { 352 if (vcardStack.isEmpty()) { 353 //not in a "VCARD" component 354 return; 355 } 356 357 vcardStack.getLast().setVersion(version); 358 } 359 360 public void readProperty(String group, String name, VCardParameters parameters, String value) { 361 if (vcardStack.isEmpty()) { 362 //not in a "VCARD" component 363 return; 364 } 365 366 if (embeddedVCardException != null) { 367 //the next property was supposed to be the start of a nested vCard, but it wasn't 368 embeddedVCardException.injectVCard(null); 369 embeddedVCardException = null; 370 } 371 372 VCard curVCard = vcardStack.getLast(); 373 VCardVersion version = curVCard.getVersion(); 374 375 //massage the parameters 376 handleNamelessParameters(parameters); 377 handleQuotedMultivaluedTypeParams(parameters); 378 379 //decode property value from quoted-printable 380 value = decodeQuotedPrintable(name, parameters, value); 381 382 //get the scribe 383 VCardPropertyScribe<? extends VCardProperty> scribe = index.getPropertyScribe(name); 384 if (scribe == null) { 385 scribe = new RawPropertyScribe(name); 386 } 387 388 //get the data type 389 VCardDataType dataType = parameters.getValue(); 390 if (dataType == null) { 391 //use the default data type if there is no VALUE parameter 392 dataType = scribe.defaultDataType(version); 393 } else { 394 //remove VALUE parameter if it is set 395 parameters.setValue(null); 396 } 397 398 VCardProperty property; 399 try { 400 Result<? extends VCardProperty> result = scribe.parseText(value, dataType, version, parameters); 401 402 for (String warning : result.getWarnings()) { 403 addWarning(name, warning); 404 } 405 406 property = result.getProperty(); 407 property.setGroup(group); 408 409 if (property instanceof Label) { 410 //LABELs must be treated specially so they can be matched up with their ADRs 411 labels.add((Label) property); 412 return; 413 } 414 } catch (SkipMeException e) { 415 addWarning(name, 22, e.getMessage()); 416 return; 417 } catch (CannotParseException e) { 418 addWarning(name, 25, value, e.getMessage()); 419 property = new RawProperty(name, value); 420 property.setGroup(group); 421 } catch (EmbeddedVCardException e) { 422 //parse an embedded vCard (i.e. the AGENT type) 423 property = e.getProperty(); 424 425 if (value.length() == 0 || version == VCardVersion.V2_1) { 426 //a nested vCard is expected to be next (2.1 style) 427 embeddedVCardException = e; 428 } else { 429 //the property value should be an embedded vCard (3.0 style) 430 value = VCardPropertyScribe.unescape(value); 431 432 VCardReader agentReader = new VCardReader(value); 433 try { 434 VCard nestedVCard = agentReader.readNext(); 435 if (nestedVCard != null) { 436 e.injectVCard(nestedVCard); 437 } 438 } catch (IOException e2) { 439 //shouldn't be thrown because we're reading from a string 440 } finally { 441 for (String w : agentReader.getWarnings()) { 442 addWarning(name, 26, w); 443 } 444 IOUtils.closeQuietly(agentReader); 445 } 446 } 447 } 448 449 curVCard.addProperty(property); 450 } 451 452 public void endComponent(String name) { 453 if (vcardStack.isEmpty()) { 454 //not in a "VCARD" component 455 return; 456 } 457 458 if (!"VCARD".equalsIgnoreCase(name)) { 459 //not a "VCARD" component 460 return; 461 } 462 463 VCard curVCard = vcardStack.removeLast(); 464 465 //assign labels to their addresses 466 for (Label label : labels) { 467 boolean orphaned = true; 468 for (Address adr : curVCard.getAddresses()) { 469 if (adr.getLabel() == null && adr.getTypes().equals(label.getTypes())) { 470 adr.setLabel(label.getValue()); 471 orphaned = false; 472 break; 473 } 474 } 475 if (orphaned) { 476 curVCard.addOrphanedLabel(label); 477 } 478 } 479 480 if (vcardStack.isEmpty()) { 481 throw new VCardRawReader.StopReadingException(); 482 } 483 } 484 485 public void invalidLine(String line) { 486 if (vcardStack.isEmpty()) { 487 //not in a "VCARD" component 488 return; 489 } 490 491 addWarning(null, 27, line); 492 } 493 494 public void invalidVersion(String version) { 495 if (vcardStack.isEmpty()) { 496 //not in a "VCARD" component 497 return; 498 } 499 500 addWarning("VERSION", 28, version); 501 } 502 } 503 }