1package eu.siacs.conversations.xml;
2
3import androidx.annotation.NonNull;
4import com.google.common.base.Optional;
5import com.google.common.collect.ImmutableList;
6import com.google.common.collect.ImmutableMap;
7import com.google.common.base.Strings;
8import com.google.common.primitives.Ints;
9import com.google.common.primitives.Longs;
10import eu.siacs.conversations.utils.XmlHelper;
11import eu.siacs.conversations.xmpp.Jid;
12import im.conversations.android.xmpp.model.stanza.Message;
13import java.util.ArrayList;
14import java.util.Collection;
15import java.util.Map;
16import java.util.Hashtable;
17import java.util.List;
18import java.util.stream.Collectors;
19
20import eu.siacs.conversations.utils.XmlHelper;
21import eu.siacs.conversations.xmpp.Jid;
22import im.conversations.android.xmpp.model.stanza.Message;
23
24public class Element implements Node {
25 private final String name;
26 private Hashtable<String, String> attributes = new Hashtable<>();
27 private List<Element> children = new ArrayList<>();
28 private List<Node> childNodes = new ArrayList<>();
29
30 public Element(String name) {
31 this.name = name;
32 }
33
34 public Element(String name, String xmlns) {
35 this.name = name;
36 this.setAttribute("xmlns", xmlns);
37 }
38
39 public Node prependChild(Node child) {
40 childNodes.add(0, child);
41 if (child instanceof Element) children.add(0, (Element) child);
42 return child;
43 }
44
45 public Node addChild(Node child) {
46 childNodes.add(child);
47 if (child instanceof Element) children.add((Element) child);
48 return child;
49 }
50
51 public Element addChild(String name) {
52 Element child = new Element(name);
53 childNodes.add(child);
54 children.add(child);
55 return child;
56 }
57
58 public Element addChild(String name, String xmlns) {
59 Element child = new Element(name);
60 child.setAttribute("xmlns", xmlns);
61 childNodes.add(child);
62 children.add(child);
63 return child;
64 }
65
66 public void addChildren(final Collection<? extends Node> children) {
67 if (children == null) return;
68
69 this.childNodes.addAll(children);
70 for (Node node : children) {
71 if (node instanceof Element) {
72 this.children.add((Element) node);
73 }
74 }
75 }
76
77 public void removeChild(Node child) {
78 if (child == null) return;
79
80 this.childNodes.remove(child);
81 if (child instanceof Element) this.children.remove(child);
82 }
83
84 public Element setContent(String content) {
85 clearChildren();
86 if (content != null) this.childNodes.add(new TextNode(content));
87 return this;
88 }
89
90 public Element findChild(String name) {
91 for (Element child : this.children) {
92 if (child.getName().equals(name)) {
93 return child;
94 }
95 }
96 return null;
97 }
98
99 public String findChildContent(String name) {
100 Element element = findChild(name);
101 return element == null ? null : element.getContent();
102 }
103
104 public Element findChild(String name, String xmlns) {
105 for (Element child : getChildren()) {
106 if (name.equals(child.getName()) && xmlns.equals(child.getAttribute("xmlns"))) {
107 return child;
108 }
109 }
110 return null;
111 }
112
113 public Element findChildEnsureSingle(String name, String xmlns) {
114 final List<Element> results = new ArrayList<>();
115 for (Element child : getChildren()) {
116 if (name.equals(child.getName()) && xmlns.equals(child.getAttribute("xmlns"))) {
117 results.add(child);
118 }
119 }
120 if (results.size() == 1) {
121 return results.get(0);
122 }
123 return null;
124 }
125
126 public String findChildContent(String name, String xmlns) {
127 Element element = findChild(name,xmlns);
128 return element == null ? null : element.getContent();
129 }
130
131 public boolean hasChild(final String name) {
132 return findChild(name) != null;
133 }
134
135 public boolean hasChild(final String name, final String xmlns) {
136 return findChild(name, xmlns) != null;
137 }
138
139 public final List<Element> getChildren() {
140 return ImmutableList.copyOf(this.children);
141 }
142
143 public void setAttribute(final String name, final boolean value) {
144 this.setAttribute(name, value ? "1" : "0");
145 }
146
147 // Deprecated: you probably want bindTo or replaceChildren
148 public Element setChildren(List<Element> children) {
149 this.childNodes = new ArrayList(children);
150 this.children = new ArrayList(children);
151 return this;
152 }
153
154 public void replaceChildren(List<Element> children) {
155 this.childNodes.clear();
156 this.childNodes.addAll(children);
157 this.children.clear();
158 this.children.addAll(children);
159 }
160
161 public void bindTo(Element original) {
162 this.attributes = original.attributes;
163 this.childNodes = original.childNodes;
164 this.children = original.children;
165 }
166
167 public final String getContent() {
168 return this.childNodes.stream().map(Node::getContent).filter(c -> c != null).collect(Collectors.joining());
169 }
170
171 public long getLongAttribute(final String name) {
172 final var value = Longs.tryParse(Strings.nullToEmpty(this.attributes.get(name)));
173 return value == null ? 0 : value;
174 }
175
176 public Optional<Integer> getOptionalIntAttribute(final String name) {
177 final String value = getAttribute(name);
178 if (value == null) {
179 return Optional.absent();
180 }
181 return Optional.fromNullable(Ints.tryParse(value));
182 }
183
184 public Jid getAttributeAsJid(String name) {
185 final String jid = this.getAttribute(name);
186 if (jid != null && !jid.isEmpty()) {
187 try {
188 return Jid.of(jid);
189 } catch (final IllegalArgumentException e) {
190 return Jid.ofOrInvalid(jid, this instanceof Message);
191 }
192 }
193 return null;
194 }
195
196 public Element setAttribute(String name, String value) {
197 if (name != null && value != null) {
198 this.attributes.put(name, value);
199 }
200 return this;
201 }
202
203 public Element setAttribute(String name, Jid value) {
204 if (name != null && value != null) {
205 this.attributes.put(name, value.toString());
206 }
207 return this;
208 }
209
210 public String toString() {
211 return toString(ImmutableMap.of());
212 }
213
214 public void appendToBuilder(final Map<String, String> parentNS, final StringBuilder elementOutput, final int skipEnd) {
215 final var mutns = new CopyOnWriteMap<>(parentNS);
216 if (childNodes.size() == 0) {
217 final var attr = getSerializableAttributes(mutns);
218 Tag emptyTag = Tag.empty(name);
219 emptyTag.setAttributes(attr);
220 emptyTag.appendToBuilder(elementOutput);
221 } else {
222 final var startTag = startTag(mutns);
223 startTag.appendToBuilder(elementOutput);
224 for (Node child : ImmutableList.copyOf(childNodes)) {
225 child.appendToBuilder(mutns.toMap(), elementOutput, Math.max(0, skipEnd - 1));
226 }
227 if (skipEnd < 1) endTag().appendToBuilder(elementOutput);
228 }
229 }
230
231 public String toString(final ImmutableMap<String, String> parentNS) {
232 final StringBuilder elementOutput = new StringBuilder();
233 appendToBuilder(parentNS, elementOutput, 0);
234 return elementOutput.toString();
235 }
236
237 public Tag startTag() {
238 return startTag(new CopyOnWriteMap<>(new Hashtable<>()));
239 }
240
241 public Tag startTag(final CopyOnWriteMap<String, String> mutns) {
242 final var attr = getSerializableAttributes(mutns);
243 final var startTag = Tag.start(name);
244 startTag.setAttributes(attr);
245 return startTag;
246 }
247
248 public Tag endTag() {
249 return Tag.end(name);
250 }
251
252 protected Hashtable<String, String> getSerializableAttributes(CopyOnWriteMap<String, String> ns) {
253 final var result = new Hashtable<String, String>(attributes.size());
254 for (final var attr : attributes.entrySet()) {
255 if (attr.getKey().charAt(0) == '{') {
256 final var uriIdx = attr.getKey().indexOf('}');
257 final var uri = attr.getKey().substring(1, uriIdx - 1);
258 if (!ns.containsKey(uri)) {
259 result.put("xmlns:ns" + ns.size(), uri);
260 ns.put(uri, "ns" + ns.size());
261 }
262 result.put(ns.get(uri) + ":" + attr.getKey().substring(uriIdx + 1), attr.getValue());
263 } else {
264 result.put(attr.getKey(), attr.getValue());
265 }
266 }
267
268 return result;
269 }
270
271 public Element removeAttribute(final String name) {
272 this.attributes.remove(name);
273 return this;
274 }
275
276 public Element setAttributes(Hashtable<String, String> attributes) {
277 this.attributes = attributes;
278 return this;
279 }
280
281 public String getAttribute(String name) {
282 if (this.attributes.containsKey(name)) {
283 return this.attributes.get(name);
284 } else {
285 return null;
286 }
287 }
288
289 public Hashtable<String, String> getAttributes() {
290 return this.attributes;
291 }
292
293 public final String getName() {
294 return name;
295 }
296
297 public void clearChildren() {
298 this.children.clear();
299 this.childNodes.clear();
300 }
301
302 public void setAttribute(String name, long value) {
303 this.setAttribute(name, Long.toString(value));
304 }
305
306 public void setAttribute(String name, int value) {
307 this.setAttribute(name, Integer.toString(value));
308 }
309
310 public boolean getAttributeAsBoolean(String name) {
311 String attr = getAttribute(name);
312 return (attr != null && (attr.equalsIgnoreCase("true") || attr.equalsIgnoreCase("1")));
313 }
314
315 public String getNamespace() {
316 return getAttribute("xmlns");
317 }
318
319 static class CopyOnWriteMap<K,V> {
320 protected final Map<K,V> original;
321 protected Hashtable<K,V> mut = null;
322
323 public CopyOnWriteMap(Map<K,V> original) {
324 this.original = original;
325 }
326
327 public int size() {
328 return mut == null ? original.size() : mut.size();
329 }
330
331 public boolean containsKey(K k) {
332 return mut == null ? original.containsKey(k) : mut.containsKey(k);
333 }
334
335 public V get(K k) {
336 return mut == null ? original.get(k) : mut.get(k);
337 }
338
339 public void put(K k, V v) {
340 if (mut == null) {
341 mut = new Hashtable<>(original);
342 }
343 mut.put(k, v);
344 }
345
346 public Map<K, V> toMap() {
347 return mut == null ? original : mut;
348 }
349 }
350}