/**
 * Copyright (c) 2016, The Android Open Source Project
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package android.net.wifi.hotspot2.omadm;

import android.net.wifi.hotspot2.PasspointConfiguration;
import android.net.wifi.hotspot2.pps.Credential;
import android.net.wifi.hotspot2.pps.HomeSp;
import android.net.wifi.hotspot2.pps.Policy;
import android.net.wifi.hotspot2.pps.UpdateParameter;
import android.text.TextUtils;
import android.util.Log;
import android.util.Pair;

import java.io.IOException;
import java.text.DateFormat;
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;

import org.xml.sax.SAXException;

/**
 * Utility class for converting OMA-DM (Open Mobile Alliance's Device Management)
 * PPS-MO (PerProviderSubscription Management Object) XML tree to a
 * {@link PasspointConfiguration} object.
 *
 * Currently this only supports PerProviderSubscription/HomeSP and
 * PerProviderSubscription/Credential subtree for Hotspot 2.0 Release 1 support.
 *
 * For more info, refer to Hotspot 2.0 PPS MO defined in section 9.1 of the Hotspot 2.0
 * Release 2 Technical Specification.
 *
 * Below is a sample XML string for a Release 1 PPS MO tree:
 *
 * <MgmtTree xmlns="syncml:dmddf1.2">
 *   <VerDTD>1.2</VerDTD>
 *   <Node>
 *     <NodeName>PerProviderSubscription</NodeName>
 *     <RTProperties>
 *       <Type>
 *         <DDFName>urn:wfa:mo:hotspot2dot0­perprovidersubscription:1.0</DDFName>
 *       </Type>
 *     </RTProperties>
 *     <Node>
 *       <NodeName>i001</NodeName>
 *       <Node>
 *         <NodeName>HomeSP</NodeName>
 *         <Node>
 *           <NodeName>FriendlyName</NodeName>
 *           <Value>Century House</Value>
 *         </Node>
 *         <Node>
 *           <NodeName>FQDN</NodeName>
 *           <Value>mi6.co.uk</Value>
 *         </Node>
 *         <Node>
 *           <NodeName>RoamingConsortiumOI</NodeName>
 *           <Value>112233,445566</Value>
 *         </Node>
 *       </Node>
 *       <Node>
 *         <NodeName>Credential</NodeName>
 *         <Node>
 *           <NodeName>Realm</NodeName>
 *           <Value>shaken.stirred.com</Value>
 *         </Node>
 *         <Node>
 *           <NodeName>UsernamePassword</NodeName>
 *           <Node>
 *             <NodeName>Username</NodeName>
 *             <Value>james</Value>
 *           </Node>
 *           <Node>
 *             <NodeName>Password</NodeName>
 *             <Value>Ym9uZDAwNw==</Value>
 *           </Node>
 *           <Node>
 *             <NodeName>EAPMethod</NodeName>
 *             <Node>
 *               <NodeName>EAPType</NodeName>
 *               <Value>21</Value>
 *             </Node>
 *             <Node>
 *               <NodeName>InnerMethod</NodeName>
 *               <Value>MS-CHAP-V2</Value>
 *             </Node>
 *           </Node>
 *         </Node>
 *       </Node>
 *     </Node>
 *   </Node>
 * </MgmtTree>
 */
public final class PpsMoParser {
    private static final String TAG = "PpsMoParser";

    /**
     * XML tags expected in the PPS MO (PerProviderSubscription Management Object) XML tree.
     */
    private static final String TAG_MANAGEMENT_TREE = "MgmtTree";
    private static final String TAG_VER_DTD = "VerDTD";
    private static final String TAG_NODE = "Node";
    private static final String TAG_NODE_NAME = "NodeName";
    private static final String TAG_RT_PROPERTIES = "RTProperties";
    private static final String TAG_TYPE = "Type";
    private static final String TAG_DDF_NAME = "DDFName";
    private static final String TAG_VALUE = "Value";

    /**
     * Name for PerProviderSubscription node.
     */
    private static final String NODE_PER_PROVIDER_SUBSCRIPTION = "PerProviderSubscription";

    /**
     * Fields under PerProviderSubscription.
     */
    private static final String NODE_UPDATE_IDENTIFIER = "UpdateIdentifier";
    private static final String NODE_AAA_SERVER_TRUST_ROOT = "AAAServerTrustRoot";
    private static final String NODE_SUBSCRIPTION_UPDATE = "SubscriptionUpdate";
    private static final String NODE_SUBSCRIPTION_PARAMETER = "SubscriptionParameter";
    private static final String NODE_TYPE_OF_SUBSCRIPTION = "TypeOfSubscription";
    private static final String NODE_USAGE_LIMITS = "UsageLimits";
    private static final String NODE_DATA_LIMIT = "DataLimit";
    private static final String NODE_START_DATE = "StartDate";
    private static final String NODE_TIME_LIMIT = "TimeLimit";
    private static final String NODE_USAGE_TIME_PERIOD = "UsageTimePeriod";
    private static final String NODE_CREDENTIAL_PRIORITY = "CredentialPriority";
    private static final String NODE_EXTENSION = "Extension";

    /**
     * Fields under HomeSP subtree.
     */
    private static final String NODE_HOMESP = "HomeSP";
    private static final String NODE_FQDN = "FQDN";
    private static final String NODE_FRIENDLY_NAME = "FriendlyName";
    private static final String NODE_ROAMING_CONSORTIUM_OI = "RoamingConsortiumOI";
    private static final String NODE_NETWORK_ID = "NetworkID";
    private static final String NODE_SSID = "SSID";
    private static final String NODE_HESSID = "HESSID";
    private static final String NODE_ICON_URL = "IconURL";
    private static final String NODE_HOME_OI_LIST = "HomeOIList";
    private static final String NODE_HOME_OI = "HomeOI";
    private static final String NODE_HOME_OI_REQUIRED = "HomeOIRequired";
    private static final String NODE_OTHER_HOME_PARTNERS = "OtherHomePartners";

    /**
     * Fields under Credential subtree.
     */
    private static final String NODE_CREDENTIAL = "Credential";
    private static final String NODE_CREATION_DATE = "CreationDate";
    private static final String NODE_EXPIRATION_DATE = "ExpirationDate";
    private static final String NODE_USERNAME_PASSWORD = "UsernamePassword";
    private static final String NODE_USERNAME = "Username";
    private static final String NODE_PASSWORD = "Password";
    private static final String NODE_MACHINE_MANAGED = "MachineManaged";
    private static final String NODE_SOFT_TOKEN_APP = "SoftTokenApp";
    private static final String NODE_ABLE_TO_SHARE = "AbleToShare";
    private static final String NODE_EAP_METHOD = "EAPMethod";
    private static final String NODE_EAP_TYPE = "EAPType";
    private static final String NODE_VENDOR_ID = "VendorId";
    private static final String NODE_VENDOR_TYPE = "VendorType";
    private static final String NODE_INNER_EAP_TYPE = "InnerEAPType";
    private static final String NODE_INNER_VENDOR_ID = "InnerVendorID";
    private static final String NODE_INNER_VENDOR_TYPE = "InnerVendorType";
    private static final String NODE_INNER_METHOD = "InnerMethod";
    private static final String NODE_DIGITAL_CERTIFICATE = "DigitalCertificate";
    private static final String NODE_CERTIFICATE_TYPE = "CertificateType";
    private static final String NODE_CERT_SHA256_FINGERPRINT = "CertSHA256Fingerprint";
    private static final String NODE_REALM = "Realm";
    private static final String NODE_SIM = "SIM";
    private static final String NODE_SIM_IMSI = "IMSI";
    private static final String NODE_CHECK_AAA_SERVER_CERT_STATUS = "CheckAAAServerCertStatus";

    /**
     * Fields under Policy subtree.
     */
    private static final String NODE_POLICY = "Policy";
    private static final String NODE_PREFERRED_ROAMING_PARTNER_LIST =
            "PreferredRoamingPartnerList";
    private static final String NODE_FQDN_MATCH = "FQDN_Match";
    private static final String NODE_PRIORITY = "Priority";
    private static final String NODE_COUNTRY = "Country";
    private static final String NODE_MIN_BACKHAUL_THRESHOLD = "MinBackhaulThreshold";
    private static final String NODE_NETWORK_TYPE = "NetworkType";
    private static final String NODE_DOWNLINK_BANDWIDTH = "DLBandwidth";
    private static final String NODE_UPLINK_BANDWIDTH = "ULBandwidth";
    private static final String NODE_POLICY_UPDATE = "PolicyUpdate";
    private static final String NODE_UPDATE_INTERVAL = "UpdateInterval";
    private static final String NODE_UPDATE_METHOD = "UpdateMethod";
    private static final String NODE_RESTRICTION = "Restriction";
    private static final String NODE_URI = "URI";
    private static final String NODE_TRUST_ROOT = "TrustRoot";
    private static final String NODE_CERT_URL = "CertURL";
    private static final String NODE_SP_EXCLUSION_LIST = "SPExclusionList";
    private static final String NODE_REQUIRED_PROTO_PORT_TUPLE = "RequiredProtoPortTuple";
    private static final String NODE_IP_PROTOCOL = "IPProtocol";
    private static final String NODE_PORT_NUMBER = "PortNumber";
    private static final String NODE_MAXIMUM_BSS_LOAD_VALUE = "MaximumBSSLoadValue";
    private static final String NODE_OTHER = "Other";

    /**
     * URN (Unique Resource Name) for PerProviderSubscription Management Object Tree.
     */
    private static final String PPS_MO_URN =
            "urn:wfa:mo:hotspot2dot0-perprovidersubscription:1.0";

    /**
     * Exception for generic parsing errors.
     */
    private static class ParsingException extends Exception {
        public ParsingException(String message) {
            super(message);
        }
    }

    /**
     * Class representing a node within the PerProviderSubscription tree.
     * This is used to flatten out and eliminate the extra layering in the XMLNode tree,
     * to make the data parsing easier and cleaner.
     *
     * A PPSNode can be an internal or a leaf node, but not both.
     *
     */
    private static abstract class PPSNode {
        private final String mName;
        public PPSNode(String name) {
            mName = name;
        }

        /**
         * @return the name of the node
         */
        public String getName() {
            return mName;
        }

        /**
         * Applies for internal node only.
         *
         * @return the list of children nodes.
         */
        public abstract List<PPSNode> getChildren();

        /**
         * Applies for leaf node only.
         *
         * @return the string value of the node
         */
        public abstract String getValue();

        /**
         * @return a flag indicating if this is a leaf or an internal node
         */
        public abstract boolean isLeaf();
    }

    /**
     * Class representing a leaf node in a PPS (PerProviderSubscription) tree.
     */
    private static class LeafNode extends PPSNode {
        private final String mValue;
        public LeafNode(String nodeName, String value) {
            super(nodeName);
            mValue = value;
        }

        @Override
        public String getValue() {
            return mValue;
        }

        @Override
        public List<PPSNode> getChildren() {
            return null;
        }

        @Override
        public boolean isLeaf() {
            return true;
        }
    }

    /**
     * Class representing an internal node in a PPS (PerProviderSubscription) tree.
     */
    private static class InternalNode extends PPSNode {
        private final List<PPSNode> mChildren;
        public InternalNode(String nodeName, List<PPSNode> children) {
            super(nodeName);
            mChildren = children;
        }

        @Override
        public String getValue() {
            return null;
        }

        @Override
        public List<PPSNode> getChildren() {
            return mChildren;
        }

        @Override
        public boolean isLeaf() {
            return false;
        }
    }

    /**
     * @hide
     */
    public PpsMoParser() {}

    /**
     * Convert a XML string representation of a PPS MO (PerProviderSubscription
     * Management Object) tree to a {@link PasspointConfiguration} object.
     *
     * @param xmlString XML string representation of a PPS MO tree
     * @return {@link PasspointConfiguration} or null
     */
    public static PasspointConfiguration parseMoText(String xmlString) {
        // Convert the XML string to a XML tree.
        XMLParser xmlParser = new XMLParser();
        XMLNode root = null;
        try {
            root = xmlParser.parse(xmlString);
        } catch(IOException | SAXException e) {
            return null;
        }
        if (root == null) {
            return null;
        }

        // Verify root node is a "MgmtTree" node.
        if (root.getTag() != TAG_MANAGEMENT_TREE) {
            Log.e(TAG, "Root is not a MgmtTree");
            return null;
        }

        String verDtd = null;    // Used for detecting duplicate VerDTD element.
        PasspointConfiguration config = null;
        for (XMLNode child : root.getChildren()) {
            switch(child.getTag()) {
                case TAG_VER_DTD:
                    if (verDtd != null) {
                        Log.e(TAG, "Duplicate VerDTD element");
                        return null;
                    }
                    verDtd = child.getText();
                    break;
                case TAG_NODE:
                    if (config != null) {
                        Log.e(TAG, "Unexpected multiple Node element under MgmtTree");
                        return null;
                    }
                    try {
                        config = parsePpsNode(child);
                    } catch (ParsingException e) {
                        Log.e(TAG, e.getMessage());
                        return null;
                    }
                    break;
                default:
                    Log.e(TAG, "Unknown node: " + child.getTag());
                    return null;
            }
        }
        return config;
    }

    /**
     * Parse a PerProviderSubscription node. Below is the format of the XML tree (with
     * each XML element represent a node in the tree):
     *
     * <Node>
     *   <NodeName>PerProviderSubscription</NodeName>
     *   <RTProperties>
     *     ...
     *   </RTPProperties>
     *   <Node>
     *     <NodeName>UpdateIdentifier</NodeName>
     *     <Value>...</Value>
     *   </Node>
     *   <Node>
     *     ...
     *   </Node>
     * </Node>
     *
     * @param node XMLNode that contains PerProviderSubscription node.
     * @return PasspointConfiguration or null
     * @throws ParsingException
     */
    private static PasspointConfiguration parsePpsNode(XMLNode node)
            throws ParsingException {
        PasspointConfiguration config = null;
        String nodeName = null;
        int updateIdentifier = Integer.MIN_VALUE;
        for (XMLNode child : node.getChildren()) {
            switch (child.getTag()) {
                case TAG_NODE_NAME:
                    if (nodeName != null) {
                        throw new ParsingException("Duplicate NodeName: " + child.getText());
                    }
                    nodeName = child.getText();
                    if (!TextUtils.equals(nodeName, NODE_PER_PROVIDER_SUBSCRIPTION)) {
                        throw new ParsingException("Unexpected NodeName: " + nodeName);
                    }
                    break;
                case TAG_NODE:
                    // A node can be either an UpdateIdentifier node or a PerProviderSubscription
                    // instance node.  Flatten out the XML tree first by converting it to a PPS
                    // tree to reduce the complexity of the parsing code.
                    PPSNode ppsNodeRoot = buildPpsNode(child);
                    if (TextUtils.equals(ppsNodeRoot.getName(), NODE_UPDATE_IDENTIFIER)) {
                        if (updateIdentifier != Integer.MIN_VALUE) {
                            throw new ParsingException("Multiple node for UpdateIdentifier");
                        }
                        updateIdentifier = parseInteger(getPpsNodeValue(ppsNodeRoot));
                    } else {
                        // Only one PerProviderSubscription instance is expected and allowed.
                        if (config != null) {
                            throw new ParsingException("Multiple PPS instance");
                        }
                        config = parsePpsInstance(ppsNodeRoot);
                    }
                    break;
                case TAG_RT_PROPERTIES:
                    // Parse and verify URN stored in the RT (Run Time) Properties.
                    String urn = parseUrn(child);
                    if (!TextUtils.equals(urn, PPS_MO_URN)) {
                        throw new ParsingException("Unknown URN: " + urn);
                    }
                    break;
                default:
                    throw new ParsingException("Unknown tag under PPS node: " + child.getTag());
            }
        }
        if (config != null && updateIdentifier != Integer.MIN_VALUE) {
            config.setUpdateIdentifier(updateIdentifier);
        }
        return config;
    }

    /**
     * Parse the URN stored in the RTProperties. Below is the format of the RTPProperties node:
     *
     * <RTProperties>
     *   <Type>
     *     <DDFName>urn:...</DDFName>
     *   </Type>
     * </RTProperties>
     *
     * @param node XMLNode that contains RTProperties node.
     * @return URN String of URN.
     * @throws ParsingException
     */
    private static String parseUrn(XMLNode node) throws ParsingException {
        if (node.getChildren().size() != 1)
            throw new ParsingException("Expect RTPProperties node to only have one child");

        XMLNode typeNode = node.getChildren().get(0);
        if (typeNode.getChildren().size() != 1) {
            throw new ParsingException("Expect Type node to only have one child");
        }
        if (!TextUtils.equals(typeNode.getTag(), TAG_TYPE)) {
            throw new ParsingException("Unexpected tag for Type: " + typeNode.getTag());
        }

        XMLNode ddfNameNode = typeNode.getChildren().get(0);
        if (!ddfNameNode.getChildren().isEmpty()) {
            throw new ParsingException("Expect DDFName node to have no child");
        }
        if (!TextUtils.equals(ddfNameNode.getTag(), TAG_DDF_NAME)) {
            throw new ParsingException("Unexpected tag for DDFName: " + ddfNameNode.getTag());
        }

        return ddfNameNode.getText();
    }

    /**
     * Convert a XML tree represented by XMLNode to a PPS (PerProviderSubscription) instance tree
     * represented by PPSNode.  This flattens out the XML tree to allow easier and cleaner parsing
     * of the PPS configuration data.  Only three types of XML tag are expected: "NodeName",
     * "Node", and "Value".
     *
     * The original XML tree (each XML element represent a node):
     *
     * <Node>
     *   <NodeName>root</NodeName>
     *   <Node>
     *     <NodeName>child1</NodeName>
     *     <Value>value1</Value>
     *   </Node>
     *   <Node>
     *     <NodeName>child2</NodeName>
     *     <Node>
     *       <NodeName>grandchild1</NodeName>
     *       ...
     *     </Node>
     *   </Node>
     *   ...
     * </Node>
     *
     * The converted PPS tree:
     *
     * [root] --- [child1, value1]
     *   |
     *   ---------[child2] --------[grandchild1] --- ...
     *
     * @param node XMLNode pointed to the root of a XML tree
     * @return PPSNode pointing to the root of a PPS tree
     * @throws ParsingException
     */
    private static PPSNode buildPpsNode(XMLNode node) throws ParsingException {
        String nodeName = null;
        String nodeValue = null;
        List<PPSNode> childNodes = new ArrayList<PPSNode>();
        // Names of parsed child nodes, use for detecting multiple child nodes with the same name.
        Set<String> parsedNodes = new HashSet<String>();

        for (XMLNode child : node.getChildren()) {
            String tag = child.getTag();
            if (TextUtils.equals(tag, TAG_NODE_NAME)) {
                if (nodeName != null) {
                    throw new ParsingException("Duplicate NodeName node");
                }
                nodeName = child.getText();
            } else if (TextUtils.equals(tag, TAG_NODE)) {
                PPSNode ppsNode = buildPpsNode(child);
                if (parsedNodes.contains(ppsNode.getName())) {
                    throw new ParsingException("Duplicate node: " + ppsNode.getName());
                }
                parsedNodes.add(ppsNode.getName());
                childNodes.add(ppsNode);
            } else if (TextUtils.equals(tag, TAG_VALUE)) {
               if (nodeValue != null) {
                   throw new ParsingException("Duplicate Value node");
               }
               nodeValue = child.getText();
            } else {
                throw new ParsingException("Unknown tag: " + tag);
            }
        }

        if (nodeName == null) {
            throw new ParsingException("Invalid node: missing NodeName");
        }
        if (nodeValue == null && childNodes.size() == 0) {
            throw new ParsingException("Invalid node: " + nodeName +
                    " missing both value and children");
        }
        if (nodeValue != null && childNodes.size() > 0) {
            throw new ParsingException("Invalid node: " + nodeName +
                    " contained both value and children");
        }

        if (nodeValue != null) {
            return new LeafNode(nodeName, nodeValue);
        }
        return new InternalNode(nodeName, childNodes);
    }

    /**
     * Return the value of a PPSNode.  An exception will be thrown if the given node
     * is not a leaf node.
     *
     * @param node PPSNode to retrieve the value from
     * @return String representing the value of the node
     * @throws ParsingException
     */
    private static String getPpsNodeValue(PPSNode node) throws ParsingException {
        if (!node.isLeaf()) {
            throw new ParsingException("Cannot get value from a non-leaf node: " + node.getName());
        }
        return node.getValue();
    }

    /**
     * Parse a PPS (PerProviderSubscription) configurations from a PPS tree.
     *
     * @param root PPSNode representing the root of the PPS tree
     * @return PasspointConfiguration
     * @throws ParsingException
     */
    private static PasspointConfiguration parsePpsInstance(PPSNode root)
            throws ParsingException {
        if (root.isLeaf()) {
            throw new ParsingException("Leaf node not expected for PPS instance");
        }

        PasspointConfiguration config = new PasspointConfiguration();
        for (PPSNode child : root.getChildren()) {
            switch(child.getName()) {
                case NODE_HOMESP:
                    config.setHomeSp(parseHomeSP(child));
                    break;
                case NODE_CREDENTIAL:
                    config.setCredential(parseCredential(child));
                    break;
                case NODE_POLICY:
                    config.setPolicy(parsePolicy(child));
                    break;
                case NODE_AAA_SERVER_TRUST_ROOT:
                    config.setTrustRootCertList(parseAAAServerTrustRootList(child));
                    break;
                case NODE_SUBSCRIPTION_UPDATE:
                    config.setSubscriptionUpdate(parseUpdateParameter(child));
                    break;
                case NODE_SUBSCRIPTION_PARAMETER:
                    parseSubscriptionParameter(child, config);
                    break;
                case NODE_CREDENTIAL_PRIORITY:
                    config.setCredentialPriority(parseInteger(getPpsNodeValue(child)));
                    break;
                case NODE_EXTENSION:
                    // All vendor specific information will be under this node.
                    Log.d(TAG, "Ignore Extension node for vendor specific information");
                    break;
                default:
                    throw new ParsingException("Unknown node: " + child.getName());
            }
        }
        return config;
    }

    /**
     * Parse configurations under PerProviderSubscription/HomeSP subtree.
     *
     * @param node PPSNode representing the root of the PerProviderSubscription/HomeSP subtree
     * @return HomeSP
     * @throws ParsingException
     */
    private static HomeSp parseHomeSP(PPSNode node) throws ParsingException {
        if (node.isLeaf()) {
            throw new ParsingException("Leaf node not expected for HomeSP");
        }

        HomeSp homeSp = new HomeSp();
        for (PPSNode child : node.getChildren()) {
            switch (child.getName()) {
                case NODE_FQDN:
                    homeSp.setFqdn(getPpsNodeValue(child));
                    break;
                case NODE_FRIENDLY_NAME:
                    homeSp.setFriendlyName(getPpsNodeValue(child));
                    break;
                case NODE_ROAMING_CONSORTIUM_OI:
                    homeSp.setRoamingConsortiumOis(
                            parseRoamingConsortiumOI(getPpsNodeValue(child)));
                    break;
                case NODE_ICON_URL:
                    homeSp.setIconUrl(getPpsNodeValue(child));
                    break;
                case NODE_NETWORK_ID:
                    homeSp.setHomeNetworkIds(parseNetworkIds(child));
                    break;
                case NODE_HOME_OI_LIST:
                    Pair<List<Long>, List<Long>> homeOIs = parseHomeOIList(child);
                    homeSp.setMatchAllOis(convertFromLongList(homeOIs.first));
                    homeSp.setMatchAnyOis(convertFromLongList(homeOIs.second));
                    break;
                case NODE_OTHER_HOME_PARTNERS:
                    homeSp.setOtherHomePartners(parseOtherHomePartners(child));
                    break;
                default:
                    throw new ParsingException("Unknown node under HomeSP: " + child.getName());
            }
        }
        return homeSp;
    }

    /**
     * Parse the roaming consortium OI string, which contains a list of OIs separated by ",".
     *
     * @param oiStr string containing list of OIs (Organization Identifiers) separated by ","
     * @return long[]
     * @throws ParsingException
     */
    private static long[] parseRoamingConsortiumOI(String oiStr)
            throws ParsingException {
        String[] oiStrArray = oiStr.split(",");
        long[] oiArray = new long[oiStrArray.length];
        for (int i = 0; i < oiStrArray.length; i++) {
            oiArray[i] = parseLong(oiStrArray[i], 16);
        }
        return oiArray;
    }

    /**
     * Parse configurations under PerProviderSubscription/HomeSP/NetworkID subtree.
     *
     * @param node PPSNode representing the root of the PerProviderSubscription/HomeSP/NetworkID
     *             subtree
     * @return HashMap<String, Long> representing list of <SSID, HESSID> pair.
     * @throws ParsingException
     */
    static private Map<String, Long> parseNetworkIds(PPSNode node)
            throws ParsingException {
        if (node.isLeaf()) {
            throw new ParsingException("Leaf node not expected for NetworkID");
        }

        Map<String, Long> networkIds = new HashMap<>();
        for (PPSNode child : node.getChildren()) {
            Pair<String, Long> networkId = parseNetworkIdInstance(child);
            networkIds.put(networkId.first, networkId.second);
        }
        return networkIds;
    }

    /**
     * Parse configurations under PerProviderSubscription/HomeSP/NetworkID/<X+> subtree.
     * The instance name (<X+>) is irrelevant and must be unique for each instance, which
     * is verified when the PPS tree is constructed {@link #buildPpsNode}.
     *
     * @param node PPSNode representing the root of the
     *             PerProviderSubscription/HomeSP/NetworkID/<X+> subtree
     * @return Pair<String, Long> representing <SSID, HESSID> pair.
     * @throws ParsingException
     */
    static private Pair<String, Long> parseNetworkIdInstance(PPSNode node)
            throws ParsingException {
        if (node.isLeaf()) {
            throw new ParsingException("Leaf node not expected for NetworkID instance");
        }

        String ssid = null;
        Long hessid = null;
        for (PPSNode child : node.getChildren()) {
            switch (child.getName()) {
                case NODE_SSID:
                    ssid = getPpsNodeValue(child);
                    break;
                case NODE_HESSID:
                    hessid = parseLong(getPpsNodeValue(child), 16);
                    break;
                default:
                    throw new ParsingException("Unknown node under NetworkID instance: " +
                            child.getName());
            }
        }
        if (ssid == null)
            throw new ParsingException("NetworkID instance missing SSID");

        return new Pair<String, Long>(ssid, hessid);
    }

    /**
     * Parse configurations under PerProviderSubscription/HomeSP/HomeOIList subtree.
     *
     * @param node PPSNode representing the root of the PerProviderSubscription/HomeSP/HomeOIList
     *             subtree
     * @return Pair<List<Long>, List<Long>> containing both MatchAllOIs and MatchAnyOIs list.
     * @throws ParsingException
     */
    private static Pair<List<Long>, List<Long>> parseHomeOIList(PPSNode node)
            throws ParsingException {
        if (node.isLeaf()) {
            throw new ParsingException("Leaf node not expected for HomeOIList");
        }

        List<Long> matchAllOIs = new ArrayList<Long>();
        List<Long> matchAnyOIs = new ArrayList<Long>();
        for (PPSNode child : node.getChildren()) {
            Pair<Long, Boolean> homeOI = parseHomeOIInstance(child);
            if (homeOI.second.booleanValue()) {
                matchAllOIs.add(homeOI.first);
            } else {
                matchAnyOIs.add(homeOI.first);
            }
        }
        return new Pair<List<Long>, List<Long>>(matchAllOIs, matchAnyOIs);
    }

    /**
     * Parse configurations under PerProviderSubscription/HomeSP/HomeOIList/<X+> subtree.
     * The instance name (<X+>) is irrelevant and must be unique for each instance, which
     * is verified when the PPS tree is constructed {@link #buildPpsNode}.
     *
     * @param node PPSNode representing the root of the
     *             PerProviderSubscription/HomeSP/HomeOIList/<X+> subtree
     * @return Pair<Long, Boolean> containing a HomeOI and a HomeOIRequired flag
     * @throws ParsingException
     */
    private static Pair<Long, Boolean> parseHomeOIInstance(PPSNode node) throws ParsingException {
        if (node.isLeaf()) {
            throw new ParsingException("Leaf node not expected for HomeOI instance");
        }

        Long oi = null;
        Boolean required = null;
        for (PPSNode child : node.getChildren()) {
            switch (child.getName()) {
                case NODE_HOME_OI:
                    try {
                        oi = Long.valueOf(getPpsNodeValue(child), 16);
                    } catch (NumberFormatException e) {
                        throw new ParsingException("Invalid HomeOI: " + getPpsNodeValue(child));
                    }
                    break;
                case NODE_HOME_OI_REQUIRED:
                    required = Boolean.valueOf(getPpsNodeValue(child));
                    break;
                default:
                    throw new ParsingException("Unknown node under NetworkID instance: " +
                            child.getName());
            }
        }
        if (oi == null) {
            throw new ParsingException("HomeOI instance missing OI field");
        }
        if (required == null) {
            throw new ParsingException("HomeOI instance missing required field");
        }
        return new Pair<Long, Boolean>(oi, required);
    }

    /**
     * Parse configurations under PerProviderSubscription/HomeSP/OtherHomePartners subtree.
     * This contains a list of FQDN (Fully Qualified Domain Name) that are considered
     * home partners.
     *
     * @param node PPSNode representing the root of the
     *             PerProviderSubscription/HomeSP/OtherHomePartners subtree
     * @return String[] list of partner's FQDN
     * @throws ParsingException
     */
    private static String[] parseOtherHomePartners(PPSNode node) throws ParsingException {
        if (node.isLeaf()) {
            throw new ParsingException("Leaf node not expected for OtherHomePartners");
        }
        List<String> otherHomePartners = new ArrayList<String>();
        for (PPSNode child : node.getChildren()) {
            String fqdn = parseOtherHomePartnerInstance(child);
            otherHomePartners.add(fqdn);
        }
        return otherHomePartners.toArray(new String[otherHomePartners.size()]);
    }

    /**
     * Parse configurations under PerProviderSubscription/HomeSP/OtherHomePartners/<X+> subtree.
     * The instance name (<X+>) is irrelevant and must be unique for each instance, which
     * is verified when the PPS tree is constructed {@link #buildPpsNode}.
     *
     * @param node PPSNode representing the root of the
     *             PerProviderSubscription/HomeSP/OtherHomePartners/<X+> subtree
     * @return String FQDN of the partner
     * @throws ParsingException
     */
    private static String parseOtherHomePartnerInstance(PPSNode node) throws ParsingException {
        if (node.isLeaf()) {
            throw new ParsingException("Leaf node not expected for OtherHomePartner instance");
        }
        String fqdn = null;
        for (PPSNode child : node.getChildren()) {
            switch (child.getName()) {
                case NODE_FQDN:
                    fqdn = getPpsNodeValue(child);
                    break;
                default:
                    throw new ParsingException(
                            "Unknown node under OtherHomePartner instance: " + child.getName());
            }
        }
        if (fqdn == null) {
            throw new ParsingException("OtherHomePartner instance missing FQDN field");
        }
        return fqdn;
    }

    /**
     * Parse configurations under PerProviderSubscription/Credential subtree.
     *
     * @param node PPSNode representing the root of the PerProviderSubscription/Credential subtree
     * @return Credential
     * @throws ParsingException
     */
    private static Credential parseCredential(PPSNode node) throws ParsingException {
        if (node.isLeaf()) {
            throw new ParsingException("Leaf node not expected for HomeSP");
        }

        Credential credential = new Credential();
        for (PPSNode child: node.getChildren()) {
            switch (child.getName()) {
                case NODE_CREATION_DATE:
                    credential.setCreationTimeInMillis(parseDate(getPpsNodeValue(child)));
                    break;
                case NODE_EXPIRATION_DATE:
                    credential.setExpirationTimeInMillis(parseDate(getPpsNodeValue(child)));
                    break;
                case NODE_USERNAME_PASSWORD:
                    credential.setUserCredential(parseUserCredential(child));
                    break;
                case NODE_DIGITAL_CERTIFICATE:
                    credential.setCertCredential(parseCertificateCredential(child));
                    break;
                case NODE_REALM:
                    credential.setRealm(getPpsNodeValue(child));
                    break;
                case NODE_CHECK_AAA_SERVER_CERT_STATUS:
                    credential.setCheckAaaServerCertStatus(
                            Boolean.parseBoolean(getPpsNodeValue(child)));
                    break;
                case NODE_SIM:
                    credential.setSimCredential(parseSimCredential(child));
                    break;
                default:
                    throw new ParsingException("Unknown node under Credential: " +
                            child.getName());
            }
        }
        return credential;
    }

    /**
     * Parse configurations under PerProviderSubscription/Credential/UsernamePassword subtree.
     *
     * @param node PPSNode representing the root of the
     *             PerProviderSubscription/Credential/UsernamePassword subtree
     * @return Credential.UserCredential
     * @throws ParsingException
     */
    private static Credential.UserCredential parseUserCredential(PPSNode node)
            throws ParsingException {
        if (node.isLeaf()) {
            throw new ParsingException("Leaf node not expected for UsernamePassword");
        }

        Credential.UserCredential userCred = new Credential.UserCredential();
        for (PPSNode child : node.getChildren()) {
            switch (child.getName()) {
                case NODE_USERNAME:
                    userCred.setUsername(getPpsNodeValue(child));
                    break;
                case NODE_PASSWORD:
                    userCred.setPassword(getPpsNodeValue(child));
                    break;
                case NODE_MACHINE_MANAGED:
                    userCred.setMachineManaged(Boolean.parseBoolean(getPpsNodeValue(child)));
                    break;
                case NODE_SOFT_TOKEN_APP:
                    userCred.setSoftTokenApp(getPpsNodeValue(child));
                    break;
                case NODE_ABLE_TO_SHARE:
                    userCred.setAbleToShare(Boolean.parseBoolean(getPpsNodeValue(child)));
                    break;
                case NODE_EAP_METHOD:
                    parseEAPMethod(child, userCred);
                    break;
                default:
                    throw new ParsingException("Unknown node under UsernamPassword: " +
                            child.getName());
            }
        }
        return userCred;
    }

    /**
     * Parse configurations under PerProviderSubscription/Credential/UsernamePassword/EAPMethod
     * subtree.
     *
     * @param node PPSNode representing the root of the
     *             PerProviderSubscription/Credential/UsernamePassword/EAPMethod subtree
     * @param userCred UserCredential to be updated with EAP method values.
     * @throws ParsingException
     */
    private static void parseEAPMethod(PPSNode node, Credential.UserCredential userCred)
            throws ParsingException {
        if (node.isLeaf()) {
            throw new ParsingException("Leaf node not expected for EAPMethod");
        }

        for (PPSNode child : node.getChildren()) {
            switch(child.getName()) {
                case NODE_EAP_TYPE:
                    userCred.setEapType(parseInteger(getPpsNodeValue(child)));
                    break;
                case NODE_INNER_METHOD:
                    userCred.setNonEapInnerMethod(getPpsNodeValue(child));
                    break;
                case NODE_VENDOR_ID:
                case NODE_VENDOR_TYPE:
                case NODE_INNER_EAP_TYPE:
                case NODE_INNER_VENDOR_ID:
                case NODE_INNER_VENDOR_TYPE:
                    // Only EAP-TTLS is currently supported for user credential, which doesn't
                    // use any of these parameters.
                    Log.d(TAG, "Ignore unsupported EAP method parameter: " + child.getName());
                    break;
                default:
                    throw new ParsingException("Unknown node under EAPMethod: " + child.getName());
            }
        }
    }

    /**
     * Parse configurations under PerProviderSubscription/Credential/DigitalCertificate subtree.
     *
     * @param node PPSNode representing the root of the
     *             PerProviderSubscription/Credential/DigitalCertificate subtree
     * @return Credential.CertificateCredential
     * @throws ParsingException
     */
    private static Credential.CertificateCredential parseCertificateCredential(PPSNode node)
            throws ParsingException {
        if (node.isLeaf()) {
            throw new ParsingException("Leaf node not expected for DigitalCertificate");
        }

        Credential.CertificateCredential certCred = new Credential.CertificateCredential();
        for (PPSNode child : node.getChildren()) {
            switch (child.getName()) {
                case NODE_CERTIFICATE_TYPE:
                    certCred.setCertType(getPpsNodeValue(child));
                    break;
                case NODE_CERT_SHA256_FINGERPRINT:
                    certCred.setCertSha256Fingerprint(parseHexString(getPpsNodeValue(child)));
                    break;
                default:
                    throw new ParsingException("Unknown node under DigitalCertificate: " +
                            child.getName());
            }
        }
        return certCred;
    }

    /**
     * Parse configurations under PerProviderSubscription/Credential/SIM subtree.
     *
     * @param node PPSNode representing the root of the PerProviderSubscription/Credential/SIM
     *             subtree
     * @return Credential.SimCredential
     * @throws ParsingException
     */
    private static Credential.SimCredential parseSimCredential(PPSNode node)
            throws ParsingException {
        if (node.isLeaf()) {
            throw new ParsingException("Leaf node not expected for SIM");
        }

        Credential.SimCredential simCred = new Credential.SimCredential();
        for (PPSNode child : node.getChildren()) {
            switch (child.getName()) {
                case NODE_SIM_IMSI:
                    simCred.setImsi(getPpsNodeValue(child));
                    break;
                case NODE_EAP_TYPE:
                    simCred.setEapType(parseInteger(getPpsNodeValue(child)));
                    break;
                default:
                    throw new ParsingException("Unknown node under SIM: " + child.getName());
            }
        }
        return simCred;
    }

    /**
     * Parse configurations under PerProviderSubscription/Policy subtree.
     *
     * @param node PPSNode representing the root of the PerProviderSubscription/Policy subtree
     * @return {@link Policy}
     * @throws ParsingException
     */
    private static Policy parsePolicy(PPSNode node) throws ParsingException {
        if (node.isLeaf()) {
            throw new ParsingException("Leaf node not expected for Policy");
        }

        Policy policy = new Policy();
        for (PPSNode child : node.getChildren()) {
            switch (child.getName()) {
                case NODE_PREFERRED_ROAMING_PARTNER_LIST:
                    policy.setPreferredRoamingPartnerList(parsePreferredRoamingPartnerList(child));
                    break;
                case NODE_MIN_BACKHAUL_THRESHOLD:
                    parseMinBackhaulThreshold(child, policy);
                    break;
                case NODE_POLICY_UPDATE:
                    policy.setPolicyUpdate(parseUpdateParameter(child));
                    break;
                case NODE_SP_EXCLUSION_LIST:
                    policy.setExcludedSsidList(parseSpExclusionList(child));
                    break;
                case NODE_REQUIRED_PROTO_PORT_TUPLE:
                    policy.setRequiredProtoPortMap(parseRequiredProtoPortTuple(child));
                    break;
                case NODE_MAXIMUM_BSS_LOAD_VALUE:
                    policy.setMaximumBssLoadValue(parseInteger(getPpsNodeValue(child)));
                    break;
                default:
                    throw new ParsingException("Unknown node under Policy: " + child.getName());
            }
        }
        return policy;
    }

    /**
     * Parse configurations under PerProviderSubscription/Policy/PreferredRoamingPartnerList
     * subtree.
     *
     * @param node PPSNode representing the root of the
     *             PerProviderSubscription/Policy/PreferredRoamingPartnerList subtree
     * @return List of {@link Policy#RoamingPartner}
     * @throws ParsingException
     */
    private static List<Policy.RoamingPartner> parsePreferredRoamingPartnerList(PPSNode node)
            throws ParsingException {
        if (node.isLeaf()) {
            throw new ParsingException("Leaf node not expected for PreferredRoamingPartnerList");
        }
        List<Policy.RoamingPartner> partnerList = new ArrayList<>();
        for (PPSNode child : node.getChildren()) {
            partnerList.add(parsePreferredRoamingPartner(child));
        }
        return partnerList;
    }

    /**
     * Parse configurations under PerProviderSubscription/Policy/PreferredRoamingPartnerList/<X+>
     * subtree.
     *
     * @param node PPSNode representing the root of the
     *             PerProviderSubscription/Policy/PreferredRoamingPartnerList/<X+> subtree
     * @return {@link Policy#RoamingPartner}
     * @throws ParsingException
     */
    private static Policy.RoamingPartner parsePreferredRoamingPartner(PPSNode node)
            throws ParsingException {
        if (node.isLeaf()) {
            throw new ParsingException("Leaf node not expected for PreferredRoamingPartner "
                    + "instance");
        }

        Policy.RoamingPartner roamingPartner = new Policy.RoamingPartner();
        for (PPSNode child : node.getChildren()) {
            switch (child.getName()) {
                case NODE_FQDN_MATCH:
                    // FQDN_Match field is in the format of "[FQDN],[MatchInfo]", where [MatchInfo]
                    // is either "exactMatch" for exact match of FQDN or "includeSubdomains" for
                    // matching all FQDNs with the same sub-domain.
                    String fqdnMatch = getPpsNodeValue(child);
                    String[] fqdnMatchArray = fqdnMatch.split(",");
                    if (fqdnMatchArray.length != 2) {
                        throw new ParsingException("Invalid FQDN_Match: " + fqdnMatch);
                    }
                    roamingPartner.setFqdn(fqdnMatchArray[0]);
                    if (TextUtils.equals(fqdnMatchArray[1], "exactMatch")) {
                        roamingPartner.setFqdnExactMatch(true);
                    } else if (TextUtils.equals(fqdnMatchArray[1], "includeSubdomains")) {
                        roamingPartner.setFqdnExactMatch(false);
                    } else {
                        throw new ParsingException("Invalid FQDN_Match: " + fqdnMatch);
                    }
                    break;
                case NODE_PRIORITY:
                    roamingPartner.setPriority(parseInteger(getPpsNodeValue(child)));
                    break;
                case NODE_COUNTRY:
                    roamingPartner.setCountries(getPpsNodeValue(child));
                    break;
                default:
                    throw new ParsingException("Unknown node under PreferredRoamingPartnerList "
                            + "instance " + child.getName());
            }
        }
        return roamingPartner;
    }

    /**
     * Parse configurations under PerProviderSubscription/Policy/MinBackhaulThreshold subtree
     * into the given policy.
     *
     * @param node PPSNode representing the root of the
     *             PerProviderSubscription/Policy/MinBackhaulThreshold subtree
     * @param policy The policy to store the MinBackhualThreshold configuration
     * @throws ParsingException
     */
    private static void parseMinBackhaulThreshold(PPSNode node, Policy policy)
            throws ParsingException {
        if (node.isLeaf()) {
            throw new ParsingException("Leaf node not expected for MinBackhaulThreshold");
        }
        for (PPSNode child : node.getChildren()) {
            parseMinBackhaulThresholdInstance(child, policy);
        }
    }

    /**
     * Parse configurations under PerProviderSubscription/Policy/MinBackhaulThreshold/<X+> subtree
     * into the given policy.
     *
     * @param node PPSNode representing the root of the
     *             PerProviderSubscription/Policy/MinBackhaulThreshold/<X+> subtree
     * @param policy The policy to store the MinBackhaulThreshold configuration
     * @throws ParsingException
     */
    private static void parseMinBackhaulThresholdInstance(PPSNode node, Policy policy)
            throws ParsingException {
        if (node.isLeaf()) {
            throw new ParsingException("Leaf node not expected for MinBackhaulThreshold instance");
        }
        String networkType = null;
        long downlinkBandwidth = Long.MIN_VALUE;
        long uplinkBandwidth = Long.MIN_VALUE;
        for (PPSNode child : node.getChildren()) {
            switch (child.getName()) {
                case NODE_NETWORK_TYPE:
                    networkType = getPpsNodeValue(child);
                    break;
                case NODE_DOWNLINK_BANDWIDTH:
                    downlinkBandwidth = parseLong(getPpsNodeValue(child), 10);
                    break;
                case NODE_UPLINK_BANDWIDTH:
                    uplinkBandwidth = parseLong(getPpsNodeValue(child), 10);
                    break;
                default:
                    throw new ParsingException("Unknown node under MinBackhaulThreshold instance "
                            + child.getName());
            }
        }
        if (networkType == null) {
            throw new ParsingException("Missing NetworkType field");
        }

        if (TextUtils.equals(networkType, "home")) {
            policy.setMinHomeDownlinkBandwidth(downlinkBandwidth);
            policy.setMinHomeUplinkBandwidth(uplinkBandwidth);
        } else if (TextUtils.equals(networkType, "roaming")) {
            policy.setMinRoamingDownlinkBandwidth(downlinkBandwidth);
            policy.setMinRoamingUplinkBandwidth(uplinkBandwidth);
        } else {
            throw new ParsingException("Invalid network type: " + networkType);
        }
    }

    /**
     * Parse update parameters. This contained configurations from either
     * PerProviderSubscription/Policy/PolicyUpdate or PerProviderSubscription/SubscriptionUpdate
     * subtree.
     *
     * @param node PPSNode representing the root of the PerProviderSubscription/Policy/PolicyUpdate
     *             or PerProviderSubscription/SubscriptionUpdate subtree
     * @return {@link UpdateParameter}
     * @throws ParsingException
     */
    private static UpdateParameter parseUpdateParameter(PPSNode node)
            throws ParsingException {
        if (node.isLeaf()) {
            throw new ParsingException("Leaf node not expected for Update Parameters");
        }

        UpdateParameter updateParam = new UpdateParameter();
        for (PPSNode child : node.getChildren()) {
            switch(child.getName()) {
                case NODE_UPDATE_INTERVAL:
                    updateParam.setUpdateIntervalInMinutes(parseLong(getPpsNodeValue(child), 10));
                    break;
                case NODE_UPDATE_METHOD:
                    updateParam.setUpdateMethod(getPpsNodeValue(child));
                    break;
                case NODE_RESTRICTION:
                    updateParam.setRestriction(getPpsNodeValue(child));
                    break;
                case NODE_URI:
                    updateParam.setServerUri(getPpsNodeValue(child));
                    break;
                case NODE_USERNAME_PASSWORD:
                    Pair<String, String> usernamePassword = parseUpdateUserCredential(child);
                    updateParam.setUsername(usernamePassword.first);
                    updateParam.setBase64EncodedPassword(usernamePassword.second);
                    break;
                case NODE_TRUST_ROOT:
                    Pair<String, byte[]> trustRoot = parseTrustRoot(child);
                    updateParam.setTrustRootCertUrl(trustRoot.first);
                    updateParam.setTrustRootCertSha256Fingerprint(trustRoot.second);
                    break;
                case NODE_OTHER:
                    Log.d(TAG, "Ignore unsupported paramter: " + child.getName());
                    break;
                default:
                    throw new ParsingException("Unknown node under Update Parameters: "
                            + child.getName());
            }
        }
        return updateParam;
    }

    /**
     * Parse username and password parameters associated with policy or subscription update.
     * This contained configurations under either
     * PerProviderSubscription/Policy/PolicyUpdate/UsernamePassword or
     * PerProviderSubscription/SubscriptionUpdate/UsernamePassword subtree.
     *
     * @param node PPSNode representing the root of the UsernamePassword subtree
     * @return Pair of username and password
     * @throws ParsingException
     */
    private static Pair<String, String> parseUpdateUserCredential(PPSNode node)
            throws ParsingException {
        if (node.isLeaf()) {
            throw new ParsingException("Leaf node not expected for UsernamePassword");
        }

        String username = null;
        String password = null;
        for (PPSNode child : node.getChildren()) {
            switch (child.getName()) {
                case NODE_USERNAME:
                    username = getPpsNodeValue(child);
                    break;
                case NODE_PASSWORD:
                    password = getPpsNodeValue(child);
                    break;
                default:
                    throw new ParsingException("Unknown node under UsernamePassword: "
                            + child.getName());
            }
        }
        return Pair.create(username, password);
    }

    /**
     * Parse the trust root parameters associated with policy update, subscription update, or AAA
     * server trust root.
     *
     * This contained configurations under either
     * PerProviderSubscription/Policy/PolicyUpdate/TrustRoot or
     * PerProviderSubscription/SubscriptionUpdate/TrustRoot or
     * PerProviderSubscription/AAAServerTrustRoot/<X+> subtree.
     *
     * @param node PPSNode representing the root of the TrustRoot subtree
     * @return Pair of Certificate URL and fingerprint
     * @throws ParsingException
     */
    private static Pair<String, byte[]> parseTrustRoot(PPSNode node)
            throws ParsingException {
        if (node.isLeaf()) {
            throw new ParsingException("Leaf node not expected for TrustRoot");
        }

        String certUrl = null;
        byte[] certFingerprint = null;
        for (PPSNode child : node.getChildren()) {
            switch (child.getName()) {
                case NODE_CERT_URL:
                    certUrl = getPpsNodeValue(child);
                    break;
                case NODE_CERT_SHA256_FINGERPRINT:
                    certFingerprint = parseHexString(getPpsNodeValue(child));
                    break;
                default:
                    throw new ParsingException("Unknown node under TrustRoot: "
                            + child.getName());
            }
        }
        return Pair.create(certUrl, certFingerprint);
    }

    /**
     * Parse configurations under PerProviderSubscription/Policy/SPExclusionList subtree.
     *
     * @param node PPSNode representing the root of the
     *             PerProviderSubscription/Policy/SPExclusionList subtree
     * @return Array of excluded SSIDs
     * @throws ParsingException
     */
    private static String[] parseSpExclusionList(PPSNode node) throws ParsingException {
        if (node.isLeaf()) {
            throw new ParsingException("Leaf node not expected for SPExclusionList");
        }
        List<String> ssidList = new ArrayList<>();
        for (PPSNode child : node.getChildren()) {
            ssidList.add(parseSpExclusionInstance(child));
        }
        return ssidList.toArray(new String[ssidList.size()]);
    }

    /**
     * Parse configurations under PerProviderSubscription/Policy/SPExclusionList/<X+> subtree.
     *
     * @param node PPSNode representing the root of the
     *             PerProviderSubscription/Policy/SPExclusionList/<X+> subtree
     * @return String
     * @throws ParsingException
     */
    private static String parseSpExclusionInstance(PPSNode node) throws ParsingException {
        if (node.isLeaf()) {
            throw new ParsingException("Leaf node not expected for SPExclusion instance");
        }
        String ssid = null;
        for (PPSNode child : node.getChildren()) {
            switch (child.getName()) {
                case NODE_SSID:
                    ssid = getPpsNodeValue(child);
                    break;
                default:
                    throw new ParsingException("Unknown node under SPExclusion instance");
            }
        }
        return ssid;
    }

    /**
     * Parse configurations under PerProviderSubscription/Policy/RequiredProtoPortTuple subtree.
     *
     * @param node PPSNode representing the root of the
     *             PerProviderSubscription/Policy/RequiredProtoPortTuple subtree
     * @return Map of IP Protocol to Port Number tuples
     * @throws ParsingException
     */
    private static Map<Integer, String> parseRequiredProtoPortTuple(PPSNode node)
            throws ParsingException {
        if (node.isLeaf()) {
            throw new ParsingException("Leaf node not expected for RequiredProtoPortTuple");
        }
        Map<Integer, String> protoPortTupleMap = new HashMap<>();
        for (PPSNode child : node.getChildren()) {
            Pair<Integer, String> protoPortTuple = parseProtoPortTuple(child);
            protoPortTupleMap.put(protoPortTuple.first, protoPortTuple.second);
        }
        return protoPortTupleMap;
    }

    /**
     * Parse configurations under PerProviderSubscription/Policy/RequiredProtoPortTuple/<X+>
     * subtree.
     *
     * @param node PPSNode representing the root of the
     *             PerProviderSubscription/Policy/RequiredProtoPortTuple/<X+> subtree
     * @return Pair of IP Protocol to Port Number tuple
     * @throws ParsingException
     */
    private static Pair<Integer, String> parseProtoPortTuple(PPSNode node)
            throws ParsingException {
        if (node.isLeaf()) {
            throw new ParsingException("Leaf node not expected for RequiredProtoPortTuple "
                    + "instance");
        }
        int proto = Integer.MIN_VALUE;
        String ports = null;
        for (PPSNode child : node.getChildren()) {
            switch (child.getName()) {
                case NODE_IP_PROTOCOL:
                    proto = parseInteger(getPpsNodeValue(child));
                    break;
                case NODE_PORT_NUMBER:
                    ports = getPpsNodeValue(child);
                    break;
                default:
                    throw new ParsingException("Unknown node under RequiredProtoPortTuple instance"
                            + child.getName());
            }
        }
        if (proto == Integer.MIN_VALUE) {
            throw new ParsingException("Missing IPProtocol field");
        }
        if (ports == null) {
            throw new ParsingException("Missing PortNumber field");
        }
        return Pair.create(proto, ports);
    }

    /**
     * Parse configurations under PerProviderSubscription/AAAServerTrustRoot subtree.
     *
     * @param node PPSNode representing the root of PerProviderSubscription/AAAServerTrustRoot
     *             subtree
     * @return Map of certificate URL with the corresponding certificate fingerprint
     * @throws ParsingException
     */
    private static Map<String, byte[]> parseAAAServerTrustRootList(PPSNode node)
            throws ParsingException {
        if (node.isLeaf()) {
            throw new ParsingException("Leaf node not expected for AAAServerTrustRoot");
        }
        Map<String, byte[]> certList = new HashMap<>();
        for (PPSNode child : node.getChildren()) {
            Pair<String, byte[]> certTuple = parseTrustRoot(child);
            certList.put(certTuple.first, certTuple.second);
        }
        return certList;
    }

    /**
     * Parse configurations under PerProviderSubscription/SubscriptionParameter subtree.
     *
     * @param node PPSNode representing the root of PerProviderSubscription/SubscriptionParameter
     *             subtree
     * @param config Instance of {@link PasspointConfiguration}
     * @throws ParsingException
     */
    private static void parseSubscriptionParameter(PPSNode node, PasspointConfiguration config)
            throws ParsingException {
        if (node.isLeaf()) {
            throw new ParsingException("Leaf node not expected for SubscriptionParameter");
        }
        for (PPSNode child : node.getChildren()) {
            switch (child.getName()) {
                case NODE_CREATION_DATE:
                    config.setSubscriptionCreationTimeInMillis(parseDate(getPpsNodeValue(child)));
                    break;
                case NODE_EXPIRATION_DATE:
                    config.setSubscriptionExpirationTimeInMillis(parseDate(getPpsNodeValue(child)));
                    break;
                case NODE_TYPE_OF_SUBSCRIPTION:
                    config.setSubscriptionType(getPpsNodeValue(child));
                    break;
                case NODE_USAGE_LIMITS:
                    parseUsageLimits(child, config);
                    break;
                default:
                    throw new ParsingException("Unknown node under SubscriptionParameter"
                            + child.getName());
            }
        }
    }

    /**
     * Parse configurations under PerProviderSubscription/SubscriptionParameter/UsageLimits
     * subtree.
     *
     * @param node PPSNode representing the root of
     *             PerProviderSubscription/SubscriptionParameter/UsageLimits subtree
     * @param config Instance of {@link PasspointConfiguration}
     * @throws ParsingException
     */
    private static void parseUsageLimits(PPSNode node, PasspointConfiguration config)
            throws ParsingException {
        if (node.isLeaf()) {
            throw new ParsingException("Leaf node not expected for UsageLimits");
        }
        for (PPSNode child : node.getChildren()) {
            switch (child.getName()) {
                case NODE_DATA_LIMIT:
                    config.setUsageLimitDataLimit(parseLong(getPpsNodeValue(child), 10));
                    break;
                case NODE_START_DATE:
                    config.setUsageLimitStartTimeInMillis(parseDate(getPpsNodeValue(child)));
                    break;
                case NODE_TIME_LIMIT:
                    config.setUsageLimitTimeLimitInMinutes(parseLong(getPpsNodeValue(child), 10));
                    break;
                case NODE_USAGE_TIME_PERIOD:
                    config.setUsageLimitUsageTimePeriodInMinutes(
                            parseLong(getPpsNodeValue(child), 10));
                    break;
                default:
                    throw new ParsingException("Unknown node under UsageLimits"
                            + child.getName());
            }
        }
    }

    /**
     * Convert a hex string to a byte array.
     *
     * @param str String containing hex values
     * @return byte[]
     * @throws ParsingException
     */
    private static byte[] parseHexString(String str) throws ParsingException {
        if ((str.length() & 1) == 1) {
            throw new ParsingException("Odd length hex string: " + str.length());
        }

        byte[] result = new byte[str.length() / 2];
        for (int i = 0; i < result.length; i++) {
          int index = i * 2;
          try {
              result[i] = (byte) Integer.parseInt(str.substring(index, index + 2), 16);
          } catch (NumberFormatException e) {
              throw new ParsingException("Invalid hex string: " + str);
          }
        }
        return result;
    }

    /**
     * Convert a date string to the number of milliseconds since January 1, 1970, 00:00:00 GMT.
     *
     * @param dateStr String in the format of yyyy-MM-dd'T'HH:mm:ss'Z'
     * @return number of milliseconds
     * @throws ParsingException
     */
    private static long parseDate(String dateStr) throws ParsingException {
        try {
            DateFormat format = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss'Z'");
            return format.parse(dateStr).getTime();
        } catch (ParseException pe) {
            throw new ParsingException("Badly formatted time: " + dateStr);
        }
    }

    /**
     * Parse an integer string.
     *
     * @param value String of integer value
     * @return int
     * @throws ParsingException
     */
    private static int parseInteger(String value) throws ParsingException {
        try {
            return Integer.parseInt(value);
        } catch (NumberFormatException e) {
            throw new ParsingException("Invalid integer value: " + value);
        }
    }

    /**
     * Parse a string representing a long integer.
     *
     * @param value String of long integer value
     * @return long
     * @throws ParsingException
     */
    private static long parseLong(String value, int radix) throws ParsingException {
        try {
            return Long.parseLong(value, radix);
        } catch (NumberFormatException e) {
            throw new ParsingException("Invalid long integer value: " + value);
        }
    }

    /**
     * Convert a List<Long> to a primitive long array long[].
     *
     * @param list List to be converted
     * @return long[]
     */
    private static long[] convertFromLongList(List<Long> list) {
        Long[] objectArray = list.toArray(new Long[list.size()]);
        long[] primitiveArray = new long[objectArray.length];
        for (int i = 0; i < objectArray.length; i++) {
            primitiveArray[i] = objectArray[i].longValue();
        }
        return primitiveArray;
    }
}
