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-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 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 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 46f052c47..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 @@ -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) @@ -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/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 e462608f7..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 @@ -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,8 +71,16 @@ 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<>(); + Set indexesWithError = new HashSet<>(); + + commands.forEach(cR -> + { + preExecute(cR, connection, idTranslationTable); + + if (cR.hasException()) + indexesWithError.add(cR.getCommand().getIndex()); + }); if (hasModifyingCommands) { @@ -83,9 +89,14 @@ public Bundle execute() throws WebApplicationException connection.setTransactionIsolation(Connection.TRANSACTION_READ_COMMITTED); } - commands.forEach(preExecute(idTranslationTable, connection, caughtExceptions)); + commands.stream().filter(Predicate.not(CommandResult::hasException)).forEach(cR -> + { + if (!indexesWithError.contains(cR.getCommand().getIndex())) + execute(cR, connection, idTranslationTable); - commands.forEach(execute(idTranslationTable, connection, caughtExceptions)); + if (cR.hasException()) + indexesWithError.add(cR.getCommand().getIndex()); + }); if (hasModifyingCommands) { @@ -94,22 +105,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 +136,123 @@ 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 and check-references of {} for entry at index {}", + command.getClass().getSimpleName(), command.getIndex()); + command.execute(idTranslationTable, largeObjectManager, connection, validationHelper); + + command.checkReferences(idTranslationTable, connection); + + 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 deleted file mode 100644 index 8969da21f..000000000 --- a/dsf-fhir/dsf-fhir-server/src/main/java/dev/dsf/fhir/dao/command/CheckReferencesCommand.java +++ /dev/null @@ -1,83 +0,0 @@ -/* - * Copyright 2018-2025 Heilbronn University of Applied Sciences - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package dev.dsf.fhir.dao.command; - -import java.sql.Connection; -import java.sql.SQLException; -import java.util.Map; - -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.IdType; -import org.hl7.fhir.r4.model.Resource; - -import dev.dsf.common.auth.conf.Identity; -import dev.dsf.fhir.dao.ResourceDao; -import dev.dsf.fhir.dao.jdbc.LargeObjectManager; -import dev.dsf.fhir.help.ExceptionHandler; -import dev.dsf.fhir.help.ParameterConverter; -import dev.dsf.fhir.help.ResponseGenerator; -import dev.dsf.fhir.prefer.PreferReturnType; -import dev.dsf.fhir.service.ReferenceExtractor; -import dev.dsf.fhir.service.ReferenceResolver; -import dev.dsf.fhir.service.ResourceReference; -import dev.dsf.fhir.validation.ValidationRules; -import jakarta.ws.rs.WebApplicationException; - -public class CheckReferencesCommand> - 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 (HTTPVerb.PUT.equals(verb)) - { - // version -1 as we are checking against the pre-update resource - return validationRules.checkReferenceAfterUpdate(resource, ref, v -> v - 1); - } - - return true; - } - - @Override - public String getResourceTypeName() - { - throw new UnsupportedOperationException(); - } -} 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..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 @@ -45,6 +45,11 @@ 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..bcefeadbe 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( @@ -246,10 +244,10 @@ public CommandList createCommands(Bundle bundle, Identity identity, PreferReturn if (bundle.getType() != null) { - 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()); + 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()) { @@ -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 3a95d008b..1bcf6f20b 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..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 @@ -43,6 +43,7 @@ import dev.dsf.fhir.service.ReferenceResolver; import dev.dsf.fhir.validation.SnapshotGenerator; import dev.dsf.fhir.validation.SnapshotGenerator.SnapshotWithValidationMessages; +import dev.dsf.fhir.validation.ValidationRules; import jakarta.ws.rs.WebApplicationException; import jakarta.ws.rs.core.Response; @@ -58,14 +59,14 @@ public class CreateStructureDefinitionCommand extends CreateCommand 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,14 @@ 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::auditLogAbort); - 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::auditLogAbort); - 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; - } - } + preExecute(connection, validationHelper, snapshotGenerator, idTranslationTable); - 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); - } + execute(connection, validationHelper, largeObjectManager, idTranslationTable); - try - { - commands.stream().limit(c.getIndex()).forEach(this::auditLogAbort); - 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()); - } + checkReferences(connection, largeObjectManager, idTranslationTable); - throw e; - } - } + postExecute(connection, largeObjectManager, transactionEventHandler); if (hasModifyingCommands) { @@ -241,25 +129,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 +167,190 @@ 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 checkReferences(Connection connection, 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 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, 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 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 487d61db6..a75f1c9f9 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; @@ -444,6 +446,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..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 @@ -46,6 +46,7 @@ import dev.dsf.fhir.service.ReferenceResolver; import dev.dsf.fhir.validation.SnapshotGenerator; import dev.dsf.fhir.validation.SnapshotGenerator.SnapshotWithValidationMessages; +import dev.dsf.fhir.validation.ValidationRules; import jakarta.ws.rs.WebApplicationException; import jakarta.ws.rs.core.Response; @@ -62,14 +63,14 @@ public class UpdateStructureDefinitionCommand extends UpdateCommand 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) { 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..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 @@ -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 { @@ -1850,6 +1936,34 @@ public void testCreateViaBatchBundle() throws Exception 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 ad1a3effe..53f59bcfb 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,136 @@ 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; + } + + @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