• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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 : "&nbsp;").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