Uploaded image for project: 'camunda BPM'
  1. camunda BPM
  2. CAM-7123

Numeric value in decision table is parsed as double (regardless of actual type)

XMLWordPrintable

    • Icon: Bug Report Bug Report
    • Resolution: Won't Fix
    • Icon: L3 - Default L3 - Default
    • None
    • 7.5.0, 7.6.0, 7.6.1
    • dmn-engine
    • 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.

      Probable Reason

      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);
              }
          }
      }
      

        This is the controller panel for Smart Panels app

              Unassigned Unassigned
              CarstenEnglert Carsten Wickner
              Votes:
              1 Vote for this issue
              Watchers:
              3 Start watching this issue

                Created:
                Updated:
                Resolved: