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