• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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