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);