• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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.i18n.phonenumbers.metadata.DigitSequence;
20 import com.google.i18n.phonenumbers.metadata.RangeSpecification;
21 import com.google.i18n.phonenumbers.metadata.model.RangesTableSchema;
22 import com.google.i18n.phonenumbers.metadata.table.Change;
23 import com.google.i18n.phonenumbers.metadata.table.Column;
24 import com.google.i18n.phonenumbers.metadata.table.CsvKeyMarshaller;
25 import com.google.i18n.phonenumbers.metadata.table.CsvSchema;
26 import com.google.i18n.phonenumbers.metadata.table.CsvTable;
27 import com.google.i18n.phonenumbers.metadata.table.RangeKey;
28 import com.google.i18n.phonenumbers.metadata.table.RangeTable;
29 import com.google.i18n.phonenumbers.metadata.table.RangeTable.OverwriteMode;
30 import com.google.i18n.phonenumbers.metadata.table.Schema;
31 import java.util.Collections;
32 import java.util.List;
33 import java.util.Optional;
34 import java.util.Set;
35 
36 /**
37  * The schema of the standard "Recipes" table with rows keyed by {@link RangeKey} and columns:
38  * <ol>
39  *   <li>{@link #OLD_FORMAT}: The original format of the represented range in the row to be changed.
40  *       'x' characters represent indexes that do not need to be changed in a number within the range
41  *       and actual digits in the string are values that need to be removed or replaced. (e.g. xx98xx).
42  *       The length of this string must match the lengths of (DigitSequence)'s produced by the Row Key.
43  *   <li>{@link #NEW_FORMAT}: The migrated format of the represented range in the row. 'x' characters
44  *       represent indexes that do not need to be changed in a number within the range and actual
45  *       digits in the string are values that need to be added at that given index.
46  *   <li>{@link #IS_FINAL_MIGRATION}: A boolean indicating whether the given recipe row would result
47  *       in the represented range being migrated into up to date, dialable formats. Recipes which
48  *       do not will require the newly formatted range to be migrated again using another matching
49  *       recipe.
50  *   <li>{@link #COUNTRY_CODE}: The BCP-47 country code in which a given recipe corresponds to.
51  *   <li>{@link #DESCRIPTION}: TThe explanation of a migration recipe in words.
52  * </ol>
53  *
54  * <p>Rows keys are serialized via the marshaller and produce leading columns:
55  * <ol>
56  *   <li>{@code "Old Prefix"}: The prefix (RangeSpecification) for the original ranges in a row
57  *   (e.g. "44123").
58  *   <li>{@code "Old Length"}: The length for the original ranges in a row (e.g. "9", "8" or "5").
59  * </ol>
60  */
61 public class RecipesTableSchema {
62 
63   /** The format of the original numbers in a given range. */
64   public static final Column<String> OLD_FORMAT = Column.ofString("Old Format");
65 
66   /** The new format of the migrated numbers in a given range. */
67   public static final Column<String> NEW_FORMAT = Column.ofString("New Format");
68 
69   /** The BCP-47 country code the given recipe belongs to. */
70   public static final Column<DigitSequence> COUNTRY_CODE =
71       Column.create(DigitSequence.class, "Country Code", DigitSequence.empty(), DigitSequence::of);
72 
73   /** Indicates whether a given recipe will result in a valid, dialable range */
74   public static final Column<Boolean> IS_FINAL_MIGRATION = Column.ofBoolean("Is Final Migration");
75 
76   /** The explanation of a migration recipe in words. */
77   public static final Column<String> DESCRIPTION = Column.ofString("Description");
78 
79   /** Marshaller for constructing CsvTable from RangeTable. */
80   private static final CsvKeyMarshaller<RangeKey> MARSHALLER = new CsvKeyMarshaller<>(
81       RangesTableSchema::write,
82       // uses a read method that will only allow a single numerical value in the 'Old Length' column
83       RecipesTableSchema::read,
84       Optional.of(RangeKey.ORDERING),
85       "Old Prefix",
86       "Old Length");
87 
88   /**
89    * Instantiates a {@link RangeKey} from the prefix and length columns of a given recipe
90    * row.
91    *
92    * @throws IllegalArgumentException when the 'Old Length' value is anything other than a number
93    */
read(List<String> parts)94   public static RangeKey read(List<String> parts) {
95     Set<Integer> rangeKeyLength;
96 
97     try {
98       rangeKeyLength = Collections.singleton(Integer.parseInt(parts.get(1)));
99     } catch (NumberFormatException e) {
100       throw new IllegalArgumentException("Invalid number '" + parts.get(1) + "' in column 'Old Length'");
101     }
102     return RangeKey.create(RangeSpecification.parse(parts.get(0)), rangeKeyLength);
103   }
104 
105   /** The columns for the serialized CSV table. */
106   private static final Schema CSV_COLUMNS =
107       Schema.builder()
108           .add(COUNTRY_CODE)
109           .add(OLD_FORMAT)
110           .add(NEW_FORMAT)
111           .add(IS_FINAL_MIGRATION)
112           .add(DESCRIPTION)
113           .build();
114 
115   /** Schema instance defining the ranges CSV table. */
116   public static final CsvSchema<RangeKey> SCHEMA = CsvSchema.of(MARSHALLER, CSV_COLUMNS);
117 
118   /** The non-key columns of a range table. */
119   private static final Schema RANGE_COLUMNS =
120       Schema.builder()
121           .add(COUNTRY_CODE)
122           .add(OLD_FORMAT)
123           .add(NEW_FORMAT)
124           .add(IS_FINAL_MIGRATION)
125           .add(DESCRIPTION)
126           .build();
127 
128   /**
129    * Converts a {@link RangeKey} based {@link CsvTable} to a {@link RangeTable}, preserving the
130    * original table columns. The {@link CsvSchema} of the returned table is not guaranteed to be
131    * the {@link #SCHEMA} instance if the given table had different columns.
132    */
toRangeTable(CsvTable<RangeKey> csv)133   public static RangeTable toRangeTable(CsvTable<RangeKey> csv) {
134     RangeTable.Builder out = RangeTable.builder(RANGE_COLUMNS);
135     for (RangeKey k : csv.getKeys()) {
136       Change.Builder change = Change.builder(k.asRangeTree());
137       csv.getRow(k).forEach(change::assign);
138       out.apply(change.build(), OverwriteMode.NEVER);
139     }
140     return out.build();
141   }
142 
143   /**
144    * Converts a {@link RangeTable} to a {@link CsvTable}, using {@link RangeKey}s as row keys and
145    * preserving the original table columns. The {@link CsvSchema} of the returned table is not
146    * guaranteed to be the {@link #SCHEMA} instance if the given table had different columns.
147    */
148   @SuppressWarnings("unchecked")
toCsv(RangeTable table)149   public static CsvTable<RangeKey> toCsv(RangeTable table) {
150     CsvTable.Builder<RangeKey> csv = CsvTable.builder(SCHEMA);
151     for (Change c : table.toChanges()) {
152       for (RangeKey k : RangeKey.decompose(c.getRanges())) {
153         c.getAssignments().forEach(a -> csv.put(k, a));
154       }
155     }
156     return csv.build();
157   }
158 
159   /** Converts recipe into format more human-friendly than the default ImmutableMap toString(). */
formatRecipe(ImmutableMap<Column<?>, Object> recipe)160   public static String formatRecipe(ImmutableMap<Column<?>, Object> recipe) {
161     StringBuilder formattedRecipe = new StringBuilder();
162     for (Column<?> column : recipe.keySet()) {
163       String columnValue = column.getName() + ": " + recipe.get(column) + "  |  ";
164       formattedRecipe.append(columnValue);
165     }
166     return formattedRecipe.toString();
167   }
168 }
169