From afea129923040d212f4b95731f5c46b27afa5d92 Mon Sep 17 00:00:00 2001 From: Hauke Hund Date: Sun, 30 Aug 2026 17:36:54 +0200 Subject: [PATCH 01/13] added new transaction and batch bundle tests --- .../integration/BinaryIntegrationTest.java | 28 ++++ .../integration/BundleIntegrationTest.java | 129 +++++++++++++++++- 2 files changed, 155 insertions(+), 2 deletions(-) diff --git a/dsf-fhir/dsf-fhir-server/src/test/java/dev/dsf/fhir/integration/BinaryIntegrationTest.java b/dsf-fhir/dsf-fhir-server/src/test/java/dev/dsf/fhir/integration/BinaryIntegrationTest.java index 8664de880..ea4ed1531 100755 --- a/dsf-fhir/dsf-fhir-server/src/test/java/dev/dsf/fhir/integration/BinaryIntegrationTest.java +++ b/dsf-fhir/dsf-fhir-server/src/test/java/dev/dsf/fhir/integration/BinaryIntegrationTest.java @@ -1849,6 +1849,34 @@ public void testCreateViaBatchBundle() throws Exception assertNotNull(responseBundle.getEntry().get(0).getResponse().getEtag()); assertEquals("W/\"1\"", responseBundle.getEntry().get(0).getResponse().getEtag()); } + + @Test + public void testCreateBinaryWithNonExistingSecurityContextViaBatchBundle() throws Exception + { + final String contentType = MediaType.TEXT_PLAIN; + final byte[] data = "Hello World".getBytes(StandardCharsets.UTF_8); + + Binary binary = new Binary(); + binary.setContentType(contentType); + binary.setData(data); + binary.getSecurityContext().setReference("ResearchStudy/"+UUID.randomUUID().toString()); + + Bundle bundle = new Bundle(); + bundle.setType(BundleType.BATCH); + bundle.addEntry().setFullUrl("urn:uuid:" + UUID.randomUUID().toString()).setResource(binary).getRequest() + .setMethod(HTTPVerb.POST).setUrl("Binary"); + + Bundle responseBundle = getWebserviceClient().postBundle(bundle); + + assertNotNull(responseBundle); + assertEquals(BundleType.BATCHRESPONSE, responseBundle.getType()); + assertEquals(1, responseBundle.getEntry().size()); + assertNotNull(responseBundle.getEntry().get(0)); + + assertNull(responseBundle.getEntry().get(0).getResource()); + assertNotNull(responseBundle.getEntry().get(0).getResponse().getStatus()); + assertEquals("403 Forbidden", responseBundle.getEntry().get(0).getResponse().getStatus()); + } @Test public void testCreateNotAllowedExternalUserViaBatchBundle() throws Exception diff --git a/dsf-fhir/dsf-fhir-server/src/test/java/dev/dsf/fhir/integration/BundleIntegrationTest.java b/dsf-fhir/dsf-fhir-server/src/test/java/dev/dsf/fhir/integration/BundleIntegrationTest.java index 72d44fd95..6dcac18b7 100644 --- a/dsf-fhir/dsf-fhir-server/src/test/java/dev/dsf/fhir/integration/BundleIntegrationTest.java +++ b/dsf-fhir/dsf-fhir-server/src/test/java/dev/dsf/fhir/integration/BundleIntegrationTest.java @@ -27,6 +27,7 @@ import java.sql.Connection; import java.sql.PreparedStatement; import java.sql.ResultSet; +import java.util.Comparator; import java.util.Date; import java.util.List; import java.util.Map; @@ -49,6 +50,8 @@ import org.hl7.fhir.r4.model.ElementDefinition; import org.hl7.fhir.r4.model.Enumerations.FHIRVersion; import org.hl7.fhir.r4.model.Enumerations.PublicationStatus; +import org.hl7.fhir.r4.model.Group; +import org.hl7.fhir.r4.model.Group.GroupType; import org.hl7.fhir.r4.model.IdType; import org.hl7.fhir.r4.model.Identifier; import org.hl7.fhir.r4.model.OperationOutcome; @@ -61,6 +64,7 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import dev.dsf.fhir.dao.GroupDao; import dev.dsf.fhir.dao.PatientDao; import dev.dsf.fhir.dao.StructureDefinitionDao; @@ -150,7 +154,10 @@ public void testDeleteTaskProfileViaBundleTestSupportedProfilesInConformanceStat request.setMethod(HTTPVerb.DELETE); request.setUrl("StructureDefinition/" + newSid.getIdPart()); - getWebserviceClient().withMinimalReturn().postBundle(deleteBundle); + Bundle returnBundle = getWebserviceClient().withMinimalReturn().postBundle(deleteBundle); + assertNotNull(returnBundle); + assertNotNull(returnBundle.getEntry()); + assertEquals(1, returnBundle.getEntry().size()); assertFalse(testProfileSupported(taskProfileUrl)); StructureDefinitionDao sDdao = getSpringWebApplicationContext().getBean("structureDefinitionDao", @@ -213,6 +220,50 @@ public void testPostTransactionBundle() throws Exception } } + @Test + public void testPostTransactionBundleAlternativeEntryOrder() throws Exception + { + Patient p0 = new Patient(); + PatientDao dao = getSpringWebApplicationContext().getBean(PatientDao.class); + Patient p1 = dao.create(p0); + + List entrySortOrder = List.of(HTTPVerb.GET, HTTPVerb.PUT, HTTPVerb.POST, HTTPVerb.DELETE); + + Bundle bundle = createTestBundle(BundleType.TRANSACTION, p1.getIdElement()); + bundle.setEntry(bundle.getEntry().stream() + .sorted(Comparator.comparing(e -> entrySortOrder.indexOf(e.getRequest().getMethod()))).toList()); + + Bundle rBundle = getWebserviceClient().postBundle(bundle); + + checkReturnBundle(BundleType.TRANSACTIONRESPONSE, rBundle, bundle.getEntry().size(), + List.of("200 OK", "200 OK", "200 OK", "404 Not Found", "200 OK", "201 Created", "200 OK", "200 OK")); + + DataSource dataSource = getSpringWebApplicationContext().getBean("dataSource", DataSource.class); + try (Connection connection = dataSource.getConnection(); + PreparedStatement statement = connection.prepareStatement("SELECT count(*) FROM current_patients"); + ResultSet result = statement.executeQuery()) + { + assertTrue(result.next()); + assertEquals(1, result.getInt(1)); + } + } + + @Test + public void testPostTransactionBundleWithBadEntry() throws Exception + { + Patient p0 = new Patient(); + PatientDao dao = getSpringWebApplicationContext().getBean(PatientDao.class); + Patient p1 = dao.create(p0); + + List entrySortOrder = List.of(HTTPVerb.GET, HTTPVerb.PUT, HTTPVerb.POST, HTTPVerb.DELETE); + + Bundle bundle = createTestBundle(BundleType.TRANSACTION, p1.getIdElement()); + bundle.setEntry(bundle.getEntry().stream() + .sorted(Comparator.comparing(e -> entrySortOrder.indexOf(e.getRequest().getMethod()))).toList()); + + expectForbidden(() -> getMinimalWebserviceClient().postBundle(bundle)); + } + @Test public void testPostBatchBundle() throws Exception { @@ -305,7 +356,7 @@ private void checkReturnBundle(BundleType type, Bundle rBundle, int expectedEntr { assertTrue(rBundle.getEntry().get(i).hasResponse()); assertTrue(rBundle.getEntry().get(i).getResponse().hasStatus()); - assertEquals(rBundle.getEntry().get(i).getResponse().getStatus(), expectedStatus.get(i)); + assertEquals(expectedStatus.get(i), rBundle.getEntry().get(i).getResponse().getStatus()); } } @@ -423,4 +474,78 @@ private StructureDefinition readTestBundleProfile(String bundleFile) throws IOEx return fhirContext.newXmlParser().parseResource(StructureDefinition.class, in); } } + + @Test + public void testBatchBundleWithNonResolvableReference() throws Exception + { + Bundle b = createBundleWithGroupResource(BundleType.BATCH, GroupType.PERSON); + + Bundle result = getWebserviceClient().postBundle(b); + assertNotNull(result); + assertEquals(1, result.getEntry().size()); + assertEquals("403 Forbidden", result.getEntry().get(0).getResponse().getStatus()); + + GroupDao groupDao = getSpringWebApplicationContext().getBean(GroupDao.class); + List all = groupDao.readAll(); + assertNotNull(all); + assertEquals(0, all.size()); + } + + @Test + public void testTransactionBundleWithNonResolvableReference() throws Exception + { + Bundle b = createBundleWithGroupResource(BundleType.TRANSACTION, GroupType.PERSON); + + expectForbidden(() -> getWebserviceClient().postBundle(b)); + + GroupDao groupDao = getSpringWebApplicationContext().getBean(GroupDao.class); + List all = groupDao.readAll(); + assertNotNull(all); + assertEquals(0, all.size()); + } + + @Test + public void testBatchBundleWithoutGroupTypeAndNonResolvableReference() throws Exception + { + Bundle b = createBundleWithGroupResource(BundleType.BATCH, null); + + Bundle result = getWebserviceClient().postBundle(b); + assertNotNull(result); + assertEquals(1, result.getEntry().size()); + assertEquals("403 Forbidden", result.getEntry().get(0).getResponse().getStatus()); + + GroupDao groupDao = getSpringWebApplicationContext().getBean(GroupDao.class); + List all = groupDao.readAll(); + assertNotNull(all); + assertEquals(0, all.size()); + } + + @Test + public void testTransactionBundleWithoutGroupTypeAndNonResolvableReference() throws Exception + { + Bundle b = createBundleWithGroupResource(BundleType.TRANSACTION, null); + + expectForbidden(() -> getWebserviceClient().postBundle(b)); + + GroupDao groupDao = getSpringWebApplicationContext().getBean(GroupDao.class); + List all = groupDao.readAll(); + assertNotNull(all); + assertEquals(0, all.size()); + } + + private Bundle createBundleWithGroupResource(BundleType bundleType, GroupType groupType) + { + Group g = new Group(); + getReadAccessHelper().addAll(g); + g.setType(groupType); + g.setActual(false); + g.getManagingEntity().setReference("Organization/" + UUID.randomUUID().toString()); + + Bundle b = new Bundle().setType(bundleType); + BundleEntryComponent e = b.addEntry(); + e.setFullUrl("urn:uuid:" + UUID.randomUUID().toString()); + e.setResource(g); + e.getRequest().setMethod(HTTPVerb.POST).setUrl("Group"); + return b; + } } \ No newline at end of file From 23a014c1a7fc37a99d46388b38738f0c9666c79c Mon Sep 17 00:00:00 2001 From: Hauke Hund Date: Mon, 31 Aug 2026 11:49:00 +0200 Subject: [PATCH 02/13] refactored code - Removed ExceptionHandler dependency from RootServiceImpl. Method handleBadBundleException moved to RootServiceImpl. - Change removes CommandList <-> ExceptionHandler dependency loop. --- .../dev/dsf/fhir/help/ExceptionHandler.java | 18 ---------- .../fhir/spring/config/WebserviceConfig.java | 3 +- .../fhir/webservice/impl/RootServiceImpl.java | 34 ++++++++++++++----- 3 files changed, 27 insertions(+), 28 deletions(-) diff --git a/dsf-fhir/dsf-fhir-server/src/main/java/dev/dsf/fhir/help/ExceptionHandler.java b/dsf-fhir/dsf-fhir-server/src/main/java/dev/dsf/fhir/help/ExceptionHandler.java index 68c126ffd..729907057 100755 --- a/dsf-fhir/dsf-fhir-server/src/main/java/dev/dsf/fhir/help/ExceptionHandler.java +++ b/dsf-fhir/dsf-fhir-server/src/main/java/dev/dsf/fhir/help/ExceptionHandler.java @@ -29,8 +29,6 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import dev.dsf.fhir.dao.command.CommandList; -import dev.dsf.fhir.dao.exception.BadBundleException; import dev.dsf.fhir.dao.exception.ResourceDeletedException; import dev.dsf.fhir.dao.exception.ResourceNotFoundException; import dev.dsf.fhir.dao.exception.ResourceNotMarkedDeletedException; @@ -365,20 +363,4 @@ public R catchAndLogSqlAndResourceNotFoundException(String resourceTypeName, return onSqlException.get(); } } - - public CommandList handleBadBundleException(Supplier commandListCreator) - { - try - { - return commandListCreator.get(); - } - catch (BadBundleException e) - { - logger.debug("Error while creating command list for bundle", e); - logger.warn("Error while creating command list for bundle: {}", e.getMessage()); - - Response response = responseGenerator.badBundleRequest(e.getMessage()); - throw new WebApplicationException(response); - } - } } diff --git a/dsf-fhir/dsf-fhir-server/src/main/java/dev/dsf/fhir/spring/config/WebserviceConfig.java b/dsf-fhir/dsf-fhir-server/src/main/java/dev/dsf/fhir/spring/config/WebserviceConfig.java index 2d4286c3d..ae649842a 100755 --- a/dsf-fhir/dsf-fhir-server/src/main/java/dev/dsf/fhir/spring/config/WebserviceConfig.java +++ b/dsf-fhir/dsf-fhir-server/src/main/java/dev/dsf/fhir/spring/config/WebserviceConfig.java @@ -933,8 +933,7 @@ private RootServiceSecure rootServiceSecure() private RootServiceImpl rootServiceImpl() { return new RootServiceImpl(commandConfig.commandFactory(), helperConfig.responseGenerator(), - helperConfig.parameterConverter(), helperConfig.exceptionHandler(), referenceConfig.referenceCleaner(), - historyConfig.historyService()); + helperConfig.parameterConverter(), referenceConfig.referenceCleaner(), historyConfig.historyService()); } @Bean diff --git a/dsf-fhir/dsf-fhir-server/src/main/java/dev/dsf/fhir/webservice/impl/RootServiceImpl.java b/dsf-fhir/dsf-fhir-server/src/main/java/dev/dsf/fhir/webservice/impl/RootServiceImpl.java index 77c714877..00adbe37f 100755 --- a/dsf-fhir/dsf-fhir-server/src/main/java/dev/dsf/fhir/webservice/impl/RootServiceImpl.java +++ b/dsf-fhir/dsf-fhir-server/src/main/java/dev/dsf/fhir/webservice/impl/RootServiceImpl.java @@ -16,16 +16,19 @@ package dev.dsf.fhir.webservice.impl; import java.util.Objects; +import java.util.function.Supplier; import org.hl7.fhir.r4.model.Bundle; import org.hl7.fhir.r4.model.OperationOutcome; import org.hl7.fhir.r4.model.OperationOutcome.IssueSeverity; import org.hl7.fhir.r4.model.OperationOutcome.IssueType; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; import org.springframework.beans.factory.InitializingBean; import dev.dsf.fhir.dao.command.CommandFactory; import dev.dsf.fhir.dao.command.CommandList; -import dev.dsf.fhir.help.ExceptionHandler; +import dev.dsf.fhir.dao.exception.BadBundleException; import dev.dsf.fhir.help.ParameterConverter; import dev.dsf.fhir.help.ResponseGenerator; import dev.dsf.fhir.history.HistoryService; @@ -33,6 +36,7 @@ import dev.dsf.fhir.webservice.base.AbstractBasicService; import dev.dsf.fhir.webservice.specification.RootService; import jakarta.ws.rs.HttpMethod; +import jakarta.ws.rs.WebApplicationException; import jakarta.ws.rs.core.CacheControl; import jakarta.ws.rs.core.HttpHeaders; import jakarta.ws.rs.core.Response; @@ -41,6 +45,8 @@ public class RootServiceImpl extends AbstractBasicService implements RootService, InitializingBean { + private static final Logger logger = LoggerFactory.getLogger(RootServiceImpl.class); + public static final String ROOT_GET = RootServiceImpl.class.getCanonicalName() + ".get"; private static final CacheControl NO_STORE = new CacheControl(); @@ -53,18 +59,15 @@ public class RootServiceImpl extends AbstractBasicService implements RootService private final CommandFactory commandFactory; private final ResponseGenerator responseGenerator; private final ParameterConverter parameterConverter; - private final ExceptionHandler exceptionHandler; private final ReferenceCleaner referenceCleaner; private final HistoryService historyService; public RootServiceImpl(CommandFactory commandFactory, ResponseGenerator responseGenerator, - ParameterConverter parameterConverter, ExceptionHandler exceptionHandler, ReferenceCleaner referenceCleaner, - HistoryService historyService) + ParameterConverter parameterConverter, ReferenceCleaner referenceCleaner, HistoryService historyService) { this.commandFactory = commandFactory; this.responseGenerator = responseGenerator; this.parameterConverter = parameterConverter; - this.exceptionHandler = exceptionHandler; this.referenceCleaner = referenceCleaner; this.historyService = historyService; } @@ -75,7 +78,6 @@ public void afterPropertiesSet() throws Exception Objects.requireNonNull(commandFactory, "commandFactory"); Objects.requireNonNull(responseGenerator, "responseGenerator"); Objects.requireNonNull(parameterConverter, "parameterConverter"); - Objects.requireNonNull(exceptionHandler, "exceptionHandler"); Objects.requireNonNull(referenceCleaner, "referenceCleaner"); Objects.requireNonNull(historyService, "historyService"); } @@ -96,8 +98,8 @@ public Response root(UriInfo uri, HttpHeaders headers) @Override public Response handleBundle(Bundle bundle, UriInfo uri, HttpHeaders headers) { - CommandList commands = exceptionHandler - .handleBadBundleException(() -> commandFactory.createCommands(bundle, getCurrentIdentity(), + CommandList commands = handleBadBundleException( + () -> commandFactory.createCommands(bundle, getCurrentIdentity(), parameterConverter.getPreferReturn(headers), parameterConverter.getPreferHandling(headers))); Bundle result = commands.execute(); // throws WebApplicationException @@ -106,6 +108,22 @@ public Response handleBundle(Bundle bundle, UriInfo uri, HttpHeaders headers) .response(Status.OK, result, parameterConverter.getMediaTypeThrowIfNotSupported(uri, headers)).build(); } + private CommandList handleBadBundleException(Supplier supplier) + { + try + { + return supplier.get(); + } + catch (BadBundleException e) + { + logger.debug("Unable to create command list for bundle", e); + logger.warn("Unable to create command list for bundle: {}", e.getMessage()); + + Response response = responseGenerator.badBundleRequest(e.getMessage()); + throw new WebApplicationException(response); + } + } + @Override public Response history(UriInfo uri, HttpHeaders headers) { From 2aa6b45f92ed42249046957bc80d1ba64c15bb47 Mon Sep 17 00:00:00 2001 From: Hauke Hund Date: Wed, 7 Oct 2026 11:51:14 +0200 Subject: [PATCH 03/13] refactored transaction and batch bundle error and audit-log handling --- .../fhir/dao/command/AbstractCommandList.java | 145 +++++++-- .../fhir/dao/command/BatchCommandList.java | 279 ++++++++-------- .../dao/command/CheckReferencesCommand.java | 12 +- .../dao/command/TransactionCommandList.java | 299 ++++++++++-------- 4 files changed, 419 insertions(+), 316 deletions(-) diff --git a/dsf-fhir/dsf-fhir-server/src/main/java/dev/dsf/fhir/dao/command/AbstractCommandList.java b/dsf-fhir/dsf-fhir-server/src/main/java/dev/dsf/fhir/dao/command/AbstractCommandList.java index b15ef23a0..4ebd1d9d8 100644 --- a/dsf-fhir/dsf-fhir-server/src/main/java/dev/dsf/fhir/dao/command/AbstractCommandList.java +++ b/dsf-fhir/dsf-fhir-server/src/main/java/dev/dsf/fhir/dao/command/AbstractCommandList.java @@ -15,13 +15,16 @@ */ package dev.dsf.fhir.dao.command; -import java.util.ArrayList; +import java.util.Comparator; import java.util.List; +import java.util.Objects; +import java.util.function.Function; import javax.sql.DataSource; import org.hl7.fhir.r4.model.Binary; import org.hl7.fhir.r4.model.Bundle.BundleEntryComponent; +import org.hl7.fhir.r4.model.Bundle.BundleEntryResponseComponent; import org.hl7.fhir.r4.model.OperationOutcome; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -37,25 +40,119 @@ abstract class AbstractCommandList { private static final Logger audit = LoggerFactory.getLogger("dsf-audit-logger"); + public static final class CommandResult + { + private final Command command; + + private BundleEntryComponent result; + private Exception exception; + + public CommandResult(Command command) + { + this.command = command; + } + + public Command getCommand() + { + return command; + } + + public void setResult(BundleEntryComponent result) + { + Objects.requireNonNull(result, "result"); + if (hasException()) + throw new IllegalStateException("Exception set"); + + this.result = result; + } + + public BundleEntryComponent getResult() + { + return result; + } + + public boolean hasResult() + { + return result != null; + } + + public void setException(Exception exception) + { + Objects.requireNonNull(exception, "exception"); + if (hasException()) + throw new IllegalStateException("Exception set"); + + this.exception = exception; + } + + public void addSuppressedException(Throwable exception) + { + Objects.requireNonNull(exception, "exception"); + if (!hasException()) + throw new IllegalStateException("Exception not set"); + + this.exception.addSuppressed(exception); + } + + public Exception getException() + { + return exception; + } + + public boolean hasException() + { + return exception != null; + } + + public void convertExceptionToResult(Function internalServerErrorConverter) + { + if (hasException()) + { + result = toEntry(exception, internalServerErrorConverter); + exception = null; + } + } + + private BundleEntryComponent toEntry(Exception exception, + Function internalServerErrorConverter) + { + BundleEntryComponent entry = new BundleEntryComponent(); + BundleEntryResponseComponent response = entry.getResponse(); + + if (!(exception instanceof WebApplicationException w) + || !(w.getResponse().getEntity() instanceof OperationOutcome)) + { + exception = internalServerErrorConverter.apply(exception); + } + + Response httpResponse = ((WebApplicationException) exception).getResponse(); + response.setStatus(httpResponse.getStatusInfo().getStatusCode() + " " + + httpResponse.getStatusInfo().getReasonPhrase()); + response.setOutcome((OperationOutcome) httpResponse.getEntity()); + + return entry; + } + } + protected final DataSource dataSource; protected final DataSource permanentDeleteDataSource; protected final String dbUsersGroup; protected final ExceptionHandler exceptionHandler; - protected final List commands = new ArrayList<>(); + protected final List commands; protected final boolean hasModifyingCommands; protected final boolean hasBinaryModifyingCommands; protected AbstractCommandList(DataSource dataSource, DataSource permanentDeleteDataSource, String dbUsersGroup, - ExceptionHandler exceptionHandler, List commands) + ExceptionHandler exceptionHandler, List commands, Comparator sortOrder) { this.dataSource = dataSource; this.permanentDeleteDataSource = permanentDeleteDataSource; this.dbUsersGroup = dbUsersGroup; this.exceptionHandler = exceptionHandler; - if (commands != null) - this.commands.addAll(commands); + this.commands = commands == null ? List.of() + : commands.stream().sorted(sortOrder).map(CommandResult::new).toList(); hasModifyingCommands = hasModifyingCommands(commands); hasBinaryModifyingCommands = hasModifyingCommands && hasBinaryModifyingCommands(commands); @@ -72,8 +169,19 @@ private static boolean hasBinaryModifyingCommands(List comman && Binary.class.getAnnotation(ResourceDef.class).name().equals(m.getResourceTypeName())); } + protected void auditLogResult(CommandResult commandResult) + { + if (commandResult.hasException()) + throw new IllegalArgumentException("CommandResult has exception"); + + auditLogResult(commandResult.getCommand(), commandResult.getResult()); + } + protected void auditLogResult(Command command, BundleEntryComponent result) { + Objects.requireNonNull(command, "command"); + Objects.requireNonNull(result, "result"); + String resultOutcome = failed(result) ? "failed" : "successful"; if (command instanceof DeleteCommand) @@ -82,13 +190,13 @@ protected void auditLogResult(Command command, BundleEntryComponent result) command.getResourceTypeName(), command.getIdentity().getName(), command.getIndex(), resultOutcome, result.getResponse().getStatus()); } - else if (command instanceof CreateCommand) + else if (command instanceof CreateCommand || command instanceof CheckReferencesCommand cr && cr.isCreate()) { audit.info("Create of {} for identity '{}' via bundle at index {} {}, status: {}", command.getResourceTypeName(), command.getIdentity().getName(), command.getIndex(), resultOutcome, result.getResponse().getStatus()); } - else if (command instanceof UpdateCommand) + else if (command instanceof UpdateCommand || command instanceof CheckReferencesCommand cr && cr.isUpdate()) { audit.info("Update of {} for identity '{}' via bundle at index {} {}, status: {}", command.getResourceTypeName(), command.getIdentity().getName(), command.getIndex(), resultOutcome, @@ -103,7 +211,7 @@ else if (command instanceof ReadCommand) } } - protected void auditLogAbbort(Command command) + protected void auditLogAbort(Command command) { if (command instanceof DeleteCommand) { @@ -129,7 +237,7 @@ else if (command instanceof ReadCommand r) private boolean failed(BundleEntryComponent result) { - if (result != null && result.hasResponse() && result.getResponse().hasStatus()) + if (result.hasResponse() && result.getResponse().hasStatus()) { String status = result.getResponse().getStatus(); if (status.length() >= 3 && status.matches("[0-9]{3}.*")) @@ -140,24 +248,5 @@ private boolean failed(BundleEntryComponent result) return false; } - protected BundleEntryComponent toEntry(Exception exception) - { - var entry = new BundleEntryComponent(); - var response = entry.getResponse(); - - if (!(exception instanceof WebApplicationException w) - || !(w.getResponse().getEntity() instanceof OperationOutcome)) - { - exception = internalServerError(exception); - } - - Response httpResponse = ((WebApplicationException) exception).getResponse(); - response.setStatus( - httpResponse.getStatusInfo().getStatusCode() + " " + httpResponse.getStatusInfo().getReasonPhrase()); - response.setOutcome((OperationOutcome) httpResponse.getEntity()); - - return entry; - } - protected abstract Exception internalServerError(Exception exception); } diff --git a/dsf-fhir/dsf-fhir-server/src/main/java/dev/dsf/fhir/dao/command/BatchCommandList.java b/dsf-fhir/dsf-fhir-server/src/main/java/dev/dsf/fhir/dao/command/BatchCommandList.java index e462608f7..5385911ee 100755 --- a/dsf-fhir/dsf-fhir-server/src/main/java/dev/dsf/fhir/dao/command/BatchCommandList.java +++ b/dsf-fhir/dsf-fhir-server/src/main/java/dev/dsf/fhir/dao/command/BatchCommandList.java @@ -17,18 +17,16 @@ import java.sql.Connection; import java.sql.SQLException; -import java.util.Comparator; import java.util.HashMap; +import java.util.HashSet; import java.util.List; import java.util.Map; -import java.util.Map.Entry; -import java.util.Optional; -import java.util.function.Consumer; +import java.util.Set; +import java.util.function.Predicate; import javax.sql.DataSource; import org.hl7.fhir.r4.model.Bundle; -import org.hl7.fhir.r4.model.Bundle.BundleEntryComponent; import org.hl7.fhir.r4.model.Bundle.BundleType; import org.hl7.fhir.r4.model.IdType; import org.postgresql.util.PSQLException; @@ -56,7 +54,7 @@ public BatchCommandList(DataSource dataSource, DataSource permanentDeleteDataSou ExceptionHandler exceptionHandler, List commands, ValidationHelper validationHelper, SnapshotGenerator snapshotGenerator, EventHandler eventHandler, ResponseGenerator responseGenerator) { - super(dataSource, permanentDeleteDataSource, selectUpdateUser, exceptionHandler, commands); + super(dataSource, permanentDeleteDataSource, selectUpdateUser, exceptionHandler, commands, (_, _) -> 0); this.validationHelper = validationHelper; this.snapshotGenerator = snapshotGenerator; @@ -73,7 +71,6 @@ public Bundle execute() throws WebApplicationException boolean initialAutoCommit = connection.getAutoCommit(); int initialTransactionIsolationLevel = connection.getTransactionIsolation(); - Map caughtExceptions = new HashMap<>((int) (commands.size() / 0.75) + 1); Map idTranslationTable = new HashMap<>(); if (hasModifyingCommands) @@ -83,9 +80,24 @@ public Bundle execute() throws WebApplicationException connection.setTransactionIsolation(Connection.TRANSACTION_READ_COMMITTED); } - commands.forEach(preExecute(idTranslationTable, connection, caughtExceptions)); + Set indexesWithError = new HashSet<>(); - commands.forEach(execute(idTranslationTable, connection, caughtExceptions)); + commands.forEach(cR -> + { + preExecute(cR, connection, idTranslationTable); + + if (cR.hasException()) + indexesWithError.add(cR.getCommand().getIndex()); + }); + + commands.stream().filter(Predicate.not(CommandResult::hasException)).forEach(cR -> + { + if (!indexesWithError.contains(cR.getCommand().getIndex())) + execute(cR, connection, idTranslationTable); + + if (cR.hasException()) + indexesWithError.add(cR.getCommand().getIndex()); + }); if (hasModifyingCommands) { @@ -94,22 +106,28 @@ public Bundle execute() throws WebApplicationException connection.setTransactionIsolation(initialTransactionIsolationLevel); } - Map results = new HashMap<>((int) (commands.size() / 0.75) + 1); - - commands.forEach(postExecute(connection, caughtExceptions, results)); - caughtExceptions.forEach((k, v) -> results.put(k, toEntry(v))); - - results.entrySet().stream().sorted(Comparator.comparing(Entry::getKey)).forEach(e -> + commands.stream().filter(Predicate.not(CommandResult::hasException)).forEach(cR -> { - Command command = commands.get(e.getKey()); - BundleEntryComponent result = e.getValue(); - auditLogResult(command, result); + if (!indexesWithError.contains(cR.getCommand().getIndex())) + postExecute(cR, connection); + + if (cR.hasException()) + indexesWithError.add(cR.getCommand().getIndex()); }); Bundle result = new Bundle(); result.setType(BundleType.BATCHRESPONSE); - results.entrySet().stream().sorted(Comparator.comparing(Entry::getKey)).map(Entry::getValue) - .forEach(result::addEntry); + + commands.forEach(c -> + { + c.convertExceptionToResult(this::internalServerError); + + if (c.hasResult()) + { + auditLogResult(c); + result.addEntry(c.getResult()); + } + }); return result; } @@ -119,154 +137,121 @@ public Bundle execute() throws WebApplicationException } } - private Consumer preExecute(Map idTranslationTable, Connection connection, - Map caughtExceptions) + private void preExecute(CommandResult commandResult, Connection connection, Map idTranslationTable) { - return command -> + if (commandResult.hasException()) + throw new IllegalStateException("Command has exception"); + + Command command = commandResult.getCommand(); + + try { - try - { - if (!caughtExceptions.containsKey(command.getIndex())) - { - logger.debug("Running pre-execute of command {} for entry at index {}", - command.getClass().getName(), command.getIndex()); - command.preExecute(idTranslationTable, connection, validationHelper, snapshotGenerator); - } - else - { - logger.info("Skipping pre-execute of command {} for entry at index {}, caught exception {}", - command.getClass().getName(), command.getIndex(), - caughtExceptions.get(command.getIndex()).getClass().getName() + ": " - + caughtExceptions.get(command.getIndex()).getMessage()); - } - } - catch (Exception e) - { - logger.debug("Error while running pre-execute of command {} for entry at index {}", - command.getClass().getName(), command.getIndex(), e); - logger.warn("Error while running pre-execute of command {} for entry at index {}: {} - {}", - command.getClass().getName(), command.getIndex(), e.getClass().getName(), e.getMessage()); + logger.debug("Running pre-execute of {} for entry at index {}", command.getClass().getSimpleName(), + command.getIndex()); + command.preExecute(idTranslationTable, connection, validationHelper, snapshotGenerator); + } + catch (Exception e) + { + logger.debug("Error while running pre-execute of {} for entry at index {}", + command.getClass().getSimpleName(), command.getIndex(), e); + logger.warn("Error while running pre-execute of {} for entry at index {}: {} - {}", + command.getClass().getSimpleName(), command.getIndex(), e.getClass().getName(), e.getMessage()); - caughtExceptions.put(command.getIndex(), e); - } - }; + commandResult.setException(e); + } } - private Consumer execute(Map idTranslationTable, Connection connection, - Map caughtExceptions) + private void execute(CommandResult commandResult, Connection connection, Map idTranslationTable) { - return command -> + if (commandResult.hasException()) + throw new IllegalStateException("Command has exception"); + + Command command = commandResult.getCommand(); + + LargeObjectManager largeObjectManager = command instanceof ModifyingCommand m + ? m.createLargeObjectManager(connection) + : LargeObjectManager.NO_OP; + + try + { + logger.debug("Running execute of {} for entry at index {}", command.getClass().getSimpleName(), + command.getIndex()); + command.execute(idTranslationTable, largeObjectManager, connection, validationHelper); + + if (!connection.getAutoCommit()) + connection.commit(); + } + catch (Exception e) { - LargeObjectManager largeObjectManager = command instanceof ModifyingCommand m - ? m.createLargeObjectManager(connection) - : LargeObjectManager.NO_OP; + logger.debug("Error while executing {}, rolling back transaction for entry at index {}", + command.getClass().getSimpleName(), command.getIndex(), e); + logger.warn("Error while executing {}, rolling back transaction for entry at index {}: {} - {}", + command.getClass().getSimpleName(), command.getIndex(), e.getClass().getName(), e.getMessage()); + + if (e instanceof PSQLException s && PSQLState.UNIQUE_VIOLATION.getState().equals(s.getSQLState())) + commandResult.setException(new WebApplicationException(responseGenerator.duplicateResourceExists())); + else + commandResult.setException(e); try { - if (!caughtExceptions.containsKey(command.getIndex())) - { - logger.debug("Running execute of command {} for entry at index {}", command.getClass().getName(), - command.getIndex()); - command.execute(idTranslationTable, largeObjectManager, connection, validationHelper); - } - else - { - logger.info("Skipping execute of command {} for entry at index {}, caught exception {}", - command.getClass().getName(), command.getIndex(), - caughtExceptions.get(command.getIndex()).getClass().getName() + ": " - + caughtExceptions.get(command.getIndex()).getMessage()); - } - if (!connection.getAutoCommit()) - connection.commit(); + connection.rollback(); } - catch (Exception exception) + catch (SQLException rollbackException) { - logger.debug("Error while executing command {}, rolling back transaction for entry at index {}", - command.getClass().getName(), command.getIndex(), exception); - logger.warn("Error while executing command {}, rolling back transaction for entry at index {}: {} - {}", - command.getClass().getName(), command.getIndex(), exception.getClass().getName(), - exception.getMessage()); - - if (exception instanceof PSQLException s - && PSQLState.UNIQUE_VIOLATION.getState().equals(s.getSQLState())) - caughtExceptions.put(command.getIndex(), - new WebApplicationException(responseGenerator.duplicateResourceExists())); - else - caughtExceptions.put(command.getIndex(), exception); - - try - { - if (!connection.getAutoCommit()) - connection.rollback(); - } - catch (SQLException rollbackException) - { - logger.debug( - "Error while executing command {}, error while rolling back transaction for entry at index {}", - command.getClass().getName(), command.getIndex(), rollbackException); - logger.warn( - "Error while executing command {}, error while rolling back transaction for entry at index {}: {} - {}", - command.getClass().getName(), command.getIndex(), rollbackException.getClass().getName(), - rollbackException.getMessage()); - - caughtExceptions.put(command.getIndex(), rollbackException); - } + logger.debug("Error while executing {}, error while rolling back transaction for entry at index {}", + command.getClass().getSimpleName(), command.getIndex(), rollbackException); + logger.warn( + "Error while executing {}, error while rolling back transaction for entry at index {}: {} - {}", + command.getClass().getSimpleName(), command.getIndex(), rollbackException.getClass().getName(), + rollbackException.getMessage()); + + commandResult.addSuppressedException(rollbackException); + } - try - { - if (!connection.getAutoCommit()) - largeObjectManager.rollback(); - } - catch (SQLException rollbackException) - { - logger.debug( - "Error while executing command {}, error while rolling back DB large object for entry at index {}", - command.getClass().getName(), command.getIndex(), rollbackException); - logger.warn( - "Error while executing command {}, error while rolling back DB large object for entry at index {}: {} - {}", - command.getClass().getName(), command.getIndex(), rollbackException.getClass().getName(), - rollbackException.getMessage()); - - caughtExceptions.put(command.getIndex(), rollbackException); - } + try + { + if (!connection.getAutoCommit()) + largeObjectManager.rollback(); } - }; + catch (SQLException rollbackException) + { + logger.debug("Error while executing {}, error while rolling back DB large object for entry at index {}", + command.getClass().getSimpleName(), command.getIndex(), rollbackException); + logger.warn( + "Error while executing {}, error while rolling back DB large object for entry at index {}: {} - {}", + command.getClass().getSimpleName(), command.getIndex(), rollbackException.getClass().getName(), + rollbackException.getMessage()); + + commandResult.addSuppressedException(rollbackException); + } + } } - private Consumer postExecute(Connection connection, Map caughtExceptions, - Map results) + private void postExecute(CommandResult commandResult, Connection connection) { - return command -> + if (commandResult.hasException()) + throw new IllegalStateException("Command has exception"); + + Command command = commandResult.getCommand(); + + try { - try - { - if (!caughtExceptions.containsKey(command.getIndex())) - { - logger.debug("Running post-execute of command {} for entry at index {}", - command.getClass().getName(), command.getIndex()); + logger.debug("Running post-execute of {} for entry at index {}", command.getClass().getSimpleName(), + command.getIndex()); - Optional optResult = command.postExecute(connection, eventHandler); - optResult.ifPresent(result -> results.put(command.getIndex(), result)); - } - else - { - logger.info("Skipping post-execute of command {} for entry at index {}, caught exception {}", - command.getClass().getName(), command.getIndex(), - caughtExceptions.get(command.getIndex()).getClass().getName() + ": " - + caughtExceptions.get(command.getIndex()).getMessage()); - } - } - catch (Exception e) - { - logger.debug("Error while running post-execute of command {} for entry at index {}", - command.getClass().getName(), command.getIndex(), e); - logger.warn("Error while running post-execute of command {} for entry at index {}: {} - {}", - command.getClass().getName(), command.getIndex(), e.getClass().getName(), e.getMessage()); + command.postExecute(connection, eventHandler).ifPresent(commandResult::setResult); + } + catch (Exception e) + { + logger.debug("Error while running post-execute of {} for entry at index {}", + command.getClass().getSimpleName(), command.getIndex(), e); + logger.warn("Error while running post-execute of {} for entry at index {}: {} - {}", + command.getClass().getSimpleName(), command.getIndex(), e.getClass().getName(), e.getMessage()); - caughtExceptions.put(command.getIndex(), e); - } - }; + commandResult.setException(e); + } } @Override diff --git a/dsf-fhir/dsf-fhir-server/src/main/java/dev/dsf/fhir/dao/command/CheckReferencesCommand.java b/dsf-fhir/dsf-fhir-server/src/main/java/dev/dsf/fhir/dao/command/CheckReferencesCommand.java index 8969da21f..82e6b9260 100644 --- a/dsf-fhir/dsf-fhir-server/src/main/java/dev/dsf/fhir/dao/command/CheckReferencesCommand.java +++ b/dsf-fhir/dsf-fhir-server/src/main/java/dev/dsf/fhir/dao/command/CheckReferencesCommand.java @@ -66,7 +66,7 @@ public void execute(Map idTranslationTable, LargeObjectManager l private boolean checkReferenceAfterUpdate(ResourceReference ref) { - if (HTTPVerb.PUT.equals(verb)) + if (isUpdate()) { // version -1 as we are checking against the pre-update resource return validationRules.checkReferenceAfterUpdate(resource, ref, v -> v - 1); @@ -75,9 +75,13 @@ private boolean checkReferenceAfterUpdate(ResourceReference ref) return true; } - @Override - public String getResourceTypeName() + public boolean isCreate() + { + return HTTPVerb.POST.equals(verb); + } + + public boolean isUpdate() { - throw new UnsupportedOperationException(); + return HTTPVerb.PUT.equals(verb); } } diff --git a/dsf-fhir/dsf-fhir-server/src/main/java/dev/dsf/fhir/dao/command/TransactionCommandList.java b/dsf-fhir/dsf-fhir-server/src/main/java/dev/dsf/fhir/dao/command/TransactionCommandList.java index fd520f394..9745633a6 100755 --- a/dsf-fhir/dsf-fhir-server/src/main/java/dev/dsf/fhir/dao/command/TransactionCommandList.java +++ b/dsf-fhir/dsf-fhir-server/src/main/java/dev/dsf/fhir/dao/command/TransactionCommandList.java @@ -17,19 +17,15 @@ import java.sql.Connection; import java.sql.SQLException; -import java.util.Collections; import java.util.Comparator; import java.util.HashMap; import java.util.List; import java.util.Map; -import java.util.Map.Entry; -import java.util.Optional; import java.util.function.Function; import javax.sql.DataSource; import org.hl7.fhir.r4.model.Bundle; -import org.hl7.fhir.r4.model.Bundle.BundleEntryComponent; import org.hl7.fhir.r4.model.Bundle.BundleType; import org.hl7.fhir.r4.model.IdType; import org.postgresql.util.PSQLException; @@ -39,6 +35,7 @@ import dev.dsf.fhir.dao.jdbc.LargeObjectManager; import dev.dsf.fhir.dao.jdbc.LargeObjectManagerJdbc; +import dev.dsf.fhir.event.EventHandler; import dev.dsf.fhir.help.ExceptionHandler; import dev.dsf.fhir.help.ResponseGenerator; import dev.dsf.fhir.validation.SnapshotGenerator; @@ -57,19 +54,16 @@ public TransactionCommandList(DataSource dataSource, DataSource permanentDeleteD ExceptionHandler exceptionHandler, List commands, Function transactionResourceFactory, ResponseGenerator responseGenerator) { - super(dataSource, permanentDeleteDataSource, dbUsersGroup, exceptionHandler, commands); + super(dataSource, permanentDeleteDataSource, dbUsersGroup, exceptionHandler, commands, + Comparator.comparing(Command::getTransactionPriority).thenComparing(Command::getIndex)); this.transactionResourceFactory = transactionResourceFactory; this.responseGenerator = responseGenerator; - - Collections.sort(this.commands, - Comparator.comparing(Command::getTransactionPriority).thenComparing(Command::getIndex)); } @Override public Bundle execute() throws WebApplicationException { - Map results = new HashMap<>((int) (commands.size() / 0.75) + 1); try { TransactionEventHandler transactionEventHandler; @@ -92,120 +86,12 @@ public Bundle execute() throws WebApplicationException : LargeObjectManager.NO_OP; Map idTranslationTable = new HashMap<>(); - for (Command c : commands) - { - try - { - logger.debug("Running pre-execute of command {} for entry at index {}", c.getClass().getName(), - c.getIndex()); - c.preExecute(idTranslationTable, connection, validationHelper, snapshotGenerator); - } - catch (Exception e) - { - logger.debug( - "Error while running pre-execute of command {} for entry at index {}, aborting transaction", - c.getClass().getSimpleName(), c.getIndex(), e); - logger.warn( - "Error while running pre-execute of command {} for entry at index {}, aborting transaction: {} - {}", - c.getClass().getSimpleName(), c.getIndex(), e.getClass().getName(), e.getMessage()); - - try - { - commands.stream().limit(c.getIndex()).forEach(this::auditLogAbbort); - auditLogResult(c, toEntry(e)); - } - catch (Exception e1) - { - logger.debug("Error while writing to audit log", e1); - logger.warn("Error while writing to audit log: {} - {}", e1.getClass().getName(), - e1.getMessage()); - } - - throw e; - } - } - - for (Command c : commands) - { - try - { - logger.debug("Running execute of command {} for entry at index {}", c.getClass().getName(), - c.getIndex()); - c.execute(idTranslationTable, largeObjectManager, connection, validationHelper); - } - catch (Exception e) - { - logger.debug("Error while executing command {} for entry at index {}, rolling back transaction", - c.getClass().getSimpleName(), c.getIndex(), e); - logger.warn( - "Error while executing command {} for entry at index {}, rolling back transaction: {} - {}", - c.getClass().getSimpleName(), c.getIndex(), e.getClass().getName(), e.getMessage()); - - if (hasModifyingCommands) - { - logger.debug("Rolling back DB transaction"); - tryRollback(connection, largeObjectManager, e); - } - - if (e instanceof PSQLException s - && PSQLState.UNIQUE_VIOLATION.getState().equals(s.getSQLState())) - e = new WebApplicationException(e, responseGenerator.duplicateResourceExists()); - - try - { - commands.stream().limit(c.getIndex()).forEach(this::auditLogAbbort); - auditLogResult(c, toEntry(e)); - } - catch (Exception e1) - { - logger.debug("Error while writing to audit log", e1); - logger.warn("Error while writing to audit log: {} - {}", e1.getClass().getName(), - e1.getMessage()); - } - - throw e; - } - } - for (Command c : commands) - { - try - { - logger.debug("Running post-execute of command {} for entry at index {}", c.getClass().getName(), - c.getIndex()); - Optional optResult = c.postExecute(connection, transactionEventHandler); - optResult.ifPresent(result -> results.putIfAbsent(c.getIndex(), result)); - } - catch (Exception e) - { - logger.debug( - "Error while running post-execute of command {} for entry at index {}, rolling back transaction", - c.getClass().getSimpleName(), c.getIndex(), e); - logger.warn( - "Error while running post-execute of command {} for entry at index {}, rolling back transaction: {} - {}", - c.getClass().getSimpleName(), c.getIndex(), e.getClass().getName(), e.getMessage()); - - if (hasModifyingCommands) - { - logger.debug("Rolling back DB transaction"); - tryRollback(connection, largeObjectManager, e); - } + preExecute(connection, validationHelper, snapshotGenerator, idTranslationTable); - try - { - commands.stream().limit(c.getIndex()).forEach(this::auditLogAbbort); - auditLogResult(c, toEntry(e)); - } - catch (Exception e1) - { - logger.debug("Error while writing to audit log", e1); - logger.warn("Error while writing to audit log: {} - {}", e1.getClass().getName(), - e1.getMessage()); - } + execute(connection, validationHelper, largeObjectManager, idTranslationTable); - throw e; - } - } + postExecute(connection, largeObjectManager, transactionEventHandler); if (hasModifyingCommands) { @@ -241,25 +127,27 @@ public Bundle execute() throws WebApplicationException logger.warn("Error while handling events: {} - {}", e.getClass().getName(), e.getMessage()); } - try - { - results.entrySet().stream().sorted(Comparator.comparing(Entry::getKey)).forEach(e -> - { - Command command = commands.get(e.getKey()); - BundleEntryComponent result = e.getValue(); - auditLogResult(command, result); - }); - } - catch (Exception e) - { - logger.debug("Error while writing to audit log", e); - logger.warn("Error while writing to audit log: {} - {}", e.getClass().getName(), e.getMessage()); - } - Bundle result = new Bundle(); result.setType(BundleType.TRANSACTIONRESPONSE); - results.entrySet().stream().sorted(Comparator.comparing(Entry::getKey)).map(Entry::getValue) - .forEach(result::addEntry); + + commands.stream().filter(CommandResult::hasResult) + .sorted(Comparator.comparing(cR -> cR.getCommand().getIndex())).forEach(cR -> + { + cR.convertExceptionToResult(this::internalServerError); + + try + { + auditLogResult(cR); + } + catch (Exception e) + { + logger.debug("Error while writing to audit log", e); + logger.warn("Error while writing to audit log: {} - {}", e.getClass().getName(), + e.getMessage()); + } + + result.addEntry(cR.getResult()); + }); return result; } @@ -277,6 +165,143 @@ public Bundle execute() throws WebApplicationException } } + private void preExecute(Connection connection, ValidationHelper validationHelper, + SnapshotGenerator snapshotGenerator, Map idTranslationTable) throws Exception + { + for (int i = 0; i < commands.size(); i++) + { + CommandResult cR = commands.get(i); + Command c = cR.getCommand(); + + try + { + logger.debug("Running pre-execute of {} for entry at index {}", c.getClass().getSimpleName(), + c.getIndex()); + c.preExecute(idTranslationTable, connection, validationHelper, snapshotGenerator); + } + catch (Exception e) + { + logger.debug("Error while running pre-execute of {} for entry at index {}, aborting transaction", + c.getClass().getSimpleName(), c.getIndex(), e); + logger.warn( + "Error while running pre-execute of {} for entry at index {}, aborting transaction: {} - {}", + c.getClass().getSimpleName(), c.getIndex(), e.getClass().getName(), e.getMessage()); + + cR.setException(e); + cR.convertExceptionToResult(this::internalServerError); + + try + { + commands.stream().map(CommandResult::getCommand).sorted(Comparator.comparing(Command::getIndex)) + .filter(r -> r.getIndex() != c.getIndex()).forEach(this::auditLogAbort); + auditLogResult(cR); + } + catch (Exception e1) + { + logger.debug("Error while writing to audit log", e1); + logger.warn("Error while writing to audit log: {} - {}", e1.getClass().getName(), e1.getMessage()); + } + + throw e; + } + } + } + + private void execute(Connection connection, ValidationHelper validationHelper, + LargeObjectManager largeObjectManager, Map idTranslationTable) throws Exception + { + for (int i = 0; i < commands.size(); i++) + { + CommandResult cR = commands.get(i); + Command c = cR.getCommand(); + + try + { + logger.debug("Running execute of {} for entry at index {}", c.getClass().getSimpleName(), c.getIndex()); + c.execute(idTranslationTable, largeObjectManager, connection, validationHelper); + } + catch (Exception e) + { + logger.debug("Error while executing {} for entry at index {}, rolling back transaction", + c.getClass().getSimpleName(), c.getIndex(), e); + logger.warn("Error while executing {} for entry at index {}, rolling back transaction: {} - {}", + c.getClass().getSimpleName(), c.getIndex(), e.getClass().getName(), e.getMessage()); + + if (hasModifyingCommands) + { + logger.debug("Rolling back DB transaction"); + tryRollback(connection, largeObjectManager, e); + } + + if (e instanceof PSQLException s && PSQLState.UNIQUE_VIOLATION.getState().equals(s.getSQLState())) + e = new WebApplicationException(responseGenerator.duplicateResourceExists()); + + cR.setException(e); + cR.convertExceptionToResult(this::internalServerError); + + try + { + commands.stream().map(CommandResult::getCommand).sorted(Comparator.comparing(Command::getIndex)) + .filter(r -> r.getIndex() != c.getIndex()).forEach(this::auditLogAbort); + auditLogResult(cR); + } + catch (Exception e1) + { + logger.debug("Error while writing to audit log", e1); + logger.warn("Error while writing to audit log: {} - {}", e1.getClass().getName(), e1.getMessage()); + } + + throw e; + } + } + } + + private void postExecute(Connection connection, LargeObjectManager largeObjectManager, EventHandler eventHandler) + throws Exception + { + for (int i = 0; i < commands.size(); i++) + { + CommandResult cR = commands.get(i); + Command c = cR.getCommand(); + + try + { + logger.debug("Running post-execute of {} for entry at index {}", c.getClass().getSimpleName(), + c.getIndex()); + + c.postExecute(connection, eventHandler).ifPresent(cR::setResult); + } + catch (Exception e) + { + logger.debug("Error while running post-execute of {} for entry at index {}, rolling back transaction", + c.getClass().getSimpleName(), c.getIndex(), e); + logger.warn( + "Error while running post-execute of {} for entry at index {}, rolling back transaction: {} - {}", + c.getClass().getSimpleName(), c.getIndex(), e.getClass().getName(), e.getMessage()); + + if (hasModifyingCommands) + { + logger.debug("Rolling back DB transaction"); + tryRollback(connection, largeObjectManager, e); + } + + try + { + commands.stream().map(CommandResult::getCommand).sorted(Comparator.comparing(Command::getIndex)) + .filter(r -> r.getIndex() != c.getIndex()).forEach(this::auditLogAbort); + auditLogResult(cR); + } + catch (Exception e1) + { + logger.debug("Error while writing to audit log", e1); + logger.warn("Error while writing to audit log: {} - {}", e1.getClass().getName(), e1.getMessage()); + } + + throw e; + } + } + } + private void tryRollback(Connection connection, LargeObjectManager largeObjectManager, Exception e) { try From cd430aedd239b1201952ab2d814774259aa459cf Mon Sep 17 00:00:00 2001 From: Hauke Hund Date: Wed, 7 Oct 2026 11:52:02 +0200 Subject: [PATCH 04/13] disabled not needed and deprecated features --- dsf-docker-dev-setup-3dic-ttp/docker-compose.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/dsf-docker-dev-setup-3dic-ttp/docker-compose.yml b/dsf-docker-dev-setup-3dic-ttp/docker-compose.yml index 6f53d56dd..6f52011f0 100644 --- a/dsf-docker-dev-setup-3dic-ttp/docker-compose.yml +++ b/dsf-docker-dev-setup-3dic-ttp/docker-compose.yml @@ -125,6 +125,7 @@ services: --https-certificate-key-file=/run/secrets/keycloak.key.plain --spi-truststore-file-file=/run/secrets/keycloak_trust_store.jks --spi-truststore-file-password=password + --features-disabled=twitter-broker,identity-brokering-api forward-proxy: build: ./forward-proxy From 89f27fe7273100454efc2fc6c35017858684b0c9 Mon Sep 17 00:00:00 2001 From: Hauke Hund Date: Wed, 7 Oct 2026 13:17:29 +0200 Subject: [PATCH 05/13] refactored code: removed CheckReferencesCommand, added new exec phase Reference check code from CheckReferencesCommand moved into new checkReferences execution phase of all commands. The new phase is executed as part of execute and before DB commits for batch Bundles, but after all executes and before the single DB commit for transaction Bundles. --- .../fhir/dao/command/AbstractCommandList.java | 4 +- .../command/AbstractCommandWithResource.java | 5 +- .../fhir/dao/command/BatchCommandList.java | 4 +- .../dao/command/CheckReferencesCommand.java | 87 ------------------- .../dev/dsf/fhir/dao/command/Command.java | 6 ++ .../fhir/dao/command/CommandFactoryImpl.java | 60 ++++--------- .../dsf/fhir/dao/command/CreateCommand.java | 14 ++- .../CreateStructureDefinitionCommand.java | 7 +- .../dao/command/TransactionCommandList.java | 55 +++++++++++- .../dsf/fhir/dao/command/UpdateCommand.java | 15 +++- .../UpdateStructureDefinitionCommand.java | 7 +- 11 files changed, 117 insertions(+), 147 deletions(-) delete mode 100644 dsf-fhir/dsf-fhir-server/src/main/java/dev/dsf/fhir/dao/command/CheckReferencesCommand.java diff --git a/dsf-fhir/dsf-fhir-server/src/main/java/dev/dsf/fhir/dao/command/AbstractCommandList.java b/dsf-fhir/dsf-fhir-server/src/main/java/dev/dsf/fhir/dao/command/AbstractCommandList.java index 4ebd1d9d8..3bb4ea121 100644 --- a/dsf-fhir/dsf-fhir-server/src/main/java/dev/dsf/fhir/dao/command/AbstractCommandList.java +++ b/dsf-fhir/dsf-fhir-server/src/main/java/dev/dsf/fhir/dao/command/AbstractCommandList.java @@ -190,13 +190,13 @@ protected void auditLogResult(Command command, BundleEntryComponent result) command.getResourceTypeName(), command.getIdentity().getName(), command.getIndex(), resultOutcome, result.getResponse().getStatus()); } - else if (command instanceof CreateCommand || command instanceof CheckReferencesCommand cr && cr.isCreate()) + else if (command instanceof CreateCommand) { audit.info("Create of {} for identity '{}' via bundle at index {} {}, status: {}", command.getResourceTypeName(), command.getIdentity().getName(), command.getIndex(), resultOutcome, result.getResponse().getStatus()); } - else if (command instanceof UpdateCommand || command instanceof CheckReferencesCommand cr && cr.isUpdate()) + else if (command instanceof UpdateCommand) { audit.info("Update of {} for identity '{}' via bundle at index {} {}, status: {}", command.getResourceTypeName(), command.getIdentity().getName(), command.getIndex(), resultOutcome, diff --git a/dsf-fhir/dsf-fhir-server/src/main/java/dev/dsf/fhir/dao/command/AbstractCommandWithResource.java b/dsf-fhir/dsf-fhir-server/src/main/java/dev/dsf/fhir/dao/command/AbstractCommandWithResource.java index def49b60f..ff80a1aa8 100755 --- a/dsf-fhir/dsf-fhir-server/src/main/java/dev/dsf/fhir/dao/command/AbstractCommandWithResource.java +++ b/dsf-fhir/dsf-fhir-server/src/main/java/dev/dsf/fhir/dao/command/AbstractCommandWithResource.java @@ -27,6 +27,7 @@ import dev.dsf.fhir.prefer.PreferReturnType; import dev.dsf.fhir.service.ReferenceExtractor; import dev.dsf.fhir.service.ReferenceResolver; +import dev.dsf.fhir.validation.ValidationRules; public abstract class AbstractCommandWithResource> extends AbstractCommand implements Command @@ -35,12 +36,13 @@ public abstract class AbstractCommandWithResource referencesHelper; public AbstractCommandWithResource(int transactionPriority, int index, Identity identity, PreferReturnType returnType, Bundle bundle, BundleEntryComponent entry, String serverBase, AuthorizationHelper authorizationHelper, R resource, D dao, ExceptionHandler exceptionHandler, - ParameterConverter parameterConverter, ResponseGenerator responseGenerator, + ParameterConverter parameterConverter, ValidationRules validationRules, ResponseGenerator responseGenerator, ReferenceExtractor referenceExtractor, ReferenceResolver referenceResolver) { super(transactionPriority, index, identity, returnType, bundle, entry, serverBase, authorizationHelper); @@ -49,6 +51,7 @@ public AbstractCommandWithResource(int transactionPriority, int index, Identity this.dao = dao; this.exceptionHandler = exceptionHandler; this.parameterConverter = parameterConverter; + this.validationRules = validationRules; referencesHelper = new ReferencesHelperImpl<>(index, resource, serverBase, referenceExtractor, referenceResolver, responseGenerator); diff --git a/dsf-fhir/dsf-fhir-server/src/main/java/dev/dsf/fhir/dao/command/BatchCommandList.java b/dsf-fhir/dsf-fhir-server/src/main/java/dev/dsf/fhir/dao/command/BatchCommandList.java index 5385911ee..75937d0a5 100755 --- a/dsf-fhir/dsf-fhir-server/src/main/java/dev/dsf/fhir/dao/command/BatchCommandList.java +++ b/dsf-fhir/dsf-fhir-server/src/main/java/dev/dsf/fhir/dao/command/BatchCommandList.java @@ -174,10 +174,12 @@ private void execute(CommandResult commandResult, Connection connection, Map> - extends AbstractCommandWithResource implements Command -{ - private final HTTPVerb verb; - private final ValidationRules validationRules; - - public CheckReferencesCommand(int index, Identity identity, PreferReturnType returnType, Bundle bundle, - BundleEntryComponent entry, String serverBase, AuthorizationHelper authorizationHelper, R resource, - HTTPVerb verb, D dao, ExceptionHandler exceptionHandler, ParameterConverter parameterConverter, - ResponseGenerator responseGenerator, ReferenceExtractor referenceExtractor, - ReferenceResolver referenceResolver, ValidationRules validationRules) - { - super(4, index, identity, returnType, bundle, entry, serverBase, authorizationHelper, resource, dao, - exceptionHandler, parameterConverter, responseGenerator, referenceExtractor, referenceResolver); - - this.verb = verb; - this.validationRules = validationRules; - } - - @Override - public void execute(Map idTranslationTable, LargeObjectManager largeObjectManager, - Connection connection, ValidationHelper validationHelper) throws SQLException, WebApplicationException - { - referencesHelper.checkReferences(idTranslationTable, connection, this::checkReferenceAfterUpdate); - } - - private boolean checkReferenceAfterUpdate(ResourceReference ref) - { - if (isUpdate()) - { - // version -1 as we are checking against the pre-update resource - return validationRules.checkReferenceAfterUpdate(resource, ref, v -> v - 1); - } - - return true; - } - - public boolean isCreate() - { - return HTTPVerb.POST.equals(verb); - } - - public boolean isUpdate() - { - return HTTPVerb.PUT.equals(verb); - } -} diff --git a/dsf-fhir/dsf-fhir-server/src/main/java/dev/dsf/fhir/dao/command/Command.java b/dsf-fhir/dsf-fhir-server/src/main/java/dev/dsf/fhir/dao/command/Command.java index 56f5a3c83..70a397f09 100755 --- a/dsf-fhir/dsf-fhir-server/src/main/java/dev/dsf/fhir/dao/command/Command.java +++ b/dsf-fhir/dsf-fhir-server/src/main/java/dev/dsf/fhir/dao/command/Command.java @@ -45,6 +45,12 @@ default void preExecute(Map idTranslationTable, Connection conne void execute(Map idTranslationTable, LargeObjectManager largeObjectManager, Connection connection, ValidationHelper validationHelper) throws SQLException, WebApplicationException; + default void checkReferences(Map idTranslationTable, Connection connection) + throws SQLException, WebApplicationException + { + + }; + default Optional postExecute(Connection connection, EventHandler eventHandler) { return Optional.empty(); diff --git a/dsf-fhir/dsf-fhir-server/src/main/java/dev/dsf/fhir/dao/command/CommandFactoryImpl.java b/dsf-fhir/dsf-fhir-server/src/main/java/dev/dsf/fhir/dao/command/CommandFactoryImpl.java index 4dbbc48e3..9a1141f82 100755 --- a/dsf-fhir/dsf-fhir-server/src/main/java/dev/dsf/fhir/dao/command/CommandFactoryImpl.java +++ b/dsf-fhir/dsf-fhir-server/src/main/java/dev/dsf/fhir/dao/command/CommandFactoryImpl.java @@ -22,13 +22,11 @@ import java.util.function.Function; import java.util.stream.Collectors; import java.util.stream.IntStream; -import java.util.stream.Stream; import javax.sql.DataSource; import org.hl7.fhir.r4.model.Bundle; import org.hl7.fhir.r4.model.Bundle.BundleEntryComponent; -import org.hl7.fhir.r4.model.Bundle.HTTPVerb; import org.hl7.fhir.r4.model.Resource; import org.hl7.fhir.r4.model.StructureDefinition; import org.slf4j.Logger; @@ -165,13 +163,13 @@ private Command post(int index, Identity identity, PreferRe if (resource instanceof StructureDefinition s) return new CreateStructureDefinitionCommand(index, identity, returnType, bundle, entry, serverBase, authorizationHelper, s, (StructureDefinitionDao) dao.get(), exceptionHandler, - parameterConverter, responseGenerator, referenceExtractor, referenceResolver, referenceCleaner, - eventGenerator, defaultProfileProvider, enableValidation, + parameterConverter, validationRules, responseGenerator, referenceExtractor, referenceResolver, + referenceCleaner, eventGenerator, defaultProfileProvider, enableValidation, daoProvider.getStructureDefinitionSnapshotDao()); else return dao .map(d -> new CreateCommand<>(index, identity, returnType, bundle, entry, serverBase, - authorizationHelper, resource, d, exceptionHandler, parameterConverter, + authorizationHelper, resource, d, exceptionHandler, parameterConverter, validationRules, responseGenerator, referenceExtractor, referenceResolver, referenceCleaner, eventGenerator, defaultProfileProvider, enableValidation)) .orElseThrow(() -> new IllegalStateException( @@ -196,13 +194,13 @@ private Command put(int index, Identity identity, PreferRet if (resource instanceof StructureDefinition s) return new UpdateStructureDefinitionCommand(index, identity, returnType, bundle, entry, serverBase, authorizationHelper, s, (StructureDefinitionDao) dao.get(), exceptionHandler, - parameterConverter, responseGenerator, referenceExtractor, referenceResolver, referenceCleaner, - eventGenerator, defaultProfileProvider, enableValidation, + parameterConverter, validationRules, responseGenerator, referenceExtractor, referenceResolver, + referenceCleaner, eventGenerator, defaultProfileProvider, enableValidation, daoProvider.getStructureDefinitionSnapshotDao()); else return dao .map(d -> new UpdateCommand<>(index, identity, returnType, bundle, entry, serverBase, - authorizationHelper, resource, d, exceptionHandler, parameterConverter, + authorizationHelper, resource, d, exceptionHandler, parameterConverter, validationRules, responseGenerator, referenceExtractor, referenceResolver, referenceCleaner, eventGenerator, defaultProfileProvider, enableValidation)) .orElseThrow(() -> new IllegalStateException( @@ -249,13 +247,13 @@ public CommandList createCommands(Bundle bundle, Identity identity, PreferReturn List commands = IntStream.range(0, bundle.getEntry().size()) .mapToObj(index -> createCommand(index, identity, returnType, handlingType, bundle, bundle.getEntry().get(index), enableValidation)) - .flatMap(Function.identity()).collect(Collectors.toList()); + .collect(Collectors.toList()); return switch (bundle.getType()) { - case BATCH -> - new BatchCommandList(dataSource, permanentDeleteDataSource, dbUsersGroup, exceptionHandler, - commands, validationHelper, snapshotGenerator, eventHandler, responseGenerator); + case BATCH -> new BatchCommandList(dataSource, permanentDeleteDataSource, dbUsersGroup, + exceptionHandler, commands, validationHelper, snapshotGenerator, eventHandler, + responseGenerator); case TRANSACTION -> new TransactionCommandList(dataSource, permanentDeleteDataSource, dbUsersGroup, exceptionHandler, commands, transactionResourcesFactory, responseGenerator); @@ -267,7 +265,7 @@ public CommandList createCommands(Bundle bundle, Identity identity, PreferReturn throw new BadBundleException("Missing bundle type"); } - protected Stream createCommand(int index, Identity identity, PreferReturnType returnType, + protected Command createCommand(int index, Identity identity, PreferReturnType returnType, PreferHandlingType handlingType, Bundle bundle, BundleEntryComponent entry, boolean enableValidation) { if (entry.hasRequest() && entry.getRequest().hasMethod()) @@ -276,9 +274,9 @@ protected Stream createCommand(int index, Identity identity, PreferRetu { return switch (entry.getRequest().getMethod()) { - case GET -> Stream.of(get(index, identity, returnType, bundle, entry, handlingType)); - case HEAD -> Stream.of(head(index, identity, returnType, bundle, entry, handlingType)); - case DELETE -> Stream.of(delete(index, identity, returnType, bundle, entry)); + case GET -> get(index, identity, returnType, bundle, entry, handlingType); + case HEAD -> head(index, identity, returnType, bundle, entry, handlingType); + case DELETE -> delete(index, identity, returnType, bundle, entry); default -> throw new BadBundleException("Request method " + entry.getRequest().getMethod() + " at index " + index + " not supported without resource"); @@ -288,13 +286,9 @@ protected Stream createCommand(int index, Identity identity, PreferRetu { return switch (entry.getRequest().getMethod()) { - case POST -> resolveReferences( - post(index, identity, returnType, bundle, entry, entry.getResource(), enableValidation), - index, identity, returnType, bundle, entry, entry.getResource(), HTTPVerb.POST); - - case PUT -> resolveReferences( - put(index, identity, returnType, bundle, entry, entry.getResource(), enableValidation), - index, identity, returnType, bundle, entry, entry.getResource(), HTTPVerb.PUT); + case POST -> post(index, identity, returnType, bundle, entry, entry.getResource(), + enableValidation); + case PUT -> put(index, identity, returnType, bundle, entry, entry.getResource(), enableValidation); default -> throw new BadBundleException("Request method " + entry.getRequest().getMethod() + " at index " + index + " not supported with resource"); @@ -304,24 +298,4 @@ protected Stream createCommand(int index, Identity identity, PreferRetu else throw new BadBundleException("BundleEntry at index " + index + " has no request or request has no method"); } - - private Stream resolveReferences(Command cmd, int index, Identity identity, - PreferReturnType returnType, Bundle bundle, BundleEntryComponent entry, R resource, HTTPVerb verb) - { - @SuppressWarnings("unchecked") - Optional> dao = (Optional>) daoProvider - .getDao(resource.getClass()); - - if (referenceExtractor.getReferences(resource).anyMatch(_ -> true)) // at least one entry - { - return dao.map(d -> Stream.of(cmd, - new CheckReferencesCommand>(index, identity, returnType, bundle, entry, - serverBase, authorizationHelper, resource, verb, d, exceptionHandler, parameterConverter, - responseGenerator, referenceExtractor, referenceResolver, validationRules))) - .orElseThrow(() -> new IllegalStateException( - "Resource of type " + resource.getClass().getName() + " not supported")); - } - else - return Stream.of(cmd); - } } diff --git a/dsf-fhir/dsf-fhir-server/src/main/java/dev/dsf/fhir/dao/command/CreateCommand.java b/dsf-fhir/dsf-fhir-server/src/main/java/dev/dsf/fhir/dao/command/CreateCommand.java index 61e820519..76948ab98 100755 --- a/dsf-fhir/dsf-fhir-server/src/main/java/dev/dsf/fhir/dao/command/CreateCommand.java +++ b/dsf-fhir/dsf-fhir-server/src/main/java/dev/dsf/fhir/dao/command/CreateCommand.java @@ -59,6 +59,7 @@ import dev.dsf.fhir.service.ReferenceExtractor; import dev.dsf.fhir.service.ReferenceResolver; import dev.dsf.fhir.validation.SnapshotGenerator; +import dev.dsf.fhir.validation.ValidationRules; import jakarta.ws.rs.WebApplicationException; import jakarta.ws.rs.core.EntityTag; import jakarta.ws.rs.core.Response; @@ -82,13 +83,14 @@ public class CreateCommand> extends public CreateCommand(int index, Identity identity, PreferReturnType returnType, Bundle bundle, BundleEntryComponent entry, String serverBase, AuthorizationHelper authorizationHelper, R resource, D dao, - ExceptionHandler exceptionHandler, ParameterConverter parameterConverter, + ExceptionHandler exceptionHandler, ParameterConverter parameterConverter, ValidationRules validationRules, ResponseGenerator responseGenerator, ReferenceExtractor referenceExtractor, ReferenceResolver referenceResolver, ReferenceCleaner referenceCleaner, EventGenerator eventGenerator, DefaultProfileProvider defaultProfileProvider, boolean enableValidation) { super(2, index, identity, returnType, bundle, entry, serverBase, authorizationHelper, resource, dao, - exceptionHandler, parameterConverter, responseGenerator, referenceExtractor, referenceResolver); + exceptionHandler, parameterConverter, validationRules, responseGenerator, referenceExtractor, + referenceResolver); this.responseGenerator = responseGenerator; this.referenceCleaner = referenceCleaner; @@ -270,6 +272,14 @@ else if (result.getTotal() > 1) return Optional.empty(); } + @Override + public void checkReferences(Map idTranslationTable, Connection connection) + throws SQLException, WebApplicationException + { + referencesHelper.checkReferences(idTranslationTable, connection, + ref -> validationRules.checkReferenceAfterCreate(resource, ref)); + } + @Override public Optional postExecute(Connection connection, EventHandler eventHandler) { diff --git a/dsf-fhir/dsf-fhir-server/src/main/java/dev/dsf/fhir/dao/command/CreateStructureDefinitionCommand.java b/dsf-fhir/dsf-fhir-server/src/main/java/dev/dsf/fhir/dao/command/CreateStructureDefinitionCommand.java index e3a14a3af..9374fe35c 100755 --- a/dsf-fhir/dsf-fhir-server/src/main/java/dev/dsf/fhir/dao/command/CreateStructureDefinitionCommand.java +++ b/dsf-fhir/dsf-fhir-server/src/main/java/dev/dsf/fhir/dao/command/CreateStructureDefinitionCommand.java @@ -42,6 +42,7 @@ import dev.dsf.fhir.service.ReferenceExtractor; import dev.dsf.fhir.service.ReferenceResolver; import dev.dsf.fhir.validation.SnapshotGenerator; +import dev.dsf.fhir.validation.ValidationRules; import dev.dsf.fhir.validation.SnapshotGenerator.SnapshotWithValidationMessages; import jakarta.ws.rs.WebApplicationException; import jakarta.ws.rs.core.Response; @@ -58,14 +59,14 @@ public class CreateStructureDefinitionCommand extends CreateCommand idTranslationTable) throws Exception + { + for (int i = 0; i < commands.size(); i++) + { + CommandResult cR = commands.get(i); + Command c = cR.getCommand(); + + try + { + logger.debug("Running check-references of {} for entry at index {}", c.getClass().getSimpleName(), + c.getIndex()); + + c.checkReferences(idTranslationTable, connection); + } + catch (Exception e) + { + logger.debug( + "Error while running check-references of {} for entry at index {}, rolling back transaction", + c.getClass().getSimpleName(), c.getIndex(), e); + logger.warn( + "Error while running check-references of {} for entry at index {}, rolling back transaction: {} - {}", + c.getClass().getSimpleName(), c.getIndex(), e.getClass().getName(), e.getMessage()); + + if (hasModifyingCommands) + { + logger.debug("Rolling back DB transaction"); + tryRollback(connection, largeObjectManager, e); + } + + try + { + commands.stream().map(CommandResult::getCommand).sorted(Comparator.comparing(Command::getIndex)) + .filter(r -> r.getIndex() != c.getIndex()).forEach(this::auditLogAbort); + auditLogResult(cR); + } + catch (Exception e1) + { + logger.debug("Error while writing to audit log", e1); + logger.warn("Error while writing to audit log: {} - {}", e1.getClass().getName(), e1.getMessage()); + } + + throw e; + } + } + } + + private void postExecute(Connection connection, LargeObjectManager largeObjectManager, + Map idTranslationTable, EventHandler eventHandler) throws Exception { for (int i = 0; i < commands.size(); i++) { diff --git a/dsf-fhir/dsf-fhir-server/src/main/java/dev/dsf/fhir/dao/command/UpdateCommand.java b/dsf-fhir/dsf-fhir-server/src/main/java/dev/dsf/fhir/dao/command/UpdateCommand.java index 4a840eb17..76b708210 100755 --- a/dsf-fhir/dsf-fhir-server/src/main/java/dev/dsf/fhir/dao/command/UpdateCommand.java +++ b/dsf-fhir/dsf-fhir-server/src/main/java/dev/dsf/fhir/dao/command/UpdateCommand.java @@ -61,6 +61,7 @@ import dev.dsf.fhir.service.ReferenceExtractor; import dev.dsf.fhir.service.ReferenceResolver; import dev.dsf.fhir.validation.SnapshotGenerator; +import dev.dsf.fhir.validation.ValidationRules; import jakarta.ws.rs.WebApplicationException; import jakarta.ws.rs.core.EntityTag; import jakarta.ws.rs.core.Response; @@ -84,13 +85,14 @@ public class UpdateCommand> extends public UpdateCommand(int index, Identity identity, PreferReturnType returnType, Bundle bundle, BundleEntryComponent entry, String serverBase, AuthorizationHelper authorizationHelper, R resource, D dao, - ExceptionHandler exceptionHandler, ParameterConverter parameterConverter, + ExceptionHandler exceptionHandler, ParameterConverter parameterConverter, ValidationRules validationRules, ResponseGenerator responseGenerator, ReferenceExtractor referenceExtractor, ReferenceResolver referenceResolver, ReferenceCleaner referenceCleaner, EventGenerator eventGenerator, DefaultProfileProvider defaultProfileProvider, boolean enableValidation) { super(3, index, identity, returnType, bundle, entry, serverBase, authorizationHelper, resource, dao, - exceptionHandler, parameterConverter, responseGenerator, referenceExtractor, referenceResolver); + exceptionHandler, parameterConverter, validationRules, responseGenerator, referenceExtractor, + referenceResolver); this.responseGenerator = responseGenerator; this.referenceCleaner = referenceCleaner; @@ -443,6 +445,15 @@ private UUID getUuid(Map idTranslationTable) throw new RuntimeException("Error while retrieving id from id translation table"); } + @Override + public void checkReferences(Map idTranslationTable, Connection connection) + throws SQLException, WebApplicationException + { + // version -1 as we are checking against the pre-update resource + referencesHelper.checkReferences(idTranslationTable, connection, + ref -> validationRules.checkReferenceAfterUpdate(resource, ref, v -> v - 1)); + } + @Override public Optional postExecute(Connection connection, EventHandler eventHandler) { diff --git a/dsf-fhir/dsf-fhir-server/src/main/java/dev/dsf/fhir/dao/command/UpdateStructureDefinitionCommand.java b/dsf-fhir/dsf-fhir-server/src/main/java/dev/dsf/fhir/dao/command/UpdateStructureDefinitionCommand.java index 65f1f9a50..8e07271e6 100755 --- a/dsf-fhir/dsf-fhir-server/src/main/java/dev/dsf/fhir/dao/command/UpdateStructureDefinitionCommand.java +++ b/dsf-fhir/dsf-fhir-server/src/main/java/dev/dsf/fhir/dao/command/UpdateStructureDefinitionCommand.java @@ -45,6 +45,7 @@ import dev.dsf.fhir.service.ReferenceExtractor; import dev.dsf.fhir.service.ReferenceResolver; import dev.dsf.fhir.validation.SnapshotGenerator; +import dev.dsf.fhir.validation.ValidationRules; import dev.dsf.fhir.validation.SnapshotGenerator.SnapshotWithValidationMessages; import jakarta.ws.rs.WebApplicationException; import jakarta.ws.rs.core.Response; @@ -62,14 +63,14 @@ public class UpdateStructureDefinitionCommand extends UpdateCommand Date: Mon, 12 Oct 2026 11:31:16 +0200 Subject: [PATCH 06/13] surefire javaagent mockito config --- dsf-bpe/dsf-bpe-process-api-v1-impl/pom.xml | 1 + dsf-bpe/dsf-bpe-process-api-v2-impl/pom.xml | 13 +++++++++++++ dsf-bpe/dsf-bpe-server/pom.xml | 1 + dsf-fhir/dsf-fhir-server/pom.xml | 1 + 4 files changed, 16 insertions(+) diff --git a/dsf-bpe/dsf-bpe-process-api-v1-impl/pom.xml b/dsf-bpe/dsf-bpe-process-api-v1-impl/pom.xml index 78e88210b..ae47c70f7 100644 --- a/dsf-bpe/dsf-bpe-process-api-v1-impl/pom.xml +++ b/dsf-bpe/dsf-bpe-process-api-v1-impl/pom.xml @@ -107,6 +107,7 @@ + properties diff --git a/dsf-bpe/dsf-bpe-process-api-v2-impl/pom.xml b/dsf-bpe/dsf-bpe-process-api-v2-impl/pom.xml index 7f009d299..1f38dd5a2 100644 --- a/dsf-bpe/dsf-bpe-process-api-v2-impl/pom.xml +++ b/dsf-bpe/dsf-bpe-process-api-v2-impl/pom.xml @@ -122,6 +122,12 @@ org.apache.maven.plugins maven-dependency-plugin + + + + properties + + copy-api-v2-dependencies-to-docker pre-integration-test @@ -289,6 +295,13 @@ + + org.apache.maven.plugins + maven-surefire-plugin + + -javaagent:${org.mockito:mockito-core:jar} + + org.apache.maven.plugins maven-clean-plugin diff --git a/dsf-bpe/dsf-bpe-server/pom.xml b/dsf-bpe/dsf-bpe-server/pom.xml index ebc72686e..d891397c0 100755 --- a/dsf-bpe/dsf-bpe-server/pom.xml +++ b/dsf-bpe/dsf-bpe-server/pom.xml @@ -320,6 +320,7 @@ + properties diff --git a/dsf-fhir/dsf-fhir-server/pom.xml b/dsf-fhir/dsf-fhir-server/pom.xml index 45ece6fb0..f6d92f129 100755 --- a/dsf-fhir/dsf-fhir-server/pom.xml +++ b/dsf-fhir/dsf-fhir-server/pom.xml @@ -316,6 +316,7 @@ + properties From ba5aad49e3385daaa747b9b0fe4849728eeab9fb Mon Sep 17 00:00:00 2001 From: Hauke Hund Date: Mon, 12 Oct 2026 12:57:41 +0200 Subject: [PATCH 07/13] added read access tests --- .../integration/BinaryIntegrationTest.java | 86 +++++++++++++++++++ 1 file changed, 86 insertions(+) diff --git a/dsf-fhir/dsf-fhir-server/src/test/java/dev/dsf/fhir/integration/BinaryIntegrationTest.java b/dsf-fhir/dsf-fhir-server/src/test/java/dev/dsf/fhir/integration/BinaryIntegrationTest.java index ea4ed1531..e2c734941 100755 --- a/dsf-fhir/dsf-fhir-server/src/test/java/dev/dsf/fhir/integration/BinaryIntegrationTest.java +++ b/dsf-fhir/dsf-fhir-server/src/test/java/dev/dsf/fhir/integration/BinaryIntegrationTest.java @@ -337,6 +337,92 @@ public void testReadAllowedExternalUserViaSecurityContext() throws Exception getExternalWebserviceClient().read(Binary.class, created.getIdElement().getIdPart()); } + @Test + public void testReadNotAllowedExternalUserViaSecurityContextAfterUpdate() throws Exception + { + OrganizationDao orgDao = getSpringWebApplicationContext().getBean(OrganizationDao.class); + PartialResult result = orgDao + .search(orgDao.createSearchQueryWithoutUserFilter(PageAndCount.single()) + .configureParameters(Map.of("name", List.of("External Test Organization")))); + assertNotNull(result); + assertEquals(1, result.getTotal()); + assertNotNull(result.getPartialResult()); + assertEquals(1, result.getPartialResult().size()); + assertNotNull(result.getPartialResult().get(0)); + + Organization org = result.getPartialResult().get(0); + + ResearchStudy rs = new ResearchStudy(); + getReadAccessHelper().addLocal(rs); + getReadAccessHelper().addOrganization(rs, org); + + ResearchStudyDao researchStudyDao = getSpringWebApplicationContext().getBean(ResearchStudyDao.class); + ResearchStudy createdRs = researchStudyDao.create(rs); + assertNotNull(createdRs); + + final String contentType = MediaType.TEXT_PLAIN; + final byte[] data = "Hello World".getBytes(StandardCharsets.UTF_8); + + Binary binary = new Binary(); + binary.setContentType(contentType); + binary.setData(data); + binary.setSecurityContext(new Reference(createdRs.getIdElement().toVersionless())); + + BinaryDao binDao = getSpringWebApplicationContext().getBean(BinaryDao.class); + Binary created = binDao.create(binary); + + getExternalWebserviceClient().read(Binary.class, created.getIdElement().getIdPart()); + + createdRs.getMeta().setTag(null); + getReadAccessHelper().addLocal(createdRs); + + ResearchStudy updatedRs = researchStudyDao.update(createdRs); + assertNotNull(updatedRs); + + expectForbidden(() -> getExternalWebserviceClient().read(Binary.class, created.getIdElement().getIdPart())); + } + + @Test + public void testReadNotAllowedExternalUserViaSecurityContextAfterDelete() throws Exception + { + OrganizationDao orgDao = getSpringWebApplicationContext().getBean(OrganizationDao.class); + PartialResult result = orgDao + .search(orgDao.createSearchQueryWithoutUserFilter(PageAndCount.single()) + .configureParameters(Map.of("name", List.of("External Test Organization")))); + assertNotNull(result); + assertEquals(1, result.getTotal()); + assertNotNull(result.getPartialResult()); + assertEquals(1, result.getPartialResult().size()); + assertNotNull(result.getPartialResult().get(0)); + + Organization org = result.getPartialResult().get(0); + + ResearchStudy rs = new ResearchStudy(); + getReadAccessHelper().addLocal(rs); + getReadAccessHelper().addOrganization(rs, org); + + ResearchStudyDao researchStudyDao = getSpringWebApplicationContext().getBean(ResearchStudyDao.class); + ResearchStudy createdRs = researchStudyDao.create(rs); + assertNotNull(createdRs); + + final String contentType = MediaType.TEXT_PLAIN; + final byte[] data = "Hello World".getBytes(StandardCharsets.UTF_8); + + Binary binary = new Binary(); + binary.setContentType(contentType); + binary.setData(data); + binary.setSecurityContext(new Reference(createdRs.getIdElement().toVersionless())); + + BinaryDao binDao = getSpringWebApplicationContext().getBean(BinaryDao.class); + Binary created = binDao.create(binary); + + getExternalWebserviceClient().read(Binary.class, created.getIdElement().getIdPart()); + + researchStudyDao.delete(UUID.fromString(createdRs.getIdElement().getIdPart())); + + expectForbidden(() -> getExternalWebserviceClient().read(Binary.class, created.getIdElement().getIdPart())); + } + @Test public void testReadAllowedExternalUserViaSecurityContextDocumentReference() throws Exception { From 6bb38de27f390e39cff15225b5ec766c28338ef3 Mon Sep 17 00:00:00 2001 From: Hauke Hund Date: Mon, 12 Oct 2026 13:12:09 +0200 Subject: [PATCH 08/13] code formatting --- .../dsf/fhir/dao/command/BatchCommandList.java | 6 +++--- .../dsf/fhir/dao/command/CommandFactoryImpl.java | 16 ++++++++-------- .../CreateStructureDefinitionCommand.java | 2 +- .../UpdateStructureDefinitionCommand.java | 2 +- .../fhir/integration/BinaryIntegrationTest.java | 4 ++-- .../fhir/integration/BundleIntegrationTest.java | 6 +++--- 6 files changed, 18 insertions(+), 18 deletions(-) diff --git a/dsf-fhir/dsf-fhir-server/src/main/java/dev/dsf/fhir/dao/command/BatchCommandList.java b/dsf-fhir/dsf-fhir-server/src/main/java/dev/dsf/fhir/dao/command/BatchCommandList.java index 75937d0a5..4986750ae 100755 --- a/dsf-fhir/dsf-fhir-server/src/main/java/dev/dsf/fhir/dao/command/BatchCommandList.java +++ b/dsf-fhir/dsf-fhir-server/src/main/java/dev/dsf/fhir/dao/command/BatchCommandList.java @@ -174,12 +174,12 @@ private void execute(CommandResult commandResult, Connection connection, Map commands = IntStream.range(0, bundle.getEntry().size()) - .mapToObj(index -> createCommand(index, identity, returnType, handlingType, bundle, - bundle.getEntry().get(index), enableValidation)) + List commands = IntStream + .range(0, bundle.getEntry().size()).mapToObj(index -> createCommand(index, identity, returnType, + handlingType, bundle, bundle.getEntry().get(index), enableValidation)) .collect(Collectors.toList()); return switch (bundle.getType()) { - case BATCH -> new BatchCommandList(dataSource, permanentDeleteDataSource, dbUsersGroup, - exceptionHandler, commands, validationHelper, snapshotGenerator, eventHandler, - responseGenerator); + case BATCH -> + new BatchCommandList(dataSource, permanentDeleteDataSource, dbUsersGroup, exceptionHandler, + commands, validationHelper, snapshotGenerator, eventHandler, responseGenerator); case TRANSACTION -> new TransactionCommandList(dataSource, permanentDeleteDataSource, dbUsersGroup, exceptionHandler, commands, transactionResourcesFactory, responseGenerator); @@ -286,8 +286,8 @@ protected Command createCommand(int index, Identity identity, PreferReturnType r { return switch (entry.getRequest().getMethod()) { - case POST -> post(index, identity, returnType, bundle, entry, entry.getResource(), - enableValidation); + case POST -> + post(index, identity, returnType, bundle, entry, entry.getResource(), enableValidation); case PUT -> put(index, identity, returnType, bundle, entry, entry.getResource(), enableValidation); default -> throw new BadBundleException("Request method " + entry.getRequest().getMethod() diff --git a/dsf-fhir/dsf-fhir-server/src/main/java/dev/dsf/fhir/dao/command/CreateStructureDefinitionCommand.java b/dsf-fhir/dsf-fhir-server/src/main/java/dev/dsf/fhir/dao/command/CreateStructureDefinitionCommand.java index 9374fe35c..028836787 100755 --- a/dsf-fhir/dsf-fhir-server/src/main/java/dev/dsf/fhir/dao/command/CreateStructureDefinitionCommand.java +++ b/dsf-fhir/dsf-fhir-server/src/main/java/dev/dsf/fhir/dao/command/CreateStructureDefinitionCommand.java @@ -42,8 +42,8 @@ import dev.dsf.fhir.service.ReferenceExtractor; import dev.dsf.fhir.service.ReferenceResolver; import dev.dsf.fhir.validation.SnapshotGenerator; -import dev.dsf.fhir.validation.ValidationRules; import dev.dsf.fhir.validation.SnapshotGenerator.SnapshotWithValidationMessages; +import dev.dsf.fhir.validation.ValidationRules; import jakarta.ws.rs.WebApplicationException; import jakarta.ws.rs.core.Response; diff --git a/dsf-fhir/dsf-fhir-server/src/main/java/dev/dsf/fhir/dao/command/UpdateStructureDefinitionCommand.java b/dsf-fhir/dsf-fhir-server/src/main/java/dev/dsf/fhir/dao/command/UpdateStructureDefinitionCommand.java index 8e07271e6..b2a9cb2b7 100755 --- a/dsf-fhir/dsf-fhir-server/src/main/java/dev/dsf/fhir/dao/command/UpdateStructureDefinitionCommand.java +++ b/dsf-fhir/dsf-fhir-server/src/main/java/dev/dsf/fhir/dao/command/UpdateStructureDefinitionCommand.java @@ -45,8 +45,8 @@ import dev.dsf.fhir.service.ReferenceExtractor; import dev.dsf.fhir.service.ReferenceResolver; import dev.dsf.fhir.validation.SnapshotGenerator; -import dev.dsf.fhir.validation.ValidationRules; import dev.dsf.fhir.validation.SnapshotGenerator.SnapshotWithValidationMessages; +import dev.dsf.fhir.validation.ValidationRules; import jakarta.ws.rs.WebApplicationException; import jakarta.ws.rs.core.Response; diff --git a/dsf-fhir/dsf-fhir-server/src/test/java/dev/dsf/fhir/integration/BinaryIntegrationTest.java b/dsf-fhir/dsf-fhir-server/src/test/java/dev/dsf/fhir/integration/BinaryIntegrationTest.java index e2c734941..734288425 100755 --- a/dsf-fhir/dsf-fhir-server/src/test/java/dev/dsf/fhir/integration/BinaryIntegrationTest.java +++ b/dsf-fhir/dsf-fhir-server/src/test/java/dev/dsf/fhir/integration/BinaryIntegrationTest.java @@ -1935,7 +1935,7 @@ public void testCreateViaBatchBundle() throws Exception assertNotNull(responseBundle.getEntry().get(0).getResponse().getEtag()); assertEquals("W/\"1\"", responseBundle.getEntry().get(0).getResponse().getEtag()); } - + @Test public void testCreateBinaryWithNonExistingSecurityContextViaBatchBundle() throws Exception { @@ -1945,7 +1945,7 @@ public void testCreateBinaryWithNonExistingSecurityContextViaBatchBundle() throw Binary binary = new Binary(); binary.setContentType(contentType); binary.setData(data); - binary.getSecurityContext().setReference("ResearchStudy/"+UUID.randomUUID().toString()); + binary.getSecurityContext().setReference("ResearchStudy/" + UUID.randomUUID().toString()); Bundle bundle = new Bundle(); bundle.setType(BundleType.BATCH); diff --git a/dsf-fhir/dsf-fhir-server/src/test/java/dev/dsf/fhir/integration/BundleIntegrationTest.java b/dsf-fhir/dsf-fhir-server/src/test/java/dev/dsf/fhir/integration/BundleIntegrationTest.java index 6dcac18b7..2cf804d39 100644 --- a/dsf-fhir/dsf-fhir-server/src/test/java/dev/dsf/fhir/integration/BundleIntegrationTest.java +++ b/dsf-fhir/dsf-fhir-server/src/test/java/dev/dsf/fhir/integration/BundleIntegrationTest.java @@ -497,7 +497,7 @@ public void testTransactionBundleWithNonResolvableReference() throws Exception Bundle b = createBundleWithGroupResource(BundleType.TRANSACTION, GroupType.PERSON); expectForbidden(() -> getWebserviceClient().postBundle(b)); - + GroupDao groupDao = getSpringWebApplicationContext().getBean(GroupDao.class); List all = groupDao.readAll(); assertNotNull(all); @@ -513,7 +513,7 @@ public void testBatchBundleWithoutGroupTypeAndNonResolvableReference() throws Ex assertNotNull(result); assertEquals(1, result.getEntry().size()); assertEquals("403 Forbidden", result.getEntry().get(0).getResponse().getStatus()); - + GroupDao groupDao = getSpringWebApplicationContext().getBean(GroupDao.class); List all = groupDao.readAll(); assertNotNull(all); @@ -526,7 +526,7 @@ public void testTransactionBundleWithoutGroupTypeAndNonResolvableReference() thr Bundle b = createBundleWithGroupResource(BundleType.TRANSACTION, null); expectForbidden(() -> getWebserviceClient().postBundle(b)); - + GroupDao groupDao = getSpringWebApplicationContext().getBean(GroupDao.class); List all = groupDao.readAll(); assertNotNull(all); From 44597c28076f5a789a022ae476cb7884cde04af7 Mon Sep 17 00:00:00 2001 From: Hauke Hund Date: Mon, 12 Oct 2026 15:56:45 +0200 Subject: [PATCH 09/13] code formatting --- .../src/main/java/dev/dsf/fhir/dao/command/Command.java | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/dsf-fhir/dsf-fhir-server/src/main/java/dev/dsf/fhir/dao/command/Command.java b/dsf-fhir/dsf-fhir-server/src/main/java/dev/dsf/fhir/dao/command/Command.java index 70a397f09..914f9a4c4 100755 --- a/dsf-fhir/dsf-fhir-server/src/main/java/dev/dsf/fhir/dao/command/Command.java +++ b/dsf-fhir/dsf-fhir-server/src/main/java/dev/dsf/fhir/dao/command/Command.java @@ -48,8 +48,7 @@ void execute(Map idTranslationTable, LargeObjectManager largeObj default void checkReferences(Map idTranslationTable, Connection connection) throws SQLException, WebApplicationException { - - }; + } default Optional postExecute(Connection connection, EventHandler eventHandler) { From 07d1b3a5f33fc30d8ae52dca79899b594cde8983 Mon Sep 17 00:00:00 2001 From: Hauke Hund Date: Mon, 12 Oct 2026 16:06:34 +0200 Subject: [PATCH 10/13] removed not needed method parameter --- .../dev/dsf/fhir/dao/command/TransactionCommandList.java | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/dsf-fhir/dsf-fhir-server/src/main/java/dev/dsf/fhir/dao/command/TransactionCommandList.java b/dsf-fhir/dsf-fhir-server/src/main/java/dev/dsf/fhir/dao/command/TransactionCommandList.java index 8eb4b5744..1957b16bb 100755 --- a/dsf-fhir/dsf-fhir-server/src/main/java/dev/dsf/fhir/dao/command/TransactionCommandList.java +++ b/dsf-fhir/dsf-fhir-server/src/main/java/dev/dsf/fhir/dao/command/TransactionCommandList.java @@ -93,7 +93,7 @@ public Bundle execute() throws WebApplicationException checkReferences(connection, largeObjectManager, idTranslationTable); - postExecute(connection, largeObjectManager, idTranslationTable, transactionEventHandler); + postExecute(connection, largeObjectManager, transactionEventHandler); if (hasModifyingCommands) { @@ -305,8 +305,8 @@ private void checkReferences(Connection connection, LargeObjectManager largeObje } } - private void postExecute(Connection connection, LargeObjectManager largeObjectManager, - Map idTranslationTable, EventHandler eventHandler) throws Exception + private void postExecute(Connection connection, LargeObjectManager largeObjectManager, EventHandler eventHandler) + throws Exception { for (int i = 0; i < commands.size(); i++) { From fff5c46aeebb5ed1c4f39dd1e14185fdcf783397 Mon Sep 17 00:00:00 2001 From: Hauke Hund Date: Tue, 13 Oct 2026 10:00:53 +0200 Subject: [PATCH 11/13] additional test cases --- .../integration/BundleIntegrationTest.java | 58 +++++++++++++++++++ 1 file changed, 58 insertions(+) diff --git a/dsf-fhir/dsf-fhir-server/src/test/java/dev/dsf/fhir/integration/BundleIntegrationTest.java b/dsf-fhir/dsf-fhir-server/src/test/java/dev/dsf/fhir/integration/BundleIntegrationTest.java index 2cf804d39..cef31b159 100644 --- a/dsf-fhir/dsf-fhir-server/src/test/java/dev/dsf/fhir/integration/BundleIntegrationTest.java +++ b/dsf-fhir/dsf-fhir-server/src/test/java/dev/dsf/fhir/integration/BundleIntegrationTest.java @@ -548,4 +548,62 @@ private Bundle createBundleWithGroupResource(BundleType bundleType, GroupType gr e.getRequest().setMethod(HTTPVerb.POST).setUrl("Group"); return b; } + + @Test + public void batchConditionalCreateMultipleMatches() throws Exception + { + PatientDao patientDao = getSpringWebApplicationContext().getBean(PatientDao.class); + for (int i = 0; i < 2; i++) + { + Patient patient = new Patient(); + patient.addIdentifier().setSystem("system").setValue("duplicate"); + patientDao.create(patient); + } + + Bundle request = new Bundle().setType(BundleType.BATCH); + request.addEntry().setFullUrl("urn:uuid:" + UUID.randomUUID()).setResource(new Patient().setActive(true)) + .getRequest().setMethod(HTTPVerb.POST).setUrl("Patient").setIfNoneExist("identifier=system|duplicate"); + + Bundle result = getWebserviceClient().postBundle(request); + + assertNotNull(result); + assertEquals(BundleType.BATCHRESPONSE, result.getType()); + assertEquals(1, result.getEntry().size()); + assertEquals("412 Precondition Failed", result.getEntryFirstRep().getResponse().getStatus()); + + assertEquals(2, patientDao.readAll().size()); + } + + @Test + public void batchConditionalCreateMultipleMatchesPLusOkResource() throws Exception + { + PatientDao patientDao = getSpringWebApplicationContext().getBean(PatientDao.class); + for (int i = 0; i < 2; i++) + { + Patient patient = new Patient(); + patient.addIdentifier().setSystem("system").setValue("duplicate"); + patientDao.create(patient); + } + + Bundle request = new Bundle().setType(BundleType.BATCH); + request.addEntry().setFullUrl("urn:uuid:" + UUID.randomUUID()).setResource(new Patient().setActive(true)) + .getRequest().setMethod(HTTPVerb.POST).setUrl("Patient").setIfNoneExist("identifier=system|duplicate"); + request.addEntry().setFullUrl("urn:uuid:" + UUID.randomUUID()) + .setResource(getReadAccessHelper().addAll(new Group()).setType(GroupType.ANIMAL).setActual(false)) + .getRequest().setMethod(HTTPVerb.POST).setUrl("Group"); + + Bundle result = getWebserviceClient().postBundle(request); + + assertNotNull(result); + + assertEquals(BundleType.BATCHRESPONSE, result.getType()); + assertEquals(2, result.getEntry().size()); + assertEquals("412 Precondition Failed", result.getEntry().get(0).getResponse().getStatus()); + assertEquals("201 Created", result.getEntry().get(1).getResponse().getStatus()); + + assertEquals(2, patientDao.readAll().size()); + + GroupDao groupDao = getSpringWebApplicationContext().getBean(GroupDao.class); + assertEquals(1, groupDao.readAll().size()); + } } \ No newline at end of file From 676e37c6ad70eb6e7ffccd8043a671f9e551141b Mon Sep 17 00:00:00 2001 From: Hauke Hund Date: Tue, 13 Oct 2026 10:04:22 +0200 Subject: [PATCH 12/13] transaction modification fix auto-commit false was previously set to early leading to a not commited transaction if a batch bundle only included one resource and failed in the preExecute phase. Commit or rollback only happens in the execute phase, everything else must be auto-commit true. --- .../dsf/fhir/dao/command/BatchCommandList.java | 17 ++++++++--------- 1 file changed, 8 insertions(+), 9 deletions(-) diff --git a/dsf-fhir/dsf-fhir-server/src/main/java/dev/dsf/fhir/dao/command/BatchCommandList.java b/dsf-fhir/dsf-fhir-server/src/main/java/dev/dsf/fhir/dao/command/BatchCommandList.java index 4986750ae..f92067a22 100755 --- a/dsf-fhir/dsf-fhir-server/src/main/java/dev/dsf/fhir/dao/command/BatchCommandList.java +++ b/dsf-fhir/dsf-fhir-server/src/main/java/dev/dsf/fhir/dao/command/BatchCommandList.java @@ -72,14 +72,6 @@ public Bundle execute() throws WebApplicationException int initialTransactionIsolationLevel = connection.getTransactionIsolation(); Map idTranslationTable = new HashMap<>(); - - if (hasModifyingCommands) - { - connection.setReadOnly(false); - connection.setAutoCommit(false); - connection.setTransactionIsolation(Connection.TRANSACTION_READ_COMMITTED); - } - Set indexesWithError = new HashSet<>(); commands.forEach(cR -> @@ -89,6 +81,13 @@ public Bundle execute() throws WebApplicationException if (cR.hasException()) indexesWithError.add(cR.getCommand().getIndex()); }); + + if (hasModifyingCommands) + { + connection.setReadOnly(false); + connection.setAutoCommit(false); + connection.setTransactionIsolation(Connection.TRANSACTION_READ_COMMITTED); + } commands.stream().filter(Predicate.not(CommandResult::hasException)).forEach(cR -> { @@ -98,7 +97,7 @@ public Bundle execute() throws WebApplicationException if (cR.hasException()) indexesWithError.add(cR.getCommand().getIndex()); }); - + if (hasModifyingCommands) { connection.setReadOnly(initialReadOnly); From f30405639ffec804667dea677757ef2e0d47981c Mon Sep 17 00:00:00 2001 From: Hauke Hund Date: Tue, 13 Oct 2026 10:08:16 +0200 Subject: [PATCH 13/13] code formatting --- .../main/java/dev/dsf/fhir/dao/command/BatchCommandList.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/dsf-fhir/dsf-fhir-server/src/main/java/dev/dsf/fhir/dao/command/BatchCommandList.java b/dsf-fhir/dsf-fhir-server/src/main/java/dev/dsf/fhir/dao/command/BatchCommandList.java index f92067a22..657a8f441 100755 --- a/dsf-fhir/dsf-fhir-server/src/main/java/dev/dsf/fhir/dao/command/BatchCommandList.java +++ b/dsf-fhir/dsf-fhir-server/src/main/java/dev/dsf/fhir/dao/command/BatchCommandList.java @@ -81,7 +81,7 @@ public Bundle execute() throws WebApplicationException if (cR.hasException()) indexesWithError.add(cR.getCommand().getIndex()); }); - + if (hasModifyingCommands) { connection.setReadOnly(false); @@ -97,7 +97,7 @@ public Bundle execute() throws WebApplicationException if (cR.hasException()) indexesWithError.add(cR.getCommand().getIndex()); }); - + if (hasModifyingCommands) { connection.setReadOnly(initialReadOnly);