-
Bug Report
-
Resolution: Won't Fix
-
L3 - Default
-
None
-
7.5.0, 7.6.0, 7.6.1
-
None
The DMN Engine allows for custom data types, e.g. a decimal data type to provide the precision offered by BigDecimal – instead of double.
Currently, defining a custom type with the intention of increased precision seems meaningless as numeric values from within a decision table are always interpreted with double precision.
Unfortunately, the org.camunda.bpm.dmn.engine.impl.el.JuelElProvider does not use the explicit typing offered by the de.odysseus.el.ExpressionFactoryImpl's createValueExpression(ELContext, String, Class<?>) method and always uses Object.class as the expected type (i.e. the third parameter) in its createExpression(String) method.
As a result, the ExpressionFactoryImpl parses each numeric text without surrounding quotes as double – leading to the numeric value being rounded before it even reaches my custom DecimalTypeImpl where it's supposed to be parsed as a BigDecimal.
Below, you can find a test class which evaluates a simple decision table with a decimal result type, showcasing it being rounded to the nearest double value if it's not surrounded by quotes. The test class includes a sample implementation of the decimal type.
You'll find two tests: testDecimalValueAsNumeral() (which tests what I actually want and currently fails) and testDecimalValueWithQuotes() (which tests my current workaround of providing the numeric value as String, which works correctly).
import java.io.ByteArrayInputStream; import java.io.IOException; import java.math.BigDecimal; import java.nio.charset.StandardCharsets; import java.util.Collections; import java.util.Map; import org.camunda.bpm.dmn.engine.DmnDecision; import org.camunda.bpm.dmn.engine.DmnDecisionTableResult; import org.camunda.bpm.dmn.engine.DmnEngine; import org.camunda.bpm.dmn.engine.impl.DefaultDmnEngineConfiguration; import org.camunda.bpm.dmn.engine.impl.spi.type.DmnDataTypeTransformer; import org.camunda.bpm.engine.variable.impl.type.PrimitiveValueTypeImpl; import org.camunda.bpm.engine.variable.impl.value.PrimitiveTypeValueImpl; import org.camunda.bpm.engine.variable.type.PrimitiveValueType; import org.camunda.bpm.engine.variable.type.ValueType; import org.camunda.bpm.engine.variable.value.NumberValue; import org.camunda.bpm.engine.variable.value.PrimitiveValue; import org.camunda.bpm.engine.variable.value.TypedValue; import org.junit.Assert; import org.junit.Before; import org.junit.Test; public class DecisionTableOutputTest { private static final String DECISION_TABLE_CONTENT = "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n" + "<definitions xmlns=\"http://www.omg.org/spec/DMN/20151101/dmn11.xsd\"\n" + " id=\"definitions\" name=\"definitions\" namespace=\"http://camunda.org/schema/1.0/dmn\">\n" + " <decision id=\"decision\" name=\"BigDecimal Test\">\n" + " <decisionTable id=\"decisionTable\">\n" + " <input id=\"input1\" label=\"Boolean Input\">\n" + " <inputExpression id=\"inputExpression1\" typeRef=\"boolean\"><text>useQuotes</text></inputExpression>\n" + " </input>\n" + " <output id=\"output1\" label=\"Decimal Output\" name=\"result\" typeRef=\"decimal\" />\n" + " <rule id=\"row-262964732-1\">\n" + " <description><![CDATA[4 999 999 999 999 999.5 should not be rounded to\n" + "5 000 000 000 000 000 (double supports only 16 digits or precision)]]></description>\n" + " <inputEntry id=\"UnaryTests_1opxikx\"><text>false</text></inputEntry>\n" + " <outputEntry id=\"LiteralExpression_0yuxh9t\"><text>4999999999999999.5</text></outputEntry>\n" + " </rule>\n" + " <rule id=\"row-262964732-2\">\n" + " <description>Work fine, if put in quotes to enforce String to BigDecimal conversion outside of Juel</description>\n" + " <inputEntry id=\"UnaryTests_03s78wj\"><text>true</text></inputEntry>\n" + " <outputEntry id=\"LiteralExpression_0m0ryts\"><text><![CDATA[\"4999999999999999.5\"]]></text></outputEntry>\n" + " </rule>\n" + " </decisionTable>\n" + " </decision>\n" + "</definitions>"; private DmnEngine engine; private DmnDecision dmnDecision; @Before public void setUp() throws IOException { DefaultDmnEngineConfiguration config = (DefaultDmnEngineConfiguration) DefaultDmnEngineConfiguration.createDefaultDmnEngineConfiguration(); config.getTransformer().getDataTypeTransformerRegistry().addTransformer("decimal", new DecimalDataTypeTransformer()); this.engine = config.buildEngine(); try (ByteArrayInputStream decisionTableStream = new ByteArrayInputStream(DECISION_TABLE_CONTENT.getBytes(StandardCharsets.UTF_8))) { this.dmnDecision = this.engine.parseDecisions(decisionTableStream).get(0); } } @Test public void testDecimalValueWithQuotes() { DmnDecisionTableResult result = this.engine.evaluateDecisionTable(this.dmnDecision, Collections.singletonMap("useQuotes", Boolean.TRUE)); Object resultValue = result.getSingleResult().get("result"); Assert.assertEquals(new BigDecimal("4999999999999999.5"), resultValue); } @Test public void testDecimalValueAsNumeral() { DmnDecisionTableResult result = this.engine.evaluateDecisionTable(this.dmnDecision, Collections.singletonMap("useQuotes", Boolean.FALSE)); Object resultValue = result.getSingleResult().get("result"); Assert.assertEquals(new BigDecimal("4999999999999999.5"), resultValue); } /* Helper Methods and Implementation of "decimal" type (instead of "double"). */ private static BigDecimal getBigDecimalFromNumber(Number value) { BigDecimal result; if (value == null) { result = null; } else if (value instanceof BigDecimal) { // make sure to not lose any precision result = (BigDecimal) value; } else { result = BigDecimal.valueOf(value.doubleValue()); } return result; } public static final class CustomValueTypes { public static final PrimitiveValueType DECIMAL = new DecimalTypeImpl(); static PrimitiveValue<BigDecimal> decimalValue(Number value) { return new DecimalValueImpl(getBigDecimalFromNumber(value)); } } public static class DecimalTypeImpl extends PrimitiveValueTypeImpl { private static final long serialVersionUID = 1L; public DecimalTypeImpl() { super(BigDecimal.class); } @Override public PrimitiveValue<BigDecimal> createValue(Object value, Map<String, Object> valueInfo) { return CustomValueTypes.decimalValue((Number) value); } @Override public ValueType getParent() { return ValueType.NUMBER; } @Override public boolean canConvertFromTypedValue(TypedValue typedValue) { return typedValue.getType() == ValueType.NUMBER; } @Override public PrimitiveValue<BigDecimal> convertFromTypedValue(TypedValue typedValue) { if (!ValueType.NUMBER.equals(typedValue.getType())) { throw new IllegalArgumentException("The type " + this.getName() + " supports no conversion from type: " + typedValue.getType().getName()); } Number actualValue = ((NumberValue) typedValue).getValue(); return CustomValueTypes.decimalValue(actualValue); } } public static class DecimalDataTypeTransformer implements DmnDataTypeTransformer { @Override public PrimitiveValue<BigDecimal> transform(Object value) throws IllegalArgumentException { Number transformedValue; if (value instanceof Number) { transformedValue = (Number) value; } else if (value instanceof String) { transformedValue = new BigDecimal((String) value); } else { throw new IllegalArgumentException(); } PrimitiveValue<BigDecimal> wrappedValue = CustomValueTypes.decimalValue(transformedValue); return wrappedValue; } } public static class DecimalValueImpl extends PrimitiveTypeValueImpl<BigDecimal> { private static final long serialVersionUID = 1L; public DecimalValueImpl(BigDecimal value) { super(value, CustomValueTypes.DECIMAL); } } }