diff options
author | Jan Nordqvist <jannq@google.com> | 2015-01-28 14:44:53 -0800 |
---|---|---|
committer | Jan Nordqvist <jannq@google.com> | 2015-01-30 15:46:45 -0800 |
commit | 71a988c8e9859244b83cd55bb6b6ee913fcaf95c (patch) | |
tree | cdb37981105d853e003efcfe9d2fd5ec26ea8165 /service/java/com/android/server/wifi/hotspot2/omadm | |
parent | 7b2caa25fb57f2d95e0d0421704c49d3af4b8e6f (diff) | |
download | android_frameworks_opt_net_wifi-71a988c8e9859244b83cd55bb6b6ee913fcaf95c.tar.gz android_frameworks_opt_net_wifi-71a988c8e9859244b83cd55bb6b6ee913fcaf95c.tar.bz2 android_frameworks_opt_net_wifi-71a988c8e9859244b83cd55bb6b6ee913fcaf95c.zip |
Credential/profile storage added, completed network matching and added HS20 Simulator.
Change-Id: I1b27dadf3d65ed0a858dee209df975180a3b90e6
Diffstat (limited to 'service/java/com/android/server/wifi/hotspot2/omadm')
12 files changed, 1505 insertions, 0 deletions
diff --git a/service/java/com/android/server/wifi/hotspot2/omadm/MOManager.java b/service/java/com/android/server/wifi/hotspot2/omadm/MOManager.java new file mode 100644 index 000000000..15dbb33bc --- /dev/null +++ b/service/java/com/android/server/wifi/hotspot2/omadm/MOManager.java @@ -0,0 +1,432 @@ +package com.android.server.wifi.hotspot2.omadm; + +import com.android.server.wifi.anqp.eap.EAP; +import com.android.server.wifi.anqp.eap.EAPMethod; +import com.android.server.wifi.anqp.eap.ExpandedEAPMethod; +import com.android.server.wifi.anqp.eap.InnerAuthEAP; +import com.android.server.wifi.anqp.eap.NonEAPInnerAuth; +import com.android.server.wifi.hotspot2.pps.Credential; +import com.android.server.wifi.hotspot2.pps.HomeSP; + +import org.xml.sax.SAXException; + +import java.io.BufferedInputStream; +import java.io.BufferedOutputStream; +import java.io.BufferedReader; +import java.io.File; +import java.io.FileInputStream; +import java.io.FileOutputStream; +import java.io.FileReader; +import java.io.IOException; +import java.io.InputStream; +import java.text.DateFormat; +import java.text.ParseException; +import java.text.SimpleDateFormat; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Date; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.TimeZone; + +/** + * Handles provisioning of PerProviderSubscription data. + */ +public class MOManager { + private final File mPpsFile; + private final Map<String, HomeSP> mSPs; + + public MOManager(File ppsFile) throws IOException { + mPpsFile = ppsFile; + mSPs = new HashMap<String, HomeSP>(); + } + + public File getPpsFile() { + return mPpsFile; + } + + public List<HomeSP> loadAllSPs() throws IOException { + List<MOTree> trees = new ArrayList<MOTree>(); + List<HomeSP> sps = new ArrayList<HomeSP>(); + + if (!mPpsFile.exists()) { + return sps; + } + + BufferedInputStream in = null; + try { + in = new BufferedInputStream(new FileInputStream(mPpsFile)); + while (in.available() > 0) { + MOTree tree = MOTree.unmarshal(in); + if (tree != null) { + trees.add(tree); + } else { + break; + } + } + } finally { + if (in != null) { + try { + in.close(); + } catch (IOException ioe) { + /**/ + } + } + } + + for (MOTree moTree : trees) { + List<HomeSP> sp = buildSPs(moTree); + if (sp != null) { + sps.addAll(sp); + } + } + + for (HomeSP sp : sps) { + if (mSPs.put(sp.getFQDN(), sp) != null) { + throw new OMAException("Multiple SPs for FQDN '" + sp.getFQDN() + "'"); + } + } + return sps; + } + + public HomeSP addSP(InputStream xmlIn) throws IOException, SAXException { + OMAParser omaParser = new OMAParser(); + MOTree tree = omaParser.parse(xmlIn, OMAConstants.LOC_PPS + ":1.0"); + List<HomeSP> spList = buildSPs(tree); + if (spList.size() != 1) { + throw new OMAException("Expected exactly one HomeSP, got " + spList.size()); + } + HomeSP sp = spList.iterator().next(); + String fqdn = sp.getFQDN(); + if (mSPs.put(fqdn, sp) != null) { + throw new OMAException("SP " + fqdn + " already exists"); + } + + BufferedOutputStream out = null; + try { + out = new BufferedOutputStream(new FileOutputStream(mPpsFile, true)); + tree.marshal(out); + out.flush(); + } finally { + if (out != null) { + try { + out.close(); + } catch (IOException ioe) { + /**/ + } + } + } + + return sp; + } + + private static final DateFormat DTFormat = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss'Z'"); + + static { + DTFormat.setTimeZone(TimeZone.getTimeZone("UTC")); + } + + public static final String TAG_AAAServerTrustRoot = "AAAServerTrustRoot"; + public static final String TAG_AbleToShare = "AbleToShare"; + public static final String TAG_CertificateType = "CertificateType"; + public static final String TAG_CertSHA256Fingerprint = "CertSHA256Fingerprint"; + public static final String TAG_CertURL = "CertURL"; + public static final String TAG_CheckAAAServerCertStatus = "CheckAAAServerCertStatus"; + public static final String TAG_Country = "Country"; + public static final String TAG_CreationDate = "CreationDate"; + public static final String TAG_Credential = "Credential"; + public static final String TAG_CredentialPriority = "CredentialPriority"; + public static final String TAG_DataLimit = "DataLimit"; + public static final String TAG_DigitalCertificate = "DigitalCertificate"; + public static final String TAG_DLBandwidth = "DLBandwidth"; + public static final String TAG_EAPMethod = "EAPMethod"; + public static final String TAG_EAPType = "EAPType"; + public static final String TAG_ExpirationDate = "ExpirationDate"; + public static final String TAG_Extension = "Extension"; + public static final String TAG_FQDN = "FQDN"; + public static final String TAG_FQDN_Match = "FQDN_Match"; + public static final String TAG_FriendlyName = "FriendlyName"; + public static final String TAG_HESSID = "HESSID"; + public static final String TAG_HomeOI = "HomeOI"; + public static final String TAG_HomeOIList = "HomeOIList"; + public static final String TAG_HomeOIRequired = "HomeOIRequired"; + public static final String TAG_HomeSP = "HomeSP"; + public static final String TAG_IconURL = "IconURL"; + public static final String TAG_IMSI = "IMSI"; + public static final String TAG_InnerEAPType = "InnerEAPType"; + public static final String TAG_InnerMethod = "InnerMethod"; + public static final String TAG_InnerVendorID = "InnerVendorID"; + public static final String TAG_InnerVendorType = "InnerVendorType"; + public static final String TAG_IPProtocol = "IPProtocol"; + public static final String TAG_MachineManaged = "MachineManaged"; + public static final String TAG_MaximumBSSLoadValue = "MaximumBSSLoadValue"; + public static final String TAG_MinBackhaulThreshold = "MinBackhaulThreshold"; + public static final String TAG_NetworkID = "NetworkID"; + public static final String TAG_NetworkType = "NetworkType"; + public static final String TAG_Other = "Other"; + public static final String TAG_OtherHomePartners = "OtherHomePartners"; + public static final String TAG_Password = "Password"; + public static final String TAG_PerProviderSubscription = "PerProviderSubscription"; + public static final String TAG_Policy = "Policy"; + public static final String TAG_PolicyUpdate = "PolicyUpdate"; + public static final String TAG_PortNumber = "PortNumber"; + public static final String TAG_PreferredRoamingPartnerList = "PreferredRoamingPartnerList"; + public static final String TAG_Priority = "Priority"; + public static final String TAG_Realm = "Realm"; + public static final String TAG_RequiredProtoPortTuple = "RequiredProtoPortTuple"; + public static final String TAG_Restriction = "Restriction"; + public static final String TAG_RoamingConsortiumOI = "RoamingConsortiumOI"; + public static final String TAG_SIM = "SIM"; + public static final String TAG_SoftTokenApp = "SoftTokenApp"; + public static final String TAG_SPExclusionList = "SPExclusionList"; + public static final String TAG_SSID = "SSID"; + public static final String TAG_StartDate = "StartDate"; + public static final String TAG_SubscriptionParameters = "SubscriptionParameters"; + public static final String TAG_SubscriptionUpdate = "SubscriptionUpdate"; + public static final String TAG_TimeLimit = "TimeLimit"; + public static final String TAG_TrustRoot = "TrustRoot"; + public static final String TAG_TypeOfSubscription = "TypeOfSubscription"; + public static final String TAG_ULBandwidth = "ULBandwidth"; + public static final String TAG_UpdateIdentifier = "UpdateIdentifier"; + public static final String TAG_UpdateInterval = "UpdateInterval"; + public static final String TAG_UpdateMethod = "UpdateMethod"; + public static final String TAG_URI = "URI"; + public static final String TAG_UsageLimits = "UsageLimits"; + public static final String TAG_UsageTimePeriod = "UsageTimePeriod"; + public static final String TAG_Username = "Username"; + public static final String TAG_UsernamePassword = "UsernamePassword"; + public static final String TAG_VendorId = "VendorId"; + public static final String TAG_VendorType = "VendorType"; + + private static List<HomeSP> buildSPs(MOTree moTree) throws OMAException { + List<String> spPath = Arrays.asList(TAG_PerProviderSubscription); + OMAConstructed spList = moTree.getRoot().getListValue(spPath.iterator()); + + List<HomeSP> homeSPs = new ArrayList<HomeSP>(); + + for (OMANode spRoot : spList.getChildren()) { + homeSPs.add(buildHomeSP(spRoot)); + } + + return homeSPs; + } + + private static HomeSP buildHomeSP(OMANode ppsRoot) throws OMAException { + OMANode spRoot = ppsRoot.getChild(TAG_HomeSP); + + String fqdn = spRoot.getScalarValue(Arrays.asList(TAG_FQDN).iterator()); + String friendlyName = spRoot.getScalarValue(Arrays.asList(TAG_FriendlyName).iterator()); + System.out.println("FQDN: " + fqdn + ", friendly: " + friendlyName); + String iconURL = spRoot.getScalarValue(Arrays.asList(TAG_IconURL).iterator()); + + Set<Long> roamingConsortiums = new HashSet<Long>(); + String oiString = spRoot.getScalarValue(Arrays.asList(TAG_RoamingConsortiumOI).iterator()); + if (oiString != null) { + for (String oi : oiString.split(",")) { + roamingConsortiums.add(Long.parseLong(oi.trim(), 16)); + } + } + + Map<String, String> ssids = new HashMap<String, String>(); + + OMANode ssidListNode = spRoot.getListValue(Arrays.asList(TAG_NetworkID).iterator()); + if (ssidListNode != null) { + for (OMANode ssidRoot : ssidListNode.getChildren()) { + OMANode hessidNode = ssidRoot.getChild(TAG_HESSID); + ssids.put(ssidRoot.getChild(TAG_SSID).getValue(), + hessidNode != null ? hessidNode.getValue() : null); + } + } + + Set<Long> matchAnyOIs = new HashSet<Long>(); + List<Long> matchAllOIs = new ArrayList<Long>(); + OMANode homeOIListNode = spRoot.getListValue(Arrays.asList(TAG_HomeOIList).iterator()); + if (homeOIListNode != null) { + for (OMANode homeOIRoot : homeOIListNode.getChildren()) { + String homeOI = homeOIRoot.getChild(TAG_HomeOI).getValue(); + if (Boolean.parseBoolean(homeOIRoot.getChild(TAG_HomeOIRequired).getValue())) { + matchAllOIs.add(Long.parseLong(homeOI, 16)); + } else { + matchAnyOIs.add(Long.parseLong(homeOI, 16)); + } + } + } + + Set<String> otherHomePartners = new HashSet<String>(); + OMANode otherListNode = + spRoot.getListValue(Arrays.asList(TAG_OtherHomePartners).iterator()); + if (otherListNode != null) { + for (OMANode fqdnNode : otherListNode.getChildren()) { + otherHomePartners.add(fqdnNode.getChild(TAG_FQDN).getValue()); + } + } + + Credential credential = buildCredential(ppsRoot.getChild(TAG_Credential)); + + return new HomeSP(ssids, fqdn, roamingConsortiums, otherHomePartners, + matchAnyOIs, matchAllOIs, friendlyName, iconURL, credential); + } + + private static Credential buildCredential(OMANode credNode) throws OMAException { + long ctime = getTime(credNode.getChild(TAG_CreationDate)); + long expTime = getTime(credNode.getChild(TAG_ExpirationDate)); + String realm = getString(credNode.getChild(TAG_Realm)); + boolean checkAAACert = getBoolean(credNode.getChild(TAG_CheckAAAServerCertStatus)); + + OMANode unNode = credNode.getChild(TAG_UsernamePassword); + OMANode certNode = credNode.getChild(TAG_DigitalCertificate); + OMANode simNode = credNode.getChild(TAG_SIM); + + int alternatives = 0; + alternatives += unNode != null ? 1 : 0; + alternatives += certNode != null ? 1 : 0; + alternatives += simNode != null ? 1 : 0; + if (alternatives != 1) { + throw new OMAException("Expected exactly one credential type, got " + alternatives); + } + + if (unNode != null) { + String userName = unNode.getChild(TAG_Username).getValue(); + String password = unNode.getChild(TAG_Password).getValue(); + boolean machineManaged = getBoolean(unNode.getChild(TAG_MachineManaged)); + String softTokenApp = getString(unNode.getChild(TAG_SoftTokenApp)); + boolean ableToShare = getBoolean(unNode.getChild(TAG_AbleToShare)); + + OMANode eapMethodNode = unNode.getChild(TAG_EAPMethod); + EAP.EAPMethodID eapMethodID = + EAP.mapEAPMethod(getInteger(eapMethodNode.getChild(TAG_EAPType))); + if (eapMethodID == null) { + throw new OMAException("Unknown EAP method"); + } + + Long vid = getOptionalInteger(eapMethodNode.getChild(TAG_VendorId)); + Long vtype = getOptionalInteger(eapMethodNode.getChild(TAG_VendorType)); + Long innerEAPType = getOptionalInteger(eapMethodNode.getChild(TAG_InnerEAPType)); + EAP.EAPMethodID innerEAPMethod = null; + if (innerEAPType != null) { + innerEAPMethod = EAP.mapEAPMethod(innerEAPType.intValue()); + if (innerEAPMethod == null) { + throw new OMAException("Bad inner EAP method: " + innerEAPType); + } + } + + Long innerVid = getOptionalInteger(eapMethodNode.getChild(TAG_InnerVendorID)); + Long innerVtype = getOptionalInteger(eapMethodNode.getChild(TAG_InnerVendorType)); + String innerNonEAPMethod = getString(eapMethodNode.getChild(TAG_InnerMethod)); + + EAPMethod eapMethod; + if (innerEAPMethod != null) { + eapMethod = new EAPMethod(eapMethodID, new InnerAuthEAP(innerEAPMethod)); + } else if (vid != null) { + eapMethod = new EAPMethod(eapMethodID, + new ExpandedEAPMethod(EAP.AuthInfoID.ExpandedEAPMethod, + vid.intValue(), vtype)); + } else if (innerVid != null) { + eapMethod = + new EAPMethod(eapMethodID, new ExpandedEAPMethod(EAP.AuthInfoID + .ExpandedInnerEAPMethod, innerVid.intValue(), innerVtype)); + } else if (innerNonEAPMethod != null) { + eapMethod = new EAPMethod(eapMethodID, new NonEAPInnerAuth(innerNonEAPMethod)); + } else { + throw new OMAException("Incomplete set of EAP parameters"); + } + + return new Credential(ctime, expTime, realm, checkAAACert, eapMethod, userName, + password, machineManaged, softTokenApp, ableToShare); + } + if (certNode != null) { + String certTypeString = getString(certNode.getChild(TAG_CertificateType)); + byte[] fingerPrint = getOctets(certNode.getChild(TAG_CertSHA256Fingerprint)); + + EAPMethod eapMethod = new EAPMethod(EAP.EAPMethodID.EAP_TLS, null); + + return new Credential(ctime, expTime, realm, checkAAACert, eapMethod, + Credential.mapCertType(certTypeString), fingerPrint); + } + if (simNode != null) { + + String imsi = getString(simNode.getChild(TAG_IMSI)); + EAPMethod eapMethod = + new EAPMethod(EAP.mapEAPMethod(getInteger(simNode.getChild(TAG_EAPType))), + null); + + return new Credential(ctime, expTime, realm, checkAAACert, eapMethod, imsi); + } + throw new OMAException("Missing credential parameters"); + } + + private static boolean getBoolean(OMANode boolNode) { + return boolNode != null && Boolean.parseBoolean(boolNode.getValue()); + } + + private static String getString(OMANode stringNode) { + return stringNode != null ? stringNode.getValue() : null; + } + + private static int getInteger(OMANode intNode) throws OMAException { + if (intNode == null) { + throw new OMAException("Missing integer value"); + } + try { + return Integer.parseInt(intNode.getValue()); + } catch (NumberFormatException nfe) { + throw new OMAException("Invalid integer: " + intNode.getValue()); + } + } + + private static Long getOptionalInteger(OMANode intNode) throws OMAException { + if (intNode == null) { + return null; + } + try { + return Long.parseLong(intNode.getValue()); + } catch (NumberFormatException nfe) { + throw new OMAException("Invalid integer: " + intNode.getValue()); + } + } + + private static long getTime(OMANode timeNode) throws OMAException { + if (timeNode == null) { + return -1; + } + String timeText = timeNode.getValue(); + try { + Date date = DTFormat.parse(timeText); + return date.getTime(); + } catch (ParseException pe) { + throw new OMAException("Badly formatted time: " + timeText); + } + } + + private static byte[] getOctets(OMANode octetNode) throws OMAException { + if (octetNode == null) { + throw new OMAException("Missing byte value"); + } + String text = octetNode.getValue(); + if ((text.length() & 1) == 1) { + throw new OMAException("Odd length octet value: " + text); + } + byte[] octets = new byte[text.length() / 2]; + for (int n = 0; n < octets.length; n++) { + octets[n] = (byte) (fromHex(text.charAt(n * 2)) << Byte.SIZE | + fromHex(text.charAt(n * 2 + 1))); + } + return octets; + } + + private static int fromHex(char ch) throws OMAException { + if (ch <= '9' && ch >= '0') { + return ch - '0'; + } else if (ch >= 'a' && ch <= 'f') { + return ch + 10 - 'a'; + } else if (ch <= 'F' && ch >= 'A') { + return ch + 10 - 'A'; + } else { + throw new OMAException("Bad hex-character: " + ch); + } + } + +} diff --git a/service/java/com/android/server/wifi/hotspot2/omadm/MOTree.java b/service/java/com/android/server/wifi/hotspot2/omadm/MOTree.java new file mode 100644 index 000000000..bb6b16d50 --- /dev/null +++ b/service/java/com/android/server/wifi/hotspot2/omadm/MOTree.java @@ -0,0 +1,218 @@ +package com.android.server.wifi.hotspot2.omadm; + +import org.xml.sax.SAXException; + +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.nio.charset.StandardCharsets; +import java.util.*; + +public class MOTree { + private static final String NodeTag = "Node"; + private static final String NodeNameTag = "NodeName"; + private static final String PathTag = "Path"; + private static final String ValueTag = "Value"; + private static final String RTPropTag = "RTProperties"; + private static final String TypeTag = "Type"; + private static final String DDFNameTag = "DDFName"; + + private final String mUrn; + private final String mDtdRev; + private final OMAConstructed mRoot; + + public MOTree(XMLNode node, String urn) throws IOException, SAXException { + Iterator<XMLNode> children = node.getChildren().iterator(); + + String dtdRev = null; + + while (children.hasNext()) { + XMLNode child = children.next(); + if (child.getTag().equals(OMAConstants.SyncMLVersionTag)) { + dtdRev = child.getText(); + children.remove(); + break; + } + } + + mUrn = urn; + mDtdRev = dtdRev; + + mRoot = new OMAConstructed(null, ".", null); + + for (XMLNode child : node.getChildren()) { + buildNode(mRoot, child); + } + } + + private MOTree(String urn, String rev, OMAConstructed root) { + mUrn = urn; + mDtdRev = rev; + mRoot = root; + } + + private static class NodeData { + private final String mName; + private String mPath; + private String mValue; + + private NodeData(String name) { + mName = name; + } + + private void setPath(String path) { + mPath = path; + } + + private void setValue(String value) { + mValue = value; + } + + public String getName() { + return mName; + } + + public String getPath() { + return mPath; + } + + public String getValue() { + return mValue; + } + } + + private static void buildNode(OMANode parent, XMLNode node) throws IOException { + if (!node.getTag().equals(NodeTag)) + throw new IOException("Node is a '" + node.getTag() + "' instead of a 'Node'"); + + Map<String, XMLNode> checkMap = new HashMap<String, XMLNode>(3); + String context = null; + List<NodeData> values = new ArrayList<NodeData>(); + List<XMLNode> children = new ArrayList<XMLNode>(); + + NodeData curValue = null; + + for (XMLNode child : node.getChildren()) { + XMLNode old = checkMap.put(child.getTag(), child); + + if (child.getTag().equals(NodeNameTag)) { + if (curValue != null) + throw new IOException(NodeNameTag + " not expected"); + curValue = new NodeData(child.getText()); + + } else if (child.getTag().equals(PathTag)) { + if (curValue == null || curValue.getPath() != null) + throw new IOException(PathTag + " not expected"); + curValue.setPath(child.getText()); + + } else if (child.getTag().equals(ValueTag)) { + if (!children.isEmpty()) + throw new IOException(ValueTag + " in constructed node"); + if (curValue == null || curValue.getValue() != null) + throw new IOException(ValueTag + " not expected"); + curValue.setValue(child.getText()); + values.add(curValue); + curValue = null; + + } else if (child.getTag().equals(RTPropTag)) { + if (old != null) + throw new IOException("Duplicate " + RTPropTag); + XMLNode typeNode = getNextNode(child, TypeTag); + XMLNode ddfName = getNextNode(typeNode, DDFNameTag); + context = ddfName.getText(); + if (context == null) + throw new IOException("No text in " + DDFNameTag); + + } else if (child.getTag().equals(NodeTag)) { + if (!values.isEmpty()) + throw new IOException("Scalar node " + node.getText() + " has Node child"); + children.add(child); + + } + } + + if (values.isEmpty()) { + if (curValue == null) + throw new IOException("Missing name"); + + OMANode subNode = parent.addChild(curValue.getName(), + context, null, curValue.getPath()); + + for (XMLNode child : children) { + buildNode(subNode, child); + } + } else { + if (!children.isEmpty()) + throw new IOException("Got both sub nodes and value(s)"); + + for (NodeData nodeData : values) { + parent.addChild(nodeData.getName(), context, + nodeData.getValue(), nodeData.getPath()); + } + } + } + + private static XMLNode getNextNode(XMLNode node, String tag) throws IOException { + if (node == null) + throw new IOException("No node for " + tag); + if (node.getChildren().size() != 1) + throw new IOException("Expected " + node.getTag() + " to have exactly one child"); + XMLNode child = node.getChildren().iterator().next(); + if (!child.getTag().equals(tag)) + throw new IOException("Expected " + node.getTag() + " to have child '" + tag + + "' instead of '" + child.getTag() + "'"); + return child; + } + + public String getUrn() { + return mUrn; + } + + public String getDtdRev() { + return mDtdRev; + } + + public OMAConstructed getRoot() { + return mRoot; + } + + @Override + public String toString() { + StringBuilder sb = new StringBuilder(); + sb.append("MO Tree v").append(mDtdRev).append(", urn ").append(mUrn).append(")\n"); + sb.append(mRoot); + + return sb.toString(); + } + + public void marshal(OutputStream out) throws IOException { + out.write("tree ".getBytes(StandardCharsets.UTF_8)); + OMAConstants.serializeString(mDtdRev, out); + out.write(String.format("(%s)\n", mUrn).getBytes(StandardCharsets.UTF_8)); + mRoot.marshal(out, 0); + } + + public static MOTree unmarshal(InputStream in) throws IOException { + boolean strip = true; + StringBuilder tree = new StringBuilder(); + for (; ; ) { + int octet = in.read(); + if (octet < 0) { + return null; + } else if (octet > ' ') { + tree.append((char) octet); + strip = false; + } else if (!strip) { + break; + } + } + if (!tree.toString().equals("tree")) + throw new IOException("Not a tree: " + tree); + + String version = OMAConstants.deserializeString(in); + String urn = OMAConstants.readURN(in); + OMAConstructed root = OMANode.unmarshal(in); + + return new MOTree(urn, version, root); + } +} diff --git a/service/java/com/android/server/wifi/hotspot2/omadm/NodeAttribute.java b/service/java/com/android/server/wifi/hotspot2/omadm/NodeAttribute.java new file mode 100644 index 000000000..1fc1adf7a --- /dev/null +++ b/service/java/com/android/server/wifi/hotspot2/omadm/NodeAttribute.java @@ -0,0 +1,30 @@ +package com.android.server.wifi.hotspot2.omadm; + +public class NodeAttribute { + private final String mName; + private final String mType; + private final String mValue; + + public NodeAttribute(String name, String type, String value) { + mName = name; + mType = type; + mValue = value; + } + + public String getName() { + return mName; + } + + public String getValue() { + return mValue; + } + + public String getType() { + return mType; + } + + @Override + public String toString() { + return String.format("%s (%s) = '%s'", mName, mType, mValue); + } +} diff --git a/service/java/com/android/server/wifi/hotspot2/omadm/OMAConstants.java b/service/java/com/android/server/wifi/hotspot2/omadm/OMAConstants.java new file mode 100644 index 000000000..1a42341ad --- /dev/null +++ b/service/java/com/android/server/wifi/hotspot2/omadm/OMAConstants.java @@ -0,0 +1,96 @@ +package com.android.server.wifi.hotspot2.omadm; + +import java.io.EOFException; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.nio.charset.StandardCharsets; +import java.util.Arrays; +import java.util.HashSet; +import java.util.Set; + +public class OMAConstants { + private OMAConstants() { + } + + public static final String TAG_PostDevData = "spp:sppPostDevData"; + public static final String TAG_SupportedVersions = "spp:supportedSPPVersions"; + public static final String TAG_SupportedMOs = "spp:supportedMOList"; + + public static final String TAG_MO_Add = "spp:addMO"; + public static final String TAG_MO_Container = "spp:moContainer"; + + public static final String ATTR_URN = "spp:moURN"; + + // Following strings excludes the trailing version number (e.g. :1.0) + public static final String LOC_PPS = "urn:wfa:mo:hotspot2dot0-perprovidersubscription"; + public static final String LOC_DEVINFO = + "urn:oma:mo:oma-dm-devinfo:1.0 urn:oma:mo:oma-dm-devdetail"; + public static final String LOC_DEVDETAIL = "urn:wfa:mo-ext:hotspot2dot0-devdetail-ext"; + + public static final String SyncMLVersionTag = "VerDTD"; + public static final String RequiredSyncMLVersion = "1.2"; + + private static final Set<String> sMOContainers = new HashSet<String>(); + + static { + sMOContainers.add(TAG_MO_Add); + sMOContainers.add(TAG_MO_Container); + } + + public static boolean isMOContainer(String tag) { + return sMOContainers.contains(tag); + } + + private static final byte[] INDENT = new byte[1024]; + + static { + Arrays.fill(INDENT, (byte) ' '); + } + + public static void serializeString(String s, OutputStream out) throws IOException { + byte[] octets = s.getBytes(StandardCharsets.UTF_8); + byte[] prefix = String.format("%x:", octets.length).getBytes(StandardCharsets.UTF_8); + out.write(prefix); + out.write(octets); + } + + public static void indent(int level, OutputStream out) throws IOException { + out.write(INDENT, 0, level); + } + + public static String deserializeString(InputStream in) throws IOException { + StringBuilder prefix = new StringBuilder(); + for (; ; ) { + byte b = (byte) in.read(); + if (b == '.') + return null; + else if (b == ':') + break; + else if (b > ' ') + prefix.append((char) b); + } + int length = Integer.parseInt(prefix.toString(), 16); + byte[] octets = new byte[length]; + int offset = 0; + while (offset < octets.length) { + int amount = in.read(octets, offset, octets.length - offset); + if (amount <= 0) + throw new EOFException(); + offset += amount; + } + return new String(octets, StandardCharsets.UTF_8); + } + + public static String readURN(InputStream in) throws IOException { + StringBuilder urn = new StringBuilder(); + + for (; ; ) { + byte b = (byte) in.read(); + if (b == ')') + break; + urn.append((char) b); + } + return urn.toString(); + } +} diff --git a/service/java/com/android/server/wifi/hotspot2/omadm/OMAConstructed.java b/service/java/com/android/server/wifi/hotspot2/omadm/OMAConstructed.java new file mode 100644 index 000000000..6646fcfe8 --- /dev/null +++ b/service/java/com/android/server/wifi/hotspot2/omadm/OMAConstructed.java @@ -0,0 +1,114 @@ +package com.android.server.wifi.hotspot2.omadm; + +import java.io.IOException; +import java.io.OutputStream; +import java.nio.charset.StandardCharsets; +import java.util.*; + +/** + * Created by jannq on 1/12/15. + */ +public class OMAConstructed extends OMANode { + private final Map<String, OMANode> m_children; + + public OMAConstructed(OMANode parent, String name, String context) { + super(parent, name, context); + m_children = new HashMap<String, OMANode>(); + } + + @Override + public OMANode addChild(String name, String context, String value, String pathString) throws IOException { + if (pathString == null) { + OMANode child = value != null ? + new OMAScalar(this, name, context, value) : + new OMAConstructed(this, name, context); + m_children.put(name, child); + return child; + } else { + OMANode target = this; + while (target.getParent() != null) + target = target.getParent(); + + for (String element : pathString.split("/")) { + target = target.getChild(element); + if (target == null) + throw new IOException("No child node '" + element + "' in " + getPathString()); + else if (target.isLeaf()) + throw new IOException("Cannot add child to leaf node: " + getPathString()); + } + return target.addChild(name, context, value, null); + } + } + + public String getScalarValue(Iterator<String> path) throws OMAException { + if (!path.hasNext()) { + throw new OMAException("Path too short for " + getPathString()); + } + String tag = path.next(); + OMANode child = m_children.get(tag); + if (child != null) { + return child.getScalarValue(path); + } else { + return null; + } + } + + @Override + public OMAConstructed getListValue(Iterator<String> path) throws OMAException { + if (!path.hasNext()) { + return this; + } + String tag = path.next(); + OMANode child = m_children.get(tag); + if (child != null) { + return child.getListValue(path); + } else { + return null; + } + } + + @Override + public boolean isLeaf() { + return false; + } + + @Override + public Collection<OMANode> getChildren() { + return Collections.unmodifiableCollection(m_children.values()); + } + + public OMANode getChild(String name) { + return m_children.get(name); + } + + @Override + public String getValue() { + throw new UnsupportedOperationException(); + } + + @Override + public void toString(StringBuilder sb, int level) { + if (getContext() != null) { + sb.append(getPathString()).append(" (").append(getContext()).append(')').append('\n'); + } + + for (OMANode node : m_children.values()) + node.toString(sb, level + 1); + } + + @Override + public void marshal(OutputStream out, int level) throws IOException { + OMAConstants.indent(level, out); + OMAConstants.serializeString(getName(), out); + if (getContext() != null) { + out.write(String.format("(%s)", getContext()).getBytes(StandardCharsets.UTF_8)); + } + out.write(new byte[] { '+', '\n' }); + + for (OMANode child : m_children.values()) { + child.marshal(out, level + 1); + } + OMAConstants.indent(level, out); + out.write(".\n".getBytes(StandardCharsets.UTF_8)); + } +} diff --git a/service/java/com/android/server/wifi/hotspot2/omadm/OMAException.java b/service/java/com/android/server/wifi/hotspot2/omadm/OMAException.java new file mode 100644 index 000000000..5f8cd112c --- /dev/null +++ b/service/java/com/android/server/wifi/hotspot2/omadm/OMAException.java @@ -0,0 +1,9 @@ +package com.android.server.wifi.hotspot2.omadm; + +import java.io.IOException; + +public class OMAException extends IOException { + public OMAException(String message) { + super(message); + } +} diff --git a/service/java/com/android/server/wifi/hotspot2/omadm/OMANode.java b/service/java/com/android/server/wifi/hotspot2/omadm/OMANode.java new file mode 100644 index 000000000..e07f92365 --- /dev/null +++ b/service/java/com/android/server/wifi/hotspot2/omadm/OMANode.java @@ -0,0 +1,118 @@ +package com.android.server.wifi.hotspot2.omadm; + +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.util.Collection; +import java.util.Iterator; +import java.util.LinkedList; +import java.util.List; + +public abstract class OMANode { + private final OMANode mParent; + private final String mName; + private final String mContext; + + protected OMANode(OMANode parent, String name, String context) { + mParent = parent; + mName = name; + mContext = context; + } + + public OMANode getParent() { + return mParent; + } + + public String getName() { + return mName; + } + + public String getContext() { + return mContext; + } + + public List<String> getPath() { + LinkedList<String> path = new LinkedList<String>(); + for (OMANode node = this; node.getParent() != null; node = node.getParent()) + path.addFirst(node.getName()); + return path; + } + + public String getPathString() { + StringBuilder sb = new StringBuilder(); + for (String element : getPath()) { + sb.append('/').append(element); + } + return sb.toString(); + } + + public abstract String getScalarValue(Iterator<String> path) throws OMAException; + + public abstract OMAConstructed getListValue(Iterator<String> path) throws OMAException; + + public abstract boolean isLeaf(); + + public abstract Collection<OMANode> getChildren(); + + public abstract OMANode getChild(String name); + + public abstract String getValue(); + + public abstract OMANode addChild(String name, String context, String value, String path) + throws IOException; + + public abstract void marshal(OutputStream out, int level) throws IOException; + + public abstract void toString(StringBuilder sb, int level); + + @Override + public String toString() { + StringBuilder sb = new StringBuilder(); + toString(sb, 0); + return sb.toString(); + } + + public static OMAConstructed unmarshal(InputStream in) throws IOException { + OMANode node = buildNode(in, null); + if (node == null || node.isLeaf()) + throw new IOException("Bad OMA tree"); + unmarshal(in, (OMAConstructed) node); + return (OMAConstructed) node; + } + + private static void unmarshal(InputStream in, OMAConstructed parent) throws IOException { + for (; ; ) { + OMANode node = buildNode(in, parent); + if (node == null) + return; + else if (!node.isLeaf()) + unmarshal(in, (OMAConstructed) node); + } + } + + private static OMANode buildNode(InputStream in, OMAConstructed parent) throws IOException { + String name = OMAConstants.deserializeString(in); + if (name == null) + return null; + + String urn = null; + int next = in.read(); + if (next == '(') { + urn = OMAConstants.readURN(in); + next = in.read(); + } + + if (next == '=') { + String value = OMAConstants.deserializeString(in); + return parent.addChild(name, urn, value, null); + } else if (next == '+') { + if (parent != null) + return parent.addChild(name, urn, null, null); + else + return new OMAConstructed(null, name, urn); + } + else { + throw new IOException("Parse error: expected = or + after node name"); + } + } +} diff --git a/service/java/com/android/server/wifi/hotspot2/omadm/OMAParser.java b/service/java/com/android/server/wifi/hotspot2/omadm/OMAParser.java new file mode 100644 index 000000000..2f352f337 --- /dev/null +++ b/service/java/com/android/server/wifi/hotspot2/omadm/OMAParser.java @@ -0,0 +1,83 @@ +package com.android.server.wifi.hotspot2.omadm; + +import org.xml.sax.Attributes; +import org.xml.sax.InputSource; +import org.xml.sax.SAXException; +import org.xml.sax.helpers.DefaultHandler; + +import javax.xml.parsers.ParserConfigurationException; +import javax.xml.parsers.SAXParser; +import javax.xml.parsers.SAXParserFactory; +import java.io.*; + +/** + * Parses an OMA-DM XML tree. + */ +public class OMAParser extends DefaultHandler +{ + private XMLNode mRoot; + private XMLNode mCurrent; + + public MOTree parse( String text, String urn ) throws IOException, SAXException + { + try + { + SAXParser parser = SAXParserFactory.newInstance().newSAXParser(); + parser.parse( new InputSource( new StringReader( text ) ), this ); + return new MOTree(mRoot, urn ); + } + catch ( ParserConfigurationException pce ) + { + throw new SAXException( pce ); + } + } + + public MOTree parse( InputStream in, String urn ) throws IOException, SAXException + { + try + { + SAXParser parser = SAXParserFactory.newInstance().newSAXParser(); + parser.parse( new InputSource( in ), this ); + return new MOTree(mRoot, urn ); + } + catch ( ParserConfigurationException pce ) + { + throw new SAXException( pce ); + } + } + + @Override + public void startElement( String uri, String localName, String qName, Attributes attributes ) throws SAXException + { + XMLNode parent = mCurrent; + + mCurrent = new XMLNode(mCurrent, qName, attributes ); + + if ( mRoot == null ) + mRoot = mCurrent; + else + parent.addChild(mCurrent); + } + + @Override + public void endElement( String uri, String localName, String qName ) throws SAXException + { + if ( ! qName.equals(mCurrent.getTag()) ) + throw new SAXException( "End tag '" + qName + "' doesn't match current node: " + mCurrent); + + try { + mCurrent.close(); + } + catch ( IOException ioe ) { + throw new SAXException("Failed to close element", ioe); + } + + mCurrent = mCurrent.getParent(); + } + + @Override + public void characters( char[] ch, int start, int length ) throws SAXException + { + mCurrent.addText(ch, start, length); + } +} diff --git a/service/java/com/android/server/wifi/hotspot2/omadm/OMAScalar.java b/service/java/com/android/server/wifi/hotspot2/omadm/OMAScalar.java new file mode 100644 index 000000000..94a121e45 --- /dev/null +++ b/service/java/com/android/server/wifi/hotspot2/omadm/OMAScalar.java @@ -0,0 +1,68 @@ +package com.android.server.wifi.hotspot2.omadm; + +import java.io.IOException; +import java.io.OutputStream; +import java.util.Collection; +import java.util.Iterator; + +public class OMAScalar extends OMANode { + private final String mValue; + + public OMAScalar(OMANode parent, String name, String context, String value) { + super(parent, name, context); + mValue = value; + } + + public String getScalarValue(Iterator<String> path) throws OMAException { + return mValue; + } + + @Override + public OMAConstructed getListValue(Iterator<String> path) throws OMAException { + throw new OMAException("Scalar encountered in list path: " + getPathString()); + } + + @Override + public boolean isLeaf() { + return true; + } + + @Override + public Collection<OMANode> getChildren() { + throw new UnsupportedOperationException(); + } + + @Override + public String getValue() { + return mValue; + } + + @Override + public OMANode getChild(String name) { + throw new UnsupportedOperationException(); + } + + @Override + public OMANode addChild(String name, String context, String value, String path) + throws IOException { + throw new UnsupportedOperationException(); + } + + @Override + public void toString(StringBuilder sb, int level) { + sb.append(getPathString()).append('=').append(mValue); + if (getContext() != null) { + sb.append(" (").append(getContext()).append(')'); + } + sb.append('\n'); + } + + @Override + public void marshal(OutputStream out, int level) throws IOException { + OMAConstants.indent(level, out); + OMAConstants.serializeString(getName(), out); + out.write((byte) '='); + OMAConstants.serializeString(getValue(), out); + out.write((byte) '\n'); + } +} diff --git a/service/java/com/android/server/wifi/hotspot2/omadm/RequestDetail.java b/service/java/com/android/server/wifi/hotspot2/omadm/RequestDetail.java new file mode 100644 index 000000000..11d5c5b5a --- /dev/null +++ b/service/java/com/android/server/wifi/hotspot2/omadm/RequestDetail.java @@ -0,0 +1,60 @@ +package com.android.server.wifi.hotspot2.omadm; + +import java.util.Arrays; +import java.util.Collection; +import java.util.Map; + +public class RequestDetail { + private final String mSppversion; + private final String mRedirectURI; + private final String mRequestReason; + private final String mSessionID; + private final String[] mSupportedVersions; + private final String[] mSupportedMOs; + private final Collection<MOTree> m_MOs; + + public enum RequestFields { + SPPVersion, + RedirectURI, + RequestReason, + SessionID, + SupportedVersions, + SupportedMOs + } + + public RequestDetail(Map<RequestFields, String> values, Collection<MOTree> mos) { + mSppversion = values.get(RequestFields.SPPVersion); + mRedirectURI = values.get(RequestFields.RedirectURI); + mRequestReason = values.get(RequestFields.RequestReason); + mSessionID = values.get(RequestFields.SessionID); + mSupportedVersions = split(values.get(RequestFields.SupportedVersions)); + mSupportedMOs = split(values.get(RequestFields.SupportedMOs)); + m_MOs = mos; + } + + public Collection<MOTree> getMOs() { + return m_MOs; + } + + private static String[] split(String list) { + return list != null ? list.split("[ \n\r]+") : null; + } + + @Override + public String toString() { + StringBuilder sb = new StringBuilder(); + + sb.append("SPPVersion").append(" = '").append(mSppversion).append("'\n"); + sb.append("RedirectURI").append(" = '").append(mRedirectURI).append("'\n"); + sb.append("RequestReason").append(" = '").append(mRequestReason).append("'\n"); + sb.append("SessionID").append(" = '").append(mSessionID).append("'\n"); + sb.append("SupportedVersions").append(" = ").append(Arrays.toString(mSupportedVersions)) + .append('\n'); + sb.append("SupportedMOs").append(" = ").append(Arrays.toString(mSupportedMOs)).append('\n'); + sb.append("MOs:\n"); + for (MOTree mo : m_MOs) + sb.append(mo); + + return sb.toString(); + } +} diff --git a/service/java/com/android/server/wifi/hotspot2/omadm/SOAPParser.java b/service/java/com/android/server/wifi/hotspot2/omadm/SOAPParser.java new file mode 100644 index 000000000..634ba4b4e --- /dev/null +++ b/service/java/com/android/server/wifi/hotspot2/omadm/SOAPParser.java @@ -0,0 +1,149 @@ +package com.android.server.wifi.hotspot2.omadm; + +import org.xml.sax.Attributes; +import org.xml.sax.SAXException; +import org.xml.sax.helpers.DefaultHandler; + +import javax.xml.parsers.ParserConfigurationException; +import javax.xml.parsers.SAXParser; +import javax.xml.parsers.SAXParserFactory; + +import java.io.*; +import java.util.*; + +import static com.android.server.wifi.hotspot2.omadm.RequestDetail.RequestFields.*; + +public class SOAPParser extends DefaultHandler { + private XMLNode mRoot; + private XMLNode mCurrent; + + private static String[] TagOnly = new String[0]; + private static final Map<RequestDetail.RequestFields, String> sSoapMappings = + new EnumMap<RequestDetail.RequestFields, String>(RequestDetail.RequestFields.class); + private static final Map<String, RequestDetail.RequestFields> sRevMappings = + new HashMap<String, RequestDetail.RequestFields>(); + private static final Map<String, String[]> sSoapAttributes = + new HashMap<String, String[]>(); + + static { + sSoapMappings.put(SPPVersion, "spp:sppVersion"); + sSoapMappings.put(RedirectURI, "redirectURI"); + sSoapMappings.put(RequestReason, "requestReason"); + sSoapMappings.put(SessionID, "spp:sessionID"); + sSoapMappings.put(SupportedVersions, "spp:supportedSPPVersions"); + sSoapMappings.put(SupportedMOs, "spp:supportedMOList"); + + for (Map.Entry<RequestDetail.RequestFields, String> entry : sSoapMappings.entrySet()) { + sRevMappings.put(entry.getValue(), entry.getKey()); + } + + // Really: The first element inside the body + sSoapAttributes.put("spp:sppPostDevDataResponse", new String[]{ + sSoapMappings.get(SPPVersion), + sSoapMappings.get(RedirectURI), + sSoapMappings.get(RequestReason), + sSoapMappings.get(SessionID)}); + + sSoapAttributes.put(sSoapMappings.get(SupportedVersions), TagOnly); + sSoapAttributes.put(sSoapMappings.get(SupportedMOs), TagOnly); + } + + public XMLNode parse(File file) throws IOException, ParserConfigurationException, SAXException { + SAXParser parser = SAXParserFactory.newInstance().newSAXParser(); + + BufferedInputStream in = new BufferedInputStream(new FileInputStream(file)); + try { + parser.parse(in, this); + } finally { + in.close(); + } + return mRoot; + } + + @Override + public void startElement(String uri, String localName, String qName, Attributes attributes) + throws SAXException { + XMLNode parent = mCurrent; + + mCurrent = new XMLNode(mCurrent, qName, attributes); + System.out.println("Added " + mCurrent.getTag() + ", atts " + mCurrent.getAttributes()); + + if (mRoot == null) + mRoot = mCurrent; + else + parent.addChild(mCurrent); + } + + @Override + public void endElement(String uri, String localName, String qName) throws SAXException { + if (!qName.equals(mCurrent.getTag())) + throw new SAXException("End tag '" + qName + "' doesn't match current node: " + + mCurrent); + + try { + mCurrent.close(); + } catch (IOException ioe) { + throw new SAXException("Failed to close element", ioe); + } + + mCurrent = mCurrent.getParent(); + } + + @Override + public void characters(char[] ch, int start, int length) throws SAXException { + mCurrent.addText(ch, start, length); + } + + public RequestDetail getRequestDetail() { + Map<RequestDetail.RequestFields, String> values = + new EnumMap<RequestDetail.RequestFields, String>(RequestDetail.RequestFields.class); + List<MOTree> mos = new ArrayList<MOTree>(); + extractFields(mRoot, values, mos); + return new RequestDetail(values, mos); + } + + private static void extractFields(XMLNode node, Map<RequestDetail.RequestFields, + String> values, Collection<MOTree> mos) { + String[] attributes = sSoapAttributes.get(node.getTag()); + + if (attributes != null) { + if (attributes.length == 0) { + RequestDetail.RequestFields field = sRevMappings.get(node.getTag()); + values.put(field, node.getText()); + } else { + for (String attribute : attributes) { + RequestDetail.RequestFields field = sRevMappings.get(attribute); + if (field != null) { + String value = node.getAttributeValue(attribute); + + if (value != null) + values.put(field, value); + } + } + } + } + + if (node.getMOTree() != null) + mos.add(node.getMOTree()); + + for (XMLNode child : node.getChildren()) { + extractFields(child, values, mos); + } + } + + public static void main(String[] args) throws Exception { + SOAPParser soapParser = new SOAPParser(); + XMLNode root = soapParser.parse(new File(args[0])); + //System.out.println( root ); + System.out.println(soapParser.getRequestDetail()); + System.out.println("Marshalled: "); + for (MOTree mo : soapParser.getRequestDetail().getMOs()) { + ByteArrayOutputStream out = new ByteArrayOutputStream(); + mo.marshal(out); + System.out.println(out.toString()); + MOTree back = MOTree.unmarshal(new ByteArrayInputStream(out.toByteArray())); + System.out.println(back); + } + System.out.println("---"); + } +} diff --git a/service/java/com/android/server/wifi/hotspot2/omadm/XMLNode.java b/service/java/com/android/server/wifi/hotspot2/omadm/XMLNode.java new file mode 100644 index 000000000..d400e2fc5 --- /dev/null +++ b/service/java/com/android/server/wifi/hotspot2/omadm/XMLNode.java @@ -0,0 +1,128 @@ +package com.android.server.wifi.hotspot2.omadm; + +import org.xml.sax.Attributes; +import org.xml.sax.SAXException; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +public class XMLNode { + private final String mTag; + private final Map<String, NodeAttribute> mAttributes; + private final List<XMLNode> mChildren; + private final XMLNode mParent; + private MOTree mMO; + private StringBuilder mTextBuilder; + private String mText; + + public XMLNode(XMLNode parent, String tag, Attributes attributes) throws SAXException { + mTag = tag; + + mAttributes = new HashMap<String, NodeAttribute>(); + + if (attributes.getLength() > 0) { + for (int n = 0; n < attributes.getLength(); n++) + mAttributes.put(attributes.getQName(n), new NodeAttribute(attributes.getQName(n), + attributes.getType(n), attributes.getValue(n))); + } + + mParent = parent; + mChildren = new ArrayList<XMLNode>(); + + mTextBuilder = new StringBuilder(); + } + + public void addText(char[] chs, int start, int length) { + String s = new String(chs, start, length); + String trimmed = s.trim(); + if (trimmed.isEmpty()) + return; + + if (s.charAt(0) != trimmed.charAt(0)) + mTextBuilder.append(' '); + mTextBuilder.append(trimmed); + if (s.charAt(s.length() - 1) != trimmed.charAt(trimmed.length() - 1)) + mTextBuilder.append(' '); + } + + public void addChild(XMLNode child) { + mChildren.add(child); + } + + public void close() throws IOException, SAXException { + String text = mTextBuilder.toString().trim(); + StringBuilder filtered = new StringBuilder(text.length()); + for (int n = 0; n < text.length(); n++) { + char ch = text.charAt(n); + if (ch >= ' ') + filtered.append(ch); + } + + mText = filtered.toString(); + mTextBuilder = null; + + if (OMAConstants.isMOContainer(mTag)) { + NodeAttribute urn = mAttributes.get(OMAConstants.ATTR_URN); + OMAParser omaParser = new OMAParser(); + mMO = omaParser.parse(mText, urn.getValue()); + } + } + + public String getTag() { + return mTag; + } + + public XMLNode getParent() { + return mParent; + } + + public String getText() { + return mText; + } + + public Map<String, NodeAttribute> getAttributes() { + return Collections.unmodifiableMap(mAttributes); + } + + public String getAttributeValue(String name) { + NodeAttribute nodeAttribute = mAttributes.get(name); + return nodeAttribute != null ? nodeAttribute.getValue() : null; + } + + public List<XMLNode> getChildren() { + return mChildren; + } + + public MOTree getMOTree() { + return mMO; + } + + private void toString(char[] indent, StringBuilder sb) { + Arrays.fill(indent, ' '); + + sb.append(indent).append('<').append(mTag).append("> ").append(mAttributes.values()); + + if (mMO != null) + sb.append('\n').append(mMO); + else if (!mText.isEmpty()) + sb.append(", text: ").append(mText); + + sb.append('\n'); + + char[] subIndent = Arrays.copyOf(indent, indent.length + 2); + for (XMLNode child : mChildren) + child.toString(subIndent, sb); + } + + @Override + public String toString() { + StringBuilder sb = new StringBuilder(); + toString(new char[0], sb); + return sb.toString(); + } +} |