Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions release-notes/CREDITS
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,11 @@ Jiri Mikulasek (@jimirocks)
* Reported #455: Can't deserialize list in JsonSubtype when type property is visible
(3.2.0)

Sam Kruglov (@Sam-Kruglov)
* Reported #496: Root name missing when root element has no attributes (add
`FromXmlParser.getRootElementName()`)
(3.2.0)

Josip Antoliš (@Antolius)
* Reported #517: XML wrapper doesn't work with java records
(3.2.0)
Expand Down
4 changes: 4 additions & 0 deletions release-notes/VERSION
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,10 @@ Version: 3.x (for earlier see VERSION-2.x)
#455: Can't deserialize list in JsonSubtype when type property is visible
(reported by Jiri M)
(fix by @cowtowncoder, w/ Claude code)
#496: Root name missing when root element has no attributes (add
`FromXmlParser.getRootElementName()`)
(reported by Sam K)
(fix by @cowtowncoder, w/ Claude code)
#517: XML wrapper doesn't work with java records
(reported by Josip A)
(fix by Christopher M)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
import java.util.Objects;
import java.util.Set;

import javax.xml.namespace.QName;
import javax.xml.stream.XMLStreamException;
import javax.xml.stream.XMLStreamReader;
import javax.xml.stream.XMLStreamWriter;
Expand Down Expand Up @@ -280,6 +281,24 @@ public XMLStreamReader getStaxReader() {
return _xmlTokens.getXmlReader();
}

/**
* Accessor for the qualified name ({@link QName}) of the root XML element,
* including local name, namespace URI and prefix. Unlike accessing
* the underlying Stax reader directly, this value is stable regardless
* of how far parsing has advanced.
*<p>
* NOTE: the local name and namespace URI are post-{@link XmlNameProcessor}
* decoding (matching what Jackson databind sees), while the prefix is the
* raw value from the XML source.
*
* @return Qualified name of the root element
*
* @since 3.2
*/
public QName getRootElementName() {
return _xmlTokens.getRootName();
}

/*
/**********************************************************************
/* ElementWrappable implementation
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
import java.io.IOException;

import javax.xml.XMLConstants;
import javax.xml.namespace.QName;
import javax.xml.stream.*;

import org.codehaus.stax2.XMLStreamLocation2;
Expand Down Expand Up @@ -115,6 +116,15 @@ public class XmlTokenStream

protected String _namespaceURI;

/**
* Root element's qualified name (namespace URI, local name, prefix),
* saved during {@link #initialize()} so it remains accessible even
* after the stream has advanced past it.
*
* @since 3.2
*/
protected QName _rootName;

/**
* Current text value for TEXT_VALUE returned
*/
Expand Down Expand Up @@ -191,7 +201,11 @@ public int initialize() throws XMLStreamException
+XMLStreamConstants.START_ELEMENT+"), instead got "+_xmlReader.getEventType());
}
_checkXsiAttributes(); // sets _attributeCount, _nextAttributeIndex
// [dataformat-xml#496] Save root element name (with prefix) before stream advances
String rootPrefix = _xmlReader.getPrefix();
_decodeElementName(_xmlReader.getNamespaceURI(), _xmlReader.getLocalName());
_rootName = new QName(_namespaceURI, _localName,
(rootPrefix == null) ? "" : rootPrefix);

// 02-Jul-2020, tatu: Two choices: if child elements OR attributes, expose
// as Object value; otherwise expose as Text
Expand Down Expand Up @@ -325,6 +339,18 @@ public void skipEndElement() throws IOException, XMLStreamException

public String getNamespaceURI() { return _namespaceURI; }

/**
* Accessor for the qualified name of the root XML element (local name,
* namespace URI, prefix), as determined during stream initialization.
* Unlike {@link #getLocalName()}, this value does not change as the
* stream advances.
*
* @return Qualified name of the root element
*
* @since 3.2
*/
public QName getRootName() { return _rootName; }

public boolean hasXsiNil() {
return _xsiNilFound;
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,132 @@
package tools.jackson.dataformat.xml.stream;

import javax.xml.namespace.QName;

import org.junit.jupiter.api.Test;

import tools.jackson.core.*;
import tools.jackson.databind.*;
import tools.jackson.databind.annotation.JsonDeserialize;

import tools.jackson.dataformat.xml.XmlMapper;
import tools.jackson.dataformat.xml.XmlTestUtil;
import tools.jackson.dataformat.xml.deser.FromXmlParser;

import static org.junit.jupiter.api.Assertions.*;

// [dataformat-xml#496] Root element name not accessible from custom deserializer
// when root has no attributes
public class RootElementName496Test extends XmlTestUtil
{
@JsonDeserialize(using = RootNameDeserializer.class)
static class RootNameHolder {
public QName rootName;

RootNameHolder(QName rootName) {
this.rootName = rootName;
}
}

static class RootNameDeserializer extends ValueDeserializer<RootNameHolder> {
@Override
public RootNameHolder deserialize(JsonParser p, DeserializationContext ctxt)
{
QName rootName = ((FromXmlParser) p).getRootElementName();
// consume the rest
while (p.nextToken() != null) { }
return new RootNameHolder(rootName);
}
}

private final XmlMapper MAPPER = newMapper();

// [dataformat-xml#496]: root name accessible without attributes
@Test
public void testRootNameWithoutAttributes() throws Exception
{
RootNameHolder result = MAPPER.readValue(
"<root><field>value</field></root>", RootNameHolder.class);
assertEquals("root", result.rootName.getLocalPart());
}

// [dataformat-xml#496]: root name accessible with attributes
@Test
public void testRootNameWithAttributes() throws Exception
{
RootNameHolder result = MAPPER.readValue(
"<root foo='bar'><field>value</field></root>", RootNameHolder.class);
assertEquals("root", result.rootName.getLocalPart());
}

// [dataformat-xml#496]: verify via parser directly, stable across full parse
@Test
public void testRootNameViaParser() throws Exception
{
try (JsonParser p = MAPPER.createParser("<myRoot><child>text</child></myRoot>")) {
FromXmlParser xp = (FromXmlParser) p;
QName rootName = xp.getRootElementName();
assertEquals("myRoot", rootName.getLocalPart());
// Advance past all tokens
while (p.nextToken() != null) { }
// Still accessible after parsing
assertEquals("myRoot", xp.getRootElementName().getLocalPart());
}
}

// [dataformat-xml#496]: empty root element
@Test
public void testRootNameEmptyElement() throws Exception
{
try (JsonParser p = MAPPER.createParser("<emptyRoot/>")) {
FromXmlParser xp = (FromXmlParser) p;
assertEquals("emptyRoot", xp.getRootElementName().getLocalPart());
}
}

// [dataformat-xml#496]: root with text-only content (scalar root value)
@Test
public void testRootNameTextOnly() throws Exception
{
try (JsonParser p = MAPPER.createParser("<textRoot>hello</textRoot>")) {
FromXmlParser xp = (FromXmlParser) p;
assertEquals("textRoot", xp.getRootElementName().getLocalPart());
}
}

// [dataformat-xml#496]: root with namespace — verify all QName components
@Test
public void testRootNameWithNamespace() throws Exception
{
try (JsonParser p = MAPPER.createParser(
"<ns:root xmlns:ns='http://example.com'><ns:child>val</ns:child></ns:root>")) {
FromXmlParser xp = (FromXmlParser) p;
QName rootName = xp.getRootElementName();
assertEquals("root", rootName.getLocalPart());
assertEquals("http://example.com", rootName.getNamespaceURI());
assertEquals("ns", rootName.getPrefix());
}
}

// [dataformat-xml#496]: root with default namespace (no prefix)
@Test
public void testRootNameWithDefaultNamespace() throws Exception
{
try (JsonParser p = MAPPER.createParser(
"<root xmlns='http://example.com'><child>val</child></root>")) {
FromXmlParser xp = (FromXmlParser) p;
QName rootName = xp.getRootElementName();
assertEquals("root", rootName.getLocalPart());
assertEquals("http://example.com", rootName.getNamespaceURI());
assertEquals("", rootName.getPrefix());
}
}

// [dataformat-xml#496]: root with multiple children (no attributes)
@Test
public void testRootNameMultipleChildren() throws Exception
{
RootNameHolder result = MAPPER.readValue(
"<document><a>1</a><b>2</b><c>3</c></document>", RootNameHolder.class);
assertEquals("document", result.rootName.getLocalPart());
}
}