• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
2  * Copyright (C) 2016 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 
17 package com.google.i18n.phonenumbers;
18 
19 import com.google.i18n.phonenumbers.Phonemetadata.PhoneMetadata;
20 import com.google.i18n.phonenumbers.Phonemetadata.PhoneNumberDesc;
21 import java.util.Arrays;
22 import java.util.TreeMap;
23 import java.util.TreeSet;
24 
25 /**
26  * Class to encapsulate the metadata filtering logic and restrict visibility into raw data
27  * structures.
28  *
29  * <p>
30  * WARNING: This is an internal API which is under development and subject to backwards-incompatible
31  * changes without notice. Any changes are not guaranteed to be reflected in the versioning scheme
32  * of the public API, nor in release notes.
33  */
34 final class MetadataFilter {
35   // The following 3 sets comprise all the PhoneMetadata fields as defined at phonemetadata.proto
36   // which may be excluded from customized serializations of the binary metadata. Fields that are
37   // core to the library functionality may not be listed here.
38   // excludableParentFields are PhoneMetadata fields of type PhoneNumberDesc.
39   // excludableChildFields are PhoneNumberDesc fields of primitive type.
40   // excludableChildlessFields are PhoneMetadata fields of primitive type.
41   // Currently we support only one non-primitive type and the depth of the "family tree" is 2,
42   // meaning a field may have only direct descendants, who may not have descendants of their own. If
43   // this changes, the blacklist handling in this class should also change.
44   // @VisibleForTesting
45   static final TreeSet<String> excludableParentFields = new TreeSet<String>(Arrays.asList(
46       "fixedLine",
47       "mobile",
48       "tollFree",
49       "premiumRate",
50       "sharedCost",
51       "personalNumber",
52       "voip",
53       "pager",
54       "uan",
55       "emergency",
56       "voicemail",
57       "shortCode",
58       "standardRate",
59       "carrierSpecific",
60       "smsServices",
61       "noInternationalDialling"));
62 
63   // Note: If this set changes, the descHasData implementation must change in PhoneNumberUtil.
64   // The current implementation assumes that all PhoneNumberDesc fields are present here, since it
65   // "clears" a PhoneNumberDesc field by simply clearing all of the fields under it. See the comment
66   // above, about all 3 sets, for more about these fields.
67   // @VisibleForTesting
68   static final TreeSet<String> excludableChildFields = new TreeSet<String>(Arrays.asList(
69       "nationalNumberPattern",
70       "possibleLength",
71       "possibleLengthLocalOnly",
72       "exampleNumber"));
73 
74   // @VisibleForTesting
75   static final TreeSet<String> excludableChildlessFields = new TreeSet<String>(Arrays.asList(
76       "preferredInternationalPrefix",
77       "nationalPrefix",
78       "preferredExtnPrefix",
79       "nationalPrefixTransformRule",
80       "sameMobileAndFixedLinePattern",
81       "mainCountryForCode",
82       "leadingZeroPossible",
83       "mobileNumberPortableRegion"));
84 
85   private final TreeMap<String, TreeSet<String>> blacklist;
86 
87   // Note: If changing the blacklist here or the name of the method, update documentation about
88   // affected methods at the same time:
89   // https://github.com/google/libphonenumber/blob/master/FAQ.md#what-is-the-metadatalitejsmetadata_lite-option
forLiteBuild()90   static MetadataFilter forLiteBuild() {
91     // "exampleNumber" is a blacklist.
92     return new MetadataFilter(parseFieldMapFromString("exampleNumber"));
93   }
94 
forSpecialBuild()95   static MetadataFilter forSpecialBuild() {
96     // "mobile" is a whitelist.
97     return new MetadataFilter(computeComplement(parseFieldMapFromString("mobile")));
98   }
99 
emptyFilter()100   static MetadataFilter emptyFilter() {
101     // Empty blacklist, meaning we filter nothing.
102     return new MetadataFilter(new TreeMap<String, TreeSet<String>>());
103   }
104 
105   // @VisibleForTesting
MetadataFilter(TreeMap<String, TreeSet<String>> blacklist)106   MetadataFilter(TreeMap<String, TreeSet<String>> blacklist) {
107     this.blacklist = blacklist;
108   }
109 
110   @Override
equals(Object obj)111   public boolean equals(Object obj) {
112     return blacklist.equals(((MetadataFilter) obj).blacklist);
113   }
114 
115   @Override
hashCode()116   public int hashCode() {
117     return blacklist.hashCode();
118   }
119 
120   /**
121    * Clears certain fields in {@code metadata} as defined by the {@code MetadataFilter} instance.
122    * Note that this changes the mutable {@code metadata} object, and is not thread-safe. If this
123    * method does not return successfully, do not assume {@code metadata} has not changed.
124    *
125    * @param metadata  The {@code PhoneMetadata} object to be filtered
126    */
filterMetadata(PhoneMetadata.Builder metadata)127   void filterMetadata(PhoneMetadata.Builder metadata) {
128     // TODO: Consider clearing if the filtered PhoneNumberDesc is empty.
129     if (metadata.hasFixedLine()) {
130       metadata.setFixedLine(getFiltered("fixedLine", metadata.getFixedLine()));
131     }
132     if (metadata.hasMobile()) {
133       metadata.setMobile(getFiltered("mobile", metadata.getMobile()));
134     }
135     if (metadata.hasTollFree()) {
136       metadata.setTollFree(getFiltered("tollFree", metadata.getTollFree()));
137     }
138     if (metadata.hasPremiumRate()) {
139       metadata.setPremiumRate(getFiltered("premiumRate", metadata.getPremiumRate()));
140     }
141     if (metadata.hasSharedCost()) {
142       metadata.setSharedCost(getFiltered("sharedCost", metadata.getSharedCost()));
143     }
144     if (metadata.hasPersonalNumber()) {
145       metadata.setPersonalNumber(getFiltered("personalNumber", metadata.getPersonalNumber()));
146     }
147     if (metadata.hasVoip()) {
148       metadata.setVoip(getFiltered("voip", metadata.getVoip()));
149     }
150     if (metadata.hasPager()) {
151       metadata.setPager(getFiltered("pager", metadata.getPager()));
152     }
153     if (metadata.hasUan()) {
154       metadata.setUan(getFiltered("uan", metadata.getUan()));
155     }
156     if (metadata.hasEmergency()) {
157       metadata.setEmergency(getFiltered("emergency", metadata.getEmergency()));
158     }
159     if (metadata.hasVoicemail()) {
160       metadata.setVoicemail(getFiltered("voicemail", metadata.getVoicemail()));
161     }
162     if (metadata.hasShortCode()) {
163       metadata.setShortCode(getFiltered("shortCode", metadata.getShortCode()));
164     }
165     if (metadata.hasStandardRate()) {
166       metadata.setStandardRate(getFiltered("standardRate", metadata.getStandardRate()));
167     }
168     if (metadata.hasCarrierSpecific()) {
169       metadata.setCarrierSpecific(getFiltered("carrierSpecific", metadata.getCarrierSpecific()));
170     }
171     if (metadata.hasSmsServices()) {
172       metadata.setSmsServices(getFiltered("smsServices", metadata.getSmsServices()));
173     }
174     if (metadata.hasNoInternationalDialling()) {
175       metadata.setNoInternationalDialling(getFiltered("noInternationalDialling",
176               metadata.getNoInternationalDialling()));
177     }
178 
179     if (shouldDrop("preferredInternationalPrefix")) {
180       metadata.clearPreferredInternationalPrefix();
181     }
182     if (shouldDrop("nationalPrefix")) {
183       metadata.clearNationalPrefix();
184     }
185     if (shouldDrop("preferredExtnPrefix")) {
186       metadata.clearPreferredExtnPrefix();
187     }
188     if (shouldDrop("nationalPrefixTransformRule")) {
189       metadata.clearNationalPrefixTransformRule();
190     }
191     if (shouldDrop("sameMobileAndFixedLinePattern")) {
192       metadata.clearSameMobileAndFixedLinePattern();
193     }
194     if (shouldDrop("mainCountryForCode")) {
195       metadata.clearMainCountryForCode();
196     }
197     if (shouldDrop("leadingZeroPossible")) {
198       metadata.clearLeadingZeroPossible();
199     }
200     if (shouldDrop("mobileNumberPortableRegion")) {
201       metadata.clearMobileNumberPortableRegion();
202     }
203   }
204 
205   /**
206    * The input blacklist or whitelist string is expected to be of the form "a(b,c):d(e):f", where
207    * b and c are children of a, e is a child of d, and f is either a parent field, a child field, or
208    * a childless field. Order and whitespace don't matter. We throw RuntimeException for any
209    * duplicates, malformed strings, or strings where field tokens do not correspond to strings in
210    * the sets of excludable fields. We also throw RuntimeException for empty strings since such
211    * strings should be treated as a special case by the flag checking code and not passed here.
212    */
213   // @VisibleForTesting
parseFieldMapFromString(String string)214   static TreeMap<String, TreeSet<String>> parseFieldMapFromString(String string) {
215     if (string == null) {
216       throw new RuntimeException("Null string should not be passed to parseFieldMapFromString");
217     }
218     // Remove whitespace.
219     string = string.replaceAll("\\s", "");
220     if (string.isEmpty()) {
221       throw new RuntimeException("Empty string should not be passed to parseFieldMapFromString");
222     }
223 
224     TreeMap<String, TreeSet<String>> fieldMap = new TreeMap<String, TreeSet<String>>();
225     TreeSet<String> wildcardChildren = new TreeSet<String>();
226     for (String group : string.split(":", -1)) {
227       int leftParenIndex = group.indexOf('(');
228       int rightParenIndex = group.indexOf(')');
229       if (leftParenIndex < 0 && rightParenIndex < 0) {
230         if (excludableParentFields.contains(group)) {
231           if (fieldMap.containsKey(group)) {
232             throw new RuntimeException(group + " given more than once in " + string);
233           }
234           fieldMap.put(group, new TreeSet<String>(excludableChildFields));
235         } else if (excludableChildlessFields.contains(group)) {
236           if (fieldMap.containsKey(group)) {
237             throw new RuntimeException(group + " given more than once in " + string);
238           }
239           fieldMap.put(group, new TreeSet<String>());
240         } else if (excludableChildFields.contains(group)) {
241           if (wildcardChildren.contains(group)) {
242             throw new RuntimeException(group + " given more than once in " + string);
243           }
244           wildcardChildren.add(group);
245         } else {
246           throw new RuntimeException(group + " is not a valid token");
247         }
248       } else if (leftParenIndex > 0 && rightParenIndex == group.length() - 1) {
249         // We don't check for duplicate parentheses or illegal characters since these will be caught
250         // as not being part of valid field tokens.
251         String parent = group.substring(0, leftParenIndex);
252         if (!excludableParentFields.contains(parent)) {
253           throw new RuntimeException(parent + " is not a valid parent token");
254         }
255         if (fieldMap.containsKey(parent)) {
256           throw new RuntimeException(parent + " given more than once in " + string);
257         }
258         TreeSet<String> children = new TreeSet<String>();
259         for (String child : group.substring(leftParenIndex + 1, rightParenIndex).split(",", -1)) {
260           if (!excludableChildFields.contains(child)) {
261             throw new RuntimeException(child + " is not a valid child token");
262           }
263           if (!children.add(child)) {
264             throw new RuntimeException(child + " given more than once in " + group);
265           }
266         }
267         fieldMap.put(parent, children);
268       } else {
269         throw new RuntimeException("Incorrect location of parantheses in " + group);
270       }
271     }
272     for (String wildcardChild : wildcardChildren) {
273       for (String parent : excludableParentFields) {
274         TreeSet<String> children = fieldMap.get(parent);
275         if (children == null) {
276           children = new TreeSet<String>();
277           fieldMap.put(parent, children);
278         }
279         if (!children.add(wildcardChild)
280             && fieldMap.get(parent).size() != excludableChildFields.size()) {
281           // The map already contains parent -> wildcardChild but not all possible children.
282           // So wildcardChild was given explicitly as a child of parent, which is a duplication
283           // since it's also given as a wildcard child.
284           throw new RuntimeException(
285               wildcardChild + " is present by itself so remove it from " + parent + "'s group");
286         }
287       }
288     }
289     return fieldMap;
290   }
291 
292   // Does not check that legal tokens are used, assuming that fieldMap is constructed using
293   // parseFieldMapFromString(String) which does check. If fieldMap contains illegal tokens or parent
294   // fields with no children or other unexpected state, the behavior of this function is undefined.
295   // @VisibleForTesting
computeComplement( TreeMap<String, TreeSet<String>> fieldMap)296   static TreeMap<String, TreeSet<String>> computeComplement(
297       TreeMap<String, TreeSet<String>> fieldMap) {
298     TreeMap<String, TreeSet<String>> complement = new TreeMap<String, TreeSet<String>>();
299     for (String parent : excludableParentFields) {
300       if (!fieldMap.containsKey(parent)) {
301         complement.put(parent, new TreeSet<String>(excludableChildFields));
302       } else {
303         TreeSet<String> otherChildren = fieldMap.get(parent);
304         // If the other map has all the children for this parent then we don't want to include the
305         // parent as a key.
306         if (otherChildren.size() != excludableChildFields.size()) {
307           TreeSet<String> children = new TreeSet<String>();
308           for (String child : excludableChildFields) {
309             if (!otherChildren.contains(child)) {
310               children.add(child);
311             }
312           }
313           complement.put(parent, children);
314         }
315       }
316     }
317     for (String childlessField : excludableChildlessFields) {
318       if (!fieldMap.containsKey(childlessField)) {
319         complement.put(childlessField, new TreeSet<String>());
320       }
321     }
322     return complement;
323   }
324 
325   // @VisibleForTesting
shouldDrop(String parent, String child)326   boolean shouldDrop(String parent, String child) {
327     if (!excludableParentFields.contains(parent)) {
328       throw new RuntimeException(parent + " is not an excludable parent field");
329     }
330     if (!excludableChildFields.contains(child)) {
331       throw new RuntimeException(child + " is not an excludable child field");
332     }
333     return blacklist.containsKey(parent) && blacklist.get(parent).contains(child);
334   }
335 
336   // @VisibleForTesting
shouldDrop(String childlessField)337   boolean shouldDrop(String childlessField) {
338     if (!excludableChildlessFields.contains(childlessField)) {
339       throw new RuntimeException(childlessField + " is not an excludable childless field");
340     }
341     return blacklist.containsKey(childlessField);
342   }
343 
getFiltered(String type, PhoneNumberDesc desc)344   private PhoneNumberDesc getFiltered(String type, PhoneNumberDesc desc) {
345     PhoneNumberDesc.Builder builder = PhoneNumberDesc.newBuilder().mergeFrom(desc);
346     if (shouldDrop(type, "nationalNumberPattern")) {
347       builder.clearNationalNumberPattern();
348     }
349     if (shouldDrop(type, "possibleLength")) {
350       builder.clearPossibleLength();
351     }
352     if (shouldDrop(type, "possibleLengthLocalOnly")) {
353       builder.clearPossibleLengthLocalOnly();
354     }
355     if (shouldDrop(type, "exampleNumber")) {
356       builder.clearExampleNumber();
357     }
358     return builder.build();
359   }
360 }
361