1 /* 2 * Copyright (C) 2009 The Libphonenumber Authors 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 17 package com.google.i18n.phonenumbers; 18 19 import com.google.i18n.phonenumbers.Phonemetadata.PhoneMetadata; 20 import com.google.i18n.phonenumbers.Phonemetadata.PhoneMetadataCollection; 21 22 import java.io.BufferedWriter; 23 import java.io.File; 24 import java.io.FileOutputStream; 25 import java.io.FileWriter; 26 import java.io.IOException; 27 import java.io.ObjectOutputStream; 28 import java.io.Writer; 29 import java.util.Formatter; 30 import java.util.List; 31 import java.util.Map; 32 import java.util.Set; 33 import java.util.SortedSet; 34 import java.util.TreeSet; 35 import java.util.regex.Matcher; 36 import java.util.regex.Pattern; 37 38 /** 39 * Tool to convert phone number metadata from the XML format to protocol buffer format. 40 * 41 * <p> 42 * Based on the name of the {@code inputFile}, some optimization and removal of unnecessary metadata 43 * is carried out to reduce the size of the output file. 44 * 45 * @author Shaopeng Jia 46 */ 47 public class BuildMetadataProtoFromXml extends Command { 48 private static final String CLASS_NAME = BuildMetadataProtoFromXml.class.getSimpleName(); 49 private static final String PACKAGE_NAME = BuildMetadataProtoFromXml.class.getPackage().getName(); 50 51 // Command line parameter names. 52 private static final String INPUT_FILE = "input-file"; 53 private static final String OUTPUT_DIR = "output-dir"; 54 private static final String DATA_PREFIX = "data-prefix"; 55 private static final String MAPPING_CLASS = "mapping-class"; 56 private static final String COPYRIGHT = "copyright"; 57 private static final String SINGLE_FILE = "single-file"; 58 private static final String LITE_BUILD = "lite-build"; 59 // Only supported for clients who have consulted with the libphonenumber team, and the behavior is 60 // subject to change without notice. 61 private static final String SPECIAL_BUILD = "special-build"; 62 63 private static final String HELP_MESSAGE = 64 "Usage: " + CLASS_NAME + " [OPTION]...\n" + 65 "\n" + 66 " --" + INPUT_FILE + "=PATH Read phone number metadata in XML format from PATH.\n" + 67 " --" + OUTPUT_DIR + "=PATH Use PATH as the root directory for output files.\n" + 68 " --" + DATA_PREFIX + 69 "=PATH Use PATH (relative to " + OUTPUT_DIR + ") as the basename when\n" + 70 " writing phone number metadata in proto format.\n" + 71 " One file per region will be written unless " + SINGLE_FILE + "\n" + 72 " is set, in which case a single file will be written with\n" + 73 " metadata for all regions.\n" + 74 " --" + MAPPING_CLASS + "=NAME Store country code mappings in the class NAME, which\n" + 75 " will be written to a file in " + OUTPUT_DIR + ".\n" + 76 " --" + COPYRIGHT + "=YEAR Use YEAR in generated copyright headers.\n" + 77 "\n" + 78 " [--" + SINGLE_FILE + "=<true|false>] Optional (default: false). Whether to write\n" + 79 " metadata to a single file, instead of one file\n" + 80 " per region.\n" + 81 " [--" + LITE_BUILD + "=<true|false>] Optional (default: false). In a lite build,\n" + 82 " certain metadata will be omitted. At this\n" + 83 " moment, example numbers information is omitted.\n" + 84 "\n" + 85 "Example command line invocation:\n" + 86 CLASS_NAME + " \\\n" + 87 " --" + INPUT_FILE + "=resources/PhoneNumberMetadata.xml \\\n" + 88 " --" + OUTPUT_DIR + "=java/libphonenumber/src/com/google/i18n/phonenumbers \\\n" + 89 " --" + DATA_PREFIX + "=data/PhoneNumberMetadataProto \\\n" + 90 " --" + MAPPING_CLASS + "=CountryCodeToRegionCodeMap \\\n" + 91 " --" + COPYRIGHT + "=2010 \\\n" + 92 " --" + SINGLE_FILE + "=false \\\n" + 93 " --" + LITE_BUILD + "=false\n"; 94 95 private static final String GENERATION_COMMENT = 96 "/* This file is automatically generated by {@link " + CLASS_NAME + "}.\n" + 97 " * Please don't modify it directly.\n" + 98 " */\n\n"; 99 100 @Override getCommandName()101 public String getCommandName() { 102 return CLASS_NAME; 103 } 104 105 @Override start()106 public boolean start() { 107 // The format of a well-formed command line parameter. 108 Pattern pattern = Pattern.compile("--(.+?)=(.*)"); 109 110 String inputFile = null; 111 String outputDir = null; 112 String dataPrefix = null; 113 String mappingClass = null; 114 String copyright = null; 115 boolean singleFile = false; 116 boolean liteBuild = false; 117 boolean specialBuild = false; 118 119 for (int i = 1; i < getArgs().length; i++) { 120 String key = null; 121 String value = null; 122 Matcher matcher = pattern.matcher(getArgs()[i]); 123 if (matcher.matches()) { 124 key = matcher.group(1); 125 value = matcher.group(2); 126 } 127 128 if (INPUT_FILE.equals(key)) { 129 inputFile = value; 130 } else if (OUTPUT_DIR.equals(key)) { 131 outputDir = value; 132 } else if (DATA_PREFIX.equals(key)) { 133 dataPrefix = value; 134 } else if (MAPPING_CLASS.equals(key)) { 135 mappingClass = value; 136 } else if (COPYRIGHT.equals(key)) { 137 copyright = value; 138 } else if (SINGLE_FILE.equals(key) && 139 ("true".equalsIgnoreCase(value) || "false".equalsIgnoreCase(value))) { 140 singleFile = "true".equalsIgnoreCase(value); 141 } else if (LITE_BUILD.equals(key) && 142 ("true".equalsIgnoreCase(value) || "false".equalsIgnoreCase(value))) { 143 liteBuild = "true".equalsIgnoreCase(value); 144 } else if (SPECIAL_BUILD.equals(key) && 145 ("true".equalsIgnoreCase(value) || "false".equalsIgnoreCase(value))) { 146 specialBuild = "true".equalsIgnoreCase(value); 147 } else { 148 System.err.println(HELP_MESSAGE); 149 System.err.println("Illegal command line parameter: " + getArgs()[i]); 150 return false; 151 } 152 } 153 154 if (inputFile == null || 155 outputDir == null || 156 dataPrefix == null || 157 mappingClass == null || 158 copyright == null) { 159 System.err.println(HELP_MESSAGE); 160 return false; 161 } 162 163 String filePrefix = new File(outputDir, dataPrefix).getPath(); 164 165 try { 166 PhoneMetadataCollection metadataCollection = 167 BuildMetadataFromXml.buildPhoneMetadataCollection(inputFile, liteBuild, specialBuild); 168 169 if (singleFile) { 170 FileOutputStream output = new FileOutputStream(filePrefix); 171 ObjectOutputStream out = new ObjectOutputStream(output); 172 metadataCollection.writeExternal(out); 173 out.close(); 174 } else { 175 for (PhoneMetadata metadata : metadataCollection.getMetadataList()) { 176 String regionCode = metadata.getId(); 177 // For non-geographical country calling codes (e.g. +800), or for alternate formats, use the 178 // country calling codes instead of the region code to form the file name. 179 if (regionCode.equals("001") || regionCode.isEmpty()) { 180 regionCode = Integer.toString(metadata.getCountryCode()); 181 } 182 PhoneMetadataCollection outMetadataCollection = new PhoneMetadataCollection(); 183 outMetadataCollection.addMetadata(metadata); 184 FileOutputStream outputForRegion = new FileOutputStream(filePrefix + "_" + regionCode); 185 ObjectOutputStream out = new ObjectOutputStream(outputForRegion); 186 outMetadataCollection.writeExternal(out); 187 out.close(); 188 } 189 } 190 191 Map<Integer, List<String>> countryCodeToRegionCodeMap = 192 BuildMetadataFromXml.buildCountryCodeToRegionCodeMap(metadataCollection); 193 194 writeCountryCallingCodeMappingToJavaFile( 195 countryCodeToRegionCodeMap, outputDir, mappingClass, copyright); 196 } catch (Exception e) { 197 e.printStackTrace(); 198 return false; 199 } 200 System.out.println("Metadata code successfully generated."); 201 return true; 202 } 203 204 private static final String MAP_COMMENT = 205 " // A mapping from a country code to the region codes which denote the\n" + 206 " // country/region represented by that country code. In the case of multiple\n" + 207 " // countries sharing a calling code, such as the NANPA countries, the one\n" + 208 " // indicated with \"isMainCountryForCode\" in the metadata should be first.\n"; 209 private static final String COUNTRY_CODE_SET_COMMENT = 210 " // A set of all country codes for which data is available.\n"; 211 private static final String REGION_CODE_SET_COMMENT = 212 " // A set of all region codes for which data is available.\n"; 213 private static final double CAPACITY_FACTOR = 0.75; 214 private static final String CAPACITY_COMMENT = 215 " // The capacity is set to %d as there are %d different entries,\n" + 216 " // and this offers a load factor of roughly " + CAPACITY_FACTOR + ".\n"; 217 writeCountryCallingCodeMappingToJavaFile( Map<Integer, List<String>> countryCodeToRegionCodeMap, String outputDir, String mappingClass, String copyright)218 private static void writeCountryCallingCodeMappingToJavaFile( 219 Map<Integer, List<String>> countryCodeToRegionCodeMap, 220 String outputDir, String mappingClass, String copyright) throws IOException { 221 // Find out whether the countryCodeToRegionCodeMap has any region codes or country 222 // calling codes listed in it. 223 boolean hasRegionCodes = false; 224 for (List<String> listWithRegionCode : countryCodeToRegionCodeMap.values()) { 225 if (!listWithRegionCode.isEmpty()) { 226 hasRegionCodes = true; 227 break; 228 } 229 } 230 boolean hasCountryCodes = countryCodeToRegionCodeMap.size() > 1; 231 232 ClassWriter writer = new ClassWriter(outputDir, mappingClass, copyright); 233 234 int capacity = (int) (countryCodeToRegionCodeMap.size() / CAPACITY_FACTOR); 235 if (hasRegionCodes && hasCountryCodes) { 236 writeMap(writer, capacity, countryCodeToRegionCodeMap); 237 } else if (hasCountryCodes) { 238 writeCountryCodeSet(writer, capacity, countryCodeToRegionCodeMap.keySet()); 239 } else { 240 List<String> regionCodeList = countryCodeToRegionCodeMap.get(0); 241 capacity = (int) (regionCodeList.size() / CAPACITY_FACTOR); 242 writeRegionCodeSet(writer, capacity, regionCodeList); 243 } 244 245 writer.writeToFile(); 246 } 247 writeMap(ClassWriter writer, int capacity, Map<Integer, List<String>> countryCodeToRegionCodeMap)248 private static void writeMap(ClassWriter writer, int capacity, 249 Map<Integer, List<String>> countryCodeToRegionCodeMap) { 250 writer.addToBody(MAP_COMMENT); 251 252 writer.addToImports("java.util.ArrayList"); 253 writer.addToImports("java.util.HashMap"); 254 writer.addToImports("java.util.List"); 255 writer.addToImports("java.util.Map"); 256 257 writer.addToBody(" public static Map<Integer, List<String>> getCountryCodeToRegionCodeMap() {\n"); 258 writer.formatToBody(CAPACITY_COMMENT, capacity, countryCodeToRegionCodeMap.size()); 259 writer.addToBody(" Map<Integer, List<String>> countryCodeToRegionCodeMap =\n"); 260 writer.addToBody(" new HashMap<Integer, List<String>>(" + capacity + ");\n"); 261 writer.addToBody("\n"); 262 writer.addToBody(" ArrayList<String> listWithRegionCode;\n"); 263 writer.addToBody("\n"); 264 265 for (Map.Entry<Integer, List<String>> entry : countryCodeToRegionCodeMap.entrySet()) { 266 int countryCallingCode = entry.getKey(); 267 List<String> regionCodes = entry.getValue(); 268 writer.addToBody(" listWithRegionCode = new ArrayList<String>(" + 269 regionCodes.size() + ");\n"); 270 for (String regionCode : regionCodes) { 271 writer.addToBody(" listWithRegionCode.add(\"" + regionCode + "\");\n"); 272 } 273 writer.addToBody(" countryCodeToRegionCodeMap.put(" + countryCallingCode + 274 ", listWithRegionCode);\n"); 275 writer.addToBody("\n"); 276 } 277 278 writer.addToBody(" return countryCodeToRegionCodeMap;\n"); 279 writer.addToBody(" }\n"); 280 } 281 writeRegionCodeSet(ClassWriter writer, int capacity, List<String> regionCodeList)282 private static void writeRegionCodeSet(ClassWriter writer, int capacity, 283 List<String> regionCodeList) { 284 writer.addToBody(REGION_CODE_SET_COMMENT); 285 286 writer.addToImports("java.util.HashSet"); 287 writer.addToImports("java.util.Set"); 288 289 writer.addToBody(" public static Set<String> getRegionCodeSet() {\n"); 290 writer.formatToBody(CAPACITY_COMMENT, capacity, regionCodeList.size()); 291 writer.addToBody(" Set<String> regionCodeSet = new HashSet<String>(" + capacity + ");\n"); 292 writer.addToBody("\n"); 293 294 for (String regionCode : regionCodeList) { 295 writer.addToBody(" regionCodeSet.add(\"" + regionCode + "\");\n"); 296 } 297 298 writer.addToBody("\n"); 299 writer.addToBody(" return regionCodeSet;\n"); 300 writer.addToBody(" }\n"); 301 } 302 writeCountryCodeSet(ClassWriter writer, int capacity, Set<Integer> countryCodeSet)303 private static void writeCountryCodeSet(ClassWriter writer, int capacity, 304 Set<Integer> countryCodeSet) { 305 writer.addToBody(COUNTRY_CODE_SET_COMMENT); 306 307 writer.addToImports("java.util.HashSet"); 308 writer.addToImports("java.util.Set"); 309 310 writer.addToBody(" public static Set<Integer> getCountryCodeSet() {\n"); 311 writer.formatToBody(CAPACITY_COMMENT, capacity, countryCodeSet.size()); 312 writer.addToBody(" Set<Integer> countryCodeSet = new HashSet<Integer>(" + capacity + ");\n"); 313 writer.addToBody("\n"); 314 315 for (int countryCallingCode : countryCodeSet) { 316 writer.addToBody(" countryCodeSet.add(" + countryCallingCode + ");\n"); 317 } 318 319 writer.addToBody("\n"); 320 writer.addToBody(" return countryCodeSet;\n"); 321 writer.addToBody(" }\n"); 322 } 323 324 private static final class ClassWriter { 325 private final String name; 326 private final String copyright; 327 328 private final SortedSet<String> imports; 329 private final StringBuffer body; 330 private final Formatter formatter; 331 private final Writer writer; 332 ClassWriter(String outputDir, String name, String copyright)333 ClassWriter(String outputDir, String name, String copyright) throws IOException { 334 this.name = name; 335 this.copyright = copyright; 336 337 imports = new TreeSet<String>(); 338 body = new StringBuffer(); 339 formatter = new Formatter(body); 340 writer = new BufferedWriter(new FileWriter(new File(outputDir, name + ".java"))); 341 } 342 addToImports(String name)343 void addToImports(String name) { 344 imports.add(name); 345 } 346 addToBody(CharSequence text)347 void addToBody(CharSequence text) { 348 body.append(text); 349 } 350 formatToBody(String format, Object... args)351 void formatToBody(String format, Object... args) { 352 formatter.format(format, args); 353 } 354 writeToFile()355 void writeToFile() throws IOException { 356 CopyrightNotice.writeTo(writer, Integer.valueOf(copyright)); 357 writer.write(GENERATION_COMMENT); 358 writer.write("package " + PACKAGE_NAME + ";\n\n"); 359 360 if (!imports.isEmpty()) { 361 for (String item : imports) { 362 writer.write("import " + item + ";\n"); 363 } 364 writer.write("\n"); 365 } 366 367 writer.write("public class " + name + " {\n"); 368 writer.write(body.toString()); 369 writer.write("}\n"); 370 371 writer.flush(); 372 writer.close(); 373 } 374 } 375 } 376