| /* |
| * Copyright (C) 2009 The Android Open Source Project |
| * |
| * Licensed under the Apache License, Version 2.0 (the "License"); |
| * you may not use this file except in compliance with the License. |
| * You may obtain a copy of the License at |
| * |
| * http://www.apache.org/licenses/LICENSE-2.0 |
| * |
| * Unless required by applicable law or agreed to in writing, software |
| * distributed under the License is distributed on an "AS IS" BASIS, |
| * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| * See the License for the specific language governing permissions and |
| * limitations under the License. |
| */ |
| package android.pim.vcard; |
| |
| import android.pim.vcard.exception.VCardException; |
| import android.pim.vcard.exception.VCardInvalidCommentLineException; |
| import android.pim.vcard.exception.VCardInvalidLineException; |
| import android.pim.vcard.exception.VCardNestedException; |
| import android.pim.vcard.exception.VCardNotSupportedException; |
| import android.pim.vcard.exception.VCardVersionException; |
| import android.util.Log; |
| |
| import java.io.BufferedReader; |
| import java.io.IOException; |
| import java.io.InputStream; |
| import java.io.InputStreamReader; |
| import java.io.Reader; |
| import java.util.ArrayList; |
| import java.util.Arrays; |
| import java.util.HashSet; |
| |
| /** |
| * This class is used to parse vcard. Please refer to vCard Specification 2.1. |
| */ |
| public class VCardParser_V21 extends VCardParser { |
| private static final String LOG_TAG = "vcard.VCardParser_V21"; |
| |
| /** Store the known-type */ |
| private static final HashSet<String> sKnownTypeSet = new HashSet<String>( |
| Arrays.asList("DOM", "INTL", "POSTAL", "PARCEL", "HOME", "WORK", |
| "PREF", "VOICE", "FAX", "MSG", "CELL", "PAGER", "BBS", |
| "MODEM", "CAR", "ISDN", "VIDEO", "AOL", "APPLELINK", |
| "ATTMAIL", "CIS", "EWORLD", "INTERNET", "IBMMAIL", |
| "MCIMAIL", "POWERSHARE", "PRODIGY", "TLX", "X400", "GIF", |
| "CGM", "WMF", "BMP", "MET", "PMB", "DIB", "PICT", "TIFF", |
| "PDF", "PS", "JPEG", "QTIME", "MPEG", "MPEG2", "AVI", |
| "WAVE", "AIFF", "PCM", "X509", "PGP")); |
| |
| /** Store the known-value */ |
| private static final HashSet<String> sKnownValueSet = new HashSet<String>( |
| Arrays.asList("INLINE", "URL", "CONTENT-ID", "CID")); |
| |
| /** Store the property names available in vCard 2.1 */ |
| private static final HashSet<String> sAvailablePropertyNameSetV21 = |
| new HashSet<String>(Arrays.asList( |
| "BEGIN", "LOGO", "PHOTO", "LABEL", "FN", "TITLE", "SOUND", |
| "VERSION", "TEL", "EMAIL", "TZ", "GEO", "NOTE", "URL", |
| "BDAY", "ROLE", "REV", "UID", "KEY", "MAILER")); |
| |
| /** |
| * Though vCard 2.1 specification does not allow "B" encoding, some data may have it. |
| * We allow it for safety... |
| */ |
| private static final HashSet<String> sAvailableEncodingV21 = |
| new HashSet<String>(Arrays.asList( |
| "7BIT", "8BIT", "QUOTED-PRINTABLE", "BASE64", "B")); |
| |
| // Used only for parsing END:VCARD. |
| private String mPreviousLine; |
| |
| /** The builder to build parsed data */ |
| protected VCardBuilder mBuilder = null; |
| |
| /** |
| * The encoding type. "Encoding" in vCard is different from "Charset". |
| * e.g. 7BIT, 8BIT, QUOTED-PRINTABLE. |
| */ |
| protected String mEncoding = null; |
| |
| protected final String sDefaultEncoding = "8BIT"; |
| |
| // Should not directly read a line from this. Use getLine() instead. |
| protected BufferedReader mReader; |
| |
| // In some cases, vCard is nested. Currently, we only consider the most interior vCard data. |
| // See v21_foma_1.vcf in test directory for more information. |
| private int mNestCount; |
| |
| // In order to reduce warning message as much as possible, we hold the value which made Logger |
| // emit a warning message. |
| protected HashSet<String> mWarningValueMap = new HashSet<String>(); |
| |
| // Just for debugging |
| private long mTimeTotal; |
| private long mTimeReadStartRecord; |
| private long mTimeReadEndRecord; |
| private long mTimeStartProperty; |
| private long mTimeEndProperty; |
| private long mTimeParseItems; |
| private long mTimeParseLineAndHandleGroup; |
| private long mTimeParsePropertyValues; |
| private long mTimeParseAdrOrgN; |
| private long mTimeHandleMiscPropertyValue; |
| private long mTimeHandleQuotedPrintable; |
| private long mTimeHandleBase64; |
| |
| /** |
| * Create a new VCard parser. |
| */ |
| public VCardParser_V21() { |
| super(); |
| } |
| |
| public VCardParser_V21(VCardSourceDetector detector) { |
| super(); |
| if (detector != null && detector.getType() == VCardSourceDetector.TYPE_FOMA) { |
| mNestCount = 1; |
| } |
| } |
| |
| /** |
| * Parse the file at the given position |
| * vcard_file = [wsls] vcard [wsls] |
| */ |
| protected void parseVCardFile() throws IOException, VCardException { |
| boolean firstReading = true; |
| while (true) { |
| if (mCanceled) { |
| break; |
| } |
| if (!parseOneVCard(firstReading)) { |
| break; |
| } |
| firstReading = false; |
| } |
| |
| if (mNestCount > 0) { |
| boolean useCache = true; |
| for (int i = 0; i < mNestCount; i++) { |
| readEndVCard(useCache, true); |
| useCache = false; |
| } |
| } |
| } |
| |
| protected String getVersion() { |
| return "2.1"; |
| } |
| |
| /** |
| * @return true when the propertyName is a valid property name. |
| */ |
| protected boolean isValidPropertyName(String propertyName) { |
| if (!(sAvailablePropertyNameSetV21.contains(propertyName.toUpperCase()) || |
| propertyName.startsWith("X-")) && |
| !mWarningValueMap.contains(propertyName)) { |
| mWarningValueMap.add(propertyName); |
| Log.w(LOG_TAG, "Property name unsupported by vCard 2.1: " + propertyName); |
| } |
| return true; |
| } |
| |
| /** |
| * @return true when the encoding is a valid encoding. |
| */ |
| protected boolean isValidEncoding(String encoding) { |
| return sAvailableEncodingV21.contains(encoding.toUpperCase()); |
| } |
| |
| /** |
| * @return String. It may be null, or its length may be 0 |
| * @throws IOException |
| */ |
| protected String getLine() throws IOException { |
| return mReader.readLine(); |
| } |
| |
| /** |
| * @return String with it's length > 0 |
| * @throws IOException |
| * @throws VCardException when the stream reached end of line |
| */ |
| protected String getNonEmptyLine() throws IOException, VCardException { |
| String line; |
| while (true) { |
| line = getLine(); |
| if (line == null) { |
| throw new VCardException("Reached end of buffer."); |
| } else if (line.trim().length() > 0) { |
| return line; |
| } |
| } |
| } |
| |
| /** |
| * vcard = "BEGIN" [ws] ":" [ws] "VCARD" [ws] 1*CRLF |
| * items *CRLF |
| * "END" [ws] ":" [ws] "VCARD" |
| */ |
| private boolean parseOneVCard(boolean firstReading) throws IOException, VCardException { |
| boolean allowGarbage = false; |
| if (firstReading) { |
| if (mNestCount > 0) { |
| for (int i = 0; i < mNestCount; i++) { |
| if (!readBeginVCard(allowGarbage)) { |
| return false; |
| } |
| allowGarbage = true; |
| } |
| } |
| } |
| |
| if (!readBeginVCard(allowGarbage)) { |
| return false; |
| } |
| long start; |
| if (mBuilder != null) { |
| start = System.currentTimeMillis(); |
| mBuilder.startRecord("VCARD"); |
| mTimeReadStartRecord += System.currentTimeMillis() - start; |
| } |
| start = System.currentTimeMillis(); |
| parseItems(); |
| mTimeParseItems += System.currentTimeMillis() - start; |
| readEndVCard(true, false); |
| if (mBuilder != null) { |
| start = System.currentTimeMillis(); |
| mBuilder.endRecord(); |
| mTimeReadEndRecord += System.currentTimeMillis() - start; |
| } |
| return true; |
| } |
| |
| /** |
| * @return True when successful. False when reaching the end of line |
| * @throws IOException |
| * @throws VCardException |
| */ |
| protected boolean readBeginVCard(boolean allowGarbage) |
| throws IOException, VCardException { |
| String line; |
| do { |
| while (true) { |
| line = getLine(); |
| if (line == null) { |
| return false; |
| } else if (line.trim().length() > 0) { |
| break; |
| } |
| } |
| String[] strArray = line.split(":", 2); |
| int length = strArray.length; |
| |
| // Though vCard 2.1/3.0 specification does not allow lower cases, |
| // some data may have them, so we allow it (Actually, previous code |
| // had explicitly allowed "BEGIN:vCard" though there's no example). |
| if (length == 2 && |
| strArray[0].trim().equalsIgnoreCase("BEGIN") && |
| strArray[1].trim().equalsIgnoreCase("VCARD")) { |
| return true; |
| } else if (!allowGarbage) { |
| if (mNestCount > 0) { |
| mPreviousLine = line; |
| return false; |
| } else { |
| throw new VCardException( |
| "Expected String \"BEGIN:VCARD\" did not come " |
| + "(Instead, \"" + line + "\" came)"); |
| } |
| } |
| } while(allowGarbage); |
| |
| throw new VCardException("Reached where must not be reached."); |
| } |
| |
| /** |
| * The arguments useCache and allowGarbase are usually true and false accordingly when |
| * this function is called outside this function itself. |
| * |
| * @param useCache When true, line is obtained from mPreviousline. Otherwise, getLine() |
| * is used. |
| * @param allowGarbage When true, ignore non "END:VCARD" line. |
| * @throws IOException |
| * @throws VCardException |
| */ |
| protected void readEndVCard(boolean useCache, boolean allowGarbage) |
| throws IOException, VCardException { |
| String line; |
| do { |
| if (useCache) { |
| // Though vCard specification does not allow lower cases, |
| // some data may have them, so we allow it. |
| line = mPreviousLine; |
| } else { |
| while (true) { |
| line = getLine(); |
| if (line == null) { |
| throw new VCardException("Expected END:VCARD was not found."); |
| } else if (line.trim().length() > 0) { |
| break; |
| } |
| } |
| } |
| |
| String[] strArray = line.split(":", 2); |
| if (strArray.length == 2 && |
| strArray[0].trim().equalsIgnoreCase("END") && |
| strArray[1].trim().equalsIgnoreCase("VCARD")) { |
| return; |
| } else if (!allowGarbage) { |
| throw new VCardException("END:VCARD != \"" + mPreviousLine + "\""); |
| } |
| useCache = false; |
| } while (allowGarbage); |
| } |
| |
| /** |
| * items = *CRLF item |
| * / item |
| */ |
| protected void parseItems() throws IOException, VCardException { |
| /* items *CRLF item / item */ |
| boolean ended = false; |
| |
| if (mBuilder != null) { |
| long start = System.currentTimeMillis(); |
| mBuilder.startProperty(); |
| mTimeStartProperty += System.currentTimeMillis() - start; |
| } |
| ended = parseItem(); |
| if (mBuilder != null && !ended) { |
| long start = System.currentTimeMillis(); |
| mBuilder.endProperty(); |
| mTimeEndProperty += System.currentTimeMillis() - start; |
| } |
| |
| while (!ended) { |
| // follow VCARD ,it wont reach endProperty |
| if (mBuilder != null) { |
| long start = System.currentTimeMillis(); |
| mBuilder.startProperty(); |
| mTimeStartProperty += System.currentTimeMillis() - start; |
| } |
| try { |
| ended = parseItem(); |
| } catch (VCardInvalidCommentLineException e) { |
| Log.e(LOG_TAG, "Invalid line which looks like some comment was found. Ignored."); |
| ended = false; |
| } |
| if (mBuilder != null && !ended) { |
| long start = System.currentTimeMillis(); |
| mBuilder.endProperty(); |
| mTimeEndProperty += System.currentTimeMillis() - start; |
| } |
| } |
| } |
| |
| /** |
| * item = [groups "."] name [params] ":" value CRLF |
| * / [groups "."] "ADR" [params] ":" addressparts CRLF |
| * / [groups "."] "ORG" [params] ":" orgparts CRLF |
| * / [groups "."] "N" [params] ":" nameparts CRLF |
| * / [groups "."] "AGENT" [params] ":" vcard CRLF |
| */ |
| protected boolean parseItem() throws IOException, VCardException { |
| mEncoding = sDefaultEncoding; |
| |
| String line = getNonEmptyLine(); |
| long start = System.currentTimeMillis(); |
| |
| String[] propertyNameAndValue = separateLineAndHandleGroup(line); |
| if (propertyNameAndValue == null) { |
| return true; |
| } |
| if (propertyNameAndValue.length != 2) { |
| throw new VCardInvalidLineException("Invalid line \"" + line + "\""); |
| } |
| String propertyName = propertyNameAndValue[0].toUpperCase(); |
| String propertyValue = propertyNameAndValue[1]; |
| |
| mTimeParseLineAndHandleGroup += System.currentTimeMillis() - start; |
| |
| if (propertyName.equals("ADR") || propertyName.equals("ORG") || |
| propertyName.equals("N")) { |
| start = System.currentTimeMillis(); |
| handleMultiplePropertyValue(propertyName, propertyValue); |
| mTimeParseAdrOrgN += System.currentTimeMillis() - start; |
| return false; |
| } else if (propertyName.equals("AGENT")) { |
| handleAgent(propertyValue); |
| return false; |
| } else if (isValidPropertyName(propertyName)) { |
| if (propertyName.equals("BEGIN")) { |
| if (propertyValue.equals("VCARD")) { |
| throw new VCardNestedException("This vCard has nested vCard data in it."); |
| } else { |
| throw new VCardException("Unknown BEGIN type: " + propertyValue); |
| } |
| } else if (propertyName.equals("VERSION") && !propertyValue.equals(getVersion())) { |
| throw new VCardVersionException("Incompatible version: " + |
| propertyValue + " != " + getVersion()); |
| } |
| start = System.currentTimeMillis(); |
| handlePropertyValue(propertyName, propertyValue); |
| mTimeParsePropertyValues += System.currentTimeMillis() - start; |
| return false; |
| } |
| |
| throw new VCardException("Unknown property name: \"" + |
| propertyName + "\""); |
| } |
| |
| static private final int STATE_GROUP_OR_PROPNAME = 0; |
| static private final int STATE_PARAMS = 1; |
| // vCard 3.1 specification allows double-quoted param-value, while vCard 2.1 does not. |
| // This is just for safety. |
| static private final int STATE_PARAMS_IN_DQUOTE = 2; |
| |
| protected String[] separateLineAndHandleGroup(String line) throws VCardException { |
| int length = line.length(); |
| int state = STATE_GROUP_OR_PROPNAME; |
| int nameIndex = 0; |
| |
| String[] propertyNameAndValue = new String[2]; |
| |
| if (length > 0 && line.charAt(0) == '#') { |
| throw new VCardInvalidCommentLineException(); |
| } |
| |
| for (int i = 0; i < length; i++) { |
| char ch = line.charAt(i); |
| switch (state) { |
| case STATE_GROUP_OR_PROPNAME: |
| if (ch == ':') { |
| String propertyName = line.substring(nameIndex, i); |
| if (propertyName.equalsIgnoreCase("END")) { |
| mPreviousLine = line; |
| return null; |
| } |
| if (mBuilder != null) { |
| mBuilder.propertyName(propertyName); |
| } |
| propertyNameAndValue[0] = propertyName; |
| if (i < length - 1) { |
| propertyNameAndValue[1] = line.substring(i + 1); |
| } else { |
| propertyNameAndValue[1] = ""; |
| } |
| return propertyNameAndValue; |
| } else if (ch == '.') { |
| String groupName = line.substring(nameIndex, i); |
| if (mBuilder != null) { |
| mBuilder.propertyGroup(groupName); |
| } |
| nameIndex = i + 1; |
| } else if (ch == ';') { |
| String propertyName = line.substring(nameIndex, i); |
| if (propertyName.equalsIgnoreCase("END")) { |
| mPreviousLine = line; |
| return null; |
| } |
| if (mBuilder != null) { |
| mBuilder.propertyName(propertyName); |
| } |
| propertyNameAndValue[0] = propertyName; |
| nameIndex = i + 1; |
| state = STATE_PARAMS; |
| } |
| break; |
| case STATE_PARAMS: |
| if (ch == '"') { |
| state = STATE_PARAMS_IN_DQUOTE; |
| } else if (ch == ';') { |
| handleParams(line.substring(nameIndex, i)); |
| nameIndex = i + 1; |
| } else if (ch == ':') { |
| handleParams(line.substring(nameIndex, i)); |
| if (i < length - 1) { |
| propertyNameAndValue[1] = line.substring(i + 1); |
| } else { |
| propertyNameAndValue[1] = ""; |
| } |
| return propertyNameAndValue; |
| } |
| break; |
| case STATE_PARAMS_IN_DQUOTE: |
| if (ch == '"') { |
| state = STATE_PARAMS; |
| } |
| break; |
| } |
| } |
| |
| throw new VCardInvalidLineException("Invalid line: \"" + line + "\""); |
| } |
| |
| |
| /** |
| * params = ";" [ws] paramlist |
| * paramlist = paramlist [ws] ";" [ws] param |
| * / param |
| * param = "TYPE" [ws] "=" [ws] ptypeval |
| * / "VALUE" [ws] "=" [ws] pvalueval |
| * / "ENCODING" [ws] "=" [ws] pencodingval |
| * / "CHARSET" [ws] "=" [ws] charsetval |
| * / "LANGUAGE" [ws] "=" [ws] langval |
| * / "X-" word [ws] "=" [ws] word |
| * / knowntype |
| */ |
| protected void handleParams(String params) throws VCardException { |
| String[] strArray = params.split("=", 2); |
| if (strArray.length == 2) { |
| String paramName = strArray[0].trim(); |
| String paramValue = strArray[1].trim(); |
| if (paramName.equals("TYPE")) { |
| handleType(paramValue); |
| } else if (paramName.equals("VALUE")) { |
| handleValue(paramValue); |
| } else if (paramName.equals("ENCODING")) { |
| handleEncoding(paramValue); |
| } else if (paramName.equals("CHARSET")) { |
| handleCharset(paramValue); |
| } else if (paramName.equals("LANGUAGE")) { |
| handleLanguage(paramValue); |
| } else if (paramName.startsWith("X-")) { |
| handleAnyParam(paramName, paramValue); |
| } else { |
| throw new VCardException("Unknown type \"" + paramName + "\""); |
| } |
| } else { |
| handleType(strArray[0]); |
| } |
| } |
| |
| /** |
| * ptypeval = knowntype / "X-" word |
| */ |
| protected void handleType(final String ptypeval) { |
| String upperTypeValue = ptypeval; |
| if (!(sKnownTypeSet.contains(upperTypeValue) || upperTypeValue.startsWith("X-")) && |
| !mWarningValueMap.contains(ptypeval)) { |
| mWarningValueMap.add(ptypeval); |
| Log.w(LOG_TAG, "Type unsupported by vCard 2.1: " + ptypeval); |
| } |
| if (mBuilder != null) { |
| mBuilder.propertyParamType("TYPE"); |
| mBuilder.propertyParamValue(upperTypeValue); |
| } |
| } |
| |
| /** |
| * pvalueval = "INLINE" / "URL" / "CONTENT-ID" / "CID" / "X-" word |
| */ |
| protected void handleValue(final String pvalueval) throws VCardException { |
| if (sKnownValueSet.contains(pvalueval.toUpperCase()) || |
| pvalueval.startsWith("X-")) { |
| if (mBuilder != null) { |
| mBuilder.propertyParamType("VALUE"); |
| mBuilder.propertyParamValue(pvalueval); |
| } |
| } else { |
| throw new VCardException("Unknown value \"" + pvalueval + "\""); |
| } |
| } |
| |
| /** |
| * pencodingval = "7BIT" / "8BIT" / "QUOTED-PRINTABLE" / "BASE64" / "X-" word |
| */ |
| protected void handleEncoding(String pencodingval) throws VCardException { |
| if (isValidEncoding(pencodingval) || |
| pencodingval.startsWith("X-")) { |
| if (mBuilder != null) { |
| mBuilder.propertyParamType("ENCODING"); |
| mBuilder.propertyParamValue(pencodingval); |
| } |
| mEncoding = pencodingval; |
| } else { |
| throw new VCardException("Unknown encoding \"" + pencodingval + "\""); |
| } |
| } |
| |
| /** |
| * vCard specification only allows us-ascii and iso-8859-xxx (See RFC 1521), |
| * but some vCard contains other charset, so we allow them. |
| */ |
| protected void handleCharset(String charsetval) { |
| if (mBuilder != null) { |
| mBuilder.propertyParamType("CHARSET"); |
| mBuilder.propertyParamValue(charsetval); |
| } |
| } |
| |
| /** |
| * See also Section 7.1 of RFC 1521 |
| */ |
| protected void handleLanguage(String langval) throws VCardException { |
| String[] strArray = langval.split("-"); |
| if (strArray.length != 2) { |
| throw new VCardException("Invalid Language: \"" + langval + "\""); |
| } |
| String tmp = strArray[0]; |
| int length = tmp.length(); |
| for (int i = 0; i < length; i++) { |
| if (!isLetter(tmp.charAt(i))) { |
| throw new VCardException("Invalid Language: \"" + langval + "\""); |
| } |
| } |
| tmp = strArray[1]; |
| length = tmp.length(); |
| for (int i = 0; i < length; i++) { |
| if (!isLetter(tmp.charAt(i))) { |
| throw new VCardException("Invalid Language: \"" + langval + "\""); |
| } |
| } |
| if (mBuilder != null) { |
| mBuilder.propertyParamType("LANGUAGE"); |
| mBuilder.propertyParamValue(langval); |
| } |
| } |
| |
| /** |
| * Mainly for "X-" type. This accepts any kind of type without check. |
| */ |
| protected void handleAnyParam(String paramName, String paramValue) { |
| if (mBuilder != null) { |
| mBuilder.propertyParamType(paramName); |
| mBuilder.propertyParamValue(paramValue); |
| } |
| } |
| |
| protected void handlePropertyValue(String propertyName, String propertyValue) throws |
| IOException, VCardException { |
| if (mEncoding.equalsIgnoreCase("QUOTED-PRINTABLE")) { |
| long start = System.currentTimeMillis(); |
| String result = getQuotedPrintable(propertyValue); |
| if (mBuilder != null) { |
| ArrayList<String> v = new ArrayList<String>(); |
| v.add(result); |
| mBuilder.propertyValues(v); |
| } |
| mTimeHandleQuotedPrintable += System.currentTimeMillis() - start; |
| } else if (mEncoding.equalsIgnoreCase("BASE64") || |
| mEncoding.equalsIgnoreCase("B")) { |
| long start = System.currentTimeMillis(); |
| // It is very rare, but some BASE64 data may be so big that |
| // OutOfMemoryError occurs. To ignore such cases, use try-catch. |
| try { |
| String result = getBase64(propertyValue); |
| if (mBuilder != null) { |
| ArrayList<String> v = new ArrayList<String>(); |
| v.add(result); |
| mBuilder.propertyValues(v); |
| } |
| } catch (OutOfMemoryError error) { |
| Log.e(LOG_TAG, "OutOfMemoryError happened during parsing BASE64 data!"); |
| if (mBuilder != null) { |
| mBuilder.propertyValues(null); |
| } |
| } |
| mTimeHandleBase64 += System.currentTimeMillis() - start; |
| } else { |
| if (!(mEncoding == null || mEncoding.equalsIgnoreCase("7BIT") |
| || mEncoding.equalsIgnoreCase("8BIT") |
| || mEncoding.toUpperCase().startsWith("X-"))) { |
| Log.w(LOG_TAG, "The encoding unsupported by vCard spec: \"" + mEncoding + "\"."); |
| } |
| |
| long start = System.currentTimeMillis(); |
| if (mBuilder != null) { |
| ArrayList<String> v = new ArrayList<String>(); |
| v.add(maybeUnescapeText(propertyValue)); |
| mBuilder.propertyValues(v); |
| } |
| mTimeHandleMiscPropertyValue += System.currentTimeMillis() - start; |
| } |
| } |
| |
| protected String getQuotedPrintable(String firstString) throws IOException, VCardException { |
| // Specifically, there may be some padding between = and CRLF. |
| // See the following: |
| // |
| // qp-line := *(qp-segment transport-padding CRLF) |
| // qp-part transport-padding |
| // qp-segment := qp-section *(SPACE / TAB) "=" |
| // ; Maximum length of 76 characters |
| // |
| // e.g. (from RFC 2045) |
| // Now's the time = |
| // for all folk to come= |
| // to the aid of their country. |
| if (firstString.trim().endsWith("=")) { |
| // remove "transport-padding" |
| int pos = firstString.length() - 1; |
| while(firstString.charAt(pos) != '=') { |
| } |
| StringBuilder builder = new StringBuilder(); |
| builder.append(firstString.substring(0, pos + 1)); |
| builder.append("\r\n"); |
| String line; |
| while (true) { |
| line = getLine(); |
| if (line == null) { |
| throw new VCardException( |
| "File ended during parsing quoted-printable String"); |
| } |
| if (line.trim().endsWith("=")) { |
| // remove "transport-padding" |
| pos = line.length() - 1; |
| while(line.charAt(pos) != '=') { |
| } |
| builder.append(line.substring(0, pos + 1)); |
| builder.append("\r\n"); |
| } else { |
| builder.append(line); |
| break; |
| } |
| } |
| return builder.toString(); |
| } else { |
| return firstString; |
| } |
| } |
| |
| protected String getBase64(String firstString) throws IOException, VCardException { |
| StringBuilder builder = new StringBuilder(); |
| builder.append(firstString); |
| |
| while (true) { |
| String line = getLine(); |
| if (line == null) { |
| throw new VCardException( |
| "File ended during parsing BASE64 binary"); |
| } |
| if (line.length() == 0) { |
| break; |
| } |
| builder.append(line); |
| } |
| |
| return builder.toString(); |
| } |
| |
| /** |
| * Mainly for "ADR", "ORG", and "N" |
| * We do not care the number of strnosemi here. |
| * |
| * addressparts = 0*6(strnosemi ";") strnosemi |
| * ; PO Box, Extended Addr, Street, Locality, Region, |
| * Postal Code, Country Name |
| * orgparts = *(strnosemi ";") strnosemi |
| * ; First is Organization Name, |
| * remainder are Organization Units. |
| * nameparts = 0*4(strnosemi ";") strnosemi |
| * ; Family, Given, Middle, Prefix, Suffix. |
| * ; Example:Public;John;Q.;Reverend Dr.;III, Esq. |
| * strnosemi = *(*nonsemi ("\;" / "\" CRLF)) *nonsemi |
| * ; To include a semicolon in this string, it must be escaped |
| * ; with a "\" character. |
| * |
| * We are not sure whether we should add "\" CRLF to each value. |
| * For now, we exclude them. |
| */ |
| protected void handleMultiplePropertyValue(String propertyName, String propertyValue) |
| throws IOException, VCardException { |
| // vCard 2.1 does not allow QUOTED-PRINTABLE here, |
| // but some softwares/devices emit such data. |
| if (mEncoding.equalsIgnoreCase("QUOTED-PRINTABLE")) { |
| propertyValue = getQuotedPrintable(propertyValue); |
| } |
| |
| if (mBuilder != null) { |
| StringBuilder builder = new StringBuilder(); |
| ArrayList<String> list = new ArrayList<String>(); |
| int length = propertyValue.length(); |
| for (int i = 0; i < length; i++) { |
| char ch = propertyValue.charAt(i); |
| if (ch == '\\' && i < length - 1) { |
| char nextCh = propertyValue.charAt(i + 1); |
| String unescapedString = maybeUnescapeCharacter(nextCh); |
| if (unescapedString != null) { |
| builder.append(unescapedString); |
| i++; |
| } else { |
| builder.append(ch); |
| } |
| } else if (ch == ';') { |
| list.add(builder.toString()); |
| builder = new StringBuilder(); |
| } else { |
| builder.append(ch); |
| } |
| } |
| list.add(builder.toString()); |
| mBuilder.propertyValues(list); |
| } |
| } |
| |
| /** |
| * vCard 2.1 specifies AGENT allows one vcard entry. It is not encoded at all. |
| * |
| * item = ... |
| * / [groups "."] "AGENT" |
| * [params] ":" vcard CRLF |
| * vcard = "BEGIN" [ws] ":" [ws] "VCARD" [ws] 1*CRLF |
| * items *CRLF "END" [ws] ":" [ws] "VCARD" |
| * |
| */ |
| protected void handleAgent(String propertyValue) throws VCardException { |
| throw new VCardNotSupportedException("AGENT Property is not supported now."); |
| /* This is insufficient support. Also, AGENT Property is very rare. |
| Ignore it for now. |
| |
| String[] strArray = propertyValue.split(":", 2); |
| if (!(strArray.length == 2 || |
| strArray[0].trim().equalsIgnoreCase("BEGIN") && |
| strArray[1].trim().equalsIgnoreCase("VCARD"))) { |
| throw new VCardException("BEGIN:VCARD != \"" + propertyValue + "\""); |
| } |
| parseItems(); |
| readEndVCard(); |
| */ |
| } |
| |
| /** |
| * For vCard 3.0. |
| */ |
| protected String maybeUnescapeText(String text) { |
| return text; |
| } |
| |
| /** |
| * Returns unescaped String if the character should be unescaped. Return null otherwise. |
| * e.g. In vCard 2.1, "\;" should be unescaped into ";" while "\x" should not be. |
| */ |
| protected String maybeUnescapeCharacter(char ch) { |
| // Original vCard 2.1 specification does not allow transformation |
| // "\:" -> ":", "\," -> ",", and "\\" -> "\", but previous implementation of |
| // this class allowed them, so keep it as is. |
| if (ch == '\\' || ch == ';' || ch == ':' || ch == ',') { |
| return String.valueOf(ch); |
| } else { |
| return null; |
| } |
| } |
| |
| @Override |
| public boolean parse(InputStream is, VCardBuilder builder) |
| throws IOException, VCardException { |
| return parse(is, VCardConfig.DEFAULT_CHARSET, builder); |
| } |
| |
| @Override |
| public boolean parse(InputStream is, String charset, VCardBuilder builder) |
| throws IOException, VCardException { |
| final InputStreamReader tmpReader = new InputStreamReader(is, charset); |
| if (VCardConfig.showPerformanceLog()) { |
| mReader = new CustomBufferedReader(tmpReader); |
| } else { |
| mReader = new BufferedReader(tmpReader); |
| } |
| |
| mBuilder = builder; |
| |
| long start = System.currentTimeMillis(); |
| if (mBuilder != null) { |
| mBuilder.start(); |
| } |
| parseVCardFile(); |
| if (mBuilder != null) { |
| mBuilder.end(); |
| } |
| mTimeTotal += System.currentTimeMillis() - start; |
| |
| if (VCardConfig.showPerformanceLog()) { |
| showPerformanceInfo(); |
| } |
| |
| return true; |
| } |
| |
| @Override |
| public void parse(InputStream is, String charset, VCardBuilder builder, boolean canceled) |
| throws IOException, VCardException { |
| mCanceled = canceled; |
| parse(is, charset, builder); |
| } |
| |
| private void showPerformanceInfo() { |
| Log.d(LOG_TAG, "Total parsing time: " + mTimeTotal + " ms"); |
| if (mReader instanceof CustomBufferedReader) { |
| Log.d(LOG_TAG, "Total readLine time: " + |
| ((CustomBufferedReader)mReader).getTotalmillisecond() + " ms"); |
| } |
| Log.d(LOG_TAG, "Time for handling the beggining of the record: " + |
| mTimeReadStartRecord + " ms"); |
| Log.d(LOG_TAG, "Time for handling the end of the record: " + |
| mTimeReadEndRecord + " ms"); |
| Log.d(LOG_TAG, "Time for parsing line, and handling group: " + |
| mTimeParseLineAndHandleGroup + " ms"); |
| Log.d(LOG_TAG, "Time for parsing ADR, ORG, and N fields:" + mTimeParseAdrOrgN + " ms"); |
| Log.d(LOG_TAG, "Time for parsing property values: " + mTimeParsePropertyValues + " ms"); |
| Log.d(LOG_TAG, "Time for handling normal property values: " + |
| mTimeHandleMiscPropertyValue + " ms"); |
| Log.d(LOG_TAG, "Time for handling Quoted-Printable: " + |
| mTimeHandleQuotedPrintable + " ms"); |
| Log.d(LOG_TAG, "Time for handling Base64: " + mTimeHandleBase64 + " ms"); |
| } |
| |
| private boolean isLetter(char ch) { |
| if ((ch >= 'a' && ch <= 'z') || (ch >= 'A' && ch <= 'Z')) { |
| return true; |
| } |
| return false; |
| } |
| } |
| |
| class CustomBufferedReader extends BufferedReader { |
| private long mTime; |
| |
| public CustomBufferedReader(Reader in) { |
| super(in); |
| } |
| |
| @Override |
| public String readLine() throws IOException { |
| long start = System.currentTimeMillis(); |
| String ret = super.readLine(); |
| long end = System.currentTimeMillis(); |
| mTime += end - start; |
| return ret; |
| } |
| |
| public long getTotalmillisecond() { |
| return mTime; |
| } |
| } |