Initial copy of buildMetadata
This commit is contained in:
78
tools/java/common/pom.xml
Normal file
78
tools/java/common/pom.xml
Normal file
@@ -0,0 +1,78 @@
|
||||
<?xml version="1.0"?>
|
||||
<project
|
||||
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"
|
||||
xmlns="http://maven.apache.org/POM/4.0.0"
|
||||
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
|
||||
<modelVersion>4.0.0</modelVersion>
|
||||
|
||||
<parent>
|
||||
<artifactId>tools</artifactId>
|
||||
<groupId>com.google.i18n.phonenumbers</groupId>
|
||||
<version>1.0-SNAPSHOT</version>
|
||||
</parent>
|
||||
|
||||
<groupId>com.google.i18n.phonenumbers.tools</groupId>
|
||||
<artifactId>common-build</artifactId>
|
||||
<version>1.0-SNAPSHOT</version>
|
||||
<name>Libphonenumber common library for build tools</name>
|
||||
|
||||
<build>
|
||||
<sourceDirectory>src</sourceDirectory>
|
||||
<testSourceDirectory>test</testSourceDirectory>
|
||||
<testResources>
|
||||
<testResource>
|
||||
<directory>src/com/google/i18n/phonenumbers</directory>
|
||||
<targetPath>com/google/i18n/phonenumbers</targetPath>
|
||||
</testResource>
|
||||
</testResources>
|
||||
<plugins>
|
||||
<plugin>
|
||||
<groupId>org.apache.maven.plugins</groupId>
|
||||
<artifactId>maven-compiler-plugin</artifactId>
|
||||
<version>3.11.0</version>
|
||||
<configuration>
|
||||
<source>8</source>
|
||||
<target>8</target>
|
||||
<encoding>UTF-8</encoding>
|
||||
</configuration>
|
||||
</plugin>
|
||||
<!-- Add ../../../java/libphonenumber/src/ to make Phonemetadata.java available to the source
|
||||
directories. -->
|
||||
<plugin>
|
||||
<groupId>org.codehaus.mojo</groupId>
|
||||
<artifactId>build-helper-maven-plugin</artifactId>
|
||||
<version>3.4.0</version>
|
||||
<executions>
|
||||
<execution>
|
||||
<id>add-source</id>
|
||||
<phase>generate-sources</phase>
|
||||
<goals>
|
||||
<goal>add-source</goal>
|
||||
</goals>
|
||||
<configuration>
|
||||
<sources>
|
||||
<source>../../../java/libphonenumber/src/</source>
|
||||
</sources>
|
||||
</configuration>
|
||||
</execution>
|
||||
</executions>
|
||||
</plugin>
|
||||
</plugins>
|
||||
</build>
|
||||
|
||||
<dependencies>
|
||||
<dependency>
|
||||
<groupId>junit</groupId>
|
||||
<artifactId>junit</artifactId>
|
||||
<version>4.13.2</version>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
<!-- https://mvnrepository.com/artifact/com.googlecode.libphonenumber/libphonenumber -->
|
||||
<dependency>
|
||||
<groupId>com.googlecode.libphonenumber</groupId>
|
||||
<artifactId>libphonenumber</artifactId>
|
||||
<version>9.0.9</version>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
|
||||
</project>
|
||||
@@ -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<String> PHONE_NUMBER_DESCS_WITHOUT_MATCHING_TYPES =
|
||||
new HashSet<String>(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<Integer, List<String>> buildCountryCodeToRegionCodeMap(
|
||||
PhoneMetadataCollection metadataCollection) {
|
||||
Map<Integer, List<String>> countryCodeToRegionCodeMap = new TreeMap<Integer, List<String>>();
|
||||
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<String> listWithRegionCode = new ArrayList<String>(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<String> buildRegionCodeList(
|
||||
PhoneMetadataCollection metadataCollection) {
|
||||
List<String> regionCodeList = new ArrayList<String>();
|
||||
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<Integer> 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<Integer> lengths = new TreeSet<Integer>();
|
||||
TreeSet<Integer> localOnlyLengths = new TreeSet<Integer>();
|
||||
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<Integer> parsePossibleLengthStringToSet(String possibleLengthString) {
|
||||
if (possibleLengthString.length() == 0) {
|
||||
throw new RuntimeException("Empty possibleLength string found.");
|
||||
}
|
||||
String[] lengths = possibleLengthString.split(",");
|
||||
Set<Integer> lengthSet = new TreeSet<Integer>();
|
||||
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<Integer> lengths,
|
||||
TreeSet<Integer> 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<Integer> thisElementLengths = parsePossibleLengthStringToSet(nationalLengths);
|
||||
if (element.hasAttribute(LOCAL_ONLY)) {
|
||||
String localLengths = element.getAttribute(LOCAL_ONLY);
|
||||
Set<Integer> thisElementLocalOnlyLengths = parsePossibleLengthStringToSet(localLengths);
|
||||
Set<Integer> intersection = new HashSet<Integer>(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<Integer> lengths = new TreeSet<Integer>();
|
||||
TreeSet<Integer> localOnlyLengths = new TreeSet<Integer>();
|
||||
// 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<Integer> lengths,
|
||||
TreeSet<Integer> 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();
|
||||
}
|
||||
}
|
||||
@@ -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).
|
||||
*
|
||||
* <p> 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;
|
||||
}
|
||||
}
|
||||
@@ -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.
|
||||
*
|
||||
* <p> 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());
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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.
|
||||
*
|
||||
* <p>
|
||||
* 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<String> excludableParentFields = new TreeSet<String>(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<String> excludableChildFields = new TreeSet<String>(Arrays.asList(
|
||||
"nationalNumberPattern",
|
||||
"possibleLength",
|
||||
"possibleLengthLocalOnly",
|
||||
"exampleNumber"));
|
||||
|
||||
// @VisibleForTesting
|
||||
static final TreeSet<String> excludableChildlessFields = new TreeSet<String>(Arrays.asList(
|
||||
"preferredInternationalPrefix",
|
||||
"nationalPrefix",
|
||||
"preferredExtnPrefix",
|
||||
"nationalPrefixTransformRule",
|
||||
"sameMobileAndFixedLinePattern",
|
||||
"mainCountryForCode",
|
||||
"mobileNumberPortableRegion"));
|
||||
|
||||
private final TreeMap<String, TreeSet<String>> 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<String, TreeSet<String>>());
|
||||
}
|
||||
|
||||
// @VisibleForTesting
|
||||
MetadataFilter(TreeMap<String, TreeSet<String>> 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<String, TreeSet<String>> 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<String, TreeSet<String>> fieldMap = new TreeMap<String, TreeSet<String>>();
|
||||
TreeSet<String> wildcardChildren = new TreeSet<String>();
|
||||
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<String>(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<String>());
|
||||
} 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<String> children = new TreeSet<String>();
|
||||
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<String> children = fieldMap.get(parent);
|
||||
if (children == null) {
|
||||
children = new TreeSet<String>();
|
||||
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<String, TreeSet<String>> computeComplement(
|
||||
TreeMap<String, TreeSet<String>> fieldMap) {
|
||||
TreeMap<String, TreeSet<String>> complement = new TreeMap<String, TreeSet<String>>();
|
||||
for (String parent : excludableParentFields) {
|
||||
if (!fieldMap.containsKey(parent)) {
|
||||
complement.put(parent, new TreeSet<String>(excludableChildFields));
|
||||
} else {
|
||||
TreeSet<String> 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<String> children = new TreeSet<String>();
|
||||
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<String>());
|
||||
}
|
||||
}
|
||||
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();
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -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;
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user