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}