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 deleteAllFilesForPrefix(filePrefix); 176 for (PhoneMetadata metadata : metadataCollection.getMetadataList()) { 177 String regionCode = metadata.getId(); 178 // For non-geographical country calling codes (e.g. +800), or for alternate formats, use the 179 // country calling codes instead of the region code to form the file name. 180 if (regionCode.equals("001") || regionCode.isEmpty()) { 181 regionCode = Integer.toString(metadata.getCountryCode()); 182 } 183 PhoneMetadataCollection outMetadataCollection = new PhoneMetadataCollection(); 184 outMetadataCollection.addMetadata(metadata); 185 FileOutputStream outputForRegion = new FileOutputStream(filePrefix + "_" + regionCode); 186 ObjectOutputStream out = new ObjectOutputStream(outputForRegion); 187 outMetadataCollection.writeExternal(out); 188 out.close(); 189 } 190 System.out.println("Generated " + metadataCollection.getMetadataCount() + " new files"); 191 } 192 193 Map<Integer, List<String>> countryCodeToRegionCodeMap = 194 BuildMetadataFromXml.buildCountryCodeToRegionCodeMap(metadataCollection); 195 196 writeCountryCallingCodeMappingToJavaFile( 197 countryCodeToRegionCodeMap, outputDir, mappingClass, copyright); 198 } catch (Exception e) { 199 e.printStackTrace(); 200 return false; 201 } 202 System.out.println("Metadata code successfully created."); 203 return true; 204 } 205 deleteAllFilesForPrefix(String filePrefix)206 private void deleteAllFilesForPrefix(String filePrefix) { 207 File[] allFiles = new File(filePrefix).getParentFile().listFiles(); 208 if (allFiles == null) { 209 allFiles = new File[0]; 210 } 211 int counter = 0; 212 for (File file: allFiles) { 213 if (file.getAbsolutePath().contains(filePrefix)) { 214 if (file.delete()) { 215 counter++; 216 } 217 } 218 } 219 System.out.println("Deleted " + counter + " old files"); 220 } 221 222 private static final String MAP_COMMENT = 223 " // A mapping from a country code to the region codes which denote the\n" + 224 " // country/region represented by that country code. In the case of multiple\n" + 225 " // countries sharing a calling code, such as the NANPA countries, the one\n" + 226 " // indicated with \"isMainCountryForCode\" in the metadata should be first.\n"; 227 private static final String COUNTRY_CODE_SET_COMMENT = 228 " // A set of all country codes for which data is available.\n"; 229 private static final String REGION_CODE_SET_COMMENT = 230 " // A set of all region codes for which data is available.\n"; 231 private static final double CAPACITY_FACTOR = 0.75; 232 private static final String CAPACITY_COMMENT = 233 " // The capacity is set to %d as there are %d different entries,\n" + 234 " // and this offers a load factor of roughly " + CAPACITY_FACTOR + ".\n"; 235 writeCountryCallingCodeMappingToJavaFile( Map<Integer, List<String>> countryCodeToRegionCodeMap, String outputDir, String mappingClass, String copyright)236 private static void writeCountryCallingCodeMappingToJavaFile( 237 Map<Integer, List<String>> countryCodeToRegionCodeMap, 238 String outputDir, String mappingClass, String copyright) throws IOException { 239 // Find out whether the countryCodeToRegionCodeMap has any region codes or country 240 // calling codes listed in it. 241 boolean hasRegionCodes = false; 242 for (List<String> listWithRegionCode : countryCodeToRegionCodeMap.values()) { 243 if (!listWithRegionCode.isEmpty()) { 244 hasRegionCodes = true; 245 break; 246 } 247 } 248 boolean hasCountryCodes = countryCodeToRegionCodeMap.size() > 1; 249 250 ClassWriter writer = new ClassWriter(outputDir, mappingClass, copyright); 251 252 int capacity = (int) (countryCodeToRegionCodeMap.size() / CAPACITY_FACTOR); 253 if (hasRegionCodes && hasCountryCodes) { 254 writeMap(writer, capacity, countryCodeToRegionCodeMap); 255 } else if (hasCountryCodes) { 256 writeCountryCodeSet(writer, capacity, countryCodeToRegionCodeMap.keySet()); 257 } else { 258 List<String> regionCodeList = countryCodeToRegionCodeMap.get(0); 259 capacity = (int) (regionCodeList.size() / CAPACITY_FACTOR); 260 writeRegionCodeSet(writer, capacity, regionCodeList); 261 } 262 263 writer.writeToFile(); 264 } 265 writeMap(ClassWriter writer, int capacity, Map<Integer, List<String>> countryCodeToRegionCodeMap)266 private static void writeMap(ClassWriter writer, int capacity, 267 Map<Integer, List<String>> countryCodeToRegionCodeMap) { 268 writer.addToBody(MAP_COMMENT); 269 270 writer.addToImports("java.util.ArrayList"); 271 writer.addToImports("java.util.HashMap"); 272 writer.addToImports("java.util.List"); 273 writer.addToImports("java.util.Map"); 274 275 writer.addToBody(" public static Map<Integer, List<String>> getCountryCodeToRegionCodeMap() {\n"); 276 writer.formatToBody(CAPACITY_COMMENT, capacity, countryCodeToRegionCodeMap.size()); 277 writer.addToBody(" Map<Integer, List<String>> countryCodeToRegionCodeMap =\n"); 278 writer.addToBody(" new HashMap<Integer, List<String>>(" + capacity + ");\n"); 279 writer.addToBody("\n"); 280 writer.addToBody(" ArrayList<String> listWithRegionCode;\n"); 281 writer.addToBody("\n"); 282 283 for (Map.Entry<Integer, List<String>> entry : countryCodeToRegionCodeMap.entrySet()) { 284 int countryCallingCode = entry.getKey(); 285 List<String> regionCodes = entry.getValue(); 286 writer.addToBody(" listWithRegionCode = new ArrayList<String>(" + 287 regionCodes.size() + ");\n"); 288 for (String regionCode : regionCodes) { 289 writer.addToBody(" listWithRegionCode.add(\"" + regionCode + "\");\n"); 290 } 291 writer.addToBody(" countryCodeToRegionCodeMap.put(" + countryCallingCode + 292 ", listWithRegionCode);\n"); 293 writer.addToBody("\n"); 294 } 295 296 writer.addToBody(" return countryCodeToRegionCodeMap;\n"); 297 writer.addToBody(" }\n"); 298 } 299 writeRegionCodeSet(ClassWriter writer, int capacity, List<String> regionCodeList)300 private static void writeRegionCodeSet(ClassWriter writer, int capacity, 301 List<String> regionCodeList) { 302 writer.addToBody(REGION_CODE_SET_COMMENT); 303 304 writer.addToImports("java.util.HashSet"); 305 writer.addToImports("java.util.Set"); 306 307 writer.addToBody(" public static Set<String> getRegionCodeSet() {\n"); 308 writer.formatToBody(CAPACITY_COMMENT, capacity, regionCodeList.size()); 309 writer.addToBody(" Set<String> regionCodeSet = new HashSet<String>(" + capacity + ");\n"); 310 writer.addToBody("\n"); 311 312 for (String regionCode : regionCodeList) { 313 writer.addToBody(" regionCodeSet.add(\"" + regionCode + "\");\n"); 314 } 315 316 writer.addToBody("\n"); 317 writer.addToBody(" return regionCodeSet;\n"); 318 writer.addToBody(" }\n"); 319 } 320 writeCountryCodeSet(ClassWriter writer, int capacity, Set<Integer> countryCodeSet)321 private static void writeCountryCodeSet(ClassWriter writer, int capacity, 322 Set<Integer> countryCodeSet) { 323 writer.addToBody(COUNTRY_CODE_SET_COMMENT); 324 325 writer.addToImports("java.util.HashSet"); 326 writer.addToImports("java.util.Set"); 327 328 writer.addToBody(" public static Set<Integer> getCountryCodeSet() {\n"); 329 writer.formatToBody(CAPACITY_COMMENT, capacity, countryCodeSet.size()); 330 writer.addToBody(" Set<Integer> countryCodeSet = new HashSet<Integer>(" + capacity + ");\n"); 331 writer.addToBody("\n"); 332 333 for (int countryCallingCode : countryCodeSet) { 334 writer.addToBody(" countryCodeSet.add(" + countryCallingCode + ");\n"); 335 } 336 337 writer.addToBody("\n"); 338 writer.addToBody(" return countryCodeSet;\n"); 339 writer.addToBody(" }\n"); 340 } 341 342 private static final class ClassWriter { 343 private final String name; 344 private final String copyright; 345 346 private final SortedSet<String> imports; 347 private final StringBuffer body; 348 private final Formatter formatter; 349 private final Writer writer; 350 ClassWriter(String outputDir, String name, String copyright)351 ClassWriter(String outputDir, String name, String copyright) throws IOException { 352 this.name = name; 353 this.copyright = copyright; 354 355 imports = new TreeSet<String>(); 356 body = new StringBuffer(); 357 formatter = new Formatter(body); 358 writer = new BufferedWriter(new FileWriter(new File(outputDir, name + ".java"))); 359 } 360 addToImports(String name)361 void addToImports(String name) { 362 imports.add(name); 363 } 364 addToBody(CharSequence text)365 void addToBody(CharSequence text) { 366 body.append(text); 367 } 368 formatToBody(String format, Object... args)369 void formatToBody(String format, Object... args) { 370 formatter.format(format, args); 371 } 372 writeToFile()373 void writeToFile() throws IOException { 374 CopyrightNotice.writeTo(writer, Integer.valueOf(copyright)); 375 writer.write(GENERATION_COMMENT); 376 writer.write("package " + PACKAGE_NAME + ";\n\n"); 377 378 if (!imports.isEmpty()) { 379 for (String item : imports) { 380 writer.write("import " + item + ";\n"); 381 } 382 writer.write("\n"); 383 } 384 385 writer.write("public class " + name + " {\n"); 386 writer.write(body.toString()); 387 writer.write("}\n"); 388 389 writer.flush(); 390 writer.close(); 391 } 392 } 393 } 394