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