Implement efficient Address pack/unpack and unit-test it.
Also unit-test legacy pack/unpack.
This commit is contained in:
parent
432d1ec3ed
commit
e8d58c01ec
@ -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();
|
||||
}
|
||||
}
|
||||
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user