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 LocalizedContent findInternationalizedChildContentInDefaultNamespace(String name) {
105 return LocalizedContent.get(this, name);
106 }
107
108 public Element findChild(String name, String xmlns) {
109 for (Element child : getChildren()) {
110 if (name.equals(child.getName()) && xmlns.equals(child.getAttribute("xmlns"))) {
111 return child;
112 }
113 }
114 return null;
115 }
116
117 public Element findChildEnsureSingle(String name, String xmlns) {
118 final List<Element> results = new ArrayList<>();
119 for (Element child : getChildren()) {
120 if (name.equals(child.getName()) && xmlns.equals(child.getAttribute("xmlns"))) {
121 results.add(child);
122 }
123 }
124 if (results.size() == 1) {
125 return results.get(0);
126 }
127 return null;
128 }
129
130 public String findChildContent(String name, String xmlns) {
131 Element element = findChild(name,xmlns);
132 return element == null ? null : element.getContent();
133 }
134
135 public boolean hasChild(final String name) {
136 return findChild(name) != null;
137 }
138
139 public boolean hasChild(final String name, final String xmlns) {
140 return findChild(name, xmlns) != null;
141 }
142
143 public final List<Element> getChildren() {
144 return ImmutableList.copyOf(this.children);
145 }
146
147 public void setAttribute(final String name, final boolean value) {
148 this.setAttribute(name, value ? "1" : "0");
149 }
150
151 // Deprecated: you probably want bindTo or replaceChildren
152 public Element setChildren(List<Element> children) {
153 this.childNodes = new ArrayList(children);
154 this.children = new ArrayList(children);
155 return this;
156 }
157
158 public void replaceChildren(List<Element> children) {
159 this.childNodes.clear();
160 this.childNodes.addAll(children);
161 this.children.clear();
162 this.children.addAll(children);
163 }
164
165 public void bindTo(Element original) {
166 this.attributes = original.attributes;
167 this.childNodes = original.childNodes;
168 this.children = original.children;
169 }
170
171 public final String getContent() {
172 return this.childNodes.stream().map(Node::getContent).filter(c -> c != null).collect(Collectors.joining());
173 }
174
175 public long getLongAttribute(final String name) {
176 final var value = Longs.tryParse(Strings.nullToEmpty(this.attributes.get(name)));
177 return value == null ? 0 : value;
178 }
179
180 public Optional<Integer> getOptionalIntAttribute(final String name) {
181 final String value = getAttribute(name);
182 if (value == null) {
183 return Optional.absent();
184 }
185 return Optional.fromNullable(Ints.tryParse(value));
186 }
187
188 public Jid getAttributeAsJid(String name) {
189 final String jid = this.getAttribute(name);
190 if (jid != null && !jid.isEmpty()) {
191 try {
192 return Jid.of(jid);
193 } catch (final IllegalArgumentException e) {
194 return Jid.ofOrInvalid(jid, this instanceof Message);
195 }
196 }
197 return null;
198 }
199
200 public Element setAttribute(String name, String value) {
201 if (name != null && value != null) {
202 this.attributes.put(name, value);
203 }
204 return this;
205 }
206
207 public Element setAttribute(String name, Jid value) {
208 if (name != null && value != null) {
209 this.attributes.put(name, value.toString());
210 }
211 return this;
212 }
213
214 public String toString() {
215 return toString(ImmutableMap.of());
216 }
217
218 public void appendToBuilder(final Map<String, String> parentNS, final StringBuilder elementOutput, final int skipEnd) {
219 final var mutns = new CopyOnWriteMap<>(parentNS);
220 if (childNodes.size() == 0) {
221 final var attr = getSerializableAttributes(mutns);
222 Tag emptyTag = Tag.empty(name);
223 emptyTag.setAttributes(attr);
224 emptyTag.appendToBuilder(elementOutput);
225 } else {
226 final var startTag = startTag(mutns);
227 startTag.appendToBuilder(elementOutput);
228 for (Node child : ImmutableList.copyOf(childNodes)) {
229 child.appendToBuilder(mutns.toMap(), elementOutput, Math.max(0, skipEnd - 1));
230 }
231 if (skipEnd < 1) endTag().appendToBuilder(elementOutput);
232 }
233 }
234
235 public String toString(final ImmutableMap<String, String> parentNS) {
236 final StringBuilder elementOutput = new StringBuilder();
237 appendToBuilder(parentNS, elementOutput, 0);
238 return elementOutput.toString();
239 }
240
241 public Tag startTag() {
242 return startTag(new CopyOnWriteMap<>(new Hashtable<>()));
243 }
244
245 public Tag startTag(final CopyOnWriteMap<String, String> mutns) {
246 final var attr = getSerializableAttributes(mutns);
247 final var startTag = Tag.start(name);
248 startTag.setAttributes(attr);
249 return startTag;
250 }
251
252 public Tag endTag() {
253 return Tag.end(name);
254 }
255
256 protected Hashtable<String, String> getSerializableAttributes(CopyOnWriteMap<String, String> ns) {
257 final var result = new Hashtable<String, String>(attributes.size());
258 for (final var attr : attributes.entrySet()) {
259 if (attr.getKey().charAt(0) == '{') {
260 final var uriIdx = attr.getKey().indexOf('}');
261 final var uri = attr.getKey().substring(1, uriIdx - 1);
262 if (!ns.containsKey(uri)) {
263 result.put("xmlns:ns" + ns.size(), uri);
264 ns.put(uri, "ns" + ns.size());
265 }
266 result.put(ns.get(uri) + ":" + attr.getKey().substring(uriIdx + 1), attr.getValue());
267 } else {
268 result.put(attr.getKey(), attr.getValue());
269 }
270 }
271
272 return result;
273 }
274
275 public Element removeAttribute(final String name) {
276 this.attributes.remove(name);
277 return this;
278 }
279
280 public Element setAttributes(Hashtable<String, String> attributes) {
281 this.attributes = attributes;
282 return this;
283 }
284
285 public String getAttribute(String name) {
286 if (this.attributes.containsKey(name)) {
287 return this.attributes.get(name);
288 } else {
289 return null;
290 }
291 }
292
293 public Hashtable<String, String> getAttributes() {
294 return this.attributes;
295 }
296
297 public final String getName() {
298 return name;
299 }
300
301 public void clearChildren() {
302 this.children.clear();
303 this.childNodes.clear();
304 }
305
306 public void setAttribute(String name, long value) {
307 this.setAttribute(name, Long.toString(value));
308 }
309
310 public void setAttribute(String name, int value) {
311 this.setAttribute(name, Integer.toString(value));
312 }
313
314 public boolean getAttributeAsBoolean(String name) {
315 String attr = getAttribute(name);
316 return (attr != null && (attr.equalsIgnoreCase("true") || attr.equalsIgnoreCase("1")));
317 }
318
319 public String getNamespace() {
320 return getAttribute("xmlns");
321 }
322
323 static class CopyOnWriteMap<K,V> {
324 protected final Map<K,V> original;
325 protected Hashtable<K,V> mut = null;
326
327 public CopyOnWriteMap(Map<K,V> original) {
328 this.original = original;
329 }
330
331 public int size() {
332 return mut == null ? original.size() : mut.size();
333 }
334
335 public boolean containsKey(K k) {
336 return mut == null ? original.containsKey(k) : mut.containsKey(k);
337 }
338
339 public V get(K k) {
340 return mut == null ? original.get(k) : mut.get(k);
341 }
342
343 public void put(K k, V v) {
344 if (mut == null) {
345 mut = new Hashtable<>(original);
346 }
347 mut.put(k, v);
348 }
349
350 public Map<K, V> toMap() {
351 return mut == null ? original : mut;
352 }
353 }
354}