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