XMLNode.java revision ee699a61a5687d7c8518b639a940c8e9d1b384dd
1package com.android.hotspot2.omadm;
2
3import org.xml.sax.Attributes;
4import org.xml.sax.SAXException;
5
6import java.io.IOException;
7import java.util.ArrayList;
8import java.util.Arrays;
9import java.util.Collections;
10import java.util.HashMap;
11import java.util.HashSet;
12import java.util.List;
13import java.util.Map;
14import java.util.Set;
15
16public 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++) {
32            XML_SPECIAL.add(XML_SPECIAL_CHARS.charAt(n));
33        }
34    }
35
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
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
71    public void setText(String text) {
72        mText = text;
73        mTextBuilder = null;
74    }
75
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
89    public void addChild(XMLNode child) {
90        mChildren.add(child);
91    }
92
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
116    public String getTag() {
117        return mTag;
118    }
119
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
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
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
143    public XMLNode getParent() {
144        return mParent;
145    }
146
147    public String getText() {
148        return mText;
149    }
150
151    public Map<String, NodeAttribute> getAttributes() {
152        return Collections.unmodifiableMap(mAttributes);
153    }
154
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
163    public String getAttributeValue(String name) {
164        NodeAttribute nodeAttribute = mAttributes.get(name);
165        return nodeAttribute != null ? nodeAttribute.getValue() : null;
166    }
167
168    public List<XMLNode> getChildren() {
169        return mChildren;
170    }
171
172    public MOTree getMOTree() {
173        return mMO;
174    }
175
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
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
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
235    public String toString() {
236        StringBuilder sb = new StringBuilder();
237        toString(new char[0], sb);
238        return sb.toString();
239    }
240}
241