diff --git a/CITATION.cff b/CITATION.cff index f26972ddd..136828e2a 100644 --- a/CITATION.cff +++ b/CITATION.cff @@ -25,7 +25,7 @@ preferred-citation: type: proceedings title: "Data Sharing Framework (DSF)" version: 2.0.0 -date-released: 2024-07-15 +date-released: 2024-10-15 url: https://dsf.dev repository-code: https://github.com/datasharingframework/dsf repository-artifact: https://github.com/datasharingframework/dsf/releases @@ -241,3 +241,23 @@ references: year: 2024 pages: 28-32 doi: 10.3233/SHTI230921 + - type: proceedings + authors: + - family-names: Schweizer + given-names: Simon Tobias + - family-names: Hund + given-names: Hauke + - family-names: Kurscheidt + given-names: Maximilian + - family-names: Zilske + given-names: Christoph + - family-names: Böhringer + given-names: Jan P. + - family-names: Fegeler + given-names: Christian + title: "Handling Complexity in Decentralized Research Networks: The Data Sharing Framework Allowlist Management Application" + journal: Stud Health Technol Inform + volume: 317 + year: 2024 + pages: 85-93 + doi: 10.3233/SHTI240841 \ No newline at end of file diff --git a/dsf-bpe/dsf-bpe-server-jetty/conf/log4j2.xml b/dsf-bpe/dsf-bpe-server-jetty/conf/log4j2.xml index 59f314620..c5abd3343 100755 --- a/dsf-bpe/dsf-bpe-server-jetty/conf/log4j2.xml +++ b/dsf-bpe/dsf-bpe-server-jetty/conf/log4j2.xml @@ -1,4 +1,4 @@ - diff --git a/dsf-bpe/dsf-bpe-server-jetty/docker/conf/log4j2.xml b/dsf-bpe/dsf-bpe-server-jetty/docker/conf/log4j2.xml index ace0d5f9f..04fddf201 100644 --- a/dsf-bpe/dsf-bpe-server-jetty/docker/conf/log4j2.xml +++ b/dsf-bpe/dsf-bpe-server-jetty/docker/conf/log4j2.xml @@ -1,4 +1,4 @@ - + @@ -17,6 +17,7 @@ + diff --git a/dsf-bpe/dsf-bpe-server/src/main/java/dev/dsf/bpe/camunda/MultiVersionSpringProcessEngineConfiguration.java b/dsf-bpe/dsf-bpe-server/src/main/java/dev/dsf/bpe/camunda/MultiVersionSpringProcessEngineConfiguration.java index e6369efc7..7ef3eab84 100644 --- a/dsf-bpe/dsf-bpe-server/src/main/java/dev/dsf/bpe/camunda/MultiVersionSpringProcessEngineConfiguration.java +++ b/dsf-bpe/dsf-bpe-server/src/main/java/dev/dsf/bpe/camunda/MultiVersionSpringProcessEngineConfiguration.java @@ -11,17 +11,10 @@ public MultiVersionSpringProcessEngineConfiguration(DelegateProvider delegatePro } @Override - protected void initTelemetry() + protected void initDiagnostics() { // override to turn telemetry collection of - // see also CamundaConfig - } - @Override - public TelemetryDataImpl getTelemetryData() - { - // NPE fix after turning off telemetry collection - // see also CamundaConfig - return new TelemetryDataImpl(null, null); + setTelemetryData(new TelemetryDataImpl(null, null)); } -} +} \ No newline at end of file diff --git a/dsf-bpe/dsf-bpe-server/src/main/java/dev/dsf/bpe/spring/config/CamundaConfig.java b/dsf-bpe/dsf-bpe-server/src/main/java/dev/dsf/bpe/spring/config/CamundaConfig.java index 0eea08eb1..6e7cd7b4e 100755 --- a/dsf-bpe/dsf-bpe-server/src/main/java/dev/dsf/bpe/spring/config/CamundaConfig.java +++ b/dsf-bpe/dsf-bpe-server/src/main/java/dev/dsf/bpe/spring/config/CamundaConfig.java @@ -99,10 +99,6 @@ public SpringProcessEngineConfiguration processEngineConfiguration() processPluginFactories.stream().flatMap(ProcessPluginFactory::getSerializer).toList()); c.setFallbackSerializerFactory(fallbackSerializerFactory()); - // see also MultiVersionSpringProcessEngineConfiguration - c.setInitializeTelemetry(false); - c.setTelemetryReporterActivate(false); - DefaultJobExecutor jobExecutor = new DefaultJobExecutor(); jobExecutor.setCorePoolSize(propertiesConfig.getProcessEngineJobExecutorCorePoolSize()); jobExecutor.setQueueSize(propertiesConfig.getProcessEngineJobExecutorQueueSize()); diff --git a/dsf-bpe/dsf-bpe-server/src/main/java/dev/dsf/bpe/subscription/LocalFhirConnectorImpl.java b/dsf-bpe/dsf-bpe-server/src/main/java/dev/dsf/bpe/subscription/LocalFhirConnectorImpl.java index d01b5f095..1e753be3c 100644 --- a/dsf-bpe/dsf-bpe-server/src/main/java/dev/dsf/bpe/subscription/LocalFhirConnectorImpl.java +++ b/dsf-bpe/dsf-bpe-server/src/main/java/dev/dsf/bpe/subscription/LocalFhirConnectorImpl.java @@ -228,7 +228,7 @@ private void connectWebsocket(Subscription subscription) { try { - WebsocketClient client = clientProvider.getLocalWebsocketClient(() -> connect(), + WebsocketClient client = clientProvider.getLocalWebsocketClient(this::connect, subscription.getIdElement().getIdPart()); EventType eventType = toEventType(subscription.getChannel().getPayload()); @@ -260,7 +260,7 @@ private Void onError(Throwable t) { // no debug log, exception previously logged by retrieveWebsocketSubscription, loadNewResources and // connectWebsocket methods - logger.error("Error while loading existing {} resources and connecting websocket: {} - {}", resourceName, + logger.error("Error loading existing {} resources and connecting websocket: {} - {}", resourceName, t.getClass().getName(), t.getMessage()); return null; diff --git a/dsf-bpe/dsf-bpe-server/src/main/resources/db/camunda/postgres_engine_7.21_to_7.22.sql b/dsf-bpe/dsf-bpe-server/src/main/resources/db/camunda/postgres_engine_7.21_to_7.22.sql new file mode 100644 index 000000000..55583c6f4 --- /dev/null +++ b/dsf-bpe/dsf-bpe-server/src/main/resources/db/camunda/postgres_engine_7.21_to_7.22.sql @@ -0,0 +1,29 @@ +-- +-- Copyright Camunda Services GmbH and/or licensed to Camunda Services GmbH +-- under one or more contributor license agreements. See the NOTICE file +-- distributed with this work for additional information regarding copyright +-- ownership. Camunda licenses this file to you under the Apache License, +-- Version 2.0; you may not use this file except in compliance with the License. +-- You may obtain a copy of the License at +-- +-- http://www.apache.org/licenses/LICENSE-2.0 +-- +-- Unless required by applicable law or agreed to in writing, software +-- distributed under the License is distributed on an "AS IS" BASIS, +-- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +-- See the License for the specific language governing permissions and +-- limitations under the License. +-- + +insert into ACT_GE_SCHEMA_LOG +values ('1100', CURRENT_TIMESTAMP, '7.22.0'); + +alter table ACT_RU_TASK add column TASK_STATE_ varchar(64); + +alter table ACT_HI_TASKINST add column TASK_STATE_ varchar(64); + +alter table ACT_RU_JOB add column BATCH_ID_ varchar(64); +alter table ACT_HI_JOB_LOG add column BATCH_ID_ varchar(64); + +alter table ACT_HI_PROCINST add RESTARTED_PROC_INST_ID_ varchar(64); +create index ACT_IDX_HI_PRO_RST_PRO_INST_ID on ACT_HI_PROCINST(RESTARTED_PROC_INST_ID_); diff --git a/dsf-bpe/dsf-bpe-server/src/main/resources/db/db.camunda_engine.changelog-1.6.0.xml b/dsf-bpe/dsf-bpe-server/src/main/resources/db/db.camunda_engine.changelog-1.6.0.xml new file mode 100644 index 000000000..c6967db93 --- /dev/null +++ b/dsf-bpe/dsf-bpe-server/src/main/resources/db/db.camunda_engine.changelog-1.6.0.xml @@ -0,0 +1,12 @@ + + + + + + + + \ No newline at end of file diff --git a/dsf-bpe/dsf-bpe-server/src/main/resources/db/db.changelog.xml b/dsf-bpe/dsf-bpe-server/src/main/resources/db/db.changelog.xml index 7dff2d181..865d2bd97 100644 --- a/dsf-bpe/dsf-bpe-server/src/main/resources/db/db.changelog.xml +++ b/dsf-bpe/dsf-bpe-server/src/main/resources/db/db.changelog.xml @@ -17,4 +17,6 @@ + + \ No newline at end of file diff --git a/dsf-bpe/dsf-bpe-server/src/test/resources/log4j2.xml b/dsf-bpe/dsf-bpe-server/src/test/resources/log4j2.xml index 4095d8e49..42507e66a 100755 --- a/dsf-bpe/dsf-bpe-server/src/test/resources/log4j2.xml +++ b/dsf-bpe/dsf-bpe-server/src/test/resources/log4j2.xml @@ -1,4 +1,4 @@ - diff --git a/dsf-common/dsf-common-auth/src/test/resources/log4j2.xml b/dsf-common/dsf-common-auth/src/test/resources/log4j2.xml index 96df92ff8..ea8c9d0c4 100644 --- a/dsf-common/dsf-common-auth/src/test/resources/log4j2.xml +++ b/dsf-common/dsf-common-auth/src/test/resources/log4j2.xml @@ -1,4 +1,4 @@ - + diff --git a/dsf-common/dsf-common-config/src/test/resources/log4j2.xml b/dsf-common/dsf-common-config/src/test/resources/log4j2.xml index aa5ddf5a5..ec2b86c10 100644 --- a/dsf-common/dsf-common-config/src/test/resources/log4j2.xml +++ b/dsf-common/dsf-common-config/src/test/resources/log4j2.xml @@ -1,4 +1,4 @@ - + diff --git a/dsf-common/dsf-common-jetty/src/main/java/dev/dsf/common/config/AbstractJettyConfig.java b/dsf-common/dsf-common-jetty/src/main/java/dev/dsf/common/config/AbstractJettyConfig.java index f5f8ea049..39b88e0cb 100644 --- a/dsf-common/dsf-common-jetty/src/main/java/dev/dsf/common/config/AbstractJettyConfig.java +++ b/dsf-common/dsf-common-jetty/src/main/java/dev/dsf/common/config/AbstractJettyConfig.java @@ -431,7 +431,7 @@ private Duration oidcClientConnectTimeout() private Proxy oidcClientProxy() { ProxyConfig config = new ProxyConfigImpl(proxyUrl, proxyUsername, proxyPassword, proxyNoProxy); - if (config.getUrl() != null && config.isNoProxyUrl(oidcProviderRealmBaseUrl)) + if (config.getUrl() != null && !config.isNoProxyUrl(oidcProviderRealmBaseUrl)) { try { diff --git a/dsf-common/dsf-common-jetty/src/test/resources/log4j2.xml b/dsf-common/dsf-common-jetty/src/test/resources/log4j2.xml index aa5ddf5a5..ec2b86c10 100644 --- a/dsf-common/dsf-common-jetty/src/test/resources/log4j2.xml +++ b/dsf-common/dsf-common-jetty/src/test/resources/log4j2.xml @@ -1,4 +1,4 @@ - + diff --git a/dsf-docker-test-setup-3dic-ttp/docker-compose.yml b/dsf-docker-test-setup-3dic-ttp/docker-compose.yml index 3fd3bd014..b7302d664 100644 --- a/dsf-docker-test-setup-3dic-ttp/docker-compose.yml +++ b/dsf-docker-test-setup-3dic-ttp/docker-compose.yml @@ -1,7 +1,7 @@ version: '3.8' services: proxy: - image: nginx:1.23 + image: nginx:1.27 restart: "no" ports: - 127.0.0.1:443:443 @@ -78,7 +78,7 @@ services: read_only: true keycloak: - image: quay.io/keycloak/keycloak:21.0 + image: quay.io/keycloak/keycloak:25.0 restart: "no" ports: - 127.0.0.1:8443:8443 @@ -104,7 +104,16 @@ services: --https-certificate-key-file=/run/secrets/keycloak_certificate_private_key.pem --spi-truststore-file-file=/run/secrets/keycloak_trust_store.jks --spi-truststore-file-password=password - --spi-truststore-file-hostname-verification-policy=STRICT + --spi-truststore-file-hostname-verification-policy=DEFAULT + + forward-proxy: + build: ./forward-proxy + restart: "no" + environment: + TZ: Europe/Berlin + networks: + forward-proxy: + internet: dic1-fhir: build: ../dsf-fhir/dsf-fhir-server-jetty/docker @@ -518,15 +527,21 @@ services: DEV_DSF_SERVER_AUTH_OIDC_PROVIDER_CLIENT_TRUST_SERVER_CERTIFICATE_CAS: /run/secrets/app_server_trust_certificates.pem DEV_DSF_SERVER_AUTH_OIDC_CLIENT_ID: dic1-bpe DEV_DSF_SERVER_AUTH_OIDC_CLIENT_SECRET: ytqFCErw9GfhVUrrM8xc0Grbu4r7qGig + DEV_DSF_PROXY_URL: http://forward-proxy:8080 + DEV_DSF_PROXY_USERNAME: proxy_user + DEV_DSF_PROXY_PASSWORD: proxy_password + DEV_DSF_PROXY_NOPROXY: keycloak networks: dic1-bpe-frontend: ipv4_address: 172.20.0.35 dic1-bpe-backend: internet: + forward-proxy: depends_on: - db - dic1-fhir - keycloak + - forward-proxy dic2-bpe: build: ../dsf-bpe/dsf-bpe-server-jetty/docker @@ -944,6 +959,7 @@ networks: - subnet: 172.20.0.56/29 ttp-bpe-backend: internet: + forward-proxy: volumes: db-data: diff --git a/dsf-docker-test-setup-3dic-ttp/forward-proxy/Dockerfile b/dsf-docker-test-setup-3dic-ttp/forward-proxy/Dockerfile new file mode 100644 index 000000000..bd4f3af20 --- /dev/null +++ b/dsf-docker-test-setup-3dic-ttp/forward-proxy/Dockerfile @@ -0,0 +1,4 @@ +FROM alpine:3.20 +RUN apk add --no-cache tinyproxy +COPY tinyproxy.conf /etc/tinyproxy/tinyproxy.conf +CMD ["tinyproxy", "-d"] \ No newline at end of file diff --git a/dsf-docker-test-setup-3dic-ttp/forward-proxy/tinyproxy.conf b/dsf-docker-test-setup-3dic-ttp/forward-proxy/tinyproxy.conf new file mode 100644 index 000000000..e08799ca1 --- /dev/null +++ b/dsf-docker-test-setup-3dic-ttp/forward-proxy/tinyproxy.conf @@ -0,0 +1,6 @@ +User tinyproxy +Group tinyproxy +Port 8080 +Timeout 100 +LogLevel Connect +BasicAuth proxy_user proxy_password \ No newline at end of file diff --git a/dsf-docker-test-setup-3dic-ttp/proxy/conf.d/default.conf b/dsf-docker-test-setup-3dic-ttp/proxy/conf.d/default.conf deleted file mode 100644 index b460be080..000000000 --- a/dsf-docker-test-setup-3dic-ttp/proxy/conf.d/default.conf +++ /dev/null @@ -1,46 +0,0 @@ -server { - listen 80; - listen [::]:80; - server_name localhost; - - #charset koi8-r; - #access_log /var/log/nginx/host.access.log main; - - location / { - root /usr/share/nginx/html; - index index.html index.htm; - } - - #error_page 404 /404.html; - - # redirect server error pages to the static page /50x.html - # - error_page 500 502 503 504 /50x.html; - location = /50x.html { - root /usr/share/nginx/html; - } - - # proxy the PHP scripts to Apache listening on 127.0.0.1:80 - # - #location ~ \.php$ { - # proxy_pass http://127.0.0.1; - #} - - # pass the PHP scripts to FastCGI server listening on 127.0.0.1:9000 - # - #location ~ \.php$ { - # root html; - # fastcgi_pass 127.0.0.1:9000; - # fastcgi_index index.php; - # fastcgi_param SCRIPT_FILENAME /scripts$fastcgi_script_name; - # include fastcgi_params; - #} - - # deny access to .htaccess files, if Apache's document root - # concurs with nginx's one - # - #location ~ /\.ht { - # deny all; - #} -} - diff --git a/dsf-docker-test-setup-3dic-ttp/proxy/nginx.conf b/dsf-docker-test-setup-3dic-ttp/proxy/nginx.conf index 04ee80c96..d9aece023 100644 --- a/dsf-docker-test-setup-3dic-ttp/proxy/nginx.conf +++ b/dsf-docker-test-setup-3dic-ttp/proxy/nginx.conf @@ -1,16 +1,6 @@ - -user nginx; -worker_processes 1; - -error_log /var/log/nginx/error.log warn; -pid /var/run/nginx.pid; - - events { - worker_connections 1024; } - http { include /etc/nginx/mime.types; default_type application/octet-stream; @@ -19,15 +9,6 @@ http { '$status $body_bytes_sent "$http_referer" ' '"$http_user_agent" "$http_x_forwarded_for"'; - access_log /var/log/nginx/access.log main; - - sendfile on; - #tcp_nopush on; - - keepalive_timeout 65; - - #gzip on; - ssl_certificate /run/secrets/proxy_certificate_and_int_cas.pem; ssl_certificate_key /run/secrets/proxy_certificate_private_key.pem; ssl_protocols TLSv1.2 TLSv1.3; @@ -45,4 +26,4 @@ http { } include /etc/nginx/conf.d/*.conf; -} +} \ No newline at end of file diff --git a/dsf-fhir/dsf-fhir-auth/src/test/resources/log4j2.xml b/dsf-fhir/dsf-fhir-auth/src/test/resources/log4j2.xml index 116ccf90e..7bafc4cfc 100644 --- a/dsf-fhir/dsf-fhir-auth/src/test/resources/log4j2.xml +++ b/dsf-fhir/dsf-fhir-auth/src/test/resources/log4j2.xml @@ -1,4 +1,4 @@ - + diff --git a/dsf-fhir/dsf-fhir-rest-adapter/src/main/java/dev/dsf/fhir/service/ReferenceExtractorImpl.java b/dsf-fhir/dsf-fhir-rest-adapter/src/main/java/dev/dsf/fhir/service/ReferenceExtractorImpl.java index 05f3e500d..db91a3950 100644 --- a/dsf-fhir/dsf-fhir-rest-adapter/src/main/java/dev/dsf/fhir/service/ReferenceExtractorImpl.java +++ b/dsf-fhir/dsf-fhir-rest-adapter/src/main/java/dev/dsf/fhir/service/ReferenceExtractorImpl.java @@ -10,6 +10,7 @@ import org.hl7.fhir.r4.model.Attachment; import org.hl7.fhir.r4.model.BackboneElement; import org.hl7.fhir.r4.model.Binary; +import org.hl7.fhir.r4.model.CanonicalType; import org.hl7.fhir.r4.model.CarePlan; import org.hl7.fhir.r4.model.CareTeam; import org.hl7.fhir.r4.model.ClaimResponse; @@ -80,18 +81,25 @@ public class ReferenceExtractorImpl implements ReferenceExtractor private Function toResourceReferenceFromReference(String referenceLocation, Class... referenceTypes) { - return ref -> new ResourceReference(referenceLocation, ref, referenceTypes); + return reference -> new ResourceReference(referenceLocation, reference, referenceTypes); } private Function toResourceReferenceFromRelatedArtifact( String relatedArtifactLocation) { - return rel -> new ResourceReference(relatedArtifactLocation, rel); + return relatedArtifact -> new ResourceReference(relatedArtifactLocation, relatedArtifact); } private Function toResourceReferenceFromAttachment(String attachmentLocation) { - return rel -> new ResourceReference(attachmentLocation, rel); + return attachment -> new ResourceReference(attachmentLocation, attachment); + } + + @SafeVarargs + private Function toResourceReferenceFromCanonical(String canonicalLocation, + Class... referenceTypes) + { + return canonical -> new ResourceReference(canonicalLocation, canonical, referenceTypes); } @SafeVarargs @@ -260,6 +268,50 @@ private Stream getReferences(E ba .map(toResourceReferenceFromReference(referenceLocation, referenceTypes)) : Stream.empty(); } + @SafeVarargs + private Stream getCanonical(R resource, Predicate hasCanonical, + Function getCanonical, String canonicalLocation, + Class... canonicalTypes) + { + return hasCanonical.test(resource) ? Stream.of(getCanonical.apply(resource)) + .map(toResourceReferenceFromCanonical(canonicalLocation, canonicalTypes)) : Stream.empty(); + } + + @SafeVarargs + private Stream getCanonicals(R resource, Predicate hasCanonical, + Function> getCanonicals, String canonicalLocation, + Class... canonicalTypes) + { + return hasCanonical.test(resource) ? Stream.of(getCanonicals.apply(resource)).flatMap(List::stream) + .map(toResourceReferenceFromCanonical(canonicalLocation, canonicalTypes)) : Stream.empty(); + } + + @SafeVarargs + private Stream getCanonical(E backboneElement, + Predicate hasCanonical, Function getCanonical, String canonicalLocation, + Class... canonicalTypes) + { + return hasCanonical.test(backboneElement) ? Stream.of(getCanonical.apply(backboneElement)) + .map(toResourceReferenceFromCanonical(canonicalLocation, canonicalTypes)) : Stream.empty(); + } + + @SafeVarargs + private Stream getBackboneElementsCanonical( + R resource, Predicate hasBackboneElements, Function> getBackboneElements, + Predicate hasCanonical, Function getCanonical, String canonicalLocation, + Class... canonicalTypes) + { + if (hasBackboneElements.test(resource)) + { + List backboneElements = getBackboneElements.apply(resource); + return backboneElements.stream() + .map(e -> getCanonical(e, hasCanonical, getCanonical, canonicalLocation, canonicalTypes)) + .flatMap(Function.identity()); + } + else + return Stream.empty(); + } + private Stream getExtensionReferences(DomainResource resource) { var extensions = resource.getExtension().stream().filter(e -> e.getValue() instanceof Reference) @@ -409,9 +461,14 @@ private Stream getReferences(Binary resource) private Stream getReferences(CodeSystem resource) { + var supplements = getCanonical(resource, CodeSystem::hasSupplementsElement, CodeSystem::getSupplementsElement, + "CodeSystem.supplements", CodeSystem.class); + var valueSet = getCanonical(resource, CodeSystem::hasValueSetElement, CodeSystem::getValueSetElement, + "CodeSystem.valueSet", ValueSet.class); + var extensionReferences = getExtensionReferences(resource); - return extensionReferences; + return concat(valueSet, supplements, extensionReferences); } private Stream getReferences(DocumentReference resource) @@ -522,6 +579,8 @@ private Stream getReferences(Location resource) private Stream getReferences(Measure resource) { + var library = getCanonicals(resource, Measure::hasLibrary, Measure::getLibrary, "Measure.library", + Library.class); var subject = getReference(resource, Measure::hasSubjectReference, Measure::getSubjectReference, "Measure.subject", Group.class); var relatedArtifacts = getRelatedArtifacts(resource, Measure::hasRelatedArtifact, Measure::getRelatedArtifact, @@ -529,11 +588,13 @@ private Stream getReferences(Measure resource) var extensionReferences = getExtensionReferences(resource); - return concat(subject, relatedArtifacts, extensionReferences); + return concat(library, subject, relatedArtifacts, extensionReferences); } private Stream getReferences(MeasureReport resource) { + var measure = getCanonical(resource, MeasureReport::hasMeasureElement, MeasureReport::getMeasureElement, + "MeasureReport.measure", Measure.class); var subject = getReference(resource, MeasureReport::hasSubject, MeasureReport::getSubject, "MeasureReport.subject", Patient.class, Practitioner.class, PractitionerRole.class, Location.class, Device.class, RelatedPerson.class, Group.class); @@ -557,7 +618,8 @@ private Stream getReferences(MeasureReport resource) var extensionReferences = getExtensionReferences(resource); - return concat(subject, reporter, subjectResults1, subjectResults2, evaluatedResource, extensionReferences); + return concat(measure, subject, reporter, subjectResults1, subjectResults2, evaluatedResource, + extensionReferences); } private Stream getReferences(NamingSystem resource) @@ -679,20 +741,24 @@ private Stream getReferences(Provenance resource) private Stream getReferences(Questionnaire resource) { + var derivedFrom = getCanonicals(resource, Questionnaire::hasDerivedFrom, Questionnaire::getDerivedFrom, + "Questionnaire.derivedFrom", Questionnaire.class); var enableWhen = getBackboneElements2Reference(resource, Questionnaire::hasItem, Questionnaire::getItem, Questionnaire.QuestionnaireItemComponent::hasEnableWhen, Questionnaire.QuestionnaireItemComponent::getEnableWhen, Questionnaire.QuestionnaireItemEnableWhenComponent::hasAnswerReference, Questionnaire.QuestionnaireItemEnableWhenComponent::getAnswerReference, "Questionnaire.item.enableWhen.answerReference"); - var answerOption = getBackboneElements2Reference(resource, Questionnaire::hasItem, Questionnaire::getItem, Questionnaire.QuestionnaireItemComponent::hasAnswerOption, Questionnaire.QuestionnaireItemComponent::getAnswerOption, Questionnaire.QuestionnaireItemAnswerOptionComponent::hasValueReference, Questionnaire.QuestionnaireItemAnswerOptionComponent::getValueReference, "Questionnaire.item.answerOption.valueReference"); - + var answerValueSet = getBackboneElementsCanonical(resource, Questionnaire::hasItem, Questionnaire::getItem, + Questionnaire.QuestionnaireItemComponent::hasAnswerValueSetElement, + Questionnaire.QuestionnaireItemComponent::getAnswerValueSetElement, "Questionnaire.item.answerValueSet", + ValueSet.class); var initial = getBackboneElements2Reference(resource, Questionnaire::hasItem, Questionnaire::getItem, Questionnaire.QuestionnaireItemComponent::hasInitial, Questionnaire.QuestionnaireItemComponent::getInitial, @@ -702,7 +768,7 @@ private Stream getReferences(Questionnaire resource) var extensionReferences = getExtensionReferences(resource); - return concat(enableWhen, answerOption, initial, extensionReferences); + return concat(derivedFrom, enableWhen, answerOption, answerValueSet, initial, extensionReferences); } private Stream getReferences(QuestionnaireResponse resource) @@ -710,26 +776,24 @@ private Stream getReferences(QuestionnaireResponse resource) var author = getReference(resource, QuestionnaireResponse::hasAuthor, QuestionnaireResponse::getAuthor, "QuestionnaireResponse.author", Device.class, Organization.class, Patient.class, Practitioner.class, PractitionerRole.class, RelatedPerson.class); - var basedOn = getReferences(resource, QuestionnaireResponse::hasBasedOn, QuestionnaireResponse::getBasedOn, "QuestionnaireResponse.basedOn", CarePlan.class, ServiceRequest.class); - var encounter = getReference(resource, QuestionnaireResponse::hasEncounter, QuestionnaireResponse::getEncounter, "QuestionnaireResponse.encounter", Encounter.class); - var partOf = getReferences(resource, QuestionnaireResponse::hasPartOf, QuestionnaireResponse::getPartOf, "QuestionnaireResponse.partOf", Observation.class, Procedure.class); - + var questionnaire = getCanonical(resource, QuestionnaireResponse::hasQuestionnaireElement, + QuestionnaireResponse::getQuestionnaireElement, "QuestionnaireResponse.questionnaire", + Questionnaire.class); var source = getReference(resource, QuestionnaireResponse::hasSource, QuestionnaireResponse::getSource, "QuestionnaireResponse.source", Patient.class, Practitioner.class, PractitionerRole.class, RelatedPerson.class); - var subject = getReference(resource, QuestionnaireResponse::hasSubject, QuestionnaireResponse::getSubject, "QuestionnaireResponse.subject"); var extensionReferences = getExtensionReferences(resource); - return concat(author, basedOn, encounter, partOf, source, subject, extensionReferences); + return concat(author, basedOn, encounter, partOf, questionnaire, source, subject, extensionReferences); } private Stream getReferences(ResearchStudy resource) @@ -758,9 +822,13 @@ private Stream getReferences(ResearchStudy resource) private Stream getReferences(StructureDefinition resource) { + var baseDefinition = getCanonical(resource, StructureDefinition::hasBaseDefinitionElement, + StructureDefinition::getBaseDefinitionElement, "StructureDefinition.baseDefinition", + StructureDefinition.class); + var extensionReferences = getExtensionReferences(resource); - return extensionReferences; + return concat(baseDefinition, extensionReferences); } private Stream getReferences(Subscription resource) @@ -773,23 +841,25 @@ private Stream getReferences(Subscription resource) private Stream getReferences(Task resource) { var basedOns = getReferences(resource, Task::hasBasedOn, Task::getBasedOn, "Task.basedOn"); - var partOfs = getReferences(resource, Task::hasPartOf, Task::getPartOf, "Task.partOf", Task.class); - var focus = getReference(resource, Task::hasFocus, Task::getFocus, "Task.focus"); - var forRef = getReference(resource, Task::hasFor, Task::getFor, "Task.for"); var encounter = getReference(resource, Task::hasEncounter, Task::getEncounter, "Task.encounter", Encounter.class); - var requester = getReference(resource, Task::hasRequester, Task::getRequester, "Task.requester", Device.class, - Organization.class, Patient.class, Practitioner.class, PractitionerRole.class, RelatedPerson.class); + var focus = getReference(resource, Task::hasFocus, Task::getFocus, "Task.focus"); + var forRef = getReference(resource, Task::hasFor, Task::getFor, "Task.for"); + var instantiatesCanonical = getCanonical(resource, Task::hasInstantiatesCanonicalElement, + Task::getInstantiatesCanonicalElement, "Task.instantiatesCanonical", ActivityDefinition.class); + var insurance = getReferences(resource, Task::hasInsurance, Task::getInsurance, "Task.insurance", + Coverage.class, ClaimResponse.class); + var location = getReference(resource, Task::hasLocation, Task::getLocation, "Task.location", Location.class); var owner = getReference(resource, Task::hasOwner, Task::getOwner, "Task.owner", Practitioner.class, PractitionerRole.class, Organization.class, CareTeam.class, HealthcareService.class, Patient.class, Device.class, RelatedPerson.class); - var location = getReference(resource, Task::hasLocation, Task::getLocation, "Task.location", Location.class); + var partOfs = getReferences(resource, Task::hasPartOf, Task::getPartOf, "Task.partOf", Task.class); var reasonReference = getReference(resource, Task::hasReasonReference, Task::getReasonReference, "Task.reasonReference"); - var insurance = getReferences(resource, Task::hasInsurance, Task::getInsurance, "Task.insurance", - Coverage.class, ClaimResponse.class); var relevanteHistories = getReferences(resource, Task::hasRelevantHistory, Task::getRelevantHistory, "Task.relevantHistory", Provenance.class); + var requester = getReference(resource, Task::hasRequester, Task::getRequester, "Task.requester", Device.class, + Organization.class, Patient.class, Practitioner.class, PractitionerRole.class, RelatedPerson.class); var restrictionRecipiets = getBackboneElementReferences(resource, Task::hasRestriction, Task::getRestriction, Task.TaskRestrictionComponent::hasRecipient, Task.TaskRestrictionComponent::getRecipient, "Task.restriction.recipient", Patient.class, Practitioner.class, PractitionerRole.class, @@ -799,16 +869,13 @@ private Stream getReferences(Task resource) var outputReferences = getOutputReferences(resource); var extensionReferences = getExtensionReferences(resource); - return concat(basedOns, partOfs, focus, forRef, encounter, requester, owner, location, reasonReference, - insurance, relevanteHistories, restrictionRecipiets, inputReferences, outputReferences, + return concat(basedOns, encounter, focus, forRef, instantiatesCanonical, insurance, location, owner, partOfs, + reasonReference, relevanteHistories, requester, restrictionRecipiets, inputReferences, outputReferences, extensionReferences); } private Stream getInputReferences(Task resource) { - if (resource == null) - return Stream.empty(); - var inputReferences = resource.getInput().stream().filter(in -> in.getValue() instanceof Reference) .map(in -> (Reference) in.getValue()) .map(toResourceReferenceFromReference(resource.getResourceType().name() + ".input")); @@ -821,9 +888,6 @@ private Stream getInputReferences(Task resource) private Stream getOutputReferences(Task resource) { - if (resource == null) - return Stream.empty(); - var outputReferences = resource.getOutput().stream().filter(out -> out.getValue() instanceof Reference) .map(in -> (Reference) in.getValue()) .map(toResourceReferenceFromReference(resource.getResourceType().name() + ".output")); diff --git a/dsf-fhir/dsf-fhir-rest-adapter/src/main/java/dev/dsf/fhir/service/ResourceReference.java b/dsf-fhir/dsf-fhir-rest-adapter/src/main/java/dev/dsf/fhir/service/ResourceReference.java index 7dccd6930..d4015defd 100644 --- a/dsf-fhir/dsf-fhir-rest-adapter/src/main/java/dev/dsf/fhir/service/ResourceReference.java +++ b/dsf-fhir/dsf-fhir-rest-adapter/src/main/java/dev/dsf/fhir/service/ResourceReference.java @@ -1,7 +1,6 @@ package dev.dsf.fhir.service; import java.util.ArrayList; -import java.util.Arrays; import java.util.Collection; import java.util.Collections; import java.util.EnumSet; @@ -11,6 +10,7 @@ import java.util.regex.Pattern; import org.hl7.fhir.r4.model.Attachment; +import org.hl7.fhir.r4.model.CanonicalType; import org.hl7.fhir.r4.model.IdType; import org.hl7.fhir.r4.model.Reference; import org.hl7.fhir.r4.model.RelatedArtifact; @@ -143,42 +143,54 @@ public enum ReferenceType /** * unknown url in Attachment */ - ATTACHMENT_UNKNOWN_URL + ATTACHMENT_UNKNOWN_URL, + /** + * canoncial reference + */ + CANONICAL } private final String location; private final Reference reference; private final RelatedArtifact relatedArtifact; private final Attachment attachment; + private final CanonicalType canonical; private final List> referenceTypes = new ArrayList<>(); @SafeVarargs public ResourceReference(String location, Reference reference, Class... referenceTypes) { - this(location, reference, null, null, Arrays.asList(referenceTypes)); + this(location, reference, null, null, null, List.of(referenceTypes)); } public ResourceReference(String location, RelatedArtifact relatedArtifact) { - this(location, null, relatedArtifact, null, Collections.emptyList()); + this(location, null, relatedArtifact, null, null, Collections.emptyList()); } public ResourceReference(String location, Attachment attachment) { - this(location, null, null, attachment, Collections.emptyList()); + this(location, null, null, attachment, null, Collections.emptyList()); + } + + @SafeVarargs + public ResourceReference(String location, CanonicalType canonical, Class... referenceTypes) + { + this(location, null, null, null, canonical, List.of(referenceTypes)); } private ResourceReference(String location, Reference reference, RelatedArtifact relatedArtifact, - Attachment attachment, Collection> referenceTypes) + Attachment attachment, CanonicalType canonical, Collection> referenceTypes) { this.location = location; - if (reference == null && relatedArtifact == null && attachment == null) - throw new IllegalArgumentException("Either reference, relatedArtifact or attachment expected"); + if (reference == null && relatedArtifact == null && attachment == null && canonical == null) + throw new IllegalArgumentException("Either reference, relatedArtifact, attachment or canonical expected"); this.reference = reference; this.relatedArtifact = relatedArtifact; this.attachment = attachment; + this.canonical = canonical; if (referenceTypes != null) this.referenceTypes.addAll(referenceTypes); @@ -214,6 +226,16 @@ public Attachment getAttachment() return attachment; } + public boolean hasCanonical() + { + return canonical != null; + } + + public CanonicalType getCanonical() + { + return canonical; + } + public String getValue() { if (hasReference()) @@ -222,8 +244,10 @@ else if (hasRelatedArtifact()) return relatedArtifact.getUrl(); else if (hasAttachment()) return attachment.getUrl(); + else if (hasCanonical()) + return canonical.getValue(); else - throw new IllegalArgumentException("reference, related artefact or attachment not set"); + throw new IllegalArgumentException("reference, related artefact, attachment or canonical not set"); } public List> getReferenceTypes() @@ -336,8 +360,15 @@ else if (reference.hasType() && reference.hasIdentifier() && reference.getIdenti return ReferenceType.UNKNOWN; } + else if (canonical != null) + { + if (canonical.hasValue()) + return ReferenceType.CANONICAL; + else + return ReferenceType.UNKNOWN; + } else - throw new IllegalStateException("Either reference or relatedArtifact expected"); + throw new IllegalStateException("Either reference, related artefact, attachment or canonical expected"); } public String getLocation() diff --git a/dsf-fhir/dsf-fhir-rest-adapter/src/test/resources/log4j2.xml b/dsf-fhir/dsf-fhir-rest-adapter/src/test/resources/log4j2.xml index d5beb117e..6d88f4a92 100644 --- a/dsf-fhir/dsf-fhir-rest-adapter/src/test/resources/log4j2.xml +++ b/dsf-fhir/dsf-fhir-rest-adapter/src/test/resources/log4j2.xml @@ -1,4 +1,4 @@ - + diff --git a/dsf-fhir/dsf-fhir-server-jetty/conf/log4j2.xml b/dsf-fhir/dsf-fhir-server-jetty/conf/log4j2.xml index 7bc2f73d0..46a5f1acc 100755 --- a/dsf-fhir/dsf-fhir-server-jetty/conf/log4j2.xml +++ b/dsf-fhir/dsf-fhir-server-jetty/conf/log4j2.xml @@ -1,4 +1,4 @@ - diff --git a/dsf-fhir/dsf-fhir-server-jetty/docker/conf/log4j2.xml b/dsf-fhir/dsf-fhir-server-jetty/docker/conf/log4j2.xml index 88c646847..8175acb1d 100644 --- a/dsf-fhir/dsf-fhir-server-jetty/docker/conf/log4j2.xml +++ b/dsf-fhir/dsf-fhir-server-jetty/docker/conf/log4j2.xml @@ -1,4 +1,4 @@ - + diff --git a/dsf-fhir/dsf-fhir-server-jetty/pom.xml b/dsf-fhir/dsf-fhir-server-jetty/pom.xml old mode 100644 new mode 100755 diff --git a/dsf-fhir/dsf-fhir-server/pom.xml b/dsf-fhir/dsf-fhir-server/pom.xml old mode 100644 new mode 100755 diff --git a/dsf-fhir/dsf-fhir-server/src/main/java/dev/dsf/fhir/authorization/AbstractAuthorizationRule.java b/dsf-fhir/dsf-fhir-server/src/main/java/dev/dsf/fhir/authorization/AbstractAuthorizationRule.java index b2343cc27..4e978a6e3 100644 --- a/dsf-fhir/dsf-fhir-server/src/main/java/dev/dsf/fhir/authorization/AbstractAuthorizationRule.java +++ b/dsf-fhir/dsf-fhir-server/src/main/java/dev/dsf/fhir/authorization/AbstractAuthorizationRule.java @@ -335,12 +335,6 @@ protected final Optional createIfLiteralInternalOrLogicalRefe return Optional.empty(); } - protected final Optional resolveReference(Connection connection, Identity identity, - Optional reference) - { - return reference.flatMap(ref -> referenceResolver.resolveReference(identity, ref, connection)); - } - @Override public Optional reasonPermanentDeleteAllowed(Identity identity, R oldResource) { diff --git a/dsf-fhir/dsf-fhir-server/src/main/java/dev/dsf/fhir/authorization/QuestionnaireResponseAuthorizationRule.java b/dsf-fhir/dsf-fhir-server/src/main/java/dev/dsf/fhir/authorization/QuestionnaireResponseAuthorizationRule.java index e0fb25748..a47fb36c4 100644 --- a/dsf-fhir/dsf-fhir-server/src/main/java/dev/dsf/fhir/authorization/QuestionnaireResponseAuthorizationRule.java +++ b/dsf-fhir/dsf-fhir-server/src/main/java/dev/dsf/fhir/authorization/QuestionnaireResponseAuthorizationRule.java @@ -1,7 +1,6 @@ package dev.dsf.fhir.authorization; import java.sql.Connection; -import java.sql.SQLException; import java.util.ArrayList; import java.util.EnumSet; import java.util.List; @@ -9,7 +8,6 @@ import java.util.Optional; import java.util.stream.Collectors; -import org.hl7.fhir.r4.model.Questionnaire; import org.hl7.fhir.r4.model.QuestionnaireResponse; import org.hl7.fhir.r4.model.QuestionnaireResponse.QuestionnaireResponseItemAnswerComponent; import org.hl7.fhir.r4.model.QuestionnaireResponse.QuestionnaireResponseItemComponent; @@ -89,12 +87,8 @@ private Optional newResourceOk(QuestionnaireResponse newResource, getItemAndValidate(newResource, CODESYSTEM_DSF_BPMN_USER_TASK_VALUE_USER_TASK_ID, errors); - String questionnaireUrlAndVersion = newResource.getQuestionnaire(); - if (!questionnaireExists(questionnaireUrlAndVersion)) - { - errors.add( - "Questionnaire ressource referenced via canonical QuestionnaireResponse.questionnaire does not exist"); - } + if (!newResource.hasQuestionnaire()) + errors.add("QuestionnaireResponse.questionnaire missing"); if (errors.isEmpty()) return Optional.empty(); @@ -149,23 +143,6 @@ private Optional getItemAndValidate(QuestionnaireResponse newResource, S return Optional.of(value.getValue()); } - private boolean questionnaireExists(String questionnaireUrlAndVersion) - { - try - { - Optional questionnaire = daoProvider.getQuestionnaireDao() - .readByUrlAndVersion(questionnaireUrlAndVersion); - - return questionnaire.isPresent(); - } - catch (SQLException e) - { - logger.warn("Could not check questionnaire with url|version '{}' for questionnaire-response - {}", - questionnaireUrlAndVersion, e.getMessage()); - throw new RuntimeException(e); - } - } - @Override public Optional reasonReadAllowed(Connection connection, Identity identity, QuestionnaireResponse existingResource) diff --git a/dsf-fhir/dsf-fhir-server/src/main/java/dev/dsf/fhir/dao/command/ReferencesHelperImpl.java b/dsf-fhir/dsf-fhir-server/src/main/java/dev/dsf/fhir/dao/command/ReferencesHelperImpl.java index 21fea87ae..1890fafb4 100644 --- a/dsf-fhir/dsf-fhir-server/src/main/java/dev/dsf/fhir/dao/command/ReferencesHelperImpl.java +++ b/dsf-fhir/dsf-fhir-server/src/main/java/dev/dsf/fhir/dao/command/ReferencesHelperImpl.java @@ -200,7 +200,7 @@ public void checkReferences(Map idTranslationTable, Connection c Predicate checkReference) throws WebApplicationException { referenceExtractor.getReferences(resource).filter(checkReference) - .filter(ref -> referenceResolver.referenceCanBeChecked(ref, connection)).forEach(ref -> + .filter(ref -> referenceResolver.referenceCanBeResolved(ref, connection)).forEach(ref -> { Optional outcome = checkReference(ref, connection); if (outcome.isPresent()) @@ -224,6 +224,9 @@ private Optional checkReference(ResourceReference reference, C case LOGICAL -> referenceResolver.checkLogicalReference(identity, resource, reference, connection, index); + case CANONICAL -> + referenceResolver.checkCanonicalReference(identity, resource, reference, connection, index); + // unknown URLs to non FHIR servers in related artifacts must not be checked case RELATED_ARTEFACT_UNKNOWN_URL, ATTACHMENT_UNKNOWN_URL -> Optional.empty(); diff --git a/dsf-fhir/dsf-fhir-server/src/main/java/dev/dsf/fhir/service/ReferenceResolver.java b/dsf-fhir/dsf-fhir-server/src/main/java/dev/dsf/fhir/service/ReferenceResolver.java index d601c234f..048d9e8ca 100755 --- a/dsf-fhir/dsf-fhir-server/src/main/java/dev/dsf/fhir/service/ReferenceResolver.java +++ b/dsf-fhir/dsf-fhir-server/src/main/java/dev/dsf/fhir/service/ReferenceResolver.java @@ -35,15 +35,6 @@ public interface ReferenceResolver */ Optional resolveReference(Identity identity, ResourceReference reference, Connection connection); - /** - * @param reference - * not null - * @param connection - * not null - * @return true if the {@link ResourceReference} can be checked - */ - boolean referenceCanBeChecked(ResourceReference reference, Connection connection); - /** * @param resource * not null @@ -160,4 +151,40 @@ Optional checkLogicalReference(Identity identity, Resource res Optional checkLogicalReference(Identity identity, Resource resource, ResourceReference resourceReference, Connection connection, Integer bundleIndex) throws IllegalArgumentException; + + /** + * @param identity + * not null + * @param resource + * not null + * @param reference + * not null + * @param connection + * not null + * @return {@link Optional#empty()} if the reference check was successful + * @throws IllegalArgumentException + * if the reference is not of type {@link ResourceReference.ReferenceType#CANONICAL} + * @see ResourceReference#getType(String) + */ + Optional checkCanonicalReference(Identity identity, Resource resource, + ResourceReference reference, Connection connection) throws IllegalArgumentException; + + /** + * @param identity + * not null + * @param resource + * not null + * @param reference + * not null + * @param connection + * not null + * @param bundleIndex + * may be null + * @return {@link Optional#empty()} if the reference check was successful + * @throws IllegalArgumentException + * if the reference is not of type {@link ResourceReference.ReferenceType#CANONICAL} + * @see ResourceReference#getType(String) + */ + Optional checkCanonicalReference(Identity identity, Resource resource, + ResourceReference reference, Connection connection, Integer bundleIndex) throws IllegalArgumentException; } diff --git a/dsf-fhir/dsf-fhir-server/src/main/java/dev/dsf/fhir/service/ReferenceResolverImpl.java b/dsf-fhir/dsf-fhir-server/src/main/java/dev/dsf/fhir/service/ReferenceResolverImpl.java index b8f59320c..3e5a64795 100755 --- a/dsf-fhir/dsf-fhir-server/src/main/java/dev/dsf/fhir/service/ReferenceResolverImpl.java +++ b/dsf-fhir/dsf-fhir-server/src/main/java/dev/dsf/fhir/service/ReferenceResolverImpl.java @@ -2,7 +2,6 @@ import java.sql.Connection; import java.util.Arrays; -import java.util.Collections; import java.util.EnumSet; import java.util.List; import java.util.Map; @@ -41,6 +40,9 @@ public class ReferenceResolverImpl implements ReferenceResolver, InitializingBea { private static final Logger logger = LoggerFactory.getLogger(ReferenceResolverImpl.class); + private static final String QUESTIONNAIRE_RESPONSE_QUESTIONNAIRE = "QuestionnaireResponse.questionnaire"; + private static final String TASK_INSTANTIATES_CANONICAL = "Task.instantiatesCanonical"; + private final String serverBase; private final DaoProvider daoProvider; private final ResponseGenerator responseGenerator; @@ -72,12 +74,6 @@ public void afterPropertiesSet() throws Exception @Override public boolean referenceCanBeResolved(ResourceReference reference, Connection connection) - { - return referenceCanBeChecked(reference, connection); - } - - @Override - public boolean referenceCanBeChecked(ResourceReference reference, Connection connection) { Objects.requireNonNull(reference, "reference"); Objects.requireNonNull(connection, "connection"); @@ -85,36 +81,19 @@ public boolean referenceCanBeChecked(ResourceReference reference, Connection con return switch (reference.getType(serverBase)) { case LITERAL_EXTERNAL, RELATED_ARTEFACT_LITERAL_EXTERNAL_URL, ATTACHMENT_LITERAL_EXTERNAL_URL -> - literalExternalReferenceCanBeCheckedAndResolved(reference); + clientProvider.endpointExists(reference.getServerBase(serverBase)); - case LOGICAL -> logicalReferenceCanBeCheckedAndResolved(reference, connection); + case LOGICAL -> exceptionHandler.handleSqlException( + () -> daoProvider.getNamingSystemDao().existsWithUniqueIdUriEntryResolvable(connection, + reference.getReference().getIdentifier().getSystem())); + + case CANONICAL -> QUESTIONNAIRE_RESPONSE_QUESTIONNAIRE.equals(reference.getLocation()) + || TASK_INSTANTIATES_CANONICAL.equals(reference.getLocation()); default -> true; }; } - private boolean logicalReferenceCanBeCheckedAndResolved(ResourceReference reference, Connection connection) - { - if (!ReferenceType.LOGICAL.equals(reference.getType(serverBase))) - throw new IllegalArgumentException("Not a logical reference"); - - return exceptionHandler.handleSqlException( - () -> daoProvider.getNamingSystemDao().existsWithUniqueIdUriEntryResolvable(connection, - reference.getReference().getIdentifier().getSystem())); - } - - private boolean literalExternalReferenceCanBeCheckedAndResolved(ResourceReference reference) - { - if (!EnumSet.of(ReferenceType.LITERAL_EXTERNAL, ReferenceType.RELATED_ARTEFACT_LITERAL_EXTERNAL_URL, - ReferenceType.ATTACHMENT_LITERAL_EXTERNAL_URL).contains(reference.getType(serverBase))) - { - throw new IllegalArgumentException( - "Not a literal external reference, related artifact literal external url or attachment literal external url"); - } - - return clientProvider.endpointExists(reference.getServerBase(serverBase)); - } - @Override public Optional resolveReference(Identity identity, ResourceReference reference, Connection connection) { @@ -131,6 +110,7 @@ public Optional resolveReference(Identity identity, ResourceReference case CONDITIONAL, RELATED_ARTEFACT_CONDITIONAL_URL, ATTACHMENT_CONDITIONAL_URL -> resolveConditionalReference(identity, reference, connection); case LOGICAL -> resolveLogicalReference(identity, reference, connection); + default -> throw new IllegalArgumentException("Reference of type " + type + " not supported"); }; } @@ -240,7 +220,9 @@ private Optional resolveConditionalReference(Identity identity, Resour Connection connection) { Objects.requireNonNull(reference, "reference"); - throwIfReferenceTypeUnexpected(reference.getType(serverBase), ReferenceType.CONDITIONAL, + + ReferenceType referenceType = reference.getType(serverBase); + throwIfReferenceTypeUnexpected(referenceType, ReferenceType.CONDITIONAL, ReferenceType.RELATED_ARTEFACT_CONDITIONAL_URL, ReferenceType.ATTACHMENT_CONDITIONAL_URL); String referenceValue = reference.getValue(); @@ -271,7 +253,7 @@ private Optional resolveConditionalReference(Identity identity, Resour return Optional.empty(); } - return search(identity, connection, d, reference, condition.getQueryParams(), true); + return search(identity, connection, d, reference, condition.getQueryParams(), referenceType); } } @@ -302,14 +284,14 @@ private Optional resolveLogicalReference(Identity identity, ResourceRe } Identifier targetIdentifier = reference.getReference().getIdentifier(); - return search(identity, connection, d, reference, Map.of("identifier", - Collections.singletonList(targetIdentifier.getSystem() + "|" + targetIdentifier.getValue())), true); + return search(identity, connection, d, reference, + Map.of("identifier", List.of(targetIdentifier.getSystem() + "|" + targetIdentifier.getValue())), + ReferenceType.LOGICAL); } } private Optional search(Identity identity, Connection connection, ResourceDao referenceTargetDao, - ResourceReference resourceReference, Map> queryParameters, - boolean logicalNotConditional) + ResourceReference resourceReference, Map> queryParameters, ReferenceType referenceType) { if (Arrays.stream(SearchQuery.STANDARD_PARAMETERS).anyMatch(queryParameters::containsKey)) { @@ -333,11 +315,17 @@ private Optional search(Identity identity, Connection connection, Reso String unsupportedQueryParametersString = unsupportedQueryParameters.stream() .map(SearchQueryParameterError::toString).collect(Collectors.joining("; ")); - logger.warn("{} reference {} at {} in resource contains unsupported queryparameter{} {}", - logicalNotConditional ? "Logical" : "Conditional", queryParameters, resourceReference.getLocation(), - unsupportedQueryParameters.size() != 1 ? "s" : "", unsupportedQueryParametersString); - - return Optional.empty(); + if (EnumSet.of(ReferenceType.CONDITIONAL, ReferenceType.RELATED_ARTEFACT_CONDITIONAL_URL, + ReferenceType.ATTACHMENT_CONDITIONAL_URL).contains(referenceType)) + { + logger.warn("Conditional reference {} at {} in resource contains unsupported queryparameter{} {}", + queryParameters, resourceReference.getLocation(), + unsupportedQueryParameters.size() != 1 ? "s" : "", unsupportedQueryParametersString); + return Optional.empty(); + } + else + throw new IllegalStateException("Unable to search for " + referenceTargetDao.getResourceTypeName() + + ": Unsupported query parameters"); } PartialResult result = exceptionHandler.handleSqlException(() -> @@ -348,39 +336,26 @@ private Optional search(Identity identity, Connection connection, Reso return referenceTargetDao.searchWithTransaction(connection, query); }); - if (result.getTotal() <= 0) - { - if (logicalNotConditional) - logger.warn("Reference target by identifier '{}|{}' of reference at {} in resource", - resourceReference.getReference().getIdentifier().getSystem(), - resourceReference.getReference().getIdentifier().getValue(), resourceReference.getLocation()); - else - logger.warn("Reference target by condition '{}' of reference at {} in resource", - UriComponentsBuilder.newInstance().path(referenceTargetDao.getResourceTypeName()) - .replaceQueryParams(CollectionUtils.toMultiValueMap(queryParameters)).toUriString(), - resourceReference.getLocation()); - - return Optional.empty(); - } - else if (result.getTotal() == 1) - { + if (result.getTotal() == 1) return Optional.of(result.getPartialResult().get(0)); - } - else // if (result.getOverallCount() > 1) + + else { int overallCount = result.getTotal(); - if (logicalNotConditional) - logger.warn( - "Found {} matches for reference target by identifier '{}|{}' of reference at {} in resource", - overallCount, resourceReference.getReference().getIdentifier().getSystem(), - resourceReference.getReference().getIdentifier().getValue(), resourceReference.getLocation()); - else - logger.warn("Found {} matches for reference target by condition '{}' of reference at {} in resource", - overallCount, + if (ReferenceType.LOGICAL.equals(referenceType)) + logger.warn("Found {} matches for reference at {} with identifier '{}|{}'", overallCount, + resourceReference.getLocation(), resourceReference.getReference().getIdentifier().getSystem(), + resourceReference.getReference().getIdentifier().getValue()); + else if (EnumSet.of(ReferenceType.CONDITIONAL, ReferenceType.RELATED_ARTEFACT_CONDITIONAL_URL, + ReferenceType.ATTACHMENT_CONDITIONAL_URL).contains(referenceType)) + logger.warn("Found {} matches for reference at {} with condition '{}'", overallCount, + resourceReference.getLocation(), UriComponentsBuilder.newInstance().path(referenceTargetDao.getResourceTypeName()) - .replaceQueryParams(CollectionUtils.toMultiValueMap(queryParameters)).toUriString(), - resourceReference.getLocation()); + .replaceQueryParams(CollectionUtils.toMultiValueMap(queryParameters)).toUriString()); + else if (ReferenceType.CANONICAL.equals(referenceType)) + logger.warn("Found {} matches for reference at {} with url '{}'", overallCount, + resourceReference.getLocation(), resourceReference.getCanonical().getValue()); return Optional.empty(); } @@ -558,8 +533,9 @@ public Optional checkLogicalReference(Identity identity, Resou Identifier targetIdentifier = reference.getReference().getIdentifier(); // Resource target = - return search(identity, resource, bundleIndex, connection, d, reference, Map.of("identifier", - Collections.singletonList(targetIdentifier.getSystem() + "|" + targetIdentifier.getValue())), true); + return search(identity, resource, bundleIndex, connection, d, reference, + Map.of("identifier", List.of(targetIdentifier.getSystem() + "|" + targetIdentifier.getValue())), + true); // resourceReference.getReference().setIdentifier(null).setReferenceElement( // new IdType(target.getResourceType().name(), target.getIdElement().getIdPart())); @@ -627,4 +603,46 @@ else if (result.getTotal() == 1) resource, resourceReference, result.getTotal())); } } + + @Override + public Optional checkCanonicalReference(Identity identity, Resource resource, + ResourceReference reference, Connection connection) throws IllegalArgumentException + { + return checkCanonicalReference(identity, resource, reference, connection, null); + } + + @Override + public Optional checkCanonicalReference(Identity identity, Resource resource, + ResourceReference reference, Connection connection, Integer bundleIndex) throws IllegalArgumentException + { + Objects.requireNonNull(identity, "identity"); + Objects.requireNonNull(resource, "resource"); + Objects.requireNonNull(reference, "reference"); + Objects.requireNonNull(connection, "connection"); + throwIfReferenceTypeUnexpected(reference.getType(serverBase), ReferenceType.CANONICAL); + + Optional> referenceDao = switch (reference.getLocation()) + { + case QUESTIONNAIRE_RESPONSE_QUESTIONNAIRE -> Optional.of(daoProvider.getQuestionnaireDao()); + case TASK_INSTANTIATES_CANONICAL -> Optional.of(daoProvider.getActivityDefinitionDao()); + + default -> Optional.empty(); + }; + + if (referenceDao.isEmpty()) + { + logger.debug( + "Canonical reference check only implemented for QuestionnaireResponse.questionnaire and Task.instantiatesCanonical, not checking {}", + reference.getLocation()); + return Optional.empty(); + } + + Optional referencedResource = referenceDao.flatMap(dao -> search(identity, connection, dao, reference, + Map.of("url", List.of(reference.getCanonical().getValue())), ReferenceType.CANONICAL)); + + if (referencedResource.isPresent()) + return Optional.empty(); + else + return Optional.of(responseGenerator.referenceTargetNotFoundLocally(bundleIndex, resource, reference)); + } } \ No newline at end of file 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 4f89bdf48..98ee47a55 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 @@ -243,7 +243,7 @@ private void checkReferences(Resource resource, Connection connection, Predicate throws WebApplicationException { referenceExtractor.getReferences(resource).filter(checkReference) - .filter(ref -> referenceResolver.referenceCanBeChecked(ref, connection)).forEach(ref -> + .filter(ref -> referenceResolver.referenceCanBeResolved(ref, connection)).forEach(ref -> { Optional outcome = checkReference(resource, connection, ref); if (outcome.isPresent()) @@ -268,6 +268,9 @@ private Optional checkReference(Resource resource, Connection case LOGICAL -> referenceResolver.checkLogicalReference(getCurrentIdentity(), resource, reference, connection); + case CANONICAL -> + referenceResolver.checkCanonicalReference(getCurrentIdentity(), resource, reference, connection); + // unknown URLs to non FHIR servers in related artifacts must not be checked case RELATED_ARTEFACT_UNKNOWN_URL, ATTACHMENT_UNKNOWN_URL -> Optional.empty(); 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 7ab4d051c..90e0f7e40 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 @@ -38,4 +38,6 @@ + + diff --git a/dsf-fhir/dsf-fhir-server/src/main/resources/db/db.read_access.changelog-1.6.0.xml b/dsf-fhir/dsf-fhir-server/src/main/resources/db/db.read_access.changelog-1.6.0.xml new file mode 100644 index 000000000..a63509673 --- /dev/null +++ b/dsf-fhir/dsf-fhir-server/src/main/resources/db/db.read_access.changelog-1.6.0.xml @@ -0,0 +1,72 @@ + + + + + + + + + SELECT + id + , version + , type + , resource + FROM ( + SELECT activity_definition_id AS id, version, 'ActivityDefinition'::text AS type, activity_definition AS resource FROM current_activity_definitions + UNION + SELECT binary_id AS id, version, 'Binary'::text AS type, binary_json AS resource FROM current_binaries + UNION + SELECT bundle_id AS id, version, 'Bundle'::text AS type, bundle AS resource FROM current_bundles + UNION + SELECT code_system_id AS id, version, 'CodeSystem'::text AS type, code_system AS resource FROM current_code_systems + UNION + SELECT document_reference_id AS id, version, 'DocumentReference'::text AS type, document_reference AS resource FROM current_document_references + UNION + SELECT endpoint_id AS id, version, 'Endpoint'::text AS type, endpoint AS resource FROM current_endpoints + UNION + SELECT group_id AS id, version, 'Group'::text AS type, group_json AS resource FROM current_groups + UNION + SELECT healthcare_service_id AS id, version, 'HealthcareService'::text AS type, healthcare_service AS resource FROM current_healthcare_services + UNION + SELECT library_id AS id, version, 'Library'::text AS type, library AS resource FROM current_libraries + UNION + SELECT location_id AS id, version, 'Location'::text AS type, location AS resource FROM current_locations + UNION + SELECT measure_report_id AS id, version, 'MeasureReport'::text AS type, measure_report AS resource FROM current_measure_reports + UNION + SELECT measure_id AS id, version, 'Measure'::text AS type, measure AS resource FROM current_measures + UNION + SELECT naming_system_id AS id, version, 'NamingSystem'::text AS type, naming_system AS resource FROM current_naming_systems + UNION + SELECT organization_id AS id, version, 'Organization'::text AS type, organization AS resource FROM current_organizations + UNION + SELECT organization_affiliation_id AS id, version, 'OrganizationAffiliation'::text AS type, organization_affiliation AS resource FROM current_organization_affiliations + UNION + SELECT patient_id AS id, version, 'Patient'::text AS type, patient AS resource FROM current_patients + UNION + SELECT practitioner_role_id AS id, version, 'PractitionerRole'::text AS type, practitioner_role AS resource FROM current_practitioner_roles + UNION + SELECT practitioner_id AS id, version, 'Practitioner'::text AS type, practitioner AS resource FROM current_practitioners + UNION + SELECT provenance_id AS id, version, 'Provenance'::text AS type, provenance AS resource FROM current_provenances + UNION + SELECT questionnaire_id AS id, version, 'Questionnaire'::text AS type, questionnaire AS resource FROM current_questionnaires + UNION + SELECT research_study_id AS id, version, 'ResearchStudy'::text AS type, research_study AS resource FROM current_research_studies + UNION + SELECT structure_definition_id AS id, version, 'StructureDefinition'::text AS type, structure_definition AS resource FROM current_structure_definitions + UNION + SELECT subscription_id AS id, version, 'Subscription'::text AS type, subscription AS resource FROM current_subscriptions + UNION + SELECT value_set_id AS id, version, 'ValueSet'::text AS type, value_set AS resource FROM current_value_sets + ) AS current_all_read_access + + + ALTER TABLE all_read_access_resources OWNER TO ${db.liquibase_user}; + GRANT ALL ON TABLE all_read_access_resources TO ${db.liquibase_user}; + GRANT SELECT ON TABLE all_read_access_resources TO ${db.server_users_group}; + + + \ No newline at end of file diff --git a/dsf-fhir/dsf-fhir-server/src/main/resources/db/trigger_functions/on_organization_affiliations_insert.sql b/dsf-fhir/dsf-fhir-server/src/main/resources/db/trigger_functions/on_organization_affiliations_insert.sql index 5437390a7..80a0d02e3 100644 --- a/dsf-fhir/dsf-fhir-server/src/main/resources/db/trigger_functions/on_organization_affiliations_insert.sql +++ b/dsf-fhir/dsf-fhir-server/src/main/resources/db/trigger_functions/on_organization_affiliations_insert.sql @@ -1,5 +1,6 @@ CREATE OR REPLACE FUNCTION on_organization_affiliations_insert() RETURNS TRIGGER AS $$ DECLARE + organization_affiliation_exists_active_roles JSONB := (SELECT organization_affiliation->'code' FROM organization_affiliations WHERE organization_affiliation_id = NEW.organization_affiliation_id AND version = NEW.version - 1 AND deleted IS NULL AND organization_affiliation->>'active' = 'true'); reference_regex TEXT := '((http|https):\/\/([A-Za-z0-9\-\\\.\:\%\$]*\/)+)?(Account|ActivityDefinition|AdverseEvent|AllergyIntolerance|Appointment|AppointmentResponse|AuditEvent|Basic|Binary|BiologicallyDerivedProduct|BodyStructure|Bundle|CapabilityStatement|CarePlan|CareTeam|CatalogEntry|ChargeItem|ChargeItemDefinition|Claim|ClaimResponse|ClinicalImpression|CodeSystem|Communication|CommunicationRequest|CompartmentDefinition|Composition|ConceptMap|Condition|Consent|Contract|Coverage|CoverageEligibilityRequest|CoverageEligibilityResponse|DetectedIssue|Device|DeviceDefinition|DeviceMetric|DeviceRequest|DeviceUseStatement|DiagnosticReport|DocumentManifest|DocumentReference|EffectEvidenceSynthesis|Encounter|Endpoint|EnrollmentRequest|EnrollmentResponse|EpisodeOfCare|EventDefinition|Evidence|EvidenceVariable|ExampleScenario|ExplanationOfBenefit|FamilyMemberHistory|Flag|Goal|GraphDefinition|Group|GuidanceResponse|HealthcareService|ImagingStudy|Immunization|ImmunizationEvaluation|ImmunizationRecommendation|ImplementationGuide|InsurancePlan|Invoice|Library|Linkage|List|Location|Measure|MeasureReport|Media|Medication|MedicationAdministration|MedicationDispense|MedicationKnowledge|MedicationRequest|MedicationStatement|MedicinalProduct|MedicinalProductAuthorization|MedicinalProductContraindication|MedicinalProductIndication|MedicinalProductIngredient|MedicinalProductInteraction|MedicinalProductManufactured|MedicinalProductPackaged|MedicinalProductPharmaceutical|MedicinalProductUndesirableEffect|MessageDefinition|MessageHeader|MolecularSequence|NamingSystem|NutritionOrder|Observation|ObservationDefinition|OperationDefinition|OperationOutcome|Organization|OrganizationAffiliation|Patient|PaymentNotice|PaymentReconciliation|Person|PlanDefinition|Practitioner|PractitionerRole|Procedure|Provenance|Questionnaire|QuestionnaireResponse|RelatedPerson|RequestGroup|ResearchDefinition|ResearchElementDefinition|ResearchStudy|ResearchSubject|RiskAssessment|RiskEvidenceSynthesis|Schedule|SearchParameter|ServiceRequest|Slot|Specimen|SpecimenDefinition|StructureDefinition|StructureMap|Subscription|Substance|SubstanceNucleicAcid|SubstancePolymer|SubstanceProtein|SubstanceReferenceInformation|SubstanceSourceMaterial|SubstanceSpecification|SupplyDelivery|SupplyRequest|Task|TerminologyCapabilities|TestReport|TestScript|ValueSet|VerificationResult|VisionPrescription)\/([A-Za-z0-9\-\.]{1,64})(\/_history\/([A-Za-z0-9\-\.]{1,64}))?'; parent_organization_identifier TEXT; member_organization_id UUID; @@ -8,16 +9,22 @@ DECLARE delete_count INT; BEGIN PERFORM on_resources_insert(NEW.organization_affiliation_id, NEW.version, NEW.organization_affiliation); - - DELETE FROM read_access - WHERE access_type = 'ROLE' - AND organization_affiliation_id = NEW.organization_affiliation_id; - GET DIAGNOSTICS delete_count = ROW_COUNT; - RAISE NOTICE 'Existing rows deleted from read_access for created/updated organization-affiliation: %', delete_count; - - RAISE NOTICE 'NEW.organization_affiliation->>''active'' = ''%''', NEW.organization_affiliation->>'active'; - IF (NEW.organization_affiliation->>'active' = 'true') THEN + IF ((NEW.organization_affiliation->>'active' = 'false') AND organization_affiliation_exists_active_roles IS NOT NULL) + OR ((NEW.organization_affiliation->>'active' = 'true') AND organization_affiliation_exists_active_roles IS NOT NULL AND NEW.organization_affiliation->'code' <> organization_affiliation_exists_active_roles) THEN + RAISE NOTICE 'new organization_affiliation inactive and old organization_affiliation exists and active -> delete'; + + DELETE FROM read_access + WHERE access_type = 'ROLE' + AND organization_affiliation_id = NEW.organization_affiliation_id; + + GET DIAGNOSTICS delete_count = ROW_COUNT; + RAISE NOTICE 'Existing rows deleted from read_access for created/updated organization-affiliation: %', delete_count; + + ELSIF ((NEW.organization_affiliation->>'active' = 'true') AND NOT organization_affiliation_exists_active_roles IS NOT NULL) + OR ((NEW.organization_affiliation->>'active' = 'true') AND organization_affiliation_exists_active_roles IS NOT NULL AND NEW.organization_affiliation->'code' <> organization_affiliation_exists_active_roles) THEN + RAISE NOTICE 'new organization_affiliation active and old organization_affiliation not exist or inactive -> insert'; + parent_organization_identifier := jsonb_path_query(organization, '$.identifier[*] ? (@.system == "http://dsf.dev/sid/organization-identifier")')->>'value' FROM current_organizations WHERE organization_id = (regexp_match(NEW.organization_affiliation->'organization'->>'reference', reference_regex))[5]::uuid @@ -43,7 +50,7 @@ BEGIN id , version , resource - FROM all_resources + FROM all_read_access_resources ) AS r ON r.resource->'meta'->'tag' @> ('[{"extension":[{"url":"http://dsf.dev/fhir/StructureDefinition/extension-read-access-parent-organization-role","extension":[{"url":"parent-organization","valueIdentifier":{"system":"http://dsf.dev/sid/organization-identifier","value":"' @@ -66,10 +73,8 @@ BEGIN GET DIAGNOSTICS binary_insert_count = ROW_COUNT; RAISE NOTICE 'Rows inserted into read_access based on Binary.securityContext: %', binary_insert_count; END IF; - - ELSIF (NEW.organization_affiliation->>'active' = 'false') THEN - RAISE NOTICE 'Not inserting any entries to read_access, created/updated organization-affiliation is not active'; END IF; + RETURN NEW; END; $$ LANGUAGE PLPGSQL \ No newline at end of file diff --git a/dsf-fhir/dsf-fhir-server/src/main/resources/db/trigger_functions/on_organizations_insert.sql b/dsf-fhir/dsf-fhir-server/src/main/resources/db/trigger_functions/on_organizations_insert.sql index 2eb3218c1..911658f2b 100644 --- a/dsf-fhir/dsf-fhir-server/src/main/resources/db/trigger_functions/on_organizations_insert.sql +++ b/dsf-fhir/dsf-fhir-server/src/main/resources/db/trigger_functions/on_organizations_insert.sql @@ -1,5 +1,6 @@ CREATE OR REPLACE FUNCTION on_organizations_insert() RETURNS TRIGGER AS $$ DECLARE + organization_exists_active BOOLEAN := EXISTS (SELECT 1 FROM organizations WHERE organization_id = NEW.organization_id AND version = NEW.version - 1 AND deleted IS NULL AND organization->>'active' = 'true'); reference_regex TEXT := '((http|https):\/\/([A-Za-z0-9\-\\\.\:\%\$]*\/)+)?(Account|ActivityDefinition|AdverseEvent|AllergyIntolerance|Appointment|AppointmentResponse|AuditEvent|Basic|Binary|BiologicallyDerivedProduct|BodyStructure|Bundle|CapabilityStatement|CarePlan|CareTeam|CatalogEntry|ChargeItem|ChargeItemDefinition|Claim|ClaimResponse|ClinicalImpression|CodeSystem|Communication|CommunicationRequest|CompartmentDefinition|Composition|ConceptMap|Condition|Consent|Contract|Coverage|CoverageEligibilityRequest|CoverageEligibilityResponse|DetectedIssue|Device|DeviceDefinition|DeviceMetric|DeviceRequest|DeviceUseStatement|DiagnosticReport|DocumentManifest|DocumentReference|EffectEvidenceSynthesis|Encounter|Endpoint|EnrollmentRequest|EnrollmentResponse|EpisodeOfCare|EventDefinition|Evidence|EvidenceVariable|ExampleScenario|ExplanationOfBenefit|FamilyMemberHistory|Flag|Goal|GraphDefinition|Group|GuidanceResponse|HealthcareService|ImagingStudy|Immunization|ImmunizationEvaluation|ImmunizationRecommendation|ImplementationGuide|InsurancePlan|Invoice|Library|Linkage|List|Location|Measure|MeasureReport|Media|Medication|MedicationAdministration|MedicationDispense|MedicationKnowledge|MedicationRequest|MedicationStatement|MedicinalProduct|MedicinalProductAuthorization|MedicinalProductContraindication|MedicinalProductIndication|MedicinalProductIngredient|MedicinalProductInteraction|MedicinalProductManufactured|MedicinalProductPackaged|MedicinalProductPharmaceutical|MedicinalProductUndesirableEffect|MessageDefinition|MessageHeader|MolecularSequence|NamingSystem|NutritionOrder|Observation|ObservationDefinition|OperationDefinition|OperationOutcome|Organization|OrganizationAffiliation|Patient|PaymentNotice|PaymentReconciliation|Person|PlanDefinition|Practitioner|PractitionerRole|Procedure|Provenance|Questionnaire|QuestionnaireResponse|RelatedPerson|RequestGroup|ResearchDefinition|ResearchElementDefinition|ResearchStudy|ResearchSubject|RiskAssessment|RiskEvidenceSynthesis|Schedule|SearchParameter|ServiceRequest|Slot|Specimen|SpecimenDefinition|StructureDefinition|StructureMap|Subscription|Substance|SubstanceNucleicAcid|SubstancePolymer|SubstanceProtein|SubstanceReferenceInformation|SubstanceSourceMaterial|SubstanceSpecification|SupplyDelivery|SupplyRequest|Task|TerminologyCapabilities|TestReport|TestScript|ValueSet|VerificationResult|VisionPrescription)\/([A-Za-z0-9\-\.]{1,64})(\/_history\/([A-Za-z0-9\-\.]{1,64}))?'; organization_identifier TEXT := jsonb_path_query(NEW.organization, '$.identifier[*]?(@.system == "http://dsf.dev/sid/organization-identifier")')->>'value'; organization_insert_count INT; @@ -9,36 +10,40 @@ DECLARE roles_delete_count INT; BEGIN PERFORM on_resources_insert(NEW.organization_id, NEW.version, NEW.organization); - - DELETE FROM read_access - WHERE access_type = 'ORGANIZATION' - AND organization_id = NEW.organization_id; - - GET DIAGNOSTICS delete_count = ROW_COUNT; - RAISE NOTICE 'Existing rows deleted from read_access for created/updated organization, ORGANIZATION Tag: %', delete_count; - - DELETE FROM read_access - WHERE access_type = 'ROLE' - AND organization_affiliation_id IN ( - SELECT organization_affiliation_id FROM current_organization_affiliations - WHERE NEW.organization_id = (regexp_match(organization_affiliation->'participatingOrganization'->>'reference', reference_regex))[5]::uuid - OR NEW.organization_id = (regexp_match(organization_affiliation->'organization'->>'reference', reference_regex))[5]::uuid - ); - GET DIAGNOSTICS roles_delete_count = ROW_COUNT; - RAISE NOTICE 'Existing rows deleted from read_access for created/updated organization, ROLE Tag: %', roles_delete_count; - - RAISE NOTICE 'NEW.organization->>''active'' = ''%''', NEW.organization->>'active'; - IF (NEW.organization->>'active' = 'true') THEN + IF (NEW.organization->>'active' = 'false') AND organization_exists_active THEN + RAISE NOTICE 'new organization inactive and old organization exists and active -> delete'; + + DELETE FROM read_access + WHERE access_type = 'ORGANIZATION' + AND organization_id = NEW.organization_id; + + GET DIAGNOSTICS delete_count = ROW_COUNT; + RAISE NOTICE 'Existing rows deleted from read_access for created/updated organization, ORGANIZATION Tag: %', delete_count; + + DELETE FROM read_access + WHERE access_type = 'ROLE' + AND organization_affiliation_id IN ( + SELECT organization_affiliation_id FROM current_organization_affiliations + WHERE NEW.organization_id = (regexp_match(organization_affiliation->'participatingOrganization'->>'reference', reference_regex))[5]::uuid + OR NEW.organization_id = (regexp_match(organization_affiliation->'organization'->>'reference', reference_regex))[5]::uuid + ); + + GET DIAGNOSTICS roles_delete_count = ROW_COUNT; + RAISE NOTICE 'Existing rows deleted from read_access for created/updated organization, ROLE Tag: %', roles_delete_count; + + ELSIF (NEW.organization->>'active' = 'true') AND NOT organization_exists_active THEN + RAISE NOTICE 'new organization active and old organization not exist or inactive -> insert'; + INSERT INTO read_access SELECT id, version, 'ORGANIZATION', NEW.organization_id, NULL - FROM all_resources + FROM all_read_access_resources WHERE jsonb_path_exists(resource,('$.meta.tag[*] ? (@.code == "ORGANIZATION" && @.system == "http://dsf.dev/fhir/CodeSystem/read-access-tag") .extension[*]?(@.url == "http://dsf.dev/fhir/StructureDefinition/extension-read-access-organization") .valueIdentifier[*]?(@.system == "http://dsf.dev/sid/organization-identifier" && @.value == "' || organization_identifier || '")')::jsonpath); - + GET DIAGNOSTICS organization_insert_count = ROW_COUNT; - + WITH temp_role_ids AS ( INSERT INTO read_access SELECT r.id, r.version, 'ROLE', member_organization_id, organization_affiliation_id FROM ( @@ -72,7 +77,7 @@ BEGIN WHERE parent_organization_organization_id = NEW.organization_id OR member_organization_id = NEW.organization_id ) AS oa LEFT JOIN ( - SELECT id, version, resource FROM all_resources + SELECT id, version, resource FROM all_read_access_resources ) AS r ON r.resource->'meta'->'tag' @> ('[{"extension":[{"url":"http://dsf.dev/fhir/StructureDefinition/extension-read-access-parent-organization-role","extension":[{"url":"parent-organization","valueIdentifier":{"system":"http://dsf.dev/sid/organization-identifier","value":"' @@ -101,10 +106,8 @@ BEGIN GET DIAGNOSTICS binary_insert_count = ROW_COUNT; RAISE NOTICE 'Rows inserted into read_access based on Binary.securityContext: %', binary_insert_count; - - ELSIF (NEW.organization->>'active' = 'false') THEN - RAISE NOTICE 'Not inserting any entries to read_access, created/updated organization is not active'; END IF; + RETURN NEW; END; $$ LANGUAGE PLPGSQL \ No newline at end of file diff --git a/dsf-fhir/dsf-fhir-server/src/test/java/dev/dsf/fhir/dao/AbstractReadAccessDaoTest.java b/dsf-fhir/dsf-fhir-server/src/test/java/dev/dsf/fhir/dao/AbstractReadAccessDaoTest.java index f4b0cb833..134f3ec3a 100644 --- a/dsf-fhir/dsf-fhir-server/src/test/java/dev/dsf/fhir/dao/AbstractReadAccessDaoTest.java +++ b/dsf-fhir/dsf-fhir-server/src/test/java/dev/dsf/fhir/dao/AbstractReadAccessDaoTest.java @@ -522,6 +522,49 @@ public void testReadAccessTriggerRoleUpdate() throws Exception assertReadAccessEntryCount(2, 1, v1, READ_ACCESS_TAG_VALUE_ROLE, createdMemberOrg, updatedAff); } + @Test + public void testReadAccessTriggerRoleUpdateRoleChange() throws Exception + { + final OrganizationAffiliationDaoJdbc organizationAffiliationDao = new OrganizationAffiliationDaoJdbc( + defaultDataSource, permanentDeleteDataSource, fhirContext); + + Organization parentOrg = new Organization(); + parentOrg.setActive(true); + parentOrg.addIdentifier().setSystem(ORGANIZATION_IDENTIFIER_SYSTEM).setValue("parent.com"); + + Organization memberOrg = new Organization(); + memberOrg.setActive(true); + memberOrg.addIdentifier().setSystem(ORGANIZATION_IDENTIFIER_SYSTEM).setValue("member.com"); + + OrganizationDao orgDao = new OrganizationDaoJdbc(defaultDataSource, permanentDeleteDataSource, fhirContext); + Organization createdParentOrg = orgDao.create(parentOrg); + Organization createdMemberOrg = orgDao.create(memberOrg); + + OrganizationAffiliation aff = new OrganizationAffiliation(); + aff.setActive(true); + aff.getCodeFirstRep().getCodingFirstRep().setSystem("http://dsf.dev/fhir/CodeSystem/organization-role") + .setCode("DIC"); + aff.getOrganization().setReference("Organization/" + createdParentOrg.getIdElement().getIdPart()); + aff.getParticipatingOrganization().setReference("Organization/" + createdMemberOrg.getIdElement().getIdPart()); + + OrganizationAffiliation createdAff = organizationAffiliationDao.create(aff); + + D d = createResource(); + readAccessHelper.addRole(d, "parent.com", "http://dsf.dev/fhir/CodeSystem/organization-role", "DIC"); + + D v1 = getDao().create(d); + assertEquals(1L, (long) v1.getIdElement().getVersionIdPartAsLong()); + + assertReadAccessEntryCount(2, 1, v1, READ_ACCESS_TAG_VALUE_LOCAL); + assertReadAccessEntryCount(2, 1, v1, READ_ACCESS_TAG_VALUE_ROLE, createdMemberOrg, createdAff); + + createdAff.getCodeFirstRep().getCodingFirstRep().setCode("HRP"); + OrganizationAffiliation updatedAff = organizationAffiliationDao.update(createdAff); + + assertReadAccessEntryCount(1, 1, v1, READ_ACCESS_TAG_VALUE_LOCAL); + assertReadAccessEntryCount(1, 0, v1, READ_ACCESS_TAG_VALUE_ROLE, createdMemberOrg, updatedAff); + } + @Test public void testReadAccessTriggerRoleUpdateMemberOrganizationNonActive() throws Exception { diff --git a/dsf-fhir/dsf-fhir-server/src/test/java/dev/dsf/fhir/dao/OrganizationAffiliationDaoTest.java b/dsf-fhir/dsf-fhir-server/src/test/java/dev/dsf/fhir/dao/OrganizationAffiliationDaoTest.java index 42bbb1c95..7e1e817c7 100644 --- a/dsf-fhir/dsf-fhir-server/src/test/java/dev/dsf/fhir/dao/OrganizationAffiliationDaoTest.java +++ b/dsf-fhir/dsf-fhir-server/src/test/java/dev/dsf/fhir/dao/OrganizationAffiliationDaoTest.java @@ -6,6 +6,8 @@ import static org.junit.Assert.assertNotNull; import static org.junit.Assert.assertTrue; +import java.io.IOException; +import java.io.Reader; import java.sql.Connection; import java.sql.SQLException; import java.util.List; @@ -15,6 +17,10 @@ import org.hl7.fhir.r4.model.Organization; import org.hl7.fhir.r4.model.OrganizationAffiliation; import org.junit.Test; +import org.postgresql.copy.CopyManager; +import org.postgresql.core.BaseConnection; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; import dev.dsf.fhir.authorization.read.ReadAccessHelper; import dev.dsf.fhir.authorization.read.ReadAccessHelperImpl; @@ -25,6 +31,8 @@ public class OrganizationAffiliationDaoTest extends AbstractReadAccessDaoTest { + private static final Logger logger = LoggerFactory.getLogger(OrganizationAffiliationDaoTest.class); + private static final String identifierSystem = "http://dsf.dev/sid/organization-identifier"; private static final String identifierValue = "identifier.test"; private static final boolean active = true; @@ -315,4 +323,114 @@ public void testExistsNotDeletedByParentOrganizationMemberOrganizationRoleAndNot assertTrue(exists2); } } + + private static class TaskAsCsvGeneratorReader extends Reader + { + public static final int TASK_ROW_LINE_LENGTH = 1615; + + private final int maxTasks; + private int currentTask; + + public TaskAsCsvGeneratorReader(int maxTasks) + { + this.maxTasks = maxTasks; + } + + @Override + public int read(char[] cbuf, int off, int len) throws IOException + { + if (len != TASK_ROW_LINE_LENGTH) + throw new IllegalArgumentException("Buffer length " + TASK_ROW_LINE_LENGTH + " expected, not " + len); + if (off % TASK_ROW_LINE_LENGTH != 0) + throw new IllegalArgumentException("Buffer offset mod " + TASK_ROW_LINE_LENGTH + " == 0 expected, not " + + (off & TASK_ROW_LINE_LENGTH) + " (off = " + off + ")"); + + if (currentTask < maxTasks) + { + String line = generateLine(); + + if (line.length() != TASK_ROW_LINE_LENGTH) + throw new IllegalArgumentException("Line length " + TASK_ROW_LINE_LENGTH + " expected"); + + System.arraycopy(line.toCharArray(), 0, cbuf, 0, TASK_ROW_LINE_LENGTH); + currentTask++; + + return TASK_ROW_LINE_LENGTH; + } + else + return -1; + } + + @Override + public void close() throws IOException + { + } + + private String generateLine() + { + String id = UUID.randomUUID().toString(); + return "${id},3,,\"{\"\"resourceType\"\":\"\"Task\"\",\"\"id\"\":\"\"${id}\"\",\"\"meta\"\":{\"\"versionId\"\":\"\"3\"\",\"\"lastUpdated\"\":\"\"2024-10-01T18:22:06.765+02:00\"\",\"\"profile\"\":[\"\"http://medizininformatik-initiative.de/fhir/StructureDefinition/feasibility-task-execute|1.0\"\"]},\"\"instantiatesCanonical\"\":\"\"http://medizininformatik-initiative.de/bpe/Process/feasibilityExecute|1.0\"\",\"\"status\"\":\"\"completed\"\",\"\"intent\"\":\"\"order\"\",\"\"authoredOn\"\":\"\"2024-10-01T18:22:07+02:00\"\",\"\"requester\"\":{\"\"type\"\":\"\"Organization\"\",\"\"identifier\"\":{\"\"system\"\":\"\"http://dsf.dev/sid/organization-identifier\"\",\"\"value\"\":\"\"organization.com\"\"}},\"\"restriction\"\":{\"\"recipient\"\":[{\"\"type\"\":\"\"Organization\"\",\"\"identifier\"\":{\"\"system\"\":\"\"http://dsf.dev/sid/organization-identifier\"\",\"\"value\"\":\"\"organization.com\"\"}}]},\"\"input\"\":[{\"\"type\"\":{\"\"coding\"\":[{\"\"system\"\":\"\"http://dsf.dev/fhir/CodeSystem/bpmn-message\"\",\"\"code\"\":\"\"message-name\"\"}]},\"\"valueString\"\":\"\"feasibilityExecuteMessage\"\"},{\"\"type\"\":{\"\"coding\"\":[{\"\"system\"\":\"\"http://dsf.dev/fhir/CodeSystem/bpmn-message\"\",\"\"code\"\":\"\"business-key\"\"}]},\"\"valueString\"\":\"\"${business-key}\"\"},{\"\"type\"\":{\"\"coding\"\":[{\"\"system\"\":\"\"http://dsf.dev/fhir/CodeSystem/bpmn-message\"\",\"\"code\"\":\"\"correlation-key\"\"}]},\"\"valueString\"\":\"\"${correlation-key}\"\"},{\"\"type\"\":{\"\"coding\"\":[{\"\"system\"\":\"\"http://medizininformatik-initiative.de/fhir/CodeSystem/feasibility\"\",\"\"code\"\":\"\"measure-reference\"\"}]},\"\"valueReference\"\":{\"\"reference\"\":\"\"https://dsf.fdpg.test.forschen-fuer-gesundheit.de/fhir/Measure/02bb7540-0d99-4c0e-8764-981e545cf646\"\"}}]}\"\n" + .replace("${id}", id).replace("${business-key}", UUID.randomUUID().toString()) + .replace("${correlation-key}", UUID.randomUUID().toString()); + } + } + + @Test + public void testBigUpdate() throws Exception + { + OrganizationDaoJdbc orgDao = new OrganizationDaoJdbc(defaultDataSource, permanentDeleteDataSource, fhirContext); + + Organization memberOrg = new Organization(); + memberOrg.setActive(true); + memberOrg.getIdentifierFirstRep().setSystem(ReadAccessHelper.ORGANIZATION_IDENTIFIER_SYSTEM) + .setValue("organization.com"); + + Organization createdMemberOrg = orgDao.create(memberOrg); + assertNotNull(createdMemberOrg); + + Organization parentOrg = new Organization(); + parentOrg.setActive(true); + parentOrg.getIdentifierFirstRep().setSystem(ReadAccessHelper.ORGANIZATION_IDENTIFIER_SYSTEM) + .setValue("organization.com"); + + Organization createdParentOrg = orgDao.create(parentOrg); + assertNotNull(createdParentOrg); + + OrganizationAffiliation affiliation = new OrganizationAffiliation(); + affiliation.setActive(true); + affiliation.getParticipatingOrganization() + .setReference("Organization/" + createdMemberOrg.getIdElement().getIdPart()); + affiliation.getOrganization().setReference("Organization/" + createdParentOrg.getIdElement().getIdPart()); + affiliation.addCode().getCodingFirstRep().setSystem("role-system").setCode("role-code"); + + OrganizationAffiliation createdAffiliation = dao.create(affiliation); + assertNotNull(createdAffiliation); + + final int taskCount = 500_000; + + logger.info("Inserting {} Task resources ...", taskCount); + try (Connection connection = defaultDataSource.getConnection()) + { + CopyManager copyManager = new CopyManager(connection.unwrap(BaseConnection.class)); + TaskAsCsvGeneratorReader taskGenerator = new TaskAsCsvGeneratorReader(taskCount); + long insertedRows = copyManager.copyIn("COPY tasks FROM STDIN (FORMAT csv)", taskGenerator, + TaskAsCsvGeneratorReader.TASK_ROW_LINE_LENGTH); + + assertEquals(taskCount, insertedRows); + } + logger.info("Inserting {} Task resources [Done]", taskCount); + + long t0 = System.currentTimeMillis(); + + OrganizationAffiliation updatedAffiliation1 = dao.update(createdAffiliation); + assertNotNull(updatedAffiliation1); + + OrganizationAffiliation updatedAffiliation2 = dao.update(updatedAffiliation1); + assertNotNull(updatedAffiliation2); + + long t1 = System.currentTimeMillis(); + + logger.info("OrganizationAffiliation updates executed in {} ms", t1 - t0); + assertTrue("OrganizationAffiliation updates took longer then 200 ms", t1 - t0 <= 200); + } } diff --git a/dsf-fhir/dsf-fhir-server/src/test/java/dev/dsf/fhir/dao/OrganizationDaoTest.java b/dsf-fhir/dsf-fhir-server/src/test/java/dev/dsf/fhir/dao/OrganizationDaoTest.java index 6d89119ac..01c2b37f8 100755 --- a/dsf-fhir/dsf-fhir-server/src/test/java/dev/dsf/fhir/dao/OrganizationDaoTest.java +++ b/dsf-fhir/dsf-fhir-server/src/test/java/dev/dsf/fhir/dao/OrganizationDaoTest.java @@ -4,6 +4,8 @@ import static org.junit.Assert.assertNotNull; import static org.junit.Assert.assertTrue; +import java.io.IOException; +import java.io.Reader; import java.nio.charset.StandardCharsets; import java.sql.Connection; import java.sql.PreparedStatement; @@ -17,7 +19,11 @@ import org.hl7.fhir.r4.model.Organization; import org.hl7.fhir.r4.model.StringType; import org.junit.Test; +import org.postgresql.copy.CopyManager; +import org.postgresql.core.BaseConnection; import org.postgresql.util.PGobject; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; import dev.dsf.fhir.authorization.read.ReadAccessHelper; import dev.dsf.fhir.authorization.read.ReadAccessHelperImpl; @@ -27,6 +33,8 @@ public class OrganizationDaoTest extends AbstractReadAccessDaoTest { + private static final Logger logger = LoggerFactory.getLogger(OrganizationDaoTest.class); + private static final String name = "Demo Organization"; private static final boolean active = true; @@ -234,8 +242,8 @@ public void testUpdateWithExistingBinary() throws Exception org.getIdentifierFirstRep().setSystem(ReadAccessHelper.ORGANIZATION_IDENTIFIER_SYSTEM) .setValue("organization.com"); - Organization cretedOrg = dao.create(org); - assertNotNull(cretedOrg); + Organization createdOrg = dao.create(org); + assertNotNull(createdOrg); Binary binary = new Binary(); binary.setContentType("text/plain"); @@ -246,6 +254,96 @@ public void testUpdateWithExistingBinary() throws Exception Binary createdBinary = binaryDao.create(binary); assertNotNull(createdBinary); - dao.update(cretedOrg); + dao.update(createdOrg); + } + + private static class TaskAsCsvGeneratorReader extends Reader + { + public static final int TASK_ROW_LINE_LENGTH = 1615; + + private final int maxTasks; + private int currentTask; + + public TaskAsCsvGeneratorReader(int maxTasks) + { + this.maxTasks = maxTasks; + } + + @Override + public int read(char[] cbuf, int off, int len) throws IOException + { + if (len != TASK_ROW_LINE_LENGTH) + throw new IllegalArgumentException("Buffer length " + TASK_ROW_LINE_LENGTH + " expected, not " + len); + if (off % TASK_ROW_LINE_LENGTH != 0) + throw new IllegalArgumentException("Buffer offset mod " + TASK_ROW_LINE_LENGTH + " == 0 expected, not " + + (off & TASK_ROW_LINE_LENGTH) + " (off = " + off + ")"); + + if (currentTask < maxTasks) + { + String line = generateLine(); + + if (line.length() != TASK_ROW_LINE_LENGTH) + throw new IllegalArgumentException("Line length " + TASK_ROW_LINE_LENGTH + " expected"); + + System.arraycopy(line.toCharArray(), 0, cbuf, 0, TASK_ROW_LINE_LENGTH); + currentTask++; + + return TASK_ROW_LINE_LENGTH; + } + else + return -1; + } + + @Override + public void close() throws IOException + { + } + + private String generateLine() + { + String id = UUID.randomUUID().toString(); + return "${id},3,,\"{\"\"resourceType\"\":\"\"Task\"\",\"\"id\"\":\"\"${id}\"\",\"\"meta\"\":{\"\"versionId\"\":\"\"3\"\",\"\"lastUpdated\"\":\"\"2024-10-01T18:22:06.765+02:00\"\",\"\"profile\"\":[\"\"http://medizininformatik-initiative.de/fhir/StructureDefinition/feasibility-task-execute|1.0\"\"]},\"\"instantiatesCanonical\"\":\"\"http://medizininformatik-initiative.de/bpe/Process/feasibilityExecute|1.0\"\",\"\"status\"\":\"\"completed\"\",\"\"intent\"\":\"\"order\"\",\"\"authoredOn\"\":\"\"2024-10-01T18:22:07+02:00\"\",\"\"requester\"\":{\"\"type\"\":\"\"Organization\"\",\"\"identifier\"\":{\"\"system\"\":\"\"http://dsf.dev/sid/organization-identifier\"\",\"\"value\"\":\"\"organization.com\"\"}},\"\"restriction\"\":{\"\"recipient\"\":[{\"\"type\"\":\"\"Organization\"\",\"\"identifier\"\":{\"\"system\"\":\"\"http://dsf.dev/sid/organization-identifier\"\",\"\"value\"\":\"\"organization.com\"\"}}]},\"\"input\"\":[{\"\"type\"\":{\"\"coding\"\":[{\"\"system\"\":\"\"http://dsf.dev/fhir/CodeSystem/bpmn-message\"\",\"\"code\"\":\"\"message-name\"\"}]},\"\"valueString\"\":\"\"feasibilityExecuteMessage\"\"},{\"\"type\"\":{\"\"coding\"\":[{\"\"system\"\":\"\"http://dsf.dev/fhir/CodeSystem/bpmn-message\"\",\"\"code\"\":\"\"business-key\"\"}]},\"\"valueString\"\":\"\"${business-key}\"\"},{\"\"type\"\":{\"\"coding\"\":[{\"\"system\"\":\"\"http://dsf.dev/fhir/CodeSystem/bpmn-message\"\",\"\"code\"\":\"\"correlation-key\"\"}]},\"\"valueString\"\":\"\"${correlation-key}\"\"},{\"\"type\"\":{\"\"coding\"\":[{\"\"system\"\":\"\"http://medizininformatik-initiative.de/fhir/CodeSystem/feasibility\"\",\"\"code\"\":\"\"measure-reference\"\"}]},\"\"valueReference\"\":{\"\"reference\"\":\"\"https://dsf.fdpg.test.forschen-fuer-gesundheit.de/fhir/Measure/02bb7540-0d99-4c0e-8764-981e545cf646\"\"}}]}\"\n" + .replace("${id}", id).replace("${business-key}", UUID.randomUUID().toString()) + .replace("${correlation-key}", UUID.randomUUID().toString()); + } + } + + @Test + public void testBigUpdate() throws Exception + { + Organization org = new Organization(); + org.setActive(true); + org.getIdentifierFirstRep().setSystem(ReadAccessHelper.ORGANIZATION_IDENTIFIER_SYSTEM) + .setValue("organization.com"); + + Organization createdOrg = dao.create(org); + assertNotNull(createdOrg); + + final int taskCount = 500_000; + + logger.info("Inserting {} Task resources ...", taskCount); + try (Connection connection = defaultDataSource.getConnection()) + { + CopyManager copyManager = new CopyManager(connection.unwrap(BaseConnection.class)); + TaskAsCsvGeneratorReader taskGenerator = new TaskAsCsvGeneratorReader(taskCount); + long insertedRows = copyManager.copyIn("COPY tasks FROM STDIN (FORMAT csv)", taskGenerator, + TaskAsCsvGeneratorReader.TASK_ROW_LINE_LENGTH); + + assertEquals(taskCount, insertedRows); + } + logger.info("Inserting {} Task resources [Done]", taskCount); + + long t0 = System.currentTimeMillis(); + + Organization updatedOrg1 = dao.update(createdOrg); + assertNotNull(updatedOrg1); + + Organization updatedOrg2 = dao.update(createdOrg); + assertNotNull(updatedOrg2); + + long t1 = System.currentTimeMillis(); + + logger.info("Organization updates executed in {} ms", t1 - t0); + assertTrue("Organization updates took longer then 200 ms", t1 - t0 <= 200); } } diff --git a/dsf-fhir/dsf-fhir-server/src/test/java/dev/dsf/fhir/integration/QuestionnaireResponseIntegrationTest.java b/dsf-fhir/dsf-fhir-server/src/test/java/dev/dsf/fhir/integration/QuestionnaireResponseIntegrationTest.java index 84966dddd..d93a490ed 100644 --- a/dsf-fhir/dsf-fhir-server/src/test/java/dev/dsf/fhir/integration/QuestionnaireResponseIntegrationTest.java +++ b/dsf-fhir/dsf-fhir-server/src/test/java/dev/dsf/fhir/integration/QuestionnaireResponseIntegrationTest.java @@ -41,7 +41,7 @@ public void testCreateValidByLocalUser() throws Exception } @Test - public void testCreateNotAllowedByLocalUser() throws Exception + public void testCreateNotAllowedByLocalUserStatusCompleted() throws Exception { QuestionnaireResponse questionnaireResponse = createQuestionnaireResponse(); questionnaireResponse.setStatus(QuestionnaireResponseStatus.COMPLETED); @@ -49,6 +49,14 @@ public void testCreateNotAllowedByLocalUser() throws Exception expectForbidden(() -> getWebserviceClient().create(questionnaireResponse)); } + @Test + public void testCreateNotAllowedByLocalUserQuestionnaireDoesNotExists() throws Exception + { + QuestionnaireResponse questionnaireResponse = createQuestionnaireResponse(); + + expectForbidden(() -> getWebserviceClient().create(questionnaireResponse)); + } + @Test public void testCreateNotAllowedByRemoteUser() throws Exception { diff --git a/dsf-fhir/dsf-fhir-server/src/test/java/dev/dsf/fhir/integration/QuestionnaireVsQuestionnaireResponseIntegrationTest.java b/dsf-fhir/dsf-fhir-server/src/test/java/dev/dsf/fhir/integration/QuestionnaireVsQuestionnaireResponseIntegrationTest.java index bde13972b..b1dc6d7bb 100644 --- a/dsf-fhir/dsf-fhir-server/src/test/java/dev/dsf/fhir/integration/QuestionnaireVsQuestionnaireResponseIntegrationTest.java +++ b/dsf-fhir/dsf-fhir-server/src/test/java/dev/dsf/fhir/integration/QuestionnaireVsQuestionnaireResponseIntegrationTest.java @@ -1,11 +1,15 @@ package dev.dsf.fhir.integration; import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertTrue; import java.util.Optional; +import java.util.UUID; +import org.hl7.fhir.r4.model.Bundle; import org.hl7.fhir.r4.model.Questionnaire; import org.hl7.fhir.r4.model.QuestionnaireResponse; +import org.hl7.fhir.r4.model.ResourceType; import org.hl7.fhir.r4.model.StringType; import org.junit.Test; @@ -146,4 +150,53 @@ public void testQuestionnaireResponseValidatesAgainstQuestionnaireProfileVersion assertNotNull(updatedQr.getIdElement().getIdPart()); assertNotNull(updatedQr.getIdElement().getVersionIdPart()); } + + @Test + public void testPostQuestionnaireAndCorrespondingQuestionnaireResponseInTransactionBundleOrderQuestionnaireBeforeQuestionnaireResponse() + throws Exception + { + Questionnaire questionnaire = createQuestionnaireProfileVersion150("1.5.3"); + QuestionnaireResponse questionnaireResponse = createQuestionnaireResponse("1.5.3"); + + Bundle bundle = new Bundle(); + bundle.setType(Bundle.BundleType.TRANSACTION); + bundle.addEntry().setResource(questionnaire).setFullUrl("urn:uuid:" + UUID.randomUUID().toString()).getRequest() + .setMethod(Bundle.HTTPVerb.POST).setUrl(ResourceType.Questionnaire.name()); + bundle.addEntry().setResource(questionnaireResponse).setFullUrl("urn:uuid:" + UUID.randomUUID().toString()) + .getRequest().setMethod(Bundle.HTTPVerb.POST).setUrl(ResourceType.QuestionnaireResponse.name()); + + assertTrue(getWebserviceClient().postBundle(bundle).getEntry().stream() + .allMatch(entry -> entry.getResponse().getStatus().equals("201 Created"))); + } + + @Test + public void testPostQuestionnaireAndCorrespondingQuestionnaireResponseInTransactionBundleOrderQuestionnaireResponseBeforeQuestionnaire() + throws Exception + { + Questionnaire questionnaire = createQuestionnaireProfileVersion150("1.5.3"); + QuestionnaireResponse questionnaireResponse = createQuestionnaireResponse("1.5.3"); + + Bundle bundle = new Bundle(); + bundle.setType(Bundle.BundleType.TRANSACTION); + bundle.addEntry().setResource(questionnaireResponse).setFullUrl("urn:uuid:" + UUID.randomUUID().toString()) + .getRequest().setMethod(Bundle.HTTPVerb.POST).setUrl(ResourceType.QuestionnaireResponse.name()); + bundle.addEntry().setResource(questionnaire).setFullUrl("urn:uuid:" + UUID.randomUUID().toString()).getRequest() + .setMethod(Bundle.HTTPVerb.POST).setUrl(ResourceType.Questionnaire.name()); + + assertTrue(getWebserviceClient().postBundle(bundle).getEntry().stream() + .allMatch(entry -> entry.getResponse().getStatus().equals("201 Created"))); + } + + @Test + public void testPostQuestionnaireResponseInTransactionBundleQuestionnaireDoesNotExistForbidden() throws Exception + { + QuestionnaireResponse questionnaireResponse = createQuestionnaireResponse("1.5.3"); + + Bundle bundle = new Bundle(); + bundle.setType(Bundle.BundleType.TRANSACTION); + bundle.addEntry().setResource(questionnaireResponse).setFullUrl("urn:uuid:" + UUID.randomUUID().toString()) + .getRequest().setMethod(Bundle.HTTPVerb.POST).setUrl(ResourceType.QuestionnaireResponse.name()); + + expectForbidden(() -> getWebserviceClient().postBundle(bundle)); + } } \ No newline at end of file diff --git a/dsf-fhir/dsf-fhir-server/src/test/resources/log4j2-maven-surefire-config.xml b/dsf-fhir/dsf-fhir-server/src/test/resources/log4j2-maven-surefire-config.xml index d72cf2a72..9c5baf9ed 100755 --- a/dsf-fhir/dsf-fhir-server/src/test/resources/log4j2-maven-surefire-config.xml +++ b/dsf-fhir/dsf-fhir-server/src/test/resources/log4j2-maven-surefire-config.xml @@ -1,4 +1,4 @@ - + diff --git a/dsf-fhir/dsf-fhir-server/src/test/resources/log4j2.xml b/dsf-fhir/dsf-fhir-server/src/test/resources/log4j2.xml index aa5ddf5a5..ec2b86c10 100755 --- a/dsf-fhir/dsf-fhir-server/src/test/resources/log4j2.xml +++ b/dsf-fhir/dsf-fhir-server/src/test/resources/log4j2.xml @@ -1,4 +1,4 @@ - + diff --git a/dsf-fhir/dsf-fhir-validation/src/test/resources/log4j2.xml b/dsf-fhir/dsf-fhir-validation/src/test/resources/log4j2.xml index 116ccf90e..7bafc4cfc 100644 --- a/dsf-fhir/dsf-fhir-validation/src/test/resources/log4j2.xml +++ b/dsf-fhir/dsf-fhir-validation/src/test/resources/log4j2.xml @@ -1,4 +1,4 @@ - + diff --git a/dsf-fhir/dsf-fhir-websocket-client/src/main/java/dev/dsf/fhir/client/ClientEndpoint.java b/dsf-fhir/dsf-fhir-websocket-client/src/main/java/dev/dsf/fhir/client/ClientEndpoint.java index 9283afe33..842548613 100755 --- a/dsf-fhir/dsf-fhir-websocket-client/src/main/java/dev/dsf/fhir/client/ClientEndpoint.java +++ b/dsf-fhir/dsf-fhir-websocket-client/src/main/java/dev/dsf/fhir/client/ClientEndpoint.java @@ -1,5 +1,6 @@ package dev.dsf.fhir.client; +import java.util.EnumSet; import java.util.function.Consumer; import java.util.function.Supplier; @@ -80,7 +81,8 @@ public void onClose(Session session, CloseReason closeReason) logger.warn("Websocket closed, session {}: {} - {}", session.getId(), closeReason.getCloseCode().getCode(), closeReason.getReasonPhrase()); - if (CloseReason.CloseCodes.CANNOT_ACCEPT.equals(closeReason.getCloseCode())) + if (EnumSet.of(CloseReason.CloseCodes.CANNOT_ACCEPT, CloseReason.CloseCodes.CLOSED_ABNORMALLY) + .contains(closeReason.getCloseCode())) { logger.info("Trying to reconnect websocket"); reconnector.run(); diff --git a/dsf-fhir/dsf-fhir-websocket-client/src/main/java/dev/dsf/fhir/client/WebsocketClientTyrus.java b/dsf-fhir/dsf-fhir-websocket-client/src/main/java/dev/dsf/fhir/client/WebsocketClientTyrus.java index 04165f981..40fe3f13f 100755 --- a/dsf-fhir/dsf-fhir-websocket-client/src/main/java/dev/dsf/fhir/client/WebsocketClientTyrus.java +++ b/dsf-fhir/dsf-fhir-websocket-client/src/main/java/dev/dsf/fhir/client/WebsocketClientTyrus.java @@ -90,8 +90,8 @@ public boolean onDisconnect(CloseReason closeReason) private final String userAgentValue; private final ClientEndpoint endpoint; - private ClientManager manager; - private Session connection; + private volatile ClientManager manager; + private volatile Session connection; private volatile boolean closed; public WebsocketClientTyrus(Runnable reconnector, URI wsUri, KeyStore trustStore, KeyStore keyStore, @@ -131,6 +131,8 @@ public void connect() if (manager != null) throw new IllegalStateException("Allready connecting/connected"); + closed = false; + manager = ClientManager.createClient(); manager.getProperties().put(ClientProperties.RECONNECT_HANDLER, reconnectHandler); manager.getProperties().put(ClientProperties.SSL_ENGINE_CONFIGURATOR, new SslEngineConfigurator(sslContext)); diff --git a/dsf-tools/dsf-tools-bundle-generator/src/main/resources/log4j2.xml b/dsf-tools/dsf-tools-bundle-generator/src/main/resources/log4j2.xml index 896f07998..e8fb24d29 100755 --- a/dsf-tools/dsf-tools-bundle-generator/src/main/resources/log4j2.xml +++ b/dsf-tools/dsf-tools-bundle-generator/src/main/resources/log4j2.xml @@ -1,4 +1,4 @@ - + diff --git a/dsf-tools/dsf-tools-documentation-generator/src/main/resources/log4j2.xml b/dsf-tools/dsf-tools-documentation-generator/src/main/resources/log4j2.xml index 896f07998..e8fb24d29 100755 --- a/dsf-tools/dsf-tools-documentation-generator/src/main/resources/log4j2.xml +++ b/dsf-tools/dsf-tools-documentation-generator/src/main/resources/log4j2.xml @@ -1,4 +1,4 @@ - + diff --git a/dsf-tools/dsf-tools-proxy-test/src/main/resources/log4j2.xml b/dsf-tools/dsf-tools-proxy-test/src/main/resources/log4j2.xml index 630850f14..bcf6fe9a0 100755 --- a/dsf-tools/dsf-tools-proxy-test/src/main/resources/log4j2.xml +++ b/dsf-tools/dsf-tools-proxy-test/src/main/resources/log4j2.xml @@ -1,4 +1,4 @@ - diff --git a/dsf-tools/dsf-tools-test-data-generator/src/main/resources/log4j2.xml b/dsf-tools/dsf-tools-test-data-generator/src/main/resources/log4j2.xml index 6e8a1fa98..e3832195b 100755 --- a/dsf-tools/dsf-tools-test-data-generator/src/main/resources/log4j2.xml +++ b/dsf-tools/dsf-tools-test-data-generator/src/main/resources/log4j2.xml @@ -1,4 +1,4 @@ - + diff --git a/pom.xml b/pom.xml index 44682aa65..f2c6b817b 100755 --- a/pom.xml +++ b/pom.xml @@ -21,17 +21,17 @@ ${project.basedir} - 2.0.13 - 2.23.1 - 12.0.10 - 3.1.7 - 2.1.5 - 6.1.10 - 2.17.2 - 7.21.0 + 2.0.16 + 2.24.1 + 12.0.14 + 3.1.9 + 2.2.0 + 6.1.13 + 2.18.0 + 7.22.0 5.1.0 - 7.4.0 - 7.4.0 + 7.4.3 + 7.4.3 1.78.1 @@ -136,7 +136,7 @@ org.mockito mockito-core - 5.12.0 + 5.14.1 org.bouncycastle @@ -158,12 +158,12 @@ org.liquibase liquibase-core - 4.28.0 + 4.29.2 org.postgresql postgresql - 42.7.3 + 42.7.4 @@ -393,12 +393,12 @@ commons-io commons-io - 2.16.1 + 2.17.0 commons-codec commons-codec - 1.17.0 + 1.17.1 @@ -420,7 +420,7 @@ org.yaml snakeyaml - 2.2 + 2.3 @@ -433,17 +433,17 @@ org.apache.maven maven-core - 3.9.8 + 3.9.9 org.apache.maven maven-plugin-api - 3.9.8 + 3.9.9 org.apache.maven.plugin-tools maven-plugin-annotations - 3.13.1 + 3.15.0 @@ -481,17 +481,17 @@ org.apache.maven.plugins maven-javadoc-plugin - 3.7.0 + 3.10.1 org.apache.maven.plugins maven-surefire-plugin - 3.3.0 + 3.5.1 org.apache.maven.plugins maven-failsafe-plugin - 3.3.0 + 3.5.1 org.apache.maven.plugins @@ -511,7 +511,7 @@ org.apache.maven.plugins maven-gpg-plugin - 3.2.4 + 3.2.7 org.codehaus.mojo @@ -532,22 +532,22 @@ net.revelc.code impsort-maven-plugin - 1.11.0 + 1.12.0 org.apache.maven.plugins maven-dependency-plugin - 3.7.1 + 3.8.0 org.codehaus.mojo buildnumber-maven-plugin - 3.2.0 + 3.2.1 org.apache.maven.plugins maven-plugin-plugin - 3.13.1 + 3.15.0 dev.dsf @@ -562,22 +562,22 @@ org.apache.maven.plugins maven-site-plugin - 3.12.1 + 3.20.0 com.github.spotbugs spotbugs-maven-plugin - 4.8.6.2 + 4.8.6.4 org.apache.maven.plugins maven-project-info-reports-plugin - 3.6.1 + 3.7.0 org.apache.maven.plugins maven-pmd-plugin - 3.23.0 + 3.25.0