1package eu.siacs.conversations.xml;
  2
  3import androidx.annotation.NonNull;
  4
  5import com.google.common.base.Optional;
  6import com.google.common.collect.ImmutableList;
  7import com.google.common.collect.ImmutableMap;
  8import com.google.common.primitives.Ints;
  9
 10import java.util.ArrayList;
 11import java.util.Collection;
 12import java.util.Hashtable;
 13import java.util.List;
 14import java.util.stream.Collectors;
 15
 16import eu.siacs.conversations.utils.XmlHelper;
 17import eu.siacs.conversations.xmpp.InvalidJid;
 18import eu.siacs.conversations.xmpp.Jid;
 19import eu.siacs.conversations.xmpp.stanzas.MessagePacket;
 20
 21public class Element implements Node {
 22	private final String name;
 23	private Hashtable<String, String> attributes = new Hashtable<>();
 24	private List<Element> children = new ArrayList<>();
 25	private List<Node> childNodes = new ArrayList<>();
 26
 27	public Element(String name) {
 28		this.name = name;
 29	}
 30
 31	public Element(String name, String xmlns) {
 32		this.name = name;
 33		this.setAttribute("xmlns", xmlns);
 34	}
 35
 36	public Node prependChild(Node child) {
 37		childNodes.add(0, child);
 38		if (child instanceof Element) children.add(0, (Element) child);
 39		return child;
 40	}
 41
 42	public Node addChild(Node child) {
 43		childNodes.add(child);
 44		if (child instanceof Element) children.add((Element) child);
 45		return child;
 46	}
 47
 48	public Element addChild(String name) {
 49		Element child = new Element(name);
 50		childNodes.add(child);
 51		children.add(child);
 52		return child;
 53	}
 54
 55	public Element addChild(String name, String xmlns) {
 56		Element child = new Element(name);
 57		child.setAttribute("xmlns", xmlns);
 58		childNodes.add(child);
 59		children.add(child);
 60		return child;
 61	}
 62
 63	public void addChildren(final Collection<? extends Node> children) {
 64		if (children == null) return;
 65
 66		this.childNodes.addAll(children);
 67		for (Node node : children) {
 68			if (node instanceof Element) {
 69				this.children.add((Element) node);
 70			}
 71		}
 72	}
 73
 74	public void removeChild(Node child) {
 75		if (child == null) return;
 76
 77		this.childNodes.remove(child);
 78		if (child instanceof Element) this.children.remove(child);
 79	}
 80
 81	public Element setContent(String content) {
 82		clearChildren();
 83		if (content != null) this.childNodes.add(new TextNode(content));
 84		return this;
 85	}
 86
 87	public Element findChild(String name) {
 88		for (Element child : this.children) {
 89			if (child.getName().equals(name)) {
 90				return child;
 91			}
 92		}
 93		return null;
 94	}
 95
 96	public String findChildContent(String name) {
 97		Element element = findChild(name);
 98		return element == null ? null : element.getContent();
 99	}
100
101	public LocalizedContent findInternationalizedChildContentInDefaultNamespace(String name) {
102		return LocalizedContent.get(this, name);
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 boolean hasChild(final String name, final String xmlns) {
137		return findChild(name, xmlns) != null;
138	}
139
140	public final List<Element> getChildren() {
141		return ImmutableList.copyOf(this.children);
142	}
143
144	// Deprecated: you probably want bindTo or replaceChildren
145	public Element setChildren(List<Element> children) {
146		this.childNodes = new ArrayList(children);
147		this.children = new ArrayList(children);
148		return this;
149	}
150
151	public void replaceChildren(List<Element> children) {
152		this.childNodes.clear();
153		this.childNodes.addAll(children);
154		this.children.clear();
155		this.children.addAll(children);
156	}
157
158	public void bindTo(Element original) {
159		this.attributes = original.attributes;
160		this.childNodes = original.childNodes;
161		this.children = original.children;
162	}
163
164	public final String getContent() {
165		return this.childNodes.stream().map(Node::getContent).filter(c -> c != null).collect(Collectors.joining());
166	}
167
168	public Element setAttribute(String name, String value) {
169		if (name != null && value != null) {
170			this.attributes.put(name, value);
171		}
172		return this;
173	}
174
175	public Element setAttribute(String name, Jid value) {
176		if (name != null && value != null) {
177			this.attributes.put(name, value.toEscapedString());
178		}
179		return this;
180	}
181
182	public String toString() {
183		return toString(ImmutableMap.of());
184	}
185
186	public String toString(final ImmutableMap<String, String> parentNS) {
187		final var mutns = new Hashtable<>(parentNS);
188		final var attr = getSerializableAttributes(mutns);
189		final StringBuilder elementOutput = new StringBuilder();
190		if (childNodes.size() == 0) {
191			Tag emptyTag = Tag.empty(name);
192			emptyTag.setAttributes(attr);
193			elementOutput.append(emptyTag.toString());
194		} else {
195			final var ns = ImmutableMap.copyOf(mutns);
196			Tag startTag = Tag.start(name);
197			startTag.setAttributes(attr);
198			elementOutput.append(startTag);
199			for (Node child : ImmutableList.copyOf(childNodes)) {
200				elementOutput.append(child.toString(ns));
201			}
202			Tag endTag = Tag.end(name);
203			elementOutput.append(endTag);
204		}
205		return elementOutput.toString();
206	}
207
208	protected Hashtable<String, String> getSerializableAttributes(Hashtable<String, String> ns) {
209		final var result = new Hashtable<String, String>();
210		for (final var attr : attributes.entrySet()) {
211			if (attr.getKey().charAt(0) == '{') {
212				final var uriIdx = attr.getKey().indexOf('}');
213				final var uri = attr.getKey().substring(1, uriIdx - 1);
214				if (!ns.containsKey(uri)) {
215					result.put("xmlns:ns" + ns.size(), uri);
216					ns.put(uri, "ns" + ns.size());
217				}
218				result.put(ns.get(uri) + ":" + attr.getKey().substring(uriIdx + 1), attr.getValue());
219			} else {
220				result.put(attr.getKey(), attr.getValue());
221			}
222		}
223
224		return result;
225	}
226
227	public Element removeAttribute(String name) {
228		this.attributes.remove(name);
229		return this;
230	}
231
232	public Element setAttributes(Hashtable<String, String> attributes) {
233		this.attributes = attributes;
234		return this;
235	}
236
237	public String getAttribute(String name) {
238		if (this.attributes.containsKey(name)) {
239			return this.attributes.get(name);
240		} else {
241			return null;
242		}
243	}
244
245	public Optional<Integer> getOptionalIntAttribute(final String name) {
246		final String value = getAttribute(name);
247		if (value == null) {
248			return Optional.absent();
249		}
250		return Optional.fromNullable(Ints.tryParse(value));
251	}
252
253	public Jid getAttributeAsJid(String name) {
254		final String jid = this.getAttribute(name);
255		if (jid != null && !jid.isEmpty()) {
256			try {
257				return Jid.ofEscaped(jid);
258			} catch (final IllegalArgumentException e) {
259				return InvalidJid.of(jid, this instanceof MessagePacket);
260			}
261		}
262		return null;
263	}
264
265	public Hashtable<String, String> getAttributes() {
266		return this.attributes;
267	}
268
269	public final String getName() {
270		return name;
271	}
272
273	public void clearChildren() {
274		this.children.clear();
275		this.childNodes.clear();
276	}
277
278	public void setAttribute(String name, long value) {
279		this.setAttribute(name, Long.toString(value));
280	}
281
282	public void setAttribute(String name, int value) {
283		this.setAttribute(name, Integer.toString(value));
284	}
285
286	public boolean getAttributeAsBoolean(String name) {
287		String attr = getAttribute(name);
288		return (attr != null && (attr.equalsIgnoreCase("true") || attr.equalsIgnoreCase("1")));
289	}
290
291	public String getNamespace() {
292		return getAttribute("xmlns");
293	}
294}