1 /*
2  * Copyright (C) 2010 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.NumberFormat;
20 import com.google.i18n.phonenumbers.Phonemetadata.PhoneMetadata;
21 import com.google.i18n.phonenumbers.Phonemetadata.PhoneMetadataCollection;
22 import com.google.i18n.phonenumbers.Phonemetadata.PhoneNumberDesc;
23 
24 import java.io.BufferedWriter;
25 import java.io.FileWriter;
26 import java.io.IOException;
27 import java.util.Formatter;
28 import java.util.List;
29 import java.util.Map;
30 
31 /**
32  * Tool to convert phone number metadata from the XML format to JSON format.
33  *
34  * @author Nikolaos Trogkanis
35  */
36 public class BuildMetadataJsonFromXml extends Command {
37   private static final String NAMESPACE = "i18n.phonenumbers.metadata";
38 
39   private static final String HELP_MESSAGE =
40       "Usage:\n" +
41       "BuildMetadataJsonFromXml <inputFile> <outputFile> [<liteBuild>] [<namespace>]\n" +
42       "\n" +
43       "where:\n" +
44       "  inputFile    The input file containing phone number metadata in XML format.\n" +
45       "  outputFile   The output file to contain phone number metadata in JSON format.\n" +
46       "  liteBuild    Whether to generate the lite-version of the metadata (default:\n" +
47       "               false). When set to true certain metadata will be omitted.\n" +
48       "               At this moment, example numbers information is omitted.\n" +
49       "  namespace    If present, the namespace to provide the metadata with (default:\n" +
50       "               " + NAMESPACE + ").\n" +
51       "\n" +
52       "Example command line invocation:\n" +
53       "BuildMetadataJsonFromXml PhoneNumberMetadata.xml metadatalite.js true i18n.phonenumbers.testmetadata\n";
54 
55   private static final String FILE_OVERVIEW =
56       "/**\n"
57       + " * @fileoverview Generated metadata for file\n"
58       + " * %s\n"
59       + " * @author Nikolaos Trogkanis\n"
60       + " */\n\n";
61 
62   private static final String COUNTRY_CODE_TO_REGION_CODE_MAP_COMMENT =
63       "/**\n"
64       + " * A mapping from a country calling code to the region codes which denote the\n"
65       + " * region represented by that country calling code. In the case of multiple\n"
66       + " * countries sharing a calling code, such as the NANPA regions, the one\n"
67       + " * indicated with \"isMainCountryForCode\" in the metadata should be first.\n"
68       + " * @type {!Object.<number, Array.<string>>}\n"
69       + " */\n";
70 
71   private static final String COUNTRY_TO_METADATA_COMMENT =
72       "/**\n"
73       + " * A mapping from a region code to the PhoneMetadata for that region.\n"
74       + " * @type {!Object.<string, Array>}\n"
75       + " */\n";
76 
77   private static final int COPYRIGHT_YEAR = 2010;
78 
79   @Override
getCommandName()80   public String getCommandName() {
81     return "BuildMetadataJsonFromXml";
82   }
83 
84   @Override
start()85   public boolean start() {
86     String[] args = getArgs();
87 
88     if (args.length != 3 && args.length != 4 && args.length != 5) {
89       System.err.println(HELP_MESSAGE);
90       return false;
91     }
92     String inputFile = args[1];
93     String outputFile = args[2];
94     boolean liteBuild = args.length > 3 && args[3].equals("true");
95     String namespace = args.length > 4 ? args[4] : NAMESPACE;
96     return start(inputFile, outputFile, liteBuild, namespace);
97   }
98 
start(String inputFile, String outputFile, boolean liteBuild)99   static boolean start(String inputFile, String outputFile, boolean liteBuild) {
100     return start(inputFile, outputFile, liteBuild, NAMESPACE);
101   }
102 
start(String inputFile, String outputFile, boolean liteBuild, String namespace)103   static boolean start(String inputFile, String outputFile, boolean liteBuild, String namespace) {
104     try {
105       PhoneMetadataCollection metadataCollection =
106           BuildMetadataFromXml.buildPhoneMetadataCollection(inputFile, liteBuild, false);
107       Map<Integer, List<String>> countryCodeToRegionCodeMap =
108           BuildMetadataFromXml.buildCountryCodeToRegionCodeMap(metadataCollection);
109 
110       BufferedWriter writer = new BufferedWriter(new FileWriter(outputFile));
111 
112       CopyrightNotice.writeTo(writer, COPYRIGHT_YEAR, true);
113       Formatter formatter = new Formatter(writer);
114       formatter.format(FILE_OVERVIEW, inputFile);
115 
116       writer.write("goog.provide('" + namespace + "');\n\n");
117 
118       writer.write(COUNTRY_CODE_TO_REGION_CODE_MAP_COMMENT);
119       writer.write(namespace + ".countryCodeToRegionCodeMap = ");
120       writeCountryCodeToRegionCodeMap(countryCodeToRegionCodeMap, writer);
121       writer.write(";\n\n");
122 
123       writer.write(COUNTRY_TO_METADATA_COMMENT);
124       writer.write(namespace + ".countryToMetadata = ");
125       writeCountryToMetadataMap(metadataCollection, writer);
126       writer.write(";\n");
127 
128       writer.flush();
129       writer.close();
130       formatter.close();
131     } catch (Exception e) {
132       e.printStackTrace();
133       return false;
134     }
135     return true;
136   }
137 
138   // Writes a PhoneMetadataCollection in JSON format.
writeCountryToMetadataMap(PhoneMetadataCollection metadataCollection, BufferedWriter writer)139   private static void writeCountryToMetadataMap(PhoneMetadataCollection metadataCollection,
140                                                 BufferedWriter writer) throws IOException {
141     writer.write("{\n");
142     boolean isFirstTimeInLoop = true;
143     for (PhoneMetadata metadata : metadataCollection.getMetadataList()) {
144       if (isFirstTimeInLoop) {
145         isFirstTimeInLoop = false;
146       } else {
147         writer.write(",");
148       }
149       String key = metadata.getId();
150       // For non-geographical country calling codes (e.g. +800), use the country calling codes
151       // instead of the region code as key in the map.
152       if (key.equals("001")) {
153         key = Integer.toString(metadata.getCountryCode());
154       }
155       JSArrayBuilder jsArrayBuilder = new JSArrayBuilder();
156       toJsArray(metadata, jsArrayBuilder);
157       writer.write("\"");
158       writer.write(key);
159       writer.write("\":");
160       writer.write(jsArrayBuilder.toString());
161     }
162     writer.write("}");
163   }
164 
165   // Writes a Map<Integer, List<String>> in JSON format.
writeCountryCodeToRegionCodeMap( Map<Integer, List<String>> countryCodeToRegionCodeMap, BufferedWriter writer)166   private static void writeCountryCodeToRegionCodeMap(
167       Map<Integer, List<String>> countryCodeToRegionCodeMap,
168       BufferedWriter writer) throws IOException {
169     writer.write("{\n");
170     boolean isFirstTimeInLoop = true;
171     for (Map.Entry<Integer, List<String>> entry : countryCodeToRegionCodeMap.entrySet()) {
172       if (isFirstTimeInLoop) {
173         isFirstTimeInLoop = false;
174       } else {
175         writer.write(",");
176       }
177       writer.write(Integer.toString(entry.getKey()));
178       writer.write(":");
179       JSArrayBuilder jsArrayBuilder = new JSArrayBuilder();
180       jsArrayBuilder.beginArray();
181       jsArrayBuilder.appendIterator(entry.getValue().iterator());
182       jsArrayBuilder.endArray();
183       writer.write(jsArrayBuilder.toString());
184     }
185     writer.write("}");
186   }
187 
188   // Converts NumberFormat to JSArray.
toJsArray(NumberFormat format, JSArrayBuilder jsArrayBuilder)189   private static void toJsArray(NumberFormat format, JSArrayBuilder jsArrayBuilder) {
190     jsArrayBuilder.beginArray();
191 
192     // missing 0
193     jsArrayBuilder.append(null);
194     // required string pattern = 1;
195     jsArrayBuilder.append(format.getPattern());
196     // required string format = 2;
197     jsArrayBuilder.append(format.getFormat());
198     // repeated string leading_digits_pattern = 3;
199     int leadingDigitsPatternSize = format.leadingDigitsPatternSize();
200     if (leadingDigitsPatternSize > 0) {
201       jsArrayBuilder.beginArray();
202       for (int i = 0; i < leadingDigitsPatternSize; i++) {
203         jsArrayBuilder.append(format.getLeadingDigitsPattern(i));
204       }
205       jsArrayBuilder.endArray();
206     } else {
207       jsArrayBuilder.append(null);
208     }
209     // optional string national_prefix_formatting_rule = 4;
210     if (format.hasNationalPrefixFormattingRule()) {
211       jsArrayBuilder.append(format.getNationalPrefixFormattingRule());
212     } else {
213       jsArrayBuilder.append(null);
214     }
215     // optional string domestic_carrier_code_formatting_rule = 5;
216     if (format.hasDomesticCarrierCodeFormattingRule()) {
217       jsArrayBuilder.append(format.getDomesticCarrierCodeFormattingRule());
218     } else {
219       jsArrayBuilder.append(null);
220     }
221     // optional bool national_prefix_optional_when_formatting = 6 [default = false];
222     if (format.hasNationalPrefixOptionalWhenFormatting()) {
223       jsArrayBuilder.append(format.getNationalPrefixOptionalWhenFormatting());
224     } else {
225       jsArrayBuilder.append(null);
226     }
227 
228     jsArrayBuilder.endArray();
229   }
230 
231   // Converts PhoneNumberDesc to JSArray.
toJsArray(PhoneNumberDesc desc, JSArrayBuilder jsArrayBuilder)232   private static void toJsArray(PhoneNumberDesc desc, JSArrayBuilder jsArrayBuilder) {
233     if (desc == null) {
234       // Some descriptions are optional; in these cases we just append null and return if they are
235       // absent.
236       jsArrayBuilder.append(null);
237       return;
238     }
239     jsArrayBuilder.beginArray();
240 
241     // missing 0
242     jsArrayBuilder.append(null);
243     // missing 1
244     jsArrayBuilder.append(null);
245     // optional string national_number_pattern = 2;
246     if (desc.hasNationalNumberPattern()) {
247       jsArrayBuilder.append(desc.getNationalNumberPattern());
248     } else {
249       jsArrayBuilder.append(null);
250     }
251     // missing 3
252     jsArrayBuilder.append(null);
253     // missing 4
254     jsArrayBuilder.append(null);
255     // missing 5
256     jsArrayBuilder.append(null);
257     // optional string example_number = 6;
258     if (desc.hasExampleNumber()) {
259       jsArrayBuilder.append(desc.getExampleNumber());
260     } else {
261       jsArrayBuilder.append(null);
262     }
263     // missing 7
264     jsArrayBuilder.append(null);
265     // missing 8
266     jsArrayBuilder.append(null);
267     // repeated int32 possible_length = 9;
268     int possibleLengthSize = desc.getPossibleLengthCount();
269     if (possibleLengthSize > 0) {
270       jsArrayBuilder.beginArray();
271       for (int i = 0; i < possibleLengthSize; i++) {
272         jsArrayBuilder.append(desc.getPossibleLength(i));
273       }
274       jsArrayBuilder.endArray();
275     } else {
276       jsArrayBuilder.append(null);
277     }
278     // repeated int32 possible_length = 10;
279     int possibleLengthLocalOnlySize = desc.getPossibleLengthLocalOnlyCount();
280     if (possibleLengthLocalOnlySize > 0) {
281       jsArrayBuilder.beginArray();
282       for (int i = 0; i < possibleLengthLocalOnlySize; i++) {
283         jsArrayBuilder.append(desc.getPossibleLengthLocalOnly(i));
284       }
285       jsArrayBuilder.endArray();
286     } else {
287       jsArrayBuilder.append(null);
288     }
289 
290     jsArrayBuilder.endArray();
291   }
292 
293   // Converts PhoneMetadata to JSArray.
toJsArray(PhoneMetadata metadata, JSArrayBuilder jsArrayBuilder)294   private static void toJsArray(PhoneMetadata metadata, JSArrayBuilder jsArrayBuilder) {
295     jsArrayBuilder.beginArray();
296 
297     // missing 0
298     jsArrayBuilder.append(null);
299     // optional PhoneNumberDesc general_desc = 1;
300     toJsArray(metadata.getGeneralDesc(), jsArrayBuilder);
301     // optional PhoneNumberDesc fixed_line = 2;
302     toJsArray(metadata.getFixedLine(), jsArrayBuilder);
303     // optional PhoneNumberDesc mobile = 3;
304     toJsArray(metadata.getMobile(), jsArrayBuilder);
305     // optional PhoneNumberDesc toll_free = 4;
306     toJsArray(metadata.getTollFree(), jsArrayBuilder);
307     // optional PhoneNumberDesc premium_rate = 5;
308     toJsArray(metadata.getPremiumRate(), jsArrayBuilder);
309     // optional PhoneNumberDesc shared_cost = 6;
310     toJsArray(metadata.getSharedCost(), jsArrayBuilder);
311     // optional PhoneNumberDesc personal_number = 7;
312     toJsArray(metadata.getPersonalNumber(), jsArrayBuilder);
313     // optional PhoneNumberDesc voip = 8;
314     toJsArray(metadata.getVoip(), jsArrayBuilder);
315     // required string id = 9;
316     jsArrayBuilder.append(metadata.getId());
317     // optional int32 country_code = 10;
318     if (metadata.hasCountryCode()) {
319       jsArrayBuilder.append(metadata.getCountryCode());
320     } else {
321       jsArrayBuilder.append(null);
322     }
323     // optional string international_prefix = 11;
324     if (metadata.hasInternationalPrefix()) {
325       jsArrayBuilder.append(metadata.getInternationalPrefix());
326     } else {
327       jsArrayBuilder.append(null);
328     }
329 
330     // optional string national_prefix = 12;
331     if (metadata.hasNationalPrefix()) {
332       jsArrayBuilder.append(metadata.getNationalPrefix());
333     } else {
334       jsArrayBuilder.append(null);
335     }
336     // optional string preferred_extn_prefix = 13;
337     if (metadata.hasPreferredExtnPrefix()) {
338       jsArrayBuilder.append(metadata.getPreferredExtnPrefix());
339     } else {
340       jsArrayBuilder.append(null);
341     }
342     // missing 14
343     jsArrayBuilder.append(null);
344     // optional string national_prefix_for_parsing = 15;
345     if (metadata.hasNationalPrefixForParsing()) {
346       jsArrayBuilder.append(metadata.getNationalPrefixForParsing());
347     } else {
348       jsArrayBuilder.append(null);
349     }
350     // optional string national_prefix_transform_rule = 16;
351     if (metadata.hasNationalPrefixTransformRule()) {
352       jsArrayBuilder.append(metadata.getNationalPrefixTransformRule());
353     } else {
354       jsArrayBuilder.append(null);
355     }
356     // optional string preferred_international_prefix = 17;
357     if (metadata.hasPreferredInternationalPrefix()) {
358       jsArrayBuilder.append(metadata.getPreferredInternationalPrefix());
359     } else {
360       jsArrayBuilder.append(null);
361     }
362     // optional bool same_mobile_and_fixed_line_pattern = 18 [default=false];
363     if (metadata.hasSameMobileAndFixedLinePattern()) {
364       jsArrayBuilder.append(metadata.getSameMobileAndFixedLinePattern());
365     } else {
366       jsArrayBuilder.append(null);
367     }
368     // repeated NumberFormat number_format = 19;
369     int numberFormatSize = metadata.numberFormatSize();
370     if (numberFormatSize > 0) {
371       jsArrayBuilder.beginArray();
372       for (int i = 0; i < numberFormatSize; i++) {
373         toJsArray(metadata.getNumberFormat(i), jsArrayBuilder);
374       }
375       jsArrayBuilder.endArray();
376     } else {
377       jsArrayBuilder.append(null);
378     }
379     // repeated NumberFormat intl_number_format = 20;
380     int intlNumberFormatSize = metadata.intlNumberFormatSize();
381     if (intlNumberFormatSize > 0) {
382       jsArrayBuilder.beginArray();
383       for (int i = 0; i < intlNumberFormatSize; i++) {
384         toJsArray(metadata.getIntlNumberFormat(i), jsArrayBuilder);
385       }
386       jsArrayBuilder.endArray();
387     } else {
388       jsArrayBuilder.append(null);
389     }
390     // optional PhoneNumberDesc pager = 21;
391     toJsArray(metadata.getPager(), jsArrayBuilder);
392     // optional bool main_country_for_code = 22 [default=false];
393     if (metadata.isMainCountryForCode()) {
394       jsArrayBuilder.append(1);
395     } else {
396       jsArrayBuilder.append(null);
397     }
398     // optional string leading_digits = 23;
399     if (metadata.hasLeadingDigits()) {
400       jsArrayBuilder.append(metadata.getLeadingDigits());
401     } else {
402       jsArrayBuilder.append(null);
403     }
404     // optional PhoneNumberDesc no_international_dialling = 24;
405     toJsArray(metadata.getNoInternationalDialling(), jsArrayBuilder);
406     // optional PhoneNumberDesc uan = 25;
407     toJsArray(metadata.getUan(), jsArrayBuilder);
408     // optional bool leading_zero_possible = 26 [default=false];
409     if (metadata.isLeadingZeroPossible()) {
410       jsArrayBuilder.append(1);
411     } else {
412       jsArrayBuilder.append(null);
413     }
414     // optional PhoneNumberDesc emergency = 27;
415     toJsArray(metadata.getEmergency(), jsArrayBuilder);
416     // optional PhoneNumberDesc voicemail = 28;
417     toJsArray(metadata.getVoicemail(), jsArrayBuilder);
418     // optional PhoneNumberDesc short_code = 29;
419     toJsArray(metadata.getShortCode(), jsArrayBuilder);
420     // optional PhoneNumberDesc standard_rate = 30;
421     toJsArray(metadata.getStandardRate(), jsArrayBuilder);
422     // optional PhoneNumberDesc carrier_specific = 31;
423     toJsArray(metadata.getCarrierSpecific(), jsArrayBuilder);
424     // optional bool mobile_number_portable_region = 32 [default=false];
425     // left as null because this data is not used in the current JS API's.
426     jsArrayBuilder.append(null);
427     // optional PhoneNumberDesc sms_services = 33;
428     toJsArray(metadata.getSmsServices(), jsArrayBuilder);
429 
430     jsArrayBuilder.endArray();
431   }
432 }
433