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
3 changes: 3 additions & 0 deletions release-notes/CREDITS
Original file line number Diff line number Diff line change
Expand Up @@ -170,3 +170,6 @@ Christian Beikov (@beikov)
(3.3.0)
* Fixed #886: Escape names already starting with prefix in `Base64NameProcessor`
(3.3.0)
* Fixed #887: Apply `XmlNameProcessor` to unwrapped `ObjectNode` root element
name
(3.3.0)
2 changes: 2 additions & 0 deletions release-notes/VERSION
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,8 @@ Version: 3.x (for earlier see VERSION-2.x)
(fix by @Sahana2524)
#886: Escape names already starting with prefix in `Base64NameProcessor`
(fix by @Sahana2524)
#887: Apply `XmlNameProcessor` to unwrapped `ObjectNode` root element name
(fix by @Sahana2524)

3.2.2 (not yet released)

Expand Down
26 changes: 20 additions & 6 deletions src/main/java/tools/jackson/dataformat/xml/ser/ToXmlGenerator.java
Original file line number Diff line number Diff line change
Expand Up @@ -518,6 +518,24 @@ public final boolean setNextNameIfMissing(QName name)
return false;
}

/**
* Method for running configured {@link XmlNameProcessor} over a name that comes
* from content being written (like {@code ObjectNode} property names), as opposed
* to statically known POJO property names.
* Needed by callers that have to construct the {@link QName} themselves instead
* of going through {@link #writeName(String)} (which applies the processor for
* names it writes).
*
* @since 3.3
*/
public QName encodeContentName(String namespaceURI, String localName)
{
_nameToEncode.namespace = (namespaceURI == null) ? "" : namespaceURI;
_nameToEncode.localPart = localName;
_nameProcessor.encodeName(_nameToEncode);
return new QName(_nameToEncode.namespace, _nameToEncode.localPart);
}

/**
* Methdod called when a structured (collection, array, map) is being
* output.
Expand Down Expand Up @@ -591,7 +609,6 @@ public JsonGenerator writeName(String name) throws JacksonException
_reportError("Can not write a property name, expecting a value");
}

String ns;
// 30-Jan-2024, tatu: Surprise!
if (XmlWriteFeature.AUTO_DETECT_XSI_TYPE.enabledIn(_formatFeatures)
&& "xsi:type".equals(name)) {
Expand All @@ -612,11 +629,8 @@ public JsonGenerator writeName(String name) throws JacksonException
}
} else {
// Should this ever get called?
ns = (_nextName == null) ? "" : _nextName.getNamespaceURI();
_nameToEncode.namespace = ns;
_nameToEncode.localPart = name;
_nameProcessor.encodeName(_nameToEncode);
setNextName(new QName(_nameToEncode.namespace, _nameToEncode.localPart));
String ns = (_nextName == null) ? "" : _nextName.getNamespaceURI();
setNextName(encodeContentName(ns, name));
}
return this;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -275,8 +275,10 @@ protected void _serializeUnwrappedObjectNode(ToXmlGenerator xgen, Object value,
Map.Entry<String, JsonNode> entry = root.properties().iterator().next();
final JsonNode newRoot = entry.getValue();

// Name comes from content, so has to go through the same XmlNameProcessor
// the generator applies to other content-derived names.
// No namespace associated with JsonNode:
_initWithRootName(xgen, new QName(entry.getKey()));
_initWithRootName(xgen, xgen.encodeContentName("", entry.getKey()));
if (ser == null) {
ser = findTypedValueSerializer(newRoot.getClass(), true);
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
package tools.jackson.dataformat.xml.node;

import org.junit.jupiter.api.Test;

import tools.jackson.databind.node.ObjectNode;

import tools.jackson.dataformat.xml.XmlFactory;
import tools.jackson.dataformat.xml.XmlMapper;
import tools.jackson.dataformat.xml.XmlNameProcessor;
import tools.jackson.dataformat.xml.XmlNameProcessors;
import tools.jackson.dataformat.xml.XmlReadFeature;
import tools.jackson.dataformat.xml.XmlTestUtil;

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

// [dataformat-xml#887]: root name of unwrapped `ObjectNode` (see
// `XmlWriteFeature.UNWRAP_ROOT_OBJECT_NODE`) needs `XmlNameProcessor` handling
public class JsonNodeSerRootName887Test extends XmlTestUtil
{
// The unwrapped root name comes from content, so it needs the same
// XmlNameProcessor treatment the name gets when written as a child element.
@Test
public void testUnwrappedRootNameIsProcessed() throws Exception
{
final String BAD_NAME = "$ I am <fancy>! &;";
XmlMapper mapper = mapperWith(XmlNameProcessors.newReplacementProcessor());

ObjectNode oneProp = mapper.createObjectNode();
oneProp.putObject(BAD_NAME).put("id", 13);

ObjectNode twoProps = mapper.createObjectNode();
twoProps.putObject(BAD_NAME).put("id", 13);
twoProps.put("other", 1);

assertEquals("<__I_am__fancy_____><id>13</id></__I_am__fancy_____>",
mapper.writeValueAsString(oneProp));
assertEquals("<ObjectNode><__I_am__fancy_____><id>13</id></__I_am__fancy_____>"
+"<other>1</other></ObjectNode>",
mapper.writeValueAsString(twoProps));
}

// ... which also makes the WRAP_ROOT_ELEMENT_NAME / UNWRAP_ROOT_OBJECT_NODE
// pairing round-trip a root name that had to be escaped
@Test
public void testUnwrappedRootNameRoundTrip() throws Exception
{
XmlMapper mapper = mapperWith(XmlNameProcessors.newBase64Processor());
// decodes to "$ I am <fancy>! &;"
final String DOC = "<base64_tag_JCBJIGFtIDxmYW5jeT4hICY7><id>13</id>"
+"</base64_tag_JCBJIGFtIDxmYW5jeT4hICY7>";

assertEquals(DOC, mapper.writeValueAsString(mapper.readTree(DOC)));
}

private XmlMapper mapperWith(XmlNameProcessor proc) {
return mapperBuilder(XmlFactory.builder().xmlNameProcessor(proc).build())
.enable(XmlReadFeature.WRAP_ROOT_ELEMENT_NAME)
.build();
}
}
Loading