1<%@ page import="com.google.appengine.repackaged.com.google.gson.Gson" %> 2<%@ page import="com.google.common.collect.ImmutableList" %> 3<%@ page import="com.google.phonenumbers.ServletMain" %> 4<%@ page import="com.google.phonenumbers.migrator.MigrationEntry" %> 5<%@ page import="com.google.phonenumbers.migrator.MigrationResult" %> 6<!DOCTYPE html> 7<%@ page contentType="text/html;charset=UTF-8" language="java" %> 8<% 9 final String E164_NUMBERS_LINK = "https://support.twilio.com/hc/en-us/articles/223183008-Formatting-International-Phone-Numbers"; 10 final String COUNTRY_CODE_LINK = "https://countrycode.org/"; 11 // TODO: use documentation link from base repository when forked repository has been merged in 12 final String DOCUMENTATION_LINK = "https://github.com/TomiwaOke/libphonenumber/tree/master/migrator/README.md"; 13 final String ISSUE_TRACKER_LINK = "https://issuetracker.google.com/issues/new?component=192347"; 14 final String GUIDELINES_LINK = "https://github.com/google/libphonenumber/blob/master/CONTRIBUTING.md#filing-a-code-issue"; 15 16 final Gson gson = new Gson(); 17 ImmutableList<MigrationResult> validMigrations = (ImmutableList<MigrationResult>) request.getAttribute("validMigrations"); 18 ImmutableList<MigrationResult> invalidMigrations = (ImmutableList<MigrationResult>) request.getAttribute("invalidMigrations"); 19 ImmutableList<MigrationEntry> validUntouchedNums = (ImmutableList<MigrationEntry>) request.getAttribute("validUntouchedNumbers"); 20 ImmutableList<MigrationEntry> invalidUntouchedNums = (ImmutableList<MigrationEntry>) request.getAttribute("invalidUntouchedNumbers"); 21%> 22<html> 23<head> 24 <link type="text/css" rel="stylesheet" href="/stylesheets/servlet-main.css" /> 25 <title>Migrator</title> 26 <script type="text/javascript" src="https://www.gstatic.com/charts/loader.js"></script> 27 <script type="text/javascript"> 28 const VALID_MIGRATIONS = 'Valid Migrations'; 29 const INVALID_MIGRATIONS = 'Invalid Migrations'; 30 const UNTOUCHED_VALID = 'Already Valid Numbers'; 31 const UNTOUCHED_INVALID = 'Invalid Non-migratable Numbers'; 32 33 const CHART_DESCRIPTIONS = new Map(); 34 CHART_DESCRIPTIONS[VALID_MIGRATIONS] = 'The following are numbers that were successfully migrated by the tool:'; 35 CHART_DESCRIPTIONS[INVALID_MIGRATIONS] = 'The following are numbers that were migrated by the tool but were not able' + 36 ' to be verified as valid numbers based on metadata for the given country code:'; 37 CHART_DESCRIPTIONS[UNTOUCHED_VALID] = 'The following are numbers that were already in valid formats:'; 38 CHART_DESCRIPTIONS[UNTOUCHED_INVALID] = 'The following numbers were not seen as valid and could not be migrated based' + 39 ' on the given country code:'; 40 41 function getNumbersForSegment(selection) { 42 if (selection === VALID_MIGRATIONS) { 43 return <%=gson.toJson(ServletMain.getMigrationResultOutputList(validMigrations))%>; 44 } else if (selection === INVALID_MIGRATIONS) { 45 return <%=gson.toJson(ServletMain.getMigrationResultOutputList(invalidMigrations))%>; 46 } else if (selection === UNTOUCHED_VALID) { 47 return <%=gson.toJson(ServletMain.getMigrationEntryOutputList(validUntouchedNums))%>; 48 } 49 return <%=gson.toJson(ServletMain.getMigrationEntryOutputList(invalidUntouchedNums))%>; 50 } 51 52 google.charts.load('current', {packages:['corechart']}); 53 google.charts.setOnLoadCallback(drawChart); 54 function drawChart() { 55 const chartData = google.visualization.arrayToDataTable([ 56 ['Task', 'Frequency'], 57 [VALID_MIGRATIONS, <%= validMigrations != null ? validMigrations.size() : 0%>], 58 [INVALID_MIGRATIONS, <%= invalidMigrations != null ? invalidMigrations.size() : 0%>], 59 [UNTOUCHED_VALID, <%= validUntouchedNums != null ? validUntouchedNums.size() : 0%>], 60 [UNTOUCHED_INVALID, <%= invalidUntouchedNums != null ? invalidUntouchedNums.size() : 0%>] 61 ]); 62 63 const chartProperties = { 64 pieHole: 0.4, 65 chartArea: { width: '90%', height: '100%' }, 66 colors: [ 67 <%=validMigrations != null && !validMigrations.isEmpty()%> ? '#277301' : '', 68 <%=invalidMigrations != null && !invalidMigrations.isEmpty()%> ? '#ffbf36' : '', 69 <%=validUntouchedNums != null && !validUntouchedNums.isEmpty()%> ? '#90ee90' : '', 70 <%=invalidUntouchedNums != null && !invalidUntouchedNums.isEmpty()%> ? '#ff472b' : ''] 71 }; 72 73 const modalBackdrop = document.getElementById("modalBackdrop"); 74 75 document.getElementById("modalButton").onclick = function() { 76 document.getElementById("numbersList").innerHTML = ''; 77 modalBackdrop.style.display = 'none'; 78 }; 79 80 window.onclick = function(event) { 81 if (event.target === modalBackdrop) { 82 document.getElementById("numbersList").innerHTML = ''; 83 modalBackdrop.style.display = 'none'; 84 } 85 }; 86 87 function onSegmentClick() { 88 const selection = chart.getSelection()[0]; 89 if (selection) { 90 const selectionName = chartData.getValue(selection.row, 0); 91 const numbersList = document.getElementById("numbersList"); 92 const segmentNumbers = getNumbersForSegment(selectionName); 93 94 segmentNumbers.forEach(number => { 95 const value = document.createElement('li'); 96 value.appendChild(document.createTextNode(number)); 97 numbersList.appendChild(value); 98 }); 99 document.getElementById("modalTitle").innerHTML = selectionName; 100 document.getElementById("modalDescription").innerHTML = CHART_DESCRIPTIONS[selectionName]; 101 modalBackdrop.style.display = 'block'; 102 } 103 } 104 105 const chart = new google.visualization.PieChart(document.getElementById('migration-chart')); 106 google.visualization.events.addListener(chart, 'select', onSegmentClick); 107 chart.draw(chartData, chartProperties); 108 } 109 </script> 110</head> 111<body> 112 <div class="page-heading"> 113 <h1>Phone Number Migrator</h1> 114 <p> 115 The migrator is a tool which takes in a given <a href="<%=E164_NUMBERS_LINK%>" target="_blank">E.164 phone number(s)</a> 116 input as well as the corresponding BCP-47 <a href="<%=COUNTRY_CODE_LINK%>" target="_blank">country code</a>. The tool 117 will then check the validity of the phone number based on the country code and if possible, will convert the number 118 into a valid, dialable format. 119 </p> 120 <p>The following are the two available migration types that can be performed:</p> 121 <ul> 122 <li> 123 <strong>Single Number Migration:</strong> input a single E.164 phone number with its corresponding BCP-47 country 124 code. If there is an available migration that can be performed on the number, it will be converted to the new 125 format based on the specified migration rules.<br><br> 126 </li> 127 <li> 128 <strong>File Migration:</strong> input a text file containing one E.164 number per line along with the BCP-47 129 country code that corresponds to the numbers in the text file. All numbers in the text file that match available 130 migrations will be migrated and there will be the option of downloading a new text file containing the updated numbers. 131 By default, invalid migrations and numbers that did not go through a process of migration will be written to file 132 in their original text file format. 133 <br><br> 134 </li> 135 </ul> 136 <p> 137 For more information on the capabilities of the migrator as well as instructions on how to install the command line 138 tool, please view the <a href="<%=DOCUMENTATION_LINK%>" target="_blank">documentation</a>. 139 </p> 140 </div> 141 142 <div class="migration-result"> 143 <% 144 if (request.getAttribute("numberError") == null && request.getAttribute("number") != null) { 145 if (request.getAttribute("validMigration") != null) { 146 out.print("<h3 class='valid'>Valid +" + request.getAttribute("numberCountryCode") + " Phone Number Produced!</h3>"); 147 out.print("<p>The stale number '" + request.getAttribute("number") + "' was successfully migrated into the" + 148 " phone number: +" + request.getAttribute("validMigration") + "</p>"); 149 } else if (request.getAttribute("invalidMigration") != null) { 150 out.print("<h3 class='invalid-migration'>Invalid +" + request.getAttribute("numberCountryCode") + " Migration</h3>"); 151 out.print("<p>The stale number '" + request.getAttribute("number") + "' was migrated into the phone number:" + 152 " +" + request.getAttribute("invalidMigration") + ". However this was not seen as valid using our internal" + 153 " metadata for country code +" + request.getAttribute("numberCountryCode") + ".</p>"); 154 } else if (request.getAttribute("alreadyValidNumber") != null) { 155 out.print("<h3 class='valid'>Already Valid +" + request.getAttribute("numberCountryCode") + " Phone Number!</h3>"); 156 out.print("<p>The entered phone number was already seen as being in a valid, dialable format based on our" + 157 " metadata for country code +" + request.getAttribute("numberCountryCode") + ". Here is the number in" + 158 " its clean E.164 format: +" + request.getAttribute("alreadyValidNumber") + "</p>"); 159 } else { 160 out.print("<h3 class='invalid-number'>Non-migratable +" + request.getAttribute("numberCountryCode") + " Phone Number</h3>"); 161 out.print("<p>The phone number '" + request.getAttribute("number") + "' was not seen as a valid number and" + 162 " no migration recipe could be found for country code +" + request.getAttribute("numberCountryCode") + 163 " to migrate it. This may be because you have entered a country code which does not correctly correspond" + 164 " to the given phone number or the specified number has never been valid.</p>"); 165 } 166 out.print("<p style='color: red; font-size: 14px'>Think there's an issue? File one <a href='" + ISSUE_TRACKER_LINK + 167 "' target='_blank'>here</a> following the given <a href='" + GUIDELINES_LINK + "' target='_blank'>guidelines</a>.</p>"); 168 } else if (request.getAttribute("fileError") == null && request.getAttribute("fileName") != null) { 169 out.print("<h3>'" + request.getAttribute("fileName") + "' Migration Report for Country Code: +" + request.getAttribute("fileCountryCode") + "</h3>"); 170 out.print("<p>Below is a chart showing the ratio of numbers from the entered file that were able to be migrated" + 171 " using '+" + request.getAttribute("fileCountryCode") + "' migration recipes. To understand more," + 172 " select a given segment from the chart below.</p>"); 173 out.print("<div class='chart-wrap'><div id='migration-chart' class='chart'></div></div>"); 174 175 out.print("<form action='" + request.getContextPath() + "/migrate' method='get' style='margin-bottom: 1rem'>"); 176 out.print("<input type='hidden' name='countryCode' value='" + request.getAttribute("fileCountryCode") + "'/>"); 177 out.print("<input type='hidden' name='fileName' value='" + request.getAttribute("fileName") + "'/>"); 178 out.print("<input type='hidden' name='fileContent' value='" + request.getAttribute("fileContent") + "'/>"); 179 out.print("<input type='submit' value='Export Results' class='button'/>"); 180 out.print("</form>"); 181 } 182 %> 183 </div> 184 185 <div class="migration-forms"> 186 <div class="migration-form"> 187 <h3>Single Number Migration</h3> 188 <div class="error-message"><%=request.getAttribute("numberError") == null ? "" : request.getAttribute("numberError")%></div> 189 <form action="${pageContext.request.contextPath}/migrate" method="post" enctype="multipart/form-data"> 190 <label for="number">Phone number:</label> 191 <p>Enter a phone number in E.164 format. Inputted numbers can include spaces, curved brackets and hyphens</p> 192 <input type="text" name="number" id="number" placeholder="+841205555555" required 193 value="<%=request.getAttribute("number") == null ? "" : request.getAttribute("number")%>"/> 194 195 <label for="numberCountryCode">Country Code:</label> 196 <p>Enter the BCP-47 country code in which the specified E.164 phone number belongs to</p> 197 <input type="number" name="numberCountryCode" id="numberCountryCode" placeholder="84" required 198 value="<%=request.getAttribute("numberCountryCode") == null ? "" : request.getAttribute("numberCountryCode")%>"/> 199 200 <label for="numberCustomRecipe">Custom Recipe:</label> 201 <p> 202 (Optional) Upload a csv file containing a custom recipes table to be used for migrations. To understand how to 203 create a custom recipe file, please view the <a href="<%=DOCUMENTATION_LINK%>" target="_blank">documentation</a>. 204 </p> 205 <input type="file" name="customRecipe" id="numberCustomRecipe" accept=".csv"/> 206 207 <input type="submit" value="Migrate Number" class="button"/> 208 </form> 209 </div> 210 211 <div class="migration-form"> 212 <h3>File Migration</h3> 213 <div class="error-message"><%=request.getAttribute("fileError") == null ? "" : request.getAttribute("fileError")%></div> 214 <form action="${pageContext.request.contextPath}/migrate" method="post" enctype="multipart/form-data"> 215 <label for="file">File:</label> 216 <p>Upload a file containing one E.164 phone number per line. Numbers can include spaces, curved brackets and hyphens</p> 217 <input type="file" name="file" id="file" accept="text/plain" required/> 218 219 <label for="fileCountryCode">Country Code:</label> 220 <p>Enter the BCP-47 country code in which the E.164 phone numbers from the specified file belong to</p> 221 <input type="number" name="fileCountryCode" id="fileCountryCode" placeholder="84" required/> 222 223 <label for="fileCustomRecipe">Custom Recipe:</label> 224 <p> 225 (Optional) Upload a csv file containing a custom recipes table to be used for migrations. To understand how to 226 create a custom recipe file, please view the <a href="<%=DOCUMENTATION_LINK%>" target="_blank">documentation</a>. 227 </p> 228 <input type="file" name="customRecipe" id="fileCustomRecipe" accept=".csv"/> 229 230 <input type="submit" value="Migrate File" class="button"/> 231 </form> 232 </div> 233 </div> 234 235 <div id="modalBackdrop" class="modal-backdrop"> 236 <div class="modal-content"> 237 <h3 id="modalTitle"></h3> 238 <p id="modalDescription" style="color: grey; font-size: 12px"></p> 239 <div class="body"> 240 <ul id="numbersList" style="padding-left: 1.5rem"></ul> 241 <p style="color: red; font-size: 14px"> 242 Think there's an issue? File one <a href="<%=ISSUE_TRACKER_LINK%>" target="_blank">here</a> 243 following the given <a href="<%=GUIDELINES_LINK%>" target="_blank">guidelines</a>. 244 </p> 245 </div> 246 <button id="modalButton" class="button">Close</button> 247 </div> 248 </div> 249</body> 250</html> 251