diff --git a/src/main/java/tools/jackson/dataformat/xml/JacksonXmlAnnotationIntrospector.java b/src/main/java/tools/jackson/dataformat/xml/JacksonXmlAnnotationIntrospector.java index c616a362..2ce0e5ba 100644 --- a/src/main/java/tools/jackson/dataformat/xml/JacksonXmlAnnotationIntrospector.java +++ b/src/main/java/tools/jackson/dataformat/xml/JacksonXmlAnnotationIntrospector.java @@ -37,12 +37,19 @@ public class JacksonXmlAnnotationIntrospector protected boolean _cfgDefaultUseWrapper; + protected final JacksonXmlAnnotationIntrospectorConfig _cfgIntrospectorConfig; + public JacksonXmlAnnotationIntrospector() { this(DEFAULT_USE_WRAPPER); } public JacksonXmlAnnotationIntrospector(boolean defaultUseWrapper) { + this(defaultUseWrapper, new JacksonXmlAnnotationIntrospectorConfig()); + } + + public JacksonXmlAnnotationIntrospector(boolean defaultUseWrapper, JacksonXmlAnnotationIntrospectorConfig introspectorConfig) { _cfgDefaultUseWrapper = defaultUseWrapper; + _cfgIntrospectorConfig = introspectorConfig; } /* @@ -208,6 +215,13 @@ public PropertyName findNameForDeserialization(MapperConfig config, Annotated PropertyName pn = PropertyName.merge(_findXmlName(a), super.findNameForDeserialization(config, a)); if (pn == null) { + JacksonXmlText jacksonXmlTextAnnotation = _findAnnotation(a, JacksonXmlText.class); + + if (jacksonXmlTextAnnotation != null && jacksonXmlTextAnnotation.value() && + !_cfgIntrospectorConfig.inferXmlTextPropertyName()) { + return _cfgIntrospectorConfig.xmlTextPropertyName(); + } + if (_hasOneOf(a, ANNOTATIONS_TO_INFER_XML_PROP)) { return PropertyName.USE_DEFAULT; } diff --git a/src/main/java/tools/jackson/dataformat/xml/JacksonXmlAnnotationIntrospectorConfig.java b/src/main/java/tools/jackson/dataformat/xml/JacksonXmlAnnotationIntrospectorConfig.java new file mode 100644 index 00000000..c0fa74d3 --- /dev/null +++ b/src/main/java/tools/jackson/dataformat/xml/JacksonXmlAnnotationIntrospectorConfig.java @@ -0,0 +1,28 @@ +package tools.jackson.dataformat.xml; + +import tools.jackson.databind.PropertyName; +import tools.jackson.dataformat.xml.deser.FromXmlParser; + +import java.io.Serializable; + +public record JacksonXmlAnnotationIntrospectorConfig( + boolean inferXmlTextPropertyName, + PropertyName xmlTextPropertyName //Only honored if inferXmlTextPropertyName is false +) implements Serializable { + + /** + * Constructs a JacksonXmlAnnotationIntrospectorConfig with the default configuration + * Does not infer the XmlTextPropertyName by default and uses {@link FromXmlParser#DEFAULT_TEXT_PROPERTY} for the {@link PropertyName}. + */ + public JacksonXmlAnnotationIntrospectorConfig() { + this(false, PropertyName.construct(FromXmlParser.DEFAULT_TEXT_PROPERTY)); + } + + public JacksonXmlAnnotationIntrospectorConfig withInferXmlTextPropertyName(boolean inferXmlTextPropertyName) { + return new JacksonXmlAnnotationIntrospectorConfig(inferXmlTextPropertyName, this.xmlTextPropertyName); + } + + public JacksonXmlAnnotationIntrospectorConfig withXmlTextPropertyName(PropertyName xmlTextPropertyName) { + return new JacksonXmlAnnotationIntrospectorConfig(this.inferXmlTextPropertyName, xmlTextPropertyName); + } +} diff --git a/src/main/java/tools/jackson/dataformat/xml/XmlFactory.java b/src/main/java/tools/jackson/dataformat/xml/XmlFactory.java index 13959a02..02f5e312 100644 --- a/src/main/java/tools/jackson/dataformat/xml/XmlFactory.java +++ b/src/main/java/tools/jackson/dataformat/xml/XmlFactory.java @@ -92,7 +92,7 @@ public XmlFactory(XMLInputFactory xmlIn) { public XmlFactory(XMLInputFactory xmlIn, XMLOutputFactory xmlOut) { this(DEFAULT_XML_READ_FEATURE_FLAGS, DEFAULT_XML_WRITE_FEATURE_FLAGS, xmlIn, xmlOut, XmlNameProcessors.newPassthroughProcessor(), - FromXmlParser.DEFAULT_UNNAMED_TEXT_PROPERTY); + FromXmlParser.DEFAULT_TEXT_PROPERTY); } protected XmlFactory(int xpFeatures, int xgFeatures, diff --git a/src/main/java/tools/jackson/dataformat/xml/XmlFactoryBuilder.java b/src/main/java/tools/jackson/dataformat/xml/XmlFactoryBuilder.java index 41afa5fd..88a982c6 100644 --- a/src/main/java/tools/jackson/dataformat/xml/XmlFactoryBuilder.java +++ b/src/main/java/tools/jackson/dataformat/xml/XmlFactoryBuilder.java @@ -83,7 +83,7 @@ protected XmlFactoryBuilder() { XmlFactory.DEFAULT_XML_WRITE_FEATURE_FLAGS); _classLoaderForStax = null; _nameProcessor = XmlNameProcessors.newPassthroughProcessor(); - _nameForTextElement = FromXmlParser.DEFAULT_UNNAMED_TEXT_PROPERTY; + _nameForTextElement = FromXmlParser.DEFAULT_TEXT_PROPERTY; } public XmlFactoryBuilder(XmlFactory base) { diff --git a/src/main/java/tools/jackson/dataformat/xml/XmlMapper.java b/src/main/java/tools/jackson/dataformat/xml/XmlMapper.java index 01d0c052..b6cbe15d 100644 --- a/src/main/java/tools/jackson/dataformat/xml/XmlMapper.java +++ b/src/main/java/tools/jackson/dataformat/xml/XmlMapper.java @@ -67,7 +67,7 @@ public Builder(XmlFactory f) { // String into `null` (where it otherwise is an error) is very useful. enable(DeserializationFeature.ACCEPT_EMPTY_STRING_AS_NULL_OBJECT); _defaultUseWrapper = JacksonXmlAnnotationIntrospector.DEFAULT_USE_WRAPPER; - _nameForTextElement = FromXmlParser.DEFAULT_UNNAMED_TEXT_PROPERTY; + _nameForTextElement = FromXmlParser.DEFAULT_TEXT_PROPERTY; // as well as AnnotationIntrospector: note, however, that "use wrapper" may well // change later on diff --git a/src/main/java/tools/jackson/dataformat/xml/deser/FromXmlParser.java b/src/main/java/tools/jackson/dataformat/xml/deser/FromXmlParser.java index eb2bdf9c..7f6f579a 100644 --- a/src/main/java/tools/jackson/dataformat/xml/deser/FromXmlParser.java +++ b/src/main/java/tools/jackson/dataformat/xml/deser/FromXmlParser.java @@ -35,10 +35,14 @@ public class FromXmlParser implements ElementWrappable { /** - * The default name placeholder for XML text segments is empty - * String (""). + * The default name placeholder for XML text segments: used because Token stream + * requires all values inside "Objects" to have names associated. + * For Jackson 3.x this is {@code }; in 2.x matching constant was defined + * as empty String ({@code ""}). + * + * @since 3.0 Constant was renamed: was {@code DEFAULT_UNNAMED_TEXT_PROPERTY} in 2.x */ - public final static String DEFAULT_UNNAMED_TEXT_PROPERTY = ""; + public final static String DEFAULT_TEXT_PROPERTY = ""; /** * XML format has some peculiarities, indicated via capability diff --git a/src/test/java/tools/jackson/dataformat/xml/deser/DifferentDeserializationPropertyNameTest.java b/src/test/java/tools/jackson/dataformat/xml/deser/DifferentDeserializationPropertyNameTest.java new file mode 100644 index 00000000..b5172a97 --- /dev/null +++ b/src/test/java/tools/jackson/dataformat/xml/deser/DifferentDeserializationPropertyNameTest.java @@ -0,0 +1,74 @@ +package tools.jackson.dataformat.xml.deser; + +import org.junit.jupiter.api.Test; +import tools.jackson.databind.DatabindException; +import tools.jackson.databind.PropertyName; +import tools.jackson.dataformat.xml.JacksonXmlAnnotationIntrospector; +import tools.jackson.dataformat.xml.JacksonXmlAnnotationIntrospectorConfig; +import tools.jackson.dataformat.xml.XmlMapper; +import tools.jackson.dataformat.xml.XmlTestUtil; +import tools.jackson.dataformat.xml.annotation.JacksonXmlProperty; +import tools.jackson.dataformat.xml.annotation.JacksonXmlText; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; + +public class DifferentDeserializationPropertyNameTest extends XmlTestUtil +{ + static class TestBean { + @JacksonXmlProperty(localName = "wrong") + String wrong; + + @JacksonXmlText + String name; + } + + /* + /********************************************************************** + /* Test methods + /********************************************************************** + */ + + @Test + public void testWithExplicitProperty() { + final XmlMapper mapper = XmlMapper.builder() + .annotationIntrospector(new JacksonXmlAnnotationIntrospector(false, + new JacksonXmlAnnotationIntrospectorConfig(false, new PropertyName("name")))) + .build(); + + String xmlInput = "ABC123"; + + TestBean testBean = mapper.readValue(xmlInput, TestBean.class); + + assertEquals("ABC123", testBean.name); + } + + @Test + public void testWithInferName() { + final XmlMapper mapper = XmlMapper.builder() + .annotationIntrospector(new JacksonXmlAnnotationIntrospector(false, + new JacksonXmlAnnotationIntrospectorConfig(true, null))) + .build(); + + String xmlInput = "DEF"; + + TestBean testBean = mapper.readValue(xmlInput, TestBean.class); + + assertEquals("DEF", testBean.name); + } + + @Test + public void testWithDuplicateExplicitProperty() { + final XmlMapper mapper = XmlMapper.builder() + .annotationIntrospector(new JacksonXmlAnnotationIntrospector(false, + new JacksonXmlAnnotationIntrospectorConfig(false, new PropertyName("wrong")))) + .build(); + + String xmlInput = "DEF"; + + Exception result = assertThrows(DatabindException.class, () -> mapper.readValue(xmlInput, TestBean.class)); + + assertTrue(result.getMessage().contains("Multiple fields representing property \"wrong\"")); + } +} diff --git a/src/test/java/tools/jackson/dataformat/xml/deser/MapDeserializationTest.java b/src/test/java/tools/jackson/dataformat/xml/deser/MapDeserializationTest.java index 55bbf4b5..ae622e3a 100644 --- a/src/test/java/tools/jackson/dataformat/xml/deser/MapDeserializationTest.java +++ b/src/test/java/tools/jackson/dataformat/xml/deser/MapDeserializationTest.java @@ -41,7 +41,7 @@ public void testMapWithAttr() throws Exception assertEquals(1, map.size()); Map inner = new LinkedHashMap<>(); inner.put("lang", "en"); - inner.put("", "John Smith"); + inner.put(FromXmlParser.DEFAULT_TEXT_PROPERTY, "John Smith"); assertEquals(Collections.singletonMap("person", inner), map); } } diff --git a/src/test/java/tools/jackson/dataformat/xml/deser/UntypedObjectDeserTest.java b/src/test/java/tools/jackson/dataformat/xml/deser/UntypedObjectDeserTest.java index af705490..931293ba 100644 --- a/src/test/java/tools/jackson/dataformat/xml/deser/UntypedObjectDeserTest.java +++ b/src/test/java/tools/jackson/dataformat/xml/deser/UntypedObjectDeserTest.java @@ -77,7 +77,7 @@ public void testMixedContent() throws Exception final String XML = "first123secondabclast"; final JsonNode fromXml = XML_MAPPER.valueToTree(XML_MAPPER.readValue(XML, Object.class)); final ObjectNode exp = XML_MAPPER.createObjectNode(); - exp.putArray("") + exp.putArray(FromXmlParser.DEFAULT_TEXT_PROPERTY) .add("first") .add("second") .add("last"); diff --git a/src/test/java/tools/jackson/dataformat/xml/node/JsonNodeBasicDeserTest.java b/src/test/java/tools/jackson/dataformat/xml/node/JsonNodeBasicDeserTest.java index b774f9e2..66024792 100644 --- a/src/test/java/tools/jackson/dataformat/xml/node/JsonNodeBasicDeserTest.java +++ b/src/test/java/tools/jackson/dataformat/xml/node/JsonNodeBasicDeserTest.java @@ -7,6 +7,7 @@ import tools.jackson.databind.node.JsonNodeType; import tools.jackson.databind.node.ObjectNode; import tools.jackson.dataformat.xml.XmlTestUtil; +import tools.jackson.dataformat.xml.deser.FromXmlParser; import static org.junit.jupiter.api.Assertions.*; @@ -43,7 +44,7 @@ public void testMixedContent() throws Exception { JsonNode fromXml = XML_MAPPER.readTree("first123secondabclast"); final ObjectNode exp = XML_MAPPER.createObjectNode(); - exp.putArray("") + exp.putArray(FromXmlParser.DEFAULT_TEXT_PROPERTY) .add("first") .add("second") .add("last"); diff --git a/src/test/java/tools/jackson/dataformat/xml/node/JsonNodeMixedContent403Test.java b/src/test/java/tools/jackson/dataformat/xml/node/JsonNodeMixedContent403Test.java index f7355b30..96cbe73c 100644 --- a/src/test/java/tools/jackson/dataformat/xml/node/JsonNodeMixedContent403Test.java +++ b/src/test/java/tools/jackson/dataformat/xml/node/JsonNodeMixedContent403Test.java @@ -6,6 +6,7 @@ import tools.jackson.databind.ObjectMapper; import tools.jackson.databind.json.JsonMapper; import tools.jackson.dataformat.xml.XmlTestUtil; +import tools.jackson.dataformat.xml.deser.FromXmlParser; import static org.junit.jupiter.api.Assertions.assertEquals; @@ -19,7 +20,7 @@ public class JsonNodeMixedContent403Test extends XmlTestUtil public void testMixedContentBefore() throws Exception { // First, before elements: - assertEquals(JSON_MAPPER.readTree(a2q("{'':'before','a':'1','b':'2'}")), + assertEquals(JSON_MAPPER.readTree(a2q(String.format("{'%s':'before','a':'1','b':'2'}", FromXmlParser.DEFAULT_TEXT_PROPERTY))), XML_MAPPER.readTree("before12")); } @@ -27,7 +28,7 @@ public void testMixedContentBefore() throws Exception public void testMixedContentBetween() throws Exception { // Second, between - assertEquals(JSON_MAPPER.readTree(a2q("{'a':'1','':'between','b':'2'}")), + assertEquals(JSON_MAPPER.readTree(a2q(String.format("{'a':'1','%s':'between','b':'2'}", FromXmlParser.DEFAULT_TEXT_PROPERTY))), XML_MAPPER.readTree("1between2")); } @@ -35,7 +36,7 @@ public void testMixedContentBetween() throws Exception public void testMixedContentAfter() throws Exception { // and then after - assertEquals(JSON_MAPPER.readTree(a2q("{'a':'1','b':'2','':'after'}")), + assertEquals(JSON_MAPPER.readTree(a2q(String.format("{'a':'1','b':'2','%s':'after'}", FromXmlParser.DEFAULT_TEXT_PROPERTY))), XML_MAPPER.readTree("12after")); } @@ -44,7 +45,7 @@ public void testMultipleMixedContent() throws Exception { // and then after assertEquals(JSON_MAPPER.readTree( - a2q("{'':['first','second','third'],'a':'1','b':'2'}")), + a2q(String.format("{'%s':['first','second','third'],'a':'1','b':'2'}", FromXmlParser.DEFAULT_TEXT_PROPERTY))), XML_MAPPER.readTree("first1second2third")); } @@ -57,7 +58,7 @@ public void testMixed226() throws Exception +" mixed2\n" +""; JsonNode fromJson = JSON_MAPPER.readTree( - a2q("{'a':{'':['mixed1 ',' mixed2'],'b':'leaf'}}")); + a2q(String.format("{'a':{'%s':['mixed1 ',' mixed2'],'b':'leaf'}}", FromXmlParser.DEFAULT_TEXT_PROPERTY))); assertEquals(fromJson, XML_MAPPER.readTree(XML)); } } diff --git a/src/test/java/tools/jackson/dataformat/xml/stream/XmlParser442Test.java b/src/test/java/tools/jackson/dataformat/xml/stream/XmlParser442Test.java index 105a20e0..131d9702 100644 --- a/src/test/java/tools/jackson/dataformat/xml/stream/XmlParser442Test.java +++ b/src/test/java/tools/jackson/dataformat/xml/stream/XmlParser442Test.java @@ -44,7 +44,7 @@ public void testMixedContentBeforeElement442() throws Exception // Here's what we are missing: assertToken(JsonToken.START_OBJECT, xp.nextToken()); assertToken(JsonToken.PROPERTY_NAME, xp.nextToken()); - assertEquals("", xp.currentName()); + assertEquals(FromXmlParser.DEFAULT_TEXT_PROPERTY, xp.currentName()); assertToken(JsonToken.VALUE_STRING, xp.nextToken()); assertEquals("text", xp.getString().trim()); diff --git a/src/test/java/tools/jackson/dataformat/xml/stream/XmlParserTest.java b/src/test/java/tools/jackson/dataformat/xml/stream/XmlParserTest.java index 280cf553..74c9fcc9 100644 --- a/src/test/java/tools/jackson/dataformat/xml/stream/XmlParserTest.java +++ b/src/test/java/tools/jackson/dataformat/xml/stream/XmlParserTest.java @@ -99,7 +99,7 @@ public void testRootScalar() throws Exception try (JsonParser p = _xmlMapper.createParser(XML)) { assertToken(JsonToken.START_OBJECT, p.nextToken()); assertToken(JsonToken.PROPERTY_NAME, p.nextToken()); - assertEquals("", p.currentName()); + assertEquals(FromXmlParser.DEFAULT_TEXT_PROPERTY, p.currentName()); assertToken(JsonToken.VALUE_STRING, p.nextToken()); assertEquals("value", p.getString()); assertToken(JsonToken.END_OBJECT, p.nextToken()); @@ -118,7 +118,7 @@ public void testRootMixed() throws Exception assertToken(JsonToken.START_OBJECT, p.nextToken()); assertToken(JsonToken.PROPERTY_NAME, p.nextToken()); - assertEquals("", p.currentName()); + assertEquals(FromXmlParser.DEFAULT_TEXT_PROPERTY, p.currentName()); assertToken(JsonToken.VALUE_STRING, p.nextToken()); assertEquals("value", p.getString()); @@ -343,7 +343,8 @@ public void testXmlAttributes() throws Exception @Test public void testMixedContent() throws Exception { - String exp = a2q("{'':'first','a':'123','':'second','b':'456','':'last'}"); + String exp = a2q(String.format("{'%1$s':'first','a':'123','%1$s':'second','b':'456','%1$s':'last'}", + FromXmlParser.DEFAULT_TEXT_PROPERTY)); String result = _readXmlWriteJson("first123second456last"); //System.err.println("result = \n"+result); @@ -373,7 +374,7 @@ public void testInferredNumbers() throws Exception assertEquals(42, xp.getIntValue()); assertToken(JsonToken.PROPERTY_NAME, xp.nextToken()); // implicit for text - assertEquals("", xp.currentName()); + assertEquals(FromXmlParser.DEFAULT_TEXT_PROPERTY, xp.currentName()); assertToken(JsonToken.VALUE_STRING, xp.nextToken()); assertTrue(xp.isExpectedNumberIntToken()); diff --git a/src/test/java/tools/jackson/dataformat/xml/tofix/XmlTextViaCreator306Test.java b/src/test/java/tools/jackson/dataformat/xml/tofix/XmlTextViaCreator306Test.java index a54b2341..9e68718a 100644 --- a/src/test/java/tools/jackson/dataformat/xml/tofix/XmlTextViaCreator306Test.java +++ b/src/test/java/tools/jackson/dataformat/xml/tofix/XmlTextViaCreator306Test.java @@ -116,7 +116,6 @@ public void testIssue306NoCtor() throws Exception } // [dataformat-xml#423] - @JacksonTestFailureExpected @Test public void testXmlTextViaCtor423() throws Exception { diff --git a/src/test/java/tools/jackson/dataformat/xml/tofix/records/XmlRecordDeser734Test.java b/src/test/java/tools/jackson/dataformat/xml/tofix/records/XmlRecordDeser734Test.java index d303abcb..b9da7dd8 100644 --- a/src/test/java/tools/jackson/dataformat/xml/tofix/records/XmlRecordDeser734Test.java +++ b/src/test/java/tools/jackson/dataformat/xml/tofix/records/XmlRecordDeser734Test.java @@ -18,7 +18,6 @@ record Amount(@JacksonXmlText String value, private final String XML = a2q("1"); - @JacksonTestFailureExpected @Test public void testDeser() throws Exception { XmlMapper mapper = new XmlMapper();