From 8a42c0ecb5974ca9246638d93ee9022a6455c266 Mon Sep 17 00:00:00 2001 From: Vlasislav Kashin <99754299+vloldik@users.noreply.github.com> Date: Fri, 11 Jul 2025 00:49:56 +0300 Subject: [PATCH] Initial copy of buildMetadata --- tools/java/.gitignore | 3 + tools/java/Readme.md | 1 + tools/java/common/pom.xml | 78 ++ .../phonenumbers/BuildMetadataFromXml.java | 783 ++++++++++++ .../com/google/i18n/phonenumbers/Command.java | 51 + .../i18n/phonenumbers/CommandDispatcher.java | 78 ++ .../i18n/phonenumbers/CopyrightNotice.java | 66 + .../google/i18n/phonenumbers/FileUtils.java | 52 + .../i18n/phonenumbers/MetadataFilter.java | 356 ++++++ .../BuildMetadataFromXmlTest.java | 1119 +++++++++++++++++ .../MetadataFilterCoverageTest.java | 93 ++ .../i18n/phonenumbers/MetadataFilterTest.java | 1000 +++++++++++++++ tools/java/pom.xml | 45 + tools/java/rust-build/pom.xml | 161 +++ .../phonenumbers/BuildMetadataCppFromXml.java | 220 ++++ .../phonenumbers/CppMetadataGenerator.java | 197 +++ .../google/i18n/phonenumbers/EntryPoint.java | 33 + .../BuildMetadataCppFromXmlTest.java | 185 +++ .../CppMetadataGeneratorTest.java | 119 ++ 19 files changed, 4640 insertions(+) create mode 100644 tools/java/.gitignore create mode 100644 tools/java/Readme.md create mode 100644 tools/java/common/pom.xml create mode 100644 tools/java/common/src/com/google/i18n/phonenumbers/BuildMetadataFromXml.java create mode 100644 tools/java/common/src/com/google/i18n/phonenumbers/Command.java create mode 100644 tools/java/common/src/com/google/i18n/phonenumbers/CommandDispatcher.java create mode 100644 tools/java/common/src/com/google/i18n/phonenumbers/CopyrightNotice.java create mode 100644 tools/java/common/src/com/google/i18n/phonenumbers/FileUtils.java create mode 100644 tools/java/common/src/com/google/i18n/phonenumbers/MetadataFilter.java create mode 100644 tools/java/common/test/com/google/i18n/phonenumbers/BuildMetadataFromXmlTest.java create mode 100644 tools/java/common/test/com/google/i18n/phonenumbers/MetadataFilterCoverageTest.java create mode 100644 tools/java/common/test/com/google/i18n/phonenumbers/MetadataFilterTest.java create mode 100644 tools/java/pom.xml create mode 100644 tools/java/rust-build/pom.xml create mode 100644 tools/java/rust-build/src/com/google/i18n/phonenumbers/BuildMetadataCppFromXml.java create mode 100644 tools/java/rust-build/src/com/google/i18n/phonenumbers/CppMetadataGenerator.java create mode 100644 tools/java/rust-build/src/com/google/i18n/phonenumbers/EntryPoint.java create mode 100644 tools/java/rust-build/test/com/google/i18n/phonenumbers/BuildMetadataCppFromXmlTest.java create mode 100644 tools/java/rust-build/test/com/google/i18n/phonenumbers/CppMetadataGeneratorTest.java 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; + } +}