diff --git a/dsf-bpe/dsf-bpe-process-api-v2-impl/src/main/java/dev/dsf/bpe/v2/plugin/ProcessPluginImpl.java b/dsf-bpe/dsf-bpe-process-api-v2-impl/src/main/java/dev/dsf/bpe/v2/plugin/ProcessPluginImpl.java index 1dcc2c95f..15cc8c072 100644 --- a/dsf-bpe/dsf-bpe-process-api-v2-impl/src/main/java/dev/dsf/bpe/v2/plugin/ProcessPluginImpl.java +++ b/dsf-bpe/dsf-bpe-process-api-v2-impl/src/main/java/dev/dsf/bpe/v2/plugin/ProcessPluginImpl.java @@ -112,7 +112,8 @@ public ProcessPluginImpl(ProcessPluginDefinition processPluginDefinition, int pr this.processPluginDefinition = processPluginDefinition; - variablesFactory = delegateExecution -> new VariablesImpl(delegateExecution, getObjectMapper()); + variablesFactory = delegateExecution -> new VariablesImpl(delegateExecution, getObjectMapper(), + getProcessPluginApi().getDsfClientProvider().getLocalDsfClient()); pluginMdc = new PluginMdcImpl(processPluginApiVersion, processPluginDefinition.getName(), processPluginDefinition.getVersion(), jarFile.toString(), serverBaseUrl, variablesFactory); } diff --git a/dsf-bpe/dsf-bpe-process-api-v2-impl/src/main/java/dev/dsf/bpe/v2/service/StartTaskUpdaterImpl.java b/dsf-bpe/dsf-bpe-process-api-v2-impl/src/main/java/dev/dsf/bpe/v2/service/StartTaskUpdaterImpl.java new file mode 100644 index 000000000..74cc90f7d --- /dev/null +++ b/dsf-bpe/dsf-bpe-process-api-v2-impl/src/main/java/dev/dsf/bpe/v2/service/StartTaskUpdaterImpl.java @@ -0,0 +1,117 @@ +package dev.dsf.bpe.v2.service; + +import java.util.List; +import java.util.Objects; +import java.util.Optional; +import java.util.function.Consumer; +import java.util.function.Predicate; +import java.util.function.Supplier; + +import org.hl7.fhir.r4.model.Coding; +import org.hl7.fhir.r4.model.Task; +import org.hl7.fhir.r4.model.Task.TaskOutputComponent; +import org.hl7.fhir.r4.model.Type; + +import dev.dsf.bpe.v2.client.dsf.DsfClient; + +public class StartTaskUpdaterImpl implements StartTaskUpdater +{ + private final DsfClient client; + + private final Supplier getStartTask; + private final Consumer updateTask; + + public StartTaskUpdaterImpl(DsfClient client, Supplier getStartTask, Consumer updateTask) + { + this.client = Objects.requireNonNull(client, "client"); + + this.getStartTask = Objects.requireNonNull(getStartTask, "getStartTask"); + this.updateTask = Objects.requireNonNull(updateTask, "updateTask"); + } + + @Override + public void addOutput(Coding outputType, Type outputValue) + { + Task task = getStartTask.get(); + task.addOutput().setValue(outputValue).getType().addCoding(outputType); + + Task updated = client.update(task); + updateTask.accept(updated); + } + + @Override + public Optional getOutput(Coding outputType) + { + checkOutputType(outputType); + + Task task = getStartTask.get(); + return doGetOutput(task, outputType); + } + + private Optional doGetOutput(Task task, Coding outputType) + { + return task.getOutput().stream().filter(matchesSystemAndCodeOptionallyVersion(outputType)).findFirst(); + } + + private Predicate matchesSystemAndCodeOptionallyVersion(Coding outputType) + { + return o -> o.getType().getCoding().stream() + .anyMatch(c -> Objects.equals(c.getSystem(), outputType.getSystem()) + && Objects.equals(c.getCode(), outputType.getCode()) && outputType.hasVersion() + ? Objects.equals(c.getVersion(), outputType.getVersion()) + : true); + } + + @Override + public void modifyOutput(Coding outputType, Type outputValue) + { + checkOutputType(outputType); + + Task task = getStartTask.get(); + + doGetOutput(task, outputType) + .orElseThrow(() -> new IllegalArgumentException("Output for type " + outputType.getSystem() + "|" + + outputType.getCode() + + (outputType.hasVersion() ? " (version: " + outputType.getVersion() + ") not found" : ""))) + .setValue(outputValue); + + Task updated = client.update(task); + updateTask.accept(updated); + } + + @Override + public void removeOutput(Coding outputType) + { + checkOutputType(outputType); + + Task task = getStartTask.get(); + + List filtered = task.getOutput().stream() + .filter(matchesSystemAndCodeOptionallyVersion(outputType).negate()).toList(); + + if (task.getOutput().size() == filtered.size()) + throw new IllegalArgumentException("Output for type " + outputType.getSystem() + "|" + outputType.getCode() + + (outputType.hasVersion() ? " (version: " + outputType.getVersion() + ") not found" : "")); + + task.setOutput(filtered); + + Task updated = client.update(task); + updateTask.accept(updated); + } + + private void checkOutputType(Coding outputType) + { + Objects.requireNonNull(outputType, "outputType"); + + Objects.requireNonNull(outputType.getSystem(), "outputType.system"); + Objects.requireNonNull(outputType.getCode(), "outputType.code"); + Objects.requireNonNull(outputType.getVersion(), "outputType.version"); + + if (outputType.getSystem().isBlank()) + throw new IllegalArgumentException("outputType.system is blank"); + if (outputType.getCode().isBlank()) + throw new IllegalArgumentException("outputType.code is blank"); + if (outputType.getVersion().isBlank()) + throw new IllegalArgumentException("outputType.version is blank"); + } +} diff --git a/dsf-bpe/dsf-bpe-process-api-v2-impl/src/main/java/dev/dsf/bpe/v2/service/TaskHelperImpl.java b/dsf-bpe/dsf-bpe-process-api-v2-impl/src/main/java/dev/dsf/bpe/v2/service/TaskHelperImpl.java index 5e2009c07..e8c60ef69 100644 --- a/dsf-bpe/dsf-bpe-process-api-v2-impl/src/main/java/dev/dsf/bpe/v2/service/TaskHelperImpl.java +++ b/dsf-bpe/dsf-bpe-process-api-v2-impl/src/main/java/dev/dsf/bpe/v2/service/TaskHelperImpl.java @@ -107,21 +107,9 @@ public ParameterComponent createInput(Type value, Coding coding) return new ParameterComponent(new CodeableConcept(coding), value); } - @Override - public ParameterComponent createInput(Type value, String system, String code) - { - return createInput(value, new Coding(system, code, null)); - } - @Override public TaskOutputComponent createOutput(Type value, Coding coding) { return new TaskOutputComponent(new CodeableConcept(coding), value); } - - @Override - public TaskOutputComponent createOutput(Type value, String system, String code) - { - return createOutput(value, new Coding(system, code, null)); - } } diff --git a/dsf-bpe/dsf-bpe-process-api-v2-impl/src/main/java/dev/dsf/bpe/v2/spring/ApiServiceConfig.java b/dsf-bpe/dsf-bpe-process-api-v2-impl/src/main/java/dev/dsf/bpe/v2/spring/ApiServiceConfig.java index ddf598465..a6c5182ee 100644 --- a/dsf-bpe/dsf-bpe-process-api-v2-impl/src/main/java/dev/dsf/bpe/v2/spring/ApiServiceConfig.java +++ b/dsf-bpe/dsf-bpe-process-api-v2-impl/src/main/java/dev/dsf/bpe/v2/spring/ApiServiceConfig.java @@ -260,7 +260,7 @@ public JsonHolderSerializer jsonVariableSerializer() @Bean public Function listenerVariablesFactory() { - return execution -> new VariablesImpl(execution, objectMapper()); + return execution -> new VariablesImpl(execution, objectMapper(), dsfClientProvider().getLocalDsfClient()); } @Bean diff --git a/dsf-bpe/dsf-bpe-process-api-v2-impl/src/main/java/dev/dsf/bpe/v2/variables/VariablesImpl.java b/dsf-bpe/dsf-bpe-process-api-v2-impl/src/main/java/dev/dsf/bpe/v2/variables/VariablesImpl.java index 7eb2703b2..9ab102fc3 100644 --- a/dsf-bpe/dsf-bpe-process-api-v2-impl/src/main/java/dev/dsf/bpe/v2/variables/VariablesImpl.java +++ b/dsf-bpe/dsf-bpe-process-api-v2-impl/src/main/java/dev/dsf/bpe/v2/variables/VariablesImpl.java @@ -23,8 +23,11 @@ import com.fasterxml.jackson.databind.ObjectMapper; import dev.dsf.bpe.api.Constants; +import dev.dsf.bpe.v2.client.dsf.DsfClient; import dev.dsf.bpe.v2.constants.BpmnExecutionVariables; import dev.dsf.bpe.v2.listener.ListenerVariables; +import dev.dsf.bpe.v2.service.StartTaskUpdater; +import dev.dsf.bpe.v2.service.StartTaskUpdaterImpl; import dev.dsf.bpe.v2.variables.FhirResourceValues.FhirResourceValue; import dev.dsf.bpe.v2.variables.FhirResourcesListValues.FhirResourcesListValue; import dev.dsf.bpe.v2.variables.TargetValues.TargetValue; @@ -69,16 +72,22 @@ public int hashCode() private final DelegateExecution execution; private final ObjectMapper objectMapper; + private final StartTaskUpdater startTaskUpdater; + /** * @param execution * not null * @param objectMapper * not null + * @param client + * not null */ - public VariablesImpl(DelegateExecution execution, ObjectMapper objectMapper) + public VariablesImpl(DelegateExecution execution, ObjectMapper objectMapper, DsfClient client) { this.execution = Objects.requireNonNull(execution, "execution"); this.objectMapper = Objects.requireNonNull(objectMapper, "objectMapper"); + + startTaskUpdater = new StartTaskUpdaterImpl(client, this::getStartTask, this::updateTask); } private JsonHolder toJsonHolder(Object json) @@ -290,6 +299,12 @@ public Task getStartTask() return getFhirResource(START_TASK); } + @Override + public StartTaskUpdater getStartTaskUpdater() + { + return startTaskUpdater; + } + @Override public Task getLatestTask() { diff --git a/dsf-bpe/dsf-bpe-process-api-v2/src/main/java/dev/dsf/bpe/v2/activity/DefaultUserTaskListener.java b/dsf-bpe/dsf-bpe-process-api-v2/src/main/java/dev/dsf/bpe/v2/activity/DefaultUserTaskListener.java index a66f3dbda..f6313454a 100644 --- a/dsf-bpe/dsf-bpe-process-api-v2/src/main/java/dev/dsf/bpe/v2/activity/DefaultUserTaskListener.java +++ b/dsf-bpe/dsf-bpe-process-api-v2/src/main/java/dev/dsf/bpe/v2/activity/DefaultUserTaskListener.java @@ -16,6 +16,7 @@ import org.hl7.fhir.r4.model.Reference; import org.hl7.fhir.r4.model.ResourceType; import org.hl7.fhir.r4.model.StringType; +import org.hl7.fhir.r4.model.Task; import org.hl7.fhir.r4.model.Type; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -88,6 +89,10 @@ Coding toCoding() private final Set practitionerRoles = new HashSet<>(); private final Set practitioners = new HashSet<>(); + private String taskOutputSystem; + private String taskOutputCode; + private String taskOutputVersion; + /** * @param practitionerRole * does nothing if null or blank @@ -144,6 +149,48 @@ public void setPractitioners(List practitioners) } } + /** + * If {@link #taskOutputSystem}, {@link #taskOutputCode} and {@link #taskOutputVersion} are set with not blank + * values, an output parameter is added to the start Task with a reference to the created + * {@link QuestionnaireResponse} resource. + * + * @param taskOutputSystem + * @deprecated only for field injection + */ + @Deprecated + public void setTaskOutputSystem(String taskOutputSystem) + { + this.taskOutputSystem = taskOutputSystem; + } + + /** + * If {@link #taskOutputSystem}, {@link #taskOutputCode} and {@link #taskOutputVersion} are set with not blank + * values, an output parameter is added to the start Task with a reference to the created + * {@link QuestionnaireResponse} resource. + * + * @param taskOutputCode + * @deprecated only for field injection + */ + @Deprecated + public void setTaskOutputCode(String taskOutputCode) + { + this.taskOutputCode = taskOutputCode; + } + + /** + * If {@link #taskOutputSystem}, {@link #taskOutputCode} and {@link #taskOutputVersion} are set with not blank + * values, an output parameter is added to the start Task with a reference to the created + * {@link QuestionnaireResponse} resource. + * + * @param taskOutputVersion + * @deprecated only for field injection + */ + @Deprecated + public void setTaskOutputVersion(String taskOutputVersion) + { + this.taskOutputVersion = taskOutputVersion; + } + @Override public void notify(ProcessPluginApi api, Variables variables, CreateQuestionnaireResponseValues createQuestionnaireResponseValues) throws Exception @@ -286,7 +333,13 @@ protected void beforeQuestionnaireResponseCreate(ProcessPluginApi api, Variables /** * Override this method to execute code after the {@link QuestionnaireResponse} resource has been created on the - * DSF FHIR server + * DSF FHIR server
+ *
+ * Default implementation will add an output parameter to the start {@link Task} with a reference to the created + * {@link QuestionnaireResponse} resource if {@link #taskOutputSystem}, {@link #taskOutputCode} and + * {@link #taskOutputVersion} are set with not blank values.
+ *
+ * Use field inject in BPMN to set value. * * @param api * not null @@ -301,7 +354,12 @@ protected void beforeQuestionnaireResponseCreate(ProcessPluginApi api, Variables protected void afterQuestionnaireResponseCreate(ProcessPluginApi api, Variables variables, CreateQuestionnaireResponseValues createQuestionnaireResponseValues, QuestionnaireResponse afterCreate) { - // Nothing to do in default behavior + if (taskOutputSystem != null && !taskOutputSystem.isBlank() && taskOutputCode != null + && !taskOutputCode.isBlank() && taskOutputVersion != null && !taskOutputVersion.isBlank()) + { + variables.getStartTaskUpdater().addOutput(taskOutputSystem, taskOutputCode, taskOutputVersion, + new Reference(afterCreate.getIdElement().toUnqualifiedVersionless())); + } } /** diff --git a/dsf-bpe/dsf-bpe-process-api-v2/src/main/java/dev/dsf/bpe/v2/service/StartTaskUpdater.java b/dsf-bpe/dsf-bpe-process-api-v2/src/main/java/dev/dsf/bpe/v2/service/StartTaskUpdater.java new file mode 100644 index 000000000..028e0ba55 --- /dev/null +++ b/dsf-bpe/dsf-bpe-process-api-v2/src/main/java/dev/dsf/bpe/v2/service/StartTaskUpdater.java @@ -0,0 +1,206 @@ +package dev.dsf.bpe.v2.service; + +import java.util.Objects; +import java.util.Optional; + +import org.hl7.fhir.r4.model.Coding; +import org.hl7.fhir.r4.model.Task; +import org.hl7.fhir.r4.model.Task.TaskOutputComponent; +import org.hl7.fhir.r4.model.Type; + +import jakarta.ws.rs.WebApplicationException; + +public interface StartTaskUpdater +{ + /** + * Adds an output parameter to the start task, updates the {@link Task} on the DSF FHIR server and updates the + * process variable. + * + * @param outputType + * not null, must have system, code and version + * @param outputValue + * may be null + * @throws WebApplicationException + * if start task can not be update at the DSF FHIR server + * @throws IllegalArgumentException + * if system, code or version of the given outputType is blank + */ + void addOutput(Coding outputType, Type outputValue) throws WebApplicationException; + + /** + * Adds an output parameter to the start task, updates the {@link Task} on the DSF FHIR server and updates the + * process variable. + * + * @param outputTypeSystem + * not null, not blank + * @param outputTypeCode + * not null, not blank + * @param outputTypeVersion + * not null, not blank + * @param outputValue + * may be null + * @throws WebApplicationException + * if start task can not be update at the DSF FHIR server + * @throws IllegalArgumentException + * if outputTypeSystem, outputTypeCode or outputTypeVersion is blank + */ + default void addOutput(String outputTypeSystem, String outputTypeCode, String outputTypeVersion, Type outputValue) + throws WebApplicationException + { + Objects.requireNonNull(outputTypeSystem, "outputTypeSystem"); + Objects.requireNonNull(outputTypeCode, "outputTypeCode"); + Objects.requireNonNull(outputTypeVersion, "outputTypeVersion"); + + if (outputTypeSystem.isBlank()) + throw new IllegalArgumentException("outputTypeSystem is blank"); + if (outputTypeCode.isBlank()) + throw new IllegalArgumentException("outputTypeCode is blank"); + if (outputTypeVersion.isBlank()) + throw new IllegalArgumentException("outputTypeVersion is blank"); + + addOutput(new Coding(outputTypeSystem, outputTypeCode, null).setVersion(outputTypeVersion), outputValue); + } + + /** + * @param outputType + * not null, must have system and code and version + * @return Output with the given outputType from the start task if present + */ + Optional getOutput(Coding outputType); + + /** + * @param outputTypeSystem + * not null, not blank + * @param outputTypeCode + * not null, not blank + * @param outputTypeVersion + * not null, not blank + * @return Output with the given outputType from the start task if present + * @throws IllegalArgumentException + * if outputTypeSystem, outputTypeCode or outputTypeVersion is blank + */ + default Optional getOutput(String outputTypeSystem, String outputTypeCode, + String outputTypeVersion) + { + Objects.requireNonNull(outputTypeSystem, "outputTypeSystem"); + Objects.requireNonNull(outputTypeCode, "outputTypeCode"); + Objects.requireNonNull(outputTypeVersion, "outputTypeVersion"); + + if (outputTypeSystem.isBlank()) + throw new IllegalArgumentException("outputTypeSystem is blank"); + if (outputTypeCode.isBlank()) + throw new IllegalArgumentException("outputTypeCode is blank"); + if (outputTypeVersion.isBlank()) + throw new IllegalArgumentException("outputTypeVersion is blank"); + + return getOutput(new Coding(outputTypeSystem, outputTypeCode, null).setVersion(outputTypeVersion)); + } + + /** + * @param outputType + * not null, must have system and code and version + * @return true if the start task has output parameter with the given outputType + */ + default boolean hasOuput(Coding outputType) + { + return getOutput(outputType).isPresent(); + } + + /** + * Set the given outputValue for an output parameter of the start task with the given outputType, + * updates the {@link Task} on the DSF FHIR server and updates the process variable. + * + * @param outputTypeSystem + * not null, not blank + * @param outputTypeCode + * not null, not blank + * @param outputTypeVersion + * not null, not blank + * @param outputValue + * may be null + * @throws WebApplicationException + * if start task can not be update at the DSF FHIR server + * @throws IllegalArgumentException + * if the start task has no output parameter with the given outputType parameters or if + * outputTypeSystem, outputTypeCode or outputTypeVersion is blank + */ + default void modifyOutput(String outputTypeSystem, String outputTypeCode, String outputTypeVersion, + Type outputValue) throws WebApplicationException + { + Objects.requireNonNull(outputTypeSystem, "outputTypeSystem"); + Objects.requireNonNull(outputTypeCode, "outputTypeCode"); + Objects.requireNonNull(outputTypeVersion, "outputTypeVersion"); + + if (outputTypeSystem.isBlank()) + throw new IllegalArgumentException("outputTypeSystem is blank"); + if (outputTypeCode.isBlank()) + throw new IllegalArgumentException("outputTypeCode is blank"); + if (outputTypeVersion.isBlank()) + throw new IllegalArgumentException("outputTypeVersion is blank"); + + modifyOutput(new Coding(outputTypeSystem, outputTypeCode, null).setVersion(outputTypeVersion), outputValue); + } + + /** + * Set the given outputValue for an output parameter of the start task with the given outputType, + * updates the {@link Task} on the DSF FHIR server and updates the process variable. + * + * @param outputType + * not null, must have system, code and version + * @param outputValue + * may be null + * @throws WebApplicationException + * if start task can not be update at the DSF FHIR server + * @throws IllegalArgumentException + * if the start task has no output parameter with the given outputType or if system, code or + * version of the given outputType is blank + */ + void modifyOutput(Coding outputType, Type outputValue) throws WebApplicationException; + + /** + * Removes an output parameter of the start task with the given outputType, updates the {@link Task} on the + * DSF FHIR server and updates the process variable. + * + * @param outputType + * not null, must have system and code and version + * @throws WebApplicationException + * if start task can not be update at the DSF FHIR server + * @throws IllegalArgumentException + * if the start task has no output parameter with the given outputType or if system, code or + * version of the given outputType is blank + */ + void removeOutput(Coding outputType) throws WebApplicationException; + + /** + * Removes an output parameter of the start task with the given outputType, updates the {@link Task} on the + * DSF FHIR server and updates the process variable. + * + * @param outputTypeSystem + * not null, not blank + * @param outputTypeCode + * not null, not blank + * @param outputTypeVersion + * not null, not blank + * @throws WebApplicationException + * if start task can not be update at the DSF FHIR server + * @throws IllegalArgumentException + * if the start task has no output parameter with the given outputType or if system, code or + * version of the given outputType is blank + */ + default void removeOutput(String outputTypeSystem, String outputTypeCode, String outputTypeVersion) + throws WebApplicationException + { + Objects.requireNonNull(outputTypeSystem, "outputTypeSystem"); + Objects.requireNonNull(outputTypeCode, "outputTypeCode"); + Objects.requireNonNull(outputTypeVersion, "outputTypeVersion"); + + if (outputTypeSystem.isBlank()) + throw new IllegalArgumentException("outputTypeSystem is blank"); + if (outputTypeCode.isBlank()) + throw new IllegalArgumentException("outputTypeCode is blank"); + if (outputTypeVersion.isBlank()) + throw new IllegalArgumentException("outputTypeVersion is blank"); + + removeOutput(new Coding(outputTypeSystem, outputTypeCode, null).setVersion(outputTypeVersion)); + } +} diff --git a/dsf-bpe/dsf-bpe-process-api-v2/src/main/java/dev/dsf/bpe/v2/service/TaskHelper.java b/dsf-bpe/dsf-bpe-process-api-v2/src/main/java/dev/dsf/bpe/v2/service/TaskHelper.java index 0b8693e97..1cd198af9 100644 --- a/dsf-bpe/dsf-bpe-process-api-v2/src/main/java/dev/dsf/bpe/v2/service/TaskHelper.java +++ b/dsf-bpe/dsf-bpe-process-api-v2/src/main/java/dev/dsf/bpe/v2/service/TaskHelper.java @@ -387,11 +387,16 @@ Stream getInputParameters(Task task, String system, String c * may be null * @param code * may be null + * @param version + * may be null * @return not null * @see ParameterComponent#setType(org.hl7.fhir.r4.model.CodeableConcept) * @see ParameterComponent#setValue(Type) */ - ParameterComponent createInput(Type value, String system, String code); + default ParameterComponent createInput(Type value, String system, String code, String version) + { + return createInput(value, new Coding(system, code, null).setVersion(version)); + } /** @@ -416,9 +421,14 @@ Stream getInputParameters(Task task, String system, String c * may be null * @param code * may be null + * @param version + * may be null * @return not null * @see TaskOutputComponent#setType(org.hl7.fhir.r4.model.CodeableConcept) * @see TaskOutputComponent#setValue(Type) */ - TaskOutputComponent createOutput(Type value, String system, String code); + default TaskOutputComponent createOutput(Type value, String system, String code, String version) + { + return createOutput(value, new Coding(system, code, null).setVersion(version)); + } } diff --git a/dsf-bpe/dsf-bpe-process-api-v2/src/main/java/dev/dsf/bpe/v2/variables/Variables.java b/dsf-bpe/dsf-bpe-process-api-v2/src/main/java/dev/dsf/bpe/v2/variables/Variables.java index 9367cdcdd..dfced4a89 100644 --- a/dsf-bpe/dsf-bpe-process-api-v2/src/main/java/dev/dsf/bpe/v2/variables/Variables.java +++ b/dsf-bpe/dsf-bpe-process-api-v2/src/main/java/dev/dsf/bpe/v2/variables/Variables.java @@ -15,6 +15,7 @@ import dev.dsf.bpe.v2.activity.task.BusinessKeyStrategies; import dev.dsf.bpe.v2.constants.BpmnExecutionVariables; +import dev.dsf.bpe.v2.service.StartTaskUpdater; /** * Gives access to process execution variables. Includes factory methods for {@link Target} and {@link Targets} values. @@ -248,6 +249,14 @@ default Targets createTargets(Target... targets) */ Task getStartTask(); + /** + * Returns a {@link StartTaskUpdater} to modify the start task during process execution and propagate modified + * output parameters to the local DSF FHIR server. + * + * @return service to update the start task + */ + StartTaskUpdater getStartTaskUpdater(); + /** * Returns the latest {@link Task} received by this process or subprocess via a intermediate message catch event or * message receive task. diff --git a/dsf-bpe/dsf-bpe-test-plugin-v2/src/main/java/dev/dsf/bpe/test/AbstractTest.java b/dsf-bpe/dsf-bpe-test-plugin-v2/src/main/java/dev/dsf/bpe/test/AbstractTest.java index d3c9b22d9..70bd16468 100644 --- a/dsf-bpe/dsf-bpe-test-plugin-v2/src/main/java/dev/dsf/bpe/test/AbstractTest.java +++ b/dsf-bpe/dsf-bpe-test-plugin-v2/src/main/java/dev/dsf/bpe/test/AbstractTest.java @@ -32,7 +32,7 @@ protected void executeTests(ProcessPluginApi api, Variables variables, Function< private Consumer output(ProcessPluginApi api, Variables variables, String code) { - return t -> variables.getStartTask().addOutput( - api.getTaskHelper().createOutput(new StringType(t), "http://dsf.dev/fhir/CodeSystem/test", code)); + return t -> variables.getStartTask().addOutput(api.getTaskHelper().createOutput(new StringType(t), + "http://dsf.dev/fhir/CodeSystem/test", code, api.getProcessPluginDefinition().getResourceVersion())); } } diff --git a/dsf-fhir/dsf-fhir-server/src/main/java/dev/dsf/fhir/authorization/TaskAuthorizationRule.java b/dsf-fhir/dsf-fhir-server/src/main/java/dev/dsf/fhir/authorization/TaskAuthorizationRule.java index 316abb3b4..7bbb1a927 100644 --- a/dsf-fhir/dsf-fhir-server/src/main/java/dev/dsf/fhir/authorization/TaskAuthorizationRule.java +++ b/dsf-fhir/dsf-fhir-server/src/main/java/dev/dsf/fhir/authorization/TaskAuthorizationRule.java @@ -871,12 +871,50 @@ else if (TaskStatus.REQUESTED.equals(oldResource.getStatus()) } } + // INPROGRESS -> INPROGRESS + else if (TaskStatus.INPROGRESS.equals(oldResource.getStatus()) + && TaskStatus.INPROGRESS.equals(newResource.getStatus())) + { + final Optional notSame = reasonNotSame(oldResource, newResource); + if (notSame.isEmpty()) + { + if (taskAllowedForRecipient(connection, newResource)) + { + logger.info( + "Update of Task/{}/_history/{} ({} -> {}) authorized for local identity '{}', old Task.status in-progress, new Task.status in-progress, process allowed for current identity", + oldResourceId, oldResourceVersion, TaskStatus.INPROGRESS.toCode(), + TaskStatus.INPROGRESS.toCode(), identity.getName()); + + return Optional.of( + "Local identity, Task.status in-progress, Task.restriction.recipient local organization, process with instantiatesCanonical and message-name allowed for current identity" + + ", Task defines needed profile, Task.instantiatesCanonical not modified, Task.requester not modified, Task.restriction not modified, Task.input not modified"); + } + else + { + logger.warn( + "Update of Task/{}/_history/{} ({} -> {}) unauthorized for local identity '{}', process with instantiatesCanonical, message-name, requester or recipient not allowed", + oldResourceId, oldResourceVersion, TaskStatus.INPROGRESS.toCode(), + TaskStatus.INPROGRESS.toCode(), identity.getName()); + + return Optional.empty(); + } + } + else + { + logger.warn( + "Update of Task/{}/_history/{} ({} -> {}) unauthorized for local identity '{}', modification of Task properties {} not allowed", + oldResourceId, oldResourceVersion, TaskStatus.INPROGRESS.toCode(), + TaskStatus.INPROGRESS.toCode(), identity.getName(), notSame.get()); + + return Optional.empty(); + } + } // INPROGRESS -> COMPLETED else if (TaskStatus.INPROGRESS.equals(oldResource.getStatus()) && TaskStatus.COMPLETED.equals(newResource.getStatus())) { - final Optional same = reasonNotSame(oldResource, newResource); - if (same.isEmpty()) + final Optional notSame = reasonNotSame(oldResource, newResource); + if (notSame.isEmpty()) { if (taskAllowedForRecipient(connection, newResource)) { @@ -904,7 +942,7 @@ else if (TaskStatus.INPROGRESS.equals(oldResource.getStatus()) logger.warn( "Update of Task/{}/_history/{} ({} -> {}) unauthorized for local identity '{}', modification of Task properties {} not allowed", oldResourceId, oldResourceVersion, TaskStatus.INPROGRESS.toCode(), - TaskStatus.COMPLETED.toCode(), identity.getName(), same.get()); + TaskStatus.COMPLETED.toCode(), identity.getName(), notSame.get()); return Optional.empty(); } @@ -914,8 +952,8 @@ else if (TaskStatus.INPROGRESS.equals(oldResource.getStatus()) else if (TaskStatus.REQUESTED.equals(oldResource.getStatus()) && TaskStatus.FAILED.equals(newResource.getStatus())) { - final Optional same = reasonNotSame(oldResource, newResource); - if (same.isEmpty()) + final Optional notSame = reasonNotSame(oldResource, newResource); + if (notSame.isEmpty()) { logger.info( "Update of Task/{}/_history/{} ({} -> {}) authorized for local identity '{}', old Task.status requested, new Task.status failed", @@ -931,7 +969,7 @@ else if (TaskStatus.REQUESTED.equals(oldResource.getStatus()) logger.warn( "Update of Task/{}/_history/{} ({} -> {}) unauthorized for local identity '{}', modification of Task properties {} not allowed", oldResourceId, oldResourceVersion, TaskStatus.REQUESTED.toCode(), - TaskStatus.FAILED.toCode(), identity.getName(), same.get()); + TaskStatus.FAILED.toCode(), identity.getName(), notSame.get()); return Optional.empty(); } @@ -940,8 +978,8 @@ else if (TaskStatus.REQUESTED.equals(oldResource.getStatus()) else if (TaskStatus.INPROGRESS.equals(oldResource.getStatus()) && TaskStatus.FAILED.equals(newResource.getStatus())) { - final Optional same = reasonNotSame(oldResource, newResource); - if (same.isEmpty()) + final Optional notSame = reasonNotSame(oldResource, newResource); + if (notSame.isEmpty()) { if (taskAllowedForRecipient(connection, newResource)) { @@ -969,7 +1007,7 @@ else if (TaskStatus.INPROGRESS.equals(oldResource.getStatus()) logger.warn( "Update of Task/{}/_history/{} ({} -> {}) unauthorized for local identity '{}', modification of Task properties {} not allowed", oldResourceId, oldResourceVersion, TaskStatus.INPROGRESS.toCode(), - TaskStatus.FAILED.toCode(), identity.getName(), same.get()); + TaskStatus.FAILED.toCode(), identity.getName(), notSame.get()); return Optional.empty(); } @@ -985,6 +1023,7 @@ else if (TaskStatus.INPROGRESS.equals(oldResource.getStatus()) identity.getName(), Stream.of(Stream.of(TaskStatus.DRAFT, TaskStatus.DRAFT), Stream.of(TaskStatus.REQUESTED, TaskStatus.INPROGRESS), + Stream.of(TaskStatus.INPROGRESS, TaskStatus.INPROGRESS), Stream.of(TaskStatus.INPROGRESS, TaskStatus.COMPLETED), Stream.of(TaskStatus.INPROGRESS, TaskStatus.FAILED)) .map(s -> s.map(TaskStatus::toCode).collect(Collectors.joining("->"))) diff --git a/dsf-fhir/dsf-fhir-server/src/main/resources/fhir/static/form.css b/dsf-fhir/dsf-fhir-server/src/main/resources/fhir/static/form.css index 25d8eb123..ab99ca6fa 100644 --- a/dsf-fhir/dsf-fhir-server/src/main/resources/fhir/static/form.css +++ b/dsf-fhir/dsf-fhir-server/src/main/resources/fhir/static/form.css @@ -201,6 +201,15 @@ input[type=number] { display: none; } +.open { + align-self: center; + margin-left: 0.5em; +} + +.open svg { + margin-top: 0.2em; +} + .copy { align-self: center; margin-left: 0.5em; @@ -218,14 +227,6 @@ input.identifier-coding-code { margin-top: 6px; } -/* .input-output-header { - font-family: monospace; - font-size: 1.75em; - color: #326F95; - padding-left: 5px; - margin-bottom: 12px; -} */ - .invisible { display: none; } diff --git a/dsf-fhir/dsf-fhir-server/src/main/resources/fhir/template/resourceTask.html b/dsf-fhir/dsf-fhir-server/src/main/resources/fhir/template/resourceTask.html index c3b52b4af..d518e9aa3 100644 --- a/dsf-fhir/dsf-fhir-server/src/main/resources/fhir/template/resourceTask.html +++ b/dsf-fhir/dsf-fhir-server/src/main/resources/fhir/template/resourceTask.html @@ -30,6 +30,9 @@

Input

Insert Placeholder Value + + Open Resource + Copy to Clipboard
@@ -79,7 +82,7 @@

Input

Output

-
+
@@ -89,6 +92,9 @@

Output

+ + Open Resource + Copy to Clipboard
@@ -131,6 +137,9 @@

Output

+ + Open Resource + Copy to Clipboard
diff --git a/dsf-fhir/dsf-fhir-server/src/test/java/dev/dsf/fhir/integration/TaskIntegrationTest.java b/dsf-fhir/dsf-fhir-server/src/test/java/dev/dsf/fhir/integration/TaskIntegrationTest.java index bb3c470cb..23332c2cc 100755 --- a/dsf-fhir/dsf-fhir-server/src/test/java/dev/dsf/fhir/integration/TaskIntegrationTest.java +++ b/dsf-fhir/dsf-fhir-server/src/test/java/dev/dsf/fhir/integration/TaskIntegrationTest.java @@ -8,10 +8,12 @@ import java.io.IOException; import java.io.InputStream; +import java.nio.charset.StandardCharsets; import java.nio.file.Files; import java.nio.file.Paths; import java.sql.Connection; import java.sql.SQLException; +import java.util.Base64; import java.util.Date; import java.util.EnumSet; import java.util.List; @@ -22,6 +24,7 @@ import javax.sql.DataSource; import org.hl7.fhir.r4.model.ActivityDefinition; +import org.hl7.fhir.r4.model.Binary; import org.hl7.fhir.r4.model.Bundle; import org.hl7.fhir.r4.model.Bundle.BundleEntryComponent; import org.hl7.fhir.r4.model.Bundle.BundleType; @@ -1381,7 +1384,8 @@ public void testSearchByProfile() throws Exception assertEquals(0, result6.getTotal()); } - private Task createTask(TaskStatus createStatus, boolean createPluginResources) throws IOException, SQLException + private Task createTaskBinary(Task task, TaskStatus createStatus, boolean createPluginResources) + throws IOException, SQLException { if (createPluginResources) { @@ -1403,7 +1407,6 @@ private Task createTask(TaskStatus createStatus, boolean createPluginResources) assertNotNull(sd.getIdElement().getIdPart()); } - Task task = readTestTaskBinary("External_Test_Organization", "Test_Organization"); task.setStatus(createStatus); TaskDao taskDao = getSpringWebApplicationContext().getBean(TaskDao.class); Task created = taskDao.create(task); @@ -1417,7 +1420,8 @@ private Task createTask(TaskStatus createStatus, boolean createPluginResources) @Test public void testUpdateTaskFromInProgressToCompletedWithNonExistingInputReferenceToExternalBinary() throws Exception { - Task created = createTask(TaskStatus.INPROGRESS, true); + Task read = readTestTaskBinary("External_Test_Organization", "Test_Organization"); + Task created = createTaskBinary(read, TaskStatus.INPROGRESS, true); created.setStatus(TaskStatus.COMPLETED); Task updatedTask = getWebserviceClient().update(created); @@ -1425,10 +1429,93 @@ public void testUpdateTaskFromInProgressToCompletedWithNonExistingInputReference assertNotNull(updatedTask.getIdElement().getIdPart()); } + @Test + public void testUpdateTaskFromInProgressToInProgressWithNonExistingInputReferenceToBinaryNotAllowed() + throws Exception + { + Task created = createTaskInProgress(); + + created.addOutput().setValue(new Reference().setReference("Binary/" + UUID.randomUUID().toString())).getType() + .getCodingFirstRep().setSystem("http://dsf.dev/fhir/CodeSystem/test").setCode("binary-ref") + .setVersion("1.7"); + + expectForbidden(() -> getWebserviceClient().update(created)); + } + + @Test + public void testUpdateTaskFromInProgressToInProgressWithExistingInputReferencesToBinaryAllowed() throws Exception + { + Binary binary = new Binary(); + getReadAccessHelper().addLocal(binary); + binary.setContent(Base64.getDecoder().decode( + ("fCAgXyBcICAvIFx8XyAgIF98LyBcICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICANCnwgfCB8IHwvIF8gXCB8IHwgLyBfIFwgICAgICAgICAg" + + "ICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgDQp8IHxffCAvIF9fXyBcfCB8LyBfX18gXCAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIA0K" + + "fF9fX18vXy8gICBcX1xfL18vICAgXF9cXyAgX19fIF8gICBfICBfX19fICAgICAgICAgICAgICAgICAgICANCi8gX19ffHwgfCB8IHwgIC8gXCAgfCAgXyBcfF8gX3wg" + + "XCB8IHwvIF9fX3wgICAgICAgICAgICAgICAgICAgDQpcX19fIFx8IHxffCB8IC8gXyBcIHwgfF8pIHx8IHx8ICBcfCB8IHwgIF8gICAgICAgICAgICAgICAgICAgIA0K" + + "IF9fXykgfCAgXyAgfC8gX19fIFx8ICBfIDwgfCB8fCB8XCAgfCB8X3wgfCAgICAgICAgICAgICAgICAgICANCnxfX19fL3xffF98Xy9fLyAgX1xfXF98X1xfXF9fX3xf" + + "fF9cX3xcX19fX3wgX19fX18gIF9fX18gIF8gIF9fDQp8ICBfX198ICBfIFwgICAgLyBcICB8ICBcLyAgfCBfX19fXCBcICAgICAgLyAvIF8gXHwgIF8gXHwgfC8gLw0K" + + "fCB8XyAgfCB8XykgfCAgLyBfIFwgfCB8XC98IHwgIF98ICBcIFwgL1wgLyAvIHwgfCB8IHxfKSB8ICcgLyANCnwgIF98IHwgIF8gPCAgLyBfX18gXHwgfCAgfCB8IHxf" + + "X18gIFwgViAgViAvfCB8X3wgfCAgXyA8fCAuIFwgDQp8X3wgICB8X3wgXF9cL18vICAgXF9cX3wgIHxffF9fX19ffCAgXF8vXF8vICBcX19fL3xffCBcX1xffFxfXA") + .getBytes(StandardCharsets.UTF_8))); + binary.setContentType("text/plain"); + + Binary createdBinary = getWebserviceClient().create(binary); + + Task created = createTaskInProgress(); + created.addOutput() + .setValue(new Reference() + .setReference(createdBinary.getIdElement().toUnqualifiedVersionless().toString())) + .getType().getCodingFirstRep().setSystem("http://dsf.dev/fhir/CodeSystem/test").setCode("binary-ref") + .setVersion("1.7"); + + Task updatedTask1 = getWebserviceClient().update(created); + assertNotNull(updatedTask1); + assertNotNull(updatedTask1.getIdElement().getIdPart()); + + updatedTask1.setOutput(null); + + Task updatedTask2 = getWebserviceClient().update(updatedTask1); + assertNotNull(updatedTask2); + assertNotNull(updatedTask2.getIdElement().getIdPart()); + } + + private Task createTaskInProgress() throws IOException, SQLException + { + ActivityDefinition ad = getWebserviceClient() + .create(readActivityDefinition("dsf-test-activity-definition1-1.0.xml")); + assertNotNull(ad); + assertNotNull(ad.getIdElement().getIdPart()); + + CodeSystem cs = getWebserviceClient().create(readTestCodeSystem()); + assertNotNull(cs); + assertNotNull(cs.getIdElement().getIdPart()); + + ValueSet vs = getWebserviceClient().create(readTestValueSet()); + assertNotNull(vs); + assertNotNull(vs.getIdElement().getIdPart()); + + StructureDefinition sd = getWebserviceClient().create(readTestTaskProfile()); + assertNotNull(sd); + assertNotNull(sd.getIdElement().getIdPart()); + + Task read = readTestTask("Test_Organization", null, "Test_Organization"); + read.setStatus(TaskStatus.INPROGRESS); + read.addInput().setValue(new StringType(UUID.randomUUID().toString())).getType().getCodingFirstRep() + .setSystem("http://dsf.dev/fhir/CodeSystem/bpmn-message").setCode("business-key"); + + TaskDao taskDao = getSpringWebApplicationContext().getBean(TaskDao.class); + Task created = taskDao.create(read); + + ReferenceCleaner cleaner = getSpringWebApplicationContext().getBean(ReferenceCleaner.class); + cleaner.cleanLiteralReferences(created); + return created; + } + @Test public void testUpdateTaskFromRequestedToInProgressWithNonExistingInputReferenceToExternalBinary() throws Exception { - Task created = createTask(TaskStatus.REQUESTED, true); + Task read = readTestTaskBinary("External_Test_Organization", "Test_Organization"); + Task created = createTaskBinary(read, TaskStatus.REQUESTED, true); created.setStatus(TaskStatus.INPROGRESS); expectForbidden(() -> getWebserviceClient().update(created)); @@ -1437,7 +1524,8 @@ public void testUpdateTaskFromRequestedToInProgressWithNonExistingInputReference @Test public void testUpdateTaskFromRequestedToFailedWithNonExistingInputReferenceToExternalBinary() throws Exception { - Task created = createTask(TaskStatus.REQUESTED, true); + Task read = readTestTaskBinary("External_Test_Organization", "Test_Organization"); + Task created = createTaskBinary(read, TaskStatus.REQUESTED, true); created.setStatus(TaskStatus.FAILED); Task updatedTask = getWebserviceClient().update(created); @@ -1449,7 +1537,8 @@ public void testUpdateTaskFromRequestedToFailedWithNonExistingInputReferenceToEx public void testUpdateTaskFromRequestedToFailedWithNonExistingInputReferenceToExternalBinaryAndNonExistingPluginValidationResource() throws Exception { - Task created = createTask(TaskStatus.REQUESTED, false); + Task read = readTestTaskBinary("External_Test_Organization", "Test_Organization"); + Task created = createTaskBinary(read, TaskStatus.REQUESTED, false); created.setStatus(TaskStatus.FAILED); Task updatedTask = getWebserviceClient().update(created); @@ -2265,7 +2354,6 @@ public void testSerachTaskWithPractitionerUserByRequester() throws Exception assertNotNull(createdTask); assertNotNull(createdTask.getIdElement().getIdPart()); - Bundle searchResult = getPractitionerWebserviceClient().search(Task.class, Map.of("requester:identifier", List.of("http://dsf.dev/sid/practitioner-identifier|" + X509Certificates.PRACTITIONER_CLIENT_MAIL))); assertNotNull(searchResult); diff --git a/dsf-fhir/dsf-fhir-server/src/test/resources/integration/task/dsf-test-task-profile-1.0.xml b/dsf-fhir/dsf-fhir-server/src/test/resources/integration/task/dsf-test-task-profile-1.0.xml index 7bca78f7e..950b3bdc3 100644 --- a/dsf-fhir/dsf-fhir-server/src/test/resources/integration/task/dsf-test-task-profile-1.0.xml +++ b/dsf-fhir/dsf-fhir-server/src/test/resources/integration/task/dsf-test-task-profile-1.0.xml @@ -1,36 +1,82 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file