| /* |
| * Copyright (C) 2010 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 com.android.vcard; |
| |
| import android.text.TextUtils; |
| import android.util.Base64; |
| import android.util.Log; |
| |
| import com.android.vcard.exception.VCardAgentNotSupportedException; |
| import com.android.vcard.exception.VCardException; |
| import com.android.vcard.exception.VCardInvalidCommentLineException; |
| import com.android.vcard.exception.VCardInvalidLineException; |
| import com.android.vcard.exception.VCardVersionException; |
| |
| 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.Collection; |
| import java.util.HashSet; |
| import java.util.List; |
| import java.util.Set; |
| |
| /** |
| * <p> |
| * Basic implementation achieving vCard parsing. Based on vCard 2.1. |
| * </p> |
| * @hide |
| */ |
| /* package */ class VCardParserImpl_V21 { |
| private static final String LOG_TAG = VCardConstants.LOG_TAG; |
| |
| protected static final class CustomBufferedReader extends BufferedReader { |
| private long mTime; |
| |
| /** |
| * Needed since "next line" may be null due to end of line. |
| */ |
| private boolean mNextLineIsValid; |
| private String mNextLine; |
| |
| public CustomBufferedReader(Reader in) { |
| super(in); |
| } |
| |
| @Override |
| public String readLine() throws IOException { |
| if (mNextLineIsValid) { |
| final String ret = mNextLine; |
| mNextLine = null; |
| mNextLineIsValid = false; |
| return ret; |
| } |
| |
| final long start = System.currentTimeMillis(); |
| final String line = super.readLine(); |
| final long end = System.currentTimeMillis(); |
| mTime += end - start; |
| return line; |
| } |
| |
| /** |
| * Read one line, but make this object store it in its queue. |
| */ |
| public String peekLine() throws IOException { |
| if (!mNextLineIsValid) { |
| final long start = System.currentTimeMillis(); |
| final String line = super.readLine(); |
| final long end = System.currentTimeMillis(); |
| mTime += end - start; |
| |
| mNextLine = line; |
| mNextLineIsValid = true; |
| } |
| |
| return mNextLine; |
| } |
| |
| public long getTotalmillisecond() { |
| return mTime; |
| } |
| } |
| |
| private static final String DEFAULT_ENCODING = "8BIT"; |
| private static final String DEFAULT_CHARSET = "UTF-8"; |
| |
| protected final String mIntermediateCharset; |
| |
| private final List<VCardInterpreter> mInterpreterList = new ArrayList<VCardInterpreter>(); |
| private boolean mCanceled; |
| |
| /** |
| * <p> |
| * The encoding type for deconding byte streams. This member variable is |
| * reset to a default encoding every time when a new item comes. |
| * </p> |
| * <p> |
| * "Encoding" in vCard is different from "Charset". It is mainly used for |
| * addresses, notes, images. "7BIT", "8BIT", "BASE64", and |
| * "QUOTED-PRINTABLE" are known examples. |
| * </p> |
| */ |
| protected String mCurrentEncoding; |
| |
| protected String mCurrentCharset; |
| |
| /** |
| * <p> |
| * The reader object to be used internally. |
| * </p> |
| * <p> |
| * Developers should not directly read a line from this object. Use |
| * getLine() unless there some reason. |
| * </p> |
| */ |
| protected CustomBufferedReader mReader; |
| |
| /** |
| * <p> |
| * Set for storing unkonwn TYPE attributes, which is not acceptable in vCard |
| * specification, but happens to be seen in real world vCard. |
| * </p> |
| * <p> |
| * We just accept those invalid types after emitting a warning for each of it. |
| * </p> |
| */ |
| protected final Set<String> mUnknownTypeSet = new HashSet<String>(); |
| |
| /** |
| * <p> |
| * Set for storing unkonwn VALUE attributes, which is not acceptable in |
| * vCard specification, but happens to be seen in real world vCard. |
| * </p> |
| * <p> |
| * We just accept those invalid types after emitting a warning for each of it. |
| * </p> |
| */ |
| protected final Set<String> mUnknownValueSet = new HashSet<String>(); |
| |
| |
| public VCardParserImpl_V21() { |
| this(VCardConfig.VCARD_TYPE_DEFAULT); |
| } |
| |
| public VCardParserImpl_V21(int vcardType) { |
| mIntermediateCharset = VCardConfig.DEFAULT_INTERMEDIATE_CHARSET; |
| } |
| |
| /** |
| * @return true when a given property name is a valid property name. |
| */ |
| protected boolean isValidPropertyName(final String propertyName) { |
| if (!(getKnownPropertyNameSet().contains(propertyName.toUpperCase()) || |
| propertyName.startsWith("X-")) |
| && !mUnknownTypeSet.contains(propertyName)) { |
| mUnknownTypeSet.add(propertyName); |
| Log.w(LOG_TAG, "Property name unsupported by vCard 2.1: " + propertyName); |
| } |
| return true; |
| } |
| |
| /** |
| * @return String. It may be null, or its length may be 0 |
| * @throws IOException |
| */ |
| protected String getLine() throws IOException { |
| return mReader.readLine(); |
| } |
| |
| protected String peekLine() throws IOException { |
| return mReader.peekLine(); |
| } |
| |
| /** |
| * @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; |
| } |
| } |
| } |
| |
| /** |
| * <code> |
| * vcard = "BEGIN" [ws] ":" [ws] "VCARD" [ws] 1*CRLF |
| * items *CRLF |
| * "END" [ws] ":" [ws] "VCARD" |
| * </code> |
| * @return False when reaching end of file. |
| */ |
| private boolean parseOneVCard() throws IOException, VCardException { |
| // reset for this entire vCard. |
| mCurrentEncoding = DEFAULT_ENCODING; |
| mCurrentCharset = DEFAULT_CHARSET; |
| |
| boolean allowGarbage = false; |
| if (!readBeginVCard(allowGarbage)) { |
| return false; |
| } |
| for (VCardInterpreter interpreter : mInterpreterList) { |
| interpreter.onEntryStarted(); |
| } |
| parseItems(); |
| for (VCardInterpreter interpreter : mInterpreterList) { |
| interpreter.onEntryEnded(); |
| } |
| 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 { |
| // TODO: use consructPropertyLine(). |
| String line; |
| do { |
| while (true) { |
| line = getLine(); |
| if (line == null) { |
| return false; |
| } else if (line.trim().length() > 0) { |
| break; |
| } |
| } |
| final String[] strArray = line.split(":", 2); |
| final int length = strArray.length; |
| |
| // Although vCard 2.1/3.0 specification does not allow lower cases, |
| // we found vCard file emitted by some external vCard expoter have such |
| // invalid Strings. |
| // e.g. BEGIN:vCard |
| if (length == 2 && strArray[0].trim().equalsIgnoreCase("BEGIN") |
| && strArray[1].trim().equalsIgnoreCase("VCARD")) { |
| return true; |
| } else if (!allowGarbage) { |
| throw new VCardException("Expected String \"BEGIN:VCARD\" did not come " |
| + "(Instead, \"" + line + "\" came)"); |
| } |
| } while (allowGarbage); |
| |
| throw new VCardException("Reached where must not be reached."); |
| } |
| |
| /** |
| * Parses lines other than the first "BEGIN:VCARD". Takes care of "END:VCARD"n and |
| * "BEGIN:VCARD" in nested vCard. |
| */ |
| /* |
| * items = *CRLF item / item |
| * |
| * Note: BEGIN/END aren't include in the original spec while this method handles them. |
| */ |
| protected void parseItems() throws IOException, VCardException { |
| boolean ended = false; |
| |
| try { |
| ended = parseItem(); |
| } catch (VCardInvalidCommentLineException e) { |
| Log.e(LOG_TAG, "Invalid line which looks like some comment was found. Ignored."); |
| } |
| |
| while (!ended) { |
| try { |
| ended = parseItem(); |
| } catch (VCardInvalidCommentLineException e) { |
| Log.e(LOG_TAG, "Invalid line which looks like some comment was found. Ignored."); |
| } |
| } |
| } |
| |
| /* |
| * 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 { |
| // Reset for an item. |
| mCurrentEncoding = DEFAULT_ENCODING; |
| |
| final String line = getNonEmptyLine(); |
| final VCardProperty propertyData = constructPropertyData(line); |
| |
| final String propertyNameUpper = propertyData.getName().toUpperCase(); |
| final String propertyRawValue = propertyData.getRawValue(); |
| |
| if (propertyNameUpper.equals(VCardConstants.PROPERTY_BEGIN)) { |
| if (propertyRawValue.equalsIgnoreCase("VCARD")) { |
| handleNest(); |
| } else { |
| throw new VCardException("Unknown BEGIN type: " + propertyRawValue); |
| } |
| } else if (propertyNameUpper.equals(VCardConstants.PROPERTY_END)) { |
| if (propertyRawValue.equalsIgnoreCase("VCARD")) { |
| return true; // Ended. |
| } else { |
| throw new VCardException("Unknown END type: " + propertyRawValue); |
| } |
| } else { |
| parseItemInter(propertyData, propertyNameUpper); |
| } |
| return false; |
| } |
| |
| private void parseItemInter(VCardProperty property, String propertyNameUpper) |
| throws IOException, VCardException { |
| String propertyRawValue = property.getRawValue(); |
| if (propertyNameUpper.equals(VCardConstants.PROPERTY_AGENT)) { |
| handleAgent(property); |
| } else if (isValidPropertyName(propertyNameUpper)) { |
| if (propertyNameUpper.equals(VCardConstants.PROPERTY_VERSION) && |
| !propertyRawValue.equals(getVersionString())) { |
| throw new VCardVersionException( |
| "Incompatible version: " + propertyRawValue + " != " + getVersionString()); |
| } |
| handlePropertyValue(property, propertyNameUpper); |
| } else { |
| throw new VCardException("Unknown property name: \"" + propertyNameUpper + "\""); |
| } |
| } |
| |
| private void handleNest() throws IOException, VCardException { |
| for (VCardInterpreter interpreter : mInterpreterList) { |
| interpreter.onEntryStarted(); |
| } |
| parseItems(); |
| for (VCardInterpreter interpreter : mInterpreterList) { |
| interpreter.onEntryEnded(); |
| } |
| } |
| |
| // For performance reason, the states for group and property name are merged into one. |
| static private final int STATE_GROUP_OR_PROPERTY_NAME = 0; |
| static private final int STATE_PARAMS = 1; |
| // vCard 3.0 specification allows double-quoted parameters, while vCard 2.1 does not. |
| static private final int STATE_PARAMS_IN_DQUOTE = 2; |
| |
| protected VCardProperty constructPropertyData(String line) throws VCardException { |
| final VCardProperty propertyData = new VCardProperty(); |
| |
| final int length = line.length(); |
| if (length > 0 && line.charAt(0) == '#') { |
| throw new VCardInvalidCommentLineException(); |
| } |
| |
| int state = STATE_GROUP_OR_PROPERTY_NAME; |
| int nameIndex = 0; |
| |
| // This loop is developed so that we don't have to take care of bottle neck here. |
| // Refactor carefully when you need to do so. |
| for (int i = 0; i < length; i++) { |
| final char ch = line.charAt(i); |
| switch (state) { |
| case STATE_GROUP_OR_PROPERTY_NAME: { |
| if (ch == ':') { // End of a property name. |
| final String propertyName = line.substring(nameIndex, i); |
| propertyData.setName(propertyName); |
| propertyData.setRawValue( i < length - 1 ? line.substring(i + 1) : ""); |
| return propertyData; |
| } else if (ch == '.') { // Each group is followed by the dot. |
| final String groupName = line.substring(nameIndex, i); |
| if (groupName.length() == 0) { |
| Log.w(LOG_TAG, "Empty group found. Ignoring."); |
| } else { |
| propertyData.addGroup(groupName); |
| } |
| nameIndex = i + 1; // Next should be another group or a property name. |
| } else if (ch == ';') { // End of property name and beginneng of parameters. |
| final String propertyName = line.substring(nameIndex, i); |
| propertyData.setName(propertyName); |
| nameIndex = i + 1; |
| state = STATE_PARAMS; // Start parameter parsing. |
| } |
| // TODO: comma support (in vCard 3.0 and 4.0). |
| break; |
| } |
| case STATE_PARAMS: { |
| if (ch == '"') { |
| if (VCardConstants.VERSION_V21.equalsIgnoreCase(getVersionString())) { |
| Log.w(LOG_TAG, "Double-quoted params found in vCard 2.1. " + |
| "Silently allow it"); |
| } |
| state = STATE_PARAMS_IN_DQUOTE; |
| } else if (ch == ';') { // Starts another param. |
| handleParams(propertyData, line.substring(nameIndex, i)); |
| nameIndex = i + 1; |
| } else if (ch == ':') { // End of param and beginenning of values. |
| handleParams(propertyData, line.substring(nameIndex, i)); |
| propertyData.setRawValue(i < length - 1 ? line.substring(i + 1) : ""); |
| return propertyData; |
| } |
| break; |
| } |
| case STATE_PARAMS_IN_DQUOTE: { |
| if (ch == '"') { |
| if (VCardConstants.VERSION_V21.equalsIgnoreCase(getVersionString())) { |
| Log.w(LOG_TAG, "Double-quoted params found in vCard 2.1. " + |
| "Silently allow it"); |
| } |
| 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(VCardProperty propertyData, String params) |
| throws VCardException { |
| final String[] strArray = params.split("=", 2); |
| if (strArray.length == 2) { |
| final String paramName = strArray[0].trim().toUpperCase(); |
| String paramValue = strArray[1].trim(); |
| if (paramName.equals("TYPE")) { |
| handleType(propertyData, paramValue); |
| } else if (paramName.equals("VALUE")) { |
| handleValue(propertyData, paramValue); |
| } else if (paramName.equals("ENCODING")) { |
| handleEncoding(propertyData, paramValue.toUpperCase()); |
| } else if (paramName.equals("CHARSET")) { |
| handleCharset(propertyData, paramValue); |
| } else if (paramName.equals("LANGUAGE")) { |
| handleLanguage(propertyData, paramValue); |
| } else if (paramName.startsWith("X-")) { |
| handleAnyParam(propertyData, paramName, paramValue); |
| } else { |
| throw new VCardException("Unknown type \"" + paramName + "\""); |
| } |
| } else { |
| handleParamWithoutName(propertyData, strArray[0]); |
| } |
| } |
| |
| /** |
| * vCard 3.0 parser implementation may throw VCardException. |
| */ |
| protected void handleParamWithoutName(VCardProperty propertyData, final String paramValue) { |
| handleType(propertyData, paramValue); |
| } |
| |
| /* |
| * ptypeval = knowntype / "X-" word |
| */ |
| protected void handleType(VCardProperty propertyData, final String ptypeval) { |
| if (!(getKnownTypeSet().contains(ptypeval.toUpperCase()) |
| || ptypeval.startsWith("X-")) |
| && !mUnknownTypeSet.contains(ptypeval)) { |
| mUnknownTypeSet.add(ptypeval); |
| Log.w(LOG_TAG, String.format("TYPE unsupported by %s: ", getVersion(), ptypeval)); |
| } |
| propertyData.addParameter(VCardConstants.PARAM_TYPE, ptypeval); |
| } |
| |
| /* |
| * pvalueval = "INLINE" / "URL" / "CONTENT-ID" / "CID" / "X-" word |
| */ |
| protected void handleValue(VCardProperty propertyData, final String pvalueval) { |
| if (!(getKnownValueSet().contains(pvalueval.toUpperCase()) |
| || pvalueval.startsWith("X-") |
| || mUnknownValueSet.contains(pvalueval))) { |
| mUnknownValueSet.add(pvalueval); |
| Log.w(LOG_TAG, String.format( |
| "The value unsupported by TYPE of %s: ", getVersion(), pvalueval)); |
| } |
| propertyData.addParameter(VCardConstants.PARAM_VALUE, pvalueval); |
| } |
| |
| /* |
| * pencodingval = "7BIT" / "8BIT" / "QUOTED-PRINTABLE" / "BASE64" / "X-" word |
| */ |
| protected void handleEncoding(VCardProperty propertyData, String pencodingval) |
| throws VCardException { |
| if (getAvailableEncodingSet().contains(pencodingval) || |
| pencodingval.startsWith("X-")) { |
| propertyData.addParameter(VCardConstants.PARAM_ENCODING, pencodingval); |
| // Update encoding right away, as this is needed to understanding other params. |
| mCurrentEncoding = pencodingval.toUpperCase(); |
| } else { |
| throw new VCardException("Unknown encoding \"" + pencodingval + "\""); |
| } |
| } |
| |
| /** |
| * <p> |
| * vCard 2.1 specification only allows us-ascii and iso-8859-xxx (See RFC 1521), |
| * but recent vCard files often contain other charset like UTF-8, SHIFT_JIS, etc. |
| * We allow any charset. |
| * </p> |
| */ |
| protected void handleCharset(VCardProperty propertyData, String charsetval) { |
| mCurrentCharset = charsetval; |
| propertyData.addParameter(VCardConstants.PARAM_CHARSET, charsetval); |
| } |
| |
| /** |
| * See also Section 7.1 of RFC 1521 |
| */ |
| protected void handleLanguage(VCardProperty propertyData, 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 (!isAsciiLetter(tmp.charAt(i))) { |
| throw new VCardException("Invalid Language: \"" + langval + "\""); |
| } |
| } |
| tmp = strArray[1]; |
| length = tmp.length(); |
| for (int i = 0; i < length; i++) { |
| if (!isAsciiLetter(tmp.charAt(i))) { |
| throw new VCardException("Invalid Language: \"" + langval + "\""); |
| } |
| } |
| propertyData.addParameter(VCardConstants.PARAM_LANGUAGE, langval); |
| } |
| |
| private boolean isAsciiLetter(char ch) { |
| if ((ch >= 'a' && ch <= 'z') || (ch >= 'A' && ch <= 'Z')) { |
| return true; |
| } |
| return false; |
| } |
| |
| /** |
| * Mainly for "X-" type. This accepts any kind of type without check. |
| */ |
| protected void handleAnyParam( |
| VCardProperty propertyData, String paramName, String paramValue) { |
| propertyData.addParameter(paramName, paramValue); |
| } |
| |
| protected void handlePropertyValue(VCardProperty property, String propertyName) |
| throws IOException, VCardException { |
| final String propertyNameUpper = property.getName().toUpperCase(); |
| String propertyRawValue = property.getRawValue(); |
| final String sourceCharset = VCardConfig.DEFAULT_INTERMEDIATE_CHARSET; |
| final Collection<String> charsetCollection = |
| property.getParameters(VCardConstants.PARAM_CHARSET); |
| String targetCharset = |
| ((charsetCollection != null) ? charsetCollection.iterator().next() : null); |
| if (TextUtils.isEmpty(targetCharset)) { |
| targetCharset = VCardConfig.DEFAULT_IMPORT_CHARSET; |
| } |
| |
| // TODO: have "separableProperty" which reflects vCard spec.. |
| if (propertyNameUpper.equals(VCardConstants.PROPERTY_ADR) |
| || propertyNameUpper.equals(VCardConstants.PROPERTY_ORG) |
| || propertyNameUpper.equals(VCardConstants.PROPERTY_N)) { |
| handleAdrOrgN(property, propertyRawValue, sourceCharset, targetCharset); |
| return; |
| } |
| |
| if (mCurrentEncoding.equals(VCardConstants.PARAM_ENCODING_QP) || |
| // If encoding attribute is missing, then attempt to detect QP encoding. |
| // This is to handle a bug where the android exporter was creating FN properties |
| // with missing encoding. b/7292017 |
| (propertyNameUpper.equals(VCardConstants.PROPERTY_FN) && |
| property.getParameters(VCardConstants.PARAM_ENCODING) == null && |
| VCardUtils.appearsLikeAndroidVCardQuotedPrintable(propertyRawValue)) |
| ) { |
| final String quotedPrintablePart = getQuotedPrintablePart(propertyRawValue); |
| final String propertyEncodedValue = |
| VCardUtils.parseQuotedPrintable(quotedPrintablePart, |
| false, sourceCharset, targetCharset); |
| property.setRawValue(quotedPrintablePart); |
| property.setValues(propertyEncodedValue); |
| for (VCardInterpreter interpreter : mInterpreterList) { |
| interpreter.onPropertyCreated(property); |
| } |
| } else if (mCurrentEncoding.equals(VCardConstants.PARAM_ENCODING_BASE64) |
| || mCurrentEncoding.equals(VCardConstants.PARAM_ENCODING_B)) { |
| // It is very rare, but some BASE64 data may be so big that |
| // OutOfMemoryError occurs. To ignore such cases, use try-catch. |
| try { |
| final String base64Property = getBase64(propertyRawValue); |
| try { |
| property.setByteValue(Base64.decode(base64Property, Base64.DEFAULT)); |
| } catch (IllegalArgumentException e) { |
| throw new VCardException("Decode error on base64 photo: " + propertyRawValue); |
| } |
| for (VCardInterpreter interpreter : mInterpreterList) { |
| interpreter.onPropertyCreated(property); |
| } |
| } catch (OutOfMemoryError error) { |
| Log.e(LOG_TAG, "OutOfMemoryError happened during parsing BASE64 data!"); |
| for (VCardInterpreter interpreter : mInterpreterList) { |
| interpreter.onPropertyCreated(property); |
| } |
| } |
| } else { |
| if (!(mCurrentEncoding.equals("7BIT") || mCurrentEncoding.equals("8BIT") || |
| mCurrentEncoding.startsWith("X-"))) { |
| Log.w(LOG_TAG, |
| String.format("The encoding \"%s\" is unsupported by vCard %s", |
| mCurrentEncoding, getVersionString())); |
| } |
| |
| // Some device uses line folding defined in RFC 2425, which is not allowed |
| // in vCard 2.1 (while needed in vCard 3.0). |
| // |
| // e.g. |
| // BEGIN:VCARD |
| // VERSION:2.1 |
| // N:;Omega;;; |
| // EMAIL;INTERNET:"Omega" |
| // <omega@example.com> |
| // FN:Omega |
| // END:VCARD |
| // |
| // The vCard above assumes that email address should become: |
| // "Omega" <omega@example.com> |
| // |
| // But vCard 2.1 requires Quote-Printable when a line contains line break(s). |
| // |
| // For more information about line folding, |
| // see "5.8.1. Line delimiting and folding" in RFC 2425. |
| // |
| // We take care of this case more formally in vCard 3.0, so we only need to |
| // do this in vCard 2.1. |
| if (getVersion() == VCardConfig.VERSION_21) { |
| StringBuilder builder = null; |
| while (true) { |
| final String nextLine = peekLine(); |
| // We don't need to care too much about this exceptional case, |
| // but we should not wrongly eat up "END:VCARD", since it critically |
| // breaks this parser's state machine. |
| // Thus we roughly look over the next line and confirm it is at least not |
| // "END:VCARD". This extra fee is worth paying. This is exceptional |
| // anyway. |
| if (!TextUtils.isEmpty(nextLine) && |
| nextLine.charAt(0) == ' ' && |
| !"END:VCARD".contains(nextLine.toUpperCase())) { |
| getLine(); // Drop the next line. |
| |
| if (builder == null) { |
| builder = new StringBuilder(); |
| builder.append(propertyRawValue); |
| } |
| builder.append(nextLine.substring(1)); |
| } else { |
| break; |
| } |
| } |
| if (builder != null) { |
| propertyRawValue = builder.toString(); |
| } |
| } |
| |
| ArrayList<String> propertyValueList = new ArrayList<String>(); |
| String value = VCardUtils.convertStringCharset( |
| maybeUnescapeText(propertyRawValue), sourceCharset, targetCharset); |
| propertyValueList.add(value); |
| property.setValues(propertyValueList); |
| for (VCardInterpreter interpreter : mInterpreterList) { |
| interpreter.onPropertyCreated(property); |
| } |
| } |
| } |
| |
| private void handleAdrOrgN(VCardProperty property, String propertyRawValue, |
| String sourceCharset, String targetCharset) throws VCardException, IOException { |
| List<String> encodedValueList = new ArrayList<String>(); |
| |
| // vCard 2.1 does not allow QUOTED-PRINTABLE here, but some softwares/devices emit |
| // such data. |
| if (mCurrentEncoding.equals(VCardConstants.PARAM_ENCODING_QP)) { |
| // First we retrieve Quoted-Printable String from vCard entry, which may include |
| // multiple lines. |
| final String quotedPrintablePart = getQuotedPrintablePart(propertyRawValue); |
| |
| // "Raw value" from the view of users should contain all part of QP string. |
| // TODO: add test for this handling |
| property.setRawValue(quotedPrintablePart); |
| |
| // We split Quoted-Printable String using semi-colon before decoding it, as |
| // the Quoted-Printable may have semi-colon, which confuses splitter. |
| final List<String> quotedPrintableValueList = |
| VCardUtils.constructListFromValue(quotedPrintablePart, getVersion()); |
| for (String quotedPrintableValue : quotedPrintableValueList) { |
| String encoded = VCardUtils.parseQuotedPrintable(quotedPrintableValue, |
| false, sourceCharset, targetCharset); |
| encodedValueList.add(encoded); |
| } |
| } else { |
| final String propertyValue = getPotentialMultiline(propertyRawValue); |
| final List<String> rawValueList = |
| VCardUtils.constructListFromValue(propertyValue, getVersion()); |
| for (String rawValue : rawValueList) { |
| encodedValueList.add(VCardUtils.convertStringCharset( |
| rawValue, sourceCharset, targetCharset)); |
| } |
| } |
| |
| property.setValues(encodedValueList); |
| for (VCardInterpreter interpreter : mInterpreterList) { |
| interpreter.onPropertyCreated(property); |
| } |
| } |
| |
| /** |
| * <p> |
| * Parses and returns Quoted-Printable. |
| * </p> |
| * |
| * @param firstString The string following a parameter name and attributes. |
| * Example: "string" in |
| * "ADR:ENCODING=QUOTED-PRINTABLE:string\n\r". |
| * @return whole Quoted-Printable string, including a given argument and |
| * following lines. Excludes the last empty line following to Quoted |
| * Printable lines. |
| * @throws IOException |
| * @throws VCardException |
| */ |
| private String getQuotedPrintablePart(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 a 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; |
| } |
| } |
| |
| /** |
| * Given the first line of a property, checks consecutive lines after it and builds a new |
| * multi-line value if it exists. |
| * |
| * @param firstString The first line of the property. |
| * @return A new property, potentially built from multiple lines. |
| * @throws IOException |
| */ |
| private String getPotentialMultiline(String firstString) throws IOException { |
| final StringBuilder builder = new StringBuilder(); |
| builder.append(firstString); |
| |
| while (true) { |
| final String line = peekLine(); |
| if (line == null || line.length() == 0) { |
| break; |
| } |
| |
| final String propertyName = getPropertyNameUpperCase(line); |
| if (propertyName != null) { |
| break; |
| } |
| |
| // vCard 2.1 does not allow multi-line of adr but microsoft vcards may have it. |
| // We will consider the next line to be a part of a multi-line value if it does not |
| // contain a property name (i.e. a colon or semi-colon). |
| // Consume the line. |
| getLine(); |
| builder.append(" ").append(line); |
| } |
| |
| return builder.toString(); |
| } |
| |
| protected String getBase64(String firstString) throws IOException, VCardException { |
| final StringBuilder builder = new StringBuilder(); |
| builder.append(firstString); |
| |
| while (true) { |
| final String line = peekLine(); |
| if (line == null) { |
| throw new VCardException("File ended during parsing BASE64 binary"); |
| } |
| |
| // vCard 2.1 requires two spaces at the end of BASE64 strings, but some vCard doesn't |
| // have them. We try to detect those cases using colon and semi-colon, given BASE64 |
| // does not contain it. |
| // E.g. |
| // TEL;TYPE=WORK:+5555555 |
| // or |
| // END:VCARD |
| String propertyName = getPropertyNameUpperCase(line); |
| if (getKnownPropertyNameSet().contains(propertyName) || |
| VCardConstants.PROPERTY_X_ANDROID_CUSTOM.equals(propertyName)) { |
| Log.w(LOG_TAG, "Found a next property during parsing a BASE64 string, " + |
| "which must not contain semi-colon or colon. Treat the line as next " |
| + "property."); |
| Log.w(LOG_TAG, "Problematic line: " + line.trim()); |
| break; |
| } |
| |
| // Consume the line. |
| getLine(); |
| |
| if (line.length() == 0) { |
| break; |
| } |
| // Trim off any extraneous whitespace to handle 2.1 implementations |
| // that use 3.0 style line continuations. This is safe because space |
| // isn't a Base64 encoding value. |
| builder.append(line.trim()); |
| } |
| |
| return builder.toString(); |
| } |
| |
| /** |
| * Extracts the property name portion of a given vCard line. |
| * <p> |
| * Properties must contain a colon. |
| * <p> |
| * E.g. |
| * TEL;TYPE=WORK:+5555555 // returns "TEL" |
| * END:VCARD // returns "END" |
| * TEL; // returns null |
| * |
| * @param line The vCard line. |
| * @return The property name portion. {@literal null} if no property name found. |
| */ |
| private String getPropertyNameUpperCase(String line) { |
| final int colonIndex = line.indexOf(":"); |
| if (colonIndex > -1) { |
| final int semiColonIndex = line.indexOf(";"); |
| |
| // Find the minimum index that is greater than -1. |
| final int minIndex; |
| if (colonIndex == -1) { |
| minIndex = semiColonIndex; |
| } else if (semiColonIndex == -1) { |
| minIndex = colonIndex; |
| } else { |
| minIndex = Math.min(colonIndex, semiColonIndex); |
| } |
| return line.substring(0, minIndex).toUpperCase(); |
| } |
| return null; |
| } |
| |
| /* |
| * vCard 2.1 specifies AGENT allows one vcard entry. Currently we emit an |
| * error toward the AGENT property. |
| * // TODO: Support AGENT property. |
| * item = |
| * ... / [groups "."] "AGENT" [params] ":" vcard CRLF vcard = "BEGIN" [ws] |
| * ":" [ws] "VCARD" [ws] 1*CRLF items *CRLF "END" [ws] ":" [ws] "VCARD" |
| */ |
| protected void handleAgent(final VCardProperty property) throws VCardException { |
| if (!property.getRawValue().toUpperCase().contains("BEGIN:VCARD")) { |
| // Apparently invalid line seen in Windows Mobile 6.5. Ignore them. |
| for (VCardInterpreter interpreter : mInterpreterList) { |
| interpreter.onPropertyCreated(property); |
| } |
| return; |
| } else { |
| throw new VCardAgentNotSupportedException("AGENT Property is not supported now."); |
| } |
| } |
| |
| /** |
| * For vCard 3.0. |
| */ |
| protected String maybeUnescapeText(final 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(final char ch) { |
| return unescapeCharacter(ch); |
| } |
| |
| /* package */ static String unescapeCharacter(final 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; |
| } |
| } |
| |
| /** |
| * @return {@link VCardConfig#VERSION_21} |
| */ |
| protected int getVersion() { |
| return VCardConfig.VERSION_21; |
| } |
| |
| /** |
| * @return {@link VCardConfig#VERSION_30} |
| */ |
| protected String getVersionString() { |
| return VCardConstants.VERSION_V21; |
| } |
| |
| protected Set<String> getKnownPropertyNameSet() { |
| return VCardParser_V21.sKnownPropertyNameSet; |
| } |
| |
| protected Set<String> getKnownTypeSet() { |
| return VCardParser_V21.sKnownTypeSet; |
| } |
| |
| protected Set<String> getKnownValueSet() { |
| return VCardParser_V21.sKnownValueSet; |
| } |
| |
| protected Set<String> getAvailableEncodingSet() { |
| return VCardParser_V21.sAvailableEncoding; |
| } |
| |
| protected String getDefaultEncoding() { |
| return DEFAULT_ENCODING; |
| } |
| |
| protected String getDefaultCharset() { |
| return DEFAULT_CHARSET; |
| } |
| |
| protected String getCurrentCharset() { |
| return mCurrentCharset; |
| } |
| |
| public void addInterpreter(VCardInterpreter interpreter) { |
| mInterpreterList.add(interpreter); |
| } |
| |
| public void parse(InputStream is) throws IOException, VCardException { |
| if (is == null) { |
| throw new NullPointerException("InputStream must not be null."); |
| } |
| |
| final InputStreamReader tmpReader = new InputStreamReader(is, mIntermediateCharset); |
| mReader = new CustomBufferedReader(tmpReader); |
| |
| final long start = System.currentTimeMillis(); |
| for (VCardInterpreter interpreter : mInterpreterList) { |
| interpreter.onVCardStarted(); |
| } |
| |
| // vcard_file = [wsls] vcard [wsls] |
| while (true) { |
| synchronized (this) { |
| if (mCanceled) { |
| Log.i(LOG_TAG, "Cancel request has come. exitting parse operation."); |
| break; |
| } |
| } |
| if (!parseOneVCard()) { |
| break; |
| } |
| } |
| |
| for (VCardInterpreter interpreter : mInterpreterList) { |
| interpreter.onVCardEnded(); |
| } |
| } |
| |
| public void parseOne(InputStream is) throws IOException, VCardException { |
| if (is == null) { |
| throw new NullPointerException("InputStream must not be null."); |
| } |
| |
| final InputStreamReader tmpReader = new InputStreamReader(is, mIntermediateCharset); |
| mReader = new CustomBufferedReader(tmpReader); |
| |
| final long start = System.currentTimeMillis(); |
| for (VCardInterpreter interpreter : mInterpreterList) { |
| interpreter.onVCardStarted(); |
| } |
| parseOneVCard(); |
| for (VCardInterpreter interpreter : mInterpreterList) { |
| interpreter.onVCardEnded(); |
| } |
| } |
| |
| public final synchronized void cancel() { |
| Log.i(LOG_TAG, "ParserImpl received cancel operation."); |
| mCanceled = true; |
| } |
| } |