1 /*
2  * Copyright (C) 2020 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 package com.google.phonenumbers.migrator;
17 
18 import com.google.common.collect.ImmutableMap;
19 import com.google.common.collect.Multimap;
20 import com.google.i18n.phonenumbers.metadata.table.Column;
21 import com.google.phonenumbers.migrator.MigrationJob.MigrationReport;
22 import java.io.IOException;
23 import java.nio.file.Files;
24 import java.nio.file.Path;
25 import java.nio.file.Paths;
26 import java.util.Scanner;
27 import picocli.CommandLine;
28 import picocli.CommandLine.ArgGroup;
29 import picocli.CommandLine.Command;
30 import picocli.CommandLine.Help.Ansi;
31 import picocli.CommandLine.Option;
32 
33 @Command(name = "Command Line Migrator Tool:",
34     description =  "Please enter a path to a text file containing E.164 phone numbers "
35       + "(e.g. +4434567891, +1234568890) from the same country or a single E.164 number as "
36       + "well as the corresponding BCP-47 country code (e.g. 44, 1) to begin migrations.\n")
37 public final class CommandLineMain {
38   /**
39    * Fields cannot be private or final to allow for @Command annotation to set and retrieve values.
40    */
41 
42   @ArgGroup(multiplicity = "1")
43   NumberInputType numberInput;
44 
45   static class NumberInputType {
46     @Option(names = {"-n", "--number"},
47         description = "Single E.164 phone number to migrate (e.g. \"+1234567890\") or an internationally formatted "
48             + "number starting with '+' (e.g. \"+12 (345) 67-890\") which will have any non-digit characters after "
49             + "the leading '+' removed for processing.")
50     String number;
51 
52     @Option(names = {"-f", "--file"},
53         description = "Text file to be migrated which contains one E.164 phone "
54             + "number per line")
55     String file;
56   }
57 
58   @Option(names = {"-c", "--countryCode"},
59       required = true,
60       description = "The BCP-47 country code the given phone number(s) belong to (e.g. 44)")
61   String countryCode;
62 
63   @ArgGroup()
64   OptionalParameterType optionalParameter;
65 
66   static class OptionalParameterType {
67     @Option(names = {"-e", "--exportInvalidMigrations"},
68         description = "boolean flag specifying that text files created after the migration process"
69             + " for standard recipe --file migrations should contain the migrated version of a given"
70             + " phone number, regardless of whether the migration resulted in an invalid phone number."
71             + " By default, a strict approach is used and when a migration is seen as invalid, the"
72             + " original phone number is written to file. Invalid numbers will be printed at the"
73             + " bottom of the text file.")
74     boolean exportInvalidMigrations;
75 
76     @Option(names = {"-r", "--customRecipe"},
77         description = "Csv file containing a custom migration recipes table. When using custom recipes"
78             + ", validity checks on migrated numbers will not be performed. Note: custom recipes must"
79             + " be run with the --exportInvalidMigrations flag.")
80     String customRecipe;
81   }
82 
83   @Option(names = {"-h", "--help"}, description = "Display help", usageHelp = true)
84   boolean help;
85 
main(String[] args)86   public static void main(String[] args) throws IOException {
87     CommandLineMain clm = CommandLine.populateCommand(new CommandLineMain(), args);
88     if (clm.help) {
89       CommandLine.usage(clm, System.out, Ansi.AUTO);
90     } else {
91       MigrationJob migrationJob;
92       if (clm.numberInput.number != null) {
93         if (clm.optionalParameter != null && clm.optionalParameter.customRecipe != null) {
94           migrationJob = MigrationFactory
95               .createCustomRecipeMigration(clm.numberInput.number, clm.countryCode,
96                       MigrationFactory.importRecipes(Files.newInputStream(Paths.get(clm.optionalParameter.customRecipe))));
97         } else {
98           migrationJob = MigrationFactory.createMigration(clm.numberInput.number, clm.countryCode);
99         }
100       } else {
101         if (clm.optionalParameter != null && clm.optionalParameter.customRecipe != null) {
102           migrationJob = MigrationFactory
103               .createCustomRecipeMigration(Paths.get(clm.numberInput.file), clm.countryCode,
104                       MigrationFactory.importRecipes(Files.newInputStream(Paths.get(clm.optionalParameter.customRecipe))));
105         } else {
106           migrationJob = MigrationFactory
107               .createMigration(Paths.get(clm.numberInput.file), clm.countryCode,
108                   clm.optionalParameter != null && clm.optionalParameter.exportInvalidMigrations);
109         }
110       }
111 
112       MigrationReport mr =  migrationJob.getMigrationReportForCountry();
113       System.out.println("Migration of country code +" + migrationJob.getCountryCode() + " phone "
114           + "number(s):");
115       if (clm.numberInput.file != null) {
116         printFileReport(mr, Paths.get(clm.numberInput.file));
117       } else {
118         printNumberReport(mr);
119       }
120     }
121   }
122 
123   /** Details printed to console after a --file type migration. */
printFileReport(MigrationReport mr, Path originalFile)124   private static void printFileReport(MigrationReport mr, Path originalFile) throws IOException {
125     String newFile = mr.exportToFile(originalFile.getFileName().toString());
126     Scanner scanner = new Scanner((System.in));
127     String response = "";
128 
129     System.out.println("New numbers file created at: " + System.getProperty("user.dir") + "/" + newFile);
130     while (!response.equals("0")) {
131       System.out.println("\n(0) Exit");
132       System.out.println("(1) Print Metrics");
133       System.out.println("(2) View All Recipes Used");
134       System.out.print("Select from the above options: ");
135       response = scanner.nextLine();
136       if (response.equals("1")) {
137         mr.printMetrics();
138       } else if (response.equals("2")) {
139         printUsedRecipes(mr);
140       }
141     }
142   }
143 
144   /** Details printed to console after a --number type migration. */
printNumberReport(MigrationReport mr)145   private static void printNumberReport(MigrationReport mr) {
146     if (mr.getValidMigrations().size() == 1) {
147       System.out.println("Successful migration into: +"
148           + mr.getValidMigrations().get(0).getMigratedNumber());
149       printUsedRecipes(mr);
150     } else if (mr.getInvalidMigrations().size() == 1) {
151       System.out.println("The number was migrated into '+"
152           + mr.getInvalidMigrations().get(0).getMigratedNumber() + "' but this number was not "
153           + "seen as being valid and dialable when inspecting our data for the given country");
154       printUsedRecipes(mr);
155     } else if (mr.getValidUntouchedEntries().size() == 1) {
156       System.out.println("This number was seen to already be valid and dialable based on "
157           + "our data for the given country");
158     } else {
159       System.out.println("This number could not be migrated using any of the recipes from the given"
160           + " recipes file");
161     }
162   }
163 
printUsedRecipes(MigrationReport mr)164   private static void printUsedRecipes(MigrationReport mr) {
165     Multimap<ImmutableMap<Column<?>, Object>, MigrationResult> recipeToNumbers = mr.getAllRecipesUsed();
166     System.out.println("\nRecipe(s) Used:");
167     for (ImmutableMap<Column<?>, Object> recipe : recipeToNumbers.keySet()) {
168       System.out.println("* " + RecipesTableSchema.formatRecipe(recipe));
169       recipeToNumbers.get(recipe).forEach(result -> System.out.println("\t" + result));
170       System.out.println("");
171     }
172   }
173 }
174