All Downloads are FREE. Search and download functionalities are using the official Maven repository.

de.telekom.phonenumbernormalizer.numberplans.PhoneLibWrapper Maven / Gradle / Ivy

/*
 * Copyright © 2023 Deutsche Telekom AG ([email protected])
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package de.telekom.phonenumbernormalizer.numberplans;


import com.google.i18n.phonenumbers.*;
import lombok.Data;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;


import java.lang.reflect.Method;
import java.util.Objects;

/**
 * Wrapper around the PhoneLib library from Google
 * 

* Using reflection to access internal information to know if a region has a nation prefix & which one it is. *

* Providing own NumberPlans logic as an alternative to PhoneLib ShortNumber. *

* @see NumberPlan */ @Data public class PhoneLibWrapper { private static final Logger LOGGER = LoggerFactory.getLogger(PhoneLibWrapper.class); public static final String UNKNOWN_REGIONCODE = "ZZ"; // see https://github.com/google/libphonenumber/blob/5e9507a46051405120bc73fcc13d0b0be1b93c29/java/libphonenumber/test/com/google/i18n/phonenumbers/RegionCode.java#L62 /** * The given number reduced to characters which could be dialed * * @see PhoneLibWrapper#PhoneLibWrapper(String, String) */ String dialableNumber; /** * The given number normalized with PhoneLib, risking we get a incorrect normalization * * @see PhoneLibWrapper#PhoneLibWrapper(String, String) * @see PhoneLibWrapper#isNormalizingTried() * @see PhoneLibWrapper#getSemiNormalizedNumber() */ Phonenumber.PhoneNumber semiNormalizedNumber; /** * The given region code for which the given number should be normalized.
* This is an ISO2 code for the country. * * @see PhoneLibWrapper#PhoneLibWrapper(String, String) */ String regionCode; /** * The number plan metadata which PhoneLib is using for the given region code. * * @see PhoneLibWrapper#PhoneLibWrapper(String, String) */ Phonemetadata.PhoneMetadata metadata; /** * An instance of the PhoneLib short number utility. */ private static final ShortNumberInfo shortNumberUtil = ShortNumberInfo.getInstance(); /** * An instance of the PhoneLib number utility. */ private static final PhoneNumberUtil phoneUtil = PhoneNumberUtil.getInstance(); /** * Storing if PhoneLib has been used to parse the given number into semiNormalizedNumber. * * @see PhoneLibWrapper#PhoneLibWrapper(String, String) * @see PhoneLibWrapper#semiNormalizedNumber * @see PhoneLibWrapper#isNormalizingTried() */ private boolean isNormalizingTried = false; /** * Initialize the wrapper by giving a phone number to be analyzed against a number plan of a given region * @param number the phone number to be analyzed * @param regionCode the ISO2 Code of the Region / Country, which telephone number plan is used */ public PhoneLibWrapper(String number, String regionCode) { this.regionCode = regionCode; this.metadata = getMetadataForRegion(); if (number != null) { this.dialableNumber = PhoneNumberUtil.normalizeDiallableCharsOnly(number); if (this.dialableNumber.isEmpty()) { this.dialableNumber = ""; } else { if (!isSpecialFormat(dialableNumber)) { // Number needs normalization: // international prefix is added by the lib even if it's not valid in the number plan. this.isNormalizingTried = true; this.semiNormalizedNumber = PhoneLibWrapper.parseNumber(dialableNumber, regionCode); } } } } /** * If PhoneLib has been used to parse the given number into semiNormalizedNumber. * * @return {@link PhoneLibWrapper#isNormalizingTried} * * @see PhoneLibWrapper#PhoneLibWrapper(String, String) */ public boolean isNormalizingTried() { return isNormalizingTried; } /** * Using PhoneLib short number utility if it identifies the given number as a short number, which would not need a NAC. *

* This is a fallback for {@link PhoneLibWrapper#isShortNumber(NumberPlan)}, when we do not have an own number plan information. *

* @return if PhoneLib identifies given number as a short number * * @see PhoneLibWrapper#PhoneLibWrapper(String, String) * @see PhoneLibWrapper#isShortNumber(NumberPlan) */ public boolean isShortNumber() { return shortNumberUtil.isPossibleShortNumber(this.getSemiNormalizedNumber()); } /** * Using own {@link NumberPlan} to identify if the given number is a short number, which would not need a NAC. *

* If no number plan is given, {@link PhoneLibWrapper#isShortNumber} is used as fallback. *

* @param numberplan the number plan we identified to be used for a check * @return if number plan or as fallback PhoneLib identifies given number as a short number * * @see PhoneLibWrapper#PhoneLibWrapper(String, String) */ public boolean isShortNumber(NumberPlan numberplan) { if (numberplan == null) { return this.isShortNumber(); } return numberplan.isMatchingShortNumber(this.getDialableNumber()); } /** * If we have a plain national number based on regions number plan and potential NAC logic. *

* For a number plan without NAC logic, it will always return false! *

* @return if given number could have CC and NAC, but does not have any of them. */ public boolean hasNoCountryCodeNorNationalAccessCode() { // if given number has no NAC and no CC, it equals national phone number (without NAC). if (! Objects.equals(dialableNumber, this.getNationalPhoneNumberWithoutNationalAccessCode())) { return false; } // checking the regions number plan, if a NAC logic can be applied - if not there would be no option of having a NAC or not. return hasRegionNationalAccessCode(); } /** * Using PhoneLib to get a E164 formatted representation of the given number *

* This is a straight invocation, so no compensation of some inaccuracy is done here. *

* @return E164 format of the given phone number * * @see PhoneLibWrapper#PhoneLibWrapper(String, String) */ public String getE164Formatted() { return phoneUtil.format(this.semiNormalizedNumber, PhoneNumberUtil.PhoneNumberFormat.E164); } /** * If we know the given region for the given number {@link PhoneLibWrapper#hasRegionNationalAccessCode()}, this method checks if the given number does not start with a NAC nor a CC, * so we could permanently add a default NDC and NAC to the given number and for this new value the method directly return a E164 formatted representation. * @param nationalAccessCode the NAC to be added e.g. for Germany it would be "0" * @param defaultNationalDestinationCode the NDC to be added depending on the use telephone line origination. * @return if possible a E164 formatted representation or just the diallable representation of the given number. * * @see PhoneLibWrapper#PhoneLibWrapper(String, String) */ public String extendNumberByDefaultAreaCodeAndCountryCode(String nationalAccessCode, String defaultNationalDestinationCode) { String nationalPhoneNumberWithoutNationalAccessCode = this.getNationalPhoneNumberWithoutNationalAccessCode(); //if the dialableNumber is same as the national Number, Without NationalPrefix, then there is no NDC, so it needs to be added. if (Objects.equals(dialableNumber, nationalPhoneNumberWithoutNationalAccessCode)) { String extendedNumber = nationalAccessCode + defaultNationalDestinationCode + nationalPhoneNumberWithoutNationalAccessCode; try { this.semiNormalizedNumber = phoneUtil.parse(extendedNumber, regionCode); // after area code has been added, we can add the country code by the lib: return getE164Formatted(); } catch (NumberParseException e) { LOGGER.warn("could not parse extended number: {}", extendedNumber); LOGGER.debug("{}", e.getMessage()); return dialableNumber; } } else { //it seems we have nationalnumber with national prefix, so we could add country code: return getE164Formatted(); } } /** * Some Special dial-able characters make a number either not necessary to be normalized ("+" is already normalized) or can't be normalized ("*" control codes) * @param value phone number representation * @return if phone number starts with special characters which makes normalization unable / not necessary */ static boolean isSpecialFormat(String value) { //+: Number is already in "+" ... International Format: //*: Number is internal and cannot be normalized if (value == null || value.length()==0) { return false; } return ("+".equals(value.substring(0, 1))) || ("*".equals(value.substring(0, 1))); } /** * Use PhoneLib to parse a number for a regions code. If any exception occurs, they are logged and null is returned. * @param number the phone number to be parsed * @param regionCode ISO2 code for the regions number plan used for parsing the number * @return either the parsed {@link Phonenumber.PhoneNumber} or null */ private static Phonenumber.PhoneNumber parseNumber(String number, String regionCode) { try { return phoneUtil.parse(number, regionCode); // international prefix is added by the lib even if it's not valid in the number plan. } catch (NumberParseException e) { LOGGER.warn("could not parse normalize number: {}", number); LOGGER.debug("{}", e.getMessage()); return null; } } /** * The National Access Code used before the National Destination Code in the given region from PhoneLib * @return NAC of given {@link PhoneLibWrapper#regionCode} */ public String getNationalAccessCode() { if (metadata == null) { return null; } return metadata.getNationalPrefix(); } /** * From PhoneLib, if a National Access Code is used before the National Destination Code in the given region * @return if given {@link PhoneLibWrapper#regionCode} is using NAC */ public boolean hasRegionNationalAccessCode() { return metadata != null && metadata.hasNationalPrefix(); } /** * Since we need the PhoneMetadta for fixing calculation of some number normalization, * we need to break encapsulation via reflection, because that data is private to phoneUtil * and Google rejected suggestion to make it public, because they did not see our need in correcting normalization. * @return {@link Phonemetadata.PhoneMetadata} of {@link PhoneLibWrapper#regionCode} */ private Phonemetadata.PhoneMetadata getMetadataForRegion() { try { Method m = phoneUtil.getClass().getDeclaredMethod("getMetadataForRegion", String.class); // violating encupsulation is intended by this method, so no need for SONAR code smell warning here m.setAccessible(true); //NOSONAR return (Phonemetadata.PhoneMetadata) m.invoke(phoneUtil, regionCode); } catch (Exception e) { LOGGER.warn("Error while accessing getMetadataForRegion on PhoneNumberUtil via Reflection."); LOGGER.debug("{}", e.getMessage()); return null; } } /** * Using PhoneLib to get the national number from the given number * * @return national number without NAC, but any other leading zero. * * @see PhoneLibWrapper#PhoneLibWrapper(String, String) * @see PhoneLibWrapper#getSemiNormalizedNumber() * @see PhoneLibWrapper#nationalPhoneNumberWithoutNationalPrefix(Phonenumber.PhoneNumber) */ private String getNationalPhoneNumberWithoutNationalAccessCode() { return PhoneLibWrapper.nationalPhoneNumberWithoutNationalPrefix(this.semiNormalizedNumber); } /** * Using PhoneLib to get the national number from a parsed phone number with leading zeros, if those are not representing a National Access Code. *

* This is necessary, because PhoneLib is storing the national number as a long, so leading "0" Digits as part of it are stored in other attributes. * @param phoneNumber A PhoneLib parsed phone number * @return national number part without NationalPrefix (aka NAC) but any other leading zero. */ private static String nationalPhoneNumberWithoutNationalPrefix(Phonenumber.PhoneNumber phoneNumber) { if (phoneNumber==null) { return null; } StringBuilder nationalNumber = new StringBuilder(Long.toString(phoneNumber.getNationalNumber())); // if-clause necessary, because getNumberOfLeadingZeros is always 1 for a possible trunc code and special 0 in Italy if (phoneNumber.hasNumberOfLeadingZeros() || phoneNumber.hasItalianLeadingZero()) for (int i = 0; i < phoneNumber.getNumberOfLeadingZeros(); i++) { nationalNumber.insert(0, "0"); } return nationalNumber.toString(); } /** * Using PhoneLib to get the Country Calling Code for a region code *

* e.g. "DE" is "49" *

* @param regionCode ISO2 code of a region * @return country calling code of the region or 0 if regionCode is invalid. */ public static int getCountryCodeForRegion(String regionCode) { return phoneUtil.getCountryCodeForRegion(regionCode); } /** * Using PhoneLib to get the region code for a Country Calling Code *

* e.g. "49" is "DE" *

* @param countryCode only digits without IDP * @return regionCode or {@link PhoneLibWrapper#UNKNOWN_REGIONCODE} if countryCode is invalid. */ public static String getRegionCodeForCountryCode(String countryCode) { try { return phoneUtil.getRegionCodeForCountryCode(Integer.parseInt(countryCode)); } catch (Exception e) { LOGGER.info("Error while parsing Country Code: {}", countryCode); LOGGER.debug("{}", e.getMessage()); return PhoneLibWrapper.UNKNOWN_REGIONCODE; } } }




© 2015 - 2024 Weber Informatics LLC | Privacy Policy