From 7f658c9f7687030719313c8fd023412b41c09929 Mon Sep 17 00:00:00 2001 From: Hauke Hund Date: Tue, 19 Nov 2024 14:27:16 +0100 Subject: [PATCH 1/6] fix: different organization roles need to be different codes not codings --- .../src/main/resources/bundle-templates/test-bundle.xml | 2 ++ 1 file changed, 2 insertions(+) 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 @@ + + From 4c9c1b2db486a7e1ca12fbbb8dd57fb57ba18dd7 Mon Sep 17 00:00:00 2001 From: Hauke Hund Date: Tue, 19 Nov 2024 14:29:14 +0100 Subject: [PATCH 2/6] added check to make sure StructureDefinition defines a base definition check needed to avoid NPE in HAPI code --- .../StructureDefinitionAuthorizationRule.java | 4 ++++ .../dev/dsf/fhir/validation/SnapshotGeneratorImpl.java | 10 ++++++++-- 2 files changed, 12 insertions(+), 2 deletions(-) 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-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(), From 7e23ea672e04776288761ec91a6152b4b4ed15c2 Mon Sep 17 00:00:00 2001 From: Hauke Hund Date: Tue, 19 Nov 2024 14:54:33 +0100 Subject: [PATCH 3/6] transaction bundle error message fix --- .../java/dev/dsf/fhir/dao/command/AbstractCommandList.java | 6 ++++-- .../java/dev/dsf/fhir/dao/command/BatchCommandList.java | 6 ++++++ .../dev/dsf/fhir/dao/command/TransactionCommandList.java | 6 ++++++ 3 files changed, 16 insertions(+), 2 deletions(-) diff --git a/dsf-fhir/dsf-fhir-server/src/main/java/dev/dsf/fhir/dao/command/AbstractCommandList.java b/dsf-fhir/dsf-fhir-server/src/main/java/dev/dsf/fhir/dao/command/AbstractCommandList.java index 91995f0d3..cbf187ab0 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"); @@ -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/BatchCommandList.java b/dsf-fhir/dsf-fhir-server/src/main/java/dev/dsf/fhir/dao/command/BatchCommandList.java index c790154c3..c51a480f8 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 @@ -244,4 +244,10 @@ private Consumer postExecute(Connection connection, Map Date: Tue, 19 Nov 2024 15:01:32 +0100 Subject: [PATCH 4/6] added "throws WebApplicationException" to improve method signatures --- .../dsf/fhir/dao/command/AuthorizationHelper.java | 14 +++++++++----- .../fhir/dao/command/AuthorizationHelperImpl.java | 7 +++++-- 2 files changed, 14 insertions(+), 7 deletions(-) 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..fba6bc8c9 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,6 +123,7 @@ 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(); @@ -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 -> From b8923ea1a73efb18d7138f25cfa3a505a8699a2b Mon Sep 17 00:00:00 2001 From: Hauke Hund Date: Tue, 19 Nov 2024 15:11:33 +0100 Subject: [PATCH 5/6] code cleanup --- .../src/main/java/dev/dsf/fhir/dao/command/Command.java | 1 - .../src/main/java/dev/dsf/fhir/dao/command/ReadCommand.java | 6 ------ 2 files changed, 7 deletions(-) diff --git a/dsf-fhir/dsf-fhir-server/src/main/java/dev/dsf/fhir/dao/command/Command.java b/dsf-fhir/dsf-fhir-server/src/main/java/dev/dsf/fhir/dao/command/Command.java index aaf540bf8..3f30dbb94 100755 --- a/dsf-fhir/dsf-fhir-server/src/main/java/dev/dsf/fhir/dao/command/Command.java +++ b/dsf-fhir/dsf-fhir-server/src/main/java/dev/dsf/fhir/dao/command/Command.java @@ -23,7 +23,6 @@ public interface Command default void preExecute(Map idTranslationTable, Connection connection, ValidationHelper validationHelper, SnapshotGenerator snapshotGenerator) - { } 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) From 0cbf256255a714153e76944382abe162131ab974 Mon Sep 17 00:00:00 2001 From: Hauke Hund Date: Tue, 19 Nov 2024 15:38:35 +0100 Subject: [PATCH 6/6] database constraint trigger to ensure resource unique criteria * Unique constraints implemented as constraint trigger run after insert * Constraint trigger functions use postgres advisory transaction locks to ensure uniqueness checks are not executed in parallel * Transaction isolation level of insert/update operations changed from repeatable read to read committed, enabling dirty reads needed to allow constraint triggers to see inserts/updates executed by parallel running transactions * New integration test to validate parallel create operations via transaction and batch bundles as well as direct POSTs --- .../fhir/dao/command/BatchCommandList.java | 44 +- .../fhir/dao/command/CommandFactoryImpl.java | 6 +- .../dao/command/TransactionCommandList.java | 33 +- .../dao/jdbc/AbstractResourceDaoJdbc.java | 13 +- .../dev/dsf/fhir/help/ResponseGenerator.java | 17 + .../impl/AbstractResourceServiceImpl.java | 25 +- .../src/main/resources/db/db.changelog.xml | 2 + .../db.constraint_trigger.changelog-1.6.1.xml | 50 + .../activity_definitions_unique.sql | 13 + .../code_systems_unique.sql | 13 + .../endpoints_unique.sql | 16 + .../naming_systems_unique.sql | 16 + .../organization_affiliations_unique.sql | 25 + .../organizations_unique.sql | 32 + .../structure_definitions_unique.sql | 13 + .../subscriptions_unique.sql | 14 + .../value_sets_unique.sql | 13 + .../integration/AbstractIntegrationTest.java | 10 +- .../ParallelCreateIntegrationTest.java | 1456 +++++++++++++++++ 19 files changed, 1760 insertions(+), 51 deletions(-) create mode 100644 dsf-fhir/dsf-fhir-server/src/main/resources/db/db.constraint_trigger.changelog-1.6.1.xml create mode 100644 dsf-fhir/dsf-fhir-server/src/main/resources/db/unique_trigger_functions/activity_definitions_unique.sql create mode 100644 dsf-fhir/dsf-fhir-server/src/main/resources/db/unique_trigger_functions/code_systems_unique.sql create mode 100644 dsf-fhir/dsf-fhir-server/src/main/resources/db/unique_trigger_functions/endpoints_unique.sql create mode 100644 dsf-fhir/dsf-fhir-server/src/main/resources/db/unique_trigger_functions/naming_systems_unique.sql create mode 100644 dsf-fhir/dsf-fhir-server/src/main/resources/db/unique_trigger_functions/organization_affiliations_unique.sql create mode 100644 dsf-fhir/dsf-fhir-server/src/main/resources/db/unique_trigger_functions/organizations_unique.sql create mode 100644 dsf-fhir/dsf-fhir-server/src/main/resources/db/unique_trigger_functions/structure_definitions_unique.sql create mode 100644 dsf-fhir/dsf-fhir-server/src/main/resources/db/unique_trigger_functions/subscriptions_unique.sql create mode 100644 dsf-fhir/dsf-fhir-server/src/main/resources/db/unique_trigger_functions/value_sets_unique.sql create mode 100644 dsf-fhir/dsf-fhir-server/src/test/java/dev/dsf/fhir/integration/ParallelCreateIntegrationTest.java 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 c51a480f8..2b6f15a90 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.dupicateResourceExists())); + else + caughtExceptions.put(command.getIndex(), e); + try { 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/TransactionCommandList.java b/dsf-fhir/dsf-fhir-server/src/main/java/dev/dsf/fhir/dao/command/TransactionCommandList.java index b76b74e3c..0392b87da 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); @@ -131,7 +138,11 @@ public Bundle execute() throws WebApplicationException e1.getMessage()); } - throw e; + if (e instanceof PSQLException s + && PSQLState.UNIQUE_VIOLATION.getState().equals(s.getSQLState())) + throw new WebApplicationException(responseGenerator.dupicateResourceExists()); + else + throw e; } } @@ -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.dupicateResourceExists()); + else + throw e; + } } } 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..7a655f80d 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 dupicateResourceExists() + { + 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 dupicateResourceExists(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..cddd2049a 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.dupicateResourceExists(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.dupicateResourceExists(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..fd9874101 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,6 @@ + + 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..e96e21fe4 --- /dev/null +++ b/dsf-fhir/dsf-fhir-server/src/main/resources/db/db.constraint_trigger.changelog-1.6.1.xml @@ -0,0 +1,50 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 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 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/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..346b0ce53 --- /dev/null +++ b/dsf-fhir/dsf-fhir-server/src/main/resources/db/unique_trigger_functions/endpoints_unique.sql @@ -0,0 +1,16 @@ +CREATE OR REPLACE FUNCTION endpoints_unique() RETURNS TRIGGER AS $$ +BEGIN + PERFORM pg_advisory_xact_lock(hashtext((NEW.endpoint->>'address') || jsonb_path_query_array(NEW.endpoint, '$.identifier[*] ? (@.system == "http://dsf.dev/sid/endpoint-identifier").value')::text)); + 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') + ))) THEN + RAISE EXCEPTION 'Conflict: Not inserting Endpoint with address % and identifier.value %, resource already exists with given address and 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..170815e2c --- /dev/null +++ b/dsf-fhir/dsf-fhir-server/src/main/resources/db/unique_trigger_functions/naming_systems_unique.sql @@ -0,0 +1,16 @@ +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') + ))) 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..4f617e15a --- /dev/null +++ b/dsf-fhir/dsf-fhir-server/src/main/resources/db/unique_trigger_functions/organization_affiliations_unique.sql @@ -0,0 +1,25 @@ +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(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') + ))) 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..933201e67 --- /dev/null +++ b/dsf-fhir/dsf-fhir-server/src/main/resources/db/unique_trigger_functions/organizations_unique.sql @@ -0,0 +1,32 @@ +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(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') + ))) 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') + ) 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/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..c2757d224 --- /dev/null +++ b/dsf-fhir/dsf-fhir-server/src/test/java/dev/dsf/fhir/integration/ParallelCreateIntegrationTest.java @@ -0,0 +1,1456 @@ +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.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.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/fhir/ActivityDefinition/test-url"; + private static final String ACTIVITY_DEFINITION_VERSION = "test-version"; + + private static final String CODE_SYSTEM_URL = "http://test.com/fhir/CodeSystem/test-url"; + 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-url"; + 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 VALUE_SET_URL = "http://test.com/fhir/ValueSet/test-url"; + 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 testCreateDuplicateSubscriptionViaTransactionBundle() throws Exception + { + Bundle bundle = createBundle(BundleType.TRANSACTION, createSubscription(), null, 2); + + expectForbidden(() -> getWebserviceClient().postBundle(bundle)); + } + + @Test + public void testCreateDuplicateSubscriptionViaBatchBundle() throws Exception + { + Bundle bundle = createBundle(BundleType.BATCH, createSubscription(), 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 testCreateDuplicateValueSetsViaTransactionBundleWithIfNoneExists() 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 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 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(createOperation1, "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 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