diff --git a/release-notes/CREDITS-2.x b/release-notes/CREDITS-2.x index 5ba8e773..6a3a9586 100644 --- a/release-notes/CREDITS-2.x +++ b/release-notes/CREDITS-2.x @@ -249,3 +249,9 @@ Arthur Chan (@arthurscchan) * Reported, contributed fix for #618: `ArrayIndexOutOfBoundsException` thrown for invalid ending XML string when using JDK default Stax XML parser (2.17.0) + +Alex H (@ahcodedthat) + +* Contribtued #643: XML serialization of floating-point infinity is incompatible + with JAXB and XML Schema + (2.17.0) diff --git a/release-notes/VERSION-2.x b/release-notes/VERSION-2.x index 1fcba996..7d9b9b66 100644 --- a/release-notes/VERSION-2.x +++ b/release-notes/VERSION-2.x @@ -18,6 +18,9 @@ Project: jackson-dataformat-xml (FromXmlParser.Feature.AUTO_DETECT_XSI_TYPE) #637: `JacksonXmlAnnotationIntrospector.findNamespace()` should properly merge namespace information +#643: XML serialization of floating-point infinity is incompatible + with JAXB and XML Schema + (contributed by Alex H) * Upgrade Woodstox to 6.6.1 (latest at the time) 2.16.2 (09-Mar-2024) 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 73c4e673..7721faeb 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 @@ -106,6 +106,37 @@ public enum Feature implements FormatFeature * @since 2.17 */ AUTO_DETECT_XSI_TYPE(false), + + /** + * Feature that determines how floating-point infinity values are + * serialized. + *

+ * By default, {@link Float#POSITIVE_INFINITY} and + * {@link Double#POSITIVE_INFINITY} are serialized as {@code Infinity}, + * and {@link Float#NEGATIVE_INFINITY} and + * {@link Double#NEGATIVE_INFINITY} are serialized as + * {@code -Infinity}. This is the representation that Java normally + * uses for these values (see {@link Float#toString(float)} and + * {@link Double#toString(double)}), but JAXB and other XML + * Schema-conforming readers won't understand it. + *

+ * With this feature enabled, these values are instead serialized as + * {@code INF} and {@code -INF}, respectively. This is the + * representation that XML Schema and JAXB use (see the XML Schema + * primitive types + * float + * and + * double). + *

+ * When deserializing, Jackson always understands both representations, + * so there is no corresponding + * {@link com.fasterxml.jackson.dataformat.xml.deser.FromXmlParser.Feature}. + *

+ * Feature is disabled by default for backwards compatibility. + * + * @since 2.17 + */ + WRITE_XML_SCHEMA_CONFORMING_FLOATS(false), ; final boolean _defaultState; @@ -1174,6 +1205,11 @@ public void writeNumber(long l) throws IOException @Override public void writeNumber(double d) throws IOException { + if (Double.isInfinite(d) && isEnabled(Feature.WRITE_XML_SCHEMA_CONFORMING_FLOATS)) { + writeNumber(d > 0d ? "INF" : "-INF"); + return; + } + _verifyValueWrite("write number"); if (_nextName == null) { handleMissingName(); @@ -1202,6 +1238,11 @@ public void writeNumber(double d) throws IOException @Override public void writeNumber(float f) throws IOException { + if (Float.isInfinite(f) && isEnabled(Feature.WRITE_XML_SCHEMA_CONFORMING_FLOATS)) { + writeNumber(f > 0f ? "INF" : "-INF"); + return; + } + _verifyValueWrite("write number"); if (_nextName == null) { handleMissingName(); diff --git a/src/test/java/com/fasterxml/jackson/dataformat/xml/ser/TestSerialization.java b/src/test/java/com/fasterxml/jackson/dataformat/xml/ser/TestSerialization.java index 0d493201..de4b490c 100644 --- a/src/test/java/com/fasterxml/jackson/dataformat/xml/ser/TestSerialization.java +++ b/src/test/java/com/fasterxml/jackson/dataformat/xml/ser/TestSerialization.java @@ -1,9 +1,9 @@ package com.fasterxml.jackson.dataformat.xml.ser; -import java.io.*; import java.util.*; import com.fasterxml.jackson.annotation.JsonProperty; + import com.fasterxml.jackson.dataformat.xml.XmlMapper; import com.fasterxml.jackson.dataformat.xml.XmlTestBase; import com.fasterxml.jackson.dataformat.xml.annotation.JacksonXmlCData; @@ -31,6 +31,22 @@ static class AttrAndElem public int attr = 42; } + static class Floats + { + public float elem; + + @JacksonXmlProperty(isAttribute=true, localName="attr") + public float attr; + } + + static class Doubles + { + public double elem; + + @JacksonXmlProperty(isAttribute=true, localName="attr") + public double attr; + } + static class WrapperBean { public T value; @@ -81,14 +97,14 @@ static class CustomMap extends LinkedHashMap { } private final XmlMapper _xmlMapper = new XmlMapper(); - public void testSimpleAttribute() throws IOException + public void testSimpleAttribute() throws Exception { String xml = _xmlMapper.writeValueAsString(new AttributeBean()); xml = removeSjsxpNamespace(xml); assertEquals("", xml); } - public void testSimpleNsElem() throws IOException + public void testSimpleNsElem() throws Exception { String xml = _xmlMapper.writeValueAsString(new NsElemBean()); xml = removeSjsxpNamespace(xml); @@ -96,7 +112,7 @@ public void testSimpleNsElem() throws IOException assertEquals("blah", xml); } - public void testSimpleNsElemWithJsonProp() throws IOException + public void testSimpleNsElemWithJsonProp() throws Exception { String xml = _xmlMapper.writeValueAsString(new NsElemBean2()); xml = removeSjsxpNamespace(xml); @@ -104,14 +120,14 @@ public void testSimpleNsElemWithJsonProp() throws IOException assertEquals("blah", xml); } - public void testSimpleAttrAndElem() throws IOException + public void testSimpleAttrAndElem() throws Exception { String xml = _xmlMapper.writeValueAsString(new AttrAndElem()); xml = removeSjsxpNamespace(xml); assertEquals("whatever", xml); } - public void testMap() throws IOException + public void testMap() throws Exception { // First, map in a general wrapper LinkedHashMap map = new LinkedHashMap(); @@ -136,7 +152,7 @@ public void testMap() throws IOException xml); } - public void testNakedMap() throws IOException + public void testNakedMap() throws Exception { CustomMap input = new CustomMap(); input.put("a", 123); @@ -152,14 +168,14 @@ public void testNakedMap() throws IOException assertEquals(Integer.valueOf(456), result.get("b")); } - public void testCDataString() throws IOException + public void testCDataString() throws Exception { String xml = _xmlMapper.writeValueAsString(new CDataStringBean()); xml = removeSjsxpNamespace(xml); assertEquals("", xml); } - public void testCDataStringArray() throws IOException + public void testCDataStringArray() throws Exception { String xml = _xmlMapper.writeValueAsString(new CDataStringArrayBean()); xml = removeSjsxpNamespace(xml); @@ -175,4 +191,62 @@ public void testJAXB() throws Exception System.out.println("JAXB -> "+sw); } */ + + public void testFloatInfinity() throws Exception + { + Floats infinite = new Floats(); + infinite.attr = Float.POSITIVE_INFINITY; + infinite.elem = Float.NEGATIVE_INFINITY; + + Floats finite = new Floats(); + finite.attr = 42.5f; + finite.elem = 1337.875f; + + checkFloatInfinity(infinite, false, "-Infinity"); + checkFloatInfinity(finite, false, "1337.875"); + checkFloatInfinity(infinite, true, "-INF"); + checkFloatInfinity(finite, true, "1337.875"); + } + + private void checkFloatInfinity(Floats original, boolean xmlSchemaConforming, String expectedXml) throws Exception + { + _xmlMapper.configure(ToXmlGenerator.Feature.WRITE_XML_SCHEMA_CONFORMING_FLOATS, xmlSchemaConforming); + + String xml = _xmlMapper.writeValueAsString(original); + xml = removeSjsxpNamespace(xml); + assertEquals(expectedXml, xml); + + Floats deserialized = _xmlMapper.readValue(xml, Floats.class); + assertEquals(original.attr, deserialized.attr); + assertEquals(original.elem, deserialized.elem); + } + + public void testDoubleInfinity() throws Exception + { + Doubles infinite = new Doubles(); + infinite.attr = Double.POSITIVE_INFINITY; + infinite.elem = Double.NEGATIVE_INFINITY; + + Doubles finite = new Doubles(); + finite.attr = 42.5d; + finite.elem = 1337.875d; + + checkDoubleInfinity(infinite, false, "-Infinity"); + checkDoubleInfinity(finite, false, "1337.875"); + checkDoubleInfinity(infinite, true, "-INF"); + checkDoubleInfinity(finite, true, "1337.875"); + } + + private void checkDoubleInfinity(Doubles original, boolean xmlSchemaConforming, String expectedXml) throws Exception + { + _xmlMapper.configure(ToXmlGenerator.Feature.WRITE_XML_SCHEMA_CONFORMING_FLOATS, xmlSchemaConforming); + + String xml = _xmlMapper.writeValueAsString(original); + xml = removeSjsxpNamespace(xml); + assertEquals(expectedXml, xml); + + Doubles deserialized = _xmlMapper.readValue(xml, Doubles.class); + assertEquals(original.attr, deserialized.attr); + assertEquals(original.elem, deserialized.elem); + } }