Make vCard 4.0 parser support SORT-AS parameter.
Add unit test for it.
Fix bugs in foundation classes..
Change-Id: I8b5ca1fd49ef3e729ec85429fb8110efde5091f1
diff --git a/java/com/android/vcard/VCardConfig.java b/java/com/android/vcard/VCardConfig.java
index 7ebb365..e833b67 100644
--- a/java/com/android/vcard/VCardConfig.java
+++ b/java/com/android/vcard/VCardConfig.java
@@ -304,6 +304,7 @@
/**
* General vCard format with the version 4.0.
+ * @hide vCard 4.0 is not published yet.
*/
public static final int VCARD_TYPE_V40_GENERIC =
(VERSION_40 | NAME_ORDER_DEFAULT | FLAG_USE_DEFACT_PROPERTY | FLAG_USE_ANDROID_PROPERTY);
diff --git a/java/com/android/vcard/VCardConstants.java b/java/com/android/vcard/VCardConstants.java
index 6a7b947..20cc093 100644
--- a/java/com/android/vcard/VCardConstants.java
+++ b/java/com/android/vcard/VCardConstants.java
@@ -141,6 +141,11 @@
public static final String PARAM_ADR_TYPE_DOM = "DOM";
public static final String PARAM_ADR_TYPE_INTL = "INTL";
+ public static final String PARAM_LANGUAGE = "LANGUAGE";
+
+ // SORT-AS parameter introduced in vCard 4.0 (as of rev.13)
+ public static final String PARAM_SORT_AS = "SORT-AS";
+
// TYPE parameters not officially valid but used in some vCard exporter.
// Do not use in composer side.
public static final String PARAM_EXTRA_TYPE_COMPANY = "COMPANY";
diff --git a/java/com/android/vcard/VCardEntry.java b/java/com/android/vcard/VCardEntry.java
index a9ecd70..b7bd1ef 100644
--- a/java/com/android/vcard/VCardEntry.java
+++ b/java/com/android/vcard/VCardEntry.java
@@ -249,17 +249,20 @@
public String companyName;
public String departmentName;
public String titleName;
+ public final String phoneticName; // We won't have this in "TITLE" property.
public boolean isPrimary;
public OrganizationData(int type,
- String companyName,
- String departmentName,
- String titleName,
- boolean isPrimary) {
+ final String companyName,
+ final String departmentName,
+ final String titleName,
+ final String phoneticName,
+ final boolean isPrimary) {
this.type = type;
this.companyName = companyName;
this.departmentName = departmentName;
this.titleName = titleName;
+ this.phoneticName = phoneticName;
this.isPrimary = isPrimary;
}
@@ -428,6 +431,14 @@
}
}
+ // TODO(dmiyakawa): vCard 4.0 logically has multiple formatted names and we need to
+ // select the most preferable one using PREF parameter.
+ //
+ // e.g. (based on rev.13)
+ // FN;PREF=1:John M. Doe
+ // FN;PREF=2:John Doe
+ // FN;PREF=3;John
+
private String mFamilyName;
private String mGivenName;
private String mMiddleName;
@@ -523,22 +534,44 @@
}
/**
- * Should be called via {@link #handleOrgValue(int, List, boolean)} or
+ * Should be called via {@link #handleOrgValue(int, List, Map, boolean) or
* {@link #handleTitleValue(String)}.
*/
private void addNewOrganization(int type, final String companyName,
final String departmentName,
- final String titleName, boolean isPrimary) {
+ final String titleName,
+ final String phoneticName,
+ final boolean isPrimary) {
if (mOrganizationList == null) {
mOrganizationList = new ArrayList<OrganizationData>();
}
mOrganizationList.add(new OrganizationData(type, companyName,
- departmentName, titleName, isPrimary));
+ departmentName, titleName, phoneticName, isPrimary));
}
private static final List<String> sEmptyList =
Collections.unmodifiableList(new ArrayList<String>(0));
+ private String buildSinglePhoneticNameFromSortAsParam(Map<String, Collection<String>> paramMap) {
+ final Collection<String> sortAsCollection = paramMap.get(VCardConstants.PARAM_SORT_AS);
+ if (sortAsCollection != null && sortAsCollection.size() != 0) {
+ if (sortAsCollection.size() > 1) {
+ Log.w(LOG_TAG, "Incorrect multiple SORT_AS parameters detected: " +
+ Arrays.toString(sortAsCollection.toArray()));
+ }
+ final List<String> sortNames =
+ VCardUtils.constructListFromValue(sortAsCollection.iterator().next(),
+ mVCardType);
+ final StringBuilder builder = new StringBuilder();
+ for (final String elem : sortNames) {
+ builder.append(elem);
+ }
+ return builder.toString();
+ } else {
+ return null;
+ }
+ }
+
/**
* Set "ORG" related values to the appropriate data. If there's more than one
* {@link OrganizationData} objects, this input data are attached to the last one which
@@ -546,7 +579,9 @@
* {@link OrganizationData} object, a new {@link OrganizationData} is created,
* whose title is set to null.
*/
- private void handleOrgValue(final int type, List<String> orgList, boolean isPrimary) {
+ private void handleOrgValue(final int type, List<String> orgList,
+ Map<String, Collection<String>> paramMap, boolean isPrimary) {
+ final String phoneticName = buildSinglePhoneticNameFromSortAsParam(paramMap);
if (orgList == null) {
orgList = sEmptyList;
}
@@ -581,7 +616,7 @@
if (mOrganizationList == null) {
// Create new first organization entry, with "null" title which may be
// added via handleTitleValue().
- addNewOrganization(type, companyName, departmentName, null, isPrimary);
+ addNewOrganization(type, companyName, departmentName, null, phoneticName, isPrimary);
return;
}
for (OrganizationData organizationData : mOrganizationList) {
@@ -599,7 +634,7 @@
}
// No OrganizatioData is available. Create another one, with "null" title, which may be
// added via handleTitleValue().
- addNewOrganization(type, companyName, departmentName, null, isPrimary);
+ addNewOrganization(type, companyName, departmentName, null, phoneticName, isPrimary);
}
/**
@@ -613,7 +648,7 @@
if (mOrganizationList == null) {
// Create new first organization entry, with "null" other info, which may be
// added via handleOrgValue().
- addNewOrganization(DEFAULT_ORGANIZATION_TYPE, null, null, title, false);
+ addNewOrganization(DEFAULT_ORGANIZATION_TYPE, null, null, title, null, false);
return;
}
for (OrganizationData organizationData : mOrganizationList) {
@@ -624,7 +659,7 @@
}
// No Organization is available. Create another one, with "null" other info, which may be
// added via handleOrgValue().
- addNewOrganization(DEFAULT_ORGANIZATION_TYPE, null, null, title, false);
+ addNewOrganization(DEFAULT_ORGANIZATION_TYPE, null, null, title, null, false);
}
private void addIm(int protocol, String customProtocol, int type,
@@ -650,11 +685,54 @@
mPhotoList.add(photoData);
}
+ /**
+ * Tries to extract paramMap, constructs SORT-AS parameter values, and store them in
+ * appropriate phonetic name variables.
+ *
+ * This method does not care the vCard version. Even when we have SORT-AS parameters in
+ * invalid versions (i.e. 2.1 and 3.0), we scilently accept them so that we won't drop
+ * meaningful information. If we had this parameter in the N field of vCard 3.0, and
+ * the contact data also have SORT-STRING, we will prefer SORT-STRING, since it is
+ * regitimate property to be understood.
+ */
+ private void tryHandleSortAsName(final Map<String, Collection<String>> paramMap) {
+ if (VCardConfig.isVersion30(mVCardType) &&
+ !(TextUtils.isEmpty(mPhoneticFamilyName) &&
+ TextUtils.isEmpty(mPhoneticMiddleName) &&
+ TextUtils.isEmpty(mPhoneticGivenName))) {
+ return;
+ }
+
+ final Collection<String> sortAsCollection = paramMap.get(VCardConstants.PARAM_SORT_AS);
+ if (sortAsCollection != null && sortAsCollection.size() != 0) {
+ if (sortAsCollection.size() > 1) {
+ Log.w(LOG_TAG, "Incorrect multiple SORT_AS parameters detected: " +
+ Arrays.toString(sortAsCollection.toArray()));
+ }
+ final List<String> sortNames =
+ VCardUtils.constructListFromValue(sortAsCollection.iterator().next(),
+ mVCardType);
+ int size = sortNames.size();
+ if (size > 3) {
+ size = 3;
+ }
+ switch (size) {
+ case 3: mPhoneticMiddleName = sortNames.get(2); //$FALL-THROUGH$
+ case 2: mPhoneticGivenName = sortNames.get(1); //$FALL-THROUGH$
+ default: mPhoneticFamilyName = sortNames.get(0); break;
+ }
+ }
+ }
+
@SuppressWarnings("fallthrough")
- private void handleNProperty(List<String> elems) {
+ private void handleNProperty(final List<String> paramValues,
+ Map<String, Collection<String>> paramMap) {
+ // in vCard 4.0, SORT-AS parameter is available.
+ tryHandleSortAsName(paramMap);
+
// Family, Given, Middle, Prefix, Suffix. (1 - 5)
int size;
- if (elems == null || (size = elems.size()) < 1) {
+ if (paramValues == null || (size = paramValues.size()) < 1) {
return;
}
if (size > 5) {
@@ -662,12 +740,12 @@
}
switch (size) {
- // fallthrough
- case 5: mSuffix = elems.get(4);
- case 4: mPrefix = elems.get(3);
- case 3: mMiddleName = elems.get(2);
- case 2: mGivenName = elems.get(1);
- default: mFamilyName = elems.get(0);
+ // Fall-through.
+ case 5: mSuffix = paramValues.get(4);
+ case 4: mPrefix = paramValues.get(3);
+ case 3: mMiddleName = paramValues.get(2);
+ case 2: mGivenName = paramValues.get(1);
+ default: mFamilyName = paramValues.get(0);
}
}
@@ -754,7 +832,7 @@
// actually exist in the real vCard data, does not exist.
mFormattedName = propValue;
} else if (propName.equals(VCardConstants.PROPERTY_N)) {
- handleNProperty(propValueList);
+ handleNProperty(propValueList, paramMap);
} else if (propName.equals(VCardConstants.PROPERTY_SORT_STRING)) {
mPhoneticFullName = propValue;
} else if (propName.equals(VCardConstants.PROPERTY_NICKNAME) ||
@@ -769,8 +847,7 @@
// which is correct behavior from the view of vCard 2.1.
// But we want it to be separated, so do the separation here.
final List<String> phoneticNameList =
- VCardUtils.constructListFromValue(propValue,
- VCardConfig.isVersion30(mVCardType));
+ VCardUtils.constructListFromValue(propValue, mVCardType);
handlePhoneticNameFromSound(phoneticNameList);
} else {
// Ignore this field since Android cannot understand what it is.
@@ -871,7 +948,7 @@
}
}
}
- handleOrgValue(type, propValueList, isPrimary);
+ handleOrgValue(type, propValueList, paramMap, isPrimary);
} else if (propName.equals(VCardConstants.PROPERTY_TITLE)) {
handleTitleValue(propValue);
} else if (propName.equals(VCardConstants.PROPERTY_ROLE)) {
@@ -970,8 +1047,7 @@
mPhoneticFamilyName = propValue;
} else if (propName.equals(VCardConstants.PROPERTY_X_ANDROID_CUSTOM)) {
final List<String> customPropertyList =
- VCardUtils.constructListFromValue(propValue,
- VCardConfig.isVersion30(mVCardType));
+ VCardUtils.constructListFromValue(propValue, mVCardType);
handleAndroidCustomProperty(customPropertyList);
} else {
}
@@ -1110,6 +1186,9 @@
if (organizationData.titleName != null) {
builder.withValue(Organization.TITLE, organizationData.titleName);
}
+ if (organizationData.phoneticName != null) {
+ builder.withValue(Organization.PHONETIC_NAME, organizationData.phoneticName);
+ }
if (organizationData.isPrimary) {
builder.withValue(Organization.IS_PRIMARY, 1);
}
diff --git a/java/com/android/vcard/VCardParserImpl_V21.java b/java/com/android/vcard/VCardParserImpl_V21.java
index ff96061..f030c6e 100644
--- a/java/com/android/vcard/VCardParserImpl_V21.java
+++ b/java/com/android/vcard/VCardParserImpl_V21.java
@@ -32,6 +32,7 @@
import java.io.Reader;
import java.util.ArrayList;
import java.util.HashSet;
+import java.util.List;
import java.util.Set;
/**
@@ -43,6 +44,42 @@
/* package */ class VCardParserImpl_V21 {
private static final String LOG_TAG = "VCardParserImpl_V21";
+ private static final class EmptyInterpreter implements VCardInterpreter {
+ @Override
+ public void end() {
+ }
+ @Override
+ public void endEntry() {
+ }
+ @Override
+ public void endProperty() {
+ }
+ @Override
+ public void propertyGroup(String group) {
+ }
+ @Override
+ public void propertyName(String name) {
+ }
+ @Override
+ public void propertyParamType(String type) {
+ }
+ @Override
+ public void propertyParamValue(String value) {
+ }
+ @Override
+ public void propertyValues(List<String> values) {
+ }
+ @Override
+ public void start() {
+ }
+ @Override
+ public void startEntry() {
+ }
+ @Override
+ public void startProperty() {
+ }
+ }
+
protected static final class CustomBufferedReader extends BufferedReader {
private long mTime;
@@ -267,21 +304,19 @@
if (!readBeginVCard(allowGarbage)) {
return false;
}
- long start;
- if (mInterpreter != null) {
- start = System.currentTimeMillis();
- mInterpreter.startEntry();
- mTimeReadStartRecord += System.currentTimeMillis() - start;
- }
- start = System.currentTimeMillis();
+ final long beforeStartEntry = System.currentTimeMillis();
+ mInterpreter.startEntry();
+ mTimeReadStartRecord += System.currentTimeMillis() - beforeStartEntry;
+
+ final long beforeParseItems = System.currentTimeMillis();
parseItems();
- mTimeParseItems += System.currentTimeMillis() - start;
+ mTimeParseItems += System.currentTimeMillis() - beforeParseItems;
+
readEndVCard(true, false);
- if (mInterpreter != null) {
- start = System.currentTimeMillis();
- mInterpreter.endEntry();
- mTimeReadEndRecord += System.currentTimeMillis() - start;
- }
+
+ final long beforeEndEntry = System.currentTimeMillis();
+ mInterpreter.endEntry();
+ mTimeReadEndRecord += System.currentTimeMillis() - beforeEndEntry;
return true;
}
@@ -375,34 +410,31 @@
protected void parseItems() throws IOException, VCardException {
boolean ended = false;
- if (mInterpreter != null) {
- long start = System.currentTimeMillis();
- mInterpreter.startProperty();
- mTimeStartProperty += System.currentTimeMillis() - start;
- }
+ final long beforeBeginProperty = System.currentTimeMillis();
+ mInterpreter.startProperty();
+ mTimeStartProperty += System.currentTimeMillis() - beforeBeginProperty;
ended = parseItem();
- if (mInterpreter != null && !ended) {
- long start = System.currentTimeMillis();
+ if (!ended) {
+ final long beforeEndProperty = System.currentTimeMillis();
mInterpreter.endProperty();
- mTimeEndProperty += System.currentTimeMillis() - start;
+ mTimeEndProperty += System.currentTimeMillis() - beforeEndProperty;
}
while (!ended) {
- if (mInterpreter != null) {
- long start = System.currentTimeMillis();
- mInterpreter.startProperty();
- mTimeStartProperty += System.currentTimeMillis() - start;
- }
+ final long beforeStartProperty = System.currentTimeMillis();
+ mInterpreter.startProperty();
+ mTimeStartProperty += System.currentTimeMillis() - beforeStartProperty;
try {
ended = parseItem();
} catch (VCardInvalidCommentLineException e) {
Log.e(LOG_TAG, "Invalid line which looks like some comment was found. Ignored.");
ended = false;
}
- if (mInterpreter != null && !ended) {
- long start = System.currentTimeMillis();
+
+ if (!ended) {
+ final long beforeEndProperty = System.currentTimeMillis();
mInterpreter.endProperty();
- mTimeEndProperty += System.currentTimeMillis() - start;
+ mTimeEndProperty += System.currentTimeMillis() - beforeEndProperty;
}
}
}
@@ -487,9 +519,7 @@
mPreviousLine = line;
return null;
}
- if (mInterpreter != null) {
- mInterpreter.propertyName(propertyName);
- }
+ mInterpreter.propertyName(propertyName);
propertyNameAndValue[0] = propertyName;
if (i < length - 1) {
propertyNameAndValue[1] = line.substring(i + 1);
@@ -501,7 +531,7 @@
final String groupName = line.substring(nameIndex, i);
if (groupName.length() == 0) {
Log.w(LOG_TAG, "Empty group found. Ignoring.");
- } else if (mInterpreter != null) {
+ } else {
mInterpreter.propertyGroup(groupName);
}
nameIndex = i + 1; // Next should be another group or a property name.
@@ -511,9 +541,7 @@
mPreviousLine = line;
return null;
}
- if (mInterpreter != null) {
- mInterpreter.propertyName(propertyName);
- }
+ mInterpreter.propertyName(propertyName);
propertyNameAndValue[0] = propertyName;
nameIndex = i + 1;
state = STATE_PARAMS; // Start parameter parsing.
@@ -608,10 +636,8 @@
mUnknownTypeSet.add(ptypeval);
Log.w(LOG_TAG, String.format("TYPE unsupported by %s: ", getVersion(), ptypeval));
}
- if (mInterpreter != null) {
- mInterpreter.propertyParamType("TYPE");
- mInterpreter.propertyParamValue(ptypeval);
- }
+ mInterpreter.propertyParamType("TYPE");
+ mInterpreter.propertyParamValue(ptypeval);
}
/*
@@ -625,10 +651,8 @@
Log.w(LOG_TAG, String.format(
"The value unsupported by TYPE of %s: ", getVersion(), pvalueval));
}
- if (mInterpreter != null) {
- mInterpreter.propertyParamType("VALUE");
- mInterpreter.propertyParamValue(pvalueval);
- }
+ mInterpreter.propertyParamType("VALUE");
+ mInterpreter.propertyParamValue(pvalueval);
}
/*
@@ -637,10 +661,8 @@
protected void handleEncoding(String pencodingval) throws VCardException {
if (getAvailableEncodingSet().contains(pencodingval) ||
pencodingval.startsWith("X-")) {
- if (mInterpreter != null) {
- mInterpreter.propertyParamType("ENCODING");
- mInterpreter.propertyParamValue(pencodingval);
- }
+ mInterpreter.propertyParamType("ENCODING");
+ mInterpreter.propertyParamValue(pencodingval);
mCurrentEncoding = pencodingval;
} else {
throw new VCardException("Unknown encoding \"" + pencodingval + "\"");
@@ -655,10 +677,8 @@
* </p>
*/
protected void handleCharset(String charsetval) {
- if (mInterpreter != null) {
- mInterpreter.propertyParamType("CHARSET");
- mInterpreter.propertyParamValue(charsetval);
- }
+ mInterpreter.propertyParamType("CHARSET");
+ mInterpreter.propertyParamValue(charsetval);
}
/**
@@ -683,10 +703,8 @@
throw new VCardException("Invalid Language: \"" + langval + "\"");
}
}
- if (mInterpreter != null) {
- mInterpreter.propertyParamType("LANGUAGE");
- mInterpreter.propertyParamValue(langval);
- }
+ mInterpreter.propertyParamType(VCardConstants.PARAM_LANGUAGE);
+ mInterpreter.propertyParamValue(langval);
}
private boolean isAsciiLetter(char ch) {
@@ -700,10 +718,8 @@
* Mainly for "X-" type. This accepts any kind of type without check.
*/
protected void handleAnyParam(String paramName, String paramValue) {
- if (mInterpreter != null) {
- mInterpreter.propertyParamType(paramName);
- mInterpreter.propertyParamValue(paramValue);
- }
+ mInterpreter.propertyParamType(paramName);
+ mInterpreter.propertyParamValue(paramValue);
}
protected void handlePropertyValue(String propertyName, String propertyValue)
@@ -712,11 +728,9 @@
if (upperEncoding.equals(VCardConstants.PARAM_ENCODING_QP)) {
final long start = System.currentTimeMillis();
final String result = getQuotedPrintable(propertyValue);
- if (mInterpreter != null) {
- ArrayList<String> v = new ArrayList<String>();
- v.add(result);
- mInterpreter.propertyValues(v);
- }
+ final ArrayList<String> v = new ArrayList<String>();
+ v.add(result);
+ mInterpreter.propertyValues(v);
mTimeHandleQuotedPrintable += System.currentTimeMillis() - start;
} else if (upperEncoding.equals(VCardConstants.PARAM_ENCODING_BASE64)
|| upperEncoding.equals(VCardConstants.PARAM_ENCODING_B)) {
@@ -724,17 +738,12 @@
// 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 result = getBase64(propertyValue);
- if (mInterpreter != null) {
- ArrayList<String> arrayList = new ArrayList<String>();
- arrayList.add(result);
- mInterpreter.propertyValues(arrayList);
- }
+ final ArrayList<String> arrayList = new ArrayList<String>();
+ arrayList.add(getBase64(propertyValue));
+ mInterpreter.propertyValues(arrayList);
} catch (OutOfMemoryError error) {
Log.e(LOG_TAG, "OutOfMemoryError happened during parsing BASE64 data!");
- if (mInterpreter != null) {
- mInterpreter.propertyValues(null);
- }
+ mInterpreter.propertyValues(null);
}
mTimeHandleBase64 += System.currentTimeMillis() - start;
} else {
@@ -797,11 +806,9 @@
}
final long start = System.currentTimeMillis();
- if (mInterpreter != null) {
- ArrayList<String> v = new ArrayList<String>();
- v.add(maybeUnescapeText(propertyValue));
- mInterpreter.propertyValues(v);
- }
+ ArrayList<String> v = new ArrayList<String>();
+ v.add(maybeUnescapeText(propertyValue));
+ mInterpreter.propertyValues(v);
mTimeHandleMiscPropertyValue += System.currentTimeMillis() - start;
}
}
@@ -908,10 +915,8 @@
propertyValue = getQuotedPrintable(propertyValue);
}
- if (mInterpreter != null) {
- mInterpreter.propertyValues(VCardUtils.constructListFromValue(propertyValue,
- (getVersion() == VCardConfig.VERSION_30)));
- }
+ mInterpreter.propertyValues(VCardUtils.constructListFromValue(propertyValue,
+ getVersion()));
}
/*
@@ -943,7 +948,7 @@
* null otherwise. e.g. In vCard 2.1, "\;" should be unescaped into ";"
* while "\x" should not be.
*/
- protected String maybeEscapeCharacter(final char ch) {
+ protected String maybeUnescapeCharacter(final char ch) {
return unescapeCharacter(ch);
}
@@ -1019,7 +1024,7 @@
final InputStreamReader tmpReader = new InputStreamReader(is, mIntermediateCharset);
mReader = new CustomBufferedReader(tmpReader);
- mInterpreter = interpreter;
+ mInterpreter = (interpreter != null ? interpreter : new EmptyInterpreter());
final long start = System.currentTimeMillis();
if (mInterpreter != null) {
diff --git a/java/com/android/vcard/VCardParserImpl_V30.java b/java/com/android/vcard/VCardParserImpl_V30.java
index 9fefe89..648ce7e 100644
--- a/java/com/android/vcard/VCardParserImpl_V30.java
+++ b/java/com/android/vcard/VCardParserImpl_V30.java
@@ -179,12 +179,13 @@
@Override
protected void handleAnyParam(final String paramName, final String paramValue) {
- super.handleAnyParam(paramName, paramValue);
+ mInterpreter.propertyParamType(paramName);
+ splitAndPutParamValue(paramValue);
}
@Override
- protected void handleParamWithoutName(final String paramValue) throws VCardException {
- super.handleParamWithoutName(paramValue);
+ protected void handleParamWithoutName(final String paramValue) {
+ handleType(paramValue);
}
/*
@@ -200,73 +201,86 @@
* QSAFE-CHAR must not contain DQUOTE, including escaped one (\").
*/
@Override
- protected void handleType(final String paramvalues) {
- if (mInterpreter != null) {
- mInterpreter.propertyParamType("TYPE");
+ protected void handleType(final String paramValue) {
+ mInterpreter.propertyParamType("TYPE");
+ splitAndPutParamValue(paramValue);
+ }
- // "comma,separated:inside.dquote",pref
- // -->
- // - comma,separated:inside.dquote
- // - pref
- //
- // Note: Though there's a code, we don't need to take much care of
- // wrongly-added quotes like the example above, as they induce
- // parse errors at the top level (when splitting a line into parts).
- StringBuilder builder = null; // Delay initialization.
- boolean insideDquote = false;
- final int length = paramvalues.length();
- for (int i = 0; i < length; i = paramvalues.offsetByCodePoints(i, 1)) {
- final int codePoint = paramvalues.codePointAt(i);
- if (codePoint == '"') {
- if (insideDquote) {
- // End of Dquote.
- mInterpreter.propertyParamValue(builder.toString());
- builder = null;
- insideDquote = false;
- } else {
- if (builder != null) {
- if (builder.length() > 0) {
- // e.g.
- // pref"quoted"
- Log.w(LOG_TAG, "Unexpected Dquote inside property.");
- } else {
- // e.g.
- // pref,"quoted"
- // "quoted",pref
- mInterpreter.propertyParamValue(builder.toString());
- }
- }
- insideDquote = true;
- }
- } else if (codePoint == ',' && !insideDquote) {
- if (builder == null) {
- Log.w(LOG_TAG, "Comma is used before actual string comes. (" +
- paramvalues + ")");
- } else {
- mInterpreter.propertyParamValue(builder.toString());
- builder = null;
- }
+ /**
+ * Splits parameter values into pieces in accordance with vCard 3.0 specification and
+ * puts pieces into mInterpreter.
+ */
+ /*
+ * param-value = ptext / quoted-string
+ * quoted-string = DQUOTE QSAFE-CHAR DQUOTE
+ * QSAFE-CHAR = WSP / %x21 / %x23-7E / NON-ASCII
+ * ; Any character except CTLs, DQUOTE
+ *
+ * QSAFE-CHAR must not contain DQUOTE, including escaped one (\")
+ */
+ private void splitAndPutParamValue(String paramValue) {
+ // "comma,separated:inside.dquote",pref
+ // -->
+ // - comma,separated:inside.dquote
+ // - pref
+ //
+ // Note: Though there's a code, we don't need to take much care of
+ // wrongly-added quotes like the example above, as they induce
+ // parse errors at the top level (when splitting a line into parts).
+ StringBuilder builder = null; // Delay initialization.
+ boolean insideDquote = false;
+ final int length = paramValue.length();
+ for (int i = 0; i < length; i++) {
+ final char ch = paramValue.charAt(i);
+ if (ch == '"') {
+ if (insideDquote) {
+ // End of Dquote.
+ mInterpreter.propertyParamValue(builder.toString());
+ builder = null;
+ insideDquote = false;
} else {
- // To stop creating empty StringBuffer at the end of parameter,
- // we delay creating this object until this point.
- if (builder == null) {
- builder = new StringBuilder();
+ if (builder != null) {
+ if (builder.length() > 0) {
+ // e.g.
+ // pref"quoted"
+ Log.w(LOG_TAG, "Unexpected Dquote inside property.");
+ } else {
+ // e.g.
+ // pref,"quoted"
+ // "quoted",pref
+ mInterpreter.propertyParamValue(builder.toString());
+ }
}
- builder.appendCodePoint(codePoint);
+ insideDquote = true;
}
- }
- if (insideDquote) {
- // e.g.
- // "non-quote-at-end
- Log.d(LOG_TAG, "Dangling Dquote.");
- }
- if (builder != null) {
- if (builder.length() == 0) {
- Log.w(LOG_TAG, "Unintended behavior. We must not see empty StringBuilder " +
- "at the end of parameter value parsing.");
+ } else if (ch == ',' && !insideDquote) {
+ if (builder == null) {
+ Log.w(LOG_TAG, "Comma is used before actual string comes. (" +
+ paramValue + ")");
} else {
mInterpreter.propertyParamValue(builder.toString());
+ builder = null;
}
+ } else {
+ // To stop creating empty StringBuffer at the end of parameter,
+ // we delay creating this object until this point.
+ if (builder == null) {
+ builder = new StringBuilder();
+ }
+ builder.append(ch);
+ }
+ }
+ if (insideDquote) {
+ // e.g.
+ // "non-quote-at-end
+ Log.d(LOG_TAG, "Dangling Dquote.");
+ }
+ if (builder != null) {
+ if (builder.length() == 0) {
+ Log.w(LOG_TAG, "Unintended behavior. We must not see empty StringBuilder " +
+ "at the end of parameter value parsing.");
+ } else {
+ mInterpreter.propertyParamValue(builder.toString());
}
}
}
@@ -356,11 +370,11 @@
}
@Override
- protected String maybeEscapeCharacter(final char ch) {
- return escapeCharacter(ch);
+ protected String maybeUnescapeCharacter(final char ch) {
+ return unescapeCharacter(ch);
}
- public static String escapeCharacter(final char ch) {
+ public static String unescapeCharacter(final char ch) {
if (ch == 'n' || ch == 'N') {
return "\n";
} else {
diff --git a/java/com/android/vcard/VCardParserImpl_V40.java b/java/com/android/vcard/VCardParserImpl_V40.java
index 73bb9c8..3a49444 100644
--- a/java/com/android/vcard/VCardParserImpl_V40.java
+++ b/java/com/android/vcard/VCardParserImpl_V40.java
@@ -77,6 +77,14 @@
return builder.toString();
}
+ public static String unescapeCharacter(final char ch) {
+ if (ch == 'n' || ch == 'N') {
+ return "\n";
+ } else {
+ return String.valueOf(ch);
+ }
+ }
+
@Override
protected Set<String> getKnownPropertyNameSet() {
return VCardParser_V40.sKnownPropertyNameSet;
diff --git a/java/com/android/vcard/VCardParser_V40.java b/java/com/android/vcard/VCardParser_V40.java
index 6431d91..700cdfd 100644
--- a/java/com/android/vcard/VCardParser_V40.java
+++ b/java/com/android/vcard/VCardParser_V40.java
@@ -41,7 +41,7 @@
"LABEL", "TEL", "EMAIL", "IMPP", "LANG", "TZ",
"GEO", "TITLE", "ROLE", "LOGO", "ORG", "MEMBER",
"RELATED", "CATEGORIES", "NOTE", "PRODID",
- "REV", "SORT-STRING", "SOUND", "UID", "CLIENTPIDMAP",
+ "REV", "SOUND", "UID", "CLIENTPIDMAP",
"URL", "VERSION", "CLASS", "KEY", "FBURL", "CALENDRURI",
"CALURI")));
diff --git a/java/com/android/vcard/VCardUtils.java b/java/com/android/vcard/VCardUtils.java
index b869d76..f3f5bdf 100644
--- a/java/com/android/vcard/VCardUtils.java
+++ b/java/com/android/vcard/VCardUtils.java
@@ -332,18 +332,33 @@
return builder.toString();
}
+ /**
+ * Splits the given value into pieces using the delimiter ';' inside it.
+ *
+ * Escaped characters in those values are automatically unescaped into original form.
+ */
public static List<String> constructListFromValue(final String value,
- final boolean isV30) {
+ final int vcardType) {
final List<String> list = new ArrayList<String>();
StringBuilder builder = new StringBuilder();
- int length = value.length();
+ final int length = value.length();
for (int i = 0; i < length; i++) {
char ch = value.charAt(i);
if (ch == '\\' && i < length - 1) {
char nextCh = value.charAt(i + 1);
- final String unescapedString =
- (isV30 ? VCardParserImpl_V30.escapeCharacter(nextCh) :
- VCardParserImpl_V21.unescapeCharacter(nextCh));
+ final String unescapedString;
+ if (VCardConfig.isVersion40(vcardType)) {
+ unescapedString = VCardParserImpl_V40.unescapeCharacter(nextCh);
+ } else if (VCardConfig.isVersion30(vcardType)) {
+ unescapedString = VCardParserImpl_V30.unescapeCharacter(nextCh);
+ } else {
+ if (!VCardConfig.isVersion21(vcardType)) {
+ // Unknown vCard type
+ Log.w(LOG_TAG, "Unknown vCard type");
+ }
+ unescapedString = VCardParserImpl_V21.unescapeCharacter(nextCh);
+ }
+
if (unescapedString != null) {
builder.append(unescapedString);
i++;
diff --git a/tests/res/raw/v40_sort_as.vcf b/tests/res/raw/v40_sort_as.vcf
new file mode 100644
index 0000000..6f6bc3b
--- /dev/null
+++ b/tests/res/raw/v40_sort_as.vcf
@@ -0,0 +1,6 @@
+BEGIN:VCARD
+VERSION:4.0
+FN:安藤 ロイド
+N;SORT-AS="あんどう;ろいど":安藤;ロイド;;;
+ORG;TYPE=WORK;SORT-AS="ぐーぐる;けんさくぶもん":グーグル;検索部門
+END:VCARD
diff --git a/tests/src/com/android/vcard/tests/VCardImporterTests.java b/tests/src/com/android/vcard/tests/VCardImporterTests.java
index fcff4fc..1148e57 100644
--- a/tests/src/com/android/vcard/tests/VCardImporterTests.java
+++ b/tests/src/com/android/vcard/tests/VCardImporterTests.java
@@ -1037,12 +1037,41 @@
.put(Phone.NUMBER, "1");
}
- public void testCommaSeparatedParamsV30_Parse() {
- mVerifier.initForImportTest(V30, R.raw.v30_comma_separated);
+ public void testSortAsV40_Parse() {
+ mVerifier.initForImportTest(V40, R.raw.v40_sort_as);
+
+ final ContentValues contentValuesForSortAsN = new ContentValues();
+ contentValuesForSortAsN.put("SORT-AS",
+ "\u3042\u3093\u3069\u3046;\u308D\u3044\u3069");
+ final ContentValues contentValuesForSortAsOrg = new ContentValues();
+ contentValuesForSortAsOrg.put("SORT-AS",
+ "\u3050\u30FC\u3050\u308B;\u3051\u3093\u3055\u304F\u3076\u3082\u3093");
+
mVerifier.addPropertyNodesVerifierElem()
- .addExpectedNodeWithOrder("N", Arrays.asList("F", "G", "M", "", ""),
- new TypeSet("PREF", "HOME"))
- .addExpectedNodeWithOrder("TEL", "1",
- new TypeSet("COMMA,SEPARATED:INSIDE.DQUOTE", "PREF"));
+ .addExpectedNodeWithOrder("FN", "\u5B89\u85E4\u0020\u30ED\u30A4\u30C9")
+ .addExpectedNodeWithOrder("N",
+ Arrays.asList("\u5B89\u85E4", "\u30ED\u30A4\u30C9", "", "", ""),
+ contentValuesForSortAsN)
+ .addExpectedNodeWithOrder("ORG",
+ Arrays.asList("\u30B0\u30FC\u30B0\u30EB", "\u691C\u7D22\u90E8\u9580"),
+ contentValuesForSortAsOrg, new TypeSet("WORK"));
+ }
+
+ public void testSortAsV40() {
+ mVerifier.initForImportTest(V40, R.raw.v40_sort_as);
+ final ContentValuesVerifierElem elem = mVerifier.addContentValuesVerifierElem();
+ elem.addExpected(StructuredName.CONTENT_ITEM_TYPE)
+ .put(StructuredName.FAMILY_NAME, "\u5B89\u85E4")
+ .put(StructuredName.GIVEN_NAME, "\u30ED\u30A4\u30C9")
+ .put(StructuredName.DISPLAY_NAME, "\u5B89\u85E4\u0020\u30ED\u30A4\u30C9")
+ .put(StructuredName.PHONETIC_FAMILY_NAME, "\u3042\u3093\u3069\u3046")
+ .put(StructuredName.PHONETIC_GIVEN_NAME,
+ "\u308D\u3044\u3069");
+ elem.addExpected(Organization.CONTENT_ITEM_TYPE)
+ .put(Organization.TYPE, Organization.TYPE_WORK)
+ .put(Organization.COMPANY, "\u30B0\u30FC\u30B0\u30EB")
+ .put(Organization.DEPARTMENT, "\u691C\u7D22\u90E8\u9580")
+ .put(Organization.PHONETIC_NAME,
+ "\u3050\u30FC\u3050\u308B\u3051\u3093\u3055\u304F\u3076\u3082\u3093");
}
}
diff --git a/tests/src/com/android/vcard/tests/VCardTestsBase.java b/tests/src/com/android/vcard/tests/VCardTestsBase.java
index 5e5ecec..dbb409f 100644
--- a/tests/src/com/android/vcard/tests/VCardTestsBase.java
+++ b/tests/src/com/android/vcard/tests/VCardTestsBase.java
@@ -28,6 +28,7 @@
public class VCardTestsBase extends AndroidTestCase {
public static final int V21 = VCardConfig.VCARD_TYPE_V21_GENERIC;
public static final int V30 = VCardConfig.VCARD_TYPE_V30_GENERIC;
+ public static final int V40 = VCardConfig.VCARD_TYPE_V40_GENERIC;
// Do not modify these during tests.
protected final ContentValues mContentValuesForQP;
diff --git a/tests/src/com/android/vcard/tests/test_utils/ContentValuesBuilder.java b/tests/src/com/android/vcard/tests/test_utils/ContentValuesBuilder.java
index fb53b8f..74cc3e5 100644
--- a/tests/src/com/android/vcard/tests/test_utils/ContentValuesBuilder.java
+++ b/tests/src/com/android/vcard/tests/test_utils/ContentValuesBuilder.java
@@ -33,6 +33,7 @@
return this;
}
+ /*
public ContentValuesBuilder put(String key, Byte value) {
mContentValues.put(key, value);
return this;
@@ -41,13 +42,14 @@
public ContentValuesBuilder put(String key, Short value) {
mContentValues.put(key, value);
return this;
- }
+ }*/
public ContentValuesBuilder put(String key, Integer value) {
mContentValues.put(key, value);
return this;
}
+ /*
public ContentValuesBuilder put(String key, Long value) {
mContentValues.put(key, value);
return this;
@@ -66,7 +68,7 @@
public ContentValuesBuilder put(String key, Boolean value) {
mContentValues.put(key, value);
return this;
- }
+ }*/
public ContentValuesBuilder put(String key, byte[] value) {
mContentValues.put(key, value);
diff --git a/tests/src/com/android/vcard/tests/test_utils/ContentValuesVerifier.java b/tests/src/com/android/vcard/tests/test_utils/ContentValuesVerifier.java
index 57dbf32..f69ea1b 100644
--- a/tests/src/com/android/vcard/tests/test_utils/ContentValuesVerifier.java
+++ b/tests/src/com/android/vcard/tests/test_utils/ContentValuesVerifier.java
@@ -29,9 +29,9 @@
private int mIndex;
public ContentValuesVerifierElem addElem(AndroidTestCase androidTestCase) {
- ContentValuesVerifierElem importVerifier = new ContentValuesVerifierElem(androidTestCase);
- mContentValuesVerifierElemList.add(importVerifier);
- return importVerifier;
+ ContentValuesVerifierElem elem = new ContentValuesVerifierElem(androidTestCase);
+ mContentValuesVerifierElemList.add(elem);
+ return elem;
}
public void onStart() {
diff --git a/tests/src/com/android/vcard/tests/test_utils/ImportTestProvider.java b/tests/src/com/android/vcard/tests/test_utils/ImportTestProvider.java
index cb3dfef..8d6fa12 100644
--- a/tests/src/com/android/vcard/tests/test_utils/ImportTestProvider.java
+++ b/tests/src/com/android/vcard/tests/test_utils/ImportTestProvider.java
@@ -37,7 +37,6 @@
import android.test.AndroidTestCase;
import android.test.mock.MockContentProvider;
import android.text.TextUtils;
-import android.util.Log;
import junit.framework.TestCase;
@@ -171,9 +170,11 @@
}
if (!checked) {
final StringBuilder builder = new StringBuilder();
+ builder.append("\n");
builder.append("Unexpected: ");
builder.append(convertToEasilyReadableString(actualContentValues));
- builder.append("\nExpected: ");
+ builder.append("\n");
+ builder.append("Expected : ");
for (ContentValues expectedContentValues : contentValuesCollection) {
builder.append(convertToEasilyReadableString(expectedContentValues));
}
@@ -240,7 +241,7 @@
}
private static boolean equalsForContentValues(
- ContentValues expected, ContentValues actual) {
+ final ContentValues expected, final ContentValues actual) {
if (expected == actual) {
return true;
} else if (expected == null || actual == null || expected.size() != actual.size()) {
@@ -253,17 +254,18 @@
if (!actual.containsKey(key)) {
return false;
}
+ // Type mismatch usuall happens as importer doesn't care the type of each value.
+ // For example, variable type might be Integer when importing the type of TEL,
+ // while variable type would be String when importing the type of RELATION.
+ final Object actualValue = actual.get(key);
if (value instanceof byte[]) {
- Object actualValue = actual.get(key);
if (!Arrays.equals((byte[])value, (byte[])actualValue)) {
byte[] e = (byte[])value;
byte[] a = (byte[])actualValue;
- Log.d("@@@", "expected (len: " + e.length + "): " + Arrays.toString(e));
- Log.d("@@@", "actual (len: " + a.length + "): " + Arrays.toString(a));
return false;
}
- } else if (!value.equals(actual.get(key))) {
- Log.d("@@@", "different.");
+ } else if (!value.equals(actualValue) &&
+ !value.toString().equals(actualValue.toString())) {
return false;
}
}
diff --git a/tests/src/com/android/vcard/tests/test_utils/PropertyNodesVerifierElem.java b/tests/src/com/android/vcard/tests/test_utils/PropertyNodesVerifierElem.java
index b6a4138..d0fb83c 100644
--- a/tests/src/com/android/vcard/tests/test_utils/PropertyNodesVerifierElem.java
+++ b/tests/src/com/android/vcard/tests/test_utils/PropertyNodesVerifierElem.java
@@ -100,6 +100,12 @@
paramMap_TYPE, null);
}
+ public PropertyNodesVerifierElem addExpectedNodeWithOrder(String propName,
+ List<String> propValueList, ContentValues paramMap, TypeSet paramMap_TYPE) {
+ return addExpectedNodeWithOrder(propName, null, propValueList, null, paramMap,
+ paramMap_TYPE, null);
+ }
+
public PropertyNodesVerifierElem addExpectedNodeWithOrder(String propName, String propValue,
ContentValues paramMap, TypeSet paramMap_TYPE) {
return addExpectedNodeWithOrder(propName, propValue, null, null,
diff --git a/tests/src/com/android/vcard/tests/test_utils/VCardVerifier.java b/tests/src/com/android/vcard/tests/test_utils/VCardVerifier.java
index e5de13f..1b5cb7e 100644
--- a/tests/src/com/android/vcard/tests/test_utils/VCardVerifier.java
+++ b/tests/src/com/android/vcard/tests/test_utils/VCardVerifier.java
@@ -235,15 +235,13 @@
private void verifyWithInputStream(InputStream is) throws IOException {
final VCardInterpreter interpreter;
if (mContentValuesVerifier != null) {
- final VNodeBuilder vnodeBuilder = mPropertyNodesVerifier;
- final VCardEntryConstructor vcardDataBuilder =
- new VCardEntryConstructor(mVCardType);
- vcardDataBuilder.addEntryHandler(mContentValuesVerifier);
+ final VCardEntryConstructor constructor = new VCardEntryConstructor(mVCardType);
+ constructor.addEntryHandler(mContentValuesVerifier);
if (mPropertyNodesVerifier != null) {
interpreter = new VCardInterpreterCollection(Arrays.asList(
- mPropertyNodesVerifier, vcardDataBuilder));
+ mPropertyNodesVerifier, constructor));
} else {
- interpreter = vnodeBuilder;
+ interpreter = constructor;
}
} else {
if (mPropertyNodesVerifier != null) {