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