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