1 package com.android.server.wifi.hotspot2.omadm; 2 3 import org.xml.sax.Attributes; 4 import org.xml.sax.SAXException; 5 6 import java.io.IOException; 7 import java.util.ArrayList; 8 import java.util.Arrays; 9 import java.util.Collections; 10 import java.util.HashMap; 11 import java.util.HashSet; 12 import java.util.Iterator; 13 import java.util.List; 14 import java.util.Map; 15 import java.util.Set; 16 17 public class XMLNode { 18 private final String mTag; 19 private final Map<String, NodeAttribute> mAttributes; 20 private final List<XMLNode> mChildren; 21 private final XMLNode mParent; 22 private MOTree mMO; 23 private StringBuilder mTextBuilder; 24 private String mText; 25 26 private static final String XML_SPECIAL_CHARS = "\"'<>&"; 27 private static final Set<Character> XML_SPECIAL = new HashSet<>(); 28 private static final String CDATA_OPEN = "<![CDATA["; 29 private static final String CDATA_CLOSE = "]]>"; 30 31 static { 32 for (int n = 0; n < XML_SPECIAL_CHARS.length(); n++) { 33 XML_SPECIAL.add(XML_SPECIAL_CHARS.charAt(n)); 34 } 35 } 36 37 public XMLNode(XMLNode parent, String tag, Attributes attributes) throws SAXException { 38 mTag = tag; 39 40 mAttributes = new HashMap<>(); 41 42 if (attributes.getLength() > 0) { 43 for (int n = 0; n < attributes.getLength(); n++) 44 mAttributes.put(attributes.getQName(n), new NodeAttribute(attributes.getQName(n), 45 attributes.getType(n), attributes.getValue(n))); 46 } 47 48 mParent = parent; 49 mChildren = new ArrayList<>(); 50 51 mTextBuilder = new StringBuilder(); 52 } 53 54 public XMLNode(XMLNode parent, String tag, Map<String, String> attributes) { 55 mTag = tag; 56 57 mAttributes = new HashMap<>(attributes == null ? 0 : attributes.size()); 58 59 if (attributes != null) { 60 for (Map.Entry<String, String> entry : attributes.entrySet()) { 61 mAttributes.put(entry.getKey(), new NodeAttribute(entry.getKey(), "", entry.getValue())); 62 } 63 } 64 65 mParent = parent; 66 mChildren = new ArrayList<>(); 67 68 mTextBuilder = new StringBuilder(); 69 } 70 71 @Override 72 public boolean equals(Object thatObject) { 73 if (thatObject == this) { 74 return true; 75 } else if (thatObject.getClass() != XMLNode.class) { 76 return false; 77 } 78 79 XMLNode that = (XMLNode) thatObject; 80 if (!getTag().equals(that.getTag()) 81 || mAttributes.size() != that.mAttributes.size() 82 || mChildren.size() != that.mChildren.size()) { 83 return false; 84 } 85 86 for (Map.Entry<String, NodeAttribute> entry : mAttributes.entrySet()) { 87 if (!entry.getValue().equals(that.mAttributes.get(entry.getKey()))) { 88 return false; 89 } 90 } 91 92 List<XMLNode> cloneOfThat = new ArrayList<>(that.mChildren); 93 for (XMLNode child : mChildren) { 94 Iterator<XMLNode> thatChildren = cloneOfThat.iterator(); 95 boolean found = false; 96 while (thatChildren.hasNext()) { 97 XMLNode thatChild = thatChildren.next(); 98 if (child.equals(thatChild)) { 99 found = true; 100 thatChildren.remove(); 101 break; 102 } 103 } 104 if (!found) { 105 return false; 106 } 107 } 108 return true; 109 } 110 111 public void setText(String text) { 112 mText = text; 113 mTextBuilder = null; 114 } 115 116 public void addText(char[] chs, int start, int length) { 117 String s = new String(chs, start, length); 118 String trimmed = s.trim(); 119 if (trimmed.isEmpty()) 120 return; 121 122 if (s.charAt(0) != trimmed.charAt(0)) 123 mTextBuilder.append(' '); 124 mTextBuilder.append(trimmed); 125 if (s.charAt(s.length() - 1) != trimmed.charAt(trimmed.length() - 1)) 126 mTextBuilder.append(' '); 127 } 128 129 public void addChild(XMLNode child) { 130 mChildren.add(child); 131 } 132 133 public void close() throws IOException, SAXException { 134 String text = mTextBuilder.toString().trim(); 135 StringBuilder filtered = new StringBuilder(text.length()); 136 for (int n = 0; n < text.length(); n++) { 137 char ch = text.charAt(n); 138 if (ch >= ' ') 139 filtered.append(ch); 140 } 141 142 mText = filtered.toString(); 143 mTextBuilder = null; 144 145 if (MOTree.hasMgmtTreeTag(mText)) { 146 try { 147 NodeAttribute urn = mAttributes.get(OMAConstants.SppMOAttribute); 148 OMAParser omaParser = new OMAParser(); 149 mMO = omaParser.parse(mText, urn != null ? urn.getValue() : null); 150 } 151 catch (SAXException | IOException e) { 152 mMO = null; 153 } 154 } 155 } 156 157 public String getTag() { 158 return mTag; 159 } 160 161 public String getNameSpace() throws OMAException { 162 String[] nsn = mTag.split(":"); 163 if (nsn.length != 2) { 164 throw new OMAException("Non-namespaced tag: '" + mTag + "'"); 165 } 166 return nsn[0]; 167 } 168 169 public String getStrippedTag() throws OMAException { 170 String[] nsn = mTag.split(":"); 171 if (nsn.length != 2) { 172 throw new OMAException("Non-namespaced tag: '" + mTag + "'"); 173 } 174 return nsn[1].toLowerCase(); 175 } 176 177 public XMLNode getSoleChild() throws OMAException{ 178 if (mChildren.size() != 1) { 179 throw new OMAException("Expected exactly one child to " + mTag); 180 } 181 return mChildren.get(0); 182 } 183 184 public XMLNode getParent() { 185 return mParent; 186 } 187 188 public String getText() { 189 return mText; 190 } 191 192 public Map<String, NodeAttribute> getAttributes() { 193 return Collections.unmodifiableMap(mAttributes); 194 } 195 196 /** 197 * Get the attributes of this node as a map of attribute name to attribute value. 198 * @return The attribute mapping. 199 */ 200 public Map<String, String> getTextualAttributes() { 201 Map<String, String> map = new HashMap<>(mAttributes.size()); 202 for (Map.Entry<String, NodeAttribute> entry : mAttributes.entrySet()) { 203 map.put(entry.getKey(), entry.getValue().getValue()); 204 } 205 return map; 206 } 207 208 public String getAttributeValue(String name) { 209 NodeAttribute nodeAttribute = mAttributes.get(name); 210 return nodeAttribute != null ? nodeAttribute.getValue() : null; 211 } 212 213 public List<XMLNode> getChildren() { 214 return mChildren; 215 } 216 217 public MOTree getMOTree() { 218 return mMO; 219 } 220 221 private void toString(char[] indent, StringBuilder sb) { 222 Arrays.fill(indent, ' '); 223 224 sb.append(indent).append('<').append(mTag); 225 for (Map.Entry<String, NodeAttribute> entry : mAttributes.entrySet()) { 226 sb.append(' ').append(entry.getKey()).append("='").append(entry.getValue().getValue()).append('\''); 227 } 228 229 if (mText != null && !mText.isEmpty()) { 230 sb.append('>').append(escapeCdata(mText)).append("</").append(mTag).append(">\n"); 231 } 232 else if (mChildren.isEmpty()) { 233 sb.append("/>\n"); 234 } 235 else { 236 sb.append(">\n"); 237 char[] subIndent = Arrays.copyOf(indent, indent.length + 2); 238 for (XMLNode child : mChildren) { 239 child.toString(subIndent, sb); 240 } 241 sb.append(indent).append("</").append(mTag).append(">\n"); 242 } 243 } 244 245 private static String escapeCdata(String text) { 246 if (!escapable(text)) { 247 return text; 248 } 249 250 // Any appearance of ]]> in the text must be split into "]]" | "]]>" | <![CDATA[ | ">" 251 // i.e. "split the sequence by putting a close CDATA and a new open CDATA before the '>' 252 StringBuilder sb = new StringBuilder(); 253 sb.append(CDATA_OPEN); 254 int start = 0; 255 for (;;) { 256 int etoken = text.indexOf(CDATA_CLOSE); 257 if (etoken >= 0) { 258 sb.append(text.substring(start, etoken + 2)).append(CDATA_CLOSE).append(CDATA_OPEN); 259 start = etoken + 2; 260 } 261 else { 262 if (start < text.length() - 1) { 263 sb.append(text.substring(start)); 264 } 265 break; 266 } 267 } 268 sb.append(CDATA_CLOSE); 269 return sb.toString(); 270 } 271 272 private static boolean escapable(String s) { 273 for (int n = 0; n < s.length(); n++) { 274 if (XML_SPECIAL.contains(s.charAt(n))) { 275 return true; 276 } 277 } 278 return false; 279 } 280 281 @Override 282 public String toString() { 283 StringBuilder sb = new StringBuilder(); 284 toString(new char[0], sb); 285 return sb.toString(); 286 } 287 } 288