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