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