From 7f658c9f7687030719313c8fd023412b41c09929 Mon Sep 17 00:00:00 2001 From: Hauke Hund Date: Tue, 19 Nov 2024 14:27:16 +0100 Subject: [PATCH 01/17] 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 02/17] 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 03/17] 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 04/17] 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 05/17] 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 06/17] 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 From 84170d54edd1226c5fefad953d022f83293dd058 Mon Sep 17 00:00:00 2001 From: Hauke Hund Date: Wed, 20 Nov 2024 12:33:57 +0100 Subject: [PATCH 07/17] added missing connection parameter This does not change any behavior as currently no delete authorization rule is dependent on the DB state. --- .../java/dev/dsf/fhir/dao/command/AuthorizationHelperImpl.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 fba6bc8c9..253678626 100644 --- a/dsf-fhir/dsf-fhir-server/src/main/java/dev/dsf/fhir/dao/command/AuthorizationHelperImpl.java +++ b/dsf-fhir/dsf-fhir-server/src/main/java/dev/dsf/fhir/dao/command/AuthorizationHelperImpl.java @@ -130,7 +130,7 @@ public void checkDeleteAllowed(int index, Connection connection, Identity identi final long resourceVersion = oldResource.getIdElement().getVersionIdPartAsLong(); Optional> optRule = getAuthorizationRule(oldResource.getClass()); - optRule.flatMap(rule -> rule.reasonDeleteAllowed(identity, oldResource)).ifPresentOrElse(reason -> + optRule.flatMap(rule -> rule.reasonDeleteAllowed(connection, identity, oldResource)).ifPresentOrElse(reason -> { audit.info("Delete of {}/{}/_history/{} allowed for identity '{}' via bundle at index {}, reason: {}", resourceTypeName, resourceId, resourceVersion, identity.getName(), index, reason); From 283e725dab78c9c7ca2b4a7b6813a9eb9fec63f4 Mon Sep 17 00:00:00 2001 From: Hauke Hund Date: Wed, 20 Nov 2024 12:34:15 +0100 Subject: [PATCH 08/17] code cleanup, removed empty line --- .../main/java/dev/dsf/fhir/authorization/AuthorizationRule.java | 1 - 1 file changed, 1 deletion(-) diff --git a/dsf-fhir/dsf-fhir-server/src/main/java/dev/dsf/fhir/authorization/AuthorizationRule.java b/dsf-fhir/dsf-fhir-server/src/main/java/dev/dsf/fhir/authorization/AuthorizationRule.java index 19cc2a997..5e3459159 100644 --- a/dsf-fhir/dsf-fhir-server/src/main/java/dev/dsf/fhir/authorization/AuthorizationRule.java +++ b/dsf-fhir/dsf-fhir-server/src/main/java/dev/dsf/fhir/authorization/AuthorizationRule.java @@ -98,7 +98,6 @@ public interface AuthorizationRule */ Optional reasonDeleteAllowed(Identity identity, R oldResource); - /** * Override this method for non default behavior. Default: Not allowed. * From 598838f37b42e1c6aa4f50b6397b65f337b73fe8 Mon Sep 17 00:00:00 2001 From: Hauke Hund Date: Wed, 20 Nov 2024 12:34:32 +0100 Subject: [PATCH 09/17] test fix --- .../dev/dsf/fhir/integration/ParallelCreateIntegrationTest.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 index c2757d224..3e61288aa 100644 --- 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 @@ -1232,7 +1232,7 @@ private void testCreateDuplicatesParallel(Runnable createOp Thread t1 = new Thread(createOperation1, "test 1"); t1.setUncaughtExceptionHandler(handler); - Thread t2 = new Thread(createOperation1, "test 2"); + Thread t2 = new Thread(createOperation2, "test 2"); t2.setUncaughtExceptionHandler(handler); t1.start(); From 5a0fe23eb00f098eb43ac5e611612fd9ac6e4a34 Mon Sep 17 00:00:00 2001 From: Hauke Hund Date: Tue, 10 Dec 2024 16:28:44 +0100 Subject: [PATCH 10/17] fixed db lock value and exception message typo --- .../db/unique_trigger_functions/endpoints_unique.sql | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) 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 index 346b0ce53..9e31c7104 100644 --- 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 @@ -1,13 +1,13 @@ 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)); + PERFORM pg_advisory_xact_lock(hashtext(NEW.endpoint->>'address')); IF EXISTS (SELECT 1 FROM current_endpoints WHERE endpoint_id <> NEW.endpoint_id AND (endpoint->>'address' = NEW.endpoint->>'address' OR (jsonb_path_exists(NEW.endpoint, '$.identifier[*] ? (@.system == "http://dsf.dev/sid/endpoint-identifier").value') AND jsonb_path_query_array(endpoint, '$.identifier[*] ? (@.system == "http://dsf.dev/sid/endpoint-identifier").value') @> jsonb_path_query_array(NEW.endpoint, '$.identifier[*] ? (@.system == "http://dsf.dev/sid/endpoint-identifier").value') ))) THEN - RAISE EXCEPTION 'Conflict: Not inserting Endpoint with address % and identifier.value %, resource already exists with given address and identifier.value', + RAISE EXCEPTION 'Conflict: Not inserting Endpoint with address % and identifier.value %, resource already exists with given address or identifier.value', NEW.endpoint->>'address', jsonb_path_query_array(NEW.endpoint, '$.identifier[*] ? (@.system == "http://dsf.dev/sid/endpoint-identifier").value') USING ERRCODE = 'unique_violation'; ELSE RETURN NEW; From 01e6518a2666e2fc0f9f356ba36c25cdffe36ac7 Mon Sep 17 00:00:00 2001 From: Hauke Hund Date: Tue, 10 Dec 2024 16:33:51 +0100 Subject: [PATCH 11/17] delete queries to remove duplicate resources --- .../src/main/resources/db/db.changelog.xml | 1 + ...te_duplicate_resources.changelog-1.6.1.xml | 256 ++++++++++++++++++ 2 files changed, 257 insertions(+) create mode 100644 dsf-fhir/dsf-fhir-server/src/main/resources/db/db.delete_duplicate_resources.changelog-1.6.1.xml 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 fd9874101..d91572e60 100644 --- a/dsf-fhir/dsf-fhir-server/src/main/resources/db/db.changelog.xml +++ b/dsf-fhir/dsf-fhir-server/src/main/resources/db/db.changelog.xml @@ -41,5 +41,6 @@ + diff --git a/dsf-fhir/dsf-fhir-server/src/main/resources/db/db.delete_duplicate_resources.changelog-1.6.1.xml b/dsf-fhir/dsf-fhir-server/src/main/resources/db/db.delete_duplicate_resources.changelog-1.6.1.xml new file mode 100644 index 000000000..75f575e74 --- /dev/null +++ b/dsf-fhir/dsf-fhir-server/src/main/resources/db/db.delete_duplicate_resources.changelog-1.6.1.xml @@ -0,0 +1,256 @@ + + + + + + + + + DELETE FROM activity_definitions WHERE activity_definition_id IN ( + SELECT activity_definition_id FROM ( + SELECT + row_number() OVER ( + PARTITION BY activity_definition->>'url', activity_definition->>'version' + ORDER BY activity_definition->>'url', activity_definition->>'version', (activity_definition->'meta'->>'lastUpdated')::timestamp DESC + ) AS rn + , activity_definition_id + FROM current_activity_definitions + ) AS ad WHERE rn > 1 + ) + + + + + DELETE FROM code_systems + WHERE code_system->'url' IS NOT NULL + AND code_system->'version' IS NOT NULL + AND code_system_id IN ( + SELECT code_system_id FROM ( + SELECT + row_number() OVER ( + PARTITION BY code_system->>'url', code_system->>'version' + ORDER BY code_system->>'url', code_system->>'version', (code_system->'meta'->>'lastUpdated')::timestamp DESC + ) AS rn + , code_system_id + FROM current_code_systems + ) AS cs WHERE rn > 1 + ) + + + + + DELETE FROM endpoints + WHERE endpoint->'address' IS NOT NULL + AND endpoint_id IN ( + SELECT endpoint_id FROM ( + SELECT + row_number() OVER ( + PARTITION BY endpoint->>'address' + ORDER BY endpoint->>'address', (endpoint->'meta'->>'lastUpdated')::timestamp DESC + ) AS rn + , endpoint_id + FROM current_endpoints + ) AS e WHERE rn > 1 + ) + + + + + DELETE FROM endpoints + WHERE jsonb_path_exists(endpoint, '$.identifier[*] ? (@.system == "http://dsf.dev/sid/endpoint-identifier").value') + AND endpoint_id IN ( + SELECT endpoint_id FROM ( + SELECT + row_number() OVER ( + PARTITION BY jsonb_path_query_array(endpoint, '$.identifier[*] ? (@.system == "http://dsf.dev/sid/endpoint-identifier").value') + ORDER BY jsonb_path_query_array(endpoint, '$.identifier[*] ? (@.system == "http://dsf.dev/sid/endpoint-identifier").value'), (endpoint->'meta'->>'lastUpdated')::timestamp DESC + ) AS rn + , endpoint_id + FROM current_endpoints + ) AS e WHERE rn > 1 + ) + + + + + DELETE FROM naming_systems + WHERE naming_system->'name' IS NOT NULL + AND naming_system_id IN ( + SELECT naming_system_id FROM ( + SELECT + row_number() OVER ( + PARTITION BY naming_system->>'name' + ORDER BY naming_system->>'name', (naming_system->'meta'->>'lastUpdated')::timestamp DESC + ) AS rn + , naming_system_id + FROM current_naming_systems + ) AS ns WHERE rn > 1 + ) + + + + + DELETE FROM naming_systems + WHERE jsonb_path_exists(naming_system, '$.uniqueId[*] ? (@.type == "other").value') + AND naming_system_id IN ( + SELECT naming_system_id FROM ( + SELECT + row_number() OVER ( + PARTITION BY jsonb_path_query_array(naming_system, '$.uniqueId[*] ? (@.type == "other").value') + ORDER BY jsonb_path_query_array(naming_system, '$.uniqueId[*] ? (@.type == "other").value'), (naming_system->'meta'->>'lastUpdated')::timestamp DESC + ) AS rn + , naming_system_id + FROM current_naming_systems + ) AS ns WHERE rn > 1 + ) + + + + + DELETE FROM organizations + WHERE jsonb_path_exists(organization, '$.meta.profile[*] ? (@ == "http://dsf.dev/fhir/StructureDefinition/organization-parent")') + AND jsonb_path_exists(organization, '$.identifier[*] ? (@.system == "http://dsf.dev/sid/organization-identifier").value') + AND organization_id IN ( + SELECT organization_id FROM ( + SELECT + row_number() OVER ( + PARTITION BY jsonb_path_query_array(organization, '$.identifier[*] ? (@.system == "http://dsf.dev/sid/organization-identifier").value') + ORDER BY jsonb_path_query_array(organization, '$.identifier[*] ? (@.system == "http://dsf.dev/sid/organization-identifier").value'), (organization->'meta'->>'lastUpdated')::timestamp DESC + ) AS rn + , organization_id + FROM current_organizations + ) AS o WHERE rn > 1 + ) + + + + + DELETE FROM organizations + WHERE jsonb_path_exists(organization, '$.meta.profile[*] ? (@ == "http://dsf.dev/fhir/StructureDefinition/organization")') + AND jsonb_path_exists(organization, '$.identifier[*] ? (@.system == "http://dsf.dev/sid/organization-identifier").value') + AND organization_id IN ( + SELECT organization_id FROM ( + SELECT + row_number() OVER ( + PARTITION BY jsonb_path_query_array(organization, '$.identifier[*] ? (@.system == "http://dsf.dev/sid/organization-identifier").value') + ORDER BY jsonb_path_query_array(organization, '$.identifier[*] ? (@.system == "http://dsf.dev/sid/organization-identifier").value'), (organization->'meta'->>'lastUpdated')::timestamp DESC + ) AS rn + , organization_id + FROM current_organizations + ) AS o WHERE rn > 1 + ) + + + + + DELETE FROM organizations + WHERE jsonb_path_exists(organization, '$.meta.profile[*] ? (@ == "http://dsf.dev/fhir/StructureDefinition/organization")') + AND jsonb_path_exists(organization, '$.extension[*] ? (@.url == "http://dsf.dev/fhir/StructureDefinition/extension-certificate-thumbprint").valueString') + AND organization_id IN ( + SELECT organization_id FROM ( + SELECT + row_number() OVER ( + PARTITION BY jsonb_path_query_array(organization, '$.extension[*] ? (@.url == "http://dsf.dev/fhir/StructureDefinition/extension-certificate-thumbprint").valueString') + ORDER BY jsonb_path_query_array(organization, '$.extension[*] ? (@.url == "http://dsf.dev/fhir/StructureDefinition/extension-certificate-thumbprint").valueString'), (organization->'meta'->>'lastUpdated')::timestamp DESC + ) AS rn + , organization_id + FROM current_organizations + ) AS o WHERE rn > 1 + ) + + + + + DELETE FROM organization_affiliations + WHERE organization_affiliation->'organization'->'reference' IS NOT NULL + AND organization_affiliation->'participatingOrganization'->'reference' IS NOT NULL + AND jsonb_path_exists(organization_affiliation, '$.endpoint[*].reference') + AND organization_affiliation_id IN ( + SELECT organization_affiliation_id FROM ( + SELECT + row_number() OVER ( + PARTITION BY organization_affiliation->'organization'->>'reference', organization_affiliation->'participatingOrganization'->>'reference', jsonb_path_query_array(organization_affiliation, '$.endpoint[*].reference') + ORDER BY organization_affiliation->'organization'->>'reference', organization_affiliation->'participatingOrganization'->>'reference', jsonb_path_query_array(organization_affiliation, '$.endpoint[*].reference'), (organization_affiliation->'meta'->>'lastUpdated')::timestamp DESC + ) AS rn + , organization_affiliation_id + FROM current_organization_affiliations + ) AS oa WHERE rn > 1 + ) + + + + + DELETE FROM organization_affiliations + WHERE organization_affiliation->'organization'->'reference' IS NOT NULL + AND organization_affiliation->'participatingOrganization'->'reference' IS NOT NULL + AND jsonb_path_exists(organization_affiliation, '$.code[*].coding[*] ? (@.system == "http://dsf.dev/fhir/CodeSystem/organization-role").code') + AND organization_affiliation_id IN ( + SELECT organization_affiliation_id FROM ( + SELECT + row_number() OVER ( + PARTITION BY organization_affiliation->'organization'->>'reference', organization_affiliation->'participatingOrganization'->>'reference', jsonb_path_query_array(organization_affiliation, '$.code[*].coding[*] ? (@.system == "http://dsf.dev/fhir/CodeSystem/organization-role").code') + ORDER BY organization_affiliation->'organization'->>'reference', organization_affiliation->'participatingOrganization'->>'reference', jsonb_path_query_array(organization_affiliation, '$.code[*].coding[*] ? (@.system == "http://dsf.dev/fhir/CodeSystem/organization-role").code'), (organization_affiliation->'meta'->>'lastUpdated')::timestamp DESC + ) AS rn + , organization_affiliation_id + FROM current_organization_affiliations + ) AS oa WHERE rn > 1 + ) + + + + + DELETE FROM structure_definition_snapshots + WHERE structure_definition_snapshot->'url' IS NOT NULL + AND structure_definition_snapshot->'version' IS NOT NULL + AND structure_definition_snapshot_id IN ( + SELECT structure_definition_snapshot_id FROM ( + SELECT + row_number() OVER ( + PARTITION BY structure_definition_snapshot->>'url', structure_definition_snapshot->>'version' + ORDER BY structure_definition_snapshot->>'url', structure_definition_snapshot->>'version', (structure_definition_snapshot->'meta'->>'lastUpdated')::timestamp DESC + ) AS rn + , structure_definition_snapshot_id + FROM current_structure_definition_snapshots + ) AS sd WHERE rn > 1 + ) + + + + + DELETE FROM structure_definitions + WHERE structure_definition->'url' IS NOT NULL + AND structure_definition->'version' IS NOT NULL + AND structure_definition_id IN ( + SELECT structure_definition_id FROM ( + SELECT + row_number() OVER ( + PARTITION BY structure_definition->>'url', structure_definition->>'version' + ORDER BY structure_definition->>'url', structure_definition->>'version', (structure_definition->'meta'->>'lastUpdated')::timestamp DESC + ) AS rn + , structure_definition_id + FROM current_structure_definitions + ) AS sd WHERE rn > 1 + ) + + + + + DELETE FROM value_sets + WHERE value_set->'url' IS NOT NULL + AND value_set->'version' IS NOT NULL + AND value_set_id IN ( + SELECT value_set_id FROM ( + SELECT + row_number() OVER ( + PARTITION BY value_set->>'url', value_set->>'version' + ORDER BY value_set->>'url', value_set->>'version', (value_set->'meta'->>'lastUpdated')::timestamp DESC + ) AS rn + , value_set_id + FROM current_value_sets + ) AS vs WHERE rn > 1 + ) + + + \ No newline at end of file From f45177bbb89e9de098e28b2faf3910d83706f0e5 Mon Sep 17 00:00:00 2001 From: Hauke Hund Date: Mon, 16 Dec 2024 16:45:46 +0100 Subject: [PATCH 12/17] debug/audit log typo fixes --- .../dev/dsf/fhir/dao/command/AbstractCommandList.java | 8 ++++---- .../dev/dsf/fhir/dao/command/TransactionCommandList.java | 4 ++-- 2 files changed, 6 insertions(+), 6 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 cbf187ab0..33b020d94 100644 --- a/dsf-fhir/dsf-fhir-server/src/main/java/dev/dsf/fhir/dao/command/AbstractCommandList.java +++ b/dsf-fhir/dsf-fhir-server/src/main/java/dev/dsf/fhir/dao/command/AbstractCommandList.java @@ -78,22 +78,22 @@ protected void auditLogAbbort(Command command) { if (command instanceof DeleteCommand) { - audit.info("Delete of {} for identity '{}' via bundle at index {} abborted", command.getResourceTypeName(), + audit.info("Delete of {} for identity '{}' via bundle at index {} aborted", command.getResourceTypeName(), command.getIdentity().getName(), command.getIndex()); } else if (command instanceof CreateCommand) { - audit.info("Create of {} for identity '{}' via bundle at index {} abborted", command.getResourceTypeName(), + audit.info("Create of {} for identity '{}' via bundle at index {} aborted", command.getResourceTypeName(), command.getIdentity().getName(), command.getIndex()); } else if (command instanceof UpdateCommand) { - audit.info("Update of {} for identity '{}' via bundle at index {} abborted", command.getResourceTypeName(), + audit.info("Update of {} for identity '{}' via bundle at index {} aborted", command.getResourceTypeName(), command.getIdentity().getName(), command.getIndex()); } else if (command instanceof ReadCommand r) { - audit.info("{} of {} for identity '{}' via bundle at index {} abborted", r.isSearch() ? "Search" : "Read", + audit.info("{} of {} for identity '{}' via bundle at index {} aborted", r.isSearch() ? "Search" : "Read", command.getResourceTypeName(), command.getIdentity().getName(), command.getIndex()); } } 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 0392b87da..7318ab957 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 @@ -82,10 +82,10 @@ public Bundle execute() throws WebApplicationException catch (Exception e) { logger.debug( - "Error while running pre-execute of command {} for entry at index {}, abborting transaction", + "Error while running pre-execute of command {} for entry at index {}, aborting transaction", c.getClass().getSimpleName(), c.getIndex(), e); logger.warn( - "Error while running pre-execute of command {} for entry at index {}, abborting transaction: {} - {}", + "Error while running pre-execute of command {} for entry at index {}, aborting transaction: {} - {}", c.getClass().getSimpleName(), c.getIndex(), e.getClass().getName(), e.getMessage()); try From 5dd67ea5e82b91faf721a508cd0642086bd7e11a Mon Sep 17 00:00:00 2001 From: Hauke Hund Date: Mon, 16 Dec 2024 16:59:47 +0100 Subject: [PATCH 13/17] improved audit log message log message improved by early wrapping of PSQLException --- .../dsf/fhir/dao/command/TransactionCommandList.java | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/dsf-fhir/dsf-fhir-server/src/main/java/dev/dsf/fhir/dao/command/TransactionCommandList.java b/dsf-fhir/dsf-fhir-server/src/main/java/dev/dsf/fhir/dao/command/TransactionCommandList.java index 7318ab957..4dc591044 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 @@ -126,6 +126,10 @@ public Bundle execute() throws WebApplicationException connection.rollback(); } + if (e instanceof PSQLException s + && PSQLState.UNIQUE_VIOLATION.getState().equals(s.getSQLState())) + e = new WebApplicationException(e, responseGenerator.dupicateResourceExists()); + try { commands.stream().limit(c.getIndex()).forEach(this::auditLogAbbort); @@ -138,11 +142,7 @@ public Bundle execute() throws WebApplicationException e1.getMessage()); } - if (e instanceof PSQLException s - && PSQLState.UNIQUE_VIOLATION.getState().equals(s.getSQLState())) - throw new WebApplicationException(responseGenerator.dupicateResourceExists()); - else - throw e; + throw e; } } From 2d2ab801c20325ee161e97105d0424fce28d1a93 Mon Sep 17 00:00:00 2001 From: Hauke Hund Date: Mon, 16 Dec 2024 17:33:35 +0100 Subject: [PATCH 14/17] improved unique constraints contains operators @> and <@ only work in one direction, added missing directions to complete intersection checks --- .../endpoints_unique.sql | 17 ++++++-- .../naming_systems_unique.sql | 17 ++++++-- .../organization_affiliations_unique.sql | 27 ++++++++---- .../organizations_unique.sql | 42 ++++++++++++++----- 4 files changed, 77 insertions(+), 26 deletions(-) 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 index 9e31c7104..6debb6292 100644 --- 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 @@ -2,11 +2,20 @@ CREATE OR REPLACE FUNCTION endpoints_unique() RETURNS TRIGGER AS $$ BEGIN PERFORM pg_advisory_xact_lock(hashtext(NEW.endpoint->>'address')); IF EXISTS (SELECT 1 FROM current_endpoints WHERE endpoint_id <> NEW.endpoint_id - AND (endpoint->>'address' = NEW.endpoint->>'address' - OR (jsonb_path_exists(NEW.endpoint, '$.identifier[*] ? (@.system == "http://dsf.dev/sid/endpoint-identifier").value') - AND jsonb_path_query_array(endpoint, '$.identifier[*] ? (@.system == "http://dsf.dev/sid/endpoint-identifier").value') @> + 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 + ) OR ( + jsonb_path_exists(endpoint, '$.identifier[*] ? (@.system == "http://dsf.dev/sid/endpoint-identifier").value') + AND + jsonb_path_query_array(NEW.endpoint, '$.identifier[*] ? (@.system == "http://dsf.dev/sid/endpoint-identifier").value') @> + jsonb_path_query_array(endpoint, '$.identifier[*] ? (@.system == "http://dsf.dev/sid/endpoint-identifier").value') + ) + )) THEN RAISE EXCEPTION 'Conflict: Not inserting Endpoint with address % and identifier.value %, resource already exists with given address or identifier.value', NEW.endpoint->>'address', jsonb_path_query_array(NEW.endpoint, '$.identifier[*] ? (@.system == "http://dsf.dev/sid/endpoint-identifier").value') USING ERRCODE = 'unique_violation'; ELSE 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 index 170815e2c..a6a504c0b 100644 --- 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 @@ -2,11 +2,20 @@ 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') @> + 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 + ) OR ( + jsonb_path_exists(naming_system, '$.uniqueId[*] ? (@.type == "other").value') + AND + jsonb_path_query_array(NEW.naming_system, '$.uniqueId[*] ? (@.type == "other").value') @> + jsonb_path_query_array(naming_system, '$.uniqueId[*] ? (@.type == "other").value') + ) + )) THEN RAISE EXCEPTION 'Conflict: Not inserting NamingSystem with name % and uniqueId.value %, resource already exists with given name or uniqueId.value', NEW.naming_system->>'name', jsonb_path_query_array(NEW.naming_system, '$.uniqueId[*] ? (@.type == "other").value') USING ERRCODE = 'unique_violation'; ELSE 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 index 4f617e15a..f3a0a2542 100644 --- 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 @@ -4,15 +4,28 @@ BEGIN 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') @> + 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') @> + ) OR ( + jsonb_path_exists(organization_affiliation, '$.endpoint[*].reference') + AND + jsonb_path_query_array(NEW.organization_affiliation, '$.endpoint[*].reference') @> + jsonb_path_query_array(organization_affiliation, '$.endpoint[*].reference') + ) OR ( + jsonb_path_exists(NEW.organization_affiliation, '$.code[*].coding[*] ? (@.system == "http://dsf.dev/fhir/CodeSystem/organization-role").code') + AND + jsonb_path_query_array(organization_affiliation, '$.code[*].coding[*] ? (@.system == "http://dsf.dev/fhir/CodeSystem/organization-role").code') @> jsonb_path_query_array(NEW.organization_affiliation, '$.code[*].coding[*] ? (@.system == "http://dsf.dev/fhir/CodeSystem/organization-role").code') - ))) THEN + ) OR ( + jsonb_path_exists(organization_affiliation, '$.code[*].coding[*] ? (@.system == "http://dsf.dev/fhir/CodeSystem/organization-role").code') + AND + jsonb_path_query_array(NEW.organization_affiliation, '$.code[*].coding[*] ? (@.system == "http://dsf.dev/fhir/CodeSystem/organization-role").code') @> + jsonb_path_query_array(organization_affiliation, '$.code[*].coding[*] ? (@.system == "http://dsf.dev/fhir/CodeSystem/organization-role").code') + ) + )) THEN RAISE EXCEPTION 'Conflict: Not inserting OrganizationAffiliation with parent organization %, member organization %, endpoint % and roles %, resource already exists with parent organization, member organization and endpoint or roles', NEW.organization_affiliation->'organization'->>'reference', NEW.organization_affiliation->'participatingOrganization'->>'reference', 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 index 933201e67..51bb32f81 100644 --- 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 @@ -4,25 +4,45 @@ BEGIN 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_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') @> + ) OR ( + jsonb_path_exists(organization, '$.identifier[*] ? (@.system == "http://dsf.dev/sid/organization-identifier").value') + AND + jsonb_path_query_array(NEW.organization, '$.identifier[*] ? (@.system == "http://dsf.dev/sid/organization-identifier").value') @> + jsonb_path_query_array(organization, '$.identifier[*] ? (@.system == "http://dsf.dev/sid/organization-identifier").value') + ) OR ( + jsonb_path_exists(NEW.organization, '$.extension[*] ? (@.url == "http://dsf.dev/fhir/StructureDefinition/extension-certificate-thumbprint").valueString') + AND + jsonb_path_query_array(organization, '$.extension[*] ? (@.url == "http://dsf.dev/fhir/StructureDefinition/extension-certificate-thumbprint").valueString') @> jsonb_path_query_array(NEW.organization, '$.extension[*] ? (@.url == "http://dsf.dev/fhir/StructureDefinition/extension-certificate-thumbprint").valueString') - ))) THEN + ) OR ( + jsonb_path_exists(organization, '$.extension[*] ? (@.url == "http://dsf.dev/fhir/StructureDefinition/extension-certificate-thumbprint").valueString') + AND + jsonb_path_query_array(NEW.organization, '$.extension[*] ? (@.url == "http://dsf.dev/fhir/StructureDefinition/extension-certificate-thumbprint").valueString') @> + jsonb_path_query_array(organization, '$.extension[*] ? (@.url == "http://dsf.dev/fhir/StructureDefinition/extension-certificate-thumbprint").valueString') + ) + )) THEN RAISE EXCEPTION 'Conflict: Not inserting member Organization with thumbprint % and identifier.value %, resource already exists with given thumbprint or identifier.value', jsonb_path_query_array(NEW.organization, '$.extension[*] ? (@.url == "http://dsf.dev/fhir/StructureDefinition/extension-certificate-thumbprint").valueString'), jsonb_path_query_array(NEW.organization, '$.identifier[*] ? (@.system == "http://dsf.dev/sid/organization-identifier").value') USING ERRCODE = 'unique_violation'; ELSIF jsonb_path_exists(NEW.organization, '$.meta.profile[*] ? (@ == "http://dsf.dev/fhir/StructureDefinition/organization-parent")') AND EXISTS (SELECT 1 FROM current_organizations WHERE organization_id <> NEW.organization_id - AND jsonb_path_exists(NEW.organization, '$.identifier[*] ? (@.system == "http://dsf.dev/sid/organization-identifier").value') - AND jsonb_path_query_array(organization, '$.identifier[*] ? (@.system == "http://dsf.dev/sid/organization-identifier").value') @> - jsonb_path_query_array(NEW.organization, '$.identifier[*] ? (@.system == "http://dsf.dev/sid/organization-identifier").value') - ) THEN + AND (( + jsonb_path_exists(NEW.organization, '$.identifier[*] ? (@.system == "http://dsf.dev/sid/organization-identifier").value') + AND + jsonb_path_query_array(organization, '$.identifier[*] ? (@.system == "http://dsf.dev/sid/organization-identifier").value') @> + jsonb_path_query_array(NEW.organization, '$.identifier[*] ? (@.system == "http://dsf.dev/sid/organization-identifier").value') + ) OR ( + jsonb_path_exists(organization, '$.identifier[*] ? (@.system == "http://dsf.dev/sid/organization-identifier").value') + AND + jsonb_path_query_array(NEW.organization, '$.identifier[*] ? (@.system == "http://dsf.dev/sid/organization-identifier").value') @> + jsonb_path_query_array(organization, '$.identifier[*] ? (@.system == "http://dsf.dev/sid/organization-identifier").value') + ) + )) THEN RAISE EXCEPTION 'Conflict: Not inserting parent Organization with identifier.value %, resource already exists with identifier.value', jsonb_path_query_array(NEW.organization, '$.identifier[*] ? (@.system == "http://dsf.dev/sid/organization-identifier").value') USING ERRCODE = 'unique_violation'; ELSE From b4344703cb51915198e1c1995b4773490e289d06 Mon Sep 17 00:00:00 2001 From: Hauke Hund Date: Mon, 16 Dec 2024 17:47:29 +0100 Subject: [PATCH 15/17] fixed typos, added missing tests, improved test constants --- .../ParallelCreateIntegrationTest.java | 34 +++++++++++++++---- 1 file changed, 27 insertions(+), 7 deletions(-) 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 index 3e61288aa..bcf347cbb 100644 --- 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 @@ -71,10 +71,10 @@ 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 ACTIVITY_DEFINITION_URL = "http://test.com/bpe/Process/test"; + private static final String ACTIVITY_DEFINITION_VERSION = "1.6"; - private static final String CODE_SYSTEM_URL = "http://test.com/fhir/CodeSystem/test-url"; + private static final String CODE_SYSTEM_URL = "http://test.com/fhir/CodeSystem/test"; private static final String CODE_SYSTEM_VERSION = "test-version"; private static final String ENDPOINT_IDENTIFIER_VALUE = "endpoint.test.org"; @@ -86,14 +86,14 @@ public class ParallelCreateIntegrationTest extends AbstractIntegrationTest 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_URL = "http://test.com/fhir/StructureDefinition/test"; private static final String STRUCTURE_DEFINITION_VERSION = "test-version"; private static final String SUBSCRIPTION_CRITERIA = "Patient"; private static final SubscriptionChannelType SUBSCRIPTION_CHANNEL_TYPE = SubscriptionChannelType.WEBSOCKET; private static final String SUBSCRIPTION_CHANNEL_PAYLOAD = "application/fhir+json"; - private static final String VALUE_SET_URL = "http://test.com/fhir/ValueSet/test-url"; + private static final String VALUE_SET_URL = "http://test.com/fhir/ValueSet/test"; private static final String VALUE_SET_VERSION = "test-version"; private void checkReturnBatchBundle(Bundle b) @@ -303,7 +303,7 @@ public void testCreateDuplicateStructureDefinitionsViaBatchBundle() throws Excep } @Test - public void testCreateDuplicateSubscriptionViaTransactionBundle() throws Exception + public void testCreateDuplicateSubscriptionsViaTransactionBundle() throws Exception { Bundle bundle = createBundle(BundleType.TRANSACTION, createSubscription(), null, 2); @@ -311,7 +311,7 @@ public void testCreateDuplicateSubscriptionViaTransactionBundle() throws Excepti } @Test - public void testCreateDuplicateSubscriptionViaBatchBundle() throws Exception + public void testCreateDuplicateSubscriptionsViaBatchBundle() throws Exception { Bundle bundle = createBundle(BundleType.BATCH, createSubscription(), null, 2); @@ -563,8 +563,28 @@ public void testCreateDuplicateSubscriptionsViaTransactionBundleWithIfNoneExists testCreateDuplicatesViaBundleWithIfNoneExists(bundle, BundleType.TRANSACTIONRESPONSE); } + @Test + public void testCreateDuplicateSubscriptionsViaBatchBundleWithIfNoneExists() throws Exception + { + Bundle bundle = createBundle(BundleType.BATCH, createSubscription(), + (s, r) -> r + .setIfNoneExist("criteria=" + s.getCriteria() + "&type=" + s.getChannel().getType().toCode()), + 2); + + testCreateDuplicatesViaBundleWithIfNoneExists(bundle, BundleType.BATCHRESPONSE); + } + @Test public void testCreateDuplicateValueSetsViaTransactionBundleWithIfNoneExists() throws Exception + { + Bundle bundle = createBundle(BundleType.TRANSACTION, createValueSet(), + (vS, r) -> r.setIfNoneExist("url=" + vS.getUrl() + "&version=" + vS.getVersion()), 2); + + testCreateDuplicatesViaBundleWithIfNoneExists(bundle, BundleType.TRANSACTIONRESPONSE); + } + + @Test + public void testCreateDuplicateValueSetsViaBatchBundleWithIfNoneExists() throws Exception { Bundle bundle = createBundle(BundleType.BATCH, createValueSet(), (vS, r) -> r.setIfNoneExist("url=" + vS.getUrl() + "&version=" + vS.getVersion()), 2); From 56d694318436fe9f8187763354a0e297c5fe3522 Mon Sep 17 00:00:00 2001 From: Hauke Hund Date: Mon, 16 Dec 2024 17:50:30 +0100 Subject: [PATCH 16/17] unique draft Task rules, db constraint, duplicate delete query and tests --- .../authorization/TaskAuthorizationRule.java | 119 ++++++++++++--- .../db.constraint_trigger.changelog-1.6.1.xml | 4 + ...te_duplicate_resources.changelog-1.6.1.xml | 18 +++ .../unique_trigger_functions/tasks_unique.sql | 27 ++++ .../ParallelCreateIntegrationTest.java | 139 ++++++++++++++++++ 5 files changed, 289 insertions(+), 18 deletions(-) create mode 100644 dsf-fhir/dsf-fhir-server/src/main/resources/db/unique_trigger_functions/tasks_unique.sql diff --git a/dsf-fhir/dsf-fhir-server/src/main/java/dev/dsf/fhir/authorization/TaskAuthorizationRule.java b/dsf-fhir/dsf-fhir-server/src/main/java/dev/dsf/fhir/authorization/TaskAuthorizationRule.java index 96fb15633..e23b70492 100644 --- a/dsf-fhir/dsf-fhir-server/src/main/java/dev/dsf/fhir/authorization/TaskAuthorizationRule.java +++ b/dsf-fhir/dsf-fhir-server/src/main/java/dev/dsf/fhir/authorization/TaskAuthorizationRule.java @@ -4,6 +4,7 @@ import java.sql.SQLException; import java.util.ArrayList; import java.util.List; +import java.util.Map; import java.util.Objects; import java.util.Optional; import java.util.function.Predicate; @@ -15,6 +16,7 @@ import org.hl7.fhir.r4.model.ActivityDefinition; import org.hl7.fhir.r4.model.CanonicalType; import org.hl7.fhir.r4.model.Coding; +import org.hl7.fhir.r4.model.Identifier; import org.hl7.fhir.r4.model.Organization; import org.hl7.fhir.r4.model.Resource; import org.hl7.fhir.r4.model.StringType; @@ -34,6 +36,10 @@ import dev.dsf.fhir.dao.TaskDao; import dev.dsf.fhir.dao.provider.DaoProvider; import dev.dsf.fhir.help.ParameterConverter; +import dev.dsf.fhir.search.PageAndCount; +import dev.dsf.fhir.search.PartialResult; +import dev.dsf.fhir.search.SearchQuery; +import dev.dsf.fhir.search.SearchQueryParameterError; import dev.dsf.fhir.service.ReferenceResolver; import dev.dsf.fhir.service.ResourceReference; @@ -88,10 +94,20 @@ public Optional reasonCreateAllowed(Connection connection, Identity iden Optional errors = draftTaskOk(connection, identity, newResource); if (errors.isEmpty()) { - logger.info("Create of Task authorized for local organization identity '{}', Task.status draft", - identity.getName()); + if (!draftTaskExists(connection, newResource)) + { + logger.info( + "Create of Task authorized for local organization identity '{}', Task.status draft", + identity.getName()); + + return Optional.of("Local identity, Task.status draft"); + } + else + { + logger.warn("Create of Task unauthorized, unique resource already exists"); - return Optional.of("Local identity, Task.status draft"); + return Optional.empty(); + } } else { @@ -284,9 +300,16 @@ private Optional draftTaskOk(Connection connection, Identity identity, T { List errors = new ArrayList<>(); - if (newResource.getIdentifier().stream().noneMatch(i -> NAMING_SYSTEM_TASK_IDENTIFIER.equals(i.getSystem()))) + if (newResource.getIdentifier().stream().filter(i -> NAMING_SYSTEM_TASK_IDENTIFIER.equals(i.getSystem())) + .count() != 1) + { + errors.add("Task.identifier[" + NAMING_SYSTEM_TASK_IDENTIFIER + "] not defined or more than once"); + } + else if (newResource.getIdentifier().stream().filter(i -> NAMING_SYSTEM_TASK_IDENTIFIER.equals(i.getSystem())) + .findFirst().filter(Identifier::hasValueElement).map(Identifier::getValueElement) + .filter(StringType::hasValue).isEmpty()) { - errors.add("Task.identifier[" + NAMING_SYSTEM_TASK_IDENTIFIER + "] missing"); + errors.add("Task.identifier[" + NAMING_SYSTEM_TASK_IDENTIFIER + "] value not defined"); } if (newResource.hasRequester()) @@ -399,6 +422,49 @@ private Stream getMessageNames(Task newResource) .filter(s -> !s.isBlank()); } + /** + * A draft {@link Task} with identifier (system {@link #NAMING_SYSTEM_TASK_IDENTIFIER}) may not exist + * + * @param connection + * not null + * @param newResource + * not null + * @return true if the given draft Task is unique + */ + private boolean draftTaskExists(Connection connection, Task newResource) + { + SearchQuery query = getDao().createSearchQueryWithoutUserFilter(PageAndCount.exists()) + .configureParameters(Map.of("identifier", + List.of(NAMING_SYSTEM_TASK_IDENTIFIER + "|" + getDraftTaskIdentifierValue(newResource)))); + + List uQp = query.getUnsupportedQueryParameters(); + if (!uQp.isEmpty()) + { + logger.warn("Unable to search for Task: Unsupported query parameters: {}", uQp); + + throw new IllegalStateException("Unable to search for Task: Unsupported query parameters"); + } + + try + { + PartialResult result = getDao().searchWithTransaction(connection, query); + return result.getTotal() >= 1; + } + catch (SQLException e) + { + logger.debug("Unable to search for Task", e); + logger.warn("Unable to search for Task: {} - {}", e.getClass().getName(), e.getMessage()); + + throw new RuntimeException("Unable to search for Task", e); + } + } + + private String getDraftTaskIdentifierValue(Task newResource) + { + return newResource.getIdentifier().stream().filter(i -> NAMING_SYSTEM_TASK_IDENTIFIER.equals(i.getSystem())) + .findFirst().map(Identifier::getValue).get(); + } + private boolean taskAllowedForRequesterAndRecipient(Connection connection, Identity requester, Task newResource) { Optional recipientOpt = organizationProvider.getLocalOrganizationAsIdentity(); @@ -549,24 +615,24 @@ public Optional reasonReadAllowed(Connection connection, Identity identi if (identity.hasDsfRole(FhirServerRole.READ)) { - if (isCurrentIdentityPartOfReferencedOrganization(connection, identity, "Task.requester", - existingResource.getRequester())) + if (identity.isLocalIdentity() && isCurrentIdentityPartOfReferencedOrganization(connection, identity, + "Task.restriction.recipient", existingResource.getRestriction().getRecipientFirstRep())) { logger.info( - "Read of Task/{}/_history/{} authorized for identity '{}', Task.requester reference could be resolved and current identity part of referenced organization", + "Read of Task/{}/_history/{} authorized for identity '{}', Task.restriction.recipient reference could be resolved and current identity part of referenced organization", resourceId, resourceVersion, identity.getName()); - return Optional.of("Task.requester resolved and identity part of referenced organization"); + return Optional + .of("Task.restriction.recipient resolved and local identity part of referenced organization"); } - else if (identity.isLocalIdentity() && isCurrentIdentityPartOfReferencedOrganization(connection, identity, - "Task.restriction.recipient", existingResource.getRestriction().getRecipientFirstRep())) + else if (isCurrentIdentityPartOfReferencedOrganization(connection, identity, "Task.requester", + existingResource.getRequester())) { logger.info( - "Read of Task/{}/_history/{} authorized for identity '{}', Task.restriction.recipient reference could be resolved and current identity part of referenced organization", + "Read of Task/{}/_history/{} authorized for identity '{}', Task.requester reference could be resolved and current identity part of referenced organization", resourceId, resourceVersion, identity.getName()); - return Optional - .of("Task.restriction.recipient resolved and local identity part of referenced organization"); + return Optional.of("Task.requester resolved and identity part of referenced organization"); } else { @@ -605,11 +671,23 @@ public Optional reasonUpdateAllowed(Connection connection, Identity iden Optional errors = draftTaskOk(connection, identity, newResource); if (errors.isEmpty()) { - logger.info("Update of Task/{}/_history/{} ({} -> {}) authorized for local identity '{}'", - oldResourceId, oldResourceVersion, TaskStatus.DRAFT.toCode(), TaskStatus.DRAFT.toCode(), - identity.getName()); + if (draftTaskIdentifierSame(oldResource, newResource)) + { + logger.info("Update of Task/{}/_history/{} ({} -> {}) authorized for local identity '{}'", + oldResourceId, oldResourceVersion, TaskStatus.DRAFT.toCode(), + TaskStatus.DRAFT.toCode(), identity.getName()); - return Optional.of("Local identity, old Task.status draft, new Task.status draft"); + return Optional.of("Local identity, old Task.status draft, new Task.status draft"); + } + else + { + logger.warn( + "Update of Task/{}/_history/{} ({} -> {}) unauthorized for identity '{}' - identifier modified", + oldResourceId, oldResourceVersion, TaskStatus.DRAFT.toCode(), + TaskStatus.DRAFT.toCode(), identity.getName()); + + return Optional.empty(); + } } else { @@ -790,6 +868,11 @@ else if (TaskStatus.INPROGRESS.equals(oldResource.getStatus()) } } + private boolean draftTaskIdentifierSame(Task oldResource, Task newResource) + { + return Objects.equals(getDraftTaskIdentifierValue(oldResource), getDraftTaskIdentifierValue(newResource)); + } + private Optional reasonNotSame(Task oldResource, Task newResource) { List errors = new ArrayList<>(); diff --git a/dsf-fhir/dsf-fhir-server/src/main/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 index e96e21fe4..0ba9ae4eb 100644 --- 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 @@ -30,6 +30,9 @@ + + + @@ -44,6 +47,7 @@ CREATE CONSTRAINT TRIGGER organization_affiliations_unique AFTER INSERT ON organization_affiliations FOR EACH ROW EXECUTE PROCEDURE organization_affiliations_unique(); CREATE CONSTRAINT TRIGGER structure_definitions_unique AFTER INSERT ON structure_definitions FOR EACH ROW EXECUTE PROCEDURE structure_definitions_unique(); CREATE CONSTRAINT TRIGGER subscriptions_unique AFTER INSERT ON subscriptions FOR EACH ROW EXECUTE PROCEDURE subscriptions_unique(); + CREATE CONSTRAINT TRIGGER tasks_unique AFTER INSERT ON tasks FOR EACH ROW EXECUTE PROCEDURE tasks_unique(); CREATE CONSTRAINT TRIGGER value_sets_unique AFTER INSERT ON value_sets FOR EACH ROW EXECUTE PROCEDURE value_sets_unique(); diff --git a/dsf-fhir/dsf-fhir-server/src/main/resources/db/db.delete_duplicate_resources.changelog-1.6.1.xml b/dsf-fhir/dsf-fhir-server/src/main/resources/db/db.delete_duplicate_resources.changelog-1.6.1.xml index 75f575e74..6a15cf8a6 100644 --- a/dsf-fhir/dsf-fhir-server/src/main/resources/db/db.delete_duplicate_resources.changelog-1.6.1.xml +++ b/dsf-fhir/dsf-fhir-server/src/main/resources/db/db.delete_duplicate_resources.changelog-1.6.1.xml @@ -235,6 +235,24 @@ ) + + + DELETE FROM tasks + WHERE task->>'status' = 'draft' + AND jsonb_path_exists(task, '$.identifier[*] ? (@.system == "http://dsf.dev/sid/task-identifier").value') + AND task_id IN ( + SELECT task_id FROM ( + SELECT + row_number() OVER ( + PARTITION BY jsonb_path_query_array(task, '$.identifier[*] ? (@.system == "http://dsf.dev/sid/task-identifier").value') + ORDER BY jsonb_path_query_array(task, '$.identifier[*] ? (@.system == "http://dsf.dev/sid/task-identifier").value'), (task->'meta'->>'lastUpdated')::timestamp DESC + ) AS rn + , task_id + FROM current_tasks + ) AS t WHERE rn > 1 + ) + + DELETE FROM value_sets diff --git a/dsf-fhir/dsf-fhir-server/src/main/resources/db/unique_trigger_functions/tasks_unique.sql b/dsf-fhir/dsf-fhir-server/src/main/resources/db/unique_trigger_functions/tasks_unique.sql new file mode 100644 index 000000000..564ebd92a --- /dev/null +++ b/dsf-fhir/dsf-fhir-server/src/main/resources/db/unique_trigger_functions/tasks_unique.sql @@ -0,0 +1,27 @@ +CREATE OR REPLACE FUNCTION tasks_unique() RETURNS TRIGGER AS $$ +BEGIN + IF NEW.task->>'status' = 'draft' THEN + PERFORM pg_advisory_xact_lock(hashtext(jsonb_path_query_array(NEW.task, '$.identifier[*] ? (@.system == "http://dsf.dev/sid/task-identifier").value')::text)); + IF EXISTS (SELECT 1 FROM current_tasks WHERE task_id <> NEW.task_id + AND (( + jsonb_path_exists(NEW.task, '$.identifier[*] ? (@.system == "http://dsf.dev/sid/task-identifier").value') + AND + jsonb_path_query_array(task, '$.identifier[*] ? (@.system == "http://dsf.dev/sid/task-identifier").value') @> + jsonb_path_query_array(NEW.task, '$.identifier[*] ? (@.system == "http://dsf.dev/sid/task-identifier").value') + ) OR ( + jsonb_path_exists(task, '$.identifier[*] ? (@.system == "http://dsf.dev/sid/task-identifier").value') + AND + jsonb_path_query_array(NEW.task, '$.identifier[*] ? (@.system == "http://dsf.dev/sid/task-identifier").value') @> + jsonb_path_query_array(task, '$.identifier[*] ? (@.system == "http://dsf.dev/sid/task-identifier").value') + ) + )) THEN + RAISE EXCEPTION 'Conflict: Not inserting Task with identifier.value %, resource already exists with given identifier.value', + jsonb_path_query_array(NEW.task, '$.identifier[*] ? (@.system == "http://dsf.dev/sid/task-identifier").value') USING ERRCODE = 'unique_violation'; + ELSE + RETURN NEW; + END IF; + ELSE + RETURN NEW; + END IF; +END; +$$ LANGUAGE PLPGSQL \ No newline at end of file diff --git a/dsf-fhir/dsf-fhir-server/src/test/java/dev/dsf/fhir/integration/ParallelCreateIntegrationTest.java b/dsf-fhir/dsf-fhir-server/src/test/java/dev/dsf/fhir/integration/ParallelCreateIntegrationTest.java index bcf347cbb..0574bfedb 100644 --- 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 @@ -48,6 +48,9 @@ import org.hl7.fhir.r4.model.Subscription.SubscriptionChannelComponent; import org.hl7.fhir.r4.model.Subscription.SubscriptionChannelType; import org.hl7.fhir.r4.model.Subscription.SubscriptionStatus; +import org.hl7.fhir.r4.model.Task; +import org.hl7.fhir.r4.model.Task.TaskIntent; +import org.hl7.fhir.r4.model.Task.TaskStatus; import org.hl7.fhir.r4.model.ValueSet; import org.junit.Test; import org.slf4j.Logger; @@ -63,6 +66,7 @@ import dev.dsf.fhir.dao.OrganizationDao; import dev.dsf.fhir.dao.ResourceDao; import dev.dsf.fhir.dao.SubscriptionDao; +import dev.dsf.fhir.dao.TaskDao; import dev.dsf.fhir.dao.ValueSetDao; import dev.dsf.fhir.dao.jdbc.StructureDefinitionDaoJdbc; import jakarta.ws.rs.WebApplicationException; @@ -93,6 +97,10 @@ public class ParallelCreateIntegrationTest extends AbstractIntegrationTest private static final SubscriptionChannelType SUBSCRIPTION_CHANNEL_TYPE = SubscriptionChannelType.WEBSOCKET; private static final String SUBSCRIPTION_CHANNEL_PAYLOAD = "application/fhir+json"; + private static final String NAMING_SYSTEM_TASK_IDENTIFIER = "http://dsf.dev/sid/task-identifier"; + private static final String TASK_IDENTIFIER_VALUE = ACTIVITY_DEFINITION_URL + "/" + ACTIVITY_DEFINITION_VERSION + + "/test"; + private static final String VALUE_SET_URL = "http://test.com/fhir/ValueSet/test"; private static final String VALUE_SET_VERSION = "test-version"; @@ -318,6 +326,30 @@ public void testCreateDuplicateSubscriptionsViaBatchBundle() throws Exception checkReturnBatchBundle(getWebserviceClient().postBundle(bundle)); } + @Test + public void testCreateDuplicateTasksViaTransactionBundle() throws Exception + { + ActivityDefinitionDao activityDefinitionDao = getSpringWebApplicationContext() + .getBean(ActivityDefinitionDao.class); + activityDefinitionDao.create(createActivityDefinition()); + + Bundle bundle = createBundle(BundleType.TRANSACTION, createTask(), null, 2); + + expectForbidden(() -> getWebserviceClient().postBundle(bundle)); + } + + @Test + public void testCreateDuplicateTasksViaBatchBundle() throws Exception + { + ActivityDefinitionDao activityDefinitionDao = getSpringWebApplicationContext() + .getBean(ActivityDefinitionDao.class); + activityDefinitionDao.create(createActivityDefinition()); + + Bundle bundle = createBundle(BundleType.BATCH, createTask(), null, 2); + + checkReturnBatchBundle(getWebserviceClient().postBundle(bundle)); + } + @Test public void testCreateDuplicateValueSetsViaTransactionBundle() throws Exception { @@ -574,6 +606,34 @@ public void testCreateDuplicateSubscriptionsViaBatchBundleWithIfNoneExists() thr testCreateDuplicatesViaBundleWithIfNoneExists(bundle, BundleType.BATCHRESPONSE); } + @Test + public void testCreateDuplicateTasksViaTransactionBundleWithIfNoneExists() throws Exception + { + ActivityDefinitionDao activityDefinitionDao = getSpringWebApplicationContext() + .getBean(ActivityDefinitionDao.class); + activityDefinitionDao.create(createActivityDefinition()); + + Bundle bundle = createBundle(BundleType.TRANSACTION, createTask(), + (t, r) -> r.setIfNoneExist("identifier=" + NAMING_SYSTEM_TASK_IDENTIFIER + "|" + TASK_IDENTIFIER_VALUE), + 2); + + testCreateDuplicatesViaBundleWithIfNoneExists(bundle, BundleType.TRANSACTIONRESPONSE); + } + + @Test + public void testCreateDuplicateTasksViaBatchBundleWithIfNoneExists() throws Exception + { + ActivityDefinitionDao activityDefinitionDao = getSpringWebApplicationContext() + .getBean(ActivityDefinitionDao.class); + activityDefinitionDao.create(createActivityDefinition()); + + Bundle bundle = createBundle(BundleType.BATCH, createTask(), + (t, r) -> r.setIfNoneExist("identifier=" + NAMING_SYSTEM_TASK_IDENTIFIER + "|" + TASK_IDENTIFIER_VALUE), + 2); + + testCreateDuplicatesViaBundleWithIfNoneExists(bundle, BundleType.BATCHRESPONSE); + } + @Test public void testCreateDuplicateValueSetsViaTransactionBundleWithIfNoneExists() throws Exception { @@ -777,6 +837,23 @@ public void testCreateDuplicateSubscriptionsParallelDirect() throws Exception && SUBSCRIPTION_CHANNEL_PAYLOAD.equals(s.getChannel().getPayload())); } + @Test + public void testCreateDuplicateTasksParallelDirect() throws Exception + { + ActivityDefinitionDao activityDefinitionDao = getSpringWebApplicationContext() + .getBean(ActivityDefinitionDao.class); + activityDefinitionDao.create(createActivityDefinition()); + + testCreateDuplicatesParallel(() -> + { + Task returnT = getWebserviceClient().create(createTask()); + assertNotNull(returnT); + }, TaskDao.class, + t -> TASK_IDENTIFIER_VALUE.equals( + t.getIdentifier().stream().filter(i -> NAMING_SYSTEM_TASK_IDENTIFIER.equals(i.getSystem())) + .findFirst().map(Identifier::getValue).get())); + } + @Test public void testCreateDuplicateValueSetsParallelDirect() throws Exception { @@ -1193,6 +1270,50 @@ public void testCreateDuplicateSubscriptionsParallelBatchBundle() throws Excepti && SUBSCRIPTION_CHANNEL_PAYLOAD.equals(s.getChannel().getPayload())); } + @Test + public void testCreateDuplicateTasksParallelTransactionBundle() throws Exception + { + ActivityDefinitionDao activityDefinitionDao = getSpringWebApplicationContext() + .getBean(ActivityDefinitionDao.class); + activityDefinitionDao.create(createActivityDefinition()); + + testCreateDuplicatesParallel(() -> + { + Bundle returnBundle = getWebserviceClient() + .postBundle(createBundle(BundleType.TRANSACTION, createTask(), null, 1)); + assertNotNull(returnBundle); + }, TaskDao.class, + t -> TASK_IDENTIFIER_VALUE.equals( + t.getIdentifier().stream().filter(i -> NAMING_SYSTEM_TASK_IDENTIFIER.equals(i.getSystem())) + .findFirst().map(Identifier::getValue).get())); + } + + @Test + public void testCreateDuplicateTasksParallelBatchBundle() throws Exception + { + ActivityDefinitionDao activityDefinitionDao = getSpringWebApplicationContext() + .getBean(ActivityDefinitionDao.class); + activityDefinitionDao.create(createActivityDefinition()); + + testCreateDuplicatesParallel(() -> + { + Bundle returnBundle = getWebserviceClient() + .postBundle(createBundle(BundleType.BATCH, createTask(), null, 1)); + assertNotNull(returnBundle); + + assertNotNull(returnBundle.getEntry()); + assertEquals(1, returnBundle.getEntry().size()); + assertNotNull(returnBundle.getEntry().get(0).getResponse()); + assertNotNull(returnBundle.getEntry().get(0).getResponse().getStatus()); + + if ("403 Forbidden".equals(returnBundle.getEntry().get(0).getResponse().getStatus())) + throw new WebApplicationException(403); + }, TaskDao.class, + t -> TASK_IDENTIFIER_VALUE.equals( + t.getIdentifier().stream().filter(i -> NAMING_SYSTEM_TASK_IDENTIFIER.equals(i.getSystem())) + .findFirst().map(Identifier::getValue).get())); + } + @Test public void testCreateDuplicateValueSetsParallelTransactionBundle() throws Exception { @@ -1464,6 +1585,24 @@ private Subscription createSubscription() return s; } + private Task createTask() + { + Task t = new Task(); + t.setStatus(TaskStatus.DRAFT); + t.setIntent(TaskIntent.ORDER); + t.setAuthoredOn(new Date()); + t.addIdentifier().setSystem(NAMING_SYSTEM_TASK_IDENTIFIER).setValue(TASK_IDENTIFIER_VALUE); + t.setInstantiatesCanonical(ACTIVITY_DEFINITION_URL + "|" + ACTIVITY_DEFINITION_VERSION); + t.getRequester().setType("Organization").getIdentifier().setSystem("http://dsf.dev/sid/organization-identifier") + .setValue("Test_Organization"); + t.getRestriction().getRecipientFirstRep().setType("Organization").getIdentifier() + .setSystem("http://dsf.dev/sid/organization-identifier").setValue("Test_Organization"); + t.getInputFirstRep().setValue(new StringType("test")).getType().getCodingFirstRep() + .setSystem("http://dsf.dev/fhir/CodeSystem/bpmn-message").setCode("message-name"); + + return t; + } + private ValueSet createValueSet() { ValueSet vS = new ValueSet().setUrl(VALUE_SET_URL).setVersion(VALUE_SET_VERSION) From 7c8ce81a418bdf311d13dcc2c87fb874679a09f0 Mon Sep 17 00:00:00 2001 From: Hauke Hund Date: Mon, 16 Dec 2024 23:01:21 +0100 Subject: [PATCH 17/17] typo fix --- .../main/java/dev/dsf/fhir/dao/command/BatchCommandList.java | 2 +- .../java/dev/dsf/fhir/dao/command/TransactionCommandList.java | 4 ++-- .../src/main/java/dev/dsf/fhir/help/ResponseGenerator.java | 4 ++-- .../dsf/fhir/webservice/impl/AbstractResourceServiceImpl.java | 4 ++-- 4 files changed, 7 insertions(+), 7 deletions(-) diff --git a/dsf-fhir/dsf-fhir-server/src/main/java/dev/dsf/fhir/dao/command/BatchCommandList.java b/dsf-fhir/dsf-fhir-server/src/main/java/dev/dsf/fhir/dao/command/BatchCommandList.java index 2b6f15a90..6bcb2588a 100755 --- a/dsf-fhir/dsf-fhir-server/src/main/java/dev/dsf/fhir/dao/command/BatchCommandList.java +++ b/dsf-fhir/dsf-fhir-server/src/main/java/dev/dsf/fhir/dao/command/BatchCommandList.java @@ -169,7 +169,7 @@ private Consumer execute(Map idTranslationTable, Connec if (e instanceof PSQLException s && PSQLState.UNIQUE_VIOLATION.getState().equals(s.getSQLState())) caughtExceptions.put(command.getIndex(), - new WebApplicationException(responseGenerator.dupicateResourceExists())); + new WebApplicationException(responseGenerator.duplicateResourceExists())); else caughtExceptions.put(command.getIndex(), e); 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 4dc591044..c92b1164e 100755 --- a/dsf-fhir/dsf-fhir-server/src/main/java/dev/dsf/fhir/dao/command/TransactionCommandList.java +++ b/dsf-fhir/dsf-fhir-server/src/main/java/dev/dsf/fhir/dao/command/TransactionCommandList.java @@ -128,7 +128,7 @@ public Bundle execute() throws WebApplicationException if (e instanceof PSQLException s && PSQLState.UNIQUE_VIOLATION.getState().equals(s.getSQLState())) - e = new WebApplicationException(e, responseGenerator.dupicateResourceExists()); + e = new WebApplicationException(e, responseGenerator.duplicateResourceExists()); try { @@ -198,7 +198,7 @@ public Bundle execute() throws WebApplicationException connection.rollback(); if (PSQLState.UNIQUE_VIOLATION.getState().equals(e.getSQLState())) - throw new WebApplicationException(responseGenerator.dupicateResourceExists()); + throw new WebApplicationException(responseGenerator.duplicateResourceExists()); else throw e; } 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 7a655f80d..c05969f94 100755 --- a/dsf-fhir/dsf-fhir-server/src/main/java/dev/dsf/fhir/help/ResponseGenerator.java +++ b/dsf-fhir/dsf-fhir-server/src/main/java/dev/dsf/fhir/help/ResponseGenerator.java @@ -381,7 +381,7 @@ public Response multipleExists(String resourceTypeName, String ifNoneExistsHeade return Response.status(Status.PRECONDITION_FAILED).entity(outcome).build(); } - public Response dupicateResourceExists() + public Response duplicateResourceExists() { logger.warn("Duplicate resources exists"); @@ -389,7 +389,7 @@ public Response dupicateResourceExists() return Response.status(Status.FORBIDDEN).entity(outcome).build(); } - public Response dupicateResourceExists(String resourceTypeName) + public Response duplicateResourceExists(String resourceTypeName) { logger.warn("Duplicate {} resources exists", resourceTypeName); 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 cddd2049a..335967e02 100755 --- a/dsf-fhir/dsf-fhir-server/src/main/java/dev/dsf/fhir/webservice/impl/AbstractResourceServiceImpl.java +++ b/dsf-fhir/dsf-fhir-server/src/main/java/dev/dsf/fhir/webservice/impl/AbstractResourceServiceImpl.java @@ -168,7 +168,7 @@ public Response create(R resource, UriInfo uri, HttpHeaders headers) connection.rollback(); if (PSQLState.UNIQUE_VIOLATION.getState().equals(e.getSQLState())) - throw new WebApplicationException(responseGenerator.dupicateResourceExists(resourceTypeName)); + throw new WebApplicationException(responseGenerator.duplicateResourceExists(resourceTypeName)); else throw e; } @@ -535,7 +535,7 @@ public Response update(String id, R resource, UriInfo uri, HttpHeaders headers) { if (PSQLState.UNIQUE_VIOLATION.getState().equals(e.getSQLState())) throw new WebApplicationException( - responseGenerator.dupicateResourceExists(resourceTypeName)); + responseGenerator.duplicateResourceExists(resourceTypeName)); connection.rollback(); throw e;