diff --git a/src/main/java/eu/siacs/conversations/utils/XmlHelper.java b/src/main/java/eu/siacs/conversations/utils/XmlHelper.java index 54880be8fc7689563d18282f9741968d6c714725..797ecca4659142ca608b1ec2c2f34f250bda4c15 100644 --- a/src/main/java/eu/siacs/conversations/utils/XmlHelper.java +++ b/src/main/java/eu/siacs/conversations/utils/XmlHelper.java @@ -21,6 +21,33 @@ public class XmlHelper { return content; } + public static void appendEncodedEntities(final String content, final StringBuilder sb) { + final var length = content.length(); + var needsEscape = false; + for (int i = 0; i < length; i++) { + final var c = content.charAt(i); + if (c == '<' || c == '&' || c == '"') { + needsEscape = true; + break; + } + } + if (needsEscape) { + for (int i = 0; i < length; i++) { + final var c = content.charAt(i); + switch (c) { + case '&': sb.append("&"); break; + case '<': sb.append("<"); break; + case '>': sb.append(">"); break; + case '"': sb.append("""); break; + case '\'': sb.append("'"); break; + default: sb.append(c); + } + } + } else { + sb.append(content); + } + } + public static String printElementNames(final Element element) { final List features = element == null diff --git a/src/main/java/eu/siacs/conversations/xml/Element.java b/src/main/java/eu/siacs/conversations/xml/Element.java index 8bfef2e91f0de7c5650013c2f7a2febfbbfbe2d3..548e2ca6ded2d5433003cd6bc2283fec3e0c0395 100644 --- a/src/main/java/eu/siacs/conversations/xml/Element.java +++ b/src/main/java/eu/siacs/conversations/xml/Element.java @@ -12,6 +12,7 @@ import eu.siacs.conversations.xmpp.Jid; import im.conversations.android.xmpp.model.stanza.Message; import java.util.ArrayList; import java.util.Collection; +import java.util.Map; import java.util.Hashtable; import java.util.List; import java.util.stream.Collectors; @@ -214,31 +215,34 @@ public class Element implements Node { return toString(ImmutableMap.of()); } - public String toString(final ImmutableMap parentNS) { - final var mutns = new Hashtable<>(parentNS); - final StringBuilder elementOutput = new StringBuilder(); + public void appendToBuilder(final Map parentNS, final StringBuilder elementOutput, final int skipEnd) { + final var mutns = new CopyOnWriteMap<>(parentNS); if (childNodes.size() == 0) { final var attr = getSerializableAttributes(mutns); Tag emptyTag = Tag.empty(name); emptyTag.setAttributes(attr); - elementOutput.append(emptyTag.toString()); + emptyTag.appendToBuilder(elementOutput); } else { - final var ns = ImmutableMap.copyOf(mutns); final var startTag = startTag(mutns); - elementOutput.append(startTag); + startTag.appendToBuilder(elementOutput); for (Node child : ImmutableList.copyOf(childNodes)) { - elementOutput.append(child.toString(ns)); + child.appendToBuilder(mutns.toMap(), elementOutput, Math.max(0, skipEnd - 1)); } - elementOutput.append(endTag()); + if (skipEnd < 1) endTag().appendToBuilder(elementOutput); } + } + + public String toString(final ImmutableMap parentNS) { + final StringBuilder elementOutput = new StringBuilder(); + appendToBuilder(parentNS, elementOutput, 0); return elementOutput.toString(); } public Tag startTag() { - return startTag(new Hashtable<>()); + return startTag(new CopyOnWriteMap<>(new Hashtable<>())); } - public Tag startTag(final Hashtable mutns) { + public Tag startTag(final CopyOnWriteMap mutns) { final var attr = getSerializableAttributes(mutns); final var startTag = Tag.start(name); startTag.setAttributes(attr); @@ -249,8 +253,8 @@ public class Element implements Node { return Tag.end(name); } - protected Hashtable getSerializableAttributes(Hashtable ns) { - final var result = new Hashtable(); + protected Hashtable getSerializableAttributes(CopyOnWriteMap ns) { + final var result = new Hashtable(attributes.size()); for (final var attr : attributes.entrySet()) { if (attr.getKey().charAt(0) == '{') { final var uriIdx = attr.getKey().indexOf('}'); @@ -315,4 +319,36 @@ public class Element implements Node { public String getNamespace() { return getAttribute("xmlns"); } + + static class CopyOnWriteMap { + protected final Map original; + protected Hashtable mut = null; + + public CopyOnWriteMap(Map original) { + this.original = original; + } + + public int size() { + return mut == null ? original.size() : mut.size(); + } + + public boolean containsKey(K k) { + return mut == null ? original.containsKey(k) : mut.containsKey(k); + } + + public V get(K k) { + return mut == null ? original.get(k) : mut.get(k); + } + + public void put(K k, V v) { + if (mut == null) { + mut = new Hashtable<>(original); + } + mut.put(k, v); + } + + public Map toMap() { + return mut == null ? original : mut; + } + } } diff --git a/src/main/java/eu/siacs/conversations/xml/Node.java b/src/main/java/eu/siacs/conversations/xml/Node.java index d06904c27411af9eaca3462b24454f6f5495ab2e..9de77d28d6c9bcde25ef5361a9b82c42da37f3eb 100644 --- a/src/main/java/eu/siacs/conversations/xml/Node.java +++ b/src/main/java/eu/siacs/conversations/xml/Node.java @@ -1,8 +1,10 @@ package eu.siacs.conversations.xml; +import java.util.Map; import com.google.common.collect.ImmutableMap; public interface Node { public String getContent(); public String toString(final ImmutableMap ns); + public void appendToBuilder(final Map ns, final StringBuilder elementOutput, final int skipEnd); } diff --git a/src/main/java/eu/siacs/conversations/xml/Tag.java b/src/main/java/eu/siacs/conversations/xml/Tag.java index 7ea9574f70a39caa7bad51deb7d75b26e11fb871..e5c571e9ae6e7335ebefe59e3285f8892c1ff295 100644 --- a/src/main/java/eu/siacs/conversations/xml/Tag.java +++ b/src/main/java/eu/siacs/conversations/xml/Tag.java @@ -80,9 +80,7 @@ public class Tag { return (this.type == NO); } - @NonNull - public String toString() { - final StringBuilder tagOutput = new StringBuilder(); + public void appendToBuilder(final StringBuilder tagOutput) { tagOutput.append('<'); if (type == END) { tagOutput.append('/'); @@ -94,7 +92,7 @@ public class Tag { tagOutput.append(' '); tagOutput.append(entry.getKey()); tagOutput.append("=\""); - tagOutput.append(XmlHelper.encodeEntities(entry.getValue())); + XmlHelper.appendEncodedEntities(entry.getValue(), tagOutput); tagOutput.append('"'); } } @@ -102,6 +100,12 @@ public class Tag { tagOutput.append('/'); } tagOutput.append('>'); + } + + @NonNull + public String toString() { + final StringBuilder tagOutput = new StringBuilder(); + appendToBuilder(tagOutput); return tagOutput.toString(); } diff --git a/src/main/java/eu/siacs/conversations/xml/TextNode.java b/src/main/java/eu/siacs/conversations/xml/TextNode.java index 4c73002a53b182a061e6f85748fc9213076613eb..299ce5da47e7dba649ae306d682e85a24d25c83a 100644 --- a/src/main/java/eu/siacs/conversations/xml/TextNode.java +++ b/src/main/java/eu/siacs/conversations/xml/TextNode.java @@ -1,5 +1,6 @@ package eu.siacs.conversations.xml; +import java.util.Map; import com.google.common.collect.ImmutableMap; import eu.siacs.conversations.utils.XmlHelper; @@ -16,6 +17,10 @@ public class TextNode implements Node { return content; } + public void appendToBuilder(final Map parentNS, final StringBuilder elementOutput, final int skipEnd) { + XmlHelper.appendEncodedEntities(content, elementOutput); + } + public String toString() { return XmlHelper.encodeEntities(content); }