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