diff --git a/tools/java/.gitignore b/tools/java/.gitignore
new file mode 100644
index 0000000..405aa4e
--- /dev/null
+++ b/tools/java/.gitignore
@@ -0,0 +1,3 @@
+target
+generated
+bin
diff --git a/tools/java/Readme.md b/tools/java/Readme.md
new file mode 100644
index 0000000..a958d94
--- /dev/null
+++ b/tools/java/Readme.md
@@ -0,0 +1 @@
+## This directory contains script for autogeneration of metadata in rust
\ No newline at end of file
diff --git a/tools/java/common/pom.xml b/tools/java/common/pom.xml
new file mode 100644
index 0000000..8b3c10a
--- /dev/null
+++ b/tools/java/common/pom.xml
@@ -0,0 +1,78 @@
+
+
+ 4.0.0
+
+
+ tools
+ com.google.i18n.phonenumbers
+ 1.0-SNAPSHOT
+
+
+ com.google.i18n.phonenumbers.tools
+ common-build
+ 1.0-SNAPSHOT
+ Libphonenumber common library for build tools
+
+
+ src
+ test
+
+
+ src/com/google/i18n/phonenumbers
+ com/google/i18n/phonenumbers
+
+
+
+
+ org.apache.maven.plugins
+ maven-compiler-plugin
+ 3.11.0
+
+ 8
+ 8
+ UTF-8
+
+
+
+
+ org.codehaus.mojo
+ build-helper-maven-plugin
+ 3.4.0
+
+
+ add-source
+ generate-sources
+
+ add-source
+
+
+
+ ../../../java/libphonenumber/src/
+
+
+
+
+
+
+
+
+
+
+ junit
+ junit
+ 4.13.2
+ test
+
+
+
+ com.googlecode.libphonenumber
+ libphonenumber
+ 9.0.9
+
+
+
+
diff --git a/tools/java/common/src/com/google/i18n/phonenumbers/BuildMetadataFromXml.java b/tools/java/common/src/com/google/i18n/phonenumbers/BuildMetadataFromXml.java
new file mode 100644
index 0000000..c6ce40b
--- /dev/null
+++ b/tools/java/common/src/com/google/i18n/phonenumbers/BuildMetadataFromXml.java
@@ -0,0 +1,783 @@
+/*
+ * Copyright (C) 2009 The Libphonenumber Authors
+ *
+ * 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.google.i18n.phonenumbers;
+
+import com.google.i18n.phonenumbers.Phonemetadata.NumberFormat;
+import com.google.i18n.phonenumbers.Phonemetadata.PhoneMetadata;
+import com.google.i18n.phonenumbers.Phonemetadata.PhoneMetadataCollection;
+import com.google.i18n.phonenumbers.Phonemetadata.PhoneNumberDesc;
+import java.io.File;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import java.util.TreeMap;
+import java.util.TreeSet;
+import java.util.logging.Level;
+import java.util.logging.Logger;
+import java.util.regex.Pattern;
+import java.util.regex.PatternSyntaxException;
+import javax.xml.parsers.DocumentBuilder;
+import javax.xml.parsers.DocumentBuilderFactory;
+import org.w3c.dom.Document;
+import org.w3c.dom.Element;
+import org.w3c.dom.NodeList;
+
+/**
+ * Library to build phone number metadata from the XML format.
+ *
+ * @author Shaopeng Jia
+ */
+public class BuildMetadataFromXml {
+ private static final Logger logger = Logger.getLogger(BuildMetadataFromXml.class.getName());
+
+ // String constants used to fetch the XML nodes and attributes.
+ private static final String CARRIER_CODE_FORMATTING_RULE = "carrierCodeFormattingRule";
+ private static final String CARRIER_SPECIFIC = "carrierSpecific";
+ private static final String COUNTRY_CODE = "countryCode";
+ private static final String EMERGENCY = "emergency";
+ private static final String EXAMPLE_NUMBER = "exampleNumber";
+ private static final String FIXED_LINE = "fixedLine";
+ private static final String FORMAT = "format";
+ private static final String GENERAL_DESC = "generalDesc";
+ private static final String INTERNATIONAL_PREFIX = "internationalPrefix";
+ private static final String INTL_FORMAT = "intlFormat";
+ private static final String LEADING_DIGITS = "leadingDigits";
+ private static final String MAIN_COUNTRY_FOR_CODE = "mainCountryForCode";
+ private static final String MOBILE = "mobile";
+ private static final String MOBILE_NUMBER_PORTABLE_REGION = "mobileNumberPortableRegion";
+ private static final String NATIONAL_NUMBER_PATTERN = "nationalNumberPattern";
+ private static final String NATIONAL_PREFIX = "nationalPrefix";
+ private static final String NATIONAL_PREFIX_FORMATTING_RULE = "nationalPrefixFormattingRule";
+ private static final String NATIONAL_PREFIX_OPTIONAL_WHEN_FORMATTING =
+ "nationalPrefixOptionalWhenFormatting";
+ private static final String NATIONAL_PREFIX_FOR_PARSING = "nationalPrefixForParsing";
+ private static final String NATIONAL_PREFIX_TRANSFORM_RULE = "nationalPrefixTransformRule";
+ private static final String NO_INTERNATIONAL_DIALLING = "noInternationalDialling";
+ private static final String NUMBER_FORMAT = "numberFormat";
+ private static final String PAGER = "pager";
+ private static final String PATTERN = "pattern";
+ private static final String PERSONAL_NUMBER = "personalNumber";
+ private static final String POSSIBLE_LENGTHS = "possibleLengths";
+ private static final String NATIONAL = "national";
+ private static final String LOCAL_ONLY = "localOnly";
+ private static final String PREFERRED_EXTN_PREFIX = "preferredExtnPrefix";
+ private static final String PREFERRED_INTERNATIONAL_PREFIX = "preferredInternationalPrefix";
+ private static final String PREMIUM_RATE = "premiumRate";
+ private static final String SHARED_COST = "sharedCost";
+ private static final String SHORT_CODE = "shortCode";
+ private static final String SMS_SERVICES = "smsServices";
+ private static final String STANDARD_RATE = "standardRate";
+ private static final String TOLL_FREE = "tollFree";
+ private static final String UAN = "uan";
+ private static final String VOICEMAIL = "voicemail";
+ private static final String VOIP = "voip";
+
+ private static final Set PHONE_NUMBER_DESCS_WITHOUT_MATCHING_TYPES =
+ new HashSet(Arrays.asList(new String[]{NO_INTERNATIONAL_DIALLING}));
+
+ // Build the PhoneMetadataCollection from the input XML file.
+ public static PhoneMetadataCollection buildPhoneMetadataCollection(String inputXmlFile,
+ boolean liteBuild, boolean specialBuild) throws Exception {
+ DocumentBuilderFactory builderFactory = DocumentBuilderFactory.newInstance();
+ DocumentBuilder builder = builderFactory.newDocumentBuilder();
+ File xmlFile = new File(inputXmlFile);
+ Document document = builder.parse(xmlFile);
+ // TODO: Look for other uses of these constants and possibly pull them out into a separate
+ // constants file.
+ boolean isShortNumberMetadata = inputXmlFile.contains("ShortNumberMetadata");
+ boolean isAlternateFormatsMetadata = inputXmlFile.contains("PhoneNumberAlternateFormats");
+ return buildPhoneMetadataCollection(document, liteBuild, specialBuild,
+ isShortNumberMetadata, isAlternateFormatsMetadata);
+ }
+
+ // @VisibleForTesting
+ static PhoneMetadataCollection buildPhoneMetadataCollection(Document document,
+ boolean liteBuild, boolean specialBuild, boolean isShortNumberMetadata,
+ boolean isAlternateFormatsMetadata) throws Exception {
+ document.getDocumentElement().normalize();
+ Element rootElement = document.getDocumentElement();
+ NodeList territory = rootElement.getElementsByTagName("territory");
+ PhoneMetadataCollection.Builder metadataCollection = PhoneMetadataCollection.newBuilder();
+ int numOfTerritories = territory.getLength();
+ // TODO: Infer filter from a single flag.
+ MetadataFilter metadataFilter = getMetadataFilter(liteBuild, specialBuild);
+ for (int i = 0; i < numOfTerritories; i++) {
+ Element territoryElement = (Element) territory.item(i);
+ String regionCode = "";
+ // For the main metadata file this should always be set, but for other supplementary data
+ // files the country calling code may be all that is needed.
+ if (territoryElement.hasAttribute("id")) {
+ regionCode = territoryElement.getAttribute("id");
+ }
+ PhoneMetadata.Builder metadata = loadCountryMetadata(regionCode, territoryElement,
+ isShortNumberMetadata, isAlternateFormatsMetadata);
+ metadataFilter.filterMetadata(metadata);
+ metadataCollection.addMetadata(metadata.build());
+ }
+ return metadataCollection.build();
+ }
+
+ // Build a mapping from a country calling code to the region codes which denote the country/region
+ // represented by that country code. In the case of multiple countries sharing a calling code,
+ // such as the NANPA countries, the one indicated with "isMainCountryForCode" in the metadata
+ // should be first.
+ public static Map> buildCountryCodeToRegionCodeMap(
+ PhoneMetadataCollection metadataCollection) {
+ Map> countryCodeToRegionCodeMap = new TreeMap>();
+ for (PhoneMetadata metadata : metadataCollection.getMetadataList()) {
+ String regionCode = metadata.getId();
+ int countryCode = metadata.getCountryCode();
+ if (countryCodeToRegionCodeMap.containsKey(countryCode)) {
+ if (metadata.getMainCountryForCode()) {
+ countryCodeToRegionCodeMap.get(countryCode).add(0, regionCode);
+ } else {
+ countryCodeToRegionCodeMap.get(countryCode).add(regionCode);
+ }
+ } else {
+ // For most countries, there will be only one region code for the country calling code.
+ List listWithRegionCode = new ArrayList(1);
+ if (!regionCode.equals("")) { // For alternate formats, there are no region codes at all.
+ listWithRegionCode.add(regionCode);
+ }
+ countryCodeToRegionCodeMap.put(countryCode, listWithRegionCode);
+ }
+ }
+ return countryCodeToRegionCodeMap;
+ }
+
+ // Build a list of region codes from the metadata
+ public static List buildRegionCodeList(
+ PhoneMetadataCollection metadataCollection) {
+ List regionCodeList = new ArrayList();
+ for (PhoneMetadata metadata : metadataCollection.getMetadataList()) {
+ String regionCode = metadata.getId();
+ regionCodeList.add(regionCode);
+ }
+ return regionCodeList;
+ }
+
+ private static String validateRE(String regex) {
+ return validateRE(regex, false);
+ }
+
+ // @VisibleForTesting
+ static String validateRE(String regex, boolean removeWhitespace) {
+ // Removes all the whitespace and newline from the regexp. Not using pattern compile options to
+ // make it work across programming languages.
+ String compressedRegex = removeWhitespace ? regex.replaceAll("\\s", "") : regex;
+ Pattern.compile(compressedRegex);
+ // We don't ever expect to see | followed by a ) in our metadata - this would be an indication
+ // of a bug. If one wants to make something optional, we prefer ? to using an empty group.
+ int errorIndex = compressedRegex.indexOf("|)");
+ if (errorIndex >= 0) {
+ logger.log(Level.SEVERE, "Error with original regex: " + regex
+ + "\n| should not be followed directly by ) in phone number regular expressions.");
+ throw new PatternSyntaxException("| followed by )", compressedRegex, errorIndex);
+ }
+ // return the regex if it is of correct syntax, i.e. compile did not fail with a
+ // PatternSyntaxException.
+ return compressedRegex;
+ }
+
+ /**
+ * Returns the national prefix of the provided country element.
+ */
+ // @VisibleForTesting
+ static String getNationalPrefix(Element element) {
+ return element.hasAttribute(NATIONAL_PREFIX) ? element.getAttribute(NATIONAL_PREFIX) : "";
+ }
+
+ // @VisibleForTesting
+ static PhoneMetadata.Builder loadTerritoryTagMetadata(String regionCode, Element element,
+ String nationalPrefix) {
+ PhoneMetadata.Builder metadata = PhoneMetadata.newBuilder();
+ metadata.setId(regionCode);
+ if (element.hasAttribute(COUNTRY_CODE)) {
+ metadata.setCountryCode(Integer.parseInt(element.getAttribute(COUNTRY_CODE)));
+ }
+ if (element.hasAttribute(LEADING_DIGITS)) {
+ metadata.setLeadingDigits(validateRE(element.getAttribute(LEADING_DIGITS)));
+ }
+ if (element.hasAttribute(INTERNATIONAL_PREFIX)) {
+ metadata.setInternationalPrefix(validateRE(element.getAttribute(INTERNATIONAL_PREFIX)));
+ }
+ if (element.hasAttribute(PREFERRED_INTERNATIONAL_PREFIX)) {
+ metadata.setPreferredInternationalPrefix(
+ element.getAttribute(PREFERRED_INTERNATIONAL_PREFIX));
+ }
+ if (element.hasAttribute(NATIONAL_PREFIX_FOR_PARSING)) {
+ metadata.setNationalPrefixForParsing(
+ validateRE(element.getAttribute(NATIONAL_PREFIX_FOR_PARSING), true));
+ if (element.hasAttribute(NATIONAL_PREFIX_TRANSFORM_RULE)) {
+ metadata.setNationalPrefixTransformRule(
+ validateRE(element.getAttribute(NATIONAL_PREFIX_TRANSFORM_RULE)));
+ }
+ }
+ if (!nationalPrefix.isEmpty()) {
+ metadata.setNationalPrefix(nationalPrefix);
+ if (!metadata.hasNationalPrefixForParsing()) {
+ metadata.setNationalPrefixForParsing(nationalPrefix);
+ }
+ }
+ if (element.hasAttribute(PREFERRED_EXTN_PREFIX)) {
+ metadata.setPreferredExtnPrefix(element.getAttribute(PREFERRED_EXTN_PREFIX));
+ }
+ if (element.hasAttribute(MAIN_COUNTRY_FOR_CODE)) {
+ metadata.setMainCountryForCode(true);
+ }
+ if (element.hasAttribute(MOBILE_NUMBER_PORTABLE_REGION)) {
+ metadata.setMobileNumberPortableRegion(true);
+ }
+ return metadata;
+ }
+
+ /**
+ * Extracts the pattern for international format. If there is no intlFormat, default to using the
+ * national format. If the intlFormat is set to "NA" the intlFormat should be ignored.
+ *
+ * @throws RuntimeException if multiple intlFormats have been encountered.
+ * @return whether an international number format is defined.
+ */
+ // @VisibleForTesting
+ static boolean loadInternationalFormat(PhoneMetadata.Builder metadata,
+ Element numberFormatElement,
+ NumberFormat nationalFormat) {
+ NumberFormat.Builder intlFormat = NumberFormat.newBuilder();
+ NodeList intlFormatPattern = numberFormatElement.getElementsByTagName(INTL_FORMAT);
+ boolean hasExplicitIntlFormatDefined = false;
+
+ if (intlFormatPattern.getLength() > 1) {
+ logger.log(Level.SEVERE,
+ "A maximum of one intlFormat pattern for a numberFormat element should be defined.");
+ String countryId = metadata.getId().length() > 0 ? metadata.getId()
+ : Integer.toString(metadata.getCountryCode());
+ throw new RuntimeException("Invalid number of intlFormat patterns for country: " + countryId);
+ } else if (intlFormatPattern.getLength() == 0) {
+ // Default to use the same as the national pattern if none is defined.
+ intlFormat.mergeFrom(nationalFormat);
+ } else {
+ intlFormat.setPattern(numberFormatElement.getAttribute(PATTERN));
+ setLeadingDigitsPatterns(numberFormatElement, intlFormat);
+ String intlFormatPatternValue = intlFormatPattern.item(0).getFirstChild().getNodeValue();
+ if (!intlFormatPatternValue.equals("NA")) {
+ intlFormat.setFormat(intlFormatPatternValue);
+ }
+ hasExplicitIntlFormatDefined = true;
+ }
+
+ if (intlFormat.hasFormat()) {
+ metadata.addIntlNumberFormat(intlFormat.build());
+ }
+ return hasExplicitIntlFormatDefined;
+ }
+
+ /**
+ * Extracts the pattern for the national format.
+ *
+ * @throws RuntimeException if multiple or no formats have been encountered.
+ */
+ // @VisibleForTesting
+ static void loadNationalFormat(PhoneMetadata.Builder metadata, Element numberFormatElement,
+ NumberFormat.Builder format) {
+ setLeadingDigitsPatterns(numberFormatElement, format);
+ format.setPattern(validateRE(numberFormatElement.getAttribute(PATTERN)));
+
+ NodeList formatPattern = numberFormatElement.getElementsByTagName(FORMAT);
+ int numFormatPatterns = formatPattern.getLength();
+ if (numFormatPatterns != 1) {
+ logger.log(Level.SEVERE, "One format pattern for a numberFormat element should be defined.");
+ String countryId = metadata.getId().length() > 0 ? metadata.getId()
+ : Integer.toString(metadata.getCountryCode());
+ throw new RuntimeException("Invalid number of format patterns (" + numFormatPatterns
+ + ") for country: " + countryId);
+ }
+ format.setFormat(formatPattern.item(0).getFirstChild().getNodeValue());
+ }
+
+ /**
+ * Extracts the available formats from the provided DOM element. If it does not contain any
+ * nationalPrefixFormattingRule, the one passed-in is retained; similarly for
+ * nationalPrefixOptionalWhenFormatting. The nationalPrefix, nationalPrefixFormattingRule and
+ * nationalPrefixOptionalWhenFormatting values are provided from the parent (territory) element.
+ */
+ // @VisibleForTesting
+ static void loadAvailableFormats(PhoneMetadata.Builder metadata,
+ Element element, String nationalPrefix,
+ String nationalPrefixFormattingRule,
+ boolean nationalPrefixOptionalWhenFormatting) {
+ String carrierCodeFormattingRule = "";
+ if (element.hasAttribute(CARRIER_CODE_FORMATTING_RULE)) {
+ carrierCodeFormattingRule = validateRE(
+ getDomesticCarrierCodeFormattingRuleFromElement(element, nationalPrefix));
+ }
+ NodeList numberFormatElements = element.getElementsByTagName(NUMBER_FORMAT);
+ boolean hasExplicitIntlFormatDefined = false;
+
+ int numOfFormatElements = numberFormatElements.getLength();
+ if (numOfFormatElements > 0) {
+ for (int i = 0; i < numOfFormatElements; i++) {
+ Element numberFormatElement = (Element) numberFormatElements.item(i);
+ NumberFormat.Builder format = NumberFormat.newBuilder();
+
+ if (numberFormatElement.hasAttribute(NATIONAL_PREFIX_FORMATTING_RULE)) {
+ format.setNationalPrefixFormattingRule(
+ getNationalPrefixFormattingRuleFromElement(numberFormatElement, nationalPrefix));
+ } else if (!nationalPrefixFormattingRule.equals("")) {
+ format.setNationalPrefixFormattingRule(nationalPrefixFormattingRule);
+ }
+ if (numberFormatElement.hasAttribute(NATIONAL_PREFIX_OPTIONAL_WHEN_FORMATTING)) {
+ format.setNationalPrefixOptionalWhenFormatting(
+ Boolean.valueOf(numberFormatElement.getAttribute(
+ NATIONAL_PREFIX_OPTIONAL_WHEN_FORMATTING)));
+ } else if (format.getNationalPrefixOptionalWhenFormatting()
+ != nationalPrefixOptionalWhenFormatting) {
+ // Inherit from the parent field if it is not already the same as the default.
+ format.setNationalPrefixOptionalWhenFormatting(nationalPrefixOptionalWhenFormatting);
+ }
+ if (numberFormatElement.hasAttribute(CARRIER_CODE_FORMATTING_RULE)) {
+ format.setDomesticCarrierCodeFormattingRule(validateRE(
+ getDomesticCarrierCodeFormattingRuleFromElement(numberFormatElement,
+ nationalPrefix)));
+ } else if (!carrierCodeFormattingRule.equals("")) {
+ format.setDomesticCarrierCodeFormattingRule(carrierCodeFormattingRule);
+ }
+ loadNationalFormat(metadata, numberFormatElement, format);
+ metadata.addNumberFormat(format);
+
+ if (loadInternationalFormat(metadata, numberFormatElement, format.build())) {
+ hasExplicitIntlFormatDefined = true;
+ }
+ }
+ // Only a small number of regions need to specify the intlFormats in the xml. For the majority
+ // of countries the intlNumberFormat metadata is an exact copy of the national NumberFormat
+ // metadata. To minimize the size of the metadata file, we only keep intlNumberFormats that
+ // actually differ in some way to the national formats.
+ if (!hasExplicitIntlFormatDefined) {
+ metadata.clearIntlNumberFormat();
+ }
+ }
+ }
+
+ // @VisibleForTesting
+ static void setLeadingDigitsPatterns(Element numberFormatElement, NumberFormat.Builder format) {
+ NodeList leadingDigitsPatternNodes = numberFormatElement.getElementsByTagName(LEADING_DIGITS);
+ int numOfLeadingDigitsPatterns = leadingDigitsPatternNodes.getLength();
+ if (numOfLeadingDigitsPatterns > 0) {
+ for (int i = 0; i < numOfLeadingDigitsPatterns; i++) {
+ format.addLeadingDigitsPattern(
+ validateRE((leadingDigitsPatternNodes.item(i)).getFirstChild().getNodeValue(), true));
+ }
+ }
+ }
+
+ // @VisibleForTesting
+ static String getNationalPrefixFormattingRuleFromElement(Element element,
+ String nationalPrefix) {
+ String nationalPrefixFormattingRule = element.getAttribute(NATIONAL_PREFIX_FORMATTING_RULE);
+ // Replace $NP with national prefix and $FG with the first group ($1).
+ nationalPrefixFormattingRule =
+ nationalPrefixFormattingRule.replaceFirst("\\$NP", nationalPrefix)
+ .replaceFirst("\\$FG", "\\$1");
+ return nationalPrefixFormattingRule;
+ }
+
+ // @VisibleForTesting
+ static String getDomesticCarrierCodeFormattingRuleFromElement(Element element,
+ String nationalPrefix) {
+ String carrierCodeFormattingRule = element.getAttribute(CARRIER_CODE_FORMATTING_RULE);
+ // Replace $FG with the first group ($1) and $NP with the national prefix.
+ carrierCodeFormattingRule = carrierCodeFormattingRule.replaceFirst("\\$FG", "\\$1")
+ .replaceFirst("\\$NP", nationalPrefix);
+ return carrierCodeFormattingRule;
+ }
+
+ /**
+ * Checks if the possible lengths provided as a sorted set are equal to the possible lengths
+ * stored already in the description pattern. Note that possibleLengths may be empty but must not
+ * be null, and the PhoneNumberDesc passed in should also not be null.
+ */
+ private static boolean arePossibleLengthsEqual(TreeSet possibleLengths,
+ PhoneNumberDesc desc) {
+ if (possibleLengths.size() != desc.getPossibleLengthCount()) {
+ return false;
+ }
+ // Note that both should be sorted already, and we know they are the same length.
+ int i = 0;
+ for (Integer length : possibleLengths) {
+ if (length != desc.getPossibleLength(i)) {
+ return false;
+ }
+ i++;
+ }
+ return true;
+ }
+
+ /**
+ * Processes a phone number description element from the XML file and returns it as a
+ * PhoneNumberDesc. If the description element is a fixed line or mobile number, the parent
+ * description will be used to fill in the whole element if necessary, or any components that are
+ * missing. For all other types, the parent description will only be used to fill in missing
+ * components if the type has a partial definition. For example, if no "tollFree" element exists,
+ * we assume there are no toll free numbers for that locale, and return a phone number description
+ * with no national number data and [-1] for the possible lengths. Note that the parent
+ * description must therefore already be processed before this method is called on any child
+ * elements.
+ *
+ * @param parentDesc a generic phone number description that will be used to fill in missing
+ * parts of the description, or null if this is the root node. This must be processed before
+ * this is run on any child elements.
+ * @param countryElement the XML element representing all the country information
+ * @param numberType the name of the number type, corresponding to the appropriate tag in the XML
+ * file with information about that type
+ * @return complete description of that phone number type
+ */
+ // @VisibleForTesting
+ static PhoneNumberDesc.Builder processPhoneNumberDescElement(PhoneNumberDesc.Builder parentDesc,
+ Element countryElement,
+ String numberType) {
+ NodeList phoneNumberDescList = countryElement.getElementsByTagName(numberType);
+ PhoneNumberDesc.Builder numberDesc = PhoneNumberDesc.newBuilder();
+ if (phoneNumberDescList.getLength() == 0) {
+ // -1 will never match a possible phone number length, so is safe to use to ensure this never
+ // matches. We don't leave it empty, since for compression reasons, we use the empty list to
+ // mean that the generalDesc possible lengths apply.
+ numberDesc.addPossibleLength(-1);
+ return numberDesc;
+ }
+ if (phoneNumberDescList.getLength() > 0) {
+ if (phoneNumberDescList.getLength() > 1) {
+ throw new RuntimeException(
+ String.format("Multiple elements with type %s found.", numberType));
+ }
+ Element element = (Element) phoneNumberDescList.item(0);
+ if (parentDesc != null) {
+ // New way of handling possible number lengths. We don't do this for the general
+ // description, since these tags won't be present; instead we will calculate its values
+ // based on the values for all the other number type descriptions (see
+ // setPossibleLengthsGeneralDesc).
+ TreeSet lengths = new TreeSet();
+ TreeSet localOnlyLengths = new TreeSet();
+ populatePossibleLengthSets(element, lengths, localOnlyLengths);
+ setPossibleLengths(lengths, localOnlyLengths, parentDesc.build(), numberDesc);
+ }
+
+ NodeList validPattern = element.getElementsByTagName(NATIONAL_NUMBER_PATTERN);
+ if (validPattern.getLength() > 0) {
+ numberDesc.setNationalNumberPattern(
+ validateRE(validPattern.item(0).getFirstChild().getNodeValue(), true));
+ }
+
+ NodeList exampleNumber = element.getElementsByTagName(EXAMPLE_NUMBER);
+ if (exampleNumber.getLength() > 0) {
+ numberDesc.setExampleNumber(exampleNumber.item(0).getFirstChild().getNodeValue());
+ }
+ }
+ return numberDesc;
+ }
+
+ // @VisibleForTesting
+ static void setRelevantDescPatterns(PhoneMetadata.Builder metadata, Element element,
+ boolean isShortNumberMetadata) {
+ PhoneNumberDesc.Builder generalDesc = processPhoneNumberDescElement(null, element,
+ GENERAL_DESC);
+ // Calculate the possible lengths for the general description. This will be based on the
+ // possible lengths of the child elements.
+ setPossibleLengthsGeneralDesc(generalDesc, metadata.getId(), element, isShortNumberMetadata);
+ metadata.setGeneralDesc(generalDesc);
+
+ if (!isShortNumberMetadata) {
+ // Set fields used by regular length phone numbers.
+ metadata.setFixedLine(processPhoneNumberDescElement(generalDesc, element, FIXED_LINE));
+ metadata.setMobile(processPhoneNumberDescElement(generalDesc, element, MOBILE));
+ metadata.setSharedCost(processPhoneNumberDescElement(generalDesc, element, SHARED_COST));
+ metadata.setVoip(processPhoneNumberDescElement(generalDesc, element, VOIP));
+ metadata.setPersonalNumber(processPhoneNumberDescElement(generalDesc, element,
+ PERSONAL_NUMBER));
+ metadata.setPager(processPhoneNumberDescElement(generalDesc, element, PAGER));
+ metadata.setUan(processPhoneNumberDescElement(generalDesc, element, UAN));
+ metadata.setVoicemail(processPhoneNumberDescElement(generalDesc, element, VOICEMAIL));
+ metadata.setNoInternationalDialling(processPhoneNumberDescElement(generalDesc, element,
+ NO_INTERNATIONAL_DIALLING));
+ boolean mobileAndFixedAreSame = metadata.getMobile().getNationalNumberPattern()
+ .equals(metadata.getFixedLine().getNationalNumberPattern());
+ if (metadata.getSameMobileAndFixedLinePattern() != mobileAndFixedAreSame) {
+ // Set this if it is not the same as the default.
+ metadata.setSameMobileAndFixedLinePattern(mobileAndFixedAreSame);
+ }
+ metadata.setTollFree(processPhoneNumberDescElement(generalDesc, element, TOLL_FREE));
+ metadata.setPremiumRate(processPhoneNumberDescElement(generalDesc, element, PREMIUM_RATE));
+ } else {
+ // Set fields used by short numbers.
+ metadata.setStandardRate(processPhoneNumberDescElement(generalDesc, element, STANDARD_RATE));
+ metadata.setShortCode(processPhoneNumberDescElement(generalDesc, element, SHORT_CODE));
+ metadata.setCarrierSpecific(processPhoneNumberDescElement(generalDesc, element,
+ CARRIER_SPECIFIC));
+ metadata.setEmergency(processPhoneNumberDescElement(generalDesc, element, EMERGENCY));
+ metadata.setTollFree(processPhoneNumberDescElement(generalDesc, element, TOLL_FREE));
+ metadata.setPremiumRate(processPhoneNumberDescElement(generalDesc, element, PREMIUM_RATE));
+ metadata.setSmsServices(processPhoneNumberDescElement(generalDesc, element, SMS_SERVICES));
+ }
+ }
+
+ /**
+ * Parses a possible length string into a set of the integers that are covered.
+ *
+ * @param possibleLengthString a string specifying the possible lengths of phone numbers. Follows
+ * this syntax: ranges or elements are separated by commas, and ranges are specified in
+ * [min-max] notation, inclusive. For example, [3-5],7,9,[11-14] should be parsed to
+ * 3,4,5,7,9,11,12,13,14.
+ */
+ private static Set parsePossibleLengthStringToSet(String possibleLengthString) {
+ if (possibleLengthString.length() == 0) {
+ throw new RuntimeException("Empty possibleLength string found.");
+ }
+ String[] lengths = possibleLengthString.split(",");
+ Set lengthSet = new TreeSet();
+ for (int i = 0; i < lengths.length; i++) {
+ String lengthSubstring = lengths[i];
+ if (lengthSubstring.length() == 0) {
+ throw new RuntimeException(String.format("Leading, trailing or adjacent commas in possible "
+ + "length string %s, these should only separate numbers or ranges.",
+ possibleLengthString));
+ } else if (lengthSubstring.charAt(0) == '[') {
+ if (lengthSubstring.charAt(lengthSubstring.length() - 1) != ']') {
+ throw new RuntimeException(String.format("Missing end of range character in possible "
+ + "length string %s.", possibleLengthString));
+ }
+ // Strip the leading and trailing [], and split on the -.
+ String[] minMax = lengthSubstring.substring(1, lengthSubstring.length() - 1).split("-");
+ if (minMax.length != 2) {
+ throw new RuntimeException(String.format("Ranges must have exactly one - character: "
+ + "missing for %s.", possibleLengthString));
+ }
+ int min = Integer.parseInt(minMax[0]);
+ int max = Integer.parseInt(minMax[1]);
+ // We don't even accept [6-7] since we prefer the shorter 6,7 variant; for a range to be in
+ // use the hyphen needs to replace at least one digit.
+ if (max - min < 2) {
+ throw new RuntimeException(String.format("The first number in a range should be two or "
+ + "more digits lower than the second. Culprit possibleLength string: %s",
+ possibleLengthString));
+ }
+ for (int j = min; j <= max; j++) {
+ if (!lengthSet.add(j)) {
+ throw new RuntimeException(String.format("Duplicate length element found (%d) in "
+ + "possibleLength string %s", j, possibleLengthString));
+ }
+ }
+ } else {
+ int length = Integer.parseInt(lengthSubstring);
+ if (!lengthSet.add(length)) {
+ throw new RuntimeException(String.format("Duplicate length element found (%d) in "
+ + "possibleLength string %s", length, possibleLengthString));
+ }
+ }
+ }
+ return lengthSet;
+ }
+
+ /**
+ * Reads the possible lengths present in the metadata and splits them into two sets: one for
+ * full-length numbers, one for local numbers.
+ *
+ * @param data one or more phone number descriptions, represented as XML nodes
+ * @param lengths a set to which to add possible lengths of full phone numbers
+ * @param localOnlyLengths a set to which to add possible lengths of phone numbers only diallable
+ * locally (e.g. within a province)
+ */
+ private static void populatePossibleLengthSets(Element data, TreeSet lengths,
+ TreeSet localOnlyLengths) {
+ NodeList possibleLengths = data.getElementsByTagName(POSSIBLE_LENGTHS);
+ for (int i = 0; i < possibleLengths.getLength(); i++) {
+ Element element = (Element) possibleLengths.item(i);
+ String nationalLengths = element.getAttribute(NATIONAL);
+ // We don't add to the phone metadata yet, since we want to sort length elements found under
+ // different nodes first, make sure there are no duplicates between them and that the
+ // localOnly lengths don't overlap with the others.
+ Set thisElementLengths = parsePossibleLengthStringToSet(nationalLengths);
+ if (element.hasAttribute(LOCAL_ONLY)) {
+ String localLengths = element.getAttribute(LOCAL_ONLY);
+ Set thisElementLocalOnlyLengths = parsePossibleLengthStringToSet(localLengths);
+ Set intersection = new HashSet(thisElementLengths);
+ intersection.retainAll(thisElementLocalOnlyLengths);
+ if (!intersection.isEmpty()) {
+ throw new RuntimeException(String.format(
+ "Possible length(s) found specified as a normal and local-only length: %s",
+ intersection));
+ }
+ // We check again when we set these lengths on the metadata itself in setPossibleLengths
+ // that the elements in localOnly are not also in lengths. For e.g. the generalDesc, it
+ // might have a local-only length for one type that is a normal length for another type. We
+ // don't consider this an error, but we do want to remove the local-only lengths.
+ localOnlyLengths.addAll(thisElementLocalOnlyLengths);
+ }
+ // It is okay if at this time we have duplicates, because the same length might be possible
+ // for e.g. fixed-line and for mobile numbers, and this method operates potentially on
+ // multiple phoneNumberDesc XML elements.
+ lengths.addAll(thisElementLengths);
+ }
+ }
+
+ /**
+ * Sets possible lengths in the general description, derived from certain child elements.
+ */
+ // @VisibleForTesting
+ static void setPossibleLengthsGeneralDesc(PhoneNumberDesc.Builder generalDesc, String metadataId,
+ Element data, boolean isShortNumberMetadata) {
+ TreeSet lengths = new TreeSet();
+ TreeSet localOnlyLengths = new TreeSet();
+ // The general description node should *always* be present if metadata for other types is
+ // present, aside from in some unit tests.
+ // (However, for e.g. formatting metadata in PhoneNumberAlternateFormats, no PhoneNumberDesc
+ // elements are present).
+ NodeList generalDescNodes = data.getElementsByTagName(GENERAL_DESC);
+ if (generalDescNodes.getLength() > 0) {
+ Element generalDescNode = (Element) generalDescNodes.item(0);
+ populatePossibleLengthSets(generalDescNode, lengths, localOnlyLengths);
+ if (!lengths.isEmpty() || !localOnlyLengths.isEmpty()) {
+ // We shouldn't have anything specified at the "general desc" level: we are going to
+ // calculate this ourselves from child elements.
+ throw new RuntimeException(String.format("Found possible lengths specified at general "
+ + "desc: this should be derived from child elements. Affected country: %s",
+ metadataId));
+ }
+ }
+ if (!isShortNumberMetadata) {
+ // Make a copy here since we want to remove some nodes, but we don't want to do that on our
+ // actual data.
+ Element allDescData = (Element) data.cloneNode(true /* deep copy */);
+ for (String tag : PHONE_NUMBER_DESCS_WITHOUT_MATCHING_TYPES) {
+ NodeList nodesToRemove = allDescData.getElementsByTagName(tag);
+ if (nodesToRemove.getLength() > 0) {
+ // We check when we process phone number descriptions that there are only one of each
+ // type, so this is safe to do.
+ allDescData.removeChild(nodesToRemove.item(0));
+ }
+ }
+ populatePossibleLengthSets(allDescData, lengths, localOnlyLengths);
+ } else {
+ // For short number metadata, we want to copy the lengths from the "short code" section only.
+ // This is because it's the more detailed validation pattern, it's not a sub-type of short
+ // codes. The other lengths will be checked later to see that they are a sub-set of these
+ // possible lengths.
+ NodeList shortCodeDescList = data.getElementsByTagName(SHORT_CODE);
+ if (shortCodeDescList.getLength() > 0) {
+ Element shortCodeDesc = (Element) shortCodeDescList.item(0);
+ populatePossibleLengthSets(shortCodeDesc, lengths, localOnlyLengths);
+ }
+ if (localOnlyLengths.size() > 0) {
+ throw new RuntimeException("Found local-only lengths in short-number metadata");
+ }
+ }
+ setPossibleLengths(lengths, localOnlyLengths, null, generalDesc);
+ }
+
+ /**
+ * Sets the possible length fields in the metadata from the sets of data passed in. Checks that
+ * the length is covered by the "parent" phone number description element if one is present, and
+ * if the lengths are exactly the same as this, they are not filled in for efficiency reasons.
+ *
+ * @param parentDesc the "general description" element or null if desc is the generalDesc itself
+ * @param desc the PhoneNumberDesc object that we are going to set lengths for
+ */
+ private static void setPossibleLengths(TreeSet lengths,
+ TreeSet localOnlyLengths, PhoneNumberDesc parentDesc, PhoneNumberDesc.Builder desc) {
+ // We clear these fields since the metadata tends to inherit from the parent element for other
+ // fields (via a mergeFrom).
+ desc.clearPossibleLength();
+ desc.clearPossibleLengthLocalOnly();
+ // Only add the lengths to this sub-type if they aren't exactly the same as the possible
+ // lengths in the general desc (for metadata size reasons).
+ if (parentDesc == null || !arePossibleLengthsEqual(lengths, parentDesc)) {
+ for (Integer length : lengths) {
+ if (parentDesc == null || parentDesc.getPossibleLengthList().contains(length)) {
+ desc.addPossibleLength(length);
+ } else {
+ // We shouldn't have possible lengths defined in a child element that are not covered by
+ // the general description. We check this here even though the general description is
+ // derived from child elements because it is only derived from a subset, and we need to
+ // ensure *all* child elements have a valid possible length.
+ throw new RuntimeException(String.format(
+ "Out-of-range possible length found (%d), parent lengths %s.",
+ length, parentDesc.getPossibleLengthList()));
+ }
+ }
+ }
+ // We check that the local-only length isn't also a normal possible length (only relevant for
+ // the general-desc, since within elements such as fixed-line we would throw an exception if we
+ // saw this) before adding it to the collection of possible local-only lengths.
+ for (Integer length : localOnlyLengths) {
+ if (!lengths.contains(length)) {
+ // We check it is covered by either of the possible length sets of the parent
+ // PhoneNumberDesc, because for example 7 might be a valid localOnly length for mobile, but
+ // a valid national length for fixedLine, so the generalDesc would have the 7 removed from
+ // localOnly.
+ if (parentDesc == null || parentDesc.getPossibleLengthLocalOnlyList().contains(length)
+ || parentDesc.getPossibleLengthList().contains(length)) {
+ desc.addPossibleLengthLocalOnly(length);
+ } else {
+ throw new RuntimeException(String.format(
+ "Out-of-range local-only possible length found (%d), parent length %s.",
+ length, parentDesc.getPossibleLengthLocalOnlyList()));
+ }
+ }
+ }
+ }
+
+ // @VisibleForTesting
+ static PhoneMetadata.Builder loadCountryMetadata(String regionCode,
+ Element element,
+ boolean isShortNumberMetadata,
+ boolean isAlternateFormatsMetadata) {
+ String nationalPrefix = getNationalPrefix(element);
+ PhoneMetadata.Builder metadata = loadTerritoryTagMetadata(regionCode, element, nationalPrefix);
+ String nationalPrefixFormattingRule =
+ getNationalPrefixFormattingRuleFromElement(element, nationalPrefix);
+ loadAvailableFormats(metadata, element, nationalPrefix,
+ nationalPrefixFormattingRule,
+ element.hasAttribute(NATIONAL_PREFIX_OPTIONAL_WHEN_FORMATTING));
+ if (!isAlternateFormatsMetadata) {
+ // The alternate formats metadata does not need most of the patterns to be set.
+ setRelevantDescPatterns(metadata, element, isShortNumberMetadata);
+ }
+ return metadata;
+ }
+
+ /**
+ * Processes the custom build flags and gets a {@code MetadataFilter} which may be used to
+ * filter {@code PhoneMetadata} objects. Incompatible flag combinations throw RuntimeException.
+ *
+ * @param liteBuild The liteBuild flag value as given by the command-line
+ * @param specialBuild The specialBuild flag value as given by the command-line
+ */
+ // @VisibleForTesting
+ static MetadataFilter getMetadataFilter(boolean liteBuild, boolean specialBuild) {
+ if (specialBuild) {
+ if (liteBuild) {
+ throw new RuntimeException("liteBuild and specialBuild may not both be set");
+ }
+ return MetadataFilter.forSpecialBuild();
+ }
+ if (liteBuild) {
+ return MetadataFilter.forLiteBuild();
+ }
+ return MetadataFilter.emptyFilter();
+ }
+}
diff --git a/tools/java/common/src/com/google/i18n/phonenumbers/Command.java b/tools/java/common/src/com/google/i18n/phonenumbers/Command.java
new file mode 100644
index 0000000..b54ef73
--- /dev/null
+++ b/tools/java/common/src/com/google/i18n/phonenumbers/Command.java
@@ -0,0 +1,51 @@
+/*
+ * Copyright (C) 2011 The Libphonenumber Authors
+ *
+ * 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.google.i18n.phonenumbers;
+
+/**
+ * Abstract class defining a common interface for commands provided by build tools (e.g: commands to
+ * generate code or to download source files).
+ *
+ *
Subclass it to create a new command (e.g: code generation step).
+ *
+ * @author Philippe Liard
+ */
+public abstract class Command {
+ // The arguments provided to this command. The first one is the name of the command.
+ private String[] args;
+
+ /**
+ * Entry point of the command called by the CommandDispatcher when requested. This method must be
+ * implemented by subclasses.
+ */
+ public abstract boolean start();
+
+ /**
+ * The name of the command is used by the CommandDispatcher to execute the requested command. The
+ * Dispatcher will pass along all command-line arguments to this command, so args[0] will be
+ * always the command name.
+ */
+ public abstract String getCommandName();
+
+ public String[] getArgs() {
+ return args;
+ }
+
+ public void setArgs(String[] args) {
+ this.args = args;
+ }
+}
diff --git a/tools/java/common/src/com/google/i18n/phonenumbers/CommandDispatcher.java b/tools/java/common/src/com/google/i18n/phonenumbers/CommandDispatcher.java
new file mode 100644
index 0000000..2806ba3
--- /dev/null
+++ b/tools/java/common/src/com/google/i18n/phonenumbers/CommandDispatcher.java
@@ -0,0 +1,78 @@
+/*
+ * Copyright (C) 2011 The Libphonenumber Authors
+ *
+ * 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.google.i18n.phonenumbers;
+
+/**
+ * This class is designed to execute a requested command among a set of provided commands.
+ * The dispatching is performed according to the requested command name, which is provided as the
+ * first string of the 'args' array. The 'args' array also contains the command arguments available
+ * from position 1 to end. The verification of the arguments' consistency is under the
+ * responsibility of the command since the dispatcher can't be aware of its underlying goals.
+ *
+ * @see Command
+ * @author Philippe Liard
+ */
+public class CommandDispatcher {
+ // Command line arguments passed to the command which will be executed. Note that the first one is
+ // the name of the command.
+ private final String[] args;
+ // Supported commands by this dispatcher.
+ private final Command[] commands;
+
+ public CommandDispatcher(String[] args, Command[] commands) {
+ this.args = args;
+ this.commands = commands;
+ }
+
+ /**
+ * Executes the command named `args[0]` if any. If the requested command (in args[0]) is not
+ * supported, display a help message.
+ *
+ *
Note that the command name comparison is case sensitive.
+ */
+ public boolean start() {
+ if (args.length != 0) {
+ String requestedCommand = args[0];
+
+ for (Command command : commands) {
+ if (command.getCommandName().equals(requestedCommand)) {
+ command.setArgs(args);
+ return command.start();
+ }
+ }
+ }
+ displayUsage();
+ return false;
+ }
+
+ /**
+ * Displays a message containing the list of the supported commands by this dispatcher.
+ */
+ private void displayUsage() {
+ StringBuilder msg = new StringBuilder("Usage: java -jar /path/to/jar [ ");
+ int i = 0;
+
+ for (Command command : commands) {
+ msg.append(command.getCommandName());
+ if (i++ != commands.length - 1) {
+ msg.append(" | ");
+ }
+ }
+ msg.append(" ] args");
+ System.err.println(msg.toString());
+ }
+}
diff --git a/tools/java/common/src/com/google/i18n/phonenumbers/CopyrightNotice.java b/tools/java/common/src/com/google/i18n/phonenumbers/CopyrightNotice.java
new file mode 100644
index 0000000..d8cd9e2
--- /dev/null
+++ b/tools/java/common/src/com/google/i18n/phonenumbers/CopyrightNotice.java
@@ -0,0 +1,66 @@
+/*
+ * Copyright (C) 2011 The Libphonenumber Authors
+ *
+ * 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.google.i18n.phonenumbers;
+
+import java.io.IOException;
+import java.io.Writer;
+import java.util.Formatter;
+
+/**
+ * Class containing the Apache copyright notice used by code generators.
+ *
+ * @author Philippe Liard
+ */
+public class CopyrightNotice {
+
+ private static final String TEXT_OPENING =
+ "/*\n";
+
+ private static final String TEXT_OPENING_FOR_JAVASCRIPT =
+ "/**\n" +
+ " * @license\n";
+
+ private static final String TEXT =
+ " * Copyright (C) %d The Libphonenumber Authors\n" +
+ " *\n" +
+ " * Licensed under the Apache License, Version 2.0 (the \"License\");\n" +
+ " * you may not use this file except in compliance with the License.\n" +
+ " * You may obtain a copy of the License at\n" +
+ " *\n" +
+ " * http://www.apache.org/licenses/LICENSE-2.0\n" +
+ " *\n" +
+ " * Unless required by applicable law or agreed to in writing, software\n" +
+ " * distributed under the License is distributed on an \"AS IS\" BASIS,\n" +
+ " * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n" +
+ " * See the License for the specific language governing permissions and\n" +
+ " * limitations under the License.\n" +
+ " */\n\n";
+
+ static final void writeTo(Writer writer, int year) throws IOException {
+ writeTo(writer, year, false);
+ }
+
+ static final void writeTo(Writer writer, int year, boolean isJavascript) throws IOException {
+ if (isJavascript) {
+ writer.write(TEXT_OPENING_FOR_JAVASCRIPT);
+ } else {
+ writer.write(TEXT_OPENING);
+ }
+ Formatter formatter = new Formatter(writer);
+ formatter.format(TEXT, year);
+ }
+}
diff --git a/tools/java/common/src/com/google/i18n/phonenumbers/FileUtils.java b/tools/java/common/src/com/google/i18n/phonenumbers/FileUtils.java
new file mode 100644
index 0000000..41a11ee
--- /dev/null
+++ b/tools/java/common/src/com/google/i18n/phonenumbers/FileUtils.java
@@ -0,0 +1,52 @@
+/*
+ * Copyright (C) 2011 The Libphonenumber Authors
+ *
+ * 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.
+ * under the License.
+ */
+
+package com.google.i18n.phonenumbers;
+
+import java.io.Closeable;
+import java.io.IOException;
+
+/**
+ * Helper class containing methods designed to ease file manipulation and generation.
+ *
+ * @author Philippe Liard
+ */
+public class FileUtils {
+ /**
+ * Silently closes a resource (i.e: don't throw any exception).
+ */
+ private static void close(Closeable closeable) {
+ if (closeable == null) {
+ return;
+ }
+ try {
+ closeable.close();
+ } catch (IOException e) {
+ System.err.println(e.getMessage());
+ }
+ }
+
+ /**
+ * Silently closes multiple resources. This method doesn't throw any exception when an error
+ * occurs when a resource is being closed.
+ */
+ public static void closeFiles(Closeable ... closeables) {
+ for (Closeable closeable : closeables) {
+ close(closeable);
+ }
+ }
+}
diff --git a/tools/java/common/src/com/google/i18n/phonenumbers/MetadataFilter.java b/tools/java/common/src/com/google/i18n/phonenumbers/MetadataFilter.java
new file mode 100644
index 0000000..a783025
--- /dev/null
+++ b/tools/java/common/src/com/google/i18n/phonenumbers/MetadataFilter.java
@@ -0,0 +1,356 @@
+/*
+ * Copyright (C) 2016 The Libphonenumber Authors
+ *
+ * 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.google.i18n.phonenumbers;
+
+import com.google.i18n.phonenumbers.Phonemetadata.PhoneMetadata;
+import com.google.i18n.phonenumbers.Phonemetadata.PhoneNumberDesc;
+import java.util.Arrays;
+import java.util.TreeMap;
+import java.util.TreeSet;
+
+/**
+ * Class to encapsulate the metadata filtering logic and restrict visibility into raw data
+ * structures.
+ *
+ *
+ * WARNING: This is an internal API which is under development and subject to backwards-incompatible
+ * changes without notice. Any changes are not guaranteed to be reflected in the versioning scheme
+ * of the public API, nor in release notes.
+ */
+final class MetadataFilter {
+ // The following 3 sets comprise all the PhoneMetadata fields as defined at phonemetadata.proto
+ // which may be excluded from customized serializations of the binary metadata. Fields that are
+ // core to the library functionality may not be listed here.
+ // excludableParentFields are PhoneMetadata fields of type PhoneNumberDesc.
+ // excludableChildFields are PhoneNumberDesc fields of primitive type.
+ // excludableChildlessFields are PhoneMetadata fields of primitive type.
+ // Currently we support only one non-primitive type and the depth of the "family tree" is 2,
+ // meaning a field may have only direct descendants, who may not have descendants of their own. If
+ // this changes, the blacklist handling in this class should also change.
+ // @VisibleForTesting
+ static final TreeSet excludableParentFields = new TreeSet(Arrays.asList(
+ "fixedLine",
+ "mobile",
+ "tollFree",
+ "premiumRate",
+ "sharedCost",
+ "personalNumber",
+ "voip",
+ "pager",
+ "uan",
+ "emergency",
+ "voicemail",
+ "shortCode",
+ "standardRate",
+ "carrierSpecific",
+ "smsServices",
+ "noInternationalDialling"));
+
+ // Note: If this set changes, the descHasData implementation must change in PhoneNumberUtil.
+ // The current implementation assumes that all PhoneNumberDesc fields are present here, since it
+ // "clears" a PhoneNumberDesc field by simply clearing all of the fields under it. See the comment
+ // above, about all 3 sets, for more about these fields.
+ // @VisibleForTesting
+ static final TreeSet excludableChildFields = new TreeSet(Arrays.asList(
+ "nationalNumberPattern",
+ "possibleLength",
+ "possibleLengthLocalOnly",
+ "exampleNumber"));
+
+ // @VisibleForTesting
+ static final TreeSet excludableChildlessFields = new TreeSet(Arrays.asList(
+ "preferredInternationalPrefix",
+ "nationalPrefix",
+ "preferredExtnPrefix",
+ "nationalPrefixTransformRule",
+ "sameMobileAndFixedLinePattern",
+ "mainCountryForCode",
+ "mobileNumberPortableRegion"));
+
+ private final TreeMap> blacklist;
+
+ // Note: If changing the blacklist here or the name of the method, update documentation about
+ // affected methods at the same time:
+ // https://github.com/google/libphonenumber/blob/master/FAQ.md#what-is-the-metadatalitejsmetadata_lite-option
+ static MetadataFilter forLiteBuild() {
+ // "exampleNumber" is a blacklist.
+ return new MetadataFilter(parseFieldMapFromString("exampleNumber"));
+ }
+
+ static MetadataFilter forSpecialBuild() {
+ // "mobile" is a whitelist.
+ return new MetadataFilter(computeComplement(parseFieldMapFromString("mobile")));
+ }
+
+ static MetadataFilter emptyFilter() {
+ // Empty blacklist, meaning we filter nothing.
+ return new MetadataFilter(new TreeMap>());
+ }
+
+ // @VisibleForTesting
+ MetadataFilter(TreeMap> blacklist) {
+ this.blacklist = blacklist;
+ }
+
+ @Override
+ public boolean equals(Object obj) {
+ return blacklist.equals(((MetadataFilter) obj).blacklist);
+ }
+
+ @Override
+ public int hashCode() {
+ return blacklist.hashCode();
+ }
+
+ /**
+ * Clears certain fields in {@code metadata} as defined by the {@code MetadataFilter} instance.
+ * Note that this changes the mutable {@code metadata} object, and is not thread-safe. If this
+ * method does not return successfully, do not assume {@code metadata} has not changed.
+ *
+ * @param metadata The {@code PhoneMetadata} object to be filtered
+ */
+ void filterMetadata(PhoneMetadata.Builder metadata) {
+ // TODO: Consider clearing if the filtered PhoneNumberDesc is empty.
+ if (metadata.hasFixedLine()) {
+ metadata.setFixedLine(getFiltered("fixedLine", metadata.getFixedLine()));
+ }
+ if (metadata.hasMobile()) {
+ metadata.setMobile(getFiltered("mobile", metadata.getMobile()));
+ }
+ if (metadata.hasTollFree()) {
+ metadata.setTollFree(getFiltered("tollFree", metadata.getTollFree()));
+ }
+ if (metadata.hasPremiumRate()) {
+ metadata.setPremiumRate(getFiltered("premiumRate", metadata.getPremiumRate()));
+ }
+ if (metadata.hasSharedCost()) {
+ metadata.setSharedCost(getFiltered("sharedCost", metadata.getSharedCost()));
+ }
+ if (metadata.hasPersonalNumber()) {
+ metadata.setPersonalNumber(getFiltered("personalNumber", metadata.getPersonalNumber()));
+ }
+ if (metadata.hasVoip()) {
+ metadata.setVoip(getFiltered("voip", metadata.getVoip()));
+ }
+ if (metadata.hasPager()) {
+ metadata.setPager(getFiltered("pager", metadata.getPager()));
+ }
+ if (metadata.hasUan()) {
+ metadata.setUan(getFiltered("uan", metadata.getUan()));
+ }
+ if (metadata.hasEmergency()) {
+ metadata.setEmergency(getFiltered("emergency", metadata.getEmergency()));
+ }
+ if (metadata.hasVoicemail()) {
+ metadata.setVoicemail(getFiltered("voicemail", metadata.getVoicemail()));
+ }
+ if (metadata.hasShortCode()) {
+ metadata.setShortCode(getFiltered("shortCode", metadata.getShortCode()));
+ }
+ if (metadata.hasStandardRate()) {
+ metadata.setStandardRate(getFiltered("standardRate", metadata.getStandardRate()));
+ }
+ if (metadata.hasCarrierSpecific()) {
+ metadata.setCarrierSpecific(getFiltered("carrierSpecific", metadata.getCarrierSpecific()));
+ }
+ if (metadata.hasSmsServices()) {
+ metadata.setSmsServices(getFiltered("smsServices", metadata.getSmsServices()));
+ }
+ if (metadata.hasNoInternationalDialling()) {
+ metadata.setNoInternationalDialling(getFiltered("noInternationalDialling",
+ metadata.getNoInternationalDialling()));
+ }
+
+ if (shouldDrop("preferredInternationalPrefix")) {
+ metadata.clearPreferredInternationalPrefix();
+ }
+ if (shouldDrop("nationalPrefix")) {
+ metadata.clearNationalPrefix();
+ }
+ if (shouldDrop("preferredExtnPrefix")) {
+ metadata.clearPreferredExtnPrefix();
+ }
+ if (shouldDrop("nationalPrefixTransformRule")) {
+ metadata.clearNationalPrefixTransformRule();
+ }
+ if (shouldDrop("sameMobileAndFixedLinePattern")) {
+ metadata.clearSameMobileAndFixedLinePattern();
+ }
+ if (shouldDrop("mainCountryForCode")) {
+ metadata.clearMainCountryForCode();
+ }
+ if (shouldDrop("mobileNumberPortableRegion")) {
+ metadata.clearMobileNumberPortableRegion();
+ }
+ }
+
+ /**
+ * The input blacklist or whitelist string is expected to be of the form "a(b,c):d(e):f", where
+ * b and c are children of a, e is a child of d, and f is either a parent field, a child field, or
+ * a childless field. Order and whitespace don't matter. We throw RuntimeException for any
+ * duplicates, malformed strings, or strings where field tokens do not correspond to strings in
+ * the sets of excludable fields. We also throw RuntimeException for empty strings since such
+ * strings should be treated as a special case by the flag checking code and not passed here.
+ */
+ // @VisibleForTesting
+ static TreeMap> parseFieldMapFromString(String string) {
+ if (string == null) {
+ throw new RuntimeException("Null string should not be passed to parseFieldMapFromString");
+ }
+ // Remove whitespace.
+ string = string.replaceAll("\\s", "");
+ if (string.isEmpty()) {
+ throw new RuntimeException("Empty string should not be passed to parseFieldMapFromString");
+ }
+
+ TreeMap> fieldMap = new TreeMap>();
+ TreeSet wildcardChildren = new TreeSet();
+ for (String group : string.split(":", -1)) {
+ int leftParenIndex = group.indexOf('(');
+ int rightParenIndex = group.indexOf(')');
+ if (leftParenIndex < 0 && rightParenIndex < 0) {
+ if (excludableParentFields.contains(group)) {
+ if (fieldMap.containsKey(group)) {
+ throw new RuntimeException(group + " given more than once in " + string);
+ }
+ fieldMap.put(group, new TreeSet(excludableChildFields));
+ } else if (excludableChildlessFields.contains(group)) {
+ if (fieldMap.containsKey(group)) {
+ throw new RuntimeException(group + " given more than once in " + string);
+ }
+ fieldMap.put(group, new TreeSet());
+ } else if (excludableChildFields.contains(group)) {
+ if (wildcardChildren.contains(group)) {
+ throw new RuntimeException(group + " given more than once in " + string);
+ }
+ wildcardChildren.add(group);
+ } else {
+ throw new RuntimeException(group + " is not a valid token");
+ }
+ } else if (leftParenIndex > 0 && rightParenIndex == group.length() - 1) {
+ // We don't check for duplicate parentheses or illegal characters since these will be caught
+ // as not being part of valid field tokens.
+ String parent = group.substring(0, leftParenIndex);
+ if (!excludableParentFields.contains(parent)) {
+ throw new RuntimeException(parent + " is not a valid parent token");
+ }
+ if (fieldMap.containsKey(parent)) {
+ throw new RuntimeException(parent + " given more than once in " + string);
+ }
+ TreeSet children = new TreeSet();
+ for (String child : group.substring(leftParenIndex + 1, rightParenIndex).split(",", -1)) {
+ if (!excludableChildFields.contains(child)) {
+ throw new RuntimeException(child + " is not a valid child token");
+ }
+ if (!children.add(child)) {
+ throw new RuntimeException(child + " given more than once in " + group);
+ }
+ }
+ fieldMap.put(parent, children);
+ } else {
+ throw new RuntimeException("Incorrect location of parantheses in " + group);
+ }
+ }
+ for (String wildcardChild : wildcardChildren) {
+ for (String parent : excludableParentFields) {
+ TreeSet children = fieldMap.get(parent);
+ if (children == null) {
+ children = new TreeSet();
+ fieldMap.put(parent, children);
+ }
+ if (!children.add(wildcardChild)
+ && fieldMap.get(parent).size() != excludableChildFields.size()) {
+ // The map already contains parent -> wildcardChild but not all possible children.
+ // So wildcardChild was given explicitly as a child of parent, which is a duplication
+ // since it's also given as a wildcard child.
+ throw new RuntimeException(
+ wildcardChild + " is present by itself so remove it from " + parent + "'s group");
+ }
+ }
+ }
+ return fieldMap;
+ }
+
+ // Does not check that legal tokens are used, assuming that fieldMap is constructed using
+ // parseFieldMapFromString(String) which does check. If fieldMap contains illegal tokens or parent
+ // fields with no children or other unexpected state, the behavior of this function is undefined.
+ // @VisibleForTesting
+ static TreeMap> computeComplement(
+ TreeMap> fieldMap) {
+ TreeMap> complement = new TreeMap>();
+ for (String parent : excludableParentFields) {
+ if (!fieldMap.containsKey(parent)) {
+ complement.put(parent, new TreeSet(excludableChildFields));
+ } else {
+ TreeSet otherChildren = fieldMap.get(parent);
+ // If the other map has all the children for this parent then we don't want to include the
+ // parent as a key.
+ if (otherChildren.size() != excludableChildFields.size()) {
+ TreeSet children = new TreeSet();
+ for (String child : excludableChildFields) {
+ if (!otherChildren.contains(child)) {
+ children.add(child);
+ }
+ }
+ complement.put(parent, children);
+ }
+ }
+ }
+ for (String childlessField : excludableChildlessFields) {
+ if (!fieldMap.containsKey(childlessField)) {
+ complement.put(childlessField, new TreeSet());
+ }
+ }
+ return complement;
+ }
+
+ // @VisibleForTesting
+ boolean shouldDrop(String parent, String child) {
+ if (!excludableParentFields.contains(parent)) {
+ throw new RuntimeException(parent + " is not an excludable parent field");
+ }
+ if (!excludableChildFields.contains(child)) {
+ throw new RuntimeException(child + " is not an excludable child field");
+ }
+ return blacklist.containsKey(parent) && blacklist.get(parent).contains(child);
+ }
+
+ // @VisibleForTesting
+ boolean shouldDrop(String childlessField) {
+ if (!excludableChildlessFields.contains(childlessField)) {
+ throw new RuntimeException(childlessField + " is not an excludable childless field");
+ }
+ return blacklist.containsKey(childlessField);
+ }
+
+ private PhoneNumberDesc getFiltered(String type, PhoneNumberDesc desc) {
+ PhoneNumberDesc.Builder builder = PhoneNumberDesc.newBuilder().mergeFrom(desc);
+ if (shouldDrop(type, "nationalNumberPattern")) {
+ builder.clearNationalNumberPattern();
+ }
+ if (shouldDrop(type, "possibleLength")) {
+ builder.clearPossibleLength();
+ }
+ if (shouldDrop(type, "possibleLengthLocalOnly")) {
+ builder.clearPossibleLengthLocalOnly();
+ }
+ if (shouldDrop(type, "exampleNumber")) {
+ builder.clearExampleNumber();
+ }
+ return builder.build();
+ }
+}
diff --git a/tools/java/common/test/com/google/i18n/phonenumbers/BuildMetadataFromXmlTest.java b/tools/java/common/test/com/google/i18n/phonenumbers/BuildMetadataFromXmlTest.java
new file mode 100644
index 0000000..19a8fb2
--- /dev/null
+++ b/tools/java/common/test/com/google/i18n/phonenumbers/BuildMetadataFromXmlTest.java
@@ -0,0 +1,1119 @@
+/*
+ * Copyright (C) 2011 The Libphonenumber Authors
+ *
+ * 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.google.i18n.phonenumbers;
+
+import com.google.i18n.phonenumbers.Phonemetadata.NumberFormat;
+import com.google.i18n.phonenumbers.Phonemetadata.PhoneMetadata;
+import com.google.i18n.phonenumbers.Phonemetadata.PhoneMetadataCollection;
+import com.google.i18n.phonenumbers.Phonemetadata.PhoneNumberDesc;
+import java.io.IOException;
+import java.io.StringReader;
+import java.util.Arrays;
+import java.util.regex.PatternSyntaxException;
+import javax.xml.parsers.DocumentBuilder;
+import javax.xml.parsers.DocumentBuilderFactory;
+import javax.xml.parsers.ParserConfigurationException;
+import junit.framework.TestCase;
+import org.w3c.dom.Document;
+import org.w3c.dom.Element;
+import org.xml.sax.InputSource;
+import org.xml.sax.SAXException;
+
+/**
+ * Unit tests for BuildMetadataFromXml.java
+ *
+ * @author Philippe Liard
+ */
+public class BuildMetadataFromXmlTest extends TestCase {
+
+ // Helper method that outputs a DOM element from a XML string.
+ private static Element parseXmlString(String xmlString)
+ throws ParserConfigurationException, SAXException, IOException {
+ DocumentBuilderFactory documentBuilderFactory = DocumentBuilderFactory.newInstance();
+ DocumentBuilder documentBuilder = documentBuilderFactory.newDocumentBuilder();
+ InputSource inputSource = new InputSource();
+ inputSource.setCharacterStream(new StringReader(xmlString));
+ return documentBuilder.parse(inputSource).getDocumentElement();
+ }
+
+ // Tests validateRE().
+ public void testValidateRERemovesWhiteSpaces() {
+ String input = " hello world ";
+ // Should remove all the white spaces contained in the provided string.
+ assertEquals("helloworld", BuildMetadataFromXml.validateRE(input, true));
+ // Make sure it only happens when the last parameter is set to true.
+ assertEquals(" hello world ", BuildMetadataFromXml.validateRE(input, false));
+ }
+
+ public void testValidateREThrowsException() {
+ String invalidPattern = "[";
+ // Should throw an exception when an invalid pattern is provided independently of the last
+ // parameter (remove white spaces).
+ try {
+ BuildMetadataFromXml.validateRE(invalidPattern, false);
+ fail();
+ } catch (PatternSyntaxException e) {
+ // Test passed.
+ }
+ try {
+ BuildMetadataFromXml.validateRE(invalidPattern, true);
+ fail();
+ } catch (PatternSyntaxException e) {
+ // Test passed.
+ }
+ // We don't allow | to be followed by ) because it introduces bugs, since we typically use it at
+ // the end of each line and when a line is deleted, if the pipe from the previous line is not
+ // removed, we end up erroneously accepting an empty group as well.
+ String patternWithPipeFollowedByClosingParentheses = "|)";
+ try {
+ BuildMetadataFromXml.validateRE(patternWithPipeFollowedByClosingParentheses, true);
+ fail();
+ } catch (PatternSyntaxException e) {
+ // Test passed.
+ }
+ String patternWithPipeFollowedByNewLineAndClosingParentheses = "|\n)";
+ try {
+ BuildMetadataFromXml.validateRE(patternWithPipeFollowedByNewLineAndClosingParentheses, true);
+ fail();
+ } catch (PatternSyntaxException e) {
+ // Test passed.
+ }
+ }
+
+ public void testValidateRE() {
+ String validPattern = "[a-zA-Z]d{1,9}";
+ // The provided pattern should be left unchanged.
+ assertEquals(validPattern, BuildMetadataFromXml.validateRE(validPattern, false));
+ }
+
+ // Tests getNationalPrefix().
+ public void testGetNationalPrefix()
+ throws ParserConfigurationException, SAXException, IOException {
+ String xmlInput = "";
+ Element territoryElement = parseXmlString(xmlInput);
+ assertEquals("00", BuildMetadataFromXml.getNationalPrefix(territoryElement));
+ }
+
+ // Tests loadTerritoryTagMetadata().
+ public void testLoadTerritoryTagMetadata()
+ throws ParserConfigurationException, SAXException, IOException {
+ String xmlInput = ""
+ + "";
+ Element territoryElement = parseXmlString(xmlInput);
+ PhoneMetadata.Builder phoneMetadata =
+ BuildMetadataFromXml.loadTerritoryTagMetadata("33", territoryElement, "0");
+ assertEquals(33, phoneMetadata.getCountryCode());
+ assertEquals("2", phoneMetadata.getLeadingDigits());
+ assertEquals("00", phoneMetadata.getInternationalPrefix());
+ assertEquals("00~11", phoneMetadata.getPreferredInternationalPrefix());
+ assertEquals("0", phoneMetadata.getNationalPrefixForParsing());
+ assertEquals("9$1", phoneMetadata.getNationalPrefixTransformRule());
+ assertEquals("0", phoneMetadata.getNationalPrefix());
+ assertEquals(" x", phoneMetadata.getPreferredExtnPrefix());
+ assertTrue(phoneMetadata.getMainCountryForCode());
+ assertTrue(phoneMetadata.isMobileNumberPortableRegion());
+ }
+
+ public void testLoadTerritoryTagMetadataSetsBooleanFieldsToFalseByDefault()
+ throws ParserConfigurationException, SAXException, IOException {
+ String xmlInput = "";
+ Element territoryElement = parseXmlString(xmlInput);
+ PhoneMetadata.Builder phoneMetadata =
+ BuildMetadataFromXml.loadTerritoryTagMetadata("33", territoryElement, "");
+ assertFalse(phoneMetadata.getMainCountryForCode());
+ assertFalse(phoneMetadata.isMobileNumberPortableRegion());
+ }
+
+ public void testLoadTerritoryTagMetadataSetsNationalPrefixForParsingByDefault()
+ throws ParserConfigurationException, SAXException, IOException {
+ String xmlInput = "";
+ Element territoryElement = parseXmlString(xmlInput);
+ PhoneMetadata.Builder phoneMetadata =
+ BuildMetadataFromXml.loadTerritoryTagMetadata("33", territoryElement, "00");
+ // When unspecified, nationalPrefixForParsing defaults to nationalPrefix.
+ assertEquals("00", phoneMetadata.getNationalPrefix());
+ assertEquals(phoneMetadata.getNationalPrefix(), phoneMetadata.getNationalPrefixForParsing());
+ }
+
+ public void testLoadTerritoryTagMetadataWithRequiredAttributesOnly()
+ throws ParserConfigurationException, SAXException, IOException {
+ String xmlInput = "";
+ Element territoryElement = parseXmlString(xmlInput);
+ // Should not throw any exception.
+ BuildMetadataFromXml.loadTerritoryTagMetadata("33", territoryElement, "");
+ }
+
+ // Tests loadInternationalFormat().
+ public void testLoadInternationalFormat()
+ throws ParserConfigurationException, SAXException, IOException {
+ String intlFormat = "$1 $2";
+ String xmlInput = "" + intlFormat + "";
+ Element numberFormatElement = parseXmlString(xmlInput);
+ PhoneMetadata.Builder metadata = PhoneMetadata.newBuilder();
+ NumberFormat nationalFormat = NumberFormat.newBuilder().build();
+
+ assertTrue(BuildMetadataFromXml.loadInternationalFormat(metadata, numberFormatElement,
+ nationalFormat));
+ assertEquals(intlFormat, metadata.getIntlNumberFormat(0).getFormat());
+ }
+
+ public void testLoadInternationalFormatWithBothNationalAndIntlFormatsDefined()
+ throws ParserConfigurationException, SAXException, IOException {
+ String intlFormat = "$1 $2";
+ String xmlInput = "" + intlFormat + "";
+ Element numberFormatElement = parseXmlString(xmlInput);
+ PhoneMetadata.Builder metadata = PhoneMetadata.newBuilder();
+ NumberFormat.Builder nationalFormat = NumberFormat.newBuilder();
+ nationalFormat.setFormat("$1");
+
+ assertTrue(BuildMetadataFromXml.loadInternationalFormat(metadata, numberFormatElement,
+ nationalFormat.build()));
+ assertEquals(intlFormat, metadata.getIntlNumberFormat(0).getFormat());
+ }
+
+ public void testLoadInternationalFormatExpectsOnlyOnePattern()
+ throws ParserConfigurationException, SAXException, IOException {
+ String xmlInput = "";
+ Element numberFormatElement = parseXmlString(xmlInput);
+ PhoneMetadata.Builder metadata = PhoneMetadata.newBuilder();
+
+ // Should throw an exception as multiple intlFormats are provided.
+ try {
+ BuildMetadataFromXml.loadInternationalFormat(metadata, numberFormatElement,
+ NumberFormat.newBuilder().build());
+ fail();
+ } catch (RuntimeException e) {
+ // Test passed.
+ }
+ }
+
+ public void testLoadInternationalFormatUsesNationalFormatByDefault()
+ throws ParserConfigurationException, SAXException, IOException {
+ String xmlInput = "";
+ Element numberFormatElement = parseXmlString(xmlInput);
+ PhoneMetadata.Builder metadata = PhoneMetadata.newBuilder();
+ NumberFormat.Builder nationalFormat = NumberFormat.newBuilder();
+ String nationalPattern = "$1 $2 $3";
+ nationalFormat.setFormat(nationalPattern);
+
+ assertFalse(BuildMetadataFromXml.loadInternationalFormat(metadata, numberFormatElement,
+ nationalFormat.build()));
+ assertEquals(nationalPattern, metadata.getIntlNumberFormat(0).getFormat());
+ }
+
+ public void testLoadInternationalFormatCopiesNationalFormatData()
+ throws ParserConfigurationException, SAXException, IOException {
+ String xmlInput = "";
+ Element numberFormatElement = parseXmlString(xmlInput);
+ PhoneMetadata.Builder metadata = PhoneMetadata.newBuilder();
+ NumberFormat.Builder nationalFormat = NumberFormat.newBuilder();
+ nationalFormat.setFormat("$1-$2");
+ nationalFormat.setNationalPrefixOptionalWhenFormatting(true);
+
+ assertFalse(BuildMetadataFromXml.loadInternationalFormat(metadata, numberFormatElement,
+ nationalFormat.build()));
+ assertTrue(metadata.getIntlNumberFormat(0).getNationalPrefixOptionalWhenFormatting());
+ }
+
+ public void testLoadNationalFormat()
+ throws ParserConfigurationException, SAXException, IOException {
+ String nationalFormat = "$1 $2";
+ String xmlInput = String.format("%s",
+ nationalFormat);
+ Element numberFormatElement = parseXmlString(xmlInput);
+ PhoneMetadata.Builder metadata = PhoneMetadata.newBuilder();
+ NumberFormat.Builder numberFormat = NumberFormat.newBuilder();
+ BuildMetadataFromXml.loadNationalFormat(metadata, numberFormatElement, numberFormat);
+ assertEquals(nationalFormat, numberFormat.getFormat());
+ }
+
+ public void testLoadNationalFormatRequiresFormat()
+ throws ParserConfigurationException, SAXException, IOException {
+ String xmlInput = "";
+ Element numberFormatElement = parseXmlString(xmlInput);
+ PhoneMetadata.Builder metadata = PhoneMetadata.newBuilder();
+ NumberFormat.Builder numberFormat = NumberFormat.newBuilder();
+
+ try {
+ BuildMetadataFromXml.loadNationalFormat(metadata, numberFormatElement, numberFormat);
+ fail();
+ } catch (RuntimeException e) {
+ // Test passed.
+ }
+ }
+
+ public void testLoadNationalFormatExpectsExactlyOneFormat()
+ throws ParserConfigurationException, SAXException, IOException {
+ String xmlInput = "";
+ Element numberFormatElement = parseXmlString(xmlInput);
+ PhoneMetadata.Builder metadata = PhoneMetadata.newBuilder();
+ NumberFormat.Builder numberFormat = NumberFormat.newBuilder();
+
+ try {
+ BuildMetadataFromXml.loadNationalFormat(metadata, numberFormatElement, numberFormat);
+ fail();
+ } catch (RuntimeException e) {
+ // Test passed.
+ }
+ }
+
+ // Tests loadAvailableFormats().
+ public void testLoadAvailableFormats()
+ throws ParserConfigurationException, SAXException, IOException {
+ String xmlInput = ""
+ + " "
+ + " "
+ + " $1 $2 $3"
+ + " "
+ + " "
+ + "";
+ Element element = parseXmlString(xmlInput);
+ PhoneMetadata.Builder metadata = PhoneMetadata.newBuilder();
+ BuildMetadataFromXml.loadAvailableFormats(
+ metadata, element, "0", "", false /* NP not optional */);
+ assertEquals("($1)", metadata.getNumberFormat(0).getNationalPrefixFormattingRule());
+ assertEquals("0 $CC ($1)", metadata.getNumberFormat(0).getDomesticCarrierCodeFormattingRule());
+ assertEquals("$1 $2 $3", metadata.getNumberFormat(0).getFormat());
+ }
+
+ public void testLoadAvailableFormatsPropagatesCarrierCodeFormattingRule()
+ throws ParserConfigurationException, SAXException, IOException {
+ String xmlInput =
+ ""
+ + " "
+ + " "
+ + " $1 $2 $3"
+ + " "
+ + " "
+ + "";
+ Element element = parseXmlString(xmlInput);
+ PhoneMetadata.Builder metadata = PhoneMetadata.newBuilder();
+ BuildMetadataFromXml.loadAvailableFormats(
+ metadata, element, "0", "", false /* NP not optional */);
+ assertEquals("($1)", metadata.getNumberFormat(0).getNationalPrefixFormattingRule());
+ assertEquals("0 $CC ($1)", metadata.getNumberFormat(0).getDomesticCarrierCodeFormattingRule());
+ assertEquals("$1 $2 $3", metadata.getNumberFormat(0).getFormat());
+ }
+
+ public void testLoadAvailableFormatsSetsProvidedNationalPrefixFormattingRule()
+ throws ParserConfigurationException, SAXException, IOException {
+ String xmlInput = ""
+ + " "
+ + " $1 $2 $3"
+ + " "
+ + "";
+ Element element = parseXmlString(xmlInput);
+ PhoneMetadata.Builder metadata = PhoneMetadata.newBuilder();
+ BuildMetadataFromXml.loadAvailableFormats(
+ metadata, element, "", "($1)", false /* NP not optional */);
+ assertEquals("($1)", metadata.getNumberFormat(0).getNationalPrefixFormattingRule());
+ }
+
+ public void testLoadAvailableFormatsClearsIntlFormat()
+ throws ParserConfigurationException, SAXException, IOException {
+ String xmlInput = ""
+ + " "
+ + " $1 $2 $3"
+ + " "
+ + "";
+ Element element = parseXmlString(xmlInput);
+ PhoneMetadata.Builder metadata = PhoneMetadata.newBuilder();
+ BuildMetadataFromXml.loadAvailableFormats(
+ metadata, element, "0", "($1)", false /* NP not optional */);
+ assertEquals(0, metadata.getIntlNumberFormatCount());
+ }
+
+ public void testLoadAvailableFormatsHandlesMultipleNumberFormats()
+ throws ParserConfigurationException, SAXException, IOException {
+ String xmlInput = ""
+ + " "
+ + " $1 $2 $3"
+ + " $1-$2"
+ + " "
+ + "";
+ Element element = parseXmlString(xmlInput);
+ PhoneMetadata.Builder metadata = PhoneMetadata.newBuilder();
+ BuildMetadataFromXml.loadAvailableFormats(
+ metadata, element, "0", "($1)", false /* NP not optional */);
+ assertEquals("$1 $2 $3", metadata.getNumberFormat(0).getFormat());
+ assertEquals("$1-$2", metadata.getNumberFormat(1).getFormat());
+ }
+
+ public void testLoadInternationalFormatDoesNotSetIntlFormatWhenNA()
+ throws ParserConfigurationException, SAXException, IOException {
+ String xmlInput = "NA";
+ Element numberFormatElement = parseXmlString(xmlInput);
+ PhoneMetadata.Builder metadata = PhoneMetadata.newBuilder();
+ NumberFormat.Builder nationalFormat = NumberFormat.newBuilder();
+ nationalFormat.setFormat("$1 $2");
+
+ BuildMetadataFromXml.loadInternationalFormat(metadata, numberFormatElement,
+ nationalFormat.build());
+ assertEquals(0, metadata.getIntlNumberFormatCount());
+ }
+
+ // Tests setLeadingDigitsPatterns().
+ public void testSetLeadingDigitsPatterns()
+ throws ParserConfigurationException, SAXException, IOException {
+ String xmlInput = ""
+ + "12"
+ + "";
+ Element numberFormatElement = parseXmlString(xmlInput);
+ NumberFormat.Builder numberFormat = NumberFormat.newBuilder();
+ BuildMetadataFromXml.setLeadingDigitsPatterns(numberFormatElement, numberFormat);
+
+ assertEquals("1", numberFormat.getLeadingDigitsPattern(0));
+ assertEquals("2", numberFormat.getLeadingDigitsPattern(1));
+ }
+
+ // Tests setLeadingDigitsPatterns() in the case of international and national formatting rules
+ // being present but not both defined for this numberFormat - we don't want to add them twice.
+ public void testSetLeadingDigitsPatternsNotAddedTwiceWhenInternationalFormatsPresent()
+ throws ParserConfigurationException, SAXException, IOException {
+ String xmlInput = ""
+ + " "
+ + " 1"
+ + " $1"
+ + " "
+ + " "
+ + " 2"
+ + " $1"
+ + " 9-$1"
+ + " "
+ + "";
+ Element element = parseXmlString(xmlInput);
+ PhoneMetadata.Builder metadata = PhoneMetadata.newBuilder();
+ BuildMetadataFromXml.loadAvailableFormats(
+ metadata, element, "0", "", false /* NP not optional */);
+ assertEquals(1, metadata.getNumberFormat(0).leadingDigitsPatternSize());
+ assertEquals(1, metadata.getNumberFormat(1).leadingDigitsPatternSize());
+ // When we merge the national format rules into the international format rules, we shouldn't add
+ // the leading digit patterns multiple times.
+ assertEquals(1, metadata.getIntlNumberFormat(0).leadingDigitsPatternSize());
+ assertEquals(1, metadata.getIntlNumberFormat(1).leadingDigitsPatternSize());
+ }
+
+ // Tests getNationalPrefixFormattingRuleFromElement().
+ public void testGetNationalPrefixFormattingRuleFromElement()
+ throws ParserConfigurationException, SAXException, IOException {
+ String xmlInput = "";
+ Element element = parseXmlString(xmlInput);
+ assertEquals("0$1",
+ BuildMetadataFromXml.getNationalPrefixFormattingRuleFromElement(element, "0"));
+ }
+
+ // Tests getDomesticCarrierCodeFormattingRuleFromElement().
+ public void testGetDomesticCarrierCodeFormattingRuleFromElement()
+ throws ParserConfigurationException, SAXException, IOException {
+ String xmlInput = "";
+ Element element = parseXmlString(xmlInput);
+ assertEquals("0$CC $1",
+ BuildMetadataFromXml.getDomesticCarrierCodeFormattingRuleFromElement(element,
+ "0"));
+ }
+
+ // Tests processPhoneNumberDescElement().
+ public void testProcessPhoneNumberDescElementWithInvalidInput()
+ throws ParserConfigurationException, SAXException, IOException {
+ PhoneNumberDesc.Builder generalDesc = PhoneNumberDesc.newBuilder();
+ Element territoryElement = parseXmlString("");
+ PhoneNumberDesc.Builder phoneNumberDesc;
+
+ phoneNumberDesc = BuildMetadataFromXml.processPhoneNumberDescElement(
+ generalDesc, territoryElement, "invalidType");
+ assertFalse(phoneNumberDesc.hasNationalNumberPattern());
+ }
+
+ public void testProcessPhoneNumberDescElementOverridesGeneralDesc()
+ throws ParserConfigurationException, SAXException, IOException {
+ PhoneNumberDesc.Builder generalDesc = PhoneNumberDesc.newBuilder();
+ generalDesc.setNationalNumberPattern("\\d{8}");
+ String xmlInput = ""
+ + " \\d{6}"
+ + "";
+ Element territoryElement = parseXmlString(xmlInput);
+ PhoneNumberDesc.Builder phoneNumberDesc;
+
+ phoneNumberDesc = BuildMetadataFromXml.processPhoneNumberDescElement(
+ generalDesc, territoryElement, "fixedLine");
+ assertEquals("\\d{6}", phoneNumberDesc.getNationalNumberPattern());
+ }
+
+ public void testBuildPhoneMetadataCollection_liteBuild() throws Exception {
+ String xmlInput =
+ ""
+ + " "
+ + " "
+ + " "
+ + " [1-9]\\d{7}"
+ + " "
+ + " "
+ + " [1-9]\\d{7}"
+ + " "
+ + " 10123456"
+ + " "
+ + " "
+ + " [1-9]\\d{7}"
+ + " "
+ + " 10123456"
+ + " "
+ + " "
+ + " "
+ + "";
+ Document document = parseXmlString(xmlInput).getOwnerDocument();
+
+ PhoneMetadataCollection metadataCollection = BuildMetadataFromXml.buildPhoneMetadataCollection(
+ document,
+ true, // liteBuild
+ false, // specialBuild
+ false, // isShortNumberMetadata
+ false); // isAlternateFormatsMetadata
+
+ assertTrue(metadataCollection.getMetadataCount() == 1);
+ PhoneMetadata metadata = metadataCollection.getMetadataList().get(0);
+ assertTrue(metadata.hasGeneralDesc());
+ assertFalse(metadata.getGeneralDesc().hasExampleNumber());
+ // Some Phonemetadata.java implementations may have custom logic, so we ensure this
+ // implementation is doing the right thing by checking the value of the example number even when
+ // hasExampleNumber is false.
+ assertEquals("", metadata.getGeneralDesc().getExampleNumber());
+ assertTrue(metadata.hasFixedLine());
+ assertFalse(metadata.getFixedLine().hasExampleNumber());
+ assertEquals("", metadata.getFixedLine().getExampleNumber());
+ assertTrue(metadata.hasMobile());
+ assertFalse(metadata.getMobile().hasExampleNumber());
+ assertEquals("", metadata.getMobile().getExampleNumber());
+ }
+
+ public void testBuildPhoneMetadataCollection_specialBuild() throws Exception {
+ String xmlInput =
+ ""
+ + " "
+ + " "
+ + " "
+ + " [1-9]\\d{7}"
+ + " "
+ + " "
+ + " [1-9]\\d{7}"
+ + " "
+ + " 10123456"
+ + " "
+ + " "
+ + " [1-9]\\d{7}"
+ + " "
+ + " 10123456"
+ + " "
+ + " "
+ + " "
+ + "";
+ Document document = parseXmlString(xmlInput).getOwnerDocument();
+
+ PhoneMetadataCollection metadataCollection = BuildMetadataFromXml.buildPhoneMetadataCollection(
+ document,
+ false, // liteBuild
+ true, // specialBuild
+ false, // isShortNumberMetadata
+ false); // isAlternateFormatsMetadata
+
+ assertTrue(metadataCollection.getMetadataCount() == 1);
+ PhoneMetadata metadata = metadataCollection.getMetadataList().get(0);
+ assertTrue(metadata.hasGeneralDesc());
+ assertFalse(metadata.getGeneralDesc().hasExampleNumber());
+ // Some Phonemetadata.java implementations may have custom logic, so we ensure this
+ // implementation is doing the right thing by checking the value of the example number even when
+ // hasExampleNumber is false.
+ assertEquals("", metadata.getGeneralDesc().getExampleNumber());
+ // TODO: Consider clearing fixed-line if empty after being filtered.
+ assertTrue(metadata.hasFixedLine());
+ assertFalse(metadata.getFixedLine().hasExampleNumber());
+ assertEquals("", metadata.getFixedLine().getExampleNumber());
+ assertTrue(metadata.hasMobile());
+ assertTrue(metadata.getMobile().hasExampleNumber());
+ assertEquals("10123456", metadata.getMobile().getExampleNumber());
+ }
+
+ public void testBuildPhoneMetadataCollection_fullBuild() throws Exception {
+ String xmlInput =
+ ""
+ + " "
+ + " "
+ + " "
+ + " [1-9]\\d{7}"
+ + " "
+ + " "
+ + " [1-9]\\d{7}"
+ + " "
+ + " 10123456"
+ + " "
+ + " "
+ + " [1-9]\\d{7}"
+ + " "
+ + " 10123456"
+ + " "
+ + " "
+ + " "
+ + "";
+ Document document = parseXmlString(xmlInput).getOwnerDocument();
+
+ PhoneMetadataCollection metadataCollection = BuildMetadataFromXml.buildPhoneMetadataCollection(
+ document,
+ false, // liteBuild
+ false, // specialBuild
+ false, // isShortNumberMetadata
+ false); // isAlternateFormatsMetadata
+
+ assertTrue(metadataCollection.getMetadataCount() == 1);
+ PhoneMetadata metadata = metadataCollection.getMetadataList().get(0);
+ assertTrue(metadata.hasGeneralDesc());
+ assertFalse(metadata.getGeneralDesc().hasExampleNumber());
+ // Some Phonemetadata.java implementations may have custom logic, so we ensure this
+ // implementation is doing the right thing by checking the value of the example number even when
+ // hasExampleNumber is false.
+ assertEquals("", metadata.getGeneralDesc().getExampleNumber());
+ assertTrue(metadata.hasFixedLine());
+ assertTrue(metadata.getFixedLine().hasExampleNumber());
+ assertEquals("10123456", metadata.getFixedLine().getExampleNumber());
+ assertTrue(metadata.hasMobile());
+ assertTrue(metadata.getMobile().hasExampleNumber());
+ assertEquals("10123456", metadata.getMobile().getExampleNumber());
+ }
+
+ public void testProcessPhoneNumberDescOutputsExampleNumberByDefault()
+ throws ParserConfigurationException, SAXException, IOException {
+ PhoneNumberDesc.Builder generalDesc = PhoneNumberDesc.newBuilder();
+ String xmlInput = ""
+ + " 01 01 01 01"
+ + "";
+ Element territoryElement = parseXmlString(xmlInput);
+ PhoneNumberDesc.Builder phoneNumberDesc;
+
+ phoneNumberDesc = BuildMetadataFromXml.processPhoneNumberDescElement(
+ generalDesc, territoryElement, "fixedLine");
+ assertEquals("01 01 01 01", phoneNumberDesc.getExampleNumber());
+ }
+
+ public void testProcessPhoneNumberDescRemovesWhiteSpacesInPatterns()
+ throws ParserConfigurationException, SAXException, IOException {
+ PhoneNumberDesc.Builder generalDesc = PhoneNumberDesc.newBuilder();
+ String xmlInput = ""
+ + " \t \\d { 6 } "
+ + "";
+ Element countryElement = parseXmlString(xmlInput);
+ PhoneNumberDesc.Builder phoneNumberDesc = BuildMetadataFromXml.processPhoneNumberDescElement(
+ generalDesc, countryElement, "fixedLine");
+ assertEquals("\\d{6}", phoneNumberDesc.getNationalNumberPattern());
+ }
+
+ // Tests setRelevantDescPatterns().
+ public void testSetRelevantDescPatternsSetsSameMobileAndFixedLinePattern()
+ throws ParserConfigurationException, SAXException, IOException {
+ String xmlInput = ""
+ + " \\d{6}"
+ + " \\d{6}"
+ + "";
+ Element territoryElement = parseXmlString(xmlInput);
+ PhoneMetadata.Builder metadata = PhoneMetadata.newBuilder();
+ // Should set sameMobileAndFixedPattern to true.
+ BuildMetadataFromXml.setRelevantDescPatterns(metadata, territoryElement,
+ false /* isShortNumberMetadata */);
+ assertTrue(metadata.getSameMobileAndFixedLinePattern());
+ }
+
+ public void testSetRelevantDescPatternsSetsAllDescriptionsForRegularLengthNumbers()
+ throws ParserConfigurationException, SAXException, IOException {
+ String xmlInput = ""
+ + " \\d{1}"
+ + " \\d{2}"
+ + " \\d{3}"
+ + " \\d{4}"
+ + " \\d{5}"
+ + " \\d{6}"
+ + " \\d{7}"
+ + " \\d{8}"
+ + " \\d{9}"
+ + "";
+ Element territoryElement = parseXmlString(xmlInput);
+ PhoneMetadata.Builder metadata = PhoneMetadata.newBuilder();
+ BuildMetadataFromXml.setRelevantDescPatterns(metadata, territoryElement,
+ false /* isShortNumberMetadata */);
+ assertEquals("\\d{1}", metadata.getFixedLine().getNationalNumberPattern());
+ assertEquals("\\d{2}", metadata.getMobile().getNationalNumberPattern());
+ assertEquals("\\d{3}", metadata.getPager().getNationalNumberPattern());
+ assertEquals("\\d{4}", metadata.getTollFree().getNationalNumberPattern());
+ assertEquals("\\d{5}", metadata.getPremiumRate().getNationalNumberPattern());
+ assertEquals("\\d{6}", metadata.getSharedCost().getNationalNumberPattern());
+ assertEquals("\\d{7}", metadata.getPersonalNumber().getNationalNumberPattern());
+ assertEquals("\\d{8}", metadata.getVoip().getNationalNumberPattern());
+ assertEquals("\\d{9}", metadata.getUan().getNationalNumberPattern());
+ }
+
+ public void testSetRelevantDescPatternsSetsAllDescriptionsForShortNumbers()
+ throws ParserConfigurationException, SAXException, IOException {
+ String xmlInput = ""
+ + " \\d{1}"
+ + " \\d{2}"
+ + " \\d{3}"
+ + " \\d{4}"
+ + " "
+ + " \\d{5}"
+ + " "
+ + " "
+ + " \\d{6}"
+ + " "
+ + "";
+ Element territoryElement = parseXmlString(xmlInput);
+ PhoneMetadata.Builder metadata = PhoneMetadata.newBuilder();
+ BuildMetadataFromXml.setRelevantDescPatterns(metadata, territoryElement,
+ true /* isShortNumberMetadata */);
+ assertEquals("\\d{1}", metadata.getTollFree().getNationalNumberPattern());
+ assertEquals("\\d{2}", metadata.getStandardRate().getNationalNumberPattern());
+ assertEquals("\\d{3}", metadata.getPremiumRate().getNationalNumberPattern());
+ assertEquals("\\d{4}", metadata.getShortCode().getNationalNumberPattern());
+ assertEquals("\\d{5}", metadata.getCarrierSpecific().getNationalNumberPattern());
+ assertEquals("\\d{6}", metadata.getSmsServices().getNationalNumberPattern());
+ }
+
+ public void testSetRelevantDescPatternsThrowsErrorIfTypePresentMultipleTimes()
+ throws ParserConfigurationException, SAXException, IOException {
+ String xmlInput = ""
+ + " \\d{6}"
+ + " \\d{6}"
+ + "";
+ Element territoryElement = parseXmlString(xmlInput);
+ PhoneMetadata.Builder metadata = PhoneMetadata.newBuilder();
+ try {
+ BuildMetadataFromXml.setRelevantDescPatterns(metadata, territoryElement,
+ false /* isShortNumberMetadata */);
+ fail("Fixed-line info present twice for France: we should fail.");
+ } catch (RuntimeException expected) {
+ assertEquals("Multiple elements with type fixedLine found.", expected.getMessage());
+ }
+ }
+
+ public void testAlternateFormatsOmitsDescPatterns()
+ throws ParserConfigurationException, SAXException, IOException {
+ String xmlInput = ""
+ + " "
+ + " "
+ + " 1"
+ + " $1"
+ + " "
+ + " "
+ + " \\d{1}"
+ + " \\d{2}"
+ + "";
+ Element territoryElement = parseXmlString(xmlInput);
+ PhoneMetadata metadata = BuildMetadataFromXml.loadCountryMetadata("FR", territoryElement,
+ false /* isShortNumberMetadata */, true /* isAlternateFormatsMetadata */).build();
+ assertEquals("(1)(\\d{3})", metadata.getNumberFormat(0).getPattern());
+ assertEquals("1", metadata.getNumberFormat(0).getLeadingDigitsPattern(0));
+ assertEquals("$1", metadata.getNumberFormat(0).getFormat());
+ assertFalse(metadata.hasFixedLine());
+ assertNull(metadata.getFixedLine());
+ assertFalse(metadata.hasShortCode());
+ assertNull(metadata.getShortCode());
+ }
+
+ public void testNationalPrefixRulesSetCorrectly()
+ throws ParserConfigurationException, SAXException, IOException {
+ String xmlInput = ""
+ + " "
+ + " "
+ + " 1"
+ + " $1"
+ + " "
+ + " "
+ + " 2"
+ + " $1"
+ + " "
+ + " "
+ + " \\d{1}"
+ + "";
+ Element territoryElement = parseXmlString(xmlInput);
+ PhoneMetadata metadata = BuildMetadataFromXml.loadCountryMetadata("FR", territoryElement,
+ false /* isShortNumberMetadata */, true /* isAlternateFormatsMetadata */).build();
+ assertTrue(metadata.getNumberFormat(0).getNationalPrefixOptionalWhenFormatting());
+ // This is inherited from the territory, with $NP replaced by the actual national prefix, and
+ // $FG replaced with $1.
+ assertEquals("0$1", metadata.getNumberFormat(0).getNationalPrefixFormattingRule());
+ // Here it is explicitly set to false.
+ assertFalse(metadata.getNumberFormat(1).getNationalPrefixOptionalWhenFormatting());
+ }
+
+ public void testProcessPhoneNumberDescElement_PossibleLengthsSetCorrectly() throws Exception {
+ PhoneNumberDesc.Builder generalDesc = PhoneNumberDesc.newBuilder();
+ // The number lengths set for the general description must be a super-set of those in the
+ // element being parsed.
+ generalDesc.addPossibleLength(4);
+ generalDesc.addPossibleLength(6);
+ generalDesc.addPossibleLength(7);
+ generalDesc.addPossibleLength(13);
+ Element territoryElement = parseXmlString(""
+ + ""
+ // Sorting will be done when parsing.
+ + " "
+ + ""
+ + "");
+
+ PhoneNumberDesc.Builder fixedLine;
+ PhoneNumberDesc.Builder mobile;
+
+ fixedLine = BuildMetadataFromXml.processPhoneNumberDescElement(
+ generalDesc, territoryElement, "fixedLine");
+ mobile = BuildMetadataFromXml.processPhoneNumberDescElement(
+ generalDesc, territoryElement, "mobile");
+
+ assertEquals(2, fixedLine.getPossibleLengthCount());
+ assertEquals(4, fixedLine.getPossibleLength(0));
+ assertEquals(13, fixedLine.getPossibleLength(1));
+ assertEquals(1, fixedLine.getPossibleLengthLocalOnlyCount());
+
+ // We use [-1] to denote that there are no possible lengths; we don't leave it empty, since for
+ // compression reasons, we use the empty list to mean that the generalDesc possible lengths
+ // apply.
+ assertEquals(1, mobile.getPossibleLengthCount());
+ assertEquals(-1, mobile.getPossibleLength(0));
+ assertEquals(0, mobile.getPossibleLengthLocalOnlyCount());
+ }
+
+ public void testSetPossibleLengthsGeneralDesc_BuiltFromChildElements() throws Exception {
+ Element territoryElement = parseXmlString(""
+ + ""
+ + " "
+ + ""
+ + ""
+ + " "
+ + ""
+ + ""
+ + " "
+ + ""
+ + "");
+ PhoneNumberDesc.Builder generalDesc = PhoneNumberDesc.newBuilder();
+ BuildMetadataFromXml.setPossibleLengthsGeneralDesc(
+ generalDesc, "someId", territoryElement, false /* not short-number metadata */);
+
+ assertEquals(2, generalDesc.getPossibleLengthCount());
+ assertEquals(13, generalDesc.getPossibleLength(0));
+ // 15 is present twice in the input in different sections, but only once in the output.
+ assertEquals(15, generalDesc.getPossibleLength(1));
+ assertEquals(2, generalDesc.getPossibleLengthLocalOnlyCount());
+ assertEquals(6, generalDesc.getPossibleLengthLocalOnly(0));
+ assertEquals(7, generalDesc.getPossibleLengthLocalOnly(1));
+ // 13 is skipped as a "local only" length, since it is also present as a normal length.
+ }
+
+ public void testSetPossibleLengthsGeneralDesc_IgnoresNoIntlDialling() throws Exception {
+ Element territoryElement = parseXmlString(""
+ + ""
+ + " "
+ + ""
+ + ""
+ + " "
+ + ""
+ + "");
+ PhoneNumberDesc.Builder generalDesc = PhoneNumberDesc.newBuilder();
+ BuildMetadataFromXml.setPossibleLengthsGeneralDesc(
+ generalDesc, "someId", territoryElement, false /* not short-number metadata */);
+
+ assertEquals(1, generalDesc.getPossibleLengthCount());
+ assertEquals(13, generalDesc.getPossibleLength(0));
+ // 15 is skipped because noInternationalDialling should not contribute to the general lengths;
+ // it isn't a particular "type" of number per se, it is a property that different types may
+ // have.
+ }
+
+ public void testSetPossibleLengthsGeneralDesc_ShortNumberMetadata() throws Exception {
+ Element territoryElement = parseXmlString(""
+ + ""
+ + " "
+ + ""
+ + ""
+ + " "
+ + ""
+ + ""
+ + " "
+ + ""
+ + ""
+ + " "
+ + ""
+ + "");
+ PhoneNumberDesc.Builder generalDesc = PhoneNumberDesc.newBuilder();
+ BuildMetadataFromXml.setPossibleLengthsGeneralDesc(
+ generalDesc, "someId", territoryElement, true /* short-number metadata */);
+
+ // All elements other than shortCode are ignored when creating the general desc.
+ assertEquals(2, generalDesc.getPossibleLengthCount());
+ assertEquals(6, generalDesc.getPossibleLength(0));
+ assertEquals(13, generalDesc.getPossibleLength(1));
+ }
+
+ public void testSetPossibleLengthsGeneralDesc_ShortNumberMetadataErrorsOnLocalLengths()
+ throws Exception {
+ Element territoryElement = parseXmlString(""
+ + ""
+ + " "
+ + ""
+ + "");
+
+ PhoneNumberDesc.Builder generalDesc = PhoneNumberDesc.newBuilder();
+ try {
+ BuildMetadataFromXml.setPossibleLengthsGeneralDesc(
+ generalDesc, "someId", territoryElement, true /* short-number metadata */);
+ fail();
+ } catch (RuntimeException expected) {
+ // This should be an error, localOnly is not permitted in short-code metadata.
+ assertEquals("Found local-only lengths in short-number metadata", expected.getMessage());
+ }
+ }
+
+ public void testProcessPhoneNumberDescElement_ErrorDuplicates() throws Exception {
+ PhoneNumberDesc.Builder generalDesc = PhoneNumberDesc.newBuilder();
+ generalDesc.addPossibleLength(6);
+
+ Element territoryElement = parseXmlString(""
+ + ""
+ + " "
+ + ""
+ + "");
+
+ try {
+ BuildMetadataFromXml.processPhoneNumberDescElement(generalDesc, territoryElement, "mobile");
+ fail("Invalid data seen: expected failure.");
+ } catch (RuntimeException expected) {
+ // This should be an error, 6 is seen twice.
+ assertEquals("Duplicate length element found (6) in possibleLength string 6,6",
+ expected.getMessage());
+ }
+ }
+
+ public void testProcessPhoneNumberDescElement_ErrorDuplicatesOneLocal() throws Exception {
+ PhoneNumberDesc.Builder generalDesc = PhoneNumberDesc.newBuilder();
+ generalDesc.addPossibleLength(6);
+
+ Element territoryElement = parseXmlString(""
+ + ""
+ + " "
+ + ""
+ + "");
+
+ try {
+ BuildMetadataFromXml.processPhoneNumberDescElement(generalDesc, territoryElement, "mobile");
+ fail("Invalid data seen: expected failure.");
+ } catch (RuntimeException expected) {
+ // This should be an error, 6 is seen twice.
+ assertEquals("Possible length(s) found specified as a normal and local-only length: [6]",
+ expected.getMessage());
+ }
+ }
+
+ public void testProcessPhoneNumberDescElement_ErrorUncoveredLengths() throws Exception {
+ PhoneNumberDesc.Builder generalDesc = PhoneNumberDesc.newBuilder();
+ generalDesc.addPossibleLength(4);
+ Element territoryElement = parseXmlString(""
+ + ""
+ // Sorting will be done when parsing.
+ + " "
+ + ""
+ + "");
+ try {
+ BuildMetadataFromXml.processPhoneNumberDescElement(
+ generalDesc, territoryElement, "noInternationalDialling");
+ fail("Lengths present not covered by the general desc: should fail.");
+ } catch (RuntimeException expected) {
+ // Lengths were present that the general description didn't know about.
+ assertTrue(expected.getMessage().contains("Out-of-range possible length"));
+ }
+ }
+
+ public void testProcessPhoneNumberDescElement_SameAsParent() throws Exception {
+ PhoneNumberDesc.Builder generalDesc = PhoneNumberDesc.newBuilder();
+ // The number lengths set for the general description must be a super-set of those in the
+ // element being parsed.
+ generalDesc.addPossibleLength(4);
+ generalDesc.addPossibleLength(6);
+ generalDesc.addPossibleLength(7);
+ generalDesc.addPossibleLengthLocalOnly(2);
+ Element territoryElement = parseXmlString(""
+ + ""
+ // Sorting will be done when parsing.
+ + " "
+ + ""
+ + "");
+
+ PhoneNumberDesc.Builder phoneNumberDesc = BuildMetadataFromXml.processPhoneNumberDescElement(
+ generalDesc, territoryElement, "fixedLine");
+ // No possible lengths should be present, because they match the general description.
+ assertEquals(0, phoneNumberDesc.getPossibleLengthCount());
+ // Local-only lengths should be present for child elements such as fixed-line.
+ assertEquals(1, phoneNumberDesc.getPossibleLengthLocalOnlyCount());
+ }
+
+ public void testProcessPhoneNumberDescElement_InvalidNumber() throws Exception {
+ PhoneNumberDesc.Builder generalDesc = PhoneNumberDesc.newBuilder();
+ generalDesc.addPossibleLength(4);
+ Element territoryElement = parseXmlString(""
+ + ""
+ + " "
+ + ""
+ + "");
+
+ try {
+ BuildMetadataFromXml.processPhoneNumberDescElement(
+ generalDesc, territoryElement, "fixedLine");
+ fail("4d is not a number.");
+ } catch (NumberFormatException expected) {
+ assertEquals("For input string: \"4d\"", expected.getMessage());
+ }
+ }
+
+ public void testLoadCountryMetadata_GeneralDescHasNumberLengthsSet() throws Exception {
+ Element territoryElement = parseXmlString(""
+ + ""
+ // This shouldn't be set, the possible lengths should be derived for generalDesc.
+ + " "
+ + ""
+ + ""
+ + " "
+ + ""
+ + "");
+
+ try {
+ BuildMetadataFromXml.loadCountryMetadata("FR", territoryElement,
+ false /* isShortNumberMetadata */, false /* isAlternateFormatsMetadata */);
+ fail("Possible lengths explicitly set for generalDesc and should not be: we should fail.");
+ } catch (RuntimeException expected) {
+ assertEquals("Found possible lengths specified at general desc: this should be derived"
+ + " from child elements. Affected country: FR", expected.getMessage());
+ }
+ }
+
+ public void testProcessPhoneNumberDescElement_ErrorEmptyPossibleLengthStringAttribute()
+ throws Exception {
+ PhoneNumberDesc.Builder generalDesc = PhoneNumberDesc.newBuilder();
+ generalDesc.addPossibleLength(4);
+ Element territoryElement = parseXmlString(""
+ + ""
+ + " "
+ + ""
+ + "");
+ try {
+ BuildMetadataFromXml.processPhoneNumberDescElement(
+ generalDesc, territoryElement, "fixedLine");
+ fail("Empty possible length string.");
+ } catch (RuntimeException expected) {
+ assertEquals("Empty possibleLength string found.", expected.getMessage());
+ }
+ }
+
+ public void testProcessPhoneNumberDescElement_ErrorRangeSpecifiedWithComma()
+ throws Exception {
+ PhoneNumberDesc.Builder generalDesc = PhoneNumberDesc.newBuilder();
+ generalDesc.addPossibleLength(4);
+ Element territoryElement = parseXmlString(""
+ + ""
+ + " "
+ + ""
+ + "");
+ try {
+ BuildMetadataFromXml.processPhoneNumberDescElement(
+ generalDesc, territoryElement, "fixedLine");
+ fail("Ranges shouldn't use a comma.");
+ } catch (RuntimeException expected) {
+ assertEquals("Missing end of range character in possible length string [4,7].",
+ expected.getMessage());
+ }
+ }
+
+ public void testProcessPhoneNumberDescElement_ErrorIncompleteRange() throws Exception {
+ PhoneNumberDesc.Builder generalDesc = PhoneNumberDesc.newBuilder();
+ generalDesc.addPossibleLength(4);
+ Element territoryElement = parseXmlString(""
+ + ""
+ + " "
+ + ""
+ + "");
+
+ try {
+ BuildMetadataFromXml.processPhoneNumberDescElement(
+ generalDesc, territoryElement, "fixedLine");
+ fail("Should fail: range incomplete.");
+ } catch (RuntimeException expected) {
+ assertEquals("Missing end of range character in possible length string [4-.",
+ expected.getMessage());
+ }
+ }
+
+ public void testProcessPhoneNumberDescElement_ErrorNoDashInRange() throws Exception {
+ PhoneNumberDesc.Builder generalDesc = PhoneNumberDesc.newBuilder();
+ generalDesc.addPossibleLength(4);
+ Element territoryElement = parseXmlString(""
+ + ""
+ + " "
+ + ""
+ + "");
+
+ try {
+ BuildMetadataFromXml.processPhoneNumberDescElement(
+ generalDesc, territoryElement, "fixedLine");
+ fail("Should fail: range incomplete.");
+ } catch (RuntimeException expected) {
+ assertEquals("Ranges must have exactly one - character: missing for [4:10].",
+ expected.getMessage());
+ }
+ }
+
+ public void testProcessPhoneNumberDescElement_ErrorRangeIsNotFromMinToMax() throws Exception {
+ PhoneNumberDesc.Builder generalDesc = PhoneNumberDesc.newBuilder();
+ generalDesc.addPossibleLength(4);
+ Element territoryElement = parseXmlString(""
+ + ""
+ + " "
+ + ""
+ + "");
+
+ try {
+ BuildMetadataFromXml.processPhoneNumberDescElement(
+ generalDesc, territoryElement, "fixedLine");
+ fail("Should fail: range even.");
+ } catch (RuntimeException expected) {
+ assertEquals("The first number in a range should be two or more digits lower than the second."
+ + " Culprit possibleLength string: [10-10]", expected.getMessage());
+ }
+ }
+
+ public void testGetMetadataFilter() {
+ assertEquals(BuildMetadataFromXml.getMetadataFilter(false, false),
+ MetadataFilter.emptyFilter());
+ assertEquals(BuildMetadataFromXml.getMetadataFilter(true, false),
+ MetadataFilter.forLiteBuild());
+ assertEquals(BuildMetadataFromXml.getMetadataFilter(false, true),
+ MetadataFilter.forSpecialBuild());
+ try {
+ BuildMetadataFromXml.getMetadataFilter(true, true);
+ fail("getMetadataFilter should fail when liteBuild and specialBuild are both set");
+ } catch (RuntimeException e) {
+ // Test passed.
+ }
+ }
+}
diff --git a/tools/java/common/test/com/google/i18n/phonenumbers/MetadataFilterCoverageTest.java b/tools/java/common/test/com/google/i18n/phonenumbers/MetadataFilterCoverageTest.java
new file mode 100644
index 0000000..01a14e5
--- /dev/null
+++ b/tools/java/common/test/com/google/i18n/phonenumbers/MetadataFilterCoverageTest.java
@@ -0,0 +1,93 @@
+/*
+ * Copyright (C) 2016 The Libphonenumber Authors
+ *
+ * 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.google.i18n.phonenumbers;
+
+import java.io.BufferedInputStream;
+import java.io.BufferedReader;
+import java.io.IOException;
+import java.io.InputStreamReader;
+import java.nio.charset.Charset;
+import junit.framework.TestCase;
+
+/**
+ * Tests to ensure that the {@link MetadataFilter} logic over excludable fields cover all applicable
+ * fields.
+ */
+public final class MetadataFilterCoverageTest extends TestCase {
+ private static final String CODE;
+
+ static {
+ try {
+ BufferedReader source = new BufferedReader(new InputStreamReader(new BufferedInputStream(
+ MetadataFilterTest.class.getResourceAsStream(
+ "/com/google/i18n/phonenumbers/MetadataFilter.java")),
+ Charset.forName("UTF-8")));
+ StringBuilder codeBuilder = new StringBuilder();
+ for (String line = source.readLine(); line != null; line = source.readLine()) {
+ codeBuilder.append(line).append("\n");
+ }
+ CODE = codeBuilder.toString();
+ } catch (IOException e) {
+ throw new RuntimeException("MetadataFilter.java resource not set up properly", e);
+ }
+ }
+
+ public void testCoverageOfExcludableParentFields() {
+ for (String field : MetadataFilter.excludableParentFields) {
+ String capitalized = Character.toUpperCase(field.charAt(0)) + field.substring(1);
+ String conditional = String.format("(?s).*if \\(metadata.has%s\\(\\)\\) \\{\\s+"
+ + "metadata.set%s\\(getFiltered\\(\"%s\",\\s+metadata.get%s\\(\\)\\)\\);\\s+\\}.*",
+ capitalized, capitalized, field, capitalized);
+ assertTrue("Code is missing correct conditional for " + field, CODE.matches(conditional));
+ }
+
+ assertEquals(countOccurrencesOf("metadata.has", CODE),
+ MetadataFilter.excludableParentFields.size());
+ }
+
+ public void testCoverageOfExcludableChildFields() {
+ for (String field : MetadataFilter.excludableChildFields) {
+ String capitalized = Character.toUpperCase(field.charAt(0)) + field.substring(1);
+ String conditional = String.format("(?s).*if \\(shouldDrop\\(type, \"%s\"\\)\\) \\{\\s+"
+ + "builder.clear%s\\(\\);\\s+\\}.*", field, capitalized);
+ assertTrue("Code is missing correct conditional for " + field, CODE.matches(conditional));
+ }
+
+ assertEquals(countOccurrencesOf("shouldDrop(type, \"", CODE),
+ MetadataFilter.excludableChildFields.size());
+ }
+
+ public void testCoverageOfExcludableChildlessFields() {
+ for (String field : MetadataFilter.excludableChildlessFields) {
+ String capitalized = Character.toUpperCase(field.charAt(0)) + field.substring(1);
+ String conditional = String.format("(?s).*if \\(shouldDrop\\(\"%s\"\\)\\) \\{\\s+"
+ + "metadata.clear%s\\(\\);\\s+\\}.*", field, capitalized);
+ assertTrue("Code is missing correct conditional for " + field, CODE.matches(conditional));
+ }
+
+ assertEquals(countOccurrencesOf("shouldDrop(\"", CODE),
+ MetadataFilter.excludableChildlessFields.size());
+ }
+
+ private static int countOccurrencesOf(String substring, String string) {
+ int count = 0;
+ for (int i = string.indexOf(substring); i != -1; i = string.indexOf(substring, i + 1)) {
+ count++;
+ }
+ return count;
+ }
+}
diff --git a/tools/java/common/test/com/google/i18n/phonenumbers/MetadataFilterTest.java b/tools/java/common/test/com/google/i18n/phonenumbers/MetadataFilterTest.java
new file mode 100644
index 0000000..6bd18cf
--- /dev/null
+++ b/tools/java/common/test/com/google/i18n/phonenumbers/MetadataFilterTest.java
@@ -0,0 +1,1000 @@
+/*
+ * Copyright (C) 2016 The Libphonenumber Authors
+ *
+ * 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.google.i18n.phonenumbers;
+
+import com.google.i18n.phonenumbers.Phonemetadata.PhoneMetadata;
+import com.google.i18n.phonenumbers.Phonemetadata.PhoneNumberDesc;
+import java.util.Arrays;
+import java.util.List;
+import java.util.TreeMap;
+import java.util.TreeSet;
+import junit.framework.TestCase;
+
+/**
+ * Unit tests for {@link MetadataFilter}.
+ */
+public class MetadataFilterTest extends TestCase {
+ private static final String ID = "AM";
+ private static final int COUNTRY_CODE = 374;
+ private static final String INTERNATIONAL_PREFIX = "0[01]";
+ private static final String PREFERRED_INTERNATIONAL_PREFIX = "00";
+ private static final String NATIONAL_NUMBER_PATTERN = "\\d{8}";
+ private static final int[] possibleLengths = {8};
+ private static final int[] possibleLengthsLocalOnly = {5, 6};
+ private static final String EXAMPLE_NUMBER = "10123456";
+
+ // If this behavior changes then consider whether the change in the blacklist is intended, or you
+ // should change the special build configuration. Also look into any change in the size of the
+ // build.
+ public void testForLiteBuild() {
+ TreeMap> blacklist = new TreeMap>();
+ blacklist.put("fixedLine", new TreeSet(Arrays.asList("exampleNumber")));
+ blacklist.put("mobile", new TreeSet(Arrays.asList("exampleNumber")));
+ blacklist.put("tollFree", new TreeSet(Arrays.asList("exampleNumber")));
+ blacklist.put("premiumRate", new TreeSet(Arrays.asList("exampleNumber")));
+ blacklist.put("sharedCost", new TreeSet(Arrays.asList("exampleNumber")));
+ blacklist.put("personalNumber", new TreeSet(Arrays.asList("exampleNumber")));
+ blacklist.put("voip", new TreeSet(Arrays.asList("exampleNumber")));
+ blacklist.put("pager", new TreeSet(Arrays.asList("exampleNumber")));
+ blacklist.put("uan", new TreeSet(Arrays.asList("exampleNumber")));
+ blacklist.put("emergency", new TreeSet(Arrays.asList("exampleNumber")));
+ blacklist.put("voicemail", new TreeSet(Arrays.asList("exampleNumber")));
+ blacklist.put("shortCode", new TreeSet(Arrays.asList("exampleNumber")));
+ blacklist.put("standardRate", new TreeSet(Arrays.asList("exampleNumber")));
+ blacklist.put("carrierSpecific", new TreeSet(Arrays.asList("exampleNumber")));
+ blacklist.put("smsServices", new TreeSet(Arrays.asList("exampleNumber")));
+ blacklist.put("noInternationalDialling",
+ new TreeSet(Arrays.asList("exampleNumber")));
+
+ assertEquals(MetadataFilter.forLiteBuild(), new MetadataFilter(blacklist));
+ }
+
+ // If this behavior changes then consider whether the change in the blacklist is intended, or you
+ // should change the special build configuration. Also look into any change in the size of the
+ // build.
+ public void testForSpecialBuild() {
+ TreeMap> blacklist = new TreeMap>();
+ blacklist.put("fixedLine", new TreeSet(MetadataFilter.excludableChildFields));
+ blacklist.put("tollFree", new TreeSet(MetadataFilter.excludableChildFields));
+ blacklist.put("premiumRate", new TreeSet(MetadataFilter.excludableChildFields));
+ blacklist.put("sharedCost", new TreeSet(MetadataFilter.excludableChildFields));
+ blacklist.put("personalNumber", new TreeSet(MetadataFilter.excludableChildFields));
+ blacklist.put("voip", new TreeSet(MetadataFilter.excludableChildFields));
+ blacklist.put("pager", new TreeSet(MetadataFilter.excludableChildFields));
+ blacklist.put("uan", new TreeSet(MetadataFilter.excludableChildFields));
+ blacklist.put("emergency", new TreeSet(MetadataFilter.excludableChildFields));
+ blacklist.put("voicemail", new TreeSet(MetadataFilter.excludableChildFields));
+ blacklist.put("shortCode", new TreeSet(MetadataFilter.excludableChildFields));
+ blacklist.put("standardRate", new TreeSet(MetadataFilter.excludableChildFields));
+ blacklist.put("carrierSpecific", new TreeSet(MetadataFilter.excludableChildFields));
+ blacklist.put("smsServices", new TreeSet(MetadataFilter.excludableChildFields));
+ blacklist.put("noInternationalDialling",
+ new TreeSet(MetadataFilter.excludableChildFields));
+ blacklist.put("preferredInternationalPrefix", new TreeSet());
+ blacklist.put("nationalPrefix", new TreeSet());
+ blacklist.put("preferredExtnPrefix", new TreeSet());
+ blacklist.put("nationalPrefixTransformRule", new TreeSet());
+ blacklist.put("sameMobileAndFixedLinePattern", new TreeSet());
+ blacklist.put("mainCountryForCode", new TreeSet());
+ blacklist.put("mobileNumberPortableRegion", new TreeSet());
+
+ assertEquals(MetadataFilter.forSpecialBuild(), new MetadataFilter(blacklist));
+ }
+
+ public void testEmptyFilter() {
+ assertEquals(MetadataFilter.emptyFilter(),
+ new MetadataFilter(new TreeMap>()));
+ }
+
+ public void testParseFieldMapFromString_parentAsGroup() {
+ TreeMap> fieldMap = new TreeMap>();
+ fieldMap.put("fixedLine", new TreeSet(Arrays.asList(
+ "nationalNumberPattern", "possibleLength", "possibleLengthLocalOnly", "exampleNumber")));
+
+ assertEquals(MetadataFilter.parseFieldMapFromString("fixedLine"), fieldMap);
+ }
+
+ public void testParseFieldMapFromString_childAsGroup() {
+ TreeMap> fieldMap = new TreeMap>();
+ fieldMap.put("fixedLine", new TreeSet(Arrays.asList("exampleNumber")));
+ fieldMap.put("mobile", new TreeSet(Arrays.asList("exampleNumber")));
+ fieldMap.put("tollFree", new TreeSet(Arrays.asList("exampleNumber")));
+ fieldMap.put("premiumRate", new TreeSet(Arrays.asList("exampleNumber")));
+ fieldMap.put("sharedCost", new TreeSet(Arrays.asList("exampleNumber")));
+ fieldMap.put("personalNumber", new TreeSet(Arrays.asList("exampleNumber")));
+ fieldMap.put("voip", new TreeSet(Arrays.asList("exampleNumber")));
+ fieldMap.put("pager", new TreeSet(Arrays.asList("exampleNumber")));
+ fieldMap.put("uan", new TreeSet(Arrays.asList("exampleNumber")));
+ fieldMap.put("emergency", new TreeSet(Arrays.asList("exampleNumber")));
+ fieldMap.put("voicemail", new TreeSet(Arrays.asList("exampleNumber")));
+ fieldMap.put("shortCode", new TreeSet(Arrays.asList("exampleNumber")));
+ fieldMap.put("standardRate", new TreeSet(Arrays.asList("exampleNumber")));
+ fieldMap.put("carrierSpecific", new TreeSet(Arrays.asList("exampleNumber")));
+ fieldMap.put("smsServices", new TreeSet(Arrays.asList("exampleNumber")));
+ fieldMap.put("noInternationalDialling", new TreeSet(Arrays.asList("exampleNumber")));
+
+ assertEquals(MetadataFilter.parseFieldMapFromString("exampleNumber"), fieldMap);
+ }
+
+ public void testParseFieldMapFromString_childlessFieldAsGroup() {
+ TreeMap> fieldMap = new TreeMap>();
+ fieldMap.put("nationalPrefix", new TreeSet());
+
+ assertEquals(MetadataFilter.parseFieldMapFromString("nationalPrefix"), fieldMap);
+ }
+
+ public void testParseFieldMapFromString_parentWithOneChildAsGroup() {
+ TreeMap> fieldMap = new TreeMap>();
+ fieldMap.put("fixedLine", new TreeSet(Arrays.asList("exampleNumber")));
+
+ assertEquals(MetadataFilter.parseFieldMapFromString("fixedLine(exampleNumber)"), fieldMap);
+ }
+
+ public void testParseFieldMapFromString_parentWithTwoChildrenAsGroup() {
+ TreeMap> fieldMap = new TreeMap>();
+ fieldMap.put("fixedLine", new TreeSet(Arrays.asList(
+ "exampleNumber", "possibleLength")));
+
+ assertEquals(
+ MetadataFilter.parseFieldMapFromString("fixedLine(exampleNumber,possibleLength)"),
+ fieldMap);
+ }
+
+ public void testParseFieldMapFromString_mixOfGroups() {
+ TreeMap> fieldMap = new TreeMap>();
+ fieldMap.put("uan", new TreeSet(Arrays.asList(
+ "possibleLength", "exampleNumber", "possibleLengthLocalOnly", "nationalNumberPattern")));
+ fieldMap.put("pager", new TreeSet(Arrays.asList(
+ "exampleNumber", "nationalNumberPattern")));
+ fieldMap.put("fixedLine", new TreeSet(Arrays.asList(
+ "nationalNumberPattern", "possibleLength", "possibleLengthLocalOnly", "exampleNumber")));
+ fieldMap.put("nationalPrefix", new TreeSet());
+ fieldMap.put("mobile", new TreeSet(Arrays.asList("nationalNumberPattern")));
+ fieldMap.put("tollFree", new TreeSet(Arrays.asList("nationalNumberPattern")));
+ fieldMap.put("premiumRate", new TreeSet(Arrays.asList("nationalNumberPattern")));
+ fieldMap.put("sharedCost", new TreeSet(Arrays.asList("nationalNumberPattern")));
+ fieldMap.put("personalNumber", new TreeSet(Arrays.asList("nationalNumberPattern")));
+ fieldMap.put("voip", new TreeSet(Arrays.asList("nationalNumberPattern")));
+ fieldMap.put("emergency", new TreeSet(Arrays.asList("nationalNumberPattern")));
+ fieldMap.put("voicemail", new TreeSet(Arrays.asList("nationalNumberPattern")));
+ fieldMap.put("shortCode", new TreeSet(Arrays.asList("nationalNumberPattern")));
+ fieldMap.put("standardRate", new TreeSet(Arrays.asList("nationalNumberPattern")));
+ fieldMap.put("carrierSpecific", new TreeSet(Arrays.asList("nationalNumberPattern")));
+ fieldMap.put("smsServices", new TreeSet(Arrays.asList("nationalNumberPattern")));
+ fieldMap.put("noInternationalDialling", new TreeSet(Arrays.asList(
+ "nationalNumberPattern")));
+
+ assertEquals(MetadataFilter.parseFieldMapFromString(
+ "uan(possibleLength,exampleNumber,possibleLengthLocalOnly)"
+ + ":pager(exampleNumber)"
+ + ":fixedLine"
+ + ":nationalPrefix"
+ + ":nationalNumberPattern"),
+ fieldMap);
+ }
+
+ // Many of the strings in this test may be possible to express in shorter ways with the current
+ // sets of excludable fields, but their shortest representation changes as those sets change, as
+ // do their semantics; therefore we allow currently longer expressions, and we allow explicit
+ // listing of children, even if these are currently all the children.
+ public void testParseFieldMapFromString_equivalentExpressions() {
+ // Listing all excludable parent fields is equivalent to listing all excludable child fields.
+ assertEquals(
+ MetadataFilter.parseFieldMapFromString(
+ "fixedLine"
+ + ":mobile"
+ + ":tollFree"
+ + ":premiumRate"
+ + ":sharedCost"
+ + ":personalNumber"
+ + ":voip"
+ + ":pager"
+ + ":uan"
+ + ":emergency"
+ + ":voicemail"
+ + ":shortCode"
+ + ":standardRate"
+ + ":carrierSpecific"
+ + ":smsServices"
+ + ":noInternationalDialling"),
+ MetadataFilter.parseFieldMapFromString(
+ "nationalNumberPattern"
+ + ":possibleLength"
+ + ":possibleLengthLocalOnly"
+ + ":exampleNumber"));
+
+ // Order and whitespace don't matter.
+ assertEquals(
+ MetadataFilter.parseFieldMapFromString(
+ " nationalNumberPattern "
+ + ": uan ( exampleNumber , possibleLengthLocalOnly, possibleLength ) "
+ + ": nationalPrefix "
+ + ": fixedLine "
+ + ": pager ( exampleNumber ) "),
+ MetadataFilter.parseFieldMapFromString(
+ "uan(possibleLength,exampleNumber,possibleLengthLocalOnly)"
+ + ":pager(exampleNumber)"
+ + ":fixedLine"
+ + ":nationalPrefix"
+ + ":nationalNumberPattern"));
+
+ // Parent explicitly listing all possible children.
+ assertEquals(
+ MetadataFilter.parseFieldMapFromString(
+ "uan(nationalNumberPattern,possibleLength,exampleNumber,possibleLengthLocalOnly)"),
+ MetadataFilter.parseFieldMapFromString("uan"));
+
+ // All parent's children covered, some implicitly and some explicitly.
+ assertEquals(
+ MetadataFilter.parseFieldMapFromString(
+ "uan(nationalNumberPattern,possibleLength,exampleNumber):possibleLengthLocalOnly"),
+ MetadataFilter.parseFieldMapFromString("uan:possibleLengthLocalOnly"));
+
+ // Child field covered by all parents explicitly.
+ // It seems this will always be better expressed as a wildcard child, but the check is complex
+ // and may not be worth it.
+ assertEquals(
+ MetadataFilter.parseFieldMapFromString(
+ "fixedLine(exampleNumber)"
+ + ":mobile(exampleNumber)"
+ + ":tollFree(exampleNumber)"
+ + ":premiumRate(exampleNumber)"
+ + ":sharedCost(exampleNumber)"
+ + ":personalNumber(exampleNumber)"
+ + ":voip(exampleNumber)"
+ + ":pager(exampleNumber)"
+ + ":uan(exampleNumber)"
+ + ":emergency(exampleNumber)"
+ + ":voicemail(exampleNumber)"
+ + ":shortCode(exampleNumber)"
+ + ":standardRate(exampleNumber)"
+ + ":carrierSpecific(exampleNumber)"
+ + ":smsServices(exampleNumber)"
+ + ":noInternationalDialling(exampleNumber)"),
+ MetadataFilter.parseFieldMapFromString("exampleNumber"));
+
+ // Child field given as a group by itself while it's covered by all parents implicitly.
+ // It seems this will always be better expressed without the wildcard child, but the check is
+ // complex and may not be worth it.
+ assertEquals(
+ MetadataFilter.parseFieldMapFromString(
+ "fixedLine"
+ + ":mobile"
+ + ":tollFree"
+ + ":premiumRate"
+ + ":sharedCost"
+ + ":personalNumber"
+ + ":voip"
+ + ":pager"
+ + ":uan"
+ + ":emergency"
+ + ":voicemail"
+ + ":shortCode"
+ + ":standardRate"
+ + ":carrierSpecific"
+ + ":smsServices"
+ + ":noInternationalDialling"
+ + ":exampleNumber"),
+ MetadataFilter.parseFieldMapFromString(
+ "fixedLine"
+ + ":mobile"
+ + ":tollFree"
+ + ":premiumRate"
+ + ":sharedCost"
+ + ":personalNumber"
+ + ":voip"
+ + ":pager"
+ + ":uan"
+ + ":emergency"
+ + ":voicemail"
+ + ":shortCode"
+ + ":standardRate"
+ + ":carrierSpecific"
+ + ":smsServices"
+ + ":noInternationalDialling"));
+ }
+
+ public void testParseFieldMapFromString_RuntimeExceptionCases() {
+ // Null input.
+ try {
+ MetadataFilter.parseFieldMapFromString(null);
+ fail();
+ } catch (RuntimeException e) {
+ // Test passed.
+ }
+
+ // Empty input.
+ try {
+ MetadataFilter.parseFieldMapFromString("");
+ fail();
+ } catch (RuntimeException e) {
+ // Test passed.
+ }
+
+ // Whitespace input.
+ try {
+ MetadataFilter.parseFieldMapFromString(" ");
+ fail();
+ } catch (RuntimeException e) {
+ // Test passed.
+ }
+
+ // Bad token given as only group.
+ try {
+ MetadataFilter.parseFieldMapFromString("something_else");
+ fail();
+ } catch (RuntimeException e) {
+ // Test passed.
+ }
+
+ // Bad token given as last group.
+ try {
+ MetadataFilter.parseFieldMapFromString("fixedLine:something_else");
+ fail();
+ } catch (RuntimeException e) {
+ // Test passed.
+ }
+
+ // Bad token given as middle group.
+ try {
+ MetadataFilter.parseFieldMapFromString(
+ "pager:nationalPrefix:something_else:nationalNumberPattern");
+ fail();
+ } catch (RuntimeException e) {
+ // Test passed.
+ }
+
+ // Childless field given as parent.
+ try {
+ MetadataFilter.parseFieldMapFromString("nationalPrefix(exampleNumber)");
+ fail();
+ } catch (RuntimeException e) {
+ // Test passed.
+ }
+
+ // Child field given as parent.
+ try {
+ MetadataFilter.parseFieldMapFromString("possibleLength(exampleNumber)");
+ fail();
+ } catch (RuntimeException e) {
+ // Test passed.
+ }
+
+ // Bad token given as parent.
+ try {
+ MetadataFilter.parseFieldMapFromString("something_else(exampleNumber)");
+ fail();
+ } catch (RuntimeException e) {
+ // Test passed.
+ }
+
+ // Parent field given as only child.
+ try {
+ MetadataFilter.parseFieldMapFromString("fixedLine(uan)");
+ fail();
+ } catch (RuntimeException e) {
+ // Test passed.
+ }
+
+ // Parent field given as first child.
+ try {
+ MetadataFilter.parseFieldMapFromString("fixedLine(uan,possibleLength)");
+ fail();
+ } catch (RuntimeException e) {
+ // Test passed.
+ }
+
+ // Parent field given as last child.
+ try {
+ MetadataFilter.parseFieldMapFromString("fixedLine(possibleLength,uan)");
+ fail();
+ } catch (RuntimeException e) {
+ // Test passed.
+ }
+
+ // Parent field given as middle child.
+ try {
+ MetadataFilter.parseFieldMapFromString("fixedLine(possibleLength,uan,exampleNumber)");
+ fail();
+ } catch (RuntimeException e) {
+ // Test passed.
+ }
+
+ // Childless field given as only child.
+ try {
+ MetadataFilter.parseFieldMapFromString("fixedLine(nationalPrefix)");
+ fail();
+ } catch (RuntimeException e) {
+ // Test passed.
+ }
+
+ // Bad token given as only child.
+ try {
+ MetadataFilter.parseFieldMapFromString("fixedLine(something_else)");
+ fail();
+ } catch (RuntimeException e) {
+ // Test passed.
+ }
+
+ // Bad token given as last child.
+ try {
+ MetadataFilter.parseFieldMapFromString("uan(possibleLength,something_else)");
+ fail();
+ } catch (RuntimeException e) {
+ // Test passed.
+ }
+
+ // Empty parent.
+ try {
+ MetadataFilter.parseFieldMapFromString("(exampleNumber)");
+ fail();
+ } catch (RuntimeException e) {
+ // Test passed.
+ }
+
+ // Whitespace parent.
+ try {
+ MetadataFilter.parseFieldMapFromString(" (exampleNumber)");
+ fail();
+ } catch (RuntimeException e) {
+ // Test passed.
+ }
+
+ // Empty child.
+ try {
+ MetadataFilter.parseFieldMapFromString("fixedLine()");
+ fail();
+ } catch (RuntimeException e) {
+ // Test passed.
+ }
+
+ // Whitespace child.
+ try {
+ MetadataFilter.parseFieldMapFromString("fixedLine( )");
+ fail();
+ } catch (RuntimeException e) {
+ // Test passed.
+ }
+
+ // Empty parent and child.
+ try {
+ MetadataFilter.parseFieldMapFromString("()");
+ fail();
+ } catch (RuntimeException e) {
+ // Test passed.
+ }
+
+ // Whitespace parent and empty child.
+ try {
+ MetadataFilter.parseFieldMapFromString(" ()");
+ fail();
+ } catch (RuntimeException e) {
+ // Test passed.
+ }
+
+ // Parent field given as a group twice.
+ try {
+ MetadataFilter.parseFieldMapFromString("fixedLine:uan:fixedLine");
+ fail();
+ } catch (RuntimeException e) {
+ // Test passed.
+ }
+
+ // Parent field given as the parent of a group and as a group by itself.
+ try {
+ MetadataFilter.parseFieldMapFromString("fixedLine(exampleNumber):fixedLine");
+ fail();
+ } catch (RuntimeException e) {
+ // Test passed.
+ }
+
+ // Parent field given as the parent of one group and then as the parent of another group.
+ try {
+ MetadataFilter.parseFieldMapFromString("fixedLine(exampleNumber):fixedLine(possibleLength)");
+ fail();
+ } catch (RuntimeException e) {
+ // Test passed.
+ }
+
+ // Childless field given twice as a group.
+ try {
+ MetadataFilter.parseFieldMapFromString("nationalPrefix:uan:nationalPrefix");
+ fail();
+ } catch (RuntimeException e) {
+ // Test passed.
+ }
+
+ // Child field given twice as a group.
+ try {
+ MetadataFilter.parseFieldMapFromString("exampleNumber:uan:exampleNumber");
+ fail();
+ } catch (RuntimeException e) {
+ // Test passed.
+ }
+
+ // Child field given first as the only child in a group and then as a group by itself.
+ try {
+ MetadataFilter.parseFieldMapFromString("fixedLine(exampleNumber):exampleNumber");
+ fail();
+ } catch (RuntimeException e) {
+ // Test passed.
+ }
+
+ // Child field given first as a child in a group and then as a group by itself.
+ try {
+ MetadataFilter.parseFieldMapFromString(
+ "uan(nationalNumberPattern,possibleLength,exampleNumber)"
+ + ":possibleLengthLocalOnly"
+ + ":exampleNumber");
+ fail();
+ } catch (RuntimeException e) {
+ // Test passed.
+ }
+
+ // Child field given twice as children of the same parent.
+ try {
+ MetadataFilter.parseFieldMapFromString(
+ "fixedLine(possibleLength,exampleNumber,possibleLength)");
+ fail();
+ } catch (RuntimeException e) {
+ // Test passed.
+ }
+
+ // Child field given as a group by itself while it's covered by all parents explicitly.
+ try {
+ MetadataFilter.parseFieldMapFromString(
+ "fixedLine(exampleNumber)"
+ + ":mobile(exampleNumber)"
+ + ":tollFree(exampleNumber)"
+ + ":premiumRate(exampleNumber)"
+ + ":sharedCost(exampleNumber)"
+ + ":personalNumber(exampleNumber)"
+ + ":voip(exampleNumber)"
+ + ":pager(exampleNumber)"
+ + ":uan(exampleNumber)"
+ + ":emergency(exampleNumber)"
+ + ":voicemail(exampleNumber)"
+ + ":shortCode(exampleNumber)"
+ + ":standardRate(exampleNumber)"
+ + ":carrierSpecific(exampleNumber)"
+ + ":noInternationalDialling(exampleNumber)"
+ + ":exampleNumber");
+ fail();
+ } catch (RuntimeException e) {
+ // Test passed.
+ }
+
+ // Child field given as a group by itself while it's covered by all parents, some implicitly and
+ // some explicitly.
+ try {
+ MetadataFilter.parseFieldMapFromString(
+ "fixedLine"
+ + ":mobile"
+ + ":tollFree"
+ + ":premiumRate"
+ + ":sharedCost"
+ + ":personalNumber"
+ + ":voip"
+ + ":pager(exampleNumber)"
+ + ":uan(exampleNumber)"
+ + ":emergency(exampleNumber)"
+ + ":voicemail(exampleNumber)"
+ + ":shortCode(exampleNumber)"
+ + ":standardRate(exampleNumber)"
+ + ":carrierSpecific(exampleNumber)"
+ + ":smsServices"
+ + ":noInternationalDialling(exampleNumber)"
+ + ":exampleNumber");
+ fail();
+ } catch (RuntimeException e) {
+ // Test passed.
+ }
+
+ // Missing right parenthesis in only group.
+ try {
+ MetadataFilter.parseFieldMapFromString("fixedLine(exampleNumber");
+ fail();
+ } catch (RuntimeException e) {
+ // Test passed.
+ }
+
+ // Missing right parenthesis in first group.
+ try {
+ MetadataFilter.parseFieldMapFromString("fixedLine(exampleNumber:pager");
+ fail();
+ } catch (RuntimeException e) {
+ // Test passed.
+ }
+
+ // Missing left parenthesis in only group.
+ try {
+ MetadataFilter.parseFieldMapFromString("fixedLineexampleNumber)");
+ fail();
+ } catch (RuntimeException e) {
+ // Test passed.
+ }
+
+ // Early right parenthesis in only group.
+ try {
+ MetadataFilter.parseFieldMapFromString("fixedLine(example_numb)er");
+ fail();
+ } catch (RuntimeException e) {
+ // Test passed.
+ }
+
+ // Extra right parenthesis at end of only group.
+ try {
+ MetadataFilter.parseFieldMapFromString("fixedLine(exampleNumber))");
+ fail();
+ } catch (RuntimeException e) {
+ // Test passed.
+ }
+
+ // Extra right parenthesis between proper parentheses.
+ try {
+ MetadataFilter.parseFieldMapFromString("fixedLine(example_numb)er)");
+ fail();
+ } catch (RuntimeException e) {
+ // Test passed.
+ }
+
+ // Extra left parenthesis in only group.
+ try {
+ MetadataFilter.parseFieldMapFromString("fixedLine((exampleNumber)");
+ fail();
+ } catch (RuntimeException e) {
+ // Test passed.
+ }
+
+ // Extra level of children.
+ try {
+ MetadataFilter.parseFieldMapFromString("fixedLine(exampleNumber(possibleLength))");
+ fail();
+ } catch (RuntimeException e) {
+ // Test passed.
+ }
+
+ // Trailing comma in children.
+ try {
+ MetadataFilter.parseFieldMapFromString("fixedLine(exampleNumber,)");
+ fail();
+ } catch (RuntimeException e) {
+ // Test passed.
+ }
+
+ // Leading comma in children.
+ try {
+ MetadataFilter.parseFieldMapFromString("fixedLine(,exampleNumber)");
+ fail();
+ } catch (RuntimeException e) {
+ // Test passed.
+ }
+
+ // Empty token between commas.
+ try {
+ MetadataFilter.parseFieldMapFromString("fixedLine(possibleLength,,exampleNumber)");
+ fail();
+ } catch (RuntimeException e) {
+ // Test passed.
+ }
+
+ // Trailing colon.
+ try {
+ MetadataFilter.parseFieldMapFromString("uan:");
+ fail();
+ } catch (RuntimeException e) {
+ // Test passed.
+ }
+
+ // Leading colon.
+ try {
+ MetadataFilter.parseFieldMapFromString(":uan");
+ fail();
+ } catch (RuntimeException e) {
+ // Test passed.
+ }
+
+ // Empty token between colons.
+ try {
+ MetadataFilter.parseFieldMapFromString("uan::fixedLine");
+ fail();
+ } catch (RuntimeException e) {
+ // Test passed.
+ }
+
+ // Missing colon between groups.
+ try {
+ MetadataFilter.parseFieldMapFromString("uan(possibleLength)pager");
+ fail();
+ } catch (RuntimeException e) {
+ // Test passed.
+ }
+ }
+
+ public void testComputeComplement_allAndNothing() {
+ TreeMap> map1 = new TreeMap>();
+ map1.put("fixedLine", new TreeSet(MetadataFilter.excludableChildFields));
+ map1.put("mobile", new TreeSet(MetadataFilter.excludableChildFields));
+ map1.put("tollFree", new TreeSet(MetadataFilter.excludableChildFields));
+ map1.put("premiumRate", new TreeSet(MetadataFilter.excludableChildFields));
+ map1.put("sharedCost", new TreeSet(MetadataFilter.excludableChildFields));
+ map1.put("personalNumber", new TreeSet(MetadataFilter.excludableChildFields));
+ map1.put("voip", new TreeSet(MetadataFilter.excludableChildFields));
+ map1.put("pager", new TreeSet(MetadataFilter.excludableChildFields));
+ map1.put("uan", new TreeSet(MetadataFilter.excludableChildFields));
+ map1.put("emergency", new TreeSet(MetadataFilter.excludableChildFields));
+ map1.put("voicemail", new TreeSet(MetadataFilter.excludableChildFields));
+ map1.put("shortCode", new TreeSet(MetadataFilter.excludableChildFields));
+ map1.put("standardRate", new TreeSet(MetadataFilter.excludableChildFields));
+ map1.put("carrierSpecific", new TreeSet(MetadataFilter.excludableChildFields));
+ map1.put("smsServices", new TreeSet(MetadataFilter.excludableChildFields));
+ map1.put("noInternationalDialling",
+ new TreeSet(MetadataFilter.excludableChildFields));
+ map1.put("preferredInternationalPrefix", new TreeSet());
+ map1.put("nationalPrefix", new TreeSet());
+ map1.put("preferredExtnPrefix", new TreeSet());
+ map1.put("nationalPrefixTransformRule", new TreeSet());
+ map1.put("sameMobileAndFixedLinePattern", new TreeSet());
+ map1.put("mainCountryForCode", new TreeSet());
+ map1.put("mobileNumberPortableRegion", new TreeSet());
+
+ TreeMap> map2 = new TreeMap>();
+
+ assertEquals(MetadataFilter.computeComplement(map1), map2);
+ assertEquals(MetadataFilter.computeComplement(map2), map1);
+ }
+
+ public void testComputeComplement_inBetween() {
+ TreeMap> map1 = new TreeMap>();
+ map1.put("fixedLine", new TreeSet(MetadataFilter.excludableChildFields));
+ map1.put("mobile", new TreeSet(MetadataFilter.excludableChildFields));
+ map1.put("tollFree", new TreeSet(MetadataFilter.excludableChildFields));
+ map1.put("premiumRate", new TreeSet(MetadataFilter.excludableChildFields));
+ map1.put("emergency", new TreeSet(Arrays.asList("nationalNumberPattern")));
+ map1.put("voicemail", new TreeSet(Arrays.asList("possibleLength", "exampleNumber")));
+ map1.put("shortCode", new TreeSet(Arrays.asList("exampleNumber")));
+ map1.put("standardRate", new TreeSet(MetadataFilter.excludableChildFields));
+ map1.put("carrierSpecific", new TreeSet(MetadataFilter.excludableChildFields));
+ map1.put("smsServices", new TreeSet(Arrays.asList("nationalNumberPattern")));
+ map1.put("noInternationalDialling",
+ new TreeSet(MetadataFilter.excludableChildFields));
+ map1.put("nationalPrefixTransformRule", new TreeSet());
+ map1.put("sameMobileAndFixedLinePattern", new TreeSet());
+ map1.put("mainCountryForCode", new TreeSet());
+ map1.put("mobileNumberPortableRegion", new TreeSet());
+
+ TreeMap> map2 = new TreeMap>();
+ map2.put("sharedCost", new TreeSet(MetadataFilter.excludableChildFields));
+ map2.put("personalNumber", new TreeSet(MetadataFilter.excludableChildFields));
+ map2.put("voip", new TreeSet(MetadataFilter.excludableChildFields));
+ map2.put("pager", new TreeSet(MetadataFilter.excludableChildFields));
+ map2.put("uan", new TreeSet(MetadataFilter.excludableChildFields));
+ map2.put("emergency", new TreeSet(Arrays.asList(
+ "possibleLength", "possibleLengthLocalOnly", "exampleNumber")));
+ map2.put("smsServices", new TreeSet(Arrays.asList(
+ "possibleLength", "possibleLengthLocalOnly", "exampleNumber")));
+ map2.put("voicemail", new TreeSet(Arrays.asList(
+ "nationalNumberPattern", "possibleLengthLocalOnly")));
+ map2.put("shortCode", new TreeSet(Arrays.asList(
+ "nationalNumberPattern", "possibleLength", "possibleLengthLocalOnly")));
+ map2.put("preferredInternationalPrefix", new TreeSet());
+ map2.put("nationalPrefix", new TreeSet());
+ map2.put("preferredExtnPrefix", new TreeSet());
+
+ assertEquals(MetadataFilter.computeComplement(map1), map2);
+ assertEquals(MetadataFilter.computeComplement(map2), map1);
+ }
+
+ public void testShouldDrop() {
+ TreeMap> blacklist = new TreeMap>();
+ blacklist.put("fixedLine", new TreeSet(MetadataFilter.excludableChildFields));
+ blacklist.put("mobile", new TreeSet(MetadataFilter.excludableChildFields));
+ blacklist.put("tollFree", new TreeSet(MetadataFilter.excludableChildFields));
+ blacklist.put("premiumRate", new TreeSet(MetadataFilter.excludableChildFields));
+ blacklist.put("emergency", new TreeSet(Arrays.asList("nationalNumberPattern")));
+ blacklist.put("voicemail", new TreeSet(Arrays.asList(
+ "possibleLength", "exampleNumber")));
+ blacklist.put("shortCode", new TreeSet(Arrays.asList("exampleNumber")));
+ blacklist.put("standardRate", new TreeSet(MetadataFilter.excludableChildFields));
+ blacklist.put("carrierSpecific", new TreeSet(MetadataFilter.excludableChildFields));
+ blacklist.put("smsServices", new TreeSet(MetadataFilter.excludableChildFields));
+ blacklist.put("noInternationalDialling",
+ new TreeSet(MetadataFilter.excludableChildFields));
+ blacklist.put("nationalPrefixTransformRule", new TreeSet());
+ blacklist.put("sameMobileAndFixedLinePattern", new TreeSet());
+ blacklist.put("mainCountryForCode", new TreeSet());
+ blacklist.put("mobileNumberPortableRegion", new TreeSet());
+
+ MetadataFilter filter = new MetadataFilter(blacklist);
+ assertTrue(filter.shouldDrop("fixedLine", "exampleNumber"));
+ assertFalse(filter.shouldDrop("sharedCost", "exampleNumber"));
+ assertFalse(filter.shouldDrop("emergency", "exampleNumber"));
+ assertTrue(filter.shouldDrop("emergency", "nationalNumberPattern"));
+ assertFalse(filter.shouldDrop("preferredInternationalPrefix"));
+ assertTrue(filter.shouldDrop("mobileNumberPortableRegion"));
+ assertTrue(filter.shouldDrop("smsServices", "nationalNumberPattern"));
+
+ // Integration tests starting with flag values.
+ assertTrue(BuildMetadataFromXml.getMetadataFilter(true, false)
+ .shouldDrop("fixedLine", "exampleNumber"));
+
+ // Integration tests starting with blacklist strings.
+ assertTrue(new MetadataFilter(MetadataFilter.parseFieldMapFromString("fixedLine"))
+ .shouldDrop("fixedLine", "exampleNumber"));
+ assertFalse(new MetadataFilter(MetadataFilter.parseFieldMapFromString("uan"))
+ .shouldDrop("fixedLine", "exampleNumber"));
+
+ // Integration tests starting with whitelist strings.
+ assertFalse(new MetadataFilter(MetadataFilter.computeComplement(
+ MetadataFilter.parseFieldMapFromString("exampleNumber")))
+ .shouldDrop("fixedLine", "exampleNumber"));
+ assertTrue(new MetadataFilter(MetadataFilter.computeComplement(
+ MetadataFilter.parseFieldMapFromString("uan"))).shouldDrop("fixedLine", "exampleNumber"));
+
+ // Integration tests with an empty blacklist.
+ assertFalse(new MetadataFilter(new TreeMap>())
+ .shouldDrop("fixedLine", "exampleNumber"));
+ }
+
+ // Test that a fake PhoneMetadata filtered for liteBuild ends up clearing exactly the expected
+ // fields. The lite build is used to clear example_number fields from all PhoneNumberDescs.
+ public void testFilterMetadata_liteBuild() {
+ PhoneMetadata.Builder metadata = getFakeArmeniaPhoneMetadata();
+
+ MetadataFilter.forLiteBuild().filterMetadata(metadata);
+
+ // id, country_code, and international_prefix should never be cleared.
+ assertEquals(metadata.getId(), ID);
+ assertEquals(metadata.getCountryCode(), COUNTRY_CODE);
+ assertEquals(metadata.getInternationalPrefix(), INTERNATIONAL_PREFIX);
+
+ // preferred_international_prefix should not be cleared in liteBuild.
+ assertEquals(metadata.getPreferredInternationalPrefix(), PREFERRED_INTERNATIONAL_PREFIX);
+
+ // All PhoneNumberDescs must have only example_number cleared.
+ for (PhoneNumberDesc desc : Arrays.asList(
+ metadata.getGeneralDesc(),
+ metadata.getFixedLine(),
+ metadata.getMobile(),
+ metadata.getTollFree())) {
+ assertEquals(desc.getNationalNumberPattern(), NATIONAL_NUMBER_PATTERN);
+ assertContentsEqual(desc.getPossibleLengthList(), possibleLengths);
+ assertContentsEqual(desc.getPossibleLengthLocalOnlyList(), possibleLengthsLocalOnly);
+ assertFalse(desc.hasExampleNumber());
+ }
+ }
+
+ // Test that a fake PhoneMetadata filtered for specialBuild ends up clearing exactly the expected
+ // fields. The special build is used to clear PhoneNumberDescs other than general_desc and mobile,
+ // and non-PhoneNumberDesc PhoneMetadata fields that aren't needed for parsing.
+ public void testFilterMetadata_specialBuild() {
+ PhoneMetadata.Builder metadata = getFakeArmeniaPhoneMetadata();
+
+ MetadataFilter.forSpecialBuild().filterMetadata(metadata);
+
+ // id, country_code, and international_prefix should never be cleared.
+ assertEquals(metadata.getId(), ID);
+ assertEquals(metadata.getCountryCode(), COUNTRY_CODE);
+ assertEquals(metadata.getInternationalPrefix(), INTERNATIONAL_PREFIX);
+
+ // preferred_international_prefix should be cleared in specialBuild.
+ assertFalse(metadata.hasPreferredInternationalPrefix());
+
+ // general_desc should have all fields but example_number; mobile should have all fields.
+ for (PhoneNumberDesc desc : Arrays.asList(
+ metadata.getGeneralDesc(),
+ metadata.getMobile())) {
+ assertEquals(desc.getNationalNumberPattern(), NATIONAL_NUMBER_PATTERN);
+ assertContentsEqual(desc.getPossibleLengthList(), possibleLengths);
+ assertContentsEqual(desc.getPossibleLengthLocalOnlyList(), possibleLengthsLocalOnly);
+ }
+ assertFalse(metadata.getGeneralDesc().hasExampleNumber());
+ assertEquals(metadata.getMobile().getExampleNumber(), EXAMPLE_NUMBER);
+
+ // All other PhoneNumberDescs must have all fields cleared.
+ for (PhoneNumberDesc desc : Arrays.asList(
+ metadata.getFixedLine(),
+ metadata.getTollFree())) {
+ assertFalse(desc.hasNationalNumberPattern());
+ assertEquals(desc.getPossibleLengthList().size(), 0);
+ assertEquals(desc.getPossibleLengthLocalOnlyList().size(), 0);
+ assertFalse(desc.hasExampleNumber());
+ }
+ }
+
+ // Test that filtering a fake PhoneMetadata with the empty MetadataFilter results in no change.
+ public void testFilterMetadata_emptyFilter() {
+ PhoneMetadata.Builder metadata = getFakeArmeniaPhoneMetadata();
+
+ MetadataFilter.emptyFilter().filterMetadata(metadata);
+
+ // None of the fields should be cleared.
+ assertEquals(metadata.getId(), ID);
+ assertEquals(metadata.getCountryCode(), COUNTRY_CODE);
+ assertEquals(metadata.getInternationalPrefix(), INTERNATIONAL_PREFIX);
+ assertEquals(metadata.getPreferredInternationalPrefix(), PREFERRED_INTERNATIONAL_PREFIX);
+ for (PhoneNumberDesc desc : Arrays.asList(
+ metadata.getGeneralDesc(),
+ metadata.getFixedLine(),
+ metadata.getMobile(),
+ metadata.getTollFree())) {
+ assertEquals(desc.getNationalNumberPattern(), NATIONAL_NUMBER_PATTERN);
+ assertContentsEqual(desc.getPossibleLengthList(), possibleLengths);
+ assertContentsEqual(desc.getPossibleLengthLocalOnlyList(), possibleLengthsLocalOnly);
+ }
+ assertFalse(metadata.getGeneralDesc().hasExampleNumber());
+ assertEquals(metadata.getFixedLine().getExampleNumber(), EXAMPLE_NUMBER);
+ assertEquals(metadata.getMobile().getExampleNumber(), EXAMPLE_NUMBER);
+ assertEquals(metadata.getTollFree().getExampleNumber(), EXAMPLE_NUMBER);
+ }
+
+ public void testIntegrityOfFieldSets() {
+ TreeSet union = new TreeSet();
+ union.addAll(MetadataFilter.excludableParentFields);
+ union.addAll(MetadataFilter.excludableChildFields);
+ union.addAll(MetadataFilter.excludableChildlessFields);
+
+ // Mutually exclusive sets.
+ assertTrue(union.size() == MetadataFilter.excludableParentFields.size()
+ + MetadataFilter.excludableChildFields.size()
+ + MetadataFilter.excludableChildlessFields.size());
+
+ // Nonempty sets.
+ assertTrue(MetadataFilter.excludableParentFields.size() > 0
+ && MetadataFilter.excludableChildFields.size() > 0
+ && MetadataFilter.excludableChildlessFields.size() > 0);
+
+ // Nonempty and canonical field names.
+ for (String field : union) {
+ assertTrue(field.length() > 0 && field.trim().equals(field));
+ }
+ }
+
+ private static PhoneMetadata.Builder getFakeArmeniaPhoneMetadata() {
+ PhoneMetadata.Builder metadata = PhoneMetadata.newBuilder();
+ metadata.setId(ID);
+ metadata.setCountryCode(COUNTRY_CODE);
+ metadata.setInternationalPrefix(INTERNATIONAL_PREFIX);
+ metadata.setPreferredInternationalPrefix(PREFERRED_INTERNATIONAL_PREFIX);
+ metadata.setGeneralDesc(getFakeArmeniaPhoneNumberDesc(true));
+ metadata.setFixedLine(getFakeArmeniaPhoneNumberDesc(false));
+ metadata.setMobile(getFakeArmeniaPhoneNumberDesc(false));
+ metadata.setTollFree(getFakeArmeniaPhoneNumberDesc(false));
+ return metadata;
+ }
+
+ private static PhoneNumberDesc getFakeArmeniaPhoneNumberDesc(boolean generalDesc) {
+ PhoneNumberDesc desc = new PhoneNumberDesc().setNationalNumberPattern(NATIONAL_NUMBER_PATTERN);
+ if (!generalDesc) {
+ desc.setExampleNumber(EXAMPLE_NUMBER);
+ }
+ for (int i : possibleLengths) {
+ desc.addPossibleLength(i);
+ }
+ for (int i : possibleLengthsLocalOnly) {
+ desc.addPossibleLengthLocalOnly(i);
+ }
+ return desc;
+ }
+
+ private static void assertContentsEqual(List list, int[] array) {
+ assertEquals(list.size(), array.length);
+ for (int i = 0; i < list.size(); i++) {
+ assertEquals((int) list.get(i), array[i]);
+ }
+ }
+}
diff --git a/tools/java/pom.xml b/tools/java/pom.xml
new file mode 100644
index 0000000..de73c6d
--- /dev/null
+++ b/tools/java/pom.xml
@@ -0,0 +1,45 @@
+
+
+ 4.0.0
+ com.google.i18n.phonenumbers
+ tools
+ pom
+ 1.0-SNAPSHOT
+ Libphonenumber build tools
+
+
+ UTF-8
+
+
+
+
+ Apache 2
+ http://www.apache.org/licenses/LICENSE-2.0.txt
+ Copyright (C) 2011 The Libphonenumber Authors
+
+
+
+
+
+ default
+
+ true
+
+
+ common
+ rust-build
+
+
+
+ github-actions
+
+ common
+
+ data
+ java-build
+
+
+
+
+
diff --git a/tools/java/rust-build/pom.xml b/tools/java/rust-build/pom.xml
new file mode 100644
index 0000000..86407e0
--- /dev/null
+++ b/tools/java/rust-build/pom.xml
@@ -0,0 +1,161 @@
+
+
+ 4.0.0
+
+
+ tools
+ com.google.i18n.phonenumbers
+ 1.0-SNAPSHOT
+
+
+ com.google.i18n.phonenumbers.tools
+ cpp-build
+ 1.0-SNAPSHOT
+ Libphonenumber C++ build tools
+
+ C++ build tools that download dependencies under base/ from the Chromium source repository, and
+ generate the C++ metadata code needed to build the libphonenumber library.
+
+
+
+ src
+ test
+
+
+ org.apache.maven.plugins
+ maven-compiler-plugin
+ 3.11.0
+
+ 8
+ 8
+ UTF-8
+
+
+
+
+ org.apache.maven.plugins
+ maven-antrun-plugin
+ 1.3
+
+
+ create-generated-directory
+ generate-sources
+
+
+
+
+
+
+ run
+
+
+
+
+
+ org.codehaus.mojo
+ build-helper-maven-plugin
+ 3.4.0
+
+
+ add-source
+ generate-sources
+
+ add-source
+
+
+
+
+ ../common/src/
+
+ generated/
+
+
+
+
+
+
+
+ org.codehaus.mojo
+ exec-maven-plugin
+ 3.1.0
+
+
+ generate-sources
+
+ exec
+
+
+
+
+ protoc
+
+ --java_out=generated
+ ../../../resources/phonemetadata.proto
+ --proto_path=../../../resources
+
+
+
+
+ org.apache.maven.plugins
+ maven-jar-plugin
+ 3.3.0
+
+
+
+ true
+ com.google.i18n.phonenumbers.EntryPoint
+
+
+
+
+
+
+ org.apache.maven.plugins
+ maven-assembly-plugin
+
+
+ jar-with-dependencies
+
+
+
+ true
+ com.google.i18n.phonenumbers.EntryPoint
+
+
+
+
+
+ make-assembly
+ package
+
+ single
+
+
+
+
+
+
+
+
+
+ junit
+ junit
+ 4.13.2
+ test
+
+
+ com.google.protobuf
+ protobuf-java
+ 3.25.5
+
+
+
+
diff --git a/tools/java/rust-build/src/com/google/i18n/phonenumbers/BuildMetadataCppFromXml.java b/tools/java/rust-build/src/com/google/i18n/phonenumbers/BuildMetadataCppFromXml.java
new file mode 100644
index 0000000..ff25165
--- /dev/null
+++ b/tools/java/rust-build/src/com/google/i18n/phonenumbers/BuildMetadataCppFromXml.java
@@ -0,0 +1,220 @@
+/*
+ * Copyright (C) 2011 The Libphonenumber Authors
+ *
+ * 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.google.i18n.phonenumbers;
+
+import com.google.i18n.phonenumbers.CppMetadataGenerator.Type;
+
+import java.io.ByteArrayOutputStream;
+import java.io.File;
+import java.io.FileNotFoundException;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.io.OutputStream;
+import java.io.OutputStreamWriter;
+import java.nio.charset.Charset;
+import java.util.Arrays;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+/**
+ * This class generates the C++ code representation of the provided XML metadata file. It lets us
+ * embed metadata directly in a native binary. We link the object resulting from the compilation of
+ * the code emitted by this class with the C++ phonenumber library.
+ *
+ * @author Philippe Liard
+ * @author David Beaumont
+ */
+public class BuildMetadataCppFromXml extends Command {
+
+ /** An enum encapsulating the variations of metadata that we can produce. */
+ public enum Variant {
+ /** The default 'full' variant which contains all the metadata. */
+ FULL("%s"),
+ /** The test variant which contains fake data for tests. */
+ TEST("test_%s"),
+ /**
+ * The lite variant contains the same metadata as the full version but excludes any example
+ * data. This is typically used for clients with space restrictions.
+ */
+ LITE("lite_%s");
+
+ private final String template;
+
+ private Variant(String template) {
+ this.template = template;
+ }
+
+ /**
+ * Returns the basename of the type by adding the name of the current variant. The basename of
+ * a Type is used to determine the name of the source file in which the metadata is defined.
+ *
+ *
Note that when the variant is {@link Variant#FULL} this method just returns the type name.
+ */
+ public String getBasename(Type type) {
+ return String.format(template, type);
+ }
+
+ /**
+ * Parses metadata variant name. By default (for a name of {@code ""} or {@code null}) we return
+ * {@link Variant#FULL}, otherwise we match against the variant name (either "test" or "lite").
+ */
+ public static Variant parse(String variantName) {
+ if ("test".equalsIgnoreCase(variantName)) {
+ return Variant.TEST;
+ } else if ("lite".equalsIgnoreCase(variantName)) {
+ return Variant.LITE;
+ } else if (variantName == null || variantName.length() == 0) {
+ return Variant.FULL;
+ } else {
+ return null;
+ }
+ }
+ }
+
+ /**
+ * An immutable options class for parsing and representing the command line options for this
+ * command.
+ */
+ // @VisibleForTesting
+ static final class Options {
+ private static final Pattern BASENAME_PATTERN =
+ Pattern.compile("(?:(test|lite)_)?([a-z_]+)");
+
+ public static Options parse(String commandName, String[] args) {
+ if (args.length == 4) {
+ String inputXmlFilePath = args[1];
+ String outputDirPath = args[2];
+ Matcher basenameMatcher = BASENAME_PATTERN.matcher(args[3]);
+ if (basenameMatcher.matches()) {
+ Variant variant = Variant.parse(basenameMatcher.group(1));
+ Type type = Type.parse(basenameMatcher.group(2));
+ if (type != null && variant != null) {
+ return new Options(inputXmlFilePath, outputDirPath, type, variant);
+ }
+ }
+ }
+ throw new IllegalArgumentException(String.format(
+ "Usage: %s ( | test_ | lite_ )\n" +
+ " where is one of: %s",
+ commandName, Arrays.asList(Type.values())));
+ }
+
+ // File path where the XML input can be found.
+ private final String inputXmlFilePath;
+ // Output directory where the generated files will be saved.
+ private final String outputDirPath;
+ private final Type type;
+ private final Variant variant;
+
+ private Options(String inputXmlFilePath, String outputDirPath, Type type, Variant variant) {
+ this.inputXmlFilePath = inputXmlFilePath;
+ this.outputDirPath = outputDirPath;
+ this.type = type;
+ this.variant = variant;
+ }
+
+ public String getInputFilePath() {
+ return inputXmlFilePath;
+ }
+
+ public String getOutputDir() {
+ return outputDirPath;
+ }
+
+ public Type getType() {
+ return type;
+ }
+
+ public Variant getVariant() {
+ return variant;
+ }
+ }
+
+ @Override
+ public String getCommandName() {
+ return "BuildMetadataCppFromXml";
+ }
+
+ /**
+ * Generates C++ header and source files to represent the metadata specified by this command's
+ * arguments. The metadata XML file is read and converted to a byte array before being written
+ * into a C++ source file as a static data array.
+ *
+ * @return true if the generation succeeded.
+ */
+ @Override
+ public boolean start() {
+ try {
+ Options opt = Options.parse(getCommandName(), getArgs());
+ byte[] data = loadMetadataBytes(opt.getInputFilePath(), opt.getVariant() == Variant.LITE);
+ CppMetadataGenerator metadata = CppMetadataGenerator.create(opt.getType(), data);
+
+ // TODO: Consider adding checking for correctness of file paths and access.
+ OutputStream headerStream = null;
+ OutputStream sourceStream = null;
+ try {
+ File dir = new File(opt.getOutputDir());
+ headerStream = openHeaderStream(dir, opt.getType());
+ sourceStream = openSourceStream(dir, opt.getType(), opt.getVariant());
+ metadata.outputHeaderFile(new OutputStreamWriter(headerStream, UTF_8));
+ metadata.outputSourceFile(new OutputStreamWriter(sourceStream, UTF_8));
+ } finally {
+ FileUtils.closeFiles(headerStream, sourceStream);
+ }
+ return true;
+ } catch (IOException e) {
+ System.err.println(e.getMessage());
+ } catch (RuntimeException e) {
+ System.err.println(e.getMessage());
+ }
+ return false;
+ }
+
+ /** Loads the metadata XML file and converts its contents to a byte array. */
+ private byte[] loadMetadataBytes(String inputFilePath, boolean liteMetadata) {
+ ByteArrayOutputStream out = new ByteArrayOutputStream();
+ try {
+ writePhoneMetadataCollection(inputFilePath, liteMetadata, out);
+ } catch (Exception e) {
+ // We cannot recover from any exceptions thrown here, so promote them to runtime exceptions.
+ throw new RuntimeException(e);
+ } finally {
+ FileUtils.closeFiles(out);
+ }
+ return out.toByteArray();
+ }
+
+ // @VisibleForTesting
+ void writePhoneMetadataCollection(
+ String inputFilePath, boolean liteMetadata, OutputStream out) throws IOException, Exception {
+ BuildMetadataFromXml.buildPhoneMetadataCollection(inputFilePath, liteMetadata, false)
+ .writeTo(out);
+ }
+
+ // @VisibleForTesting
+ OutputStream openHeaderStream(File dir, Type type) throws FileNotFoundException {
+ return new FileOutputStream(new File(dir, type + ".h"));
+ }
+
+ // @VisibleForTesting
+ OutputStream openSourceStream(File dir, Type type, Variant variant) throws FileNotFoundException {
+ return new FileOutputStream(new File(dir, variant.getBasename(type) + ".cc"));
+ }
+
+ /** The charset in which our source and header files will be written. */
+ private static final Charset UTF_8 = Charset.forName("UTF-8");
+}
diff --git a/tools/java/rust-build/src/com/google/i18n/phonenumbers/CppMetadataGenerator.java b/tools/java/rust-build/src/com/google/i18n/phonenumbers/CppMetadataGenerator.java
new file mode 100644
index 0000000..22e832c
--- /dev/null
+++ b/tools/java/rust-build/src/com/google/i18n/phonenumbers/CppMetadataGenerator.java
@@ -0,0 +1,197 @@
+/*
+ * Copyright (C) 2012 The Libphonenumber Authors
+ *
+ * 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.google.i18n.phonenumbers;
+
+import java.io.IOException;
+import java.io.PrintWriter;
+import java.io.Writer;
+import java.util.Locale;
+
+/**
+ * Encapsulation of binary metadata created from XML to be included as static data in C++ source
+ * files.
+ *
+ * @author David Beaumont
+ * @author Philippe Liard
+ */
+public final class CppMetadataGenerator {
+
+ /**
+ * The metadata type represents the known types of metadata and includes additional information
+ * such as the copyright year. It is expected that the generated files will be named after the
+ * {@link #toString} of their type.
+ */
+ public enum Type {
+ /** The basic phone number metadata (expected to be written to metadata.[h/cc]). */
+ METADATA("metadata", 2011),
+ /** The alternate format metadata (expected to be written to alternate_format.[h/cc]). */
+ ALTERNATE_FORMAT("alternate_format", 2012),
+ /** Metadata for short numbers (expected to be written to short_metadata.[h/cc]). */
+ SHORT_NUMBERS("short_metadata", 2013);
+
+ private final String typeName;
+ private final int copyrightYear;
+
+ private Type(String typeName, int copyrightYear) {
+ this.typeName = typeName;
+ this.copyrightYear = copyrightYear;
+ }
+
+ /** Returns the year in which this metadata type was first introduced. */
+ public int getCopyrightYear() {
+ return copyrightYear;
+ }
+
+ /**
+ * Returns the name of this type for use in C++ source/header files. Use this in preference to
+ * using {@link #name}.
+ */
+ @Override public String toString() {
+ return typeName;
+ }
+
+ /**
+ * Parses the type from a string case-insensitively.
+ *
+ * @return the matching Type instance or null if not matched.
+ */
+ public static Type parse(String typeName) {
+ if (Type.METADATA.toString().equalsIgnoreCase(typeName)) {
+ return Type.METADATA;
+ } else if (Type.ALTERNATE_FORMAT.toString().equalsIgnoreCase(typeName)) {
+ return Type.ALTERNATE_FORMAT;
+ } else if (Type.SHORT_NUMBERS.toString().equalsIgnoreCase(typeName)) {
+ return Type.SHORT_NUMBERS;
+ } else {
+ return null;
+ }
+ }
+ }
+
+ /**
+ * Creates a metadata instance that can write C++ source and header files to represent this given
+ * byte array as a static unsigned char array. Note that a direct reference to the byte[] is
+ * retained by the newly created CppXmlMetadata instance, so the caller should treat the array as
+ * immutable after making this call.
+ */
+ public static CppMetadataGenerator create(Type type, byte[] data) {
+ return new CppMetadataGenerator(type, data);
+ }
+
+ private final Type type;
+ private final byte[] data;
+ private final String guardName; // e.g. "I18N_PHONENUMBERS__H_"
+ private final String headerInclude; // e.g. "phonenumbers/.h"
+
+ private CppMetadataGenerator(Type type, byte[] data) {
+ this.type = type;
+ this.data = data;
+ this.guardName = createGuardName(type);
+ this.headerInclude = createHeaderInclude(type);
+ }
+
+ /**
+ * Writes the header file for the C++ representation of the metadata to the given writer. Note
+ * that this method does not close the given writer.
+ */
+ public void outputHeaderFile(Writer out) throws IOException {
+ PrintWriter pw = new PrintWriter(out);
+ CopyrightNotice.writeTo(pw, type.getCopyrightYear());
+ pw.println("#ifndef " + guardName);
+ pw.println("#define " + guardName);
+ pw.println();
+ emitNamespaceStart(pw);
+ pw.println();
+ pw.println("int " + type + "_size();");
+ pw.println("const void* " + type + "_get();");
+ pw.println();
+ emitNamespaceEnd(pw);
+ pw.println();
+ pw.println("#endif // " + guardName);
+ pw.flush();
+ }
+
+ /**
+ * Writes the source file for the C++ representation of the metadata, including a static array
+ * containing the data itself, to the given writer. Note that this method does not close the given
+ * writer.
+ */
+ public void outputSourceFile(Writer out) throws IOException {
+ // TODO: Consider outputting a load method to return the parsed proto directly.
+ PrintWriter pw = new PrintWriter(out);
+ CopyrightNotice.writeTo(pw, type.getCopyrightYear());
+ pw.println("#include \"" + headerInclude + "\"");
+ pw.println();
+ emitNamespaceStart(pw);
+ pw.println();
+ pw.println("namespace {");
+ pw.println("static const unsigned char data[] = {");
+ emitStaticArrayData(pw, data);
+ pw.println("};");
+ pw.println("} // namespace");
+ pw.println();
+ pw.println("int " + type + "_size() {");
+ pw.println(" return sizeof(data) / sizeof(data[0]);");
+ pw.println("}");
+ pw.println();
+ pw.println("const void* " + type + "_get() {");
+ pw.println(" return data;");
+ pw.println("}");
+ pw.println();
+ emitNamespaceEnd(pw);
+ pw.flush();
+ }
+
+ private static String createGuardName(Type type) {
+ return String.format("I18N_PHONENUMBERS_%s_H_", type.toString().toUpperCase(Locale.ENGLISH));
+ }
+
+ private static String createHeaderInclude(Type type) {
+ return String.format("phonenumbers/%s.h", type);
+ }
+
+ private static void emitNamespaceStart(PrintWriter pw) {
+ pw.println("namespace i18n {");
+ pw.println("namespace phonenumbers {");
+ }
+
+ private static void emitNamespaceEnd(PrintWriter pw) {
+ pw.println("} // namespace phonenumbers");
+ pw.println("} // namespace i18n");
+ }
+
+ /** Emits the C++ code corresponding to the binary metadata as a static byte array. */
+ // @VisibleForTesting
+ static void emitStaticArrayData(PrintWriter pw, byte[] data) {
+ String separator = " ";
+ for (int i = 0; i < data.length; i++) {
+ pw.print(separator);
+ emitHexByte(pw, data[i]);
+ separator = ((i + 1) % 13 == 0) ? ",\n " : ", ";
+ }
+ pw.println();
+ }
+
+ /** Emits a single byte in the form 0xHH, where H is an upper case hex digit in [0-9A-F]. */
+ private static void emitHexByte(PrintWriter pw, byte v) {
+ pw.print("0x");
+ pw.print(UPPER_HEX[(v & 0xF0) >>> 4]);
+ pw.print(UPPER_HEX[v & 0xF]);
+ }
+
+ private static final char[] UPPER_HEX = "0123456789ABCDEF".toCharArray();
+}
diff --git a/tools/java/rust-build/src/com/google/i18n/phonenumbers/EntryPoint.java b/tools/java/rust-build/src/com/google/i18n/phonenumbers/EntryPoint.java
new file mode 100644
index 0000000..d08184d
--- /dev/null
+++ b/tools/java/rust-build/src/com/google/i18n/phonenumbers/EntryPoint.java
@@ -0,0 +1,33 @@
+/*
+ * Copyright (C) 2011 The Libphonenumber Authors
+ *
+ * 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.google.i18n.phonenumbers;
+
+/**
+ * Entry point class for C++ build tools.
+ *
+ * @author Philippe Liard
+ */
+public class EntryPoint {
+
+ public static void main(String[] args) {
+ boolean status = new CommandDispatcher(args, new Command[] {
+ new BuildMetadataCppFromXml()
+ }).start();
+
+ System.exit(status ? 0 : 1);
+ }
+}
diff --git a/tools/java/rust-build/test/com/google/i18n/phonenumbers/BuildMetadataCppFromXmlTest.java b/tools/java/rust-build/test/com/google/i18n/phonenumbers/BuildMetadataCppFromXmlTest.java
new file mode 100644
index 0000000..b1976e8
--- /dev/null
+++ b/tools/java/rust-build/test/com/google/i18n/phonenumbers/BuildMetadataCppFromXmlTest.java
@@ -0,0 +1,185 @@
+/*
+ * Copyright (C) 2011 The Libphonenumber Authors
+ *
+ * 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.google.i18n.phonenumbers;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNull;
+import static org.junit.Assert.assertTrue;
+import static org.junit.Assert.fail;
+
+import com.google.i18n.phonenumbers.BuildMetadataCppFromXml.Options;
+import com.google.i18n.phonenumbers.BuildMetadataCppFromXml.Variant;
+import com.google.i18n.phonenumbers.CppMetadataGenerator.Type;
+
+import org.junit.Test;
+
+import java.io.ByteArrayOutputStream;
+import java.io.File;
+import java.io.OutputStream;
+import java.nio.charset.Charset;
+
+/**
+ * Tests the BuildMetadataCppFromXml implementation to make sure it parses command line options and
+ * generates code correctly.
+ */
+public class BuildMetadataCppFromXmlTest {
+
+ // Various repeated test strings and data.
+ private static final String IGNORED = "IGNORED";
+ private static final String OUTPUT_DIR = "output/dir";
+ private static final String INPUT_PATH_XML = "input/path.xml";
+ private static final byte[] TEST_DATA =
+ new byte[] { (byte) 0xCA, (byte) 0xFE, (byte) 0xBA, (byte) 0xBE };
+ private static final String CPP_TEST_DATA = "0xCA, 0xFE, 0xBA, 0xBE";
+
+ @Test
+ public void parseVariant() {
+ assertNull(Variant.parse("xxx"));
+ assertEquals(Variant.FULL, Variant.parse(null));
+ assertEquals(Variant.FULL, Variant.parse(""));
+ assertEquals(Variant.LITE, Variant.parse("lite"));
+ assertEquals(Variant.TEST, Variant.parse("test"));
+ assertEquals(Variant.LITE, Variant.parse("LITE"));
+ assertEquals(Variant.TEST, Variant.parse("Test"));
+ }
+
+ @Test
+ public void parseBadOptions() {
+ try {
+ BuildMetadataCppFromXml.Options.parse("MyCommand", new String[] { IGNORED });
+ fail("Expected exception not thrown");
+ } catch (IllegalArgumentException e) {
+ assertTrue(e.getMessage().contains("MyCommand"));
+ }
+ }
+
+ @Test
+ public void parseGoodOptions() {
+ Options opt = BuildMetadataCppFromXml.Options.parse("MyCommand",
+ new String[] { IGNORED, INPUT_PATH_XML, OUTPUT_DIR, "test_alternate_format" });
+ assertEquals(Type.ALTERNATE_FORMAT, opt.getType());
+ assertEquals(Variant.TEST, opt.getVariant());
+ assertEquals(INPUT_PATH_XML, opt.getInputFilePath());
+ assertEquals(OUTPUT_DIR, opt.getOutputDir());
+ }
+
+ @Test
+ public void generateMetadata() {
+ String[] args = new String[] {
+ IGNORED, INPUT_PATH_XML, OUTPUT_DIR, "metadata" };
+ // Most of the useful asserts are done in the mock class.
+ MockedCommand command = new MockedCommand(
+ INPUT_PATH_XML, false, OUTPUT_DIR, Type.METADATA, Variant.FULL);
+ command.setArgs(args);
+ command.start();
+ // Sanity check the captured data (asserting implicitly that the mocked methods were called).
+ String headerString = command.capturedHeaderFile();
+ assertTrue(headerString.contains("const void* metadata_get()"));
+ assertTrue(headerString.contains("int metadata_size()"));
+ String sourceString = command.capturedSourceFile();
+ assertTrue(sourceString.contains("const void* metadata_get()"));
+ assertTrue(sourceString.contains("int metadata_size()"));
+ assertTrue(sourceString.contains(CPP_TEST_DATA));
+ }
+
+ @Test
+ public void generateLiteMetadata() {
+ String[] args = new String[] {
+ IGNORED, INPUT_PATH_XML, OUTPUT_DIR, "lite_metadata" };
+ // Most of the useful asserts are done in the mock class.
+ MockedCommand command = new MockedCommand(
+ INPUT_PATH_XML, true, OUTPUT_DIR, Type.METADATA, Variant.LITE);
+ command.setArgs(args);
+ command.start();
+ // Sanity check the captured data (asserting implicitly that the mocked methods were called).
+ String headerString = command.capturedHeaderFile();
+ assertTrue(headerString.contains("const void* metadata_get()"));
+ assertTrue(headerString.contains("int metadata_size()"));
+ String sourceString = command.capturedSourceFile();
+ assertTrue(sourceString.contains("const void* metadata_get()"));
+ assertTrue(sourceString.contains("int metadata_size()"));
+ assertTrue(sourceString.contains(CPP_TEST_DATA));
+ }
+
+ @Test
+ public void generateAlternateFormat() {
+ String[] args = new String[] {
+ IGNORED, INPUT_PATH_XML, OUTPUT_DIR, "alternate_format" };
+ // Most of the useful asserts are done in the mock class.
+ MockedCommand command = new MockedCommand(
+ INPUT_PATH_XML, false, OUTPUT_DIR, Type.ALTERNATE_FORMAT, Variant.FULL);
+ command.setArgs(args);
+ command.start();
+ // Sanity check the captured data (asserting implicitly that the mocked methods were called).
+ String headerString = command.capturedHeaderFile();
+ assertTrue(headerString.contains("const void* alternate_format_get()"));
+ assertTrue(headerString.contains("int alternate_format_size()"));
+ String sourceString = command.capturedSourceFile();
+ assertTrue(sourceString.contains("const void* alternate_format_get()"));
+ assertTrue(sourceString.contains("int alternate_format_size()"));
+ assertTrue(sourceString.contains(CPP_TEST_DATA));
+ }
+
+ /**
+ * Manually mocked subclass of BuildMetadataCppFromXml which overrides all file related behavior
+ * while asserting the validity of any parameters passed to the mocked methods. After starting
+ * this command, the captured header and source file contents can be retrieved for testing.
+ */
+ static class MockedCommand extends BuildMetadataCppFromXml {
+ private static final Charset UTF_8 = Charset.forName("UTF-8");
+ private final String expectedInputFilePath;
+ private final boolean expectedLiteMetadata;
+ private final String expectedOutputDirPath;
+ private final Type expectedType;
+ private final Variant expectedVariant;
+ private final ByteArrayOutputStream headerOut = new ByteArrayOutputStream();
+ private final ByteArrayOutputStream sourceOut = new ByteArrayOutputStream();
+
+ public MockedCommand(String expectedInputFilePath, boolean expectedLiteMetadata,
+ String expectedOutputDirPath, Type expectedType, Variant expectedVariant) {
+
+ this.expectedInputFilePath = expectedInputFilePath;
+ this.expectedLiteMetadata = expectedLiteMetadata;
+ this.expectedOutputDirPath = expectedOutputDirPath;
+ this.expectedType = expectedType;
+ this.expectedVariant = expectedVariant;
+ }
+ @Override void writePhoneMetadataCollection(
+ String inputFilePath, boolean liteMetadata, OutputStream out) throws Exception {
+ assertEquals(expectedInputFilePath, inputFilePath);
+ assertEquals(expectedLiteMetadata, liteMetadata);
+ out.write(TEST_DATA, 0, TEST_DATA.length);
+ }
+ @Override OutputStream openHeaderStream(File dir, Type type) {
+ assertEquals(expectedOutputDirPath, dir.getPath());
+ assertEquals(expectedType, type);
+ return headerOut;
+ }
+ @Override OutputStream openSourceStream(File dir, Type type, Variant variant) {
+ assertEquals(expectedOutputDirPath, dir.getPath());
+ assertEquals(expectedType, type);
+ assertEquals(expectedVariant, variant);
+ return sourceOut;
+ }
+ String capturedHeaderFile() {
+ return new String(headerOut.toByteArray(), UTF_8);
+ }
+ String capturedSourceFile() {
+ return new String(sourceOut.toByteArray(), UTF_8);
+ }
+ }
+}
diff --git a/tools/java/rust-build/test/com/google/i18n/phonenumbers/CppMetadataGeneratorTest.java b/tools/java/rust-build/test/com/google/i18n/phonenumbers/CppMetadataGeneratorTest.java
new file mode 100644
index 0000000..d0bf3d3
--- /dev/null
+++ b/tools/java/rust-build/test/com/google/i18n/phonenumbers/CppMetadataGeneratorTest.java
@@ -0,0 +1,119 @@
+/*
+ * Copyright (C) 2012 The Libphonenumber Authors
+ *
+ * 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.google.i18n.phonenumbers;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertTrue;
+
+import com.google.i18n.phonenumbers.CppMetadataGenerator.Type;
+
+import org.junit.Test;
+
+import java.io.BufferedReader;
+import java.io.IOException;
+import java.io.PrintWriter;
+import java.io.StringReader;
+import java.io.StringWriter;
+import java.util.ArrayList;
+import java.util.Iterator;
+import java.util.List;
+
+/**
+ * Tests that the CppXmlMetadata class emits the expected source and header files for metadata.
+ */
+public class CppMetadataGeneratorTest {
+
+ @Test
+ public void emitStaticArrayData() {
+ // 13 bytes per line, so have 16 bytes to test > 1 line (general case).
+ // Use all hex digits in both nibbles to test hex formatting.
+ byte[] data = new byte[] {
+ (byte) 0xF0, (byte) 0xE1, (byte) 0xD2, (byte) 0xC3,
+ (byte) 0xB4, (byte) 0xA5, (byte) 0x96, (byte) 0x87,
+ (byte) 0x78, (byte) 0x69, (byte) 0x5A, (byte) 0x4B,
+ (byte) 0x3C, (byte) 0x2D, (byte) 0x1E, (byte) 0x0F,
+ };
+
+ StringWriter writer = new StringWriter();
+ CppMetadataGenerator.emitStaticArrayData(new PrintWriter(writer), data);
+
+ }
+
+ @Test
+ public void outputHeaderFile() throws IOException {
+ byte[] data = new byte[] { (byte) 0xCA, (byte) 0xFE, (byte) 0xBA, (byte) 0xBE };
+ CppMetadataGenerator metadata = CppMetadataGenerator.create(Type.METADATA, data);
+
+ StringWriter writer = new StringWriter();
+ metadata.outputHeaderFile(writer);
+ Iterator lines = toLines(writer.toString()).iterator();
+ // Sanity check that at least some of the expected lines are present.
+ assertTrue(consumeUntil(" * Copyright (C) 2011 The Libphonenumber Authors", lines));
+ assertTrue(consumeUntil("#ifndef I18N_PHONENUMBERS_METADATA_H_", lines));
+ assertTrue(consumeUntil("#define I18N_PHONENUMBERS_METADATA_H_", lines));
+ assertTrue(consumeUntil("namespace i18n {", lines));
+ assertTrue(consumeUntil("namespace phonenumbers {", lines));
+ assertTrue(consumeUntil("int metadata_size();", lines));
+ assertTrue(consumeUntil("const void* metadata_get();", lines));
+ assertTrue(consumeUntil("#endif // I18N_PHONENUMBERS_METADATA_H_", lines));
+ }
+
+ @Test
+ public void outputSourceFile() throws IOException {
+ byte[] data = new byte[] { (byte) 0xCA, (byte) 0xFE, (byte) 0xBA, (byte) 0xBE };
+ CppMetadataGenerator metadata = CppMetadataGenerator.create(Type.ALTERNATE_FORMAT, data);
+
+ StringWriter writer = new StringWriter();
+ metadata.outputSourceFile(writer);
+ Iterator lines = toLines(writer.toString()).iterator();
+ // Sanity check that at least some of the expected lines are present.
+ assertTrue(consumeUntil(" * Copyright (C) 2012 The Libphonenumber Authors", lines));
+ assertTrue(consumeUntil("namespace i18n {", lines));
+ assertTrue(consumeUntil("namespace phonenumbers {", lines));
+ assertTrue(consumeUntil("namespace {", lines));
+ assertTrue(consumeUntil("static const unsigned char data[] = {", lines));
+ assertTrue(consumeUntil(" 0xCA, 0xFE, 0xBA, 0xBE", lines));
+ assertTrue(consumeUntil("int alternate_format_size() {", lines));
+ assertTrue(consumeUntil("const void* alternate_format_get() {", lines));
+ }
+
+ /** Converts a string containing newlines into a list of lines. */
+ private static List toLines(String s) throws IOException {
+ BufferedReader reader = new BufferedReader(new StringReader(s));
+ List lines = new ArrayList();
+ for (String line = reader.readLine(); line != null; line = reader.readLine()) {
+ lines.add(line);
+ }
+ return lines;
+ }
+
+ /**
+ * Consumes strings from the given iterator until the expected string is reached (it is also
+ * consumed). If the expected string is not found, the iterator is exhausted and {@code false} is
+ * returned.
+ *
+ * @return true if the expected string was found while consuming the iterator.
+ */
+ private static boolean consumeUntil(String expected, Iterator it) {
+ while (it.hasNext()) {
+ if (it.next().equals(expected)) {
+ return true;
+ }
+ }
+ return false;
+ }
+}