From 059f4867e8c23e31f032ded973f149884c147a5d Mon Sep 17 00:00:00 2001 From: mayc Date: Fri, 12 Jun 2026 14:54:08 -0400 Subject: [PATCH 1/5] Fix method complexity --- .../imsweb/staging/engine/DecisionEngine.java | 219 ++++++++++-------- 1 file changed, 122 insertions(+), 97 deletions(-) diff --git a/src/main/java/com/imsweb/staging/engine/DecisionEngine.java b/src/main/java/com/imsweb/staging/engine/DecisionEngine.java index 103c06c9..e5bed210 100644 --- a/src/main/java/com/imsweb/staging/engine/DecisionEngine.java +++ b/src/main/java/com/imsweb/staging/engine/DecisionEngine.java @@ -41,7 +41,6 @@ /** * An engine for processing declarative algorithms. */ -@SuppressWarnings("java:S3776") public class DecisionEngine { // string to use for blank or null in error strings @@ -651,25 +650,42 @@ public Result process(String schemaId, Map context) { public Result process(Schema schema, Map context) { Result result = new Result(context); - // trim all context Strings; " " will match "" - for (Entry entry : context.entrySet()) - if (entry.getValue() != null) - context.put(entry.getKey(), entry.getValue().trim()); + trimContext(context); - // validate inputs + if (!validateInputs(schema, context, result)) { + result.setType(Result.Type.FAILED_INPUT); + return result; + } + + initializeSchemaContext(schema, context); + + executeMappings(schema, context, result); + + validateOutputs(schema, context, result); + + return result; + } + + private void trimContext(Map context) { + for (Entry entry : context.entrySet()) { + if (entry.getValue() != null) { + entry.setValue(entry.getValue().trim()); + } + } + } + + private boolean validateInputs(Schema schema, Map context, Result result) { boolean stopForBadInput = false; for (String key : schema.getInputMap().keySet()) { Input input = schema.getInputMap().get(key); String value = context.get(input.getKey()); - // if value not supplied, use the default or defaultTable and set it back into the context; if not supplied and no default, set the input the blank if (value == null) { value = getDefault(input, context, result); context.put(input.getKey(), value); } - // validate value against associated table if supplied; if a value is not supplied, or blank, there is no need to validate it against the table if (value != null && !value.isEmpty() && input.getTable() != null) { Table lookup = getProvider().getTable(input.getTable()); @@ -683,97 +699,109 @@ public Result process(Schema schema, Map context) { result.addError(new ErrorBuilder(Boolean.TRUE.equals(input.getUsedForStaging()) ? Type.INVALID_REQUIRED_INPUT : Type.INVALID_NON_REQUIRED_INPUT).message( "Invalid '" + input.getKey() + "' value (" + value + ")").key(input.getKey()).table(input.getTable()).build()); - // if the schema error handling is set to FAIL or if the input is required for staging and the error handling is set to FAIL_WHEN_REQUIRED_FOR_STAGING, - // then stop processing and return a failure result if (Schema.StagingInputErrorHandler.FAIL.equals(schema.getOnInvalidInput()) || (Boolean.TRUE.equals(input.getUsedForStaging()) && Schema.StagingInputErrorHandler.FAIL_WHEN_USED_FOR_STAGING.equals(schema.getOnInvalidInput()))) stopForBadInput = true; } } } + return !stopForBadInput; + } - // if an invalid input was flagged to stop processing, set result and exit - if (stopForBadInput) { - result.setType(Result.Type.FAILED_INPUT); - return result; - } - - // add all output keys to the context; if no default is supplied, use an empty string + private void initializeSchemaContext(Schema schema, Map context) { + // Output defaults must be available to the schema's initial-context expressions. for (Entry entry : schema.getOutputMap().entrySet()) context.put(entry.getValue().getKey(), entry.getValue().getDefault() != null ? translateValue(entry.getValue().getDefault(), context) : ""); - // add the initial context if (schema.getInitialContext() != null) for (KeyValue keyValue : schema.getInitialContext()) context.put(keyValue.getKey(), translateValue(keyValue.getValue(), context)); + } - // process each mapping if it is "involved", which is checked using the current context against inclusion/exclusion criteria - if (schema.getMappings() != null) { - for (Mapping mapping : schema.getMappings()) { - // make sure mapping passes inclusion/exclusion tables if present - if (isMappingInvolved(mapping, context)) { - // if there are any inclusion/exclusion tables, add them to path - if (mapping.getInclusionTables() != null) - for (TablePath path : mapping.getInclusionTables()) - result.addPath(mapping.getId(), path.getId()); - if (mapping.getExclusionTables() != null) - for (TablePath path : mapping.getExclusionTables()) - result.addPath(mapping.getId(), path.getId()); - - // set the mapping-specific initial context if any - if (mapping.getInitialContext() != null) - for (KeyValue keyValue : mapping.getInitialContext()) - context.put(keyValue.getKey(), keyValue.getValue()); - - // loop over all table paths in the mapping - if (mapping.getTablePaths() != null) { - for (TablePath path : mapping.getTablePaths()) { - String tableId = path.getId(); - - // if there is input mapping defined, add the new mapping to the context - if (path.getInputMapping() != null) { - for (KeyMapping key : path.getInputMapping()) { - String mapFromKey = key.getFrom(); - - if (!context.containsKey(mapFromKey)) { - result.addError(new ErrorBuilder(Type.UNKNOWN_INPUT_MAPPING).message("Input mapping '" + mapFromKey + "' does not exist for table '" + tableId + "'").key( - mapFromKey).table(tableId).build()); - continue; - } - - context.put(key.getTo(), context.get(mapFromKey)); - } - } + private void executeMappings(Schema schema, Map context, Result result) { + if (schema.getMappings() == null) + return; - // create a stack to keep track of table calls and ensure there is no infinite recursion - Deque stack = new ArrayDeque<>(); + for (Mapping mapping : schema.getMappings()) { + if (!isMappingInvolved(mapping, context)) + continue; - // recursively process the mapping; if false is returned, stop all processing - boolean continueProcessing = process(mapping.getId(), tableId, path, result, stack); + recordInvolvementPaths(mapping, result); + initializeMappingContext(mapping, context); + executeTablePaths(mapping, context, result); + } + } - // remove the temporary input mappings - if (path.getInputMapping() != null) { - for (KeyMapping key : path.getInputMapping()) - context.remove(key.getTo()); - } + private void recordInvolvementPaths(Mapping mapping, Result result) { + recordPaths(mapping.getId(), mapping.getInclusionTables(), result); + recordPaths(mapping.getId(), mapping.getExclusionTables(), result); + } - if (!continueProcessing) - break; - } - } - } + private void recordPaths(String mappingId, List paths, Result result) { + if (paths != null) + for (TablePath path : paths) + result.addPath(mappingId, path.getId()); + } + + private void initializeMappingContext(Mapping mapping, Map context) { + if (mapping.getInitialContext() != null) + for (KeyValue keyValue : mapping.getInitialContext()) + context.put(keyValue.getKey(), keyValue.getValue()); + } + + private void executeTablePaths(Mapping mapping, Map context, Result result) { + if (mapping.getTablePaths() == null) + return; + for (TablePath path : mapping.getTablePaths()) { + if (!executeTablePath(mapping.getId(), path, context, result)) + break; + } + } + + private boolean executeTablePath(String mappingId, TablePath path, Map context, Result result) { + applyInputMappings(path, context, result); + try { + return process(mappingId, path.getId(), path, result, new ArrayDeque<>()); + } + finally { + // Input-mapping destinations are temporary aliases scoped to this table path. + removeInputMappings(path, context); + } + } + + private void applyInputMappings(TablePath path, Map context, Result result) { + if (path.getInputMapping() == null) + return; + + for (KeyMapping key : path.getInputMapping()) { + String sourceKey = key.getFrom(); + if (!context.containsKey(sourceKey)) { + result.addError(new ErrorBuilder(Type.UNKNOWN_INPUT_MAPPING) + .message("Input mapping '" + sourceKey + "' does not exist for table '" + path.getId() + "'") + .key(sourceKey) + .table(path.getId()) + .build()); + continue; } + + context.put(key.getTo(), context.get(sourceKey)); } + } + + private void removeInputMappings(TablePath path, Map context) { + if (path.getInputMapping() != null) + for (KeyMapping key : path.getInputMapping()) + context.remove(key.getTo()); + } - // if outputs were specified, remove any extra keys and validate the others if a table was specified + private void validateOutputs(Schema schema, Map context, Result result) { if (schema.getOutputMap() != null && !schema.getOutputMap().isEmpty()) { Iterator> iter = context.entrySet().iterator(); while (iter.hasNext()) { Map.Entry entry = iter.next(); Output output = schema.getOutputMap().get(entry.getKey()); - // if the key is not defined in the output, remove it if (output == null) iter.remove(); else if (output.getTable() != null) { @@ -784,7 +812,6 @@ else if (output.getTable() != null) { continue; } - // verify the value of the output key is contained in the associated table List endpoints = matchTable(lookup, context); if (endpoints == null) { String value = context.get(output.getKey()); @@ -794,8 +821,6 @@ else if (output.getTable() != null) { } } } - - return result; } /** @@ -804,7 +829,7 @@ else if (output.getTable() != null) { * @param tableId a Table identifier * @param path a TablePath * @param result a Result - * @param stack a stack which tracks the path and makes sure the path doesn't enter an infinite recusive state + * @param stack a stack which tracks the path and makes sure the path doesn't enter an infinite recursive state * @return a boolean indicating whether processing should continue */ @@ -854,28 +879,8 @@ else if (EndpointType.ERROR.equals(endpoint.getType())) { result.addError(new ErrorBuilder(Type.STAGING_ERROR).message(message).table(tableId).columns(Collections.singletonList(endpoint.getResultKey())).build()); } - else if (EndpointType.VALUE.equals(endpoint.getType())) { - // if output mapping(s) were provided, check whether the key was mapped - List mappedKeys = new ArrayList<>(); - if (path.getOutputMapping() != null) { - for (KeyMapping key : path.getOutputMapping()) { - if (key.getFrom().equals(endpoint.getResultKey())) - mappedKeys.add(key.getTo()); - } - } - - // if the value is null, that is indicating that the key should be removed from the context; otherwise set the value into the context - if (mappedKeys.isEmpty()) - mappedKeys = Collections.singletonList(endpoint.getResultKey()); - - // iterate over all the mappings for this endpoint key - for (String key : mappedKeys) { - if (endpoint.getValue() == null) - result.getContext().remove(key); - else - result.getContext().put(key, translateValue(endpoint.getValue(), result.getContext())); - } - } + else if (EndpointType.VALUE.equals(endpoint.getType())) + applyEndpointValue(endpoint, path, result.getContext()); } } @@ -885,4 +890,24 @@ else if (EndpointType.VALUE.equals(endpoint.getType())) { return continueProcessing; } + private void applyEndpointValue(Endpoint endpoint, TablePath path, Map context) { + for (String key : getMappedOutputKeys(endpoint.getResultKey(), path)) { + if (endpoint.getValue() == null) + context.remove(key); + else + context.put(key, translateValue(endpoint.getValue(), context)); + } + } + + private List getMappedOutputKeys(String resultKey, TablePath path) { + if (path.getOutputMapping() == null) + return Collections.singletonList(resultKey); + + List mappedKeys = path.getOutputMapping().stream() + .filter(key -> key.getFrom().equals(resultKey)) + .map(KeyMapping::getTo) + .toList(); + return mappedKeys.isEmpty() ? Collections.singletonList(resultKey) : mappedKeys; + } + } From 4c121be5b757ca6d2874fab60f4104bcc0ae6e2d Mon Sep 17 00:00:00 2001 From: mayc Date: Fri, 12 Jun 2026 14:54:57 -0400 Subject: [PATCH 2/5] Tweak --- .../java/com/imsweb/staging/engine/DecisionEngine.java | 7 ++++--- .../java/com/imsweb/staging/engine/DecisionEngineTest.java | 6 +++--- 2 files changed, 7 insertions(+), 6 deletions(-) diff --git a/src/main/java/com/imsweb/staging/engine/DecisionEngine.java b/src/main/java/com/imsweb/staging/engine/DecisionEngine.java index e5bed210..8ea0a7e6 100644 --- a/src/main/java/com/imsweb/staging/engine/DecisionEngine.java +++ b/src/main/java/com/imsweb/staging/engine/DecisionEngine.java @@ -44,7 +44,8 @@ public class DecisionEngine { // string to use for blank or null in error strings - public static final String _BLANK_OUTPUT = ""; + public static final String BLANK_OUTPUT = ""; + private static final Pattern _TEMPLATE_REFERENCE = Pattern.compile("\\{\\{(.*?)}}"); private final DataProvider _provider; @@ -205,7 +206,7 @@ static String getTableInputsAsString(Table table, Map context) { for (ColumnDefinition def : table.getColumnDefinitions()) if (ColumnType.INPUT.equals(def.getType())) { String value = context.get(def.getKey()); - inputs.add((value == null || value.trim().isEmpty()) ? _BLANK_OUTPUT : value.trim()); + inputs.add((value == null || value.trim().isEmpty()) ? BLANK_OUTPUT : value.trim()); } return String.join(",", inputs); @@ -815,7 +816,7 @@ else if (output.getTable() != null) { List endpoints = matchTable(lookup, context); if (endpoints == null) { String value = context.get(output.getKey()); - result.addError(new ErrorBuilder(Type.INVALID_OUTPUT).message("Invalid '" + output.getKey() + "' value (" + (value.isEmpty() ? _BLANK_OUTPUT : value) + ")").key( + result.addError(new ErrorBuilder(Type.INVALID_OUTPUT).message("Invalid '" + output.getKey() + "' value (" + (value.isEmpty() ? BLANK_OUTPUT : value) + ")").key( output.getKey()).table(output.getTable()).build()); } } diff --git a/src/test/java/com/imsweb/staging/engine/DecisionEngineTest.java b/src/test/java/com/imsweb/staging/engine/DecisionEngineTest.java index 866b8cf1..093df06c 100644 --- a/src/test/java/com/imsweb/staging/engine/DecisionEngineTest.java +++ b/src/test/java/com/imsweb/staging/engine/DecisionEngineTest.java @@ -1477,10 +1477,10 @@ void testGetTableInputsAsString() { Map context = new HashMap<>(); - assertEquals(DecisionEngine._BLANK_OUTPUT + "," + DecisionEngine._BLANK_OUTPUT, DecisionEngine.getTableInputsAsString(table, context)); + assertEquals(DecisionEngine.BLANK_OUTPUT + "," + DecisionEngine.BLANK_OUTPUT, DecisionEngine.getTableInputsAsString(table, context)); context.put("b", "25"); - assertEquals(DecisionEngine._BLANK_OUTPUT + ",25", DecisionEngine.getTableInputsAsString(table, context)); + assertEquals(DecisionEngine.BLANK_OUTPUT + ",25", DecisionEngine.getTableInputsAsString(table, context)); context.put("a", "7"); assertEquals("7,25", DecisionEngine.getTableInputsAsString(table, context)); @@ -1551,7 +1551,7 @@ void testOutputsAndDefaults() { input.put("input1", "000"); result = engine.process("sample_outputs", input); assertTrue(result.hasErrors()); - assertEquals("Invalid 'output1' value (" + DecisionEngine._BLANK_OUTPUT + ")", result.getErrors().getFirst().getMessage()); + assertEquals("Invalid 'output1' value (" + DecisionEngine.BLANK_OUTPUT + ")", result.getErrors().getFirst().getMessage()); assertEquals(new HashSet<>(Arrays.asList("table_input", "table_output")), engine.getInvolvedTables(schema)); From fa59f5b3d45e947022e3e29fa9cb8a682a066d93 Mon Sep 17 00:00:00 2001 From: mayc Date: Fri, 12 Jun 2026 14:55:48 -0400 Subject: [PATCH 3/5] Tweak --- src/main/java/com/imsweb/staging/engine/DecisionEngine.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/main/java/com/imsweb/staging/engine/DecisionEngine.java b/src/main/java/com/imsweb/staging/engine/DecisionEngine.java index 8ea0a7e6..ee82d48a 100644 --- a/src/main/java/com/imsweb/staging/engine/DecisionEngine.java +++ b/src/main/java/com/imsweb/staging/engine/DecisionEngine.java @@ -47,10 +47,10 @@ public class DecisionEngine { public static final String BLANK_OUTPUT = ""; private static final Pattern _TEMPLATE_REFERENCE = Pattern.compile("\\{\\{(.*?)}}"); - private final DataProvider _provider; - private static final String _CONTEXT_MISSING_MESSAGE = "Context must not be missing"; + private final DataProvider _provider; + /** * Construct the decision engine with the passed data provider * @param provider a DataProvider From 988b36ba872e0203a03fceab118d1d69e6f62453 Mon Sep 17 00:00:00 2001 From: mayc Date: Fri, 12 Jun 2026 15:02:13 -0400 Subject: [PATCH 4/5] Added missing documentation --- .../imsweb/staging/engine/DecisionEngine.java | 95 ++++++++++++++++++- 1 file changed, 94 insertions(+), 1 deletion(-) diff --git a/src/main/java/com/imsweb/staging/engine/DecisionEngine.java b/src/main/java/com/imsweb/staging/engine/DecisionEngine.java index ee82d48a..f6b3bef3 100644 --- a/src/main/java/com/imsweb/staging/engine/DecisionEngine.java +++ b/src/main/java/com/imsweb/staging/engine/DecisionEngine.java @@ -667,7 +667,12 @@ public Result process(Schema schema, Map context) { return result; } + /** + * Trims every non-null value in the supplied context. + * @param context the context to normalize + */ private void trimContext(Map context) { + // Trim all context strings so whitespace-only values are treated as blank. for (Entry entry : context.entrySet()) { if (entry.getValue() != null) { entry.setValue(entry.getValue().trim()); @@ -675,6 +680,13 @@ private void trimContext(Map context) { } } + /** + * Resolves missing input defaults and validates non-blank inputs against their configured tables. + * @param schema the schema being processed + * @param context the current processing context + * @param result the result to receive validation errors + * @return {@code true} when processing should continue; {@code false} when the schema's invalid-input policy requires failure + */ private boolean validateInputs(Schema schema, Map context, Result result) { boolean stopForBadInput = false; for (String key : schema.getInputMap().keySet()) { @@ -682,11 +694,13 @@ private boolean validateInputs(Schema schema, Map context, Resul String value = context.get(input.getKey()); + // If no value was supplied, resolve its default and add it to the context. if (value == null) { value = getDefault(input, context, result); context.put(input.getKey(), value); } + // Blank inputs do not need validation against their associated table. if (value != null && !value.isEmpty() && input.getTable() != null) { Table lookup = getProvider().getTable(input.getTable()); @@ -700,6 +714,7 @@ private boolean validateInputs(Schema schema, Map context, Resul result.addError(new ErrorBuilder(Boolean.TRUE.equals(input.getUsedForStaging()) ? Type.INVALID_REQUIRED_INPUT : Type.INVALID_NON_REQUIRED_INPUT).message( "Invalid '" + input.getKey() + "' value (" + value + ")").key(input.getKey()).table(input.getTable()).build()); + // The schema controls whether this invalid input should stop processing. if (Schema.StagingInputErrorHandler.FAIL.equals(schema.getOnInvalidInput()) || (Boolean.TRUE.equals(input.getUsedForStaging()) && Schema.StagingInputErrorHandler.FAIL_WHEN_USED_FOR_STAGING.equals(schema.getOnInvalidInput()))) stopForBadInput = true; @@ -709,6 +724,11 @@ private boolean validateInputs(Schema schema, Map context, Resul return !stopForBadInput; } + /** + * Initializes output defaults followed by schema-level initial-context values. + * @param schema the schema being processed + * @param context the context to initialize + */ private void initializeSchemaContext(Schema schema, Map context) { // Output defaults must be available to the schema's initial-context expressions. for (Entry entry : schema.getOutputMap().entrySet()) @@ -719,11 +739,18 @@ private void initializeSchemaContext(Schema schema, Map context) context.put(keyValue.getKey(), translateValue(keyValue.getValue(), context)); } + /** + * Executes each mapping whose inclusion and exclusion criteria match the current context. + * @param schema the schema containing the mappings + * @param context the current processing context + * @param result the result to update + */ private void executeMappings(Schema schema, Map context, Result result) { if (schema.getMappings() == null) return; for (Mapping mapping : schema.getMappings()) { + // Only mappings that pass their inclusion and exclusion criteria are processed. if (!isMappingInvolved(mapping, context)) continue; @@ -733,34 +760,68 @@ private void executeMappings(Schema schema, Map context, Result } } + /** + * Records the mapping's inclusion and exclusion tables in the result path. + * @param mapping the involved mapping + * @param result the result to update + */ private void recordInvolvementPaths(Mapping mapping, Result result) { + // Inclusion and exclusion tables participate in processing and belong in the result path. recordPaths(mapping.getId(), mapping.getInclusionTables(), result); recordPaths(mapping.getId(), mapping.getExclusionTables(), result); } + /** + * Records a collection of table paths for a mapping. + * @param mappingId the mapping identifier + * @param paths the table paths to record, or {@code null} + * @param result the result to update + */ private void recordPaths(String mappingId, List paths, Result result) { if (paths != null) for (TablePath path : paths) result.addPath(mappingId, path.getId()); } + /** + * Adds mapping-level initial-context values to the processing context. + * @param mapping the mapping being processed + * @param context the context to initialize + */ private void initializeMappingContext(Mapping mapping, Map context) { + // Mapping-specific values are available to every table path in this mapping. if (mapping.getInitialContext() != null) for (KeyValue keyValue : mapping.getInitialContext()) context.put(keyValue.getKey(), keyValue.getValue()); } + /** + * Executes the mapping's table paths in order until all paths complete or a STOP endpoint is reached. + * @param mapping the mapping being processed + * @param context the current processing context + * @param result the result to update + */ private void executeTablePaths(Mapping mapping, Map context, Result result) { if (mapping.getTablePaths() == null) return; + // A STOP endpoint ends the remaining table paths for this mapping. for (TablePath path : mapping.getTablePaths()) { if (!executeTablePath(mapping.getId(), path, context, result)) break; } } + /** + * Applies temporary input mappings and executes one table path, including any JUMP tables. + * @param mappingId the mapping identifier + * @param path the table path to execute + * @param context the current processing context + * @param result the result to update + * @return {@code true} when processing should continue; {@code false} when a STOP endpoint was reached + */ private boolean executeTablePath(String mappingId, TablePath path, Map context, Result result) { + // Input mappings create aliases used while processing this path and any JUMP tables it reaches. applyInputMappings(path, context, result); try { return process(mappingId, path.getId(), path, result, new ArrayDeque<>()); @@ -771,6 +832,12 @@ private boolean executeTablePath(String mappingId, TablePath path, Map context, Result result) { if (path.getInputMapping() == null) return; @@ -790,12 +857,23 @@ private void applyInputMappings(TablePath path, Map context, Res } } + /** + * Removes the table path's temporary input aliases from the context. + * @param path the table path defining the aliases + * @param context the context to update + */ private void removeInputMappings(TablePath path, Map context) { if (path.getInputMapping() != null) for (KeyMapping key : path.getInputMapping()) context.remove(key.getTo()); } + /** + * Removes non-output values and validates configured outputs against their associated tables. + * @param schema the schema defining the outputs + * @param context the final processing context + * @param result the result to receive validation errors + */ private void validateOutputs(Schema schema, Map context, Result result) { if (schema.getOutputMap() != null && !schema.getOutputMap().isEmpty()) { Iterator> iter = context.entrySet().iterator(); @@ -803,6 +881,7 @@ private void validateOutputs(Schema schema, Map context, Result Map.Entry entry = iter.next(); Output output = schema.getOutputMap().get(entry.getKey()); + // Once outputs are defined, internal and input values are removed from the returned context. if (output == null) iter.remove(); else if (output.getTable() != null) { @@ -813,6 +892,7 @@ else if (output.getTable() != null) { continue; } + // Validate the final output value when the output declares a validation table. List endpoints = matchTable(lookup, context); if (endpoints == null) { String value = context.get(output.getKey()); @@ -833,7 +913,6 @@ else if (output.getTable() != null) { * @param stack a stack which tracks the path and makes sure the path doesn't enter an infinite recursive state * @return a boolean indicating whether processing should continue */ - protected boolean process(String mappingId, String tableId, TablePath path, Result result, Deque stack) { boolean continueProcessing = true; @@ -891,7 +970,14 @@ else if (EndpointType.VALUE.equals(endpoint.getType())) return continueProcessing; } + /** + * Applies a value endpoint to its mapped output keys, resolving templates against the current context. + * @param endpoint the value endpoint to apply + * @param path the table path defining output mappings + * @param context the context to update + */ private void applyEndpointValue(Endpoint endpoint, TablePath path, Map context) { + // A null endpoint value removes its destination; otherwise templates resolve against the current context. for (String key : getMappedOutputKeys(endpoint.getResultKey(), path)) { if (endpoint.getValue() == null) context.remove(key); @@ -900,10 +986,17 @@ private void applyEndpointValue(Endpoint endpoint, TablePath path, Map getMappedOutputKeys(String resultKey, TablePath path) { if (path.getOutputMapping() == null) return Collections.singletonList(resultKey); + // One endpoint can populate multiple destination keys through output mappings. List mappedKeys = path.getOutputMapping().stream() .filter(key -> key.getFrom().equals(resultKey)) .map(KeyMapping::getTo) From b3c0ecbf8f2bd5733af2038840be31e60748f997 Mon Sep 17 00:00:00 2001 From: mayc Date: Fri, 12 Jun 2026 15:22:28 -0400 Subject: [PATCH 5/5] Cleanup --- .../java/com/imsweb/staging/engine/DecisionEngineTest.java | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/src/test/java/com/imsweb/staging/engine/DecisionEngineTest.java b/src/test/java/com/imsweb/staging/engine/DecisionEngineTest.java index 093df06c..dbb27086 100644 --- a/src/test/java/com/imsweb/staging/engine/DecisionEngineTest.java +++ b/src/test/java/com/imsweb/staging/engine/DecisionEngineTest.java @@ -39,6 +39,7 @@ import com.imsweb.staging.entities.impl.StagingTable; import com.imsweb.staging.entities.impl.StagingTablePath; +import static com.imsweb.staging.engine.DecisionEngine.BLANK_OUTPUT; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.tuple; import static org.junit.jupiter.api.Assertions.assertEquals; @@ -1477,10 +1478,10 @@ void testGetTableInputsAsString() { Map context = new HashMap<>(); - assertEquals(DecisionEngine.BLANK_OUTPUT + "," + DecisionEngine.BLANK_OUTPUT, DecisionEngine.getTableInputsAsString(table, context)); + assertEquals(BLANK_OUTPUT + "," + BLANK_OUTPUT, DecisionEngine.getTableInputsAsString(table, context)); context.put("b", "25"); - assertEquals(DecisionEngine.BLANK_OUTPUT + ",25", DecisionEngine.getTableInputsAsString(table, context)); + assertEquals(BLANK_OUTPUT + ",25", DecisionEngine.getTableInputsAsString(table, context)); context.put("a", "7"); assertEquals("7,25", DecisionEngine.getTableInputsAsString(table, context)); @@ -1551,7 +1552,7 @@ void testOutputsAndDefaults() { input.put("input1", "000"); result = engine.process("sample_outputs", input); assertTrue(result.hasErrors()); - assertEquals("Invalid 'output1' value (" + DecisionEngine.BLANK_OUTPUT + ")", result.getErrors().getFirst().getMessage()); + assertEquals("Invalid 'output1' value (" + BLANK_OUTPUT + ")", result.getErrors().getFirst().getMessage()); assertEquals(new HashSet<>(Arrays.asList("table_input", "table_output")), engine.getInvolvedTables(schema));