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