diff --git a/src/main/java/com/fasterxml/jackson/databind/ObjectMapper.java b/src/main/java/com/fasterxml/jackson/databind/ObjectMapper.java index 102607f0d3..10cda9efc6 100644 --- a/src/main/java/com/fasterxml/jackson/databind/ObjectMapper.java +++ b/src/main/java/com/fasterxml/jackson/databind/ObjectMapper.java @@ -1896,6 +1896,24 @@ public ObjectMapper setDefaultPropertyInclusion(JsonInclude.Value incl) { return this; } + /** + * Method for setting default alternative radix that applies to integral types for serialization + * and deserialization of such types as strings. + * This configuration override is applied for all integral properties for which there are no per-type + * or per-property overrides (via annotations or config overrides). + *
+ * NOTE: in Jackson 3.x all configuration goes through {@code ObjectMapper} builders, + * see {@link com.fasterxml.jackson.databind.cfg.MapperBuilder}, + * and this method will be removed from 3.0. + * + * @since 2.21 + */ + public ObjectMapper setDefaultFormat(String radix) { + _configOverrides.setDefaultRadix(radix); + return this; + } + + /** * Short-cut for: *
diff --git a/src/main/java/com/fasterxml/jackson/databind/cfg/BaseSettings.java b/src/main/java/com/fasterxml/jackson/databind/cfg/BaseSettings.java index 76f2d9fe1a..e2e66cb524 100644 --- a/src/main/java/com/fasterxml/jackson/databind/cfg/BaseSettings.java +++ b/src/main/java/com/fasterxml/jackson/databind/cfg/BaseSettings.java @@ -184,7 +184,7 @@ public BaseSettings(ClassIntrospector ci, AnnotationIntrospector ai, _defaultBase64 = defaultBase64; _typeValidator = ptv; _accessorNaming = accNaming; - _cacheProvider = cacheProvider; + _cacheProvider = cacheProvider;; } /** diff --git a/src/main/java/com/fasterxml/jackson/databind/cfg/ConfigOverrides.java b/src/main/java/com/fasterxml/jackson/databind/cfg/ConfigOverrides.java index 4961de2386..4fed2bc044 100644 --- a/src/main/java/com/fasterxml/jackson/databind/cfg/ConfigOverrides.java +++ b/src/main/java/com/fasterxml/jackson/databind/cfg/ConfigOverrides.java @@ -17,6 +17,7 @@ public class ConfigOverrides implements java.io.Serializable { private static final long serialVersionUID = 1L; + private static final String DEFAULT_RADIX = "10"; /** * Per-type override definitions @@ -55,6 +56,14 @@ public class ConfigOverrides */ protected Boolean _defaultLeniency; + /** + * Global default radix to apply to an integral type outputted as string. This has the lowest precedence out of all + * other methods of enforcing an alternative radix. + * + * @since 2.21 + */ + protected String _defaultRadix;//TODO(Davyd Fridman): Change from string to int once JsonFormat has an actual radix field + /* /********************************************************************** /* Life cycle @@ -67,13 +76,32 @@ public ConfigOverrides() { JsonInclude.Value.empty(), JsonSetter.Value.empty(), VisibilityChecker.Std.defaultInstance(), - null, null + null, null, DEFAULT_RADIX ); } + /** + * @since 2.21 + */ + protected ConfigOverrides(Map, MutableConfigOverride> overrides, + JsonInclude.Value defIncl, JsonSetter.Value defSetter, + VisibilityChecker> defVisibility, Boolean defMergeable, Boolean defLeniency, + String defRadix) + { + _overrides = overrides; + _defaultInclusion = defIncl; + _defaultSetterInfo = defSetter; + _visibilityChecker = defVisibility; + _defaultMergeable = defMergeable; + _defaultLeniency = defLeniency; + _defaultRadix = defRadix; + } + /** * @since 2.10 + * @deprecated since 2.21 */ + @Deprecated protected ConfigOverrides(Map , MutableConfigOverride> overrides, JsonInclude.Value defIncl, JsonSetter.Value defSetter, VisibilityChecker> defVisibility, Boolean defMergeable, Boolean defLeniency) @@ -197,6 +225,13 @@ public VisibilityChecker> getDefaultVisibility() { return _visibilityChecker; } + /** + * @since 2.21 + */ + public String getDefaultRadix() { + return _defaultRadix; + } + /** * @since 2.9 */ @@ -232,6 +267,13 @@ public void setDefaultVisibility(VisibilityChecker> v) { _visibilityChecker = v; } + /** + * @since 2.21 + */ + public void setDefaultRadix(String v) { + this._defaultRadix = v; + } + /* /********************************************************************** /* Helper methods diff --git a/src/main/java/com/fasterxml/jackson/databind/cfg/MapperBuilder.java b/src/main/java/com/fasterxml/jackson/databind/cfg/MapperBuilder.java index cbfe4277d2..cc9f7fa613 100644 --- a/src/main/java/com/fasterxml/jackson/databind/cfg/MapperBuilder.java +++ b/src/main/java/com/fasterxml/jackson/databind/cfg/MapperBuilder.java @@ -7,6 +7,7 @@ import java.util.function.Consumer; import com.fasterxml.jackson.annotation.JsonAutoDetect; +import com.fasterxml.jackson.annotation.JsonFormat; import com.fasterxml.jackson.annotation.JsonInclude; import com.fasterxml.jackson.annotation.JsonSetter; import com.fasterxml.jackson.annotation.JsonTypeInfo; @@ -747,6 +748,20 @@ public B defaultPropertyInclusion(JsonInclude.Value incl) { return _this(); } + /** + * Method for configured default radix to use for serialization/deserialization of integral types as strings. + * + * @param radix Default radix to use on integral properties + * + * @return This builder instance to allow call chaining + * + * @since 2.11 + */ + public B defaultFormat(String radix) { + _mapper.setDefaultFormat(radix); + return _this(); + } + /* /********************************************************************** /* Configuring Mix-ins diff --git a/src/main/java/com/fasterxml/jackson/databind/cfg/MapperConfig.java b/src/main/java/com/fasterxml/jackson/databind/cfg/MapperConfig.java index 061224e1e5..97a861601c 100644 --- a/src/main/java/com/fasterxml/jackson/databind/cfg/MapperConfig.java +++ b/src/main/java/com/fasterxml/jackson/databind/cfg/MapperConfig.java @@ -507,6 +507,14 @@ public JsonInclude.Value getDefaultInclusion(Class> baseType, return result; } + /** + * Accessor for default radix to apply to integral types when serializing them as string. + * The radix obtained from this accessor should have the lowest precedence. + * + * @since 2.21 + */ + public abstract String getDefaultRadix(); + /** * Accessor for default format settings to use for serialization (and, to a degree * deserialization), considering baseline settings and per-type defaults diff --git a/src/main/java/com/fasterxml/jackson/databind/cfg/MapperConfigBase.java b/src/main/java/com/fasterxml/jackson/databind/cfg/MapperConfigBase.java index 139253571a..b0fcdb3456 100644 --- a/src/main/java/com/fasterxml/jackson/databind/cfg/MapperConfigBase.java +++ b/src/main/java/com/fasterxml/jackson/databind/cfg/MapperConfigBase.java @@ -777,6 +777,11 @@ public final JsonInclude.Value getDefaultInclusion(Class> baseType, return def.withOverrides(v); } + @Override + public String getDefaultRadix() { + return _configOverrides.getDefaultRadix(); + } + @Override public final JsonFormat.Value getDefaultPropertyFormat(Class> type) { return _configOverrides.findFormatDefaults(type); diff --git a/src/main/java/com/fasterxml/jackson/databind/deser/std/FromStringWithRadixToNumberDeserializer.java b/src/main/java/com/fasterxml/jackson/databind/deser/std/FromStringWithRadixToNumberDeserializer.java new file mode 100644 index 0000000000..bafcb69f12 --- /dev/null +++ b/src/main/java/com/fasterxml/jackson/databind/deser/std/FromStringWithRadixToNumberDeserializer.java @@ -0,0 +1,52 @@ +package com.fasterxml.jackson.databind.deser.std; + +import com.fasterxml.jackson.core.JsonParser; +import com.fasterxml.jackson.core.JsonToken; +import com.fasterxml.jackson.databind.DeserializationContext; + +import java.io.IOException; +import java.math.BigInteger; + +/** + * Deserializer used for a string that represents a number in specific radix (base). + * + * @since 2.21 + */ +public class FromStringWithRadixToNumberDeserializer + extends StdDeserializer { + private final int radix; + + public FromStringWithRadixToNumberDeserializer(StdDeserializer> src, int radix) { + super(src); + this.radix = radix; + } + + @Override + public Number deserialize(JsonParser p, DeserializationContext ctxt) + throws IOException { + Class> handledType = handledType(); + + if (p.currentToken() != JsonToken.VALUE_STRING) { + ctxt.reportInputMismatch(handledType, + "Read something other than string when deserializing a value using FromStringWithRadixToNumberDeserializer."); + } + + String text = p.getText(); + + if (handledType.equals(BigInteger.class)) { + return new BigInteger(text, radix); + } else if (handledType.equals(byte.class) || handledType.equals(Byte.class)) { + return Byte.parseByte(text, radix); + } else if (handledType.equals(short.class) || handledType.equals(Short.class)) { + return Short.parseShort(text, radix); + } else if (handledType.equals(int.class) || handledType.equals(Integer.class)) { + return Integer.parseInt(text, radix); + } else if (handledType.equals(long.class) || handledType.equals(Long.class)) { + return Long.parseLong(text, radix); + } else { + ctxt.reportInputMismatch(handledType, + "Trying to deserialize a non-whole number with NumberToStringWithRadixSerializer"); + return null;//should not reach here + } + } +} 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 029e36e1ef..4ba35ea868 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 @@ -5,13 +5,16 @@ import java.math.BigInteger; import java.util.HashSet; +import com.fasterxml.jackson.annotation.JsonFormat; import com.fasterxml.jackson.core.*; import com.fasterxml.jackson.core.io.NumberInput; import com.fasterxml.jackson.databind.*; import com.fasterxml.jackson.databind.annotation.JacksonStdImpl; import com.fasterxml.jackson.databind.cfg.CoercionAction; import com.fasterxml.jackson.databind.cfg.CoercionInputShape; +import com.fasterxml.jackson.databind.deser.ContextualDeserializer; import com.fasterxml.jackson.databind.jsontype.TypeDeserializer; +import com.fasterxml.jackson.databind.ser.std.NumberToStringWithRadixSerializer; import com.fasterxml.jackson.databind.type.LogicalType; import com.fasterxml.jackson.databind.util.AccessPattern; import com.fasterxml.jackson.databind.util.ClassUtil; @@ -26,6 +29,8 @@ public class NumberDeserializers { private final static HashSet _classNames = new HashSet (); + private final static int DEFAULT_RADIX = 10; + static { // note: can skip primitive types; other ways to check them: Class>[] numberTypes = new Class>[] { @@ -250,7 +255,7 @@ public Boolean deserializeWithType(JsonParser p, DeserializationContext ctxt, @JacksonStdImpl public static class ByteDeserializer - extends PrimitiveOrWrapperDeserializer + extends PrimitiveOrWrapperDeserializer implements ContextualDeserializer { private static final long serialVersionUID = 1L; @@ -274,6 +279,12 @@ public Byte deserialize(JsonParser p, DeserializationContext ctxt) throws IOExce return _parseByte(p, ctxt); } + @Override + public JsonDeserializer> createContextual(DeserializationContext ctxt, BeanProperty property) + throws JsonMappingException { + return _createRadixStringDeserializer(this, ctxt, property); + } + protected Byte _parseByte(JsonParser p, DeserializationContext ctxt) throws IOException { @@ -342,7 +353,7 @@ protected Byte _parseByte(JsonParser p, DeserializationContext ctxt) @JacksonStdImpl public static class ShortDeserializer - extends PrimitiveOrWrapperDeserializer + extends PrimitiveOrWrapperDeserializer implements ContextualDeserializer { private static final long serialVersionUID = 1L; @@ -367,6 +378,12 @@ public Short deserialize(JsonParser p, DeserializationContext ctxt) return _parseShort(p, ctxt); } + @Override + public JsonDeserializer> createContextual(DeserializationContext ctxt, BeanProperty property) + throws JsonMappingException { + return _createRadixStringDeserializer(this, ctxt, property); + } + protected Short _parseShort(JsonParser p, DeserializationContext ctxt) throws IOException { @@ -517,8 +534,7 @@ public Character deserialize(JsonParser p, DeserializationContext ctxt) @JacksonStdImpl public final static class IntegerDeserializer - extends PrimitiveOrWrapperDeserializer - { + extends PrimitiveOrWrapperDeserializer implements ContextualDeserializer { private static final long serialVersionUID = 1L; final static IntegerDeserializer primitiveInstance = new IntegerDeserializer(Integer.TYPE, 0); @@ -557,11 +573,17 @@ public Integer deserializeWithType(JsonParser p, DeserializationContext ctxt, } return _parseInteger(p, ctxt, Integer.class); } + + @Override + public JsonDeserializer> createContextual(DeserializationContext ctxt, BeanProperty property) + throws JsonMappingException { + return _createRadixStringDeserializer(this, ctxt, property); + } } @JacksonStdImpl public final static class LongDeserializer - extends PrimitiveOrWrapperDeserializer + extends PrimitiveOrWrapperDeserializer implements ContextualDeserializer { private static final long serialVersionUID = 1L; @@ -586,6 +608,12 @@ public Long deserialize(JsonParser p, DeserializationContext ctxt) throws IOExce } return _parseLong(p, ctxt, Long.class); } + + @Override + public JsonDeserializer> createContextual(DeserializationContext ctxt, BeanProperty property) + throws JsonMappingException { + return _createRadixStringDeserializer(this, ctxt, property); + } } @JacksonStdImpl @@ -936,7 +964,7 @@ public Object deserializeWithType(JsonParser p, DeserializationContext ctxt, @SuppressWarnings("serial") @JacksonStdImpl public static class BigIntegerDeserializer - extends StdScalarDeserializer + extends StdScalarDeserializer implements ContextualDeserializer { public final static BigIntegerDeserializer instance = new BigIntegerDeserializer(); @@ -1011,6 +1039,12 @@ public BigInteger deserialize(JsonParser p, DeserializationContext ctxt) throws return (BigInteger) ctxt.handleWeirdStringValue(_valueClass, text, "not a valid representation"); } + + @Override + public JsonDeserializer> createContextual(DeserializationContext ctxt, BeanProperty property) + throws JsonMappingException { + return _createRadixStringDeserializer(this, ctxt, property); + } } @SuppressWarnings("serial") @@ -1089,4 +1123,42 @@ public BigDecimal deserialize(JsonParser p, DeserializationContext ctxt) "not a valid representation"); } } + + /** + * Method used to create a string deserializer for a number. + * If configuration is set properly, we create an alternative radix serializer {@link NumberToStringWithRadixSerializer}. + * + * @since 2.21 + */ + private static StdDeserializer extends Number> _createRadixStringDeserializer(StdScalarDeserializer extends Number> initialDeser, + DeserializationContext ctxt, BeanProperty property) + { + JsonFormat.Value format = initialDeser.findFormatOverrides(ctxt, property, initialDeser.handledType()); + + if (format == null || format.getShape() != JsonFormat.Shape.STRING) { + return initialDeser; + } + + if (isSerializeWithRadixOverride(format)) { + int radix = Integer.parseInt(format.getPattern()); + return new FromStringWithRadixToNumberDeserializer(initialDeser, radix); + } + + return initialDeser; + } + + /** + * Check if we have a proper {@link JsonFormat} annotation for serializing a number + * using an alternative radix specified in the annotation. + */ + private static boolean isSerializeWithRadixOverride(JsonFormat.Value format) { + String pattern = format.getPattern(); + boolean isInteger = pattern.chars().allMatch(Character::isDigit); + if (!isInteger || pattern.isEmpty()) { + return false; + } + + int radix = Integer.parseInt(pattern); + return radix != DEFAULT_RADIX; + } } diff --git a/src/main/java/com/fasterxml/jackson/databind/introspect/ConcreteBeanPropertyBase.java b/src/main/java/com/fasterxml/jackson/databind/introspect/ConcreteBeanPropertyBase.java index 315bb76906..fbe786be56 100644 --- a/src/main/java/com/fasterxml/jackson/databind/introspect/ConcreteBeanPropertyBase.java +++ b/src/main/java/com/fasterxml/jackson/databind/introspect/ConcreteBeanPropertyBase.java @@ -71,6 +71,7 @@ public final JsonFormat.Value findFormatOverrides(AnnotationIntrospector intr) { @Override public JsonFormat.Value findPropertyFormat(MapperConfig> config, Class> baseType) { + JsonFormat.Value v0 = EMPTY_FORMAT.withPattern(config.getDefaultRadix());//TODO(Davyd Fridman): change to withRadix JsonFormat.Value v1 = config.getDefaultPropertyFormat(baseType); JsonFormat.Value v2 = null; AnnotationIntrospector intr = config.getAnnotationIntrospector(); @@ -80,10 +81,18 @@ public JsonFormat.Value findPropertyFormat(MapperConfig> config, Class> base v2 = intr.findFormat(member); } } - if (v1 == null) { - return (v2 == null) ? EMPTY_FORMAT : v2; + + JsonFormat.Value formatValue = EMPTY_FORMAT; + if (v0 != null) { + formatValue = formatValue.withOverrides(v0); + } + if (v1 != null) { + formatValue = formatValue.withOverrides(v1); + } + if (v2 != null) { + formatValue = formatValue.withOverrides(v2); } - return (v2 == null) ? v1 : v1.withOverrides(v2); + return formatValue; } @Override diff --git a/src/main/java/com/fasterxml/jackson/databind/ser/std/NumberSerializer.java b/src/main/java/com/fasterxml/jackson/databind/ser/std/NumberSerializer.java index 56c2391dae..6e970f88e5 100644 --- a/src/main/java/com/fasterxml/jackson/databind/ser/std/NumberSerializer.java +++ b/src/main/java/com/fasterxml/jackson/databind/ser/std/NumberSerializer.java @@ -37,6 +37,8 @@ public class NumberSerializer protected final boolean _isInt; + protected static final int DEFAULT_RADIX = 10; + /** * @since 2.5 */ @@ -58,7 +60,8 @@ public JsonSerializer> createContextual(SerializerProvider prov, if (((Class>) handledType()) == BigDecimal.class) { return bigDecimalAsStringSerializer(); } - return ToStringSerializer.instance; + return createStringSerializer(prov, format, _isInt); + default: } } @@ -114,6 +117,35 @@ public void acceptJsonFormatVisitor(JsonFormatVisitorWrapper visitor, JavaType t } } + /** + * Method used to create a string serializer for a number. If the number is integer, and configuration is set properly, + * we create an alternative radix serializer {@link NumberToStringWithRadixSerializer}. + * + * @since 2.21 + */ + public static ToStringSerializerBase createStringSerializer(SerializerProvider prov, JsonFormat.Value format, boolean isInt) { + if (isInt && isSerializeWithRadixOverride(format)) { + int radix = Integer.parseInt(format.getPattern()); + return new NumberToStringWithRadixSerializer(radix); + } + return ToStringSerializer.instance; + } + + /** + * Check if we have a proper {@link JsonFormat} annotation for serializing a number + * using an alternative radix specified in the annotation. + */ + private static boolean isSerializeWithRadixOverride(JsonFormat.Value format) { + String pattern = format.getPattern(); + boolean isInteger = pattern.chars().allMatch(Character::isDigit); + if (!isInteger || pattern.isEmpty()) { + return false; + } + + int radix = Integer.parseInt(pattern); + return radix != DEFAULT_RADIX; + } + /** * @since 2.10 */ diff --git a/src/main/java/com/fasterxml/jackson/databind/ser/std/NumberSerializers.java b/src/main/java/com/fasterxml/jackson/databind/ser/std/NumberSerializers.java index be7e956a2f..f5a36006a9 100644 --- a/src/main/java/com/fasterxml/jackson/databind/ser/std/NumberSerializers.java +++ b/src/main/java/com/fasterxml/jackson/databind/ser/std/NumberSerializers.java @@ -106,7 +106,7 @@ public JsonSerializer> createContextual(SerializerProvider prov, if (((Class>) handledType()) == BigDecimal.class) { return NumberSerializer.bigDecimalAsStringSerializer(); } - return ToStringSerializer.instance; + return NumberSerializer.createStringSerializer(prov, format, _isInt); default: } } diff --git a/src/main/java/com/fasterxml/jackson/databind/ser/std/NumberToStringWithRadixSerializer.java b/src/main/java/com/fasterxml/jackson/databind/ser/std/NumberToStringWithRadixSerializer.java new file mode 100644 index 0000000000..edef76bc23 --- /dev/null +++ b/src/main/java/com/fasterxml/jackson/databind/ser/std/NumberToStringWithRadixSerializer.java @@ -0,0 +1,67 @@ +package com.fasterxml.jackson.databind.ser.std; + +import com.fasterxml.jackson.core.JsonGenerator; +import com.fasterxml.jackson.databind.SerializerProvider; +import com.fasterxml.jackson.databind.annotation.JacksonStdImpl; + +import java.io.IOException; +import java.math.BigInteger; + +/** + * Serializer used to convert numbers into a representation for a specified radix (base) and serialize + * the representation as string. + * + * @since 2.21 + */ +@JacksonStdImpl +public class NumberToStringWithRadixSerializer extends ToStringSerializerBase { + private final int radix; + + public NumberToStringWithRadixSerializer(int radix) { super(Object.class); + this.radix = radix; + } + + public NumberToStringWithRadixSerializer(Class> handledType, int radix) { + super(handledType); + this.radix = radix; + } + + @Override + public boolean isEmpty(SerializerProvider prov, Object value) { + return false; + } + + @Override + public void serialize(Object value, JsonGenerator gen, SerializerProvider provider) + throws IOException + { + if (radix < Character.MIN_RADIX || radix > Character.MAX_RADIX) { + String errorMsg = String.format("To use a custom radix for string serialization, use radix within [%d, %d]", Character.MIN_RADIX, Character.MAX_RADIX); + provider.reportBadDefinition(handledType(), errorMsg); + } + + String text = ""; + if (value instanceof BigInteger) { + BigInteger bigIntegerValue = (BigInteger) value; + text = bigIntegerValue.toString(radix); + } else if (value instanceof Byte + || value instanceof Short + || value instanceof Integer + || value instanceof Long) { + long longValue = ((Number) value).longValue(); + text = Long.toString(longValue, radix); + } else { + provider.reportBadDefinition(handledType(), + "Trying to serialize a non-whole number with NumberToStringWithRadixSerializer"); + } + + gen.writeString(text); + + } + + @Override + public String valueToString(Object value) { + // should never be called + throw new IllegalStateException(); + } +} diff --git a/src/test/java/com/fasterxml/jackson/databind/SerializeUsingJDKTest.java b/src/test/java/com/fasterxml/jackson/databind/SerializeUsingJDKTest.java index d607fc4f76..f8943f39c7 100644 --- a/src/test/java/com/fasterxml/jackson/databind/SerializeUsingJDKTest.java +++ b/src/test/java/com/fasterxml/jackson/databind/SerializeUsingJDKTest.java @@ -238,6 +238,8 @@ public void testTypeFactory() throws Exception public void testObjectReaderSerializationWithPolymorphism() throws Exception { + Properties props = System.getProperties(); + props.setProperty("sun.io.serialization.extendedDebugInfo", "true"); Class>[] classes = new Class>[] { FooClass.class, FooDeduction.class, diff --git a/src/test/java/com/fasterxml/jackson/databind/format/DifferentRadixNumberFormatTest.java b/src/test/java/com/fasterxml/jackson/databind/format/DifferentRadixNumberFormatTest.java new file mode 100644 index 0000000000..4ab8783d05 --- /dev/null +++ b/src/test/java/com/fasterxml/jackson/databind/format/DifferentRadixNumberFormatTest.java @@ -0,0 +1,216 @@ +package com.fasterxml.jackson.databind.format; + +import com.fasterxml.jackson.annotation.JsonFormat; +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.testutil.DatabindTestUtil; +import org.junit.jupiter.api.Test; + +import java.math.BigInteger; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; + +public class DifferentRadixNumberFormatTest extends DatabindTestUtil { + + private static final String HEX_RADIX = "16"; + public static final String BINARY_RADIX = "2"; + + private static class IntegerWrapper { + public Integer value; + + public IntegerWrapper() {} + public IntegerWrapper(Integer v) { value = v; } + } + + private static class IntWrapper { + public int value; + + public IntWrapper() {} + public IntWrapper(int v) { value = v; } + } + + private static class AnnotatedMethodIntWrapper { + private int value; + + public AnnotatedMethodIntWrapper() { + } + public AnnotatedMethodIntWrapper(int v) { + value = v; + } + + @JsonFormat(shape = JsonFormat.Shape.STRING, pattern = HEX_RADIX) + public int getValue() { + return value; + } + } + + private static class IncorrectlyAnnotatedMethodIntWrapper { + private int value; + + public IncorrectlyAnnotatedMethodIntWrapper() { + } + public IncorrectlyAnnotatedMethodIntWrapper(int v) { + value = v; + } + + @JsonFormat(shape = JsonFormat.Shape.STRING) + public int getValue() { + return value; + } + } + + private static class AllIntegralTypeWrapper { + @JsonFormat(shape = JsonFormat.Shape.STRING, pattern = BINARY_RADIX) + public byte byteValue; + @JsonFormat(shape = JsonFormat.Shape.STRING, pattern = BINARY_RADIX) + public Byte ByteValue; + + @JsonFormat(shape = JsonFormat.Shape.STRING, pattern = BINARY_RADIX) + public short shortValue; + @JsonFormat(shape = JsonFormat.Shape.STRING, pattern = BINARY_RADIX) + public Short ShortValue; + + @JsonFormat(shape = JsonFormat.Shape.STRING, pattern = BINARY_RADIX) + public int intValue; + @JsonFormat(shape = JsonFormat.Shape.STRING, pattern = BINARY_RADIX) + public Integer IntegerValue; + + @JsonFormat(shape = JsonFormat.Shape.STRING, pattern = BINARY_RADIX) + public long longValue; + @JsonFormat(shape = JsonFormat.Shape.STRING, pattern = BINARY_RADIX) + public Long LongValue; + + @JsonFormat(shape = JsonFormat.Shape.STRING, pattern = BINARY_RADIX) + public BigInteger bigInteger; + + public AllIntegralTypeWrapper() { + } + + public AllIntegralTypeWrapper(byte byteValue, Byte ByteValue, short shortValue, Short ShortValue, int intValue, + Integer IntegerValue, long longValue, Long LongValue, BigInteger bigInteger) { + this.byteValue = byteValue; + this.ByteValue = ByteValue; + this.shortValue = shortValue; + this.ShortValue = ShortValue; + this.intValue = intValue; + this.IntegerValue = IntegerValue; + this.longValue = longValue; + this.LongValue = LongValue; + this.bigInteger = bigInteger; + } + } + + @Test + void testIntegerSerializedAsHexString() + throws JsonProcessingException { + ObjectMapper mapper = newJsonMapper(); + mapper.configOverride(Integer.class).setFormat(JsonFormat.Value.forShape(JsonFormat.Shape.STRING).withPattern(HEX_RADIX)); + IntegerWrapper initialIntegerWrapper = new IntegerWrapper(10); + String json = mapper.writeValueAsString(initialIntegerWrapper); + String expectedJson = "{'value':'a'}"; + + assertEquals(a2q(expectedJson), json); + + IntegerWrapper readBackIntegerWrapper = mapper.readValue(a2q(expectedJson), IntegerWrapper.class); + + assertNotNull(readBackIntegerWrapper); + assertEquals(initialIntegerWrapper.value, readBackIntegerWrapper.value); + } + + + @Test + void testIntSerializedAsHexString() + throws JsonProcessingException { + ObjectMapper mapper = newJsonMapper(); + mapper.configOverride(int.class) + .setFormat(JsonFormat.Value.forShape(JsonFormat.Shape.STRING).withPattern(HEX_RADIX)); + IntWrapper intialIntWrapper = new IntWrapper(10); + String expectedJson = "{'value':'a'}"; + + String json = mapper.writeValueAsString(intialIntWrapper); + + assertEquals(a2q(expectedJson), json); + + IntWrapper readBackIntWrapper = mapper.readValue(a2q(expectedJson), IntWrapper.class); + + assertNotNull(readBackIntWrapper); + assertEquals(intialIntWrapper.value, readBackIntWrapper.value); + + } + + @Test + void testAnnotatedAccessorSerializedAsHexString() + throws JsonProcessingException { + ObjectMapper mapper = newJsonMapper(); + AnnotatedMethodIntWrapper initialIntWrapper = new AnnotatedMethodIntWrapper(10); + String expectedJson = "{'value':'a'}"; + + String json = mapper.writeValueAsString(initialIntWrapper); + + assertEquals(a2q(expectedJson), json); + + AnnotatedMethodIntWrapper readBackIntWrapper = mapper.readValue(a2q(expectedJson), AnnotatedMethodIntWrapper.class); + + assertNotNull(readBackIntWrapper); + assertEquals(initialIntWrapper.value, readBackIntWrapper.value); + } + + @Test + void testAnnotatedAccessorWithoutPatternDoesNotThrow() + throws JsonProcessingException { + ObjectMapper mapper = newJsonMapper(); + IncorrectlyAnnotatedMethodIntWrapper initialIntWrapper = new IncorrectlyAnnotatedMethodIntWrapper(10); + String expectedJson = "{'value':'10'}"; + + String json = mapper.writeValueAsString(initialIntWrapper); + + assertEquals(a2q(expectedJson), json); + } + + @Test + void testUsingDefaultConfigOverrideRadixToSerializeAsHexString() + throws JsonProcessingException { + ObjectMapper mapper = newJsonMapper(); + mapper.configOverride(Integer.class) + .setFormat(JsonFormat.Value.forShape(JsonFormat.Shape.STRING)); + mapper.setDefaultFormat(HEX_RADIX); + IntegerWrapper intialIntegerWrapper = new IntegerWrapper(10); + String expectedJson = "{'value':'a'}"; + + String json = mapper.writeValueAsString(intialIntegerWrapper); + + assertEquals(a2q(expectedJson), json); + + IntegerWrapper readBackIntegerWrapper = mapper.readValue(a2q(expectedJson), IntegerWrapper.class); + + assertNotNull(readBackIntegerWrapper); + assertEquals(intialIntegerWrapper.value, readBackIntegerWrapper.value); + } + + @Test + void testAllIntegralTypesGetSerializedAsBinary() + throws JsonProcessingException { + ObjectMapper mapper = newJsonMapper(); + AllIntegralTypeWrapper initialIntegralTypeWrapper = new AllIntegralTypeWrapper((byte) 1, + (byte) 2, (short) 3, (short) 4, 5, 6, 7L, 8L, new BigInteger("9")); + String expectedJson = "{'byteValue':'1','ByteValue':'10','shortValue':'11','ShortValue':'100','intValue':'101','IntegerValue':'110','longValue':'111','LongValue':'1000','bigInteger':'1001'}"; + + String json = mapper.writeValueAsString(initialIntegralTypeWrapper); + + assertEquals(a2q(expectedJson), json); + + AllIntegralTypeWrapper readbackIntegralTypeWrapper = mapper.readValue(a2q(expectedJson), AllIntegralTypeWrapper.class); + + assertNotNull(readbackIntegralTypeWrapper); + assertEquals(initialIntegralTypeWrapper.byteValue, readbackIntegralTypeWrapper.byteValue); + assertEquals(initialIntegralTypeWrapper.ByteValue, readbackIntegralTypeWrapper.ByteValue); + assertEquals(initialIntegralTypeWrapper.shortValue, readbackIntegralTypeWrapper.shortValue); + assertEquals(initialIntegralTypeWrapper.ShortValue, readbackIntegralTypeWrapper.ShortValue); + assertEquals(initialIntegralTypeWrapper.intValue, readbackIntegralTypeWrapper.intValue); + assertEquals(initialIntegralTypeWrapper.IntegerValue, readbackIntegralTypeWrapper.IntegerValue); + assertEquals(initialIntegralTypeWrapper.longValue, readbackIntegralTypeWrapper.longValue); + assertEquals(initialIntegralTypeWrapper.LongValue, readbackIntegralTypeWrapper.LongValue); + assertEquals(initialIntegralTypeWrapper.bigInteger, readbackIntegralTypeWrapper.bigInteger); + } +} diff --git a/src/test/java/com/fasterxml/jackson/databind/introspect/ConcreteBeanPropertyBaseTest.java b/src/test/java/com/fasterxml/jackson/databind/introspect/ConcreteBeanPropertyBaseTest.java new file mode 100644 index 0000000000..f7fe25d607 --- /dev/null +++ b/src/test/java/com/fasterxml/jackson/databind/introspect/ConcreteBeanPropertyBaseTest.java @@ -0,0 +1,181 @@ +package com.fasterxml.jackson.databind.introspect; + +import com.fasterxml.jackson.annotation.JsonFormat; +import com.fasterxml.jackson.databind.AnnotationIntrospector; +import com.fasterxml.jackson.databind.JavaType; +import com.fasterxml.jackson.databind.JsonMappingException; +import com.fasterxml.jackson.databind.PropertyMetadata; +import com.fasterxml.jackson.databind.PropertyName; +import com.fasterxml.jackson.databind.SerializerProvider; +import com.fasterxml.jackson.databind.cfg.MapperConfig; +import com.fasterxml.jackson.databind.jsonFormatVisitors.JsonObjectFormatVisitor; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import java.lang.annotation.Annotation; +import java.lang.reflect.AnnotatedElement; +import java.lang.reflect.Member; + +import static org.hamcrest.MatcherAssert.assertThat; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +//TODO: Once mockito is updated to include Premain-Class in its MANIFEST.MF, we need to add -javaagent:/${m2directory}/.m2/repository/org/mockito/mockito-core/${mockit-version}/$33{mockit-version}.jar +class ConcreteBeanPropertyBaseTest { + + private static final class TestConcreteBeanPropertyBase extends ConcreteBeanPropertyBase { + + TestConcreteBeanPropertyBase(PropertyMetadata md) { + super(md); + } + + @Override + public String getName() { + return ""; + } + + @Override + public PropertyName getFullName() { + return null; + } + + @Override + public JavaType getType() { + return null; + } + + @Override + public PropertyName getWrapperName() { + return null; + } + + @Override + public A getAnnotation(Class acls) { + return null; + } + + @Override + public A getContextAnnotation(Class acls) { + return null; + } + + @Override + public AnnotatedMember getMember() { + return new TestAnnotatedMember(null, null); + } + + @Override + public void depositSchemaProperty(JsonObjectFormatVisitor objectVisitor, SerializerProvider provider) + throws JsonMappingException { + + } + } + + private static final class TestAnnotatedMember extends AnnotatedMember { + + TestAnnotatedMember(TypeResolutionContext ctxt, AnnotationMap annotations) { + super(ctxt, annotations); + } + + @Override + public Annotated withAnnotations(AnnotationMap fallback) { + return null; + } + + @Override + public Class> getDeclaringClass() { + return null; + } + + @Override + public Member getMember() { + return null; + } + + @Override + public void setValue(Object pojo, Object value) + throws UnsupportedOperationException, IllegalArgumentException { + + } + + @Override + public Object getValue(Object pojo) + throws UnsupportedOperationException, IllegalArgumentException { + return null; + } + + @Override + public AnnotatedElement getAnnotated() { + return null; + } + + @Override + protected int getModifiers() { + return 0; + } + + @Override + public String getName() { + return ""; + } + + @Override + public JavaType getType() { + return null; + } + + @Override + public Class> getRawType() { + return null; + } + + @Override + public boolean equals(Object o) { + return false; + } + + @Override + public int hashCode() { + return 0; + } + + @Override + public String toString() { + return ""; + } + } + + private TestConcreteBeanPropertyBase testConcreteBeanProperty; + private Class someType; + private MapperConfig> mapperConfig; + private AnnotationIntrospector annotationIntrospector; + + @BeforeEach + void setUp() { + mapperConfig = mock(MapperConfig.class); + testConcreteBeanProperty = new TestConcreteBeanPropertyBase( + PropertyMetadata.STD_REQUIRED); + annotationIntrospector = mock(AnnotationIntrospector.class); + when(mapperConfig.getAnnotationIntrospector()).thenReturn(annotationIntrospector); + someType = Class.class; + } + + @Test + void testFormatPrecedenceIsFollowed() { + String lowestPrecedenceFormat = "Low Precedence"; + JsonFormat.Value midPrecedenceFormat = new JsonFormat.Value("Mid Precedence", null, + (String) null, null, null, null); + JsonFormat.Value highestPrecedence = new JsonFormat.Value("High Precedence", null, + (String) null, null, null, null); + when(mapperConfig.getDefaultRadix()).thenReturn(lowestPrecedenceFormat); + when(mapperConfig.getDefaultPropertyFormat(any())).thenReturn(midPrecedenceFormat); + when(annotationIntrospector.findFormat(any())).thenReturn(highestPrecedence); + + JsonFormat.Value resultFormat = testConcreteBeanProperty.findPropertyFormat(mapperConfig, someType); + + + assertEquals(highestPrecedence.getPattern(), resultFormat.getPattern()); + } +} \ No newline at end of file