DEV Community

Anastasiia Vorobeva
Anastasiia Vorobeva

Posted on

Inside Java Enterprise code: let's check Flowable

Java is famous for its widespread use in enterprise applications. Business processes need to be managed. The Flowable platform can help with that! What's more, the project is written in Java and is open source. So, we can analyze it and search for errors using PVS-Studio.

1195_flowable_article/image1.png

Terrifying words

What's in the foreword is pretty much all I can say about the project without just repeating the information on Wikipedia or the official website. So, this is a good idea to visit it: Flowable: Business Process Automation | Low-code | Workflow Automation.

Business means Enterprise. Java and Enterprise are a well-known bundle! That's why many lines of code can be compared to the length of an essay. This made me bother to slightly format the code to increase its readability. Note that most fragments shown here probably look different in the original code.

To make sure we're really working with Java, look at some short class names:

  • TimerChangeProcessDefinitionSuspensionStateJobHandler;
  • IntermediateThrowCompensationEventActivityBehavior;
  • AbstractExternalInvocationBpmnParseHandler;
  • ProcessInstanceStartEventSubscriptionModificationBuilderImpl;
  • UnfinishedHistoricActivityInstanceByProcessInstanceIdMatcher;
  • ParallelMultiInstanceWithNoWaitStatesAsyncLeaveJobHandler;
  • EventSubProcessVariableListenerlStartEventActivityBehavior.

But wait, there's more. The record holder for length is the UpdateChannelDefinitionTypeAndImplementationForAllChannelDefinitionsCmd class. And when it comes to tests, the same name with the Test suffix wins.

Now that we've seen the Enterprise names, I suggest we move on to the code base. To keep with tradition, we'll use PVS-Studio for this task, and see if we can find the most interesting things. Put on your suit jackets and let's go.

Instant execution

We begin our journey with tests. Although tests are designed to detect bugs in the program logic, it's just as easy to make a mistake in them as it is in regular code. Nobody writes tests for tests, but a static analyzer can check them.

By the way, just recently we've written a separate article about finding bugs in unit tests! I encourage testers to read it.

Here though, let's take a look at the following snippet:

@Test
@CmmnDeployment
public void testExpressionReturnsFuture() {
  CountDownLatch latch = new CountDownLatch(2);
  TestBeanReturnsFuture testBean = new TestBeanReturnsFuture(latch, 
    cmmnEngineConfiguration.getAsyncTaskExecutor());
  CaseInstance caseInstance = cmmnRuntimeService.createCaseInstanceBuilder()
    .caseDefinitionKey("myCase")
    .transientVariable("bean", testBean)
    .start();

  assertCaseInstanceEnded(caseInstance);

  if (....) {
    HistoricCaseInstance historicCaseInstance = ....;

    // Every service task sleeps for 1s, 
    // but when we use futures it should take less then 2s.
    long durationInMillis = historicCaseInstance.getEndTime().getTime()  
      historicCaseInstance.getEndTime().getTime();
    assertThat(durationInMillis).isLessThan(1500);
  }
}
Enter fullscreen mode Exit fullscreen mode

The test checks the service execution time. Unfortunately, durationInMillis is always zero because it subtracts getEndTime() from getEndTime(). So, the test that checks whether this value is less than 1500 is always positive because 0 < 1500, which is easy to prove after reading Principia Mathematica.

The fix is quite easy:

long durationInMillis = historicCaseInstance.getEndTime().getTime() 
  historicCaseInstance.getStartTime().getTime();
Enter fullscreen mode Exit fullscreen mode

I restarted the test with this fix, and it passed. Now let's look at the PVS-Studio analyzer message that helped us find the issue:

V6001 There are identical sub-expressions historicCaseInstance.getEndTime().getTime()' to the left and to the right of the '-' operator. ServiceTaskWithFuturesTest.java 76

BigDecimal

Next up is BigDecimal, a legend of fintech:

public class ELExecutionContextBuilder {
  ....
  protected static void preProcessInputVariables(DecisionTable decisionTable,
    Map<String, Object> inputVariables) {
    ....
    // check if transformation is needed
    for (Map.Entry<String, Object> inputVariable : inputVariables.entrySet()) {
      String inputVariableName = inputVariable.getKey();
      try {
        Object inputVariableValue = inputVariable.getValue();
        if (inputVariableValue instanceof LocalDate) {
          Date transformedDate = ((LocalDate) inputVariableValue).toDate();
          inputVariables.put(inputVariableName, transformedDate);
        } else if (inputVariableValue instanceof java.time.LocalDate) {
          Date transformedDate = 
            Date.from(((java.time.LocalDate) inputVariableValue).atStartOfDay()
              .atZone(ZoneId.systemDefault())
              .toInstant());
          inputVariables.put(inputVariableName, transformedDate);
        } else if (....) {
          BigInteger transformedNumber = 
            new BigInteger(inputVariableValue.toString());
          inputVariables.put(inputVariableName, transformedNumber);
        } else if (inputVariableValue instanceof Double) {
          BigDecimal transformedNumber = 
            new BigDecimal((Double) inputVariableValue);
          inputVariables.put(inputVariableName, transformedNumber);
        } else if (inputVariableValue instanceof Float) {
          double doubleValue = 
            Double.parseDouble(inputVariableValue.toString());
          BigDecimal transformedNumber = new BigDecimal(doubleValue);   // <=
          inputVariables.put(inputVariableName, transformedNumber);
        }
      } catch (Exception ex) {
        throw new FlowableException(....);
      }
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

I recommend that you don't gaze into the code for too long, or it will gaze back at you. We're interested in working with the aforementioned BigDecimal. Let's look at the following note in JavaDoc to clarify the details:

The results of this constructor can be somewhat unpredictable. One might assume that writing new BigDecimal(0.1) in Java creates a BigDecimal which is exactly equal to 0.1 (an unscaled value of 1, with a scale of 1), but it is actually equal to 0.1000000000000000055511151231257827021181583404541015625. This is because 0.1 cannot be represented exactly as a double (or, for that matter, as a binary fraction of any finite length). Thus, the value that is being passed in to the constructor is not exactly equal to 0.1, appearances notwithstanding.

The same document recommends using BigDecimal.valueOf(double) or new BigDecimal(String). _At the same time, the value in the specified fragment is parsed to _String and then back to double. Does it make any difference? Let's check out just for the fun of it:

void main() {
  double d = 0.1;
  BigDecimal bigDecimal;

  bigDecimal = new BigDecimal(d);
  println("new BigDecimal(double): \t" + bigDecimal.toPlainString());

  bigDecimal = new BigDecimal(String.valueOf(d));
  println("new BigDecimal(double -> string): \t" + 
    bigDecimal.toPlainString());

  bigDecimal = new BigDecimal(Double.parseDouble(String.valueOf(d)));
  println("new BigDecimal(double -> string -> double): \t" + 
    bigDecimal.toPlainString());

  bigDecimal = BigDecimal.valueOf(d);
  println("BigDecimal.valueOf(double): \t" + bigDecimal.toPlainString());
}
Enter fullscreen mode Exit fullscreen mode

void main()? Is this Java code?
Yes! For details, see JEP-477. However, at the time of writing, this is still a preview feature. I used it in the article to simplify this already simple fragment and not to write public static void main(String[] args). We have enough wordy strings in the Enterprise class names.

We get the following output:

new BigDecimal(double): 
  0.1000000000000000055511151231257827021181583404541015625
new BigDecimal(double -> string): 0.1
new BigDecimal(double -> string -> double):
  0.1000000000000000055511151231257827021181583404541015625
BigDecimal.valueOf(double): 0.1
Enter fullscreen mode Exit fullscreen mode

Well, additional conversions of double to string and back don't change the behavior. In any case, we need to use either valueOf or the constructor with the String argument if we want to get 0.1 from the 0.1 input value (based on the method name, it handles input variables).

As a result, not only do we get rid of a potentially unexpected value, but we also eliminate the redundant String to double parsing operation:

new BigDecimal(inputVariableValue.toString());
Enter fullscreen mode Exit fullscreen mode

Lastly, here's the PVS-Studio analyzer warning:

V6068 Constructor call can result in imprecise representation of the initialized value. ELExecutionContextBuilder.java 137

Space is always at the end

public class CronExpression implements Serializable, Cloneable {
  ....
  protected int findNextWhiteSpace(int i, String s) {
    for (; i < s.length() && (s.charAt(i) != ' ' || s.charAt(i) != '\t'); i++) {
    }

    return i;
  }
  ....
}
Enter fullscreen mode Exit fullscreen mode

I'd like to note that the loop body is empty in the original code fragment (otherwise it would have "....").

Here is an error we already encountered in other projects—one of the expressions is always true. We're talking about s.charAt(i) != ' ' || s.charAt(i) != '\t'.

To get a false OR result, a character must be equal to two different characters at the same time. Of course, this is impossible.

The result is that the loop is executed as long as the i < s.length() condition is true, and the method output is always the end of the string index, not the space character.

The PVS-Studio warning:

V6007 Expression 's.charAt(i) != '\t'' is always true. CronExpression.java 963

Immortal tests

@Test
void convertJsonToModelKeyWithFixedValue() {
  ChannelModel channelModel = readJson(....);
  assertThat(channelModel)
    .isInstanceOfSatisfying(KafkaOutboundChannelModel.class, model -> {
      model.getRecordKey().getFixedValue().equals("customer");
    });
}

@Test
void convertJsonToModelKeyWithDelegateExpression() {
  ChannelModel channelModel = readJson(....);
  assertThat(channelModel)
    .isInstanceOfSatisfying(KafkaOutboundChannelModel.class, model -> {
      model.getRecordKey().getDelegateExpression().equals("${test}");
    });
}

@Test
void convertJsonToModelKeyWithEventField() {
  ChannelModel channelModel = readJson(....);
  assertThat(channelModel)
    B.isInstanceOfSatisfying(KafkaOutboundChannelModel.class, model -> {
      model.getRecordKey().getEventField().equals("customer");
    });
}
Enter fullscreen mode Exit fullscreen mode

We do a short loop and meet the tests again. Once again, they aren't falling. Let's first look at the warnings that the PVS-Studio analyzer issues, then we'll fix them:

  • V6010 The return value of function 'equals' is required to be utilized. KafkaOutboundChannelJsonConverterTest.java 45
  • V6010 The return value of function 'equals' is required to be utilized. KafkaOutboundChannelJsonConverterTest.java 53
  • V6010 The return value of function 'equals' is required to be utilized. KafkaOutboundChannelJsonConverterTest.java 61

The developer probably expected the second argument of the isInstanceOfSatisfying method to be Function for some reason. I have no idea why there might have been such an expectation, but in reality, the method takes Consumer. Maybe the problem is simple carelessness, and developers may have thought that the equals method was equivalent to AssertEquals.

For the test to fail, we need an exception. Let's just wrap the equals calls in assertThat(....).isEqualTo(....). Here's an example:

assertThat(model.getRecordKey().getEventField()).isEqualTo("customer");
Enter fullscreen mode Exit fullscreen mode

Self-parent

public class ProcessExecutionLogger {
  protected void internalPopulateExecutionTree(....) {
    if (parentMapping.containsKey(parentNode.getId())) {
      for (....) {
        ....

        childNode.setParentNode(childNode);                        // <=
        parentNode.getChildNodes().add(childNode);

        internalPopulateExecutionTree(childNode, parentMapping);
      }
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

The childNode object is a parent of itself due to the broken hierarchy here. The necessary dependency is indirectly indicated by the line that immediately follows: parentNode.getChildNodes().add(childNote). So, the parent is aware of the child but not the other way around. I'm wondering if there's a risk when iterating over such nodes to reach a StackOverflowError?

Here's the PVS-Studio analyzer message about this anomaly:

V6100 An object is used as an argument to its own method. Consider checking the first actual argument of the 'setParentNode' method. ProcessExecutionLogger.java 107

Single iteration

protected String getTargetNameSpace(XMLStreamReader xmlStreamReader) {
  String targetNameSpace = null;
  try {
    while (xmlStreamReader.hasNext()) {
      try {
        xmlStreamReader.next();
      } catch (Exception e) {
        LOGGER.debug("Error reading XML document", e);
        throw new DmnXMLException("Error reading XML", e);
      }
      targetNameSpace = xmlStreamReader.getNamespaceURI();
      break;
    }
  } catch (XMLStreamException e) {
    LOGGER.error("Error processing DMN document", e);
    throw new DmnXMLException("Error processing DMN document", e);
  }

  return targetNameSpace;
}
Enter fullscreen mode Exit fullscreen mode

Let's dig into an unexpected break that reduces the number of iterations to one regardless of the content in xmlStreamReader. That may have been the intention, but then the devs should've used if. This is a very strange resource handling.

A small reference to git enables finding the previous version of the method (it has been renamed):

protected boolean isDMN12(XMLStreamReader xtr) {
    try {
        while (xtr.hasNext()) {
            try {
                xtr.next();
            } catch (Exception e) {
                LOGGER.debug("Error reading XML document", e);
                throw new DmnXMLException("Error reading XML", e);
            }
            return DMN_12_TARGET_NAMESPACE.equals(xtr.getNamespaceURI());
        }
        return false;
    } catch (XMLStreamException e) {
        LOGGER.error("Error processing DMN document", e);
        throw new DmnXMLException("Error processing DMN document", e);
    }
}
Enter fullscreen mode Exit fullscreen mode

This version uses return instead of break, and there's no condition again. The developers probably just decided to use while in this method because the xtr.hasNext() method is called.

Regardless of the original method concept, PVS-Studio correctly issues a warning, because such code raises too many questions:

V6037 An unconditional 'break' within a loop. DmnXMLConverter.java 187

Non-empty collection

protected void addRecipient(MimeMessage message, 
  Collection<String> recipients, 
  Message.RecipientType recipientType) {
  if (recipients == null || recipients.isEmpty()) {
    return;
  }
  Collection<String> newRecipients = recipients;
  Collection<String> forceRecipients = defaultsConfiguration.forceTo();
  if (forceRecipients != null && !forceRecipients.isEmpty()) {
    newRecipients = forceRecipients;
  }
  if (!newRecipients.isEmpty()) {
    for (String t : newRecipients) {
      try {
        message.addRecipient(recipientType, createInternetAddress(t));
      } catch (MessagingException e) {
        throw new FlowableMailException(....);
      }
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

This is a pointless check because newRecipients takes the value of either forceRecipients or recipients. Both are checked for isEmpty beforehand.

It's also pointless because the for (String t : newRecipients) loop wouldn't iterate over an empty collection anyway.

The PVS-Studio analyzer pointed out this redundant code:

Expression '!newRecipients.isEmpty()' is always true. JakartaMailFlowableMailClient.java 194

Field without value

public class CallbackData {
  protected String callbackId;
  protected String callbackType;
  protected String instanceId;
  protected String oldState;
  protected String newState;
  protected Map<String, Object> additionalData = null;

  public CallbackData(String callbackId, String callbackType, 
      String instanceId, String oldState, 
      String newState, Map<String, Object> additionalData) {
    this.callbackId = callbackId;
    this.callbackType = callbackType;
    this.instanceId = instanceId;
    this.oldState = oldState;
    this.newState = newState;
  }

  ....

  public Map<String, Object> getAdditionalData() {
    return additionalData;
  }

  public void setAdditionalData(Map<String, Object> additionalData) {
    this.additionalData = additionalData;
  }
}
Enter fullscreen mode Exit fullscreen mode

The PVS-Studio warning:

V6022 Parameter 'additionalData' is not used inside constructor body. CallbackData.java 29

This is a rather hypothetical case. A developer's use of the specified constructor can have unpleasant consequences, because if they pass additionalData to it, the value won't be written. A nasty NullPointerException will occur, for example, when using the getAdditionalData() method if it's not expected to return null.

Possible null

protected void endAllHistoricActivities(....) {
  ProcessEngineConfigurationImpl processEngineConfiguration = CommandContextUtil
      .getProcessEngineConfiguration();
  if (!processEngineConfiguration.getHistoryLevel()
        .isAtLeast(HistoryLevel.ACTIVITY)) {
    return;
  }

  List<HistoricActivityInstanceEntity> historicActivityInstances = ....;

  Clock clock = processEngineConfiguration.getClock();
  for (var historicActivityInstance : historicActivityInstances) {
    historicActivityInstance.markEnded(deleteReason, clock.getCurrentTime());

    // Fire event
    FlowableEventDispatcher eventDispatcher = null;
    if (processEngineConfiguration != null) {                        // <=
      eventDispatcher = processEngineConfiguration.getEventDispatcher();
    }
    if (eventDispatcher != null && eventDispatcher.isEnabled()) {
      seventDispatcher.dispatchEvent(....),
      processEngineConfiguration.getEngineCfgKey());                 // <=
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

CommandContextUtil.getProcessEngineConfiguration() can return null. So, since the null check is done in the code, it is expected. To be sure, let's look at this method:

public static ProcessEngineConfigurationImpl getProcessEngineConfiguration() {
    return getProcessEngineConfiguration(getCommandContext());
}

public static ProcessEngineConfigurationImpl
  getProcessEngineConfiguration(CommandContext commandContext) {
    if (commandContext != null) {
        return ....
    }
    return null;
}
Enter fullscreen mode Exit fullscreen mode

Can commandContext be null? We continue through the call chain:

public static CommandContext getCommandContext() {
    return Context.getCommandContext();
}

public static CommandContext getCommandContext() {
    Stack<CommandContext> stack = getStack(commandContextThreadLocal);
    if (stack.isEmpty()) {
        return null;
    }
    return stack.peek();
}
Enter fullscreen mode Exit fullscreen mode

If stack is empty, null is returned. It can be difficult to check whether we can get an empty collection, so let's stop here for now.

So, the call of_ processEngineConfiguration.getHistoryLevel()_ can result in NullPointerException.

The PVS-Studio warning:

V6060 The 'processEngineConfiguration' reference was utilized before it was verified against null. TerminateEndEventActivityBehavior.java 166, TerminateEndEventActivityBehavior.java 179

ClassCastException

public class IntegerDataObject extends ValuedDataObject {

  @Override
  public void setValue(Object value) {
    if (value instanceof String &&
      !StringUtils.isEmpty(((String) value).trim())) {
      this.value = Integer.valueOf(value.toString());
    } else if (value instanceof Number) {
      this.value = (Integer) value;                  // <=
    }
  }

  @Override
  public IntegerDataObject clone() {
    IntegerDataObject clone = new IntegerDataObject();
    clone.setValues(this);
    return clone;
  }
}
Enter fullscreen mode Exit fullscreen mode

Here we've got a strange check for Number _with a following casting to _Integer, as well as to Long and Double in other ValuedDataObject subclasses. Perhaps the developer expected this to have some effect, or, as with primitive types, there would be an appropriate conversion. However, the following program execution actually ends with a ClassCastException:

void main() {
    Number integer = 528;
    println((Float)integer);
}
Enter fullscreen mode Exit fullscreen mode

Here's the result:

Exception in thread "main" java.lang.ClassCastException: 
  class java.lang.Integer cannot be cast to class java.lang.Float 
  (java.lang.Integer and java.lang.Float 
    are in module java.base of loader 'bootstrap')
        at Exception.main(Exception.java:3)
Enter fullscreen mode Exit fullscreen mode

So, I think we need to reconsider what's going on in these methods.

The PVS-Studio warning:

  • V6042 The expression is checked for compatibility with type 'java.lang.Number' but is cast to type 'java.lang.Integer'. IntegerDataObject.java 27
  • V6042 The expression is checked for compatibility with type 'java.lang.Number' but is cast to type 'java.lang.Long'. LongDataObject.java 27
  • V6042 The expression is checked for compatibility with type 'java.lang.Number' but is cast to type 'java.lang.Double'. DoubleDataObject.java 27

Editing code style

@Override
protected DmnElement convertXMLToElement(XMLStreamReader xtr, 
  ConversionHelper conversionHelper) throws Exception {
  DecisionService decisionService = new DecisionService();
  decisionService.setId(xtr.getAttributeValue(null, ATTRIBUTE_ID));
  decisionService.setName(xtr.getAttributeValue(null, ATTRIBUTE_NAME));
  decisionService.setLabel(xtr.getAttributeValue(null, ATTRIBUTE_LABEL));

  boolean readyWithDecisionService = false;
  try {
    while (!readyWithDecisionService && xtr.hasNext()) {
      xtr.next();
      if (xtr.isStartElement() &&
          ELEMENT_OUTPUT_DECISION.equalsIgnoreCase(xtr.getLocalName())) {
        DmnElementReference ref = new DmnElementReference();
        ref.setHref(xtr.getAttributeValue(null, ATTRIBUTE_HREF));
        decisionService.addOutputDecision(ref);
      } if (xtr.isStartElement() &&
            ELEMENT_ENCAPSULATED_DECISION
              .equalsIgnoreCase(xtr.getLocalName())) {
        DmnElementReference ref = new DmnElementReference();
        ref.setHref(xtr.getAttributeValue(null, ATTRIBUTE_HREF));
        decisionService.addEncapsulatedDecision(ref);
      } if (xtr.isStartElement() && 
            ELEMENT_INPUT_DATA.equalsIgnoreCase(xtr.getLocalName())) {
        DmnElementReference ref = new DmnElementReference();
        ref.setHref(xtr.getAttributeValue(null, ATTRIBUTE_HREF));
        decisionService.addInputData(ref);
      } else if (xtr.isEndElement() && 
          getXMLElementName().equalsIgnoreCase(xtr.getLocalName())) {
        readyWithDecisionService = true;
      }
    }
  } catch (Exception e) {
    LOGGER.warn("Error parsing output entry", e);
  }
  return decisionService;
}
Enter fullscreen mode Exit fullscreen mode

I wanted to find an issue where the if statement instead of else if would cause the last else block execution even if one of the if blocks worked out. The case is similar to what I found in the GeoGebra project. However, everything here ends with else if, which means that nothing tragic can happen.

As a result, all we get is unpleasant code formatting. To improve readability, we can move if to the next lines or add else. The second option may be better, as it avoids performing multiple pointless CPU operations.

The PVS-Studio warning:

  • V6086 Suspicious code formatting. 'else' keyword is probably missing. DecisionServiceXMLConverter.java 45
  • V6086 Suspicious code formatting. 'else' keyword is probably missing. DecisionServiceXMLConverter.java 49

Redundant check

protected void leaveFlowNode(FlowNode flowNode) {
  ....

  // Determine which sequence flows can be used for leaving
  List<SequenceFlow> outgoingSequenceFlows = new ArrayList<>();
  for (SequenceFlow sequenceFlow : flowNode.getOutgoingFlows()) {

    String skipExpressionString = sequenceFlow.getSkipExpression();
    if (!SkipExpressionUtil.isSkipExpressionEnabled(
         skipExpressionString, sequenceFlow.getId(), 
         execution, commandContext)
      ) {

      if (!evaluateConditions                                        // <=
      || (evaluateConditions &&                                      // <=
          ConditionUtil.hasTrueCondition(sequenceFlow, execution) &&
         (defaultSequenceFlowId == null ||
          !defaultSequenceFlowId.equals(sequenceFlow.getId())))) {
        outgoingSequenceFlows.add(sequenceFlow);
      }

    } else if (flowNode.getOutgoingFlows().size() == 1 ||
        SkipExpressionUtil.shouldSkipFlowElement(
          skipExpressionString, sequenceFlow.getId(), execution, commandContext)
        ) {

      ....
      outgoingSequenceFlows.add(sequenceFlow);
    }
  }
  ....
}
Enter fullscreen mode Exit fullscreen mode

I found a lot of similar code fragments. The condition in if is extremely cumbersome and redundant as it is:

V6007 Expression 'evaluateConditions' is always true. TakeOutgoingSequenceFlowsOperation.java 220

Entering the second expression in the or statement occurs only if evaluateConditions is false. So, the expression on the left is meaningless in the (evaluateConditions && ...) check.

Conclusion

Let's wrap up our tour on potential project bugs for now. You can see that a lot of them were caused by simple carelessness. For the same reason, the devs didn't see them during code review. The size of a single PR can make you never want to open it again, so let's not blame the developers.

Static analysis tools, on the other hand, check code regardless of its size. PVS-Studio helped us find those problematic fragments. Although, that's not all the project has in store. I didn't have to study the code base for a long time to navigate through the project structure, I just ran the analyzer. While full integration into the development process will enable you to immediately detect errors in the code.

Also, you can try PVS-Studio for free :)

Top comments (0)