From 9276eacd71b29f6af8d29b9cc70151ae74e69e78 Mon Sep 17 00:00:00 2001 From: Hauke Hund Date: Mon, 20 Oct 2025 23:35:53 +0200 Subject: [PATCH 1/6] integration tests to verify in-progress to in-progress updates --- .../fhir/integration/TaskIntegrationTest.java | 102 ++++++++++++++-- .../task/dsf-test-task-profile-1.0.xml | 114 ++++++++++++------ 2 files changed, 175 insertions(+), 41 deletions(-) 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 From 5ef267cc0f3d69271dc9b12e2f23f94436c6fd09 Mon Sep 17 00:00:00 2001 From: Hauke Hund Date: Mon, 20 Oct 2025 23:36:59 +0200 Subject: [PATCH 2/6] modifications to allow in-progress to in-progress updates --- .../authorization/TaskAuthorizationRule.java | 57 ++++++++++++++++--- 1 file changed, 48 insertions(+), 9 deletions(-) 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("->"))) From d88fc51cf8a6b84d385ff17fdd7100fff27142cc Mon Sep 17 00:00:00 2001 From: Hauke Hund Date: Mon, 20 Oct 2025 23:42:37 +0200 Subject: [PATCH 3/6] mods to show outputs for in-progress tasks, open link for "url" types - Output parameters can now be displayed for Task resources with status in-progress. - When the Task status is not draft, output and input parameters now show an open icon with a link to the value of FHIR Types UriType and Reference (not identifier). --- .../src/main/resources/fhir/static/form.css | 17 +++++++++-------- .../resources/fhir/template/resourceTask.html | 11 ++++++++++- 2 files changed, 19 insertions(+), 9 deletions(-) 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
From 5a2e4fffba6fb21a7951512461fe0449c9ed7835 Mon Sep 17 00:00:00 2001 From: Hauke Hund Date: Tue, 21 Oct 2025 00:01:15 +0200 Subject: [PATCH 4/6] added version parameter to create method, moved code to interface - Added version parameter to the input- and output component create methods. - Moved some code from TaskHelperImpl to TaskHelper as default methods. --- .../dev/dsf/bpe/v2/service/TaskHelperImpl.java | 12 ------------ .../java/dev/dsf/bpe/v2/service/TaskHelper.java | 14 ++++++++++++-- .../main/java/dev/dsf/bpe/test/AbstractTest.java | 4 ++-- 3 files changed, 14 insertions(+), 16 deletions(-) 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/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-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())); } } From 5d532361371cd6fbf51d7d46b90f587fbe2097a5 Mon Sep 17 00:00:00 2001 From: Hauke Hund Date: Tue, 21 Oct 2025 00:47:23 +0200 Subject: [PATCH 5/6] new service to modify start task output components - Access to StartTaskUpdater service via Variables interface. The implementation needs access to the start task process variable and thus adding a getStartTaskUpdater() method to the Variables interface is the solution with the smallest impact on existing interfaces. Alternatively we could add the StartTaskUpdater as a third parameter to the ServiceTask#execute(ProcessPluginApi, Variables) method or have the user supply the start task manually to the StartTaskUpdater methods. --- .../dsf/bpe/v2/plugin/ProcessPluginImpl.java | 3 +- .../bpe/v2/service/StartTaskUpdaterImpl.java | 117 ++++++++++ .../dsf/bpe/v2/spring/ApiServiceConfig.java | 2 +- .../dsf/bpe/v2/variables/VariablesImpl.java | 17 +- .../dsf/bpe/v2/service/StartTaskUpdater.java | 206 ++++++++++++++++++ .../dev/dsf/bpe/v2/variables/Variables.java | 9 + 6 files changed, 351 insertions(+), 3 deletions(-) create mode 100644 dsf-bpe/dsf-bpe-process-api-v2-impl/src/main/java/dev/dsf/bpe/v2/service/StartTaskUpdaterImpl.java create mode 100644 dsf-bpe/dsf-bpe-process-api-v2/src/main/java/dev/dsf/bpe/v2/service/StartTaskUpdater.java 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/spring/ApiServiceConfig.java b/dsf-bpe/dsf-bpe-process-api-v2-impl/src/main/java/dev/dsf/bpe/v2/spring/ApiServiceConfig.java index 62623ae2d..da4509f60 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 @@ -254,7 +254,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/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/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. From 30204c9d6b133ce0be50554536d2e604dd681bfe Mon Sep 17 00:00:00 2001 From: Hauke Hund Date: Tue, 21 Oct 2025 00:52:42 +0200 Subject: [PATCH 6/6] option to add reference to created QuestionnaireResponse to start task - Three new String fields to be configured via BPMN field injections to define system, code and version of the output parameter for an automatically created reference to the created QuestionnaireResponse resource. Reference will only be created if taskOutputSystem, taskOutputCode and taskOutputVersion are set with non null and not blank values. --- .../v2/activity/DefaultUserTaskListener.java | 62 ++++++++++++++++++- 1 file changed, 60 insertions(+), 2 deletions(-) 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())); + } } /**