• 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.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 : "&nbsp;").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