VCardParserImpl_V21.java revision 4560bdde6dd75cca49fc55b58aafb5d416b88ca3
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.text.TextUtils;
19import android.util.Log;
20
21import com.android.vcard.exception.VCardAgentNotSupportedException;
22import com.android.vcard.exception.VCardException;
23import com.android.vcard.exception.VCardInvalidCommentLineException;
24import com.android.vcard.exception.VCardInvalidLineException;
25import com.android.vcard.exception.VCardNestedException;
26import com.android.vcard.exception.VCardVersionException;
27
28import java.io.BufferedReader;
29import java.io.IOException;
30import java.io.InputStream;
31import java.io.InputStreamReader;
32import java.io.Reader;
33import java.util.ArrayList;
34import java.util.HashSet;
35import java.util.List;
36import 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@example.com>
766            // FN:Omega
767            // END:VCARD
768            //
769            // The vCard above assumes that email address should become:
770            // "Omega" <omega@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