Skip to content

Fix #643: Add ToXmlGenerator.Feature or allowing XML Schema/JAXB compatible Infinity representation #644

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 2 commits into from
Mar 13, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions release-notes/CREDITS-2.x
Original file line number Diff line number Diff line change
Expand Up @@ -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)
3 changes: 3 additions & 0 deletions release-notes/VERSION-2.x
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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.
*<p>
* 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.
*<p>
* 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
* <a href="https://www.w3.org/TR/xmlschema-2/#float"><code>float</code></a>
* and
* <a href="https://www.w3.org/TR/xmlschema-2/#double"><code>double</code></a>).
*<p>
* When deserializing, Jackson always understands both representations,
* so there is no corresponding
* {@link com.fasterxml.jackson.dataformat.xml.deser.FromXmlParser.Feature}.
*<p>
* Feature is disabled by default for backwards compatibility.
*
* @since 2.17
*/
WRITE_XML_SCHEMA_CONFORMING_FLOATS(false),
;

final boolean _defaultState;
Expand Down Expand Up @@ -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();
Expand Down Expand Up @@ -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();
Expand Down
Original file line number Diff line number Diff line change
@@ -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;
Expand Down Expand Up @@ -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<T>
{
public T value;
Expand Down Expand Up @@ -81,37 +97,37 @@ static class CustomMap extends LinkedHashMap<String, Integer> { }

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("<AttributeBean attr=\"something\"/>", xml);
}

public void testSimpleNsElem() throws IOException
public void testSimpleNsElem() throws Exception
{
String xml = _xmlMapper.writeValueAsString(new NsElemBean());
xml = removeSjsxpNamespace(xml);
// here we assume woodstox automatic prefixes, not very robust but:
assertEquals("<NsElemBean><wstxns1:text xmlns:wstxns1=\"http://foo\">blah</wstxns1:text></NsElemBean>", xml);
}

public void testSimpleNsElemWithJsonProp() throws IOException
public void testSimpleNsElemWithJsonProp() throws Exception
{
String xml = _xmlMapper.writeValueAsString(new NsElemBean2());
xml = removeSjsxpNamespace(xml);
// here we assume woodstox automatic prefixes, not very robust but:
assertEquals("<NsElemBean2><wstxns1:text xmlns:wstxns1=\"http://foo\">blah</wstxns1:text></NsElemBean2>", xml);
}

public void testSimpleAttrAndElem() throws IOException
public void testSimpleAttrAndElem() throws Exception
{
String xml = _xmlMapper.writeValueAsString(new AttrAndElem());
xml = removeSjsxpNamespace(xml);
assertEquals("<AttrAndElem id=\"42\"><elem>whatever</elem></AttrAndElem>", xml);
}

public void testMap() throws IOException
public void testMap() throws Exception
{
// First, map in a general wrapper
LinkedHashMap<String,Integer> map = new LinkedHashMap<String,Integer>();
Expand All @@ -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);
Expand All @@ -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("<CDataStringBean><value><![CDATA[<some<data\"]]></value></CDataStringBean>", xml);
}

public void testCDataStringArray() throws IOException
public void testCDataStringArray() throws Exception
{
String xml = _xmlMapper.writeValueAsString(new CDataStringArrayBean());
xml = removeSjsxpNamespace(xml);
Expand All @@ -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, "<Floats attr=\"Infinity\"><elem>-Infinity</elem></Floats>");
checkFloatInfinity(finite, false, "<Floats attr=\"42.5\"><elem>1337.875</elem></Floats>");
checkFloatInfinity(infinite, true, "<Floats attr=\"INF\"><elem>-INF</elem></Floats>");
checkFloatInfinity(finite, true, "<Floats attr=\"42.5\"><elem>1337.875</elem></Floats>");
}

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, "<Doubles attr=\"Infinity\"><elem>-Infinity</elem></Doubles>");
checkDoubleInfinity(finite, false, "<Doubles attr=\"42.5\"><elem>1337.875</elem></Doubles>");
checkDoubleInfinity(infinite, true, "<Doubles attr=\"INF\"><elem>-INF</elem></Doubles>");
checkDoubleInfinity(finite, true, "<Doubles attr=\"42.5\"><elem>1337.875</elem></Doubles>");
}

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