Skip to content

Commit

Permalink
Fix #643: Add ToXmlGenerator.Feature or allowing XML Schema/JAXB co…
Browse files Browse the repository at this point in the history
…mpatible Infinity representation (#644)
  • Loading branch information
ahcodedthat committed Mar 13, 2024
1 parent e80909f commit be5e719
Show file tree
Hide file tree
Showing 4 changed files with 133 additions and 9 deletions.
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.2 (09-Mar-2024)
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);
}
}

0 comments on commit be5e719

Please sign in to comment.