diff --git a/src/main/java/com/fasterxml/jackson/dataformat/xml/XmlTypeResolverBuilder.java b/src/main/java/com/fasterxml/jackson/dataformat/xml/XmlTypeResolverBuilder.java index a90f0697d..a89749dc8 100644 --- a/src/main/java/com/fasterxml/jackson/dataformat/xml/XmlTypeResolverBuilder.java +++ b/src/main/java/com/fasterxml/jackson/dataformat/xml/XmlTypeResolverBuilder.java @@ -34,7 +34,6 @@ public XmlTypeResolverBuilder(JsonTypeInfo.Value settings) { @Override public StdTypeResolverBuilder init(JsonTypeInfo.Id idType, TypeIdResolver idRes) { - super.init(idType, idRes); if (_typeProperty != null) { _typeProperty = StaxUtil.sanitizeXmlTypeName(_typeProperty); @@ -138,6 +137,8 @@ protected static String decodeXmlClassName(String className) protected static class XmlClassNameIdResolver extends ClassNameIdResolver { + private static final long serialVersionUID = 1L; + public XmlClassNameIdResolver(JavaType baseType, TypeFactory typeFactory, PolymorphicTypeValidator ptv) { @@ -159,6 +160,8 @@ public JavaType typeFromId(DatabindContext context, String id) throws IOExceptio protected static class XmlMinimalClassNameIdResolver extends MinimalClassNameIdResolver { + private static final long serialVersionUID = 1L; + public XmlMinimalClassNameIdResolver(JavaType baseType, TypeFactory typeFactory, PolymorphicTypeValidator ptv) { diff --git a/src/main/java/com/fasterxml/jackson/dataformat/xml/ser/ToXmlGenerator.java b/src/main/java/com/fasterxml/jackson/dataformat/xml/ser/ToXmlGenerator.java index 735a171f4..f8361422a 100644 --- a/src/main/java/com/fasterxml/jackson/dataformat/xml/ser/ToXmlGenerator.java +++ b/src/main/java/com/fasterxml/jackson/dataformat/xml/ser/ToXmlGenerator.java @@ -93,6 +93,19 @@ public enum Feature implements FormatFeature * @since 2.13 */ UNWRAP_ROOT_OBJECT_NODE(false), + + /** + * Feature that enables automatic conversion of logical property + * name {@code "xsi:type"} into matching XML name where "type" + * is the local name and "xsi" prefix is bound to URI + * {@link XMLConstants#W3C_XML_SCHEMA_INSTANCE_NS_URI}, + * and output is indicated to be done as XML Attribute. + * This is mostly desirable for Polymorphic handling where it is difficult + * to specify XML Namespace for type identifier + * + * @since 2.17 + */ + AUTO_DETECT_XSI_TYPE(false), ; final boolean _defaultState; @@ -247,20 +260,26 @@ public void initGenerator() throws IOException } _initialized = true; try { + boolean xmlDeclWritten; if (Feature.WRITE_XML_1_1.enabledIn(_formatFeatures)) { _xmlWriter.writeStartDocument("UTF-8", "1.1"); + xmlDeclWritten = true; } else if (Feature.WRITE_XML_DECLARATION.enabledIn(_formatFeatures)) { _xmlWriter.writeStartDocument("UTF-8", "1.0"); + xmlDeclWritten = true; } else { - return; + xmlDeclWritten = false; } // as per [dataformat-xml#172], try adding indentation - if (_xmlPrettyPrinter != null) { + if (xmlDeclWritten && (_xmlPrettyPrinter != null)) { // ... but only if it is likely to succeed: if (!_stax2Emulation) { _xmlPrettyPrinter.writePrologLinefeed(_xmlWriter); } } + if (Feature.AUTO_DETECT_XSI_TYPE.enabledIn(_formatFeatures)) { + _xmlWriter.setPrefix("xsi", XMLConstants.W3C_XML_SCHEMA_INSTANCE_NS_URI); + } } catch (XMLStreamException e) { StaxUtil.throwAsGenerationException(e, this); } @@ -487,10 +506,12 @@ public void writeRepeatedFieldName() throws IOException /* JsonGenerator method overrides /********************************************************** */ - - /* Most overrides in this section are just to make methods final, - * to allow better inlining... - */ + + @Override + public void writeFieldName(SerializableString name) throws IOException + { + writeFieldName(name.getValue()); + } @Override public final void writeFieldName(String name) throws IOException @@ -498,12 +519,22 @@ public final void writeFieldName(String name) throws IOException if (_writeContext.writeFieldName(name) == JsonWriteContext.STATUS_EXPECT_VALUE) { _reportError("Can not write a field name, expecting a value"); } - // Should this ever get called? - String ns = (_nextName == null) ? "" : _nextName.getNamespaceURI(); - _nameToEncode.namespace = ns; - _nameToEncode.localPart = name; - _nameProcessor.encodeName(_nameToEncode); - setNextName(new QName(_nameToEncode.namespace, _nameToEncode.localPart)); + + String ns; + // 30-Jan-2024, tatu: Surprise! + if (Feature.AUTO_DETECT_XSI_TYPE.enabledIn(_formatFeatures) + && "xsi:type".equals(name)) { + setNextName(new QName(XMLConstants.W3C_XML_SCHEMA_INSTANCE_NS_URI, + "type", "xsi")); + setNextIsAttribute(true); + } 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)); + } } @Override @@ -519,7 +550,7 @@ public final void writeStringField(String fieldName, String value) throws IOExce // handling... // // See [dataformat-xml#4] for more context. - + /* // @since 2.9 public WritableTypeId writeTypePrefix(WritableTypeId typeIdDef) throws IOException @@ -640,12 +671,6 @@ public final void _handleEndObject() throws IOException /********************************************************** */ - @Override - public void writeFieldName(SerializableString name) throws IOException - { - writeFieldName(name.getValue()); - } - @Override public void writeString(String text) throws IOException { diff --git a/src/test/java/com/fasterxml/jackson/dataformat/xml/ser/XsiTypeWriteTest.java b/src/test/java/com/fasterxml/jackson/dataformat/xml/ser/XsiTypeWriteTest.java new file mode 100644 index 000000000..19a9382a5 --- /dev/null +++ b/src/test/java/com/fasterxml/jackson/dataformat/xml/ser/XsiTypeWriteTest.java @@ -0,0 +1,60 @@ +package com.fasterxml.jackson.dataformat.xml.ser; + +import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.annotation.JsonRootName; +import com.fasterxml.jackson.annotation.JsonTypeInfo; +import com.fasterxml.jackson.annotation.JsonTypeInfo.As; +import com.fasterxml.jackson.annotation.JsonTypeInfo.Id; +import com.fasterxml.jackson.dataformat.xml.XmlMapper; +import com.fasterxml.jackson.dataformat.xml.XmlTestBase; + +// [dataformat-xml#324] +public class XsiTypeWriteTest extends XmlTestBase +{ + @JsonRootName("Typed") + static class TypeBean { + @JsonProperty("xsi:type") + public String typeId = "abc"; + } + + @JsonRootName("Poly") + @JsonTypeInfo(use = Id.SIMPLE_NAME, include = As.PROPERTY, property="xsi:type") + static class PolyBean { + public int value = 42; + } + + private final XmlMapper NO_XSI_MAPPER = XmlMapper.builder() + .configure(ToXmlGenerator.Feature.AUTO_DETECT_XSI_TYPE, false) + .build(); + + private final XmlMapper XSI_ENABLED_MAPPER = XmlMapper.builder() + .configure(ToXmlGenerator.Feature.AUTO_DETECT_XSI_TYPE, true) + .build(); + + public void testExplicitXsiTypeWriteDisabled() throws Exception + { + assertEquals("abc", + NO_XSI_MAPPER.writeValueAsString(new TypeBean())); + } + + public void testExplicitXsiTypeWriteEnabled() throws Exception + { + assertEquals( + a2q(""), + a2q(XSI_ENABLED_MAPPER.writeValueAsString(new TypeBean()))); + } + + public void testXsiTypeAsTypeIdWriteDisabled() throws Exception + { + assertEquals("abc42", + NO_XSI_MAPPER.writeValueAsString(new PolyBean())); + } + + public void testXsiTypeAsTypeIdWriteEnabled() throws Exception + { + assertEquals( + a2q("" + +"42"), + a2q(XSI_ENABLED_MAPPER.writeValueAsString(new PolyBean()))); + } +}