1 /**
2  * Copyright (c) 2016, The Android Open Source Project
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 android.net.wifi.hotspot2.omadm;
18 
19 import android.net.wifi.hotspot2.PasspointConfiguration;
20 import android.net.wifi.hotspot2.pps.Credential;
21 import android.net.wifi.hotspot2.pps.HomeSp;
22 import android.net.wifi.hotspot2.pps.Policy;
23 import android.net.wifi.hotspot2.pps.UpdateParameter;
24 import android.text.TextUtils;
25 import android.util.Log;
26 import android.util.Pair;
27 
28 import com.android.modules.utils.build.SdkLevel;
29 
30 import org.xml.sax.SAXException;
31 
32 import java.io.IOException;
33 import java.text.DateFormat;
34 import java.text.ParseException;
35 import java.text.SimpleDateFormat;
36 import java.util.ArrayList;
37 import java.util.HashMap;
38 import java.util.HashSet;
39 import java.util.List;
40 import java.util.Map;
41 import java.util.Set;
42 
43 /**
44  * Utility class for converting OMA-DM (Open Mobile Alliance's Device Management)
45  * PPS-MO (PerProviderSubscription Management Object) XML tree to a
46  * {@link PasspointConfiguration} object.
47  *
48  * Currently this only supports PerProviderSubscription/HomeSP and
49  * PerProviderSubscription/Credential subtree for Hotspot 2.0 Release 1 support.
50  *
51  * For more info, refer to Hotspot 2.0 PPS MO defined in section 9.1 of the Hotspot 2.0
52  * Release 2 Technical Specification.
53  *
54  * Below is a sample XML string for a Release 1 PPS MO tree:
55  *
56  * <MgmtTree xmlns="syncml:dmddf1.2">
57  *   <VerDTD>1.2</VerDTD>
58  *   <Node>
59  *     <NodeName>PerProviderSubscription</NodeName>
60  *     <RTProperties>
61  *       <Type>
62  *         <DDFName>urn:wfa:mo:hotspot2dot0­perprovidersubscription:1.0</DDFName>
63  *       </Type>
64  *     </RTProperties>
65  *     <Node>
66  *       <NodeName>i001</NodeName>
67  *       <Node>
68  *         <NodeName>HomeSP</NodeName>
69  *         <Node>
70  *           <NodeName>FriendlyName</NodeName>
71  *           <Value>Century House</Value>
72  *         </Node>
73  *         <Node>
74  *           <NodeName>FQDN</NodeName>
75  *           <Value>mi6.co.uk</Value>
76  *         </Node>
77  *         <Node>
78  *           <NodeName>RoamingConsortiumOI</NodeName>
79  *           <Value>112233,445566</Value>
80  *         </Node>
81  *       </Node>
82  *       <Node>
83  *         <NodeName>Credential</NodeName>
84  *         <Node>
85  *           <NodeName>Realm</NodeName>
86  *           <Value>shaken.stirred.com</Value>
87  *         </Node>
88  *         <Node>
89  *           <NodeName>UsernamePassword</NodeName>
90  *           <Node>
91  *             <NodeName>Username</NodeName>
92  *             <Value>james</Value>
93  *           </Node>
94  *           <Node>
95  *             <NodeName>Password</NodeName>
96  *             <Value>Ym9uZDAwNw==</Value>
97  *           </Node>
98  *           <Node>
99  *             <NodeName>EAPMethod</NodeName>
100  *             <Node>
101  *               <NodeName>EAPType</NodeName>
102  *               <Value>21</Value>
103  *             </Node>
104  *             <Node>
105  *               <NodeName>InnerMethod</NodeName>
106  *               <Value>MS-CHAP-V2</Value>
107  *             </Node>
108  *           </Node>
109  *         </Node>
110  *       </Node>
111  *     </Node>
112  *   </Node>
113  * </MgmtTree>
114  */
115 public final class PpsMoParser {
116     private static final String TAG = "PpsMoParser";
117 
118     /**
119      * XML tags expected in the PPS MO (PerProviderSubscription Management Object) XML tree.
120      */
121     private static final String TAG_MANAGEMENT_TREE = "MgmtTree";
122     private static final String TAG_VER_DTD = "VerDTD";
123     private static final String TAG_NODE = "Node";
124     private static final String TAG_NODE_NAME = "NodeName";
125     private static final String TAG_RT_PROPERTIES = "RTProperties";
126     private static final String TAG_TYPE = "Type";
127     private static final String TAG_DDF_NAME = "DDFName";
128     private static final String TAG_VALUE = "Value";
129 
130     /**
131      * Name for PerProviderSubscription node.
132      */
133     private static final String NODE_PER_PROVIDER_SUBSCRIPTION = "PerProviderSubscription";
134 
135     /**
136      * Fields under PerProviderSubscription.
137      */
138     private static final String NODE_UPDATE_IDENTIFIER = "UpdateIdentifier";
139     private static final String NODE_AAA_SERVER_TRUST_ROOT = "AAAServerTrustRoot";
140     private static final String NODE_SUBSCRIPTION_UPDATE = "SubscriptionUpdate";
141     private static final String NODE_SUBSCRIPTION_PARAMETER = "SubscriptionParameters";
142     private static final String NODE_TYPE_OF_SUBSCRIPTION = "TypeOfSubscription";
143     private static final String NODE_USAGE_LIMITS = "UsageLimits";
144     private static final String NODE_DATA_LIMIT = "DataLimit";
145     private static final String NODE_START_DATE = "StartDate";
146     private static final String NODE_TIME_LIMIT = "TimeLimit";
147     private static final String NODE_USAGE_TIME_PERIOD = "UsageTimePeriod";
148     private static final String NODE_CREDENTIAL_PRIORITY = "CredentialPriority";
149     private static final String NODE_EXTENSION = "Extension";
150 
151     /**
152      * Fields under Extension/Android subtree.
153      */
154     /*
155      * This node is used to put Android specific extension nodes and must be put
156      * under "Extension" node. Nodes with unknown names are allowed under this subtree.
157      * If there is any new node added in later release, it won't break older release parsing.
158      * <p>
159      * Ex:
160      * <Node>
161      *   <NodeName>Extension</NodeName>
162      *   <Node>
163      *     <NodeName>Android</NodeName>
164      *     <Node>
165      *       <NodeName>AndroidSpecificAttribute</NodeName>
166      *       <Value>AndroidSpecificValue</Value>
167      *     </Node>
168      *     <Node>
169      *       <NodeName>AndroidSpecificAttribute2</NodeName>
170      *       <Value>AndroidSpecificValue2</Value>
171      *     </Node>
172      *   </Node>
173      * </Node>
174      */
175     private static final String NODE_VENDOR_ANDROID = "Android";
176     /*
177      * This node describes AAA server trusted names. The trusted name must be put in
178      * a leaf named "FQDN". More than one trusted names can be provided by using
179      * semicolons to separate the strings (e.g., example.org;example.com).
180      * <p>
181      * Ex:
182      * <Node>
183      *   <NodeName>AAAServerTrustedNames</NodeName>
184      *   <Node>
185      *     <NodeName>FQDN</NodeName>
186      *     <Value>trusted.com;auth.net</Value>
187      *  </Node>
188      * <Node>
189      */
190     private static final String NODE_AAA_SERVER_TRUSTED_NAMES = "AAAServerTrustedNames";
191 
192     private static final String NODE_VENDOR_WBA = "WBA";
193     private static final String NODE_EXTENSION_NAI = "NAI";
194     private static final String NODE_DECORATED_IDENTITY_PREFIX = "DecoratedPrefix";
195 
196     /**
197      * Fields under HomeSP subtree.
198      */
199     private static final String NODE_HOMESP = "HomeSP";
200     private static final String NODE_FQDN = "FQDN";
201     private static final String NODE_FRIENDLY_NAME = "FriendlyName";
202     private static final String NODE_ROAMING_CONSORTIUM_OI = "RoamingConsortiumOI";
203     private static final String NODE_NETWORK_ID = "NetworkID";
204     private static final String NODE_SSID = "SSID";
205     private static final String NODE_HESSID = "HESSID";
206     private static final String NODE_ICON_URL = "IconURL";
207     private static final String NODE_HOME_OI_LIST = "HomeOIList";
208     private static final String NODE_HOME_OI = "HomeOI";
209     private static final String NODE_HOME_OI_REQUIRED = "HomeOIRequired";
210     private static final String NODE_OTHER_HOME_PARTNERS = "OtherHomePartners";
211 
212     /**
213      * Fields under Credential subtree.
214      */
215     private static final String NODE_CREDENTIAL = "Credential";
216     private static final String NODE_CREATION_DATE = "CreationDate";
217     private static final String NODE_EXPIRATION_DATE = "ExpirationDate";
218     private static final String NODE_USERNAME_PASSWORD = "UsernamePassword";
219     private static final String NODE_USERNAME = "Username";
220     private static final String NODE_PASSWORD = "Password";
221     private static final String NODE_MACHINE_MANAGED = "MachineManaged";
222     private static final String NODE_SOFT_TOKEN_APP = "SoftTokenApp";
223     private static final String NODE_ABLE_TO_SHARE = "AbleToShare";
224     private static final String NODE_EAP_METHOD = "EAPMethod";
225     private static final String NODE_EAP_TYPE = "EAPType";
226     private static final String NODE_VENDOR_ID = "VendorId";
227     private static final String NODE_VENDOR_TYPE = "VendorType";
228     private static final String NODE_INNER_EAP_TYPE = "InnerEAPType";
229     private static final String NODE_INNER_VENDOR_ID = "InnerVendorID";
230     private static final String NODE_INNER_VENDOR_TYPE = "InnerVendorType";
231     private static final String NODE_INNER_METHOD = "InnerMethod";
232     private static final String NODE_DIGITAL_CERTIFICATE = "DigitalCertificate";
233     private static final String NODE_CERTIFICATE_TYPE = "CertificateType";
234     private static final String NODE_CERT_SHA256_FINGERPRINT = "CertSHA256Fingerprint";
235     private static final String NODE_REALM = "Realm";
236     private static final String NODE_SIM = "SIM";
237     private static final String NODE_SIM_IMSI = "IMSI";
238     private static final String NODE_CHECK_AAA_SERVER_CERT_STATUS = "CheckAAAServerCertStatus";
239 
240     /**
241      * Fields under Policy subtree.
242      */
243     private static final String NODE_POLICY = "Policy";
244     private static final String NODE_PREFERRED_ROAMING_PARTNER_LIST =
245             "PreferredRoamingPartnerList";
246     private static final String NODE_FQDN_MATCH = "FQDN_Match";
247     private static final String NODE_PRIORITY = "Priority";
248     private static final String NODE_COUNTRY = "Country";
249     private static final String NODE_MIN_BACKHAUL_THRESHOLD = "MinBackhaulThreshold";
250     private static final String NODE_NETWORK_TYPE = "NetworkType";
251     private static final String NODE_DOWNLINK_BANDWIDTH = "DLBandwidth";
252     private static final String NODE_UPLINK_BANDWIDTH = "ULBandwidth";
253     private static final String NODE_POLICY_UPDATE = "PolicyUpdate";
254     private static final String NODE_UPDATE_INTERVAL = "UpdateInterval";
255     private static final String NODE_UPDATE_METHOD = "UpdateMethod";
256     private static final String NODE_RESTRICTION = "Restriction";
257     private static final String NODE_URI = "URI";
258     private static final String NODE_TRUST_ROOT = "TrustRoot";
259     private static final String NODE_CERT_URL = "CertURL";
260     private static final String NODE_SP_EXCLUSION_LIST = "SPExclusionList";
261     private static final String NODE_REQUIRED_PROTO_PORT_TUPLE = "RequiredProtoPortTuple";
262     private static final String NODE_IP_PROTOCOL = "IPProtocol";
263     private static final String NODE_PORT_NUMBER = "PortNumber";
264     private static final String NODE_MAXIMUM_BSS_LOAD_VALUE = "MaximumBSSLoadValue";
265     private static final String NODE_OTHER = "Other";
266 
267     /**
268      * URN (Unique Resource Name) for PerProviderSubscription Management Object Tree.
269      */
270     private static final String PPS_MO_URN =
271             "urn:wfa:mo:hotspot2dot0-perprovidersubscription:1.0";
272 
273     /**
274      * Exception for generic parsing errors.
275      */
276     private static class ParsingException extends Exception {
ParsingException(String message)277         public ParsingException(String message) {
278             super(message);
279         }
280     }
281 
282     /**
283      * Class representing a node within the PerProviderSubscription tree.
284      * This is used to flatten out and eliminate the extra layering in the XMLNode tree,
285      * to make the data parsing easier and cleaner.
286      *
287      * A PPSNode can be an internal or a leaf node, but not both.
288      *
289      */
290     private static abstract class PPSNode {
291         private final String mName;
PPSNode(String name)292         public PPSNode(String name) {
293             mName = name;
294         }
295 
296         /**
297          * @return the name of the node
298          */
getName()299         public String getName() {
300             return mName;
301         }
302 
303         /**
304          * Applies for internal node only.
305          *
306          * @return the list of children nodes.
307          */
getChildren()308         public abstract List<PPSNode> getChildren();
309 
310         /**
311          * Applies for leaf node only.
312          *
313          * @return the string value of the node
314          */
getValue()315         public abstract String getValue();
316 
317         /**
318          * @return a flag indicating if this is a leaf or an internal node
319          */
isLeaf()320         public abstract boolean isLeaf();
321     }
322 
323     /**
324      * Class representing a leaf node in a PPS (PerProviderSubscription) tree.
325      */
326     private static class LeafNode extends PPSNode {
327         private final String mValue;
LeafNode(String nodeName, String value)328         public LeafNode(String nodeName, String value) {
329             super(nodeName);
330             mValue = value;
331         }
332 
333         @Override
getValue()334         public String getValue() {
335             return mValue;
336         }
337 
338         @Override
getChildren()339         public List<PPSNode> getChildren() {
340             return null;
341         }
342 
343         @Override
isLeaf()344         public boolean isLeaf() {
345             return true;
346         }
347     }
348 
349     /**
350      * Class representing an internal node in a PPS (PerProviderSubscription) tree.
351      */
352     private static class InternalNode extends PPSNode {
353         private final List<PPSNode> mChildren;
InternalNode(String nodeName, List<PPSNode> children)354         public InternalNode(String nodeName, List<PPSNode> children) {
355             super(nodeName);
356             mChildren = children;
357         }
358 
359         @Override
getValue()360         public String getValue() {
361             return null;
362         }
363 
364         @Override
getChildren()365         public List<PPSNode> getChildren() {
366             return mChildren;
367         }
368 
369         @Override
isLeaf()370         public boolean isLeaf() {
371             return false;
372         }
373     }
374 
375     /**
376      * @hide
377      */
PpsMoParser()378     public PpsMoParser() {}
379 
380     /**
381      * Convert a XML string representation of a PPS MO (PerProviderSubscription
382      * Management Object) tree to a {@link PasspointConfiguration} object.
383      *
384      * @param xmlString XML string representation of a PPS MO tree
385      * @return {@link PasspointConfiguration} or null
386      */
parseMoText(String xmlString)387     public static PasspointConfiguration parseMoText(String xmlString) {
388         // Convert the XML string to a XML tree.
389         XMLParser xmlParser = new XMLParser();
390         XMLNode root = null;
391         try {
392             root = xmlParser.parse(xmlString);
393         } catch(IOException | SAXException e) {
394             Log.e(TAG, "Failed to parse XML input");
395             return null;
396         }
397         if (root == null) {
398             Log.e(TAG, "Root is not available");
399             return null;
400         }
401 
402         // Verify root node is a "MgmtTree" node.
403         if (root.getTag() != TAG_MANAGEMENT_TREE) {
404             Log.e(TAG, "Root is not a MgmtTree");
405             return null;
406         }
407 
408         String verDtd = null;    // Used for detecting duplicate VerDTD element.
409         PasspointConfiguration config = null;
410         for (XMLNode child : root.getChildren()) {
411             switch(child.getTag()) {
412                 case TAG_VER_DTD:
413                     if (verDtd != null) {
414                         Log.e(TAG, "Duplicate VerDTD element");
415                         return null;
416                     }
417                     verDtd = child.getText();
418                     break;
419                 case TAG_NODE:
420                     if (config != null) {
421                         Log.e(TAG, "Unexpected multiple Node element under MgmtTree");
422                         return null;
423                     }
424                     try {
425                         config = parsePpsNode(child);
426                     } catch (ParsingException e) {
427                         Log.e(TAG, e.getMessage(), e);
428                         return null;
429                     }
430                     break;
431                 default:
432                     Log.e(TAG, "Unknown node: " + child.getTag());
433                     return null;
434             }
435         }
436         return config;
437     }
438 
439     /**
440      * Parse a PerProviderSubscription node. Below is the format of the XML tree (with
441      * each XML element represent a node in the tree):
442      *
443      * <Node>
444      *   <NodeName>PerProviderSubscription</NodeName>
445      *   <RTProperties>
446      *     ...
447      *   </RTPProperties>
448      *   <Node>
449      *     <NodeName>UpdateIdentifier</NodeName>
450      *     <Value>...</Value>
451      *   </Node>
452      *   <Node>
453      *     ...
454      *   </Node>
455      * </Node>
456      *
457      * @param node XMLNode that contains PerProviderSubscription node.
458      * @return PasspointConfiguration or null
459      * @throws ParsingException
460      */
parsePpsNode(XMLNode node)461     private static PasspointConfiguration parsePpsNode(XMLNode node)
462             throws ParsingException {
463         PasspointConfiguration config = null;
464         String nodeName = null;
465         int updateIdentifier = Integer.MIN_VALUE;
466         for (XMLNode child : node.getChildren()) {
467             switch (child.getTag()) {
468                 case TAG_NODE_NAME:
469                     if (nodeName != null) {
470                         throw new ParsingException("Duplicate NodeName: " + child.getText());
471                     }
472                     nodeName = child.getText();
473                     if (!TextUtils.equals(nodeName, NODE_PER_PROVIDER_SUBSCRIPTION)) {
474                         throw new ParsingException("Unexpected NodeName: " + nodeName);
475                     }
476                     break;
477                 case TAG_NODE:
478                     // A node can be either an UpdateIdentifier node or a PerProviderSubscription
479                     // instance node.  Flatten out the XML tree first by converting it to a PPS
480                     // tree to reduce the complexity of the parsing code.
481                     PPSNode ppsNodeRoot = buildPpsNode(child);
482                     if (TextUtils.equals(ppsNodeRoot.getName(), NODE_UPDATE_IDENTIFIER)) {
483                         if (updateIdentifier != Integer.MIN_VALUE) {
484                             throw new ParsingException("Multiple node for UpdateIdentifier");
485                         }
486                         updateIdentifier = parseInteger(getPpsNodeValue(ppsNodeRoot));
487                     } else {
488                         // Only one PerProviderSubscription instance is expected and allowed.
489                         if (config != null) {
490                             throw new ParsingException("Multiple PPS instance");
491                         }
492                         config = parsePpsInstance(ppsNodeRoot);
493                     }
494                     break;
495                 case TAG_RT_PROPERTIES:
496                     // Parse and verify URN stored in the RT (Run Time) Properties.
497                     String urn = parseUrn(child);
498                     if (!TextUtils.equals(urn, PPS_MO_URN)) {
499                         throw new ParsingException("Unknown URN: " + urn);
500                     }
501                     break;
502                 default:
503                     throw new ParsingException("Unknown tag under PPS node: " + child.getTag());
504             }
505         }
506         if (config != null && updateIdentifier != Integer.MIN_VALUE) {
507             config.setUpdateIdentifier(updateIdentifier);
508         }
509         return config;
510     }
511 
512     /**
513      * Parse the URN stored in the RTProperties. Below is the format of the RTPProperties node:
514      *
515      * <RTProperties>
516      *   <Type>
517      *     <DDFName>urn:...</DDFName>
518      *   </Type>
519      * </RTProperties>
520      *
521      * @param node XMLNode that contains RTProperties node.
522      * @return URN String of URN.
523      * @throws ParsingException
524      */
parseUrn(XMLNode node)525     private static String parseUrn(XMLNode node) throws ParsingException {
526         if (node.getChildren().size() != 1)
527             throw new ParsingException("Expect RTPProperties node to only have one child");
528 
529         XMLNode typeNode = node.getChildren().get(0);
530         if (typeNode.getChildren().size() != 1) {
531             throw new ParsingException("Expect Type node to only have one child");
532         }
533         if (!TextUtils.equals(typeNode.getTag(), TAG_TYPE)) {
534             throw new ParsingException("Unexpected tag for Type: " + typeNode.getTag());
535         }
536 
537         XMLNode ddfNameNode = typeNode.getChildren().get(0);
538         if (!ddfNameNode.getChildren().isEmpty()) {
539             throw new ParsingException("Expect DDFName node to have no child");
540         }
541         if (!TextUtils.equals(ddfNameNode.getTag(), TAG_DDF_NAME)) {
542             throw new ParsingException("Unexpected tag for DDFName: " + ddfNameNode.getTag());
543         }
544 
545         return ddfNameNode.getText();
546     }
547 
548     /**
549      * Convert a XML tree represented by XMLNode to a PPS (PerProviderSubscription) instance tree
550      * represented by PPSNode.  This flattens out the XML tree to allow easier and cleaner parsing
551      * of the PPS configuration data.  Only three types of XML tag are expected: "NodeName",
552      * "Node", and "Value".
553      *
554      * The original XML tree (each XML element represent a node):
555      *
556      * <Node>
557      *   <NodeName>root</NodeName>
558      *   <Node>
559      *     <NodeName>child1</NodeName>
560      *     <Value>value1</Value>
561      *   </Node>
562      *   <Node>
563      *     <NodeName>child2</NodeName>
564      *     <Node>
565      *       <NodeName>grandchild1</NodeName>
566      *       ...
567      *     </Node>
568      *   </Node>
569      *   ...
570      * </Node>
571      *
572      * The converted PPS tree:
573      *
574      * [root] --- [child1, value1]
575      *   |
576      *   ---------[child2] --------[grandchild1] --- ...
577      *
578      * @param node XMLNode pointed to the root of a XML tree
579      * @return PPSNode pointing to the root of a PPS tree
580      * @throws ParsingException
581      */
buildPpsNode(XMLNode node)582     private static PPSNode buildPpsNode(XMLNode node) throws ParsingException {
583         String nodeName = null;
584         String nodeValue = null;
585         List<PPSNode> childNodes = new ArrayList<PPSNode>();
586         // Names of parsed child nodes, use for detecting multiple child nodes with the same name.
587         Set<String> parsedNodes = new HashSet<String>();
588 
589         for (XMLNode child : node.getChildren()) {
590             String tag = child.getTag();
591             if (TextUtils.equals(tag, TAG_NODE_NAME)) {
592                 if (nodeName != null) {
593                     throw new ParsingException("Duplicate NodeName node");
594                 }
595                 nodeName = child.getText();
596             } else if (TextUtils.equals(tag, TAG_NODE)) {
597                 PPSNode ppsNode = buildPpsNode(child);
598                 if (parsedNodes.contains(ppsNode.getName())) {
599                     throw new ParsingException("Duplicate node: " + ppsNode.getName());
600                 }
601                 parsedNodes.add(ppsNode.getName());
602                 childNodes.add(ppsNode);
603             } else if (TextUtils.equals(tag, TAG_VALUE)) {
604                if (nodeValue != null) {
605                    throw new ParsingException("Duplicate Value node");
606                }
607                nodeValue = child.getText();
608             } else {
609                 throw new ParsingException("Unknown tag: " + tag);
610             }
611         }
612 
613         if (nodeName == null) {
614             throw new ParsingException("Invalid node: missing NodeName");
615         }
616         if (nodeValue == null && childNodes.size() == 0) {
617             throw new ParsingException("Invalid node: " + nodeName +
618                     " missing both value and children");
619         }
620         if (nodeValue != null && childNodes.size() > 0) {
621             throw new ParsingException("Invalid node: " + nodeName +
622                     " contained both value and children");
623         }
624 
625         if (nodeValue != null) {
626             return new LeafNode(nodeName, nodeValue);
627         }
628         return new InternalNode(nodeName, childNodes);
629     }
630 
631     /**
632      * Return the value of a PPSNode.  An exception will be thrown if the given node
633      * is not a leaf node.
634      *
635      * @param node PPSNode to retrieve the value from
636      * @return String representing the value of the node
637      * @throws ParsingException
638      */
getPpsNodeValue(PPSNode node)639     private static String getPpsNodeValue(PPSNode node) throws ParsingException {
640         if (!node.isLeaf()) {
641             throw new ParsingException("Cannot get value from a non-leaf node: " + node.getName());
642         }
643         return node.getValue();
644     }
645 
646     /**
647      * Parse a PPS (PerProviderSubscription) configurations from a PPS tree.
648      *
649      * @param root PPSNode representing the root of the PPS tree
650      * @return PasspointConfiguration
651      * @throws ParsingException
652      */
parsePpsInstance(PPSNode root)653     private static PasspointConfiguration parsePpsInstance(PPSNode root)
654             throws ParsingException {
655         if (root.isLeaf()) {
656             throw new ParsingException("Leaf node not expected for PPS instance");
657         }
658 
659         PasspointConfiguration config = new PasspointConfiguration();
660         for (PPSNode child : root.getChildren()) {
661             switch(child.getName()) {
662                 case NODE_HOMESP:
663                     config.setHomeSp(parseHomeSP(child));
664                     break;
665                 case NODE_CREDENTIAL:
666                     config.setCredential(parseCredential(child));
667                     break;
668                 case NODE_POLICY:
669                     config.setPolicy(parsePolicy(child));
670                     break;
671                 case NODE_AAA_SERVER_TRUST_ROOT:
672                     config.setTrustRootCertList(parseAAAServerTrustRootList(child));
673                     break;
674                 case NODE_SUBSCRIPTION_UPDATE:
675                     config.setSubscriptionUpdate(parseUpdateParameter(child));
676                     break;
677                 case NODE_SUBSCRIPTION_PARAMETER:
678                     parseSubscriptionParameter(child, config);
679                     break;
680                 case NODE_CREDENTIAL_PRIORITY:
681                     config.setCredentialPriority(parseInteger(getPpsNodeValue(child)));
682                     break;
683                 case NODE_EXTENSION:
684                     // All vendor specific information will be under this node.
685                     parseExtension(child, config);
686                     break;
687                 default:
688                     throw new ParsingException("Unknown node: " + child.getName());
689             }
690         }
691         return config;
692     }
693 
694     /**
695      * Parse configurations under PerProviderSubscription/HomeSP subtree.
696      *
697      * @param node PPSNode representing the root of the PerProviderSubscription/HomeSP subtree
698      * @return HomeSP
699      * @throws ParsingException
700      */
parseHomeSP(PPSNode node)701     private static HomeSp parseHomeSP(PPSNode node) throws ParsingException {
702         if (node.isLeaf()) {
703             throw new ParsingException("Leaf node not expected for HomeSP");
704         }
705 
706         HomeSp homeSp = new HomeSp();
707         for (PPSNode child : node.getChildren()) {
708             switch (child.getName()) {
709                 case NODE_FQDN:
710                     homeSp.setFqdn(getPpsNodeValue(child));
711                     break;
712                 case NODE_FRIENDLY_NAME:
713                     homeSp.setFriendlyName(getPpsNodeValue(child));
714                     break;
715                 case NODE_ROAMING_CONSORTIUM_OI:
716                     homeSp.setRoamingConsortiumOis(
717                             parseRoamingConsortiumOI(getPpsNodeValue(child)));
718                     break;
719                 case NODE_ICON_URL:
720                     homeSp.setIconUrl(getPpsNodeValue(child));
721                     break;
722                 case NODE_NETWORK_ID:
723                     homeSp.setHomeNetworkIds(parseNetworkIds(child));
724                     break;
725                 case NODE_HOME_OI_LIST:
726                     Pair<List<Long>, List<Long>> homeOIs = parseHomeOIList(child);
727                     homeSp.setMatchAllOis(convertFromLongList(homeOIs.first));
728                     homeSp.setMatchAnyOis(convertFromLongList(homeOIs.second));
729                     break;
730                 case NODE_OTHER_HOME_PARTNERS:
731                     homeSp.setOtherHomePartners(parseOtherHomePartners(child));
732                     break;
733                 default:
734                     throw new ParsingException("Unknown node under HomeSP: " + child.getName());
735             }
736         }
737         return homeSp;
738     }
739 
740     /**
741      * Parse the roaming consortium OI string, which contains a list of OIs separated by ",".
742      *
743      * @param oiStr string containing list of OIs (Organization Identifiers) separated by ","
744      * @return long[]
745      * @throws ParsingException
746      */
parseRoamingConsortiumOI(String oiStr)747     private static long[] parseRoamingConsortiumOI(String oiStr)
748             throws ParsingException {
749         String[] oiStrArray = oiStr.split(",");
750         long[] oiArray = new long[oiStrArray.length];
751         for (int i = 0; i < oiStrArray.length; i++) {
752             oiArray[i] = parseLong(oiStrArray[i], 16);
753         }
754         return oiArray;
755     }
756 
757     /**
758      * Parse configurations under PerProviderSubscription/HomeSP/NetworkID subtree.
759      *
760      * @param node PPSNode representing the root of the PerProviderSubscription/HomeSP/NetworkID
761      *             subtree
762      * @return HashMap<String, Long> representing list of <SSID, HESSID> pair.
763      * @throws ParsingException
764      */
parseNetworkIds(PPSNode node)765     static private Map<String, Long> parseNetworkIds(PPSNode node)
766             throws ParsingException {
767         if (node.isLeaf()) {
768             throw new ParsingException("Leaf node not expected for NetworkID");
769         }
770 
771         Map<String, Long> networkIds = new HashMap<>();
772         for (PPSNode child : node.getChildren()) {
773             Pair<String, Long> networkId = parseNetworkIdInstance(child);
774             networkIds.put(networkId.first, networkId.second);
775         }
776         return networkIds;
777     }
778 
779     /**
780      * Parse configurations under PerProviderSubscription/HomeSP/NetworkID/<X+> subtree.
781      * The instance name (<X+>) is irrelevant and must be unique for each instance, which
782      * is verified when the PPS tree is constructed {@link #buildPpsNode}.
783      *
784      * @param node PPSNode representing the root of the
785      *             PerProviderSubscription/HomeSP/NetworkID/<X+> subtree
786      * @return Pair<String, Long> representing <SSID, HESSID> pair.
787      * @throws ParsingException
788      */
parseNetworkIdInstance(PPSNode node)789     static private Pair<String, Long> parseNetworkIdInstance(PPSNode node)
790             throws ParsingException {
791         if (node.isLeaf()) {
792             throw new ParsingException("Leaf node not expected for NetworkID instance");
793         }
794 
795         String ssid = null;
796         Long hessid = null;
797         for (PPSNode child : node.getChildren()) {
798             switch (child.getName()) {
799                 case NODE_SSID:
800                     ssid = getPpsNodeValue(child);
801                     break;
802                 case NODE_HESSID:
803                     hessid = parseLong(getPpsNodeValue(child), 16);
804                     break;
805                 default:
806                     throw new ParsingException("Unknown node under NetworkID instance: " +
807                             child.getName());
808             }
809         }
810         if (ssid == null)
811             throw new ParsingException("NetworkID instance missing SSID");
812 
813         return new Pair<String, Long>(ssid, hessid);
814     }
815 
816     /**
817      * Parse configurations under PerProviderSubscription/HomeSP/HomeOIList subtree.
818      *
819      * @param node PPSNode representing the root of the PerProviderSubscription/HomeSP/HomeOIList
820      *             subtree
821      * @return Pair<List<Long>, List<Long>> containing both MatchAllOIs and MatchAnyOIs list.
822      * @throws ParsingException
823      */
parseHomeOIList(PPSNode node)824     private static Pair<List<Long>, List<Long>> parseHomeOIList(PPSNode node)
825             throws ParsingException {
826         if (node.isLeaf()) {
827             throw new ParsingException("Leaf node not expected for HomeOIList");
828         }
829 
830         List<Long> matchAllOIs = new ArrayList<Long>();
831         List<Long> matchAnyOIs = new ArrayList<Long>();
832         for (PPSNode child : node.getChildren()) {
833             Pair<Long, Boolean> homeOI = parseHomeOIInstance(child);
834             if (homeOI.second.booleanValue()) {
835                 matchAllOIs.add(homeOI.first);
836             } else {
837                 matchAnyOIs.add(homeOI.first);
838             }
839         }
840         return new Pair<List<Long>, List<Long>>(matchAllOIs, matchAnyOIs);
841     }
842 
843     /**
844      * Parse configurations under PerProviderSubscription/HomeSP/HomeOIList/<X+> subtree.
845      * The instance name (<X+>) is irrelevant and must be unique for each instance, which
846      * is verified when the PPS tree is constructed {@link #buildPpsNode}.
847      *
848      * @param node PPSNode representing the root of the
849      *             PerProviderSubscription/HomeSP/HomeOIList/<X+> subtree
850      * @return Pair<Long, Boolean> containing a HomeOI and a HomeOIRequired flag
851      * @throws ParsingException
852      */
parseHomeOIInstance(PPSNode node)853     private static Pair<Long, Boolean> parseHomeOIInstance(PPSNode node) throws ParsingException {
854         if (node.isLeaf()) {
855             throw new ParsingException("Leaf node not expected for HomeOI instance");
856         }
857 
858         Long oi = null;
859         Boolean required = null;
860         for (PPSNode child : node.getChildren()) {
861             switch (child.getName()) {
862                 case NODE_HOME_OI:
863                     try {
864                         oi = Long.valueOf(getPpsNodeValue(child), 16);
865                     } catch (NumberFormatException e) {
866                         throw new ParsingException("Invalid HomeOI: " + getPpsNodeValue(child));
867                     }
868                     break;
869                 case NODE_HOME_OI_REQUIRED:
870                     required = Boolean.valueOf(getPpsNodeValue(child));
871                     break;
872                 default:
873                     throw new ParsingException("Unknown node under NetworkID instance: " +
874                             child.getName());
875             }
876         }
877         if (oi == null) {
878             throw new ParsingException("HomeOI instance missing OI field");
879         }
880         if (required == null) {
881             throw new ParsingException("HomeOI instance missing required field");
882         }
883         return new Pair<Long, Boolean>(oi, required);
884     }
885 
886     /**
887      * Parse configurations under PerProviderSubscription/HomeSP/OtherHomePartners subtree.
888      * This contains a list of FQDN (Fully Qualified Domain Name) that are considered
889      * home partners.
890      *
891      * @param node PPSNode representing the root of the
892      *             PerProviderSubscription/HomeSP/OtherHomePartners subtree
893      * @return String[] list of partner's FQDN
894      * @throws ParsingException
895      */
parseOtherHomePartners(PPSNode node)896     private static String[] parseOtherHomePartners(PPSNode node) throws ParsingException {
897         if (node.isLeaf()) {
898             throw new ParsingException("Leaf node not expected for OtherHomePartners");
899         }
900         List<String> otherHomePartners = new ArrayList<String>();
901         for (PPSNode child : node.getChildren()) {
902             String fqdn = parseOtherHomePartnerInstance(child);
903             otherHomePartners.add(fqdn);
904         }
905         return otherHomePartners.toArray(new String[otherHomePartners.size()]);
906     }
907 
908     /**
909      * Parse configurations under PerProviderSubscription/HomeSP/OtherHomePartners/<X+> subtree.
910      * The instance name (<X+>) is irrelevant and must be unique for each instance, which
911      * is verified when the PPS tree is constructed {@link #buildPpsNode}.
912      *
913      * @param node PPSNode representing the root of the
914      *             PerProviderSubscription/HomeSP/OtherHomePartners/<X+> subtree
915      * @return String FQDN of the partner
916      * @throws ParsingException
917      */
parseOtherHomePartnerInstance(PPSNode node)918     private static String parseOtherHomePartnerInstance(PPSNode node) throws ParsingException {
919         if (node.isLeaf()) {
920             throw new ParsingException("Leaf node not expected for OtherHomePartner instance");
921         }
922         String fqdn = null;
923         for (PPSNode child : node.getChildren()) {
924             switch (child.getName()) {
925                 case NODE_FQDN:
926                     fqdn = getPpsNodeValue(child);
927                     break;
928                 default:
929                     throw new ParsingException(
930                             "Unknown node under OtherHomePartner instance: " + child.getName());
931             }
932         }
933         if (fqdn == null) {
934             throw new ParsingException("OtherHomePartner instance missing FQDN field");
935         }
936         return fqdn;
937     }
938 
939     /**
940      * Parse configurations under PerProviderSubscription/Credential subtree.
941      *
942      * @param node PPSNode representing the root of the PerProviderSubscription/Credential subtree
943      * @return Credential
944      * @throws ParsingException
945      */
parseCredential(PPSNode node)946     private static Credential parseCredential(PPSNode node) throws ParsingException {
947         if (node.isLeaf()) {
948             throw new ParsingException("Leaf node not expected for Credential");
949         }
950 
951         Credential credential = new Credential();
952         for (PPSNode child: node.getChildren()) {
953             switch (child.getName()) {
954                 case NODE_CREATION_DATE:
955                     credential.setCreationTimeInMillis(parseDate(getPpsNodeValue(child)));
956                     break;
957                 case NODE_EXPIRATION_DATE:
958                     credential.setExpirationTimeInMillis(parseDate(getPpsNodeValue(child)));
959                     break;
960                 case NODE_USERNAME_PASSWORD:
961                     credential.setUserCredential(parseUserCredential(child));
962                     break;
963                 case NODE_DIGITAL_CERTIFICATE:
964                     credential.setCertCredential(parseCertificateCredential(child));
965                     break;
966                 case NODE_REALM:
967                     credential.setRealm(getPpsNodeValue(child));
968                     break;
969                 case NODE_CHECK_AAA_SERVER_CERT_STATUS:
970                     credential.setCheckAaaServerCertStatus(
971                             Boolean.parseBoolean(getPpsNodeValue(child)));
972                     break;
973                 case NODE_SIM:
974                     credential.setSimCredential(parseSimCredential(child));
975                     break;
976                 default:
977                     throw new ParsingException("Unknown node under Credential: " +
978                             child.getName());
979             }
980         }
981         return credential;
982     }
983 
984     /**
985      * Parse configurations under PerProviderSubscription/Credential/UsernamePassword subtree.
986      *
987      * @param node PPSNode representing the root of the
988      *             PerProviderSubscription/Credential/UsernamePassword subtree
989      * @return Credential.UserCredential
990      * @throws ParsingException
991      */
parseUserCredential(PPSNode node)992     private static Credential.UserCredential parseUserCredential(PPSNode node)
993             throws ParsingException {
994         if (node.isLeaf()) {
995             throw new ParsingException("Leaf node not expected for UsernamePassword");
996         }
997 
998         Credential.UserCredential userCred = new Credential.UserCredential();
999         for (PPSNode child : node.getChildren()) {
1000             switch (child.getName()) {
1001                 case NODE_USERNAME:
1002                     userCred.setUsername(getPpsNodeValue(child));
1003                     break;
1004                 case NODE_PASSWORD:
1005                     userCred.setPassword(getPpsNodeValue(child));
1006                     break;
1007                 case NODE_MACHINE_MANAGED:
1008                     userCred.setMachineManaged(Boolean.parseBoolean(getPpsNodeValue(child)));
1009                     break;
1010                 case NODE_SOFT_TOKEN_APP:
1011                     userCred.setSoftTokenApp(getPpsNodeValue(child));
1012                     break;
1013                 case NODE_ABLE_TO_SHARE:
1014                     userCred.setAbleToShare(Boolean.parseBoolean(getPpsNodeValue(child)));
1015                     break;
1016                 case NODE_EAP_METHOD:
1017                     parseEAPMethod(child, userCred);
1018                     break;
1019                 default:
1020                     throw new ParsingException("Unknown node under UsernamePassword: "
1021                             + child.getName());
1022             }
1023         }
1024         return userCred;
1025     }
1026 
1027     /**
1028      * Parse configurations under PerProviderSubscription/Credential/UsernamePassword/EAPMethod
1029      * subtree.
1030      *
1031      * @param node PPSNode representing the root of the
1032      *             PerProviderSubscription/Credential/UsernamePassword/EAPMethod subtree
1033      * @param userCred UserCredential to be updated with EAP method values.
1034      * @throws ParsingException
1035      */
parseEAPMethod(PPSNode node, Credential.UserCredential userCred)1036     private static void parseEAPMethod(PPSNode node, Credential.UserCredential userCred)
1037             throws ParsingException {
1038         if (node.isLeaf()) {
1039             throw new ParsingException("Leaf node not expected for EAPMethod");
1040         }
1041 
1042         for (PPSNode child : node.getChildren()) {
1043             switch(child.getName()) {
1044                 case NODE_EAP_TYPE:
1045                     userCred.setEapType(parseInteger(getPpsNodeValue(child)));
1046                     break;
1047                 case NODE_INNER_METHOD:
1048                     userCred.setNonEapInnerMethod(getPpsNodeValue(child));
1049                     break;
1050                 case NODE_VENDOR_ID:
1051                 case NODE_VENDOR_TYPE:
1052                 case NODE_INNER_EAP_TYPE:
1053                 case NODE_INNER_VENDOR_ID:
1054                 case NODE_INNER_VENDOR_TYPE:
1055                     // Only EAP-TTLS is currently supported for user credential, which doesn't
1056                     // use any of these parameters.
1057                     Log.d(TAG, "Ignore unsupported EAP method parameter: " + child.getName());
1058                     break;
1059                 default:
1060                     throw new ParsingException("Unknown node under EAPMethod: " + child.getName());
1061             }
1062         }
1063     }
1064 
1065     /**
1066      * Parse configurations under PerProviderSubscription/Credential/DigitalCertificate subtree.
1067      *
1068      * @param node PPSNode representing the root of the
1069      *             PerProviderSubscription/Credential/DigitalCertificate subtree
1070      * @return Credential.CertificateCredential
1071      * @throws ParsingException
1072      */
parseCertificateCredential(PPSNode node)1073     private static Credential.CertificateCredential parseCertificateCredential(PPSNode node)
1074             throws ParsingException {
1075         if (node.isLeaf()) {
1076             throw new ParsingException("Leaf node not expected for CertificateCredential");
1077         }
1078 
1079         Credential.CertificateCredential certCred = new Credential.CertificateCredential();
1080         for (PPSNode child : node.getChildren()) {
1081             switch (child.getName()) {
1082                 case NODE_CERTIFICATE_TYPE:
1083                     certCred.setCertType(getPpsNodeValue(child));
1084                     break;
1085                 case NODE_CERT_SHA256_FINGERPRINT:
1086                     certCred.setCertSha256Fingerprint(parseHexString(getPpsNodeValue(child)));
1087                     break;
1088                 default:
1089                     throw new ParsingException("Unknown node under CertificateCredential: "
1090                             + child.getName());
1091             }
1092         }
1093         return certCred;
1094     }
1095 
1096     /**
1097      * Parse configurations under PerProviderSubscription/Credential/SIM subtree.
1098      *
1099      * @param node PPSNode representing the root of the PerProviderSubscription/Credential/SIM
1100      *             subtree
1101      * @return Credential.SimCredential
1102      * @throws ParsingException
1103      */
parseSimCredential(PPSNode node)1104     private static Credential.SimCredential parseSimCredential(PPSNode node)
1105             throws ParsingException {
1106         if (node.isLeaf()) {
1107             throw new ParsingException("Leaf node not expected for SimCredential");
1108         }
1109 
1110         Credential.SimCredential simCred = new Credential.SimCredential();
1111         for (PPSNode child : node.getChildren()) {
1112             switch (child.getName()) {
1113                 case NODE_SIM_IMSI:
1114                     simCred.setImsi(getPpsNodeValue(child));
1115                     break;
1116                 case NODE_EAP_TYPE:
1117                     simCred.setEapType(parseInteger(getPpsNodeValue(child)));
1118                     break;
1119                 default:
1120                     throw new ParsingException("Unknown node under SimCredential: "
1121                             + child.getName());
1122             }
1123         }
1124         return simCred;
1125     }
1126 
1127     /**
1128      * Parse configurations under PerProviderSubscription/Policy subtree.
1129      *
1130      * @param node PPSNode representing the root of the PerProviderSubscription/Policy subtree
1131      * @return {@link Policy}
1132      * @throws ParsingException
1133      */
parsePolicy(PPSNode node)1134     private static Policy parsePolicy(PPSNode node) throws ParsingException {
1135         if (node.isLeaf()) {
1136             throw new ParsingException("Leaf node not expected for Policy");
1137         }
1138 
1139         Policy policy = new Policy();
1140         for (PPSNode child : node.getChildren()) {
1141             switch (child.getName()) {
1142                 case NODE_PREFERRED_ROAMING_PARTNER_LIST:
1143                     policy.setPreferredRoamingPartnerList(parsePreferredRoamingPartnerList(child));
1144                     break;
1145                 case NODE_MIN_BACKHAUL_THRESHOLD:
1146                     parseMinBackhaulThreshold(child, policy);
1147                     break;
1148                 case NODE_POLICY_UPDATE:
1149                     policy.setPolicyUpdate(parseUpdateParameter(child));
1150                     break;
1151                 case NODE_SP_EXCLUSION_LIST:
1152                     policy.setExcludedSsidList(parseSpExclusionList(child));
1153                     break;
1154                 case NODE_REQUIRED_PROTO_PORT_TUPLE:
1155                     policy.setRequiredProtoPortMap(parseRequiredProtoPortTuple(child));
1156                     break;
1157                 case NODE_MAXIMUM_BSS_LOAD_VALUE:
1158                     policy.setMaximumBssLoadValue(parseInteger(getPpsNodeValue(child)));
1159                     break;
1160                 default:
1161                     throw new ParsingException("Unknown node under Policy: " + child.getName());
1162             }
1163         }
1164         return policy;
1165     }
1166 
1167     /**
1168      * Parse configurations under PerProviderSubscription/Policy/PreferredRoamingPartnerList
1169      * subtree.
1170      *
1171      * @param node PPSNode representing the root of the
1172      *             PerProviderSubscription/Policy/PreferredRoamingPartnerList subtree
1173      * @return List of {@link Policy#RoamingPartner}
1174      * @throws ParsingException
1175      */
parsePreferredRoamingPartnerList(PPSNode node)1176     private static List<Policy.RoamingPartner> parsePreferredRoamingPartnerList(PPSNode node)
1177             throws ParsingException {
1178         if (node.isLeaf()) {
1179             throw new ParsingException("Leaf node not expected for PreferredRoamingPartnerList");
1180         }
1181         List<Policy.RoamingPartner> partnerList = new ArrayList<>();
1182         for (PPSNode child : node.getChildren()) {
1183             partnerList.add(parsePreferredRoamingPartner(child));
1184         }
1185         return partnerList;
1186     }
1187 
1188     /**
1189      * Parse configurations under PerProviderSubscription/Policy/PreferredRoamingPartnerList/<X+>
1190      * subtree.
1191      *
1192      * @param node PPSNode representing the root of the
1193      *             PerProviderSubscription/Policy/PreferredRoamingPartnerList/<X+> subtree
1194      * @return {@link Policy#RoamingPartner}
1195      * @throws ParsingException
1196      */
parsePreferredRoamingPartner(PPSNode node)1197     private static Policy.RoamingPartner parsePreferredRoamingPartner(PPSNode node)
1198             throws ParsingException {
1199         if (node.isLeaf()) {
1200             throw new ParsingException("Leaf node not expected for PreferredRoamingPartner "
1201                     + "instance");
1202         }
1203 
1204         Policy.RoamingPartner roamingPartner = new Policy.RoamingPartner();
1205         for (PPSNode child : node.getChildren()) {
1206             switch (child.getName()) {
1207                 case NODE_FQDN_MATCH:
1208                     // FQDN_Match field is in the format of "[FQDN],[MatchInfo]", where [MatchInfo]
1209                     // is either "exactMatch" for exact match of FQDN or "includeSubdomains" for
1210                     // matching all FQDNs with the same sub-domain.
1211                     String fqdnMatch = getPpsNodeValue(child);
1212                     String[] fqdnMatchArray = fqdnMatch.split(",");
1213                     if (fqdnMatchArray.length != 2) {
1214                         throw new ParsingException("Invalid FQDN_Match: " + fqdnMatch);
1215                     }
1216                     roamingPartner.setFqdn(fqdnMatchArray[0]);
1217                     if (TextUtils.equals(fqdnMatchArray[1], "exactMatch")) {
1218                         roamingPartner.setFqdnExactMatch(true);
1219                     } else if (TextUtils.equals(fqdnMatchArray[1], "includeSubdomains")) {
1220                         roamingPartner.setFqdnExactMatch(false);
1221                     } else {
1222                         throw new ParsingException("Invalid FQDN_Match: " + fqdnMatch);
1223                     }
1224                     break;
1225                 case NODE_PRIORITY:
1226                     roamingPartner.setPriority(parseInteger(getPpsNodeValue(child)));
1227                     break;
1228                 case NODE_COUNTRY:
1229                     roamingPartner.setCountries(getPpsNodeValue(child));
1230                     break;
1231                 default:
1232                     throw new ParsingException("Unknown node under PreferredRoamingPartnerList "
1233                             + "instance " + child.getName());
1234             }
1235         }
1236         return roamingPartner;
1237     }
1238 
1239     /**
1240      * Parse configurations under PerProviderSubscription/Policy/MinBackhaulThreshold subtree
1241      * into the given policy.
1242      *
1243      * @param node PPSNode representing the root of the
1244      *             PerProviderSubscription/Policy/MinBackhaulThreshold subtree
1245      * @param policy The policy to store the MinBackhualThreshold configuration
1246      * @throws ParsingException
1247      */
parseMinBackhaulThreshold(PPSNode node, Policy policy)1248     private static void parseMinBackhaulThreshold(PPSNode node, Policy policy)
1249             throws ParsingException {
1250         if (node.isLeaf()) {
1251             throw new ParsingException("Leaf node not expected for MinBackhaulThreshold");
1252         }
1253         for (PPSNode child : node.getChildren()) {
1254             parseMinBackhaulThresholdInstance(child, policy);
1255         }
1256     }
1257 
1258     /**
1259      * Parse configurations under PerProviderSubscription/Policy/MinBackhaulThreshold/<X+> subtree
1260      * into the given policy.
1261      *
1262      * @param node PPSNode representing the root of the
1263      *             PerProviderSubscription/Policy/MinBackhaulThreshold/<X+> subtree
1264      * @param policy The policy to store the MinBackhaulThreshold configuration
1265      * @throws ParsingException
1266      */
parseMinBackhaulThresholdInstance(PPSNode node, Policy policy)1267     private static void parseMinBackhaulThresholdInstance(PPSNode node, Policy policy)
1268             throws ParsingException {
1269         if (node.isLeaf()) {
1270             throw new ParsingException("Leaf node not expected for MinBackhaulThreshold instance");
1271         }
1272         String networkType = null;
1273         long downlinkBandwidth = Long.MIN_VALUE;
1274         long uplinkBandwidth = Long.MIN_VALUE;
1275         for (PPSNode child : node.getChildren()) {
1276             switch (child.getName()) {
1277                 case NODE_NETWORK_TYPE:
1278                     networkType = getPpsNodeValue(child);
1279                     break;
1280                 case NODE_DOWNLINK_BANDWIDTH:
1281                     downlinkBandwidth = parseLong(getPpsNodeValue(child), 10);
1282                     break;
1283                 case NODE_UPLINK_BANDWIDTH:
1284                     uplinkBandwidth = parseLong(getPpsNodeValue(child), 10);
1285                     break;
1286                 default:
1287                     throw new ParsingException("Unknown node under MinBackhaulThreshold instance "
1288                             + child.getName());
1289             }
1290         }
1291         if (networkType == null) {
1292             throw new ParsingException("Missing NetworkType field");
1293         }
1294 
1295         if (TextUtils.equals(networkType, "home")) {
1296             policy.setMinHomeDownlinkBandwidth(downlinkBandwidth);
1297             policy.setMinHomeUplinkBandwidth(uplinkBandwidth);
1298         } else if (TextUtils.equals(networkType, "roaming")) {
1299             policy.setMinRoamingDownlinkBandwidth(downlinkBandwidth);
1300             policy.setMinRoamingUplinkBandwidth(uplinkBandwidth);
1301         } else {
1302             throw new ParsingException("Invalid network type: " + networkType);
1303         }
1304     }
1305 
1306     /**
1307      * Parse update parameters. This contained configurations from either
1308      * PerProviderSubscription/Policy/PolicyUpdate or PerProviderSubscription/SubscriptionUpdate
1309      * subtree.
1310      *
1311      * @param node PPSNode representing the root of the PerProviderSubscription/Policy/PolicyUpdate
1312      *             or PerProviderSubscription/SubscriptionUpdate subtree
1313      * @return {@link UpdateParameter}
1314      * @throws ParsingException
1315      */
parseUpdateParameter(PPSNode node)1316     private static UpdateParameter parseUpdateParameter(PPSNode node)
1317             throws ParsingException {
1318         if (node.isLeaf()) {
1319             throw new ParsingException("Leaf node not expected for Update Parameters");
1320         }
1321 
1322         UpdateParameter updateParam = new UpdateParameter();
1323         for (PPSNode child : node.getChildren()) {
1324             switch(child.getName()) {
1325                 case NODE_UPDATE_INTERVAL:
1326                     updateParam.setUpdateIntervalInMinutes(parseLong(getPpsNodeValue(child), 10));
1327                     break;
1328                 case NODE_UPDATE_METHOD:
1329                     updateParam.setUpdateMethod(getPpsNodeValue(child));
1330                     break;
1331                 case NODE_RESTRICTION:
1332                     updateParam.setRestriction(getPpsNodeValue(child));
1333                     break;
1334                 case NODE_URI:
1335                     updateParam.setServerUri(getPpsNodeValue(child));
1336                     break;
1337                 case NODE_USERNAME_PASSWORD:
1338                     Pair<String, String> usernamePassword = parseUpdateUserCredential(child);
1339                     updateParam.setUsername(usernamePassword.first);
1340                     updateParam.setBase64EncodedPassword(usernamePassword.second);
1341                     break;
1342                 case NODE_TRUST_ROOT:
1343                     Pair<String, byte[]> trustRoot = parseTrustRoot(child);
1344                     updateParam.setTrustRootCertUrl(trustRoot.first);
1345                     updateParam.setTrustRootCertSha256Fingerprint(trustRoot.second);
1346                     break;
1347                 case NODE_OTHER:
1348                     Log.d(TAG, "Ignore unsupported paramter: " + child.getName());
1349                     break;
1350                 default:
1351                     throw new ParsingException("Unknown node under Update Parameters: "
1352                             + child.getName());
1353             }
1354         }
1355         return updateParam;
1356     }
1357 
1358     /**
1359      * Parse username and password parameters associated with policy or subscription update.
1360      * This contained configurations under either
1361      * PerProviderSubscription/Policy/PolicyUpdate/UsernamePassword or
1362      * PerProviderSubscription/SubscriptionUpdate/UsernamePassword subtree.
1363      *
1364      * @param node PPSNode representing the root of the UsernamePassword subtree
1365      * @return Pair of username and password
1366      * @throws ParsingException
1367      */
parseUpdateUserCredential(PPSNode node)1368     private static Pair<String, String> parseUpdateUserCredential(PPSNode node)
1369             throws ParsingException {
1370         if (node.isLeaf()) {
1371             throw new ParsingException("Leaf node not expected for UsernamePassword");
1372         }
1373 
1374         String username = null;
1375         String password = null;
1376         for (PPSNode child : node.getChildren()) {
1377             switch (child.getName()) {
1378                 case NODE_USERNAME:
1379                     username = getPpsNodeValue(child);
1380                     break;
1381                 case NODE_PASSWORD:
1382                     password = getPpsNodeValue(child);
1383                     break;
1384                 default:
1385                     throw new ParsingException("Unknown node under UsernamePassword: "
1386                             + child.getName());
1387             }
1388         }
1389         return Pair.create(username, password);
1390     }
1391 
1392     /**
1393      * Parse the trust root parameters associated with policy update, subscription update, or AAA
1394      * server trust root.
1395      *
1396      * This contained configurations under either
1397      * PerProviderSubscription/Policy/PolicyUpdate/TrustRoot or
1398      * PerProviderSubscription/SubscriptionUpdate/TrustRoot or
1399      * PerProviderSubscription/AAAServerTrustRoot/<X+> subtree.
1400      *
1401      * @param node PPSNode representing the root of the TrustRoot subtree
1402      * @return Pair of Certificate URL and fingerprint
1403      * @throws ParsingException
1404      */
parseTrustRoot(PPSNode node)1405     private static Pair<String, byte[]> parseTrustRoot(PPSNode node)
1406             throws ParsingException {
1407         if (node.isLeaf()) {
1408             throw new ParsingException("Leaf node not expected for TrustRoot");
1409         }
1410 
1411         String certUrl = null;
1412         byte[] certFingerprint = null;
1413         for (PPSNode child : node.getChildren()) {
1414             switch (child.getName()) {
1415                 case NODE_CERT_URL:
1416                     certUrl = getPpsNodeValue(child);
1417                     break;
1418                 case NODE_CERT_SHA256_FINGERPRINT:
1419                     certFingerprint = parseHexString(getPpsNodeValue(child));
1420                     break;
1421                 default:
1422                     throw new ParsingException("Unknown node under TrustRoot: "
1423                             + child.getName());
1424             }
1425         }
1426         return Pair.create(certUrl, certFingerprint);
1427     }
1428 
1429     /**
1430      * Parse configurations under PerProviderSubscription/Policy/SPExclusionList subtree.
1431      *
1432      * @param node PPSNode representing the root of the
1433      *             PerProviderSubscription/Policy/SPExclusionList subtree
1434      * @return Array of excluded SSIDs
1435      * @throws ParsingException
1436      */
parseSpExclusionList(PPSNode node)1437     private static String[] parseSpExclusionList(PPSNode node) throws ParsingException {
1438         if (node.isLeaf()) {
1439             throw new ParsingException("Leaf node not expected for SPExclusionList");
1440         }
1441         List<String> ssidList = new ArrayList<>();
1442         for (PPSNode child : node.getChildren()) {
1443             ssidList.add(parseSpExclusionInstance(child));
1444         }
1445         return ssidList.toArray(new String[ssidList.size()]);
1446     }
1447 
1448     /**
1449      * Parse configurations under PerProviderSubscription/Policy/SPExclusionList/<X+> subtree.
1450      *
1451      * @param node PPSNode representing the root of the
1452      *             PerProviderSubscription/Policy/SPExclusionList/<X+> subtree
1453      * @return String
1454      * @throws ParsingException
1455      */
parseSpExclusionInstance(PPSNode node)1456     private static String parseSpExclusionInstance(PPSNode node) throws ParsingException {
1457         if (node.isLeaf()) {
1458             throw new ParsingException("Leaf node not expected for SPExclusion instance");
1459         }
1460         String ssid = null;
1461         for (PPSNode child : node.getChildren()) {
1462             switch (child.getName()) {
1463                 case NODE_SSID:
1464                     ssid = getPpsNodeValue(child);
1465                     break;
1466                 default:
1467                     throw new ParsingException("Unknown node under SPExclusion instance");
1468             }
1469         }
1470         return ssid;
1471     }
1472 
1473     /**
1474      * Parse configurations under PerProviderSubscription/Policy/RequiredProtoPortTuple subtree.
1475      *
1476      * @param node PPSNode representing the root of the
1477      *             PerProviderSubscription/Policy/RequiredProtoPortTuple subtree
1478      * @return Map of IP Protocol to Port Number tuples
1479      * @throws ParsingException
1480      */
parseRequiredProtoPortTuple(PPSNode node)1481     private static Map<Integer, String> parseRequiredProtoPortTuple(PPSNode node)
1482             throws ParsingException {
1483         if (node.isLeaf()) {
1484             throw new ParsingException("Leaf node not expected for RequiredProtoPortTuple");
1485         }
1486         Map<Integer, String> protoPortTupleMap = new HashMap<>();
1487         for (PPSNode child : node.getChildren()) {
1488             Pair<Integer, String> protoPortTuple = parseProtoPortTuple(child);
1489             protoPortTupleMap.put(protoPortTuple.first, protoPortTuple.second);
1490         }
1491         return protoPortTupleMap;
1492     }
1493 
1494     /**
1495      * Parse configurations under PerProviderSubscription/Policy/RequiredProtoPortTuple/<X+>
1496      * subtree.
1497      *
1498      * @param node PPSNode representing the root of the
1499      *             PerProviderSubscription/Policy/RequiredProtoPortTuple/<X+> subtree
1500      * @return Pair of IP Protocol to Port Number tuple
1501      * @throws ParsingException
1502      */
parseProtoPortTuple(PPSNode node)1503     private static Pair<Integer, String> parseProtoPortTuple(PPSNode node)
1504             throws ParsingException {
1505         if (node.isLeaf()) {
1506             throw new ParsingException("Leaf node not expected for RequiredProtoPortTuple "
1507                     + "instance");
1508         }
1509         int proto = Integer.MIN_VALUE;
1510         String ports = null;
1511         for (PPSNode child : node.getChildren()) {
1512             switch (child.getName()) {
1513                 case NODE_IP_PROTOCOL:
1514                     proto = parseInteger(getPpsNodeValue(child));
1515                     break;
1516                 case NODE_PORT_NUMBER:
1517                     ports = getPpsNodeValue(child);
1518                     break;
1519                 default:
1520                     throw new ParsingException("Unknown node under RequiredProtoPortTuple instance"
1521                             + child.getName());
1522             }
1523         }
1524         if (proto == Integer.MIN_VALUE) {
1525             throw new ParsingException("Missing IPProtocol field");
1526         }
1527         if (ports == null) {
1528             throw new ParsingException("Missing PortNumber field");
1529         }
1530         return Pair.create(proto, ports);
1531     }
1532 
1533     /**
1534      * Parse configurations under PerProviderSubscription/AAAServerTrustRoot subtree.
1535      *
1536      * @param node PPSNode representing the root of PerProviderSubscription/AAAServerTrustRoot
1537      *             subtree
1538      * @return Map of certificate URL with the corresponding certificate fingerprint
1539      * @throws ParsingException
1540      */
parseAAAServerTrustRootList(PPSNode node)1541     private static Map<String, byte[]> parseAAAServerTrustRootList(PPSNode node)
1542             throws ParsingException {
1543         if (node.isLeaf()) {
1544             throw new ParsingException("Leaf node not expected for AAAServerTrustRoot");
1545         }
1546         Map<String, byte[]> certList = new HashMap<>();
1547         for (PPSNode child : node.getChildren()) {
1548             Pair<String, byte[]> certTuple = parseTrustRoot(child);
1549             certList.put(certTuple.first, certTuple.second);
1550         }
1551         return certList;
1552     }
1553 
1554     /**
1555      * Parse configurations under PerProviderSubscription/SubscriptionParameter subtree.
1556      *
1557      * @param node PPSNode representing the root of PerProviderSubscription/SubscriptionParameter
1558      *             subtree
1559      * @param config Instance of {@link PasspointConfiguration}
1560      * @throws ParsingException
1561      */
parseSubscriptionParameter(PPSNode node, PasspointConfiguration config)1562     private static void parseSubscriptionParameter(PPSNode node, PasspointConfiguration config)
1563             throws ParsingException {
1564         if (node.isLeaf()) {
1565             throw new ParsingException("Leaf node not expected for SubscriptionParameter");
1566         }
1567         for (PPSNode child : node.getChildren()) {
1568             switch (child.getName()) {
1569                 case NODE_CREATION_DATE:
1570                     config.setSubscriptionCreationTimeInMillis(parseDate(getPpsNodeValue(child)));
1571                     break;
1572                 case NODE_EXPIRATION_DATE:
1573                     config.setSubscriptionExpirationTimeInMillis(parseDate(getPpsNodeValue(child)));
1574                     break;
1575                 case NODE_TYPE_OF_SUBSCRIPTION:
1576                     config.setSubscriptionType(getPpsNodeValue(child));
1577                     break;
1578                 case NODE_USAGE_LIMITS:
1579                     parseUsageLimits(child, config);
1580                     break;
1581                 default:
1582                     throw new ParsingException("Unknown node under SubscriptionParameter"
1583                             + child.getName());
1584             }
1585         }
1586     }
1587 
1588     /**
1589      * Parse configurations under PerProviderSubscription/SubscriptionParameter/UsageLimits
1590      * subtree.
1591      *
1592      * @param node PPSNode representing the root of
1593      *             PerProviderSubscription/SubscriptionParameter/UsageLimits subtree
1594      * @param config Instance of {@link PasspointConfiguration}
1595      * @throws ParsingException
1596      */
parseUsageLimits(PPSNode node, PasspointConfiguration config)1597     private static void parseUsageLimits(PPSNode node, PasspointConfiguration config)
1598             throws ParsingException {
1599         if (node.isLeaf()) {
1600             throw new ParsingException("Leaf node not expected for UsageLimits");
1601         }
1602         for (PPSNode child : node.getChildren()) {
1603             switch (child.getName()) {
1604                 case NODE_DATA_LIMIT:
1605                     config.setUsageLimitDataLimit(parseLong(getPpsNodeValue(child), 10));
1606                     break;
1607                 case NODE_START_DATE:
1608                     config.setUsageLimitStartTimeInMillis(parseDate(getPpsNodeValue(child)));
1609                     break;
1610                 case NODE_TIME_LIMIT:
1611                     config.setUsageLimitTimeLimitInMinutes(parseLong(getPpsNodeValue(child), 10));
1612                     break;
1613                 case NODE_USAGE_TIME_PERIOD:
1614                     config.setUsageLimitUsageTimePeriodInMinutes(
1615                             parseLong(getPpsNodeValue(child), 10));
1616                     break;
1617                 default:
1618                     throw new ParsingException("Unknown node under UsageLimits"
1619                             + child.getName());
1620             }
1621         }
1622     }
1623 
1624     /**
1625      * Parse configurations under PerProviderSubscription/Extension/Android/AAAServerTrustedNames
1626      * subtree.
1627      *
1628      * @param node PPSNode representing the root of the
1629      *             PerProviderSubscription/Extension/Android/AAAServerTrustedNames subtree
1630      * @return String[] list of trusted name
1631      * @throws ParsingException
1632      */
parseAaaServerTrustedNames(PPSNode node)1633     private static String[] parseAaaServerTrustedNames(PPSNode node) throws ParsingException {
1634         if (node.isLeaf()) {
1635             throw new ParsingException("Leaf node not expected for AAAServerTrustedNames instance");
1636         }
1637         String fqdnListStr = null;
1638         String[] fqdnListArray = null;
1639         for (PPSNode child : node.getChildren()) {
1640             switch (child.getName()) {
1641                 case NODE_FQDN:
1642                     fqdnListStr = getPpsNodeValue(child);
1643                     fqdnListArray = fqdnListStr.split(";");
1644                     break;
1645                 default:
1646                     throw new ParsingException(
1647                             "Unknown node under AAAServerTrustedNames instance: "
1648                             + child.getName());
1649             }
1650         }
1651         if (fqdnListArray == null) {
1652             throw new ParsingException("AAAServerTrustedNames instance missing FQDN field");
1653         }
1654 
1655         return fqdnListArray;
1656     }
1657 
1658     /**
1659      * Parse configurations under PerProviderSubscription/Extension/Android/NAI
1660      * subtree.
1661      *
1662      * @param node PPSNode representing the root of the
1663      *             PerProviderSubscription/Extension/Android/NAI subtree
1664      * @throws ParsingException
1665      */
parseVendorWbaExtensionNai(PPSNode node, PasspointConfiguration config)1666     private static void parseVendorWbaExtensionNai(PPSNode node,
1667             PasspointConfiguration config) throws ParsingException {
1668         if (node.isLeaf()) {
1669             throw new ParsingException("Leaf node not expected for NAI instance");
1670         }
1671         for (PPSNode child : node.getChildren()) {
1672             switch (child.getName()) {
1673                 case NODE_DECORATED_IDENTITY_PREFIX:
1674                     if (SdkLevel.isAtLeastS()) {
1675                         config.setDecoratedIdentityPrefix(parseDecoratedIdentityPrefix(child));
1676                     }
1677                     break;
1678                 default:
1679                     Log.w(TAG, "Unknown node under NAI instance: " + child.getName());
1680                     break;
1681             }
1682         }
1683     }
1684 
1685     /**
1686      * Parse configurations under PerProviderSubscription/Extension/Android/NAI/DecoratedPrefix
1687      * leaf. This leaf node must contain a list of realms (could be a list of 1) delimited by a '!'
1688      * character. e.g. homerealm.example.org! or proxyrealm.example.net!homerealm.example.org!
1689      * as per RFC 7542.
1690      *
1691      * @param node PPSNode representing the root of the
1692      *             PerProviderSubscription/Extension/Android/NAI/DecoratedPrefix leaf
1693      * @return Decorated identity prefix
1694      * @throws ParsingException
1695      */
parseDecoratedIdentityPrefix(PPSNode node)1696     private static String parseDecoratedIdentityPrefix(PPSNode node) throws ParsingException {
1697         if (!node.isLeaf()) {
1698             throw new ParsingException("Leaf node expected for " + NODE_DECORATED_IDENTITY_PREFIX);
1699         }
1700         String decoratedIdentityPrefix = getPpsNodeValue(node);
1701 
1702         if (TextUtils.isEmpty(decoratedIdentityPrefix) || !decoratedIdentityPrefix.endsWith("!")) {
1703             throw new ParsingException("Invalid value for node " + NODE_DECORATED_IDENTITY_PREFIX);
1704         }
1705 
1706         return decoratedIdentityPrefix;
1707     }
1708 
1709     /**
1710      * Parse configurations under PerProviderSubscription/Extension/Android subtree.
1711      *
1712      * @param node PPSNode representing the root of PerProviderSubscription/Extension
1713      *             subtree
1714      * @param config Instance of {@link PasspointConfiguration}
1715      * @throws ParsingException
1716      */
parseVendorAndroidExtension(PPSNode node, PasspointConfiguration config)1717     private static void parseVendorAndroidExtension(PPSNode node, PasspointConfiguration config)
1718             throws ParsingException {
1719         if (node.isLeaf()) {
1720             throw new ParsingException("Leaf node not expected for AndroidExtension");
1721         }
1722         for (PPSNode child : node.getChildren()) {
1723             switch (child.getName()) {
1724                 case NODE_AAA_SERVER_TRUSTED_NAMES:
1725                     config.setAaaServerTrustedNames(parseAaaServerTrustedNames(child));
1726                     break;
1727                 default:
1728                     // Don't raise an exception for unknown nodes to avoid breaking old release
1729                     Log.w(TAG, "Unknown node under Android Extension: " + child.getName());
1730             }
1731         }
1732     }
1733 
1734     /**
1735      * Parse configurations under PerProviderSubscription/Extension/WBA subtree.
1736      *
1737      * @param node PPSNode representing the root of PerProviderSubscription/Extension
1738      *             subtree
1739      * @param config Instance of {@link PasspointConfiguration}
1740      * @throws ParsingException
1741      */
parseVendorWbaExtension(PPSNode node, PasspointConfiguration config)1742     private static void parseVendorWbaExtension(PPSNode node, PasspointConfiguration config)
1743             throws ParsingException {
1744         if (node.isLeaf()) {
1745             throw new ParsingException("Leaf node not expected for WBA Extension");
1746         }
1747         for (PPSNode child : node.getChildren()) {
1748             switch (child.getName()) {
1749                 case NODE_EXTENSION_NAI:
1750                     parseVendorWbaExtensionNai(child, config);
1751                     break;
1752                 default:
1753                     // Don't raise an exception for unknown nodes
1754                     Log.w(TAG, "Unknown node under WBA Extension: " + child.getName());
1755             }
1756         }
1757     }
1758 
1759     /**
1760      * Parse configurations under PerProviderSubscription/Extension subtree.
1761      *
1762      * @param node PPSNode representing the root of PerProviderSubscription/Extension
1763      *             subtree
1764      * @param config Instance of {@link PasspointConfiguration}
1765      * @throws ParsingException
1766      */
parseExtension(PPSNode node, PasspointConfiguration config)1767     private static void parseExtension(PPSNode node, PasspointConfiguration config)
1768             throws ParsingException {
1769         if (node.isLeaf()) {
1770             throw new ParsingException("Leaf node not expected for Extension");
1771         }
1772         for (PPSNode child : node.getChildren()) {
1773             switch (child.getName()) {
1774                 case NODE_VENDOR_ANDROID:
1775                     parseVendorAndroidExtension(child, config);
1776                     break;
1777                 case NODE_VENDOR_WBA:
1778                     parseVendorWbaExtension(child, config);
1779                     break;
1780                 default:
1781                     // Unknown nodes under Extension won't raise exception.
1782                     // This allows adding new nodes in the future and
1783                     // won't break older release.
1784                     Log.w(TAG, "Unknown node under Extension: " + child.getName());
1785             }
1786         }
1787     }
1788 
1789     /**
1790      * Convert a hex string to a byte array.
1791      *
1792      * @param str String containing hex values
1793      * @return byte[]
1794      * @throws ParsingException
1795      */
parseHexString(String str)1796     private static byte[] parseHexString(String str) throws ParsingException {
1797         if ((str.length() & 1) == 1) {
1798             throw new ParsingException("Odd length hex string: " + str + ", length: "
1799                     + str.length());
1800         }
1801 
1802         byte[] result = new byte[str.length() / 2];
1803         for (int i = 0; i < result.length; i++) {
1804           int index = i * 2;
1805           try {
1806               result[i] = (byte) Integer.parseInt(str.substring(index, index + 2), 16);
1807           } catch (NumberFormatException e) {
1808               throw new ParsingException("Invalid hex string: " + str);
1809           }
1810         }
1811         return result;
1812     }
1813 
1814     /**
1815      * Convert a date string to the number of milliseconds since January 1, 1970, 00:00:00 GMT.
1816      *
1817      * @param dateStr String in the format of yyyy-MM-dd'T'HH:mm:ss'Z'
1818      * @return number of milliseconds
1819      * @throws ParsingException
1820      */
parseDate(String dateStr)1821     private static long parseDate(String dateStr) throws ParsingException {
1822         try {
1823             DateFormat format = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss'Z'");
1824             return format.parse(dateStr).getTime();
1825         } catch (ParseException pe) {
1826             throw new ParsingException("Badly formatted time: " + dateStr);
1827         }
1828     }
1829 
1830     /**
1831      * Parse an integer string.
1832      *
1833      * @param value String of integer value
1834      * @return int
1835      * @throws ParsingException
1836      */
parseInteger(String value)1837     private static int parseInteger(String value) throws ParsingException {
1838         try {
1839             return Integer.parseInt(value);
1840         } catch (NumberFormatException e) {
1841             throw new ParsingException("Invalid integer value: " + value);
1842         }
1843     }
1844 
1845     /**
1846      * Parse a string representing a long integer.
1847      *
1848      * @param value String of long integer value
1849      * @return long
1850      * @throws ParsingException
1851      */
parseLong(String value, int radix)1852     private static long parseLong(String value, int radix) throws ParsingException {
1853         try {
1854             return Long.parseLong(value, radix);
1855         } catch (NumberFormatException e) {
1856             throw new ParsingException("Invalid long integer value: " + value);
1857         }
1858     }
1859 
1860     /**
1861      * Convert a List<Long> to a primitive long array long[].
1862      *
1863      * @param list List to be converted
1864      * @return long[]
1865      */
convertFromLongList(List<Long> list)1866     private static long[] convertFromLongList(List<Long> list) {
1867         Long[] objectArray = list.toArray(new Long[list.size()]);
1868         long[] primitiveArray = new long[objectArray.length];
1869         for (int i = 0; i < objectArray.length; i++) {
1870             primitiveArray[i] = objectArray[i].longValue();
1871         }
1872         return primitiveArray;
1873     }
1874 }
1875