diff --git a/dsf-fhir/dsf-fhir-server/src/main/java/dev/dsf/fhir/authorization/AuthorizationRule.java b/dsf-fhir/dsf-fhir-server/src/main/java/dev/dsf/fhir/authorization/AuthorizationRule.java index 19cc2a997..5e3459159 100644 --- a/dsf-fhir/dsf-fhir-server/src/main/java/dev/dsf/fhir/authorization/AuthorizationRule.java +++ b/dsf-fhir/dsf-fhir-server/src/main/java/dev/dsf/fhir/authorization/AuthorizationRule.java @@ -98,7 +98,6 @@ public interface AuthorizationRule */ Optional reasonDeleteAllowed(Identity identity, R oldResource); - /** * Override this method for non default behavior. Default: Not allowed. * diff --git a/dsf-fhir/dsf-fhir-server/src/main/java/dev/dsf/fhir/authorization/StructureDefinitionAuthorizationRule.java b/dsf-fhir/dsf-fhir-server/src/main/java/dev/dsf/fhir/authorization/StructureDefinitionAuthorizationRule.java index 68a079a3a..37dbc8a49 100644 --- a/dsf-fhir/dsf-fhir-server/src/main/java/dev/dsf/fhir/authorization/StructureDefinitionAuthorizationRule.java +++ b/dsf-fhir/dsf-fhir-server/src/main/java/dev/dsf/fhir/authorization/StructureDefinitionAuthorizationRule.java @@ -73,6 +73,10 @@ private Optional newResourceOk(Connection connection, StructureDefinitio { errors.add("StructureDefinition.version not defined"); } + if (!newResource.hasBaseDefinition()) + { + errors.add("StructureDefinition.baseDefinition not defined"); + } if (!hasValidReadAccessTag(connection, newResource)) { diff --git a/dsf-fhir/dsf-fhir-server/src/main/java/dev/dsf/fhir/authorization/TaskAuthorizationRule.java b/dsf-fhir/dsf-fhir-server/src/main/java/dev/dsf/fhir/authorization/TaskAuthorizationRule.java index 96fb15633..e23b70492 100644 --- a/dsf-fhir/dsf-fhir-server/src/main/java/dev/dsf/fhir/authorization/TaskAuthorizationRule.java +++ b/dsf-fhir/dsf-fhir-server/src/main/java/dev/dsf/fhir/authorization/TaskAuthorizationRule.java @@ -4,6 +4,7 @@ import java.sql.SQLException; import java.util.ArrayList; import java.util.List; +import java.util.Map; import java.util.Objects; import java.util.Optional; import java.util.function.Predicate; @@ -15,6 +16,7 @@ import org.hl7.fhir.r4.model.ActivityDefinition; import org.hl7.fhir.r4.model.CanonicalType; import org.hl7.fhir.r4.model.Coding; +import org.hl7.fhir.r4.model.Identifier; import org.hl7.fhir.r4.model.Organization; import org.hl7.fhir.r4.model.Resource; import org.hl7.fhir.r4.model.StringType; @@ -34,6 +36,10 @@ import dev.dsf.fhir.dao.TaskDao; import dev.dsf.fhir.dao.provider.DaoProvider; import dev.dsf.fhir.help.ParameterConverter; +import dev.dsf.fhir.search.PageAndCount; +import dev.dsf.fhir.search.PartialResult; +import dev.dsf.fhir.search.SearchQuery; +import dev.dsf.fhir.search.SearchQueryParameterError; import dev.dsf.fhir.service.ReferenceResolver; import dev.dsf.fhir.service.ResourceReference; @@ -88,10 +94,20 @@ public Optional reasonCreateAllowed(Connection connection, Identity iden Optional errors = draftTaskOk(connection, identity, newResource); if (errors.isEmpty()) { - logger.info("Create of Task authorized for local organization identity '{}', Task.status draft", - identity.getName()); + if (!draftTaskExists(connection, newResource)) + { + logger.info( + "Create of Task authorized for local organization identity '{}', Task.status draft", + identity.getName()); + + return Optional.of("Local identity, Task.status draft"); + } + else + { + logger.warn("Create of Task unauthorized, unique resource already exists"); - return Optional.of("Local identity, Task.status draft"); + return Optional.empty(); + } } else { @@ -284,9 +300,16 @@ private Optional draftTaskOk(Connection connection, Identity identity, T { List errors = new ArrayList<>(); - if (newResource.getIdentifier().stream().noneMatch(i -> NAMING_SYSTEM_TASK_IDENTIFIER.equals(i.getSystem()))) + if (newResource.getIdentifier().stream().filter(i -> NAMING_SYSTEM_TASK_IDENTIFIER.equals(i.getSystem())) + .count() != 1) + { + errors.add("Task.identifier[" + NAMING_SYSTEM_TASK_IDENTIFIER + "] not defined or more than once"); + } + else if (newResource.getIdentifier().stream().filter(i -> NAMING_SYSTEM_TASK_IDENTIFIER.equals(i.getSystem())) + .findFirst().filter(Identifier::hasValueElement).map(Identifier::getValueElement) + .filter(StringType::hasValue).isEmpty()) { - errors.add("Task.identifier[" + NAMING_SYSTEM_TASK_IDENTIFIER + "] missing"); + errors.add("Task.identifier[" + NAMING_SYSTEM_TASK_IDENTIFIER + "] value not defined"); } if (newResource.hasRequester()) @@ -399,6 +422,49 @@ private Stream getMessageNames(Task newResource) .filter(s -> !s.isBlank()); } + /** + * A draft {@link Task} with identifier (system {@link #NAMING_SYSTEM_TASK_IDENTIFIER}) may not exist + * + * @param connection + * not null + * @param newResource + * not null + * @return true if the given draft Task is unique + */ + private boolean draftTaskExists(Connection connection, Task newResource) + { + SearchQuery query = getDao().createSearchQueryWithoutUserFilter(PageAndCount.exists()) + .configureParameters(Map.of("identifier", + List.of(NAMING_SYSTEM_TASK_IDENTIFIER + "|" + getDraftTaskIdentifierValue(newResource)))); + + List uQp = query.getUnsupportedQueryParameters(); + if (!uQp.isEmpty()) + { + logger.warn("Unable to search for Task: Unsupported query parameters: {}", uQp); + + throw new IllegalStateException("Unable to search for Task: Unsupported query parameters"); + } + + try + { + PartialResult result = getDao().searchWithTransaction(connection, query); + return result.getTotal() >= 1; + } + catch (SQLException e) + { + logger.debug("Unable to search for Task", e); + logger.warn("Unable to search for Task: {} - {}", e.getClass().getName(), e.getMessage()); + + throw new RuntimeException("Unable to search for Task", e); + } + } + + private String getDraftTaskIdentifierValue(Task newResource) + { + return newResource.getIdentifier().stream().filter(i -> NAMING_SYSTEM_TASK_IDENTIFIER.equals(i.getSystem())) + .findFirst().map(Identifier::getValue).get(); + } + private boolean taskAllowedForRequesterAndRecipient(Connection connection, Identity requester, Task newResource) { Optional recipientOpt = organizationProvider.getLocalOrganizationAsIdentity(); @@ -549,24 +615,24 @@ public Optional reasonReadAllowed(Connection connection, Identity identi if (identity.hasDsfRole(FhirServerRole.READ)) { - if (isCurrentIdentityPartOfReferencedOrganization(connection, identity, "Task.requester", - existingResource.getRequester())) + if (identity.isLocalIdentity() && isCurrentIdentityPartOfReferencedOrganization(connection, identity, + "Task.restriction.recipient", existingResource.getRestriction().getRecipientFirstRep())) { logger.info( - "Read of Task/{}/_history/{} authorized for identity '{}', Task.requester reference could be resolved and current identity part of referenced organization", + "Read of Task/{}/_history/{} authorized for identity '{}', Task.restriction.recipient reference could be resolved and current identity part of referenced organization", resourceId, resourceVersion, identity.getName()); - return Optional.of("Task.requester resolved and identity part of referenced organization"); + return Optional + .of("Task.restriction.recipient resolved and local identity part of referenced organization"); } - else if (identity.isLocalIdentity() && isCurrentIdentityPartOfReferencedOrganization(connection, identity, - "Task.restriction.recipient", existingResource.getRestriction().getRecipientFirstRep())) + else if (isCurrentIdentityPartOfReferencedOrganization(connection, identity, "Task.requester", + existingResource.getRequester())) { logger.info( - "Read of Task/{}/_history/{} authorized for identity '{}', Task.restriction.recipient reference could be resolved and current identity part of referenced organization", + "Read of Task/{}/_history/{} authorized for identity '{}', Task.requester reference could be resolved and current identity part of referenced organization", resourceId, resourceVersion, identity.getName()); - return Optional - .of("Task.restriction.recipient resolved and local identity part of referenced organization"); + return Optional.of("Task.requester resolved and identity part of referenced organization"); } else { @@ -605,11 +671,23 @@ public Optional reasonUpdateAllowed(Connection connection, Identity iden Optional errors = draftTaskOk(connection, identity, newResource); if (errors.isEmpty()) { - logger.info("Update of Task/{}/_history/{} ({} -> {}) authorized for local identity '{}'", - oldResourceId, oldResourceVersion, TaskStatus.DRAFT.toCode(), TaskStatus.DRAFT.toCode(), - identity.getName()); + if (draftTaskIdentifierSame(oldResource, newResource)) + { + logger.info("Update of Task/{}/_history/{} ({} -> {}) authorized for local identity '{}'", + oldResourceId, oldResourceVersion, TaskStatus.DRAFT.toCode(), + TaskStatus.DRAFT.toCode(), identity.getName()); - return Optional.of("Local identity, old Task.status draft, new Task.status draft"); + return Optional.of("Local identity, old Task.status draft, new Task.status draft"); + } + else + { + logger.warn( + "Update of Task/{}/_history/{} ({} -> {}) unauthorized for identity '{}' - identifier modified", + oldResourceId, oldResourceVersion, TaskStatus.DRAFT.toCode(), + TaskStatus.DRAFT.toCode(), identity.getName()); + + return Optional.empty(); + } } else { @@ -790,6 +868,11 @@ else if (TaskStatus.INPROGRESS.equals(oldResource.getStatus()) } } + private boolean draftTaskIdentifierSame(Task oldResource, Task newResource) + { + return Objects.equals(getDraftTaskIdentifierValue(oldResource), getDraftTaskIdentifierValue(newResource)); + } + private Optional reasonNotSame(Task oldResource, Task newResource) { List errors = new ArrayList<>(); 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 91995f0d3..33b020d94 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 @@ -16,7 +16,7 @@ import jakarta.ws.rs.core.Response.Status; import jakarta.ws.rs.core.Response.Status.Family; -class AbstractCommandList +abstract class AbstractCommandList { private static final Logger audit = LoggerFactory.getLogger("dsf-audit-logger"); @@ -78,22 +78,22 @@ protected void auditLogAbbort(Command command) { if (command instanceof DeleteCommand) { - audit.info("Delete of {} for identity '{}' via bundle at index {} abborted", command.getResourceTypeName(), + audit.info("Delete of {} for identity '{}' via bundle at index {} aborted", command.getResourceTypeName(), command.getIdentity().getName(), command.getIndex()); } else if (command instanceof CreateCommand) { - audit.info("Create of {} for identity '{}' via bundle at index {} abborted", command.getResourceTypeName(), + audit.info("Create of {} for identity '{}' via bundle at index {} aborted", command.getResourceTypeName(), command.getIdentity().getName(), command.getIndex()); } else if (command instanceof UpdateCommand) { - audit.info("Update of {} for identity '{}' via bundle at index {} abborted", command.getResourceTypeName(), + audit.info("Update of {} for identity '{}' via bundle at index {} aborted", command.getResourceTypeName(), command.getIdentity().getName(), command.getIndex()); } else if (command instanceof ReadCommand r) { - audit.info("{} of {} for identity '{}' via bundle at index {} abborted", r.isSearch() ? "Search" : "Read", + audit.info("{} of {} for identity '{}' via bundle at index {} aborted", r.isSearch() ? "Search" : "Read", command.getResourceTypeName(), command.getIdentity().getName(), command.getIndex()); } } @@ -119,7 +119,7 @@ protected BundleEntryComponent toEntry(Exception exception) if (!(exception instanceof WebApplicationException w) || !(w.getResponse().getEntity() instanceof OperationOutcome)) { - exception = exceptionHandler.internalServerErrorBundleBatch(exception); + exception = internalServerError(exception); } Response httpResponse = ((WebApplicationException) exception).getResponse(); @@ -129,4 +129,6 @@ protected BundleEntryComponent toEntry(Exception exception) return entry; } + + protected abstract Exception internalServerError(Exception exception); } diff --git a/dsf-fhir/dsf-fhir-server/src/main/java/dev/dsf/fhir/dao/command/AuthorizationHelper.java b/dsf-fhir/dsf-fhir-server/src/main/java/dev/dsf/fhir/dao/command/AuthorizationHelper.java index ef7f88803..4aea8d870 100644 --- a/dsf-fhir/dsf-fhir-server/src/main/java/dev/dsf/fhir/dao/command/AuthorizationHelper.java +++ b/dsf-fhir/dsf-fhir-server/src/main/java/dev/dsf/fhir/dao/command/AuthorizationHelper.java @@ -6,19 +6,23 @@ import org.hl7.fhir.r4.model.Resource; import dev.dsf.common.auth.conf.Identity; +import jakarta.ws.rs.WebApplicationException; public interface AuthorizationHelper { - void checkCreateAllowed(int index, Connection connection, Identity identity, Resource newResource); + void checkCreateAllowed(int index, Connection connection, Identity identity, Resource newResource) + throws WebApplicationException; - void checkReadAllowed(int index, Connection connection, Identity identity, Resource existingResource); + void checkReadAllowed(int index, Connection connection, Identity identity, Resource existingResource) + throws WebApplicationException; void checkUpdateAllowed(int index, Connection connection, Identity identity, Resource oldResource, - Resource newResource); + Resource newResource) throws WebApplicationException; - void checkDeleteAllowed(int index, Connection connection, Identity identity, Resource oldResource); + void checkDeleteAllowed(int index, Connection connection, Identity identity, Resource oldResource) + throws WebApplicationException; - void checkSearchAllowed(int index, Identity identity, String resourceTypeName); + void checkSearchAllowed(int index, Identity identity, String resourceTypeName) throws WebApplicationException; void filterIncludeResults(int index, Connection connection, Identity identity, Bundle multipleResult); } diff --git a/dsf-fhir/dsf-fhir-server/src/main/java/dev/dsf/fhir/dao/command/AuthorizationHelperImpl.java b/dsf-fhir/dsf-fhir-server/src/main/java/dev/dsf/fhir/dao/command/AuthorizationHelperImpl.java index 975001d42..253678626 100644 --- a/dsf-fhir/dsf-fhir-server/src/main/java/dev/dsf/fhir/dao/command/AuthorizationHelperImpl.java +++ b/dsf-fhir/dsf-fhir-server/src/main/java/dev/dsf/fhir/dao/command/AuthorizationHelperImpl.java @@ -54,6 +54,7 @@ private WebApplicationException forbidden(String operation, Identity identity) t @Override public void checkCreateAllowed(int index, Connection connection, Identity identity, Resource newResource) + throws WebApplicationException { final String resourceTypeName = getResourceTypeName(newResource); @@ -77,6 +78,7 @@ private String getResourceTypeName(Resource resource) @Override public void checkReadAllowed(int index, Connection connection, Identity identity, Resource existingResource) + throws WebApplicationException { final String resourceTypeName = getResourceTypeName(existingResource); final String resourceId = existingResource.getIdElement().getIdPart(); @@ -98,7 +100,7 @@ public void checkReadAllowed(int index, Connection connection, Identity identity @Override public void checkUpdateAllowed(int index, Connection connection, Identity identity, Resource oldResource, - Resource newResource) + Resource newResource) throws WebApplicationException { final String resourceTypeName = getResourceTypeName(oldResource); final String resourceId = oldResource.getIdElement().getIdPart(); @@ -121,13 +123,14 @@ public void checkUpdateAllowed(int index, Connection connection, Identity identi @Override public void checkDeleteAllowed(int index, Connection connection, Identity identity, Resource oldResource) + throws WebApplicationException { final String resourceTypeName = getResourceTypeName(oldResource); final String resourceId = oldResource.getIdElement().getIdPart(); final long resourceVersion = oldResource.getIdElement().getVersionIdPartAsLong(); Optional> optRule = getAuthorizationRule(oldResource.getClass()); - optRule.flatMap(rule -> rule.reasonDeleteAllowed(identity, oldResource)).ifPresentOrElse(reason -> + optRule.flatMap(rule -> rule.reasonDeleteAllowed(connection, identity, oldResource)).ifPresentOrElse(reason -> { audit.info("Delete of {}/{}/_history/{} allowed for identity '{}' via bundle at index {}, reason: {}", resourceTypeName, resourceId, resourceVersion, identity.getName(), index, reason); @@ -140,7 +143,7 @@ public void checkDeleteAllowed(int index, Connection connection, Identity identi } @Override - public void checkSearchAllowed(int index, Identity identity, String resourceTypeName) + public void checkSearchAllowed(int index, Identity identity, String resourceTypeName) throws WebApplicationException { Optional> optRule = getAuthorizationRule(resourceTypeName); optRule.flatMap(rule -> rule.reasonSearchAllowed(identity)).ifPresentOrElse(reason -> 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 c790154c3..6bcb2588a 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 @@ -16,11 +16,14 @@ 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; +import org.postgresql.util.PSQLState; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import dev.dsf.fhir.event.EventHandler; import dev.dsf.fhir.help.ExceptionHandler; +import dev.dsf.fhir.help.ResponseGenerator; import dev.dsf.fhir.validation.SnapshotGenerator; import jakarta.ws.rs.WebApplicationException; @@ -31,15 +34,18 @@ public class BatchCommandList extends AbstractCommandList implements CommandList private final ValidationHelper validationHelper; private final SnapshotGenerator snapshotGenerator; private final EventHandler eventHandler; + private final ResponseGenerator responseGenerator; public BatchCommandList(DataSource dataSource, ExceptionHandler exceptionHandler, List commands, - ValidationHelper validationHelper, SnapshotGenerator snapshotGenerator, EventHandler eventHandler) + ValidationHelper validationHelper, SnapshotGenerator snapshotGenerator, EventHandler eventHandler, + ResponseGenerator responseGenerator) { super(dataSource, exceptionHandler, commands); this.validationHelper = validationHelper; this.snapshotGenerator = snapshotGenerator; this.eventHandler = eventHandler; + this.responseGenerator = responseGenerator; } @Override @@ -50,23 +56,15 @@ public Bundle execute() throws WebApplicationException boolean initialReadOnly = connection.isReadOnly(); boolean initialAutoCommit = connection.getAutoCommit(); int initialTransactionIsolationLevel = connection.getTransactionIsolation(); - logger.debug( - "Running batch with DB connection setting: read-only {}, auto-commit {}, transaction-isolation-level {}", - initialReadOnly, initialAutoCommit, - getTransactionIsolationLevelString(initialTransactionIsolationLevel)); Map caughtExceptions = new HashMap<>((int) (commands.size() / 0.75) + 1); Map idTranslationTable = new HashMap<>(); if (hasModifyingCommands) { - logger.debug( - "Elevating DB connection setting to: read-only {}, auto-commit {}, transaction-isolation-level {}", - false, false, getTransactionIsolationLevelString(Connection.TRANSACTION_REPEATABLE_READ)); - connection.setReadOnly(false); connection.setAutoCommit(false); - connection.setTransactionIsolation(Connection.TRANSACTION_REPEATABLE_READ); + connection.setTransactionIsolation(Connection.TRANSACTION_READ_COMMITTED); } commands.forEach(preExecute(idTranslationTable, connection, caughtExceptions)); @@ -75,11 +73,6 @@ public Bundle execute() throws WebApplicationException if (hasModifyingCommands) { - logger.debug( - "Reseting DB connection setting to: read-only {}, auto-commit {}, transaction-isolation-level {}", - initialReadOnly, initialAutoCommit, - getTransactionIsolationLevelString(initialTransactionIsolationLevel)); - connection.setReadOnly(initialReadOnly); connection.setAutoCommit(initialAutoCommit); connection.setTransactionIsolation(initialTransactionIsolationLevel); @@ -110,20 +103,6 @@ public Bundle execute() throws WebApplicationException } } - private String getTransactionIsolationLevelString(int level) - { - return switch (level) - { - case Connection.TRANSACTION_NONE -> "NONE"; - case Connection.TRANSACTION_READ_UNCOMMITTED -> "READ_UNCOMMITTED"; - case Connection.TRANSACTION_READ_COMMITTED -> "READ_COMMITTED"; - case Connection.TRANSACTION_REPEATABLE_READ -> "REPEATABLE_READ"; - case Connection.TRANSACTION_SERIALIZABLE -> "SERIALIZABLE"; - - default -> "?"; - }; - } - private Consumer preExecute(Map idTranslationTable, Connection connection, Map caughtExceptions) { @@ -188,7 +167,12 @@ private Consumer execute(Map idTranslationTable, Connec logger.warn("Error while executing command {}, rolling back transaction for entry at index {}: {} - {}", command.getClass().getName(), command.getIndex(), e.getClass().getName(), e.getMessage()); - caughtExceptions.put(command.getIndex(), e); + if (e instanceof PSQLException s && PSQLState.UNIQUE_VIOLATION.getState().equals(s.getSQLState())) + caughtExceptions.put(command.getIndex(), + new WebApplicationException(responseGenerator.duplicateResourceExists())); + else + caughtExceptions.put(command.getIndex(), e); + try { @@ -244,4 +228,10 @@ private Consumer postExecute(Connection connection, Map idTranslationTable, Connection connection, ValidationHelper validationHelper, SnapshotGenerator snapshotGenerator) - { } 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 a99afcc9a..70de17320 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 @@ -210,10 +210,10 @@ public CommandList createCommands(Bundle bundle, Identity identity, PreferReturn return switch (bundle.getType()) { case BATCH -> new BatchCommandList(dataSource, exceptionHandler, commands, validationHelper, - snapshotGenerator, eventHandler); + snapshotGenerator, eventHandler, responseGenerator); - case TRANSACTION -> - new TransactionCommandList(dataSource, exceptionHandler, commands, transactionResourcesFactory); + case TRANSACTION -> new TransactionCommandList(dataSource, exceptionHandler, commands, + transactionResourcesFactory, responseGenerator); default -> throw new BadBundleException("Unsupported bundle type " + bundle.getType()); }; diff --git a/dsf-fhir/dsf-fhir-server/src/main/java/dev/dsf/fhir/dao/command/ReadCommand.java b/dsf-fhir/dsf-fhir-server/src/main/java/dev/dsf/fhir/dao/command/ReadCommand.java index e23704f73..04b14e9ab 100755 --- a/dsf-fhir/dsf-fhir-server/src/main/java/dev/dsf/fhir/dao/command/ReadCommand.java +++ b/dsf-fhir/dsf-fhir-server/src/main/java/dev/dsf/fhir/dao/command/ReadCommand.java @@ -82,12 +82,6 @@ public ReadCommand(int index, Identity identity, PreferReturnType returnType, Bu this.handlingType = handlingType; } - @Override - public void preExecute(Map idTranslationTable, Connection connection, - ValidationHelper validationHelper, SnapshotGenerator snapshotGenerator) - { - } - @Override public void execute(Map idTranslationTable, Connection connection, ValidationHelper validationHelper, SnapshotGenerator snapshotGenerator) 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 be6894026..c92b1164e 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 @@ -1,6 +1,7 @@ package dev.dsf.fhir.dao.command; import java.sql.Connection; +import java.sql.SQLException; import java.util.Collections; import java.util.Comparator; import java.util.HashMap; @@ -16,10 +17,13 @@ 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; +import org.postgresql.util.PSQLState; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import dev.dsf.fhir.help.ExceptionHandler; +import dev.dsf.fhir.help.ResponseGenerator; import dev.dsf.fhir.validation.SnapshotGenerator; import jakarta.ws.rs.WebApplicationException; import jakarta.ws.rs.core.Response; @@ -30,13 +34,16 @@ public class TransactionCommandList extends AbstractCommandList implements Comma private static final Logger logger = LoggerFactory.getLogger(TransactionCommandList.class); private final Function transactionResourceFactory; + private final ResponseGenerator responseGenerator; public TransactionCommandList(DataSource dataSource, ExceptionHandler exceptionHandler, - List commands, Function transactionResourceFactory) + List commands, Function transactionResourceFactory, + ResponseGenerator responseGenerator) { super(dataSource, exceptionHandler, commands); this.transactionResourceFactory = transactionResourceFactory; + this.responseGenerator = responseGenerator; Collections.sort(this.commands, Comparator.comparing(Command::getTransactionPriority).thenComparing(Command::getIndex)); @@ -55,7 +62,7 @@ public Bundle execute() throws WebApplicationException { connection.setReadOnly(false); connection.setAutoCommit(false); - connection.setTransactionIsolation(Connection.TRANSACTION_REPEATABLE_READ); + connection.setTransactionIsolation(Connection.TRANSACTION_READ_COMMITTED); } TransactionResources transactionResources = transactionResourceFactory.apply(connection); @@ -75,10 +82,10 @@ public Bundle execute() throws WebApplicationException catch (Exception e) { logger.debug( - "Error while running pre-execute of command {} for entry at index {}, abborting transaction", + "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 {}, abborting transaction: {} - {}", + "Error while running pre-execute of command {} for entry at index {}, aborting transaction: {} - {}", c.getClass().getSimpleName(), c.getIndex(), e.getClass().getName(), e.getMessage()); try @@ -119,6 +126,10 @@ public Bundle execute() throws WebApplicationException connection.rollback(); } + 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); @@ -177,8 +188,20 @@ public Bundle execute() throws WebApplicationException if (hasModifyingCommands) { - logger.debug("Committing DB transaction"); - connection.commit(); + try + { + logger.debug("Committing DB transaction"); + connection.commit(); + } + catch (SQLException e) + { + connection.rollback(); + + if (PSQLState.UNIQUE_VIOLATION.getState().equals(e.getSQLState())) + throw new WebApplicationException(responseGenerator.duplicateResourceExists()); + else + throw e; + } } } @@ -228,4 +251,10 @@ public Bundle execute() throws WebApplicationException throw exceptionHandler.internalServerErrorBundleTransaction(e); } } + + @Override + protected Exception internalServerError(Exception exception) + { + return exceptionHandler.internalServerErrorBundleTransaction(exception); + } } diff --git a/dsf-fhir/dsf-fhir-server/src/main/java/dev/dsf/fhir/dao/jdbc/AbstractResourceDaoJdbc.java b/dsf-fhir/dsf-fhir-server/src/main/java/dev/dsf/fhir/dao/jdbc/AbstractResourceDaoJdbc.java index 196bea629..51473b675 100755 --- a/dsf-fhir/dsf-fhir-server/src/main/java/dev/dsf/fhir/dao/jdbc/AbstractResourceDaoJdbc.java +++ b/dsf-fhir/dsf-fhir-server/src/main/java/dev/dsf/fhir/dao/jdbc/AbstractResourceDaoJdbc.java @@ -252,7 +252,7 @@ public Connection newReadWriteTransaction() throws SQLException { Connection connection = dataSource.getConnection(); connection.setReadOnly(false); - connection.setTransactionIsolation(Connection.TRANSACTION_REPEATABLE_READ); + connection.setTransactionIsolation(Connection.TRANSACTION_READ_COMMITTED); connection.setAutoCommit(false); return connection; @@ -565,12 +565,8 @@ public final R update(R resource, Long expectedVersion) Objects.requireNonNull(resource, "resource"); // expectedVersion may be null - try (Connection connection = dataSource.getConnection()) + try (Connection connection = newReadWriteTransaction()) { - connection.setReadOnly(false); - connection.setTransactionIsolation(Connection.TRANSACTION_REPEATABLE_READ); - connection.setAutoCommit(false); - try { R updatedResource = updateWithTransaction(connection, resource, expectedVersion); @@ -596,9 +592,8 @@ public R updateWithTransaction(Connection connection, R resource, Long expectedV // expectedVersion may be null if (connection.isReadOnly()) throw new IllegalArgumentException("Connection is read-only"); - if (connection.getTransactionIsolation() != Connection.TRANSACTION_REPEATABLE_READ - && connection.getTransactionIsolation() != Connection.TRANSACTION_SERIALIZABLE) - throw new IllegalArgumentException("Connection transaction isolation not REPEATABLE_READ or SERIALIZABLE"); + if (connection.getTransactionIsolation() != Connection.TRANSACTION_READ_COMMITTED) + throw new IllegalArgumentException("Connection transaction isolation not READ_COMMITTED"); if (connection.getAutoCommit()) throw new IllegalArgumentException("Connection transaction is in auto commit mode"); diff --git a/dsf-fhir/dsf-fhir-server/src/main/java/dev/dsf/fhir/help/ResponseGenerator.java b/dsf-fhir/dsf-fhir-server/src/main/java/dev/dsf/fhir/help/ResponseGenerator.java index 33eeaf8b0..c05969f94 100755 --- a/dsf-fhir/dsf-fhir-server/src/main/java/dev/dsf/fhir/help/ResponseGenerator.java +++ b/dsf-fhir/dsf-fhir-server/src/main/java/dev/dsf/fhir/help/ResponseGenerator.java @@ -381,6 +381,23 @@ public Response multipleExists(String resourceTypeName, String ifNoneExistsHeade return Response.status(Status.PRECONDITION_FAILED).entity(outcome).build(); } + public Response duplicateResourceExists() + { + logger.warn("Duplicate resources exists"); + + OperationOutcome outcome = createOutcome(IssueSeverity.ERROR, IssueType.DUPLICATE, "Duplicate resources exist"); + return Response.status(Status.FORBIDDEN).entity(outcome).build(); + } + + public Response duplicateResourceExists(String resourceTypeName) + { + logger.warn("Duplicate {} resources exists", resourceTypeName); + + OperationOutcome outcome = createOutcome(IssueSeverity.ERROR, IssueType.DUPLICATE, + "Duplicate " + resourceTypeName + " resources exist"); + return Response.status(Status.FORBIDDEN).entity(outcome).build(); + } + public Response badIfNoneExistHeaderValue(String logMessageReason, String ifNoneExistsHeaderValue) { logger.warn("Bad If-None-Exist header value: {}", logMessageReason); diff --git a/dsf-fhir/dsf-fhir-server/src/main/java/dev/dsf/fhir/webservice/impl/AbstractResourceServiceImpl.java b/dsf-fhir/dsf-fhir-server/src/main/java/dev/dsf/fhir/webservice/impl/AbstractResourceServiceImpl.java index a97e8c735..335967e02 100755 --- a/dsf-fhir/dsf-fhir-server/src/main/java/dev/dsf/fhir/webservice/impl/AbstractResourceServiceImpl.java +++ b/dsf-fhir/dsf-fhir-server/src/main/java/dev/dsf/fhir/webservice/impl/AbstractResourceServiceImpl.java @@ -27,6 +27,7 @@ import org.hl7.fhir.r4.model.IdType; import org.hl7.fhir.r4.model.OperationOutcome; import org.hl7.fhir.r4.model.Resource; +import org.postgresql.util.PSQLState; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.InitializingBean; @@ -162,7 +163,16 @@ public Response create(R resource, UriInfo uri, HttpHeaders headers) return created; } - catch (SQLException | WebApplicationException e) + catch (SQLException e) + { + connection.rollback(); + + if (PSQLState.UNIQUE_VIOLATION.getState().equals(e.getSQLState())) + throw new WebApplicationException(responseGenerator.duplicateResourceExists(resourceTypeName)); + else + throw e; + } + catch (WebApplicationException e) { connection.rollback(); throw e; @@ -513,7 +523,7 @@ public Response update(String id, R resource, UriInfo uri, HttpHeaders headers) { resolveLogicalReferences(resource, connection); - R updated = dao.update(resource, ifMatch.orElse(null)); + R updated = dao.updateWithTransaction(connection, resource, ifMatch.orElse(null)); checkReferences(resource, connection, ref -> checkReferenceAfterUpdate(updated, ref)); @@ -521,7 +531,16 @@ public Response update(String id, R resource, UriInfo uri, HttpHeaders headers) return updated; } - catch (SQLException | WebApplicationException e) + catch (SQLException e) + { + if (PSQLState.UNIQUE_VIOLATION.getState().equals(e.getSQLState())) + throw new WebApplicationException( + responseGenerator.duplicateResourceExists(resourceTypeName)); + + connection.rollback(); + throw e; + } + catch (WebApplicationException e) { connection.rollback(); throw e; diff --git a/dsf-fhir/dsf-fhir-server/src/main/resources/db/db.changelog.xml b/dsf-fhir/dsf-fhir-server/src/main/resources/db/db.changelog.xml index 90e0f7e40..d91572e60 100644 --- a/dsf-fhir/dsf-fhir-server/src/main/resources/db/db.changelog.xml +++ b/dsf-fhir/dsf-fhir-server/src/main/resources/db/db.changelog.xml @@ -40,4 +40,7 @@ + + + diff --git a/dsf-fhir/dsf-fhir-server/src/main/resources/db/db.constraint_trigger.changelog-1.6.1.xml b/dsf-fhir/dsf-fhir-server/src/main/resources/db/db.constraint_trigger.changelog-1.6.1.xml new file mode 100644 index 000000000..0ba9ae4eb --- /dev/null +++ b/dsf-fhir/dsf-fhir-server/src/main/resources/db/db.constraint_trigger.changelog-1.6.1.xml @@ -0,0 +1,54 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + CREATE CONSTRAINT TRIGGER activity_definitions_unique AFTER INSERT ON activity_definitions FOR EACH ROW EXECUTE PROCEDURE activity_definitions_unique(); + CREATE CONSTRAINT TRIGGER code_systems_unique AFTER INSERT ON code_systems FOR EACH ROW EXECUTE PROCEDURE code_systems_unique(); + CREATE CONSTRAINT TRIGGER endpoints_unique AFTER INSERT ON endpoints FOR EACH ROW EXECUTE PROCEDURE endpoints_unique(); + CREATE CONSTRAINT TRIGGER naming_systems_unique AFTER INSERT ON naming_systems FOR EACH ROW EXECUTE PROCEDURE naming_systems_unique(); + CREATE CONSTRAINT TRIGGER organizations_unique AFTER INSERT ON organizations FOR EACH ROW EXECUTE PROCEDURE organizations_unique(); + CREATE CONSTRAINT TRIGGER organization_affiliations_unique AFTER INSERT ON organization_affiliations FOR EACH ROW EXECUTE PROCEDURE organization_affiliations_unique(); + CREATE CONSTRAINT TRIGGER structure_definitions_unique AFTER INSERT ON structure_definitions FOR EACH ROW EXECUTE PROCEDURE structure_definitions_unique(); + CREATE CONSTRAINT TRIGGER subscriptions_unique AFTER INSERT ON subscriptions FOR EACH ROW EXECUTE PROCEDURE subscriptions_unique(); + CREATE CONSTRAINT TRIGGER tasks_unique AFTER INSERT ON tasks FOR EACH ROW EXECUTE PROCEDURE tasks_unique(); + CREATE CONSTRAINT TRIGGER value_sets_unique AFTER INSERT ON value_sets FOR EACH ROW EXECUTE PROCEDURE value_sets_unique(); + + + \ No newline at end of file diff --git a/dsf-fhir/dsf-fhir-server/src/main/resources/db/db.delete_duplicate_resources.changelog-1.6.1.xml b/dsf-fhir/dsf-fhir-server/src/main/resources/db/db.delete_duplicate_resources.changelog-1.6.1.xml new file mode 100644 index 000000000..6a15cf8a6 --- /dev/null +++ b/dsf-fhir/dsf-fhir-server/src/main/resources/db/db.delete_duplicate_resources.changelog-1.6.1.xml @@ -0,0 +1,274 @@ + + + + + + + + + DELETE FROM activity_definitions WHERE activity_definition_id IN ( + SELECT activity_definition_id FROM ( + SELECT + row_number() OVER ( + PARTITION BY activity_definition->>'url', activity_definition->>'version' + ORDER BY activity_definition->>'url', activity_definition->>'version', (activity_definition->'meta'->>'lastUpdated')::timestamp DESC + ) AS rn + , activity_definition_id + FROM current_activity_definitions + ) AS ad WHERE rn > 1 + ) + + + + + DELETE FROM code_systems + WHERE code_system->'url' IS NOT NULL + AND code_system->'version' IS NOT NULL + AND code_system_id IN ( + SELECT code_system_id FROM ( + SELECT + row_number() OVER ( + PARTITION BY code_system->>'url', code_system->>'version' + ORDER BY code_system->>'url', code_system->>'version', (code_system->'meta'->>'lastUpdated')::timestamp DESC + ) AS rn + , code_system_id + FROM current_code_systems + ) AS cs WHERE rn > 1 + ) + + + + + DELETE FROM endpoints + WHERE endpoint->'address' IS NOT NULL + AND endpoint_id IN ( + SELECT endpoint_id FROM ( + SELECT + row_number() OVER ( + PARTITION BY endpoint->>'address' + ORDER BY endpoint->>'address', (endpoint->'meta'->>'lastUpdated')::timestamp DESC + ) AS rn + , endpoint_id + FROM current_endpoints + ) AS e WHERE rn > 1 + ) + + + + + DELETE FROM endpoints + WHERE jsonb_path_exists(endpoint, '$.identifier[*] ? (@.system == "http://dsf.dev/sid/endpoint-identifier").value') + AND endpoint_id IN ( + SELECT endpoint_id FROM ( + SELECT + row_number() OVER ( + PARTITION BY jsonb_path_query_array(endpoint, '$.identifier[*] ? (@.system == "http://dsf.dev/sid/endpoint-identifier").value') + ORDER BY jsonb_path_query_array(endpoint, '$.identifier[*] ? (@.system == "http://dsf.dev/sid/endpoint-identifier").value'), (endpoint->'meta'->>'lastUpdated')::timestamp DESC + ) AS rn + , endpoint_id + FROM current_endpoints + ) AS e WHERE rn > 1 + ) + + + + + DELETE FROM naming_systems + WHERE naming_system->'name' IS NOT NULL + AND naming_system_id IN ( + SELECT naming_system_id FROM ( + SELECT + row_number() OVER ( + PARTITION BY naming_system->>'name' + ORDER BY naming_system->>'name', (naming_system->'meta'->>'lastUpdated')::timestamp DESC + ) AS rn + , naming_system_id + FROM current_naming_systems + ) AS ns WHERE rn > 1 + ) + + + + + DELETE FROM naming_systems + WHERE jsonb_path_exists(naming_system, '$.uniqueId[*] ? (@.type == "other").value') + AND naming_system_id IN ( + SELECT naming_system_id FROM ( + SELECT + row_number() OVER ( + PARTITION BY jsonb_path_query_array(naming_system, '$.uniqueId[*] ? (@.type == "other").value') + ORDER BY jsonb_path_query_array(naming_system, '$.uniqueId[*] ? (@.type == "other").value'), (naming_system->'meta'->>'lastUpdated')::timestamp DESC + ) AS rn + , naming_system_id + FROM current_naming_systems + ) AS ns WHERE rn > 1 + ) + + + + + DELETE FROM organizations + WHERE jsonb_path_exists(organization, '$.meta.profile[*] ? (@ == "http://dsf.dev/fhir/StructureDefinition/organization-parent")') + AND jsonb_path_exists(organization, '$.identifier[*] ? (@.system == "http://dsf.dev/sid/organization-identifier").value') + AND organization_id IN ( + SELECT organization_id FROM ( + SELECT + row_number() OVER ( + PARTITION BY jsonb_path_query_array(organization, '$.identifier[*] ? (@.system == "http://dsf.dev/sid/organization-identifier").value') + ORDER BY jsonb_path_query_array(organization, '$.identifier[*] ? (@.system == "http://dsf.dev/sid/organization-identifier").value'), (organization->'meta'->>'lastUpdated')::timestamp DESC + ) AS rn + , organization_id + FROM current_organizations + ) AS o WHERE rn > 1 + ) + + + + + DELETE FROM organizations + WHERE jsonb_path_exists(organization, '$.meta.profile[*] ? (@ == "http://dsf.dev/fhir/StructureDefinition/organization")') + AND jsonb_path_exists(organization, '$.identifier[*] ? (@.system == "http://dsf.dev/sid/organization-identifier").value') + AND organization_id IN ( + SELECT organization_id FROM ( + SELECT + row_number() OVER ( + PARTITION BY jsonb_path_query_array(organization, '$.identifier[*] ? (@.system == "http://dsf.dev/sid/organization-identifier").value') + ORDER BY jsonb_path_query_array(organization, '$.identifier[*] ? (@.system == "http://dsf.dev/sid/organization-identifier").value'), (organization->'meta'->>'lastUpdated')::timestamp DESC + ) AS rn + , organization_id + FROM current_organizations + ) AS o WHERE rn > 1 + ) + + + + + DELETE FROM organizations + WHERE jsonb_path_exists(organization, '$.meta.profile[*] ? (@ == "http://dsf.dev/fhir/StructureDefinition/organization")') + AND jsonb_path_exists(organization, '$.extension[*] ? (@.url == "http://dsf.dev/fhir/StructureDefinition/extension-certificate-thumbprint").valueString') + AND organization_id IN ( + SELECT organization_id FROM ( + SELECT + row_number() OVER ( + PARTITION BY jsonb_path_query_array(organization, '$.extension[*] ? (@.url == "http://dsf.dev/fhir/StructureDefinition/extension-certificate-thumbprint").valueString') + ORDER BY jsonb_path_query_array(organization, '$.extension[*] ? (@.url == "http://dsf.dev/fhir/StructureDefinition/extension-certificate-thumbprint").valueString'), (organization->'meta'->>'lastUpdated')::timestamp DESC + ) AS rn + , organization_id + FROM current_organizations + ) AS o WHERE rn > 1 + ) + + + + + DELETE FROM organization_affiliations + WHERE organization_affiliation->'organization'->'reference' IS NOT NULL + AND organization_affiliation->'participatingOrganization'->'reference' IS NOT NULL + AND jsonb_path_exists(organization_affiliation, '$.endpoint[*].reference') + AND organization_affiliation_id IN ( + SELECT organization_affiliation_id FROM ( + SELECT + row_number() OVER ( + PARTITION BY organization_affiliation->'organization'->>'reference', organization_affiliation->'participatingOrganization'->>'reference', jsonb_path_query_array(organization_affiliation, '$.endpoint[*].reference') + ORDER BY organization_affiliation->'organization'->>'reference', organization_affiliation->'participatingOrganization'->>'reference', jsonb_path_query_array(organization_affiliation, '$.endpoint[*].reference'), (organization_affiliation->'meta'->>'lastUpdated')::timestamp DESC + ) AS rn + , organization_affiliation_id + FROM current_organization_affiliations + ) AS oa WHERE rn > 1 + ) + + + + + DELETE FROM organization_affiliations + WHERE organization_affiliation->'organization'->'reference' IS NOT NULL + AND organization_affiliation->'participatingOrganization'->'reference' IS NOT NULL + AND jsonb_path_exists(organization_affiliation, '$.code[*].coding[*] ? (@.system == "http://dsf.dev/fhir/CodeSystem/organization-role").code') + AND organization_affiliation_id IN ( + SELECT organization_affiliation_id FROM ( + SELECT + row_number() OVER ( + PARTITION BY organization_affiliation->'organization'->>'reference', organization_affiliation->'participatingOrganization'->>'reference', jsonb_path_query_array(organization_affiliation, '$.code[*].coding[*] ? (@.system == "http://dsf.dev/fhir/CodeSystem/organization-role").code') + ORDER BY organization_affiliation->'organization'->>'reference', organization_affiliation->'participatingOrganization'->>'reference', jsonb_path_query_array(organization_affiliation, '$.code[*].coding[*] ? (@.system == "http://dsf.dev/fhir/CodeSystem/organization-role").code'), (organization_affiliation->'meta'->>'lastUpdated')::timestamp DESC + ) AS rn + , organization_affiliation_id + FROM current_organization_affiliations + ) AS oa WHERE rn > 1 + ) + + + + + DELETE FROM structure_definition_snapshots + WHERE structure_definition_snapshot->'url' IS NOT NULL + AND structure_definition_snapshot->'version' IS NOT NULL + AND structure_definition_snapshot_id IN ( + SELECT structure_definition_snapshot_id FROM ( + SELECT + row_number() OVER ( + PARTITION BY structure_definition_snapshot->>'url', structure_definition_snapshot->>'version' + ORDER BY structure_definition_snapshot->>'url', structure_definition_snapshot->>'version', (structure_definition_snapshot->'meta'->>'lastUpdated')::timestamp DESC + ) AS rn + , structure_definition_snapshot_id + FROM current_structure_definition_snapshots + ) AS sd WHERE rn > 1 + ) + + + + + DELETE FROM structure_definitions + WHERE structure_definition->'url' IS NOT NULL + AND structure_definition->'version' IS NOT NULL + AND structure_definition_id IN ( + SELECT structure_definition_id FROM ( + SELECT + row_number() OVER ( + PARTITION BY structure_definition->>'url', structure_definition->>'version' + ORDER BY structure_definition->>'url', structure_definition->>'version', (structure_definition->'meta'->>'lastUpdated')::timestamp DESC + ) AS rn + , structure_definition_id + FROM current_structure_definitions + ) AS sd WHERE rn > 1 + ) + + + + + DELETE FROM tasks + WHERE task->>'status' = 'draft' + AND jsonb_path_exists(task, '$.identifier[*] ? (@.system == "http://dsf.dev/sid/task-identifier").value') + AND task_id IN ( + SELECT task_id FROM ( + SELECT + row_number() OVER ( + PARTITION BY jsonb_path_query_array(task, '$.identifier[*] ? (@.system == "http://dsf.dev/sid/task-identifier").value') + ORDER BY jsonb_path_query_array(task, '$.identifier[*] ? (@.system == "http://dsf.dev/sid/task-identifier").value'), (task->'meta'->>'lastUpdated')::timestamp DESC + ) AS rn + , task_id + FROM current_tasks + ) AS t WHERE rn > 1 + ) + + + + + DELETE FROM value_sets + WHERE value_set->'url' IS NOT NULL + AND value_set->'version' IS NOT NULL + AND value_set_id IN ( + SELECT value_set_id FROM ( + SELECT + row_number() OVER ( + PARTITION BY value_set->>'url', value_set->>'version' + ORDER BY value_set->>'url', value_set->>'version', (value_set->'meta'->>'lastUpdated')::timestamp DESC + ) AS rn + , value_set_id + FROM current_value_sets + ) AS vs WHERE rn > 1 + ) + + + \ No newline at end of file diff --git a/dsf-fhir/dsf-fhir-server/src/main/resources/db/unique_trigger_functions/activity_definitions_unique.sql b/dsf-fhir/dsf-fhir-server/src/main/resources/db/unique_trigger_functions/activity_definitions_unique.sql new file mode 100644 index 000000000..b0ea5bed3 --- /dev/null +++ b/dsf-fhir/dsf-fhir-server/src/main/resources/db/unique_trigger_functions/activity_definitions_unique.sql @@ -0,0 +1,13 @@ +CREATE OR REPLACE FUNCTION activity_definitions_unique() RETURNS TRIGGER AS $$ +BEGIN + PERFORM pg_advisory_xact_lock(hashtext((NEW.activity_definition->>'url') || (NEW.activity_definition->>'version'))); + IF EXISTS (SELECT 1 FROM current_activity_definitions WHERE activity_definition_id <> NEW.activity_definition_id + AND activity_definition->>'url' = NEW.activity_definition->>'url' + AND activity_definition->>'version' = NEW.activity_definition->>'version') THEN + RAISE EXCEPTION 'Conflict: Not inserting ActivityDefinition with url % and version %, resource already exists with given url and version', + NEW.activity_definition->>'url', NEW.activity_definition->>'version' USING ERRCODE = 'unique_violation'; + ELSE + RETURN NEW; + END IF; +END; +$$ LANGUAGE PLPGSQL \ No newline at end of file diff --git a/dsf-fhir/dsf-fhir-server/src/main/resources/db/unique_trigger_functions/code_systems_unique.sql b/dsf-fhir/dsf-fhir-server/src/main/resources/db/unique_trigger_functions/code_systems_unique.sql new file mode 100644 index 000000000..d04448cb8 --- /dev/null +++ b/dsf-fhir/dsf-fhir-server/src/main/resources/db/unique_trigger_functions/code_systems_unique.sql @@ -0,0 +1,13 @@ +CREATE OR REPLACE FUNCTION code_systems_unique() RETURNS TRIGGER AS $$ +BEGIN + PERFORM pg_advisory_xact_lock(hashtext((NEW.code_system->>'url') || (NEW.code_system->>'version'))); + IF EXISTS (SELECT 1 FROM current_code_systems WHERE code_system_id <> NEW.code_system_id + AND code_system->>'url' = NEW.code_system->>'url' + AND code_system->>'version' = NEW.code_system->>'version') THEN + RAISE EXCEPTION 'Conflict: Not inserting CodeSystem with url % and version %, resource already exists with given url and version', + NEW.code_system->>'url', NEW.code_system->>'version' USING ERRCODE = 'unique_violation'; + ELSE + RETURN NEW; + END IF; +END; +$$ LANGUAGE PLPGSQL \ No newline at end of file diff --git a/dsf-fhir/dsf-fhir-server/src/main/resources/db/unique_trigger_functions/endpoints_unique.sql b/dsf-fhir/dsf-fhir-server/src/main/resources/db/unique_trigger_functions/endpoints_unique.sql new file mode 100644 index 000000000..6debb6292 --- /dev/null +++ b/dsf-fhir/dsf-fhir-server/src/main/resources/db/unique_trigger_functions/endpoints_unique.sql @@ -0,0 +1,25 @@ +CREATE OR REPLACE FUNCTION endpoints_unique() RETURNS TRIGGER AS $$ +BEGIN + PERFORM pg_advisory_xact_lock(hashtext(NEW.endpoint->>'address')); + IF EXISTS (SELECT 1 FROM current_endpoints WHERE endpoint_id <> NEW.endpoint_id + AND ( + endpoint->>'address' = NEW.endpoint->>'address' + OR ( + jsonb_path_exists(NEW.endpoint, '$.identifier[*] ? (@.system == "http://dsf.dev/sid/endpoint-identifier").value') + AND + jsonb_path_query_array(endpoint, '$.identifier[*] ? (@.system == "http://dsf.dev/sid/endpoint-identifier").value') @> + jsonb_path_query_array(NEW.endpoint, '$.identifier[*] ? (@.system == "http://dsf.dev/sid/endpoint-identifier").value') + ) OR ( + jsonb_path_exists(endpoint, '$.identifier[*] ? (@.system == "http://dsf.dev/sid/endpoint-identifier").value') + AND + jsonb_path_query_array(NEW.endpoint, '$.identifier[*] ? (@.system == "http://dsf.dev/sid/endpoint-identifier").value') @> + jsonb_path_query_array(endpoint, '$.identifier[*] ? (@.system == "http://dsf.dev/sid/endpoint-identifier").value') + ) + )) THEN + RAISE EXCEPTION 'Conflict: Not inserting Endpoint with address % and identifier.value %, resource already exists with given address or identifier.value', + NEW.endpoint->>'address', jsonb_path_query_array(NEW.endpoint, '$.identifier[*] ? (@.system == "http://dsf.dev/sid/endpoint-identifier").value') USING ERRCODE = 'unique_violation'; + ELSE + RETURN NEW; + END IF; +END; +$$ LANGUAGE PLPGSQL \ No newline at end of file diff --git a/dsf-fhir/dsf-fhir-server/src/main/resources/db/unique_trigger_functions/naming_systems_unique.sql b/dsf-fhir/dsf-fhir-server/src/main/resources/db/unique_trigger_functions/naming_systems_unique.sql new file mode 100644 index 000000000..a6a504c0b --- /dev/null +++ b/dsf-fhir/dsf-fhir-server/src/main/resources/db/unique_trigger_functions/naming_systems_unique.sql @@ -0,0 +1,25 @@ +CREATE OR REPLACE FUNCTION naming_systems_unique() RETURNS TRIGGER AS $$ +BEGIN + PERFORM pg_advisory_xact_lock(hashtext(NEW.naming_system->>'name')); + IF EXISTS (SELECT 1 FROM current_naming_systems WHERE naming_system_id <> NEW.naming_system_id + AND ( + naming_system->>'name' = NEW.naming_system->>'name' + OR ( + jsonb_path_exists(NEW.naming_system, '$.uniqueId[*] ? (@.type == "other").value') + AND + jsonb_path_query_array(naming_system, '$.uniqueId[*] ? (@.type == "other").value') @> + jsonb_path_query_array(NEW.naming_system, '$.uniqueId[*] ? (@.type == "other").value') + ) OR ( + jsonb_path_exists(naming_system, '$.uniqueId[*] ? (@.type == "other").value') + AND + jsonb_path_query_array(NEW.naming_system, '$.uniqueId[*] ? (@.type == "other").value') @> + jsonb_path_query_array(naming_system, '$.uniqueId[*] ? (@.type == "other").value') + ) + )) THEN + RAISE EXCEPTION 'Conflict: Not inserting NamingSystem with name % and uniqueId.value %, resource already exists with given name or uniqueId.value', + NEW.naming_system->>'name', jsonb_path_query_array(NEW.naming_system, '$.uniqueId[*] ? (@.type == "other").value') USING ERRCODE = 'unique_violation'; + ELSE + RETURN NEW; + END IF; +END; +$$ LANGUAGE PLPGSQL \ No newline at end of file diff --git a/dsf-fhir/dsf-fhir-server/src/main/resources/db/unique_trigger_functions/organization_affiliations_unique.sql b/dsf-fhir/dsf-fhir-server/src/main/resources/db/unique_trigger_functions/organization_affiliations_unique.sql new file mode 100644 index 000000000..f3a0a2542 --- /dev/null +++ b/dsf-fhir/dsf-fhir-server/src/main/resources/db/unique_trigger_functions/organization_affiliations_unique.sql @@ -0,0 +1,38 @@ +CREATE OR REPLACE FUNCTION organization_affiliations_unique() RETURNS TRIGGER AS $$ +BEGIN + PERFORM pg_advisory_xact_lock(hashtext((NEW.organization_affiliation->'organization'->>'reference') || (NEW.organization_affiliation->'participatingOrganization'->>'reference'))); + IF EXISTS (SELECT 1 FROM current_organization_affiliations WHERE organization_affiliation_id <> NEW.organization_affiliation_id + AND organization_affiliation->'organization'->>'reference' = NEW.organization_affiliation->'organization'->>'reference' + AND organization_affiliation->'participatingOrganization'->>'reference' = NEW.organization_affiliation->'participatingOrganization'->>'reference' + AND (( + jsonb_path_exists(NEW.organization_affiliation, '$.endpoint[*].reference') + AND + jsonb_path_query_array(organization_affiliation, '$.endpoint[*].reference') @> + jsonb_path_query_array(NEW.organization_affiliation, '$.endpoint[*].reference') + ) OR ( + jsonb_path_exists(organization_affiliation, '$.endpoint[*].reference') + AND + jsonb_path_query_array(NEW.organization_affiliation, '$.endpoint[*].reference') @> + jsonb_path_query_array(organization_affiliation, '$.endpoint[*].reference') + ) OR ( + jsonb_path_exists(NEW.organization_affiliation, '$.code[*].coding[*] ? (@.system == "http://dsf.dev/fhir/CodeSystem/organization-role").code') + AND + jsonb_path_query_array(organization_affiliation, '$.code[*].coding[*] ? (@.system == "http://dsf.dev/fhir/CodeSystem/organization-role").code') @> + jsonb_path_query_array(NEW.organization_affiliation, '$.code[*].coding[*] ? (@.system == "http://dsf.dev/fhir/CodeSystem/organization-role").code') + ) OR ( + jsonb_path_exists(organization_affiliation, '$.code[*].coding[*] ? (@.system == "http://dsf.dev/fhir/CodeSystem/organization-role").code') + AND + jsonb_path_query_array(NEW.organization_affiliation, '$.code[*].coding[*] ? (@.system == "http://dsf.dev/fhir/CodeSystem/organization-role").code') @> + jsonb_path_query_array(organization_affiliation, '$.code[*].coding[*] ? (@.system == "http://dsf.dev/fhir/CodeSystem/organization-role").code') + ) + )) THEN + RAISE EXCEPTION 'Conflict: Not inserting OrganizationAffiliation with parent organization %, member organization %, endpoint % and roles %, resource already exists with parent organization, member organization and endpoint or roles', + NEW.organization_affiliation->'organization'->>'reference', + NEW.organization_affiliation->'participatingOrganization'->>'reference', + jsonb_path_query_array(NEW.organization_affiliation, '$.endpoint[*].reference'), + jsonb_path_query_array(NEW.organization_affiliation, '$.code[*].coding[*] ? (@.system == "http://dsf.dev/fhir/CodeSystem/organization-role").code') USING ERRCODE = 'unique_violation'; + ELSE + RETURN NEW; + END IF; +END; +$$ LANGUAGE PLPGSQL \ No newline at end of file diff --git a/dsf-fhir/dsf-fhir-server/src/main/resources/db/unique_trigger_functions/organizations_unique.sql b/dsf-fhir/dsf-fhir-server/src/main/resources/db/unique_trigger_functions/organizations_unique.sql new file mode 100644 index 000000000..51bb32f81 --- /dev/null +++ b/dsf-fhir/dsf-fhir-server/src/main/resources/db/unique_trigger_functions/organizations_unique.sql @@ -0,0 +1,52 @@ +CREATE OR REPLACE FUNCTION organizations_unique() RETURNS TRIGGER AS $$ +BEGIN + PERFORM pg_advisory_xact_lock(hashtext(jsonb_path_query_array(NEW.organization, '$.identifier[*] ? (@.system == "http://dsf.dev/sid/organization-identifier").value')::text)); + IF jsonb_path_exists(NEW.organization, '$.meta.profile[*] ? (@ == "http://dsf.dev/fhir/StructureDefinition/organization")') + AND EXISTS (SELECT 1 FROM current_organizations WHERE organization_id <> NEW.organization_id + AND (( + jsonb_path_exists(NEW.organization, '$.identifier[*] ? (@.system == "http://dsf.dev/sid/organization-identifier").value') + AND + jsonb_path_query_array(organization, '$.identifier[*] ? (@.system == "http://dsf.dev/sid/organization-identifier").value') @> + jsonb_path_query_array(NEW.organization, '$.identifier[*] ? (@.system == "http://dsf.dev/sid/organization-identifier").value') + ) OR ( + jsonb_path_exists(organization, '$.identifier[*] ? (@.system == "http://dsf.dev/sid/organization-identifier").value') + AND + jsonb_path_query_array(NEW.organization, '$.identifier[*] ? (@.system == "http://dsf.dev/sid/organization-identifier").value') @> + jsonb_path_query_array(organization, '$.identifier[*] ? (@.system == "http://dsf.dev/sid/organization-identifier").value') + ) OR ( + jsonb_path_exists(NEW.organization, '$.extension[*] ? (@.url == "http://dsf.dev/fhir/StructureDefinition/extension-certificate-thumbprint").valueString') + AND + jsonb_path_query_array(organization, '$.extension[*] ? (@.url == "http://dsf.dev/fhir/StructureDefinition/extension-certificate-thumbprint").valueString') @> + jsonb_path_query_array(NEW.organization, '$.extension[*] ? (@.url == "http://dsf.dev/fhir/StructureDefinition/extension-certificate-thumbprint").valueString') + ) OR ( + jsonb_path_exists(organization, '$.extension[*] ? (@.url == "http://dsf.dev/fhir/StructureDefinition/extension-certificate-thumbprint").valueString') + AND + jsonb_path_query_array(NEW.organization, '$.extension[*] ? (@.url == "http://dsf.dev/fhir/StructureDefinition/extension-certificate-thumbprint").valueString') @> + jsonb_path_query_array(organization, '$.extension[*] ? (@.url == "http://dsf.dev/fhir/StructureDefinition/extension-certificate-thumbprint").valueString') + ) + )) THEN + RAISE EXCEPTION 'Conflict: Not inserting member Organization with thumbprint % and identifier.value %, resource already exists with given thumbprint or identifier.value', + jsonb_path_query_array(NEW.organization, '$.extension[*] ? (@.url == "http://dsf.dev/fhir/StructureDefinition/extension-certificate-thumbprint").valueString'), + jsonb_path_query_array(NEW.organization, '$.identifier[*] ? (@.system == "http://dsf.dev/sid/organization-identifier").value') USING ERRCODE = 'unique_violation'; + + ELSIF jsonb_path_exists(NEW.organization, '$.meta.profile[*] ? (@ == "http://dsf.dev/fhir/StructureDefinition/organization-parent")') + AND EXISTS (SELECT 1 FROM current_organizations WHERE organization_id <> NEW.organization_id + AND (( + jsonb_path_exists(NEW.organization, '$.identifier[*] ? (@.system == "http://dsf.dev/sid/organization-identifier").value') + AND + jsonb_path_query_array(organization, '$.identifier[*] ? (@.system == "http://dsf.dev/sid/organization-identifier").value') @> + jsonb_path_query_array(NEW.organization, '$.identifier[*] ? (@.system == "http://dsf.dev/sid/organization-identifier").value') + ) OR ( + jsonb_path_exists(organization, '$.identifier[*] ? (@.system == "http://dsf.dev/sid/organization-identifier").value') + AND + jsonb_path_query_array(NEW.organization, '$.identifier[*] ? (@.system == "http://dsf.dev/sid/organization-identifier").value') @> + jsonb_path_query_array(organization, '$.identifier[*] ? (@.system == "http://dsf.dev/sid/organization-identifier").value') + ) + )) THEN + RAISE EXCEPTION 'Conflict: Not inserting parent Organization with identifier.value %, resource already exists with identifier.value', + jsonb_path_query_array(NEW.organization, '$.identifier[*] ? (@.system == "http://dsf.dev/sid/organization-identifier").value') USING ERRCODE = 'unique_violation'; + ELSE + RETURN NEW; + END IF; +END; +$$ LANGUAGE PLPGSQL \ No newline at end of file diff --git a/dsf-fhir/dsf-fhir-server/src/main/resources/db/unique_trigger_functions/structure_definitions_unique.sql b/dsf-fhir/dsf-fhir-server/src/main/resources/db/unique_trigger_functions/structure_definitions_unique.sql new file mode 100644 index 000000000..7d1ea95c1 --- /dev/null +++ b/dsf-fhir/dsf-fhir-server/src/main/resources/db/unique_trigger_functions/structure_definitions_unique.sql @@ -0,0 +1,13 @@ +CREATE OR REPLACE FUNCTION structure_definitions_unique() RETURNS TRIGGER AS $$ +BEGIN + PERFORM pg_advisory_xact_lock(hashtext((NEW.structure_definition->>'url') || (NEW.structure_definition->>'version'))); + IF EXISTS (SELECT 1 FROM current_structure_definitions WHERE structure_definition_id <> NEW.structure_definition_id + AND structure_definition->>'url' = NEW.structure_definition->>'url' + AND structure_definition->>'version' = NEW.structure_definition->>'version') THEN + RAISE EXCEPTION 'Conflict: Not inserting StructureDefinition with url % and version %, resource already exists with given url and version', + NEW.structure_definition->>'url', NEW.structure_definition->>'version' USING ERRCODE = 'unique_violation'; + ELSE + RETURN NEW; + END IF; +END; +$$ LANGUAGE PLPGSQL \ No newline at end of file diff --git a/dsf-fhir/dsf-fhir-server/src/main/resources/db/unique_trigger_functions/subscriptions_unique.sql b/dsf-fhir/dsf-fhir-server/src/main/resources/db/unique_trigger_functions/subscriptions_unique.sql new file mode 100644 index 000000000..95b391275 --- /dev/null +++ b/dsf-fhir/dsf-fhir-server/src/main/resources/db/unique_trigger_functions/subscriptions_unique.sql @@ -0,0 +1,14 @@ +CREATE OR REPLACE FUNCTION subscriptions_unique() RETURNS TRIGGER AS $$ +BEGIN + PERFORM pg_advisory_xact_lock(hashtext((NEW.subscription->>'criteria') || (NEW.subscription->'channel'->>'type') || (NEW.subscription->'channel'->>'payload'))); + IF EXISTS (SELECT 1 FROM current_subscriptions WHERE subscription_id <> NEW.subscription_id + AND subscription->>'criteria' = NEW.subscription->>'criteria' + AND subscription->'channel'->>'type' = NEW.subscription->'channel'->>'type' + AND subscription->'channel'->>'payload' = NEW.subscription->'channel'->>'payload') THEN + RAISE EXCEPTION 'Conflict: Not inserting Subscription with criteria %, channel.type % and channel.payload %, resource already exists with given criteria, channel type and channel payload', + NEW.subscription->>'criteria', NEW.subscription->'channel'->>'type', NEW.subscription->'channel'->>'payload' USING ERRCODE = 'unique_violation'; + ELSE + RETURN NEW; + END IF; +END; +$$ LANGUAGE PLPGSQL \ No newline at end of file diff --git a/dsf-fhir/dsf-fhir-server/src/main/resources/db/unique_trigger_functions/tasks_unique.sql b/dsf-fhir/dsf-fhir-server/src/main/resources/db/unique_trigger_functions/tasks_unique.sql new file mode 100644 index 000000000..564ebd92a --- /dev/null +++ b/dsf-fhir/dsf-fhir-server/src/main/resources/db/unique_trigger_functions/tasks_unique.sql @@ -0,0 +1,27 @@ +CREATE OR REPLACE FUNCTION tasks_unique() RETURNS TRIGGER AS $$ +BEGIN + IF NEW.task->>'status' = 'draft' THEN + PERFORM pg_advisory_xact_lock(hashtext(jsonb_path_query_array(NEW.task, '$.identifier[*] ? (@.system == "http://dsf.dev/sid/task-identifier").value')::text)); + IF EXISTS (SELECT 1 FROM current_tasks WHERE task_id <> NEW.task_id + AND (( + jsonb_path_exists(NEW.task, '$.identifier[*] ? (@.system == "http://dsf.dev/sid/task-identifier").value') + AND + jsonb_path_query_array(task, '$.identifier[*] ? (@.system == "http://dsf.dev/sid/task-identifier").value') @> + jsonb_path_query_array(NEW.task, '$.identifier[*] ? (@.system == "http://dsf.dev/sid/task-identifier").value') + ) OR ( + jsonb_path_exists(task, '$.identifier[*] ? (@.system == "http://dsf.dev/sid/task-identifier").value') + AND + jsonb_path_query_array(NEW.task, '$.identifier[*] ? (@.system == "http://dsf.dev/sid/task-identifier").value') @> + jsonb_path_query_array(task, '$.identifier[*] ? (@.system == "http://dsf.dev/sid/task-identifier").value') + ) + )) THEN + RAISE EXCEPTION 'Conflict: Not inserting Task with identifier.value %, resource already exists with given identifier.value', + jsonb_path_query_array(NEW.task, '$.identifier[*] ? (@.system == "http://dsf.dev/sid/task-identifier").value') USING ERRCODE = 'unique_violation'; + ELSE + RETURN NEW; + END IF; + ELSE + RETURN NEW; + END IF; +END; +$$ LANGUAGE PLPGSQL \ No newline at end of file diff --git a/dsf-fhir/dsf-fhir-server/src/main/resources/db/unique_trigger_functions/value_sets_unique.sql b/dsf-fhir/dsf-fhir-server/src/main/resources/db/unique_trigger_functions/value_sets_unique.sql new file mode 100644 index 000000000..1313cdf81 --- /dev/null +++ b/dsf-fhir/dsf-fhir-server/src/main/resources/db/unique_trigger_functions/value_sets_unique.sql @@ -0,0 +1,13 @@ +CREATE OR REPLACE FUNCTION value_sets_unique() RETURNS TRIGGER AS $$ +BEGIN + PERFORM pg_advisory_xact_lock(hashtext((NEW.value_set->>'url') || (NEW.value_set->>'version'))); + IF EXISTS (SELECT 1 FROM current_value_sets WHERE value_set_id <> NEW.value_set_id + AND value_set->>'url' = NEW.value_set->>'url' + AND value_set->>'version' = NEW.value_set->>'version') THEN + RAISE EXCEPTION 'Conflict: Not inserting ValueSet with url % and version %, resource already exists with given url and version', + NEW.value_set->>'url', NEW.value_set->>'version' USING ERRCODE = 'unique_violation'; + ELSE + RETURN NEW; + END IF; +END; +$$ LANGUAGE PLPGSQL \ No newline at end of file diff --git a/dsf-fhir/dsf-fhir-server/src/test/java/dev/dsf/fhir/integration/AbstractIntegrationTest.java b/dsf-fhir/dsf-fhir-server/src/test/java/dev/dsf/fhir/integration/AbstractIntegrationTest.java index d559e57dd..f96f19b23 100755 --- a/dsf-fhir/dsf-fhir-server/src/test/java/dev/dsf/fhir/integration/AbstractIntegrationTest.java +++ b/dsf-fhir/dsf-fhir-server/src/test/java/dev/dsf/fhir/integration/AbstractIntegrationTest.java @@ -68,6 +68,8 @@ import dev.dsf.common.auth.DsfSecurityHandler; import dev.dsf.common.auth.StatusPortAuthenticator; import dev.dsf.common.jetty.JettyServer; +import dev.dsf.fhir.authorization.process.ProcessAuthorizationHelper; +import dev.dsf.fhir.authorization.process.ProcessAuthorizationHelperImpl; import dev.dsf.fhir.authorization.read.ReadAccessHelper; import dev.dsf.fhir.authorization.read.ReadAccessHelperImpl; import dev.dsf.fhir.client.FhirWebserviceClient; @@ -110,7 +112,8 @@ public abstract class AbstractIntegrationTest extends AbstractDbTest private static final List FILES_TO_DELETE = Arrays.asList(FHIR_BUNDLE_FILE); protected static final FhirContext fhirContext = FhirContext.forR4(); - protected static final ReadAccessHelperImpl readAccessHelper = new ReadAccessHelperImpl(); + protected static final ReadAccessHelper readAccessHelper = new ReadAccessHelperImpl(); + protected static final ProcessAuthorizationHelper processAuthorizationHelper = new ProcessAuthorizationHelperImpl(); private static final ReferenceCleaner referenceCleaner = new ReferenceCleanerImpl(new ReferenceExtractorImpl()); @@ -413,6 +416,11 @@ protected static final ReadAccessHelper getReadAccessHelper() return readAccessHelper; } + protected static final ProcessAuthorizationHelper getProcessAuthorizationHelper() + { + return processAuthorizationHelper; + } + protected static void expectBadRequest(Runnable operation) throws Exception { expectWebApplicationException(operation, Status.BAD_REQUEST); diff --git a/dsf-fhir/dsf-fhir-server/src/test/java/dev/dsf/fhir/integration/ParallelCreateIntegrationTest.java b/dsf-fhir/dsf-fhir-server/src/test/java/dev/dsf/fhir/integration/ParallelCreateIntegrationTest.java new file mode 100644 index 000000000..0574bfedb --- /dev/null +++ b/dsf-fhir/dsf-fhir-server/src/test/java/dev/dsf/fhir/integration/ParallelCreateIntegrationTest.java @@ -0,0 +1,1615 @@ +package dev.dsf.fhir.integration; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertTrue; +import static org.junit.Assert.fail; + +import java.lang.Thread.UncaughtExceptionHandler; +import java.sql.SQLException; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.Date; +import java.util.List; +import java.util.UUID; +import java.util.function.BiConsumer; +import java.util.function.Predicate; + +import org.hl7.fhir.r4.model.ActivityDefinition; +import org.hl7.fhir.r4.model.Bundle; +import org.hl7.fhir.r4.model.Bundle.BundleEntryComponent; +import org.hl7.fhir.r4.model.Bundle.BundleEntryRequestComponent; +import org.hl7.fhir.r4.model.Bundle.BundleType; +import org.hl7.fhir.r4.model.Bundle.HTTPVerb; +import org.hl7.fhir.r4.model.CodeSystem; +import org.hl7.fhir.r4.model.CodeSystem.CodeSystemContentMode; +import org.hl7.fhir.r4.model.CodeableConcept; +import org.hl7.fhir.r4.model.Coding; +import org.hl7.fhir.r4.model.ElementDefinition; +import org.hl7.fhir.r4.model.Endpoint; +import org.hl7.fhir.r4.model.Endpoint.EndpointStatus; +import org.hl7.fhir.r4.model.Enumerations.PublicationStatus; +import org.hl7.fhir.r4.model.Extension; +import org.hl7.fhir.r4.model.Identifier; +import org.hl7.fhir.r4.model.NamingSystem; +import org.hl7.fhir.r4.model.NamingSystem.NamingSystemIdentifierType; +import org.hl7.fhir.r4.model.NamingSystem.NamingSystemType; +import org.hl7.fhir.r4.model.NamingSystem.NamingSystemUniqueIdComponent; +import org.hl7.fhir.r4.model.Organization; +import org.hl7.fhir.r4.model.OrganizationAffiliation; +import org.hl7.fhir.r4.model.Reference; +import org.hl7.fhir.r4.model.Resource; +import org.hl7.fhir.r4.model.StringType; +import org.hl7.fhir.r4.model.StructureDefinition; +import org.hl7.fhir.r4.model.StructureDefinition.StructureDefinitionKind; +import org.hl7.fhir.r4.model.StructureDefinition.TypeDerivationRule; +import org.hl7.fhir.r4.model.Subscription; +import org.hl7.fhir.r4.model.Subscription.SubscriptionChannelComponent; +import org.hl7.fhir.r4.model.Subscription.SubscriptionChannelType; +import org.hl7.fhir.r4.model.Subscription.SubscriptionStatus; +import org.hl7.fhir.r4.model.Task; +import org.hl7.fhir.r4.model.Task.TaskIntent; +import org.hl7.fhir.r4.model.Task.TaskStatus; +import org.hl7.fhir.r4.model.ValueSet; +import org.junit.Test; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import dev.dsf.fhir.authorization.process.Recipient; +import dev.dsf.fhir.authorization.process.Requester; +import dev.dsf.fhir.dao.ActivityDefinitionDao; +import dev.dsf.fhir.dao.CodeSystemDao; +import dev.dsf.fhir.dao.EndpointDao; +import dev.dsf.fhir.dao.NamingSystemDao; +import dev.dsf.fhir.dao.OrganizationAffiliationDao; +import dev.dsf.fhir.dao.OrganizationDao; +import dev.dsf.fhir.dao.ResourceDao; +import dev.dsf.fhir.dao.SubscriptionDao; +import dev.dsf.fhir.dao.TaskDao; +import dev.dsf.fhir.dao.ValueSetDao; +import dev.dsf.fhir.dao.jdbc.StructureDefinitionDaoJdbc; +import jakarta.ws.rs.WebApplicationException; + +public class ParallelCreateIntegrationTest extends AbstractIntegrationTest +{ + private static final Logger logger = LoggerFactory.getLogger(ParallelCreateIntegrationTest.class); + + private static final String ACTIVITY_DEFINITION_URL = "http://test.com/bpe/Process/test"; + private static final String ACTIVITY_DEFINITION_VERSION = "1.6"; + + private static final String CODE_SYSTEM_URL = "http://test.com/fhir/CodeSystem/test"; + private static final String CODE_SYSTEM_VERSION = "test-version"; + + private static final String ENDPOINT_IDENTIFIER_VALUE = "endpoint.test.org"; + private static final String ENDPOINT_ADDRESS = "https://endpoint.test.org/fhir"; + + private static final String NAMING_SYSTEM_NAME = "TestNamingSystem"; + private static final String NAMING_SYSTEM_UNIQUE_ID_VALUE = "http://dsf.dev/sid/test-identifier"; + + private static final String ORGANIZATION_IDENTIFIER_VALUE_PARENT = "parent.org"; + private static final String ORGANIZATION_IDENTIFIER_VALUE_MEMBER = "member.org"; + + private static final String STRUCTURE_DEFINITION_URL = "http://test.com/fhir/StructureDefinition/test"; + private static final String STRUCTURE_DEFINITION_VERSION = "test-version"; + + private static final String SUBSCRIPTION_CRITERIA = "Patient"; + private static final SubscriptionChannelType SUBSCRIPTION_CHANNEL_TYPE = SubscriptionChannelType.WEBSOCKET; + private static final String SUBSCRIPTION_CHANNEL_PAYLOAD = "application/fhir+json"; + + private static final String NAMING_SYSTEM_TASK_IDENTIFIER = "http://dsf.dev/sid/task-identifier"; + private static final String TASK_IDENTIFIER_VALUE = ACTIVITY_DEFINITION_URL + "/" + ACTIVITY_DEFINITION_VERSION + + "/test"; + + private static final String VALUE_SET_URL = "http://test.com/fhir/ValueSet/test"; + private static final String VALUE_SET_VERSION = "test-version"; + + private void checkReturnBatchBundle(Bundle b) + { + assertNotNull(b); + assertEquals(BundleType.BATCHRESPONSE, b.getType()); + assertEquals(2, b.getEntry().size()); + + BundleEntryComponent e0 = b.getEntry().get(0); + assertNotNull(e0); + assertTrue(e0.hasResponse()); + assertEquals("201 Created", e0.getResponse().getStatus()); + + BundleEntryComponent e1 = b.getEntry().get(1); + assertNotNull(e1); + assertTrue(e1.hasResponse()); + assertEquals("403 Forbidden", e1.getResponse().getStatus()); + } + + @Test + public void testCreateDuplicateActivityDefinitionsViaTransactionBundle() throws Exception + { + Bundle bundle = createBundle(BundleType.TRANSACTION, createActivityDefinition(), null, 2); + + expectForbidden(() -> getWebserviceClient().postBundle(bundle)); + } + + @Test + public void testCreateDuplicateActivityDefinitionsViaBatchBundle() throws Exception + { + Bundle bundle = createBundle(BundleType.BATCH, createActivityDefinition(), null, 2); + + checkReturnBatchBundle(getWebserviceClient().postBundle(bundle)); + } + + @Test + public void testCreateDuplicateCodeSystemsViaTransactionBundle() throws Exception + { + Bundle bundle = createBundle(BundleType.TRANSACTION, createCodeSystem(), null, 2); + + expectForbidden(() -> getWebserviceClient().postBundle(bundle)); + } + + @Test + public void testCreateDuplicateCodeSystemsViaBatchBundle() throws Exception + { + Bundle bundle = createBundle(BundleType.BATCH, createCodeSystem(), null, 2); + + checkReturnBatchBundle(getWebserviceClient().postBundle(bundle)); + } + + @Test + public void testCreateDuplicateEndpointsViaTransactionBundle() throws Exception + { + Bundle bundle = createBundle(BundleType.TRANSACTION, createEndpoint(), null, 2); + + expectForbidden(() -> getWebserviceClient().postBundle(bundle)); + } + + @Test + public void testCreateDuplicateEndpointsViaBatchBundle() throws Exception + { + Bundle bundle = createBundle(BundleType.BATCH, createEndpoint(), null, 2); + + checkReturnBatchBundle(getWebserviceClient().postBundle(bundle)); + } + + @Test + public void testCreateDuplicateNamingSystemsViaTransactionBundle() throws Exception + { + Bundle bundle = createBundle(BundleType.TRANSACTION, createNamingSystem(), null, 2); + + expectForbidden(() -> getWebserviceClient().postBundle(bundle)); + } + + @Test + public void testCreateDuplicateNamingSystemsViaBatchBundle() throws Exception + { + Bundle bundle = createBundle(BundleType.BATCH, createNamingSystem(), null, 2); + + checkReturnBatchBundle(getWebserviceClient().postBundle(bundle)); + } + + @Test + public void testCreateDuplicateParentOrganizationsViaTransactionBundle() throws Exception + { + Bundle bundle = createBundle(BundleType.TRANSACTION, createParentOrganization(), null, 2); + + expectForbidden(() -> getWebserviceClient().postBundle(bundle)); + } + + @Test + public void testCreateDuplicateParentOrganizationsViaBatchBundle() throws Exception + { + Bundle bundle = createBundle(BundleType.BATCH, createParentOrganization(), null, 2); + + checkReturnBatchBundle(getWebserviceClient().postBundle(bundle)); + } + + private Bundle testCreateDuplicateMemberOrganizationsViaBundle(BundleType bundleType) throws SQLException + { + EndpointDao endpointDao = getSpringWebApplicationContext().getBean(EndpointDao.class); + Endpoint endpoint = endpointDao.create(createEndpoint()); + + return createBundle(bundleType, createMemberOrganization(endpoint), null, 2); + } + + @Test + public void testCreateDuplicateMemberOrganizationsViaTransactionBundle() throws Exception + { + Bundle bundle = testCreateDuplicateMemberOrganizationsViaBundle(BundleType.TRANSACTION); + + expectForbidden(() -> getWebserviceClient().postBundle(bundle)); + } + + @Test + public void testCreateDuplicateMemberOrganizationsViaBatchBundle() throws Exception + { + Bundle bundle = testCreateDuplicateMemberOrganizationsViaBundle(BundleType.BATCH); + + checkReturnBatchBundle(getWebserviceClient().postBundle(bundle)); + } + + private Bundle testCreateDuplicateOrganizationAffiliationsSameEndpointViaBundle(BundleType bundleType) + throws SQLException + { + EndpointDao endpointDao = getSpringWebApplicationContext().getBean(EndpointDao.class); + Endpoint endpoint = endpointDao.create(createEndpoint()); + + OrganizationDao organizationDao = getSpringWebApplicationContext().getBean(OrganizationDao.class); + Organization memberOrganization = organizationDao.create(createMemberOrganization(endpoint)); + Organization parentOrganization = organizationDao.create(createParentOrganization()); + + OrganizationAffiliation a1 = createOrganizationAffiliation(parentOrganization, memberOrganization, endpoint, + List.of("DIC")); + OrganizationAffiliation a2 = createOrganizationAffiliation(parentOrganization, memberOrganization, endpoint, + List.of("COS")); + + return createBundle(bundleType, a1, a2, null); + } + + @Test + public void testCreateDuplicateOrganizationAffiliationsSameEndpointViaTransactionBundle() throws Exception + { + Bundle bundle = testCreateDuplicateOrganizationAffiliationsSameEndpointViaBundle(BundleType.TRANSACTION); + + expectForbidden(() -> getWebserviceClient().postBundle(bundle)); + } + + @Test + public void testCreateDuplicateOrganizationAffiliationsSameEndpointViaBatchBundle() throws Exception + { + Bundle bundle = testCreateDuplicateOrganizationAffiliationsSameEndpointViaBundle(BundleType.BATCH); + + checkReturnBatchBundle(getWebserviceClient().postBundle(bundle)); + } + + private Bundle testCreateDuplicateOrganizationAffiliationsSameRoleViaBundle(BundleType bundleType) + throws SQLException + { + EndpointDao endpointDao = getSpringWebApplicationContext().getBean(EndpointDao.class); + Endpoint e1 = endpointDao.create(createEndpoint(ENDPOINT_IDENTIFIER_VALUE, ENDPOINT_ADDRESS)); + Endpoint e2 = endpointDao.create(createEndpoint("endpoint2.test.org", "https://endpoint2.test.org/fhir")); + + OrganizationDao organizationDao = getSpringWebApplicationContext().getBean(OrganizationDao.class); + Organization memberOrganization = organizationDao.create(createMemberOrganization(e1, e2)); + Organization parentOrganization = organizationDao.create(createParentOrganization()); + + OrganizationAffiliation oA1 = createOrganizationAffiliation(parentOrganization, memberOrganization, e1, + List.of("DIC")); + OrganizationAffiliation oA2 = createOrganizationAffiliation(parentOrganization, memberOrganization, e2, + List.of("DIC", "COS")); + + return createBundle(bundleType, oA1, oA2, null); + } + + @Test + public void testCreateDuplicateOrganizationAffiliationsSameRoleViaTransactionBundle() throws Exception + { + Bundle bundle = testCreateDuplicateOrganizationAffiliationsSameRoleViaBundle(BundleType.TRANSACTION); + + expectForbidden(() -> getWebserviceClient().postBundle(bundle)); + } + + @Test + public void testCreateDuplicateOrganizationAffiliationsSameRoleViaBatchBundle() throws Exception + { + Bundle bundle = testCreateDuplicateOrganizationAffiliationsSameRoleViaBundle(BundleType.BATCH); + + checkReturnBatchBundle(getWebserviceClient().postBundle(bundle)); + } + + @Test + public void testCreateDuplicateStructureDefinitionsViaTransactionBundle() throws Exception + { + Bundle bundle = createBundle(BundleType.TRANSACTION, createStructureDefinition(), null, 2); + + expectForbidden(() -> getWebserviceClient().postBundle(bundle)); + } + + @Test + public void testCreateDuplicateStructureDefinitionsViaBatchBundle() throws Exception + { + Bundle bundle = createBundle(BundleType.BATCH, createStructureDefinition(), null, 2); + + checkReturnBatchBundle(getWebserviceClient().postBundle(bundle)); + } + + @Test + public void testCreateDuplicateSubscriptionsViaTransactionBundle() throws Exception + { + Bundle bundle = createBundle(BundleType.TRANSACTION, createSubscription(), null, 2); + + expectForbidden(() -> getWebserviceClient().postBundle(bundle)); + } + + @Test + public void testCreateDuplicateSubscriptionsViaBatchBundle() throws Exception + { + Bundle bundle = createBundle(BundleType.BATCH, createSubscription(), null, 2); + + checkReturnBatchBundle(getWebserviceClient().postBundle(bundle)); + } + + @Test + public void testCreateDuplicateTasksViaTransactionBundle() throws Exception + { + ActivityDefinitionDao activityDefinitionDao = getSpringWebApplicationContext() + .getBean(ActivityDefinitionDao.class); + activityDefinitionDao.create(createActivityDefinition()); + + Bundle bundle = createBundle(BundleType.TRANSACTION, createTask(), null, 2); + + expectForbidden(() -> getWebserviceClient().postBundle(bundle)); + } + + @Test + public void testCreateDuplicateTasksViaBatchBundle() throws Exception + { + ActivityDefinitionDao activityDefinitionDao = getSpringWebApplicationContext() + .getBean(ActivityDefinitionDao.class); + activityDefinitionDao.create(createActivityDefinition()); + + Bundle bundle = createBundle(BundleType.BATCH, createTask(), null, 2); + + checkReturnBatchBundle(getWebserviceClient().postBundle(bundle)); + } + + @Test + public void testCreateDuplicateValueSetsViaTransactionBundle() throws Exception + { + Bundle bundle = createBundle(BundleType.TRANSACTION, createValueSet(), null, 2); + + expectForbidden(() -> getWebserviceClient().postBundle(bundle)); + } + + @Test + public void testCreateDuplicateValueSetsViaBatchBundle() throws Exception + { + Bundle bundle = createBundle(BundleType.BATCH, createValueSet(), null, 2); + + checkReturnBatchBundle(getWebserviceClient().postBundle(bundle)); + } + + // ------------------------------------------------------------------------------------------------------------------ + + @Test + public void testCreateDuplicateActivityDefinitonsViaTransactionBundleWithIfNoneExists() throws Exception + { + Bundle bundle = createBundle(BundleType.TRANSACTION, createActivityDefinition(), + (aD, r) -> r.setIfNoneExist("url=" + aD.getUrl() + "&version=" + aD.getVersion()), 2); + + testCreateDuplicatesViaBundleWithIfNoneExists(bundle, BundleType.TRANSACTIONRESPONSE); + } + + @Test + public void testCreateDuplicateActivityDefinitonsViaBatchBundleWithIfNoneExists() throws Exception + { + Bundle bundle = createBundle(BundleType.BATCH, createActivityDefinition(), + (aD, r) -> r.setIfNoneExist("url=" + aD.getUrl() + "&version=" + aD.getVersion()), 2); + + testCreateDuplicatesViaBundleWithIfNoneExists(bundle, BundleType.BATCHRESPONSE); + } + + @Test + public void testCreateDuplicateCodeSystemsViaTransactionBundleWithIfNoneExists() throws Exception + { + Bundle bundle = createBundle(BundleType.TRANSACTION, createCodeSystem(), + (cS, r) -> r.setIfNoneExist("url=" + cS.getUrl() + "&version=" + cS.getVersion()), 2); + + testCreateDuplicatesViaBundleWithIfNoneExists(bundle, BundleType.TRANSACTIONRESPONSE); + } + + @Test + public void testCreateDuplicateCodeSystemsViaBatchBundleWithIfNoneExists() throws Exception + { + Bundle bundle = createBundle(BundleType.BATCH, createCodeSystem(), + (cS, r) -> r.setIfNoneExist("url=" + cS.getUrl() + "&version=" + cS.getVersion()), 2); + + testCreateDuplicatesViaBundleWithIfNoneExists(bundle, BundleType.BATCHRESPONSE); + } + + @Test + public void testCreateDuplicateEndpointsViaTransactionBundleWithIfNoneExists() throws Exception + { + Bundle bundle = createBundle(BundleType.TRANSACTION, createEndpoint(), (e, r) -> r.setIfNoneExist( + "identifier=" + e.getIdentifierFirstRep().getSystem() + "|" + e.getIdentifierFirstRep().getValue()), 2); + + testCreateDuplicatesViaBundleWithIfNoneExists(bundle, BundleType.TRANSACTIONRESPONSE); + } + + @Test + public void testCreateDuplicateEndpointsViaBatchBundleWithIfNoneExists() throws Exception + { + Bundle bundle = createBundle(BundleType.BATCH, createEndpoint(), (e, r) -> r.setIfNoneExist( + "identifier=" + e.getIdentifierFirstRep().getSystem() + "|" + e.getIdentifierFirstRep().getValue()), 2); + + testCreateDuplicatesViaBundleWithIfNoneExists(bundle, BundleType.BATCHRESPONSE); + } + + @Test + public void testCreateDuplicateNamingSystemsViaTransactionBundleWithIfNoneExists() throws Exception + { + Bundle bundle = createBundle(BundleType.TRANSACTION, createNamingSystem(), + (nS, r) -> r.setIfNoneExist("name=" + nS.getName()), 2); + + testCreateDuplicatesViaBundleWithIfNoneExists(bundle, BundleType.TRANSACTIONRESPONSE); + } + + @Test + public void testCreateDuplicateNamingSystemsViaBatchBundleWithIfNoneExists() throws Exception + { + Bundle bundle = createBundle(BundleType.BATCH, createNamingSystem(), + (nS, r) -> r.setIfNoneExist("name=" + nS.getName()), 2); + + testCreateDuplicatesViaBundleWithIfNoneExists(bundle, BundleType.BATCHRESPONSE); + } + + @Test + public void testCreateDuplicateParentOrganizationsViaTransactionBundleWithIfNoneExists() throws Exception + { + Bundle bundle = createBundle(BundleType.TRANSACTION, createParentOrganization(), (o, r) -> r.setIfNoneExist( + "identifier=" + o.getIdentifierFirstRep().getSystem() + "|" + o.getIdentifierFirstRep().getValue()), 2); + + testCreateDuplicatesViaBundleWithIfNoneExists(bundle, BundleType.TRANSACTIONRESPONSE); + } + + @Test + public void testCreateDuplicateParentOrganizationsViaBatchBundleWithIfNoneExists() throws Exception + { + Bundle bundle = createBundle(BundleType.BATCH, createParentOrganization(), (o, r) -> r.setIfNoneExist( + "identifier=" + o.getIdentifierFirstRep().getSystem() + "|" + o.getIdentifierFirstRep().getValue()), 2); + + testCreateDuplicatesViaBundleWithIfNoneExists(bundle, BundleType.BATCHRESPONSE); + } + + private Bundle testCreateDuplicateMemberOrganizationsViaBundleWithIfNoneExists(BundleType bundleType) + throws SQLException + { + EndpointDao endpointDao = getSpringWebApplicationContext().getBean(EndpointDao.class); + Endpoint endpoint = endpointDao.create(createEndpoint()); + + return createBundle(bundleType, createMemberOrganization(endpoint), (o, r) -> r.setIfNoneExist( + "identifier=" + o.getIdentifierFirstRep().getSystem() + "|" + o.getIdentifierFirstRep().getValue()), 2); + } + + @Test + public void testCreateDuplicateMemberOrganizationsViaTransactionBundleWithIfNoneExists() throws Exception + { + Bundle bundle = testCreateDuplicateMemberOrganizationsViaBundleWithIfNoneExists(BundleType.TRANSACTION); + + testCreateDuplicatesViaBundleWithIfNoneExists(bundle, BundleType.TRANSACTIONRESPONSE); + } + + @Test + public void testCreateDuplicateMemberOrganizationsViaBatchBundleWithIfNoneExists() throws Exception + { + Bundle bundle = testCreateDuplicateMemberOrganizationsViaBundleWithIfNoneExists(BundleType.BATCH); + + testCreateDuplicatesViaBundleWithIfNoneExists(bundle, BundleType.BATCHRESPONSE); + } + + private Bundle testCreateDuplicateOrganizationAffiliationsSameEndpointViaBundleWithIfNoneExists( + BundleType bundleType) throws SQLException + { + EndpointDao endpointDao = getSpringWebApplicationContext().getBean(EndpointDao.class); + Endpoint endpoint = endpointDao.create(createEndpoint()); + + OrganizationDao organizationDao = getSpringWebApplicationContext().getBean(OrganizationDao.class); + Organization memberOrganization = organizationDao.create(createMemberOrganization(endpoint)); + Organization parentOrganization = organizationDao.create(createParentOrganization()); + + OrganizationAffiliation a1 = createOrganizationAffiliation(parentOrganization, memberOrganization, endpoint, + List.of("DIC")); + OrganizationAffiliation a2 = createOrganizationAffiliation(parentOrganization, memberOrganization, endpoint, + List.of("COS")); + + return createBundle(bundleType, a1, a2, + (a, r) -> r.setIfNoneExist("primary-organization:identifier=http://dsf.dev/sid/organization-identifier|" + + ORGANIZATION_IDENTIFIER_VALUE_PARENT + + "&participating-organization:identifier=http://dsf.dev/sid/organization-identifier|" + + ORGANIZATION_IDENTIFIER_VALUE_MEMBER)); + } + + @Test + public void testCreateDuplicateOrganizationAffiliationsSameEndpointViaTransactionBundleWithIfNoneExists() + throws Exception + { + Bundle bundle = testCreateDuplicateOrganizationAffiliationsSameEndpointViaBundleWithIfNoneExists( + BundleType.TRANSACTION); + + testCreateDuplicatesViaBundleWithIfNoneExists(bundle, BundleType.TRANSACTIONRESPONSE); + } + + @Test + public void testCreateDuplicateOrganizationAffiliationsSameEndpointViaBatchBundleWithIfNoneExists() throws Exception + { + Bundle bundle = testCreateDuplicateOrganizationAffiliationsSameEndpointViaBundleWithIfNoneExists( + BundleType.BATCH); + + testCreateDuplicatesViaBundleWithIfNoneExists(bundle, BundleType.BATCHRESPONSE); + } + + private Bundle testCreateDuplicateOrganizationAffiliationsSameRoleViaBundleWithIfNoneExists(BundleType bundleType) + throws SQLException + { + EndpointDao endpointDao = getSpringWebApplicationContext().getBean(EndpointDao.class); + Endpoint e1 = endpointDao.create(createEndpoint(ENDPOINT_IDENTIFIER_VALUE, ENDPOINT_ADDRESS)); + Endpoint e2 = endpointDao.create(createEndpoint("endpoint2.test.org", "https://endpoint2.test.org/fhir")); + + OrganizationDao organizationDao = getSpringWebApplicationContext().getBean(OrganizationDao.class); + Organization memberOrganization = organizationDao.create(createMemberOrganization(e1, e2)); + Organization parentOrganization = organizationDao.create(createParentOrganization()); + + OrganizationAffiliation oA1 = createOrganizationAffiliation(parentOrganization, memberOrganization, e1, + List.of("DIC")); + OrganizationAffiliation oA2 = createOrganizationAffiliation(parentOrganization, memberOrganization, e2, + List.of("DIC", "COS")); + + return createBundle(bundleType, oA1, oA2, + (a, r) -> r.setIfNoneExist("primary-organization:identifier=http://dsf.dev/sid/organization-identifier|" + + ORGANIZATION_IDENTIFIER_VALUE_PARENT + + "&participating-organization:identifier=http://dsf.dev/sid/organization-identifier|" + + ORGANIZATION_IDENTIFIER_VALUE_MEMBER)); + } + + @Test + public void testCreateDuplicateOrganizationAffiliationsSameRoleViaTransactionBundleWithIfNoneExists() + throws Exception + { + Bundle bundle = testCreateDuplicateOrganizationAffiliationsSameRoleViaBundleWithIfNoneExists( + BundleType.TRANSACTION); + + testCreateDuplicatesViaBundleWithIfNoneExists(bundle, BundleType.TRANSACTIONRESPONSE); + } + + @Test + public void testCreateDuplicateOrganizationAffiliationsSameRoleViaBatchBundleWithIfNoneExists() throws Exception + { + Bundle bundle = testCreateDuplicateOrganizationAffiliationsSameRoleViaBundleWithIfNoneExists(BundleType.BATCH); + + testCreateDuplicatesViaBundleWithIfNoneExists(bundle, BundleType.BATCHRESPONSE); + } + + @Test + public void testCreateDuplicateStructureDefinitionsViaTransactionBundleWithIfNoneExists() throws Exception + { + Bundle bundle = createBundle(BundleType.TRANSACTION, createStructureDefinition(), + (sD, r) -> r.setIfNoneExist("url=" + sD.getUrl() + "&version=" + sD.getVersion()), 2); + + testCreateDuplicatesViaBundleWithIfNoneExists(bundle, BundleType.TRANSACTIONRESPONSE); + } + + @Test + public void testCreateDuplicateStructureDefinitionsViaBatchBundleWithIfNoneExists() throws Exception + { + Bundle bundle = createBundle(BundleType.BATCH, createStructureDefinition(), + (sD, r) -> r.setIfNoneExist("url=" + sD.getUrl() + "&version=" + sD.getVersion()), 2); + + testCreateDuplicatesViaBundleWithIfNoneExists(bundle, BundleType.BATCHRESPONSE); + } + + @Test + public void testCreateDuplicateSubscriptionsViaTransactionBundleWithIfNoneExists() throws Exception + { + Bundle bundle = createBundle(BundleType.TRANSACTION, createSubscription(), + (s, r) -> r + .setIfNoneExist("criteria=" + s.getCriteria() + "&type=" + s.getChannel().getType().toCode()), + 2); + + testCreateDuplicatesViaBundleWithIfNoneExists(bundle, BundleType.TRANSACTIONRESPONSE); + } + + @Test + public void testCreateDuplicateSubscriptionsViaBatchBundleWithIfNoneExists() throws Exception + { + Bundle bundle = createBundle(BundleType.BATCH, createSubscription(), + (s, r) -> r + .setIfNoneExist("criteria=" + s.getCriteria() + "&type=" + s.getChannel().getType().toCode()), + 2); + + testCreateDuplicatesViaBundleWithIfNoneExists(bundle, BundleType.BATCHRESPONSE); + } + + @Test + public void testCreateDuplicateTasksViaTransactionBundleWithIfNoneExists() throws Exception + { + ActivityDefinitionDao activityDefinitionDao = getSpringWebApplicationContext() + .getBean(ActivityDefinitionDao.class); + activityDefinitionDao.create(createActivityDefinition()); + + Bundle bundle = createBundle(BundleType.TRANSACTION, createTask(), + (t, r) -> r.setIfNoneExist("identifier=" + NAMING_SYSTEM_TASK_IDENTIFIER + "|" + TASK_IDENTIFIER_VALUE), + 2); + + testCreateDuplicatesViaBundleWithIfNoneExists(bundle, BundleType.TRANSACTIONRESPONSE); + } + + @Test + public void testCreateDuplicateTasksViaBatchBundleWithIfNoneExists() throws Exception + { + ActivityDefinitionDao activityDefinitionDao = getSpringWebApplicationContext() + .getBean(ActivityDefinitionDao.class); + activityDefinitionDao.create(createActivityDefinition()); + + Bundle bundle = createBundle(BundleType.BATCH, createTask(), + (t, r) -> r.setIfNoneExist("identifier=" + NAMING_SYSTEM_TASK_IDENTIFIER + "|" + TASK_IDENTIFIER_VALUE), + 2); + + testCreateDuplicatesViaBundleWithIfNoneExists(bundle, BundleType.BATCHRESPONSE); + } + + @Test + public void testCreateDuplicateValueSetsViaTransactionBundleWithIfNoneExists() throws Exception + { + Bundle bundle = createBundle(BundleType.TRANSACTION, createValueSet(), + (vS, r) -> r.setIfNoneExist("url=" + vS.getUrl() + "&version=" + vS.getVersion()), 2); + + testCreateDuplicatesViaBundleWithIfNoneExists(bundle, BundleType.TRANSACTIONRESPONSE); + } + + @Test + public void testCreateDuplicateValueSetsViaBatchBundleWithIfNoneExists() throws Exception + { + Bundle bundle = createBundle(BundleType.BATCH, createValueSet(), + (vS, r) -> r.setIfNoneExist("url=" + vS.getUrl() + "&version=" + vS.getVersion()), 2); + + testCreateDuplicatesViaBundleWithIfNoneExists(bundle, BundleType.BATCHRESPONSE); + } + + private void testCreateDuplicatesViaBundleWithIfNoneExists(Bundle bundle, + BundleType returnBundleType) throws Exception + { + if (BundleType.TRANSACTIONRESPONSE.equals(returnBundleType)) + assertEquals(BundleType.TRANSACTION, bundle.getType()); + else if (BundleType.BATCHRESPONSE.equals(returnBundleType)) + assertEquals(BundleType.BATCH, bundle.getType()); + else + fail("transaction-response or batch-response expected as returnBundleType"); + + Bundle returnBundle = getWebserviceClient().postBundle(bundle); + + assertNotNull(returnBundle); + assertEquals(returnBundleType, returnBundle.getType()); + assertEquals(2, returnBundle.getEntry().size()); + + BundleEntryComponent e0 = returnBundle.getEntry().get(0); + assertNotNull(e0); + assertTrue(e0.hasResponse()); + assertEquals("201 Created", e0.getResponse().getStatus()); + + BundleEntryComponent e1 = returnBundle.getEntry().get(1); + assertNotNull(e1); + assertTrue(e1.hasResponse()); + assertEquals("200 OK", e1.getResponse().getStatus()); + } + + // ------------------------------------------------------------------------------------------------------------------ + + @Test + public void testCreateDuplicateActivityDefinitionsParallelDirect() throws Exception + { + testCreateDuplicatesParallel(() -> + { + ActivityDefinition returnAd = getWebserviceClient().create(createActivityDefinition()); + assertNotNull(returnAd); + }, ActivityDefinitionDao.class, aD -> ACTIVITY_DEFINITION_URL.equals(aD.getUrl()) + && ACTIVITY_DEFINITION_VERSION.equals(aD.getVersion())); + } + + @Test + public void testCreateDuplicateCodeSystemsParallelDirect() throws Exception + { + testCreateDuplicatesParallel(() -> + { + CodeSystem returnCs = getWebserviceClient().create(createCodeSystem()); + assertNotNull(returnCs); + }, CodeSystemDao.class, + cS -> CODE_SYSTEM_URL.equals(cS.getUrl()) && CODE_SYSTEM_VERSION.equals(cS.getVersion())); + } + + @Test + public void testCreateDuplicateEndpointsParallelDirect() throws Exception + { + testCreateDuplicatesParallel(() -> + { + Endpoint returnE = getWebserviceClient().create(createEndpoint()); + assertNotNull(returnE); + }, EndpointDao.class, e -> ENDPOINT_ADDRESS.equals(e.getAddress()) && e.getIdentifier().stream() + .map(Identifier::getValue).filter(v -> ENDPOINT_IDENTIFIER_VALUE.equals(v)).count() == 1); + } + + @Test + public void testCreateDuplicateNamingSystemsParallelDirect() throws Exception + { + testCreateDuplicatesParallel(() -> + { + NamingSystem returnNs = getWebserviceClient().create(createNamingSystem()); + assertNotNull(returnNs); + }, NamingSystemDao.class, + nS -> NAMING_SYSTEM_NAME.equals(nS.getName()) + && nS.getUniqueId().stream().map(NamingSystemUniqueIdComponent::getValue) + .filter(v -> NAMING_SYSTEM_UNIQUE_ID_VALUE.equals(v)).count() == 1); + } + + @Test + public void testCreateDuplicateParentOrganizationsParallelDirect() throws Exception + { + testCreateDuplicatesParallel(() -> + { + Organization returnO = getWebserviceClient().create(createParentOrganization()); + assertNotNull(returnO); + }, OrganizationDao.class, o -> o.getIdentifier().stream().map(Identifier::getValue) + .filter(v -> ORGANIZATION_IDENTIFIER_VALUE_PARENT.equals(v)).count() == 1); + } + + @Test + public void testCreateDuplicateMemberOrganizationsParallelDirect() throws Exception + { + EndpointDao endpointDao = getSpringWebApplicationContext().getBean(EndpointDao.class); + Endpoint endpoint = endpointDao.create(createEndpoint()); + + testCreateDuplicatesParallel(() -> + { + Organization returnO = getWebserviceClient().create(createMemberOrganization(endpoint)); + assertNotNull(returnO); + }, OrganizationDao.class, o -> o.getIdentifier().stream().map(Identifier::getValue) + .filter(v -> ORGANIZATION_IDENTIFIER_VALUE_MEMBER.equals(v)).count() == 1); + } + + @Test + public void testCreateDuplicateOrganizationAffiliationsSameEndpointParallelDirect() throws Exception + { + EndpointDao endpointDao = getSpringWebApplicationContext().getBean(EndpointDao.class); + Endpoint endpoint = endpointDao.create(createEndpoint()); + + OrganizationDao organizationDao = getSpringWebApplicationContext().getBean(OrganizationDao.class); + Organization memberOrganization = organizationDao.create(createMemberOrganization(endpoint)); + Organization parentOrganization = organizationDao.create(createParentOrganization()); + + OrganizationAffiliation oA1 = createOrganizationAffiliation(parentOrganization, memberOrganization, endpoint, + List.of("DIC")); + OrganizationAffiliation oA2 = createOrganizationAffiliation(parentOrganization, memberOrganization, endpoint, + List.of("COS")); + + testCreateDuplicatesParallel(() -> + { + OrganizationAffiliation returnOa = getWebserviceClient().create(oA1); + assertNotNull(returnOa); + }, () -> + { + OrganizationAffiliation returnOa = getWebserviceClient().create(oA2); + assertNotNull(returnOa); + }, OrganizationAffiliationDao.class, + oA -> parentOrganization.getIdElement().toVersionless().toString() + .equals(oA.getOrganization().getReference()) + && memberOrganization.getIdElement().toVersionless().toString() + .equals(oA.getParticipatingOrganization().getReference())); + } + + @Test + public void testCreateDuplicateOrganizationAffiliationsSameRoleParallelDirect() throws Exception + { + EndpointDao endpointDao = getSpringWebApplicationContext().getBean(EndpointDao.class); + Endpoint e1 = endpointDao.create(createEndpoint(ENDPOINT_IDENTIFIER_VALUE, ENDPOINT_ADDRESS)); + Endpoint e2 = endpointDao.create(createEndpoint("endpoint2.test.org", "https://endpoint2.test.org/fhir")); + + OrganizationDao organizationDao = getSpringWebApplicationContext().getBean(OrganizationDao.class); + Organization memberOrganization = organizationDao.create(createMemberOrganization(e1, e2)); + Organization parentOrganization = organizationDao.create(createParentOrganization()); + + OrganizationAffiliation oA1 = createOrganizationAffiliation(parentOrganization, memberOrganization, e1, + List.of("DIC")); + OrganizationAffiliation oA2 = createOrganizationAffiliation(parentOrganization, memberOrganization, e2, + List.of("DIC", "COS")); + + testCreateDuplicatesParallel(() -> + { + OrganizationAffiliation returnOa = getWebserviceClient().create(oA1); + assertNotNull(returnOa); + }, () -> + { + OrganizationAffiliation returnOa = getWebserviceClient().create(oA2); + assertNotNull(returnOa); + }, OrganizationAffiliationDao.class, + oA -> parentOrganization.getIdElement().toVersionless().toString() + .equals(oA.getOrganization().getReference()) + && memberOrganization.getIdElement().toVersionless().toString() + .equals(oA.getParticipatingOrganization().getReference())); + } + + @Test + public void testCreateDuplicateStructureDefinitionsParallelDirect() throws Exception + { + testCreateDuplicatesParallel(() -> + { + StructureDefinition returnSd = getWebserviceClient().create(createStructureDefinition()); + assertNotNull(returnSd); + }, StructureDefinitionDaoJdbc.class, sD -> STRUCTURE_DEFINITION_URL.equals(sD.getUrl()) + && STRUCTURE_DEFINITION_VERSION.equals(sD.getVersion())); + } + + @Test + public void testCreateDuplicateSubscriptionsParallelDirect() throws Exception + { + testCreateDuplicatesParallel(() -> + { + Subscription returnS = getWebserviceClient().create(createSubscription()); + assertNotNull(returnS); + }, SubscriptionDao.class, + s -> SUBSCRIPTION_CRITERIA.equals(s.getCriteria()) + && SUBSCRIPTION_CHANNEL_TYPE.equals(s.getChannel().getType()) + && SUBSCRIPTION_CHANNEL_PAYLOAD.equals(s.getChannel().getPayload())); + } + + @Test + public void testCreateDuplicateTasksParallelDirect() throws Exception + { + ActivityDefinitionDao activityDefinitionDao = getSpringWebApplicationContext() + .getBean(ActivityDefinitionDao.class); + activityDefinitionDao.create(createActivityDefinition()); + + testCreateDuplicatesParallel(() -> + { + Task returnT = getWebserviceClient().create(createTask()); + assertNotNull(returnT); + }, TaskDao.class, + t -> TASK_IDENTIFIER_VALUE.equals( + t.getIdentifier().stream().filter(i -> NAMING_SYSTEM_TASK_IDENTIFIER.equals(i.getSystem())) + .findFirst().map(Identifier::getValue).get())); + } + + @Test + public void testCreateDuplicateValueSetsParallelDirect() throws Exception + { + testCreateDuplicatesParallel(() -> + { + ValueSet returnVs = getWebserviceClient().create(createValueSet()); + assertNotNull(returnVs); + }, ValueSetDao.class, vS -> VALUE_SET_URL.equals(vS.getUrl()) && VALUE_SET_VERSION.equals(vS.getVersion())); + } + + // ------------------------------------------------------------------------------------------------------------------ + + @Test + public void testCreateDuplicateActivityDefinitionsParallelTransactionBundle() throws Exception + { + testCreateDuplicatesParallel(() -> + { + Bundle returnBundle = getWebserviceClient() + .postBundle(createBundle(BundleType.TRANSACTION, createActivityDefinition(), null, 1)); + assertNotNull(returnBundle); + }, ActivityDefinitionDao.class, aD -> ACTIVITY_DEFINITION_URL.equals(aD.getUrl()) + && ACTIVITY_DEFINITION_VERSION.equals(aD.getVersion())); + } + + @Test + public void testCreateDuplicateActivityDefinitionsParallelBatchBundle() throws Exception + { + testCreateDuplicatesParallel(() -> + { + Bundle returnBundle = getWebserviceClient() + .postBundle(createBundle(BundleType.BATCH, createActivityDefinition(), null, 1)); + assertNotNull(returnBundle); + + assertNotNull(returnBundle.getEntry()); + assertEquals(1, returnBundle.getEntry().size()); + assertNotNull(returnBundle.getEntry().get(0).getResponse()); + assertNotNull(returnBundle.getEntry().get(0).getResponse().getStatus()); + + if ("403 Forbidden".equals(returnBundle.getEntry().get(0).getResponse().getStatus())) + throw new WebApplicationException(403); + + }, ActivityDefinitionDao.class, aD -> ACTIVITY_DEFINITION_URL.equals(aD.getUrl()) + && ACTIVITY_DEFINITION_VERSION.equals(aD.getVersion())); + } + + @Test + public void testCreateDuplicateEndpointsParallelTransactionBundle() throws Exception + { + testCreateDuplicatesParallel(() -> + { + Bundle returnBundle = getWebserviceClient() + .postBundle(createBundle(BundleType.TRANSACTION, createEndpoint(), null, 1)); + assertNotNull(returnBundle); + }, EndpointDao.class, e -> ENDPOINT_ADDRESS.equals(e.getAddress()) && e.getIdentifier().stream() + .map(Identifier::getValue).filter(v -> ENDPOINT_IDENTIFIER_VALUE.equals(v)).count() == 1); + } + + @Test + public void testCreateDuplicateEndpointsParallelBatchBundle() throws Exception + { + testCreateDuplicatesParallel(() -> + { + Bundle returnBundle = getWebserviceClient() + .postBundle(createBundle(BundleType.BATCH, createEndpoint(), null, 1)); + assertNotNull(returnBundle); + + assertNotNull(returnBundle.getEntry()); + assertEquals(1, returnBundle.getEntry().size()); + assertNotNull(returnBundle.getEntry().get(0).getResponse()); + assertNotNull(returnBundle.getEntry().get(0).getResponse().getStatus()); + + if ("403 Forbidden".equals(returnBundle.getEntry().get(0).getResponse().getStatus())) + throw new WebApplicationException(403); + + }, EndpointDao.class, e -> ENDPOINT_ADDRESS.equals(e.getAddress()) && e.getIdentifier().stream() + .map(Identifier::getValue).filter(v -> ENDPOINT_IDENTIFIER_VALUE.equals(v)).count() == 1); + } + + @Test + public void testCreateDuplicateCodeSystemsParallelTransactionBundle() throws Exception + { + testCreateDuplicatesParallel(() -> + { + Bundle returnBundle = getWebserviceClient() + .postBundle(createBundle(BundleType.TRANSACTION, createCodeSystem(), null, 1)); + assertNotNull(returnBundle); + }, CodeSystemDao.class, + cS -> CODE_SYSTEM_URL.equals(cS.getUrl()) && CODE_SYSTEM_VERSION.equals(cS.getVersion())); + } + + @Test + public void testCreateDuplicateCodeSystemsParallelBatchBundle() throws Exception + { + testCreateDuplicatesParallel(() -> + { + Bundle returnBundle = getWebserviceClient() + .postBundle(createBundle(BundleType.BATCH, createCodeSystem(), null, 1)); + assertNotNull(returnBundle); + + assertNotNull(returnBundle.getEntry()); + assertEquals(1, returnBundle.getEntry().size()); + assertNotNull(returnBundle.getEntry().get(0).getResponse()); + assertNotNull(returnBundle.getEntry().get(0).getResponse().getStatus()); + + if ("403 Forbidden".equals(returnBundle.getEntry().get(0).getResponse().getStatus())) + throw new WebApplicationException(403); + + }, CodeSystemDao.class, + cS -> CODE_SYSTEM_URL.equals(cS.getUrl()) && CODE_SYSTEM_VERSION.equals(cS.getVersion())); + } + + @Test + public void testCreateDuplicateNamingSystemsParallelTransactionBundle() throws Exception + { + testCreateDuplicatesParallel(() -> + { + Bundle returnBundle = getWebserviceClient() + .postBundle(createBundle(BundleType.TRANSACTION, createNamingSystem(), null, 1)); + assertNotNull(returnBundle); + }, NamingSystemDao.class, + nS -> NAMING_SYSTEM_NAME.equals(nS.getName()) + && nS.getUniqueId().stream().map(NamingSystemUniqueIdComponent::getValue) + .filter(v -> NAMING_SYSTEM_UNIQUE_ID_VALUE.equals(v)).count() == 1); + } + + @Test + public void testCreateDuplicateNamingSystemsParallelBatchBundle() throws Exception + { + testCreateDuplicatesParallel(() -> + { + Bundle returnBundle = getWebserviceClient() + .postBundle(createBundle(BundleType.BATCH, createNamingSystem(), null, 1)); + assertNotNull(returnBundle); + + assertNotNull(returnBundle.getEntry()); + assertEquals(1, returnBundle.getEntry().size()); + assertNotNull(returnBundle.getEntry().get(0).getResponse()); + assertNotNull(returnBundle.getEntry().get(0).getResponse().getStatus()); + + if ("403 Forbidden".equals(returnBundle.getEntry().get(0).getResponse().getStatus())) + throw new WebApplicationException(403); + + }, NamingSystemDao.class, + nS -> NAMING_SYSTEM_NAME.equals(nS.getName()) + && nS.getUniqueId().stream().map(NamingSystemUniqueIdComponent::getValue) + .filter(v -> NAMING_SYSTEM_UNIQUE_ID_VALUE.equals(v)).count() == 1); + } + + @Test + public void testCreateDuplicateParentOrganizationsParallelTransactionBundle() throws Exception + { + testCreateDuplicatesParallel(() -> + { + Bundle returnBundle = getWebserviceClient() + .postBundle(createBundle(BundleType.TRANSACTION, createParentOrganization(), null, 1)); + assertNotNull(returnBundle); + }, OrganizationDao.class, o -> o.getIdentifier().stream().map(Identifier::getValue) + .filter(v -> ORGANIZATION_IDENTIFIER_VALUE_PARENT.equals(v)).count() == 1); + } + + @Test + public void testCreateDuplicateParentOrganizationsParallelBatchBundle() throws Exception + { + testCreateDuplicatesParallel(() -> + { + Bundle returnBundle = getWebserviceClient() + .postBundle(createBundle(BundleType.BATCH, createParentOrganization(), null, 1)); + assertNotNull(returnBundle); + + assertNotNull(returnBundle.getEntry()); + assertEquals(1, returnBundle.getEntry().size()); + assertNotNull(returnBundle.getEntry().get(0).getResponse()); + assertNotNull(returnBundle.getEntry().get(0).getResponse().getStatus()); + + if ("403 Forbidden".equals(returnBundle.getEntry().get(0).getResponse().getStatus())) + throw new WebApplicationException(403); + + }, OrganizationDao.class, o -> o.getIdentifier().stream().map(Identifier::getValue) + .filter(v -> ORGANIZATION_IDENTIFIER_VALUE_PARENT.equals(v)).count() == 1); + } + + private void testCreateDuplicateMemberOrganizationsParallelBundle(BundleType bundleType) throws Exception + { + EndpointDao endpointDao = getSpringWebApplicationContext().getBean(EndpointDao.class); + Endpoint endpoint = endpointDao.create(createEndpoint()); + + testCreateDuplicatesParallel(() -> + { + Bundle returnBundle = getWebserviceClient() + .postBundle(createBundle(bundleType, createMemberOrganization(endpoint), null, 1)); + assertNotNull(returnBundle); + + if (BundleType.BATCH.equals(bundleType)) + { + assertNotNull(returnBundle.getEntry()); + assertEquals(1, returnBundle.getEntry().size()); + assertNotNull(returnBundle.getEntry().get(0).getResponse()); + assertNotNull(returnBundle.getEntry().get(0).getResponse().getStatus()); + + if ("403 Forbidden".equals(returnBundle.getEntry().get(0).getResponse().getStatus())) + throw new WebApplicationException(403); + } + }, OrganizationDao.class, o -> o.getIdentifier().stream().map(Identifier::getValue) + .filter(v -> ORGANIZATION_IDENTIFIER_VALUE_MEMBER.equals(v)).count() == 1); + } + + @Test + public void testCreateDuplicateMemberOrganizationsParallelTransactionBundle() throws Exception + { + testCreateDuplicateMemberOrganizationsParallelBundle(BundleType.TRANSACTION); + } + + @Test + public void testCreateDuplicateMemberOrganizationsParallelBatchBundle() throws Exception + { + testCreateDuplicateMemberOrganizationsParallelBundle(BundleType.BATCH); + } + + private void testCreateDuplicateOrganizationAffiliationsSameEndpointParallelBundle(BundleType bundleType) + throws Exception + { + EndpointDao endpointDao = getSpringWebApplicationContext().getBean(EndpointDao.class); + Endpoint endpoint = endpointDao.create(createEndpoint()); + + OrganizationDao organizationDao = getSpringWebApplicationContext().getBean(OrganizationDao.class); + Organization memberOrganization = organizationDao.create(createMemberOrganization(endpoint)); + Organization parentOrganization = organizationDao.create(createParentOrganization()); + + OrganizationAffiliation oA1 = createOrganizationAffiliation(parentOrganization, memberOrganization, endpoint, + List.of("DIC")); + OrganizationAffiliation oA2 = createOrganizationAffiliation(parentOrganization, memberOrganization, endpoint, + List.of("COS")); + + testCreateDuplicatesParallel(() -> + { + Bundle returnBundle = getWebserviceClient().postBundle(createBundle(bundleType, oA1, null, 1)); + assertNotNull(returnBundle); + + if (BundleType.BATCH.equals(bundleType)) + { + assertNotNull(returnBundle.getEntry()); + assertEquals(1, returnBundle.getEntry().size()); + assertNotNull(returnBundle.getEntry().get(0).getResponse()); + assertNotNull(returnBundle.getEntry().get(0).getResponse().getStatus()); + + if ("403 Forbidden".equals(returnBundle.getEntry().get(0).getResponse().getStatus())) + throw new WebApplicationException(403); + } + }, () -> + { + Bundle returnBundle = getWebserviceClient().postBundle(createBundle(bundleType, oA2, null, 1)); + assertNotNull(returnBundle); + + if (BundleType.BATCH.equals(bundleType)) + { + assertNotNull(returnBundle.getEntry()); + assertEquals(1, returnBundle.getEntry().size()); + assertNotNull(returnBundle.getEntry().get(0).getResponse()); + assertNotNull(returnBundle.getEntry().get(0).getResponse().getStatus()); + + if ("403 Forbidden".equals(returnBundle.getEntry().get(0).getResponse().getStatus())) + throw new WebApplicationException(403); + } + }, OrganizationAffiliationDao.class, + oA -> parentOrganization.getIdElement().toVersionless().toString() + .equals(oA.getOrganization().getReference()) + && memberOrganization.getIdElement().toVersionless().toString() + .equals(oA.getParticipatingOrganization().getReference())); + } + + @Test + public void testCreateDuplicateOrganizationAffiliationsSameEndpointParallelTransactionBundle() throws Exception + { + testCreateDuplicateOrganizationAffiliationsSameEndpointParallelBundle(BundleType.TRANSACTION); + } + + @Test + public void testCreateDuplicateOrganizationAffiliationsSameEndpointParallelBatchBundle() throws Exception + { + testCreateDuplicateOrganizationAffiliationsSameEndpointParallelBundle(BundleType.BATCH); + } + + private void testCreateDuplicateOrganizationAffiliationsSameRoletParallelBundle(BundleType bundleType) + throws Exception + { + EndpointDao endpointDao = getSpringWebApplicationContext().getBean(EndpointDao.class); + Endpoint e1 = endpointDao.create(createEndpoint(ENDPOINT_IDENTIFIER_VALUE, ENDPOINT_ADDRESS)); + Endpoint e2 = endpointDao.create(createEndpoint("endpoint2.test.org", "https://endpoint2.test.org/fhir")); + + OrganizationDao organizationDao = getSpringWebApplicationContext().getBean(OrganizationDao.class); + Organization memberOrganization = organizationDao.create(createMemberOrganization(e1, e2)); + Organization parentOrganization = organizationDao.create(createParentOrganization()); + + OrganizationAffiliation oA1 = createOrganizationAffiliation(parentOrganization, memberOrganization, e1, + List.of("DIC")); + OrganizationAffiliation oA2 = createOrganizationAffiliation(parentOrganization, memberOrganization, e2, + List.of("DIC", "COS")); + + testCreateDuplicatesParallel(() -> + { + Bundle returnBundle = getWebserviceClient().postBundle(createBundle(bundleType, oA1, null, 1)); + assertNotNull(returnBundle); + + if (BundleType.BATCH.equals(bundleType)) + { + assertNotNull(returnBundle.getEntry()); + assertEquals(1, returnBundle.getEntry().size()); + assertNotNull(returnBundle.getEntry().get(0).getResponse()); + assertNotNull(returnBundle.getEntry().get(0).getResponse().getStatus()); + + if ("403 Forbidden".equals(returnBundle.getEntry().get(0).getResponse().getStatus())) + throw new WebApplicationException(403); + } + }, () -> + { + Bundle returnBundle = getWebserviceClient().postBundle(createBundle(bundleType, oA2, null, 1)); + assertNotNull(returnBundle); + + if (BundleType.BATCH.equals(bundleType)) + { + assertNotNull(returnBundle.getEntry()); + assertEquals(1, returnBundle.getEntry().size()); + assertNotNull(returnBundle.getEntry().get(0).getResponse()); + assertNotNull(returnBundle.getEntry().get(0).getResponse().getStatus()); + + if ("403 Forbidden".equals(returnBundle.getEntry().get(0).getResponse().getStatus())) + throw new WebApplicationException(403); + } + }, OrganizationAffiliationDao.class, + oA -> parentOrganization.getIdElement().toVersionless().toString() + .equals(oA.getOrganization().getReference()) + && memberOrganization.getIdElement().toVersionless().toString() + .equals(oA.getParticipatingOrganization().getReference())); + } + + @Test + public void testCreateDuplicateOrganizationAffiliationsSameRoletParallelTransactionBundle() throws Exception + { + testCreateDuplicateOrganizationAffiliationsSameRoletParallelBundle(BundleType.TRANSACTION); + } + + @Test + public void testCreateDuplicateOrganizationAffiliationsSameRoletParallelBatchBundle() throws Exception + { + testCreateDuplicateOrganizationAffiliationsSameRoletParallelBundle(BundleType.BATCH); + } + + @Test + public void testCreateDuplicateStructureDefinitionsParallelTransactionBundle() throws Exception + { + testCreateDuplicatesParallel(() -> + { + Bundle returnBundle = getWebserviceClient() + .postBundle(createBundle(BundleType.TRANSACTION, createStructureDefinition(), null, 1)); + assertNotNull(returnBundle); + }, StructureDefinitionDaoJdbc.class, sD -> STRUCTURE_DEFINITION_URL.equals(sD.getUrl()) + && STRUCTURE_DEFINITION_VERSION.equals(sD.getVersion())); + } + + @Test + public void testCreateDuplicateStructureDefinitionsParallelBatchBundle() throws Exception + { + testCreateDuplicatesParallel(() -> + { + Bundle returnBundle = getWebserviceClient() + .postBundle(createBundle(BundleType.BATCH, createStructureDefinition(), null, 1)); + assertNotNull(returnBundle); + + assertNotNull(returnBundle.getEntry()); + assertEquals(1, returnBundle.getEntry().size()); + assertNotNull(returnBundle.getEntry().get(0).getResponse()); + assertNotNull(returnBundle.getEntry().get(0).getResponse().getStatus()); + + if ("403 Forbidden".equals(returnBundle.getEntry().get(0).getResponse().getStatus())) + throw new WebApplicationException(403); + + }, StructureDefinitionDaoJdbc.class, sD -> STRUCTURE_DEFINITION_URL.equals(sD.getUrl()) + && STRUCTURE_DEFINITION_VERSION.equals(sD.getVersion())); + } + + @Test + public void testCreateDuplicateSubscriptionsParallelTransactionBundle() throws Exception + { + testCreateDuplicatesParallel(() -> + { + Bundle returnBundle = getWebserviceClient() + .postBundle(createBundle(BundleType.TRANSACTION, createSubscription(), null, 1)); + assertNotNull(returnBundle); + }, SubscriptionDao.class, + s -> SUBSCRIPTION_CRITERIA.equals(s.getCriteria()) + && SUBSCRIPTION_CHANNEL_TYPE.equals(s.getChannel().getType()) + && SUBSCRIPTION_CHANNEL_PAYLOAD.equals(s.getChannel().getPayload())); + } + + @Test + public void testCreateDuplicateSubscriptionsParallelBatchBundle() throws Exception + { + testCreateDuplicatesParallel(() -> + { + Bundle returnBundle = getWebserviceClient() + .postBundle(createBundle(BundleType.BATCH, createSubscription(), null, 1)); + assertNotNull(returnBundle); + + assertNotNull(returnBundle.getEntry()); + assertEquals(1, returnBundle.getEntry().size()); + assertNotNull(returnBundle.getEntry().get(0).getResponse()); + assertNotNull(returnBundle.getEntry().get(0).getResponse().getStatus()); + + if ("403 Forbidden".equals(returnBundle.getEntry().get(0).getResponse().getStatus())) + throw new WebApplicationException(403); + }, SubscriptionDao.class, + s -> SUBSCRIPTION_CRITERIA.equals(s.getCriteria()) + && SUBSCRIPTION_CHANNEL_TYPE.equals(s.getChannel().getType()) + && SUBSCRIPTION_CHANNEL_PAYLOAD.equals(s.getChannel().getPayload())); + } + + @Test + public void testCreateDuplicateTasksParallelTransactionBundle() throws Exception + { + ActivityDefinitionDao activityDefinitionDao = getSpringWebApplicationContext() + .getBean(ActivityDefinitionDao.class); + activityDefinitionDao.create(createActivityDefinition()); + + testCreateDuplicatesParallel(() -> + { + Bundle returnBundle = getWebserviceClient() + .postBundle(createBundle(BundleType.TRANSACTION, createTask(), null, 1)); + assertNotNull(returnBundle); + }, TaskDao.class, + t -> TASK_IDENTIFIER_VALUE.equals( + t.getIdentifier().stream().filter(i -> NAMING_SYSTEM_TASK_IDENTIFIER.equals(i.getSystem())) + .findFirst().map(Identifier::getValue).get())); + } + + @Test + public void testCreateDuplicateTasksParallelBatchBundle() throws Exception + { + ActivityDefinitionDao activityDefinitionDao = getSpringWebApplicationContext() + .getBean(ActivityDefinitionDao.class); + activityDefinitionDao.create(createActivityDefinition()); + + testCreateDuplicatesParallel(() -> + { + Bundle returnBundle = getWebserviceClient() + .postBundle(createBundle(BundleType.BATCH, createTask(), null, 1)); + assertNotNull(returnBundle); + + assertNotNull(returnBundle.getEntry()); + assertEquals(1, returnBundle.getEntry().size()); + assertNotNull(returnBundle.getEntry().get(0).getResponse()); + assertNotNull(returnBundle.getEntry().get(0).getResponse().getStatus()); + + if ("403 Forbidden".equals(returnBundle.getEntry().get(0).getResponse().getStatus())) + throw new WebApplicationException(403); + }, TaskDao.class, + t -> TASK_IDENTIFIER_VALUE.equals( + t.getIdentifier().stream().filter(i -> NAMING_SYSTEM_TASK_IDENTIFIER.equals(i.getSystem())) + .findFirst().map(Identifier::getValue).get())); + } + + @Test + public void testCreateDuplicateValueSetsParallelTransactionBundle() throws Exception + { + testCreateDuplicatesParallel(() -> + { + Bundle returnBundle = getWebserviceClient() + .postBundle(createBundle(BundleType.TRANSACTION, createValueSet(), null, 1)); + assertNotNull(returnBundle); + }, ValueSetDao.class, vS -> VALUE_SET_URL.equals(vS.getUrl()) && VALUE_SET_VERSION.equals(vS.getVersion())); + } + + @Test + public void testCreateDuplicateValueSetsParallelBatchBundle() throws Exception + { + testCreateDuplicatesParallel(() -> + { + Bundle returnBundle = getWebserviceClient() + .postBundle(createBundle(BundleType.BATCH, createValueSet(), null, 1)); + assertNotNull(returnBundle); + + assertNotNull(returnBundle.getEntry()); + assertEquals(1, returnBundle.getEntry().size()); + assertNotNull(returnBundle.getEntry().get(0).getResponse()); + assertNotNull(returnBundle.getEntry().get(0).getResponse().getStatus()); + + if ("403 Forbidden".equals(returnBundle.getEntry().get(0).getResponse().getStatus())) + throw new WebApplicationException(403); + }, ValueSetDao.class, vS -> VALUE_SET_URL.equals(vS.getUrl()) && VALUE_SET_VERSION.equals(vS.getVersion())); + } + + // ------------------------------------------------------------------------------------------------------------------ + + private void testCreateDuplicatesParallel(Runnable createOperation, + Class> resourceDaoType, Predicate createdResourceMatcher) + throws InterruptedException, SQLException + { + testCreateDuplicatesParallel(createOperation, createOperation, resourceDaoType, createdResourceMatcher); + } + + private void testCreateDuplicatesParallel(Runnable createOperation1, Runnable createOperation2, + Class> resourceDaoType, Predicate createdResourceMatcher) + throws InterruptedException, SQLException + { + List caughtConflictWebApplicationException = Collections.synchronizedList(new ArrayList<>()); + UncaughtExceptionHandler handler = (t, e) -> + { + if (e instanceof WebApplicationException w) + if (w.getResponse().getStatus() == 403) + caughtConflictWebApplicationException.add(e); + else + logger.warn("Thread {} uncaught WebApplicationException with status: {}", t.getName(), + w.getResponse().getStatus(), e); + else + logger.warn("Thread {} uncaught Exception", t.getName(), e); + }; + + Thread t1 = new Thread(createOperation1, "test 1"); + t1.setUncaughtExceptionHandler(handler); + + Thread t2 = new Thread(createOperation2, "test 2"); + t2.setUncaughtExceptionHandler(handler); + + t1.start(); + t2.start(); + t1.join(); + t2.join(); + + ResourceDao dao = getSpringWebApplicationContext().getBean(resourceDaoType); + assertEquals(1, dao.readAll().stream().filter(createdResourceMatcher).count()); + + assertEquals("Creating two identical " + dao.getResourceTypeName() + + " in parallel should not be possible, one WebApplicationException with status 403 Forbidden expected", + 1, caughtConflictWebApplicationException.size()); + + logger.info("Expected exception caught {} - {}, status {}", + caughtConflictWebApplicationException.get(0).getClass().getName(), + caughtConflictWebApplicationException.get(0).getMessage(), + caughtConflictWebApplicationException.get(0) instanceof WebApplicationException e + ? e.getResponse().getStatus() + : "?"); + } + + private Bundle createBundle(BundleType bundleType, R resource, + BiConsumer requestModifier, int entries) + { + BundleEntryComponent e = new BundleEntryComponent(); + e.setResource(resource); + e.setFullUrl("urn:uuid:" + UUID.randomUUID().toString()); + + BundleEntryRequestComponent r = e.getRequest(); + r.setMethod(HTTPVerb.POST); + r.setUrl(resource.getResourceType().name()); + if (requestModifier != null) + requestModifier.accept(resource, r); + + Bundle b = new Bundle().setType(bundleType); + + for (int i = 0; i < entries; i++) + b.addEntry(e); + + return b; + } + + private Bundle createBundle(BundleType bundleType, OrganizationAffiliation a1, OrganizationAffiliation a2, + BiConsumer requestModifier) + { + BundleEntryComponent e1 = new BundleEntryComponent(); + e1.setResource(a1); + e1.setFullUrl("urn:uuid:" + UUID.randomUUID().toString()); + + BundleEntryRequestComponent r1 = e1.getRequest(); + r1.setMethod(HTTPVerb.POST); + r1.setUrl(a1.getResourceType().name()); + if (requestModifier != null) + requestModifier.accept(a1, r1); + + BundleEntryComponent e2 = new BundleEntryComponent(); + e2.setResource(a2); + e2.setFullUrl("urn:uuid:" + UUID.randomUUID().toString()); + + BundleEntryRequestComponent r2 = e2.getRequest(); + r2.setMethod(HTTPVerb.POST); + r2.setUrl(a2.getResourceType().name()); + if (requestModifier != null) + requestModifier.accept(a2, r2); + + Bundle b = new Bundle().setType(bundleType); + b.addEntry(e1); + b.addEntry(e2); + + return b; + } + + // ------------------------------------------------------------------------------------------------------------------ + + private ActivityDefinition createActivityDefinition() + { + ActivityDefinition aD = new ActivityDefinition().setUrl(ACTIVITY_DEFINITION_URL) + .setVersion(ACTIVITY_DEFINITION_VERSION).setStatus(PublicationStatus.ACTIVE) + .setName("TestActivityDefinition"); + + getProcessAuthorizationHelper().add(aD, "test-message", "http://test.com/fhir/StructureDefinition/task-profile", + Requester.remoteAll(), Recipient.localAll()); + + getReadAccessHelper().addAll(aD); + + return aD; + } + + private CodeSystem createCodeSystem() + { + CodeSystem cS = new CodeSystem().setUrl(CODE_SYSTEM_URL).setVersion(CODE_SYSTEM_VERSION) + .setStatus(PublicationStatus.ACTIVE).setStatus(PublicationStatus.ACTIVE).setName("TestCodeSystem") + .setContent(CodeSystemContentMode.COMPLETE); + + getReadAccessHelper().addAll(cS); + + return cS; + } + + private Endpoint createEndpoint() + { + return createEndpoint(ENDPOINT_IDENTIFIER_VALUE, ENDPOINT_ADDRESS); + } + + private Endpoint createEndpoint(String identifierValue, String address) + { + Endpoint e = new Endpoint() + .addIdentifier( + new Identifier().setSystem("http://dsf.dev/sid/endpoint-identifier").setValue(identifierValue)) + .setAddress(address) + .addPayloadType(new CodeableConcept() + .addCoding(new Coding().setSystem("http://hl7.org/fhir/resource-types").setCode("Task"))) + .setConnectionType( + new Coding().setSystem("http://terminology.hl7.org/CodeSystem/endpoint-connection-type") + .setCode("hl7-fhir-rest")) + .setStatus(EndpointStatus.ACTIVE); + + getReadAccessHelper().addAll(e); + + return e; + } + + private NamingSystem createNamingSystem() + { + NamingSystem nS = new NamingSystem().setStatus(PublicationStatus.ACTIVE).setName(NAMING_SYSTEM_NAME) + .setDate(new Date()).setKind(NamingSystemType.IDENTIFIER) + .addUniqueId(new NamingSystemUniqueIdComponent().setType(NamingSystemIdentifierType.OTHER) + .setValue(NAMING_SYSTEM_UNIQUE_ID_VALUE)); + + getReadAccessHelper().addAll(nS); + + return nS; + } + + private Organization createParentOrganization() + { + Organization o = new Organization().addIdentifier(new Identifier() + .setSystem("http://dsf.dev/sid/organization-identifier").setValue(ORGANIZATION_IDENTIFIER_VALUE_PARENT)) + .setActive(true); + + o.getMeta().addProfile("http://dsf.dev/fhir/StructureDefinition/organization-parent"); + + getReadAccessHelper().addAll(o); + + return o; + } + + private Organization createMemberOrganization(Endpoint... endpoints) + { + Organization o = new Organization().addIdentifier(new Identifier() + .setSystem("http://dsf.dev/sid/organization-identifier").setValue(ORGANIZATION_IDENTIFIER_VALUE_MEMBER)) + .setActive(true); + + Arrays.stream(endpoints).forEach(e -> o.addEndpoint(new Reference(e.getIdElement().toVersionless()))); + + o.getMeta().addProfile("http://dsf.dev/fhir/StructureDefinition/organization"); + o.addExtension(new Extension("http://dsf.dev/fhir/StructureDefinition/extension-certificate-thumbprint") + .setValue(new StringType( + "f143826e22f1a95830ab32dde7b388c154039ed0633c9b0d1526078a9ee7f403540e3cd3459331a3c2caf72e006daff2f71ab7cd2136272e5e022ef392c32246"))); + + getReadAccessHelper().addAll(o); + + return o; + } + + private OrganizationAffiliation createOrganizationAffiliation(Organization parent, Organization member, + Endpoint endpoint, List roles) + { + OrganizationAffiliation oA = new OrganizationAffiliation().setActive(true); + oA.setOrganization(new Reference(parent.getIdElement().toVersionless())); + oA.setParticipatingOrganization(new Reference(member.getIdElement().toVersionless())); + oA.addEndpoint(new Reference(endpoint.getIdElement().toVersionless())); + roles.forEach( + r -> oA.addCode().addCoding(new Coding("http://dsf.dev/fhir/CodeSystem/organization-role", r, null))); + + getReadAccessHelper().addAll(oA); + + return oA; + } + + private StructureDefinition createStructureDefinition() + { + StructureDefinition sD = new StructureDefinition().setUrl(STRUCTURE_DEFINITION_URL) + .setVersion(STRUCTURE_DEFINITION_VERSION).setStatus(PublicationStatus.ACTIVE) + .setName("TestStructureDefinition").setStatus(PublicationStatus.ACTIVE) + .setBaseDefinition("http://hl7.org/fhir/StructureDefinition/Patient") + .setKind(StructureDefinitionKind.RESOURCE).setAbstract(false).setType("Patient") + .setDerivation(TypeDerivationRule.CONSTRAINT); + + ElementDefinition e = sD.getDifferential().addElement(); + e.setId("Patient.active"); + e.setPath("Patient.active"); + e.setMin(1); + + getReadAccessHelper().addAll(sD); + + return sD; + } + + private Subscription createSubscription() + { + Subscription s = new Subscription().setStatus(SubscriptionStatus.ACTIVE).setReason("some reason") + .setCriteria(SUBSCRIPTION_CRITERIA).setChannel(new SubscriptionChannelComponent() + .setType(SUBSCRIPTION_CHANNEL_TYPE).setPayload(SUBSCRIPTION_CHANNEL_PAYLOAD)); + + getReadAccessHelper().addAll(s); + + return s; + } + + private Task createTask() + { + Task t = new Task(); + t.setStatus(TaskStatus.DRAFT); + t.setIntent(TaskIntent.ORDER); + t.setAuthoredOn(new Date()); + t.addIdentifier().setSystem(NAMING_SYSTEM_TASK_IDENTIFIER).setValue(TASK_IDENTIFIER_VALUE); + t.setInstantiatesCanonical(ACTIVITY_DEFINITION_URL + "|" + ACTIVITY_DEFINITION_VERSION); + t.getRequester().setType("Organization").getIdentifier().setSystem("http://dsf.dev/sid/organization-identifier") + .setValue("Test_Organization"); + t.getRestriction().getRecipientFirstRep().setType("Organization").getIdentifier() + .setSystem("http://dsf.dev/sid/organization-identifier").setValue("Test_Organization"); + t.getInputFirstRep().setValue(new StringType("test")).getType().getCodingFirstRep() + .setSystem("http://dsf.dev/fhir/CodeSystem/bpmn-message").setCode("message-name"); + + return t; + } + + private ValueSet createValueSet() + { + ValueSet vS = new ValueSet().setUrl(VALUE_SET_URL).setVersion(VALUE_SET_VERSION) + .setStatus(PublicationStatus.ACTIVE).setStatus(PublicationStatus.ACTIVE).setName("TestValueSet"); + + getReadAccessHelper().addAll(vS); + + return vS; + } +} \ No newline at end of file diff --git a/dsf-fhir/dsf-fhir-validation/src/main/java/dev/dsf/fhir/validation/SnapshotGeneratorImpl.java b/dsf-fhir/dsf-fhir-validation/src/main/java/dev/dsf/fhir/validation/SnapshotGeneratorImpl.java index 28575f517..634e321bc 100755 --- a/dsf-fhir/dsf-fhir-validation/src/main/java/dev/dsf/fhir/validation/SnapshotGeneratorImpl.java +++ b/dsf-fhir/dsf-fhir-validation/src/main/java/dev/dsf/fhir/validation/SnapshotGeneratorImpl.java @@ -2,13 +2,14 @@ import java.util.ArrayList; import java.util.List; -import java.util.Objects; import org.hl7.fhir.r4.conformance.ProfileUtilities; import org.hl7.fhir.r4.context.IWorkerContext; import org.hl7.fhir.r4.hapi.ctx.HapiWorkerContext; import org.hl7.fhir.r4.model.StructureDefinition; import org.hl7.fhir.utilities.validation.ValidationMessage; +import org.hl7.fhir.utilities.validation.ValidationMessage.IssueSeverity; +import org.hl7.fhir.utilities.validation.ValidationMessage.IssueType; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -43,7 +44,12 @@ public SnapshotWithValidationMessages generateSnapshot(StructureDefinition diffe public SnapshotWithValidationMessages generateSnapshot(StructureDefinition differential, String baseAbsoluteUrlPrefix) { - Objects.requireNonNull(differential, "differential"); + if (differential == null) + return new SnapshotWithValidationMessages(differential, List.of(new ValidationMessage(null, + IssueType.PROCESSING, null, "StructureDefinition is null", IssueSeverity.ERROR))); + if (!differential.hasBaseDefinition()) + return new SnapshotWithValidationMessages(differential, List.of(new ValidationMessage(null, + IssueType.PROCESSING, null, "StructureDefinition.baseDefinition missing", IssueSeverity.ERROR))); logger.debug("Generating snapshot for StructureDefinition with id {}, url {}, version {}, base {}", differential.getIdElement().getIdPart(), differential.getUrl(), differential.getVersion(), diff --git a/dsf-tools/dsf-tools-test-data-generator/src/main/resources/bundle-templates/test-bundle.xml b/dsf-tools/dsf-tools-test-data-generator/src/main/resources/bundle-templates/test-bundle.xml index ecb0489bd..7347b18d4 100644 --- a/dsf-tools/dsf-tools-test-data-generator/src/main/resources/bundle-templates/test-bundle.xml +++ b/dsf-tools/dsf-tools-test-data-generator/src/main/resources/bundle-templates/test-bundle.xml @@ -123,6 +123,8 @@ + +