|
|
/*
|
|
|
* 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.auto.value.AutoValue;
|
|
|
import com.google.common.collect.ImmutableSortedSet;
|
|
|
import com.google.i18n.phonenumbers.Phonemetadata.PhoneMetadata;
|
|
|
import com.google.i18n.phonenumbers.Phonemetadata.PhoneMetadataCollection;
|
|
|
|
|
|
import java.io.BufferedWriter;
|
|
|
import java.io.File;
|
|
|
import java.io.FileOutputStream;
|
|
|
import java.io.FileWriter;
|
|
|
import java.io.IOException;
|
|
|
import java.io.ObjectOutputStream;
|
|
|
import java.io.Writer;
|
|
|
import java.util.Collection;
|
|
|
import java.util.Formatter;
|
|
|
import java.util.List;
|
|
|
import java.util.Map;
|
|
|
import java.util.Set;
|
|
|
import java.util.SortedSet;
|
|
|
import java.util.TreeSet;
|
|
|
import java.util.regex.Matcher;
|
|
|
import java.util.regex.Pattern;
|
|
|
import javax.annotation.Nullable;
|
|
|
|
|
|
/**
|
|
|
* Tool to convert phone number metadata from the XML format to protocol buffer format.
|
|
|
*
|
|
|
* <p>
|
|
|
* Based on the name of the {@code inputFile}, some optimization and removal of unnecessary metadata
|
|
|
* is carried out to reduce the size of the output file.
|
|
|
*
|
|
|
* @author Shaopeng Jia
|
|
|
*/
|
|
|
public class BuildMetadataProtoFromXml extends Command {
|
|
|
private static final String CLASS_NAME = BuildMetadataProtoFromXml.class.getSimpleName();
|
|
|
private static final String PACKAGE_NAME = BuildMetadataProtoFromXml.class.getPackage().getName();
|
|
|
|
|
|
// Command line parameter names.
|
|
|
private static final String INPUT_FILE = "input-file";
|
|
|
private static final String OUTPUT_DIR = "output-dir";
|
|
|
private static final String DATA_PREFIX = "data-prefix";
|
|
|
private static final String MAPPING_CLASS = "mapping-class";
|
|
|
private static final String COPYRIGHT = "copyright";
|
|
|
private static final String SINGLE_FILE = "single-file";
|
|
|
private static final String LITE_BUILD = "lite-build";
|
|
|
private static final String BUILD_REGIONCODE = "build-regioncode";
|
|
|
// Only supported for clients who have consulted with the libphonenumber team, and the behavior is
|
|
|
// subject to change without notice.
|
|
|
private static final String SPECIAL_BUILD = "special-build";
|
|
|
|
|
|
private static final String HELP_MESSAGE =
|
|
|
"Usage: " + CLASS_NAME + " [OPTION]...\n" +
|
|
|
"\n" +
|
|
|
" --" + INPUT_FILE + "=PATH Read phone number metadata in XML format from PATH.\n" +
|
|
|
" --" + OUTPUT_DIR + "=PATH Use PATH as the root directory for output files.\n" +
|
|
|
" --" + DATA_PREFIX +
|
|
|
"=PATH Use PATH (relative to " + OUTPUT_DIR + ") as the basename when\n" +
|
|
|
" writing phone number metadata in proto format.\n" +
|
|
|
" One file per region will be written unless " + SINGLE_FILE + "\n" +
|
|
|
" is set, in which case a single file will be written with\n" +
|
|
|
" metadata for all regions.\n" +
|
|
|
" --" + MAPPING_CLASS + "=NAME Store country code mappings in the class NAME, which\n" +
|
|
|
" will be written to a file in " + OUTPUT_DIR + ".\n" +
|
|
|
" --" + COPYRIGHT + "=YEAR Use YEAR in generated copyright headers.\n" +
|
|
|
"\n" +
|
|
|
" [--" + SINGLE_FILE + "=<true|false>] Optional (default: false). Whether to write\n" +
|
|
|
" metadata to a single file, instead of one file\n" +
|
|
|
" per region.\n" +
|
|
|
" [--" + LITE_BUILD + "=<true|false>] Optional (default: false). In a lite build,\n" +
|
|
|
" certain metadata will be omitted. At this\n" +
|
|
|
" moment, example numbers information is omitted.\n" +
|
|
|
" [--" + BUILD_REGIONCODE + "=<true|false>] Optional (default: false). Generate\n" +
|
|
|
" RegionCode class with constants for all region codes.\n" +
|
|
|
"\n" +
|
|
|
"Example command line invocation:\n" +
|
|
|
CLASS_NAME + " \\\n" +
|
|
|
" --" + INPUT_FILE + "=resources/PhoneNumberMetadata.xml \\\n" +
|
|
|
" --" + OUTPUT_DIR + "=java/libphonenumber/src/com/google/i18n/phonenumbers \\\n" +
|
|
|
" --" + DATA_PREFIX + "=data/PhoneNumberMetadataProto \\\n" +
|
|
|
" --" + MAPPING_CLASS + "=CountryCodeToRegionCodeMap \\\n" +
|
|
|
" --" + COPYRIGHT + "=2010 \\\n" +
|
|
|
" --" + SINGLE_FILE + "=false \\\n" +
|
|
|
" --" + LITE_BUILD + "=false\n" +
|
|
|
" --" + BUILD_REGIONCODE + "=true\n";
|
|
|
|
|
|
private static final String GENERATION_COMMENT =
|
|
|
"/* This file is automatically generated by {@link " + CLASS_NAME + "}.\n" +
|
|
|
" * Please don't modify it directly.\n" +
|
|
|
" */\n\n";
|
|
|
|
|
|
private static final String MAP_COMMENT =
|
|
|
" // A mapping from a country code to the region codes which denote the\n" +
|
|
|
" // country/region represented by that country code. In the case of multiple\n" +
|
|
|
" // countries sharing a calling code, such as the NANPA countries, the one\n" +
|
|
|
" // indicated with \"isMainCountryForCode\" in the metadata should be first.\n";
|
|
|
private static final String COUNTRY_CODE_SET_COMMENT =
|
|
|
" // A set of all country codes for which data is available.\n";
|
|
|
private static final String REGION_CODE_SET_COMMENT =
|
|
|
" // A set of all region codes for which data is available.\n";
|
|
|
private static final double CAPACITY_FACTOR = 0.75;
|
|
|
private static final String CAPACITY_COMMENT =
|
|
|
" // The capacity is set to %d as there are %d different entries,\n" +
|
|
|
" // and this offers a load factor of roughly " + CAPACITY_FACTOR + ".\n";
|
|
|
|
|
|
private static final String REGION_CODE_CONSTS_JAVADOC =
|
|
|
"/**\n" +
|
|
|
" * Class containing string constants of region codes for easier testing.\n" +
|
|
|
" */\n";
|
|
|
|
|
|
@Override
|
|
|
public String getCommandName() {
|
|
|
return CLASS_NAME;
|
|
|
}
|
|
|
|
|
|
@Override
|
|
|
public boolean start() {
|
|
|
// The format of a well-formed command line parameter.
|
|
|
Pattern pattern = Pattern.compile("--(.+?)=(.*)");
|
|
|
|
|
|
String inputFile = null;
|
|
|
String outputDir = null;
|
|
|
String dataPrefix = null;
|
|
|
String mappingClass = null;
|
|
|
String copyright = null;
|
|
|
boolean singleFile = false;
|
|
|
boolean liteBuild = false;
|
|
|
boolean specialBuild = false;
|
|
|
boolean buildRegioncode = false;
|
|
|
|
|
|
for (int i = 1; i < getArgs().length; i++) {
|
|
|
String key = null;
|
|
|
String value = null;
|
|
|
Matcher matcher = pattern.matcher(getArgs()[i]);
|
|
|
if (matcher.matches()) {
|
|
|
key = matcher.group(1);
|
|
|
value = matcher.group(2);
|
|
|
}
|
|
|
|
|
|
if (INPUT_FILE.equals(key)) {
|
|
|
inputFile = value;
|
|
|
} else if (OUTPUT_DIR.equals(key)) {
|
|
|
outputDir = value;
|
|
|
} else if (DATA_PREFIX.equals(key)) {
|
|
|
dataPrefix = value;
|
|
|
} else if (MAPPING_CLASS.equals(key)) {
|
|
|
mappingClass = value;
|
|
|
} else if (COPYRIGHT.equals(key)) {
|
|
|
copyright = value;
|
|
|
} else if (SINGLE_FILE.equals(key) &&
|
|
|
("true".equalsIgnoreCase(value) || "false".equalsIgnoreCase(value))) {
|
|
|
singleFile = "true".equalsIgnoreCase(value);
|
|
|
} else if (LITE_BUILD.equals(key) &&
|
|
|
("true".equalsIgnoreCase(value) || "false".equalsIgnoreCase(value))) {
|
|
|
liteBuild = "true".equalsIgnoreCase(value);
|
|
|
} else if (SPECIAL_BUILD.equals(key) &&
|
|
|
("true".equalsIgnoreCase(value) || "false".equalsIgnoreCase(value))) {
|
|
|
specialBuild = "true".equalsIgnoreCase(value);
|
|
|
} else if (BUILD_REGIONCODE.equals(key) &&
|
|
|
("true".equalsIgnoreCase(value) || "false".equalsIgnoreCase(value))) {
|
|
|
buildRegioncode = "true".equalsIgnoreCase(value);
|
|
|
} else {
|
|
|
System.err.println(HELP_MESSAGE);
|
|
|
System.err.println("Illegal command line parameter: " + getArgs()[i]);
|
|
|
return false;
|
|
|
}
|
|
|
}
|
|
|
|
|
|
if (inputFile == null ||
|
|
|
outputDir == null ||
|
|
|
dataPrefix == null ||
|
|
|
mappingClass == null ||
|
|
|
copyright == null) {
|
|
|
System.err.println(HELP_MESSAGE);
|
|
|
return false;
|
|
|
}
|
|
|
|
|
|
String filePrefix = new File(outputDir, dataPrefix).getPath();
|
|
|
|
|
|
try {
|
|
|
PhoneMetadataCollection metadataCollection =
|
|
|
BuildMetadataFromXml.buildPhoneMetadataCollection(inputFile, liteBuild, specialBuild);
|
|
|
|
|
|
if (singleFile) {
|
|
|
FileOutputStream output = new FileOutputStream(filePrefix);
|
|
|
ObjectOutputStream out = new ObjectOutputStream(output);
|
|
|
metadataCollection.writeExternal(out);
|
|
|
out.close();
|
|
|
} else {
|
|
|
deleteAllFilesForPrefix(filePrefix);
|
|
|
for (PhoneMetadata metadata : metadataCollection.getMetadataList()) {
|
|
|
String regionCode = metadata.getId();
|
|
|
// For non-geographical country calling codes (e.g. +800), or for alternate formats, use the
|
|
|
// country calling codes instead of the region code to form the file name.
|
|
|
if (regionCode.equals("001") || regionCode.isEmpty()) {
|
|
|
regionCode = Integer.toString(metadata.getCountryCode());
|
|
|
}
|
|
|
PhoneMetadataCollection outMetadataCollection = new PhoneMetadataCollection();
|
|
|
outMetadataCollection.addMetadata(metadata);
|
|
|
FileOutputStream outputForRegion = new FileOutputStream(filePrefix + "_" + regionCode);
|
|
|
ObjectOutputStream out = new ObjectOutputStream(outputForRegion);
|
|
|
outMetadataCollection.writeExternal(out);
|
|
|
out.close();
|
|
|
}
|
|
|
System.out.println("Generated " + metadataCollection.getMetadataCount() + " new files");
|
|
|
}
|
|
|
|
|
|
Map<Integer, List<String>> countryCodeToRegionCodeMap =
|
|
|
BuildMetadataFromXml.buildCountryCodeToRegionCodeMap(metadataCollection);
|
|
|
|
|
|
writeCountryCallingCodeMappingToJavaFile(
|
|
|
countryCodeToRegionCodeMap, outputDir, mappingClass, copyright);
|
|
|
|
|
|
if (buildRegioncode) {
|
|
|
SortedSet<String> regionCodeSet = new TreeSet<String>();
|
|
|
// Official code for the unknown region.
|
|
|
regionCodeSet.add("ZZ");
|
|
|
regionCodeSet.addAll(BuildMetadataFromXml.buildRegionCodeList(metadataCollection));
|
|
|
System.out.println("Found " + regionCodeSet.size() + " region codes");
|
|
|
|
|
|
writeRegionCodeConstantsToJavaFile(regionCodeSet, outputDir, copyright);
|
|
|
}
|
|
|
} catch (Exception e) {
|
|
|
e.printStackTrace();
|
|
|
return false;
|
|
|
}
|
|
|
System.out.println("Metadata code successfully created.");
|
|
|
return true;
|
|
|
}
|
|
|
|
|
|
private void deleteAllFilesForPrefix(String filePrefix) {
|
|
|
File[] allFiles = new File(filePrefix).getParentFile().listFiles();
|
|
|
if (allFiles == null) {
|
|
|
allFiles = new File[0];
|
|
|
}
|
|
|
int counter = 0;
|
|
|
for (File file: allFiles) {
|
|
|
if (file.getAbsolutePath().contains(filePrefix)) {
|
|
|
if (file.delete()) {
|
|
|
counter++;
|
|
|
}
|
|
|
}
|
|
|
}
|
|
|
System.out.println("Deleted " + counter + " old files");
|
|
|
}
|
|
|
|
|
|
private static void writeCountryCallingCodeMappingToJavaFile(
|
|
|
Map<Integer, List<String>> countryCodeToRegionCodeMap,
|
|
|
String outputDir, String mappingClass, String copyright) throws IOException {
|
|
|
// Find out whether the countryCodeToRegionCodeMap has any region codes or country
|
|
|
// calling codes listed in it.
|
|
|
boolean hasRegionCodes = false;
|
|
|
for (List<String> listWithRegionCode : countryCodeToRegionCodeMap.values()) {
|
|
|
if (!listWithRegionCode.isEmpty()) {
|
|
|
hasRegionCodes = true;
|
|
|
break;
|
|
|
}
|
|
|
}
|
|
|
boolean hasCountryCodes = countryCodeToRegionCodeMap.size() > 1;
|
|
|
|
|
|
ClassWriter.Builder writer = ClassWriter.builder()
|
|
|
.setOutputDir(outputDir)
|
|
|
.setCopyright(Integer.parseInt(copyright))
|
|
|
.setModifiers("public")
|
|
|
.setName(mappingClass);
|
|
|
|
|
|
int capacity = (int) (countryCodeToRegionCodeMap.size() / CAPACITY_FACTOR);
|
|
|
if (hasRegionCodes && hasCountryCodes) {
|
|
|
writeMap(writer, capacity, countryCodeToRegionCodeMap);
|
|
|
} else if (hasCountryCodes) {
|
|
|
writeCountryCodeSet(writer, capacity, countryCodeToRegionCodeMap.keySet());
|
|
|
} else {
|
|
|
List<String> regionCodeList = countryCodeToRegionCodeMap.get(0);
|
|
|
capacity = (int) (regionCodeList.size() / CAPACITY_FACTOR);
|
|
|
writeRegionCodeSet(writer, capacity, regionCodeList);
|
|
|
}
|
|
|
|
|
|
writer.build().writeToFile();
|
|
|
}
|
|
|
|
|
|
private static void writeMap(ClassWriter.Builder writer, int capacity,
|
|
|
Map<Integer, List<String>> countryCodeToRegionCodeMap) {
|
|
|
writer.addToBody(MAP_COMMENT);
|
|
|
|
|
|
writer.addToImports("java.util.ArrayList");
|
|
|
writer.addToImports("java.util.HashMap");
|
|
|
writer.addToImports("java.util.List");
|
|
|
writer.addToImports("java.util.Map");
|
|
|
|
|
|
writer.addToBody(" public static Map<Integer, List<String>> getCountryCodeToRegionCodeMap() {\n");
|
|
|
writer.formatToBody(CAPACITY_COMMENT, capacity, countryCodeToRegionCodeMap.size());
|
|
|
writer.addToBody(" Map<Integer, List<String>> countryCodeToRegionCodeMap =\n");
|
|
|
writer.addToBody(" new HashMap<Integer, List<String>>(" + capacity + ");\n");
|
|
|
writer.addToBody("\n");
|
|
|
writer.addToBody(" ArrayList<String> listWithRegionCode;\n");
|
|
|
writer.addToBody("\n");
|
|
|
|
|
|
for (Map.Entry<Integer, List<String>> entry : countryCodeToRegionCodeMap.entrySet()) {
|
|
|
int countryCallingCode = entry.getKey();
|
|
|
List<String> regionCodes = entry.getValue();
|
|
|
writer.addToBody(" listWithRegionCode = new ArrayList<String>(" +
|
|
|
regionCodes.size() + ");\n");
|
|
|
for (String regionCode : regionCodes) {
|
|
|
writer.addToBody(" listWithRegionCode.add(\"" + regionCode + "\");\n");
|
|
|
}
|
|
|
writer.addToBody(" countryCodeToRegionCodeMap.put(" + countryCallingCode +
|
|
|
", listWithRegionCode);\n");
|
|
|
writer.addToBody("\n");
|
|
|
}
|
|
|
|
|
|
writer.addToBody(" return countryCodeToRegionCodeMap;\n");
|
|
|
writer.addToBody(" }\n");
|
|
|
}
|
|
|
|
|
|
private static void writeRegionCodeSet(ClassWriter.Builder writer, int capacity,
|
|
|
List<String> regionCodeList) {
|
|
|
writer.addToBody(REGION_CODE_SET_COMMENT);
|
|
|
|
|
|
writer.addToImports("java.util.HashSet");
|
|
|
writer.addToImports("java.util.Set");
|
|
|
|
|
|
writer.addToBody(" public static Set<String> getRegionCodeSet() {\n");
|
|
|
writer.formatToBody(CAPACITY_COMMENT, capacity, regionCodeList.size());
|
|
|
writer.addToBody(" Set<String> regionCodeSet = new HashSet<String>(" + capacity + ");\n");
|
|
|
writer.addToBody("\n");
|
|
|
|
|
|
for (String regionCode : regionCodeList) {
|
|
|
writer.addToBody(" regionCodeSet.add(\"" + regionCode + "\");\n");
|
|
|
}
|
|
|
|
|
|
writer.addToBody("\n");
|
|
|
writer.addToBody(" return regionCodeSet;\n");
|
|
|
writer.addToBody(" }\n");
|
|
|
}
|
|
|
|
|
|
private static void writeCountryCodeSet(ClassWriter.Builder writer, int capacity,
|
|
|
Set<Integer> countryCodeSet) {
|
|
|
writer.addToBody(COUNTRY_CODE_SET_COMMENT);
|
|
|
|
|
|
writer.addToImports("java.util.HashSet");
|
|
|
writer.addToImports("java.util.Set");
|
|
|
|
|
|
writer.addToBody(" public static Set<Integer> getCountryCodeSet() {\n");
|
|
|
writer.formatToBody(CAPACITY_COMMENT, capacity, countryCodeSet.size());
|
|
|
writer.addToBody(" Set<Integer> countryCodeSet = new HashSet<Integer>(" + capacity + ");\n");
|
|
|
writer.addToBody("\n");
|
|
|
|
|
|
for (int countryCallingCode : countryCodeSet) {
|
|
|
writer.addToBody(" countryCodeSet.add(" + countryCallingCode + ");\n");
|
|
|
}
|
|
|
|
|
|
writer.addToBody("\n");
|
|
|
writer.addToBody(" return countryCodeSet;\n");
|
|
|
writer.addToBody(" }\n");
|
|
|
}
|
|
|
|
|
|
private static void writeRegionCodeConstantsToJavaFile(Collection<String> regionCodeList,
|
|
|
String outputDir, String copyright) throws IOException {
|
|
|
ClassWriter.Builder writer = ClassWriter.builder()
|
|
|
.setOutputDir(outputDir)
|
|
|
.setName("RegionCode")
|
|
|
.setModifiers("final")
|
|
|
.setCopyright(Integer.parseInt(copyright));
|
|
|
|
|
|
writer.setJavadoc(REGION_CODE_CONSTS_JAVADOC);
|
|
|
|
|
|
for (String regionCode : regionCodeList) {
|
|
|
String variableName = regionCode.toUpperCase();
|
|
|
if (variableName.equals("001")) {
|
|
|
writer.addToBody(" // Region code for global networks (e.g. +800 numbers).\n");
|
|
|
variableName = "UN001";
|
|
|
} else if (variableName.equals("ZZ")) {
|
|
|
writer.addToBody(" // Official code for the unknown region.\n");
|
|
|
}
|
|
|
writer.addToBody(" static final String " + variableName + " = \"" + regionCode + "\";\n");
|
|
|
}
|
|
|
|
|
|
writer.build().writeToFile();
|
|
|
}
|
|
|
|
|
|
|
|
|
@AutoValue
|
|
|
abstract static class ClassWriter {
|
|
|
abstract String outputDir();
|
|
|
|
|
|
abstract Integer copyright();
|
|
|
abstract ImmutableSortedSet<String> imports();
|
|
|
abstract String javadoc();
|
|
|
abstract String modifiers();
|
|
|
abstract String name();
|
|
|
abstract String body();
|
|
|
|
|
|
static Builder builder() {
|
|
|
return new AutoValue_BuildMetadataProtoFromXml_ClassWriter.Builder()
|
|
|
.setJavadoc("").setModifiers("");
|
|
|
}
|
|
|
|
|
|
@AutoValue.Builder
|
|
|
abstract static class Builder {
|
|
|
abstract Builder setOutputDir(String outputDir);
|
|
|
|
|
|
abstract Builder setCopyright(Integer copyright);
|
|
|
|
|
|
abstract ImmutableSortedSet.Builder<String> importsBuilder();
|
|
|
final Builder addToImports(String name) {
|
|
|
importsBuilder().add(name);
|
|
|
return this;
|
|
|
}
|
|
|
|
|
|
abstract Builder setJavadoc(String javadoc);
|
|
|
abstract Builder setName(String name);
|
|
|
abstract Builder setModifiers(String modifiers);
|
|
|
|
|
|
abstract Builder setBody(String body);
|
|
|
private final StringBuilder bodyBuilder = new StringBuilder();
|
|
|
final Builder addToBody(String text) {
|
|
|
bodyBuilder.append(text);
|
|
|
return this;
|
|
|
}
|
|
|
final Builder formatToBody(String format, Object... args) {
|
|
|
Formatter formatter = new Formatter(bodyBuilder);
|
|
|
formatter.format(format, args);
|
|
|
return this;
|
|
|
}
|
|
|
|
|
|
abstract ClassWriter autoBuild();
|
|
|
|
|
|
final ClassWriter build() {
|
|
|
setBody(bodyBuilder.toString());
|
|
|
return autoBuild();
|
|
|
}
|
|
|
}
|
|
|
|
|
|
void writeToFile() throws IOException {
|
|
|
Writer writer = new BufferedWriter(new FileWriter(new File(outputDir(), name() + ".java")));
|
|
|
|
|
|
CopyrightNotice.writeTo(writer, copyright());
|
|
|
writer.write(GENERATION_COMMENT);
|
|
|
writer.write("package " + PACKAGE_NAME + ";\n\n");
|
|
|
|
|
|
if (!imports().isEmpty()) {
|
|
|
for (String item : imports()) {
|
|
|
writer.write("import " + item + ";\n");
|
|
|
}
|
|
|
writer.write("\n");
|
|
|
}
|
|
|
|
|
|
writer.write(javadoc());
|
|
|
if (!modifiers().isEmpty()) {
|
|
|
writer.write(modifiers() + " ");
|
|
|
}
|
|
|
writer.write("class " + name() + " {\n");
|
|
|
writer.write(body());
|
|
|
writer.write("}\n");
|
|
|
|
|
|
writer.flush();
|
|
|
writer.close();
|
|
|
}
|
|
|
}
|
|
|
}
|