1 /* 2 * Copyright (C) 2011 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 * @author Shaopeng Jia 17 */ 18 19 package com.google.phonenumbers; 20 21 import static java.nio.charset.StandardCharsets.UTF_8; 22 import static java.util.Locale.ENGLISH; 23 24 import com.google.i18n.phonenumbers.AsYouTypeFormatter; 25 import com.google.i18n.phonenumbers.NumberParseException; 26 import com.google.i18n.phonenumbers.PhoneNumberToCarrierMapper; 27 import com.google.i18n.phonenumbers.PhoneNumberToTimeZonesMapper; 28 import com.google.i18n.phonenumbers.PhoneNumberUtil; 29 import com.google.i18n.phonenumbers.PhoneNumberUtil.PhoneNumberFormat; 30 import com.google.i18n.phonenumbers.PhoneNumberUtil.PhoneNumberType; 31 import com.google.i18n.phonenumbers.PhoneNumberUtil.ValidationResult; 32 import com.google.i18n.phonenumbers.Phonenumber.PhoneNumber; 33 import com.google.i18n.phonenumbers.ShortNumberInfo; 34 import com.google.i18n.phonenumbers.geocoding.PhoneNumberOfflineGeocoder; 35 import java.io.IOException; 36 import java.io.InputStream; 37 import java.io.UnsupportedEncodingException; 38 import java.net.URLEncoder; 39 import java.util.Locale; 40 import java.util.StringTokenizer; 41 import javax.servlet.http.HttpServlet; 42 import javax.servlet.http.HttpServletRequest; 43 import javax.servlet.http.HttpServletResponse; 44 import org.apache.commons.fileupload.FileItemIterator; 45 import org.apache.commons.fileupload.FileItemStream; 46 import org.apache.commons.fileupload.FileUploadException; 47 import org.apache.commons.fileupload.servlet.ServletFileUpload; 48 import org.apache.commons.fileupload.util.Streams; 49 import org.apache.commons.io.IOUtils; 50 import org.apache.commons.lang.StringEscapeUtils; 51 52 /** 53 * A servlet that accepts requests that contain strings representing a phone number and a default 54 * country, and responds with results from parsing, validating and formatting the number. The 55 * default country is a two-letter region code representing the country that we are expecting the 56 * number to be from. 57 */ 58 @SuppressWarnings("serial") 59 public class PhoneNumberParserServlet extends HttpServlet { 60 private PhoneNumberUtil phoneUtil = PhoneNumberUtil.getInstance(); 61 private ShortNumberInfo shortInfo = ShortNumberInfo.getInstance(); 62 doPost(HttpServletRequest req, HttpServletResponse resp)63 public void doPost(HttpServletRequest req, HttpServletResponse resp) throws IOException { 64 String phoneNumber = null; 65 String defaultCountry = null; 66 String languageCode = "en"; // Default languageCode to English if nothing is entered. 67 String regionCode = ""; 68 String fileContents = null; 69 ServletFileUpload upload = new ServletFileUpload(); 70 upload.setSizeMax(50000); 71 try { 72 FileItemIterator iterator = upload.getItemIterator(req); 73 while (iterator.hasNext()) { 74 FileItemStream item = iterator.next(); 75 InputStream in = item.openStream(); 76 if (item.isFormField()) { 77 String fieldName = item.getFieldName(); 78 if (fieldName.equals("phoneNumber")) { 79 phoneNumber = Streams.asString(in, UTF_8.name()); 80 } else if (fieldName.equals("defaultCountry")) { 81 defaultCountry = Streams.asString(in).toUpperCase(); 82 } else if (fieldName.equals("languageCode")) { 83 String languageEntered = Streams.asString(in).toLowerCase(); 84 if (languageEntered.length() > 0) { 85 languageCode = languageEntered; 86 } 87 } else if (fieldName.equals("regionCode")) { 88 regionCode = Streams.asString(in).toUpperCase(); 89 } 90 } else { 91 try { 92 fileContents = IOUtils.toString(in); 93 } finally { 94 IOUtils.closeQuietly(in); 95 } 96 } 97 } 98 } catch (FileUploadException e1) { 99 e1.printStackTrace(); 100 } 101 102 StringBuilder output; 103 resp.setContentType("text/html"); 104 resp.setCharacterEncoding(UTF_8.name()); 105 if (fileContents == null || fileContents.length() == 0) { 106 // Redirect to a URL with the given input encoded in the query parameters. 107 Locale geocodingLocale = new Locale(languageCode, regionCode); 108 resp.sendRedirect( 109 getPermaLinkURL(phoneNumber, defaultCountry, geocodingLocale, false /* absoluteURL */)); 110 } else { 111 resp.getWriter().println(getOutputForFile(defaultCountry, fileContents)); 112 } 113 } 114 115 /** Handle the get request to get information about a number based on query parameters. */ doGet(HttpServletRequest req, HttpServletResponse resp)116 public void doGet(HttpServletRequest req, HttpServletResponse resp) throws IOException { 117 String phoneNumber = req.getParameter("number"); 118 if (phoneNumber == null) { 119 phoneNumber = ""; 120 } 121 String defaultCountry = req.getParameter("country"); 122 if (defaultCountry == null) { 123 defaultCountry = ""; 124 } 125 String geocodingParam = req.getParameter("geocodingLocale"); 126 Locale geocodingLocale; 127 if (geocodingParam == null) { 128 geocodingLocale = ENGLISH; // Default languageCode to English if nothing is entered. 129 } else { 130 geocodingLocale = Locale.forLanguageTag(geocodingParam); 131 } 132 resp.setContentType("text/html"); 133 resp.setCharacterEncoding(UTF_8.name()); 134 resp.getWriter() 135 .println(getOutputForSingleNumber(phoneNumber, defaultCountry, geocodingLocale)); 136 } 137 getOutputForFile(String defaultCountry, String fileContents)138 private StringBuilder getOutputForFile(String defaultCountry, String fileContents) { 139 StringBuilder output = 140 new StringBuilder( 141 "<HTML><HEAD><TITLE>Results generated from phone numbers in the file provided:" 142 + "</TITLE></HEAD><BODY>"); 143 output.append("<TABLE align=center border=1>"); 144 output.append("<TH align=center>ID</TH>"); 145 output.append("<TH align=center>Raw phone number</TH>"); 146 output.append("<TH align=center>Pretty formatting</TH>"); 147 output.append("<TH align=center>International format</TH>"); 148 149 int phoneNumberId = 0; 150 StringTokenizer tokenizer = new StringTokenizer(fileContents, ","); 151 while (tokenizer.hasMoreTokens()) { 152 String numberStr = tokenizer.nextToken(); 153 phoneNumberId++; 154 output.append("<TR>"); 155 output.append("<TD align=center>").append(phoneNumberId).append(" </TD> \n"); 156 output 157 .append("<TD align=center>") 158 .append(StringEscapeUtils.escapeHtml(numberStr)) 159 .append(" </TD> \n"); 160 try { 161 PhoneNumber number = phoneUtil.parseAndKeepRawInput(numberStr, defaultCountry); 162 boolean isNumberValid = phoneUtil.isValidNumber(number); 163 String prettyFormat = 164 isNumberValid ? phoneUtil.formatInOriginalFormat(number, defaultCountry) : "invalid"; 165 String internationalFormat = 166 isNumberValid ? phoneUtil.format(number, PhoneNumberFormat.INTERNATIONAL) : "invalid"; 167 168 output 169 .append("<TD align=center>") 170 .append(StringEscapeUtils.escapeHtml(prettyFormat)) 171 .append(" </TD> \n"); 172 output 173 .append("<TD align=center>") 174 .append(StringEscapeUtils.escapeHtml(internationalFormat)) 175 .append(" </TD> \n"); 176 } catch (NumberParseException e) { 177 output 178 .append("<TD align=center colspan=2>") 179 .append(StringEscapeUtils.escapeHtml(e.toString())) 180 .append(" </TD> \n"); 181 } 182 output.append("</TR>"); 183 } 184 output.append("</BODY></HTML>"); 185 return output; 186 } 187 appendLine(String title, String data, StringBuilder output)188 private void appendLine(String title, String data, StringBuilder output) { 189 output.append("<TR>"); 190 output.append("<TH>").append(title).append("</TH>"); 191 output.append("<TD>").append(data.length() > 0 ? data : " ").append("</TD>"); 192 output.append("</TR>"); 193 } 194 195 /** Returns a stable URL pointing to the result page for the given input. */ getPermaLinkURL( String phoneNumber, String defaultCountry, Locale geocodingLocale, boolean absoluteURL)196 private String getPermaLinkURL( 197 String phoneNumber, String defaultCountry, Locale geocodingLocale, boolean absoluteURL) { 198 // If absoluteURL is false, generate a relative path. Otherwise, produce an absolute URL. 199 StringBuilder permaLink = 200 new StringBuilder( 201 absoluteURL 202 ? "http://libphonenumber.appspot.com/phonenumberparser" 203 : "/phonenumberparser"); 204 try { 205 permaLink.append( 206 "?number=" + URLEncoder.encode(phoneNumber != null ? phoneNumber : "", UTF_8.name())); 207 if (defaultCountry != null && !defaultCountry.isEmpty()) { 208 permaLink.append("&country=" + URLEncoder.encode(defaultCountry, UTF_8.name())); 209 } 210 if (!geocodingLocale.getLanguage().equals(ENGLISH.getLanguage()) 211 || !geocodingLocale.getCountry().isEmpty()) { 212 permaLink.append( 213 "&geocodingLocale=" + URLEncoder.encode(geocodingLocale.toLanguageTag(), UTF_8.name())); 214 } 215 } catch (UnsupportedEncodingException e) { 216 // UTF-8 is guaranteed in Java, so this should be impossible. 217 throw new AssertionError(e); 218 } 219 return permaLink.toString(); 220 } 221 222 private static final String NEW_ISSUE_BASE_URL = 223 "https://issuetracker.google.com/issues/new?component=192347&title="; 224 225 /** Returns a link to create a new github issue with the relevant information. */ getNewIssueLink( String phoneNumber, String defaultCountry, Locale geocodingLocale)226 private String getNewIssueLink( 227 String phoneNumber, String defaultCountry, Locale geocodingLocale) { 228 boolean hasDefaultCountry = !defaultCountry.isEmpty() && defaultCountry != "ZZ"; 229 String issueTitle = 230 "Validation issue with " 231 + phoneNumber 232 + (hasDefaultCountry ? " (" + defaultCountry + ")" : ""); 233 234 String newIssueLink = NEW_ISSUE_BASE_URL; 235 try { 236 newIssueLink += URLEncoder.encode(issueTitle, UTF_8.name()); 237 } catch (UnsupportedEncodingException e) { 238 // UTF-8 is guaranteed in Java, so this should be impossible. 239 throw new AssertionError(e); 240 } 241 return newIssueLink; 242 } 243 244 /** 245 * The defaultCountry here is used for parsing phoneNumber. The geocodingLocale is used to specify 246 * the language used for displaying the area descriptions generated from phone number geocoding. 247 */ getOutputForSingleNumber( String phoneNumber, String defaultCountry, Locale geocodingLocale)248 private StringBuilder getOutputForSingleNumber( 249 String phoneNumber, String defaultCountry, Locale geocodingLocale) { 250 StringBuilder output = new StringBuilder("<HTML><HEAD>"); 251 output.append("<LINK type=\"text/css\" rel=\"stylesheet\" href=\"/stylesheets/main.css\" />"); 252 output.append("</HEAD>"); 253 output.append("<BODY>"); 254 output.append("Phone Number entered: " + StringEscapeUtils.escapeHtml(phoneNumber) + "<BR>"); 255 output.append( 256 "defaultCountry entered: " + StringEscapeUtils.escapeHtml(defaultCountry) + "<BR>"); 257 output.append( 258 "Language entered: " 259 + StringEscapeUtils.escapeHtml(geocodingLocale.toLanguageTag()) 260 + "<BR>"); 261 try { 262 PhoneNumber number = phoneUtil.parseAndKeepRawInput(phoneNumber, defaultCountry); 263 output.append("<DIV>"); 264 output.append("<TABLE border=1>"); 265 output.append("<TR><TD colspan=2>Parsing Result (parseAndKeepRawInput())</TD></TR>"); 266 267 appendLine("country_code", Integer.toString(number.getCountryCode()), output); 268 appendLine("national_number", Long.toString(number.getNationalNumber()), output); 269 appendLine("extension", number.getExtension(), output); 270 appendLine("country_code_source", number.getCountryCodeSource().toString(), output); 271 appendLine("italian_leading_zero", Boolean.toString(number.isItalianLeadingZero()), output); 272 appendLine("raw_input", number.getRawInput(), output); 273 output.append("</TABLE>"); 274 output.append("</DIV>"); 275 276 boolean isPossible = phoneUtil.isPossibleNumber(number); 277 boolean isNumberValid = phoneUtil.isValidNumber(number); 278 PhoneNumberType numberType = phoneUtil.getNumberType(number); 279 boolean hasDefaultCountry = !defaultCountry.isEmpty() && defaultCountry != "ZZ"; 280 281 output.append("<DIV>"); 282 output.append("<TABLE border=1>"); 283 output.append("<TR><TD colspan=2>Validation Results</TD></TR>"); 284 appendLine("Result from isPossibleNumber()", Boolean.toString(isPossible), output); 285 if (isPossible) { 286 if (phoneUtil.isPossibleNumberWithReason(number) 287 == ValidationResult.IS_POSSIBLE_LOCAL_ONLY) { 288 appendLine("Result from isPossibleNumberWithReason()", "IS_POSSIBLE_LOCAL_ONLY", output); 289 output.append( 290 "<TR><TD colspan=2>Number is considered invalid as it is " 291 + "not a possible national number.</TD></TR>"); 292 } else { 293 appendLine("Result from isValidNumber()", Boolean.toString(isNumberValid), output); 294 if (isNumberValid && hasDefaultCountry) { 295 appendLine( 296 "Result from isValidNumberForRegion()", 297 Boolean.toString(phoneUtil.isValidNumberForRegion(number, defaultCountry)), 298 output); 299 } 300 String region = phoneUtil.getRegionCodeForNumber(number); 301 appendLine("Phone Number region", region == null ? "" : region, output); 302 appendLine("Result from getNumberType()", numberType.toString(), output); 303 } 304 } else { 305 appendLine( 306 "Result from isPossibleNumberWithReason()", 307 phoneUtil.isPossibleNumberWithReason(number).toString(), 308 output); 309 output.append( 310 "<TR><TD colspan=2>Note: Numbers that are not possible have type UNKNOWN," 311 + " an unknown region, and are considered invalid.</TD></TR>"); 312 } 313 output.append("</TABLE>"); 314 output.append("</DIV>"); 315 316 if (!isNumberValid) { 317 output.append("<DIV>"); 318 output.append("<TABLE border=1>"); 319 output.append("<TR><TD colspan=2>Short Number Results</TD></TR>"); 320 boolean isPossibleShort = shortInfo.isPossibleShortNumber(number); 321 appendLine( 322 "Result from isPossibleShortNumber()", Boolean.toString(isPossibleShort), output); 323 if (isPossibleShort) { 324 appendLine( 325 "Result from isValidShortNumber()", 326 Boolean.toString(shortInfo.isValidShortNumber(number)), 327 output); 328 if (hasDefaultCountry) { 329 boolean isPossibleShortForRegion = 330 shortInfo.isPossibleShortNumberForRegion(number, defaultCountry); 331 appendLine( 332 "Result from isPossibleShortNumberForRegion()", 333 Boolean.toString(isPossibleShortForRegion), 334 output); 335 if (isPossibleShortForRegion) { 336 appendLine( 337 "Result from isValidShortNumberForRegion()", 338 Boolean.toString(shortInfo.isValidShortNumberForRegion(number, defaultCountry)), 339 output); 340 } 341 } 342 } 343 output.append("</TABLE>"); 344 output.append("</DIV>"); 345 } 346 347 output.append("<DIV>"); 348 output.append("<TABLE border=1>"); 349 output.append("<TR><TD colspan=2>Formatting Results</TD></TR>"); 350 appendLine( 351 "E164 format", 352 isNumberValid ? phoneUtil.format(number, PhoneNumberFormat.E164) : "invalid", 353 output); 354 appendLine( 355 "Original format", phoneUtil.formatInOriginalFormat(number, defaultCountry), output); 356 appendLine("National format", phoneUtil.format(number, PhoneNumberFormat.NATIONAL), output); 357 appendLine( 358 "International format", 359 isNumberValid ? phoneUtil.format(number, PhoneNumberFormat.INTERNATIONAL) : "invalid", 360 output); 361 appendLine( 362 "Out-of-country format from US", 363 isNumberValid ? phoneUtil.formatOutOfCountryCallingNumber(number, "US") : "invalid", 364 output); 365 appendLine( 366 "Out-of-country format from CH", 367 isNumberValid ? phoneUtil.formatOutOfCountryCallingNumber(number, "CH") : "invalid", 368 output); 369 output.append("</TABLE>"); 370 output.append("</DIV>"); 371 372 AsYouTypeFormatter formatter = phoneUtil.getAsYouTypeFormatter(defaultCountry); 373 int rawNumberLength = phoneNumber.length(); 374 output.append("<DIV>"); 375 output.append("<TABLE border=1>"); 376 output.append("<TR><TD colspan=2>AsYouTypeFormatter Results</TD></TR>"); 377 for (int i = 0; i < rawNumberLength; i++) { 378 // Note this doesn't handle supplementary characters, but it shouldn't be a big deal as 379 // there are no dial-pad characters in the supplementary range. 380 char inputChar = phoneNumber.charAt(i); 381 appendLine( 382 "Char entered: '" + inputChar + "' Output: ", formatter.inputDigit(inputChar), output); 383 } 384 output.append("</TABLE>"); 385 output.append("</DIV>"); 386 387 if (isNumberValid) { 388 output.append("<DIV>"); 389 output.append("<TABLE border=1>"); 390 output.append("<TR><TD colspan=2>PhoneNumberOfflineGeocoder Results</TD></TR>"); 391 appendLine( 392 "Location", 393 PhoneNumberOfflineGeocoder.getInstance() 394 .getDescriptionForNumber(number, geocodingLocale), 395 output); 396 output.append("</TABLE>"); 397 output.append("</DIV>"); 398 399 output.append("<DIV>"); 400 output.append("<TABLE border=1>"); 401 output.append("<TR><TD colspan=2>PhoneNumberToTimeZonesMapper Results</TD></TR>"); 402 appendLine( 403 "Time zone(s)", 404 PhoneNumberToTimeZonesMapper.getInstance().getTimeZonesForNumber(number).toString(), 405 output); 406 output.append("</TABLE>"); 407 output.append("</DIV>"); 408 409 if (numberType == PhoneNumberType.MOBILE 410 || numberType == PhoneNumberType.FIXED_LINE_OR_MOBILE 411 || numberType == PhoneNumberType.PAGER) { 412 output.append("<DIV>"); 413 output.append("<TABLE border=1>"); 414 output.append("<TR><TD colspan=2>PhoneNumberToCarrierMapper Results</TD></TR>"); 415 appendLine( 416 "Carrier", 417 PhoneNumberToCarrierMapper.getInstance().getNameForNumber(number, geocodingLocale), 418 output); 419 output.append("</TABLE>"); 420 output.append("</DIV>"); 421 } 422 } 423 424 String newIssueLink = getNewIssueLink(phoneNumber, defaultCountry, geocodingLocale); 425 String guidelinesLink = 426 "https://github.com/google/libphonenumber/blob/master/CONTRIBUTING.md"; 427 output.append( 428 "<b style=\"color:red\">File an issue</b>: by clicking on " 429 + "<a target=\"_blank\" href=\"" 430 + newIssueLink 431 + "\">this link</a>, I confirm that I " 432 + "have read the <a target=\"_blank\" href=\"" 433 + guidelinesLink 434 + "\">contributor's guidelines</a>."); 435 } catch (NumberParseException e) { 436 output.append(StringEscapeUtils.escapeHtml(e.toString())); 437 } 438 output.append("</BODY></HTML>"); 439 return output; 440 } 441 } 442