Skip to content

Commit

Permalink
#182 Make BeanMapper support BigInteger and BigDecimal
Browse files Browse the repository at this point in the history
- Add default support for BigInteger and BigDecimal to the bean mapper
- Change test demonstrating custom type support to use custom class instead of BigInteger, which is now supported out of the box
  • Loading branch information
ljacqu committed Apr 19, 2021
1 parent 97ea6d4 commit ed6c466
Show file tree
Hide file tree
Showing 3 changed files with 265 additions and 23 deletions.
Original file line number Diff line number Diff line change
@@ -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;
Expand Down Expand Up @@ -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;
}
Expand Down Expand Up @@ -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<?>, Class<?>> PRIMITIVE_NUMBERS_MAP = buildPrimitiveNumberMap();
Expand Down Expand Up @@ -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) {
Expand All @@ -161,4 +168,87 @@ private static Map<Class<?>, 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();
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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 <a href="https://github.com/AuthMe/ConfigMe/issues/182">Issue #182</a>
*/
class BeanWithCustomBigIntegerTypeHandlerTest {
class BeanWithCustomTypeHandlerTest {

@TempDir
Path tempDir;
Expand Down Expand Up @@ -64,44 +64,76 @@ 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<RangeCollection> 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;
}

@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<String, Range> rangeByName;
Expand All @@ -115,32 +147,35 @@ public void setRangeByName(Map<String, Range> 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;
}

Expand Down
Loading

0 comments on commit ed6c466

Please sign in to comment.