Initial copy of buildMetadata

This commit is contained in:
Vlasislav Kashin
2025-07-11 00:49:56 +03:00
parent e75eda86e6
commit 8a42c0ecb5
19 changed files with 4640 additions and 0 deletions

3
tools/java/.gitignore vendored Normal file
View File

@@ -0,0 +1,3 @@
target
generated
bin

1
tools/java/Readme.md Normal file
View File

@@ -0,0 +1 @@
## This directory contains script for autogeneration of metadata in rust

78
tools/java/common/pom.xml Normal file
View 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>

View File

@@ -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();
}
}

View File

@@ -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;
}
}

View File

@@ -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());
}
}

View File

@@ -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);
}
}

View File

@@ -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);
}
}
}

View File

@@ -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

View File

@@ -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

45
tools/java/pom.xml Normal file
View File

@@ -0,0 +1,45 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>com.google.i18n.phonenumbers</groupId>
<artifactId>tools</artifactId>
<packaging>pom</packaging>
<version>1.0-SNAPSHOT</version>
<name>Libphonenumber build tools</name>
<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
</properties>
<licenses>
<license>
<name>Apache 2</name>
<url>http://www.apache.org/licenses/LICENSE-2.0.txt</url>
<comments>Copyright (C) 2011 The Libphonenumber Authors</comments>
</license>
</licenses>
<profiles>
<profile>
<id>default</id>
<activation>
<activeByDefault>true</activeByDefault>
</activation>
<modules>
<module>common</module>
<module>rust-build</module>
</modules>
</profile>
<profile>
<id>github-actions</id>
<modules>
<module>common</module>
<!-- TODO: Add cpp-build once the protoc dependency or the generated Phonemetadata.java is
hermetic at tools/java/cpp-build/pom.xml. -->
<module>data</module>
<module>java-build</module>
</modules>
</profile>
</profiles>
</project>

View File

@@ -0,0 +1,161 @@
<?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>cpp-build</artifactId>
<version>1.0-SNAPSHOT</version>
<name>Libphonenumber C++ build tools</name>
<description>
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.
</description>
<build>
<sourceDirectory>src</sourceDirectory>
<testSourceDirectory>test</testSourceDirectory>
<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>
<!-- Create a directory called 'generated'. -->
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-antrun-plugin</artifactId>
<version>1.3</version>
<executions>
<execution>
<id>create-generated-directory</id>
<phase>generate-sources</phase>
<configuration>
<tasks>
<mkdir dir="generated"/>
</tasks>
</configuration>
<goals>
<goal>run</goal>
</goals>
</execution>
</executions>
</plugin>
<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>
<!-- Make BuildMetadataFromXml.java available to the source directories. -->
<source>../common/src/</source>
<!-- Make Phonemetadata.java available to the source directories.
BuildMetadataFromXml.java has to work with both
tools/java/cpp-build/generated/com/google/i18n/phonenumbers/Phonemetadata.java
and java/libphonenumber/src/com/google/i18n/phonenumbers/Phonemetadata.java.
TODO: This Phonemetadata.java is generated via a protoc dependency that is not
hermetic and may get out of sync with the other one. Make this file hermetic or
find another way to enable Travis CI on this build. -->
<source>generated/</source>
</sources>
</configuration>
</execution>
</executions>
</plugin>
<!-- Invoke Protocol Buffers compiler to generate Phonemetadata.java. -->
<plugin>
<groupId>org.codehaus.mojo</groupId>
<artifactId>exec-maven-plugin</artifactId>
<version>3.1.0</version>
<executions>
<execution>
<phase>generate-sources</phase>
<goals>
<goal>exec</goal>
</goals>
</execution>
</executions>
<configuration>
<executable>protoc</executable>
<arguments>
<argument>--java_out=generated</argument>
<argument>../../../resources/phonemetadata.proto</argument>
<argument>--proto_path=../../../resources</argument>
</arguments>
</configuration>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-jar-plugin</artifactId>
<version>3.3.0</version>
<configuration>
<archive>
<manifest>
<addClasspath>true</addClasspath>
<mainClass>com.google.i18n.phonenumbers.EntryPoint</mainClass>
</manifest>
</archive>
</configuration>
</plugin>
<!-- Build a JAR with its dependencies (protocol buffers and common library). This JAR
contains the C++ build tools invoked by CMake during the libphonenumber C++ build. -->
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-assembly-plugin</artifactId>
<configuration>
<descriptorRefs>
<descriptorRef>jar-with-dependencies</descriptorRef>
</descriptorRefs>
<archive>
<manifest>
<addClasspath>true</addClasspath>
<mainClass>com.google.i18n.phonenumbers.EntryPoint</mainClass>
</manifest>
</archive>
</configuration>
<executions>
<execution>
<id>make-assembly</id>
<phase>package</phase>
<goals>
<goal>single</goal>
</goals>
</execution>
</executions>
</plugin>
</plugins>
</build>
<dependencies>
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>4.13.2</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>com.google.protobuf</groupId>
<artifactId>protobuf-java</artifactId>
<version>3.25.5</version>
</dependency>
</dependencies>
</project>

View File

@@ -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.
*
* <p>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 <inputXmlFile> <outputDir> ( <type> | test_<type> | lite_<type> )\n" +
" where <type> 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");
}

View File

@@ -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_<TYPE>_H_"
private final String headerInclude; // e.g. "phonenumbers/<type>.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();
}

View File

@@ -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);
}
}

View File

@@ -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);
}
}
}

View File

@@ -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<String> 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<String> 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<String> toLines(String s) throws IOException {
BufferedReader reader = new BufferedReader(new StringReader(s));
List<String> lines = new ArrayList<String>();
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<String> it) {
while (it.hasNext()) {
if (it.next().equals(expected)) {
return true;
}
}
return false;
}
}