From 6b52007baa61b603e60c8fa2d6b7f10d9252cc93 Mon Sep 17 00:00:00 2001 From: Tatu Saloranta Date: Fri, 12 Jun 2020 14:11:24 -0700 Subject: [PATCH] CoercionConfig changes: int-to-boolean --- .../deser/std/NumberDeserializers.java | 8 +- .../databind/deser/std/StdDeserializer.java | 38 ++++- .../convert/CoerceJDKScalarsTest.java | 75 ++------- .../databind/convert/CoerceToBooleanTest.java | 142 ++++++++++++++++++ 4 files changed, 191 insertions(+), 72 deletions(-) create mode 100644 src/test/java/com/fasterxml/jackson/databind/convert/CoerceToBooleanTest.java diff --git a/src/main/java/com/fasterxml/jackson/databind/deser/std/NumberDeserializers.java b/src/main/java/com/fasterxml/jackson/databind/deser/std/NumberDeserializers.java index 8f602ff45c..47a22e267d 100644 --- a/src/main/java/com/fasterxml/jackson/databind/deser/std/NumberDeserializers.java +++ b/src/main/java/com/fasterxml/jackson/databind/deser/std/NumberDeserializers.java @@ -251,7 +251,7 @@ protected final Boolean _parseBoolean(JsonParser p, DeserializationContext ctxt) } // should accept ints too, (0 == false, otherwise true) if (t == JsonToken.VALUE_NUMBER_INT) { - return Boolean.valueOf(_parseBooleanFromInt(p, ctxt)); + return _coerceBooleanFromInt(ctxt, p, Boolean.class); } // And finally, let's allow Strings to be converted too if (t == JsonToken.VALUE_STRING) { @@ -456,7 +456,11 @@ public Character deserialize(JsonParser p, DeserializationContext ctxt) { switch (p.currentTokenId()) { case JsonTokenId.ID_NUMBER_INT: // ok iff Unicode value - _verifyNumberForScalarCoercion(ctxt, p); + // 12-Jun-2020, tatu: inlined from `StdDeserializer` + if (!ctxt.isEnabled(MapperFeature.ALLOW_COERCION_OF_SCALARS)) { + ctxt.reportInputMismatch(this, +"Cannot coerce Integer value to `Character` (enable `MapperFeature.ALLOW_COERCION_OF_SCALARS` to allow)"); + } int value = p.getIntValue(); if (value >= 0 && value <= 0xFFFF) { return Character.valueOf((char) value); diff --git a/src/main/java/com/fasterxml/jackson/databind/deser/std/StdDeserializer.java b/src/main/java/com/fasterxml/jackson/databind/deser/std/StdDeserializer.java index 71cb5dc02e..d72d3523fc 100644 --- a/src/main/java/com/fasterxml/jackson/databind/deser/std/StdDeserializer.java +++ b/src/main/java/com/fasterxml/jackson/databind/deser/std/StdDeserializer.java @@ -372,7 +372,9 @@ protected final boolean _parseBooleanPrimitive(DeserializationContext ctxt, // should accept ints too, (0 == false, otherwise true) if (t == JsonToken.VALUE_NUMBER_INT) { - return _parseBooleanFromInt(p, ctxt); + Boolean b = _coerceBooleanFromInt(ctxt, p, Boolean.TYPE); + // may get `null`, Boolean.TRUE or Boolean.FALSE so: + return (b == Boolean.TRUE); } // And finally, let's allow Strings to be converted too if (t == JsonToken.VALUE_STRING) { @@ -413,6 +415,7 @@ protected final boolean _parseBooleanPrimitive(DeserializationContext ctxt, return ((Boolean) ctxt.handleUnexpectedToken(targetType, p)).booleanValue(); } + @Deprecated // since 2.12 protected boolean _parseBooleanFromInt(JsonParser p, DeserializationContext ctxt) throws IOException { @@ -864,7 +867,7 @@ protected final static boolean _isBlank(String text) * @since 2.12 */ protected CoercionAction _checkFromStringCoercion(DeserializationContext ctxt, String value) - throws JsonMappingException + throws IOException { return _checkFromStringCoercion(ctxt, value, logicalType(), handledType()); } @@ -874,7 +877,7 @@ protected CoercionAction _checkFromStringCoercion(DeserializationContext ctxt, S */ protected CoercionAction _checkFromStringCoercion(DeserializationContext ctxt, String value, LogicalType logicalType, Class rawTargetType) - throws JsonMappingException + throws IOException { final CoercionAction act; @@ -904,6 +907,33 @@ protected CoercionAction _checkFromStringCoercion(DeserializationContext ctxt, S return act; } + /** + * @since 2.12 + */ + protected Boolean _coerceBooleanFromInt(DeserializationContext ctxt, JsonParser p, + Class rawTargetType) + throws IOException + { + + final CoercionAction act = ctxt.findCoercionAction(LogicalType.Boolean, rawTargetType, CoercionInputShape.Integer); + switch (act) { + case Fail: + ctxt.reportInputMismatch(this, +"Cannot coerce Integer value (%s) to %s (but might if enabling coercion using `CoercionConfig`)", +p.getText(), _coercedTypeDesc()); + case AsNull: + return null; + case AsEmpty: + return Boolean.FALSE; + default: + } + // 13-Oct-2016, tatu: As per [databind#1324], need to be careful wrt + // degenerate case of huge integers, legal in JSON. + // Also note that number tokens can not have WS to trim: + boolean b = !"0".equals(p.getText()); + return b; + } + /* /**************************************************** /* Helper methods for sub-classes, coercions, older (pre-2.12) @@ -1055,7 +1085,7 @@ protected final void _verifyNullForScalarCoercion(DeserializationContext ctxt, S } } - // @since 2.9 + @Deprecated // since 2.12 protected void _verifyNumberForScalarCoercion(DeserializationContext ctxt, JsonParser p) throws IOException { MapperFeature feat = MapperFeature.ALLOW_COERCION_OF_SCALARS; diff --git a/src/test/java/com/fasterxml/jackson/databind/convert/CoerceJDKScalarsTest.java b/src/test/java/com/fasterxml/jackson/databind/convert/CoerceJDKScalarsTest.java index aa69454850..6e553e401d 100644 --- a/src/test/java/com/fasterxml/jackson/databind/convert/CoerceJDKScalarsTest.java +++ b/src/test/java/com/fasterxml/jackson/databind/convert/CoerceJDKScalarsTest.java @@ -14,10 +14,6 @@ // Tests for "old" coercions (pre-2.12), with `MapperFeature.ALLOW_COERCION_OF_SCALARS` public class CoerceJDKScalarsTest extends BaseMapTest { - static class BooleanPOJO { - public boolean value; - } - private final ObjectMapper COERCING_MAPPER = jsonMapperBuilder() .enable(MapperFeature.ALLOW_COERCION_OF_SCALARS) .build(); @@ -172,28 +168,16 @@ public void testStringCoercionFailFloat() throws Exception _verifyRootStringCoerceFail("123.0", BigDecimal.class); } - public void testToBooleanCoercionFailBytes() throws Exception - { - final String beanDoc = aposToQuotes("{'value':1}"); - _verifyBooleanCoerceFail("1", true, JsonToken.VALUE_NUMBER_INT, "1", Boolean.TYPE); - _verifyBooleanCoerceFail("1", true, JsonToken.VALUE_NUMBER_INT, "1", Boolean.class); - _verifyBooleanCoerceFail(beanDoc, true, JsonToken.VALUE_NUMBER_INT, "1", BooleanPOJO.class); - } - - public void testToBooleanCoercionFailChars() throws Exception - { - final String beanDoc = aposToQuotes("{'value':1}"); - _verifyBooleanCoerceFail("1", false, JsonToken.VALUE_NUMBER_INT, "1", Boolean.TYPE); - _verifyBooleanCoerceFail("1", false, JsonToken.VALUE_NUMBER_INT, "1", Boolean.class); - _verifyBooleanCoerceFail(beanDoc, false, JsonToken.VALUE_NUMBER_INT, "1", BooleanPOJO.class); - } - public void testMiscCoercionFail() throws Exception { // And then we have coercions from more esoteric types too - _verifyCoerceFail("65", Character.class); - _verifyCoerceFail("65", Character.TYPE); + _verifyCoerceFail("65", Character.class, + "Cannot coerce Integer value to `Character", + "enable `MapperFeature.ALLOW_COERCION_OF_SCALARS` to allow"); + _verifyCoerceFail("65", Character.TYPE, + "Cannot coerce Integer value to `Character", + "enable `MapperFeature.ALLOW_COERCION_OF_SCALARS` to allow"); } /* @@ -209,16 +193,15 @@ private void _verifyCoerceSuccess(String input, Class type, Object exp) throw assertEquals(exp, result); } - private void _verifyCoerceFail(String input, Class type) throws IOException + private void _verifyCoerceFail(String input, Class type, + String... expMatches) throws IOException { try { NOT_COERCING_MAPPER.readerFor(type) .readValue(input); fail("Should not have allowed coercion"); } catch (MismatchedInputException e) { - verifyException(e, "Cannot coerce "); - verifyException(e, " to `"); - verifyException(e, "enable `MapperFeature.ALLOW_COERCION_OF_SCALARS` to allow"); + verifyException(e, expMatches); } } @@ -256,44 +239,4 @@ private void _verifyStringCoerceFail(JsonParser p, } } - private void _verifyBooleanCoerceFail(String doc, boolean useBytes, - JsonToken tokenType, String tokenValue, Class targetType) throws IOException - { - // Test failure for root value: for both byte- and char-backed sources. - - // [databind#2635]: important, need to use `readValue()` that takes content and NOT - // JsonParser, as this forces closing of underlying parser and exposes more issues. - - final ObjectReader r = NOT_COERCING_MAPPER.readerFor(targetType); - try { - if (useBytes) { - r.readValue(utf8Bytes(doc)); - } else { - r.readValue(doc); - } - fail("Should not have allowed coercion"); - } catch (MismatchedInputException e) { - _verifyBooleanCoerceFailReason(e, tokenType, tokenValue); - } - } - - @SuppressWarnings("resource") - private void _verifyBooleanCoerceFailReason(MismatchedInputException e, - JsonToken tokenType, String tokenValue) throws IOException - { - verifyException(e, "Cannot coerce "); - verifyException(e, " to `"); - - JsonParser p = (JsonParser) e.getProcessor(); - - assertToken(tokenType, p.currentToken()); - - final String text = p.getText(); - - if (!tokenValue.equals(text)) { - String textDesc = (text == null) ? "NULL" : quote(text); - fail("Token text ("+textDesc+") via parser of type "+p.getClass().getName() - +" not as expected ("+quote(tokenValue)+")"); - } - } } diff --git a/src/test/java/com/fasterxml/jackson/databind/convert/CoerceToBooleanTest.java b/src/test/java/com/fasterxml/jackson/databind/convert/CoerceToBooleanTest.java new file mode 100644 index 0000000000..ab4c73c16a --- /dev/null +++ b/src/test/java/com/fasterxml/jackson/databind/convert/CoerceToBooleanTest.java @@ -0,0 +1,142 @@ +package com.fasterxml.jackson.databind.convert; + +import java.io.IOException; +import java.util.concurrent.atomic.AtomicBoolean; + +import com.fasterxml.jackson.core.JsonParser; +import com.fasterxml.jackson.core.JsonToken; + +import com.fasterxml.jackson.databind.BaseMapTest; +import com.fasterxml.jackson.databind.MapperFeature; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.ObjectReader; +import com.fasterxml.jackson.databind.exc.MismatchedInputException; + +public class CoerceToBooleanTest extends BaseMapTest +{ + static class BooleanPOJO { + public boolean value; + } + + private final ObjectMapper DEFAULT_MAPPER = sharedMapper(); + + private final ObjectMapper LEGACY_NONCOERCING_MAPPER = jsonMapperBuilder() + .disable(MapperFeature.ALLOW_COERCION_OF_SCALARS) + .build(); + + private final static String DOC_WITH_0 = aposToQuotes("{'value':0}"); + private final static String DOC_WITH_1 = aposToQuotes("{'value':1}"); + + /* + /********************************************************** + /* Unit tests: default, legacy configuration + /********************************************************** + */ + + public void testToBooleanCoercionSuccessPojo() throws Exception + { + BooleanPOJO p; + final ObjectReader r = DEFAULT_MAPPER.readerFor(BooleanPOJO.class); + + p = r.readValue(DOC_WITH_0); + assertEquals(false, p.value); + p = r.readValue(utf8Bytes(DOC_WITH_0)); + assertEquals(false, p.value); + + p = r.readValue(DOC_WITH_1); + assertEquals(true, p.value); + p = r.readValue(utf8Bytes(DOC_WITH_1)); + assertEquals(true, p.value); + } + + public void testToBooleanCoercionSuccessRoot() throws Exception + { + final ObjectReader br = DEFAULT_MAPPER.readerFor(Boolean.class); + + assertEquals(Boolean.FALSE, br.readValue(" 0")); + assertEquals(Boolean.FALSE, br.readValue(utf8Bytes(" 0"))); + assertEquals(Boolean.TRUE, br.readValue(" -1")); + assertEquals(Boolean.TRUE, br.readValue(utf8Bytes(" -1"))); + + final ObjectReader atomicR = DEFAULT_MAPPER.readerFor(AtomicBoolean.class); + + AtomicBoolean ab; + + ab = atomicR.readValue(" 0"); + ab = atomicR.readValue(utf8Bytes(" 0")); + assertEquals(false, ab.get()); + + ab = atomicR.readValue(" 111"); + assertEquals(true, ab.get()); + ab = atomicR.readValue(utf8Bytes(" 111")); + assertEquals(true, ab.get()); + } + + public void testToBooleanCoercionFailBytes() throws Exception + { + _verifyBooleanCoerceFail(aposToQuotes("{'value':1}"), true, JsonToken.VALUE_NUMBER_INT, "1", BooleanPOJO.class); + + _verifyBooleanCoerceFail("1", true, JsonToken.VALUE_NUMBER_INT, "1", Boolean.TYPE); + _verifyBooleanCoerceFail("1", true, JsonToken.VALUE_NUMBER_INT, "1", Boolean.class); + } + + public void testToBooleanCoercionFailChars() throws Exception + { + _verifyBooleanCoerceFail(aposToQuotes("{'value':1}"), false, JsonToken.VALUE_NUMBER_INT, "1", BooleanPOJO.class); + + _verifyBooleanCoerceFail("1", false, JsonToken.VALUE_NUMBER_INT, "1", Boolean.TYPE); + _verifyBooleanCoerceFail("1", false, JsonToken.VALUE_NUMBER_INT, "1", Boolean.class); + } + + /* + /********************************************************** + /* Unit tests: new CoercionConfig + /********************************************************** + */ + + /* + /********************************************************** + /* Helper methods + /********************************************************** + */ + + private void _verifyBooleanCoerceFail(String doc, boolean useBytes, + JsonToken tokenType, String tokenValue, Class targetType) throws IOException + { + // Test failure for root value: for both byte- and char-backed sources. + + // [databind#2635]: important, need to use `readValue()` that takes content and NOT + // JsonParser, as this forces closing of underlying parser and exposes more issues. + + final ObjectReader r = LEGACY_NONCOERCING_MAPPER.readerFor(targetType); + try { + if (useBytes) { + r.readValue(utf8Bytes(doc)); + } else { + r.readValue(doc); + } + fail("Should not have allowed coercion"); + } catch (MismatchedInputException e) { + _verifyBooleanCoerceFailReason(e, tokenType, tokenValue); + } + } + + @SuppressWarnings("resource") + private void _verifyBooleanCoerceFailReason(MismatchedInputException e, + JsonToken tokenType, String tokenValue) throws IOException + { + verifyException(e, "Cannot coerce "); + verifyException(e, " to `"); + + JsonParser p = (JsonParser) e.getProcessor(); + + assertToken(tokenType, p.currentToken()); + + final String text = p.getText(); + if (!tokenValue.equals(text)) { + String textDesc = (text == null) ? "NULL" : quote(text); + fail("Token text ("+textDesc+") via parser of type "+p.getClass().getName() + +" not as expected ("+quote(tokenValue)+")"); + } + } +}