1 /* 2 * Copyright (C) 2009 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 android.pim.vcard; 17 18 import android.pim.vcard.exception.VCardAgentNotSupportedException; 19 import android.pim.vcard.exception.VCardException; 20 import android.pim.vcard.exception.VCardInvalidCommentLineException; 21 import android.pim.vcard.exception.VCardInvalidLineException; 22 import android.pim.vcard.exception.VCardNestedException; 23 import android.pim.vcard.exception.VCardVersionException; 24 import android.util.Log; 25 26 import java.io.BufferedReader; 27 import java.io.IOException; 28 import java.io.InputStream; 29 import java.io.InputStreamReader; 30 import java.io.Reader; 31 import java.util.ArrayList; 32 import java.util.Arrays; 33 import java.util.HashSet; 34 import java.util.Set; 35 36 /** 37 * This class is used to parse vCard. Please refer to vCard Specification 2.1 for more detail. 38 */ 39 public class VCardParser_V21 extends VCardParser { 40 private static final String LOG_TAG = "VCardParser_V21"; 41 42 /** Store the known-type */ 43 private static final HashSet<String> sKnownTypeSet = new HashSet<String>( 44 Arrays.asList("DOM", "INTL", "POSTAL", "PARCEL", "HOME", "WORK", 45 "PREF", "VOICE", "FAX", "MSG", "CELL", "PAGER", "BBS", 46 "MODEM", "CAR", "ISDN", "VIDEO", "AOL", "APPLELINK", 47 "ATTMAIL", "CIS", "EWORLD", "INTERNET", "IBMMAIL", 48 "MCIMAIL", "POWERSHARE", "PRODIGY", "TLX", "X400", "GIF", 49 "CGM", "WMF", "BMP", "MET", "PMB", "DIB", "PICT", "TIFF", 50 "PDF", "PS", "JPEG", "QTIME", "MPEG", "MPEG2", "AVI", 51 "WAVE", "AIFF", "PCM", "X509", "PGP")); 52 53 /** Store the known-value */ 54 private static final HashSet<String> sKnownValueSet = new HashSet<String>( 55 Arrays.asList("INLINE", "URL", "CONTENT-ID", "CID")); 56 57 /** Store the property names available in vCard 2.1 */ 58 private static final HashSet<String> sAvailablePropertyNameSetV21 = 59 new HashSet<String>(Arrays.asList( 60 "BEGIN", "LOGO", "PHOTO", "LABEL", "FN", "TITLE", "SOUND", 61 "VERSION", "TEL", "EMAIL", "TZ", "GEO", "NOTE", "URL", 62 "BDAY", "ROLE", "REV", "UID", "KEY", "MAILER")); 63 64 /** 65 * Though vCard 2.1 specification does not allow "B" encoding, some data may have it. 66 * We allow it for safety... 67 */ 68 private static final HashSet<String> sAvailableEncodingV21 = 69 new HashSet<String>(Arrays.asList( 70 "7BIT", "8BIT", "QUOTED-PRINTABLE", "BASE64", "B")); 71 72 // Used only for parsing END:VCARD. 73 private String mPreviousLine; 74 75 /** The builder to build parsed data */ 76 protected VCardInterpreter mBuilder = null; 77 78 /** 79 * The encoding type. "Encoding" in vCard is different from "Charset". 80 * e.g. 7BIT, 8BIT, QUOTED-PRINTABLE. 81 */ 82 protected String mEncoding = null; 83 84 protected final String sDefaultEncoding = "8BIT"; 85 86 // Should not directly read a line from this object. Use getLine() instead. 87 protected BufferedReader mReader; 88 89 // In some cases, vCard is nested. Currently, we only consider the most interior vCard data. 90 // See v21_foma_1.vcf in test directory for more information. 91 private int mNestCount; 92 93 // In order to reduce warning message as much as possible, we hold the value which made Logger 94 // emit a warning message. 95 protected Set<String> mUnknownTypeMap = new HashSet<String>(); 96 protected Set<String> mUnknownValueMap = new HashSet<String>(); 97 98 // For measuring performance. 99 private long mTimeTotal; 100 private long mTimeReadStartRecord; 101 private long mTimeReadEndRecord; 102 private long mTimeStartProperty; 103 private long mTimeEndProperty; 104 private long mTimeParseItems; 105 private long mTimeParseLineAndHandleGroup; 106 private long mTimeParsePropertyValues; 107 private long mTimeParseAdrOrgN; 108 private long mTimeHandleMiscPropertyValue; 109 private long mTimeHandleQuotedPrintable; 110 private long mTimeHandleBase64; 111 112 public VCardParser_V21() { 113 this(null); 114 } 115 116 public VCardParser_V21(VCardSourceDetector detector) { 117 this(detector != null ? detector.getEstimatedType() : VCardConfig.PARSE_TYPE_UNKNOWN); 118 } 119 120 public VCardParser_V21(int parseType) { 121 super(parseType); 122 if (parseType == VCardConfig.PARSE_TYPE_FOMA) { 123 mNestCount = 1; 124 } 125 } 126 127 /** 128 * Parses the file at the given position. 129 * 130 * vcard_file = [wsls] vcard [wsls] 131 */ 132 protected void parseVCardFile() throws IOException, VCardException { 133 boolean firstReading = true; 134 while (true) { 135 if (mCanceled) { 136 break; 137 } 138 if (!parseOneVCard(firstReading)) { 139 break; 140 } 141 firstReading = false; 142 } 143 144 if (mNestCount > 0) { 145 boolean useCache = true; 146 for (int i = 0; i < mNestCount; i++) { 147 readEndVCard(useCache, true); 148 useCache = false; 149 } 150 } 151 } 152 153 protected int getVersion() { 154 return VCardConfig.FLAG_V21; 155 } 156 157 protected String getVersionString() { 158 return VCardConstants.VERSION_V21; 159 } 160 161 /** 162 * @return true when the propertyName is a valid property name. 163 */ 164 protected boolean isValidPropertyName(String propertyName) { 165 if (!(sAvailablePropertyNameSetV21.contains(propertyName.toUpperCase()) || 166 propertyName.startsWith("X-")) && 167 !mUnknownTypeMap.contains(propertyName)) { 168 mUnknownTypeMap.add(propertyName); 169 Log.w(LOG_TAG, "Property name unsupported by vCard 2.1: " + propertyName); 170 } 171 return true; 172 } 173 174 /** 175 * @return true when the encoding is a valid encoding. 176 */ 177 protected boolean isValidEncoding(String encoding) { 178 return sAvailableEncodingV21.contains(encoding.toUpperCase()); 179 } 180 181 /** 182 * @return String. It may be null, or its length may be 0 183 * @throws IOException 184 */ 185 protected String getLine() throws IOException { 186 return mReader.readLine(); 187 } 188 189 /** 190 * @return String with it's length > 0 191 * @throws IOException 192 * @throws VCardException when the stream reached end of line 193 */ 194 protected String getNonEmptyLine() throws IOException, VCardException { 195 String line; 196 while (true) { 197 line = getLine(); 198 if (line == null) { 199 throw new VCardException("Reached end of buffer."); 200 } else if (line.trim().length() > 0) { 201 return line; 202 } 203 } 204 } 205 206 /** 207 * vcard = "BEGIN" [ws] ":" [ws] "VCARD" [ws] 1*CRLF 208 * items *CRLF 209 * "END" [ws] ":" [ws] "VCARD" 210 */ 211 private boolean parseOneVCard(boolean firstReading) throws IOException, VCardException { 212 boolean allowGarbage = false; 213 if (firstReading) { 214 if (mNestCount > 0) { 215 for (int i = 0; i < mNestCount; i++) { 216 if (!readBeginVCard(allowGarbage)) { 217 return false; 218 } 219 allowGarbage = true; 220 } 221 } 222 } 223 224 if (!readBeginVCard(allowGarbage)) { 225 return false; 226 } 227 long start; 228 if (mBuilder != null) { 229 start = System.currentTimeMillis(); 230 mBuilder.startEntry(); 231 mTimeReadStartRecord += System.currentTimeMillis() - start; 232 } 233 start = System.currentTimeMillis(); 234 parseItems(); 235 mTimeParseItems += System.currentTimeMillis() - start; 236 readEndVCard(true, false); 237 if (mBuilder != null) { 238 start = System.currentTimeMillis(); 239 mBuilder.endEntry(); 240 mTimeReadEndRecord += System.currentTimeMillis() - start; 241 } 242 return true; 243 } 244 245 /** 246 * @return True when successful. False when reaching the end of line 247 * @throws IOException 248 * @throws VCardException 249 */ 250 protected boolean readBeginVCard(boolean allowGarbage) throws IOException, VCardException { 251 String line; 252 do { 253 while (true) { 254 line = getLine(); 255 if (line == null) { 256 return false; 257 } else if (line.trim().length() > 0) { 258 break; 259 } 260 } 261 String[] strArray = line.split(":", 2); 262 int length = strArray.length; 263 264 // Though vCard 2.1/3.0 specification does not allow lower cases, 265 // vCard file emitted by some external vCard expoter have such invalid Strings. 266 // So we allow it. 267 // e.g. BEGIN:vCard 268 if (length == 2 && 269 strArray[0].trim().equalsIgnoreCase("BEGIN") && 270 strArray[1].trim().equalsIgnoreCase("VCARD")) { 271 return true; 272 } else if (!allowGarbage) { 273 if (mNestCount > 0) { 274 mPreviousLine = line; 275 return false; 276 } else { 277 throw new VCardException( 278 "Expected String \"BEGIN:VCARD\" did not come " 279 + "(Instead, \"" + line + "\" came)"); 280 } 281 } 282 } while(allowGarbage); 283 284 throw new VCardException("Reached where must not be reached."); 285 } 286 287 /** 288 * The arguments useCache and allowGarbase are usually true and false accordingly when 289 * this function is called outside this function itself. 290 * 291 * @param useCache When true, line is obtained from mPreviousline. Otherwise, getLine() 292 * is used. 293 * @param allowGarbage When true, ignore non "END:VCARD" line. 294 * @throws IOException 295 * @throws VCardException 296 */ 297 protected void readEndVCard(boolean useCache, boolean allowGarbage) 298 throws IOException, VCardException { 299 String line; 300 do { 301 if (useCache) { 302 // Though vCard specification does not allow lower cases, 303 // some data may have them, so we allow it. 304 line = mPreviousLine; 305 } else { 306 while (true) { 307 line = getLine(); 308 if (line == null) { 309 throw new VCardException("Expected END:VCARD was not found."); 310 } else if (line.trim().length() > 0) { 311 break; 312 } 313 } 314 } 315 316 String[] strArray = line.split(":", 2); 317 if (strArray.length == 2 && 318 strArray[0].trim().equalsIgnoreCase("END") && 319 strArray[1].trim().equalsIgnoreCase("VCARD")) { 320 return; 321 } else if (!allowGarbage) { 322 throw new VCardException("END:VCARD != \"" + mPreviousLine + "\""); 323 } 324 useCache = false; 325 } while (allowGarbage); 326 } 327 328 /** 329 * items = *CRLF item 330 * / item 331 */ 332 protected void parseItems() throws IOException, VCardException { 333 boolean ended = false; 334 335 if (mBuilder != null) { 336 long start = System.currentTimeMillis(); 337 mBuilder.startProperty(); 338 mTimeStartProperty += System.currentTimeMillis() - start; 339 } 340 ended = parseItem(); 341 if (mBuilder != null && !ended) { 342 long start = System.currentTimeMillis(); 343 mBuilder.endProperty(); 344 mTimeEndProperty += System.currentTimeMillis() - start; 345 } 346 347 while (!ended) { 348 // follow VCARD ,it wont reach endProperty 349 if (mBuilder != null) { 350 long start = System.currentTimeMillis(); 351 mBuilder.startProperty(); 352 mTimeStartProperty += System.currentTimeMillis() - start; 353 } 354 try { 355 ended = parseItem(); 356 } catch (VCardInvalidCommentLineException e) { 357 Log.e(LOG_TAG, "Invalid line which looks like some comment was found. Ignored."); 358 ended = false; 359 } 360 if (mBuilder != null && !ended) { 361 long start = System.currentTimeMillis(); 362 mBuilder.endProperty(); 363 mTimeEndProperty += System.currentTimeMillis() - start; 364 } 365 } 366 } 367 368 /** 369 * item = [groups "."] name [params] ":" value CRLF 370 * / [groups "."] "ADR" [params] ":" addressparts CRLF 371 * / [groups "."] "ORG" [params] ":" orgparts CRLF 372 * / [groups "."] "N" [params] ":" nameparts CRLF 373 * / [groups "."] "AGENT" [params] ":" vcard CRLF 374 */ 375 protected boolean parseItem() throws IOException, VCardException { 376 mEncoding = sDefaultEncoding; 377 378 final String line = getNonEmptyLine(); 379 long start = System.currentTimeMillis(); 380 381 String[] propertyNameAndValue = separateLineAndHandleGroup(line); 382 if (propertyNameAndValue == null) { 383 return true; 384 } 385 if (propertyNameAndValue.length != 2) { 386 throw new VCardInvalidLineException("Invalid line \"" + line + "\""); 387 } 388 String propertyName = propertyNameAndValue[0].toUpperCase(); 389 String propertyValue = propertyNameAndValue[1]; 390 391 mTimeParseLineAndHandleGroup += System.currentTimeMillis() - start; 392 393 if (propertyName.equals("ADR") || propertyName.equals("ORG") || 394 propertyName.equals("N")) { 395 start = System.currentTimeMillis(); 396 handleMultiplePropertyValue(propertyName, propertyValue); 397 mTimeParseAdrOrgN += System.currentTimeMillis() - start; 398 return false; 399 } else if (propertyName.equals("AGENT")) { 400 handleAgent(propertyValue); 401 return false; 402 } else if (isValidPropertyName(propertyName)) { 403 if (propertyName.equals("BEGIN")) { 404 if (propertyValue.equals("VCARD")) { 405 throw new VCardNestedException("This vCard has nested vCard data in it."); 406 } else { 407 throw new VCardException("Unknown BEGIN type: " + propertyValue); 408 } 409 } else if (propertyName.equals("VERSION") && 410 !propertyValue.equals(getVersionString())) { 411 throw new VCardVersionException("Incompatible version: " + 412 propertyValue + " != " + getVersionString()); 413 } 414 start = System.currentTimeMillis(); 415 handlePropertyValue(propertyName, propertyValue); 416 mTimeParsePropertyValues += System.currentTimeMillis() - start; 417 return false; 418 } 419 420 throw new VCardException("Unknown property name: \"" + propertyName + "\""); 421 } 422 423 static private final int STATE_GROUP_OR_PROPNAME = 0; 424 static private final int STATE_PARAMS = 1; 425 // vCard 3.0 specification allows double-quoted param-value, while vCard 2.1 does not. 426 // This is just for safety. 427 static private final int STATE_PARAMS_IN_DQUOTE = 2; 428 429 protected String[] separateLineAndHandleGroup(String line) throws VCardException { 430 int state = STATE_GROUP_OR_PROPNAME; 431 int nameIndex = 0; 432 433 final String[] propertyNameAndValue = new String[2]; 434 435 final int length = line.length(); 436 if (length > 0 && line.charAt(0) == '#') { 437 throw new VCardInvalidCommentLineException(); 438 } 439 440 for (int i = 0; i < length; i++) { 441 char ch = line.charAt(i); 442 switch (state) { 443 case STATE_GROUP_OR_PROPNAME: { 444 if (ch == ':') { 445 final String propertyName = line.substring(nameIndex, i); 446 if (propertyName.equalsIgnoreCase("END")) { 447 mPreviousLine = line; 448 return null; 449 } 450 if (mBuilder != null) { 451 mBuilder.propertyName(propertyName); 452 } 453 propertyNameAndValue[0] = propertyName; 454 if (i < length - 1) { 455 propertyNameAndValue[1] = line.substring(i + 1); 456 } else { 457 propertyNameAndValue[1] = ""; 458 } 459 return propertyNameAndValue; 460 } else if (ch == '.') { 461 String groupName = line.substring(nameIndex, i); 462 if (mBuilder != null) { 463 mBuilder.propertyGroup(groupName); 464 } 465 nameIndex = i + 1; 466 } else if (ch == ';') { 467 String propertyName = line.substring(nameIndex, i); 468 if (propertyName.equalsIgnoreCase("END")) { 469 mPreviousLine = line; 470 return null; 471 } 472 if (mBuilder != null) { 473 mBuilder.propertyName(propertyName); 474 } 475 propertyNameAndValue[0] = propertyName; 476 nameIndex = i + 1; 477 state = STATE_PARAMS; 478 } 479 break; 480 } 481 case STATE_PARAMS: { 482 if (ch == '"') { 483 state = STATE_PARAMS_IN_DQUOTE; 484 } else if (ch == ';') { 485 handleParams(line.substring(nameIndex, i)); 486 nameIndex = i + 1; 487 } else if (ch == ':') { 488 handleParams(line.substring(nameIndex, i)); 489 if (i < length - 1) { 490 propertyNameAndValue[1] = line.substring(i + 1); 491 } else { 492 propertyNameAndValue[1] = ""; 493 } 494 return propertyNameAndValue; 495 } 496 break; 497 } 498 case STATE_PARAMS_IN_DQUOTE: { 499 if (ch == '"') { 500 state = STATE_PARAMS; 501 } 502 break; 503 } 504 } 505 } 506 507 throw new VCardInvalidLineException("Invalid line: \"" + line + "\""); 508 } 509 510 /** 511 * params = ";" [ws] paramlist 512 * paramlist = paramlist [ws] ";" [ws] param 513 * / param 514 * param = "TYPE" [ws] "=" [ws] ptypeval 515 * / "VALUE" [ws] "=" [ws] pvalueval 516 * / "ENCODING" [ws] "=" [ws] pencodingval 517 * / "CHARSET" [ws] "=" [ws] charsetval 518 * / "LANGUAGE" [ws] "=" [ws] langval 519 * / "X-" word [ws] "=" [ws] word 520 * / knowntype 521 */ 522 protected void handleParams(String params) throws VCardException { 523 String[] strArray = params.split("=", 2); 524 if (strArray.length == 2) { 525 final String paramName = strArray[0].trim().toUpperCase(); 526 String paramValue = strArray[1].trim(); 527 if (paramName.equals("TYPE")) { 528 handleType(paramValue); 529 } else if (paramName.equals("VALUE")) { 530 handleValue(paramValue); 531 } else if (paramName.equals("ENCODING")) { 532 handleEncoding(paramValue); 533 } else if (paramName.equals("CHARSET")) { 534 handleCharset(paramValue); 535 } else if (paramName.equals("LANGUAGE")) { 536 handleLanguage(paramValue); 537 } else if (paramName.startsWith("X-")) { 538 handleAnyParam(paramName, paramValue); 539 } else { 540 throw new VCardException("Unknown type \"" + paramName + "\""); 541 } 542 } else { 543 handleParamWithoutName(strArray[0]); 544 } 545 } 546 547 /** 548 * vCard 3.0 parser may throw VCardException. 549 */ 550 @SuppressWarnings("unused") 551 protected void handleParamWithoutName(final String paramValue) throws VCardException { 552 handleType(paramValue); 553 } 554 555 /** 556 * ptypeval = knowntype / "X-" word 557 */ 558 protected void handleType(final String ptypeval) { 559 String upperTypeValue = ptypeval; 560 if (!(sKnownTypeSet.contains(upperTypeValue) || upperTypeValue.startsWith("X-")) && 561 !mUnknownTypeMap.contains(ptypeval)) { 562 mUnknownTypeMap.add(ptypeval); 563 Log.w(LOG_TAG, "TYPE unsupported by vCard 2.1: " + ptypeval); 564 } 565 if (mBuilder != null) { 566 mBuilder.propertyParamType("TYPE"); 567 mBuilder.propertyParamValue(upperTypeValue); 568 } 569 } 570 571 /** 572 * pvalueval = "INLINE" / "URL" / "CONTENT-ID" / "CID" / "X-" word 573 */ 574 protected void handleValue(final String pvalueval) { 575 if (!sKnownValueSet.contains(pvalueval.toUpperCase()) && 576 pvalueval.startsWith("X-") && 577 !mUnknownValueMap.contains(pvalueval)) { 578 mUnknownValueMap.add(pvalueval); 579 Log.w(LOG_TAG, "VALUE unsupported by vCard 2.1: " + pvalueval); 580 } 581 if (mBuilder != null) { 582 mBuilder.propertyParamType("VALUE"); 583 mBuilder.propertyParamValue(pvalueval); 584 } 585 } 586 587 /** 588 * pencodingval = "7BIT" / "8BIT" / "QUOTED-PRINTABLE" / "BASE64" / "X-" word 589 */ 590 protected void handleEncoding(String pencodingval) throws VCardException { 591 if (isValidEncoding(pencodingval) || 592 pencodingval.startsWith("X-")) { 593 if (mBuilder != null) { 594 mBuilder.propertyParamType("ENCODING"); 595 mBuilder.propertyParamValue(pencodingval); 596 } 597 mEncoding = pencodingval; 598 } else { 599 throw new VCardException("Unknown encoding \"" + pencodingval + "\""); 600 } 601 } 602 603 /** 604 * vCard 2.1 specification only allows us-ascii and iso-8859-xxx (See RFC 1521), 605 * but today's vCard often contains other charset, so we allow them. 606 */ 607 protected void handleCharset(String charsetval) { 608 if (mBuilder != null) { 609 mBuilder.propertyParamType("CHARSET"); 610 mBuilder.propertyParamValue(charsetval); 611 } 612 } 613 614 /** 615 * See also Section 7.1 of RFC 1521 616 */ 617 protected void handleLanguage(String langval) throws VCardException { 618 String[] strArray = langval.split("-"); 619 if (strArray.length != 2) { 620 throw new VCardException("Invalid Language: \"" + langval + "\""); 621 } 622 String tmp = strArray[0]; 623 int length = tmp.length(); 624 for (int i = 0; i < length; i++) { 625 if (!isLetter(tmp.charAt(i))) { 626 throw new VCardException("Invalid Language: \"" + langval + "\""); 627 } 628 } 629 tmp = strArray[1]; 630 length = tmp.length(); 631 for (int i = 0; i < length; i++) { 632 if (!isLetter(tmp.charAt(i))) { 633 throw new VCardException("Invalid Language: \"" + langval + "\""); 634 } 635 } 636 if (mBuilder != null) { 637 mBuilder.propertyParamType("LANGUAGE"); 638 mBuilder.propertyParamValue(langval); 639 } 640 } 641 642 /** 643 * Mainly for "X-" type. This accepts any kind of type without check. 644 */ 645 protected void handleAnyParam(String paramName, String paramValue) { 646 if (mBuilder != null) { 647 mBuilder.propertyParamType(paramName); 648 mBuilder.propertyParamValue(paramValue); 649 } 650 } 651 652 protected void handlePropertyValue(String propertyName, String propertyValue) 653 throws IOException, VCardException { 654 if (mEncoding.equalsIgnoreCase("QUOTED-PRINTABLE")) { 655 final long start = System.currentTimeMillis(); 656 final String result = getQuotedPrintable(propertyValue); 657 if (mBuilder != null) { 658 ArrayList<String> v = new ArrayList<String>(); 659 v.add(result); 660 mBuilder.propertyValues(v); 661 } 662 mTimeHandleQuotedPrintable += System.currentTimeMillis() - start; 663 } else if (mEncoding.equalsIgnoreCase("BASE64") || 664 mEncoding.equalsIgnoreCase("B")) { 665 final long start = System.currentTimeMillis(); 666 // It is very rare, but some BASE64 data may be so big that 667 // OutOfMemoryError occurs. To ignore such cases, use try-catch. 668 try { 669 final String result = getBase64(propertyValue); 670 if (mBuilder != null) { 671 ArrayList<String> v = new ArrayList<String>(); 672 v.add(result); 673 mBuilder.propertyValues(v); 674 } 675 } catch (OutOfMemoryError error) { 676 Log.e(LOG_TAG, "OutOfMemoryError happened during parsing BASE64 data!"); 677 if (mBuilder != null) { 678 mBuilder.propertyValues(null); 679 } 680 } 681 mTimeHandleBase64 += System.currentTimeMillis() - start; 682 } else { 683 if (!(mEncoding == null || mEncoding.equalsIgnoreCase("7BIT") 684 || mEncoding.equalsIgnoreCase("8BIT") 685 || mEncoding.toUpperCase().startsWith("X-"))) { 686 Log.w(LOG_TAG, "The encoding unsupported by vCard spec: \"" + mEncoding + "\"."); 687 } 688 689 final long start = System.currentTimeMillis(); 690 if (mBuilder != null) { 691 ArrayList<String> v = new ArrayList<String>(); 692 v.add(maybeUnescapeText(propertyValue)); 693 mBuilder.propertyValues(v); 694 } 695 mTimeHandleMiscPropertyValue += System.currentTimeMillis() - start; 696 } 697 } 698 699 protected String getQuotedPrintable(String firstString) throws IOException, VCardException { 700 // Specifically, there may be some padding between = and CRLF. 701 // See the following: 702 // 703 // qp-line := *(qp-segment transport-padding CRLF) 704 // qp-part transport-padding 705 // qp-segment := qp-section *(SPACE / TAB) "=" 706 // ; Maximum length of 76 characters 707 // 708 // e.g. (from RFC 2045) 709 // Now's the time = 710 // for all folk to come= 711 // to the aid of their country. 712 if (firstString.trim().endsWith("=")) { 713 // remove "transport-padding" 714 int pos = firstString.length() - 1; 715 while(firstString.charAt(pos) != '=') { 716 } 717 StringBuilder builder = new StringBuilder(); 718 builder.append(firstString.substring(0, pos + 1)); 719 builder.append("\r\n"); 720 String line; 721 while (true) { 722 line = getLine(); 723 if (line == null) { 724 throw new VCardException( 725 "File ended during parsing quoted-printable String"); 726 } 727 if (line.trim().endsWith("=")) { 728 // remove "transport-padding" 729 pos = line.length() - 1; 730 while(line.charAt(pos) != '=') { 731 } 732 builder.append(line.substring(0, pos + 1)); 733 builder.append("\r\n"); 734 } else { 735 builder.append(line); 736 break; 737 } 738 } 739 return builder.toString(); 740 } else { 741 return firstString; 742 } 743 } 744 745 protected String getBase64(String firstString) throws IOException, VCardException { 746 StringBuilder builder = new StringBuilder(); 747 builder.append(firstString); 748 749 while (true) { 750 String line = getLine(); 751 if (line == null) { 752 throw new VCardException( 753 "File ended during parsing BASE64 binary"); 754 } 755 if (line.length() == 0) { 756 break; 757 } 758 builder.append(line); 759 } 760 761 return builder.toString(); 762 } 763 764 /** 765 * Mainly for "ADR", "ORG", and "N" 766 * We do not care the number of strnosemi here. 767 * 768 * addressparts = 0*6(strnosemi ";") strnosemi 769 * ; PO Box, Extended Addr, Street, Locality, Region, 770 * Postal Code, Country Name 771 * orgparts = *(strnosemi ";") strnosemi 772 * ; First is Organization Name, 773 * remainder are Organization Units. 774 * nameparts = 0*4(strnosemi ";") strnosemi 775 * ; Family, Given, Middle, Prefix, Suffix. 776 * ; Example:Public;John;Q.;Reverend Dr.;III, Esq. 777 * strnosemi = *(*nonsemi ("\;" / "\" CRLF)) *nonsemi 778 * ; To include a semicolon in this string, it must be escaped 779 * ; with a "\" character. 780 * 781 * We are not sure whether we should add "\" CRLF to each value. 782 * For now, we exclude them. 783 */ 784 protected void handleMultiplePropertyValue(String propertyName, String propertyValue) 785 throws IOException, VCardException { 786 // vCard 2.1 does not allow QUOTED-PRINTABLE here, 787 // but some softwares/devices emit such data. 788 if (mEncoding.equalsIgnoreCase("QUOTED-PRINTABLE")) { 789 propertyValue = getQuotedPrintable(propertyValue); 790 } 791 792 if (mBuilder != null) { 793 mBuilder.propertyValues(VCardUtils.constructListFromValue( 794 propertyValue, (getVersion() == VCardConfig.FLAG_V30))); 795 } 796 } 797 798 /** 799 * vCard 2.1 specifies AGENT allows one vcard entry. It is not encoded at all. 800 * 801 * item = ... 802 * / [groups "."] "AGENT" 803 * [params] ":" vcard CRLF 804 * vcard = "BEGIN" [ws] ":" [ws] "VCARD" [ws] 1*CRLF 805 * items *CRLF "END" [ws] ":" [ws] "VCARD" 806 */ 807 protected void handleAgent(final String propertyValue) throws VCardException { 808 if (!propertyValue.toUpperCase().contains("BEGIN:VCARD")) { 809 // Apparently invalid line seen in Windows Mobile 6.5. Ignore them. 810 return; 811 } else { 812 throw new VCardAgentNotSupportedException("AGENT Property is not supported now."); 813 } 814 // TODO: Support AGENT property. 815 } 816 817 /** 818 * For vCard 3.0. 819 */ 820 protected String maybeUnescapeText(final String text) { 821 return text; 822 } 823 824 /** 825 * Returns unescaped String if the character should be unescaped. Return null otherwise. 826 * e.g. In vCard 2.1, "\;" should be unescaped into ";" while "\x" should not be. 827 */ 828 protected String maybeUnescapeCharacter(final char ch) { 829 return unescapeCharacter(ch); 830 } 831 832 public static String unescapeCharacter(final char ch) { 833 // Original vCard 2.1 specification does not allow transformation 834 // "\:" -> ":", "\," -> ",", and "\\" -> "\", but previous implementation of 835 // this class allowed them, so keep it as is. 836 if (ch == '\\' || ch == ';' || ch == ':' || ch == ',') { 837 return String.valueOf(ch); 838 } else { 839 return null; 840 } 841 } 842 843 @Override 844 public boolean parse(final InputStream is, final VCardInterpreter builder) 845 throws IOException, VCardException { 846 return parse(is, VCardConfig.DEFAULT_CHARSET, builder); 847 } 848 849 @Override 850 public boolean parse(InputStream is, String charset, VCardInterpreter builder) 851 throws IOException, VCardException { 852 if (charset == null) { 853 charset = VCardConfig.DEFAULT_CHARSET; 854 } 855 final InputStreamReader tmpReader = new InputStreamReader(is, charset); 856 if (VCardConfig.showPerformanceLog()) { 857 mReader = new CustomBufferedReader(tmpReader); 858 } else { 859 mReader = new BufferedReader(tmpReader); 860 } 861 862 mBuilder = builder; 863 864 long start = System.currentTimeMillis(); 865 if (mBuilder != null) { 866 mBuilder.start(); 867 } 868 parseVCardFile(); 869 if (mBuilder != null) { 870 mBuilder.end(); 871 } 872 mTimeTotal += System.currentTimeMillis() - start; 873 874 if (VCardConfig.showPerformanceLog()) { 875 showPerformanceInfo(); 876 } 877 878 return true; 879 } 880 881 @Override 882 public void parse(InputStream is, String charset, VCardInterpreter builder, boolean canceled) 883 throws IOException, VCardException { 884 mCanceled = canceled; 885 parse(is, charset, builder); 886 } 887 888 private void showPerformanceInfo() { 889 Log.d(LOG_TAG, "Total parsing time: " + mTimeTotal + " ms"); 890 if (mReader instanceof CustomBufferedReader) { 891 Log.d(LOG_TAG, "Total readLine time: " + 892 ((CustomBufferedReader)mReader).getTotalmillisecond() + " ms"); 893 } 894 Log.d(LOG_TAG, "Time for handling the beggining of the record: " + 895 mTimeReadStartRecord + " ms"); 896 Log.d(LOG_TAG, "Time for handling the end of the record: " + 897 mTimeReadEndRecord + " ms"); 898 Log.d(LOG_TAG, "Time for parsing line, and handling group: " + 899 mTimeParseLineAndHandleGroup + " ms"); 900 Log.d(LOG_TAG, "Time for parsing ADR, ORG, and N fields:" + mTimeParseAdrOrgN + " ms"); 901 Log.d(LOG_TAG, "Time for parsing property values: " + mTimeParsePropertyValues + " ms"); 902 Log.d(LOG_TAG, "Time for handling normal property values: " + 903 mTimeHandleMiscPropertyValue + " ms"); 904 Log.d(LOG_TAG, "Time for handling Quoted-Printable: " + 905 mTimeHandleQuotedPrintable + " ms"); 906 Log.d(LOG_TAG, "Time for handling Base64: " + mTimeHandleBase64 + " ms"); 907 } 908 909 private boolean isLetter(char ch) { 910 if ((ch >= 'a' && ch <= 'z') || (ch >= 'A' && ch <= 'Z')) { 911 return true; 912 } 913 return false; 914 } 915 } 916 917 class CustomBufferedReader extends BufferedReader { 918 private long mTime; 919 920 public CustomBufferedReader(Reader in) { 921 super(in); 922 } 923 924 @Override 925 public String readLine() throws IOException { 926 long start = System.currentTimeMillis(); 927 String ret = super.readLine(); 928 long end = System.currentTimeMillis(); 929 mTime += end - start; 930 return ret; 931 } 932 933 public long getTotalmillisecond() { 934 return mTime; 935 } 936 } 937