diff --git a/src/main/java/ch/jalu/configme/beanmapper/leafvaluehandler/StandardLeafValueHandlers.java b/src/main/java/ch/jalu/configme/beanmapper/leafvaluehandler/StandardLeafValueHandlers.java index f9fc629f..a50f7f9a 100644 --- a/src/main/java/ch/jalu/configme/beanmapper/leafvaluehandler/StandardLeafValueHandlers.java +++ b/src/main/java/ch/jalu/configme/beanmapper/leafvaluehandler/StandardLeafValueHandlers.java @@ -1,5 +1,8 @@ package ch.jalu.configme.beanmapper.leafvaluehandler; +import javax.annotation.Nullable; +import java.math.BigDecimal; +import java.math.BigInteger; import java.util.Collections; import java.util.HashMap; import java.util.Map; @@ -27,7 +30,7 @@ private StandardLeafValueHandlers() { public static LeafValueHandler getDefaultLeafValueHandler() { if (defaultHandler == null) { defaultHandler = new CombiningLeafValueHandler(new StringHandler(), new EnumHandler(), - new BooleanHandler(), new ObjectHandler(), new NumberHandler()); + new BooleanHandler(), new NumberHandler(), new BigNumberHandler(), new ObjectHandler()); } return defaultHandler; } @@ -109,7 +112,7 @@ public Object toExportValue(Object value) { } } - /** Number handler. */ + /** Number handler for types without arbitrary precision. */ public static class NumberHandler extends AbstractLeafValueHandler { private static final Map, Class> PRIMITIVE_NUMBERS_MAP = buildPrimitiveNumberMap(); @@ -142,7 +145,11 @@ public Object convert(Class clazz, Object value) { @Override public Object toExportValue(Object value) { - return (value instanceof Number) ? value : null; + if (value instanceof Number) { + // TODO #182: Turn check around so no values are ever exported that are not supported by the handler. + return (value instanceof BigInteger || value instanceof BigDecimal) ? null : value; + } + return null; } protected Class asReferenceClass(Class clazz) { @@ -161,4 +168,87 @@ private static Map, Class> buildPrimitiveNumberMap() { return Collections.unmodifiableMap(map); } } + + /** + * Number handler for 'Big' types that have arbitrary precision (BigInteger, BigDecimal) + * and should be represented as strings in the config. + */ + public static class BigNumberHandler extends AbstractLeafValueHandler { + + /** Value after which scientific notation (like "1E+30") might be used when exporting BigDecimal values. */ + private static final BigDecimal BIG_DECIMAL_SCIENTIFIC_THRESHOLD = new BigDecimal("1E10"); + + @Override + protected Object convert(Class clazz, Object value) { + if (clazz != BigInteger.class && clazz != BigDecimal.class) { + return null; + } + if (value instanceof String) { + return fromString(clazz, (String) value); + } else if (value instanceof Number) { + return fromNumber(clazz, (Number) value); + } + return null; + } + + @Override + public Object toExportValue(Object value) { + if (value instanceof BigInteger) { + return value.toString(); + } else if (value instanceof BigDecimal) { + BigDecimal bigDecimal = (BigDecimal) value; + return bigDecimal.abs().compareTo(BIG_DECIMAL_SCIENTIFIC_THRESHOLD) >= 0 + ? bigDecimal.toString() + : bigDecimal.toPlainString(); + } + return null; + } + + /** + * Creates a BigInteger or BigDecimal value from the given string value, if possible. + * + * @param targetClass the target class to convert to (can only be BigInteger or BigDecimal) + * @param value the value to convert + * @return BigInteger or BigDecimal as defined by the target class, or null if no conversion was possible + */ + @Nullable + protected Object fromString(Class targetClass, String value) { + try { + return targetClass == BigInteger.class + ? new BigInteger(value) + : new BigDecimal(value); + } catch (NumberFormatException e) { + return null; + } + } + + /** + * Creates a BigInteger or BigDecimal value from the given number value, if possible. + * + * @param targetClass the target class to convert to (can only be BigInteger or BigDecimal) + * @param value the value to convert + * @return BigInteger or BigDecimal as defined by the target class + */ + protected Object fromNumber(Class targetClass, Number value) { + if (targetClass.isInstance(value)) { + return value; + } + + if (targetClass == BigInteger.class) { + // Don't handle value = BigDecimal separately as property readers should only use basic types anyway + if (value instanceof Double || value instanceof Float) { + return BigDecimal.valueOf(value.doubleValue()).toBigInteger(); + } + return BigInteger.valueOf(value.longValue()); + } + + // targetClass is BigDecimal if we reach this part. Check for Long first as we might lose precision if we + // use doubleValue (seems like integer would be fine, but let's do it anyway too). + // Smaller types like short are fine as all values can be precisely represented as a double. + if (value instanceof Integer || value instanceof Long) { + return BigDecimal.valueOf(value.longValue()); + } + return BigDecimal.valueOf(value.doubleValue()).stripTrailingZeros(); + } + } } diff --git a/src/test/java/ch/jalu/configme/beanmapper/BeanWithCustomBigIntegerTypeHandlerTest.java b/src/test/java/ch/jalu/configme/beanmapper/BeanWithCustomTypeHandlerTest.java similarity index 62% rename from src/test/java/ch/jalu/configme/beanmapper/BeanWithCustomBigIntegerTypeHandlerTest.java rename to src/test/java/ch/jalu/configme/beanmapper/BeanWithCustomTypeHandlerTest.java index 493fe298..0a3fa43a 100644 --- a/src/test/java/ch/jalu/configme/beanmapper/BeanWithCustomBigIntegerTypeHandlerTest.java +++ b/src/test/java/ch/jalu/configme/beanmapper/BeanWithCustomTypeHandlerTest.java @@ -15,7 +15,6 @@ import javax.annotation.Nullable; import java.io.IOException; -import java.math.BigInteger; import java.nio.file.Files; import java.nio.file.Path; import java.util.Map; @@ -26,11 +25,12 @@ import static org.hamcrest.Matchers.equalTo; /** - * Tests bean properties with BigInteger field, which are handled by a custom type handler. + * Tests that the bean mapper can be extended to support custom types. Bean properties with fields of a + * {@link CustomInteger custom type} are used, which are handled by an additional custom value handler. * * @see Issue #182 */ -class BeanWithCustomBigIntegerTypeHandlerTest { +class BeanWithCustomTypeHandlerTest { @TempDir Path tempDir; @@ -64,30 +64,39 @@ void shouldLoadMap() throws IOException { assertThat(ranges.getRangeByName().get("speed"), equalTo(new Range(2, 7))); } + /** + * Settings holder class with a bean property defined to use a custom bean mapper. + */ public static final class MyTestSettings implements SettingsHolder { public static final Property RANGES = - new BeanProperty<>(RangeCollection.class, "", new RangeCollection(), new MapperWithBigIntSupport()); + new BeanProperty<>(RangeCollection.class, "", new RangeCollection(), new MapperWithCustomIntSupport()); private MyTestSettings() { } } - public static final class MapperWithBigIntSupport extends MapperImpl { + /** + * Mapper extension with a custom type handler so that {@link CustomInteger} is supported. + */ + public static final class MapperWithCustomIntSupport extends MapperImpl { - MapperWithBigIntSupport() { + MapperWithCustomIntSupport() { super(new BeanDescriptionFactoryImpl(), new CombiningLeafValueHandler(StandardLeafValueHandlers.getDefaultLeafValueHandler(), - new BigIntegerLeafValueHandler())); + new CustomIntegerLeafValueHandler())); } } - public static final class BigIntegerLeafValueHandler extends AbstractLeafValueHandler { + /** + * Provides {@link CustomInteger} when reading from and writing to a property resource. + */ + public static final class CustomIntegerLeafValueHandler extends AbstractLeafValueHandler { @Override protected Object convert(Class clazz, Object value) { - if (clazz.equals(BigInteger.class) && value instanceof Number) { - return BigInteger.valueOf(((Number) value).longValue()); + if (clazz == CustomInteger.class && value instanceof Number) { + return new CustomInteger(((Number) value).intValue(), false); } return null; } @@ -95,13 +104,36 @@ protected Object convert(Class clazz, Object value) { @Nullable @Override public Object toExportValue(@Nullable Object value) { - if (value instanceof BigInteger) { - return ((BigInteger) value).longValue(); + if (value instanceof CustomInteger) { + return ((CustomInteger) value).value; } return null; } } + /** + * Custom integer - dummy class that wraps an integer. + */ + public static class CustomInteger { + + private final int value; + + CustomInteger(int value, boolean otherParam) { + // 'otherParam' is just here to show that it is a custom initialization and not a constructor the mapper + // could somehow pick up automatically. + this.value = value; + } + + @Override + public boolean equals(Object that) { + return this == that + || (that instanceof CustomInteger && this.value == ((CustomInteger) that).value); + } + } + + /** + * Range collection: bean type as used in the the bean property of {@link MyTestSettings}. + */ public static class RangeCollection { private Map rangeByName; @@ -115,32 +147,35 @@ public void setRangeByName(Map rangeByName) { } } + /** + * Bean type which represents a range using the custom integer type. Used in {@link RangeCollection}. + */ public static class Range { - private BigInteger min; - private BigInteger max; + private CustomInteger min; + private CustomInteger max; public Range() { } public Range(int min, int max) { - this.min = BigInteger.valueOf(min); - this.max = BigInteger.valueOf(max); + this.min = new CustomInteger(min, false); + this.max = new CustomInteger(max, false); } - public BigInteger getMin() { + public CustomInteger getMin() { return min; } - public void setMin(BigInteger min) { + public void setMin(CustomInteger min) { this.min = min; } - public BigInteger getMax() { + public CustomInteger getMax() { return max; } - public void setMax(BigInteger max) { + public void setMax(CustomInteger max) { this.max = max; } diff --git a/src/test/java/ch/jalu/configme/beanmapper/leafvaluehandler/StandardLeafValueHandlersTest.java b/src/test/java/ch/jalu/configme/beanmapper/leafvaluehandler/StandardLeafValueHandlersTest.java index cb509843..4bf97217 100644 --- a/src/test/java/ch/jalu/configme/beanmapper/leafvaluehandler/StandardLeafValueHandlersTest.java +++ b/src/test/java/ch/jalu/configme/beanmapper/leafvaluehandler/StandardLeafValueHandlersTest.java @@ -6,17 +6,21 @@ import org.junit.jupiter.api.Test; import java.lang.reflect.Type; +import java.math.BigDecimal; +import java.math.BigInteger; import java.util.Arrays; import java.util.Collections; import java.util.List; import java.util.Map; import java.util.Optional; +import java.util.concurrent.TimeUnit; import java.util.stream.Stream; import static ch.jalu.configme.TestUtils.transform; import static org.hamcrest.MatcherAssert.assertThat; import static org.hamcrest.Matchers.containsInAnyOrder; import static org.hamcrest.Matchers.equalTo; +import static org.hamcrest.Matchers.not; import static org.hamcrest.Matchers.nullValue; import static org.hamcrest.Matchers.sameInstance; @@ -208,6 +212,115 @@ void shouldNotExportOtherValues() { assertThat(transformer.toExportValue(Arrays.asList("", 5)), nullValue()); } + // for reference, max values for each type: + // * short 32,767 + // * int 2,147,483,647 + // * long 9,223,372,036,854,775,807 + // * float 3.40282347E+38 + // * double 1.797693134...E+308 + + @Test + void shouldTransformNumbersToBigInteger() { + // given + LeafValueHandler handler = StandardLeafValueHandlers.getDefaultLeafValueHandler(); + TypeInformation typeInformation = of(BigInteger.class); + + long longImpreciseAsDouble = 4611686018427387903L; + assertThat(longImpreciseAsDouble, not(equalTo(Double.valueOf(longImpreciseAsDouble).longValue()))); + + // when / then + assertThat(handler.convert(typeInformation, 3), equalTo(BigInteger.valueOf(3))); + assertThat(handler.convert(typeInformation, 27.88), equalTo(BigInteger.valueOf(27))); + assertThat(handler.convert(typeInformation, longImpreciseAsDouble), equalTo(newBigInteger("4611686018427387903"))); + assertThat(handler.convert(typeInformation, -1976453120), equalTo(newBigInteger("-1976453120"))); + assertThat(handler.convert(typeInformation, 2e50), equalTo(newBigInteger("2E+50"))); + assertThat(handler.convert(typeInformation, 1e20d), equalTo(newBigInteger("1E+20"))); + assertThat(handler.convert(typeInformation, 1e20f), equalTo(newBigInteger("100000002004087730000"))); + assertThat(handler.convert(typeInformation, (byte) -120), equalTo(BigInteger.valueOf(-120))); + assertThat(handler.convert(typeInformation, (short) 32504), equalTo(BigInteger.valueOf(32504))); + + BigInteger bigInteger = newBigInteger("3.141592E+500"); + assertThat(handler.convert(typeInformation, bigInteger), equalTo(bigInteger)); + } + + @Test + void shouldTransformNumbersToBigDecimal() { + // given + LeafValueHandler handler = StandardLeafValueHandlers.getDefaultLeafValueHandler(); + TypeInformation typeInformation = of(BigDecimal.class); + + long longImpreciseAsDouble = 5076541234567890123L; + assertThat(longImpreciseAsDouble, not(equalTo(Double.valueOf(longImpreciseAsDouble).longValue()))); + + // when / then + assertThat(handler.convert(typeInformation, 6), equalTo(new BigDecimal("6"))); + assertThat(handler.convert(typeInformation, -1131.25116), equalTo(new BigDecimal("-1131.25116"))); + assertThat(handler.convert(typeInformation, longImpreciseAsDouble), equalTo(new BigDecimal("5076541234567890123"))); + assertThat(handler.convert(typeInformation, 2e50), equalTo(new BigDecimal("2E+50"))); + assertThat(handler.convert(typeInformation, -1e18f), equalTo(new BigDecimal("-9.9999998430674944E+17"))); + assertThat(handler.convert(typeInformation, (byte) 101), equalTo(new BigDecimal("101"))); + assertThat(handler.convert(typeInformation, (short) -32724), equalTo(new BigDecimal("-32724"))); + + BigDecimal bigDecimal = new BigDecimal("3.0000283746E+422"); + assertThat(handler.convert(typeInformation, bigDecimal), equalTo(bigDecimal)); + } + + @Test + void shouldTransformStringsToBigDecimalAndBigInteger() { + // given + LeafValueHandler handler = StandardLeafValueHandlers.getDefaultLeafValueHandler(); + + // when / then + assertThat(handler.convert(of(BigInteger.class), "141414"), equalTo(BigInteger.valueOf(141414L))); + assertThat(handler.convert(of(BigInteger.class), "88223372036854775807"), equalTo(new BigInteger("88223372036854775807"))); + assertThat(handler.convert(of(BigInteger.class), "invalid"), nullValue()); + assertThat(handler.convert(of(BigInteger.class), "7.5"), nullValue()); + + assertThat(handler.convert(of(BigDecimal.class), "1234567"), equalTo(new BigDecimal("1234567"))); + assertThat(handler.convert(of(BigDecimal.class), "88223372036854775807.999"), equalTo(new BigDecimal("88223372036854775807.999"))); + assertThat(handler.convert(of(BigDecimal.class), "1.4237E+725"), equalTo(new BigDecimal("1.4237E+725"))); + assertThat(handler.convert(of(BigDecimal.class), "invalid"), nullValue()); + assertThat(handler.convert(of(BigDecimal.class), "7E+34E"), nullValue()); + } + + @Test + void shouldHandleUnsupportedTypesWhenTransformingToBigIntegerOrBigDecimal() { + // given + LeafValueHandler handler = StandardLeafValueHandlers.getDefaultLeafValueHandler(); + + // when / then + Stream.of(TimeUnit.SECONDS, true, null, new Object()).forEach(invalidParam -> { + assertThat(handler.convert(of(BigInteger.class), null), nullValue()); + assertThat(handler.convert(of(BigDecimal.class), null), nullValue()); + }); + } + + @Test + void shouldExportBigIntegerValuesCorrectly() { + // given + LeafValueHandler handler = StandardLeafValueHandlers.getDefaultLeafValueHandler(); + + // when / then + assertThat(handler.toExportValue(new BigInteger("0")), equalTo("0")); + assertThat(handler.toExportValue(new BigInteger("123987")), equalTo("123987")); + assertThat(handler.toExportValue(new BigInteger("-16541234560123456789")), equalTo("-16541234560123456789")); + } + + @Test + void shouldExportBigDecimalValuesCorrectly() { + // given + LeafValueHandler handler = StandardLeafValueHandlers.getDefaultLeafValueHandler(); + + // when / then + assertThat(handler.toExportValue(new BigDecimal("0")), equalTo("0")); + assertThat(handler.toExportValue(new BigDecimal("-123987.440")), equalTo("-123987.440")); + assertThat(handler.toExportValue(new BigDecimal("5.2348997563E+300")), equalTo("5.2348997563E+300")); + assertThat(handler.toExportValue(new BigDecimal("9123456789.43214321")), equalTo("9123456789.43214321")); + assertThat(handler.toExportValue(new BigDecimal("-9999999999.999999")), equalTo("-9999999999.999999")); + assertThat(handler.toExportValue(new BigDecimal("-2E3")), equalTo("-2000")); + assertThat(handler.toExportValue(new BigDecimal("-2.5E+10")), equalTo("-2.5E+10")); + } + private void assertExportValueSameAsInput(LeafValueHandler transformer, Object input) { assertThat(transformer.toExportValue(input), sameInstance(input)); } @@ -215,4 +328,8 @@ private void assertExportValueSameAsInput(LeafValueHandler transformer, Object i private static TypeInformation of(Type type) { return new TypeInformation(type); } + + private static BigInteger newBigInteger(String value) { + return new BigDecimal(value).toBigIntegerExact(); + } }