Improve handling phone context in RFC3966 format. (#3792)
* Improve handling phone context in RFC3966 format.
* Throw if the phone-context parameter appears before the tel: prefix.
* Update exception message to be more clear.
* Add test for phone context without semicolon.
diff --git a/java/libphonenumber/src/com/google/i18n/phonenumbers/PhoneNumberUtil.java b/java/libphonenumber/src/com/google/i18n/phonenumbers/PhoneNumberUtil.java
index ea289fa..4e3a6e7 100644
--- a/java/libphonenumber/src/com/google/i18n/phonenumbers/PhoneNumberUtil.java
+++ b/java/libphonenumber/src/com/google/i18n/phonenumbers/PhoneNumberUtil.java
@@ -3203,49 +3203,92 @@
}
/**
- * Converts numberToParse to a form that we can parse and write it to nationalNumber if it is
- * written in RFC3966; otherwise extract a possible number out of it and write to nationalNumber.
+ * Converts numberToParse to a form that we can parse and write it to outputNumber if it is
+ * written in RFC3966; otherwise extract a possible number out of it and write to outputNumber.
*/
- private void buildNationalNumberForParsing(String numberToParse, StringBuilder nationalNumber)
+ // @VisibleForTesting
+ void buildNationalNumberForParsing(String numberToParse, StringBuilder outputNumber)
throws NumberParseException {
- int indexOfPhoneContext = numberToParse.indexOf(Constants.RFC3966_PHONE_CONTEXT);
-
PhoneContext phoneContext = phoneContextParser.parse(numberToParse);
- if (phoneContext != null) {
- // If the phone context contains a phone number prefix, we need to capture it, whereas domains
- // will be ignored.
- if (phoneContext.getRawContext().charAt(0) == Constants.PLUS_SIGN) {
- // Additional parameters might follow the phone context. If so, we will remove them here
- // because the parameters after phone context are not important for parsing the phone
- // number.
- nationalNumber.append(phoneContext.getRawContext());
- }
-
- // Now append everything between the "tel:" prefix and the phone-context. This should include
- // the national number, an optional extension or isdn-subaddress component. Note we also
- // handle the case when "tel:" is missing, as we have seen in some of the phone number inputs.
- // In that case, we append everything from the beginning.
- int indexOfRfc3966Prefix = numberToParse.indexOf(RFC3966_PREFIX);
- int indexOfNationalNumber =
- (indexOfRfc3966Prefix >= 0) ? indexOfRfc3966Prefix + RFC3966_PREFIX.length() : 0;
- nationalNumber.append(numberToParse.substring(indexOfNationalNumber, indexOfPhoneContext));
- } else {
+ if (phoneContext == null) {
// Extract a possible number from the string passed in (this strips leading characters that
// could not be the start of a phone number.)
- nationalNumber.append(extractPossibleNumber(numberToParse));
+ outputNumber.append(extractPossibleNumber(numberToParse));
+
+ // Strip the isdn parameter if present
+ int idsnPrefixStart = outputNumber.indexOf(RFC3966_ISDN_SUBADDRESS);
+ if (idsnPrefixStart > 0) {
+ outputNumber.delete(idsnPrefixStart, outputNumber.length());
+ }
+ return;
}
- // Delete the isdn-subaddress and everything after it if it is present. Note extension won't
- // appear at the same time with isdn-subaddress according to paragraph 5.3 of the RFC3966 spec,
- int indexOfIsdn = nationalNumber.indexOf(RFC3966_ISDN_SUBADDRESS);
- if (indexOfIsdn > 0) {
- nationalNumber.delete(indexOfIsdn, nationalNumber.length());
+ // Note we also handle the case when "tel:" is missing. In that case, we consider the start of
+ // the string to be the start of the number.
+ int rfc3966PrefixStart = numberToParse.indexOf(RFC3966_PREFIX);
+ int numberStart =
+ (rfc3966PrefixStart >= 0) ? rfc3966PrefixStart + RFC3966_PREFIX.length() : 0;
+ int numberEnd = numberToParse.indexOf(";", numberStart);
+
+ if (numberEnd < 0) {
+ throw new NumberParseException(NumberParseException.ErrorType.NOT_A_NUMBER,
+ "The string supplied does not form a valid RFC3966 number.");
}
- // If both phone context and isdn-subaddress are absent but other parameters are present, the
- // parameters are left in nationalNumber. This is because we are concerned about deleting
- // content from a potential number string when there is no strong evidence that the number is
- // actually written in RFC3966.
+
+ String numberPart = numberToParse.substring(numberStart, numberEnd);
+
+ outputNumber.append(constructE164(phoneContext, numberPart));
+
+ // Append the extension if present.
+ int extnPrefixStart = numberToParse.indexOf(RFC3966_EXTN_PREFIX);
+ if (extnPrefixStart >= 0) {
+ int extnStart = extnPrefixStart + RFC3966_EXTN_PREFIX.length();
+ int extnEnd = numberToParse.indexOf(";", extnStart);
+ if (extnEnd < 0) {
+ extnEnd = numberToParse.length();
+ }
+ outputNumber.append(DEFAULT_EXTN_PREFIX).append(numberToParse, extnStart, extnEnd);
+ }
+ }
+
+ /**
+ * Attempts to construct an E164 number from the parsed phone context and numberPart and returns
+ * it.
+ *
+ * If the phone context is a country code, the national prefix is stripped from the numberPart and
+ * the E164 number is constructed from the country code and the stripped number.
+ *
+ * If the phone context is more than just a country code, we fall back to concatenating the whole
+ * context with the numberPart.
+ */
+ private String constructE164(PhoneContext phoneContext, String numberPart) {
+ if (phoneContext.getRawContext().charAt(0) != Constants.PLUS_SIGN) {
+ return numberPart;
+ }
+
+ if (phoneContext.getCountryCode() == null) {
+ // Fall back to prefixing the national number with the country calling code if the context
+ // is more than just a country calling code.
+ return phoneContext.getRawContext() + numberPart;
+ }
+
+ // Get the region code and metadata from the phone context country code.
+ String regionCode = getRegionCodeForCountryCode(phoneContext.getCountryCode());
+ PhoneMetadata regionMetadata = getMetadataForRegionOrCallingCode(phoneContext.getCountryCode(), regionCode);
+
+ if (regionMetadata == null) {
+ // Fall back to prefixing the national number with the country calling code if the country
+ // code is invalid.
+ return phoneContext.getRawContext() + numberPart;
+ }
+
+ StringBuilder numberWithoutNationalPrefix = new StringBuilder(numberPart);
+ maybeStripNationalPrefixAndCarrierCode(numberWithoutNationalPrefix, regionMetadata,
+ new StringBuilder());
+
+ return Constants.PLUS_SIGN + phoneContext.getCountryCode().toString()
+ + numberWithoutNationalPrefix;
}
/**
diff --git a/java/libphonenumber/test/com/google/i18n/phonenumbers/PhoneNumberUtilTest.java b/java/libphonenumber/test/com/google/i18n/phonenumbers/PhoneNumberUtilTest.java
index f3bdf44..3e49820 100644
--- a/java/libphonenumber/test/com/google/i18n/phonenumbers/PhoneNumberUtilTest.java
+++ b/java/libphonenumber/test/com/google/i18n/phonenumbers/PhoneNumberUtilTest.java
@@ -16,8 +16,13 @@
package com.google.i18n.phonenumbers;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertNull;
import static org.junit.Assert.assertThrows;
-
+import static org.junit.Assert.assertTrue;
+import static org.junit.Assert.fail;
import com.google.i18n.phonenumbers.PhoneNumberUtil.PhoneNumberFormat;
import com.google.i18n.phonenumbers.PhoneNumberUtil.PhoneNumberType;
import com.google.i18n.phonenumbers.PhoneNumberUtil.ValidationResult;
@@ -2949,6 +2954,10 @@
assertThrowsForInvalidPhoneContext("tel:033316005;phone-context=3phone");
assertThrowsForInvalidPhoneContext("tel:033316005;phone-context=a-.nz");
assertThrowsForInvalidPhoneContext("tel:033316005;phone-context=a{b}c");
+
+ // The phone-context parameter comes before the phone number.
+ assertThrowsForInvalidPhoneContext(";phone-context=+64;tel:03-331-6005");
+ assertThrowsForInvalidPhoneContext("phone-context=+64;tel:03-331-6005");
}
private void assertThrowsForInvalidPhoneContext(String numberToParse) {
@@ -3279,4 +3288,29 @@
}
});
}
+
+ public void testBuildNationalNumberForParsing() throws Exception {
+ // Test that the national prefix is stripped from the numberPart when the phone context is a
+ // country code.
+ StringBuilder nationalNumber = new StringBuilder();
+ phoneUtil.buildNationalNumberForParsing("tel:033316005;phone-context=+64", nationalNumber);
+ assertEquals("+6433316005", nationalNumber.toString());
+ nationalNumber.setLength(0);
+ // Test that the phone context is ignored if it is not a country code.
+ phoneUtil.buildNationalNumberForParsing("tel:033316005;phone-context=abc.nz", nationalNumber);
+ assertEquals("033316005", nationalNumber.toString());
+ // Test that extensions are correctly parsed.
+ nationalNumber.setLength(0);
+ phoneUtil.buildNationalNumberForParsing("tel:033316005;ext=1234;phone-context=+64",
+ nationalNumber);
+ assertEquals("+6433316005 ext. 1234", nationalNumber.toString());
+ nationalNumber.setLength(0);
+ phoneUtil.buildNationalNumberForParsing("tel:033316005;phone-context=+64;ext=1234",
+ nationalNumber);
+ assertEquals("+6433316005 ext. 1234", nationalNumber.toString());
+ // Test that the isub parameter is removed.
+ nationalNumber.setLength(0);
+ phoneUtil.buildNationalNumberForParsing("tel:033316005;isub=1234", nationalNumber);
+ assertEquals("033316005", nationalNumber.toString());
+ }
}