Better handling of namespaced attributes

Stephen Paul Weber created

Don't store in the data model using the prefix -- the prefix is
meaningless.  Store by URI and then generate prefixes when serializing.

Change summary

src/main/java/eu/siacs/conversations/xml/Element.java   | 33 ++++++++++-
src/main/java/eu/siacs/conversations/xml/Node.java      |  3 +
src/main/java/eu/siacs/conversations/xml/TextNode.java  |  6 ++
src/main/java/eu/siacs/conversations/xml/XmlReader.java | 15 ++---
4 files changed, 45 insertions(+), 12 deletions(-)

Detailed changes

src/main/java/eu/siacs/conversations/xml/Element.java 🔗

@@ -4,6 +4,7 @@ import androidx.annotation.NonNull;
 
 import com.google.common.base.Optional;
 import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableMap;
 import com.google.common.primitives.Ints;
 
 import java.util.ArrayList;
@@ -179,17 +180,24 @@ public class Element implements Node {
 	}
 
 	public String toString() {
+		return toString(ImmutableMap.of());
+	}
+
+	public String toString(final ImmutableMap<String, String> parentNS) {
+		final var mutns = new Hashtable<>(parentNS);
+		final var attr = getSerializableAttributes(mutns);
 		final StringBuilder elementOutput = new StringBuilder();
 		if (childNodes.size() == 0) {
 			Tag emptyTag = Tag.empty(name);
-			emptyTag.setAttributes(this.attributes);
+			emptyTag.setAttributes(attr);
 			elementOutput.append(emptyTag.toString());
 		} else {
+			final var ns = ImmutableMap.copyOf(mutns);
 			Tag startTag = Tag.start(name);
-			startTag.setAttributes(this.attributes);
+			startTag.setAttributes(attr);
 			elementOutput.append(startTag);
 			for (Node child : ImmutableList.copyOf(childNodes)) {
-				elementOutput.append(child.toString());
+				elementOutput.append(child.toString(ns));
 			}
 			Tag endTag = Tag.end(name);
 			elementOutput.append(endTag);
@@ -197,6 +205,25 @@ public class Element implements Node {
 		return elementOutput.toString();
 	}
 
+	protected Hashtable<String, String> getSerializableAttributes(Hashtable<String, String> ns) {
+		final var result = new Hashtable<String, String>();
+		for (final var attr : attributes.entrySet()) {
+			if (attr.getKey().charAt(0) == '{') {
+				final var uriIdx = attr.getKey().indexOf('}');
+				final var uri = attr.getKey().substring(1, uriIdx - 1);
+				if (!ns.containsKey(uri)) {
+					result.put("ns" + ns.size() + ":xmlns", uri);
+					ns.put(uri, "ns" + ns.size());
+				}
+				result.put(ns.get(uri) + ":" + attr.getKey().substring(uriIdx + 1), attr.getValue());
+			} else {
+				result.put(attr.getKey(), attr.getValue());
+			}
+		}
+
+		return result;
+	}
+
 	public Element removeAttribute(String name) {
 		this.attributes.remove(name);
 		return this;

src/main/java/eu/siacs/conversations/xml/Node.java 🔗

@@ -1,5 +1,8 @@
 package eu.siacs.conversations.xml;
 
+import com.google.common.collect.ImmutableMap;
+
 public interface Node {
 	public String getContent();
+	public String toString(final ImmutableMap<String, String> ns);
 }

src/main/java/eu/siacs/conversations/xml/TextNode.java 🔗

@@ -1,5 +1,7 @@
 package eu.siacs.conversations.xml;
 
+import com.google.common.collect.ImmutableMap;
+
 import eu.siacs.conversations.utils.XmlHelper;
 
 public class TextNode implements Node {
@@ -17,4 +19,8 @@ public class TextNode implements Node {
 	public String toString() {
 		return XmlHelper.encodeEntities(content);
 	}
+
+	public String toString(final ImmutableMap<String, String> ns) {
+		return toString();
+	}
 }

src/main/java/eu/siacs/conversations/xml/XmlReader.java 🔗

@@ -61,21 +61,18 @@ public class XmlReader implements Closeable {
 					Tag tag = Tag.start(parser.getName());
 					final String xmlns = parser.getNamespace();
 					for (int i = 0; i < parser.getAttributeCount(); ++i) {
-						final String prefix = parser.getAttributePrefix(i);
+						final var prefix = parser.getAttributePrefix(i);
+						final var ns = parser.getAttributeNamespace(i);
 						String name;
-						if (prefix != null && !prefix.isEmpty()) {
-							name = prefix+":"+parser.getAttributeName(i);
+						if ("xml".equals(prefix)) {
+							name = "xml:" + parser.getAttributeName(i);
+						} else if (ns != null && !ns.isEmpty()) {
+							name = "{" + ns + "}" + parser.getAttributeName(i);
 						} else {
 							name = parser.getAttributeName(i);
 						}
 						tag.setAttribute(name,parser.getAttributeValue(i));
 					}
-					int nsStart = parser.getNamespaceCount(parser.getDepth()-1);
-					int nsEnd = parser.getNamespaceCount(parser.getDepth());
-					for (int i = nsStart; i < nsEnd; i++) {
-						final var prefix = parser.getNamespacePrefix(i);
-						tag.setAttribute("xmlns" + (prefix == null ? "" : ":" + prefix), parser.getNamespaceUri(i));
-					}
 					if (xmlns != null) {
 						tag.setAttribute("xmlns", xmlns);
 					}