Implement efficient Address pack/unpack and unit-test it.

Also unit-test legacy pack/unpack.
This commit is contained in:
Mihai Preda 2009-07-07 17:08:57 -07:00
parent 432d1ec3ed
commit e8d58c01ec
2 changed files with 183 additions and 47 deletions

View File

@ -25,8 +25,6 @@ import android.text.TextUtils;
import android.text.util.Rfc822Token;
import android.text.util.Rfc822Tokenizer;
import java.io.UnsupportedEncodingException;
import java.net.URLEncoder;
import java.util.ArrayList;
import java.util.regex.Pattern;
@ -62,6 +60,10 @@ public class Address {
private static final Address[] EMPTY_ADDRESS_ARRAY = new Address[0];
// delimiters are chars that do not appear in an email address, used by pack/unpack
private static final char LIST_DELIMITER_EMAIL = '\1';
private static final char LIST_DELIMITER_PERSONAL = '\2';
public Address(String address, String personal) {
setAddress(address);
setPersonal(personal);
@ -292,13 +294,98 @@ public class Address {
}
return sb.toString();
}
/**
* Unpacks an address list previously packed with packAddressList()
* @param list
* @return
* Unpacks an address list previously packed with pack()
* @param addressList String with packed addresses as returned by pack()
* @return array of addresses resulting from unpack
*/
public static Address[] unpack(String addressList) {
if (addressList == null || addressList.length() == 0) {
return EMPTY_ADDRESS_ARRAY;
}
ArrayList<Address> addresses = new ArrayList<Address>();
int length = addressList.length();
int pairStartIndex = 0;
int pairEndIndex = 0;
/* addressEndIndex is only re-scanned (indexOf()) when a LIST_DELIMITER_PERSONAL
is used, not for every email address; i.e. not for every iteration of the while().
This reduces the theoretical complexity from quadratic to linear,
and provides some speed-up in practice by removing redundant scans of the string.
*/
int addressEndIndex = addressList.indexOf(LIST_DELIMITER_PERSONAL);
while (pairStartIndex < length) {
pairEndIndex = addressList.indexOf(LIST_DELIMITER_EMAIL, pairStartIndex);
if (pairEndIndex == -1) {
pairEndIndex = length;
}
Address address;
if (addressEndIndex == -1 || pairEndIndex <= addressEndIndex) {
// in this case the DELIMITER_PERSONAL is in a future pair,
// so don't use personal, and don't update addressEndIndex
address = new Address(addressList.substring(pairStartIndex, pairEndIndex), null);
} else {
address = new Address(addressList.substring(pairStartIndex, addressEndIndex),
addressList.substring(addressEndIndex + 1, pairEndIndex));
// only update addressEndIndex when we use the LIST_DELIMITER_PERSONAL
addressEndIndex = addressList.indexOf(LIST_DELIMITER_PERSONAL, pairEndIndex + 1);
}
addresses.add(address);
pairStartIndex = pairEndIndex + 1;
}
return addresses.toArray(EMPTY_ADDRESS_ARRAY);
}
/**
* Packs an address list into a String that is very quick to read
* and parse. Packed lists can be unpacked with unpack().
* The format is a series of packed addresses separated by LIST_DELIMITER_EMAIL.
* Each address is packed as
* a pair of address and personal separated by LIST_DELIMITER_PERSONAL,
* where the personal and delimiter are optional.
* E.g. "foo@x.com\1joe@x.com\2Joe Doe"
* @param addresses Array of addresses
* @return a string containing the packed addresses.
*/
public static String pack(Address[] addresses) {
// TODO: return same value for both null & empty list
if (addresses == null) {
return null;
}
final int nAddr = addresses.length;
if (nAddr == 0) {
return "";
}
// shortcut: one email with no displayName
if (nAddr == 1 && addresses[0].getPersonal() == null) {
return addresses[0].getAddress();
}
StringBuffer sb = new StringBuffer();
for (int i = 0; i < nAddr; i++) {
final Address address = addresses[i];
sb.append(address.getAddress());
final String displayName = address.getPersonal();
if (displayName != null) {
sb.append(LIST_DELIMITER_PERSONAL);
sb.append(displayName);
}
if (i < nAddr - 1) {
sb.append(LIST_DELIMITER_EMAIL);
}
}
return sb.toString();
}
/**
* Legacy unpack() used for reading the old data (migration),
* as found in LocalStore (Donut; db version up to 24).
* @See unpack()
*/
/* package */ static Address[] legacyUnpack(String addressList) {
if (addressList == null || addressList.length() == 0) {
return new Address[] { };
}
@ -316,49 +403,18 @@ public class Address {
String address = null;
String personal = null;
if (addressEndIndex == -1 || addressEndIndex > pairEndIndex) {
address = Utility.fastUrlDecode(addressList.substring(pairStartIndex, pairEndIndex));
address =
Utility.fastUrlDecode(addressList.substring(pairStartIndex, pairEndIndex));
}
else {
address = Utility.fastUrlDecode(addressList.substring(pairStartIndex, addressEndIndex));
personal = Utility.fastUrlDecode(addressList.substring(addressEndIndex + 1, pairEndIndex));
address =
Utility.fastUrlDecode(addressList.substring(pairStartIndex, addressEndIndex));
personal =
Utility.fastUrlDecode(addressList.substring(addressEndIndex + 1, pairEndIndex));
}
addresses.add(new Address(address, personal));
pairStartIndex = pairEndIndex + 1;
}
return addresses.toArray(new Address[] { });
}
/**
* Packs an address list into a String that is very quick to read
* and parse. Packed lists can be unpacked with unpackAddressList()
* The packed list is a comma separated list of:
* URLENCODE(address)[;URLENCODE(personal)]
* @param list
* @return
*/
public static String pack(Address[] addresses) {
if (addresses == null) {
return null;
} else if (addresses.length == 0) {
return "";
}
StringBuffer sb = new StringBuffer();
for (int i = 0, count = addresses.length; i < count; i++) {
Address address = addresses[i];
try {
sb.append(URLEncoder.encode(address.getAddress(), "UTF-8"));
if (address.getPersonal() != null) {
sb.append(';');
sb.append(URLEncoder.encode(address.getPersonal(), "UTF-8"));
}
if (i < count - 1) {
sb.append(',');
}
}
catch (UnsupportedEncodingException uee) {
return null;
}
}
return sb.toString();
}
}

View File

@ -19,6 +19,9 @@ package com.android.email.mail;
import android.test.AndroidTestCase;
import android.test.suitebuilder.annotation.SmallTest;
import java.net.URLEncoder;
import java.io.UnsupportedEncodingException;
/**
* This is a series of unit tests for the Address class. These tests must be locally
* complete - no server(s) required.
@ -37,6 +40,16 @@ public class AddressUnitTests extends AndroidTestCase {
+ "\uD834\uDF01\uD834\uDF46 <address8@ne.jp>,"
+ "\"\uD834\uDF01\uD834\uDF46\" <address9@ne.jp>";
private static final int MULTI_ADDRESSES_COUNT = 9;
private static final Address PACK_ADDR_1 = new Address("john@gmail.com", "John Doe");
private static final Address PACK_ADDR_2 = new Address("foo@bar.com", null);
private static final Address PACK_ADDR_3 = new Address("mar.y+test@gmail.com", "Mar-y, B; B*arr");
private static final Address[][] PACK_CASES = {
{PACK_ADDR_2}, {PACK_ADDR_1},
{PACK_ADDR_1, PACK_ADDR_2}, {PACK_ADDR_2, PACK_ADDR_1},
{PACK_ADDR_1, PACK_ADDR_3}, {PACK_ADDR_2, PACK_ADDR_2},
{PACK_ADDR_1, PACK_ADDR_2, PACK_ADDR_3}, {PACK_ADDR_3, PACK_ADDR_1, PACK_ADDR_2}
};
Address mAddress1;
Address mAddress2;
@ -472,10 +485,6 @@ public class AddressUnitTests extends AndroidTestCase {
assertEquals("personal1,address2,address3", Address.toFriendly(list4));
}
/**
* TODO: more in-depth tests for pack() and unpack()
*/
/**
* Simple quick checks of empty-input edge conditions for pack()
*
@ -512,6 +521,45 @@ public class AddressUnitTests extends AndroidTestCase {
assertTrue("unpacking zero-length", result != null && result.length == 0);
}
private static boolean addressEquals(Address a1, Address a2) {
if (!a1.equals(a2)) {
return false;
}
final String displayName1 = a1.getPersonal();
final String displayName2 = a2.getPersonal();
if (displayName1 == null) {
return displayName2 == null;
} else {
return displayName1.equals(displayName2);
}
}
private static boolean addressArrayEquals(Address[] array1, Address[] array2) {
if (array1.length != array2.length) {
return false;
}
for (int i = array1.length - 1; i >= 0; --i) {
if (!addressEquals(array1[i], array2[i])) {
return false;
}
}
return true;
}
public void testPackUnpack() {
for (Address[] list : PACK_CASES) {
String packed = Address.pack(list);
assertTrue(packed, addressArrayEquals(list, Address.unpack(packed)));
}
}
public void testLegacyPackUnpack() {
for (Address[] list : PACK_CASES) {
String packed = legacyPack(list);
assertTrue(packed, addressArrayEquals(list, Address.legacyUnpack(packed)));
}
}
public void testIsValidAddress() {
String notValid[] = {"", "foo", "john@", "x@y", "x@y.", "foo.com"};
String valid[] = {"x@y.z", "john@gmail.com", "a@b.c.d"};
@ -525,4 +573,36 @@ public class AddressUnitTests extends AndroidTestCase {
// isAllValid() must accept empty address list as valid
assertTrue("Empty address list is valid", Address.isAllValid(""));
}
/**
* Legacy pack() used for testing legacyUnpack().
* The packed list is a comma separated list of:
* URLENCODE(address)[;URLENCODE(personal)]
* @See pack()
*/
private static String legacyPack(Address[] addresses) {
if (addresses == null) {
return null;
} else if (addresses.length == 0) {
return "";
}
StringBuffer sb = new StringBuffer();
for (int i = 0, count = addresses.length; i < count; i++) {
Address address = addresses[i];
try {
sb.append(URLEncoder.encode(address.getAddress(), "UTF-8"));
if (address.getPersonal() != null) {
sb.append(';');
sb.append(URLEncoder.encode(address.getPersonal(), "UTF-8"));
}
if (i < count - 1) {
sb.append(',');
}
}
catch (UnsupportedEncodingException uee) {
return null;
}
}
return sb.toString();
}
}