Skip to content

Commit a52427d

Browse files
committed
Merge branch '2.15'
2 parents 35c21c9 + 2f12257 commit a52427d

File tree

7 files changed

+170
-59
lines changed

7 files changed

+170
-59
lines changed

cbor/src/main/java/tools/jackson/dataformat/cbor/CBORGenerator.java

Lines changed: 67 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -97,6 +97,19 @@ public enum Feature implements FormatFeature {
9797
* @since 2.15
9898
*/
9999
STRINGREF(false),
100+
101+
/**
102+
* Feature that determines whether generator should try to write doubles
103+
* as floats: if {@code true}, will write a {@code double} as a 4-byte float if no
104+
* precision loss will occur; if {@code false}, will always write a {@code double}
105+
* as an 8-byte double.
106+
* <p>
107+
* Default value is {@code false} meaning that doubles will always be written as
108+
* 8-byte values.
109+
*
110+
* @since 2.15
111+
*/
112+
WRITE_MINIMAL_DOUBLES(false),
100113
;
101114

102115
protected final boolean _defaultState;
@@ -160,17 +173,19 @@ public int getMask() {
160173
/**********************************************************************
161174
*/
162175

163-
final protected IOContext _ioContext;
176+
protected final IOContext _ioContext;
164177

165-
final protected OutputStream _out;
178+
protected final OutputStream _out;
166179

167180
/**
168181
* Bit flag composed of bits that indicate which
169182
* {@link CBORGenerator.Feature}s are enabled.
170183
*/
171-
protected int _formatFeatures;
184+
protected final int _formatFeatures;
172185

173-
protected boolean _cfgMinimalInts;
186+
protected final boolean _cfgMinimalInts;
187+
188+
protected final boolean _cfgMinimalDoubles;
174189

175190
/*
176191
/**********************************************************************
@@ -271,6 +286,7 @@ public CBORGenerator(ObjectWriteContext writeCtxt, IOContext ctxt,
271286
: null;
272287
_streamWriteContext = CBORWriteContext.createRootContext(dups);
273288
_cfgMinimalInts = Feature.WRITE_MINIMAL_INTS.enabledIn(formatFeatures);
289+
_cfgMinimalDoubles = Feature.WRITE_MINIMAL_DOUBLES.enabledIn(formatFeatures);
274290
_out = out;
275291
_bufferRecyclable = true;
276292
_stringRefs = Feature.STRINGREF.enabledIn(formatFeatures) ? new HashMap<>() : null;
@@ -308,6 +324,7 @@ public CBORGenerator(ObjectWriteContext writeCtxt, IOContext ctxt,
308324
: null;
309325
_streamWriteContext = CBORWriteContext.createRootContext(dups);
310326
_cfgMinimalInts = Feature.WRITE_MINIMAL_INTS.enabledIn(formatFeatures);
327+
_cfgMinimalDoubles = Feature.WRITE_MINIMAL_DOUBLES.enabledIn(formatFeatures);
311328
_out = out;
312329
_bufferRecyclable = bufferRecyclable;
313330
_outputTail = offset;
@@ -389,10 +406,13 @@ public TokenStreamContext streamWriteContext() {
389406
/**********************************************************************
390407
*/
391408

409+
/*
392410
public CBORGenerator enable(Feature f) {
393411
_formatFeatures |= f.getMask();
394412
if (f == Feature.WRITE_MINIMAL_INTS) {
395413
_cfgMinimalInts = true;
414+
} else if (f == Feature.WRITE_MINIMAL_DOUBLES) {
415+
_cfgMinimalDoubles = true;
396416
}
397417
return this;
398418
}
@@ -401,9 +421,12 @@ public CBORGenerator disable(Feature f) {
401421
_formatFeatures &= ~f.getMask();
402422
if (f == Feature.WRITE_MINIMAL_INTS) {
403423
_cfgMinimalInts = false;
424+
} else if (f == Feature.WRITE_MINIMAL_DOUBLES) {
425+
_cfgMinimalDoubles = false;
404426
}
405427
return this;
406428
}
429+
*/
407430

408431
public final boolean isEnabled(Feature f) {
409432
return (_formatFeatures & f.getMask()) != 0;
@@ -610,8 +633,14 @@ public JsonGenerator writeArray(double[] array, int offset, int length) throws J
610633
// short-cut, do not create child array context etc
611634
_verifyValueWrite("write int array");
612635
_writeLengthMarker(PREFIX_TYPE_ARRAY, length);
613-
for (int i = offset, end = offset+length; i < end; ++i) {
614-
_writeDoubleNoCheck(array[i]);
636+
if (_cfgMinimalDoubles) {
637+
for (int i = offset, end = offset+length; i < end; ++i) {
638+
_writeDoubleMinimal(array[i]);
639+
}
640+
} else {
641+
for (int i = offset, end = offset+length; i < end; ++i) {
642+
_writeDoubleNoCheck(array[i]);
643+
}
615644
}
616645
return this;
617646
}
@@ -705,8 +734,24 @@ private final void _writeLongNoCheck(long l) throws JacksonException
705734
_outputBuffer[_outputTail++] = (byte) i;
706735
}
707736

737+
private final void _writeFloatNoCheck(float f) throws JacksonException {
738+
_ensureRoomForOutput(5);
739+
/*
740+
* 17-Apr-2010, tatu: could also use 'floatToIntBits', but it seems more
741+
* accurate to use exact representation; and possibly faster. However,
742+
* if there are cases where collapsing of NaN was needed (for non-Java
743+
* clients), this can be changed
744+
*/
745+
int i = Float.floatToRawIntBits(f);
746+
_outputBuffer[_outputTail++] = BYTE_FLOAT32;
747+
_outputBuffer[_outputTail++] = (byte) (i >> 24);
748+
_outputBuffer[_outputTail++] = (byte) (i >> 16);
749+
_outputBuffer[_outputTail++] = (byte) (i >> 8);
750+
_outputBuffer[_outputTail++] = (byte) i;
751+
}
752+
708753
private final void _writeDoubleNoCheck(double d) throws JacksonException {
709-
_ensureRoomForOutput(11);
754+
_ensureRoomForOutput(9);
710755
// 17-Apr-2010, tatu: could also use 'doubleToIntBits', but it seems
711756
// more accurate to use exact representation; and possibly faster.
712757
// However, if there are cases where collapsing of NaN was needed (for
@@ -726,6 +771,15 @@ private final void _writeDoubleNoCheck(double d) throws JacksonException {
726771
_outputBuffer[_outputTail++] = (byte) i;
727772
}
728773

774+
private final void _writeDoubleMinimal(double d) throws JacksonException {
775+
float f = (float)d;
776+
if (f == d) {
777+
_writeFloatNoCheck(f);
778+
} else {
779+
_writeDoubleNoCheck(d);
780+
}
781+
}
782+
729783
/*
730784
/**********************************************************************
731785
/* Output method implementations, textual
@@ -1110,47 +1164,18 @@ protected void _write(BigInteger v) throws JacksonException {
11101164
@Override
11111165
public JsonGenerator writeNumber(double d) throws JacksonException {
11121166
_verifyValueWrite("write number");
1113-
_ensureRoomForOutput(11);
1114-
/*
1115-
* 17-Apr-2010, tatu: could also use 'doubleToIntBits', but it seems
1116-
* more accurate to use exact representation; and possibly faster.
1117-
* However, if there are cases where collapsing of NaN was needed (for
1118-
* non-Java clients), this can be changed
1119-
*/
1120-
long l = Double.doubleToRawLongBits(d);
1121-
_outputBuffer[_outputTail++] = BYTE_FLOAT64;
1122-
1123-
int i = (int) (l >> 32);
1124-
_outputBuffer[_outputTail++] = (byte) (i >> 24);
1125-
_outputBuffer[_outputTail++] = (byte) (i >> 16);
1126-
_outputBuffer[_outputTail++] = (byte) (i >> 8);
1127-
_outputBuffer[_outputTail++] = (byte) i;
1128-
i = (int) l;
1129-
_outputBuffer[_outputTail++] = (byte) (i >> 24);
1130-
_outputBuffer[_outputTail++] = (byte) (i >> 16);
1131-
_outputBuffer[_outputTail++] = (byte) (i >> 8);
1132-
_outputBuffer[_outputTail++] = (byte) i;
1167+
if (_cfgMinimalDoubles) {
1168+
_writeDoubleMinimal(d);
1169+
} else {
1170+
_writeDoubleNoCheck(d);
1171+
}
11331172
return this;
11341173
}
11351174

11361175
@Override
11371176
public JsonGenerator writeNumber(float f) throws JacksonException {
1138-
// Ok, now, we needed token type byte plus 5 data bytes (7 bits each)
1139-
_ensureRoomForOutput(6);
11401177
_verifyValueWrite("write number");
1141-
1142-
/*
1143-
* 17-Apr-2010, tatu: could also use 'floatToIntBits', but it seems more
1144-
* accurate to use exact representation; and possibly faster. However,
1145-
* if there are cases where collapsing of NaN was needed (for non-Java
1146-
* clients), this can be changed
1147-
*/
1148-
int i = Float.floatToRawIntBits(f);
1149-
_outputBuffer[_outputTail++] = BYTE_FLOAT32;
1150-
_outputBuffer[_outputTail++] = (byte) (i >> 24);
1151-
_outputBuffer[_outputTail++] = (byte) (i >> 16);
1152-
_outputBuffer[_outputTail++] = (byte) (i >> 8);
1153-
_outputBuffer[_outputTail++] = (byte) i;
1178+
_writeFloatNoCheck(f);
11541179
return this;
11551180
}
11561181

cbor/src/test/java/tools/jackson/dataformat/cbor/CBORTestBase.java

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -73,7 +73,11 @@ protected CBORParser cborParser(InputStream in) {
7373
}
7474

7575
protected CBORMapper cborMapper() {
76-
return new CBORMapper(cborFactory());
76+
return cborMapper(cborFactory());
77+
}
78+
79+
protected CBORMapper cborMapper(CBORFactory f) {
80+
return new CBORMapper(f);
7781
}
7882

7983
protected CBORMapper.Builder cborMapperBuilder() {

cbor/src/test/java/tools/jackson/dataformat/cbor/gen/ArrayGenerationTest.java

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
import tools.jackson.core.*;
66

77
import tools.jackson.core.JsonParser.NumberType;
8+
import tools.jackson.databind.ObjectMapper;
89
import tools.jackson.dataformat.cbor.CBORGenerator;
910
import tools.jackson.dataformat.cbor.CBORParser;
1011
import tools.jackson.dataformat.cbor.CBORTestBase;
@@ -155,6 +156,66 @@ public void testMinimalIntValuesForLong() throws Exception
155156
p.close();
156157
}
157158

159+
public void testMinimalFloatValuesForDouble() throws Exception
160+
{
161+
// Array with 2 values, one that can be represented as a float without losing precision and
162+
// one that cannot.
163+
final double[] input = new double[] {
164+
1.5, // can be exactly represented as a float
165+
0.123456789 // must be kept as double
166+
};
167+
ByteArrayOutputStream bytes = new ByteArrayOutputStream();
168+
final ObjectMapper mapperWithMinimalFloats = cborMapper(
169+
cborFactoryBuilder().enable(CBORGenerator.Feature.WRITE_MINIMAL_DOUBLES)
170+
.build());
171+
172+
CBORGenerator gen = (CBORGenerator) mapperWithMinimalFloats.createGenerator(bytes);
173+
assertTrue(gen.isEnabled(CBORGenerator.Feature.WRITE_MINIMAL_DOUBLES));
174+
gen.writeArray(input, 0, 2);
175+
gen.close();
176+
177+
// With minimal doubles enabled, should get:
178+
byte[] encoded = bytes.toByteArray();
179+
assertEquals(15, encoded.length);
180+
181+
// then verify contents
182+
183+
CBORParser p = (CBORParser) mapperWithMinimalFloats.createParser(encoded);
184+
assertToken(JsonToken.START_ARRAY, p.nextToken());
185+
assertToken(JsonToken.VALUE_NUMBER_FLOAT, p.nextToken());
186+
assertEquals(NumberType.FLOAT, p.getNumberType());
187+
assertEquals(input[0], p.getDoubleValue());
188+
assertToken(JsonToken.VALUE_NUMBER_FLOAT, p.nextToken());
189+
assertEquals(NumberType.DOUBLE, p.getNumberType());
190+
assertEquals(input[1], p.getDoubleValue());
191+
assertToken(JsonToken.END_ARRAY, p.nextToken());
192+
p.close();
193+
194+
// but then also check without minimization
195+
bytes = new ByteArrayOutputStream();
196+
gen = (CBORGenerator) MAPPER.createGenerator(bytes);
197+
198+
gen.writeArray(input, 0, 2);
199+
gen.close();
200+
201+
// With default settings, should get:
202+
encoded = bytes.toByteArray();
203+
assertEquals(19, encoded.length);
204+
205+
// then verify contents
206+
207+
p = (CBORParser) MAPPER.createParser(encoded);
208+
assertToken(JsonToken.START_ARRAY, p.nextToken());
209+
assertToken(JsonToken.VALUE_NUMBER_FLOAT, p.nextToken());
210+
assertEquals(NumberType.DOUBLE, p.getNumberType());
211+
assertEquals(input[0], p.getDoubleValue());
212+
assertToken(JsonToken.VALUE_NUMBER_FLOAT, p.nextToken());
213+
assertEquals(NumberType.DOUBLE, p.getNumberType());
214+
assertEquals(input[1], p.getDoubleValue());
215+
assertToken(JsonToken.END_ARRAY, p.nextToken());
216+
p.close();
217+
}
218+
158219
private void _testIntArray() throws Exception {
159220
// first special cases of 0, 1 values
160221
_testIntArray(0, 0, 0);

cbor/src/test/java/tools/jackson/dataformat/cbor/gen/GeneratorSimpleTest.java

Lines changed: 10 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,10 @@ public class GeneratorSimpleTest extends CBORTestBase
1919
{
2020
private final ObjectMapper MAPPER = cborMapper();
2121

22+
private final ObjectMapper MAPPER_NO_MINIMAL_INT = cborMapper(
23+
cborFactoryBuilder().disable(CBORGenerator.Feature.WRITE_MINIMAL_INTS)
24+
.build());
25+
2226
/**
2327
* Test for verifying handling of 'true', 'false' and 'null' literals
2428
*/
@@ -51,7 +55,8 @@ public void testSimpleLiterals() throws Exception
5155
public void testMinimalIntValues() throws Exception
5256
{
5357
ByteArrayOutputStream out = new ByteArrayOutputStream();
54-
CBORGenerator gen = cborGenerator(out);
58+
59+
CBORGenerator gen = (CBORGenerator) MAPPER.createGenerator(out);
5560
assertTrue(gen.isEnabled(CBORGenerator.Feature.WRITE_MINIMAL_INTS));
5661
gen.writeNumber(17);
5762
gen.close();
@@ -60,24 +65,24 @@ public void testMinimalIntValues() throws Exception
6065

6166
// then without minimal
6267
out = new ByteArrayOutputStream();
63-
gen = cborGenerator(out);
64-
gen.disable(CBORGenerator.Feature.WRITE_MINIMAL_INTS);
68+
gen = (CBORGenerator) MAPPER_NO_MINIMAL_INT.createGenerator(out);
6569
gen.writeNumber(17);
6670
gen.close();
6771
_verifyBytes(out.toByteArray(),
6872
(byte) (CBORConstants.PREFIX_TYPE_INT_POS + 26),
6973
(byte) 0, (byte) 0, (byte) 0, (byte) 17);
7074

75+
// And again with minimal
7176
out = new ByteArrayOutputStream();
72-
gen = cborGenerator(out);
77+
gen = (CBORGenerator) MAPPER.createGenerator(out);
7378
gen.writeNumber((long) Integer.MAX_VALUE);
7479
gen.close();
7580
_verifyBytes(out.toByteArray(),
7681
(byte) (CBORConstants.PREFIX_TYPE_INT_POS + 26),
7782
(byte) 0x7F, (byte) 0xFF, (byte) 0xFF, (byte) 0xFF);
7883

7984
out = new ByteArrayOutputStream();
80-
gen = cborGenerator(out);
85+
gen = (CBORGenerator) MAPPER.createGenerator(out);
8186
gen.writeNumber((long) Integer.MIN_VALUE);
8287
gen.close();
8388
_verifyBytes(out.toByteArray(),

cbor/src/test/java/tools/jackson/dataformat/cbor/mapper/CBORMapperTest.java

Lines changed: 21 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,8 @@
44
import tools.jackson.dataformat.cbor.CBORTestBase;
55
import tools.jackson.dataformat.cbor.databind.CBORMapper;
66

7+
import org.junit.Assert;
8+
79
public class CBORMapperTest extends CBORTestBase
810
{
911
/*
@@ -14,23 +16,31 @@ public class CBORMapperTest extends CBORTestBase
1416

1517
public void testStreamingFeaturesViaMapper() throws Exception
1618
{
17-
final Integer SMALL_INT = Integer.valueOf(3);
18-
CBORMapper mapperWithMinimalInts = CBORMapper.builder()
19+
final int SMALL_INT = 3;
20+
final int BIG_INT = 0x7FFFFFFF;
21+
final double LOW_RPECISION_DOUBLE = 1.5;
22+
final double HIGH_RPECISION_DOUBLE = 0.123456789;
23+
Object[] values = {SMALL_INT, BIG_INT, LOW_RPECISION_DOUBLE, HIGH_RPECISION_DOUBLE};
24+
Object[] minimalValues = {
25+
SMALL_INT, BIG_INT, (float)LOW_RPECISION_DOUBLE, HIGH_RPECISION_DOUBLE};
26+
CBORMapper mapperWithMinimal = CBORMapper.builder()
1927
.enable(CBORGenerator.Feature.WRITE_MINIMAL_INTS)
28+
.enable(CBORGenerator.Feature.WRITE_MINIMAL_DOUBLES)
2029
.build();
21-
byte[] encodedMinimal = mapperWithMinimalInts.writeValueAsBytes(SMALL_INT);
22-
assertEquals(1, encodedMinimal.length);
30+
byte[] encodedMinimal = mapperWithMinimal.writeValueAsBytes(values);
31+
assertEquals(21, encodedMinimal.length);
2332

24-
CBORMapper mapperFullInts = CBORMapper.builder()
33+
CBORMapper mapperFull = CBORMapper.builder()
2534
.disable(CBORGenerator.Feature.WRITE_MINIMAL_INTS)
35+
.disable(CBORGenerator.Feature.WRITE_MINIMAL_DOUBLES)
2636
.build();
27-
byte[] encodedNotMinimal = mapperFullInts.writeValueAsBytes(SMALL_INT);
28-
assertEquals(5, encodedNotMinimal.length);
37+
byte[] encodedNotMinimal = mapperFull.writeValueAsBytes(values);
38+
assertEquals(29, encodedNotMinimal.length);
2939

3040
// And then verify we can read it back, either way
31-
assertEquals(SMALL_INT, mapperWithMinimalInts.readValue(encodedMinimal, Object.class));
32-
assertEquals(SMALL_INT, mapperWithMinimalInts.readValue(encodedNotMinimal, Object.class));
33-
assertEquals(SMALL_INT, mapperFullInts.readValue(encodedMinimal, Object.class));
34-
assertEquals(SMALL_INT, mapperFullInts.readValue(encodedNotMinimal, Object.class));
41+
Assert.assertArrayEquals(minimalValues, mapperWithMinimal.readValue(encodedMinimal, Object[].class));
42+
Assert.assertArrayEquals(values, mapperWithMinimal.readValue(encodedNotMinimal, Object[].class));
43+
Assert.assertArrayEquals(minimalValues, mapperFull.readValue(encodedMinimal, Object[].class));
44+
Assert.assertArrayEquals(values, mapperFull.readValue(encodedNotMinimal, Object[].class));
3545
}
3646
}

release-notes/CREDITS-2.x

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -270,3 +270,6 @@ Aaron Barany (here-abarany@github)
270270

271271
* Contributed #347: (cbor) Add support for CBOR stringref extension (`CBORGenerator.Feature.STRINGREF`)
272272
(2.15.0)
273+
* Contributed #356: (cbor) Add `CBORGenerat.Feature.WRITE_MINIMAL_DOUBLES` for writing `double`s
274+
as `float`s if safe to do so
275+
(2.15.0)

0 commit comments

Comments
 (0)