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}