From 1fff0d3dc8722c105cd4a9d593158cf3823d5dab Mon Sep 17 00:00:00 2001 From: Lucio Lelii Date: Fri, 24 Jul 2026 14:17:28 +0200 Subject: [PATCH] refactor: rename ExclusiveMergeBlock to BranchRejoinBlock "Merge" was misleading: the block never combines values, it only lets exactly one of several mutually-exclusive branches (an ExclusiveOR reconvergence, e.g. after a HumanDecisionBlock split) pass through as the single downstream value; two arriving values is an error, not something to combine. BranchRejoinBlock names what it actually does. There is still no block in the platform that genuinely merges/combines multiple parallel values into one - this rename doesn't add one, it just stops "merge" from implying it exists. Renamed throughout: block/config/factory/executor/activation-policy classes and their tests, the NodeVisualRole.MERGE -> BRANCH_REJOIN visual role and NodeTypeCapabilities.merge() -> branchRejoin() factory method, the six EXCLUSIVE_MERGE_* validation error codes -> BRANCH_REJOIN_*, the bundled flows.json (11 uses across the Jensen flows), and the two docs that reference it. Added a legacy-id fallback in DynamicBlockConfigurationTypeResolver (type id "ExclusiveMergeBlockConfiguration") and BlockTypes (typeName "ExclusiveMergeBlock"), mirroring the existing ChatHumanInteraction -> ChatInteraction precedent - found necessary the hard way: without it, any flow already persisted under the old type id fails to deserialize and brings the whole application down at startup, not just that one flow. Locked this in with LegacyBlockTypeIdTest. Co-Authored-By: Claude Sonnet 5 --- ...va => BranchRejoinBlockConfiguration.java} | 22 +++---- ...MergeInput.java => BranchRejoinInput.java} | 6 +- ...DynamicBlockConfigurationTypeResolver.java | 5 ++ ...ory.java => BranchRejoinBlockFactory.java} | 22 +++---- .../manager/blocks/types/BlockTypes.java | 4 ++ ...ckType.java => BranchRejoinBlockType.java} | 12 ++-- ...xecutor.java => BranchRejoinExecutor.java} | 20 +++--- ...java => BranchRejoinActivationPolicy.java} | 2 +- .../StepActivationPolicyRegistry.java | 8 +-- .../capabilities/NodeTypeCapabilities.java | 4 +- .../model/capabilities/NodeVisualRole.java | 2 +- .../flows/validation/FlowDataValidator.java | 22 +++---- .../flows/validation/ValidationErrorCode.java | 12 ++-- .../resources/workflow-editor-init/flows.json | 46 ++++++------- .../BlockConfigurationValidationTest.java | 32 ++++----- .../configurations/LegacyBlockTypeIdTest.java | 65 +++++++++++++++++++ ....java => BranchRejoinConcurrencyTest.java} | 24 +++---- .../ControlFlowGatewayIntegrationTest.java | 20 +++--- .../JsonDossierExecutionIntegrationTest.java | 18 ++--- .../ControlFlowBlockBiasCapabilitiesTest.java | 6 +- ...est.java => BranchRejoinExecutorTest.java} | 24 +++---- ... => BranchRejoinActivationPolicyTest.java} | 4 +- .../flows/JensenStructuredFlowsTest.java | 6 +- ... BranchRejoinStructureValidationTest.java} | 24 +++---- .../NodeTypeCapabilitiesIntegrationTest.java | 6 +- 25 files changed, 245 insertions(+), 171 deletions(-) rename src/main/java/it/cnr/isti/workflow/manager/blocks/configurations/{ExclusiveMergeBlockConfiguration.java => BranchRejoinBlockConfiguration.java} (73%) rename src/main/java/it/cnr/isti/workflow/manager/blocks/configurations/{ExclusiveMergeInput.java => BranchRejoinInput.java} (83%) rename src/main/java/it/cnr/isti/workflow/manager/blocks/factories/{ExclusiveMergeBlockFactory.java => BranchRejoinBlockFactory.java} (72%) rename src/main/java/it/cnr/isti/workflow/manager/blocks/types/{ExclusiveMergeBlockType.java => BranchRejoinBlockType.java} (69%) rename src/main/java/it/cnr/isti/workflow/manager/executions/executors/blocks/{ExclusiveMergeExecutor.java => BranchRejoinExecutor.java} (65%) rename src/main/java/it/cnr/isti/workflow/manager/executions/steps/activation/{ExclusiveMergeActivationPolicy.java => BranchRejoinActivationPolicy.java} (94%) create mode 100644 src/test/java/it/cnr/isti/workflow/manager/blocks/configurations/LegacyBlockTypeIdTest.java rename src/test/java/it/cnr/isti/workflow/manager/executions/{ExclusiveMergeConcurrencyTest.java => BranchRejoinConcurrencyTest.java} (92%) rename src/test/java/it/cnr/isti/workflow/manager/executions/executors/blocks/{ExclusiveMergeExecutorTest.java => BranchRejoinExecutorTest.java} (72%) rename src/test/java/it/cnr/isti/workflow/manager/executions/steps/activation/{ExclusiveMergeActivationPolicyTest.java => BranchRejoinActivationPolicyTest.java} (95%) rename src/test/java/it/cnr/isti/workflow/manager/flows/validation/{ExclusiveMergeStructureValidationTest.java => BranchRejoinStructureValidationTest.java} (83%) diff --git a/src/main/java/it/cnr/isti/workflow/manager/blocks/configurations/ExclusiveMergeBlockConfiguration.java b/src/main/java/it/cnr/isti/workflow/manager/blocks/configurations/BranchRejoinBlockConfiguration.java similarity index 73% rename from src/main/java/it/cnr/isti/workflow/manager/blocks/configurations/ExclusiveMergeBlockConfiguration.java rename to src/main/java/it/cnr/isti/workflow/manager/blocks/configurations/BranchRejoinBlockConfiguration.java index cba3d33..eda6981 100644 --- a/src/main/java/it/cnr/isti/workflow/manager/blocks/configurations/ExclusiveMergeBlockConfiguration.java +++ b/src/main/java/it/cnr/isti/workflow/manager/blocks/configurations/BranchRejoinBlockConfiguration.java @@ -5,7 +5,7 @@ import java.util.List; import com.fasterxml.jackson.annotation.JsonProperty; import com.fasterxml.jackson.annotation.JsonIgnore; -import it.cnr.isti.workflow.manager.blocks.types.ExclusiveMergeBlockType; +import it.cnr.isti.workflow.manager.blocks.types.BranchRejoinBlockType; import it.cnr.isti.workflow.manager.configurations.annotations.Structural; import it.cnr.isti.workflow.manager.configurations.annotations.UiOrder; import it.cnr.isti.workflow.manager.configurations.annotations.UiUniqueItemsBy; @@ -22,7 +22,7 @@ import lombok.NonNull; @Getter @EqualsAndHashCode(callSuper = true) @NoArgsConstructor(access = lombok.AccessLevel.PROTECTED) -public class ExclusiveMergeBlockConfiguration extends BlockConfiguration { +public class BranchRejoinBlockConfiguration extends BlockConfiguration { @UiOrder(30) @Structural @@ -30,7 +30,7 @@ public class ExclusiveMergeBlockConfiguration extends BlockConfiguration inputs = List.of(); + private List inputs = List.of(); @UiOrder(40) @Structural @@ -40,7 +40,7 @@ public class ExclusiveMergeBlockConfiguration extends BlockConfiguration inputs, + public BranchRejoinBlockConfiguration(@NonNull String name, List inputs, String outputName) { super(name); this.inputs = inputs == null ? List.of() : List.copyOf(inputs); @@ -48,14 +48,14 @@ public class ExclusiveMergeBlockConfiguration extends BlockConfiguration getBlockType() { - return ExclusiveMergeBlockType.class; + public Class getBlockType() { + return BranchRejoinBlockType.class; } - public static ExclusiveMergeBlockConfiguration empty() { - return ExclusiveMergeBlockConfiguration.builder() - .name(ExclusiveMergeBlockType.TYPE) - .inputs(List.of(new ExclusiveMergeInput("branchA"), new ExclusiveMergeInput("branchB"))) + public static BranchRejoinBlockConfiguration empty() { + return BranchRejoinBlockConfiguration.builder() + .name(BranchRejoinBlockType.TYPE) + .inputs(List.of(new BranchRejoinInput("branchA"), new BranchRejoinInput("branchB"))) .outputName("output") .build(); } @@ -71,7 +71,7 @@ public class ExclusiveMergeBlockConfiguration extends BlockConfiguration input != null && input.name() != null && !input.name().isBlank()) - .map(ExclusiveMergeInput::name) + .map(BranchRejoinInput::name) .distinct() .count(); return valid == inputs.size() && distinct == valid; diff --git a/src/main/java/it/cnr/isti/workflow/manager/blocks/configurations/ExclusiveMergeInput.java b/src/main/java/it/cnr/isti/workflow/manager/blocks/configurations/BranchRejoinInput.java similarity index 83% rename from src/main/java/it/cnr/isti/workflow/manager/blocks/configurations/ExclusiveMergeInput.java rename to src/main/java/it/cnr/isti/workflow/manager/blocks/configurations/BranchRejoinInput.java index 1205a04..e0e673c 100644 --- a/src/main/java/it/cnr/isti/workflow/manager/blocks/configurations/ExclusiveMergeInput.java +++ b/src/main/java/it/cnr/isti/workflow/manager/blocks/configurations/BranchRejoinInput.java @@ -6,16 +6,16 @@ import it.cnr.isti.workflow.manager.ios.IOType; import jakarta.validation.constraints.NotBlank; import jakarta.validation.constraints.Size; -public record ExclusiveMergeInput( +public record BranchRejoinInput( @NotBlank @Size(max = 64) String name, @Size(max = 255) String label, @JsonProperty(required = false) IOType type) { - public ExclusiveMergeInput(String name) { + public BranchRejoinInput(String name) { this(name, name, IOType.ANY); } - public ExclusiveMergeInput { + public BranchRejoinInput { label = label == null || label.isBlank() ? name : label; type = type == null ? IOType.ANY : type; } diff --git a/src/main/java/it/cnr/isti/workflow/manager/blocks/configurations/DynamicBlockConfigurationTypeResolver.java b/src/main/java/it/cnr/isti/workflow/manager/blocks/configurations/DynamicBlockConfigurationTypeResolver.java index aeb7172..cf4c450 100644 --- a/src/main/java/it/cnr/isti/workflow/manager/blocks/configurations/DynamicBlockConfigurationTypeResolver.java +++ b/src/main/java/it/cnr/isti/workflow/manager/blocks/configurations/DynamicBlockConfigurationTypeResolver.java @@ -17,6 +17,8 @@ public class DynamicBlockConfigurationTypeResolver extends TypeIdResolverBase { private static final String CHAT_INTERACTION_LEGACY_CONFIGURATION_ID = "ChatHumanInteractionBlockConfiguration"; private static final String CHAT_INTERACTION_CONFIGURATION_ID = "ChatInteractionBlockConfiguration"; + private static final String BRANCH_REJOIN_LEGACY_CONFIGURATION_ID = "ExclusiveMergeBlockConfiguration"; + private static final String BRANCH_REJOIN_CONFIGURATION_ID = "BranchRejoinBlockConfiguration"; private Map> idToClass = new HashMap<>(); private Map, String> classToId = new HashMap<>(); @@ -51,6 +53,9 @@ public class DynamicBlockConfigurationTypeResolver extends TypeIdResolverBase { if (CHAT_INTERACTION_LEGACY_CONFIGURATION_ID.equals(id)) { id = CHAT_INTERACTION_CONFIGURATION_ID; } + if (BRANCH_REJOIN_LEGACY_CONFIGURATION_ID.equals(id)) { + id = BRANCH_REJOIN_CONFIGURATION_ID; + } Class clazz = idToClass.get(id); if (clazz == null && id != null) { String normalized = id; diff --git a/src/main/java/it/cnr/isti/workflow/manager/blocks/factories/ExclusiveMergeBlockFactory.java b/src/main/java/it/cnr/isti/workflow/manager/blocks/factories/BranchRejoinBlockFactory.java similarity index 72% rename from src/main/java/it/cnr/isti/workflow/manager/blocks/factories/ExclusiveMergeBlockFactory.java rename to src/main/java/it/cnr/isti/workflow/manager/blocks/factories/BranchRejoinBlockFactory.java index 6e0978f..c439a21 100644 --- a/src/main/java/it/cnr/isti/workflow/manager/blocks/factories/ExclusiveMergeBlockFactory.java +++ b/src/main/java/it/cnr/isti/workflow/manager/blocks/factories/BranchRejoinBlockFactory.java @@ -8,24 +8,24 @@ import org.springframework.stereotype.Component; import it.cnr.isti.workflow.manager.blocks.Block; import it.cnr.isti.workflow.manager.blocks.IOCapability; import it.cnr.isti.workflow.manager.blocks.IOCapabilityType; -import it.cnr.isti.workflow.manager.blocks.configurations.ExclusiveMergeBlockConfiguration; -import it.cnr.isti.workflow.manager.blocks.types.ExclusiveMergeBlockType; +import it.cnr.isti.workflow.manager.blocks.configurations.BranchRejoinBlockConfiguration; +import it.cnr.isti.workflow.manager.blocks.types.BranchRejoinBlockType; import it.cnr.isti.workflow.manager.ios.IODescriptor; import it.cnr.isti.workflow.manager.ios.IOType; @Component -public class ExclusiveMergeBlockFactory - implements BlockFactory { +public class BranchRejoinBlockFactory + implements BlockFactory { public static final String DEFAULT_OUTPUT = "output"; private static final List ANY_CAPABILITY = List.of(new IOCapability(IOCapabilityType.ANY, false)); @Autowired - ExclusiveMergeBlockType blockType; + BranchRejoinBlockType blockType; @Override - public Block create(ExclusiveMergeBlockConfiguration configuration) { - Block.BlockBuilder builder = Block.builder() + public Block create(BranchRejoinBlockConfiguration configuration) { + Block.BlockBuilder builder = Block.builder() .specificConfiguration(configuration) .type(blockType); configuration.getInputs().forEach(input -> builder.input(IODescriptor.input( @@ -35,13 +35,13 @@ public class ExclusiveMergeBlockFactory } @Override - public Block createEmpty() { - return create(ExclusiveMergeBlockConfiguration.empty()); + public Block createEmpty() { + return create(BranchRejoinBlockConfiguration.empty()); } @Override - public Class getBlockType() { - return ExclusiveMergeBlockType.class; + public Class getBlockType() { + return BranchRejoinBlockType.class; } @Override diff --git a/src/main/java/it/cnr/isti/workflow/manager/blocks/types/BlockTypes.java b/src/main/java/it/cnr/isti/workflow/manager/blocks/types/BlockTypes.java index 0e9f0c7..76fab67 100644 --- a/src/main/java/it/cnr/isti/workflow/manager/blocks/types/BlockTypes.java +++ b/src/main/java/it/cnr/isti/workflow/manager/blocks/types/BlockTypes.java @@ -11,6 +11,7 @@ public class BlockTypes { private static Map blockTypes = new HashMap<>(); private static final String CHAT_INTERACTION_LEGACY_TYPE = "ChatHumanInteraction"; + private static final String BRANCH_REJOIN_LEGACY_TYPE = "ExclusiveMergeBlock"; public static BlockType get(String name) { @@ -21,6 +22,9 @@ public class BlockTypes { if (CHAT_INTERACTION_LEGACY_TYPE.equals(name)) { return blockTypes.get("ChatInteraction"); } + if (BRANCH_REJOIN_LEGACY_TYPE.equals(name)) { + return blockTypes.get("BranchRejoinBlock"); + } return null; } diff --git a/src/main/java/it/cnr/isti/workflow/manager/blocks/types/ExclusiveMergeBlockType.java b/src/main/java/it/cnr/isti/workflow/manager/blocks/types/BranchRejoinBlockType.java similarity index 69% rename from src/main/java/it/cnr/isti/workflow/manager/blocks/types/ExclusiveMergeBlockType.java rename to src/main/java/it/cnr/isti/workflow/manager/blocks/types/BranchRejoinBlockType.java index 0f1cd2a..55cc538 100644 --- a/src/main/java/it/cnr/isti/workflow/manager/blocks/types/ExclusiveMergeBlockType.java +++ b/src/main/java/it/cnr/isti/workflow/manager/blocks/types/BranchRejoinBlockType.java @@ -3,13 +3,13 @@ package it.cnr.isti.workflow.manager.blocks.types; import org.springframework.stereotype.Component; import it.cnr.isti.workflow.manager.blocks.configurations.BlockConfiguration; -import it.cnr.isti.workflow.manager.blocks.configurations.ExclusiveMergeBlockConfiguration; +import it.cnr.isti.workflow.manager.blocks.configurations.BranchRejoinBlockConfiguration; import it.cnr.isti.workflow.manager.flows.model.capabilities.NodeTypeCapabilities; -@Component(ExclusiveMergeBlockType.TYPE) -public class ExclusiveMergeBlockType implements BlockType { +@Component(BranchRejoinBlockType.TYPE) +public class BranchRejoinBlockType implements BlockType { - public static final String TYPE = "ExclusiveMergeBlock"; + public static final String TYPE = "BranchRejoinBlock"; @Override public String getName() { @@ -33,11 +33,11 @@ public class ExclusiveMergeBlockType implements BlockType { @Override public NodeTypeCapabilities getCapabilities() { - return NodeTypeCapabilities.merge(); + return NodeTypeCapabilities.branchRejoin(); } @Override public Class> getBlockConfigurationClass() { - return ExclusiveMergeBlockConfiguration.class; + return BranchRejoinBlockConfiguration.class; } } diff --git a/src/main/java/it/cnr/isti/workflow/manager/executions/executors/blocks/ExclusiveMergeExecutor.java b/src/main/java/it/cnr/isti/workflow/manager/executions/executors/blocks/BranchRejoinExecutor.java similarity index 65% rename from src/main/java/it/cnr/isti/workflow/manager/executions/executors/blocks/ExclusiveMergeExecutor.java rename to src/main/java/it/cnr/isti/workflow/manager/executions/executors/blocks/BranchRejoinExecutor.java index 62c3035..675c9cd 100644 --- a/src/main/java/it/cnr/isti/workflow/manager/executions/executors/blocks/ExclusiveMergeExecutor.java +++ b/src/main/java/it/cnr/isti/workflow/manager/executions/executors/blocks/BranchRejoinExecutor.java @@ -6,8 +6,8 @@ import java.util.Map; import org.springframework.stereotype.Component; import it.cnr.isti.workflow.manager.blocks.Block; -import it.cnr.isti.workflow.manager.blocks.configurations.ExclusiveMergeBlockConfiguration; -import it.cnr.isti.workflow.manager.blocks.types.ExclusiveMergeBlockType; +import it.cnr.isti.workflow.manager.blocks.configurations.BranchRejoinBlockConfiguration; +import it.cnr.isti.workflow.manager.blocks.types.BranchRejoinBlockType; import it.cnr.isti.workflow.manager.executions.ExecutionEventLogger; import it.cnr.isti.workflow.manager.executions.ExecutionVariableDescriptor; import it.cnr.isti.workflow.manager.executions.NodeExecutionException; @@ -15,30 +15,30 @@ import it.cnr.isti.workflow.manager.executions.steps.Input; import it.cnr.isti.workflow.manager.executions.steps.InputResolutionState; @Component -public class ExclusiveMergeExecutor implements BlockExecutor { +public class BranchRejoinExecutor implements BlockExecutor { @Override - public Map execute(Block block, List inputs, + public Map execute(Block block, List inputs, Map authorizations, Map executionVariables, Map executionVariableDescriptors, ExecutionEventLogger eventLogger) { List selected = inputs.stream() .filter(input -> input.getResolutionState() == InputResolutionState.VALUE) .toList(); if (inputs.stream().anyMatch(input -> input.getResolutionState() == InputResolutionState.UNAVAILABLE)) { - throw new NodeExecutionException("EXCLUSIVE_MERGE_INPUT_UNAVAILABLE", + throw new NodeExecutionException("BRANCH_REJOIN_INPUT_UNAVAILABLE", "Exclusive merge received an unavailable input"); } if (selected.size() != 1) { - throw new NodeExecutionException("EXCLUSIVE_MERGE_MULTIPLE_VALUES", + throw new NodeExecutionException("BRANCH_REJOIN_MULTIPLE_VALUES", "Exclusive merge expected exactly one selected input but received " + selected.size()); } - ExclusiveMergeBlockConfiguration configuration = - (ExclusiveMergeBlockConfiguration) block.getSpecificConfiguration(); + BranchRejoinBlockConfiguration configuration = + (BranchRejoinBlockConfiguration) block.getSpecificConfiguration(); return Map.of(configuration.getOutputName(), selected.getFirst().getValue()); } @Override - public Class getBlockType() { - return ExclusiveMergeBlockType.class; + public Class getBlockType() { + return BranchRejoinBlockType.class; } } diff --git a/src/main/java/it/cnr/isti/workflow/manager/executions/steps/activation/ExclusiveMergeActivationPolicy.java b/src/main/java/it/cnr/isti/workflow/manager/executions/steps/activation/BranchRejoinActivationPolicy.java similarity index 94% rename from src/main/java/it/cnr/isti/workflow/manager/executions/steps/activation/ExclusiveMergeActivationPolicy.java rename to src/main/java/it/cnr/isti/workflow/manager/executions/steps/activation/BranchRejoinActivationPolicy.java index 07ff4fc..4c49f74 100644 --- a/src/main/java/it/cnr/isti/workflow/manager/executions/steps/activation/ExclusiveMergeActivationPolicy.java +++ b/src/main/java/it/cnr/isti/workflow/manager/executions/steps/activation/BranchRejoinActivationPolicy.java @@ -6,7 +6,7 @@ import it.cnr.isti.workflow.manager.executions.steps.Input; import it.cnr.isti.workflow.manager.executions.steps.InputResolutionState; import it.cnr.isti.workflow.manager.executions.steps.StepSkipReason; -public final class ExclusiveMergeActivationPolicy implements StepActivationPolicy { +public final class BranchRejoinActivationPolicy implements StepActivationPolicy { @Override public StepActivationDecision evaluate(List inputs, boolean dependenciesSatisfied, diff --git a/src/main/java/it/cnr/isti/workflow/manager/executions/steps/activation/StepActivationPolicyRegistry.java b/src/main/java/it/cnr/isti/workflow/manager/executions/steps/activation/StepActivationPolicyRegistry.java index d57fa3b..9d413d5 100644 --- a/src/main/java/it/cnr/isti/workflow/manager/executions/steps/activation/StepActivationPolicyRegistry.java +++ b/src/main/java/it/cnr/isti/workflow/manager/executions/steps/activation/StepActivationPolicyRegistry.java @@ -1,13 +1,13 @@ package it.cnr.isti.workflow.manager.executions.steps.activation; import it.cnr.isti.workflow.manager.blocks.Block; -import it.cnr.isti.workflow.manager.blocks.types.ExclusiveMergeBlockType; +import it.cnr.isti.workflow.manager.blocks.types.BranchRejoinBlockType; import it.cnr.isti.workflow.manager.flows.model.FlowNode; public final class StepActivationPolicyRegistry { private static final StepActivationPolicy DEFAULT_POLICY = new AllRequiredActivationPolicy(); - private static final StepActivationPolicy EXCLUSIVE_MERGE_POLICY = new ExclusiveMergeActivationPolicy(); + private static final StepActivationPolicy BRANCH_REJOIN_POLICY = new BranchRejoinActivationPolicy(); private StepActivationPolicyRegistry() { } @@ -16,8 +16,8 @@ public final class StepActivationPolicyRegistry { if (node == null) { throw new IllegalArgumentException("Flow node is required to resolve its activation policy"); } - if (node instanceof Block block && block.getType() instanceof ExclusiveMergeBlockType) { - return EXCLUSIVE_MERGE_POLICY; + if (node instanceof Block block && block.getType() instanceof BranchRejoinBlockType) { + return BRANCH_REJOIN_POLICY; } return DEFAULT_POLICY; } diff --git a/src/main/java/it/cnr/isti/workflow/manager/flows/model/capabilities/NodeTypeCapabilities.java b/src/main/java/it/cnr/isti/workflow/manager/flows/model/capabilities/NodeTypeCapabilities.java index 2714159..d84dfbe 100644 --- a/src/main/java/it/cnr/isti/workflow/manager/flows/model/capabilities/NodeTypeCapabilities.java +++ b/src/main/java/it/cnr/isti/workflow/manager/flows/model/capabilities/NodeTypeCapabilities.java @@ -21,8 +21,8 @@ public record NodeTypeCapabilities( return new NodeTypeCapabilities(NodeVisualRole.DECISION, false, true, true, true, true, true); } - public static NodeTypeCapabilities merge() { - return new NodeTypeCapabilities(NodeVisualRole.MERGE, false, true, true, true, true, true); + public static NodeTypeCapabilities branchRejoin() { + return new NodeTypeCapabilities(NodeVisualRole.BRANCH_REJOIN, false, true, true, true, true, true); } public static NodeTypeCapabilities end() { diff --git a/src/main/java/it/cnr/isti/workflow/manager/flows/model/capabilities/NodeVisualRole.java b/src/main/java/it/cnr/isti/workflow/manager/flows/model/capabilities/NodeVisualRole.java index 016f01f..1ab018d 100644 --- a/src/main/java/it/cnr/isti/workflow/manager/flows/model/capabilities/NodeVisualRole.java +++ b/src/main/java/it/cnr/isti/workflow/manager/flows/model/capabilities/NodeVisualRole.java @@ -3,7 +3,7 @@ package it.cnr.isti.workflow.manager.flows.model.capabilities; public enum NodeVisualRole { ACTIVITY, DECISION, - MERGE, + BRANCH_REJOIN, END, CONTAINER } diff --git a/src/main/java/it/cnr/isti/workflow/manager/flows/validation/FlowDataValidator.java b/src/main/java/it/cnr/isti/workflow/manager/flows/validation/FlowDataValidator.java index e42da47..bb26b9a 100644 --- a/src/main/java/it/cnr/isti/workflow/manager/flows/validation/FlowDataValidator.java +++ b/src/main/java/it/cnr/isti/workflow/manager/flows/validation/FlowDataValidator.java @@ -14,12 +14,12 @@ import org.springframework.stereotype.Component; import it.cnr.isti.workflow.manager.blocks.Block; import it.cnr.isti.workflow.manager.blocks.configurations.BlockConfiguration; import it.cnr.isti.workflow.manager.blocks.configurations.EndBlockConfiguration; -import it.cnr.isti.workflow.manager.blocks.configurations.ExclusiveMergeBlockConfiguration; +import it.cnr.isti.workflow.manager.blocks.configurations.BranchRejoinBlockConfiguration; import it.cnr.isti.workflow.manager.blocks.factories.BlockFactory; import it.cnr.isti.workflow.manager.blocks.factories.ConditionalBlockFactory; import it.cnr.isti.workflow.manager.blocks.factories.SwitchBlockFactory; import it.cnr.isti.workflow.manager.blocks.types.ConditionalBlockType; -import it.cnr.isti.workflow.manager.blocks.types.ExclusiveMergeBlockType; +import it.cnr.isti.workflow.manager.blocks.types.BranchRejoinBlockType; import it.cnr.isti.workflow.manager.blocks.types.HumanDecisionBlockType; import it.cnr.isti.workflow.manager.blocks.types.SwitchBlockType; import it.cnr.isti.workflow.manager.containers.Container; @@ -99,7 +99,7 @@ public class FlowDataValidator implements ConstraintValidator mergedNodes = new LinkedHashSet<>(reachableByOutput.get(leftOutput)); mergedNodes.retainAll(reachableByOutput.get(rightOutput)); Set explicitlyMergedNodes = mergedNodes.stream() - .filter(nodeId -> isExclusiveMerge(nodesById.get(nodeId))) + .filter(nodeId -> isBranchRejoin(nodesById.get(nodeId))) .collect(java.util.stream.Collectors.toCollection(LinkedHashSet::new)); Set allowedSharedNodes = new LinkedHashSet<>(); for (String mergeNodeId : explicitlyMergedNodes) { @@ -585,21 +585,21 @@ public class FlowDataValidator implements ConstraintValidator block && ExclusiveMergeBlockType.TYPE.equals(block.getType().getName()); + private boolean isBranchRejoin(FlowNode node) { + return node instanceof Block block && BranchRejoinBlockType.TYPE.equals(block.getType().getName()); } - private void validateExclusiveMergeNodes(List nodes, List connections) { + private void validateBranchRejoinNodes(List nodes, List connections) { for (FlowNode node : nodes) { if (!(node instanceof Block block) - || !(block.getSpecificConfiguration() instanceof ExclusiveMergeBlockConfiguration configuration)) { + || !(block.getSpecificConfiguration() instanceof BranchRejoinBlockConfiguration configuration)) { continue; } List incoming = connections.stream() .filter(connection -> block.getId().equals(connection.getTargetId())) .toList(); if (configuration.getInputs().size() < 2 || incoming.size() < 2) { - throw validationError(error(ValidationErrorCode.EXCLUSIVE_MERGE_MIN_INPUTS, "block", block.getId(), + throw validationError(error(ValidationErrorCode.BRANCH_REJOIN_MIN_INPUTS, "block", block.getId(), "inputs", "Exclusive merge requires at least two connected inputs")); } for (IODescriptor input : block.getInputs()) { @@ -607,11 +607,11 @@ public class FlowDataValidator implements ConstraintValidator input.getName().equals(connection.getTargetName())) .count(); if (connectionCount == 0) { - throw validationError(error(ValidationErrorCode.EXCLUSIVE_MERGE_INPUT_NOT_CONNECTED, + throw validationError(error(ValidationErrorCode.BRANCH_REJOIN_INPUT_NOT_CONNECTED, "block", block.getId(), "inputs", "Exclusive merge input is not connected: " + input.getName())); } if (connectionCount > 1) { - throw validationError(error(ValidationErrorCode.EXCLUSIVE_MERGE_INPUT_MULTIPLE_CONNECTIONS, + throw validationError(error(ValidationErrorCode.BRANCH_REJOIN_INPUT_MULTIPLE_CONNECTIONS, "block", block.getId(), "inputs", "Exclusive merge input has multiple connections: " + input.getName())); } diff --git a/src/main/java/it/cnr/isti/workflow/manager/flows/validation/ValidationErrorCode.java b/src/main/java/it/cnr/isti/workflow/manager/flows/validation/ValidationErrorCode.java index edb5d85..cec47cc 100644 --- a/src/main/java/it/cnr/isti/workflow/manager/flows/validation/ValidationErrorCode.java +++ b/src/main/java/it/cnr/isti/workflow/manager/flows/validation/ValidationErrorCode.java @@ -66,12 +66,12 @@ public enum ValidationErrorCode { NODE_CANNOT_DEPEND_ON_OTHER_NODES, NODE_CANNOT_HAVE_DEPENDENT_NODES, EXCLUSIVE_BRANCH_MERGE, - EXCLUSIVE_MERGE_MIN_INPUTS, - EXCLUSIVE_MERGE_INPUT_NOT_CONNECTED, - EXCLUSIVE_MERGE_INPUT_MULTIPLE_CONNECTIONS, - EXCLUSIVE_MERGE_MULTIPLE_VALUES, - EXCLUSIVE_MERGE_INPUT_UNAVAILABLE, - UNSTRUCTURED_EXCLUSIVE_MERGE, + BRANCH_REJOIN_MIN_INPUTS, + BRANCH_REJOIN_INPUT_NOT_CONNECTED, + BRANCH_REJOIN_INPUT_MULTIPLE_CONNECTIONS, + BRANCH_REJOIN_MULTIPLE_VALUES, + BRANCH_REJOIN_INPUT_UNAVAILABLE, + UNSTRUCTURED_BRANCH_REJOIN, HUMAN_DECISION_INVALID_CHOICE, HUMAN_DECISION_RATIONALE_REQUIRED, END_OUTCOME_REQUIRED, diff --git a/src/main/resources/workflow-editor-init/flows.json b/src/main/resources/workflow-editor-init/flows.json index dbb5ad4..3375452 100644 --- a/src/main/resources/workflow-editor-init/flows.json +++ b/src/main/resources/workflow-editor-init/flows.json @@ -671,7 +671,7 @@ }, { "name": "Jensen Recruitment Process - Full Revised (Structured)", - "description": "Node-per-node structured port of the Full Revised Jensen recruitment UML, built with explicit HumanDecisionBlock gateways, ExclusiveMergeBlock rejoins, EndBlock outcomes and FlowLane swimlanes. Does not replace the existing macro-flow seed.", + "description": "Node-per-node structured port of the Full Revised Jensen recruitment UML, built with explicit HumanDecisionBlock gateways, BranchRejoinBlock rejoins, EndBlock outcomes and FlowLane swimlanes. Does not replace the existing macro-flow seed.", "owner": "testuser", "createdAt": "2026-07-22T10:00:00", "lastUpdateAt": "2026-07-22T10:00:00", @@ -1056,7 +1056,7 @@ ], "laneId": "2bc166dc-fe39-550d-a1cf-f4d7a6e7686c", "specificConfiguration": { - "type": "ExclusiveMergeBlockConfiguration", + "type": "BranchRejoinBlockConfiguration", "name": "merge-job-description", "inputs": [ { @@ -1072,7 +1072,7 @@ ], "outputName": "output" }, - "typeName": "ExclusiveMergeBlock" + "typeName": "BranchRejoinBlock" }, { "id": "287ca7f6-074d-5430-87fb-131a76acc91e", @@ -1224,7 +1224,7 @@ ], "laneId": "18a8d278-ebcd-53f2-b3e8-3a4fb4c4a83e", "specificConfiguration": { - "type": "ExclusiveMergeBlockConfiguration", + "type": "BranchRejoinBlockConfiguration", "name": "merge-job-advertisement", "inputs": [ { @@ -1240,7 +1240,7 @@ ], "outputName": "output" }, - "typeName": "ExclusiveMergeBlock" + "typeName": "BranchRejoinBlock" }, { "id": "fb6beef9-8de2-5b40-a169-33e8d0b5777f", @@ -1433,7 +1433,7 @@ ], "laneId": "8036d9f4-67bd-5198-942f-53bb41afa29c", "specificConfiguration": { - "type": "ExclusiveMergeBlockConfiguration", + "type": "BranchRejoinBlockConfiguration", "name": "merge-external-recruitment-start", "inputs": [ { @@ -1449,7 +1449,7 @@ ], "outputName": "output" }, - "typeName": "ExclusiveMergeBlock" + "typeName": "BranchRejoinBlock" }, { "id": "e67b2d57-359c-5710-b63a-6df27e3b6bd0", @@ -1682,7 +1682,7 @@ ], "laneId": "91bbb8f9-8882-5cc3-ba02-f41fc60e75b7", "specificConfiguration": { - "type": "ExclusiveMergeBlockConfiguration", + "type": "BranchRejoinBlockConfiguration", "name": "merge-application-submission-entry", "inputs": [ { @@ -1698,7 +1698,7 @@ ], "outputName": "output" }, - "typeName": "ExclusiveMergeBlock" + "typeName": "BranchRejoinBlock" }, { "id": "298edbd7-7bda-511c-b72e-93cf65468939", @@ -2006,7 +2006,7 @@ ], "laneId": "47b1f830-b4d1-5788-85a6-538193521f23", "specificConfiguration": { - "type": "ExclusiveMergeBlockConfiguration", + "type": "BranchRejoinBlockConfiguration", "name": "merge-post-assessment", "inputs": [ { @@ -2027,7 +2027,7 @@ ], "outputName": "output" }, - "typeName": "ExclusiveMergeBlock" + "typeName": "BranchRejoinBlock" }, { "id": "851978f2-057f-59bf-ad7f-e75e0f412005", @@ -3009,7 +3009,7 @@ ], "laneId": "df5fcc5f-c0cd-5592-bb50-7815a7f96d70", "specificConfiguration": { - "type": "ExclusiveMergeBlockConfiguration", + "type": "BranchRejoinBlockConfiguration", "name": "merge-job-description", "inputs": [ { @@ -3025,7 +3025,7 @@ ], "outputName": "output" }, - "typeName": "ExclusiveMergeBlock" + "typeName": "BranchRejoinBlock" }, { "id": "0159e7e4-9dba-5bde-b86a-e5a8cd97c005", @@ -3322,7 +3322,7 @@ ], "laneId": "906bae7d-fbe8-5dc1-a33a-91094e302d78", "specificConfiguration": { - "type": "ExclusiveMergeBlockConfiguration", + "type": "BranchRejoinBlockConfiguration", "name": "merge-advertisement-review-method", "inputs": [ { @@ -3338,7 +3338,7 @@ ], "outputName": "output" }, - "typeName": "ExclusiveMergeBlock" + "typeName": "BranchRejoinBlock" }, { "id": "1163d218-5c38-5ed2-96f4-3ba9901f8daa", @@ -3397,7 +3397,7 @@ ], "laneId": "906bae7d-fbe8-5dc1-a33a-91094e302d78", "specificConfiguration": { - "type": "ExclusiveMergeBlockConfiguration", + "type": "BranchRejoinBlockConfiguration", "name": "merge-job-advertisement", "inputs": [ { @@ -3413,7 +3413,7 @@ ], "outputName": "output" }, - "typeName": "ExclusiveMergeBlock" + "typeName": "BranchRejoinBlock" }, { "id": "d6aa9f6d-ae17-5b8b-b42b-f274768c227a", @@ -3686,7 +3686,7 @@ ], "laneId": "e3844222-ee0a-5bd8-8448-187364789eca", "specificConfiguration": { - "type": "ExclusiveMergeBlockConfiguration", + "type": "BranchRejoinBlockConfiguration", "name": "merge-external-recruitment-start", "inputs": [ { @@ -3702,7 +3702,7 @@ ], "outputName": "output" }, - "typeName": "ExclusiveMergeBlock" + "typeName": "BranchRejoinBlock" }, { "id": "188536f9-3f93-5b9e-9fa5-93223be338e4", @@ -3992,7 +3992,7 @@ ], "laneId": "efaffb7a-47b3-521c-b11f-0f9074212cdf", "specificConfiguration": { - "type": "ExclusiveMergeBlockConfiguration", + "type": "BranchRejoinBlockConfiguration", "name": "merge-application-submission-entry", "inputs": [ { @@ -4008,7 +4008,7 @@ ], "outputName": "output" }, - "typeName": "ExclusiveMergeBlock" + "typeName": "BranchRejoinBlock" }, { "id": "a3903787-c097-5602-acf0-b7d0012a62e4", @@ -4387,7 +4387,7 @@ ], "laneId": "595b4e4b-0c5d-504f-aa1c-b35e9f2a6a15", "specificConfiguration": { - "type": "ExclusiveMergeBlockConfiguration", + "type": "BranchRejoinBlockConfiguration", "name": "merge-post-assessment", "inputs": [ { @@ -4408,7 +4408,7 @@ ], "outputName": "output" }, - "typeName": "ExclusiveMergeBlock" + "typeName": "BranchRejoinBlock" }, { "id": "2ba31b21-8d5e-5a9e-8f75-140274fb6389", diff --git a/src/test/java/it/cnr/isti/workflow/manager/blocks/configurations/BlockConfigurationValidationTest.java b/src/test/java/it/cnr/isti/workflow/manager/blocks/configurations/BlockConfigurationValidationTest.java index 6354f34..2748798 100644 --- a/src/test/java/it/cnr/isti/workflow/manager/blocks/configurations/BlockConfigurationValidationTest.java +++ b/src/test/java/it/cnr/isti/workflow/manager/blocks/configurations/BlockConfigurationValidationTest.java @@ -17,7 +17,7 @@ import tools.jackson.databind.ObjectMapper; /** * Bean-validation coverage for the dynamic configurations introduced alongside - * ExclusiveMergeBlock, HumanDecisionBlock and EndBlock: cardinality bounds, uniqueness + * BranchRejoinBlock, HumanDecisionBlock and EndBlock: cardinality bounds, uniqueness * checks and required fields. */ class BlockConfigurationValidationTest { @@ -25,10 +25,10 @@ class BlockConfigurationValidationTest { private final Validator validator = Validation.buildDefaultValidatorFactory().getValidator(); @Test - void exclusiveMergeRejectsFewerThanTwoInputs() { - ExclusiveMergeBlockConfiguration configuration = ExclusiveMergeBlockConfiguration.builder() + void branchRejoinRejectsFewerThanTwoInputs() { + BranchRejoinBlockConfiguration configuration = BranchRejoinBlockConfiguration.builder() .name("merge") - .inputs(List.of(new ExclusiveMergeInput("only"))) + .inputs(List.of(new BranchRejoinInput("only"))) .outputName("output") .build(); @@ -36,11 +36,11 @@ class BlockConfigurationValidationTest { } @Test - void exclusiveMergeRejectsMoreThanTenInputs() { - List inputs = java.util.stream.IntStream.range(0, 11) - .mapToObj(index -> new ExclusiveMergeInput("branch" + index)) + void branchRejoinRejectsMoreThanTenInputs() { + List inputs = java.util.stream.IntStream.range(0, 11) + .mapToObj(index -> new BranchRejoinInput("branch" + index)) .toList(); - ExclusiveMergeBlockConfiguration configuration = ExclusiveMergeBlockConfiguration.builder() + BranchRejoinBlockConfiguration configuration = BranchRejoinBlockConfiguration.builder() .name("merge") .inputs(inputs) .outputName("output") @@ -50,23 +50,23 @@ class BlockConfigurationValidationTest { } @Test - void exclusiveMergeRejectsDuplicateInputNames() { - ExclusiveMergeBlockConfiguration configuration = ExclusiveMergeBlockConfiguration.builder() + void branchRejoinRejectsDuplicateInputNames() { + BranchRejoinBlockConfiguration configuration = BranchRejoinBlockConfiguration.builder() .name("merge") - .inputs(List.of(new ExclusiveMergeInput("branch", "A", IOType.ANY), - new ExclusiveMergeInput("branch", "B", IOType.ANY))) + .inputs(List.of(new BranchRejoinInput("branch", "A", IOType.ANY), + new BranchRejoinInput("branch", "B", IOType.ANY))) .outputName("output") .build(); - Set> violations = validator.validate(configuration); + Set> violations = validator.validate(configuration); assertTrue(violations.stream().anyMatch(v -> v.getMessage().contains("unique"))); } @Test - void exclusiveMergeAcceptsAWellFormedConfiguration() { - ExclusiveMergeBlockConfiguration configuration = ExclusiveMergeBlockConfiguration.builder() + void branchRejoinAcceptsAWellFormedConfiguration() { + BranchRejoinBlockConfiguration configuration = BranchRejoinBlockConfiguration.builder() .name("merge") - .inputs(List.of(new ExclusiveMergeInput("existing"), new ExclusiveMergeInput("new"))) + .inputs(List.of(new BranchRejoinInput("existing"), new BranchRejoinInput("new"))) .outputName("output") .build(); diff --git a/src/test/java/it/cnr/isti/workflow/manager/blocks/configurations/LegacyBlockTypeIdTest.java b/src/test/java/it/cnr/isti/workflow/manager/blocks/configurations/LegacyBlockTypeIdTest.java new file mode 100644 index 0000000..753e99e --- /dev/null +++ b/src/test/java/it/cnr/isti/workflow/manager/blocks/configurations/LegacyBlockTypeIdTest.java @@ -0,0 +1,65 @@ +package it.cnr.isti.workflow.manager.blocks.configurations; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertInstanceOf; +import static org.junit.jupiter.api.Assertions.assertNotNull; + +import org.junit.jupiter.api.Test; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.test.context.TestPropertySource; + +import it.cnr.isti.workflow.manager.app.ObjectMapperHolder; +import it.cnr.isti.workflow.manager.blocks.Block; +import it.cnr.isti.workflow.manager.blocks.types.BranchRejoinBlockType; + +/** + * A renamed block type (e.g. ExclusiveMergeBlock -> BranchRejoinBlock) must + * still deserialize flows that were persisted under the old type id before + * the rename - flow_data in the database isn't migrated automatically. + * DynamicBlockConfigurationTypeResolver/BlockTypes carry a legacy-id + * fallback for exactly this; this test locks in the current one (found the + * hard way: renaming without it breaks application startup on any + * already-persisted flow using the old id). + */ +@SpringBootTest +@TestPropertySource(locations = "classpath:test.properties") +class LegacyBlockTypeIdTest { + + private static final String LEGACY_BLOCK_JSON = """ + { + "id": "legacy-merge-block", + "name": "merge", + "inputs": [ + {"name": "a", "type": "ANY", "multiple": false}, + {"name": "b", "type": "ANY", "multiple": false} + ], + "outputs": [{"name": "output", "type": "ANY", "multiple": false}], + "specificConfiguration": { + "type": "ExclusiveMergeBlockConfiguration", + "name": "merge", + "inputs": [ + {"name": "a", "label": "A", "type": "ANY"}, + {"name": "b", "label": "B", "type": "ANY"} + ], + "outputName": "output" + }, + "typeName": "ExclusiveMergeBlock" + } + """; + + @Test + void legacyExclusiveMergeConfigurationIdResolvesToBranchRejoin() throws Exception { + Block block = ObjectMapperHolder.mapper.readValue(LEGACY_BLOCK_JSON, Block.class); + + assertNotNull(block.getSpecificConfiguration()); + assertInstanceOf(BranchRejoinBlockConfiguration.class, block.getSpecificConfiguration()); + } + + @Test + void legacyExclusiveMergeTypeNameResolvesToBranchRejoinBlockType() throws Exception { + Block block = ObjectMapperHolder.mapper.readValue(LEGACY_BLOCK_JSON, Block.class); + + assertNotNull(block.getType()); + assertEquals(BranchRejoinBlockType.TYPE, block.getType().getName()); + } +} diff --git a/src/test/java/it/cnr/isti/workflow/manager/executions/ExclusiveMergeConcurrencyTest.java b/src/test/java/it/cnr/isti/workflow/manager/executions/BranchRejoinConcurrencyTest.java similarity index 92% rename from src/test/java/it/cnr/isti/workflow/manager/executions/ExclusiveMergeConcurrencyTest.java rename to src/test/java/it/cnr/isti/workflow/manager/executions/BranchRejoinConcurrencyTest.java index 8b3be69..744c1a2 100644 --- a/src/test/java/it/cnr/isti/workflow/manager/executions/ExclusiveMergeConcurrencyTest.java +++ b/src/test/java/it/cnr/isti/workflow/manager/executions/BranchRejoinConcurrencyTest.java @@ -22,28 +22,28 @@ import org.springframework.test.context.TestPropertySource; import it.cnr.isti.workflow.manager.blocks.Block; import it.cnr.isti.workflow.manager.blocks.configurations.ConditionalBlockConfiguration; import it.cnr.isti.workflow.manager.blocks.configurations.EndBlockConfiguration; -import it.cnr.isti.workflow.manager.blocks.configurations.ExclusiveMergeBlockConfiguration; -import it.cnr.isti.workflow.manager.blocks.configurations.ExclusiveMergeInput; +import it.cnr.isti.workflow.manager.blocks.configurations.BranchRejoinBlockConfiguration; +import it.cnr.isti.workflow.manager.blocks.configurations.BranchRejoinInput; import it.cnr.isti.workflow.manager.blocks.factories.ConditionalBlockFactory; import it.cnr.isti.workflow.manager.blocks.factories.EndBlockFactory; -import it.cnr.isti.workflow.manager.blocks.factories.ExclusiveMergeBlockFactory; +import it.cnr.isti.workflow.manager.blocks.factories.BranchRejoinBlockFactory; import it.cnr.isti.workflow.manager.blocks.types.ConditionalBlockType; import it.cnr.isti.workflow.manager.blocks.types.EndBlockType; -import it.cnr.isti.workflow.manager.blocks.types.ExclusiveMergeBlockType; +import it.cnr.isti.workflow.manager.blocks.types.BranchRejoinBlockType; import it.cnr.isti.workflow.manager.executions.steps.StepStatus; import it.cnr.isti.workflow.manager.flows.model.Connection; import it.cnr.isti.workflow.manager.flows.model.FlowData; import it.cnr.isti.workflow.manager.ios.IOType; /** - * Concurrency coverage for ExclusiveMergeBlock scheduling: repeated split+merge executions + * Concurrency coverage for BranchRejoinBlock scheduling: repeated split+merge executions * under the engine's own asynchronous executors must never double-schedule the merge, must * only complete it once every input is resolved, and must stay consistent under cancel and * under concurrent snapshot reads. */ @SpringBootTest @TestPropertySource(locations = "classpath:test.properties") -class ExclusiveMergeConcurrencyTest { +class BranchRejoinConcurrencyTest { @Autowired ExecutionsService executionsService; @@ -52,7 +52,7 @@ class ExclusiveMergeConcurrencyTest { ConditionalBlockFactory conditionalBlockFactory; @Autowired - ExclusiveMergeBlockFactory exclusiveMergeBlockFactory; + BranchRejoinBlockFactory branchRejoinBlockFactory; @Autowired EndBlockFactory endBlockFactory; @@ -98,7 +98,7 @@ class ExclusiveMergeConcurrencyTest { .filter(block -> block.getType() instanceof ConditionalBlockType) .findFirst().orElseThrow(); Block merge = flow.getBlocks().stream() - .filter(block -> block.getType() instanceof ExclusiveMergeBlockType) + .filter(block -> block.getType() instanceof BranchRejoinBlockType) .findFirst().orElseThrow(); ExecutionObject execution = executionsService.createExecution("Concurrent merge - " + decision, flow); @@ -212,12 +212,12 @@ class ExclusiveMergeConcurrencyTest { .condition("${{decision}} == 'yes'") .outputTemplate("${{payload}}") .build()); - Block merge = exclusiveMergeBlockFactory.create( - ExclusiveMergeBlockConfiguration.builder() + Block merge = branchRejoinBlockFactory.create( + BranchRejoinBlockConfiguration.builder() .name("Eligibility merge") .inputs(List.of( - new ExclusiveMergeInput("accepted", "Accepted", IOType.TEXT), - new ExclusiveMergeInput("rejected", "Rejected", IOType.TEXT))) + new BranchRejoinInput("accepted", "Accepted", IOType.TEXT), + new BranchRejoinInput("rejected", "Rejected", IOType.TEXT))) .outputName("result") .build()); Block end = endBlockFactory.create(EndBlockConfiguration.builder() diff --git a/src/test/java/it/cnr/isti/workflow/manager/executions/ControlFlowGatewayIntegrationTest.java b/src/test/java/it/cnr/isti/workflow/manager/executions/ControlFlowGatewayIntegrationTest.java index 975bbec..0f58864 100644 --- a/src/test/java/it/cnr/isti/workflow/manager/executions/ControlFlowGatewayIntegrationTest.java +++ b/src/test/java/it/cnr/isti/workflow/manager/executions/ControlFlowGatewayIntegrationTest.java @@ -16,17 +16,17 @@ import org.springframework.test.context.TestPropertySource; import it.cnr.isti.workflow.manager.blocks.Block; import it.cnr.isti.workflow.manager.blocks.configurations.ConditionalBlockConfiguration; import it.cnr.isti.workflow.manager.blocks.configurations.EndBlockConfiguration; -import it.cnr.isti.workflow.manager.blocks.configurations.ExclusiveMergeBlockConfiguration; -import it.cnr.isti.workflow.manager.blocks.configurations.ExclusiveMergeInput; +import it.cnr.isti.workflow.manager.blocks.configurations.BranchRejoinBlockConfiguration; +import it.cnr.isti.workflow.manager.blocks.configurations.BranchRejoinInput; import it.cnr.isti.workflow.manager.blocks.configurations.HumanDecisionBlockConfiguration; import it.cnr.isti.workflow.manager.blocks.configurations.HumanDecisionOption; import it.cnr.isti.workflow.manager.blocks.factories.ConditionalBlockFactory; import it.cnr.isti.workflow.manager.blocks.factories.EndBlockFactory; -import it.cnr.isti.workflow.manager.blocks.factories.ExclusiveMergeBlockFactory; +import it.cnr.isti.workflow.manager.blocks.factories.BranchRejoinBlockFactory; import it.cnr.isti.workflow.manager.blocks.factories.HumanDecisionBlockFactory; import it.cnr.isti.workflow.manager.blocks.types.ConditionalBlockType; import it.cnr.isti.workflow.manager.blocks.types.EndBlockType; -import it.cnr.isti.workflow.manager.blocks.types.ExclusiveMergeBlockType; +import it.cnr.isti.workflow.manager.blocks.types.BranchRejoinBlockType; import it.cnr.isti.workflow.manager.blocks.types.HumanDecisionBlockType; import it.cnr.isti.workflow.manager.executions.bias.BiasExecutionContext; import it.cnr.isti.workflow.manager.executions.bias.BiasExecutionMode; @@ -54,7 +54,7 @@ class ControlFlowGatewayIntegrationTest { ConditionalBlockFactory conditionalBlockFactory; @Autowired - ExclusiveMergeBlockFactory exclusiveMergeBlockFactory; + BranchRejoinBlockFactory branchRejoinBlockFactory; @Autowired HumanDecisionBlockFactory humanDecisionBlockFactory; @@ -69,12 +69,12 @@ class ControlFlowGatewayIntegrationTest { .condition("${{decision}} == 'yes'") .outputTemplate("${{payload}}") .build()); - Block merge = exclusiveMergeBlockFactory.create( - ExclusiveMergeBlockConfiguration.builder() + Block merge = branchRejoinBlockFactory.create( + BranchRejoinBlockConfiguration.builder() .name("Eligibility merge") .inputs(List.of( - new ExclusiveMergeInput("accepted", "Accepted", IOType.TEXT), - new ExclusiveMergeInput("rejected", "Rejected", IOType.TEXT))) + new BranchRejoinInput("accepted", "Accepted", IOType.TEXT), + new BranchRejoinInput("rejected", "Rejected", IOType.TEXT))) .outputName("result") .build()); Block end = endBlockFactory.create(end("DONE", "Completed")); @@ -88,7 +88,7 @@ class ControlFlowGatewayIntegrationTest { .build(); for (String decision : List.of("yes", "no")) { - ExecutionObject execution = executionsService.createExecution("Explicit exclusive merge", flow); + ExecutionObject execution = executionsService.createExecution("Explicit branch rejoin", flow); executionsService.prepareInput(execution.getId(), router.getId(), "decision", decision); executionsService.prepareInput(execution.getId(), router.getId(), "payload", "case-42"); executionsService.startExecution(execution.getId()); diff --git a/src/test/java/it/cnr/isti/workflow/manager/executions/JsonDossierExecutionIntegrationTest.java b/src/test/java/it/cnr/isti/workflow/manager/executions/JsonDossierExecutionIntegrationTest.java index f0dfe1c..4440a09 100644 --- a/src/test/java/it/cnr/isti/workflow/manager/executions/JsonDossierExecutionIntegrationTest.java +++ b/src/test/java/it/cnr/isti/workflow/manager/executions/JsonDossierExecutionIntegrationTest.java @@ -17,15 +17,15 @@ import tools.jackson.databind.JsonNode; import it.cnr.isti.workflow.manager.blocks.Block; import it.cnr.isti.workflow.manager.blocks.configurations.EndBlockConfiguration; -import it.cnr.isti.workflow.manager.blocks.configurations.ExclusiveMergeBlockConfiguration; -import it.cnr.isti.workflow.manager.blocks.configurations.ExclusiveMergeInput; +import it.cnr.isti.workflow.manager.blocks.configurations.BranchRejoinBlockConfiguration; +import it.cnr.isti.workflow.manager.blocks.configurations.BranchRejoinInput; import it.cnr.isti.workflow.manager.blocks.configurations.HumanDecisionBlockConfiguration; import it.cnr.isti.workflow.manager.blocks.configurations.HumanDecisionOption; import it.cnr.isti.workflow.manager.blocks.factories.EndBlockFactory; -import it.cnr.isti.workflow.manager.blocks.factories.ExclusiveMergeBlockFactory; +import it.cnr.isti.workflow.manager.blocks.factories.BranchRejoinBlockFactory; import it.cnr.isti.workflow.manager.blocks.factories.HumanDecisionBlockFactory; import it.cnr.isti.workflow.manager.blocks.types.EndBlockType; -import it.cnr.isti.workflow.manager.blocks.types.ExclusiveMergeBlockType; +import it.cnr.isti.workflow.manager.blocks.types.BranchRejoinBlockType; import it.cnr.isti.workflow.manager.blocks.types.HumanDecisionBlockType; import it.cnr.isti.workflow.manager.executions.steps.InputResolutionState; import it.cnr.isti.workflow.manager.flows.model.Connection; @@ -48,7 +48,7 @@ class JsonDossierExecutionIntegrationTest { HumanDecisionBlockFactory humanDecisionBlockFactory; @Autowired - ExclusiveMergeBlockFactory exclusiveMergeBlockFactory; + BranchRejoinBlockFactory branchRejoinBlockFactory; @Autowired EndBlockFactory endBlockFactory; @@ -62,12 +62,12 @@ class JsonDossierExecutionIntegrationTest { new HumanDecisionOption("existing", "Existing position"), new HumanDecisionOption("new", "New position"))) .build()); - Block merge = exclusiveMergeBlockFactory.create( - ExclusiveMergeBlockConfiguration.builder() + Block merge = branchRejoinBlockFactory.create( + BranchRejoinBlockConfiguration.builder() .name("merge-existing-position") .inputs(List.of( - new ExclusiveMergeInput("existing", "Existing position", IOType.JSON), - new ExclusiveMergeInput("new", "New position", IOType.JSON))) + new BranchRejoinInput("existing", "Existing position", IOType.JSON), + new BranchRejoinInput("new", "New position", IOType.JSON))) .outputName("output") .build()); Block end = endBlockFactory.create(EndBlockConfiguration.builder() diff --git a/src/test/java/it/cnr/isti/workflow/manager/executions/bias/ControlFlowBlockBiasCapabilitiesTest.java b/src/test/java/it/cnr/isti/workflow/manager/executions/bias/ControlFlowBlockBiasCapabilitiesTest.java index cea246c..bce8dfe 100644 --- a/src/test/java/it/cnr/isti/workflow/manager/executions/bias/ControlFlowBlockBiasCapabilitiesTest.java +++ b/src/test/java/it/cnr/isti/workflow/manager/executions/bias/ControlFlowBlockBiasCapabilitiesTest.java @@ -9,7 +9,7 @@ import org.springframework.boot.test.context.SpringBootTest; import org.springframework.test.context.TestPropertySource; import it.cnr.isti.workflow.manager.blocks.types.EndBlockType; -import it.cnr.isti.workflow.manager.blocks.types.ExclusiveMergeBlockType; +import it.cnr.isti.workflow.manager.blocks.types.BranchRejoinBlockType; import it.cnr.isti.workflow.manager.blocks.types.HumanDecisionBlockType; import it.cnr.isti.workflow.manager.controllers.BiasCapabilitiesController; import it.cnr.isti.workflow.manager.flows.model.bias.BiasActivationMode; @@ -34,8 +34,8 @@ class ControlFlowBlockBiasCapabilitiesTest { } @Test - void exclusiveMergeBlockDoesNotSupportRoutingOverride() { - var capabilities = capabilitiesController.getCapabilities(ExclusiveMergeBlockType.TYPE); + void branchRejoinBlockDoesNotSupportRoutingOverride() { + var capabilities = capabilitiesController.getCapabilities(BranchRejoinBlockType.TYPE); assertFalse(capabilities.activationModes().contains(BiasActivationMode.ROUTING_OVERRIDE)); } diff --git a/src/test/java/it/cnr/isti/workflow/manager/executions/executors/blocks/ExclusiveMergeExecutorTest.java b/src/test/java/it/cnr/isti/workflow/manager/executions/executors/blocks/BranchRejoinExecutorTest.java similarity index 72% rename from src/test/java/it/cnr/isti/workflow/manager/executions/executors/blocks/ExclusiveMergeExecutorTest.java rename to src/test/java/it/cnr/isti/workflow/manager/executions/executors/blocks/BranchRejoinExecutorTest.java index d74e429..797c858 100644 --- a/src/test/java/it/cnr/isti/workflow/manager/executions/executors/blocks/ExclusiveMergeExecutorTest.java +++ b/src/test/java/it/cnr/isti/workflow/manager/executions/executors/blocks/BranchRejoinExecutorTest.java @@ -9,17 +9,17 @@ import java.util.Map; import org.junit.jupiter.api.Test; import it.cnr.isti.workflow.manager.blocks.Block; -import it.cnr.isti.workflow.manager.blocks.configurations.ExclusiveMergeBlockConfiguration; -import it.cnr.isti.workflow.manager.blocks.configurations.ExclusiveMergeInput; -import it.cnr.isti.workflow.manager.blocks.types.ExclusiveMergeBlockType; +import it.cnr.isti.workflow.manager.blocks.configurations.BranchRejoinBlockConfiguration; +import it.cnr.isti.workflow.manager.blocks.configurations.BranchRejoinInput; +import it.cnr.isti.workflow.manager.blocks.types.BranchRejoinBlockType; import it.cnr.isti.workflow.manager.executions.NodeExecutionException; import it.cnr.isti.workflow.manager.executions.steps.Input; import it.cnr.isti.workflow.manager.ios.IODescriptor; import it.cnr.isti.workflow.manager.ios.IOType; -class ExclusiveMergeExecutorTest { +class BranchRejoinExecutorTest { - private final ExclusiveMergeExecutor executor = new ExclusiveMergeExecutor(); + private final BranchRejoinExecutor executor = new BranchRejoinExecutor(); @Test void propagatesTheSingleSelectedValueUnderTheConfiguredOutputName() { @@ -41,7 +41,7 @@ class ExclusiveMergeExecutorTest { NodeExecutionException exception = assertThrows(NodeExecutionException.class, () -> executor.execute(block(), List.of(existing, newValue), Map.of(), Map.of(), Map.of(), null)); - assertEquals("EXCLUSIVE_MERGE_MULTIPLE_VALUES", exception.getErrorCode()); + assertEquals("BRANCH_REJOIN_MULTIPLE_VALUES", exception.getErrorCode()); } @Test @@ -53,18 +53,18 @@ class ExclusiveMergeExecutorTest { NodeExecutionException exception = assertThrows(NodeExecutionException.class, () -> executor.execute(block(), List.of(existing, unavailable), Map.of(), Map.of(), Map.of(), null)); - assertEquals("EXCLUSIVE_MERGE_INPUT_UNAVAILABLE", exception.getErrorCode()); + assertEquals("BRANCH_REJOIN_INPUT_UNAVAILABLE", exception.getErrorCode()); } - private Block block() { - ExclusiveMergeBlockConfiguration configuration = ExclusiveMergeBlockConfiguration.builder() + private Block block() { + BranchRejoinBlockConfiguration configuration = BranchRejoinBlockConfiguration.builder() .name("merge") - .inputs(List.of(new ExclusiveMergeInput("existing"), new ExclusiveMergeInput("new"))) + .inputs(List.of(new BranchRejoinInput("existing"), new BranchRejoinInput("new"))) .outputName("output") .build(); - return Block.builder() + return Block.builder() .specificConfiguration(configuration) - .type(new ExclusiveMergeBlockType()) + .type(new BranchRejoinBlockType()) .build(); } } diff --git a/src/test/java/it/cnr/isti/workflow/manager/executions/steps/activation/ExclusiveMergeActivationPolicyTest.java b/src/test/java/it/cnr/isti/workflow/manager/executions/steps/activation/BranchRejoinActivationPolicyTest.java similarity index 95% rename from src/test/java/it/cnr/isti/workflow/manager/executions/steps/activation/ExclusiveMergeActivationPolicyTest.java rename to src/test/java/it/cnr/isti/workflow/manager/executions/steps/activation/BranchRejoinActivationPolicyTest.java index faed6e9..b7ed072 100644 --- a/src/test/java/it/cnr/isti/workflow/manager/executions/steps/activation/ExclusiveMergeActivationPolicyTest.java +++ b/src/test/java/it/cnr/isti/workflow/manager/executions/steps/activation/BranchRejoinActivationPolicyTest.java @@ -11,9 +11,9 @@ import it.cnr.isti.workflow.manager.executions.steps.StepSkipReason; import it.cnr.isti.workflow.manager.ios.IODescriptor; import it.cnr.isti.workflow.manager.ios.IOType; -class ExclusiveMergeActivationPolicyTest { +class BranchRejoinActivationPolicyTest { - private final ExclusiveMergeActivationPolicy policy = new ExclusiveMergeActivationPolicy(); + private final BranchRejoinActivationPolicy policy = new BranchRejoinActivationPolicy(); private final IODescriptor existingInput = IODescriptor.of("existing", IOType.TEXT); private final IODescriptor newInput = IODescriptor.of("new", IOType.TEXT); diff --git a/src/test/java/it/cnr/isti/workflow/manager/flows/JensenStructuredFlowsTest.java b/src/test/java/it/cnr/isti/workflow/manager/flows/JensenStructuredFlowsTest.java index 11c59c6..942855a 100644 --- a/src/test/java/it/cnr/isti/workflow/manager/flows/JensenStructuredFlowsTest.java +++ b/src/test/java/it/cnr/isti/workflow/manager/flows/JensenStructuredFlowsTest.java @@ -17,7 +17,7 @@ import org.springframework.test.context.TestPropertySource; import it.cnr.isti.workflow.manager.app.ObjectMapperHolder; import it.cnr.isti.workflow.manager.blocks.Block; import it.cnr.isti.workflow.manager.blocks.types.EndBlockType; -import it.cnr.isti.workflow.manager.blocks.types.ExclusiveMergeBlockType; +import it.cnr.isti.workflow.manager.blocks.types.BranchRejoinBlockType; import it.cnr.isti.workflow.manager.blocks.types.HumanDecisionBlockType; import it.cnr.isti.workflow.manager.flows.model.FlowNode; import it.cnr.isti.workflow.manager.flows.model.bias.BlockBiasAnnotation; @@ -96,13 +96,13 @@ class JensenStructuredFlowsTest { .filter(node -> node instanceof Block block && block.getType() instanceof HumanDecisionBlockType) .count(); long merges = nodes.stream() - .filter(node -> node instanceof Block block && block.getType() instanceof ExclusiveMergeBlockType) + .filter(node -> node instanceof Block block && block.getType() instanceof BranchRejoinBlockType) .count(); long ends = nodes.stream() .filter(node -> node instanceof Block block && block.getType() instanceof EndBlockType) .count(); assertEquals(expectedDecisions, decisions, () -> flow.name() + ": HumanDecisionBlock count"); - assertEquals(expectedMerges, merges, () -> flow.name() + ": ExclusiveMergeBlock count"); + assertEquals(expectedMerges, merges, () -> flow.name() + ": BranchRejoinBlock count"); assertEquals(expectedEnds, ends, () -> flow.name() + ": EndBlock count"); assertEquals(expectedLanes, flow.flow().getLanes().size(), () -> flow.name() + ": lane count"); } diff --git a/src/test/java/it/cnr/isti/workflow/manager/flows/validation/ExclusiveMergeStructureValidationTest.java b/src/test/java/it/cnr/isti/workflow/manager/flows/validation/BranchRejoinStructureValidationTest.java similarity index 83% rename from src/test/java/it/cnr/isti/workflow/manager/flows/validation/ExclusiveMergeStructureValidationTest.java rename to src/test/java/it/cnr/isti/workflow/manager/flows/validation/BranchRejoinStructureValidationTest.java index ebf7492..6552b57 100644 --- a/src/test/java/it/cnr/isti/workflow/manager/flows/validation/ExclusiveMergeStructureValidationTest.java +++ b/src/test/java/it/cnr/isti/workflow/manager/flows/validation/BranchRejoinStructureValidationTest.java @@ -13,14 +13,14 @@ import org.springframework.test.context.TestPropertySource; import it.cnr.isti.workflow.manager.blocks.Block; import it.cnr.isti.workflow.manager.blocks.configurations.ConditionalBlockConfiguration; -import it.cnr.isti.workflow.manager.blocks.configurations.ExclusiveMergeBlockConfiguration; -import it.cnr.isti.workflow.manager.blocks.configurations.ExclusiveMergeInput; +import it.cnr.isti.workflow.manager.blocks.configurations.BranchRejoinBlockConfiguration; +import it.cnr.isti.workflow.manager.blocks.configurations.BranchRejoinInput; import it.cnr.isti.workflow.manager.blocks.configurations.HumanInteractiveBlockConfiguration; import it.cnr.isti.workflow.manager.blocks.factories.ConditionalBlockFactory; -import it.cnr.isti.workflow.manager.blocks.factories.ExclusiveMergeBlockFactory; +import it.cnr.isti.workflow.manager.blocks.factories.BranchRejoinBlockFactory; import it.cnr.isti.workflow.manager.blocks.factories.HumanInteractiveBlockFactory; import it.cnr.isti.workflow.manager.blocks.types.ConditionalBlockType; -import it.cnr.isti.workflow.manager.blocks.types.ExclusiveMergeBlockType; +import it.cnr.isti.workflow.manager.blocks.types.BranchRejoinBlockType; import it.cnr.isti.workflow.manager.blocks.types.HumanInteractionBlockType; import it.cnr.isti.workflow.manager.flows.model.Connection; import it.cnr.isti.workflow.manager.flows.model.FlowData; @@ -30,12 +30,12 @@ import jakarta.validation.Validator; /** * Confirms the "merge incrociato rifiutato" requirement: two exclusive branches from the same * Conditional cannot reconverge on a shared downstream node unless that node is an explicit - * ExclusiveMergeBlock. Complements the Phase 0 characterization tests, which only assert this + * BranchRejoinBlock. Complements the Phase 0 characterization tests, which only assert this * behavior indirectly. */ @SpringBootTest @TestPropertySource(locations = "classpath:test.properties") -class ExclusiveMergeStructureValidationTest { +class BranchRejoinStructureValidationTest { @Autowired Validator validator; @@ -47,10 +47,10 @@ class ExclusiveMergeStructureValidationTest { HumanInteractiveBlockFactory humanInteractiveBlockFactory; @Autowired - ExclusiveMergeBlockFactory exclusiveMergeBlockFactory; + BranchRejoinBlockFactory branchRejoinBlockFactory; @Test - void implicitReconvergenceWithoutAnExclusiveMergeBlockIsRejected() { + void implicitReconvergenceWithoutAnBranchRejoinBlockIsRejected() { Block conditional = conditional(); Block shared = humanInteraction("shared-downstream"); @@ -67,12 +67,12 @@ class ExclusiveMergeStructureValidationTest { } @Test - void reconvergenceThroughAnExplicitExclusiveMergeBlockIsAccepted() { + void reconvergenceThroughAnExplicitBranchRejoinBlockIsAccepted() { Block conditional = conditional(); - Block merge = exclusiveMergeBlockFactory.create( - ExclusiveMergeBlockConfiguration.builder() + Block merge = branchRejoinBlockFactory.create( + BranchRejoinBlockConfiguration.builder() .name("merge") - .inputs(List.of(new ExclusiveMergeInput("onTrue"), new ExclusiveMergeInput("onFalse"))) + .inputs(List.of(new BranchRejoinInput("onTrue"), new BranchRejoinInput("onFalse"))) .outputName("output") .build()); Block shared = humanInteraction("shared-downstream"); diff --git a/src/test/java/it/cnr/isti/workflow/manager/flows/validation/NodeTypeCapabilitiesIntegrationTest.java b/src/test/java/it/cnr/isti/workflow/manager/flows/validation/NodeTypeCapabilitiesIntegrationTest.java index a689388..8df2956 100644 --- a/src/test/java/it/cnr/isti/workflow/manager/flows/validation/NodeTypeCapabilitiesIntegrationTest.java +++ b/src/test/java/it/cnr/isti/workflow/manager/flows/validation/NodeTypeCapabilitiesIntegrationTest.java @@ -18,7 +18,7 @@ import it.cnr.isti.workflow.manager.blocks.configurations.HumanInteractiveBlockC import it.cnr.isti.workflow.manager.blocks.factories.EndBlockFactory; import it.cnr.isti.workflow.manager.blocks.factories.HumanInteractiveBlockFactory; import it.cnr.isti.workflow.manager.blocks.types.EndBlockType; -import it.cnr.isti.workflow.manager.blocks.types.ExclusiveMergeBlockType; +import it.cnr.isti.workflow.manager.blocks.types.BranchRejoinBlockType; import it.cnr.isti.workflow.manager.blocks.types.HumanDecisionBlockType; import it.cnr.isti.workflow.manager.blocks.types.HumanInteractionBlockType; import it.cnr.isti.workflow.manager.containers.types.GenericContainerType; @@ -68,8 +68,8 @@ class NodeTypeCapabilitiesIntegrationTest { assertEquals(NodeVisualRole.DECISION, decision.visualRole()); assertTrue(decision.biasAnnotationsAllowed()); - var merge = blocksController.getConfigurationDescriptorForType(ExclusiveMergeBlockType.TYPE).capabilities(); - assertEquals(NodeVisualRole.MERGE, merge.visualRole()); + var branchRejoin = blocksController.getConfigurationDescriptorForType(BranchRejoinBlockType.TYPE).capabilities(); + assertEquals(NodeVisualRole.BRANCH_REJOIN, branchRejoin.visualRole()); var container = containersController.getTypes().stream() .filter(descriptor -> GenericContainerType.TYPE.equals(descriptor.type()))