From a0c88ec55a004c18356f05441497c04fb1d87dc1 Mon Sep 17 00:00:00 2001 From: "Guillaume V." Date: Fri, 10 Apr 2026 16:55:49 +0200 Subject: [PATCH 1/5] 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/5] 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/5] 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"); + } + } +} From 1e74f8981d11bcb5fe75eb1cd6c0c829e4bb2727 Mon Sep 17 00:00:00 2001 From: "Guillaume V." Date: Wed, 29 Apr 2026 11:50:35 +0200 Subject: [PATCH 4/5] New [Dot inputs] : Fill dot inputs with default values when needed, portal-side --- .../server/rpc/WorkflowServiceImpl.java | 66 +++++++++++++++++++ 1 file changed, 66 insertions(+) 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 924bb6c35..3e821b07f 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 @@ -4,18 +4,23 @@ import java.io.File; import java.io.FileReader; import java.io.IOException; +import java.util.Arrays; import java.util.ArrayList; import java.util.Date; import java.util.List; import java.util.Map; +import java.util.Collections; +import java.util.stream.Collectors; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import fr.insalyon.creatis.boutiques.model.BoutiquesDescriptor; +import fr.insalyon.creatis.boutiques.model.Input; import fr.insalyon.creatis.devtools.FileUtils; import fr.insalyon.creatis.moteur.plugins.workflowsdb.dao.WorkflowsDBDAOException; import fr.insalyon.creatis.vip.application.client.rpc.WorkflowService; +import fr.insalyon.creatis.vip.application.client.ApplicationConstants; import fr.insalyon.creatis.vip.application.models.Activity; import fr.insalyon.creatis.vip.application.models.AppVersion; import fr.insalyon.creatis.vip.application.models.InOutData; @@ -146,6 +151,65 @@ private void fillInOverriddenInputs(Map parametersMap, } } + private void fillInDefaultDotInputs(Map parametersMap, + String applicationName, String applicationVersion) throws VipException { + AppVersion appVersion = appVersionBusiness.getVersion(applicationName, applicationVersion); + BoutiquesDescriptor descriptor = boutiquesBusiness.parseBoutiquesString(appVersion.getDescriptor()); + + final String dotKeyName = "vip:dot"; + if (!descriptor.getCustom().getAdditionalProperties().containsKey(dotKeyName)) { + return; + } + + Object dotInputs = descriptor.getCustom().getAdditionalProperties().get(dotKeyName); + if (!(dotInputs instanceof List)) { + return; + } + // Get the string list of dot inputs + List dotInputList = ((List) dotInputs).stream() + .filter(String.class::isInstance) + .map(String.class::cast) + .toList(); + // Build a map of inputId to Input for easy lookup + Map dotInputsById = descriptor.getInputs().stream() + .filter(input -> dotInputList.contains(input.getId())) + .collect(Collectors.toMap(Input::getId, i -> i)); + // Find the maximum number of dot inputs + int dotMaxCount = parametersMap.entrySet().stream() + .filter(entry -> dotInputsById.containsKey(entry.getKey())) + .mapToInt(v -> v.getValue().split(ApplicationConstants.SEPARATOR_LIST, -1).length) + .max() + .orElse(1); + + for (Input dotInput : dotInputsById.values()) { + if (dotInput.getDefaultValue() == null) { + continue; + } + + String defaultVal = dotInput.getDefaultValue().toString(); + String dotInputId = dotInput.getId(); + // If the input is not set, fill it with default values + if (!parametersMap.containsKey(dotInputId)) { + parametersMap.put(dotInputId, + String.join(ApplicationConstants.SEPARATOR_LIST, + Collections.nCopies(dotMaxCount, defaultVal))); + } else { + String[] currentValues = parametersMap.get(dotInputId) + .split(ApplicationConstants.SEPARATOR_LIST, -1); + // Complete the input with default values if it has fewer values than the maximum + if (currentValues.length < dotMaxCount) { + List valuesList = new ArrayList<>(Arrays.asList(currentValues)); + while (valuesList.size() < dotMaxCount) { + valuesList.add(defaultVal); + } + + parametersMap.put(dotInputId, + String.join(ApplicationConstants.SEPARATOR_LIST, valuesList)); + } + } + } + } + @Override public void launchSimulation(Map parametersMap, String applicationName, String applicationVersion, @@ -164,7 +228,9 @@ public void launchSimulation(Map parametersMap, for (Map.Entry p : parametersMap.entrySet()) { logger.info("received param {} : {}", p.getKey(), p.getValue()); } + fillInOverriddenInputs(parametersMap, applicationName, applicationVersion); + fillInDefaultDotInputs(parametersMap, applicationName, applicationVersion); List> parametersMaps = new ArrayList<>(); parametersMaps.add(parametersMap); String simulationID = workflowBusiness.launch(user, groups, From 9ace75ba39f589e2f521f0f3e5a2182bc5f614c1 Mon Sep 17 00:00:00 2001 From: "Guillaume V." Date: Wed, 29 Apr 2026 15:40:56 +0200 Subject: [PATCH 5/5] New [Dot inputs] : Fill dot inputs with default values when needed, api-side, and add generic access functions --- .../vip/api/business/ExecutionBusiness.java | 93 ++++++++++++++++--- .../vip/api/business/PipelineBusiness.java | 6 ++ .../creatis/vip/api/model/Pipeline.java | 12 +++ .../server/business/BoutiquesBusiness.java | 23 +++++ .../server/rpc/WorkflowServiceImpl.java | 15 +-- 5 files changed, 122 insertions(+), 27 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 0e7ea2e0d..575be7097 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,8 @@ import java.util.Map; import java.util.Map.Entry; import java.util.function.Supplier; +import java.util.Collections; +import java.util.Arrays; import java.util.stream.Collectors; import org.slf4j.Logger; @@ -441,21 +443,8 @@ private String initExecution(String pipelineId, } } - // fill in overriddenInputs from explicit inputs - Map overriddenInputs = p.getOverriddenInputs(); - if (overriddenInputs != null) { - for (String key : overriddenInputs.keySet()) { - String value = overriddenInputs.get(key); - 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); - } - } - } - } + fillInOverriddenInputs(inputValues, p); + fillInDefaultDotInputs(inputValues, p); boolean inputsContainsResultsDirectoryInput = inputValues.stream() .allMatch(inputMap -> inputMap.containsKey(CoreConstants.RESULTS_DIRECTORY_PARAM_NAME)); @@ -563,4 +552,78 @@ public void checkIfUserCanAccessExecution(String executionId) throws VipExceptio throw new VipException("Permission denied"); } + private void fillInOverriddenInputs(List> inputValues, Pipeline p) throws VipException { + // fill in overriddenInputs from explicit inputs + Map overriddenInputs = p.getOverriddenInputs(); + if (overriddenInputs != null) { + for (String key : overriddenInputs.keySet()) { + String value = overriddenInputs.get(key); + for (Map inputMap : inputValues) { + if (inputMap.containsKey(value)) { + inputMap.put(key, inputMap.get(value)); + } else { + logger.error("Error initialising {}, missing {} parameter", p.getIdentifier(), value); + throw new VipException(ApiError.INPUT_FIELD_MISSING, value); + } + } + } + } + } + + private void fillInDefaultDotInputs(List> inputValues, Pipeline p) throws VipException { + List dotInputList = p.getDotInputs(); + if (dotInputList == null || dotInputList.isEmpty()) { + return; + } + + boolean isInputMapList = inputValues.size() > 1; + // Find the maximum number of dot inputs + int dotMaxCount = isInputMapList ? 1 : inputValues.getFirst().entrySet().stream() + .filter(entry -> dotInputList.contains(entry.getKey())) + .mapToInt(v -> v.getValue().split(ApplicationConstants.SEPARATOR_LIST, -1).length) + .max() + .orElse(1); + + ArrayList dotParameters = p.getParameters().stream() + .filter(pp -> dotInputList.contains(pp.getName())) + .collect(Collectors.toCollection(ArrayList::new)); + + for (PipelineParameter pp : dotParameters) { + // always true on vip + if (pp.isReturnedValue()) { + continue; + } + + if (pp.getDefaultValue() == null) { + continue; + } + + String defaultVal = pp.getDefaultValue().toString(); + for (Map inputMap : inputValues) { + if (!inputMap.containsKey(pp.getName())) { + // One map per job + if (isInputMapList) { + inputMap.put(pp.getName(), defaultVal); + } else { + inputMap.put(pp.getName(), + String.join(ApplicationConstants.SEPARATOR_LIST, + Collections.nCopies(dotMaxCount, defaultVal))); + } + } else { + String[] currentValues = inputMap.get(pp.getName()) + .split(ApplicationConstants.SEPARATOR_LIST, -1); + // Complete the input with default values if it has fewer values than the maximum + if (currentValues.length < dotMaxCount) { + List valuesList = new ArrayList<>(Arrays.asList(currentValues)); + while (valuesList.size() < dotMaxCount) { + valuesList.add(defaultVal); + } + + inputMap.put(pp.getName(), + String.join(ApplicationConstants.SEPARATOR_LIST, valuesList)); + } + } + } + } + } } diff --git a/vip-api/src/main/java/fr/insalyon/creatis/vip/api/business/PipelineBusiness.java b/vip-api/src/main/java/fr/insalyon/creatis/vip/api/business/PipelineBusiness.java index 5d58a8dbd..504305f38 100644 --- a/vip-api/src/main/java/fr/insalyon/creatis/vip/api/business/PipelineBusiness.java +++ b/vip-api/src/main/java/fr/insalyon/creatis/vip/api/business/PipelineBusiness.java @@ -151,6 +151,7 @@ private Pipeline getPipelineFromBoutiquesDescriptor(String pipelineId) throws Vi p.setDescription(boutiques.getDescription()); Map overriddenInputs = boutiquesBusiness.getOverriddenInputs(boutiques); + List dotInputs = boutiquesBusiness.getDotInputs(boutiques); for (Input input : boutiques.getInputs()) { if (overriddenInputs != null && overriddenInputs.containsKey(input.getId())) { continue; // hide overriddenInputs from pipeline visible parameters @@ -165,6 +166,11 @@ private Pipeline getPipelineFromBoutiquesDescriptor(String pipelineId) throws Vi if (overriddenInputs != null) { p.setOverriddenInputs(overriddenInputs); } + + if (dotInputs != null) { + p.setDotInputs(dotInputs); + } + return p; } diff --git a/vip-api/src/main/java/fr/insalyon/creatis/vip/api/model/Pipeline.java b/vip-api/src/main/java/fr/insalyon/creatis/vip/api/model/Pipeline.java index 85ca2dac2..45c985dcc 100644 --- a/vip-api/src/main/java/fr/insalyon/creatis/vip/api/model/Pipeline.java +++ b/vip-api/src/main/java/fr/insalyon/creatis/vip/api/model/Pipeline.java @@ -4,6 +4,7 @@ import com.fasterxml.jackson.annotation.JsonIgnore; import java.util.ArrayList; +import java.util.List; import java.util.Map; public class Pipeline { @@ -16,6 +17,8 @@ public class Pipeline { private boolean canExecute; @JsonIgnore private Map overriddenInputs; + @JsonIgnore + private List dotInputs; public Pipeline() { } @@ -64,4 +67,13 @@ public Map getOverriddenInputs() { public void setOverriddenInputs(Map overriddenInputs) { this.overriddenInputs = overriddenInputs; } + + public List getDotInputs() { + return dotInputs; + } + + public void setDotInputs(List dotInputs) { + this.dotInputs = dotInputs; + } + } diff --git a/vip-application/src/main/java/fr/insalyon/creatis/vip/application/server/business/BoutiquesBusiness.java b/vip-application/src/main/java/fr/insalyon/creatis/vip/application/server/business/BoutiquesBusiness.java index 2c73381b2..ee5ad1083 100644 --- a/vip-application/src/main/java/fr/insalyon/creatis/vip/application/server/business/BoutiquesBusiness.java +++ b/vip-application/src/main/java/fr/insalyon/creatis/vip/application/server/business/BoutiquesBusiness.java @@ -272,4 +272,27 @@ public Map getOverriddenInputs(BoutiquesDescriptor descriptor) { } return result; } + + public List getDotInputs(BoutiquesDescriptor descriptor) { + final String customKeyName = "vip:dot"; + Custom custom = descriptor.getCustom(); + if (custom == null) { + return null; + } + + Map customProperties = custom.getAdditionalProperties(); + if (!customProperties.containsKey(customKeyName)) { + return null; + } +; + Object dotInputs = customProperties.get(customKeyName); + if (!(dotInputs instanceof List)) { + return null; + } + + return ((List) dotInputs).stream() + .filter(String.class::isInstance) + .map(String.class::cast) + .toList(); + } } 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 3e821b07f..4cc2e2474 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 @@ -155,21 +155,12 @@ private void fillInDefaultDotInputs(Map parametersMap, String applicationName, String applicationVersion) throws VipException { AppVersion appVersion = appVersionBusiness.getVersion(applicationName, applicationVersion); BoutiquesDescriptor descriptor = boutiquesBusiness.parseBoutiquesString(appVersion.getDescriptor()); - - final String dotKeyName = "vip:dot"; - if (!descriptor.getCustom().getAdditionalProperties().containsKey(dotKeyName)) { + // Get the string list of dot inputs + List dotInputList = boutiquesBusiness.getDotInputs(descriptor); + if (dotInputList == null || dotInputList.isEmpty()) { return; } - Object dotInputs = descriptor.getCustom().getAdditionalProperties().get(dotKeyName); - if (!(dotInputs instanceof List)) { - return; - } - // Get the string list of dot inputs - List dotInputList = ((List) dotInputs).stream() - .filter(String.class::isInstance) - .map(String.class::cast) - .toList(); // Build a map of inputId to Input for easy lookup Map dotInputsById = descriptor.getInputs().stream() .filter(input -> dotInputList.contains(input.getId()))