diff --git a/jbpm/jbpm-flow/src/main/java/org/jbpm/workflow/instance/node/CompositeNodeInstance.java b/jbpm/jbpm-flow/src/main/java/org/jbpm/workflow/instance/node/CompositeNodeInstance.java index 2612be7998b..ea9a6774629 100755 --- a/jbpm/jbpm-flow/src/main/java/org/jbpm/workflow/instance/node/CompositeNodeInstance.java +++ b/jbpm/jbpm-flow/src/main/java/org/jbpm/workflow/instance/node/CompositeNodeInstance.java @@ -54,7 +54,7 @@ /** * Runtime counterpart of a composite node. - * + * */ public class CompositeNodeInstance extends StateBasedNodeInstance implements NodeInstanceContainer, EventNodeInstanceInterface, EventBasedNodeInstanceInterface { @@ -190,7 +190,7 @@ public void cancel(CancelType cancelType) { @Override public void addNodeInstance(final NodeInstance nodeInstance) { if (nodeInstance.getStringId() == null) { - // assign new id only if it does not exist as it might already be set by marshalling + // assign new id only if it does not exist as it might already be set by marshalling // it's important to keep same ids of node instances as they might be references e.g. exclusive group ((NodeInstanceImpl) nodeInstance).setId(UUID.randomUUID().toString()); } @@ -204,7 +204,7 @@ public void removeNodeInstance(final NodeInstance nodeInstance) { @Override public Collection getNodeInstances() { - return new ArrayList<>(getNodeInstances(false)); + return Collections.unmodifiableCollection(nodeInstances); } @Override diff --git a/jbpm/jbpm-flow/src/main/java/org/jbpm/workflow/instance/node/ForEachNodeInstance.java b/jbpm/jbpm-flow/src/main/java/org/jbpm/workflow/instance/node/ForEachNodeInstance.java index 339f48d3f2a..9b17022e460 100755 --- a/jbpm/jbpm-flow/src/main/java/org/jbpm/workflow/instance/node/ForEachNodeInstance.java +++ b/jbpm/jbpm-flow/src/main/java/org/jbpm/workflow/instance/node/ForEachNodeInstance.java @@ -26,6 +26,8 @@ import java.util.List; import java.util.Map; import java.util.Optional; +import java.util.Set; +import java.util.stream.Collectors; import org.jbpm.process.core.ContextContainer; import org.jbpm.process.core.context.variable.VariableScope; @@ -58,6 +60,8 @@ public class ForEachNodeInstance extends CompositeContextNodeInstance { private static final long serialVersionUID = 510L; + private static final Set> NOT_SERIALIZABLE_CLASSES = Set.of(ForEachJoinNodeInstance.class); // using Arrays.asList to allow multiple exclusions + public static final String TEMP_OUTPUT_VAR = "foreach_output"; private int totalInstances; @@ -346,10 +350,25 @@ public ContextInstance getContextInstance(String contextId) { @Override public int getLevelForNode(String uniqueID) { - // always 1 for for each + // always 1 for each return 1; } + @Override + public Collection getSerializableNodeInstances() { + return getNodeInstances().stream().filter(ForEachNodeInstance::isSerializable).collect(Collectors.toUnmodifiableList()); + } + + /** + * Check if the given org.kie.api.runtime.process.NodeInstance is serializable. + * + * @param toCheck + * @return + */ + static boolean isSerializable(org.kie.api.runtime.process.NodeInstance toCheck) { + return !NOT_SERIALIZABLE_CLASSES.contains(toCheck.getClass()); + } + private class ForEachNodeInstanceResolverFactory extends NodeInstanceResolverFactory { private static final long serialVersionUID = -8856846610671009685L; diff --git a/jbpm/jbpm-flow/src/test/java/org/jbpm/workflow/instance/node/ForEachNodeInstanceTest.java b/jbpm/jbpm-flow/src/test/java/org/jbpm/workflow/instance/node/ForEachNodeInstanceTest.java new file mode 100644 index 00000000000..23194d30272 --- /dev/null +++ b/jbpm/jbpm-flow/src/test/java/org/jbpm/workflow/instance/node/ForEachNodeInstanceTest.java @@ -0,0 +1,75 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.jbpm.workflow.instance.node; + +import java.util.Collection; + +import org.junit.jupiter.api.Test; +import org.kie.api.runtime.process.NodeInstance; + +import static org.assertj.core.api.Assertions.assertThat; + +class ForEachNodeInstanceTest { + + @Test + void getNodeInstances() { + ForEachNodeInstance toTest = new ForEachNodeInstance(); + CompositeNodeInstance compositeNodeInstance = new CompositeNodeInstance(); + toTest.addNodeInstance(compositeNodeInstance); + Collection nodeInstances = toTest.getNodeInstances(); + assertThat(nodeInstances) + .isNotNull() + .hasSize(1) + .contains(compositeNodeInstance); + ForEachNodeInstance.ForEachJoinNodeInstance forEachJoinNodeInstance = toTest.new ForEachJoinNodeInstance(); + toTest.addNodeInstance(forEachJoinNodeInstance); + nodeInstances = toTest.getNodeInstances(); + assertThat(nodeInstances) + .isNotNull() + .hasSize(2) + .contains(compositeNodeInstance, forEachJoinNodeInstance); + } + + @Test + void getSerializableNodeInstances() { + ForEachNodeInstance toTest = new ForEachNodeInstance(); + CompositeNodeInstance compositeNodeInstance = new CompositeNodeInstance(); + toTest.addNodeInstance(compositeNodeInstance); + Collection serializableNodeInstances = toTest.getSerializableNodeInstances(); + assertThat(serializableNodeInstances) + .isNotNull() + .hasSize(1) + .contains(compositeNodeInstance); + ForEachNodeInstance.ForEachJoinNodeInstance forEachJoinNodeInstance = toTest.new ForEachJoinNodeInstance(); + toTest.addNodeInstance(forEachJoinNodeInstance); + serializableNodeInstances = toTest.getSerializableNodeInstances(); + assertThat(serializableNodeInstances) + .isNotNull() + .hasSize(1) + .contains(compositeNodeInstance); + } + + @Test + void isSerializable() { + assertThat(ForEachNodeInstance.isSerializable(new CompositeNodeInstance())).isTrue(); + assertThat(ForEachNodeInstance.isSerializable(new ForEachNodeInstance())).isTrue(); + assertThat(ForEachNodeInstance.isSerializable(new ForEachNodeInstance().new ForEachJoinNodeInstance())).isFalse(); + } + +} diff --git a/jbpm/process-serialization-protobuf/src/main/java/org/jbpm/flow/serialization/impl/ProtobufProcessInstanceWriter.java b/jbpm/process-serialization-protobuf/src/main/java/org/jbpm/flow/serialization/impl/ProtobufProcessInstanceWriter.java index b0f67430dbb..700d7abd422 100644 --- a/jbpm/process-serialization-protobuf/src/main/java/org/jbpm/flow/serialization/impl/ProtobufProcessInstanceWriter.java +++ b/jbpm/process-serialization-protobuf/src/main/java/org/jbpm/flow/serialization/impl/ProtobufProcessInstanceWriter.java @@ -175,7 +175,7 @@ private List buildSwimlaneContexts(Swimlane return contexts; } - private KogitoTypesProtobuf.WorkflowContext buildWorkflowContext(List nodeInstances, + protected KogitoTypesProtobuf.WorkflowContext buildWorkflowContext(List nodeInstances, List exclusiveGroupInstances, List> variables, List> iterationlevels) { @@ -248,8 +248,8 @@ public FieldDescriptor getContextField(GeneratedMessageV3.Builder builder) { return null; } - private WorkflowContext buildWorkflowContext(T nodeInstance) { - List nodeInstances = new ArrayList<>(nodeInstance.getNodeInstances()); + protected WorkflowContext buildWorkflowContext(T nodeInstance) { + List nodeInstances = new ArrayList<>(nodeInstance.getSerializableNodeInstances()); List exclusiveGroupInstances = nodeInstance.getContextInstances(ExclusiveGroup.EXCLUSIVE_GROUP); VariableScopeInstance variableScopeInstance = (VariableScopeInstance) nodeInstance.getContextInstance(VariableScope.VARIABLE_SCOPE); List> variables = (variableScopeInstance != null) ? new ArrayList<>(variableScopeInstance.getVariables().entrySet()) : Collections.emptyList(); diff --git a/jbpm/process-serialization-protobuf/src/test/java/org/jbpm/flow/serialization/impl/ProtobufProcessInstanceWriterTest.java b/jbpm/process-serialization-protobuf/src/test/java/org/jbpm/flow/serialization/impl/ProtobufProcessInstanceWriterTest.java new file mode 100644 index 00000000000..4d37ade30f2 --- /dev/null +++ b/jbpm/process-serialization-protobuf/src/test/java/org/jbpm/flow/serialization/impl/ProtobufProcessInstanceWriterTest.java @@ -0,0 +1,149 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.jbpm.flow.serialization.impl; + +import java.io.ByteArrayOutputStream; +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; +import java.util.Map; +import java.util.ServiceLoader; + +import org.jbpm.flow.serialization.MarshallerContextName; +import org.jbpm.flow.serialization.NodeInstanceWriter; +import org.jbpm.flow.serialization.ObjectMarshallerStrategyHelper; +import org.jbpm.process.core.context.variable.VariableScope; +import org.jbpm.process.instance.ContextInstance; +import org.jbpm.process.instance.context.variable.VariableScopeInstance; +import org.jbpm.ruleflow.core.WorkflowElementIdentifierFactory; +import org.jbpm.util.JbpmClassLoaderUtil; +import org.jbpm.workflow.core.impl.WorkflowProcessImpl; +import org.jbpm.workflow.instance.node.ForEachNodeInstance; +import org.jbpm.workflow.instance.node.HumanTaskNodeInstance; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.kie.api.definition.process.WorkflowElementIdentifier; +import org.kie.api.runtime.process.NodeInstance; +import org.kie.kogito.internal.process.runtime.KogitoProcessRuntime; +import org.kie.kogito.process.impl.AbstractProcess; +import org.mockito.ArgumentCaptor; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.spy; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +class ProtobufProcessInstanceWriterTest { + + private static AbstractProcess mockProcess; + private static NodeInstanceWriter[] nodeInstanceWriters; + private ProtobufProcessMarshallerWriteContext writeContext; + + @BeforeAll + static void setup() { + ServiceLoader writerLoader = ServiceLoader.load(NodeInstanceWriter.class, JbpmClassLoaderUtil.findClassLoader()); + int items = (int) writerLoader.stream().count(); + nodeInstanceWriters = writerLoader.stream().map(ServiceLoader.Provider::get).map(NodeInstanceWriter.class::cast).toArray(value -> new NodeInstanceWriter[items]); + mockProcess = getMockedProcess(); + } + + @BeforeEach + void init() { + writeContext = new ProtobufProcessMarshallerWriteContext(new ByteArrayOutputStream()); + writeContext.set(MarshallerContextName.OBJECT_MARSHALLING_STRATEGIES, ObjectMarshallerStrategyHelper.defaultStrategies()); + writeContext.set(MarshallerContextName.MARSHALLER_PROCESS, mockProcess); + writeContext.set(MarshallerContextName.MARSHALLER_NODE_INSTANCE_WRITER, nodeInstanceWriters); + } + + @SuppressWarnings("unchecked") + @Test + void buildWorkflowContext() { + ProtobufProcessInstanceWriter spiedProtobufProcessInstanceWriter = spy(new ProtobufProcessInstanceWriter(writeContext)); + ForEachNodeInstance nodeInstance = getNodeInstanceContainer(); + try { + spiedProtobufProcessInstanceWriter.buildWorkflowContext(nodeInstance); + } catch (Exception e) { + // expected due to partial instantiation + assertThat(e).isInstanceOf(NullPointerException.class); + ArgumentCaptor> nodeInstancesCapture = ArgumentCaptor.forClass(ArrayList.class); + ArgumentCaptor> exclusiveGroupInstancesCapture = ArgumentCaptor.forClass(ArrayList.class); + ArgumentCaptor>> variablesCapture = ArgumentCaptor.forClass(ArrayList.class); + ArgumentCaptor>> iterationlevelsCapture = ArgumentCaptor.forClass(ArrayList.class); + verify(spiedProtobufProcessInstanceWriter).buildWorkflowContext(nodeInstancesCapture.capture(), exclusiveGroupInstancesCapture.capture(), variablesCapture.capture(), + iterationlevelsCapture.capture()); + Collection expected = nodeInstance.getSerializableNodeInstances(); + List retrieved = nodeInstancesCapture.getValue(); + assertThat(retrieved).isNotNull().hasSize(expected.size()).allMatch(expected::contains); + } + } + + private ForEachNodeInstance getNodeInstanceContainer() { + String id = "NodeInstanceContainer"; + ForEachNodeInstance toReturn = new ForEachNodeInstance(); + toReturn.setId(id); + toReturn.setLevel(1); + toReturn.addNodeInstance(getNodeInstanceSerializable(id)); + toReturn.addNodeInstance(getNodeInstanceNotSerializable(id)); + toReturn.setNodeId(getWorkflowElementIdentifier(id)); + toReturn.setContextInstance(VariableScope.VARIABLE_SCOPE, new VariableScopeInstance()); + Collection nodeInstances = toReturn.getNodeInstances(); + assertThat(nodeInstances) + .isNotNull() + .hasSize(2) + .anyMatch(HumanTaskNodeInstance.class::isInstance) + .anyMatch(ForEachNodeInstance.ForEachJoinNodeInstance.class::isInstance); + Collection serializableNodeInstances = toReturn.getSerializableNodeInstances(); + assertThat(serializableNodeInstances) + .isNotNull() + .hasSize(1) + .allMatch(HumanTaskNodeInstance.class::isInstance); + return toReturn; + } + + private ForEachNodeInstance.ForEachJoinNodeInstance getNodeInstanceNotSerializable(String parent) { + String id = String.format("%s-%s", parent, "nestedNodeInstanceNotSerializable"); + ForEachNodeInstance.ForEachJoinNodeInstance toReturn = new ForEachNodeInstance().new ForEachJoinNodeInstance(); + toReturn.setId(id); + toReturn.setLevel(2); + return toReturn; + } + + private HumanTaskNodeInstance getNodeInstanceSerializable(String parent) { + String id = String.format("%s-%s", parent, "nestedNodeInstanceSerializable"); + HumanTaskNodeInstance toReturn = new HumanTaskNodeInstance(); + toReturn.setId(id); + toReturn.setNodeId(getWorkflowElementIdentifier(id)); + return toReturn; + } + + private WorkflowElementIdentifier getWorkflowElementIdentifier(String parent) { + String id = String.format("%s-%s", parent, "workflowElementIdentifier"); + return WorkflowElementIdentifierFactory.fromExternalFormat(id); + } + + private static AbstractProcess getMockedProcess() { + AbstractProcess toReturn = mock(AbstractProcess.class); + when(toReturn.getProcessRuntime()).thenReturn(mock(KogitoProcessRuntime.class)); + when(toReturn.get()).thenReturn(new WorkflowProcessImpl()); + return toReturn; + } + +}