1 /* 2 * Copyright (C) 2010 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 package com.android.vcard; 17 18 import android.text.TextUtils; 19 import android.util.Log; 20 21 import com.android.vcard.exception.VCardAgentNotSupportedException; 22 import com.android.vcard.exception.VCardException; 23 import com.android.vcard.exception.VCardInvalidCommentLineException; 24 import com.android.vcard.exception.VCardInvalidLineException; 25 import com.android.vcard.exception.VCardNestedException; 26 import com.android.vcard.exception.VCardVersionException; 27 28 import java.io.BufferedReader; 29 import java.io.IOException; 30 import java.io.InputStream; 31 import java.io.InputStreamReader; 32 import java.io.Reader; 33 import java.util.ArrayList; 34 import java.util.HashSet; 35 import java.util.List; 36 import java.util.Set; 37 38 /** 39 * <p> 40 * Basic implementation achieving vCard parsing. Based on vCard 2.1, 41 * </p> 42 * @hide 43 */ 44 /* package */ class VCardParserImpl_V21 { 45 private static final String LOG_TAG = "VCardParserImpl_V21"; 46 47 private static final class EmptyInterpreter implements VCardInterpreter { 48 @Override 49 public void end() { 50 } 51 @Override 52 public void endEntry() { 53 } 54 @Override 55 public void endProperty() { 56 } 57 @Override 58 public void propertyGroup(String group) { 59 } 60 @Override 61 public void propertyName(String name) { 62 } 63 @Override 64 public void propertyParamType(String type) { 65 } 66 @Override 67 public void propertyParamValue(String value) { 68 } 69 @Override 70 public void propertyValues(List<String> values) { 71 } 72 @Override 73 public void start() { 74 } 75 @Override 76 public void startEntry() { 77 } 78 @Override 79 public void startProperty() { 80 } 81 } 82 83 protected static final class CustomBufferedReader extends BufferedReader { 84 private long mTime; 85 86 /** 87 * Needed since "next line" may be null due to end of line. 88 */ 89 private boolean mNextLineIsValid; 90 private String mNextLine; 91 92 public CustomBufferedReader(Reader in) { 93 super(in); 94 } 95 96 @Override 97 public String readLine() throws IOException { 98 if (mNextLineIsValid) { 99 final String ret = mNextLine; 100 mNextLine = null; 101 mNextLineIsValid = false; 102 return ret; 103 } 104 105 long start = System.currentTimeMillis(); 106 final String line = super.readLine(); 107 long end = System.currentTimeMillis(); 108 mTime += end - start; 109 return line; 110 } 111 112 /** 113 * Read one line, but make this object store it in its queue. 114 */ 115 public String peekLine() throws IOException { 116 if (!mNextLineIsValid) { 117 long start = System.currentTimeMillis(); 118 final String line = super.readLine(); 119 long end = System.currentTimeMillis(); 120 mTime += end - start; 121 122 mNextLine = line; 123 mNextLineIsValid = true; 124 } 125 126 return mNextLine; 127 } 128 129 public long getTotalmillisecond() { 130 return mTime; 131 } 132 } 133 134 private static final String DEFAULT_ENCODING = "8BIT"; 135 136 protected boolean mCanceled; 137 protected VCardInterpreter mInterpreter; 138 139 protected final String mIntermediateCharset; 140 141 /** 142 * <p> 143 * The encoding type for deconding byte streams. This member variable is 144 * reset to a default encoding every time when a new item comes. 145 * </p> 146 * <p> 147 * "Encoding" in vCard is different from "Charset". It is mainly used for 148 * addresses, notes, images. "7BIT", "8BIT", "BASE64", and 149 * "QUOTED-PRINTABLE" are known examples. 150 * </p> 151 */ 152 protected String mCurrentEncoding; 153 154 /** 155 * <p> 156 * The reader object to be used internally. 157 * </p> 158 * <p> 159 * Developers should not directly read a line from this object. Use 160 * getLine() unless there some reason. 161 * </p> 162 */ 163 protected CustomBufferedReader mReader; 164 165 /** 166 * <p> 167 * Set for storing unkonwn TYPE attributes, which is not acceptable in vCard 168 * specification, but happens to be seen in real world vCard. 169 * </p> 170 */ 171 protected final Set<String> mUnknownTypeSet = new HashSet<String>(); 172 173 /** 174 * <p> 175 * Set for storing unkonwn VALUE attributes, which is not acceptable in 176 * vCard specification, but happens to be seen in real world vCard. 177 * </p> 178 */ 179 protected final Set<String> mUnknownValueSet = new HashSet<String>(); 180 181 182 // In some cases, vCard is nested. Currently, we only consider the most 183 // interior vCard data. 184 // See v21_foma_1.vcf in test directory for more information. 185 // TODO: Don't ignore by using count, but read all of information outside vCard. 186 private int mNestCount; 187 188 // Used only for parsing END:VCARD. 189 private String mPreviousLine; 190 191 // For measuring performance. 192 private long mTimeTotal; 193 private long mTimeReadStartRecord; 194 private long mTimeReadEndRecord; 195 private long mTimeStartProperty; 196 private long mTimeEndProperty; 197 private long mTimeParseItems; 198 private long mTimeParseLineAndHandleGroup; 199 private long mTimeParsePropertyValues; 200 private long mTimeParseAdrOrgN; 201 private long mTimeHandleMiscPropertyValue; 202 private long mTimeHandleQuotedPrintable; 203 private long mTimeHandleBase64; 204 205 public VCardParserImpl_V21() { 206 this(VCardConfig.VCARD_TYPE_DEFAULT); 207 } 208 209 public VCardParserImpl_V21(int vcardType) { 210 if ((vcardType & VCardConfig.FLAG_TORELATE_NEST) != 0) { 211 mNestCount = 1; 212 } 213 214 mIntermediateCharset = VCardConfig.DEFAULT_INTERMEDIATE_CHARSET; 215 } 216 217 /** 218 * <p> 219 * Parses the file at the given position. 220 * </p> 221 */ 222 // <pre class="prettyprint">vcard_file = [wsls] vcard [wsls]</pre> 223 protected void parseVCardFile() throws IOException, VCardException { 224 boolean readingFirstFile = true; 225 while (true) { 226 if (mCanceled) { 227 break; 228 } 229 if (!parseOneVCard(readingFirstFile)) { 230 break; 231 } 232 readingFirstFile = false; 233 } 234 235 if (mNestCount > 0) { 236 boolean useCache = true; 237 for (int i = 0; i < mNestCount; i++) { 238 readEndVCard(useCache, true); 239 useCache = false; 240 } 241 } 242 } 243 244 /** 245 * @return true when a given property name is a valid property name. 246 */ 247 protected boolean isValidPropertyName(final String propertyName) { 248 if (!(getKnownPropertyNameSet().contains(propertyName.toUpperCase()) || 249 propertyName.startsWith("X-")) 250 && !mUnknownTypeSet.contains(propertyName)) { 251 mUnknownTypeSet.add(propertyName); 252 Log.w(LOG_TAG, "Property name unsupported by vCard 2.1: " + propertyName); 253 } 254 return true; 255 } 256 257 /** 258 * @return String. It may be null, or its length may be 0 259 * @throws IOException 260 */ 261 protected String getLine() throws IOException { 262 return mReader.readLine(); 263 } 264 265 protected String peekLine() throws IOException { 266 return mReader.peekLine(); 267 } 268 269 /** 270 * @return String with it's length > 0 271 * @throws IOException 272 * @throws VCardException when the stream reached end of line 273 */ 274 protected String getNonEmptyLine() throws IOException, VCardException { 275 String line; 276 while (true) { 277 line = getLine(); 278 if (line == null) { 279 throw new VCardException("Reached end of buffer."); 280 } else if (line.trim().length() > 0) { 281 return line; 282 } 283 } 284 } 285 286 /* 287 * vcard = "BEGIN" [ws] ":" [ws] "VCARD" [ws] 1*CRLF 288 * items *CRLF 289 * "END" [ws] ":" [ws] "VCARD" 290 */ 291 private boolean parseOneVCard(boolean firstRead) throws IOException, VCardException { 292 boolean allowGarbage = false; 293 if (firstRead) { 294 if (mNestCount > 0) { 295 for (int i = 0; i < mNestCount; i++) { 296 if (!readBeginVCard(allowGarbage)) { 297 return false; 298 } 299 allowGarbage = true; 300 } 301 } 302 } 303 304 if (!readBeginVCard(allowGarbage)) { 305 return false; 306 } 307 final long beforeStartEntry = System.currentTimeMillis(); 308 mInterpreter.startEntry(); 309 mTimeReadStartRecord += System.currentTimeMillis() - beforeStartEntry; 310 311 final long beforeParseItems = System.currentTimeMillis(); 312 parseItems(); 313 mTimeParseItems += System.currentTimeMillis() - beforeParseItems; 314 315 readEndVCard(true, false); 316 317 final long beforeEndEntry = System.currentTimeMillis(); 318 mInterpreter.endEntry(); 319 mTimeReadEndRecord += System.currentTimeMillis() - beforeEndEntry; 320 return true; 321 } 322 323 /** 324 * @return True when successful. False when reaching the end of line 325 * @throws IOException 326 * @throws VCardException 327 */ 328 protected boolean readBeginVCard(boolean allowGarbage) throws IOException, VCardException { 329 String line; 330 do { 331 while (true) { 332 line = getLine(); 333 if (line == null) { 334 return false; 335 } else if (line.trim().length() > 0) { 336 break; 337 } 338 } 339 final String[] strArray = line.split(":", 2); 340 final int length = strArray.length; 341 342 // Although vCard 2.1/3.0 specification does not allow lower cases, 343 // we found vCard file emitted by some external vCard expoter have such 344 // invalid Strings. 345 // So we allow it. 346 // e.g. 347 // BEGIN:vCard 348 if (length == 2 && strArray[0].trim().equalsIgnoreCase("BEGIN") 349 && strArray[1].trim().equalsIgnoreCase("VCARD")) { 350 return true; 351 } else if (!allowGarbage) { 352 if (mNestCount > 0) { 353 mPreviousLine = line; 354 return false; 355 } else { 356 throw new VCardException("Expected String \"BEGIN:VCARD\" did not come " 357 + "(Instead, \"" + line + "\" came)"); 358 } 359 } 360 } while (allowGarbage); 361 362 throw new VCardException("Reached where must not be reached."); 363 } 364 365 /** 366 * <p> 367 * The arguments useCache and allowGarbase are usually true and false 368 * accordingly when this function is called outside this function itself. 369 * </p> 370 * 371 * @param useCache When true, line is obtained from mPreviousline. 372 * Otherwise, getLine() is used. 373 * @param allowGarbage When true, ignore non "END:VCARD" line. 374 * @throws IOException 375 * @throws VCardException 376 */ 377 protected void readEndVCard(boolean useCache, boolean allowGarbage) throws IOException, 378 VCardException { 379 String line; 380 do { 381 if (useCache) { 382 // Though vCard specification does not allow lower cases, 383 // some data may have them, so we allow it. 384 line = mPreviousLine; 385 } else { 386 while (true) { 387 line = getLine(); 388 if (line == null) { 389 throw new VCardException("Expected END:VCARD was not found."); 390 } else if (line.trim().length() > 0) { 391 break; 392 } 393 } 394 } 395 396 String[] strArray = line.split(":", 2); 397 if (strArray.length == 2 && strArray[0].trim().equalsIgnoreCase("END") 398 && strArray[1].trim().equalsIgnoreCase("VCARD")) { 399 return; 400 } else if (!allowGarbage) { 401 throw new VCardException("END:VCARD != \"" + mPreviousLine + "\""); 402 } 403 useCache = false; 404 } while (allowGarbage); 405 } 406 407 /* 408 * items = *CRLF item / item 409 */ 410 protected void parseItems() throws IOException, VCardException { 411 boolean ended = false; 412 413 final long beforeBeginProperty = System.currentTimeMillis(); 414 mInterpreter.startProperty(); 415 mTimeStartProperty += System.currentTimeMillis() - beforeBeginProperty; 416 ended = parseItem(); 417 if (!ended) { 418 final long beforeEndProperty = System.currentTimeMillis(); 419 mInterpreter.endProperty(); 420 mTimeEndProperty += System.currentTimeMillis() - beforeEndProperty; 421 } 422 423 while (!ended) { 424 final long beforeStartProperty = System.currentTimeMillis(); 425 mInterpreter.startProperty(); 426 mTimeStartProperty += System.currentTimeMillis() - beforeStartProperty; 427 try { 428 ended = parseItem(); 429 } catch (VCardInvalidCommentLineException e) { 430 Log.e(LOG_TAG, "Invalid line which looks like some comment was found. Ignored."); 431 ended = false; 432 } 433 434 if (!ended) { 435 final long beforeEndProperty = System.currentTimeMillis(); 436 mInterpreter.endProperty(); 437 mTimeEndProperty += System.currentTimeMillis() - beforeEndProperty; 438 } 439 } 440 } 441 442 /* 443 * item = [groups "."] name [params] ":" value CRLF / [groups "."] "ADR" 444 * [params] ":" addressparts CRLF / [groups "."] "ORG" [params] ":" orgparts 445 * CRLF / [groups "."] "N" [params] ":" nameparts CRLF / [groups "."] 446 * "AGENT" [params] ":" vcard CRLF 447 */ 448 protected boolean parseItem() throws IOException, VCardException { 449 mCurrentEncoding = DEFAULT_ENCODING; 450 451 final String line = getNonEmptyLine(); 452 long start = System.currentTimeMillis(); 453 454 String[] propertyNameAndValue = separateLineAndHandleGroup(line); 455 if (propertyNameAndValue == null) { 456 return true; 457 } 458 if (propertyNameAndValue.length != 2) { 459 throw new VCardInvalidLineException("Invalid line \"" + line + "\""); 460 } 461 String propertyName = propertyNameAndValue[0].toUpperCase(); 462 String propertyValue = propertyNameAndValue[1]; 463 464 mTimeParseLineAndHandleGroup += System.currentTimeMillis() - start; 465 466 if (propertyName.equals("ADR") || propertyName.equals("ORG") || propertyName.equals("N")) { 467 start = System.currentTimeMillis(); 468 handleMultiplePropertyValue(propertyName, propertyValue); 469 mTimeParseAdrOrgN += System.currentTimeMillis() - start; 470 return false; 471 } else if (propertyName.equals("AGENT")) { 472 handleAgent(propertyValue); 473 return false; 474 } else if (isValidPropertyName(propertyName)) { 475 if (propertyName.equals("BEGIN")) { 476 if (propertyValue.equals("VCARD")) { 477 throw new VCardNestedException("This vCard has nested vCard data in it."); 478 } else { 479 throw new VCardException("Unknown BEGIN type: " + propertyValue); 480 } 481 } else if (propertyName.equals("VERSION") && !propertyValue.equals(getVersionString())) { 482 throw new VCardVersionException("Incompatible version: " + propertyValue + " != " 483 + getVersionString()); 484 } 485 start = System.currentTimeMillis(); 486 handlePropertyValue(propertyName, propertyValue); 487 mTimeParsePropertyValues += System.currentTimeMillis() - start; 488 return false; 489 } 490 491 throw new VCardException("Unknown property name: \"" + propertyName + "\""); 492 } 493 494 // For performance reason, the states for group and property name are merged into one. 495 static private final int STATE_GROUP_OR_PROPERTY_NAME = 0; 496 static private final int STATE_PARAMS = 1; 497 // vCard 3.0 specification allows double-quoted parameters, while vCard 2.1 does not. 498 static private final int STATE_PARAMS_IN_DQUOTE = 2; 499 500 protected String[] separateLineAndHandleGroup(String line) throws VCardException { 501 final String[] propertyNameAndValue = new String[2]; 502 final int length = line.length(); 503 if (length > 0 && line.charAt(0) == '#') { 504 throw new VCardInvalidCommentLineException(); 505 } 506 507 int state = STATE_GROUP_OR_PROPERTY_NAME; 508 int nameIndex = 0; 509 510 // This loop is developed so that we don't have to take care of bottle neck here. 511 // Refactor carefully when you need to do so. 512 for (int i = 0; i < length; i++) { 513 final char ch = line.charAt(i); 514 switch (state) { 515 case STATE_GROUP_OR_PROPERTY_NAME: { 516 if (ch == ':') { // End of a property name. 517 final String propertyName = line.substring(nameIndex, i); 518 if (propertyName.equalsIgnoreCase("END")) { 519 mPreviousLine = line; 520 return null; 521 } 522 mInterpreter.propertyName(propertyName); 523 propertyNameAndValue[0] = propertyName; 524 if (i < length - 1) { 525 propertyNameAndValue[1] = line.substring(i + 1); 526 } else { 527 propertyNameAndValue[1] = ""; 528 } 529 return propertyNameAndValue; 530 } else if (ch == '.') { // Each group is followed by the dot. 531 final String groupName = line.substring(nameIndex, i); 532 if (groupName.length() == 0) { 533 Log.w(LOG_TAG, "Empty group found. Ignoring."); 534 } else { 535 mInterpreter.propertyGroup(groupName); 536 } 537 nameIndex = i + 1; // Next should be another group or a property name. 538 } else if (ch == ';') { // End of property name and beginneng of parameters. 539 final String propertyName = line.substring(nameIndex, i); 540 if (propertyName.equalsIgnoreCase("END")) { 541 mPreviousLine = line; 542 return null; 543 } 544 mInterpreter.propertyName(propertyName); 545 propertyNameAndValue[0] = propertyName; 546 nameIndex = i + 1; 547 state = STATE_PARAMS; // Start parameter parsing. 548 } 549 // TODO: comma support (in vCard 3.0 and 4.0). 550 break; 551 } 552 case STATE_PARAMS: { 553 if (ch == '"') { 554 if (VCardConstants.VERSION_V21.equalsIgnoreCase(getVersionString())) { 555 Log.w(LOG_TAG, "Double-quoted params found in vCard 2.1. " + 556 "Silently allow it"); 557 } 558 state = STATE_PARAMS_IN_DQUOTE; 559 } else if (ch == ';') { // Starts another param. 560 handleParams(line.substring(nameIndex, i)); 561 nameIndex = i + 1; 562 } else if (ch == ':') { // End of param and beginenning of values. 563 handleParams(line.substring(nameIndex, i)); 564 if (i < length - 1) { 565 propertyNameAndValue[1] = line.substring(i + 1); 566 } else { 567 propertyNameAndValue[1] = ""; 568 } 569 return propertyNameAndValue; 570 } 571 break; 572 } 573 case STATE_PARAMS_IN_DQUOTE: { 574 if (ch == '"') { 575 if (VCardConstants.VERSION_V21.equalsIgnoreCase(getVersionString())) { 576 Log.w(LOG_TAG, "Double-quoted params found in vCard 2.1. " + 577 "Silently allow it"); 578 } 579 state = STATE_PARAMS; 580 } 581 break; 582 } 583 } 584 } 585 586 throw new VCardInvalidLineException("Invalid line: \"" + line + "\""); 587 } 588 589 /* 590 * params = ";" [ws] paramlist paramlist = paramlist [ws] ";" [ws] param / 591 * param param = "TYPE" [ws] "=" [ws] ptypeval / "VALUE" [ws] "=" [ws] 592 * pvalueval / "ENCODING" [ws] "=" [ws] pencodingval / "CHARSET" [ws] "=" 593 * [ws] charsetval / "LANGUAGE" [ws] "=" [ws] langval / "X-" word [ws] "=" 594 * [ws] word / knowntype 595 */ 596 protected void handleParams(String params) throws VCardException { 597 final String[] strArray = params.split("=", 2); 598 if (strArray.length == 2) { 599 final String paramName = strArray[0].trim().toUpperCase(); 600 String paramValue = strArray[1].trim(); 601 if (paramName.equals("TYPE")) { 602 handleType(paramValue); 603 } else if (paramName.equals("VALUE")) { 604 handleValue(paramValue); 605 } else if (paramName.equals("ENCODING")) { 606 handleEncoding(paramValue); 607 } else if (paramName.equals("CHARSET")) { 608 handleCharset(paramValue); 609 } else if (paramName.equals("LANGUAGE")) { 610 handleLanguage(paramValue); 611 } else if (paramName.startsWith("X-")) { 612 handleAnyParam(paramName, paramValue); 613 } else { 614 throw new VCardException("Unknown type \"" + paramName + "\""); 615 } 616 } else { 617 handleParamWithoutName(strArray[0]); 618 } 619 } 620 621 /** 622 * vCard 3.0 parser implementation may throw VCardException. 623 */ 624 @SuppressWarnings("unused") 625 protected void handleParamWithoutName(final String paramValue) throws VCardException { 626 handleType(paramValue); 627 } 628 629 /* 630 * ptypeval = knowntype / "X-" word 631 */ 632 protected void handleType(final String ptypeval) { 633 if (!(getKnownTypeSet().contains(ptypeval.toUpperCase()) 634 || ptypeval.startsWith("X-")) 635 && !mUnknownTypeSet.contains(ptypeval)) { 636 mUnknownTypeSet.add(ptypeval); 637 Log.w(LOG_TAG, String.format("TYPE unsupported by %s: ", getVersion(), ptypeval)); 638 } 639 mInterpreter.propertyParamType("TYPE"); 640 mInterpreter.propertyParamValue(ptypeval); 641 } 642 643 /* 644 * pvalueval = "INLINE" / "URL" / "CONTENT-ID" / "CID" / "X-" word 645 */ 646 protected void handleValue(final String pvalueval) { 647 if (!(getKnownValueSet().contains(pvalueval.toUpperCase()) 648 || pvalueval.startsWith("X-") 649 || mUnknownValueSet.contains(pvalueval))) { 650 mUnknownValueSet.add(pvalueval); 651 Log.w(LOG_TAG, String.format( 652 "The value unsupported by TYPE of %s: ", getVersion(), pvalueval)); 653 } 654 mInterpreter.propertyParamType("VALUE"); 655 mInterpreter.propertyParamValue(pvalueval); 656 } 657 658 /* 659 * pencodingval = "7BIT" / "8BIT" / "QUOTED-PRINTABLE" / "BASE64" / "X-" word 660 */ 661 protected void handleEncoding(String pencodingval) throws VCardException { 662 if (getAvailableEncodingSet().contains(pencodingval) || 663 pencodingval.startsWith("X-")) { 664 mInterpreter.propertyParamType("ENCODING"); 665 mInterpreter.propertyParamValue(pencodingval); 666 mCurrentEncoding = pencodingval; 667 } else { 668 throw new VCardException("Unknown encoding \"" + pencodingval + "\""); 669 } 670 } 671 672 /** 673 * <p> 674 * vCard 2.1 specification only allows us-ascii and iso-8859-xxx (See RFC 1521), 675 * but recent vCard files often contain other charset like UTF-8, SHIFT_JIS, etc. 676 * We allow any charset. 677 * </p> 678 */ 679 protected void handleCharset(String charsetval) { 680 mInterpreter.propertyParamType("CHARSET"); 681 mInterpreter.propertyParamValue(charsetval); 682 } 683 684 /** 685 * See also Section 7.1 of RFC 1521 686 */ 687 protected void handleLanguage(String langval) throws VCardException { 688 String[] strArray = langval.split("-"); 689 if (strArray.length != 2) { 690 throw new VCardException("Invalid Language: \"" + langval + "\""); 691 } 692 String tmp = strArray[0]; 693 int length = tmp.length(); 694 for (int i = 0; i < length; i++) { 695 if (!isAsciiLetter(tmp.charAt(i))) { 696 throw new VCardException("Invalid Language: \"" + langval + "\""); 697 } 698 } 699 tmp = strArray[1]; 700 length = tmp.length(); 701 for (int i = 0; i < length; i++) { 702 if (!isAsciiLetter(tmp.charAt(i))) { 703 throw new VCardException("Invalid Language: \"" + langval + "\""); 704 } 705 } 706 mInterpreter.propertyParamType(VCardConstants.PARAM_LANGUAGE); 707 mInterpreter.propertyParamValue(langval); 708 } 709 710 private boolean isAsciiLetter(char ch) { 711 if ((ch >= 'a' && ch <= 'z') || (ch >= 'A' && ch <= 'Z')) { 712 return true; 713 } 714 return false; 715 } 716 717 /** 718 * Mainly for "X-" type. This accepts any kind of type without check. 719 */ 720 protected void handleAnyParam(String paramName, String paramValue) { 721 mInterpreter.propertyParamType(paramName); 722 mInterpreter.propertyParamValue(paramValue); 723 } 724 725 protected void handlePropertyValue(String propertyName, String propertyValue) 726 throws IOException, VCardException { 727 final String upperEncoding = mCurrentEncoding.toUpperCase(); 728 if (upperEncoding.equals(VCardConstants.PARAM_ENCODING_QP)) { 729 final long start = System.currentTimeMillis(); 730 final String result = getQuotedPrintable(propertyValue); 731 final ArrayList<String> v = new ArrayList<String>(); 732 v.add(result); 733 mInterpreter.propertyValues(v); 734 mTimeHandleQuotedPrintable += System.currentTimeMillis() - start; 735 } else if (upperEncoding.equals(VCardConstants.PARAM_ENCODING_BASE64) 736 || upperEncoding.equals(VCardConstants.PARAM_ENCODING_B)) { 737 final long start = System.currentTimeMillis(); 738 // It is very rare, but some BASE64 data may be so big that 739 // OutOfMemoryError occurs. To ignore such cases, use try-catch. 740 try { 741 final ArrayList<String> arrayList = new ArrayList<String>(); 742 arrayList.add(getBase64(propertyValue)); 743 mInterpreter.propertyValues(arrayList); 744 } catch (OutOfMemoryError error) { 745 Log.e(LOG_TAG, "OutOfMemoryError happened during parsing BASE64 data!"); 746 mInterpreter.propertyValues(null); 747 } 748 mTimeHandleBase64 += System.currentTimeMillis() - start; 749 } else { 750 if (!(upperEncoding.equals("7BIT") || upperEncoding.equals("8BIT") || 751 upperEncoding.startsWith("X-"))) { 752 Log.w(LOG_TAG, 753 String.format("The encoding \"%s\" is unsupported by vCard %s", 754 mCurrentEncoding, getVersionString())); 755 } 756 757 // Some device uses line folding defined in RFC 2425, which is not allowed 758 // in vCard 2.1 (while needed in vCard 3.0). 759 // 760 // e.g. 761 // BEGIN:VCARD 762 // VERSION:2.1 763 // N:;Omega;;; 764 // EMAIL;INTERNET:"Omega" 765 // <omega (at) example.com> 766 // FN:Omega 767 // END:VCARD 768 // 769 // The vCard above assumes that email address should become: 770 // "Omega" <omega (at) example.com> 771 // 772 // But vCard 2.1 requires Quote-Printable when a line contains line break(s). 773 // 774 // For more information about line folding, 775 // see "5.8.1. Line delimiting and folding" in RFC 2425. 776 // 777 // We take care of this case more formally in vCard 3.0, so we only need to 778 // do this in vCard 2.1. 779 if (getVersion() == VCardConfig.VERSION_21) { 780 StringBuilder builder = null; 781 while (true) { 782 final String nextLine = peekLine(); 783 // We don't need to care too much about this exceptional case, 784 // but we should not wrongly eat up "END:VCARD", since it critically 785 // breaks this parser's state machine. 786 // Thus we roughly look over the next line and confirm it is at least not 787 // "END:VCARD". This extra fee is worth paying. This is exceptional 788 // anyway. 789 if (!TextUtils.isEmpty(nextLine) && 790 nextLine.charAt(0) == ' ' && 791 !"END:VCARD".contains(nextLine.toUpperCase())) { 792 getLine(); // Drop the next line. 793 794 if (builder == null) { 795 builder = new StringBuilder(); 796 builder.append(propertyValue); 797 } 798 builder.append(nextLine.substring(1)); 799 } else { 800 break; 801 } 802 } 803 if (builder != null) { 804 propertyValue = builder.toString(); 805 } 806 } 807 808 final long start = System.currentTimeMillis(); 809 ArrayList<String> v = new ArrayList<String>(); 810 v.add(maybeUnescapeText(propertyValue)); 811 mInterpreter.propertyValues(v); 812 mTimeHandleMiscPropertyValue += System.currentTimeMillis() - start; 813 } 814 } 815 816 /** 817 * <p> 818 * Parses and returns Quoted-Printable. 819 * </p> 820 * 821 * @param firstString The string following a parameter name and attributes. 822 * Example: "string" in 823 * "ADR:ENCODING=QUOTED-PRINTABLE:string\n\r". 824 * @return whole Quoted-Printable string, including a given argument and 825 * following lines. Excludes the last empty line following to Quoted 826 * Printable lines. 827 * @throws IOException 828 * @throws VCardException 829 */ 830 private String getQuotedPrintable(String firstString) throws IOException, VCardException { 831 // Specifically, there may be some padding between = and CRLF. 832 // See the following: 833 // 834 // qp-line := *(qp-segment transport-padding CRLF) 835 // qp-part transport-padding 836 // qp-segment := qp-section *(SPACE / TAB) "=" 837 // ; Maximum length of 76 characters 838 // 839 // e.g. (from RFC 2045) 840 // Now's the time = 841 // for all folk to come= 842 // to the aid of their country. 843 if (firstString.trim().endsWith("=")) { 844 // remove "transport-padding" 845 int pos = firstString.length() - 1; 846 while (firstString.charAt(pos) != '=') { 847 } 848 StringBuilder builder = new StringBuilder(); 849 builder.append(firstString.substring(0, pos + 1)); 850 builder.append("\r\n"); 851 String line; 852 while (true) { 853 line = getLine(); 854 if (line == null) { 855 throw new VCardException("File ended during parsing a Quoted-Printable String"); 856 } 857 if (line.trim().endsWith("=")) { 858 // remove "transport-padding" 859 pos = line.length() - 1; 860 while (line.charAt(pos) != '=') { 861 } 862 builder.append(line.substring(0, pos + 1)); 863 builder.append("\r\n"); 864 } else { 865 builder.append(line); 866 break; 867 } 868 } 869 return builder.toString(); 870 } else { 871 return firstString; 872 } 873 } 874 875 protected String getBase64(String firstString) throws IOException, VCardException { 876 StringBuilder builder = new StringBuilder(); 877 builder.append(firstString); 878 879 while (true) { 880 String line = getLine(); 881 if (line == null) { 882 throw new VCardException("File ended during parsing BASE64 binary"); 883 } 884 if (line.length() == 0) { 885 break; 886 } 887 builder.append(line); 888 } 889 890 return builder.toString(); 891 } 892 893 /** 894 * <p> 895 * Mainly for "ADR", "ORG", and "N" 896 * </p> 897 */ 898 /* 899 * addressparts = 0*6(strnosemi ";") strnosemi ; PO Box, Extended Addr, 900 * Street, Locality, Region, Postal Code, Country Name orgparts = 901 * *(strnosemi ";") strnosemi ; First is Organization Name, remainder are 902 * Organization Units. nameparts = 0*4(strnosemi ";") strnosemi ; Family, 903 * Given, Middle, Prefix, Suffix. ; Example:Public;John;Q.;Reverend Dr.;III, 904 * Esq. strnosemi = *(*nonsemi ("\;" / "\" CRLF)) *nonsemi ; To include a 905 * semicolon in this string, it must be escaped ; with a "\" character. We 906 * do not care the number of "strnosemi" here. We are not sure whether we 907 * should add "\" CRLF to each value. We exclude them for now. 908 */ 909 protected void handleMultiplePropertyValue(String propertyName, String propertyValue) 910 throws IOException, VCardException { 911 // vCard 2.1 does not allow QUOTED-PRINTABLE here, but some 912 // softwares/devices 913 // emit such data. 914 if (mCurrentEncoding.equalsIgnoreCase("QUOTED-PRINTABLE")) { 915 propertyValue = getQuotedPrintable(propertyValue); 916 } 917 918 mInterpreter.propertyValues(VCardUtils.constructListFromValue(propertyValue, 919 getVersion())); 920 } 921 922 /* 923 * vCard 2.1 specifies AGENT allows one vcard entry. Currently we emit an 924 * error toward the AGENT property. 925 * // TODO: Support AGENT property. 926 * item = 927 * ... / [groups "."] "AGENT" [params] ":" vcard CRLF vcard = "BEGIN" [ws] 928 * ":" [ws] "VCARD" [ws] 1*CRLF items *CRLF "END" [ws] ":" [ws] "VCARD" 929 */ 930 protected void handleAgent(final String propertyValue) throws VCardException { 931 if (!propertyValue.toUpperCase().contains("BEGIN:VCARD")) { 932 // Apparently invalid line seen in Windows Mobile 6.5. Ignore them. 933 return; 934 } else { 935 throw new VCardAgentNotSupportedException("AGENT Property is not supported now."); 936 } 937 } 938 939 /** 940 * For vCard 3.0. 941 */ 942 protected String maybeUnescapeText(final String text) { 943 return text; 944 } 945 946 /** 947 * Returns unescaped String if the character should be unescaped. Return 948 * null otherwise. e.g. In vCard 2.1, "\;" should be unescaped into ";" 949 * while "\x" should not be. 950 */ 951 protected String maybeUnescapeCharacter(final char ch) { 952 return unescapeCharacter(ch); 953 } 954 955 /* package */ static String unescapeCharacter(final char ch) { 956 // Original vCard 2.1 specification does not allow transformation 957 // "\:" -> ":", "\," -> ",", and "\\" -> "\", but previous 958 // implementation of 959 // this class allowed them, so keep it as is. 960 if (ch == '\\' || ch == ';' || ch == ':' || ch == ',') { 961 return String.valueOf(ch); 962 } else { 963 return null; 964 } 965 } 966 967 private void showPerformanceInfo() { 968 Log.d(LOG_TAG, "Total parsing time: " + mTimeTotal + " ms"); 969 Log.d(LOG_TAG, "Total readLine time: " + mReader.getTotalmillisecond() + " ms"); 970 Log.d(LOG_TAG, "Time for handling the beggining of the record: " + mTimeReadStartRecord 971 + " ms"); 972 Log.d(LOG_TAG, "Time for handling the end of the record: " + mTimeReadEndRecord + " ms"); 973 Log.d(LOG_TAG, "Time for parsing line, and handling group: " + mTimeParseLineAndHandleGroup 974 + " ms"); 975 Log.d(LOG_TAG, "Time for parsing ADR, ORG, and N fields:" + mTimeParseAdrOrgN + " ms"); 976 Log.d(LOG_TAG, "Time for parsing property values: " + mTimeParsePropertyValues + " ms"); 977 Log.d(LOG_TAG, "Time for handling normal property values: " + mTimeHandleMiscPropertyValue 978 + " ms"); 979 Log.d(LOG_TAG, "Time for handling Quoted-Printable: " + mTimeHandleQuotedPrintable + " ms"); 980 Log.d(LOG_TAG, "Time for handling Base64: " + mTimeHandleBase64 + " ms"); 981 } 982 983 /** 984 * @return {@link VCardConfig#VERSION_21} 985 */ 986 protected int getVersion() { 987 return VCardConfig.VERSION_21; 988 } 989 990 /** 991 * @return {@link VCardConfig#VERSION_30} 992 */ 993 protected String getVersionString() { 994 return VCardConstants.VERSION_V21; 995 } 996 997 protected Set<String> getKnownPropertyNameSet() { 998 return VCardParser_V21.sKnownPropertyNameSet; 999 } 1000 1001 protected Set<String> getKnownTypeSet() { 1002 return VCardParser_V21.sKnownTypeSet; 1003 } 1004 1005 protected Set<String> getKnownValueSet() { 1006 return VCardParser_V21.sKnownValueSet; 1007 } 1008 1009 protected Set<String> getAvailableEncodingSet() { 1010 return VCardParser_V21.sAvailableEncoding; 1011 } 1012 1013 protected String getDefaultEncoding() { 1014 return DEFAULT_ENCODING; 1015 } 1016 1017 1018 public void parse(InputStream is, VCardInterpreter interpreter) 1019 throws IOException, VCardException { 1020 if (is == null) { 1021 throw new NullPointerException("InputStream must not be null."); 1022 } 1023 1024 final InputStreamReader tmpReader = new InputStreamReader(is, mIntermediateCharset); 1025 mReader = new CustomBufferedReader(tmpReader); 1026 1027 mInterpreter = (interpreter != null ? interpreter : new EmptyInterpreter()); 1028 1029 final long start = System.currentTimeMillis(); 1030 if (mInterpreter != null) { 1031 mInterpreter.start(); 1032 } 1033 parseVCardFile(); 1034 if (mInterpreter != null) { 1035 mInterpreter.end(); 1036 } 1037 mTimeTotal += System.currentTimeMillis() - start; 1038 1039 if (VCardConfig.showPerformanceLog()) { 1040 showPerformanceInfo(); 1041 } 1042 } 1043 1044 public final void cancel() { 1045 mCanceled = true; 1046 } 1047 } 1048