From dfdd1679cbce5b39f2c594052a06fec458162bfd Mon Sep 17 00:00:00 2001 From: ahcodedthat <83854662+ahcodedthat@users.noreply.github.com> Date: Fri, 8 Mar 2024 14:45:53 -0800 Subject: [PATCH 1/2] Add a `ToXmlGenerator.Feature` to use XML Schema-compatible representation for floating-point infinity. Fixes #643. --- .../dataformat/xml/ser/ToXmlGenerator.java | 39 ++++++++++ .../dataformat/xml/ser/TestSerialization.java | 74 +++++++++++++++++++ 2 files changed, 113 insertions(+) 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 73c4e6735..45de08796 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 @@ -20,6 +20,7 @@ import com.fasterxml.jackson.core.json.JsonWriteContext; import com.fasterxml.jackson.core.util.JacksonFeatureSet; import com.fasterxml.jackson.dataformat.xml.XmlPrettyPrinter; +import com.fasterxml.jackson.dataformat.xml.deser.FromXmlParser; import com.fasterxml.jackson.dataformat.xml.util.DefaultXmlPrettyPrinter; import com.fasterxml.jackson.dataformat.xml.util.StaxUtil; @@ -106,6 +107,34 @@ 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 FromXmlParser.Feature}. + *

+ * Feature is disabled by default for backwards compatibility. + */ + XML_SCHEMA_CONFORMING_FLOATS(false), ; final boolean _defaultState; @@ -1174,6 +1203,11 @@ public void writeNumber(long l) throws IOException @Override public void writeNumber(double d) throws IOException { + if (Double.isInfinite(d) && isEnabled(Feature.XML_SCHEMA_CONFORMING_FLOATS)) { + writeNumber(d > 0d ? "INF" : "-INF"); + return; + } + _verifyValueWrite("write number"); if (_nextName == null) { handleMissingName(); @@ -1202,6 +1236,11 @@ public void writeNumber(double d) throws IOException @Override public void writeNumber(float f) throws IOException { + if (Float.isInfinite(f) && isEnabled(Feature.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 0d4932017..a54e8b252 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 @@ -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; @@ -175,4 +191,62 @@ public void testJAXB() throws Exception System.out.println("JAXB -> "+sw); } */ + + public void testFloatInfinity() throws IOException + { + 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 IOException + { + _xmlMapper.configure(ToXmlGenerator.Feature.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 IOException + { + 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 IOException + { + _xmlMapper.configure(ToXmlGenerator.Feature.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); + } } From a5aadd692a4e224a3fbfcb7a94a4676148a517d3 Mon Sep 17 00:00:00 2001 From: Tatu Saloranta Date: Tue, 12 Mar 2024 17:41:32 -0700 Subject: [PATCH 2/2] Add release notes, minor other tweaks --- release-notes/CREDITS-2.x | 6 ++++ release-notes/VERSION-2.x | 3 ++ .../dataformat/xml/ser/ToXmlGenerator.java | 12 ++++---- .../dataformat/xml/ser/TestSerialization.java | 30 +++++++++---------- 4 files changed, 31 insertions(+), 20 deletions(-) diff --git a/release-notes/CREDITS-2.x b/release-notes/CREDITS-2.x index 5ba8e773c..6a3a95861 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 d9cd9be2a..a4ddecee2 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.1 (24-Dec-2023) 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 45de08796..7721faebe 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 @@ -20,7 +20,6 @@ import com.fasterxml.jackson.core.json.JsonWriteContext; import com.fasterxml.jackson.core.util.JacksonFeatureSet; import com.fasterxml.jackson.dataformat.xml.XmlPrettyPrinter; -import com.fasterxml.jackson.dataformat.xml.deser.FromXmlParser; import com.fasterxml.jackson.dataformat.xml.util.DefaultXmlPrettyPrinter; import com.fasterxml.jackson.dataformat.xml.util.StaxUtil; @@ -130,11 +129,14 @@ public enum Feature implements FormatFeature * double). *

* When deserializing, Jackson always understands both representations, - * so there is no corresponding {@link FromXmlParser.Feature}. + * 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 */ - XML_SCHEMA_CONFORMING_FLOATS(false), + WRITE_XML_SCHEMA_CONFORMING_FLOATS(false), ; final boolean _defaultState; @@ -1203,7 +1205,7 @@ public void writeNumber(long l) throws IOException @Override public void writeNumber(double d) throws IOException { - if (Double.isInfinite(d) && isEnabled(Feature.XML_SCHEMA_CONFORMING_FLOATS)) { + if (Double.isInfinite(d) && isEnabled(Feature.WRITE_XML_SCHEMA_CONFORMING_FLOATS)) { writeNumber(d > 0d ? "INF" : "-INF"); return; } @@ -1236,7 +1238,7 @@ public void writeNumber(double d) throws IOException @Override public void writeNumber(float f) throws IOException { - if (Float.isInfinite(f) && isEnabled(Feature.XML_SCHEMA_CONFORMING_FLOATS)) { + if (Float.isInfinite(f) && isEnabled(Feature.WRITE_XML_SCHEMA_CONFORMING_FLOATS)) { writeNumber(f > 0f ? "INF" : "-INF"); return; } 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 a54e8b252..de4b490c4 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; @@ -97,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); @@ -112,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); @@ -120,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(); @@ -152,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); @@ -168,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); @@ -192,7 +192,7 @@ public void testJAXB() throws Exception } */ - public void testFloatInfinity() throws IOException + public void testFloatInfinity() throws Exception { Floats infinite = new Floats(); infinite.attr = Float.POSITIVE_INFINITY; @@ -208,9 +208,9 @@ public void testFloatInfinity() throws IOException checkFloatInfinity(finite, true, "1337.875"); } - private void checkFloatInfinity(Floats original, boolean xmlSchemaConforming, String expectedXml) throws IOException + private void checkFloatInfinity(Floats original, boolean xmlSchemaConforming, String expectedXml) throws Exception { - _xmlMapper.configure(ToXmlGenerator.Feature.XML_SCHEMA_CONFORMING_FLOATS, xmlSchemaConforming); + _xmlMapper.configure(ToXmlGenerator.Feature.WRITE_XML_SCHEMA_CONFORMING_FLOATS, xmlSchemaConforming); String xml = _xmlMapper.writeValueAsString(original); xml = removeSjsxpNamespace(xml); @@ -221,7 +221,7 @@ private void checkFloatInfinity(Floats original, boolean xmlSchemaConforming, St assertEquals(original.elem, deserialized.elem); } - public void testDoubleInfinity() throws IOException + public void testDoubleInfinity() throws Exception { Doubles infinite = new Doubles(); infinite.attr = Double.POSITIVE_INFINITY; @@ -237,9 +237,9 @@ public void testDoubleInfinity() throws IOException checkDoubleInfinity(finite, true, "1337.875"); } - private void checkDoubleInfinity(Doubles original, boolean xmlSchemaConforming, String expectedXml) throws IOException + private void checkDoubleInfinity(Doubles original, boolean xmlSchemaConforming, String expectedXml) throws Exception { - _xmlMapper.configure(ToXmlGenerator.Feature.XML_SCHEMA_CONFORMING_FLOATS, xmlSchemaConforming); + _xmlMapper.configure(ToXmlGenerator.Feature.WRITE_XML_SCHEMA_CONFORMING_FLOATS, xmlSchemaConforming); String xml = _xmlMapper.writeValueAsString(original); xml = removeSjsxpNamespace(xml);