Speed up Element#toString

Stephen Paul Weber created

Change summary

src/main/java/eu/siacs/conversations/utils/XmlHelper.java | 27 ++++
src/main/java/eu/siacs/conversations/xml/Element.java     | 60 +++++++-
src/main/java/eu/siacs/conversations/xml/Node.java        |  2 
src/main/java/eu/siacs/conversations/xml/Tag.java         | 12 +
src/main/java/eu/siacs/conversations/xml/TextNode.java    |  5 
5 files changed, 90 insertions(+), 16 deletions(-)

Detailed changes

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("&amp;"); break;
+                case '<': sb.append("&lt;"); break;
+                case '>': sb.append("&gt;"); break;
+                case '"': sb.append("&quot;"); break;
+                case '\'': sb.append("&apos;"); break;
+                default: sb.append(c);
+                }
+            }
+        } else {
+            sb.append(content);
+        }
+    }
+
     public static String printElementNames(final Element element) {
         final List<String> features =
                 element == null

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<String, String> parentNS) {
-		final var mutns = new Hashtable<>(parentNS);
-		final StringBuilder elementOutput = new StringBuilder();
+	public void appendToBuilder(final Map<String, String> 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<String, String> 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<String, String> mutns) {
+	public Tag startTag(final CopyOnWriteMap<String, String> 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<String, String> getSerializableAttributes(Hashtable<String, String> ns) {
-		final var result = new Hashtable<String, String>();
+	protected Hashtable<String, String> getSerializableAttributes(CopyOnWriteMap<String, String> ns) {
+		final var result = new Hashtable<String, String>(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<K,V> {
+		protected final Map<K,V> original;
+		protected Hashtable<K,V> mut = null;
+
+		public CopyOnWriteMap(Map<K,V> 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<K, V> toMap() {
+			return mut == null ? original : mut;
+		}
+	}
 }

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<String, String> ns);
+	public void appendToBuilder(final Map<String, String> ns, final StringBuilder elementOutput, final int skipEnd);
 }

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();
     }
 

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<String, String> parentNS, final StringBuilder elementOutput, final int skipEnd) {
+		XmlHelper.appendEncodedEntities(content, elementOutput);
+	}
+
 	public String toString() {
 		return XmlHelper.encodeEntities(content);
 	}