| /* |
| * Copyright (C) 2009 The Libphonenumber Authors |
| * |
| * 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 com.google.i18n.phonenumbers; |
| |
| import com.google.i18n.phonenumbers.Phonemetadata.NumberFormat; |
| import com.google.i18n.phonenumbers.Phonemetadata.PhoneMetadata; |
| import com.google.i18n.phonenumbers.Phonemetadata.PhoneMetadataCollection; |
| import com.google.i18n.phonenumbers.Phonemetadata.PhoneNumberDesc; |
| import java.io.File; |
| import java.util.ArrayList; |
| import java.util.Arrays; |
| import java.util.HashSet; |
| import java.util.List; |
| import java.util.Map; |
| import java.util.Set; |
| import java.util.TreeMap; |
| import java.util.TreeSet; |
| import java.util.logging.Level; |
| import java.util.logging.Logger; |
| import java.util.regex.Pattern; |
| import java.util.regex.PatternSyntaxException; |
| import javax.xml.parsers.DocumentBuilder; |
| import javax.xml.parsers.DocumentBuilderFactory; |
| import org.w3c.dom.Document; |
| import org.w3c.dom.Element; |
| import org.w3c.dom.NodeList; |
| |
| /** |
| * Library to build phone number metadata from the XML format. |
| * |
| * @author Shaopeng Jia |
| */ |
| public class BuildMetadataFromXml { |
| private static final Logger logger = Logger.getLogger(BuildMetadataFromXml.class.getName()); |
| |
| // String constants used to fetch the XML nodes and attributes. |
| private static final String CARRIER_CODE_FORMATTING_RULE = "carrierCodeFormattingRule"; |
| private static final String CARRIER_SPECIFIC = "carrierSpecific"; |
| private static final String COUNTRY_CODE = "countryCode"; |
| private static final String EMERGENCY = "emergency"; |
| private static final String EXAMPLE_NUMBER = "exampleNumber"; |
| private static final String FIXED_LINE = "fixedLine"; |
| private static final String FORMAT = "format"; |
| private static final String GENERAL_DESC = "generalDesc"; |
| private static final String INTERNATIONAL_PREFIX = "internationalPrefix"; |
| private static final String INTL_FORMAT = "intlFormat"; |
| private static final String LEADING_DIGITS = "leadingDigits"; |
| private static final String MAIN_COUNTRY_FOR_CODE = "mainCountryForCode"; |
| private static final String MOBILE = "mobile"; |
| private static final String MOBILE_NUMBER_PORTABLE_REGION = "mobileNumberPortableRegion"; |
| private static final String NATIONAL_NUMBER_PATTERN = "nationalNumberPattern"; |
| private static final String NATIONAL_PREFIX = "nationalPrefix"; |
| private static final String NATIONAL_PREFIX_FORMATTING_RULE = "nationalPrefixFormattingRule"; |
| private static final String NATIONAL_PREFIX_OPTIONAL_WHEN_FORMATTING = |
| "nationalPrefixOptionalWhenFormatting"; |
| private static final String NATIONAL_PREFIX_FOR_PARSING = "nationalPrefixForParsing"; |
| private static final String NATIONAL_PREFIX_TRANSFORM_RULE = "nationalPrefixTransformRule"; |
| private static final String NO_INTERNATIONAL_DIALLING = "noInternationalDialling"; |
| private static final String NUMBER_FORMAT = "numberFormat"; |
| private static final String PAGER = "pager"; |
| private static final String PATTERN = "pattern"; |
| private static final String PERSONAL_NUMBER = "personalNumber"; |
| private static final String POSSIBLE_LENGTHS = "possibleLengths"; |
| private static final String NATIONAL = "national"; |
| private static final String LOCAL_ONLY = "localOnly"; |
| private static final String PREFERRED_EXTN_PREFIX = "preferredExtnPrefix"; |
| private static final String PREFERRED_INTERNATIONAL_PREFIX = "preferredInternationalPrefix"; |
| private static final String PREMIUM_RATE = "premiumRate"; |
| private static final String SHARED_COST = "sharedCost"; |
| private static final String SHORT_CODE = "shortCode"; |
| private static final String SMS_SERVICES = "smsServices"; |
| private static final String STANDARD_RATE = "standardRate"; |
| private static final String TOLL_FREE = "tollFree"; |
| private static final String UAN = "uan"; |
| private static final String VOICEMAIL = "voicemail"; |
| private static final String VOIP = "voip"; |
| |
| private static final Set<String> PHONE_NUMBER_DESCS_WITHOUT_MATCHING_TYPES = |
| new HashSet<String>(Arrays.asList(new String[]{NO_INTERNATIONAL_DIALLING})); |
| |
| // Build the PhoneMetadataCollection from the input XML file. |
| public static PhoneMetadataCollection buildPhoneMetadataCollection(String inputXmlFile, |
| boolean liteBuild, boolean specialBuild) throws Exception { |
| DocumentBuilderFactory builderFactory = DocumentBuilderFactory.newInstance(); |
| DocumentBuilder builder = builderFactory.newDocumentBuilder(); |
| File xmlFile = new File(inputXmlFile); |
| Document document = builder.parse(xmlFile); |
| // TODO: Look for other uses of these constants and possibly pull them out into a separate |
| // constants file. |
| boolean isShortNumberMetadata = inputXmlFile.contains("ShortNumberMetadata"); |
| boolean isAlternateFormatsMetadata = inputXmlFile.contains("PhoneNumberAlternateFormats"); |
| return buildPhoneMetadataCollection(document, liteBuild, specialBuild, |
| isShortNumberMetadata, isAlternateFormatsMetadata); |
| } |
| |
| // @VisibleForTesting |
| static PhoneMetadataCollection buildPhoneMetadataCollection(Document document, |
| boolean liteBuild, boolean specialBuild, boolean isShortNumberMetadata, |
| boolean isAlternateFormatsMetadata) throws Exception { |
| document.getDocumentElement().normalize(); |
| Element rootElement = document.getDocumentElement(); |
| NodeList territory = rootElement.getElementsByTagName("territory"); |
| PhoneMetadataCollection.Builder metadataCollection = PhoneMetadataCollection.newBuilder(); |
| int numOfTerritories = territory.getLength(); |
| // TODO: Infer filter from a single flag. |
| MetadataFilter metadataFilter = getMetadataFilter(liteBuild, specialBuild); |
| for (int i = 0; i < numOfTerritories; i++) { |
| Element territoryElement = (Element) territory.item(i); |
| String regionCode = ""; |
| // For the main metadata file this should always be set, but for other supplementary data |
| // files the country calling code may be all that is needed. |
| if (territoryElement.hasAttribute("id")) { |
| regionCode = territoryElement.getAttribute("id"); |
| } |
| PhoneMetadata.Builder metadata = loadCountryMetadata(regionCode, territoryElement, |
| isShortNumberMetadata, isAlternateFormatsMetadata); |
| metadataFilter.filterMetadata(metadata); |
| metadataCollection.addMetadata(metadata.build()); |
| } |
| return metadataCollection.build(); |
| } |
| |
| // Build a mapping from a country calling code to the region codes which denote the country/region |
| // represented by that country code. In the case of multiple countries sharing a calling code, |
| // such as the NANPA countries, the one indicated with "isMainCountryForCode" in the metadata |
| // should be first. |
| public static Map<Integer, List<String>> buildCountryCodeToRegionCodeMap( |
| PhoneMetadataCollection metadataCollection) { |
| Map<Integer, List<String>> countryCodeToRegionCodeMap = new TreeMap<Integer, List<String>>(); |
| for (PhoneMetadata metadata : metadataCollection.getMetadataList()) { |
| String regionCode = metadata.getId(); |
| int countryCode = metadata.getCountryCode(); |
| if (countryCodeToRegionCodeMap.containsKey(countryCode)) { |
| if (metadata.getMainCountryForCode()) { |
| countryCodeToRegionCodeMap.get(countryCode).add(0, regionCode); |
| } else { |
| countryCodeToRegionCodeMap.get(countryCode).add(regionCode); |
| } |
| } else { |
| // For most countries, there will be only one region code for the country calling code. |
| List<String> listWithRegionCode = new ArrayList<String>(1); |
| if (!regionCode.equals("")) { // For alternate formats, there are no region codes at all. |
| listWithRegionCode.add(regionCode); |
| } |
| countryCodeToRegionCodeMap.put(countryCode, listWithRegionCode); |
| } |
| } |
| return countryCodeToRegionCodeMap; |
| } |
| |
| // Build a list of region codes from the metadata |
| public static List<String> buildRegionCodeList( |
| PhoneMetadataCollection metadataCollection) { |
| List<String> regionCodeList = new ArrayList<String>(); |
| for (PhoneMetadata metadata : metadataCollection.getMetadataList()) { |
| String regionCode = metadata.getId(); |
| regionCodeList.add(regionCode); |
| } |
| return regionCodeList; |
| } |
| |
| private static String validateRE(String regex) { |
| return validateRE(regex, false); |
| } |
| |
| // @VisibleForTesting |
| static String validateRE(String regex, boolean removeWhitespace) { |
| // Removes all the whitespace and newline from the regexp. Not using pattern compile options to |
| // make it work across programming languages. |
| String compressedRegex = removeWhitespace ? regex.replaceAll("\\s", "") : regex; |
| Pattern.compile(compressedRegex); |
| // We don't ever expect to see | followed by a ) in our metadata - this would be an indication |
| // of a bug. If one wants to make something optional, we prefer ? to using an empty group. |
| int errorIndex = compressedRegex.indexOf("|)"); |
| if (errorIndex >= 0) { |
| logger.log(Level.SEVERE, "Error with original regex: " + regex |
| + "\n| should not be followed directly by ) in phone number regular expressions."); |
| throw new PatternSyntaxException("| followed by )", compressedRegex, errorIndex); |
| } |
| // return the regex if it is of correct syntax, i.e. compile did not fail with a |
| // PatternSyntaxException. |
| return compressedRegex; |
| } |
| |
| /** |
| * Returns the national prefix of the provided country element. |
| */ |
| // @VisibleForTesting |
| static String getNationalPrefix(Element element) { |
| return element.hasAttribute(NATIONAL_PREFIX) ? element.getAttribute(NATIONAL_PREFIX) : ""; |
| } |
| |
| // @VisibleForTesting |
| static PhoneMetadata.Builder loadTerritoryTagMetadata(String regionCode, Element element, |
| String nationalPrefix) { |
| PhoneMetadata.Builder metadata = PhoneMetadata.newBuilder(); |
| metadata.setId(regionCode); |
| if (element.hasAttribute(COUNTRY_CODE)) { |
| metadata.setCountryCode(Integer.parseInt(element.getAttribute(COUNTRY_CODE))); |
| } |
| if (element.hasAttribute(LEADING_DIGITS)) { |
| metadata.setLeadingDigits(validateRE(element.getAttribute(LEADING_DIGITS))); |
| } |
| if (element.hasAttribute(INTERNATIONAL_PREFIX)) { |
| metadata.setInternationalPrefix(validateRE(element.getAttribute(INTERNATIONAL_PREFIX))); |
| } |
| if (element.hasAttribute(PREFERRED_INTERNATIONAL_PREFIX)) { |
| metadata.setPreferredInternationalPrefix( |
| element.getAttribute(PREFERRED_INTERNATIONAL_PREFIX)); |
| } |
| if (element.hasAttribute(NATIONAL_PREFIX_FOR_PARSING)) { |
| metadata.setNationalPrefixForParsing( |
| validateRE(element.getAttribute(NATIONAL_PREFIX_FOR_PARSING), true)); |
| if (element.hasAttribute(NATIONAL_PREFIX_TRANSFORM_RULE)) { |
| metadata.setNationalPrefixTransformRule( |
| validateRE(element.getAttribute(NATIONAL_PREFIX_TRANSFORM_RULE))); |
| } |
| } |
| if (!nationalPrefix.isEmpty()) { |
| metadata.setNationalPrefix(nationalPrefix); |
| if (!metadata.hasNationalPrefixForParsing()) { |
| metadata.setNationalPrefixForParsing(nationalPrefix); |
| } |
| } |
| if (element.hasAttribute(PREFERRED_EXTN_PREFIX)) { |
| metadata.setPreferredExtnPrefix(element.getAttribute(PREFERRED_EXTN_PREFIX)); |
| } |
| if (element.hasAttribute(MAIN_COUNTRY_FOR_CODE)) { |
| metadata.setMainCountryForCode(true); |
| } |
| if (element.hasAttribute(MOBILE_NUMBER_PORTABLE_REGION)) { |
| metadata.setMobileNumberPortableRegion(true); |
| } |
| return metadata; |
| } |
| |
| /** |
| * Extracts the pattern for international format. If there is no intlFormat, default to using the |
| * national format. If the intlFormat is set to "NA" the intlFormat should be ignored. |
| * |
| * @throws RuntimeException if multiple intlFormats have been encountered. |
| * @return whether an international number format is defined. |
| */ |
| // @VisibleForTesting |
| static boolean loadInternationalFormat(PhoneMetadata.Builder metadata, |
| Element numberFormatElement, |
| NumberFormat nationalFormat) { |
| NumberFormat.Builder intlFormat = NumberFormat.newBuilder(); |
| NodeList intlFormatPattern = numberFormatElement.getElementsByTagName(INTL_FORMAT); |
| boolean hasExplicitIntlFormatDefined = false; |
| |
| if (intlFormatPattern.getLength() > 1) { |
| logger.log(Level.SEVERE, |
| "A maximum of one intlFormat pattern for a numberFormat element should be defined."); |
| String countryId = metadata.getId().length() > 0 ? metadata.getId() |
| : Integer.toString(metadata.getCountryCode()); |
| throw new RuntimeException("Invalid number of intlFormat patterns for country: " + countryId); |
| } else if (intlFormatPattern.getLength() == 0) { |
| // Default to use the same as the national pattern if none is defined. |
| intlFormat.mergeFrom(nationalFormat); |
| } else { |
| intlFormat.setPattern(numberFormatElement.getAttribute(PATTERN)); |
| setLeadingDigitsPatterns(numberFormatElement, intlFormat); |
| String intlFormatPatternValue = intlFormatPattern.item(0).getFirstChild().getNodeValue(); |
| if (!intlFormatPatternValue.equals("NA")) { |
| intlFormat.setFormat(intlFormatPatternValue); |
| } |
| hasExplicitIntlFormatDefined = true; |
| } |
| |
| if (intlFormat.hasFormat()) { |
| metadata.addIntlNumberFormat(intlFormat.build()); |
| } |
| return hasExplicitIntlFormatDefined; |
| } |
| |
| /** |
| * Extracts the pattern for the national format. |
| * |
| * @throws RuntimeException if multiple or no formats have been encountered. |
| */ |
| // @VisibleForTesting |
| static void loadNationalFormat(PhoneMetadata.Builder metadata, Element numberFormatElement, |
| NumberFormat.Builder format) { |
| setLeadingDigitsPatterns(numberFormatElement, format); |
| format.setPattern(validateRE(numberFormatElement.getAttribute(PATTERN))); |
| |
| NodeList formatPattern = numberFormatElement.getElementsByTagName(FORMAT); |
| int numFormatPatterns = formatPattern.getLength(); |
| if (numFormatPatterns != 1) { |
| logger.log(Level.SEVERE, "One format pattern for a numberFormat element should be defined."); |
| String countryId = metadata.getId().length() > 0 ? metadata.getId() |
| : Integer.toString(metadata.getCountryCode()); |
| throw new RuntimeException("Invalid number of format patterns (" + numFormatPatterns |
| + ") for country: " + countryId); |
| } |
| format.setFormat(formatPattern.item(0).getFirstChild().getNodeValue()); |
| } |
| |
| /** |
| * Extracts the available formats from the provided DOM element. If it does not contain any |
| * nationalPrefixFormattingRule, the one passed-in is retained; similarly for |
| * nationalPrefixOptionalWhenFormatting. The nationalPrefix, nationalPrefixFormattingRule and |
| * nationalPrefixOptionalWhenFormatting values are provided from the parent (territory) element. |
| */ |
| // @VisibleForTesting |
| static void loadAvailableFormats(PhoneMetadata.Builder metadata, |
| Element element, String nationalPrefix, |
| String nationalPrefixFormattingRule, |
| boolean nationalPrefixOptionalWhenFormatting) { |
| String carrierCodeFormattingRule = ""; |
| if (element.hasAttribute(CARRIER_CODE_FORMATTING_RULE)) { |
| carrierCodeFormattingRule = validateRE( |
| getDomesticCarrierCodeFormattingRuleFromElement(element, nationalPrefix)); |
| } |
| NodeList numberFormatElements = element.getElementsByTagName(NUMBER_FORMAT); |
| boolean hasExplicitIntlFormatDefined = false; |
| |
| int numOfFormatElements = numberFormatElements.getLength(); |
| if (numOfFormatElements > 0) { |
| for (int i = 0; i < numOfFormatElements; i++) { |
| Element numberFormatElement = (Element) numberFormatElements.item(i); |
| NumberFormat.Builder format = NumberFormat.newBuilder(); |
| |
| if (numberFormatElement.hasAttribute(NATIONAL_PREFIX_FORMATTING_RULE)) { |
| format.setNationalPrefixFormattingRule( |
| getNationalPrefixFormattingRuleFromElement(numberFormatElement, nationalPrefix)); |
| } else if (!nationalPrefixFormattingRule.equals("")) { |
| format.setNationalPrefixFormattingRule(nationalPrefixFormattingRule); |
| } |
| if (numberFormatElement.hasAttribute(NATIONAL_PREFIX_OPTIONAL_WHEN_FORMATTING)) { |
| format.setNationalPrefixOptionalWhenFormatting( |
| Boolean.valueOf(numberFormatElement.getAttribute( |
| NATIONAL_PREFIX_OPTIONAL_WHEN_FORMATTING))); |
| } else if (format.getNationalPrefixOptionalWhenFormatting() |
| != nationalPrefixOptionalWhenFormatting) { |
| // Inherit from the parent field if it is not already the same as the default. |
| format.setNationalPrefixOptionalWhenFormatting(nationalPrefixOptionalWhenFormatting); |
| } |
| if (numberFormatElement.hasAttribute(CARRIER_CODE_FORMATTING_RULE)) { |
| format.setDomesticCarrierCodeFormattingRule(validateRE( |
| getDomesticCarrierCodeFormattingRuleFromElement(numberFormatElement, |
| nationalPrefix))); |
| } else if (!carrierCodeFormattingRule.equals("")) { |
| format.setDomesticCarrierCodeFormattingRule(carrierCodeFormattingRule); |
| } |
| loadNationalFormat(metadata, numberFormatElement, format); |
| metadata.addNumberFormat(format); |
| |
| if (loadInternationalFormat(metadata, numberFormatElement, format.build())) { |
| hasExplicitIntlFormatDefined = true; |
| } |
| } |
| // Only a small number of regions need to specify the intlFormats in the xml. For the majority |
| // of countries the intlNumberFormat metadata is an exact copy of the national NumberFormat |
| // metadata. To minimize the size of the metadata file, we only keep intlNumberFormats that |
| // actually differ in some way to the national formats. |
| if (!hasExplicitIntlFormatDefined) { |
| metadata.clearIntlNumberFormat(); |
| } |
| } |
| } |
| |
| // @VisibleForTesting |
| static void setLeadingDigitsPatterns(Element numberFormatElement, NumberFormat.Builder format) { |
| NodeList leadingDigitsPatternNodes = numberFormatElement.getElementsByTagName(LEADING_DIGITS); |
| int numOfLeadingDigitsPatterns = leadingDigitsPatternNodes.getLength(); |
| if (numOfLeadingDigitsPatterns > 0) { |
| for (int i = 0; i < numOfLeadingDigitsPatterns; i++) { |
| format.addLeadingDigitsPattern( |
| validateRE((leadingDigitsPatternNodes.item(i)).getFirstChild().getNodeValue(), true)); |
| } |
| } |
| } |
| |
| // @VisibleForTesting |
| static String getNationalPrefixFormattingRuleFromElement(Element element, |
| String nationalPrefix) { |
| String nationalPrefixFormattingRule = element.getAttribute(NATIONAL_PREFIX_FORMATTING_RULE); |
| // Replace $NP with national prefix and $FG with the first group ($1). |
| nationalPrefixFormattingRule = |
| nationalPrefixFormattingRule.replaceFirst("\\$NP", nationalPrefix) |
| .replaceFirst("\\$FG", "\\$1"); |
| return nationalPrefixFormattingRule; |
| } |
| |
| // @VisibleForTesting |
| static String getDomesticCarrierCodeFormattingRuleFromElement(Element element, |
| String nationalPrefix) { |
| String carrierCodeFormattingRule = element.getAttribute(CARRIER_CODE_FORMATTING_RULE); |
| // Replace $FG with the first group ($1) and $NP with the national prefix. |
| carrierCodeFormattingRule = carrierCodeFormattingRule.replaceFirst("\\$FG", "\\$1") |
| .replaceFirst("\\$NP", nationalPrefix); |
| return carrierCodeFormattingRule; |
| } |
| |
| /** |
| * Checks if the possible lengths provided as a sorted set are equal to the possible lengths |
| * stored already in the description pattern. Note that possibleLengths may be empty but must not |
| * be null, and the PhoneNumberDesc passed in should also not be null. |
| */ |
| private static boolean arePossibleLengthsEqual(TreeSet<Integer> possibleLengths, |
| PhoneNumberDesc desc) { |
| if (possibleLengths.size() != desc.getPossibleLengthCount()) { |
| return false; |
| } |
| // Note that both should be sorted already, and we know they are the same length. |
| int i = 0; |
| for (Integer length : possibleLengths) { |
| if (length != desc.getPossibleLength(i)) { |
| return false; |
| } |
| i++; |
| } |
| return true; |
| } |
| |
| /** |
| * Processes a phone number description element from the XML file and returns it as a |
| * PhoneNumberDesc. If the description element is a fixed line or mobile number, the parent |
| * description will be used to fill in the whole element if necessary, or any components that are |
| * missing. For all other types, the parent description will only be used to fill in missing |
| * components if the type has a partial definition. For example, if no "tollFree" element exists, |
| * we assume there are no toll free numbers for that locale, and return a phone number description |
| * with no national number data and [-1] for the possible lengths. Note that the parent |
| * description must therefore already be processed before this method is called on any child |
| * elements. |
| * |
| * @param parentDesc a generic phone number description that will be used to fill in missing |
| * parts of the description, or null if this is the root node. This must be processed before |
| * this is run on any child elements. |
| * @param countryElement the XML element representing all the country information |
| * @param numberType the name of the number type, corresponding to the appropriate tag in the XML |
| * file with information about that type |
| * @return complete description of that phone number type |
| */ |
| // @VisibleForTesting |
| static PhoneNumberDesc.Builder processPhoneNumberDescElement(PhoneNumberDesc.Builder parentDesc, |
| Element countryElement, |
| String numberType) { |
| NodeList phoneNumberDescList = countryElement.getElementsByTagName(numberType); |
| PhoneNumberDesc.Builder numberDesc = PhoneNumberDesc.newBuilder(); |
| if (phoneNumberDescList.getLength() == 0) { |
| // -1 will never match a possible phone number length, so is safe to use to ensure this never |
| // matches. We don't leave it empty, since for compression reasons, we use the empty list to |
| // mean that the generalDesc possible lengths apply. |
| numberDesc.addPossibleLength(-1); |
| return numberDesc; |
| } |
| if (phoneNumberDescList.getLength() > 0) { |
| if (phoneNumberDescList.getLength() > 1) { |
| throw new RuntimeException( |
| String.format("Multiple elements with type %s found.", numberType)); |
| } |
| Element element = (Element) phoneNumberDescList.item(0); |
| if (parentDesc != null) { |
| // New way of handling possible number lengths. We don't do this for the general |
| // description, since these tags won't be present; instead we will calculate its values |
| // based on the values for all the other number type descriptions (see |
| // setPossibleLengthsGeneralDesc). |
| TreeSet<Integer> lengths = new TreeSet<Integer>(); |
| TreeSet<Integer> localOnlyLengths = new TreeSet<Integer>(); |
| populatePossibleLengthSets(element, lengths, localOnlyLengths); |
| setPossibleLengths(lengths, localOnlyLengths, parentDesc.build(), numberDesc); |
| } |
| |
| NodeList validPattern = element.getElementsByTagName(NATIONAL_NUMBER_PATTERN); |
| if (validPattern.getLength() > 0) { |
| numberDesc.setNationalNumberPattern( |
| validateRE(validPattern.item(0).getFirstChild().getNodeValue(), true)); |
| } |
| |
| NodeList exampleNumber = element.getElementsByTagName(EXAMPLE_NUMBER); |
| if (exampleNumber.getLength() > 0) { |
| numberDesc.setExampleNumber(exampleNumber.item(0).getFirstChild().getNodeValue()); |
| } |
| } |
| return numberDesc; |
| } |
| |
| // @VisibleForTesting |
| static void setRelevantDescPatterns(PhoneMetadata.Builder metadata, Element element, |
| boolean isShortNumberMetadata) { |
| PhoneNumberDesc.Builder generalDesc = processPhoneNumberDescElement(null, element, |
| GENERAL_DESC); |
| // Calculate the possible lengths for the general description. This will be based on the |
| // possible lengths of the child elements. |
| setPossibleLengthsGeneralDesc(generalDesc, metadata.getId(), element, isShortNumberMetadata); |
| metadata.setGeneralDesc(generalDesc); |
| |
| if (!isShortNumberMetadata) { |
| // Set fields used by regular length phone numbers. |
| metadata.setFixedLine(processPhoneNumberDescElement(generalDesc, element, FIXED_LINE)); |
| metadata.setMobile(processPhoneNumberDescElement(generalDesc, element, MOBILE)); |
| metadata.setSharedCost(processPhoneNumberDescElement(generalDesc, element, SHARED_COST)); |
| metadata.setVoip(processPhoneNumberDescElement(generalDesc, element, VOIP)); |
| metadata.setPersonalNumber(processPhoneNumberDescElement(generalDesc, element, |
| PERSONAL_NUMBER)); |
| metadata.setPager(processPhoneNumberDescElement(generalDesc, element, PAGER)); |
| metadata.setUan(processPhoneNumberDescElement(generalDesc, element, UAN)); |
| metadata.setVoicemail(processPhoneNumberDescElement(generalDesc, element, VOICEMAIL)); |
| metadata.setNoInternationalDialling(processPhoneNumberDescElement(generalDesc, element, |
| NO_INTERNATIONAL_DIALLING)); |
| boolean mobileAndFixedAreSame = metadata.getMobile().getNationalNumberPattern() |
| .equals(metadata.getFixedLine().getNationalNumberPattern()); |
| if (metadata.getSameMobileAndFixedLinePattern() != mobileAndFixedAreSame) { |
| // Set this if it is not the same as the default. |
| metadata.setSameMobileAndFixedLinePattern(mobileAndFixedAreSame); |
| } |
| metadata.setTollFree(processPhoneNumberDescElement(generalDesc, element, TOLL_FREE)); |
| metadata.setPremiumRate(processPhoneNumberDescElement(generalDesc, element, PREMIUM_RATE)); |
| } else { |
| // Set fields used by short numbers. |
| metadata.setStandardRate(processPhoneNumberDescElement(generalDesc, element, STANDARD_RATE)); |
| metadata.setShortCode(processPhoneNumberDescElement(generalDesc, element, SHORT_CODE)); |
| metadata.setCarrierSpecific(processPhoneNumberDescElement(generalDesc, element, |
| CARRIER_SPECIFIC)); |
| metadata.setEmergency(processPhoneNumberDescElement(generalDesc, element, EMERGENCY)); |
| metadata.setTollFree(processPhoneNumberDescElement(generalDesc, element, TOLL_FREE)); |
| metadata.setPremiumRate(processPhoneNumberDescElement(generalDesc, element, PREMIUM_RATE)); |
| metadata.setSmsServices(processPhoneNumberDescElement(generalDesc, element, SMS_SERVICES)); |
| } |
| } |
| |
| /** |
| * Parses a possible length string into a set of the integers that are covered. |
| * |
| * @param possibleLengthString a string specifying the possible lengths of phone numbers. Follows |
| * this syntax: ranges or elements are separated by commas, and ranges are specified in |
| * [min-max] notation, inclusive. For example, [3-5],7,9,[11-14] should be parsed to |
| * 3,4,5,7,9,11,12,13,14. |
| */ |
| private static Set<Integer> parsePossibleLengthStringToSet(String possibleLengthString) { |
| if (possibleLengthString.length() == 0) { |
| throw new RuntimeException("Empty possibleLength string found."); |
| } |
| String[] lengths = possibleLengthString.split(","); |
| Set<Integer> lengthSet = new TreeSet<Integer>(); |
| for (int i = 0; i < lengths.length; i++) { |
| String lengthSubstring = lengths[i]; |
| if (lengthSubstring.length() == 0) { |
| throw new RuntimeException(String.format("Leading, trailing or adjacent commas in possible " |
| + "length string %s, these should only separate numbers or ranges.", |
| possibleLengthString)); |
| } else if (lengthSubstring.charAt(0) == '[') { |
| if (lengthSubstring.charAt(lengthSubstring.length() - 1) != ']') { |
| throw new RuntimeException(String.format("Missing end of range character in possible " |
| + "length string %s.", possibleLengthString)); |
| } |
| // Strip the leading and trailing [], and split on the -. |
| String[] minMax = lengthSubstring.substring(1, lengthSubstring.length() - 1).split("-"); |
| if (minMax.length != 2) { |
| throw new RuntimeException(String.format("Ranges must have exactly one - character: " |
| + "missing for %s.", possibleLengthString)); |
| } |
| int min = Integer.parseInt(minMax[0]); |
| int max = Integer.parseInt(minMax[1]); |
| // We don't even accept [6-7] since we prefer the shorter 6,7 variant; for a range to be in |
| // use the hyphen needs to replace at least one digit. |
| if (max - min < 2) { |
| throw new RuntimeException(String.format("The first number in a range should be two or " |
| + "more digits lower than the second. Culprit possibleLength string: %s", |
| possibleLengthString)); |
| } |
| for (int j = min; j <= max; j++) { |
| if (!lengthSet.add(j)) { |
| throw new RuntimeException(String.format("Duplicate length element found (%d) in " |
| + "possibleLength string %s", j, possibleLengthString)); |
| } |
| } |
| } else { |
| int length = Integer.parseInt(lengthSubstring); |
| if (!lengthSet.add(length)) { |
| throw new RuntimeException(String.format("Duplicate length element found (%d) in " |
| + "possibleLength string %s", length, possibleLengthString)); |
| } |
| } |
| } |
| return lengthSet; |
| } |
| |
| /** |
| * Reads the possible lengths present in the metadata and splits them into two sets: one for |
| * full-length numbers, one for local numbers. |
| * |
| * @param data one or more phone number descriptions, represented as XML nodes |
| * @param lengths a set to which to add possible lengths of full phone numbers |
| * @param localOnlyLengths a set to which to add possible lengths of phone numbers only diallable |
| * locally (e.g. within a province) |
| */ |
| private static void populatePossibleLengthSets(Element data, TreeSet<Integer> lengths, |
| TreeSet<Integer> localOnlyLengths) { |
| NodeList possibleLengths = data.getElementsByTagName(POSSIBLE_LENGTHS); |
| for (int i = 0; i < possibleLengths.getLength(); i++) { |
| Element element = (Element) possibleLengths.item(i); |
| String nationalLengths = element.getAttribute(NATIONAL); |
| // We don't add to the phone metadata yet, since we want to sort length elements found under |
| // different nodes first, make sure there are no duplicates between them and that the |
| // localOnly lengths don't overlap with the others. |
| Set<Integer> thisElementLengths = parsePossibleLengthStringToSet(nationalLengths); |
| if (element.hasAttribute(LOCAL_ONLY)) { |
| String localLengths = element.getAttribute(LOCAL_ONLY); |
| Set<Integer> thisElementLocalOnlyLengths = parsePossibleLengthStringToSet(localLengths); |
| Set<Integer> intersection = new HashSet<Integer>(thisElementLengths); |
| intersection.retainAll(thisElementLocalOnlyLengths); |
| if (!intersection.isEmpty()) { |
| throw new RuntimeException(String.format( |
| "Possible length(s) found specified as a normal and local-only length: %s", |
| intersection)); |
| } |
| // We check again when we set these lengths on the metadata itself in setPossibleLengths |
| // that the elements in localOnly are not also in lengths. For e.g. the generalDesc, it |
| // might have a local-only length for one type that is a normal length for another type. We |
| // don't consider this an error, but we do want to remove the local-only lengths. |
| localOnlyLengths.addAll(thisElementLocalOnlyLengths); |
| } |
| // It is okay if at this time we have duplicates, because the same length might be possible |
| // for e.g. fixed-line and for mobile numbers, and this method operates potentially on |
| // multiple phoneNumberDesc XML elements. |
| lengths.addAll(thisElementLengths); |
| } |
| } |
| |
| /** |
| * Sets possible lengths in the general description, derived from certain child elements. |
| */ |
| // @VisibleForTesting |
| static void setPossibleLengthsGeneralDesc(PhoneNumberDesc.Builder generalDesc, String metadataId, |
| Element data, boolean isShortNumberMetadata) { |
| TreeSet<Integer> lengths = new TreeSet<Integer>(); |
| TreeSet<Integer> localOnlyLengths = new TreeSet<Integer>(); |
| // The general description node should *always* be present if metadata for other types is |
| // present, aside from in some unit tests. |
| // (However, for e.g. formatting metadata in PhoneNumberAlternateFormats, no PhoneNumberDesc |
| // elements are present). |
| NodeList generalDescNodes = data.getElementsByTagName(GENERAL_DESC); |
| if (generalDescNodes.getLength() > 0) { |
| Element generalDescNode = (Element) generalDescNodes.item(0); |
| populatePossibleLengthSets(generalDescNode, lengths, localOnlyLengths); |
| if (!lengths.isEmpty() || !localOnlyLengths.isEmpty()) { |
| // We shouldn't have anything specified at the "general desc" level: we are going to |
| // calculate this ourselves from child elements. |
| throw new RuntimeException(String.format("Found possible lengths specified at general " |
| + "desc: this should be derived from child elements. Affected country: %s", |
| metadataId)); |
| } |
| } |
| if (!isShortNumberMetadata) { |
| // Make a copy here since we want to remove some nodes, but we don't want to do that on our |
| // actual data. |
| Element allDescData = (Element) data.cloneNode(true /* deep copy */); |
| for (String tag : PHONE_NUMBER_DESCS_WITHOUT_MATCHING_TYPES) { |
| NodeList nodesToRemove = allDescData.getElementsByTagName(tag); |
| if (nodesToRemove.getLength() > 0) { |
| // We check when we process phone number descriptions that there are only one of each |
| // type, so this is safe to do. |
| allDescData.removeChild(nodesToRemove.item(0)); |
| } |
| } |
| populatePossibleLengthSets(allDescData, lengths, localOnlyLengths); |
| } else { |
| // For short number metadata, we want to copy the lengths from the "short code" section only. |
| // This is because it's the more detailed validation pattern, it's not a sub-type of short |
| // codes. The other lengths will be checked later to see that they are a sub-set of these |
| // possible lengths. |
| NodeList shortCodeDescList = data.getElementsByTagName(SHORT_CODE); |
| if (shortCodeDescList.getLength() > 0) { |
| Element shortCodeDesc = (Element) shortCodeDescList.item(0); |
| populatePossibleLengthSets(shortCodeDesc, lengths, localOnlyLengths); |
| } |
| if (localOnlyLengths.size() > 0) { |
| throw new RuntimeException("Found local-only lengths in short-number metadata"); |
| } |
| } |
| setPossibleLengths(lengths, localOnlyLengths, null, generalDesc); |
| } |
| |
| /** |
| * Sets the possible length fields in the metadata from the sets of data passed in. Checks that |
| * the length is covered by the "parent" phone number description element if one is present, and |
| * if the lengths are exactly the same as this, they are not filled in for efficiency reasons. |
| * |
| * @param parentDesc the "general description" element or null if desc is the generalDesc itself |
| * @param desc the PhoneNumberDesc object that we are going to set lengths for |
| */ |
| private static void setPossibleLengths(TreeSet<Integer> lengths, |
| TreeSet<Integer> localOnlyLengths, PhoneNumberDesc parentDesc, PhoneNumberDesc.Builder desc) { |
| // We clear these fields since the metadata tends to inherit from the parent element for other |
| // fields (via a mergeFrom). |
| desc.clearPossibleLength(); |
| desc.clearPossibleLengthLocalOnly(); |
| // Only add the lengths to this sub-type if they aren't exactly the same as the possible |
| // lengths in the general desc (for metadata size reasons). |
| if (parentDesc == null || !arePossibleLengthsEqual(lengths, parentDesc)) { |
| for (Integer length : lengths) { |
| if (parentDesc == null || parentDesc.getPossibleLengthList().contains(length)) { |
| desc.addPossibleLength(length); |
| } else { |
| // We shouldn't have possible lengths defined in a child element that are not covered by |
| // the general description. We check this here even though the general description is |
| // derived from child elements because it is only derived from a subset, and we need to |
| // ensure *all* child elements have a valid possible length. |
| throw new RuntimeException(String.format( |
| "Out-of-range possible length found (%d), parent lengths %s.", |
| length, parentDesc.getPossibleLengthList())); |
| } |
| } |
| } |
| // We check that the local-only length isn't also a normal possible length (only relevant for |
| // the general-desc, since within elements such as fixed-line we would throw an exception if we |
| // saw this) before adding it to the collection of possible local-only lengths. |
| for (Integer length : localOnlyLengths) { |
| if (!lengths.contains(length)) { |
| // We check it is covered by either of the possible length sets of the parent |
| // PhoneNumberDesc, because for example 7 might be a valid localOnly length for mobile, but |
| // a valid national length for fixedLine, so the generalDesc would have the 7 removed from |
| // localOnly. |
| if (parentDesc == null || parentDesc.getPossibleLengthLocalOnlyList().contains(length) |
| || parentDesc.getPossibleLengthList().contains(length)) { |
| desc.addPossibleLengthLocalOnly(length); |
| } else { |
| throw new RuntimeException(String.format( |
| "Out-of-range local-only possible length found (%d), parent length %s.", |
| length, parentDesc.getPossibleLengthLocalOnlyList())); |
| } |
| } |
| } |
| } |
| |
| // @VisibleForTesting |
| static PhoneMetadata.Builder loadCountryMetadata(String regionCode, |
| Element element, |
| boolean isShortNumberMetadata, |
| boolean isAlternateFormatsMetadata) { |
| String nationalPrefix = getNationalPrefix(element); |
| PhoneMetadata.Builder metadata = loadTerritoryTagMetadata(regionCode, element, nationalPrefix); |
| String nationalPrefixFormattingRule = |
| getNationalPrefixFormattingRuleFromElement(element, nationalPrefix); |
| loadAvailableFormats(metadata, element, nationalPrefix, |
| nationalPrefixFormattingRule, |
| element.hasAttribute(NATIONAL_PREFIX_OPTIONAL_WHEN_FORMATTING)); |
| if (!isAlternateFormatsMetadata) { |
| // The alternate formats metadata does not need most of the patterns to be set. |
| setRelevantDescPatterns(metadata, element, isShortNumberMetadata); |
| } |
| return metadata; |
| } |
| |
| /** |
| * Processes the custom build flags and gets a {@code MetadataFilter} which may be used to |
| * filter {@code PhoneMetadata} objects. Incompatible flag combinations throw RuntimeException. |
| * |
| * @param liteBuild The liteBuild flag value as given by the command-line |
| * @param specialBuild The specialBuild flag value as given by the command-line |
| */ |
| // @VisibleForTesting |
| static MetadataFilter getMetadataFilter(boolean liteBuild, boolean specialBuild) { |
| if (specialBuild) { |
| if (liteBuild) { |
| throw new RuntimeException("liteBuild and specialBuild may not both be set"); |
| } |
| return MetadataFilter.forSpecialBuild(); |
| } |
| if (liteBuild) { |
| return MetadataFilter.forLiteBuild(); |
| } |
| return MetadataFilter.emptyFilter(); |
| } |
| } |