From 05fcf5b2d3b529385563619d0693eb4a2b7847d0 Mon Sep 17 00:00:00 2001 From: Guillaume Nodet Date: Tue, 28 Nov 2023 00:11:34 +0100 Subject: [PATCH] [MNG-6036] Add namespace to XmlNode (#1318) --- .../org/apache/maven/api/xml/XmlNode.java | 10 +- .../apache/maven/model/v4/ModelXmlTest.java | 36 +++++ .../maven/internal/xml/XmlNodeBuilder.java | 48 +++--- .../maven/internal/xml/XmlNodeImpl.java | 98 ++++++++---- .../maven/internal/xml/XmlNodeWriter.java | 143 +++++++++++++++--- 5 files changed, 252 insertions(+), 83 deletions(-) diff --git a/api/maven-api-xml/src/main/java/org/apache/maven/api/xml/XmlNode.java b/api/maven-api-xml/src/main/java/org/apache/maven/api/xml/XmlNode.java index df6335ee8d..87ad7ae7e3 100644 --- a/api/maven-api-xml/src/main/java/org/apache/maven/api/xml/XmlNode.java +++ b/api/maven-api-xml/src/main/java/org/apache/maven/api/xml/XmlNode.java @@ -30,8 +30,6 @@ import org.apache.maven.api.annotations.ThreadSafe; /** * An immutable xml node. * - * TODO: v4: add support for namespaces - * * @since 4.0.0 */ @Experimental @@ -82,6 +80,12 @@ public interface XmlNode { @Nonnull String getName(); + @Nonnull + String getNamespaceUri(); + + @Nonnull + String getPrefix(); + @Nullable String getValue(); @@ -106,8 +110,6 @@ public interface XmlNode { XmlNode merge(@Nullable XmlNode source, @Nullable Boolean childMergeOverride); - XmlNode clone(); - /** * Merge recessive into dominant and return either {@code dominant} * with merged information or a clone of {@code recessive} if diff --git a/maven-model/src/test/java/org/apache/maven/model/v4/ModelXmlTest.java b/maven-model/src/test/java/org/apache/maven/model/v4/ModelXmlTest.java index 682ff7d238..ece1d046ea 100644 --- a/maven-model/src/test/java/org/apache/maven/model/v4/ModelXmlTest.java +++ b/maven-model/src/test/java/org/apache/maven/model/v4/ModelXmlTest.java @@ -27,9 +27,13 @@ import java.util.LinkedHashMap; import java.util.Map; import org.apache.maven.api.model.Model; +import org.apache.maven.api.model.Plugin; +import org.apache.maven.api.xml.XmlNode; import org.junit.jupiter.api.Test; import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNotNull; class ModelXmlTest { @@ -50,6 +54,38 @@ class ModelXmlTest { } } + @Test + void testNamespaceInXmlNode() throws XMLStreamException { + String xml = "\n" + + " \n" + + " \n" + + " \n" + + " \n" + + " foo\n" + + " \n" + + " \n" + + " \n" + + " \n" + + ""; + + Model model = fromXml(xml); + Plugin plugin = model.getBuild().getPlugins().get(0); + XmlNode node = plugin.getConfiguration(); + assertNotNull(node); + assertEquals("http://maven.apache.org/POM/4.0.0", node.getNamespaceUri()); + assertEquals("m", node.getPrefix()); + assertEquals("configuration", node.getName()); + assertEquals(1, node.getChildren().size()); + XmlNode myConfig = node.getChildren().get(0); + assertEquals("http://fabric8.io/fabric8-maven-plugin", myConfig.getNamespaceUri()); + assertEquals("", myConfig.getPrefix()); + assertEquals("myConfig", myConfig.getName()); + String config = node.toString(); + assertFalse(config.isEmpty()); + } + String toXml(Model model) throws IOException, XMLStreamException { StringWriter sw = new StringWriter(); MavenStaxWriter writer = new MavenStaxWriter(); diff --git a/maven-xml-impl/src/main/java/org/apache/maven/internal/xml/XmlNodeBuilder.java b/maven-xml-impl/src/main/java/org/apache/maven/internal/xml/XmlNodeBuilder.java index 75a83d0a1b..91b199ee96 100644 --- a/maven-xml-impl/src/main/java/org/apache/maven/internal/xml/XmlNodeBuilder.java +++ b/maven-xml-impl/src/main/java/org/apache/maven/internal/xml/XmlNodeBuilder.java @@ -176,8 +176,10 @@ public class XmlNodeBuilder { public static XmlNodeImpl build(XMLStreamReader parser, boolean trim, InputLocationBuilderStax locationBuilder) throws XMLStreamException { boolean spacePreserve = false; - String name = null; - String value = null; + String lPrefix = null; + String lNamespaceUri = null; + String lName = null; + String lValue = null; Object location = null; Map attrs = null; List children = null; @@ -187,31 +189,29 @@ public class XmlNodeBuilder { if (eventType == XMLStreamReader.START_ELEMENT) { lastStartTag = parser.getLocation().getLineNumber() * 1000 + parser.getLocation().getColumnNumber(); - if (name == null) { + if (lName == null) { int namespacesSize = parser.getNamespaceCount(); - name = parser.getLocalName(); - String pfx = parser.getPrefix(); - if (pfx != null && !pfx.isEmpty()) { - name = pfx + ":" + name; - } + lPrefix = parser.getPrefix(); + lNamespaceUri = parser.getNamespaceURI(); + lName = parser.getLocalName(); location = locationBuilder != null ? locationBuilder.toInputLocation(parser) : null; int attributesSize = parser.getAttributeCount(); if (attributesSize > 0 || namespacesSize > 0) { attrs = new HashMap<>(); for (int i = 0; i < namespacesSize; i++) { - String prefix = parser.getNamespacePrefix(i); - String namespace = parser.getNamespaceURI(i); - attrs.put(prefix != null && !prefix.isEmpty() ? "xmlns:" + prefix : "xmlns", namespace); + String nsPrefix = parser.getNamespacePrefix(i); + String nsUri = parser.getNamespaceURI(i); + attrs.put(nsPrefix != null && !nsPrefix.isEmpty() ? "xmlns:" + nsPrefix : "xmlns", nsUri); } for (int i = 0; i < attributesSize; i++) { - String aname = parser.getAttributeLocalName(i); - String avalue = parser.getAttributeValue(i); - String apfx = parser.getAttributePrefix(i); - if (apfx != null && !apfx.isEmpty()) { - aname = apfx + ":" + aname; + String aName = parser.getAttributeLocalName(i); + String aValue = parser.getAttributeValue(i); + String aPrefix = parser.getAttributePrefix(i); + if (aPrefix != null && !aPrefix.isEmpty()) { + aName = aPrefix + ":" + aName; } - attrs.put(aname, avalue); - spacePreserve = spacePreserve || ("xml:space".equals(aname) && "preserve".equals(avalue)); + attrs.put(aName, aValue); + spacePreserve = spacePreserve || ("xml:space".equals(aName) && "preserve".equals(aValue)); } } } else { @@ -223,17 +223,19 @@ public class XmlNodeBuilder { } } else if (eventType == XMLStreamReader.CHARACTERS || eventType == XMLStreamReader.CDATA) { String text = parser.getText(); - value = value != null ? value + text : text; + lValue = lValue != null ? lValue + text : text; } else if (eventType == XMLStreamReader.END_ELEMENT) { boolean emptyTag = lastStartTag == parser.getLocation().getLineNumber() * 1000 + parser.getLocation().getColumnNumber(); - if (value != null && trim && !spacePreserve) { - value = value.trim(); + if (lValue != null && trim && !spacePreserve) { + lValue = lValue.trim(); } return new XmlNodeImpl( - name, - children == null ? (value != null ? value : emptyTag ? null : "") : null, + lPrefix, + lNamespaceUri, + lName, + children == null ? (lValue != null ? lValue : emptyTag ? null : "") : null, attrs, children, location); diff --git a/maven-xml-impl/src/main/java/org/apache/maven/internal/xml/XmlNodeImpl.java b/maven-xml-impl/src/main/java/org/apache/maven/internal/xml/XmlNodeImpl.java index f405112791..1210b81320 100644 --- a/maven-xml-impl/src/main/java/org/apache/maven/internal/xml/XmlNodeImpl.java +++ b/maven-xml-impl/src/main/java/org/apache/maven/internal/xml/XmlNodeImpl.java @@ -18,7 +18,8 @@ */ package org.apache.maven.internal.xml; -import java.io.IOException; +import javax.xml.stream.XMLStreamException; + import java.io.Serializable; import java.io.StringWriter; import java.util.ArrayList; @@ -31,14 +32,11 @@ import java.util.Map; import java.util.Objects; import java.util.Optional; import java.util.Set; +import java.util.function.Function; import java.util.stream.Collectors; import java.util.stream.Stream; import org.apache.maven.api.xml.XmlNode; -import org.codehaus.plexus.util.xml.PrettyPrintXMLWriter; -import org.codehaus.plexus.util.xml.SerializerXMLWriter; -import org.codehaus.plexus.util.xml.XMLWriter; -import org.codehaus.plexus.util.xml.pull.XmlSerializer; /** * NOTE: remove all the util code in here when separated, this class should be pure data. @@ -46,6 +44,10 @@ import org.codehaus.plexus.util.xml.pull.XmlSerializer; public class XmlNodeImpl implements Serializable, XmlNode { private static final long serialVersionUID = 2567894443061173996L; + protected final String prefix; + + protected final String namespaceUri; + protected final String name; protected final String value; @@ -70,6 +72,19 @@ public class XmlNodeImpl implements Serializable, XmlNode { public XmlNodeImpl( String name, String value, Map attributes, List children, Object location) { + this("", "", name, value, attributes, children, location); + } + + public XmlNodeImpl( + String prefix, + String namespaceUri, + String name, + String value, + Map attributes, + List children, + Object location) { + this.prefix = prefix == null ? "" : prefix; + this.namespaceUri = namespaceUri == null ? "" : namespaceUri; this.name = Objects.requireNonNull(name); this.value = value; this.attributes = @@ -84,14 +99,21 @@ public class XmlNodeImpl implements Serializable, XmlNode { return merge(this, source, childMergeOverride); } - public XmlNode clone() { - return this; - } - // ---------------------------------------------------------------------- // Name handling // ---------------------------------------------------------------------- + @Override + public String getPrefix() { + return prefix; + } + + @Override + public String getNamespaceUri() { + return namespaceUri; + } + + @Override public String getName() { return name; } @@ -158,16 +180,6 @@ public class XmlNodeImpl implements Serializable, XmlNode { // Helpers // ---------------------------------------------------------------------- - public void writeToSerializer(String namespace, XmlSerializer serializer) throws IOException { - // TODO: WARNING! Later versions of plexus-utils psit out an header due to thinking this is a new - // document - not the desired behaviour! - SerializerXMLWriter xmlWriter = new SerializerXMLWriter(namespace, serializer); - XmlNodeWriter.write(xmlWriter, this); - if (xmlWriter.getExceptions().size() > 0) { - throw (IOException) xmlWriter.getExceptions().get(0); - } - } - /** * Merges one DOM into another, given a specific algorithm and possible override points for that algorithm.

* The algorithm is as follows: @@ -238,7 +250,7 @@ public class XmlNodeImpl implements Serializable, XmlNode { } } - if (recessive.getChildren().size() > 0) { + if (!recessive.getChildren().isEmpty()) { boolean mergeChildren = true; if (childMergeOverride != null) { mergeChildren = childMergeOverride; @@ -256,7 +268,7 @@ public class XmlNodeImpl implements Serializable, XmlNode { List dominantChildren = dominant.getChildren().stream() .filter(n -> n.getName().equals(name)) .collect(Collectors.toList()); - if (dominantChildren.size() > 0) { + if (!dominantChildren.isEmpty()) { commonChildren.put(name, dominantChildren.iterator()); } } @@ -267,7 +279,7 @@ public class XmlNodeImpl implements Serializable, XmlNode { String idValue = recessiveChild.getAttribute(ID_COMBINATION_MODE_ATTRIBUTE); XmlNode childDom = null; - if (isNotEmpty(idValue)) { + if (!isEmpty(idValue)) { for (XmlNode dominantChild : dominant.getChildren()) { if (idValue.equals(dominantChild.getAttribute(ID_COMBINATION_MODE_ATTRIBUTE))) { childDom = dominantChild; @@ -275,7 +287,7 @@ public class XmlNodeImpl implements Serializable, XmlNode { mergeChildren = true; } } - } else if (isNotEmpty(keysValue)) { + } else if (!isEmpty(keysValue)) { String[] keys = keysValue.split(","); Map> recessiveKeyValues = Stream.of(keys) .collect(Collectors.toMap( @@ -395,23 +407,47 @@ public class XmlNodeImpl implements Serializable, XmlNode { @Override public String toString() { + try { + return toStringXml(); + } catch (XMLStreamException e) { + return toStringObject(); + } + } + + public String toStringXml() throws XMLStreamException { StringWriter writer = new StringWriter(); XmlNodeWriter.write(writer, this); return writer.toString(); } - public String toUnescapedString() { - StringWriter writer = new StringWriter(); - XMLWriter xmlWriter = new PrettyPrintXMLWriter(writer); - XmlNodeWriter.write(xmlWriter, this, false); - return writer.toString(); + public String toStringObject() { + StringBuilder sb = new StringBuilder(); + sb.append("XmlNode["); + boolean w = false; + w = addToStringField(sb, prefix, o -> !o.isEmpty(), "prefix", w); + w = addToStringField(sb, namespaceUri, o -> !o.isEmpty(), "namespaceUri", w); + w = addToStringField(sb, name, o -> !o.isEmpty(), "name", w); + w = addToStringField(sb, value, o -> !o.isEmpty(), "value", w); + w = addToStringField(sb, attributes, o -> !o.isEmpty(), "attributes", w); + w = addToStringField(sb, children, o -> !o.isEmpty(), "children", w); + w = addToStringField(sb, location, Objects::nonNull, "location", w); + sb.append("]"); + return sb.toString(); } - private static boolean isNotEmpty(String str) { - return ((str != null) && (str.length() > 0)); + private static boolean addToStringField(StringBuilder sb, T o, Function p, String n, boolean w) { + if (!p.apply(o)) { + if (w) { + sb.append(", "); + } else { + w = true; + } + sb.append(n).append("='").append(o).append('\''); + } + return w; } private static boolean isEmpty(String str) { - return ((str == null) || (str.length() == 0)); + return str == null || str.isEmpty(); } } diff --git a/maven-xml-impl/src/main/java/org/apache/maven/internal/xml/XmlNodeWriter.java b/maven-xml-impl/src/main/java/org/apache/maven/internal/xml/XmlNodeWriter.java index 812d7d8d4f..ee50fca1da 100644 --- a/maven-xml-impl/src/main/java/org/apache/maven/internal/xml/XmlNodeWriter.java +++ b/maven-xml-impl/src/main/java/org/apache/maven/internal/xml/XmlNodeWriter.java @@ -18,49 +18,142 @@ */ package org.apache.maven.internal.xml; -import java.io.PrintWriter; +import javax.xml.stream.XMLOutputFactory; +import javax.xml.stream.XMLStreamException; +import javax.xml.stream.XMLStreamWriter; + import java.io.Writer; import java.util.Map; import org.apache.maven.api.xml.XmlNode; -import org.codehaus.plexus.util.xml.PrettyPrintXMLWriter; -import org.codehaus.plexus.util.xml.XMLWriter; +import org.codehaus.stax2.util.StreamWriterDelegate; /** * */ public class XmlNodeWriter { - public static void write(Writer writer, XmlNode dom) { - write(new PrettyPrintXMLWriter(writer), dom); + public static void write(Writer writer, XmlNode dom) throws XMLStreamException { + XMLOutputFactory factory = new com.ctc.wstx.stax.WstxOutputFactory(); + factory.setProperty(XMLOutputFactory.IS_REPAIRING_NAMESPACES, false); + factory.setProperty(com.ctc.wstx.api.WstxOutputProperties.P_USE_DOUBLE_QUOTES_IN_XML_DECL, true); + factory.setProperty(com.ctc.wstx.api.WstxOutputProperties.P_ADD_SPACE_AFTER_EMPTY_ELEM, true); + XMLStreamWriter serializer = new IndentingXMLStreamWriter(factory.createXMLStreamWriter(writer)); + write(serializer, dom); + serializer.close(); } - public static void write(PrintWriter writer, XmlNode dom) { - write(new PrettyPrintXMLWriter(writer), dom); - } - - public static void write(XMLWriter xmlWriter, XmlNode dom) { - write(xmlWriter, dom, true); - } - - public static void write(XMLWriter xmlWriter, XmlNode dom, boolean escape) { - // TODO: move to XMLWriter? - xmlWriter.startElement(dom.getName()); + public static void write(XMLStreamWriter xmlWriter, XmlNode dom) throws XMLStreamException { + xmlWriter.writeStartElement(dom.getPrefix(), dom.getName(), dom.getNamespaceUri()); for (Map.Entry attr : dom.getAttributes().entrySet()) { - xmlWriter.addAttribute(attr.getKey(), attr.getValue()); + xmlWriter.writeAttribute(attr.getKey(), attr.getValue()); } for (XmlNode aChildren : dom.getChildren()) { - write(xmlWriter, aChildren, escape); + write(xmlWriter, aChildren); } - String value = dom.getValue(); if (value != null) { - if (escape) { - xmlWriter.writeText(value); - } else { - xmlWriter.writeMarkup(value); - } + xmlWriter.writeCharacters(value); + } + xmlWriter.writeEndElement(); + } + + static class IndentingXMLStreamWriter extends StreamWriterDelegate { + + int depth = 0; + boolean hasChildren = false; + boolean anew = true; + + IndentingXMLStreamWriter(XMLStreamWriter parent) { + super(parent); } - xmlWriter.endElement(); + @Override + public void writeStartDocument() throws XMLStreamException { + super.writeStartDocument(); + anew = false; + } + + @Override + public void writeStartDocument(String version) throws XMLStreamException { + super.writeStartDocument(version); + anew = false; + } + + @Override + public void writeStartDocument(String encoding, String version) throws XMLStreamException { + super.writeStartDocument(encoding, version); + anew = false; + } + + @Override + public void writeEmptyElement(String localName) throws XMLStreamException { + indent(); + super.writeEmptyElement(localName); + hasChildren = true; + anew = false; + } + + @Override + public void writeEmptyElement(String namespaceURI, String localName) throws XMLStreamException { + indent(); + super.writeEmptyElement(namespaceURI, localName); + hasChildren = true; + anew = false; + } + + @Override + public void writeEmptyElement(String prefix, String localName, String namespaceURI) throws XMLStreamException { + indent(); + super.writeEmptyElement(prefix, localName, namespaceURI); + hasChildren = true; + anew = false; + } + + @Override + public void writeStartElement(String localName) throws XMLStreamException { + indent(); + super.writeStartElement(localName); + depth++; + hasChildren = false; + anew = false; + } + + @Override + public void writeStartElement(String namespaceURI, String localName) throws XMLStreamException { + indent(); + super.writeStartElement(namespaceURI, localName); + depth++; + hasChildren = false; + anew = false; + } + + @Override + public void writeStartElement(String prefix, String localName, String namespaceURI) throws XMLStreamException { + indent(); + super.writeStartElement(prefix, localName, namespaceURI); + depth++; + hasChildren = false; + anew = false; + } + + @Override + public void writeEndElement() throws XMLStreamException { + depth--; + if (hasChildren) { + indent(); + } + super.writeEndElement(); + hasChildren = true; + anew = false; + } + + private void indent() throws XMLStreamException { + if (!anew) { + super.writeCharacters("\n"); + } + for (int i = 0; i < depth; i++) { + super.writeCharacters(" "); + } + } } }