diff --git a/release-notes/CREDITS b/release-notes/CREDITS index f6481e84..71db2625 100644 --- a/release-notes/CREDITS +++ b/release-notes/CREDITS @@ -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) diff --git a/release-notes/VERSION b/release-notes/VERSION index 1f878a56..19ebaca1 100644 --- a/release-notes/VERSION +++ b/release-notes/VERSION @@ -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) diff --git a/src/main/java/tools/jackson/dataformat/xml/ser/ToXmlGenerator.java b/src/main/java/tools/jackson/dataformat/xml/ser/ToXmlGenerator.java index 5096e029..1fde0228 100644 --- a/src/main/java/tools/jackson/dataformat/xml/ser/ToXmlGenerator.java +++ b/src/main/java/tools/jackson/dataformat/xml/ser/ToXmlGenerator.java @@ -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. @@ -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)) { @@ -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; } diff --git a/src/main/java/tools/jackson/dataformat/xml/ser/XmlSerializationContext.java b/src/main/java/tools/jackson/dataformat/xml/ser/XmlSerializationContext.java index b89d7d48..a0a69bff 100644 --- a/src/main/java/tools/jackson/dataformat/xml/ser/XmlSerializationContext.java +++ b/src/main/java/tools/jackson/dataformat/xml/ser/XmlSerializationContext.java @@ -275,8 +275,10 @@ protected void _serializeUnwrappedObjectNode(ToXmlGenerator xgen, Object value, Map.Entry 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); } diff --git a/src/test/java/tools/jackson/dataformat/xml/node/JsonNodeSerRootName887Test.java b/src/test/java/tools/jackson/dataformat/xml/node/JsonNodeSerRootName887Test.java new file mode 100644 index 00000000..4d9072da --- /dev/null +++ b/src/test/java/tools/jackson/dataformat/xml/node/JsonNodeSerRootName887Test.java @@ -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 ! &;"; + 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_____>13", + mapper.writeValueAsString(oneProp)); + assertEquals("<__I_am__fancy_____>13" + +"1", + 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 ! &;" + final String DOC = "13" + +""; + + 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(); + } +}