From a0c88ec55a004c18356f05441497c04fb1d87dc1 Mon Sep 17 00:00:00 2001 From: "Guillaume V." Date: Fri, 10 Apr 2026 16:55:49 +0200 Subject: [PATCH 1/3] New [Inputs] : Handle lists of inputs maps from API --- .../vip/api/business/ExecutionBusiness.java | 60 +++++++++++-------- .../creatis/vip/api/model/Execution.java | 8 +-- .../vip/api/data/ExecutionTestUtils.java | 20 ++++--- .../processing/ExecutionControllerIT.java | 4 +- .../server/business/WorkflowBusiness.java | 11 ++-- .../business/WorkflowExecutionBusiness.java | 4 +- .../server/rpc/WorkflowServiceImpl.java | 4 +- 7 files changed, 65 insertions(+), 46 deletions(-) diff --git a/vip-api/src/main/java/fr/insalyon/creatis/vip/api/business/ExecutionBusiness.java b/vip-api/src/main/java/fr/insalyon/creatis/vip/api/business/ExecutionBusiness.java index b0b16cce7..40cda9a4b 100644 --- a/vip-api/src/main/java/fr/insalyon/creatis/vip/api/business/ExecutionBusiness.java +++ b/vip-api/src/main/java/fr/insalyon/creatis/vip/api/business/ExecutionBusiness.java @@ -174,8 +174,9 @@ private Execution getExecutionFromSimulation(Simulation s, boolean summarize) th for (InOutData iod : inputs) { String key = iod.getProcessor(); String value = iod.getPath(); - - ((List) e.getInputValues().computeIfAbsent(key, k -> new ArrayList<>())).add(value); + for (Map inputMap : e.getInputValues()) { + ((List) inputMap.computeIfAbsent(key, k -> new ArrayList<>())).add(value); + } } // retrieves results directory List resDirList = (List) e.getInputValues().get(RESULTS_DIRECTORY_PARAM_NAME); @@ -315,25 +316,29 @@ public void updateExecution(Execution execution) throws VipException { } public String initExecution(Execution execution) throws VipException { - Map inputMap = new HashMap<>(); + List> inputMaps = new ArrayList<>(); + Object resultsLocation = execution.getResultsLocation(); + for (Map inputValuesMap : execution.getInputValues()) { + Map inputMap = new HashMap<>(); + for (Entry restInput : inputValuesMap.entrySet()) { + inputMap.put( + restInput.getKey(), + handleRestParameter(restInput.getKey(), restInput.getValue())); + } - for (Entry restInput : execution.getInputValues().entrySet()) { - inputMap.put( - restInput.getKey(), - handleRestParameter(restInput.getKey(), restInput.getValue())); - } + // We handle resultsLocation the same as others restInputs, since it can either be a String or a List + if (resultsLocation != null) { + inputMap.put( + CoreConstants.RESULTS_DIRECTORY_PARAM_NAME, + handleRestParameter(CoreConstants.RESULTS_DIRECTORY_PARAM_NAME, resultsLocation)); + } - // We handle resultsLocation the same as others restInputs, since it can either be a String or a List - Object resultsLocation = execution.getResultsLocation(); - if (resultsLocation != null) { - inputMap.put( - CoreConstants.RESULTS_DIRECTORY_PARAM_NAME, - handleRestParameter(CoreConstants.RESULTS_DIRECTORY_PARAM_NAME, resultsLocation)); + inputMaps.add(inputMap); } checkInputExecNameIsValid(execution.getName()); return initExecution( - execution.getPipelineIdentifier(), inputMap, execution.getTimeout(), + execution.getPipelineIdentifier(), inputMaps, execution.getTimeout(), execution.getName(), execution.getStudyIdentifier()); } @@ -378,7 +383,7 @@ private void checkInputExecNameIsValid(String input) throws VipException { } private String initExecution(String pipelineId, - Map inputValues, + List> inputValues, Integer timeoutInSeconds, String executionName, String studyId) throws VipException { @@ -401,12 +406,15 @@ private String initExecution(String pipelineId, continue; } // ok if input is present - if (inputValues.get(pp.getName()) != null) { + if (inputValues.stream().allMatch(inputMap -> inputMap.containsKey(pp.getName()))) { continue; } // then ok if input has a default value (and we set it) if (pp.getDefaultValue() != null) { - inputValues.put(pp.getName(), pp.getDefaultValue().toString()); + for (Map inputMap : inputValues) { + inputMap.put(pp.getName(), pp.getDefaultValue().toString()); + } + continue; } // then ok if it is optional @@ -423,17 +431,19 @@ private String initExecution(String pipelineId, if (overriddenInputs != null) { for (String key : overriddenInputs.keySet()) { String value = overriddenInputs.get(key); - if (inputValues.containsKey(value)) { - inputValues.put(key, inputValues.get(value)); - } else { - logger.error("Error initialising {}, missing {} parameter", pipelineId, value); - throw new VipException(ApiError.INPUT_FIELD_MISSING, value); + for (Map inputMap : inputValues) { + if (inputMap.containsKey(value)) { + inputMap.put(key, inputMap.get(value)); + } else { + logger.error("Error initialising {}, missing {} parameter", pipelineId, value); + throw new VipException(ApiError.INPUT_FIELD_MISSING, value); + } } } } - boolean inputsContainsResultsDirectoryInput = inputValues - .containsKey(CoreConstants.RESULTS_DIRECTORY_PARAM_NAME); + boolean inputsContainsResultsDirectoryInput = inputValues.stream() + .allMatch(inputMap -> inputMap.containsKey(CoreConstants.RESULTS_DIRECTORY_PARAM_NAME)); boolean pipelineHasResultsDirectoryInput = p.getParameters().stream() .anyMatch(param -> param.getName().equals(CoreConstants.RESULTS_DIRECTORY_PARAM_NAME)); diff --git a/vip-api/src/main/java/fr/insalyon/creatis/vip/api/model/Execution.java b/vip-api/src/main/java/fr/insalyon/creatis/vip/api/model/Execution.java index 1a87194eb..6a8148e71 100644 --- a/vip-api/src/main/java/fr/insalyon/creatis/vip/api/model/Execution.java +++ b/vip-api/src/main/java/fr/insalyon/creatis/vip/api/model/Execution.java @@ -13,7 +13,7 @@ public class Execution { private int timeout; private ExecutionStatus status; @NotNull - private Map inputValues; + private List> inputValues; private Map> returnedFiles; // optional arguments @@ -25,7 +25,7 @@ public class Execution { private Map> jobs; // jobId -> status public Execution() { - inputValues = new HashMap<>(); + inputValues = new ArrayList<>(); returnedFiles = new HashMap<>(); jobs = new HashMap<>(); } @@ -94,11 +94,11 @@ public void setStatus(ExecutionStatus status) { this.status = status; } - public Map getInputValues() { + public List> getInputValues() { return inputValues; } - public void setInputValues(Map inputValues) { + public void setInputValues(List> inputValues) { this.inputValues = inputValues; } diff --git a/vip-api/src/test/java/fr/insalyon/creatis/vip/api/data/ExecutionTestUtils.java b/vip-api/src/test/java/fr/insalyon/creatis/vip/api/data/ExecutionTestUtils.java index ef9997d5b..f49f2ae84 100644 --- a/vip-api/src/test/java/fr/insalyon/creatis/vip/api/data/ExecutionTestUtils.java +++ b/vip-api/src/test/java/fr/insalyon/creatis/vip/api/data/ExecutionTestUtils.java @@ -30,11 +30,12 @@ public class ExecutionTestUtils { new GregorianCalendar(2016, 9, 2).getTime(), "Exec test 1", SimulationStatus.Running.toString(), "engine 1", null); execution1 = getExecution(simulation1, ExecutionStatus.RUNNING); - execution1.setInputValues(new HashMap() {{ - put("param 1", "value 1"); - put("param 2", "42"); - }} - ); + List> parametersMaps = new ArrayList<>(); + parametersMaps.add(new HashMap<>() {{ + put("param 1", "value 1"); + put("param 2", "42"); + }}); + execution1.setInputValues(parametersMaps); execution1.clearReturnedFiles(); simulation1InData = Arrays.asList( @@ -47,10 +48,11 @@ public class ExecutionTestUtils { new GregorianCalendar(2016, 4, 29).getTime(), "Exec test 2", SimulationStatus.Completed.toString(), "engine 1", null); execution2 = getExecution(simulation2, ExecutionStatus.FINISHED); - execution2.setInputValues(new HashMap() {{ - put("param2-1", "5.3"); - }} - ); + parametersMaps = new ArrayList<>(); + parametersMaps.add(new HashMap<>() {{ + put("param2-1", "5.3"); + }}); + execution2.setInputValues(parametersMaps); execution2.setReturnedFiles(new HashMap>() {{ put("param2-res", Collections.singletonList("/vip/Home/testFile1.xml")); }}); diff --git a/vip-api/src/test/java/fr/insalyon/creatis/vip/api/rest/itest/processing/ExecutionControllerIT.java b/vip-api/src/test/java/fr/insalyon/creatis/vip/api/rest/itest/processing/ExecutionControllerIT.java index c381585ef..e3f44e1b6 100644 --- a/vip-api/src/test/java/fr/insalyon/creatis/vip/api/rest/itest/processing/ExecutionControllerIT.java +++ b/vip-api/src/test/java/fr/insalyon/creatis/vip/api/rest/itest/processing/ExecutionControllerIT.java @@ -426,7 +426,9 @@ public void testInitBoutiquesExecution() throws Exception expectedParams.put("testTextInput", List.of("best test text value")); expectedParams.put("testFlagInput", List.of("false")); expectedParams.put("results-directory", List.of("lfn:" + ServerMockConfig.TEST_USERS_ROOT + "/" + baseUser1.getFolder())); - String expectedInputs = workflowExecutionBusiness.getParametersAsJSONInput(expectedParams); + List>> paramsList = new ArrayList<>(); + paramsList.add(expectedParams); + String expectedInputs = workflowExecutionBusiness.getParametersAsJSONInput(paramsList); Assertions.assertEquals(expectedInputs, inputs); // verify created workflow diff --git a/vip-application/src/main/java/fr/insalyon/creatis/vip/application/server/business/WorkflowBusiness.java b/vip-application/src/main/java/fr/insalyon/creatis/vip/application/server/business/WorkflowBusiness.java index 7f2dc8725..f155dee9c 100644 --- a/vip-application/src/main/java/fr/insalyon/creatis/vip/application/server/business/WorkflowBusiness.java +++ b/vip-application/src/main/java/fr/insalyon/creatis/vip/application/server/business/WorkflowBusiness.java @@ -139,15 +139,18 @@ protected InputFileParser getInputFileParser(String currentUserFolder) { return null; } - public synchronized String launch(User user, List groups, Map parametersMap, - String appName, String version, String simulationName) throws VipException { + public synchronized String launch(User user, List groups, List> parametersMaps, + String appName, String version, String simulationName) throws VipException { Workflow workflow = null; try { checkVIPCapacities(user, simulationName); AppVersion appVersion = appVersionBusiness.getVersion(appName, version); - Map> parameters = getParameters(appVersion.getDescriptor(), parametersMap, user, groups); + List>> parametersMapList = new ArrayList<>(); + for (Map parameterMap : parametersMaps) { + parametersMapList.add(getParameters(appVersion.getDescriptor(), parameterMap, user, groups)); + } List resources = resourceBusiness.getAvailableForExecution(user, appVersion); if (resources.isEmpty()) { @@ -159,7 +162,7 @@ public synchronized String launch(User user, List groups, Map> parameters, String executorConfig) throws VipException { + List>> parameters, String executorConfig) throws VipException { try { String workflowContent = appVersion.getDescriptor(); @@ -66,7 +66,7 @@ public void kill(String engineEndpoint, String simulationID) throws VipException engine.kill(engineEndpoint, simulationID); } - public String getParametersAsJSONInput(Map> parameters) throws VipException { + public String getParametersAsJSONInput(List>> parameters) throws VipException { try { ObjectMapper mapper = new ObjectMapper(); diff --git a/vip-application/src/main/java/fr/insalyon/creatis/vip/application/server/rpc/WorkflowServiceImpl.java b/vip-application/src/main/java/fr/insalyon/creatis/vip/application/server/rpc/WorkflowServiceImpl.java index bcb6d9d51..924bb6c35 100644 --- a/vip-application/src/main/java/fr/insalyon/creatis/vip/application/server/rpc/WorkflowServiceImpl.java +++ b/vip-application/src/main/java/fr/insalyon/creatis/vip/application/server/rpc/WorkflowServiceImpl.java @@ -165,8 +165,10 @@ public void launchSimulation(Map parametersMap, logger.info("received param {} : {}", p.getKey(), p.getValue()); } fillInOverriddenInputs(parametersMap, applicationName, applicationVersion); + List> parametersMaps = new ArrayList<>(); + parametersMaps.add(parametersMap); String simulationID = workflowBusiness.launch(user, groups, - parametersMap, applicationName, applicationVersion, simulationName); + parametersMaps, applicationName, applicationVersion, simulationName); trace(logger, "Simulation '" + simulationName + "' launched with ID '" + simulationID + "'."); } From 6d3934bc6d7d27ab83d640d420eed4028589ff5a Mon Sep 17 00:00:00 2001 From: "Guillaume V." Date: Fri, 10 Apr 2026 18:14:03 +0200 Subject: [PATCH 2/3] Fix [Tests] : Update input test files --- .../creatis/vip/api/data/ExecutionTestUtils.java | 8 ++++---- .../resources/jsonObjects/execution1-name-updated.json | 10 ++++++---- vip-api/src/test/resources/jsonObjects/execution1.json | 10 ++++++---- 3 files changed, 16 insertions(+), 12 deletions(-) diff --git a/vip-api/src/test/java/fr/insalyon/creatis/vip/api/data/ExecutionTestUtils.java b/vip-api/src/test/java/fr/insalyon/creatis/vip/api/data/ExecutionTestUtils.java index f49f2ae84..0ab441f46 100644 --- a/vip-api/src/test/java/fr/insalyon/creatis/vip/api/data/ExecutionTestUtils.java +++ b/vip-api/src/test/java/fr/insalyon/creatis/vip/api/data/ExecutionTestUtils.java @@ -31,10 +31,10 @@ public class ExecutionTestUtils { "Exec test 1", SimulationStatus.Running.toString(), "engine 1", null); execution1 = getExecution(simulation1, ExecutionStatus.RUNNING); List> parametersMaps = new ArrayList<>(); - parametersMaps.add(new HashMap<>() {{ - put("param 1", "value 1"); - put("param 2", "42"); - }}); + Map map = new HashMap<>(); + map.put("param 1", "value 1"); + map.put("param 2", "42"); + parametersMaps.add(map); execution1.setInputValues(parametersMaps); execution1.clearReturnedFiles(); diff --git a/vip-api/src/test/resources/jsonObjects/execution1-name-updated.json b/vip-api/src/test/resources/jsonObjects/execution1-name-updated.json index 7f77ef80a..9be93400c 100644 --- a/vip-api/src/test/resources/jsonObjects/execution1-name-updated.json +++ b/vip-api/src/test/resources/jsonObjects/execution1-name-updated.json @@ -1,8 +1,10 @@ { "name" : "Exec test 1 - modified", "pipelineIdentifier" : "application 1/4.2", - "inputValues" : { - "param 1" : "test text", - "param 2" : "/path/test" - } + "inputValues" : [ + { + "param 1" : "test text", + "param 2" : "/path/test" + } + ] } \ No newline at end of file diff --git a/vip-api/src/test/resources/jsonObjects/execution1.json b/vip-api/src/test/resources/jsonObjects/execution1.json index 657cd07db..556bacea1 100644 --- a/vip-api/src/test/resources/jsonObjects/execution1.json +++ b/vip-api/src/test/resources/jsonObjects/execution1.json @@ -1,8 +1,10 @@ { "name" : "Exec test 1", "pipelineIdentifier" : "test application/4.2", - "inputValues" : { - "testFileInput" : "/vip/Home/path/to/input.in", - "testTextInput" : "best test text value" - } + "inputValues" : [ + { + "testFileInput" : "/vip/Home/path/to/input.in", + "testTextInput" : "best test text value" + } + ] } \ No newline at end of file From 6b298e65c4c0e75ebef44a8f8541deab7ffe2ec9 Mon Sep 17 00:00:00 2001 From: "Guillaume V." Date: Tue, 28 Apr 2026 14:10:58 +0200 Subject: [PATCH 3/3] Update [Execution] : Add InputValues Json deserializer and fix some minor issues --- .../vip/api/business/ExecutionBusiness.java | 57 ++++++++++++------- .../creatis/vip/api/model/Execution.java | 6 ++ .../serializing/InputValuesDeserializer.java | 43 ++++++++++++++ 3 files changed, 85 insertions(+), 21 deletions(-) create mode 100644 vip-api/src/main/java/fr/insalyon/creatis/vip/api/model/serializing/InputValuesDeserializer.java diff --git a/vip-api/src/main/java/fr/insalyon/creatis/vip/api/business/ExecutionBusiness.java b/vip-api/src/main/java/fr/insalyon/creatis/vip/api/business/ExecutionBusiness.java index 40cda9a4b..0e7ea2e0d 100644 --- a/vip-api/src/main/java/fr/insalyon/creatis/vip/api/business/ExecutionBusiness.java +++ b/vip-api/src/main/java/fr/insalyon/creatis/vip/api/business/ExecutionBusiness.java @@ -11,6 +11,7 @@ import java.util.Map; import java.util.Map.Entry; import java.util.function.Supplier; +import java.util.stream.Collectors; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -176,17 +177,19 @@ private Execution getExecutionFromSimulation(Simulation s, boolean summarize) th String value = iod.getPath(); for (Map inputMap : e.getInputValues()) { ((List) inputMap.computeIfAbsent(key, k -> new ArrayList<>())).add(value); + + // retrieves results directory + List resDirList = (List) inputMap.get(RESULTS_DIRECTORY_PARAM_NAME); + if (resDirList == null) { + resDirList = new ArrayList<>(); + } + if (!resDirList.isEmpty()) { + e.setResultsLocation(resDirList); + inputMap.remove(RESULTS_DIRECTORY_PARAM_NAME); + } } } - // retrieves results directory - List resDirList = (List) e.getInputValues().get(RESULTS_DIRECTORY_PARAM_NAME); - if (resDirList == null) { - resDirList = new ArrayList<>(); - } - if (!resDirList.isEmpty()) { - e.setResultsLocation(resDirList); - e.getInputValues().remove(RESULTS_DIRECTORY_PARAM_NAME); - } + List outputs = workflowBusiness.getOutputData(s.getID(), userFolder); for (InOutData iod : outputs) { String key = iod.getProcessor(); @@ -318,9 +321,15 @@ public void updateExecution(Execution execution) throws VipException { public String initExecution(Execution execution) throws VipException { List> inputMaps = new ArrayList<>(); Object resultsLocation = execution.getResultsLocation(); + boolean isInputMapList = execution.getInputValues().size() > 1; for (Map inputValuesMap : execution.getInputValues()) { Map inputMap = new HashMap<>(); for (Entry restInput : inputValuesMap.entrySet()) { + if (isInputMapList && restInput.getValue() instanceof List) { + throw new VipException( + "Parameter '" + restInput.getKey() + "' contains a list, it should only have a single value when providing a list of input maps."); + } + inputMap.put( restInput.getKey(), handleRestParameter(restInput.getKey(), restInput.getValue())); @@ -405,25 +414,31 @@ private String initExecution(String pipelineId, if (pp.isReturnedValue()) { continue; } + + List> mapsWithoutKey = inputValues.stream() + .filter(inputMap -> !inputMap.containsKey(pp.getName())) + .toList(); + // ok if input is present - if (inputValues.stream().allMatch(inputMap -> inputMap.containsKey(pp.getName()))) { + if (mapsWithoutKey.isEmpty()) { continue; } - // then ok if input has a default value (and we set it) - if (pp.getDefaultValue() != null) { - for (Map inputMap : inputValues) { + + for (Map inputMap : mapsWithoutKey) { + // then ok if input has a default value (and we set it) + if (pp.getDefaultValue() != null) { inputMap.put(pp.getName(), pp.getDefaultValue().toString()); + continue; + } + // then ok if it is optional + if (pp.isOptional()) { + continue; } - continue; - } - // then ok if it is optional - if (pp.isOptional()) { - continue; + // error : pp is an empty input with no default value and it is not optional + logger.error("Error initialising {}, missing {} parameter", pipelineId, pp.getName()); + throw new VipException(ApiError.INPUT_FIELD_MISSING, pp.getName()); } - // error : pp is an empty input with no default value and it is not optional - logger.error("Error initialising {}, missing {} parameter", pipelineId, pp.getName()); - throw new VipException(ApiError.INPUT_FIELD_MISSING, pp.getName()); } // fill in overriddenInputs from explicit inputs diff --git a/vip-api/src/main/java/fr/insalyon/creatis/vip/api/model/Execution.java b/vip-api/src/main/java/fr/insalyon/creatis/vip/api/model/Execution.java index 6a8148e71..895d27b87 100644 --- a/vip-api/src/main/java/fr/insalyon/creatis/vip/api/model/Execution.java +++ b/vip-api/src/main/java/fr/insalyon/creatis/vip/api/model/Execution.java @@ -1,8 +1,13 @@ package fr.insalyon.creatis.vip.api.model; import java.util.*; + import jakarta.validation.constraints.NotNull; +import com.fasterxml.jackson.databind.annotation.JsonDeserialize; + +import fr.insalyon.creatis.vip.api.model.serializing.InputValuesDeserializer; + public class Execution { private String identifier; @@ -13,6 +18,7 @@ public class Execution { private int timeout; private ExecutionStatus status; @NotNull + @JsonDeserialize(using = InputValuesDeserializer.class) private List> inputValues; private Map> returnedFiles; diff --git a/vip-api/src/main/java/fr/insalyon/creatis/vip/api/model/serializing/InputValuesDeserializer.java b/vip-api/src/main/java/fr/insalyon/creatis/vip/api/model/serializing/InputValuesDeserializer.java new file mode 100644 index 000000000..deecaee7b --- /dev/null +++ b/vip-api/src/main/java/fr/insalyon/creatis/vip/api/model/serializing/InputValuesDeserializer.java @@ -0,0 +1,43 @@ +package fr.insalyon.creatis.vip.api.model.serializing; + +import com.fasterxml.jackson.core.JsonToken; +import com.fasterxml.jackson.databind.JavaType; +import com.fasterxml.jackson.databind.deser.std.StdDeserializer; +import com.fasterxml.jackson.core.JsonParser; +import com.fasterxml.jackson.databind.DeserializationContext; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.Map; + +public class InputValuesDeserializer extends StdDeserializer>> { + + public InputValuesDeserializer() { + super(List.class); + } + + @Override + public List> deserialize(JsonParser p, DeserializationContext ctx) throws IOException { + JavaType mapType = ctx.getTypeFactory().constructMapType(Map.class, String.class, Object.class); + // Array of dictionaries + if (p.currentToken() == JsonToken.START_ARRAY) { + List> list = new ArrayList<>(); + p.nextToken(); + while (p.currentToken() != JsonToken.END_ARRAY) { + list.add(ctx.readValue(p, mapType)); + p.nextToken(); + } + + return list; + // Unique dictionary + } else if (p.currentToken() == JsonToken.START_OBJECT) { + Map map = ctx.readValue(p, mapType); + return Collections.singletonList(map); + } else { + throw ctx.wrongTokenException(p, List.class, JsonToken.START_ARRAY, + "InputValues must be a dictionary or an array of dictionaries"); + } + } +}