From 82186e7b0245b817a41800eaaa4e867a6e792323 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E8=8B=8F=E4=B9=89=E8=B6=85?= Date: Thu, 20 Nov 2025 22:51:41 +0800 Subject: [PATCH 1/7] add filter some null map keys and unit test --- .../expand/CuringParamsServiceImpl.java | 225 ++++--- .../expand/CuringParamsServiceImplTest.java | 584 ++++++++++++++++++ .../expand/CuringParamsServiceTest.java | 273 -------- .../parameters/AbstractParametersTest.java | 84 +++ .../task/shell/ShellTaskChannelTest.java | 137 ++++ 5 files changed, 954 insertions(+), 349 deletions(-) create mode 100644 dolphinscheduler-service/src/test/java/org/apache/dolphinscheduler/service/expand/CuringParamsServiceImplTest.java delete mode 100644 dolphinscheduler-service/src/test/java/org/apache/dolphinscheduler/service/expand/CuringParamsServiceTest.java create mode 100644 dolphinscheduler-task-plugin/dolphinscheduler-task-shell/src/test/java/org/apache/dolphinscheduler/plugin/task/shell/ShellTaskChannelTest.java diff --git a/dolphinscheduler-service/src/main/java/org/apache/dolphinscheduler/service/expand/CuringParamsServiceImpl.java b/dolphinscheduler-service/src/main/java/org/apache/dolphinscheduler/service/expand/CuringParamsServiceImpl.java index d98b2cfc4ca4..bf06ffe701db 100644 --- a/dolphinscheduler-service/src/main/java/org/apache/dolphinscheduler/service/expand/CuringParamsServiceImpl.java +++ b/dolphinscheduler-service/src/main/java/org/apache/dolphinscheduler/service/expand/CuringParamsServiceImpl.java @@ -55,7 +55,6 @@ import java.util.Collections; import java.util.Date; import java.util.HashMap; -import java.util.Iterator; import java.util.LinkedHashMap; import java.util.List; import java.util.Map; @@ -66,10 +65,12 @@ import javax.annotation.Nullable; import lombok.NonNull; +import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Component; +@Slf4j @Component public class CuringParamsServiceImpl implements CuringParamsService { @@ -160,107 +161,179 @@ public Map parseWorkflowFatherParam(@Nullable Map The priority of the parameters is as follows: - *

varpool > command parameters > local parameters > global parameters > project parameters > built-in parameters - * todo: Use TaskRuntimeParams to represent this. + * Prepares the final map of task execution parameters by merging parameters from multiple sources + * in a well-defined priority order. The resulting map is guaranteed to contain only valid entries: + * - Keys are non-null and non-blank strings + * - Values are non-null {@link Property} objects * - * @param taskInstance - * @param parameters - * @param workflowInstance - * @param projectName - * @param workflowDefinitionName - * @return + *

Parameter Precedence (highest to lowest): + *

    + *
  1. Built-in system parameters (e.g., ${task.id}, ${workflow.instance.id})
  2. + *
  3. Project-level parameters
  4. + *
  5. Workflow global parameters
  6. + *
  7. Task local parameters
  8. + *
  9. Command-line / complement parameters (e.g., from补数 or API)
  10. + *
  11. VarPool overrides (only for {@link Direct#IN} parameters)
  12. + *
  13. Business/scheduling time parameters (e.g., ${system.datetime})
  14. + *
+ * + *

Important Notes: + *

    + *
  • All parameter sources are sanitized via {@link #safePutAll(Map, Map)} to prevent {@code null} + * or blank keys, which would cause JSON serialization failures (e.g., Jackson's + * "Null key for a Map not allowed in JSON").
  • + *
  • Placeholders (e.g., {@code "${var}"}) in parameter values are resolved after all sources + * are merged, using the consolidated parameter map. Global parameters are already + * solidified (fully resolved at workflow instance creation), so no recursive + * placeholder expansion is needed.
  • + *
  • {@code VarPool} values (from upstream tasks) only override parameters marked as + * {@link Direct#IN}; output or constant parameters are left unchanged.
  • + *
+ * + * @param taskInstance the current task instance (must not be null) + * @param parameters the parsed task-specific parameters (must not be null) + * @param workflowInstance the parent workflow instance (must not be null) + * @param projectName name of the project containing the workflow + * @param workflowDefinitionName name of the workflow definition + * @return a safe, fully resolved map of parameter name to {@link Property}, ready for task execution */ @Override - public Map paramParsingPreparation(@NonNull TaskInstance taskInstance, + public Map paramParsingPreparation( + @NonNull TaskInstance taskInstance, @NonNull AbstractParameters parameters, @NonNull WorkflowInstance workflowInstance, String projectName, String workflowDefinitionName) { - Map prepareParamsMap = new HashMap<>(); - // assign value to definedParams here - Map globalParams = parseGlobalParamsMap(workflowInstance); - - // combining local and global parameters - Map localParams = parameters.getInputLocalParametersMap(); - - // stream pass params - List varPools = parseVarPool(taskInstance); + Map prepareParamsMap = new HashMap<>(); - // if it is a complement, - // you need to pass in the task instance id to locate the time - // of the process instance complement + // Parse command param (defensive: commandParam may be null for normal runs) ICommandParam commandParam = JSONUtils.parseObject(workflowInstance.getCommandParam(), ICommandParam.class); - String timeZone = commandParam.getTimeZone(); + String timeZone = (commandParam != null) ? commandParam.getTimeZone() : null; - // built-in params - Map builtInParams = - setBuiltInParamsMap(taskInstance, workflowInstance, timeZone, projectName, workflowDefinitionName); + // 1. Built-in system parameters (e.g., task.id, workflow.instance.id, etc.) + Map builtInParams = setBuiltInParamsMap( + taskInstance, workflowInstance, timeZone, projectName, workflowDefinitionName); + safePutAll(prepareParamsMap, ParameterUtils.getUserDefParamsMap(builtInParams)); - // project-level params + // 2. Project-level parameters (shared across all workflows in the project) Map projectParams = getProjectParameterMap(taskInstance.getProjectCode()); + safePutAll(prepareParamsMap, projectParams); - if (MapUtils.isNotEmpty(builtInParams)) { - prepareParamsMap.putAll(ParameterUtils.getUserDefParamsMap(builtInParams)); - } - - if (MapUtils.isNotEmpty(projectParams)) { - prepareParamsMap.putAll(projectParams); - } - - if (MapUtils.isNotEmpty(globalParams)) { - prepareParamsMap.putAll(globalParams); - } - - if (MapUtils.isNotEmpty(localParams)) { - prepareParamsMap.putAll(localParams); - } + // 3. Workflow global parameters (defined at workflow level, solidified at instance creation) + Map globalParams = parseGlobalParamsMap(workflowInstance); + safePutAll(prepareParamsMap, globalParams); - if (CollectionUtils.isNotEmpty(commandParam.getCommandParams())) { - prepareParamsMap.putAll(commandParam.getCommandParams().stream() - .collect(Collectors.toMap(Property::getProp, Function.identity()))); + // 4. Task local parameters (defined in the task node itself) + Map localParams = parameters.getInputLocalParametersMap(); + safePutAll(prepareParamsMap, localParams); + + // 5. Command-line or complement (补数) parameters passed at runtime + if (commandParam != null && CollectionUtils.isNotEmpty(commandParam.getCommandParams())) { + Map commandParamsMap = commandParam.getCommandParams().stream() + .filter(prop -> StringUtils.isNotBlank(prop.getProp())) // exclude invalid keys + .collect(Collectors.toMap( + Property::getProp, + Function.identity(), + (v1, v2) -> v2 // on duplicate keys, keep the last occurrence + )); + safePutAll(prepareParamsMap, commandParamsMap); } + // 6. VarPool: override only input (Direct.IN) parameters with values from upstream tasks + List varPools = parseVarPool(taskInstance); if (CollectionUtils.isNotEmpty(varPools)) { - // overwrite the in parameter by varPool for (Property varPool : varPools) { - Property property = prepareParamsMap.get(varPool.getProp()); - if (property == null || property.getDirect() != Direct.IN) { - continue; + if (StringUtils.isBlank(varPool.getProp())) + continue; // skip invalid + Property existing = prepareParamsMap.get(varPool.getProp()); + if (existing != null && Direct.IN.equals(existing.getDirect())) { + existing.setValue(varPool.getValue()); } - property.setValue(varPool.getValue()); } } - Iterator> iter = prepareParamsMap.entrySet().iterator(); - while (iter.hasNext()) { - Map.Entry en = iter.next(); - Property property = en.getValue(); - - if (StringUtils.isNotEmpty(property.getValue()) - && property.getValue().contains(Constants.FUNCTION_START_WITH)) { - /** - * local parameter refers to global parameter with the same name - * note: the global parameters of the process instance here are solidified parameters, - * and there are no variables in them. - */ - String val = property.getValue(); - - // handle some chain parameter assign, such as `{"var1": "${var2}", "var2": 1}` should be convert to - // `{"var1": 1, "var2": 1}` - val = convertParameterPlaceholders(val, prepareParamsMap); - property.setValue(val); - } + // 7. Resolve placeholders (e.g., "${output_dir}") using the fully merged parameter map + resolvePlaceholders(prepareParamsMap); + + // 8. Business/scheduling time parameters (e.g., ${system.datetime}, ${schedule.time}) + Map businessParams = preBuildBusinessParams(workflowInstance); + safePutAll(prepareParamsMap, businessParams); + + return prepareParamsMap; + } + + /** + * Safely merges entries from the {@code source} map into the {@code target} map, + * skipping any entry with a {@code null}, empty, or blank key, or a {@code null} value. + * + *

This method is critical for ensuring that the resulting parameter map can be + * safely serialized to JSON (e.g., by Jackson), which does not allow {@code null} keys + * in maps. Invalid entries are logged as warnings to aid in debugging misconfigured + * parameters (e.g., project or workflow parameters with missing names). + * + *

Example of skipped entry: + *

+     *   key = null        → skipped
+     *   key = ""          → skipped
+     *   key = "  \t\n"    → skipped
+     *   value = null      → skipped
+     * 
+ * + *

All valid entries (non-blank key and non-null value) are added to {@code target} + * using standard {@link Map#put(Object, Object)} semantics (later values overwrite earlier ones). + * + * @param target the destination map to merge into (must not be null) + * @param source the source map whose valid entries will be copied (may be null or empty) + */ + private void safePutAll(Map target, Map source) { + if (MapUtils.isEmpty(source)) { + return; } + source.forEach((key, value) -> { + if (StringUtils.isNotBlank(key) && value != null) { + target.put(key, value); + } else { + log.warn("Skipped invalid parameter entry: key='{}', value={}", key, value); + } + }); + } - // put schedule time param to params map - Map paramsMap = preBuildBusinessParams(workflowInstance); - if (MapUtils.isNotEmpty(paramsMap)) { - prepareParamsMap.putAll(paramsMap); + /** + * Resolves placeholder expressions (e.g., "${var}") in parameter values by substituting them + * with actual values from the current {@code paramsMap}. + * + *

This method supports parameter references where a local task parameter refers to a global + * workflow parameter of the same name. For example: + *

+     * Global parameters (solidified at workflow instance creation):
+     *   "output_dir" → "/data/20251119"
+     *
+     * Local task parameter definition:
+     *   "log_path" → "${output_dir}/task.log"
+     *
+     * After resolution:
+     *   "log_path" → "/data/20251119/task.log"
+     * 
+ * + *

Important: The global parameters included in {@code paramsMap} are + * solidified—meaning they were fully resolved at workflow instance creation time + * and contain no unresolved placeholders (e.g., no nested "${...}" expressions). + * Therefore, this resolution pass only needs to perform a single-level (or iterative chain) + * substitution without worrying about recursive variable expansion in global values. + * + *

The method processes all properties in-place. Only values containing + * {@link Constants#FUNCTION_START_WITH} (typically "${") are processed. + * + * @param paramsMap the map of parameters (key: parameter name, value: {@link Property}) to resolve. + */ + private void resolvePlaceholders(Map paramsMap) { + for (Property prop : paramsMap.values()) { + String val = prop.getValue(); + if (StringUtils.isNotEmpty(val) && val.contains(Constants.FUNCTION_START_WITH)) { + prop.setValue(convertParameterPlaceholders(val, paramsMap)); + } } - return prepareParamsMap; } /** diff --git a/dolphinscheduler-service/src/test/java/org/apache/dolphinscheduler/service/expand/CuringParamsServiceImplTest.java b/dolphinscheduler-service/src/test/java/org/apache/dolphinscheduler/service/expand/CuringParamsServiceImplTest.java new file mode 100644 index 000000000000..1eea462f4a8d --- /dev/null +++ b/dolphinscheduler-service/src/test/java/org/apache/dolphinscheduler/service/expand/CuringParamsServiceImplTest.java @@ -0,0 +1,584 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.dolphinscheduler.service.expand; + +import static com.google.common.truth.Truth.assertThat; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.mockito.Mockito.when; + +import org.apache.dolphinscheduler.common.constants.DateConstants; +import org.apache.dolphinscheduler.common.enums.CommandType; +import org.apache.dolphinscheduler.common.utils.DateUtils; +import org.apache.dolphinscheduler.common.utils.JSONUtils; +import org.apache.dolphinscheduler.dao.entity.Project; +import org.apache.dolphinscheduler.dao.entity.ProjectParameter; +import org.apache.dolphinscheduler.dao.entity.TaskDefinition; +import org.apache.dolphinscheduler.dao.entity.TaskInstance; +import org.apache.dolphinscheduler.dao.entity.WorkflowDefinition; +import org.apache.dolphinscheduler.dao.entity.WorkflowInstance; +import org.apache.dolphinscheduler.dao.mapper.ProjectParameterMapper; +import org.apache.dolphinscheduler.extract.master.command.BackfillWorkflowCommandParam; +import org.apache.dolphinscheduler.plugin.task.api.TaskConstants; +import org.apache.dolphinscheduler.plugin.task.api.enums.DataType; +import org.apache.dolphinscheduler.plugin.task.api.enums.Direct; +import org.apache.dolphinscheduler.plugin.task.api.model.Property; +import org.apache.dolphinscheduler.plugin.task.api.parameters.AbstractParameters; +import org.apache.dolphinscheduler.plugin.task.api.parameters.SubWorkflowParameters; + +import org.apache.commons.collections4.MapUtils; + +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.Date; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.Mockito; +import org.mockito.junit.jupiter.MockitoExtension; + +import com.google.common.collect.Lists; + +@ExtendWith(MockitoExtension.class) +public class CuringParamsServiceImplTest { + + private static final String placeHolderName = "$[yyyy-MM-dd-1]"; + + @Mock + private CuringParamsService curingParamsService; + + @InjectMocks + private CuringParamsServiceImpl curingParamsServiceImpl; + + @Mock + private ProjectParameterMapper projectParameterMapper; + + private final Map paramMap = new HashMap<>(); + + @BeforeEach + public void init() { + paramMap.put("globalParams1", new Property("globalParams1", Direct.IN, DataType.VARCHAR, "Params1")); + } + + @Test + public void testConvertParameterPlaceholders() { + when(curingParamsService.convertParameterPlaceholders(placeHolderName, paramMap)) + .thenReturn("2022-06-26"); + String result = curingParamsService.convertParameterPlaceholders(placeHolderName, paramMap); + Assertions.assertNotNull(result); + } + + @Test + public void testCuringGlobalParams() { + // define globalMap + Map globalParamMap = new HashMap<>(); + globalParamMap.put("globalParams1", "Params1"); + + // define globalParamList + List globalParamList = new ArrayList<>(); + + // define scheduleTime + Date scheduleTime = DateUtils.stringToDate("2019-12-20 00:00:00"); + + // test globalParamList is null + String result = curingParamsServiceImpl.curingGlobalParams(1, globalParamMap, globalParamList, + CommandType.START_CURRENT_TASK_PROCESS, scheduleTime, null); + Assertions.assertNull(result); + Assertions.assertNull(curingParamsServiceImpl.curingGlobalParams(1, null, null, + CommandType.START_CURRENT_TASK_PROCESS, null, null)); + Assertions.assertNull(curingParamsServiceImpl.curingGlobalParams(1, globalParamMap, null, + CommandType.START_CURRENT_TASK_PROCESS, scheduleTime, null)); + + // test globalParamList is not null + Property property = new Property("testGlobalParam", Direct.IN, DataType.VARCHAR, "testGlobalParam"); + globalParamList.add(property); + + String result2 = curingParamsServiceImpl.curingGlobalParams(1, null, globalParamList, + CommandType.START_CURRENT_TASK_PROCESS, scheduleTime, null); + Assertions.assertEquals(result2, JSONUtils.toJsonString(globalParamList)); + + String result3 = curingParamsServiceImpl.curingGlobalParams(1, globalParamMap, globalParamList, + CommandType.START_CURRENT_TASK_PROCESS, null, null); + Assertions.assertEquals(result3, JSONUtils.toJsonString(globalParamList)); + + String result4 = curingParamsServiceImpl.curingGlobalParams(1, globalParamMap, globalParamList, + CommandType.START_CURRENT_TASK_PROCESS, scheduleTime, null); + Assertions.assertEquals(result4, JSONUtils.toJsonString(globalParamList)); + + // test var $ startsWith + globalParamMap.put("bizDate", "${system.biz.date}"); + globalParamMap.put("b1zCurdate", "${system.biz.curdate}"); + + Property property2 = new Property("testParamList1", Direct.IN, DataType.VARCHAR, "testParamList"); + Property property3 = new Property("testParamList2", Direct.IN, DataType.VARCHAR, "{testParamList1}"); + Property property4 = new Property("testParamList3", Direct.IN, DataType.VARCHAR, "${b1zCurdate}"); + + globalParamList.add(property2); + globalParamList.add(property3); + globalParamList.add(property4); + + String result5 = curingParamsServiceImpl.curingGlobalParams(1, globalParamMap, globalParamList, + CommandType.START_CURRENT_TASK_PROCESS, scheduleTime, null); + Assertions.assertEquals(result5, JSONUtils.toJsonString(globalParamList)); + + Property testStartParamProperty = new Property("testStartParam", Direct.IN, DataType.VARCHAR, ""); + globalParamList.add(testStartParamProperty); + Property testStartParam2Property = + new Property("testStartParam2", Direct.IN, DataType.VARCHAR, "$[yyyy-MM-dd+1]"); + globalParamList.add(testStartParam2Property); + globalParamMap.put("testStartParam", ""); + globalParamMap.put("testStartParam2", "$[yyyy-MM-dd+1]"); + + Map startParamMap = new HashMap<>(2); + startParamMap.put("testStartParam", "$[yyyyMMdd]"); + + for (Map.Entry param : globalParamMap.entrySet()) { + String val = startParamMap.get(param.getKey()); + if (val != null) { + param.setValue(val); + } + } + + String result6 = curingParamsServiceImpl.curingGlobalParams(1, globalParamMap, globalParamList, + CommandType.START_CURRENT_TASK_PROCESS, scheduleTime, null); + Assertions.assertEquals(result6, JSONUtils.toJsonString(globalParamList)); + } + + @Test + public void testParamParsingPreparation() { + TaskInstance taskInstance = new TaskInstance(); + taskInstance.setId(1); + taskInstance.setExecutePath("home/path/execute"); + + TaskDefinition taskDefinition = new TaskDefinition(); + taskDefinition.setName("TaskName-1"); + taskDefinition.setCode(1000001L); + + WorkflowInstance workflowInstance = new WorkflowInstance(); + workflowInstance.setId(2); + final BackfillWorkflowCommandParam backfillWorkflowCommandParam = BackfillWorkflowCommandParam.builder() + .timeZone("Asia/Shanghai") + .build(); + workflowInstance.setCommandParam(JSONUtils.toJsonString(backfillWorkflowCommandParam)); + workflowInstance.setHistoryCmd(CommandType.COMPLEMENT_DATA.toString()); + Property property = new Property(); + property.setDirect(Direct.IN); + property.setProp("global_params"); + property.setValue("hello world"); + property.setType(DataType.VARCHAR); + List properties = Lists.newArrayList(property); + workflowInstance.setGlobalParams(JSONUtils.toJsonString(properties)); + + WorkflowDefinition workflowDefinition = new WorkflowDefinition(); + workflowDefinition.setName("ProcessName-1"); + workflowDefinition.setProjectName("ProjectName"); + workflowDefinition.setProjectCode(3000001L); + workflowDefinition.setCode(200001L); + + Project project = new Project(); + project.setName("ProjectName"); + project.setCode(3000001L); + + workflowInstance.setWorkflowDefinitionCode(workflowDefinition.getCode()); + workflowInstance.setProjectCode(workflowDefinition.getProjectCode()); + taskInstance.setTaskCode(taskDefinition.getCode()); + taskInstance.setTaskDefinitionVersion(taskDefinition.getVersion()); + taskInstance.setProjectCode(workflowDefinition.getProjectCode()); + taskInstance.setWorkflowInstanceId(workflowInstance.getId()); + + AbstractParameters parameters = new SubWorkflowParameters(); + + when(projectParameterMapper.queryByProjectCode(Mockito.anyLong())).thenReturn(Collections.emptyList()); + + Map propertyMap = + curingParamsServiceImpl.paramParsingPreparation(taskInstance, parameters, workflowInstance, + project.getName(), workflowDefinition.getName()); + Assertions.assertNotNull(propertyMap); + Assertions.assertEquals(propertyMap.get(TaskConstants.PARAMETER_TASK_INSTANCE_ID).getValue(), + String.valueOf(taskInstance.getId())); + Assertions.assertEquals(propertyMap.get(TaskConstants.PARAMETER_TASK_EXECUTE_PATH).getValue(), + taskInstance.getExecutePath()); + Assertions.assertEquals(propertyMap.get(TaskConstants.PARAMETER_WORKFLOW_INSTANCE_ID).getValue(), + String.valueOf(workflowInstance.getId())); + Assertions.assertEquals(propertyMap.get(TaskConstants.PARAMETER_PROJECT_NAME).getValue(), + workflowDefinition.getProjectName()); + Assertions.assertEquals(propertyMap.get(TaskConstants.PARAMETER_PROJECT_CODE).getValue(), + String.valueOf(workflowDefinition.getProjectCode())); + Assertions.assertEquals(propertyMap.get(TaskConstants.PARAMETER_TASK_DEFINITION_CODE).getValue(), + String.valueOf(taskDefinition.getCode())); + Assertions.assertEquals(propertyMap.get(TaskConstants.PARAMETER_WORKFLOW_DEFINITION_CODE).getValue(), + String.valueOf(workflowDefinition.getCode())); + } + + @Test + public void testParseWorkflowStartParam() { + Map result; + // empty cmd param + Map startParamMap = new HashMap<>(); + result = curingParamsServiceImpl.parseWorkflowStartParam(startParamMap); + assertTrue(MapUtils.isEmpty(result)); + + // without key + startParamMap.put("testStartParam", "$[yyyyMMdd]"); + result = curingParamsServiceImpl.parseWorkflowStartParam(startParamMap); + assertTrue(MapUtils.isEmpty(result)); + + startParamMap.put("StartParams", "{\"param1\":\"11111\", \"param2\":\"22222\"}"); + result = curingParamsServiceImpl.parseWorkflowStartParam(startParamMap); + assertTrue(MapUtils.isNotEmpty(result)); + Assertions.assertEquals(2, result.keySet().size()); + Assertions.assertEquals("11111", result.get("param1").getValue()); + Assertions.assertEquals("22222", result.get("param2").getValue()); + } + + @Test + public void testParseWorkflowFatherParam() { + Map result; + // empty cmd param + Map startParamMap = new HashMap<>(); + result = curingParamsServiceImpl.parseWorkflowFatherParam(startParamMap); + assertTrue(MapUtils.isEmpty(result)); + + // without key + startParamMap.put("testfatherParams", "$[yyyyMMdd]"); + result = curingParamsServiceImpl.parseWorkflowFatherParam(startParamMap); + assertTrue(MapUtils.isEmpty(result)); + + startParamMap.put("fatherParams", "{\"param1\":\"11111\", \"param2\":\"22222\"}"); + result = curingParamsServiceImpl.parseWorkflowFatherParam(startParamMap); + assertTrue(MapUtils.isNotEmpty(result)); + Assertions.assertEquals(2, result.keySet().size()); + Assertions.assertEquals("11111", result.get("param1").getValue()); + Assertions.assertEquals("22222", result.get("param2").getValue()); + } + + @Test + public void testParseGlobalParamsMap() throws Exception { + WorkflowInstance workflowInstance = new WorkflowInstance(); + workflowInstance.setGlobalParams( + "[{\"prop\":\"param1\",\"value\":\"11111\"},{\"prop\":\"param2\",\"value\":\"22222\"}]"); + + Method method = CuringParamsServiceImpl.class.getDeclaredMethod("parseGlobalParamsMap", WorkflowInstance.class); + method.setAccessible(true); + + @SuppressWarnings("unchecked") + Map result = (Map) method.invoke(curingParamsServiceImpl, workflowInstance); + + assertTrue(MapUtils.isNotEmpty(result)); + Assertions.assertEquals(2, result.keySet().size()); + Assertions.assertEquals("11111", result.get("param1").getValue()); + Assertions.assertEquals("22222", result.get("param2").getValue()); + } + + @Test + public void testParseGlobalParamsMap_whenGlobalParamsIsNull() throws Exception { + WorkflowInstance workflowInstance = new WorkflowInstance(); + workflowInstance.setGlobalParams(null); + + Method method = CuringParamsServiceImpl.class.getDeclaredMethod("parseGlobalParamsMap", WorkflowInstance.class); + method.setAccessible(true); + + @SuppressWarnings("unchecked") + Map result = (Map) method.invoke(curingParamsServiceImpl, workflowInstance); + + assertTrue(MapUtils.isEmpty(result)); + } + + @Test + public void testParseGlobalParamsMap_whenGlobalParamsIsEmpty() throws Exception { + WorkflowInstance workflowInstance = new WorkflowInstance(); + workflowInstance.setGlobalParams(""); + + Method method = CuringParamsServiceImpl.class.getDeclaredMethod("parseGlobalParamsMap", WorkflowInstance.class); + method.setAccessible(true); + + @SuppressWarnings("unchecked") + Map result = (Map) method.invoke(curingParamsServiceImpl, workflowInstance); + + assertTrue(MapUtils.isEmpty(result)); + } + + @Test + public void testParseGlobalParamsMap_withNullProp() throws Exception { + WorkflowInstance workflowInstance = new WorkflowInstance(); + workflowInstance.setGlobalParams("[{\"prop\":null,\"direct\":\"IN\",\"type\":\"VARCHAR\",\"value\":\"\"}]"); + + Method method = CuringParamsServiceImpl.class.getDeclaredMethod("parseGlobalParamsMap", WorkflowInstance.class); + method.setAccessible(true); + + @SuppressWarnings("unchecked") + Map result = (Map) method.invoke(curingParamsServiceImpl, workflowInstance); + + // The current implementation will include a null key + assertTrue(MapUtils.isNotEmpty(result)); + Assertions.assertEquals(1, result.size()); + assertTrue(result.containsKey(null)); + Assertions.assertEquals("", result.get(null).getValue()); + } + + @Test + public void testParseVarPool_withValidVarPool() throws Exception { + TaskInstance taskInstance = new TaskInstance(); + taskInstance.setVarPool("[{\"prop\":\"var1\",\"value\":\"val1\"}]"); + + Method method = CuringParamsServiceImpl.class.getDeclaredMethod("parseVarPool", TaskInstance.class); + method.setAccessible(true); + + @SuppressWarnings("unchecked") + List result = (List) method.invoke(curingParamsServiceImpl, taskInstance); + + Assertions.assertFalse(result.isEmpty()); + Assertions.assertEquals(1, result.size()); + Assertions.assertEquals("var1", result.get(0).getProp()); + Assertions.assertEquals("val1", result.get(0).getValue()); + } + + @Test + public void testParseVarPool_withNullVarPool() throws Exception { + TaskInstance taskInstance = new TaskInstance(); + taskInstance.setVarPool(null); + + Method method = CuringParamsServiceImpl.class.getDeclaredMethod("parseVarPool", TaskInstance.class); + method.setAccessible(true); + + @SuppressWarnings("unchecked") + List result = (List) method.invoke(curingParamsServiceImpl, taskInstance); + + assertTrue(result.isEmpty()); + } + + @Test + public void testParseVarPool_withEmptyVarPool() throws Exception { + TaskInstance taskInstance = new TaskInstance(); + taskInstance.setVarPool(""); + + Method method = CuringParamsServiceImpl.class.getDeclaredMethod("parseVarPool", TaskInstance.class); + method.setAccessible(true); + + @SuppressWarnings("unchecked") + List result = (List) method.invoke(curingParamsServiceImpl, taskInstance); + + assertTrue(result.isEmpty()); + } + + @Test + public void testParseVarPool_withBlankVarPool_throwsException() throws NoSuchMethodException { + TaskInstance taskInstance = new TaskInstance(); + taskInstance.setVarPool(" "); + + Method method = CuringParamsServiceImpl.class.getDeclaredMethod("parseVarPool", TaskInstance.class); + method.setAccessible(true); + + InvocationTargetException exception = assertThrows(InvocationTargetException.class, + () -> method.invoke(curingParamsServiceImpl, taskInstance)); + + // Check the root cause + assertThat(exception.getCause()).isInstanceOf(IllegalArgumentException.class); + assertThat(exception.getCause().getMessage()).contains("Parse json"); + } + + @Test + public void testPreBuildBusinessParams_withScheduleTime() { + WorkflowInstance workflowInstance = new WorkflowInstance(); + workflowInstance.setScheduleTime(new Date(1234567890L)); // fixed timestamp + + Map result = curingParamsServiceImpl.preBuildBusinessParams(workflowInstance); + + assertTrue(MapUtils.isNotEmpty(result)); + Assertions.assertEquals(1, result.size()); + assertTrue(result.containsKey(DateConstants.PARAMETER_DATETIME)); + Assertions.assertEquals("19700115145607", result.get(DateConstants.PARAMETER_DATETIME).getValue()); + } + + @Test + public void testPreBuildBusinessParams_withoutScheduleTime() { + WorkflowInstance workflowInstance = new WorkflowInstance(); + workflowInstance.setScheduleTime(null); + + Map result = curingParamsServiceImpl.preBuildBusinessParams(workflowInstance); + + assertTrue(MapUtils.isEmpty(result)); + } + + @Test + public void testGetProjectParameterMap_withParameters() { + long projectCode = 123456L; + + ProjectParameter param1 = new ProjectParameter(); + param1.setParamName("env"); + param1.setParamValue("prod"); + param1.setParamDataType("VARCHAR"); + + ProjectParameter param2 = new ProjectParameter(); + param2.setParamName("timeout"); + param2.setParamValue("30"); + param2.setParamDataType("INTEGER"); + + List mockList = Arrays.asList(param1, param2); + when(projectParameterMapper.queryByProjectCode(projectCode)).thenReturn(mockList); + + Map result = curingParamsServiceImpl.getProjectParameterMap(projectCode); + + assertTrue(MapUtils.isNotEmpty(result)); + Assertions.assertEquals(2, result.size()); + assertTrue(result.containsKey("env")); + assertTrue(result.containsKey("timeout")); + + Property envProp = result.get("env"); + Assertions.assertEquals("prod", envProp.getValue()); + Assertions.assertEquals(Direct.IN, envProp.getDirect()); + Assertions.assertEquals(DataType.VARCHAR, envProp.getType()); + + Property timeoutProp = result.get("timeout"); + Assertions.assertEquals("30", timeoutProp.getValue()); + Assertions.assertEquals(DataType.INTEGER, timeoutProp.getType()); + } + + @Test + public void testGetProjectParameterMap_withNullParamName() { + long projectCode = 123456L; + + ProjectParameter param = new ProjectParameter(); + param.setParamName(null); // ← null paramName + param.setParamValue("test-value"); + param.setParamDataType("VARCHAR"); + + List mockList = Collections.singletonList(param); + when(projectParameterMapper.queryByProjectCode(projectCode)).thenReturn(mockList); + + Map result = curingParamsServiceImpl.getProjectParameterMap(projectCode); + + assertTrue(MapUtils.isNotEmpty(result)); + Assertions.assertEquals(1, result.size()); + assertTrue(result.containsKey(null)); // null key is present + Property prop = result.get(null); + Assertions.assertEquals("test-value", prop.getValue()); + Assertions.assertEquals(DataType.VARCHAR, prop.getType()); + } + + @Test + public void testGetProjectParameterMap_withNoParameters() { + long projectCode = 999L; + when(projectParameterMapper.queryByProjectCode(projectCode)).thenReturn(Collections.emptyList()); + + Map result = curingParamsServiceImpl.getProjectParameterMap(projectCode); + + assertTrue(MapUtils.isEmpty(result)); + } + + @Test + public void testSafePutAll() throws Exception { + // Arrange + Map target = new HashMap<>(); + Map source = new HashMap<>(); + + Property prop1 = new Property(); + prop1.setProp("validKey"); + prop1.setValue("validValue"); + + Property prop2 = new Property(); + prop2.setProp(""); // invalid: blank key + prop2.setValue("shouldBeSkipped"); + + Property prop3 = new Property(); + prop3.setProp("anotherValid"); + prop3.setValue("anotherValue"); + + source.put("validKey", prop1); + source.put("", prop2); // blank key → should be skipped + source.put("anotherValid", prop3); + source.put(null, prop1); // null key → should be skipped + source.put("nullValueKey", null); // null value → should be skipped + + // Get private method + Method method = CuringParamsServiceImpl.class.getDeclaredMethod( + "safePutAll", + Map.class, + Map.class); + method.setAccessible(true); + + // Act + method.invoke(curingParamsServiceImpl, target, source); + + // Assert + Assertions.assertEquals(2, target.size()); + assertTrue(target.containsKey("validKey")); + assertTrue(target.containsKey("anotherValid")); + Assertions.assertEquals("validValue", target.get("validKey").getValue()); + Assertions.assertEquals("anotherValue", target.get("anotherValid").getValue()); + + // Ensure invalid entries were NOT added + Assertions.assertFalse(target.containsKey("")); + Assertions.assertFalse(target.containsKey(null)); + } + + @Test + public void testResolvePlaceholders() throws Exception { + // Arrange: prepare a paramsMap with placeholders and references + Map paramsMap = new HashMap<>(); + + Property p1 = new Property(); + p1.setProp("name"); + p1.setValue("Alice"); + + Property p2 = new Property(); + p2.setProp("greeting"); + p2.setValue("Hello, ${name}!"); // contains placeholder + + Property p3 = new Property(); + p3.setProp("farewell"); + p3.setValue("${greeting} Goodbye."); // chained reference + + Property p4 = new Property(); + p4.setProp("static"); + p4.setValue("no placeholder"); // should remain unchanged + + paramsMap.put("name", p1); + paramsMap.put("greeting", p2); + paramsMap.put("farewell", p3); + paramsMap.put("static", p4); + + // Get the private method via reflection + Method method = CuringParamsServiceImpl.class.getDeclaredMethod( + "resolvePlaceholders", + Map.class); + method.setAccessible(true); + + // Act: invoke the private method + method.invoke(curingParamsServiceImpl, paramsMap); + + // Assert: check that placeholders were resolved correctly + Assertions.assertEquals("Alice", paramsMap.get("name").getValue()); // unchanged + Assertions.assertEquals("Hello, Alice!", paramsMap.get("greeting").getValue()); + Assertions.assertEquals("Hello, Alice! Goodbye.", paramsMap.get("farewell").getValue()); + Assertions.assertEquals("no placeholder", paramsMap.get("static").getValue()); + + // Ensure no unintended side effects + Assertions.assertEquals(4, paramsMap.size()); + } +} diff --git a/dolphinscheduler-service/src/test/java/org/apache/dolphinscheduler/service/expand/CuringParamsServiceTest.java b/dolphinscheduler-service/src/test/java/org/apache/dolphinscheduler/service/expand/CuringParamsServiceTest.java deleted file mode 100644 index f0f2874a9d29..000000000000 --- a/dolphinscheduler-service/src/test/java/org/apache/dolphinscheduler/service/expand/CuringParamsServiceTest.java +++ /dev/null @@ -1,273 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one or more - * contributor license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright ownership. - * The ASF licenses this file to You under the Apache License, Version 2.0 - * (the "License"); you may not use this file except in compliance with - * the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.apache.dolphinscheduler.service.expand; - -import org.apache.dolphinscheduler.common.enums.CommandType; -import org.apache.dolphinscheduler.common.utils.DateUtils; -import org.apache.dolphinscheduler.common.utils.JSONUtils; -import org.apache.dolphinscheduler.dao.entity.Project; -import org.apache.dolphinscheduler.dao.entity.TaskDefinition; -import org.apache.dolphinscheduler.dao.entity.TaskInstance; -import org.apache.dolphinscheduler.dao.entity.WorkflowDefinition; -import org.apache.dolphinscheduler.dao.entity.WorkflowInstance; -import org.apache.dolphinscheduler.dao.mapper.ProjectParameterMapper; -import org.apache.dolphinscheduler.extract.master.command.BackfillWorkflowCommandParam; -import org.apache.dolphinscheduler.plugin.task.api.TaskConstants; -import org.apache.dolphinscheduler.plugin.task.api.enums.DataType; -import org.apache.dolphinscheduler.plugin.task.api.enums.Direct; -import org.apache.dolphinscheduler.plugin.task.api.model.Property; -import org.apache.dolphinscheduler.plugin.task.api.parameters.AbstractParameters; -import org.apache.dolphinscheduler.plugin.task.api.parameters.SubWorkflowParameters; - -import org.apache.commons.collections4.MapUtils; - -import java.util.ArrayList; -import java.util.Collections; -import java.util.Date; -import java.util.HashMap; -import java.util.List; -import java.util.Map; - -import org.junit.jupiter.api.Assertions; -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Test; -import org.junit.jupiter.api.extension.ExtendWith; -import org.mockito.InjectMocks; -import org.mockito.Mock; -import org.mockito.Mockito; -import org.mockito.junit.jupiter.MockitoExtension; - -import com.google.common.collect.Lists; - -@ExtendWith(MockitoExtension.class) -public class CuringParamsServiceTest { - - private static final String placeHolderName = "$[yyyy-MM-dd-1]"; - - @Mock - private CuringParamsService curingGlobalParamsService; - - @InjectMocks - private CuringParamsServiceImpl dolphinSchedulerCuringGlobalParams; - - @Mock - private ProjectParameterMapper projectParameterMapper; - - private final Map globalParamMap = new HashMap<>(); - private final Map paramMap = new HashMap<>(); - - @BeforeEach - public void init() { - globalParamMap.put("globalParams1", "Params1"); - paramMap.put("globalParams1", new Property("globalParams1", Direct.IN, DataType.VARCHAR, "Params1")); - } - - @Test - public void testConvertParameterPlaceholders() { - Mockito.when(curingGlobalParamsService.convertParameterPlaceholders(placeHolderName, paramMap)) - .thenReturn("2022-06-26"); - String result = curingGlobalParamsService.convertParameterPlaceholders(placeHolderName, paramMap); - Assertions.assertNotNull(result); - } - - @Test - public void testCuringGlobalParams() { - // define globalMap - Map globalParamMap = new HashMap<>(); - globalParamMap.put("globalParams1", "Params1"); - - // define globalParamList - List globalParamList = new ArrayList<>(); - - // define scheduleTime - Date scheduleTime = DateUtils.stringToDate("2019-12-20 00:00:00"); - - // test globalParamList is null - String result = dolphinSchedulerCuringGlobalParams.curingGlobalParams(1, globalParamMap, globalParamList, - CommandType.START_CURRENT_TASK_PROCESS, scheduleTime, null); - Assertions.assertNull(result); - Assertions.assertNull(dolphinSchedulerCuringGlobalParams.curingGlobalParams(1, null, null, - CommandType.START_CURRENT_TASK_PROCESS, null, null)); - Assertions.assertNull(dolphinSchedulerCuringGlobalParams.curingGlobalParams(1, globalParamMap, null, - CommandType.START_CURRENT_TASK_PROCESS, scheduleTime, null)); - - // test globalParamList is not null - Property property = new Property("testGlobalParam", Direct.IN, DataType.VARCHAR, "testGlobalParam"); - globalParamList.add(property); - - String result2 = dolphinSchedulerCuringGlobalParams.curingGlobalParams(1, null, globalParamList, - CommandType.START_CURRENT_TASK_PROCESS, scheduleTime, null); - Assertions.assertEquals(result2, JSONUtils.toJsonString(globalParamList)); - - String result3 = dolphinSchedulerCuringGlobalParams.curingGlobalParams(1, globalParamMap, globalParamList, - CommandType.START_CURRENT_TASK_PROCESS, null, null); - Assertions.assertEquals(result3, JSONUtils.toJsonString(globalParamList)); - - String result4 = dolphinSchedulerCuringGlobalParams.curingGlobalParams(1, globalParamMap, globalParamList, - CommandType.START_CURRENT_TASK_PROCESS, scheduleTime, null); - Assertions.assertEquals(result4, JSONUtils.toJsonString(globalParamList)); - - // test var $ startsWith - globalParamMap.put("bizDate", "${system.biz.date}"); - globalParamMap.put("b1zCurdate", "${system.biz.curdate}"); - - Property property2 = new Property("testParamList1", Direct.IN, DataType.VARCHAR, "testParamList"); - Property property3 = new Property("testParamList2", Direct.IN, DataType.VARCHAR, "{testParamList1}"); - Property property4 = new Property("testParamList3", Direct.IN, DataType.VARCHAR, "${b1zCurdate}"); - - globalParamList.add(property2); - globalParamList.add(property3); - globalParamList.add(property4); - - String result5 = dolphinSchedulerCuringGlobalParams.curingGlobalParams(1, globalParamMap, globalParamList, - CommandType.START_CURRENT_TASK_PROCESS, scheduleTime, null); - Assertions.assertEquals(result5, JSONUtils.toJsonString(globalParamList)); - - Property testStartParamProperty = new Property("testStartParam", Direct.IN, DataType.VARCHAR, ""); - globalParamList.add(testStartParamProperty); - Property testStartParam2Property = - new Property("testStartParam2", Direct.IN, DataType.VARCHAR, "$[yyyy-MM-dd+1]"); - globalParamList.add(testStartParam2Property); - globalParamMap.put("testStartParam", ""); - globalParamMap.put("testStartParam2", "$[yyyy-MM-dd+1]"); - - Map startParamMap = new HashMap<>(2); - startParamMap.put("testStartParam", "$[yyyyMMdd]"); - - for (Map.Entry param : globalParamMap.entrySet()) { - String val = startParamMap.get(param.getKey()); - if (val != null) { - param.setValue(val); - } - } - - String result6 = dolphinSchedulerCuringGlobalParams.curingGlobalParams(1, globalParamMap, globalParamList, - CommandType.START_CURRENT_TASK_PROCESS, scheduleTime, null); - Assertions.assertEquals(result6, JSONUtils.toJsonString(globalParamList)); - } - - @Test - public void testParamParsingPreparation() { - TaskInstance taskInstance = new TaskInstance(); - taskInstance.setId(1); - taskInstance.setExecutePath("home/path/execute"); - - TaskDefinition taskDefinition = new TaskDefinition(); - taskDefinition.setName("TaskName-1"); - taskDefinition.setCode(1000001l); - - WorkflowInstance workflowInstance = new WorkflowInstance(); - workflowInstance.setId(2); - final BackfillWorkflowCommandParam backfillWorkflowCommandParam = BackfillWorkflowCommandParam.builder() - .timeZone("Asia/Shanghai") - .build(); - workflowInstance.setCommandParam(JSONUtils.toJsonString(backfillWorkflowCommandParam)); - workflowInstance.setHistoryCmd(CommandType.COMPLEMENT_DATA.toString()); - Property property = new Property(); - property.setDirect(Direct.IN); - property.setProp("global_params"); - property.setValue("hello world"); - property.setType(DataType.VARCHAR); - List properties = Lists.newArrayList(property); - workflowInstance.setGlobalParams(JSONUtils.toJsonString(properties)); - - WorkflowDefinition workflowDefinition = new WorkflowDefinition(); - workflowDefinition.setName("ProcessName-1"); - workflowDefinition.setProjectName("ProjectName-1"); - workflowDefinition.setProjectCode(3000001L); - workflowDefinition.setCode(200001L); - - Project project = new Project(); - project.setName("ProjectName"); - project.setCode(3000001L); - - workflowInstance.setWorkflowDefinitionCode(workflowDefinition.getCode()); - workflowInstance.setProjectCode(workflowDefinition.getProjectCode()); - taskInstance.setTaskCode(taskDefinition.getCode()); - taskInstance.setTaskDefinitionVersion(taskDefinition.getVersion()); - taskInstance.setProjectCode(workflowDefinition.getProjectCode()); - taskInstance.setWorkflowInstanceId(workflowInstance.getId()); - - AbstractParameters parameters = new SubWorkflowParameters(); - - Mockito.when(projectParameterMapper.queryByProjectCode(Mockito.anyLong())).thenReturn(Collections.emptyList()); - - Map propertyMap = - dolphinSchedulerCuringGlobalParams.paramParsingPreparation(taskInstance, parameters, workflowInstance, - project.getName(), workflowDefinition.getName()); - Assertions.assertNotNull(propertyMap); - Assertions.assertEquals(propertyMap.get(TaskConstants.PARAMETER_TASK_INSTANCE_ID).getValue(), - String.valueOf(taskInstance.getId())); - Assertions.assertEquals(propertyMap.get(TaskConstants.PARAMETER_TASK_EXECUTE_PATH).getValue(), - taskInstance.getExecutePath()); - Assertions.assertEquals(propertyMap.get(TaskConstants.PARAMETER_WORKFLOW_INSTANCE_ID).getValue(), - String.valueOf(workflowInstance.getId())); - // Assertions.assertEquals(propertyMap.get(TaskConstants.PARAMETER_WORKFLOW_DEFINITION_NAME).getValue(), - // processDefinition.getName()); - // Assertions.assertEquals(propertyMap.get(TaskConstants.PARAMETER_PROJECT_NAME).getValue(), - // processDefinition.getProjectName()); - Assertions.assertEquals(propertyMap.get(TaskConstants.PARAMETER_PROJECT_CODE).getValue(), - String.valueOf(workflowDefinition.getProjectCode())); - Assertions.assertEquals(propertyMap.get(TaskConstants.PARAMETER_TASK_DEFINITION_CODE).getValue(), - String.valueOf(taskDefinition.getCode())); - Assertions.assertEquals(propertyMap.get(TaskConstants.PARAMETER_WORKFLOW_DEFINITION_CODE).getValue(), - String.valueOf(workflowDefinition.getCode())); - } - - @Test - public void testParseWorkflowStartParam() { - Map result = new HashMap<>(); - // empty cmd param - Map startParamMap = new HashMap<>(); - result = dolphinSchedulerCuringGlobalParams.parseWorkflowStartParam(startParamMap); - Assertions.assertTrue(MapUtils.isEmpty(result)); - - // without key - startParamMap.put("testStartParam", "$[yyyyMMdd]"); - result = dolphinSchedulerCuringGlobalParams.parseWorkflowStartParam(startParamMap); - Assertions.assertTrue(MapUtils.isEmpty(result)); - - startParamMap.put("StartParams", "{\"param1\":\"11111\", \"param2\":\"22222\"}"); - result = dolphinSchedulerCuringGlobalParams.parseWorkflowStartParam(startParamMap); - Assertions.assertTrue(MapUtils.isNotEmpty(result)); - Assertions.assertEquals(2, result.keySet().size()); - Assertions.assertEquals("11111", result.get("param1").getValue()); - Assertions.assertEquals("22222", result.get("param2").getValue()); - } - - @Test - public void testParseWorkflowFatherParam() { - Map result = new HashMap<>(); - // empty cmd param - Map startParamMap = new HashMap<>(); - result = dolphinSchedulerCuringGlobalParams.parseWorkflowFatherParam(startParamMap); - Assertions.assertTrue(MapUtils.isEmpty(result)); - - // without key - startParamMap.put("testfatherParams", "$[yyyyMMdd]"); - result = dolphinSchedulerCuringGlobalParams.parseWorkflowFatherParam(startParamMap); - Assertions.assertTrue(MapUtils.isEmpty(result)); - - startParamMap.put("fatherParams", "{\"param1\":\"11111\", \"param2\":\"22222\"}"); - result = dolphinSchedulerCuringGlobalParams.parseWorkflowFatherParam(startParamMap); - Assertions.assertTrue(MapUtils.isNotEmpty(result)); - Assertions.assertEquals(2, result.keySet().size()); - Assertions.assertEquals("11111", result.get("param1").getValue()); - Assertions.assertEquals("22222", result.get("param2").getValue()); - } -} diff --git a/dolphinscheduler-task-plugin/dolphinscheduler-task-api/src/test/java/org/apache/dolphinscheduler/plugin/task/api/parameters/AbstractParametersTest.java b/dolphinscheduler-task-plugin/dolphinscheduler-task-api/src/test/java/org/apache/dolphinscheduler/plugin/task/api/parameters/AbstractParametersTest.java index 435b5f60c2ed..7197dbe07e24 100644 --- a/dolphinscheduler-task-plugin/dolphinscheduler-task-api/src/test/java/org/apache/dolphinscheduler/plugin/task/api/parameters/AbstractParametersTest.java +++ b/dolphinscheduler-task-plugin/dolphinscheduler-task-api/src/test/java/org/apache/dolphinscheduler/plugin/task/api/parameters/AbstractParametersTest.java @@ -30,6 +30,13 @@ public class AbstractParametersTest { + /** + * Verifies that getInputLocalParametersMap() includes parameters with: + * - direct = null (treated as IN by default) + * - direct = Direct.IN + * and excludes parameters with: + * - direct = Direct.OUT + */ @Test public void testGetInputLocalParametersMap() { AbstractParameters parameters = new AbstractParameters() { @@ -52,4 +59,81 @@ public boolean checkParameters() { Assertions.assertTrue(inputLocalParametersMap.containsKey("key1")); Assertions.assertTrue(inputLocalParametersMap.containsKey("key2")); } + + /** + * Tests behavior when a Property has a null 'prop' field. + * + * ⚠️ WARNING: The current implementation will insert a (null, Property) entry into the map, + * which causes JSON serialization to fail with: + * "Null key for a Map not allowed in JSON" + * + * This test exposes the risk. After fixing the method to skip null/empty prop names, + * this test should assert size == 0. + */ + @Test + public void testGetInputLocalParametersMap_withNullProp_shouldNotPutNullKey() { + AbstractParameters parameters = new AbstractParameters() { + + @Override + public boolean checkParameters() { + return false; + } + }; + + List localParams = new ArrayList<>(); + // Dangerous: prop is null + localParams.add(new Property(null, Direct.IN, DataType.VARCHAR, "dangerValue")); + + parameters.setLocalParams(localParams); + + Map inputLocalParametersMap = parameters.getInputLocalParametersMap(); + + // Current behavior: null key is inserted + Assertions.assertEquals(1, inputLocalParametersMap.size()); + Assertions.assertTrue(inputLocalParametersMap.containsKey(null)); // ❌ This breaks JSON serialization! + + } + + /** + * Tests behavior when a Property has an empty string as 'prop'. + * While Java allows empty string keys, they may cause issues downstream (e.g., template parsing). + */ + @Test + public void testGetInputLocalParametersMap_withEmptyProp() { + AbstractParameters parameters = new AbstractParameters() { + + @Override + public boolean checkParameters() { + return false; + } + }; + + List localParams = new ArrayList<>(); + localParams.add(new Property("", Direct.IN, DataType.VARCHAR, "emptyKeyVal")); + + parameters.setLocalParams(localParams); + + Map inputLocalParametersMap = parameters.getInputLocalParametersMap(); + + Assertions.assertEquals(1, inputLocalParametersMap.size()); + Assertions.assertTrue(inputLocalParametersMap.containsKey("")); + } + + /** + * Ensures the method handles null localParams gracefully (returns empty map). + */ + @Test + public void testGetInputLocalParametersMap_localParamsIsNull() { + AbstractParameters parameters = new AbstractParameters() { + + @Override + public boolean checkParameters() { + return false; + } + }; + parameters.setLocalParams(null); + + Map result = parameters.getInputLocalParametersMap(); + Assertions.assertEquals(0, result.size()); + } } diff --git a/dolphinscheduler-task-plugin/dolphinscheduler-task-shell/src/test/java/org/apache/dolphinscheduler/plugin/task/shell/ShellTaskChannelTest.java b/dolphinscheduler-task-plugin/dolphinscheduler-task-shell/src/test/java/org/apache/dolphinscheduler/plugin/task/shell/ShellTaskChannelTest.java new file mode 100644 index 000000000000..7dc2975f9b8b --- /dev/null +++ b/dolphinscheduler-task-plugin/dolphinscheduler-task-shell/src/test/java/org/apache/dolphinscheduler/plugin/task/shell/ShellTaskChannelTest.java @@ -0,0 +1,137 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.dolphinscheduler.plugin.task.shell; + +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 static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertThrows; + +import org.apache.dolphinscheduler.plugin.task.api.model.ResourceInfo; +import org.apache.dolphinscheduler.plugin.task.api.parameters.AbstractParameters; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +public class ShellTaskChannelTest { + + private final ShellTaskChannel shellTaskChannel = new ShellTaskChannel(); + + @Test + @DisplayName("parseParameters should return ShellParameters when given valid JSON") + public void testParseParametersWithValidJson() { + String validTaskParams = "{\n" + + " \"rawScript\": \"echo 'hello world'\",\n" + + " \"localParams\": [\n" + + " {\n" + + " \"prop\": \"name\",\n" + + " \"direct\": \"IN\",\n" + + " \"type\": \"VARCHAR\",\n" + + " \"value\": \"test\"\n" + + " }\n" + + " ]\n" + + "}"; + + AbstractParameters params = shellTaskChannel.parseParameters(validTaskParams); + + assertNotNull(params, "Parsed parameters should not be null"); + assertInstanceOf(ShellParameters.class, params, "Should be instance of ShellParameters"); + + ShellParameters shellParams = (ShellParameters) params; + assertEquals("echo 'hello world'", shellParams.getRawScript()); + assertNotNull(shellParams.getLocalParams()); + assertEquals(1, shellParams.getLocalParams().size()); + assertEquals("name", shellParams.getLocalParams().get(0).getProp()); + } + + @Test + @DisplayName("parseParameters should parse task_params with simple script and one resource") + public void testParseShellTaskParamsWithSimpleScript() { + // Given: 转义后的 JSON 字符串(JDK 8 兼容) + String taskParams = "{\n" + + " \"localParams\": [],\n" + + " \"rawScript\": \"#!/bin/bash\\nset -e\\n\\n\\n \\necho \\\"====================================\\\"\",\n" + + + " \"resourceList\": [\n" + + " {\n" + + " \"resourceName\": \"hdfs://abc/dolphinscheduler/default/123.sql\"\n" + + " }\n" + + " ]\n" + + "}"; + + // When + AbstractParameters parsed = shellTaskChannel.parseParameters(taskParams); + + // Then + assertNotNull(parsed, "Parsed parameters must not be null"); + assertInstanceOf(ShellParameters.class, parsed, "Should be instance of ShellParameters"); + + ShellParameters params = (ShellParameters) parsed; + + // Verify rawScript + String expectedRawScript = "#!/bin/bash\nset -e\n\n\n \necho \"====================================\""; + assertEquals(expectedRawScript, params.getRawScript(), "rawScript content mismatch"); + + // Verify localParams is empty + assertNotNull(params.getLocalParams(), "localParams should not be null"); + assertEquals(0, params.getLocalParams().size(), "localParams should be empty list"); + + // Verify resourceList + assertNotNull(params.getResourceList(), "resourceList should not be null"); + assertEquals(1, params.getResourceList().size(), "resourceList should contain exactly one item"); + + ResourceInfo resource = params.getResourceList().get(0); + assertEquals( + "hdfs://abc/dolphinscheduler/default/123.sql", + resource.getResourceName(), + "Resource name does not match"); + } + + @Test + @DisplayName("parseParameters should return empty ShellParameters when given empty JSON object") + public void testParseParametersWithEmptyJson() { + String emptyJson = "{}"; + AbstractParameters params = shellTaskChannel.parseParameters(emptyJson); + assertNotNull(params); + assertInstanceOf(ShellParameters.class, params); + assertNull(((ShellParameters) params).getRawScript()); + } + + @Test + @DisplayName("parseParameters should handle null input gracefully") + public void testParseParametersWithNullInput() { + assertNull(shellTaskChannel.parseParameters(null)); + } + + @Test + @DisplayName("parseParameters should handle empty string input") + public void testParseParametersWithEmptyString() { + assertNull(shellTaskChannel.parseParameters("")); + } + + @Test + @DisplayName("parseParameters should throw exception on malformed JSON") + public void testParseParametersWithInvalidJson() { + String invalidJson = "{ rawScript: 'missing quotes' }"; + + assertThrows(RuntimeException.class, () -> { + shellTaskChannel.parseParameters(invalidJson); + }); + } +} From 9d7dd46b2e63852c61ac2c449d9c0d8b67fb2e7b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E8=8B=8F=E4=B9=89=E8=B6=85?= Date: Fri, 21 Nov 2025 15:56:28 +0800 Subject: [PATCH 2/7] update test --- .../expand/CuringParamsServiceImplTest.java | 80 +++++++++---------- 1 file changed, 39 insertions(+), 41 deletions(-) diff --git a/dolphinscheduler-service/src/test/java/org/apache/dolphinscheduler/service/expand/CuringParamsServiceImplTest.java b/dolphinscheduler-service/src/test/java/org/apache/dolphinscheduler/service/expand/CuringParamsServiceImplTest.java index 1eea462f4a8d..8d63421988f9 100644 --- a/dolphinscheduler-service/src/test/java/org/apache/dolphinscheduler/service/expand/CuringParamsServiceImplTest.java +++ b/dolphinscheduler-service/src/test/java/org/apache/dolphinscheduler/service/expand/CuringParamsServiceImplTest.java @@ -17,11 +17,6 @@ package org.apache.dolphinscheduler.service.expand; -import static com.google.common.truth.Truth.assertThat; -import static org.junit.jupiter.api.Assertions.assertThrows; -import static org.junit.jupiter.api.Assertions.assertTrue; -import static org.mockito.Mockito.when; - import org.apache.dolphinscheduler.common.constants.DateConstants; import org.apache.dolphinscheduler.common.enums.CommandType; import org.apache.dolphinscheduler.common.utils.DateUtils; @@ -63,11 +58,12 @@ import org.mockito.junit.jupiter.MockitoExtension; import com.google.common.collect.Lists; +import com.google.common.truth.Truth; @ExtendWith(MockitoExtension.class) public class CuringParamsServiceImplTest { - private static final String placeHolderName = "$[yyyy-MM-dd-1]"; + private static final String YESTERDAY_DATE_PLACEHOLDER = "$[yyyy-MM-dd-1]"; @Mock private CuringParamsService curingParamsService; @@ -87,9 +83,9 @@ public void init() { @Test public void testConvertParameterPlaceholders() { - when(curingParamsService.convertParameterPlaceholders(placeHolderName, paramMap)) + Mockito.when(curingParamsService.convertParameterPlaceholders(YESTERDAY_DATE_PLACEHOLDER, paramMap)) .thenReturn("2022-06-26"); - String result = curingParamsService.convertParameterPlaceholders(placeHolderName, paramMap); + String result = curingParamsService.convertParameterPlaceholders(YESTERDAY_DATE_PLACEHOLDER, paramMap); Assertions.assertNotNull(result); } @@ -213,7 +209,7 @@ public void testParamParsingPreparation() { AbstractParameters parameters = new SubWorkflowParameters(); - when(projectParameterMapper.queryByProjectCode(Mockito.anyLong())).thenReturn(Collections.emptyList()); + Mockito.when(projectParameterMapper.queryByProjectCode(Mockito.anyLong())).thenReturn(Collections.emptyList()); Map propertyMap = curingParamsServiceImpl.paramParsingPreparation(taskInstance, parameters, workflowInstance, @@ -241,16 +237,16 @@ public void testParseWorkflowStartParam() { // empty cmd param Map startParamMap = new HashMap<>(); result = curingParamsServiceImpl.parseWorkflowStartParam(startParamMap); - assertTrue(MapUtils.isEmpty(result)); + Assertions.assertTrue(MapUtils.isEmpty(result)); // without key startParamMap.put("testStartParam", "$[yyyyMMdd]"); result = curingParamsServiceImpl.parseWorkflowStartParam(startParamMap); - assertTrue(MapUtils.isEmpty(result)); + Assertions.assertTrue(MapUtils.isEmpty(result)); startParamMap.put("StartParams", "{\"param1\":\"11111\", \"param2\":\"22222\"}"); result = curingParamsServiceImpl.parseWorkflowStartParam(startParamMap); - assertTrue(MapUtils.isNotEmpty(result)); + Assertions.assertTrue(MapUtils.isNotEmpty(result)); Assertions.assertEquals(2, result.keySet().size()); Assertions.assertEquals("11111", result.get("param1").getValue()); Assertions.assertEquals("22222", result.get("param2").getValue()); @@ -262,16 +258,16 @@ public void testParseWorkflowFatherParam() { // empty cmd param Map startParamMap = new HashMap<>(); result = curingParamsServiceImpl.parseWorkflowFatherParam(startParamMap); - assertTrue(MapUtils.isEmpty(result)); + Assertions.assertTrue(MapUtils.isEmpty(result)); // without key startParamMap.put("testfatherParams", "$[yyyyMMdd]"); result = curingParamsServiceImpl.parseWorkflowFatherParam(startParamMap); - assertTrue(MapUtils.isEmpty(result)); + Assertions.assertTrue(MapUtils.isEmpty(result)); startParamMap.put("fatherParams", "{\"param1\":\"11111\", \"param2\":\"22222\"}"); result = curingParamsServiceImpl.parseWorkflowFatherParam(startParamMap); - assertTrue(MapUtils.isNotEmpty(result)); + Assertions.assertTrue(MapUtils.isNotEmpty(result)); Assertions.assertEquals(2, result.keySet().size()); Assertions.assertEquals("11111", result.get("param1").getValue()); Assertions.assertEquals("22222", result.get("param2").getValue()); @@ -289,7 +285,7 @@ public void testParseGlobalParamsMap() throws Exception { @SuppressWarnings("unchecked") Map result = (Map) method.invoke(curingParamsServiceImpl, workflowInstance); - assertTrue(MapUtils.isNotEmpty(result)); + Assertions.assertTrue(MapUtils.isNotEmpty(result)); Assertions.assertEquals(2, result.keySet().size()); Assertions.assertEquals("11111", result.get("param1").getValue()); Assertions.assertEquals("22222", result.get("param2").getValue()); @@ -306,7 +302,7 @@ public void testParseGlobalParamsMap_whenGlobalParamsIsNull() throws Exception { @SuppressWarnings("unchecked") Map result = (Map) method.invoke(curingParamsServiceImpl, workflowInstance); - assertTrue(MapUtils.isEmpty(result)); + Assertions.assertTrue(MapUtils.isEmpty(result)); } @Test @@ -320,7 +316,7 @@ public void testParseGlobalParamsMap_whenGlobalParamsIsEmpty() throws Exception @SuppressWarnings("unchecked") Map result = (Map) method.invoke(curingParamsServiceImpl, workflowInstance); - assertTrue(MapUtils.isEmpty(result)); + Assertions.assertTrue(MapUtils.isEmpty(result)); } @Test @@ -335,9 +331,9 @@ public void testParseGlobalParamsMap_withNullProp() throws Exception { Map result = (Map) method.invoke(curingParamsServiceImpl, workflowInstance); // The current implementation will include a null key - assertTrue(MapUtils.isNotEmpty(result)); + Assertions.assertTrue(MapUtils.isNotEmpty(result)); Assertions.assertEquals(1, result.size()); - assertTrue(result.containsKey(null)); + Assertions.assertTrue(result.containsKey(null)); Assertions.assertEquals("", result.get(null).getValue()); } @@ -369,7 +365,7 @@ public void testParseVarPool_withNullVarPool() throws Exception { @SuppressWarnings("unchecked") List result = (List) method.invoke(curingParamsServiceImpl, taskInstance); - assertTrue(result.isEmpty()); + Assertions.assertTrue(result.isEmpty()); } @Test @@ -383,7 +379,7 @@ public void testParseVarPool_withEmptyVarPool() throws Exception { @SuppressWarnings("unchecked") List result = (List) method.invoke(curingParamsServiceImpl, taskInstance); - assertTrue(result.isEmpty()); + Assertions.assertTrue(result.isEmpty()); } @Test @@ -394,25 +390,27 @@ public void testParseVarPool_withBlankVarPool_throwsException() throws NoSuchMet Method method = CuringParamsServiceImpl.class.getDeclaredMethod("parseVarPool", TaskInstance.class); method.setAccessible(true); - InvocationTargetException exception = assertThrows(InvocationTargetException.class, + InvocationTargetException exception = Assertions.assertThrows(InvocationTargetException.class, () -> method.invoke(curingParamsServiceImpl, taskInstance)); // Check the root cause - assertThat(exception.getCause()).isInstanceOf(IllegalArgumentException.class); - assertThat(exception.getCause().getMessage()).contains("Parse json"); + Truth.assertThat(exception.getCause()).isInstanceOf(IllegalArgumentException.class); + Truth.assertThat(exception.getCause().getMessage()).contains("Parse json"); } @Test public void testPreBuildBusinessParams_withScheduleTime() { + // 1234567890 ms since epoch = 1970-01-15T06:56:07Z WorkflowInstance workflowInstance = new WorkflowInstance(); - workflowInstance.setScheduleTime(new Date(1234567890L)); // fixed timestamp + workflowInstance.setScheduleTime(new Date(1234567890L)); Map result = curingParamsServiceImpl.preBuildBusinessParams(workflowInstance); - assertTrue(MapUtils.isNotEmpty(result)); + Assertions.assertTrue(MapUtils.isNotEmpty(result)); Assertions.assertEquals(1, result.size()); - assertTrue(result.containsKey(DateConstants.PARAMETER_DATETIME)); - Assertions.assertEquals("19700115145607", result.get(DateConstants.PARAMETER_DATETIME).getValue()); + Assertions.assertTrue(result.containsKey(DateConstants.PARAMETER_DATETIME)); + // Expect UTC time string + Assertions.assertEquals("19700115065607", result.get(DateConstants.PARAMETER_DATETIME).getValue()); } @Test @@ -422,7 +420,7 @@ public void testPreBuildBusinessParams_withoutScheduleTime() { Map result = curingParamsServiceImpl.preBuildBusinessParams(workflowInstance); - assertTrue(MapUtils.isEmpty(result)); + Assertions.assertTrue(MapUtils.isEmpty(result)); } @Test @@ -440,14 +438,14 @@ public void testGetProjectParameterMap_withParameters() { param2.setParamDataType("INTEGER"); List mockList = Arrays.asList(param1, param2); - when(projectParameterMapper.queryByProjectCode(projectCode)).thenReturn(mockList); + Mockito.when(projectParameterMapper.queryByProjectCode(projectCode)).thenReturn(mockList); Map result = curingParamsServiceImpl.getProjectParameterMap(projectCode); - assertTrue(MapUtils.isNotEmpty(result)); + Assertions.assertTrue(MapUtils.isNotEmpty(result)); Assertions.assertEquals(2, result.size()); - assertTrue(result.containsKey("env")); - assertTrue(result.containsKey("timeout")); + Assertions.assertTrue(result.containsKey("env")); + Assertions.assertTrue(result.containsKey("timeout")); Property envProp = result.get("env"); Assertions.assertEquals("prod", envProp.getValue()); @@ -469,13 +467,13 @@ public void testGetProjectParameterMap_withNullParamName() { param.setParamDataType("VARCHAR"); List mockList = Collections.singletonList(param); - when(projectParameterMapper.queryByProjectCode(projectCode)).thenReturn(mockList); + Mockito.when(projectParameterMapper.queryByProjectCode(projectCode)).thenReturn(mockList); Map result = curingParamsServiceImpl.getProjectParameterMap(projectCode); - assertTrue(MapUtils.isNotEmpty(result)); + Assertions.assertTrue(MapUtils.isNotEmpty(result)); Assertions.assertEquals(1, result.size()); - assertTrue(result.containsKey(null)); // null key is present + Assertions.assertTrue(result.containsKey(null)); // null key is present Property prop = result.get(null); Assertions.assertEquals("test-value", prop.getValue()); Assertions.assertEquals(DataType.VARCHAR, prop.getType()); @@ -484,11 +482,11 @@ public void testGetProjectParameterMap_withNullParamName() { @Test public void testGetProjectParameterMap_withNoParameters() { long projectCode = 999L; - when(projectParameterMapper.queryByProjectCode(projectCode)).thenReturn(Collections.emptyList()); + Mockito.when(projectParameterMapper.queryByProjectCode(projectCode)).thenReturn(Collections.emptyList()); Map result = curingParamsServiceImpl.getProjectParameterMap(projectCode); - assertTrue(MapUtils.isEmpty(result)); + Assertions.assertTrue(MapUtils.isEmpty(result)); } @Test @@ -527,8 +525,8 @@ public void testSafePutAll() throws Exception { // Assert Assertions.assertEquals(2, target.size()); - assertTrue(target.containsKey("validKey")); - assertTrue(target.containsKey("anotherValid")); + Assertions.assertTrue(target.containsKey("validKey")); + Assertions.assertTrue(target.containsKey("anotherValid")); Assertions.assertEquals("validValue", target.get("validKey").getValue()); Assertions.assertEquals("anotherValue", target.get("anotherValid").getValue()); From a5ddade5833654c236fbdbe6703e7dc260fcfab4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E8=8B=8F=E4=B9=89=E8=B6=85?= Date: Mon, 24 Nov 2025 14:31:24 +0800 Subject: [PATCH 3/7] update comment --- .../expand/CuringParamsServiceImpl.java | 40 +++++++++---------- 1 file changed, 19 insertions(+), 21 deletions(-) diff --git a/dolphinscheduler-service/src/main/java/org/apache/dolphinscheduler/service/expand/CuringParamsServiceImpl.java b/dolphinscheduler-service/src/main/java/org/apache/dolphinscheduler/service/expand/CuringParamsServiceImpl.java index bf06ffe701db..38e5e0617bac 100644 --- a/dolphinscheduler-service/src/main/java/org/apache/dolphinscheduler/service/expand/CuringParamsServiceImpl.java +++ b/dolphinscheduler-service/src/main/java/org/apache/dolphinscheduler/service/expand/CuringParamsServiceImpl.java @@ -168,13 +168,12 @@ public Map parseWorkflowFatherParam(@Nullable MapParameter Precedence (highest to lowest): *

    - *
  1. Built-in system parameters (e.g., ${task.id}, ${workflow.instance.id})
  2. + *
  3. Business/scheduling time parameters (e.g., {@code ${system.datetime}})
  4. + *
  5. Command-line or runtime complement parameters
  6. + *
  7. Task-local parameters
  8. + *
  9. Workflow global parameters (solidified at instance creation)
  10. *
  11. Project-level parameters
  12. - *
  13. Workflow global parameters
  14. - *
  15. Task local parameters
  16. - *
  17. Command-line / complement parameters (e.g., from补数 or API)
  18. - *
  19. VarPool overrides (only for {@link Direct#IN} parameters)
  20. - *
  21. Business/scheduling time parameters (e.g., ${system.datetime})
  22. + *
  23. Built-in system parameters (e.g., {@code ${task.id}})
  24. *
* *

Important Notes: @@ -207,45 +206,44 @@ public Map paramParsingPreparation( Map prepareParamsMap = new HashMap<>(); - // Parse command param (defensive: commandParam may be null for normal runs) ICommandParam commandParam = JSONUtils.parseObject(workflowInstance.getCommandParam(), ICommandParam.class); - String timeZone = (commandParam != null) ? commandParam.getTimeZone() : null; + String timeZone = commandParam != null ? commandParam.getTimeZone() : null; - // 1. Built-in system parameters (e.g., task.id, workflow.instance.id, etc.) + // 1. Built-in parameters Map builtInParams = setBuiltInParamsMap( taskInstance, workflowInstance, timeZone, projectName, workflowDefinitionName); safePutAll(prepareParamsMap, ParameterUtils.getUserDefParamsMap(builtInParams)); - // 2. Project-level parameters (shared across all workflows in the project) + // 2. Project-level parameters Map projectParams = getProjectParameterMap(taskInstance.getProjectCode()); safePutAll(prepareParamsMap, projectParams); - // 3. Workflow global parameters (defined at workflow level, solidified at instance creation) + // 3. Global parameters Map globalParams = parseGlobalParamsMap(workflowInstance); safePutAll(prepareParamsMap, globalParams); - // 4. Task local parameters (defined in the task node itself) + // 4. Local parameters Map localParams = parameters.getInputLocalParametersMap(); safePutAll(prepareParamsMap, localParams); - // 5. Command-line or complement (补数) parameters passed at runtime + // 5. Command parameters if (commandParam != null && CollectionUtils.isNotEmpty(commandParam.getCommandParams())) { Map commandParamsMap = commandParam.getCommandParams().stream() - .filter(prop -> StringUtils.isNotBlank(prop.getProp())) // exclude invalid keys + .filter(prop -> StringUtils.isNotBlank(prop.getProp())) .collect(Collectors.toMap( Property::getProp, Function.identity(), - (v1, v2) -> v2 // on duplicate keys, keep the last occurrence - )); + (v1, v2) -> v2)); safePutAll(prepareParamsMap, commandParamsMap); } - // 6. VarPool: override only input (Direct.IN) parameters with values from upstream tasks + // 6. VarPool: override only existing Direct.IN parameters List varPools = parseVarPool(taskInstance); if (CollectionUtils.isNotEmpty(varPools)) { for (Property varPool : varPools) { - if (StringUtils.isBlank(varPool.getProp())) - continue; // skip invalid + if (StringUtils.isBlank(varPool.getProp())) { + continue; + } Property existing = prepareParamsMap.get(varPool.getProp()); if (existing != null && Direct.IN.equals(existing.getDirect())) { existing.setValue(varPool.getValue()); @@ -253,10 +251,10 @@ public Map paramParsingPreparation( } } - // 7. Resolve placeholders (e.g., "${output_dir}") using the fully merged parameter map + // 7. Resolve placeholders resolvePlaceholders(prepareParamsMap); - // 8. Business/scheduling time parameters (e.g., ${system.datetime}, ${schedule.time}) + // 8. Business/scheduling parameters (highest priority) Map businessParams = preBuildBusinessParams(workflowInstance); safePutAll(prepareParamsMap, businessParams); From 268af32493c2aa8889ef42ef1ebfa5d4658dfc9d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E8=8B=8F=E4=B9=89=E8=B6=85?= Date: Mon, 24 Nov 2025 15:04:06 +0800 Subject: [PATCH 4/7] update comment --- .../expand/CuringParamsServiceImpl.java | 60 +++++++++---------- 1 file changed, 30 insertions(+), 30 deletions(-) diff --git a/dolphinscheduler-service/src/main/java/org/apache/dolphinscheduler/service/expand/CuringParamsServiceImpl.java b/dolphinscheduler-service/src/main/java/org/apache/dolphinscheduler/service/expand/CuringParamsServiceImpl.java index 38e5e0617bac..8746f48ddb99 100644 --- a/dolphinscheduler-service/src/main/java/org/apache/dolphinscheduler/service/expand/CuringParamsServiceImpl.java +++ b/dolphinscheduler-service/src/main/java/org/apache/dolphinscheduler/service/expand/CuringParamsServiceImpl.java @@ -163,8 +163,10 @@ public Map parseWorkflowFatherParam(@Nullable Map + *

  • Keys are non-null and non-blank strings
  • + *
  • Values are non-null {@link Property} objects
  • + * * *

    Parameter Precedence (highest to lowest): *

      @@ -184,9 +186,9 @@ public Map parseWorkflowFatherParam(@Nullable MapPlaceholders (e.g., {@code "${var}"}) in parameter values are resolved after all sources * are merged, using the consolidated parameter map. Global parameters are already * solidified (fully resolved at workflow instance creation), so no recursive - * placeholder expansion is needed. + * placeholder expansion is required. *
    1. {@code VarPool} values (from upstream tasks) only override parameters marked as - * {@link Direct#IN}; output or constant parameters are left unchanged.
    2. + * {@link Direct#IN}; output or constant parameters remain unchanged. * * * @param taskInstance the current task instance (must not be null) @@ -209,7 +211,7 @@ public Map paramParsingPreparation( ICommandParam commandParam = JSONUtils.parseObject(workflowInstance.getCommandParam(), ICommandParam.class); String timeZone = commandParam != null ? commandParam.getTimeZone() : null; - // 1. Built-in parameters + // 1. Built-in parameters (lowest precedence) Map builtInParams = setBuiltInParamsMap( taskInstance, workflowInstance, timeZone, projectName, workflowDefinitionName); safePutAll(prepareParamsMap, ParameterUtils.getUserDefParamsMap(builtInParams)); @@ -218,43 +220,44 @@ public Map paramParsingPreparation( Map projectParams = getProjectParameterMap(taskInstance.getProjectCode()); safePutAll(prepareParamsMap, projectParams); - // 3. Global parameters + // 3. Workflow global parameters Map globalParams = parseGlobalParamsMap(workflowInstance); safePutAll(prepareParamsMap, globalParams); - // 4. Local parameters + // 4. Task-local parameters Map localParams = parameters.getInputLocalParametersMap(); safePutAll(prepareParamsMap, localParams); - // 5. Command parameters + // 5. Command-line / complement parameters if (commandParam != null && CollectionUtils.isNotEmpty(commandParam.getCommandParams())) { Map commandParamsMap = commandParam.getCommandParams().stream() .filter(prop -> StringUtils.isNotBlank(prop.getProp())) .collect(Collectors.toMap( Property::getProp, Function.identity(), - (v1, v2) -> v2)); + (v1, v2) -> v2 // retain last on duplicate key + )); safePutAll(prepareParamsMap, commandParamsMap); } - // 6. VarPool: override only existing Direct.IN parameters + // 6. VarPool: override values only for existing IN-direction parameters List varPools = parseVarPool(taskInstance); if (CollectionUtils.isNotEmpty(varPools)) { for (Property varPool : varPools) { if (StringUtils.isBlank(varPool.getProp())) { continue; } - Property existing = prepareParamsMap.get(varPool.getProp()); - if (existing != null && Direct.IN.equals(existing.getDirect())) { - existing.setValue(varPool.getValue()); + Property targetParam = prepareParamsMap.get(varPool.getProp()); + if (targetParam != null && Direct.IN.equals(targetParam.getDirect())) { + targetParam.setValue(varPool.getValue()); } } } - // 7. Resolve placeholders + // 7. Resolve placeholders (e.g., "${output_dir}") using the current parameter context resolvePlaceholders(prepareParamsMap); - // 8. Business/scheduling parameters (highest priority) + // 8. Business/scheduling parameters (highest precedence) Map businessParams = preBuildBusinessParams(workflowInstance); safePutAll(prepareParamsMap, businessParams); @@ -265,12 +268,11 @@ public Map paramParsingPreparation( * Safely merges entries from the {@code source} map into the {@code target} map, * skipping any entry with a {@code null}, empty, or blank key, or a {@code null} value. * - *

      This method is critical for ensuring that the resulting parameter map can be - * safely serialized to JSON (e.g., by Jackson), which does not allow {@code null} keys - * in maps. Invalid entries are logged as warnings to aid in debugging misconfigured - * parameters (e.g., project or workflow parameters with missing names). + *

      This method ensures the resulting parameter map can be safely serialized to JSON + * (e.g., by Jackson), which prohibits {@code null} keys in maps. Invalid entries are + * logged as warnings to aid in debugging misconfigured parameters. * - *

      Example of skipped entry: + *

      Examples of skipped entries: *

            *   key = null        → skipped
            *   key = ""          → skipped
      @@ -301,8 +303,8 @@ private void safePutAll(Map target, Map sour
            * Resolves placeholder expressions (e.g., "${var}") in parameter values by substituting them
            * with actual values from the current {@code paramsMap}.
            *
      -     * 

      This method supports parameter references where a local task parameter refers to a global - * workflow parameter of the same name. For example: + *

      This supports references where a local task parameter refers to another parameter + * (e.g., a global workflow parameter). For example: *

            * Global parameters (solidified at workflow instance creation):
            *   "output_dir" → "/data/20251119"
      @@ -314,16 +316,14 @@ private void safePutAll(Map target, Map sour
            *   "log_path" → "/data/20251119/task.log"
            * 
      * - *

      Important: The global parameters included in {@code paramsMap} are - * solidified—meaning they were fully resolved at workflow instance creation time - * and contain no unresolved placeholders (e.g., no nested "${...}" expressions). - * Therefore, this resolution pass only needs to perform a single-level (or iterative chain) - * substitution without worrying about recursive variable expansion in global values. + *

      Note: Global parameters in {@code paramsMap} are already solidified + * (i.e., contain no unresolved placeholders). Therefore, this method only needs to perform + * iterative substitution within the current context without recursive expansion. * - *

      The method processes all properties in-place. Only values containing - * {@link Constants#FUNCTION_START_WITH} (typically "${") are processed. + *

      Only values containing {@link Constants#FUNCTION_START_WITH} (typically "${") are processed. + * Substitution is performed in-place. * - * @param paramsMap the map of parameters (key: parameter name, value: {@link Property}) to resolve. + * @param paramsMap the map of parameters (key: parameter name, value: {@link Property}) to resolve */ private void resolvePlaceholders(Map paramsMap) { for (Property prop : paramsMap.values()) { From 47d256d7029b715ac635571f0e16053065d9d6dd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E8=8B=8F=E4=B9=89=E8=B6=85?= Date: Wed, 26 Nov 2025 11:12:54 +0800 Subject: [PATCH 5/7] update paramParsingPreparation --- .../expand/CuringParamsServiceImpl.java | 23 +++++++++++-------- 1 file changed, 14 insertions(+), 9 deletions(-) diff --git a/dolphinscheduler-service/src/main/java/org/apache/dolphinscheduler/service/expand/CuringParamsServiceImpl.java b/dolphinscheduler-service/src/main/java/org/apache/dolphinscheduler/service/expand/CuringParamsServiceImpl.java index 8746f48ddb99..96ba07b966fb 100644 --- a/dolphinscheduler-service/src/main/java/org/apache/dolphinscheduler/service/expand/CuringParamsServiceImpl.java +++ b/dolphinscheduler-service/src/main/java/org/apache/dolphinscheduler/service/expand/CuringParamsServiceImpl.java @@ -48,6 +48,7 @@ import org.apache.dolphinscheduler.plugin.task.api.utils.ParameterUtils; import org.apache.dolphinscheduler.plugin.task.api.utils.PropertyUtils; import org.apache.dolphinscheduler.plugin.task.api.utils.VarPoolUtils; +import org.apache.dolphinscheduler.service.exceptions.ServiceException; import org.apache.commons.collections4.CollectionUtils; import org.apache.commons.lang3.StringUtils; @@ -199,17 +200,21 @@ public Map parseWorkflowFatherParam(@Nullable Map paramParsingPreparation( - @NonNull TaskInstance taskInstance, + public Map paramParsingPreparation(@NonNull TaskInstance taskInstance, @NonNull AbstractParameters parameters, @NonNull WorkflowInstance workflowInstance, String projectName, String workflowDefinitionName) { - Map prepareParamsMap = new HashMap<>(); + // If it is a complement, you need to pass in the task instance id + // to locate the time of the process instance complement. ICommandParam commandParam = JSONUtils.parseObject(workflowInstance.getCommandParam(), ICommandParam.class); - String timeZone = commandParam != null ? commandParam.getTimeZone() : null; + if (commandParam == null) { + throw new ServiceException(String.format("Failed to parse command parameter for workflow instance %s", + workflowInstance.getId())); + } + String timeZone = commandParam.getTimeZone(); // 1. Built-in parameters (lowest precedence) Map builtInParams = setBuiltInParamsMap( @@ -229,7 +234,7 @@ public Map paramParsingPreparation( safePutAll(prepareParamsMap, localParams); // 5. Command-line / complement parameters - if (commandParam != null && CollectionUtils.isNotEmpty(commandParam.getCommandParams())) { + if (CollectionUtils.isNotEmpty(commandParam.getCommandParams())) { Map commandParamsMap = commandParam.getCommandParams().stream() .filter(prop -> StringUtils.isNotBlank(prop.getProp())) .collect(Collectors.toMap( @@ -254,13 +259,13 @@ public Map paramParsingPreparation( } } - // 7. Resolve placeholders (e.g., "${output_dir}") using the current parameter context - resolvePlaceholders(prepareParamsMap); - - // 8. Business/scheduling parameters (highest precedence) + // 7. Inject business/scheduling parameters (e.g., ${datetime}), which may contain or reference placeholders Map businessParams = preBuildBusinessParams(workflowInstance); safePutAll(prepareParamsMap, businessParams); + // 8. Resolve all placeholders (e.g., "${output_dir}") using the current parameter context + resolvePlaceholders(prepareParamsMap); + return prepareParamsMap; } From 0e5471c7363171c3f0a03490669b62f4c5f8d495 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E8=8B=8F=E4=B9=89=E8=B6=85?= Date: Wed, 26 Nov 2025 11:23:05 +0800 Subject: [PATCH 6/7] update unit test --- .../dolphinscheduler/plugin/task/shell/ShellTaskChannelTest.java | 1 - 1 file changed, 1 deletion(-) diff --git a/dolphinscheduler-task-plugin/dolphinscheduler-task-shell/src/test/java/org/apache/dolphinscheduler/plugin/task/shell/ShellTaskChannelTest.java b/dolphinscheduler-task-plugin/dolphinscheduler-task-shell/src/test/java/org/apache/dolphinscheduler/plugin/task/shell/ShellTaskChannelTest.java index 7dc2975f9b8b..0cafab1f2ab7 100644 --- a/dolphinscheduler-task-plugin/dolphinscheduler-task-shell/src/test/java/org/apache/dolphinscheduler/plugin/task/shell/ShellTaskChannelTest.java +++ b/dolphinscheduler-task-plugin/dolphinscheduler-task-shell/src/test/java/org/apache/dolphinscheduler/plugin/task/shell/ShellTaskChannelTest.java @@ -63,7 +63,6 @@ public void testParseParametersWithValidJson() { @Test @DisplayName("parseParameters should parse task_params with simple script and one resource") public void testParseShellTaskParamsWithSimpleScript() { - // Given: 转义后的 JSON 字符串(JDK 8 兼容) String taskParams = "{\n" + " \"localParams\": [],\n" + " \"rawScript\": \"#!/bin/bash\\nset -e\\n\\n\\n \\necho \\\"====================================\\\"\",\n" From 67c4d29b508d7baa5200cb9604d46ea6bd13b850 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E8=8B=8F=E4=B9=89=E8=B6=85?= Date: Tue, 2 Dec 2025 13:59:57 +0800 Subject: [PATCH 7/7] add it test case --- .../cases/WorkflowStartTestCase.java | 44 +++++++ .../start/workflow_with_null_key_param.yaml | 108 ++++++++++++++++++ .../expand/CuringParamsServiceImpl.java | 72 ++---------- 3 files changed, 161 insertions(+), 63 deletions(-) create mode 100644 dolphinscheduler-master/src/test/resources/it/start/workflow_with_null_key_param.yaml diff --git a/dolphinscheduler-master/src/test/java/org/apache/dolphinscheduler/server/master/integration/cases/WorkflowStartTestCase.java b/dolphinscheduler-master/src/test/java/org/apache/dolphinscheduler/server/master/integration/cases/WorkflowStartTestCase.java index 80eddee92319..a2f0331f3483 100644 --- a/dolphinscheduler-master/src/test/java/org/apache/dolphinscheduler/server/master/integration/cases/WorkflowStartTestCase.java +++ b/dolphinscheduler-master/src/test/java/org/apache/dolphinscheduler/server/master/integration/cases/WorkflowStartTestCase.java @@ -775,6 +775,50 @@ public void testStartWorkflow_fakeTask_usingLocalParamOverWriteByVarPool() { masterContainer.assertAllResourceReleased(); } + @Test + @DisplayName("Test start a workflow which using null key params") + public void testStartWorkflow_usingNullKeyParam() { + final String yaml = "/it/start/workflow_with_null_key_param.yaml"; + final WorkflowTestCaseContext context = workflowTestCaseContextFactory.initializeContextFromYaml(yaml); + final WorkflowDefinition workflow = context.getOneWorkflow(); + + final RunWorkflowCommandParam runWorkflowCommandParam = RunWorkflowCommandParam.builder() + .commandParams(Lists.newArrayList(Property.builder() + .prop(null) + .direct(Direct.IN) + .type(DataType.VARCHAR) + .value("commandParam") + .build())) + .build(); + + final WorkflowOperator.WorkflowTriggerDTO workflowTriggerDTO = WorkflowOperator.WorkflowTriggerDTO.builder() + .workflowDefinition(workflow) + .runWorkflowCommandParam(runWorkflowCommandParam) + .build(); + workflowOperator.manualTriggerWorkflow(workflowTriggerDTO); + + await() + .atMost(Duration.ofMinutes(1)) + .untilAsserted(() -> { + Assertions + .assertThat(repository.queryWorkflowInstance(workflow)) + .satisfiesExactly(workflowInstance -> assertThat(workflowInstance.getState()) + .isEqualTo(WorkflowExecutionStatus.SUCCESS)); + Assertions + .assertThat(repository.queryTaskInstance(workflow)) + .hasSize(2) + .anySatisfy(taskInstance -> { + assertThat(taskInstance.getName()).isEqualTo("A"); + assertThat(taskInstance.getState()).isEqualTo(TaskExecutionStatus.SUCCESS); + }) + .anySatisfy(taskInstance -> { + assertThat(taskInstance.getName()).isEqualTo("B"); + assertThat(taskInstance.getState()).isEqualTo(TaskExecutionStatus.SUCCESS); + }); + }); + masterContainer.assertAllResourceReleased(); + } + @Test @DisplayName("Test start a workflow with one fake task(A) failed") public void testStartWorkflow_with_oneFailedTask() { diff --git a/dolphinscheduler-master/src/test/resources/it/start/workflow_with_null_key_param.yaml b/dolphinscheduler-master/src/test/resources/it/start/workflow_with_null_key_param.yaml new file mode 100644 index 000000000000..17d3d990aad7 --- /dev/null +++ b/dolphinscheduler-master/src/test/resources/it/start/workflow_with_null_key_param.yaml @@ -0,0 +1,108 @@ +# +# Licensed to the Apache Software Foundation (ASF) under one or more +# contributor license agreements. See the NOTICE file distributed with +# this work for additional information regarding copyright ownership. +# The ASF licenses this file to You under the Apache License, Version 2.0 +# (the "License"); you may not use this file except in compliance with +# the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +project: + name: MasterIntegrationTest + code: 1 + description: This is a fake project + userId: 1 + userName: admin + createTime: 2024-08-12 00:00:00 + updateTime: 2024-08-12 00:00:00 + +workflows: + - name: workflow_with_one_fake_task_success + code: 1 + version: 1 + projectCode: 1 + description: This is a fake workflow with single task + releaseState: ONLINE + createTime: 2024-08-12 00:00:00 + updateTime: 2024-08-12 00:00:00 + userId: 1 + globalParams: '[{"prop":null,"value":"workflowParam","direct":"IN","type":"VARCHAR"}]' + executionType: PARALLEL + +tasks: + - name: A + code: 1 + version: 1 + projectCode: 1 + userId: 1 + taskType: LogicFakeTask + taskParams: > + { + "localParams": [ + { + "prop": "", + "direct": "IN", + "type": "VARCHAR", + "value": "" + } + ], + "shellScript": "echo 111", + "resourceList": [] + } + workerGroup: default + createTime: 2024-08-12 00:00:00 + updateTime: 2024-08-12 00:00:00 + taskExecuteType: BATCH + - name: B + code: 2 + version: 1 + projectCode: 1 + userId: 1 + taskType: LogicFakeTask + taskParams: > + { + "localParams": [ + { + "prop": null, + "direct": "IN", + "type": "VARCHAR", + "value": "" + } + ], + "shellScript": "echo 111", + "resourceList": [] + } + workerGroup: default + createTime: 2024-08-12 00:00:00 + updateTime: 2024-08-12 00:00:00 + taskExecuteType: BATCH + + + +taskRelations: + - projectCode: 1 + workflowDefinitionCode: 1 + workflowDefinitionVersion: 1 + preTaskCode: 0 + preTaskVersion: 0 + postTaskCode: 1 + postTaskVersion: 1 + createTime: 2024-08-12 00:00:00 + updateTime: 2024-08-12 00:00:00 + - projectCode: 1 + workflowDefinitionCode: 1 + workflowDefinitionVersion: 1 + preTaskCode: 0 + preTaskVersion: 0 + postTaskCode: 2 + postTaskVersion: 1 + createTime: 2024-08-12 00:00:00 + updateTime: 2024-08-12 00:00:00 diff --git a/dolphinscheduler-service/src/main/java/org/apache/dolphinscheduler/service/expand/CuringParamsServiceImpl.java b/dolphinscheduler-service/src/main/java/org/apache/dolphinscheduler/service/expand/CuringParamsServiceImpl.java index 96ba07b966fb..225bb0691794 100644 --- a/dolphinscheduler-service/src/main/java/org/apache/dolphinscheduler/service/expand/CuringParamsServiceImpl.java +++ b/dolphinscheduler-service/src/main/java/org/apache/dolphinscheduler/service/expand/CuringParamsServiceImpl.java @@ -169,35 +169,16 @@ public Map parseWorkflowFatherParam(@Nullable MapValues are non-null {@link Property} objects * * - *

      Parameter Precedence (highest to lowest): - *

        - *
      1. Business/scheduling time parameters (e.g., {@code ${system.datetime}})
      2. - *
      3. Command-line or runtime complement parameters
      4. - *
      5. Task-local parameters
      6. - *
      7. Workflow global parameters (solidified at instance creation)
      8. - *
      9. Project-level parameters
      10. - *
      11. Built-in system parameters (e.g., {@code ${task.id}})
      12. - *
      + *

      The priority of the parameters is as follows: + *

      varpool > command parameters > local parameters > global parameters > project parameters > built-in parameters + * todo: Use TaskRuntimeParams to represent this. * - *

      Important Notes: - *

        - *
      • All parameter sources are sanitized via {@link #safePutAll(Map, Map)} to prevent {@code null} - * or blank keys, which would cause JSON serialization failures (e.g., Jackson's - * "Null key for a Map not allowed in JSON").
      • - *
      • Placeholders (e.g., {@code "${var}"}) in parameter values are resolved after all sources - * are merged, using the consolidated parameter map. Global parameters are already - * solidified (fully resolved at workflow instance creation), so no recursive - * placeholder expansion is required.
      • - *
      • {@code VarPool} values (from upstream tasks) only override parameters marked as - * {@link Direct#IN}; output or constant parameters remain unchanged.
      • - *
      - * - * @param taskInstance the current task instance (must not be null) - * @param parameters the parsed task-specific parameters (must not be null) - * @param workflowInstance the parent workflow instance (must not be null) - * @param projectName name of the project containing the workflow - * @param workflowDefinitionName name of the workflow definition - * @return a safe, fully resolved map of parameter name to {@link Property}, ready for task execution + * @param taskInstance + * @param parameters + * @param workflowInstance + * @param projectName + * @param workflowDefinitionName + * @return */ @Override public Map paramParsingPreparation(@NonNull TaskInstance taskInstance, @@ -273,21 +254,6 @@ public Map paramParsingPreparation(@NonNull TaskInstance taskI * Safely merges entries from the {@code source} map into the {@code target} map, * skipping any entry with a {@code null}, empty, or blank key, or a {@code null} value. * - *

      This method ensures the resulting parameter map can be safely serialized to JSON - * (e.g., by Jackson), which prohibits {@code null} keys in maps. Invalid entries are - * logged as warnings to aid in debugging misconfigured parameters. - * - *

      Examples of skipped entries: - *

      -     *   key = null        → skipped
      -     *   key = ""          → skipped
      -     *   key = "  \t\n"    → skipped
      -     *   value = null      → skipped
      -     * 
      - * - *

      All valid entries (non-blank key and non-null value) are added to {@code target} - * using standard {@link Map#put(Object, Object)} semantics (later values overwrite earlier ones). - * * @param target the destination map to merge into (must not be null) * @param source the source map whose valid entries will be copied (may be null or empty) */ @@ -308,26 +274,6 @@ private void safePutAll(Map target, Map sour * Resolves placeholder expressions (e.g., "${var}") in parameter values by substituting them * with actual values from the current {@code paramsMap}. * - *

      This supports references where a local task parameter refers to another parameter - * (e.g., a global workflow parameter). For example: - *

      -     * Global parameters (solidified at workflow instance creation):
      -     *   "output_dir" → "/data/20251119"
      -     *
      -     * Local task parameter definition:
      -     *   "log_path" → "${output_dir}/task.log"
      -     *
      -     * After resolution:
      -     *   "log_path" → "/data/20251119/task.log"
      -     * 
      - * - *

      Note: Global parameters in {@code paramsMap} are already solidified - * (i.e., contain no unresolved placeholders). Therefore, this method only needs to perform - * iterative substitution within the current context without recursive expansion. - * - *

      Only values containing {@link Constants#FUNCTION_START_WITH} (typically "${") are processed. - * Substitution is performed in-place. - * * @param paramsMap the map of parameters (key: parameter name, value: {@link Property}) to resolve */ private void resolvePlaceholders(Map paramsMap) {