Home | History | Annotate | Download | only in vcard
      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