Skip to content

Commit df04a74

Browse files
committed
Add a ToXmlGenerator.Feature to use XML Schema-compatible representation for floating-point infinity.
Fixes FasterXML#643.
1 parent b782f4b commit df04a74

File tree

2 files changed

+141
-28
lines changed

2 files changed

+141
-28
lines changed

src/main/java/com/fasterxml/jackson/dataformat/xml/ser/ToXmlGenerator.java

Lines changed: 60 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@
2020
import com.fasterxml.jackson.core.json.JsonWriteContext;
2121
import com.fasterxml.jackson.core.util.JacksonFeatureSet;
2222
import com.fasterxml.jackson.dataformat.xml.XmlPrettyPrinter;
23+
import com.fasterxml.jackson.dataformat.xml.deser.FromXmlParser;
2324
import com.fasterxml.jackson.dataformat.xml.util.DefaultXmlPrettyPrinter;
2425
import com.fasterxml.jackson.dataformat.xml.util.StaxUtil;
2526

@@ -106,6 +107,34 @@ public enum Feature implements FormatFeature
106107
* @since 2.17
107108
*/
108109
AUTO_DETECT_XSI_TYPE(false),
110+
111+
/**
112+
* Feature that determines how floating-point infinity values are
113+
* serialized.
114+
*<p>
115+
* By default, {@link Float#POSITIVE_INFINITY} and
116+
* {@link Double#POSITIVE_INFINITY} are serialized as {@code Infinity},
117+
* and {@link Float#NEGATIVE_INFINITY} and
118+
* {@link Double#NEGATIVE_INFINITY} are serialized as
119+
* {@code -Infinity}. This is the representation that Java normally
120+
* uses for these values (see {@link Float#toString(float)} and
121+
* {@link Double#toString(double)}), but JAXB and other XML
122+
* Schema-conforming readers won't understand it.
123+
*<p>
124+
* With this feature enabled, these values are instead serialized as
125+
* {@code INF} and {@code -INF}, respectively. This is the
126+
* representation that XML Schema and JAXB use (see the XML Schema
127+
* primitive types
128+
* <a href="https://www.w3.org/TR/xmlschema-2/#float"><code>float</code></a>
129+
* and
130+
* <a href="https://www.w3.org/TR/xmlschema-2/#double"><code>double</code></a>).
131+
*<p>
132+
* When deserializing, Jackson always understands both representations,
133+
* so there is no corresponding {@link FromXmlParser.Feature}.
134+
*<p>
135+
* Feature is disabled by default for backwards compatibility.
136+
*/
137+
XML_SCHEMA_CONFORMING_FLOATS(false),
109138
;
110139

111140
final boolean _defaultState;
@@ -145,7 +174,7 @@ private Feature(boolean defaultState) {
145174
protected final XMLStreamWriter2 _xmlWriter;
146175

147176
protected final XMLStreamWriter _originalXmlWriter;
148-
177+
149178
/**
150179
* Marker flag set if the underlying stream writer has to emulate
151180
* Stax2 API: this is problematic if trying to use {@link #writeRaw} calls.
@@ -184,7 +213,7 @@ private Feature(boolean defaultState) {
184213

185214
/**
186215
* Marker set when {@link #initGenerator()} has been called or not.
187-
*
216+
*
188217
* @since 2.2
189218
*/
190219
protected boolean _initialized;
@@ -218,7 +247,7 @@ private Feature(boolean defaultState) {
218247

219248
/**
220249
* To support proper serialization of arrays it is necessary to keep
221-
* stack of element names, so that we can "revert" to earlier
250+
* stack of element names, so that we can "revert" to earlier
222251
*/
223252
protected LinkedList<QName> _elementNameStack = new LinkedList<QName>();
224253

@@ -403,7 +432,7 @@ public boolean inRoot() {
403432
public XMLStreamWriter getStaxWriter() {
404433
return _xmlWriter;
405434
}
406-
435+
407436
/*
408437
/**********************************************************
409438
/* Extended API, passing XML specific settings
@@ -424,7 +453,7 @@ public void setNextIsCData(boolean isCData)
424453
{
425454
_nextIsCData = isCData;
426455
}
427-
456+
428457
public final void setNextName(QName name)
429458
{
430459
_nextName = name;
@@ -433,7 +462,7 @@ public final void setNextName(QName name)
433462
/**
434463
* Method that does same as {@link #setNextName}, unless
435464
* a name has already been set.
436-
*
465+
*
437466
* @since 2.1.2
438467
*/
439468
public final boolean setNextNameIfMissing(QName name)
@@ -444,11 +473,11 @@ public final boolean setNextNameIfMissing(QName name)
444473
}
445474
return false;
446475
}
447-
476+
448477
/**
449478
* Methdod called when a structured (collection, array, map) is being
450479
* output.
451-
*
480+
*
452481
* @param wrapperName Element used as wrapper around elements, if any (null if none)
453482
* @param wrappedName Element used around individual content items (can not
454483
* be null)
@@ -491,7 +520,7 @@ public void finishWrappedValue(QName wrapperName, QName wrappedName) throws IOEx
491520

492521
/**
493522
* Trivial helper method called when to add a replicated wrapper name
494-
*
523+
*
495524
* @since 2.2
496525
*/
497526
public void writeRepeatedFieldName() throws IOException
@@ -500,7 +529,7 @@ public void writeRepeatedFieldName() throws IOException
500529
_reportError("Can not write a field name, expecting a value");
501530
}
502531
}
503-
532+
504533
/*
505534
/**********************************************************
506535
/* JsonGenerator method overrides
@@ -582,7 +611,7 @@ public final void writeStartArray() throws IOException
582611
// nothing to do here; no-operation
583612
}
584613
}
585-
614+
586615
@Override
587616
public final void writeEndArray() throws IOException
588617
{
@@ -640,7 +669,7 @@ public final void _handleStartObject() throws IOException
640669
StaxUtil.throwAsGenerationException(e, this);
641670
}
642671
}
643-
672+
644673
// note: public just because pretty printer needs to make a callback
645674
public final void _handleEndObject() throws IOException
646675
{
@@ -664,7 +693,7 @@ public final void _handleEndObject() throws IOException
664693
StaxUtil.throwAsGenerationException(e, this);
665694
}
666695
}
667-
696+
668697
/*
669698
/**********************************************************
670699
/* Output method implementations, textual
@@ -706,12 +735,12 @@ public void writeString(String text) throws IOException
706735
_xmlWriter.writeCharacters(text);
707736
}
708737
_xmlWriter.writeEndElement();
709-
}
738+
}
710739
} catch (XMLStreamException e) {
711740
StaxUtil.throwAsGenerationException(e, this);
712741
}
713-
}
714-
742+
}
743+
715744
@Override
716745
public void writeString(char[] text, int offset, int len) throws IOException
717746
{
@@ -751,7 +780,7 @@ public void writeString(char[] text, int offset, int len) throws IOException
751780
public void writeString(SerializableString text) throws IOException {
752781
writeString(text.getValue());
753782
}
754-
783+
755784
@Override
756785
public void writeRawUTF8String(byte[] text, int offset, int length) throws IOException
757786
{
@@ -901,7 +930,7 @@ public void writeRaw(char c) throws IOException
901930
{
902931
writeRaw(String.valueOf(c));
903932
}
904-
933+
905934
/*
906935
/**********************************************************
907936
/* Output method implementations, base64-encoded binary
@@ -987,7 +1016,7 @@ public int writeBinary(Base64Variant b64variant, InputStream data, int dataLengt
9871016
}
9881017

9891018
private void writeStreamAsBinary(org.codehaus.stax2.typed.Base64Variant stax2base64v,
990-
InputStream data, int len) throws IOException, XMLStreamException
1019+
InputStream data, int len) throws IOException, XMLStreamException
9911020
{
9921021
// base64 encodes up to 3 bytes into a 4 bytes string
9931022
byte[] tmp = new byte[3];
@@ -1024,7 +1053,7 @@ private byte[] toFullBuffer(byte[] data, int offset, int len)
10241053
return result;
10251054
}
10261055

1027-
private byte[] toFullBuffer(InputStream data, final int len) throws IOException
1056+
private byte[] toFullBuffer(InputStream data, final int len) throws IOException
10281057
{
10291058
byte[] result = new byte[len];
10301059
int offset = 0;
@@ -1174,6 +1203,11 @@ public void writeNumber(long l) throws IOException
11741203
@Override
11751204
public void writeNumber(double d) throws IOException
11761205
{
1206+
if (Double.isInfinite(d) && isEnabled(Feature.XML_SCHEMA_CONFORMING_FLOATS)) {
1207+
writeNumber(d > 0d ? "INF" : "-INF");
1208+
return;
1209+
}
1210+
11771211
_verifyValueWrite("write number");
11781212
if (_nextName == null) {
11791213
handleMissingName();
@@ -1202,6 +1236,11 @@ public void writeNumber(double d) throws IOException
12021236
@Override
12031237
public void writeNumber(float f) throws IOException
12041238
{
1239+
if (Float.isInfinite(f) && isEnabled(Feature.XML_SCHEMA_CONFORMING_FLOATS)) {
1240+
writeNumber(f > 0f ? "INF" : "-INF");
1241+
return;
1242+
}
1243+
12051244
_verifyValueWrite("write number");
12061245
if (_nextName == null) {
12071246
handleMissingName();
@@ -1418,7 +1457,7 @@ protected boolean checkNextIsUnwrapped()
14181457
}
14191458
return false;
14201459
}
1421-
1460+
14221461
protected void handleMissingName() {
14231462
throw new IllegalStateException("No element/attribute name specified when trying to output element");
14241463
}

src/test/java/com/fasterxml/jackson/dataformat/xml/ser/TestSerialization.java

Lines changed: 81 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -26,11 +26,27 @@ static class AttributeBean
2626
static class AttrAndElem
2727
{
2828
public String elem = "whatever";
29-
29+
3030
@JacksonXmlProperty(isAttribute=true, localName="id")
3131
public int attr = 42;
3232
}
3333

34+
static class Floats
35+
{
36+
public float elem;
37+
38+
@JacksonXmlProperty(isAttribute=true, localName="attr")
39+
public float attr;
40+
}
41+
42+
static class Doubles
43+
{
44+
public double elem;
45+
46+
@JacksonXmlProperty(isAttribute=true, localName="attr")
47+
public double attr;
48+
}
49+
3450
static class WrapperBean<T>
3551
{
3652
public T value;
@@ -58,7 +74,7 @@ static class NsElemBean2
5874
@JsonProperty(namespace="http://foo")
5975
public String text = "blah";
6076
}
61-
77+
6278
static class CDataStringBean
6379
{
6480
@JacksonXmlCData
@@ -103,7 +119,7 @@ public void testSimpleNsElemWithJsonProp() throws IOException
103119
// here we assume woodstox automatic prefixes, not very robust but:
104120
assertEquals("<NsElemBean2><wstxns1:text xmlns:wstxns1=\"http://foo\">blah</wstxns1:text></NsElemBean2>", xml);
105121
}
106-
122+
107123
public void testSimpleAttrAndElem() throws IOException
108124
{
109125
String xml = _xmlMapper.writeValueAsString(new AttrAndElem());
@@ -119,7 +135,7 @@ public void testMap() throws IOException
119135
map.put("b", 2);
120136

121137
String xml;
122-
138+
123139
xml = _xmlMapper.writeValueAsString(new WrapperBean<Map<?,?>>(map));
124140
assertEquals("<WrapperBean><value>"
125141
+"<a>1</a>"
@@ -138,14 +154,14 @@ public void testMap() throws IOException
138154

139155
public void testNakedMap() throws IOException
140156
{
141-
CustomMap input = new CustomMap();
157+
CustomMap input = new CustomMap();
142158
input.put("a", 123);
143159
input.put("b", 456);
144160
String xml = _xmlMapper.writeValueAsString(input);
145161

146-
162+
147163
// System.err.println("XML = "+xml);
148-
164+
149165
CustomMap result = _xmlMapper.readValue(xml, CustomMap.class);
150166
assertEquals(2, result.size());
151167

@@ -175,4 +191,62 @@ public void testJAXB() throws Exception
175191
System.out.println("JAXB -> "+sw);
176192
}
177193
*/
194+
195+
public void testFloatInfinity() throws IOException
196+
{
197+
Floats infinite = new Floats();
198+
infinite.attr = Float.POSITIVE_INFINITY;
199+
infinite.elem = Float.NEGATIVE_INFINITY;
200+
201+
Floats finite = new Floats();
202+
finite.attr = 42.5f;
203+
finite.elem = 1337.875f;
204+
205+
checkFloatInfinity(infinite, false, "<Floats attr=\"Infinity\"><elem>-Infinity</elem></Floats>");
206+
checkFloatInfinity(finite, false, "<Floats attr=\"42.5\"><elem>1337.875</elem></Floats>");
207+
checkFloatInfinity(infinite, true, "<Floats attr=\"INF\"><elem>-INF</elem></Floats>");
208+
checkFloatInfinity(finite, true, "<Floats attr=\"42.5\"><elem>1337.875</elem></Floats>");
209+
}
210+
211+
private void checkFloatInfinity(Floats original, boolean xmlSchemaConforming, String expectedXml) throws IOException
212+
{
213+
_xmlMapper.configure(ToXmlGenerator.Feature.XML_SCHEMA_CONFORMING_FLOATS, xmlSchemaConforming);
214+
215+
String xml = _xmlMapper.writeValueAsString(original);
216+
xml = removeSjsxpNamespace(xml);
217+
assertEquals(expectedXml, xml);
218+
219+
Floats deserialized = _xmlMapper.readValue(xml, Floats.class);
220+
assertEquals(original.attr, deserialized.attr);
221+
assertEquals(original.elem, deserialized.elem);
222+
}
223+
224+
public void testDoubleInfinity() throws IOException
225+
{
226+
Doubles infinite = new Doubles();
227+
infinite.attr = Double.POSITIVE_INFINITY;
228+
infinite.elem = Double.NEGATIVE_INFINITY;
229+
230+
Doubles finite = new Doubles();
231+
finite.attr = 42.5d;
232+
finite.elem = 1337.875d;
233+
234+
checkDoubleInfinity(infinite, false, "<Doubles attr=\"Infinity\"><elem>-Infinity</elem></Doubles>");
235+
checkDoubleInfinity(finite, false, "<Doubles attr=\"42.5\"><elem>1337.875</elem></Doubles>");
236+
checkDoubleInfinity(infinite, true, "<Doubles attr=\"INF\"><elem>-INF</elem></Doubles>");
237+
checkDoubleInfinity(finite, true, "<Doubles attr=\"42.5\"><elem>1337.875</elem></Doubles>");
238+
}
239+
240+
private void checkDoubleInfinity(Doubles original, boolean xmlSchemaConforming, String expectedXml) throws IOException
241+
{
242+
_xmlMapper.configure(ToXmlGenerator.Feature.XML_SCHEMA_CONFORMING_FLOATS, xmlSchemaConforming);
243+
244+
String xml = _xmlMapper.writeValueAsString(original);
245+
xml = removeSjsxpNamespace(xml);
246+
assertEquals(expectedXml, xml);
247+
248+
Doubles deserialized = _xmlMapper.readValue(xml, Doubles.class);
249+
assertEquals(original.attr, deserialized.attr);
250+
assertEquals(original.elem, deserialized.elem);
251+
}
178252
}

0 commit comments

Comments
 (0)