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}