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.setContentType("text/html"); 136 resp.setCharacterEncoding(UTF_8.name()); 137 resp.getWriter().println( 138 getOutputForSingleNumber(phoneNumber, defaultCountry, geocodingLocale)); 139 } 140 getOutputForFile(String defaultCountry, String fileContents)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 appendLine(String title, String data, StringBuilder output)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 */ getPermaLinkURL( String phoneNumber, String defaultCountry, Locale geocodingLocale, boolean absoluteURL)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 private static final String NEW_ISSUE_BASE_URL = 218 "https://issuetracker.google.com/issues/new?component=192347&title="; 219 220 /** 221 * Returns a link to create a new github issue with the relevant information. 222 */ getNewIssueLink( String phoneNumber, String defaultCountry, Locale geocodingLocale)223 private String getNewIssueLink( 224 String phoneNumber, String defaultCountry, Locale geocodingLocale) { 225 boolean hasDefaultCountry = !defaultCountry.isEmpty() && defaultCountry != "ZZ"; 226 String issueTitle = "Validation issue with " + phoneNumber 227 + (hasDefaultCountry ? " (" + defaultCountry + ")" : ""); 228 229 String newIssueLink = NEW_ISSUE_BASE_URL; 230 try { 231 newIssueLink += URLEncoder.encode(issueTitle, UTF_8.name()); 232 } catch(UnsupportedEncodingException e) { 233 // UTF-8 is guaranteed in Java, so this should be impossible. 234 throw new AssertionError(e); 235 } 236 return newIssueLink; 237 } 238 239 /** 240 * The defaultCountry here is used for parsing phoneNumber. The geocodingLocale is used to specify 241 * the language used for displaying the area descriptions generated from phone number geocoding. 242 */ getOutputForSingleNumber( String phoneNumber, String defaultCountry, Locale geocodingLocale)243 private StringBuilder getOutputForSingleNumber( 244 String phoneNumber, String defaultCountry, Locale geocodingLocale) { 245 StringBuilder output = new StringBuilder("<HTML><HEAD>"); 246 output.append( 247 "<LINK type=\"text/css\" rel=\"stylesheet\" href=\"/stylesheets/main.css\" />"); 248 output.append("</HEAD>"); 249 output.append("<BODY>"); 250 output.append("Phone Number entered: " + StringEscapeUtils.escapeHtml(phoneNumber) + "<BR>"); 251 output.append("defaultCountry entered: " + StringEscapeUtils.escapeHtml(defaultCountry) 252 + "<BR>"); 253 output.append("Language entered: " 254 + StringEscapeUtils.escapeHtml(geocodingLocale.toLanguageTag()) + "<BR>"); 255 try { 256 PhoneNumber number = phoneUtil.parseAndKeepRawInput(phoneNumber, defaultCountry); 257 output.append("<DIV>"); 258 output.append("<TABLE border=1>"); 259 output.append("<TR><TD colspan=2>Parsing Result (parseAndKeepRawInput())</TD></TR>"); 260 261 appendLine("country_code", Integer.toString(number.getCountryCode()), output); 262 appendLine("national_number", Long.toString(number.getNationalNumber()), output); 263 appendLine("extension", number.getExtension(), output); 264 appendLine("country_code_source", number.getCountryCodeSource().toString(), output); 265 appendLine("italian_leading_zero", Boolean.toString(number.isItalianLeadingZero()), output); 266 appendLine("raw_input", number.getRawInput(), output); 267 output.append("</TABLE>"); 268 output.append("</DIV>"); 269 270 boolean isPossible = phoneUtil.isPossibleNumber(number); 271 boolean isNumberValid = phoneUtil.isValidNumber(number); 272 PhoneNumberType numberType = phoneUtil.getNumberType(number); 273 boolean hasDefaultCountry = !defaultCountry.isEmpty() && defaultCountry != "ZZ"; 274 275 output.append("<DIV>"); 276 output.append("<TABLE border=1>"); 277 output.append("<TR><TD colspan=2>Validation Results</TD></TR>"); 278 appendLine("Result from isPossibleNumber()", Boolean.toString(isPossible), output); 279 if (!isPossible) { 280 appendLine("Result from isPossibleNumberWithReason()", 281 phoneUtil.isPossibleNumberWithReason(number).toString(), output); 282 output.append("<TR><TD colspan=2>Note: numbers that are not possible have type " + 283 "UNKNOWN, an unknown region, and are considered invalid.</TD></TR>"); 284 } else { 285 appendLine("Result from isValidNumber()", Boolean.toString(isNumberValid), output); 286 if (isNumberValid) { 287 if (hasDefaultCountry) { 288 appendLine( 289 "Result from isValidNumberForRegion()", 290 Boolean.toString(phoneUtil.isValidNumberForRegion(number, defaultCountry)), 291 output); 292 } 293 } 294 String region = phoneUtil.getRegionCodeForNumber(number); 295 appendLine("Phone Number region", region == null ? "" : region, output); 296 appendLine("Result from getNumberType()", numberType.toString(), output); 297 } 298 output.append("</TABLE>"); 299 output.append("</DIV>"); 300 301 if (!isNumberValid) { 302 output.append("<DIV>"); 303 output.append("<TABLE border=1>"); 304 output.append("<TR><TD colspan=2>Short Number Results</TD></TR>"); 305 boolean isPossibleShort = shortInfo.isPossibleShortNumber(number); 306 appendLine("Result from isPossibleShortNumber()", 307 Boolean.toString(isPossibleShort), output); 308 if (isPossibleShort) { 309 appendLine("Result from isValidShortNumber()", 310 Boolean.toString(shortInfo.isValidShortNumber(number)), output); 311 if (hasDefaultCountry) { 312 boolean isPossibleShortForRegion = 313 shortInfo.isPossibleShortNumberForRegion(number, defaultCountry); 314 appendLine("Result from isPossibleShortNumberForRegion()", 315 Boolean.toString(isPossibleShortForRegion), output); 316 if (isPossibleShortForRegion) { 317 appendLine("Result from isValidShortNumberForRegion()", 318 Boolean.toString(shortInfo.isValidShortNumberForRegion(number, 319 defaultCountry)), output); 320 } 321 } 322 } 323 output.append("</TABLE>"); 324 output.append("</DIV>"); 325 } 326 327 output.append("<DIV>"); 328 output.append("<TABLE border=1>"); 329 output.append("<TR><TD colspan=2>Formatting Results</TD></TR>"); 330 appendLine("E164 format", 331 isNumberValid ? phoneUtil.format(number, PhoneNumberFormat.E164) : "invalid", 332 output); 333 appendLine("Original format", 334 phoneUtil.formatInOriginalFormat(number, defaultCountry), output); 335 appendLine("National format", phoneUtil.format(number, PhoneNumberFormat.NATIONAL), output); 336 appendLine( 337 "International format", 338 isNumberValid ? phoneUtil.format(number, PhoneNumberFormat.INTERNATIONAL) : "invalid", 339 output); 340 appendLine( 341 "Out-of-country format from US", 342 isNumberValid ? phoneUtil.formatOutOfCountryCallingNumber(number, "US") : "invalid", 343 output); 344 appendLine( 345 "Out-of-country format from CH", 346 isNumberValid ? phoneUtil.formatOutOfCountryCallingNumber(number, "CH") : "invalid", 347 output); 348 output.append("</TABLE>"); 349 output.append("</DIV>"); 350 351 AsYouTypeFormatter formatter = phoneUtil.getAsYouTypeFormatter(defaultCountry); 352 int rawNumberLength = phoneNumber.length(); 353 output.append("<DIV>"); 354 output.append("<TABLE border=1>"); 355 output.append("<TR><TD colspan=2>AsYouTypeFormatter Results</TD></TR>"); 356 for (int i = 0; i < rawNumberLength; i++) { 357 // Note this doesn't handle supplementary characters, but it shouldn't be a big deal as 358 // there are no dial-pad characters in the supplementary range. 359 char inputChar = phoneNumber.charAt(i); 360 appendLine("Char entered: '" + inputChar + "' Output: ", 361 formatter.inputDigit(inputChar), output); 362 } 363 output.append("</TABLE>"); 364 output.append("</DIV>"); 365 366 if (isNumberValid) { 367 output.append("<DIV>"); 368 output.append("<TABLE border=1>"); 369 output.append("<TR><TD colspan=2>PhoneNumberOfflineGeocoder Results</TD></TR>"); 370 appendLine( 371 "Location", 372 PhoneNumberOfflineGeocoder.getInstance().getDescriptionForNumber( 373 number, geocodingLocale), 374 output); 375 output.append("</TABLE>"); 376 output.append("</DIV>"); 377 378 output.append("<DIV>"); 379 output.append("<TABLE border=1>"); 380 output.append("<TR><TD colspan=2>PhoneNumberToTimeZonesMapper Results</TD></TR>"); 381 appendLine( 382 "Time zone(s)", 383 PhoneNumberToTimeZonesMapper.getInstance().getTimeZonesForNumber(number).toString(), 384 output); 385 output.append("</TABLE>"); 386 output.append("</DIV>"); 387 388 if (numberType == PhoneNumberType.MOBILE || 389 numberType == PhoneNumberType.FIXED_LINE_OR_MOBILE || 390 numberType == PhoneNumberType.PAGER) { 391 output.append("<DIV>"); 392 output.append("<TABLE border=1>"); 393 output.append("<TR><TD colspan=2>PhoneNumberToCarrierMapper Results</TD></TR>"); 394 appendLine( 395 "Carrier", 396 PhoneNumberToCarrierMapper.getInstance().getNameForNumber(number, geocodingLocale), 397 output); 398 output.append("</TABLE>"); 399 output.append("</DIV>"); 400 } 401 } 402 403 String newIssueLink = getNewIssueLink(phoneNumber, defaultCountry, geocodingLocale); 404 String guidelinesLink = 405 "https://github.com/googlei18n/libphonenumber/blob/master/CONTRIBUTING.md"; 406 output.append("<b style=\"color:red\">File an issue</b>: by clicking on " 407 + "<a target=\"_blank\" href=\"" + newIssueLink + "\">this link</a>, I confirm that I " 408 + "have read the <a target=\"_blank\" href=\"" + guidelinesLink 409 + "\">contributor's guidelines</a>."); 410 } catch (NumberParseException e) { 411 output.append(StringEscapeUtils.escapeHtml(e.toString())); 412 } 413 output.append("</BODY></HTML>"); 414 return output; 415 } 416 } 417