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