[MNG-6036] Add namespace to XmlNode (#1318)

This commit is contained in:
Guillaume Nodet 2023-11-28 00:11:34 +01:00 committed by GitHub
parent eee037e676
commit 05fcf5b2d3
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
5 changed files with 252 additions and 83 deletions

View File

@ -30,8 +30,6 @@
/** /**
* An immutable xml node. * An immutable xml node.
* *
* TODO: v4: add support for namespaces
*
* @since 4.0.0 * @since 4.0.0
*/ */
@Experimental @Experimental
@ -82,6 +80,12 @@ public interface XmlNode {
@Nonnull @Nonnull
String getName(); String getName();
@Nonnull
String getNamespaceUri();
@Nonnull
String getPrefix();
@Nullable @Nullable
String getValue(); String getValue();
@ -106,8 +110,6 @@ default XmlNode merge(@Nullable XmlNode source) {
XmlNode merge(@Nullable XmlNode source, @Nullable Boolean childMergeOverride); XmlNode merge(@Nullable XmlNode source, @Nullable Boolean childMergeOverride);
XmlNode clone();
/** /**
* Merge recessive into dominant and return either {@code dominant} * Merge recessive into dominant and return either {@code dominant}
* with merged information or a clone of {@code recessive} if * with merged information or a clone of {@code recessive} if

View File

@ -27,9 +27,13 @@
import java.util.Map; import java.util.Map;
import org.apache.maven.api.model.Model; 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 org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.assertEquals; 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 { class ModelXmlTest {
@ -50,6 +54,38 @@ void testXmlRoundtripWithProperties() throws Exception {
} }
} }
@Test
void testNamespaceInXmlNode() throws XMLStreamException {
String xml = "<project xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\"\n"
+ " xmlns=\"http://maven.apache.org/POM/4.0.0\"\n"
+ " xsi:schemaLocation=\"http://maven.apache.org/POM/4.0.0 http://maven.apache.org/POM/4.0.0\">\n"
+ " <build>\n"
+ " <plugins>\n"
+ " <plugin>\n"
+ " <m:configuration xmlns:m=\"http://maven.apache.org/POM/4.0.0\" xmlns=\"http://fabric8.io/fabric8-maven-plugin\">\n"
+ " <myConfig>foo</myConfig>\n"
+ " </m:configuration>\n"
+ " </plugin>\n"
+ " </plugins>\n"
+ " </build>\n"
+ "</project>";
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 { String toXml(Model model) throws IOException, XMLStreamException {
StringWriter sw = new StringWriter(); StringWriter sw = new StringWriter();
MavenStaxWriter writer = new MavenStaxWriter(); MavenStaxWriter writer = new MavenStaxWriter();

View File

@ -176,8 +176,10 @@ public static XmlNodeImpl build(XMLStreamReader parser, InputLocationBuilderStax
public static XmlNodeImpl build(XMLStreamReader parser, boolean trim, InputLocationBuilderStax locationBuilder) public static XmlNodeImpl build(XMLStreamReader parser, boolean trim, InputLocationBuilderStax locationBuilder)
throws XMLStreamException { throws XMLStreamException {
boolean spacePreserve = false; boolean spacePreserve = false;
String name = null; String lPrefix = null;
String value = null; String lNamespaceUri = null;
String lName = null;
String lValue = null;
Object location = null; Object location = null;
Map<String, String> attrs = null; Map<String, String> attrs = null;
List<XmlNode> children = null; List<XmlNode> children = null;
@ -187,31 +189,29 @@ public static XmlNodeImpl build(XMLStreamReader parser, boolean trim, InputLocat
if (eventType == XMLStreamReader.START_ELEMENT) { if (eventType == XMLStreamReader.START_ELEMENT) {
lastStartTag = parser.getLocation().getLineNumber() * 1000 lastStartTag = parser.getLocation().getLineNumber() * 1000
+ parser.getLocation().getColumnNumber(); + parser.getLocation().getColumnNumber();
if (name == null) { if (lName == null) {
int namespacesSize = parser.getNamespaceCount(); int namespacesSize = parser.getNamespaceCount();
name = parser.getLocalName(); lPrefix = parser.getPrefix();
String pfx = parser.getPrefix(); lNamespaceUri = parser.getNamespaceURI();
if (pfx != null && !pfx.isEmpty()) { lName = parser.getLocalName();
name = pfx + ":" + name;
}
location = locationBuilder != null ? locationBuilder.toInputLocation(parser) : null; location = locationBuilder != null ? locationBuilder.toInputLocation(parser) : null;
int attributesSize = parser.getAttributeCount(); int attributesSize = parser.getAttributeCount();
if (attributesSize > 0 || namespacesSize > 0) { if (attributesSize > 0 || namespacesSize > 0) {
attrs = new HashMap<>(); attrs = new HashMap<>();
for (int i = 0; i < namespacesSize; i++) { for (int i = 0; i < namespacesSize; i++) {
String prefix = parser.getNamespacePrefix(i); String nsPrefix = parser.getNamespacePrefix(i);
String namespace = parser.getNamespaceURI(i); String nsUri = parser.getNamespaceURI(i);
attrs.put(prefix != null && !prefix.isEmpty() ? "xmlns:" + prefix : "xmlns", namespace); attrs.put(nsPrefix != null && !nsPrefix.isEmpty() ? "xmlns:" + nsPrefix : "xmlns", nsUri);
} }
for (int i = 0; i < attributesSize; i++) { for (int i = 0; i < attributesSize; i++) {
String aname = parser.getAttributeLocalName(i); String aName = parser.getAttributeLocalName(i);
String avalue = parser.getAttributeValue(i); String aValue = parser.getAttributeValue(i);
String apfx = parser.getAttributePrefix(i); String aPrefix = parser.getAttributePrefix(i);
if (apfx != null && !apfx.isEmpty()) { if (aPrefix != null && !aPrefix.isEmpty()) {
aname = apfx + ":" + aname; aName = aPrefix + ":" + aName;
} }
attrs.put(aname, avalue); attrs.put(aName, aValue);
spacePreserve = spacePreserve || ("xml:space".equals(aname) && "preserve".equals(avalue)); spacePreserve = spacePreserve || ("xml:space".equals(aName) && "preserve".equals(aValue));
} }
} }
} else { } else {
@ -223,17 +223,19 @@ public static XmlNodeImpl build(XMLStreamReader parser, boolean trim, InputLocat
} }
} else if (eventType == XMLStreamReader.CHARACTERS || eventType == XMLStreamReader.CDATA) { } else if (eventType == XMLStreamReader.CHARACTERS || eventType == XMLStreamReader.CDATA) {
String text = parser.getText(); String text = parser.getText();
value = value != null ? value + text : text; lValue = lValue != null ? lValue + text : text;
} else if (eventType == XMLStreamReader.END_ELEMENT) { } else if (eventType == XMLStreamReader.END_ELEMENT) {
boolean emptyTag = lastStartTag boolean emptyTag = lastStartTag
== parser.getLocation().getLineNumber() * 1000 == parser.getLocation().getLineNumber() * 1000
+ parser.getLocation().getColumnNumber(); + parser.getLocation().getColumnNumber();
if (value != null && trim && !spacePreserve) { if (lValue != null && trim && !spacePreserve) {
value = value.trim(); lValue = lValue.trim();
} }
return new XmlNodeImpl( return new XmlNodeImpl(
name, lPrefix,
children == null ? (value != null ? value : emptyTag ? null : "") : null, lNamespaceUri,
lName,
children == null ? (lValue != null ? lValue : emptyTag ? null : "") : null,
attrs, attrs,
children, children,
location); location);

View File

@ -18,7 +18,8 @@
*/ */
package org.apache.maven.internal.xml; package org.apache.maven.internal.xml;
import java.io.IOException; import javax.xml.stream.XMLStreamException;
import java.io.Serializable; import java.io.Serializable;
import java.io.StringWriter; import java.io.StringWriter;
import java.util.ArrayList; import java.util.ArrayList;
@ -31,14 +32,11 @@
import java.util.Objects; import java.util.Objects;
import java.util.Optional; import java.util.Optional;
import java.util.Set; import java.util.Set;
import java.util.function.Function;
import java.util.stream.Collectors; import java.util.stream.Collectors;
import java.util.stream.Stream; import java.util.stream.Stream;
import org.apache.maven.api.xml.XmlNode; 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. * NOTE: remove all the util code in here when separated, this class should be pure data.
@ -46,6 +44,10 @@
public class XmlNodeImpl implements Serializable, XmlNode { public class XmlNodeImpl implements Serializable, XmlNode {
private static final long serialVersionUID = 2567894443061173996L; private static final long serialVersionUID = 2567894443061173996L;
protected final String prefix;
protected final String namespaceUri;
protected final String name; protected final String name;
protected final String value; protected final String value;
@ -70,6 +72,19 @@ public XmlNodeImpl(XmlNode from, String name) {
public XmlNodeImpl( public XmlNodeImpl(
String name, String value, Map<String, String> attributes, List<XmlNode> children, Object location) { String name, String value, Map<String, String> attributes, List<XmlNode> children, Object location) {
this("", "", name, value, attributes, children, location);
}
public XmlNodeImpl(
String prefix,
String namespaceUri,
String name,
String value,
Map<String, String> attributes,
List<XmlNode> children,
Object location) {
this.prefix = prefix == null ? "" : prefix;
this.namespaceUri = namespaceUri == null ? "" : namespaceUri;
this.name = Objects.requireNonNull(name); this.name = Objects.requireNonNull(name);
this.value = value; this.value = value;
this.attributes = this.attributes =
@ -84,14 +99,21 @@ public XmlNode merge(XmlNode source, Boolean childMergeOverride) {
return merge(this, source, childMergeOverride); return merge(this, source, childMergeOverride);
} }
public XmlNode clone() {
return this;
}
// ---------------------------------------------------------------------- // ----------------------------------------------------------------------
// Name handling // Name handling
// ---------------------------------------------------------------------- // ----------------------------------------------------------------------
@Override
public String getPrefix() {
return prefix;
}
@Override
public String getNamespaceUri() {
return namespaceUri;
}
@Override
public String getName() { public String getName() {
return name; return name;
} }
@ -158,16 +180,6 @@ public Object getInputLocation() {
// Helpers // Helpers
// ---------------------------------------------------------------------- // ----------------------------------------------------------------------
public void writeToSerializer(String namespace, XmlSerializer serializer) throws IOException {
// TODO: WARNING! Later versions of plexus-utils psit out an <?xml ?> 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.<p> * Merges one DOM into another, given a specific algorithm and possible override points for that algorithm.<p>
* The algorithm is as follows: * The algorithm is as follows:
@ -238,7 +250,7 @@ public static XmlNode merge(XmlNode dominant, XmlNode recessive, Boolean childMe
} }
} }
if (recessive.getChildren().size() > 0) { if (!recessive.getChildren().isEmpty()) {
boolean mergeChildren = true; boolean mergeChildren = true;
if (childMergeOverride != null) { if (childMergeOverride != null) {
mergeChildren = childMergeOverride; mergeChildren = childMergeOverride;
@ -256,7 +268,7 @@ public static XmlNode merge(XmlNode dominant, XmlNode recessive, Boolean childMe
List<XmlNode> dominantChildren = dominant.getChildren().stream() List<XmlNode> dominantChildren = dominant.getChildren().stream()
.filter(n -> n.getName().equals(name)) .filter(n -> n.getName().equals(name))
.collect(Collectors.toList()); .collect(Collectors.toList());
if (dominantChildren.size() > 0) { if (!dominantChildren.isEmpty()) {
commonChildren.put(name, dominantChildren.iterator()); commonChildren.put(name, dominantChildren.iterator());
} }
} }
@ -267,7 +279,7 @@ public static XmlNode merge(XmlNode dominant, XmlNode recessive, Boolean childMe
String idValue = recessiveChild.getAttribute(ID_COMBINATION_MODE_ATTRIBUTE); String idValue = recessiveChild.getAttribute(ID_COMBINATION_MODE_ATTRIBUTE);
XmlNode childDom = null; XmlNode childDom = null;
if (isNotEmpty(idValue)) { if (!isEmpty(idValue)) {
for (XmlNode dominantChild : dominant.getChildren()) { for (XmlNode dominantChild : dominant.getChildren()) {
if (idValue.equals(dominantChild.getAttribute(ID_COMBINATION_MODE_ATTRIBUTE))) { if (idValue.equals(dominantChild.getAttribute(ID_COMBINATION_MODE_ATTRIBUTE))) {
childDom = dominantChild; childDom = dominantChild;
@ -275,7 +287,7 @@ public static XmlNode merge(XmlNode dominant, XmlNode recessive, Boolean childMe
mergeChildren = true; mergeChildren = true;
} }
} }
} else if (isNotEmpty(keysValue)) { } else if (!isEmpty(keysValue)) {
String[] keys = keysValue.split(","); String[] keys = keysValue.split(",");
Map<String, Optional<String>> recessiveKeyValues = Stream.of(keys) Map<String, Optional<String>> recessiveKeyValues = Stream.of(keys)
.collect(Collectors.toMap( .collect(Collectors.toMap(
@ -395,23 +407,47 @@ public int hashCode() {
@Override @Override
public String toString() { public String toString() {
try {
return toStringXml();
} catch (XMLStreamException e) {
return toStringObject();
}
}
public String toStringXml() throws XMLStreamException {
StringWriter writer = new StringWriter(); StringWriter writer = new StringWriter();
XmlNodeWriter.write(writer, this); XmlNodeWriter.write(writer, this);
return writer.toString(); return writer.toString();
} }
public String toUnescapedString() { public String toStringObject() {
StringWriter writer = new StringWriter(); StringBuilder sb = new StringBuilder();
XMLWriter xmlWriter = new PrettyPrintXMLWriter(writer); sb.append("XmlNode[");
XmlNodeWriter.write(xmlWriter, this, false); boolean w = false;
return writer.toString(); 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) { private static <T> boolean addToStringField(StringBuilder sb, T o, Function<T, Boolean> p, String n, boolean w) {
return ((str != null) && (str.length() > 0)); 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) { private static boolean isEmpty(String str) {
return ((str == null) || (str.length() == 0)); return str == null || str.isEmpty();
} }
} }

View File

@ -18,49 +18,142 @@
*/ */
package org.apache.maven.internal.xml; 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.io.Writer;
import java.util.Map; import java.util.Map;
import org.apache.maven.api.xml.XmlNode; import org.apache.maven.api.xml.XmlNode;
import org.codehaus.plexus.util.xml.PrettyPrintXMLWriter; import org.codehaus.stax2.util.StreamWriterDelegate;
import org.codehaus.plexus.util.xml.XMLWriter;
/** /**
* *
*/ */
public class XmlNodeWriter { public class XmlNodeWriter {
public static void write(Writer writer, XmlNode dom) { public static void write(Writer writer, XmlNode dom) throws XMLStreamException {
write(new PrettyPrintXMLWriter(writer), dom); 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) { public static void write(XMLStreamWriter xmlWriter, XmlNode dom) throws XMLStreamException {
write(new PrettyPrintXMLWriter(writer), dom); xmlWriter.writeStartElement(dom.getPrefix(), dom.getName(), dom.getNamespaceUri());
}
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());
for (Map.Entry<String, String> attr : dom.getAttributes().entrySet()) { for (Map.Entry<String, String> attr : dom.getAttributes().entrySet()) {
xmlWriter.addAttribute(attr.getKey(), attr.getValue()); xmlWriter.writeAttribute(attr.getKey(), attr.getValue());
} }
for (XmlNode aChildren : dom.getChildren()) { for (XmlNode aChildren : dom.getChildren()) {
write(xmlWriter, aChildren, escape); write(xmlWriter, aChildren);
} }
String value = dom.getValue(); String value = dom.getValue();
if (value != null) { if (value != null) {
if (escape) { xmlWriter.writeCharacters(value);
xmlWriter.writeText(value);
} else {
xmlWriter.writeMarkup(value);
} }
xmlWriter.writeEndElement();
} }
xmlWriter.endElement(); static class IndentingXMLStreamWriter extends StreamWriterDelegate {
int depth = 0;
boolean hasChildren = false;
boolean anew = true;
IndentingXMLStreamWriter(XMLStreamWriter parent) {
super(parent);
}
@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(" ");
}
}
} }
} }