~singpolyma/cheogram-android

758c85a1c9d85600de84b961d246a784511b1916 — Stephen Paul Weber 1 year, 1 month ago d79ec4d
Preserve Text Nodes in XML

The XML model that was being used was not able to represent elements with text
nodes and element children interspersed (such as in "markup like" XML:
<x>abc <y>def</y> ghi</x>).  This switches from a single content String to a
list of child Node.  Content is pulled from all children recursively.  A list of
Element children is also maintained since this list is frequently traversed so
it saves runtime checks in all of those loops at the expense of a small amount
of memory.  Since the children and childNode lists must be kept in sync, they
are both made private to avoid a child class trying to mutate one of them
without a safe helper.
M src/main/java/eu/siacs/conversations/xml/Element.java => src/main/java/eu/siacs/conversations/xml/Element.java +41 -19
@@ 3,19 3,21 @@ package eu.siacs.conversations.xml;
import org.jetbrains.annotations.NotNull;

import java.util.ArrayList;
import java.util.Collection;
import java.util.Hashtable;
import java.util.List;
import java.util.stream.Collectors;

import eu.siacs.conversations.utils.XmlHelper;
import eu.siacs.conversations.xmpp.InvalidJid;
import eu.siacs.conversations.xmpp.Jid;
import eu.siacs.conversations.xmpp.stanzas.MessagePacket;

public class Element {
public class Element implements Node {
	private final String name;
	private Hashtable<String, String> attributes = new Hashtable<>();
	private String content;
	protected List<Element> children = new ArrayList<>();
	private List<Element> children = new ArrayList<>();
	private List<Node> childNodes = new ArrayList<>();

	public Element(String name) {
		this.name = name;


@@ 26,30 28,52 @@ public class Element {
		this.setAttribute("xmlns", xmlns);
	}

	public Element addChild(Element child) {
		this.content = null;
		children.add(child);
	public Node prependChild(Node child) {
		childNodes.add(0, child);
		if (child instanceof Element) children.add(0, (Element) child);
		return child;
	}

	public Node addChild(Node child) {
		childNodes.add(child);
		if (child instanceof Element) children.add((Element) child);
		return child;
	}

	public Element addChild(String name) {
		this.content = null;
		Element child = new Element(name);
		childNodes.add(child);
		children.add(child);
		return child;
	}

	public Element addChild(String name, String xmlns) {
		this.content = null;
		Element child = new Element(name);
		child.setAttribute("xmlns", xmlns);
		childNodes.add(child);
		children.add(child);
		return child;
	}

	public void addChildren(final Collection<? extends Node> children) {
		if (children == null) return;

		this.childNodes.addAll(children);
		for (Node node : children) {
			if (node instanceof Element) {
				this.children.add((Element) node);
			}
		}
	}

	public void removeChild(Node child) {
		this.childNodes.remove(child);
		if (child instanceof Element) this.children.remove(child);
	}

	public Element setContent(String content) {
		this.content = content;
		this.children.clear();
		clearChildren();
		if (content != null) this.childNodes.add(new TextNode(content));
		return this;
	}



@@ 106,17 130,18 @@ public class Element {
		return findChild(name, xmlns) != null;
	}

	public List<Element> getChildren() {
	public final List<Element> getChildren() {
		return this.children;
	}

	public Element setChildren(List<Element> children) {
		this.childNodes = new ArrayList(children);
		this.children = children;
		return this;
	}

	public final String getContent() {
		return content;
		return this.childNodes.stream().map(Node::getContent).filter(c -> c != null).collect(Collectors.joining());
	}

	public Element setAttribute(String name, String value) {


@@ 170,7 195,7 @@ public class Element {
	@NotNull
	public String toString() {
		final StringBuilder elementOutput = new StringBuilder();
		if ((content == null) && (children.size() == 0)) {
		if (childNodes.size() == 0) {
			Tag emptyTag = Tag.empty(name);
			emptyTag.setAtttributes(this.attributes);
			elementOutput.append(emptyTag.toString());


@@ 178,12 203,8 @@ public class Element {
			Tag startTag = Tag.start(name);
			startTag.setAtttributes(this.attributes);
			elementOutput.append(startTag);
			if (content != null) {
				elementOutput.append(XmlHelper.encodeEntities(content));
			} else {
				for (Element child : children) {
					elementOutput.append(child.toString());
				}
			for (Node child : childNodes) {
				elementOutput.append(child.toString());
			}
			Tag endTag = Tag.end(name);
			elementOutput.append(endTag);


@@ 197,6 218,7 @@ public class Element {

	public void clearChildren() {
		this.children.clear();
		this.childNodes.clear();
	}

	public void setAttribute(String name, long value) {

M src/main/java/eu/siacs/conversations/xml/LocalizedContent.java => src/main/java/eu/siacs/conversations/xml/LocalizedContent.java +1 -1
@@ 23,7 23,7 @@ public class LocalizedContent {
    public static LocalizedContent get(final Element element, String name) {
        final HashMap<String, String> contents = new HashMap<>();
        final String parentLanguage = element.getAttribute("xml:lang");
        for(Element child : element.children) {
        for(Element child : element.getChildren()) {
            if (name.equals(child.getName())) {
                final String namespace = child.getNamespace();
                final String childLanguage = child.getAttribute("xml:lang");

A src/main/java/eu/siacs/conversations/xml/Node.java => src/main/java/eu/siacs/conversations/xml/Node.java +5 -0
@@ 0,0 1,5 @@
package eu.siacs.conversations.xml;

public interface Node {
	public String getContent();
}

A src/main/java/eu/siacs/conversations/xml/TextNode.java => src/main/java/eu/siacs/conversations/xml/TextNode.java +20 -0
@@ 0,0 1,20 @@
package eu.siacs.conversations.xml;

import eu.siacs.conversations.utils.XmlHelper;

public class TextNode implements Node {
	protected String content;

	public TextNode(final String content) {
		if (content == null) throw new IllegalArgumentException("null TextNode is not allowed");
		this.content = content;
	}

	public String getContent() {
		return content;
	}

	public String toString() {
		return XmlHelper.encodeEntities(content);
	}
}

M src/main/java/eu/siacs/conversations/xml/XmlReader.java => src/main/java/eu/siacs/conversations/xml/XmlReader.java +3 -8
@@ 94,15 94,10 @@ public class XmlReader implements Closeable {
		if (nextTag == null) {
			throw new IOException("interrupted mid tag");
		}
		if (nextTag.isNo()) {
			element.setContent(nextTag.getName());
			nextTag = this.readTag();
			if (nextTag == null) {
				throw new IOException("interrupted mid tag");
			}
		}
		while (!nextTag.isEnd(element.getName())) {
			if (!nextTag.isNo()) {
			if (nextTag.isNo()) {
				element.addChild(new TextNode(nextTag.getName()));
			} else {
				Element child = this.readElement(nextTag);
				element.addChild(child);
			}

M src/main/java/eu/siacs/conversations/xmpp/forms/Data.java => src/main/java/eu/siacs/conversations/xmpp/forms/Data.java +2 -7
@@ 4,8 4,8 @@ import android.os.Bundle;

import java.util.ArrayList;
import java.util.Collection;
import java.util.Iterator;
import java.util.List;
import java.util.stream.Collectors;

import eu.siacs.conversations.xml.Element;
import eu.siacs.conversations.xml.Namespace;


@@ 77,12 77,7 @@ public class Data extends Element {
	}

	private void removeUnnecessaryChildren() {
		for(Iterator<Element> iterator = this.children.iterator(); iterator.hasNext();) {
			Element element = iterator.next();
			if (!element.getName().equals("field") && !element.getName().equals("title")) {
				iterator.remove();
			}
		}
		setChildren(getChildren().stream().filter(element -> element.getName().equals("field") || element.getName().equals("title")).collect(Collectors.toList()));
	}

	public static Data parse(Element element) {

M src/main/java/eu/siacs/conversations/xmpp/forms/Field.java => src/main/java/eu/siacs/conversations/xmpp/forms/Field.java +4 -13
@@ 2,8 2,8 @@ package eu.siacs.conversations.xmpp.forms;

import java.util.ArrayList;
import java.util.Collection;
import java.util.Iterator;
import java.util.List;
import java.util.stream.Collectors;

import eu.siacs.conversations.xml.Element;



@@ 23,24 23,15 @@ public class Field extends Element {
	}

	public void setValue(String value) {
		this.children.clear();
		this.addChild("value").setContent(value);
		setChildren(List.of(new Element("value").setContent(value)));
	}

	public void setValues(Collection<String> values) {
		this.children.clear();
		for(String value : values) {
			this.addChild("value").setContent(value);
		}
		setChildren(values.stream().map(val -> new Element("value").setContent(val)).collect(Collectors.toList()));
	}

	public void removeNonValueChildren() {
		for(Iterator<Element> iterator = this.children.iterator(); iterator.hasNext();) {
			Element element = iterator.next();
			if (!element.getName().equals("value")) {
				iterator.remove();
			}
		}
		setChildren(getChildren().stream().filter(element -> element.getName().equals("value")).collect(Collectors.toList()));
	}

	public static Field parse(Element element) {

M src/main/java/eu/siacs/conversations/xmpp/jingle/stanzas/Group.java => src/main/java/eu/siacs/conversations/xmpp/jingle/stanzas/Group.java +1 -1
@@ 29,7 29,7 @@ public class Group extends Element {

    public List<String> getIdentificationTags() {
        final ImmutableList.Builder<String> builder = new ImmutableList.Builder<>();
        for (final Element child : this.children) {
        for (final Element child : getChildren()) {
            if ("content".equals(child.getName())) {
                final String name = child.getAttribute("name");
                if (name != null) {

M src/main/java/eu/siacs/conversations/xmpp/jingle/stanzas/Propose.java => src/main/java/eu/siacs/conversations/xmpp/jingle/stanzas/Propose.java +1 -1
@@ 15,7 15,7 @@ public class Propose extends Element {

    public List<GenericDescription> getDescriptions() {
        final ImmutableList.Builder<GenericDescription> builder = new ImmutableList.Builder<>();
        for (final Element child : this.children) {
        for (final Element child : getChildren()) {
            if ("description".equals(child.getName())) {
                final String namespace = child.getNamespace();
                if (FileTransferDescription.NAMESPACES.contains(namespace)) {

M src/main/java/eu/siacs/conversations/xmpp/jingle/stanzas/RtpDescription.java => src/main/java/eu/siacs/conversations/xmpp/jingle/stanzas/RtpDescription.java +5 -19
@@ 66,7 66,7 @@ public class RtpDescription extends GenericDescription {

    public List<Source> getSources() {
        final ImmutableList.Builder<Source> builder = new ImmutableList.Builder<>();
        for (final Element child : this.children) {
        for (final Element child : getChildren()) {
            if ("source".equals(child.getName()) && Namespace.JINGLE_RTP_SOURCE_SPECIFIC_MEDIA_ATTRIBUTES.equals(child.getNamespace())) {
                builder.add(Source.upgrade(child));
            }


@@ 76,7 76,7 @@ public class RtpDescription extends GenericDescription {

    public List<SourceGroup> getSourceGroups() {
        final ImmutableList.Builder<SourceGroup> builder = new ImmutableList.Builder<>();
        for (final Element child : this.children) {
        for (final Element child : getChildren()) {
            if ("ssrc-group".equals(child.getName()) && Namespace.JINGLE_RTP_SOURCE_SPECIFIC_MEDIA_ATTRIBUTES.equals(child.getNamespace())) {
                builder.add(SourceGroup.upgrade(child));
            }


@@ 326,16 326,8 @@ public class RtpDescription extends GenericDescription {
            return null;
        }

        public void addChildren(final List<Element> children) {
            if (children != null) {
                this.children.addAll(children);
            }
        }

        public void addParameters(List<Parameter> parameters) {
            if (parameters != null) {
                this.children.addAll(parameters);
            }
            addChildren(parameters);
        }
    }



@@ 442,7 434,7 @@ public class RtpDescription extends GenericDescription {

        public List<Parameter> getParameters() {
            ImmutableList.Builder<Parameter> builder = new ImmutableList.Builder<>();
            for (Element child : this.children) {
            for (Element child : getChildren()) {
                if ("parameter".equals(child.getName())) {
                    builder.add(Parameter.upgrade(child));
                }


@@ 512,7 504,7 @@ public class RtpDescription extends GenericDescription {

        public List<String> getSsrcs() {
            ImmutableList.Builder<String> builder = new ImmutableList.Builder<>();
            for (Element child : this.children) {
            for (Element child : getChildren()) {
                if ("source".equals(child.getName())) {
                    final String ssrc = child.getAttribute("ssrc");
                    if (ssrc != null) {


@@ 610,10 602,4 @@ public class RtpDescription extends GenericDescription {
        }
        return rtpDescription;
    }

    private void addChildren(List<Element> elements) {
        if (elements != null) {
            this.children.addAll(elements);
        }
    }
}

M src/main/java/eu/siacs/conversations/xmpp/stanzas/MessagePacket.java => src/main/java/eu/siacs/conversations/xmpp/stanzas/MessagePacket.java +4 -6
@@ 22,15 22,13 @@ public class MessagePacket extends AbstractAcknowledgeableStanza {
	}

	public void setBody(String text) {
		this.children.remove(findChild("body"));
		Element body = new Element("body");
		body.setContent(text);
		this.children.add(0, body);
		removeChild(findChild("body"));
		prependChild(new Element("body").setContent(text));
	}

	public void setAxolotlMessage(Element axolotlMessage) {
		this.children.remove(findChild("body"));
		this.children.add(0, axolotlMessage);
		removeChild(findChild("body"));
		prependChild(axolotlMessage);
	}

	public void setType(int type) {