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(); 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 */ 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.setContentType("text/html"); 136 resp.setCharacterEncoding(UTF_8.name()); 137 resp.getWriter().println( 138 getOutputForSingleNumber(phoneNumber, defaultCountry, geocodingLocale)); 139 } 140 141 private StringBuilder getOutputForFile(String defaultCountry, String fileContents) { 142 StringBuilder output = new StringBuilder( 143 "<HTML><HEAD><TITLE>Results generated from phone numbers in the file provided:" 144 + "</TITLE></HEAD><BODY>"); 145 output.append("<TABLE align=center border=1>"); 146 output.append("<TH align=center>ID</TH>"); 147 output.append("<TH align=center>Raw phone number</TH>"); 148 output.append("<TH align=center>Pretty formatting</TH>"); 149 output.append("<TH align=center>International format</TH>"); 150 151 int phoneNumberId = 0; 152 StringTokenizer tokenizer = new StringTokenizer(fileContents, ","); 153 while (tokenizer.hasMoreTokens()) { 154 String numberStr = tokenizer.nextToken(); 155 phoneNumberId++; 156 output.append("<TR>"); 157 output.append("<TD align=center>").append(phoneNumberId).append(" </TD> \n"); 158 output.append("<TD align=center>").append( 159 StringEscapeUtils.escapeHtml(numberStr)).append(" </TD> \n"); 160 try { 161 PhoneNumber number = phoneUtil.parseAndKeepRawInput(numberStr, defaultCountry); 162 boolean isNumberValid = phoneUtil.isValidNumber(number); 163 String prettyFormat = isNumberValid 164 ? phoneUtil.formatInOriginalFormat(number, defaultCountry) 165 : "invalid"; 166 String internationalFormat = isNumberValid 167 ? phoneUtil.format(number, PhoneNumberFormat.INTERNATIONAL) 168 : "invalid"; 169 170 output.append("<TD align=center>").append( 171 StringEscapeUtils.escapeHtml(prettyFormat)).append(" </TD> \n"); 172 output.append("<TD align=center>").append( 173 StringEscapeUtils.escapeHtml(internationalFormat)).append(" </TD> \n"); 174 } catch (NumberParseException e) { 175 output.append("<TD align=center colspan=2>").append( 176 StringEscapeUtils.escapeHtml(e.toString())).append(" </TD> \n"); 177 } 178 output.append("</TR>"); 179 } 180 output.append("</BODY></HTML>"); 181 return output; 182 } 183 184 private void appendLine(String title, String data, StringBuilder output) { 185 output.append("<TR>"); 186 output.append("<TH>").append(title).append("</TH>"); 187 output.append("<TD>").append(data.length() > 0 ? data : " ").append("</TD>"); 188 output.append("</TR>"); 189 } 190 191 /** 192 * Returns a stable URL pointing to the result page for the given input. 193 */ 194 private String getPermaLinkURL( 195 String phoneNumber, String defaultCountry, Locale geocodingLocale, boolean absoluteURL) { 196 // If absoluteURL is false, generate a relative path. Otherwise, produce an absolute URL. 197 StringBuilder permaLink = new StringBuilder( 198 absoluteURL ? "http://libphonenumber.appspot.com/phonenumberparser" : "/phonenumberparser"); 199 try { 200 permaLink.append( 201 "?number=" + URLEncoder.encode(phoneNumber != null ? phoneNumber : "", UTF_8.name())); 202 if (defaultCountry != null && !defaultCountry.isEmpty()) { 203 permaLink.append("&country=" + URLEncoder.encode(defaultCountry, UTF_8.name())); 204 } 205 if (!geocodingLocale.getLanguage().equals(ENGLISH.getLanguage()) || 206 !geocodingLocale.getCountry().isEmpty()) { 207 permaLink.append("&geocodingLocale=" + 208 URLEncoder.encode(geocodingLocale.toLanguageTag(), UTF_8.name())); 209 } 210 } catch(UnsupportedEncodingException e) { 211 // UTF-8 is guaranteed in Java, so this should be impossible. 212 throw new AssertionError(e); 213 } 214 return permaLink.toString(); 215 } 216 217 /** 218 * Returns a link to create a new github issue with the relevant information. 219 */ 220 private String getNewIssueLink( 221 String phoneNumber, String defaultCountry, Locale geocodingLocale) { 222 boolean hasDefaultCountry = !defaultCountry.isEmpty() && defaultCountry != "ZZ"; 223 String issueTitle = "Validation issue with " + phoneNumber 224 + (hasDefaultCountry ? " (" + defaultCountry + ")" : ""); 225 226 // Issue template. This must be kept in sync with the template in 227 // https://github.com/googlei18n/libphonenumber/blob/master/CONTRIBUTING.md. 228 StringBuilder issueTemplate = new StringBuilder( 229 "Please read the \"guidelines for contributing\" (linked above) and fill " 230 + "in the template below.\n\n"); 231 issueTemplate.append("Country/region affected (e.g., \"US\"): ") 232 .append(defaultCountry).append("\n\n"); 233 issueTemplate.append("Example number(s) affected (\"+1 555 555-1234\"): ") 234 .append(phoneNumber).append("\n\n"); 235 issueTemplate.append( 236 "The phone number range(s) to which the issue applies (\"+1 555 555-XXXX\"): \n\n"); 237 issueTemplate.append( 238 "The type of the number(s) (\"fixed-line\", \"mobile\", \"short code\", etc.): \n\n"); 239 issueTemplate.append( 240 "The cost, if applicable (\"toll-free\", \"premium rate\", \"shared cost\"): \n\n"); 241 issueTemplate.append( 242 "Supporting evidence (for example, national numbering plan, announcement from mobile " 243 + "carrier, news article): **IMPORTANT - anything posted here is made public. " 244 + "Read the guidelines first!** \n\n"); 245 issueTemplate.append("[link to demo](" 246 + getPermaLinkURL(phoneNumber, defaultCountry, geocodingLocale, true /* absoluteURL */) 247 + ")\n\n"); 248 String newIssueLink = "https://github.com/googlei18n/libphonenumber/issues/new?title="; 249 try { 250 newIssueLink += URLEncoder.encode(issueTitle, UTF_8.name()) + "&body=" 251 + URLEncoder.encode(issueTemplate.toString(), UTF_8.name()); 252 } catch(UnsupportedEncodingException e) { 253 // UTF-8 is guaranteed in Java, so this should be impossible. 254 throw new AssertionError(e); 255 } 256 return newIssueLink; 257 } 258 259 /** 260 * The defaultCountry here is used for parsing phoneNumber. The geocodingLocale is used to specify 261 * the language used for displaying the area descriptions generated from phone number geocoding. 262 */ 263 private StringBuilder getOutputForSingleNumber( 264 String phoneNumber, String defaultCountry, Locale geocodingLocale) { 265 StringBuilder output = new StringBuilder("<HTML><HEAD>"); 266 output.append( 267 "<LINK type=\"text/css\" rel=\"stylesheet\" href=\"/stylesheets/main.css\" />"); 268 output.append("</HEAD>"); 269 output.append("<BODY>"); 270 output.append("Phone Number entered: " + StringEscapeUtils.escapeHtml(phoneNumber) + "<BR>"); 271 output.append("defaultCountry entered: " + StringEscapeUtils.escapeHtml(defaultCountry) 272 + "<BR>"); 273 output.append("Language entered: " 274 + StringEscapeUtils.escapeHtml(geocodingLocale.toLanguageTag()) + "<BR>"); 275 try { 276 PhoneNumber number = phoneUtil.parseAndKeepRawInput(phoneNumber, defaultCountry); 277 output.append("<DIV>"); 278 output.append("<TABLE border=1>"); 279 output.append("<TR><TD colspan=2>Parsing Result (parseAndKeepRawInput())</TD></TR>"); 280 281 appendLine("country_code", Integer.toString(number.getCountryCode()), output); 282 appendLine("national_number", Long.toString(number.getNationalNumber()), output); 283 appendLine("extension", number.getExtension(), output); 284 appendLine("country_code_source", number.getCountryCodeSource().toString(), output); 285 appendLine("italian_leading_zero", Boolean.toString(number.isItalianLeadingZero()), output); 286 appendLine("raw_input", number.getRawInput(), output); 287 output.append("</TABLE>"); 288 output.append("</DIV>"); 289 290 boolean isPossible = phoneUtil.isPossibleNumber(number); 291 boolean isNumberValid = phoneUtil.isValidNumber(number); 292 PhoneNumberType numberType = phoneUtil.getNumberType(number); 293 boolean hasDefaultCountry = !defaultCountry.isEmpty() && defaultCountry != "ZZ"; 294 295 output.append("<DIV>"); 296 output.append("<TABLE border=1>"); 297 output.append("<TR><TD colspan=2>Validation Results</TD></TR>"); 298 appendLine("Result from isPossibleNumber()", Boolean.toString(isPossible), output); 299 if (!isPossible) { 300 appendLine("Result from isPossibleNumberWithReason()", 301 phoneUtil.isPossibleNumberWithReason(number).toString(), output); 302 output.append("<TR><TD colspan=2>Note: numbers that are not possible have type " + 303 "UNKNOWN, an unknown region, and are considered invalid.</TD></TR>"); 304 } else { 305 appendLine("Result from isValidNumber()", Boolean.toString(isNumberValid), output); 306 if (isNumberValid) { 307 if (hasDefaultCountry) { 308 appendLine( 309 "Result from isValidNumberForRegion()", 310 Boolean.toString(phoneUtil.isValidNumberForRegion(number, defaultCountry)), 311 output); 312 } 313 } 314 String region = phoneUtil.getRegionCodeForNumber(number); 315 appendLine("Phone Number region", region == null ? "" : region, output); 316 appendLine("Result from getNumberType()", numberType.toString(), output); 317 } 318 output.append("</TABLE>"); 319 output.append("</DIV>"); 320 321 if (!isNumberValid) { 322 output.append("<DIV>"); 323 output.append("<TABLE border=1>"); 324 output.append("<TR><TD colspan=2>Short Number Results</TD></TR>"); 325 boolean isPossibleShort = shortInfo.isPossibleShortNumber(number); 326 appendLine("Result from isPossibleShortNumber()", 327 Boolean.toString(isPossibleShort), output); 328 if (isPossibleShort) { 329 appendLine("Result from isValidShortNumber()", 330 Boolean.toString(shortInfo.isValidShortNumber(number)), output); 331 if (hasDefaultCountry) { 332 boolean isPossibleShortForRegion = 333 shortInfo.isPossibleShortNumberForRegion(number, defaultCountry); 334 appendLine("Result from isPossibleShortNumberForRegion()", 335 Boolean.toString(isPossibleShortForRegion), output); 336 if (isPossibleShortForRegion) { 337 appendLine("Result from isValidShortNumberForRegion()", 338 Boolean.toString(shortInfo.isValidShortNumberForRegion(number, 339 defaultCountry)), 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("E164 format", 351 isNumberValid ? phoneUtil.format(number, PhoneNumberFormat.E164) : "invalid", 352 output); 353 appendLine("Original format", 354 phoneUtil.formatInOriginalFormat(number, defaultCountry), output); 355 appendLine("National format", phoneUtil.format(number, PhoneNumberFormat.NATIONAL), output); 356 appendLine( 357 "International format", 358 isNumberValid ? phoneUtil.format(number, PhoneNumberFormat.INTERNATIONAL) : "invalid", 359 output); 360 appendLine( 361 "Out-of-country format from US", 362 isNumberValid ? phoneUtil.formatOutOfCountryCallingNumber(number, "US") : "invalid", 363 output); 364 appendLine( 365 "Out-of-country format from CH", 366 isNumberValid ? phoneUtil.formatOutOfCountryCallingNumber(number, "CH") : "invalid", 367 output); 368 output.append("</TABLE>"); 369 output.append("</DIV>"); 370 371 AsYouTypeFormatter formatter = phoneUtil.getAsYouTypeFormatter(defaultCountry); 372 int rawNumberLength = phoneNumber.length(); 373 output.append("<DIV>"); 374 output.append("<TABLE border=1>"); 375 output.append("<TR><TD colspan=2>AsYouTypeFormatter Results</TD></TR>"); 376 for (int i = 0; i < rawNumberLength; i++) { 377 // Note this doesn't handle supplementary characters, but it shouldn't be a big deal as 378 // there are no dial-pad characters in the supplementary range. 379 char inputChar = phoneNumber.charAt(i); 380 appendLine("Char entered: '" + inputChar + "' Output: ", 381 formatter.inputDigit(inputChar), output); 382 } 383 output.append("</TABLE>"); 384 output.append("</DIV>"); 385 386 if (isNumberValid) { 387 output.append("<DIV>"); 388 output.append("<TABLE border=1>"); 389 output.append("<TR><TD colspan=2>PhoneNumberOfflineGeocoder Results</TD></TR>"); 390 appendLine( 391 "Location", 392 PhoneNumberOfflineGeocoder.getInstance().getDescriptionForNumber( 393 number, geocodingLocale), 394 output); 395 output.append("</TABLE>"); 396 output.append("</DIV>"); 397 398 output.append("<DIV>"); 399 output.append("<TABLE border=1>"); 400 output.append("<TR><TD colspan=2>PhoneNumberToTimeZonesMapper Results</TD></TR>"); 401 appendLine( 402 "Time zone(s)", 403 PhoneNumberToTimeZonesMapper.getInstance().getTimeZonesForNumber(number).toString(), 404 output); 405 output.append("</TABLE>"); 406 output.append("</DIV>"); 407 408 if (numberType == PhoneNumberType.MOBILE || 409 numberType == PhoneNumberType.FIXED_LINE_OR_MOBILE || 410 numberType == PhoneNumberType.PAGER) { 411 output.append("<DIV>"); 412 output.append("<TABLE border=1>"); 413 output.append("<TR><TD colspan=2>PhoneNumberToCarrierMapper Results</TD></TR>"); 414 appendLine( 415 "Carrier", 416 PhoneNumberToCarrierMapper.getInstance().getNameForNumber(number, geocodingLocale), 417 output); 418 output.append("</TABLE>"); 419 output.append("</DIV>"); 420 } 421 } 422 423 String newIssueLink = getNewIssueLink(phoneNumber, defaultCountry, geocodingLocale); 424 String guidelinesLink = 425 "https://github.com/googlei18n/libphonenumber/blob/master/CONTRIBUTING.md"; 426 output.append("<b style=\"color:red\">File an issue</b>: by clicking on " 427 + "<a target=\"_blank\" href=\"" + newIssueLink + "\">this link</a>, I confirm that I " 428 + "have read the <a target=\"_blank\" href=\"" + guidelinesLink 429 + "\">contributor's guidelines</a>."); 430 } catch (NumberParseException e) { 431 output.append(StringEscapeUtils.escapeHtml(e.toString())); 432 } 433 output.append("</BODY></HTML>"); 434 return output; 435 } 436 } 437