• 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       "mobileNumberPortableRegion"));
83 
84   private final TreeMap<String, TreeSet<String>> blacklist;
85 
86   // Note: If changing the blacklist here or the name of the method, update documentation about
87   // affected methods at the same time:
88   // https://github.com/google/libphonenumber/blob/master/FAQ.md#what-is-the-metadatalitejsmetadata_lite-option
forLiteBuild()89   static MetadataFilter forLiteBuild() {
90     // "exampleNumber" is a blacklist.
91     return new MetadataFilter(parseFieldMapFromString("exampleNumber"));
92   }
93 
forSpecialBuild()94   static MetadataFilter forSpecialBuild() {
95     // "mobile" is a whitelist.
96     return new MetadataFilter(computeComplement(parseFieldMapFromString("mobile")));
97   }
98 
emptyFilter()99   static MetadataFilter emptyFilter() {
100     // Empty blacklist, meaning we filter nothing.
101     return new MetadataFilter(new TreeMap<String, TreeSet<String>>());
102   }
103 
104   // @VisibleForTesting
MetadataFilter(TreeMap<String, TreeSet<String>> blacklist)105   MetadataFilter(TreeMap<String, TreeSet<String>> blacklist) {
106     this.blacklist = blacklist;
107   }
108 
109   @Override
equals(Object obj)110   public boolean equals(Object obj) {
111     return blacklist.equals(((MetadataFilter) obj).blacklist);
112   }
113 
114   @Override
hashCode()115   public int hashCode() {
116     return blacklist.hashCode();
117   }
118 
119   /**
120    * Clears certain fields in {@code metadata} as defined by the {@code MetadataFilter} instance.
121    * Note that this changes the mutable {@code metadata} object, and is not thread-safe. If this
122    * method does not return successfully, do not assume {@code metadata} has not changed.
123    *
124    * @param metadata  The {@code PhoneMetadata} object to be filtered
125    */
filterMetadata(PhoneMetadata.Builder metadata)126   void filterMetadata(PhoneMetadata.Builder metadata) {
127     // TODO: Consider clearing if the filtered PhoneNumberDesc is empty.
128     if (metadata.hasFixedLine()) {
129       metadata.setFixedLine(getFiltered("fixedLine", metadata.getFixedLine()));
130     }
131     if (metadata.hasMobile()) {
132       metadata.setMobile(getFiltered("mobile", metadata.getMobile()));
133     }
134     if (metadata.hasTollFree()) {
135       metadata.setTollFree(getFiltered("tollFree", metadata.getTollFree()));
136     }
137     if (metadata.hasPremiumRate()) {
138       metadata.setPremiumRate(getFiltered("premiumRate", metadata.getPremiumRate()));
139     }
140     if (metadata.hasSharedCost()) {
141       metadata.setSharedCost(getFiltered("sharedCost", metadata.getSharedCost()));
142     }
143     if (metadata.hasPersonalNumber()) {
144       metadata.setPersonalNumber(getFiltered("personalNumber", metadata.getPersonalNumber()));
145     }
146     if (metadata.hasVoip()) {
147       metadata.setVoip(getFiltered("voip", metadata.getVoip()));
148     }
149     if (metadata.hasPager()) {
150       metadata.setPager(getFiltered("pager", metadata.getPager()));
151     }
152     if (metadata.hasUan()) {
153       metadata.setUan(getFiltered("uan", metadata.getUan()));
154     }
155     if (metadata.hasEmergency()) {
156       metadata.setEmergency(getFiltered("emergency", metadata.getEmergency()));
157     }
158     if (metadata.hasVoicemail()) {
159       metadata.setVoicemail(getFiltered("voicemail", metadata.getVoicemail()));
160     }
161     if (metadata.hasShortCode()) {
162       metadata.setShortCode(getFiltered("shortCode", metadata.getShortCode()));
163     }
164     if (metadata.hasStandardRate()) {
165       metadata.setStandardRate(getFiltered("standardRate", metadata.getStandardRate()));
166     }
167     if (metadata.hasCarrierSpecific()) {
168       metadata.setCarrierSpecific(getFiltered("carrierSpecific", metadata.getCarrierSpecific()));
169     }
170     if (metadata.hasSmsServices()) {
171       metadata.setSmsServices(getFiltered("smsServices", metadata.getSmsServices()));
172     }
173     if (metadata.hasNoInternationalDialling()) {
174       metadata.setNoInternationalDialling(getFiltered("noInternationalDialling",
175               metadata.getNoInternationalDialling()));
176     }
177 
178     if (shouldDrop("preferredInternationalPrefix")) {
179       metadata.clearPreferredInternationalPrefix();
180     }
181     if (shouldDrop("nationalPrefix")) {
182       metadata.clearNationalPrefix();
183     }
184     if (shouldDrop("preferredExtnPrefix")) {
185       metadata.clearPreferredExtnPrefix();
186     }
187     if (shouldDrop("nationalPrefixTransformRule")) {
188       metadata.clearNationalPrefixTransformRule();
189     }
190     if (shouldDrop("sameMobileAndFixedLinePattern")) {
191       metadata.clearSameMobileAndFixedLinePattern();
192     }
193     if (shouldDrop("mainCountryForCode")) {
194       metadata.clearMainCountryForCode();
195     }
196     if (shouldDrop("mobileNumberPortableRegion")) {
197       metadata.clearMobileNumberPortableRegion();
198     }
199   }
200 
201   /**
202    * The input blacklist or whitelist string is expected to be of the form "a(b,c):d(e):f", where
203    * b and c are children of a, e is a child of d, and f is either a parent field, a child field, or
204    * a childless field. Order and whitespace don't matter. We throw RuntimeException for any
205    * duplicates, malformed strings, or strings where field tokens do not correspond to strings in
206    * the sets of excludable fields. We also throw RuntimeException for empty strings since such
207    * strings should be treated as a special case by the flag checking code and not passed here.
208    */
209   // @VisibleForTesting
parseFieldMapFromString(String string)210   static TreeMap<String, TreeSet<String>> parseFieldMapFromString(String string) {
211     if (string == null) {
212       throw new RuntimeException("Null string should not be passed to parseFieldMapFromString");
213     }
214     // Remove whitespace.
215     string = string.replaceAll("\\s", "");
216     if (string.isEmpty()) {
217       throw new RuntimeException("Empty string should not be passed to parseFieldMapFromString");
218     }
219 
220     TreeMap<String, TreeSet<String>> fieldMap = new TreeMap<String, TreeSet<String>>();
221     TreeSet<String> wildcardChildren = new TreeSet<String>();
222     for (String group : string.split(":", -1)) {
223       int leftParenIndex = group.indexOf('(');
224       int rightParenIndex = group.indexOf(')');
225       if (leftParenIndex < 0 && rightParenIndex < 0) {
226         if (excludableParentFields.contains(group)) {
227           if (fieldMap.containsKey(group)) {
228             throw new RuntimeException(group + " given more than once in " + string);
229           }
230           fieldMap.put(group, new TreeSet<String>(excludableChildFields));
231         } else if (excludableChildlessFields.contains(group)) {
232           if (fieldMap.containsKey(group)) {
233             throw new RuntimeException(group + " given more than once in " + string);
234           }
235           fieldMap.put(group, new TreeSet<String>());
236         } else if (excludableChildFields.contains(group)) {
237           if (wildcardChildren.contains(group)) {
238             throw new RuntimeException(group + " given more than once in " + string);
239           }
240           wildcardChildren.add(group);
241         } else {
242           throw new RuntimeException(group + " is not a valid token");
243         }
244       } else if (leftParenIndex > 0 && rightParenIndex == group.length() - 1) {
245         // We don't check for duplicate parentheses or illegal characters since these will be caught
246         // as not being part of valid field tokens.
247         String parent = group.substring(0, leftParenIndex);
248         if (!excludableParentFields.contains(parent)) {
249           throw new RuntimeException(parent + " is not a valid parent token");
250         }
251         if (fieldMap.containsKey(parent)) {
252           throw new RuntimeException(parent + " given more than once in " + string);
253         }
254         TreeSet<String> children = new TreeSet<String>();
255         for (String child : group.substring(leftParenIndex + 1, rightParenIndex).split(",", -1)) {
256           if (!excludableChildFields.contains(child)) {
257             throw new RuntimeException(child + " is not a valid child token");
258           }
259           if (!children.add(child)) {
260             throw new RuntimeException(child + " given more than once in " + group);
261           }
262         }
263         fieldMap.put(parent, children);
264       } else {
265         throw new RuntimeException("Incorrect location of parantheses in " + group);
266       }
267     }
268     for (String wildcardChild : wildcardChildren) {
269       for (String parent : excludableParentFields) {
270         TreeSet<String> children = fieldMap.get(parent);
271         if (children == null) {
272           children = new TreeSet<String>();
273           fieldMap.put(parent, children);
274         }
275         if (!children.add(wildcardChild)
276             && fieldMap.get(parent).size() != excludableChildFields.size()) {
277           // The map already contains parent -> wildcardChild but not all possible children.
278           // So wildcardChild was given explicitly as a child of parent, which is a duplication
279           // since it's also given as a wildcard child.
280           throw new RuntimeException(
281               wildcardChild + " is present by itself so remove it from " + parent + "'s group");
282         }
283       }
284     }
285     return fieldMap;
286   }
287 
288   // Does not check that legal tokens are used, assuming that fieldMap is constructed using
289   // parseFieldMapFromString(String) which does check. If fieldMap contains illegal tokens or parent
290   // fields with no children or other unexpected state, the behavior of this function is undefined.
291   // @VisibleForTesting
computeComplement( TreeMap<String, TreeSet<String>> fieldMap)292   static TreeMap<String, TreeSet<String>> computeComplement(
293       TreeMap<String, TreeSet<String>> fieldMap) {
294     TreeMap<String, TreeSet<String>> complement = new TreeMap<String, TreeSet<String>>();
295     for (String parent : excludableParentFields) {
296       if (!fieldMap.containsKey(parent)) {
297         complement.put(parent, new TreeSet<String>(excludableChildFields));
298       } else {
299         TreeSet<String> otherChildren = fieldMap.get(parent);
300         // If the other map has all the children for this parent then we don't want to include the
301         // parent as a key.
302         if (otherChildren.size() != excludableChildFields.size()) {
303           TreeSet<String> children = new TreeSet<String>();
304           for (String child : excludableChildFields) {
305             if (!otherChildren.contains(child)) {
306               children.add(child);
307             }
308           }
309           complement.put(parent, children);
310         }
311       }
312     }
313     for (String childlessField : excludableChildlessFields) {
314       if (!fieldMap.containsKey(childlessField)) {
315         complement.put(childlessField, new TreeSet<String>());
316       }
317     }
318     return complement;
319   }
320 
321   // @VisibleForTesting
shouldDrop(String parent, String child)322   boolean shouldDrop(String parent, String child) {
323     if (!excludableParentFields.contains(parent)) {
324       throw new RuntimeException(parent + " is not an excludable parent field");
325     }
326     if (!excludableChildFields.contains(child)) {
327       throw new RuntimeException(child + " is not an excludable child field");
328     }
329     return blacklist.containsKey(parent) && blacklist.get(parent).contains(child);
330   }
331 
332   // @VisibleForTesting
shouldDrop(String childlessField)333   boolean shouldDrop(String childlessField) {
334     if (!excludableChildlessFields.contains(childlessField)) {
335       throw new RuntimeException(childlessField + " is not an excludable childless field");
336     }
337     return blacklist.containsKey(childlessField);
338   }
339 
getFiltered(String type, PhoneNumberDesc desc)340   private PhoneNumberDesc getFiltered(String type, PhoneNumberDesc desc) {
341     PhoneNumberDesc.Builder builder = PhoneNumberDesc.newBuilder().mergeFrom(desc);
342     if (shouldDrop(type, "nationalNumberPattern")) {
343       builder.clearNationalNumberPattern();
344     }
345     if (shouldDrop(type, "possibleLength")) {
346       builder.clearPossibleLength();
347     }
348     if (shouldDrop(type, "possibleLengthLocalOnly")) {
349       builder.clearPossibleLengthLocalOnly();
350     }
351     if (shouldDrop(type, "exampleNumber")) {
352       builder.clearExampleNumber();
353     }
354     return builder.build();
355   }
356 }
357