/* * 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.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.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; /** * Tool to convert phone number metadata from the XML format to protocol buffer format. * *

* 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"; // 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 + "=] Optional (default: false). Whether to write\n" + " metadata to a single file, instead of one file\n" + " per region.\n" + " [--" + LITE_BUILD + "=] Optional (default: false). In a lite build,\n" + " certain metadata will be omitted. At this\n" + " moment, example numbers information is omitted.\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"; 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"; @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; 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 { 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> countryCodeToRegionCodeMap = BuildMetadataFromXml.buildCountryCodeToRegionCodeMap(metadataCollection); writeCountryCallingCodeMappingToJavaFile( countryCodeToRegionCodeMap, outputDir, mappingClass, 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 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 void writeCountryCallingCodeMappingToJavaFile( Map> 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 listWithRegionCode : countryCodeToRegionCodeMap.values()) { if (!listWithRegionCode.isEmpty()) { hasRegionCodes = true; break; } } boolean hasCountryCodes = countryCodeToRegionCodeMap.size() > 1; ClassWriter writer = new ClassWriter(outputDir, mappingClass, copyright); int capacity = (int) (countryCodeToRegionCodeMap.size() / CAPACITY_FACTOR); if (hasRegionCodes && hasCountryCodes) { writeMap(writer, capacity, countryCodeToRegionCodeMap); } else if (hasCountryCodes) { writeCountryCodeSet(writer, capacity, countryCodeToRegionCodeMap.keySet()); } else { List regionCodeList = countryCodeToRegionCodeMap.get(0); capacity = (int) (regionCodeList.size() / CAPACITY_FACTOR); writeRegionCodeSet(writer, capacity, regionCodeList); } writer.writeToFile(); } private static void writeMap(ClassWriter writer, int capacity, Map> 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> getCountryCodeToRegionCodeMap() {\n"); writer.formatToBody(CAPACITY_COMMENT, capacity, countryCodeToRegionCodeMap.size()); writer.addToBody(" Map> countryCodeToRegionCodeMap =\n"); writer.addToBody(" new HashMap>(" + capacity + ");\n"); writer.addToBody("\n"); writer.addToBody(" ArrayList listWithRegionCode;\n"); writer.addToBody("\n"); for (Map.Entry> entry : countryCodeToRegionCodeMap.entrySet()) { int countryCallingCode = entry.getKey(); List regionCodes = entry.getValue(); writer.addToBody(" listWithRegionCode = new ArrayList(" + 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 writer, int capacity, List regionCodeList) { writer.addToBody(REGION_CODE_SET_COMMENT); writer.addToImports("java.util.HashSet"); writer.addToImports("java.util.Set"); writer.addToBody(" public static Set getRegionCodeSet() {\n"); writer.formatToBody(CAPACITY_COMMENT, capacity, regionCodeList.size()); writer.addToBody(" Set regionCodeSet = new HashSet(" + 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 writer, int capacity, Set countryCodeSet) { writer.addToBody(COUNTRY_CODE_SET_COMMENT); writer.addToImports("java.util.HashSet"); writer.addToImports("java.util.Set"); writer.addToBody(" public static Set getCountryCodeSet() {\n"); writer.formatToBody(CAPACITY_COMMENT, capacity, countryCodeSet.size()); writer.addToBody(" Set countryCodeSet = new HashSet(" + 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 final class ClassWriter { private final String name; private final String copyright; private final SortedSet imports; private final StringBuffer body; private final Formatter formatter; private final Writer writer; ClassWriter(String outputDir, String name, String copyright) throws IOException { this.name = name; this.copyright = copyright; imports = new TreeSet(); body = new StringBuffer(); formatter = new Formatter(body); writer = new BufferedWriter(new FileWriter(new File(outputDir, name + ".java"))); } void addToImports(String name) { imports.add(name); } void addToBody(CharSequence text) { body.append(text); } void formatToBody(String format, Object... args) { formatter.format(format, args); } void writeToFile() throws IOException { CopyrightNotice.writeTo(writer, Integer.valueOf(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("public class " + name + " {\n"); writer.write(body.toString()); writer.write("}\n"); writer.flush(); writer.close(); } } }