From b89c512dcccd661a39cf578053bce32791eac759 Mon Sep 17 00:00:00 2001 From: Hauke Hund Date: Mon, 23 Mar 2026 15:18:24 +0100 Subject: [PATCH 01/18] improved xml transformer config --- .../dev/dsf/bpe/webservice/ProcessService.java | 16 ++++++++++++++++ .../adapter/ThymeleafTemplateServiceImpl.java | 18 +++++++++++++++++- .../dev/dsf/maven/bundle/BundleGenerator.java | 16 ++++++++++++++++ 3 files changed, 49 insertions(+), 1 deletion(-) diff --git a/dsf-bpe/dsf-bpe-server/src/main/java/dev/dsf/bpe/webservice/ProcessService.java b/dsf-bpe/dsf-bpe-server/src/main/java/dev/dsf/bpe/webservice/ProcessService.java index 210ef364f..940826777 100755 --- a/dsf-bpe/dsf-bpe-server/src/main/java/dev/dsf/bpe/webservice/ProcessService.java +++ b/dsf-bpe/dsf-bpe-server/src/main/java/dev/dsf/bpe/webservice/ProcessService.java @@ -23,7 +23,9 @@ import java.util.Objects; import java.util.function.Consumer; +import javax.xml.XMLConstants; import javax.xml.transform.Transformer; +import javax.xml.transform.TransformerConfigurationException; import javax.xml.transform.TransformerException; import javax.xml.transform.TransformerFactory; import javax.xml.transform.dom.DOMSource; @@ -46,6 +48,7 @@ import jakarta.ws.rs.core.Response; import jakarta.ws.rs.core.Response.Status; import jakarta.ws.rs.core.StreamingOutput; +import net.sf.saxon.lib.FeatureKeys; @RolesAllowed("ADMIN") @Path(ProcessService.PATH) @@ -61,7 +64,20 @@ public ProcessService(ThymeleafTemplateService templateService, RepositoryServic super(templateService, "Process"); this.repositoryService = repositoryService; + transformerFactory = TransformerFactory.newInstance(); + transformerFactory.setAttribute(XMLConstants.ACCESS_EXTERNAL_DTD, ""); + transformerFactory.setAttribute(XMLConstants.ACCESS_EXTERNAL_STYLESHEET, ""); + + try + { + transformerFactory.setFeature(XMLConstants.FEATURE_SECURE_PROCESSING, true); + transformerFactory.setFeature(FeatureKeys.ALLOW_EXTERNAL_FUNCTIONS, false); + } + catch (TransformerConfigurationException e) + { + throw new RuntimeException(e); + } } @Override diff --git a/dsf-fhir/dsf-fhir-server/src/main/java/dev/dsf/fhir/adapter/ThymeleafTemplateServiceImpl.java b/dsf-fhir/dsf-fhir-server/src/main/java/dev/dsf/fhir/adapter/ThymeleafTemplateServiceImpl.java index e13f36f22..91d26ffa6 100644 --- a/dsf-fhir/dsf-fhir-server/src/main/java/dev/dsf/fhir/adapter/ThymeleafTemplateServiceImpl.java +++ b/dsf-fhir/dsf-fhir-server/src/main/java/dev/dsf/fhir/adapter/ThymeleafTemplateServiceImpl.java @@ -34,6 +34,7 @@ import java.util.regex.Pattern; import java.util.stream.Collectors; +import javax.xml.XMLConstants; import javax.xml.transform.OutputKeys; import javax.xml.transform.Transformer; import javax.xml.transform.TransformerConfigurationException; @@ -63,6 +64,7 @@ import jakarta.ws.rs.core.PathSegment; import jakarta.ws.rs.core.SecurityContext; import jakarta.ws.rs.core.UriInfo; +import net.sf.saxon.lib.FeatureKeys; public class ThymeleafTemplateServiceImpl implements ThymeleafTemplateService, InitializingBean { @@ -106,7 +108,7 @@ public class ThymeleafTemplateServiceImpl implements ThymeleafTemplateService, I private final Map, List> contextsByResourceType; - private final TransformerFactory transformerFactory = TransformerFactory.newInstance(); + private final TransformerFactory transformerFactory; private final TemplateEngine templateEngine = new TemplateEngine(); /** @@ -139,6 +141,20 @@ public ThymeleafTemplateServiceImpl(String serverBaseUrl, Theme theme, FhirConte resolver.setCacheable(cacheEnabled); templateEngine.setTemplateResolver(resolver); + + transformerFactory = TransformerFactory.newInstance(); + transformerFactory.setAttribute(XMLConstants.ACCESS_EXTERNAL_DTD, ""); + transformerFactory.setAttribute(XMLConstants.ACCESS_EXTERNAL_STYLESHEET, ""); + + try + { + transformerFactory.setFeature(XMLConstants.FEATURE_SECURE_PROCESSING, true); + transformerFactory.setFeature(FeatureKeys.ALLOW_EXTERNAL_FUNCTIONS, false); + } + catch (TransformerConfigurationException e) + { + throw new RuntimeException(e); + } } @Override diff --git a/dsf-maven/dsf-maven-plugin/src/main/java/dev/dsf/maven/bundle/BundleGenerator.java b/dsf-maven/dsf-maven-plugin/src/main/java/dev/dsf/maven/bundle/BundleGenerator.java index 577b3a1ad..b3d9f937a 100755 --- a/dsf-maven/dsf-maven-plugin/src/main/java/dev/dsf/maven/bundle/BundleGenerator.java +++ b/dsf-maven/dsf-maven-plugin/src/main/java/dev/dsf/maven/bundle/BundleGenerator.java @@ -36,8 +36,10 @@ import java.util.stream.Collectors; import java.util.stream.Stream; +import javax.xml.XMLConstants; import javax.xml.transform.OutputKeys; import javax.xml.transform.Transformer; +import javax.xml.transform.TransformerConfigurationException; import javax.xml.transform.TransformerException; import javax.xml.transform.TransformerFactory; import javax.xml.transform.stream.StreamResult; @@ -70,6 +72,7 @@ import ca.uhn.fhir.context.support.IValidationSupport; import ca.uhn.fhir.parser.IParser; import ca.uhn.fhir.parser.LenientErrorHandler; +import net.sf.saxon.lib.FeatureKeys; public class BundleGenerator { @@ -380,6 +383,19 @@ private void saveBundle(Bundle bundle, Path bundleFilename) throws IOException, { // minimized output: empty-element tags, no indentation, no line-breaks TransformerFactory transformerFactory = TransformerFactory.newInstance(); + transformerFactory.setAttribute(XMLConstants.ACCESS_EXTERNAL_DTD, ""); + transformerFactory.setAttribute(XMLConstants.ACCESS_EXTERNAL_STYLESHEET, ""); + + try + { + transformerFactory.setFeature(XMLConstants.FEATURE_SECURE_PROCESSING, true); + transformerFactory.setFeature(FeatureKeys.ALLOW_EXTERNAL_FUNCTIONS, false); + } + catch (TransformerConfigurationException e) + { + throw new RuntimeException(e); + } + Transformer transformer = transformerFactory.newTransformer(); transformer.setOutputProperty(OutputKeys.METHOD, "xml"); transformer.setOutputProperty(OutputKeys.OMIT_XML_DECLARATION, "yes"); From 7e2de0ed8d425256b17542ba81f315a5a5693d79 Mon Sep 17 00:00:00 2001 From: Hauke Hund Date: Mon, 23 Mar 2026 15:39:42 +0100 Subject: [PATCH 02/18] improved validation of db username/group config parameters --- .../dsf/bpe/config/BpeDbMigratorConfig.java | 23 ++++++++++++++++++- .../bpe/spring/config/PropertiesConfig.java | 8 +++++++ .../common/db/migration/DbMigratorConfig.java | 4 ++++ .../dsf/fhir/config/FhirDbMigratorConfig.java | 23 ++++++++++++++++++- .../fhir/spring/config/PropertiesConfig.java | 11 +++++++++ 5 files changed, 67 insertions(+), 2 deletions(-) diff --git a/dsf-bpe/dsf-bpe-server-jetty/src/main/java/dev/dsf/bpe/config/BpeDbMigratorConfig.java b/dsf-bpe/dsf-bpe-server-jetty/src/main/java/dev/dsf/bpe/config/BpeDbMigratorConfig.java index bb64598c2..f59b523a7 100644 --- a/dsf-bpe/dsf-bpe-server-jetty/src/main/java/dev/dsf/bpe/config/BpeDbMigratorConfig.java +++ b/dsf-bpe/dsf-bpe-server-jetty/src/main/java/dev/dsf/bpe/config/BpeDbMigratorConfig.java @@ -17,6 +17,7 @@ import java.util.Map; +import org.springframework.beans.factory.InitializingBean; import org.springframework.beans.factory.annotation.Value; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; @@ -32,7 +33,7 @@ @Configuration @PropertySource(value = "file:conf/config.properties", encoding = "UTF-8", ignoreResourceNotFound = true) -public class BpeDbMigratorConfig implements DbMigratorConfig +public class BpeDbMigratorConfig implements DbMigratorConfig, InitializingBean { private static final String DB_LIQUIBASE_USER = "db.liquibase_user"; private static final String DB_SERVER_USERS_GROUP = "db.server_users_group"; @@ -97,6 +98,26 @@ public static PropertySourcesPlaceholderConfigurer propertySourcesPlaceholderCon return new PropertySourcesPlaceholderConfigurer(); } + @Override + public void afterPropertiesSet() throws Exception + { + if (!POSTGRES_UNQUOTED_IDENTIFIER.matcher(dbLiquibaseUsername).matches()) + throw new RuntimeException("Property 'dev.dsf.bpe.db.liquibase.username' value not matching " + + POSTGRES_UNQUOTED_IDENTIFIER_STRING); + if (!POSTGRES_UNQUOTED_IDENTIFIER.matcher(dbUsersGroup).matches()) + throw new RuntimeException( + "Property 'dev.dsf.bpe.db.user.group' value not matching " + POSTGRES_UNQUOTED_IDENTIFIER_STRING); + if (!POSTGRES_UNQUOTED_IDENTIFIER.matcher(dbUsername).matches()) + throw new RuntimeException("Property 'dev.dsf.bpe.db.user.username' value not matching " + + POSTGRES_UNQUOTED_IDENTIFIER_STRING); + if (!POSTGRES_UNQUOTED_IDENTIFIER.matcher(dbEngineUsersGroup).matches()) + throw new RuntimeException("Property 'dev.dsf.bpe.db.user.engine.group' value not matching " + + POSTGRES_UNQUOTED_IDENTIFIER_STRING); + if (!POSTGRES_UNQUOTED_IDENTIFIER.matcher(dbEngineUsername).matches()) + throw new RuntimeException("Property 'dev.dsf.bpe.db.user.engine.username' value not matching " + + POSTGRES_UNQUOTED_IDENTIFIER_STRING); + } + @Override public String getDbUrl() { diff --git a/dsf-bpe/dsf-bpe-server/src/main/java/dev/dsf/bpe/spring/config/PropertiesConfig.java b/dsf-bpe/dsf-bpe-server/src/main/java/dev/dsf/bpe/spring/config/PropertiesConfig.java index b0cf8d986..9965e151f 100644 --- a/dsf-bpe/dsf-bpe-server/src/main/java/dev/dsf/bpe/spring/config/PropertiesConfig.java +++ b/dsf-bpe/dsf-bpe-server/src/main/java/dev/dsf/bpe/spring/config/PropertiesConfig.java @@ -51,6 +51,7 @@ import dev.dsf.common.config.AbstractCertificateConfig; import dev.dsf.common.config.ProxyConfig; import dev.dsf.common.config.ProxyConfigImpl; +import dev.dsf.common.db.migration.DbMigratorConfig; import dev.dsf.common.docker.secrets.DockerSecretsPropertySourceFactory; import dev.dsf.common.documentation.Documentation; import dev.dsf.common.ui.theme.Theme; @@ -451,6 +452,13 @@ else if (oldPropertyValue != null && newPropertyValue == null) @Override public void afterPropertiesSet() throws Exception { + if (!DbMigratorConfig.POSTGRES_UNQUOTED_IDENTIFIER.matcher(dbUsername).matches()) + throw new RuntimeException("Property 'dev.dsf.bpe.db.user.username' value not matching " + + DbMigratorConfig.POSTGRES_UNQUOTED_IDENTIFIER_STRING); + if (!DbMigratorConfig.POSTGRES_UNQUOTED_IDENTIFIER.matcher(dbEngineUsername).matches()) + throw new RuntimeException("Property 'dev.dsf.bpe.db.user.engine.username' value not matching " + + DbMigratorConfig.POSTGRES_UNQUOTED_IDENTIFIER_STRING); + URL url = new URI(dsfServerBaseUrl).toURL(); if (!List.of("http", "https").contains(url.getProtocol())) { diff --git a/dsf-common/dsf-common-db/src/main/java/dev/dsf/common/db/migration/DbMigratorConfig.java b/dsf-common/dsf-common-db/src/main/java/dev/dsf/common/db/migration/DbMigratorConfig.java index 497ba8bf3..31d02c851 100644 --- a/dsf-common/dsf-common-db/src/main/java/dev/dsf/common/db/migration/DbMigratorConfig.java +++ b/dsf-common/dsf-common-db/src/main/java/dev/dsf/common/db/migration/DbMigratorConfig.java @@ -16,9 +16,13 @@ package dev.dsf.common.db.migration; import java.util.Map; +import java.util.regex.Pattern; public interface DbMigratorConfig { + String POSTGRES_UNQUOTED_IDENTIFIER_STRING = "^[a-zA-Z_][a-zA-Z0-9_$]{0,62}$"; + Pattern POSTGRES_UNQUOTED_IDENTIFIER = Pattern.compile(POSTGRES_UNQUOTED_IDENTIFIER_STRING); + String getDbUrl(); String getDbLiquibaseUsername(); diff --git a/dsf-fhir/dsf-fhir-server-jetty/src/main/java/dev/dsf/fhir/config/FhirDbMigratorConfig.java b/dsf-fhir/dsf-fhir-server-jetty/src/main/java/dev/dsf/fhir/config/FhirDbMigratorConfig.java index c3b34a89e..9873d6518 100644 --- a/dsf-fhir/dsf-fhir-server-jetty/src/main/java/dev/dsf/fhir/config/FhirDbMigratorConfig.java +++ b/dsf-fhir/dsf-fhir-server-jetty/src/main/java/dev/dsf/fhir/config/FhirDbMigratorConfig.java @@ -17,6 +17,7 @@ import java.util.Map; +import org.springframework.beans.factory.InitializingBean; import org.springframework.beans.factory.annotation.Value; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; @@ -31,7 +32,7 @@ @Configuration @PropertySource(value = "file:conf/config.properties", encoding = "UTF-8", ignoreResourceNotFound = true) -public class FhirDbMigratorConfig implements DbMigratorConfig +public class FhirDbMigratorConfig implements DbMigratorConfig, InitializingBean { private static final String DB_LIQUIBASE_USER = "db.liquibase_user"; private static final String DB_SERVER_USERS_GROUP = "db.server_users_group"; @@ -93,6 +94,26 @@ public static PropertySourcesPlaceholderConfigurer propertySourcesPlaceholderCon return new PropertySourcesPlaceholderConfigurer(); } + @Override + public void afterPropertiesSet() throws Exception + { + if (!POSTGRES_UNQUOTED_IDENTIFIER.matcher(dbLiquibaseUsername).matches()) + throw new RuntimeException("Property 'dev.dsf.fhir.db.liquibase.username' value not matching " + + POSTGRES_UNQUOTED_IDENTIFIER_STRING); + if (!POSTGRES_UNQUOTED_IDENTIFIER.matcher(dbUsersGroup).matches()) + throw new RuntimeException( + "Property 'dev.dsf.fhir.db.user.group' value not matching " + POSTGRES_UNQUOTED_IDENTIFIER_STRING); + if (!POSTGRES_UNQUOTED_IDENTIFIER.matcher(dbUsername).matches()) + throw new RuntimeException("Property 'dev.dsf.fhir.db.user.username' value not matching " + + POSTGRES_UNQUOTED_IDENTIFIER_STRING); + if (!POSTGRES_UNQUOTED_IDENTIFIER.matcher(dbPermanentDeleteUsersGroup).matches()) + throw new RuntimeException("Property 'dev.dsf.fhir.db.user.permanent.delete.group' value not matching " + + POSTGRES_UNQUOTED_IDENTIFIER_STRING); + if (!POSTGRES_UNQUOTED_IDENTIFIER.matcher(dbPermanentDeleteUsername).matches()) + throw new RuntimeException("Property 'dev.dsf.fhir.db.user.permanent.delete.username' value not matching " + + POSTGRES_UNQUOTED_IDENTIFIER_STRING); + } + @Override public String getDbUrl() { diff --git a/dsf-fhir/dsf-fhir-server/src/main/java/dev/dsf/fhir/spring/config/PropertiesConfig.java b/dsf-fhir/dsf-fhir-server/src/main/java/dev/dsf/fhir/spring/config/PropertiesConfig.java index 80ea4f731..87573ad30 100644 --- a/dsf-fhir/dsf-fhir-server/src/main/java/dev/dsf/fhir/spring/config/PropertiesConfig.java +++ b/dsf-fhir/dsf-fhir-server/src/main/java/dev/dsf/fhir/spring/config/PropertiesConfig.java @@ -49,6 +49,7 @@ import dev.dsf.common.config.AbstractCertificateConfig; import dev.dsf.common.config.ProxyConfig; import dev.dsf.common.config.ProxyConfigImpl; +import dev.dsf.common.db.migration.DbMigratorConfig; import dev.dsf.common.docker.secrets.DockerSecretsPropertySourceFactory; import dev.dsf.common.documentation.Documentation; import dev.dsf.common.ui.theme.Theme; @@ -245,6 +246,16 @@ private static void computeOrganizationThumbprintPropertyIfPossible(Configurable @Override public void afterPropertiesSet() throws Exception { + if (!DbMigratorConfig.POSTGRES_UNQUOTED_IDENTIFIER.matcher(dbUsersGroup).matches()) + throw new RuntimeException("Property 'dev.dsf.fhir.db.user.group' value not matching " + + DbMigratorConfig.POSTGRES_UNQUOTED_IDENTIFIER_STRING); + if (!DbMigratorConfig.POSTGRES_UNQUOTED_IDENTIFIER.matcher(dbUsername).matches()) + throw new RuntimeException("Property 'dev.dsf.fhir.db.user.username' value not matching " + + DbMigratorConfig.POSTGRES_UNQUOTED_IDENTIFIER_STRING); + if (!DbMigratorConfig.POSTGRES_UNQUOTED_IDENTIFIER.matcher(dbPermanentDeleteUsername).matches()) + throw new RuntimeException("Property 'dev.dsf.fhir.db.user.permanent.delete.username' value not matching " + + DbMigratorConfig.POSTGRES_UNQUOTED_IDENTIFIER_STRING); + URL url = new URI(serverBaseUrl).toURL(); if (!List.of("http", "https").contains(url.getProtocol())) { From bdeddf30814815104db1de296dc91437b9ce0167 Mon Sep 17 00:00:00 2001 From: Hauke Hund Date: Mon, 23 Mar 2026 16:05:21 +0100 Subject: [PATCH 03/18] enforced https for jwks and token endpoints from oidc config --- .../java/dev/dsf/bpe/client/oidc/OidcClientJersey.java | 7 ++++++- .../java/dev/dsf/common/oidc/BaseOidcClientJersey.java | 7 ++++++- 2 files changed, 12 insertions(+), 2 deletions(-) diff --git a/dsf-bpe/dsf-bpe-server/src/main/java/dev/dsf/bpe/client/oidc/OidcClientJersey.java b/dsf-bpe/dsf-bpe-server/src/main/java/dev/dsf/bpe/client/oidc/OidcClientJersey.java index fe4aa9924..ad77c4033 100644 --- a/dsf-bpe/dsf-bpe-server/src/main/java/dev/dsf/bpe/client/oidc/OidcClientJersey.java +++ b/dsf-bpe/dsf-bpe-server/src/main/java/dev/dsf/bpe/client/oidc/OidcClientJersey.java @@ -144,7 +144,12 @@ public DecodedJWT getAccessTokenDecoded(OidcConfiguration configuration, Jwks jw "OIDC provider does not support Client Credentials Grant, supported grant types: " + configuration.grantTypesSupported()); - Response response = client.target(configuration.tokenEndpoint()).request(MediaType.APPLICATION_JSON_TYPE) + String tokenEndpoint = configuration.tokenEndpoint(); + if (tokenEndpoint == null || !tokenEndpoint.startsWith("https://")) + throw new OidcClientException( + "Token endpoint URL from OIDC configuration resource is null or does not start with 'https://'"); + + Response response = client.target(tokenEndpoint).request(MediaType.APPLICATION_JSON_TYPE) .header(HttpHeaders.AUTHORIZATION, "Basic " + Base64.getEncoder() .encodeToString(new StringBuilder().append(clientId).append(':').append(clientSecret) diff --git a/dsf-common/dsf-common-oidc/src/main/java/dev/dsf/common/oidc/BaseOidcClientJersey.java b/dsf-common/dsf-common-oidc/src/main/java/dev/dsf/common/oidc/BaseOidcClientJersey.java index bda15957b..81799bc30 100644 --- a/dsf-common/dsf-common-oidc/src/main/java/dev/dsf/common/oidc/BaseOidcClientJersey.java +++ b/dsf-common/dsf-common-oidc/src/main/java/dev/dsf/common/oidc/BaseOidcClientJersey.java @@ -167,7 +167,12 @@ public Jwks getJwks(OidcConfiguration configuration) throws OidcClientException { Objects.requireNonNull(configuration, "configuration"); - Response response = client.target(configuration.jwksUri()).request(MediaType.APPLICATION_JSON_TYPE).get(); + String jwksUri = configuration.jwksUri(); + if (jwksUri == null || !jwksUri.startsWith("https://")) + throw new OidcClientException( + "JWKS URL from OIDC configuration resource is null or does not start with 'https://'"); + + Response response = client.target(jwksUri).request(MediaType.APPLICATION_JSON_TYPE).get(); if (response.getStatus() == Status.OK.getStatusCode()) { From 31c2e974dfd4351756104ee8c53dbcd666192fef Mon Sep 17 00:00:00 2001 From: Hauke Hund Date: Mon, 23 Mar 2026 20:15:03 +0100 Subject: [PATCH 04/18] refactored code, improved oidc / jwks handling (use='sig', min RSA) --- .../dsf/bpe/client/oidc/OidcClientJersey.java | 29 ++------ .../bpe/client/oidc/OidcClientWithCache.java | 20 +++--- .../common/auth/DsfOpenIdLoginService.java | 13 ---- .../common/config/AbstractJettyConfig.java | 40 +++++++++-- .../common/oidc/BaseOidcClientWithCache.java | 46 +++++++----- .../main/java/dev/dsf/common/oidc/Jwks.java | 71 ++++++++++++++----- .../dev/dsf/common/oidc/JwtVerifierImpl.java | 40 +++++++---- .../java/dev/dsf/common/oidc/JwksTest.java | 41 +++++++++-- 8 files changed, 198 insertions(+), 102 deletions(-) diff --git a/dsf-bpe/dsf-bpe-server/src/main/java/dev/dsf/bpe/client/oidc/OidcClientJersey.java b/dsf-bpe/dsf-bpe-server/src/main/java/dev/dsf/bpe/client/oidc/OidcClientJersey.java index ad77c4033..513d8597e 100644 --- a/dsf-bpe/dsf-bpe-server/src/main/java/dev/dsf/bpe/client/oidc/OidcClientJersey.java +++ b/dsf-bpe/dsf-bpe-server/src/main/java/dev/dsf/bpe/client/oidc/OidcClientJersey.java @@ -169,22 +169,6 @@ public DecodedJWT getAccessTokenDecoded(OidcConfiguration configuration, Jwks jw } } - /** - * Does not verify if the access token is expired. Supported algorithms: RS256, RS384, RS512, ES256, ES384 and - * ES512. - * - * @param accessToken - * not null - * @param jwks - * not null - * @return decoded access token - * @throws OidcClientException - * if verification fails, the public key to verify is unknown or a unsupported signature algorithm was - * used. - * - * @see DecodedJWT#getExpiresAt() - * @see DecodedJWT#getExpiresAtAsInstant() - */ private DecodedJWT verifyAndDecodeAccessToken(String accessToken, Jwks jwks) throws OidcClientException { try @@ -196,14 +180,15 @@ private DecodedJWT verifyAndDecodeAccessToken(String accessToken, Jwks jwks) thr throw new OidcClientException("Access token has no kid property"); Optional key = jwks.getKey(keyId); - if (key.isEmpty()) - throw new OidcClientException("Access token key with kid '" + keyId + "' not in JWKS"); + if (key.isEmpty() || !key.get().use().equals("sig")) + throw new OidcClientException("Access token key with kid '" + keyId + "' and use 'sig' not in JWKS"); - Optional algorithm = key.map(JwksKey::toAlgorithm); + Optional algorithm = key.flatMap(JwksKey::toAlgorithm); if (key.isEmpty()) + { throw new OidcClientException("Access token key with kid '" + keyId - + "' has unsupported type (kty) / algorithm (alg) in JWKS '" + key.get().kty() + "' / '" - + key.get().alg() + "'"); + + "' has unsupported type (kty) / algorithm (alg) / key-size in JWKS"); + } try { @@ -231,7 +216,7 @@ else if (requiredAudiences.size() > 1) } catch (TokenExpiredException e) { - throw new OidcClientException("JWT verification failed: claim missing", e); + throw new OidcClientException("JWT verification failed: token expired", e); } catch (IncorrectClaimException e) { diff --git a/dsf-bpe/dsf-bpe-server/src/main/java/dev/dsf/bpe/client/oidc/OidcClientWithCache.java b/dsf-bpe/dsf-bpe-server/src/main/java/dev/dsf/bpe/client/oidc/OidcClientWithCache.java index 8dac3292a..d0c3dbd82 100644 --- a/dsf-bpe/dsf-bpe-server/src/main/java/dev/dsf/bpe/client/oidc/OidcClientWithCache.java +++ b/dsf-bpe/dsf-bpe-server/src/main/java/dev/dsf/bpe/client/oidc/OidcClientWithCache.java @@ -32,7 +32,7 @@ private static final record CacheEntry(ZonedDateTime timeout, T resource) { } - private final Duration cacheTimeoutconfigurationResource; + private final Duration cacheTimeoutConfigurationResource; private final Duration cacheTimeoutJwksResource; private final Duration cacheTimeoutAccessTokenBeforeExpiration; private final OidcClientWithDecodedJwt delegate; @@ -42,7 +42,7 @@ private static final record CacheEntry(ZonedDateTime timeout, T resource) private CacheEntry accessTokenCache; /** - * @param cacheTimeoutconfigurationResource + * @param cacheTimeoutConfigurationResource * not null, not negative * @param cacheTimeoutJwksResource * not null, not negative @@ -51,13 +51,13 @@ private static final record CacheEntry(ZonedDateTime timeout, T resource) * @param delegate * not null */ - public OidcClientWithCache(Duration cacheTimeoutconfigurationResource, Duration cacheTimeoutJwksResource, + public OidcClientWithCache(Duration cacheTimeoutConfigurationResource, Duration cacheTimeoutJwksResource, Duration cacheTimeoutAccessTokenBeforeExpiration, OidcClientWithDecodedJwt delegate) { - this.cacheTimeoutconfigurationResource = Objects.requireNonNull(cacheTimeoutconfigurationResource, - "cacheTimeoutconfigurationResource"); - if (cacheTimeoutconfigurationResource.isNegative()) - throw new IllegalArgumentException("cacheTimeoutconfigurationResource negative"); + this.cacheTimeoutConfigurationResource = Objects.requireNonNull(cacheTimeoutConfigurationResource, + "cacheTimeoutConfigurationResource"); + if (cacheTimeoutConfigurationResource.isNegative()) + throw new IllegalArgumentException("cacheTimeoutConfigurationResource negative"); this.cacheTimeoutJwksResource = Objects.requireNonNull(cacheTimeoutJwksResource, "cacheTimeoutJwksResource"); if (cacheTimeoutJwksResource.isNegative()) @@ -73,13 +73,13 @@ public OidcClientWithCache(Duration cacheTimeoutconfigurationResource, Duration @Override public Configuration getConfiguration() throws OidcClientException { - if (configurationCache != null && configurationCache.timeout.isBefore(ZonedDateTime.now())) + if (configurationCache != null && configurationCache.timeout.isAfter(ZonedDateTime.now())) return configurationCache.resource; else { Configuration configuration = delegate.getConfiguration(); configurationCache = new CacheEntry( - ZonedDateTime.now().plus(cacheTimeoutconfigurationResource), configuration); + ZonedDateTime.now().plus(cacheTimeoutConfigurationResource), configuration); return configuration; } } @@ -89,7 +89,7 @@ public Jwks getJwks() throws OidcClientException { Configuration configuration = getConfiguration(); - if (jwksCache != null && jwksCache.timeout.isBefore(ZonedDateTime.now())) + if (jwksCache != null && jwksCache.timeout.isAfter(ZonedDateTime.now())) return jwksCache.resource; else { diff --git a/dsf-common/dsf-common-jetty/src/main/java/dev/dsf/common/auth/DsfOpenIdLoginService.java b/dsf-common/dsf-common-jetty/src/main/java/dev/dsf/common/auth/DsfOpenIdLoginService.java index 032c595cd..b7ec15986 100644 --- a/dsf-common/dsf-common-jetty/src/main/java/dev/dsf/common/auth/DsfOpenIdLoginService.java +++ b/dsf-common/dsf-common-jetty/src/main/java/dev/dsf/common/auth/DsfOpenIdLoginService.java @@ -33,14 +33,12 @@ public class DsfOpenIdLoginService extends OpenIdLoginService { private static final Logger logger = LoggerFactory.getLogger(DsfOpenIdLoginService.class); - private final OpenIdConfiguration configuration; private final LoginService loginService; public DsfOpenIdLoginService(OpenIdConfiguration configuration, LoginService loginService) { super(configuration, loginService); - this.configuration = configuration; this.loginService = loginService; } @@ -49,17 +47,6 @@ public UserIdentity login(String identifier, Object credentials, Request request Function getOrCreateSession) { OpenIdCredentials openIdCredentials = (OpenIdCredentials) credentials; - try - { - openIdCredentials.redeemAuthCode(configuration); - } - catch (Exception e) - { - logger.debug("Unable to redeem auth code", e); - logger.warn("Unable to redeem auth code: {} - {}", e.getClass().getName(), e.getMessage()); - - return null; - } return loginService.login(openIdCredentials.getUserId(), credentials, request, getOrCreateSession); } 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 425e1de6c..a3dbdce8b 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 @@ -162,6 +162,14 @@ public abstract class AbstractJettyConfig extends AbstractCertificateConfig @Value("${dev.dsf.server.auth.oidc.provider.discovery.path:/.well-known/openid-configuration}") private String oidcProviderDiscoveryPath; + @Documentation(description = "OIDC provider client cache timeout of the 'openid-configuration' discovery resource") + @Value("${dev.dsf.server.auth.oidc.provider.client.cache.timeout.configuration.resource:PT1H}") + private String oidcProviderClientCacheConfigurationResourceTimeout; + + @Documentation(description = "OIDC provider client cache timeout of the jwks resource") + @Value("${dev.dsf.server.auth.oidc.provider.client.cache.timeout.jwks.resource:PT1H}") + private String oidcProviderClientCacheJwksResourceTimeout; + @Documentation(description = "OIDC provider client connect timeout") @Value("${dev.dsf.server.auth.oidc.provider.client.timeout.connect:PT5S}") private String oidcProviderClientTimeoutConnect; @@ -407,24 +415,44 @@ public BaseOidcClient baseOidcClient() char[] keyStorePassword = UUID.randomUUID().toString().toCharArray(); KeyStore keyStore = oidcProviderClientKeyStore(keyStorePassword); - return new BaseOidcClientWithCache(new BaseOidcClientJersey(oidcProviderRealmBaseUrl, oidcProviderDiscoveryPath, - trustStore, keyStore, keyStore == null ? null : keyStorePassword, proxyUrl, proxyUsername, - proxyPassword, buildInfoReader().getUserAgentValue(), oidcProviderClientTimeoutConnect(), - oidcProviderClientTimeoutRead(), false)); + return new BaseOidcClientWithCache(getOidcProviderClientCacheConfigurationResourceTimeout(), + getOidcProviderClientCacheJwksResourceTimeout(), + new BaseOidcClientJersey(oidcProviderRealmBaseUrl, oidcProviderDiscoveryPath, trustStore, keyStore, + keyStore == null ? null : keyStorePassword, proxyUrl, proxyUsername, proxyPassword, + buildInfoReader().getUserAgentValue(), oidcProviderClientTimeoutConnect(), + oidcProviderClientTimeoutRead(), false)); + } + + private Duration getOidcProviderClientCacheConfigurationResourceTimeout() + { + return assertPositive(Duration.parse(oidcProviderClientCacheConfigurationResourceTimeout)); + } + + private Duration getOidcProviderClientCacheJwksResourceTimeout() + { + return assertPositive(Duration.parse(oidcProviderClientCacheJwksResourceTimeout)); } @Bean @Lazy public Duration oidcProviderClientTimeoutRead() { - return Duration.parse(oidcProviderClientTimeoutRead); + return assertPositive(Duration.parse(oidcProviderClientTimeoutRead)); } @Bean @Lazy public Duration oidcProviderClientTimeoutConnect() { - return Duration.parse(oidcProviderClientTimeoutConnect); + return assertPositive(Duration.parse(oidcProviderClientTimeoutConnect)); + } + + private Duration assertPositive(Duration duration) + { + if (duration != null && duration.isNegative()) + throw new IllegalArgumentException("configured duration is negative"); + else + return duration; } private Proxy oidcClientProxy() diff --git a/dsf-common/dsf-common-oidc/src/main/java/dev/dsf/common/oidc/BaseOidcClientWithCache.java b/dsf-common/dsf-common-oidc/src/main/java/dev/dsf/common/oidc/BaseOidcClientWithCache.java index f9be7f2b4..6fc07b303 100644 --- a/dsf-common/dsf-common-oidc/src/main/java/dev/dsf/common/oidc/BaseOidcClientWithCache.java +++ b/dsf-common/dsf-common-oidc/src/main/java/dev/dsf/common/oidc/BaseOidcClientWithCache.java @@ -15,51 +15,65 @@ */ package dev.dsf.common.oidc; +import java.time.Duration; +import java.time.ZonedDateTime; import java.util.Objects; import java.util.concurrent.atomic.AtomicReference; import java.util.function.Supplier; public class BaseOidcClientWithCache implements BaseOidcClient { - private final BaseOidcClient delegate; + private static final record CacheEntry(ZonedDateTime timeout, T resource) + { + } + + private final Duration cacheTimeoutConfigurationResource; + private final Duration cacheTimeoutJwksResource; + + private final AtomicReference> configurationCache = new AtomicReference<>(); + private final AtomicReference> jwksCache = new AtomicReference<>(); - private final AtomicReference oidcConfiguration = new AtomicReference<>(); - private final AtomicReference jwks = new AtomicReference<>(); + private final BaseOidcClient delegate; /** + * @param cacheTimeoutconfigurationResource + * not null + * @param cacheTimeoutJwksResource + * not null * @param delegate * not null */ - public BaseOidcClientWithCache(BaseOidcClient delegate) + public BaseOidcClientWithCache(Duration cacheTimeoutconfigurationResource, Duration cacheTimeoutJwksResource, + BaseOidcClient delegate) { + this.cacheTimeoutConfigurationResource = Objects.requireNonNull(cacheTimeoutconfigurationResource, + "cacheTimeoutconfigurationResource"); + this.cacheTimeoutJwksResource = Objects.requireNonNull(cacheTimeoutJwksResource, "cacheTimeoutJwksResource"); this.delegate = Objects.requireNonNull(delegate, "delegate"); } @Override public OidcConfiguration getConfiguration() throws OidcClientException { - return getOrSet(oidcConfiguration, delegate::getConfiguration); + return getOrSet(configurationCache, cacheTimeoutConfigurationResource, delegate::getConfiguration); } - private T getOrSet(AtomicReference cache, Supplier supplier) + private T getOrSet(AtomicReference> cache, Duration timeout, Supplier supplier) { - T cached = cache.get(); - if (cached == null) + CacheEntry cached = cache.get(); + if (cached != null && cached.timeout.isAfter(ZonedDateTime.now())) + return cached.resource; + else { - T value = supplier.get(); - if (cache.compareAndSet(cached, value)) - return value; - else - return cache.get(); + cache.compareAndSet(cached, new CacheEntry<>(ZonedDateTime.now().plus(timeout), supplier.get())); + return cache.get().resource; } - else - return cached; } @Override public Jwks getJwks() throws OidcClientException { - return getOrSet(jwks, () -> delegate.getJwks(getConfiguration())); + return getOrSet(jwksCache, cacheTimeoutJwksResource, () -> delegate.getJwks(getConfiguration())); } @Override diff --git a/dsf-common/dsf-common-oidc/src/main/java/dev/dsf/common/oidc/Jwks.java b/dsf-common/dsf-common-oidc/src/main/java/dev/dsf/common/oidc/Jwks.java index a48043c15..34a1200b4 100644 --- a/dsf-common/dsf-common-oidc/src/main/java/dev/dsf/common/oidc/Jwks.java +++ b/dsf-common/dsf-common-oidc/src/main/java/dev/dsf/common/oidc/Jwks.java @@ -39,6 +39,9 @@ import java.util.function.Function; import java.util.stream.Collectors; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + import com.auth0.jwt.algorithms.Algorithm; import com.auth0.jwt.interfaces.ECDSAKeyProvider; import com.auth0.jwt.interfaces.RSAKeyProvider; @@ -49,6 +52,10 @@ @JsonIgnoreProperties(ignoreUnknown = true) public class Jwks { + private static final Logger logger = LoggerFactory.getLogger(Jwks.class); + + private static final int RSA_MIN_KEY_LENGTH = 2048; + @JsonIgnoreProperties(ignoreUnknown = true) public static record JwksKey(@JsonProperty("kid") String kid, @JsonProperty("kty") String kty, @JsonProperty("alg") String alg, @JsonProperty("crv") String crv, @JsonProperty("use") String use, @@ -77,20 +84,27 @@ public JwksKey(@JsonProperty("kid") String kid, @JsonProperty("kty") String kty, * @throws JwksException * if {@link Algorithm} can't be created or is not supported for the enclosed key material */ - public Algorithm toAlgorithm() throws JwksException + public Optional toAlgorithm() throws JwksException { - return switch (kty) + return Optional.ofNullable(switch (kty) { case "RSA" -> toRsaAlgorithm(); case "EC" -> toEcdsaAlgorithm(); - default -> throw new JwksException("JWKS kty property value '" + kty + "' not one of 'RSA' or 'EC'"); - }; + default -> { + logger.warn("JWKS kty property value '{}' not one of 'RSA' or 'EC'", kty); + yield null; + } + }); } private Algorithm toRsaAlgorithm() { RSAPublicKey key = toRsaPublicKey(n, e); + + if (key == null) + return null; + RSAKeyProvider keyProvider = toRsaKeyProvider(key, kid); return switch (alg) @@ -99,14 +113,20 @@ private Algorithm toRsaAlgorithm() case "RS384" -> Algorithm.RSA384(keyProvider); case "RS512" -> Algorithm.RSA512(keyProvider); - default -> throw new JwksException( - "JWKS alg property value '" + alg + "' not one of 'RSA256', 'RSA384' or 'RSA512'"); + default -> { + logger.warn("JWKS alg property value '{}' not one of 'RS256', 'RS384' or 'RS512'", alg); + yield null; + } }; } private Algorithm toEcdsaAlgorithm() { ECPublicKey key = toEcPublicKey(x, y, crv); + + if (key == null) + return null; + ECDSAKeyProvider keyProvider = toEcKeyProvider(key, kid); return switch (alg) @@ -115,8 +135,10 @@ private Algorithm toEcdsaAlgorithm() case "ES384" -> Algorithm.ECDSA384(keyProvider); case "ES512" -> Algorithm.ECDSA512(keyProvider); - default -> throw new JwksException( - "JWKS crv property value '" + alg + "' not one of 'ES256', 'ES384' or 'ES512'"); + default -> { + logger.warn("JWKS crv property value '{}' not one of 'ES256', 'ES384' or 'ES512'", alg); + yield null; + } }; } @@ -152,6 +174,12 @@ private RSAPublicKey toRsaPublicKey(String n, String e) BigInteger modulus = new BigInteger(1, Base64.getUrlDecoder().decode(n)); BigInteger exponent = new BigInteger(1, Base64.getUrlDecoder().decode(e)); + if (modulus.bitLength() < RSA_MIN_KEY_LENGTH) + { + logger.warn("JWKS RSA key (kid: '{}') length {} <{} bit", kid, modulus.bitLength(), RSA_MIN_KEY_LENGTH); + return null; + } + try { RSAPublicKeySpec keySpec = new RSAPublicKeySpec(modulus, exponent); @@ -161,7 +189,10 @@ private RSAPublicKey toRsaPublicKey(String n, String e) } catch (InvalidKeySpecException | NoSuchAlgorithmException ex) { - throw new JwksException("Unable to create RSA public key", ex); + logger.debug("Unable to create RSA public key: {} - {}", ex.getClass().getName(), ex.getMessage()); + logger.warn("Unable to create RSA public key", ex); + + return null; } } @@ -197,16 +228,21 @@ private ECPublicKey toEcPublicKey(String x, String y, String crv) BigInteger xCoordinate = new BigInteger(1, Base64.getUrlDecoder().decode(x)); BigInteger yCoordinate = new BigInteger(1, Base64.getUrlDecoder().decode(y)); - ECGenParameterSpec curve = switch (crv) + return switch (crv) { - case "P-256" -> new ECGenParameterSpec("secp256r1"); - case "P-384" -> new ECGenParameterSpec("secp384r1"); - case "P-521" -> new ECGenParameterSpec("secp521r1"); + case "P-256" -> toEcPublicKey(xCoordinate, yCoordinate, new ECGenParameterSpec("secp256r1")); + case "P-384" -> toEcPublicKey(xCoordinate, yCoordinate, new ECGenParameterSpec("secp384r1")); + case "P-521" -> toEcPublicKey(xCoordinate, yCoordinate, new ECGenParameterSpec("secp521r1")); - default -> throw new JwksException( - "JWKS crv property value '" + crv + "' not one of 'P-256', 'P-384' or 'P-521'"); + default -> { + logger.warn("JWKS crv property value '{}' not one of 'P-256', 'P-384' or 'P-521'", crv); + yield null; + } }; + } + private ECPublicKey toEcPublicKey(BigInteger xCoordinate, BigInteger yCoordinate, ECGenParameterSpec curve) + { try { AlgorithmParameters parameters = AlgorithmParameters.getInstance("EC"); @@ -219,7 +255,10 @@ private ECPublicKey toEcPublicKey(String x, String y, String crv) } catch (NoSuchAlgorithmException | InvalidParameterSpecException | InvalidKeySpecException ex) { - throw new JwksException("Unable to create EC public key", ex); + logger.debug("Unable to create EC public key", ex); + logger.warn("Unable to create EC public key: {} - {}", ex.getClass().getName(), ex.getMessage()); + + return null; } } } diff --git a/dsf-common/dsf-common-oidc/src/main/java/dev/dsf/common/oidc/JwtVerifierImpl.java b/dsf-common/dsf-common-oidc/src/main/java/dev/dsf/common/oidc/JwtVerifierImpl.java index 3328c6376..5afb0b7e7 100644 --- a/dsf-common/dsf-common-oidc/src/main/java/dev/dsf/common/oidc/JwtVerifierImpl.java +++ b/dsf-common/dsf-common-oidc/src/main/java/dev/dsf/common/oidc/JwtVerifierImpl.java @@ -16,6 +16,7 @@ package dev.dsf.common.oidc; import java.util.Objects; +import java.util.Optional; import com.auth0.jwt.JWT; import com.auth0.jwt.algorithms.Algorithm; @@ -57,14 +58,21 @@ public DecodedJWT verifyBackchannelLogout(String token) throws JWTVerificationEx { final String keyId = JWT.decode(token).getKeyId(); - JWTVerifier verifier = oidcClient.getJwks().getKey(keyId).map(JwksKey::toAlgorithm).map(algorithm -> + Optional key = oidcClient.getJwks().getKey(keyId); + if (key.isEmpty() || !key.get().use().equals("sig")) + throw new OidcClientException("Logout token key with kid '" + keyId + "' and use 'sig' not in JWKS"); + + Optional algorithm = key.flatMap(JwksKey::toAlgorithm); + if (key.isEmpty()) { - return createVerification(algorithm).withAudience(clientId).withClaim("events", - (claim, _) -> claim.asMap().containsKey("http://schemas.openid.net/event/backchannel-logout")) - .build(); + throw new OidcClientException("Logout token key with kid '" + keyId + + "' has unsupported type (kty) / algorithm (alg) / key-size in JWKS"); + } - }).orElseThrow(() -> new OidcClientException( - "Key with id " + keyId + " not found in JWKS resource from OIDC provider")); + JWTVerifier verifier = createVerification(algorithm.get()).withAudience(clientId) + .withClaim("events", + (claim, _) -> claim.asMap().containsKey("http://schemas.openid.net/event/backchannel-logout")) + .build(); return verifier.verify(token); } @@ -79,17 +87,23 @@ public DecodedJWT verifyBearerToken(String token) throws JWTVerificationExceptio { final String keyId = JWT.decode(token).getKeyId(); - JWTVerifier verifier = oidcClient.getJwks().getKey(keyId).map(JwksKey::toAlgorithm).map(algorithm -> + Optional key = oidcClient.getJwks().getKey(keyId); + if (key.isEmpty() || !key.get().use().equals("sig")) + throw new OidcClientException("Bearer token key with kid '" + keyId + "' and use 'sig' not in JWKS"); + + Optional algorithm = key.flatMap(JwksKey::toAlgorithm); + if (key.isEmpty()) { - Verification verification = createVerification(algorithm).acceptLeeway(1); + throw new OidcClientException("Bearer token key with kid '" + keyId + + "' has unsupported type (kty) / algorithm (alg) / key-size in JWKS"); + } - if (!bearerTokenAudience.isBlank()) - verification.withAnyOfAudience(bearerTokenAudience); + Verification verification = createVerification(algorithm.get()); - return verification.build(); + if (!bearerTokenAudience.isBlank()) + verification.withAnyOfAudience(bearerTokenAudience); - }).orElseThrow(() -> new OidcClientException( - "Key with id " + keyId + " not found in JWKS resource from OIDC provider")); + JWTVerifier verifier = verification.build(); return verifier.verify(token); } diff --git a/dsf-common/dsf-common-oidc/src/test/java/dev/dsf/common/oidc/JwksTest.java b/dsf-common/dsf-common-oidc/src/test/java/dev/dsf/common/oidc/JwksTest.java index 20aa3631c..54b734b85 100644 --- a/dsf-common/dsf-common-oidc/src/test/java/dev/dsf/common/oidc/JwksTest.java +++ b/dsf-common/dsf-common-oidc/src/test/java/dev/dsf/common/oidc/JwksTest.java @@ -16,14 +16,15 @@ package dev.dsf.common.oidc; import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertNotNull; -import static org.junit.Assert.assertThrows; import static org.junit.Assert.assertTrue; import java.util.Optional; import org.junit.Test; +import com.auth0.jwt.algorithms.Algorithm; import com.fasterxml.jackson.databind.ObjectMapper; import dev.dsf.common.oidc.Jwks.JwksKey; @@ -58,7 +59,21 @@ public class JwksTest ], "x5t": "dQL-LEROCVCUfvs0W_5ayioFWjA", "x5t#S256": "yi-b9TklWk5X5d_Pr_moQVmdkdVa4wZTuYnDxWXrXag" - } + }, + { + "kid": "BMvf48wBJERBDMGInNfOsSiTWAnNiWGinVPnjSCeWcg", + "kty": "EC", + "alg": "ES384", + "use": "sig", + "x5c": [ + "MIIBTTCB1AIGAZ0bZiPeMAoGCCqGSM49BAMCMBIxEDAOBgNVBAMMB2VjX3Rlc3QwHhcNMjYwMzIzMTU1MTExWhcNMzYwMzIzMTU1MjUxWjASMRAwDgYDVQQDDAdlY190ZXN0MHYwEAYHKoZIzj0CAQYFK4EEACIDYgAE3Nb3kbLLkQ6y/E8yDKrB3LZVo1aXQV12OR3yWFl9D/Xc4RZ874NngV9FVccqVS41WZp5HbiH/OhIgvKvyRE97BRZu/i+UhZz59l74fogV4sNGLtbnbzA62eQbIAu/c/kMAoGCCqGSM49BAMCA2gAMGUCMFI+m0Z21NfMGjlPpr4v64DOEzjoP4Y9fS4SJK3YIEfHvkVidDgm2A1lZD0ZRKXb7AIxAPi1ScvwtrIl0oadty4Qjg1+lWFAp943fHdFEuNE6GeagkB/dm9Nuo1wqy6hKm5O9Q==" + ], + "x5t": "3luSUTuX5v_MVEGVP_WDwy1e9GI", + "x5t#S256": "ra4lrnu_zhxYpamDEvtcxEvAFenH9CAuS5OvEiDtTho", + "crv": "P-384", + "x": "3Nb3kbLLkQ6y_E8yDKrB3LZVo1aXQV12OR3yWFl9D_Xc4RZ874NngV9FVccqVS41", + "y": "WZp5HbiH_OhIgvKvyRE97BRZu_i-UhZz59l74fogV4sNGLtbnbzA62eQbIAu_c_k" + } ] }"""; @@ -70,27 +85,41 @@ public void testDecodeJwks() throws Exception assertNotNull(jwks); assertNotNull(jwks.getKeys()); - assertEquals(2, jwks.getKeys().size()); + assertEquals(3, jwks.getKeys().size()); Optional jwk0o = jwks.getKey("kncc6492FTtclCO8qJvhS2PvYap_VabfAPOLhK3mkfA"); Optional jwk1o = jwks.getKey("Zp7ockRwsxqM6FrZlDJUOVwAxPICO2jBW0Rbk25oYGk"); + Optional jwk2o = jwks.getKey("BMvf48wBJERBDMGInNfOsSiTWAnNiWGinVPnjSCeWcg"); assertTrue(jwk0o.isPresent()); assertTrue(jwk1o.isPresent()); + assertTrue(jwk2o.isPresent()); assertTrue(jwks.getKey(null).isEmpty()); assertTrue(jwks.getKey("not existing").isEmpty()); JwksKey jwk0 = jwk0o.get(); JwksKey jwk1 = jwk1o.get(); + JwksKey jwk2 = jwk2o.get(); assertNotNull(jwk0.kid()); assertEquals("kncc6492FTtclCO8qJvhS2PvYap_VabfAPOLhK3mkfA", jwk0.kid()); assertNotNull(jwk1.kid()); assertEquals("Zp7ockRwsxqM6FrZlDJUOVwAxPICO2jBW0Rbk25oYGk", jwk1.kid()); + assertNotNull(jwk2.kid()); + assertEquals("BMvf48wBJERBDMGInNfOsSiTWAnNiWGinVPnjSCeWcg", jwk2.kid()); + + Optional jwk0a = jwk0.toAlgorithm(); + assertNotNull(jwk0a); + assertTrue(jwk0a.isPresent()); + assertEquals("RS256", jwk0.toAlgorithm().get().getName()); - assertNotNull(jwk0.toAlgorithm()); - assertEquals("RS256", jwk0.toAlgorithm().getName()); + Optional jwk1a = jwk1.toAlgorithm(); + assertNotNull(jwk1a); + assertFalse(jwk1a.isPresent()); - assertThrows(JwksException.class, jwk1::toAlgorithm); + Optional jwk2a = jwk2.toAlgorithm(); + assertNotNull(jwk2a); + assertTrue(jwk2a.isPresent()); + assertEquals("ES384", jwk2.toAlgorithm().get().getName()); } } From cc0bb71ef98acb621911058a499c47a6680c931a Mon Sep 17 00:00:00 2001 From: Hauke Hund Date: Mon, 23 Mar 2026 20:16:00 +0100 Subject: [PATCH 05/18] removed access token claim log messages --- .../auth/conf/AbstractIdentityProvider.java | 17 ----------------- .../common/auth/BearerTokenAuthenticator.java | 1 - 2 files changed, 18 deletions(-) diff --git a/dsf-common/dsf-common-auth/src/main/java/dev/dsf/common/auth/conf/AbstractIdentityProvider.java b/dsf-common/dsf-common-auth/src/main/java/dev/dsf/common/auth/conf/AbstractIdentityProvider.java index 68c22b425..aa3808752 100644 --- a/dsf-common/dsf-common-auth/src/main/java/dev/dsf/common/auth/conf/AbstractIdentityProvider.java +++ b/dsf-common/dsf-common-auth/src/main/java/dev/dsf/common/auth/conf/AbstractIdentityProvider.java @@ -139,12 +139,6 @@ public final Identity getIdentity(DsfOpenIdCredentials credentials) protected final List getGroupsFromTokens(Map parsedIdToken, Map parsedAccessToken) { - if (logger.isDebugEnabled()) - { - logger.debug("id_token: groups: {}", getPropertyArray(parsedIdToken, "groups")); - logger.debug("access_token: groups: {}", getPropertyArray(parsedAccessToken, "groups")); - } - return Stream.concat(getPropertyArray(parsedIdToken, "groups").stream(), getPropertyArray(parsedAccessToken, "groups").stream()).toList(); } @@ -158,17 +152,6 @@ protected final List getRolesFromTokens(Map idToken, Map @SuppressWarnings("unchecked") private Stream getRolesFromToken(String tokenName, Map token) { - if (logger.isDebugEnabled()) - { - logger.debug("{}: realm_access.roles: {}", tokenName, - getPropertyArray(getPropertyMap(token, "realm_access"), "roles")); - logger.debug("{}: resource_access.*.roles: {}", tokenName, - getPropertyMap(token, "resource_access").entrySet().stream() - .flatMap(e -> getPropertyArray((Map) e.getValue(), "roles").stream() - .map(r -> e.getKey() + "." + r)) - .toList()); - } - return Stream.concat(getPropertyArray(getPropertyMap(token, "realm_access"), "roles").stream(), getPropertyMap(token, "resource_access").entrySet().stream() .flatMap(e -> getPropertyArray((Map) e.getValue(), "roles").stream() diff --git a/dsf-common/dsf-common-jetty/src/main/java/dev/dsf/common/auth/BearerTokenAuthenticator.java b/dsf-common/dsf-common-jetty/src/main/java/dev/dsf/common/auth/BearerTokenAuthenticator.java index 5d7efea81..6213778d5 100644 --- a/dsf-common/dsf-common-jetty/src/main/java/dev/dsf/common/auth/BearerTokenAuthenticator.java +++ b/dsf-common/dsf-common-jetty/src/main/java/dev/dsf/common/auth/BearerTokenAuthenticator.java @@ -80,7 +80,6 @@ public AuthenticationState validateRequest(Request request, Response response, C return AuthenticationState.SEND_FAILURE; } - logger.debug("Access token claims: {}", jwt.getClaims()); UserIdentity user = login(null, token, request, response); if (user == null) { From d7b4e74a69fb6b57573182ee13fcb05d4451d722 Mon Sep 17 00:00:00 2001 From: Hauke Hund Date: Mon, 23 Mar 2026 20:22:21 +0100 Subject: [PATCH 06/18] refactored code, added equals/hashCode impls to Identity classes --- .../common/auth/conf/AbstractIdentity.java | 28 ++++++++++++- .../dev/dsf/common/auth/conf/Identity.java | 2 +- .../auth/conf/OrganizationIdentityImpl.java | 5 +-- .../auth/conf/PractitionerIdentity.java | 2 +- .../auth/conf/PractitionerIdentityImpl.java | 40 +++++++++++++++++-- .../auth/logging/CurrentUserMdcLogger.java | 6 +-- .../adapter/ThymeleafTemplateServiceImpl.java | 8 ++-- ...uestionnaireResponseAuthorizationRule.java | 15 +++---- .../authorization/TaskAuthorizationRule.java | 25 +++++------- .../QuestionnaireResponseIdentityFilter.java | 8 ++-- .../search/filter/TaskIdentityFilter.java | 8 ++-- .../authentication/IdentityProviderTest.java | 8 ++-- .../process/TestOrganizationIdentity.java | 2 +- .../process/TestPractitionerIdentity.java | 6 +-- 14 files changed, 105 insertions(+), 58 deletions(-) diff --git a/dsf-common/dsf-common-auth/src/main/java/dev/dsf/common/auth/conf/AbstractIdentity.java b/dsf-common/dsf-common-auth/src/main/java/dev/dsf/common/auth/conf/AbstractIdentity.java index c3079bd49..f2ee39eef 100644 --- a/dsf-common/dsf-common-auth/src/main/java/dev/dsf/common/auth/conf/AbstractIdentity.java +++ b/dsf-common/dsf-common-auth/src/main/java/dev/dsf/common/auth/conf/AbstractIdentity.java @@ -36,6 +36,8 @@ public abstract class AbstractIdentity implements Identity private final Set dsfRoles = new HashSet<>(); private final X509CertificateWrapper certificate; + private final String organizationIdentifierValue; + /** * @param localIdentity * true if this is a local identity @@ -59,6 +61,28 @@ public AbstractIdentity(boolean localIdentity, Organization organization, Endpoi this.dsfRoles.addAll(dsfRoles); this.certificate = certificate; + + this.organizationIdentifierValue = getIdentifierValue(organization::getIdentifier, + ORGANIZATION_IDENTIFIER_SYSTEM).orElseThrow(); + } + + @Override + public int hashCode() + { + return Objects.hash(organizationIdentifierValue); + } + + @Override + public boolean equals(Object obj) + { + if (this == obj) + return true; + if (obj == null) + return false; + if (getClass() != obj.getClass()) + return false; + + return Objects.equals(organizationIdentifierValue, ((AbstractIdentity) obj).organizationIdentifierValue); } @Override @@ -74,9 +98,9 @@ public Organization getOrganization() } @Override - public Optional getOrganizationIdentifierValue() + public String getOrganizationIdentifierValue() { - return getIdentifierValue(organization::getIdentifier, ORGANIZATION_IDENTIFIER_SYSTEM); + return organizationIdentifierValue; } protected Optional getIdentifierValue(Supplier> identifiers, String identifierSystem) diff --git a/dsf-common/dsf-common-auth/src/main/java/dev/dsf/common/auth/conf/Identity.java b/dsf-common/dsf-common-auth/src/main/java/dev/dsf/common/auth/conf/Identity.java index 61337528b..e4e562102 100644 --- a/dsf-common/dsf-common-auth/src/main/java/dev/dsf/common/auth/conf/Identity.java +++ b/dsf-common/dsf-common-auth/src/main/java/dev/dsf/common/auth/conf/Identity.java @@ -34,7 +34,7 @@ public interface Identity extends Principal */ Organization getOrganization(); - Optional getOrganizationIdentifierValue(); + String getOrganizationIdentifierValue(); Set getDsfRoles(); diff --git a/dsf-common/dsf-common-auth/src/main/java/dev/dsf/common/auth/conf/OrganizationIdentityImpl.java b/dsf-common/dsf-common-auth/src/main/java/dev/dsf/common/auth/conf/OrganizationIdentityImpl.java index f743078ce..fdcec8ae5 100644 --- a/dsf-common/dsf-common-auth/src/main/java/dev/dsf/common/auth/conf/OrganizationIdentityImpl.java +++ b/dsf-common/dsf-common-auth/src/main/java/dev/dsf/common/auth/conf/OrganizationIdentityImpl.java @@ -20,7 +20,6 @@ import org.hl7.fhir.r4.model.Endpoint; import org.hl7.fhir.r4.model.Organization; -// TODO implement equals, hashCode, toString methods based on the DSF organization identifier to fully comply with the java.security.Principal specification public class OrganizationIdentityImpl extends AbstractIdentity implements OrganizationIdentity { /** @@ -44,12 +43,12 @@ public OrganizationIdentityImpl(boolean localIdentity, Organization organization @Override public String getName() { - return getOrganizationIdentifierValue().orElse("?"); + return getOrganizationIdentifierValue(); } @Override public String getDisplayName() { - return getOrganizationIdentifierValue().orElse("?"); + return getOrganizationIdentifierValue(); } } diff --git a/dsf-common/dsf-common-auth/src/main/java/dev/dsf/common/auth/conf/PractitionerIdentity.java b/dsf-common/dsf-common-auth/src/main/java/dev/dsf/common/auth/conf/PractitionerIdentity.java index 4d22bf8a6..1a67d77f6 100644 --- a/dsf-common/dsf-common-auth/src/main/java/dev/dsf/common/auth/conf/PractitionerIdentity.java +++ b/dsf-common/dsf-common-auth/src/main/java/dev/dsf/common/auth/conf/PractitionerIdentity.java @@ -34,7 +34,7 @@ public interface PractitionerIdentity extends Identity */ Practitioner getPractitioner(); - Optional getPractitionerIdentifierValue(); + String getPractitionerIdentifierValue(); /** * @return never null diff --git a/dsf-common/dsf-common-auth/src/main/java/dev/dsf/common/auth/conf/PractitionerIdentityImpl.java b/dsf-common/dsf-common-auth/src/main/java/dev/dsf/common/auth/conf/PractitionerIdentityImpl.java index c0d1bfdb4..c1c5ba3f6 100644 --- a/dsf-common/dsf-common-auth/src/main/java/dev/dsf/common/auth/conf/PractitionerIdentityImpl.java +++ b/dsf-common/dsf-common-auth/src/main/java/dev/dsf/common/auth/conf/PractitionerIdentityImpl.java @@ -29,7 +29,6 @@ import dev.dsf.common.auth.DsfOpenIdCredentials; -// TODO implement equals, hashCode, toString methods based on the DSF organization identifier to fully comply with the java.security.Principal specification public class PractitionerIdentityImpl extends AbstractIdentity implements PractitionerIdentity { private final Practitioner practitioner; @@ -37,6 +36,8 @@ public class PractitionerIdentityImpl extends AbstractIdentity implements Practi private final Set practitionerRoles = new HashSet<>(); + private final String practitionerIdentifierValue; + /** * @param organization * not null @@ -59,6 +60,7 @@ public PractitionerIdentityImpl(Organization organization, Endpoint endpoint, { super(true, organization, endpoint, dsfRoles, certificate); + this.practitioner = Objects.requireNonNull(practitioner, "practitioner"); if (practitionerRoles != null) @@ -66,12 +68,42 @@ public PractitionerIdentityImpl(Organization organization, Endpoint endpoint, // null if login via client certificate this.credentials = credentials; + + this.practitionerIdentifierValue = getIdentifierValue(practitioner::getIdentifier, + PRACTITIONER_IDENTIFIER_SYSTEM).orElseThrow(); + } + + @Override + public int hashCode() + { + final int prime = 31; + int result = super.hashCode(); + result = prime * result + Objects.hash(getOrganizationIdentifierValue()); + result = prime * result + Objects.hash(practitionerIdentifierValue); + return result; + } + + @Override + public boolean equals(Object obj) + { + if (this == obj) + return true; + if (obj == null) + return false; + if (!super.equals(obj)) + return false; + if (getClass() != obj.getClass()) + return false; + + PractitionerIdentityImpl other = (PractitionerIdentityImpl) obj; + return Objects.equals(getOrganizationIdentifierValue(), other.getOrganizationIdentifierValue()) + && Objects.equals(practitionerIdentifierValue, other.practitionerIdentifierValue); } @Override public String getName() { - return getOrganizationIdentifierValue().orElse("?") + "/" + getPractitionerIdentifierValue().orElse("?"); + return getOrganizationIdentifierValue() + "/" + practitionerIdentifierValue; } @Override @@ -87,9 +119,9 @@ public Practitioner getPractitioner() } @Override - public Optional getPractitionerIdentifierValue() + public String getPractitionerIdentifierValue() { - return getIdentifierValue(practitioner::getIdentifier, PRACTITIONER_IDENTIFIER_SYSTEM); + return practitionerIdentifierValue; } @Override diff --git a/dsf-common/dsf-common-auth/src/main/java/dev/dsf/common/auth/logging/CurrentUserMdcLogger.java b/dsf-common/dsf-common-auth/src/main/java/dev/dsf/common/auth/logging/CurrentUserMdcLogger.java index ee179acb0..6080e2414 100644 --- a/dsf-common/dsf-common-auth/src/main/java/dev/dsf/common/auth/logging/CurrentUserMdcLogger.java +++ b/dsf-common/dsf-common-auth/src/main/java/dev/dsf/common/auth/logging/CurrentUserMdcLogger.java @@ -55,7 +55,7 @@ protected void before(OrganizationIdentity organization) organization.getCertificate().map(X509CertificateWrapper::subjectDn) .ifPresent(d -> MDC.put(DSF_ORGANIZATION_DN, d)); - organization.getOrganizationIdentifierValue().ifPresent(i -> MDC.put(DSF_ORGANIZATION_IDENTIFIER, i)); + MDC.put(DSF_ORGANIZATION_IDENTIFIER, organization.getOrganizationIdentifierValue()); organization.getEndpointIdentifierValue().ifPresent(i -> MDC.put(DSF_ENDPOINT_IDENTIFIER, i)); } @@ -71,9 +71,9 @@ protected void before(PractitionerIdentity practitioner) practitioner.getCredentials().map(DsfOpenIdCredentials::getUserId) .ifPresent(i -> MDC.put(DSF_PRACTITIONER_SUB, i)); - practitioner.getOrganizationIdentifierValue().ifPresent(i -> MDC.put(DSF_ORGANIZATION_IDENTIFIER, i)); + MDC.put(DSF_ORGANIZATION_IDENTIFIER, practitioner.getOrganizationIdentifierValue()); practitioner.getEndpointIdentifierValue().ifPresent(i -> MDC.put(DSF_ENDPOINT_IDENTIFIER, i)); - practitioner.getPractitionerIdentifierValue().ifPresent(i -> MDC.put(DSF_PRACTITIONER_IDENTIFIER, i)); + MDC.put(DSF_PRACTITIONER_IDENTIFIER, practitioner.getPractitionerIdentifierValue()); if (!practitioner.getPractionerRoles().isEmpty()) MDC.put(DSF_PRACTITIONER_ROLES, practitioner.getPractionerRoles().stream() diff --git a/dsf-fhir/dsf-fhir-server/src/main/java/dev/dsf/fhir/adapter/ThymeleafTemplateServiceImpl.java b/dsf-fhir/dsf-fhir-server/src/main/java/dev/dsf/fhir/adapter/ThymeleafTemplateServiceImpl.java index 91d26ffa6..4dfdaa0cc 100644 --- a/dsf-fhir/dsf-fhir-server/src/main/java/dev/dsf/fhir/adapter/ThymeleafTemplateServiceImpl.java +++ b/dsf-fhir/dsf-fhir-server/src/main/java/dev/dsf/fhir/adapter/ThymeleafTemplateServiceImpl.java @@ -181,9 +181,9 @@ public void writeTo(Resource resource, Class type, MediaType mediaType, UriIn String usernameTitle = ""; if (securityContext.getUserPrincipal() instanceof PractitionerIdentity p) { - if (p.getPractitionerIdentifierValue().isPresent()) - usernameTitle += "Mail: " + p.getPractitionerIdentifierValue().get(); - if (p.getPractitionerIdentifierValue().isPresent() && !p.getPractionerRoles().isEmpty()) + if (p.getPractitionerIdentifierValue() != null) + usernameTitle += "Mail: " + p.getPractitionerIdentifierValue(); + if (p.getPractitionerIdentifierValue() != null && !p.getPractionerRoles().isEmpty()) usernameTitle += " - "; if (!p.getPractionerRoles().isEmpty()) usernameTitle += p.getPractionerRoles().stream() @@ -195,7 +195,7 @@ public void writeTo(Resource resource, Class type, MediaType mediaType, UriIn context.setVariable("practitionerIdentifierValue", securityContext.getUserPrincipal() instanceof PractitionerIdentity p - ? p.getPractitionerIdentifierValue().orElse(null) + ? p.getPractitionerIdentifierValue() : null); context.setVariable("openid", "OPENID".equals(securityContext.getAuthenticationScheme())); 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 ccbd3b303..039986a81 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 @@ -151,14 +151,10 @@ private Optional newResourceOk(Connection connection, Identity identity, errors.add("QuestionnaireResponse.author.identifier.system not " + NAMING_SYSTEM_PRACTITIONER_IDENTIFIER); - Optional practitionerIdentifierValue = p.getPractitionerIdentifierValue(); - if (practitionerIdentifierValue.isPresent()) - { - if (!practitionerIdentifierValue.get().equals(identifier.getValue())) - errors.add("QuestionnaireResponse.author not current practitioner identity"); - } - else - throw new RuntimeException("Authenticated practitioner user has no identifier"); + String practitionerIdentifierValue = p.getPractitionerIdentifierValue(); + if (practitionerIdentifierValue == null + || !practitionerIdentifierValue.equals(identifier.getValue())) + errors.add("QuestionnaireResponse.author not current practitioner identity"); } else if (identity instanceof OrganizationIdentity) { @@ -322,7 +318,8 @@ private boolean isPractitionerAuthorized(QuestionnaireResponse existingResource, && e.getValue() instanceof Identifier i && i.hasSystem() && i.hasValue()) { return NAMING_SYSTEM_PRACTITIONER_IDENTIFIER.equals(i.getSystem()) - && identity.getPractitionerIdentifierValue().map(v -> v.equals(i.getValue())).orElse(false); + && identity.getPractitionerIdentifierValue() != null + && identity.getPractitionerIdentifierValue().equals(i.getValue()); } else if (EXTENSION_QUESTIONNAIRE_AUTHORIZATION_PRACTITIONER_ROLE.equals(e.getUrl()) && e.getValue() instanceof Coding c && c.hasSystem() && c.hasCode()) diff --git a/dsf-fhir/dsf-fhir-server/src/main/java/dev/dsf/fhir/authorization/TaskAuthorizationRule.java b/dsf-fhir/dsf-fhir-server/src/main/java/dev/dsf/fhir/authorization/TaskAuthorizationRule.java index 3587f6513..0b790a5df 100644 --- a/dsf-fhir/dsf-fhir-server/src/main/java/dev/dsf/fhir/authorization/TaskAuthorizationRule.java +++ b/dsf-fhir/dsf-fhir-server/src/main/java/dev/dsf/fhir/authorization/TaskAuthorizationRule.java @@ -251,14 +251,10 @@ private Optional requestedTaskOk(Connection connection, Identity identit if (!NAMING_SYSTEM_PRACTITIONER_IDENTIFIER.equals(identifier.getSystem())) errors.add("Task.requester.identifier.system not " + NAMING_SYSTEM_PRACTITIONER_IDENTIFIER); - Optional practitionerIdentifierValue = p.getPractitionerIdentifierValue(); - if (practitionerIdentifierValue.isPresent()) - { - if (!practitionerIdentifierValue.get().equals(identifier.getValue())) - errors.add("Task.requester not current practitioner identity"); - } - else - throw new RuntimeException("Authenticated practitioner user has no identifier"); + String practitionerIdentifierValue = p.getPractitionerIdentifierValue(); + if (practitionerIdentifierValue == null + || !practitionerIdentifierValue.equals(identifier.getValue())) + errors.add("Task.requester not current practitioner identity"); } else if (identity instanceof OrganizationIdentity) { @@ -566,7 +562,7 @@ private boolean taskAllowedForRequesterAndRecipient(Connection connection, Ident boolean okForRequester = processAuthorizationHelper .getRequesters(activityDefinition, processUrl, processVersion, messageName, taskProfiles) .anyMatch(r -> r.isRequesterAuthorized(requester, - getAffiliations(connection, requester.getOrganizationIdentifierValue().orElse(null), + getAffiliations(connection, requester.getOrganizationIdentifierValue(), requester.getEndpointIdentifierValue().orElse(null)))); if (!okForRecipient && !okForRequester) @@ -733,12 +729,11 @@ && isCurrentIdentityPartOfReferencedOrganization(connection, identity, return Optional.of( "Identity is local practitioner, has role DSF_ADMIN and organization referenced as recipient"); } - else if (p.getPractitionerIdentifierValue() - .map(v -> existingResource.getRequester().hasIdentifier() - && NAMING_SYSTEM_PRACTITIONER_IDENTIFIER - .equals(existingResource.getRequester().getIdentifier().getSystem()) - && v.equals(existingResource.getRequester().getIdentifier().getValue())) - .orElse(false)) + else if (existingResource.getRequester().hasIdentifier() + && NAMING_SYSTEM_PRACTITIONER_IDENTIFIER + .equals(existingResource.getRequester().getIdentifier().getSystem()) + && p.getPractitionerIdentifierValue() != null && p.getPractitionerIdentifierValue() + .equals(existingResource.getRequester().getIdentifier().getValue())) { logger.info( "Read of Task/{}/_history/{} authorized for identity '{}', identity is local practitioner and practitioner referenced as requester", diff --git a/dsf-fhir/dsf-fhir-server/src/main/java/dev/dsf/fhir/search/filter/QuestionnaireResponseIdentityFilter.java b/dsf-fhir/dsf-fhir-server/src/main/java/dev/dsf/fhir/search/filter/QuestionnaireResponseIdentityFilter.java index 63e90b788..c0980de93 100644 --- a/dsf-fhir/dsf-fhir-server/src/main/java/dev/dsf/fhir/search/filter/QuestionnaireResponseIdentityFilter.java +++ b/dsf-fhir/dsf-fhir-server/src/main/java/dev/dsf/fhir/search/filter/QuestionnaireResponseIdentityFilter.java @@ -60,7 +60,7 @@ public String getFilterQuery() if (identity instanceof OrganizationIdentity || (identity instanceof PractitionerIdentity p && p.hasPractionerRole("DSF_ADMIN"))) return ""; - else if (identity instanceof PractitionerIdentity p && p.getPractitionerIdentifierValue().isPresent()) + else if (identity instanceof PractitionerIdentity p && p.getPractitionerIdentifierValue() != null) return "EXISTS (SELECT 1 FROM jsonb_array_elements(" + resourceColumn + "->'extension') AS authExt " + "WHERE authExt->>'url' = 'http://dsf.dev/fhir/StructureDefinition/extension-questionnaire-authorization' " + "AND EXISTS (SELECT 1 FROM jsonb_array_elements(authExt->'extension') AS ext " @@ -79,7 +79,7 @@ public int getSqlParameterCount() { if (identity.isLocalIdentity() && identity.hasDsfRole(operationRole) && identity.hasDsfRole(READ_ROLE) && identity instanceof PractitionerIdentity p && !p.hasPractionerRole("DSF_ADMIN") - && p.getPractitionerIdentifierValue().isPresent()) + && p.getPractitionerIdentifierValue() != null) return 2; else return 0; @@ -91,10 +91,10 @@ public void modifyStatement(int parameterIndex, int subqueryParameterIndex, Prep { if (identity.isLocalIdentity() && identity.hasDsfRole(operationRole) && identity.hasDsfRole(READ_ROLE) && identity instanceof PractitionerIdentity p && !p.hasPractionerRole("DSF_ADMIN") - && p.getPractitionerIdentifierValue().isPresent()) + && p.getPractitionerIdentifierValue() != null) { if (subqueryParameterIndex == 1) - statement.setString(parameterIndex, p.getPractitionerIdentifierValue().get()); + statement.setString(parameterIndex, p.getPractitionerIdentifierValue()); else if (subqueryParameterIndex == 2) statement.setString(parameterIndex, toJson(p.getPractionerRoles())); } diff --git a/dsf-fhir/dsf-fhir-server/src/main/java/dev/dsf/fhir/search/filter/TaskIdentityFilter.java b/dsf-fhir/dsf-fhir-server/src/main/java/dev/dsf/fhir/search/filter/TaskIdentityFilter.java index 5cd2945a7..a8c31f5ac 100644 --- a/dsf-fhir/dsf-fhir-server/src/main/java/dev/dsf/fhir/search/filter/TaskIdentityFilter.java +++ b/dsf-fhir/dsf-fhir-server/src/main/java/dev/dsf/fhir/search/filter/TaskIdentityFilter.java @@ -65,7 +65,7 @@ else if (identity instanceof PractitionerIdentity p) { if (p.hasPractionerRole("DSF_ADMIN")) return resourceColumn + "->'restriction'->'recipient' @> ?::jsonb"; - else if (p.getPractitionerIdentifierValue().isPresent()) + else if (p.getPractitionerIdentifierValue() != null) { return "((" + resourceColumn + "->'requester'->'identifier'->>'system' = '" + PractitionerIdentity.PRACTITIONER_IDENTIFIER_SYSTEM + "' AND " + resourceColumn @@ -93,7 +93,7 @@ else if (identity instanceof PractitionerIdentity p) { if (p.hasPractionerRole("DSF_ADMIN")) return 1; - else if (p.getPractitionerIdentifierValue().isPresent()) + else if (p.getPractitionerIdentifierValue() != null) return 2; else return 1; @@ -129,10 +129,10 @@ else if (identity instanceof PractitionerIdentity p) statement.setString(parameterIndex, "[{\"reference\": \"" + identity.getOrganization().getIdElement().toUnqualifiedVersionless().getValue() + "\"}]"); } - else if (p.getPractitionerIdentifierValue().isPresent()) + else if (p.getPractitionerIdentifierValue() != null) { if (subqueryParameterIndex == 1) - statement.setString(parameterIndex, p.getPractitionerIdentifierValue().get()); + statement.setString(parameterIndex, p.getPractitionerIdentifierValue()); else if (subqueryParameterIndex == 2) { statement.setString(parameterIndex, "[{\"reference\": \"" diff --git a/dsf-fhir/dsf-fhir-server/src/test/java/dev/dsf/fhir/authentication/IdentityProviderTest.java b/dsf-fhir/dsf-fhir-server/src/test/java/dev/dsf/fhir/authentication/IdentityProviderTest.java index 5bb709afe..d134e64fa 100644 --- a/dsf-fhir/dsf-fhir-server/src/test/java/dev/dsf/fhir/authentication/IdentityProviderTest.java +++ b/dsf-fhir/dsf-fhir-server/src/test/java/dev/dsf/fhir/authentication/IdentityProviderTest.java @@ -223,7 +223,7 @@ public void testGetOrganizationIdentityByX509CertificateLocalOrganization() thro assertEquals(FhirServerRoleImpl.LOCAL_ORGANIZATION, orgI.getDsfRoles()); assertEquals(LOCAL_ORGANIZATION_IDENTIFIER_VALUE, orgI.getName()); assertEquals(LOCAL_ORGANIZATION, orgI.getOrganization()); - assertEquals(LOCAL_ORGANIZATION_IDENTIFIER_VALUE, orgI.getOrganizationIdentifierValue().get()); + assertEquals(LOCAL_ORGANIZATION_IDENTIFIER_VALUE, orgI.getOrganizationIdentifierValue()); ArgumentCaptor cArg1 = ArgumentCaptor.forClass(String.class); verify(organizationProvider).getOrganization(cArg1.capture()); @@ -255,7 +255,7 @@ public void testGetOrganizationIdentityByX509CertificateRemoteOrganization() thr assertEquals(FhirServerRoleImpl.REMOTE_ORGANIZATION, orgI.getDsfRoles()); assertEquals(REMOTE_ORGANIZATION_IDENTIFIER_VALUE, orgI.getName()); assertEquals(REMOTE_ORGANIZATION, orgI.getOrganization()); - assertEquals(REMOTE_ORGANIZATION_IDENTIFIER_VALUE, orgI.getOrganizationIdentifierValue().get()); + assertEquals(REMOTE_ORGANIZATION_IDENTIFIER_VALUE, orgI.getOrganizationIdentifierValue()); ArgumentCaptor getOrgArg1 = ArgumentCaptor.forClass(String.class); verify(organizationProvider).getOrganization(getOrgArg1.capture()); @@ -324,7 +324,7 @@ public void testGetPractitionerIdentityByX509Certificate() throws Exception Operation.DELETE.toFhirServerRoleAllResources()), practitionerI.getDsfRoles()); assertEquals(LOCAL_ORGANIZATION_IDENTIFIER_VALUE + "/" + LOCAL_PRACTITIONER_MAIL, practitionerI.getName()); assertEquals(LOCAL_ORGANIZATION, practitionerI.getOrganization()); - assertEquals(LOCAL_ORGANIZATION_IDENTIFIER_VALUE, practitionerI.getOrganizationIdentifierValue().get()); + assertEquals(LOCAL_ORGANIZATION_IDENTIFIER_VALUE, practitionerI.getOrganizationIdentifierValue()); assertEquals(Set.of(PRACTIONER_ROLE1, PRACTIONER_ROLE2), practitionerI.getPractionerRoles()); assertNotNull(practitionerI.getPractitioner()); @@ -432,7 +432,7 @@ public void testGetPractitionerIdentityByOpenIdCredentials() throws Exception new FhirServerRoleImpl(Operation.PERMANENT_DELETE, List.of())), practitionerI.getDsfRoles()); assertEquals(LOCAL_ORGANIZATION_IDENTIFIER_VALUE + "/" + LOCAL_PRACTITIONER_MAIL, practitionerI.getName()); assertEquals(LOCAL_ORGANIZATION, practitionerI.getOrganization()); - assertEquals(LOCAL_ORGANIZATION_IDENTIFIER_VALUE, practitionerI.getOrganizationIdentifierValue().get()); + assertEquals(LOCAL_ORGANIZATION_IDENTIFIER_VALUE, practitionerI.getOrganizationIdentifierValue()); assertNotNull(practitionerI.getPractionerRoles()); Set expectedPractitionerRoles = Set.of(PRACTIONER_ROLE1, PRACTIONER_ROLE2, PRACTIONER_ROLE3, diff --git a/dsf-fhir/dsf-fhir-server/src/test/java/dev/dsf/fhir/authorization/process/TestOrganizationIdentity.java b/dsf-fhir/dsf-fhir-server/src/test/java/dev/dsf/fhir/authorization/process/TestOrganizationIdentity.java index 0c3d7e615..cf31271fc 100644 --- a/dsf-fhir/dsf-fhir-server/src/test/java/dev/dsf/fhir/authorization/process/TestOrganizationIdentity.java +++ b/dsf-fhir/dsf-fhir-server/src/test/java/dev/dsf/fhir/authorization/process/TestOrganizationIdentity.java @@ -72,7 +72,7 @@ public Organization getOrganization() } @Override - public Optional getOrganizationIdentifierValue() + public String getOrganizationIdentifierValue() { throw new UnsupportedOperationException(); } diff --git a/dsf-fhir/dsf-fhir-server/src/test/java/dev/dsf/fhir/authorization/process/TestPractitionerIdentity.java b/dsf-fhir/dsf-fhir-server/src/test/java/dev/dsf/fhir/authorization/process/TestPractitionerIdentity.java index 6ace10d12..0de228cca 100644 --- a/dsf-fhir/dsf-fhir-server/src/test/java/dev/dsf/fhir/authorization/process/TestPractitionerIdentity.java +++ b/dsf-fhir/dsf-fhir-server/src/test/java/dev/dsf/fhir/authorization/process/TestPractitionerIdentity.java @@ -74,7 +74,7 @@ public Organization getOrganization() } @Override - public Optional getOrganizationIdentifierValue() + public String getOrganizationIdentifierValue() { throw new UnsupportedOperationException(); } @@ -129,8 +129,8 @@ public Optional getEndpointIdentifierValue() } @Override - public Optional getPractitionerIdentifierValue() + public String getPractitionerIdentifierValue() { - return Optional.empty(); + throw new UnsupportedOperationException(); } } From 5cc00611b28ab97f5ab293da8f6999a2459a1e02 Mon Sep 17 00:00:00 2001 From: Hauke Hund Date: Mon, 23 Mar 2026 20:25:02 +0100 Subject: [PATCH 07/18] improved session cookie config --- .../java/dev/dsf/common/config/AbstractJettyConfig.java | 8 ++++++++ 1 file changed, 8 insertions(+) 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 a3dbdce8b..972d96fa0 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 @@ -42,6 +42,7 @@ import org.eclipse.jetty.client.transport.HttpClientTransportOverHTTP; import org.eclipse.jetty.ee11.servlet.SessionHandler; import org.eclipse.jetty.ee11.webapp.WebAppContext; +import org.eclipse.jetty.http.HttpCookie.SameSite; import org.eclipse.jetty.http.HttpField; import org.eclipse.jetty.http.HttpHeader; import org.eclipse.jetty.io.ClientConnector; @@ -91,6 +92,7 @@ import dev.dsf.common.oidc.JwtVerifier; import dev.dsf.common.oidc.JwtVerifierImpl; import jakarta.servlet.ServletContainerInitializer; +import jakarta.servlet.SessionCookieConfig; @Configuration @PropertySource(value = "file:conf/jetty.properties", encoding = "UTF-8", ignoreResourceNotFound = true) @@ -315,6 +317,12 @@ private KeyStore clientCertificateTrustStore() private void configureSecurityHandler(WebAppContext webAppContext, Supplier statusPortSupplier) { SessionHandler sessionHandler = webAppContext.getSessionHandler(); + sessionHandler.setSameSite(SameSite.LAX); + + SessionCookieConfig sessionCookieConfig = sessionHandler.getSessionCookieConfig(); + sessionCookieConfig.setSecure(true); + sessionCookieConfig.setHttpOnly(true); + DsfLoginService dsfLoginService = new DsfLoginService(webAppContext); OpenIdConfiguration openIdConfiguration = null; From 37c0282c6eeb3781ecccacfb80bb5d6fea71aadb Mon Sep 17 00:00:00 2001 From: Hauke Hund Date: Mon, 23 Mar 2026 20:29:49 +0100 Subject: [PATCH 08/18] refactored db code to generate query json parameters via objectmapper --- .../AbstractPreparedStatementFactory.java | 64 +------- .../dao/jdbc/AbstractResourceDaoJdbc.java | 12 +- .../AbstractStructureDefinitionDaoJdbc.java | 10 +- .../dao/jdbc/ActivityDefinitionDaoJdbc.java | 9 +- .../dev/dsf/fhir/dao/jdbc/BinaryDaoJdbc.java | 6 +- .../dev/dsf/fhir/dao/jdbc/BundleDaoJdbc.java | 11 +- .../dsf/fhir/dao/jdbc/CodeSystemDaoJdbc.java | 9 +- .../dao/jdbc/DocumentReferenceDaoJdbc.java | 12 +- .../dsf/fhir/dao/jdbc/EndpointDaoJdbc.java | 15 +- .../dev/dsf/fhir/dao/jdbc/GroupDaoJdbc.java | 9 +- .../dao/jdbc/HealthcareServiceDaoJdbc.java | 9 +- .../dev/dsf/fhir/dao/jdbc/HistroyDaoJdbc.java | 46 ++---- .../fhir/dao/jdbc/LargeObjectManagerJdbc.java | 12 +- .../dev/dsf/fhir/dao/jdbc/LibraryDaoJdbc.java | 9 +- .../dsf/fhir/dao/jdbc/LocationDaoJdbc.java | 7 +- .../dev/dsf/fhir/dao/jdbc/MeasureDaoJdbc.java | 9 +- .../fhir/dao/jdbc/MeasureReportDaoJdbc.java | 7 +- .../fhir/dao/jdbc/NamingSystemDaoJdbc.java | 27 +-- .../jdbc/OrganizationAffiliationDaoJdbc.java | 25 +-- .../fhir/dao/jdbc/OrganizationDaoJdbc.java | 32 ++-- .../dev/dsf/fhir/dao/jdbc/PatientDaoJdbc.java | 9 +- .../dsf/fhir/dao/jdbc/PgObjectFactory.java | 84 ++++++++++ .../fhir/dao/jdbc/PgObjectFactoryImpl.java | 131 +++++++++++++++ .../fhir/dao/jdbc/PractitionerDaoJdbc.java | 9 +- .../dao/jdbc/PractitionerRoleDaoJdbc.java | 9 +- .../dao/jdbc/PreparedStatementFactory.java | 11 +- .../jdbc/PreparedStatementFactoryBinary.java | 6 +- .../jdbc/PreparedStatementFactoryDefault.java | 8 +- .../dsf/fhir/dao/jdbc/ProvenanceDaoJdbc.java | 9 +- .../fhir/dao/jdbc/QuestionnaireDaoJdbc.java | 7 +- .../jdbc/QuestionnaireResponseDaoJdbc.java | 6 +- .../fhir/dao/jdbc/ResearchStudyDaoJdbc.java | 7 +- .../dao/jdbc/StructureDefinitionDaoJdbc.java | 8 +- .../StructureDefinitionSnapshotDaoJdbc.java | 6 +- .../fhir/dao/jdbc/SubscriptionDaoJdbc.java | 9 +- .../dev/dsf/fhir/dao/jdbc/TaskDaoJdbc.java | 7 +- .../dsf/fhir/dao/jdbc/ValueSetDaoJdbc.java | 9 +- .../java/dev/dsf/fhir/search/SearchQuery.java | 29 ++-- .../search/SearchQueryIdentityFilter.java | 8 +- .../dsf/fhir/search/SearchQueryParameter.java | 4 +- ...etaTagAuthorizationRoleIdentityFilter.java | 5 +- .../QuestionnaireResponseIdentityFilter.java | 21 +-- .../search/filter/TaskIdentityFilter.java | 35 ++-- .../search/parameters/BinaryContentType.java | 4 +- .../DocumentReferenceIdentifier.java | 49 +++--- .../search/parameters/EndpointAddress.java | 4 +- .../parameters/EndpointOrganization.java | 49 +++--- .../search/parameters/EndpointStatus.java | 4 +- .../search/parameters/MeasureDependsOn.java | 11 +- .../OrganizationAffiliationEndpoint.java | 49 +++--- ...nAffiliationParticipatingOrganization.java | 49 +++--- ...izationAffiliationPrimaryOrganization.java | 49 +++--- .../OrganizationAffiliationRole.java | 37 ++--- .../parameters/OrganizationEndpoint.java | 49 +++--- .../search/parameters/OrganizationType.java | 37 ++--- .../PractitionerRoleOrganization.java | 49 +++--- .../PractitionerRolePractitioner.java | 49 +++--- .../QuestionnaireResponseAuthor.java | 36 ++-- .../QuestionnaireResponseQuestionnaire.java | 4 +- .../QuestionnaireResponseStatus.java | 4 +- .../QuestionnaireResponseSubject.java | 57 +++---- .../parameters/ResearchStudyEnrollment.java | 49 +++--- .../ResearchStudyPrincipalInvestigator.java | 57 +++---- .../fhir/search/parameters/ResourceId.java | 4 +- .../search/parameters/ResourceProfile.java | 4 +- .../parameters/SubscriptionCriteria.java | 4 +- .../parameters/SubscriptionPayload.java | 4 +- .../search/parameters/SubscriptionStatus.java | 4 +- .../search/parameters/SubscriptionType.java | 4 +- .../fhir/search/parameters/TaskRequester.java | 32 ++-- .../fhir/search/parameters/TaskStatus.java | 4 +- .../basic/AbstractActiveParameter.java | 4 +- .../basic/AbstractDateTimeParameter.java | 4 +- .../basic/AbstractIdentifierParameter.java | 34 ++-- .../basic/AbstractNameOrAliasParameter.java | 4 +- .../basic/AbstractNameParameter.java | 4 +- .../AbstractSingleIdentifierParameter.java | 29 ++-- .../basic/AbstractStatusParameter.java | 4 +- .../basic/AbstractUrlAndVersionParameter.java | 4 +- .../basic/AbstractVersionParameter.java | 4 +- .../dev/dsf/fhir/spring/config/DaoConfig.java | 86 ++++++---- .../fhir/dao/AbstractReadAccessDaoTest.java | 95 ++++++----- .../dsf/fhir/dao/AbstractResourceDaoTest.java | 21 ++- .../java/dev/dsf/fhir/dao/BinaryDaoTest.java | 155 ++++++++++-------- .../java/dev/dsf/fhir/dao/HistoryDaoTest.java | 18 +- .../dao/OrganizationAffiliationDaoTest.java | 22 ++- .../dev/dsf/fhir/dao/OrganizationDaoTest.java | 107 +++++++++++- 87 files changed, 1295 insertions(+), 833 deletions(-) create mode 100644 dsf-fhir/dsf-fhir-server/src/main/java/dev/dsf/fhir/dao/jdbc/PgObjectFactory.java create mode 100644 dsf-fhir/dsf-fhir-server/src/main/java/dev/dsf/fhir/dao/jdbc/PgObjectFactoryImpl.java diff --git a/dsf-fhir/dsf-fhir-server/src/main/java/dev/dsf/fhir/dao/jdbc/AbstractPreparedStatementFactory.java b/dsf-fhir/dsf-fhir-server/src/main/java/dev/dsf/fhir/dao/jdbc/AbstractPreparedStatementFactory.java index 1c6b1fc3f..d413b2e68 100755 --- a/dsf-fhir/dsf-fhir-server/src/main/java/dev/dsf/fhir/dao/jdbc/AbstractPreparedStatementFactory.java +++ b/dsf-fhir/dsf-fhir-server/src/main/java/dev/dsf/fhir/dao/jdbc/AbstractPreparedStatementFactory.java @@ -15,20 +15,17 @@ */ package dev.dsf.fhir.dao.jdbc; -import java.sql.SQLException; import java.util.Objects; -import java.util.UUID; import org.hl7.fhir.r4.model.Resource; -import org.postgresql.util.PGobject; + +import com.fasterxml.jackson.databind.ObjectMapper; import ca.uhn.fhir.context.FhirContext; -import ca.uhn.fhir.parser.DataFormatException; -import ca.uhn.fhir.parser.IParser; -abstract class AbstractPreparedStatementFactory implements PreparedStatementFactory +abstract class AbstractPreparedStatementFactory extends PgObjectFactoryImpl + implements PreparedStatementFactory { - private final FhirContext fhirContext; private final Class resourceType; private final String createSql; @@ -36,10 +33,11 @@ abstract class AbstractPreparedStatementFactory implements P private final String readByIdAndVersionSql; private final String updateSql; - protected AbstractPreparedStatementFactory(FhirContext fhirContext, Class resourceType, String createSql, - String readByIdSql, String readByIdAndVersionSql, String updateSql) + protected AbstractPreparedStatementFactory(FhirContext fhirContext, ObjectMapper objectMapper, + Class resourceType, String createSql, String readByIdSql, String readByIdAndVersionSql, String updateSql) { - this.fhirContext = Objects.requireNonNull(fhirContext, "fhirContext"); + super(fhirContext, objectMapper); + this.resourceType = Objects.requireNonNull(resourceType, "resourceType"); this.createSql = Objects.requireNonNull(createSql, "createSql"); this.readByIdSql = Objects.requireNonNull(readByIdSql, "readByIdSql"); @@ -47,57 +45,11 @@ protected AbstractPreparedStatementFactory(FhirContext fhirContext, Class res this.updateSql = Objects.requireNonNull(updateSql, "updateSql"); } - @Override - public IParser getJsonParser() - { - IParser p = fhirContext.newJsonParser(); - p.setStripVersionsFromReferences(false); - return p; - } - protected final R jsonToResource(String json) { return getJsonParser().parseResource(resourceType, json); } - @Override - public final PGobject resourceToPgObject(R resource) - { - if (resource == null) - return null; - - try - { - PGobject o = new PGobject(); - o.setType("JSONB"); - o.setValue(getJsonParser().encodeResourceToString(resource)); - return o; - } - catch (DataFormatException | SQLException e) - { - throw new RuntimeException(e); - } - } - - @Override - public final PGobject uuidToPgObject(UUID uuid) - { - if (uuid == null) - return null; - - try - { - PGobject o = new PGobject(); - o.setType("UUID"); - o.setValue(uuid.toString()); - return o; - } - catch (DataFormatException | SQLException e) - { - throw new RuntimeException(e); - } - } - @Override public final String getCreateSql() { diff --git a/dsf-fhir/dsf-fhir-server/src/main/java/dev/dsf/fhir/dao/jdbc/AbstractResourceDaoJdbc.java b/dsf-fhir/dsf-fhir-server/src/main/java/dev/dsf/fhir/dao/jdbc/AbstractResourceDaoJdbc.java index 75424714e..3f5b3f150 100755 --- a/dsf-fhir/dsf-fhir-server/src/main/java/dev/dsf/fhir/dao/jdbc/AbstractResourceDaoJdbc.java +++ b/dsf-fhir/dsf-fhir-server/src/main/java/dev/dsf/fhir/dao/jdbc/AbstractResourceDaoJdbc.java @@ -42,6 +42,7 @@ import org.slf4j.LoggerFactory; import org.springframework.beans.factory.InitializingBean; +import com.fasterxml.jackson.databind.ObjectMapper; import com.google.gson.JsonArray; import com.google.gson.JsonElement; import com.google.gson.JsonParser; @@ -168,14 +169,14 @@ protected static SearchQueryParameterFactory factory(Str } AbstractResourceDaoJdbc(DataSource dataSource, DataSource permanentDeleteDataSource, FhirContext fhirContext, - Class resourceType, String resourceTable, String resourceColumn, String resourceIdColumn, - Function userFilter, + ObjectMapper objectMapper, Class resourceType, String resourceTable, String resourceColumn, + String resourceIdColumn, Function userFilter, List> searchParameterFactories, List searchRevIncludeParameterFactories) { this(dataSource, permanentDeleteDataSource, resourceType, resourceTable, resourceColumn, resourceIdColumn, - new PreparedStatementFactoryDefault<>(fhirContext, resourceType, resourceTable, resourceIdColumn, - resourceColumn), + new PreparedStatementFactoryDefault<>(fhirContext, objectMapper, resourceType, resourceTable, + resourceIdColumn, resourceColumn), userFilter, searchParameterFactories, searchRevIncludeParameterFactories); } @@ -971,7 +972,8 @@ private SearchQuery doCreateSearchQuery(Identity identity, PageAndCount pageA { Objects.requireNonNull(pageAndCount, "pageAndCount"); - var builder = SearchQueryBuilder.create(resourceType, getResourceTable(), getResourceColumn(), pageAndCount); + var builder = SearchQueryBuilder.create(preparedStatementFactory, resourceType, getResourceTable(), + getResourceColumn(), pageAndCount); if (identity != null) builder = builder.with(identityFilter.apply(identity)); diff --git a/dsf-fhir/dsf-fhir-server/src/main/java/dev/dsf/fhir/dao/jdbc/AbstractStructureDefinitionDaoJdbc.java b/dsf-fhir/dsf-fhir-server/src/main/java/dev/dsf/fhir/dao/jdbc/AbstractStructureDefinitionDaoJdbc.java index 349986c0b..1c745cf49 100755 --- a/dsf-fhir/dsf-fhir-server/src/main/java/dev/dsf/fhir/dao/jdbc/AbstractStructureDefinitionDaoJdbc.java +++ b/dsf-fhir/dsf-fhir-server/src/main/java/dev/dsf/fhir/dao/jdbc/AbstractStructureDefinitionDaoJdbc.java @@ -30,6 +30,8 @@ import org.hl7.fhir.r4.model.Resource; import org.hl7.fhir.r4.model.StructureDefinition; +import com.fasterxml.jackson.databind.ObjectMapper; + import ca.uhn.fhir.context.FhirContext; import dev.dsf.common.auth.conf.Identity; import dev.dsf.fhir.dao.StructureDefinitionDao; @@ -62,11 +64,11 @@ private static SearchQueryParameterFactory factory(Strin private final String readByBaseDefinition; protected AbstractStructureDefinitionDaoJdbc(DataSource dataSource, DataSource permanentDeleteDataSource, - FhirContext fhirContext, String resourceTable, String resourceColumn, String resourceIdColumn, - Function userFilter) + FhirContext fhirContext, ObjectMapper objectMapper, String resourceTable, String resourceColumn, + String resourceIdColumn, Function userFilter) { - super(dataSource, permanentDeleteDataSource, fhirContext, StructureDefinition.class, resourceTable, - resourceColumn, resourceIdColumn, userFilter, + super(dataSource, permanentDeleteDataSource, fhirContext, objectMapper, StructureDefinition.class, + resourceTable, resourceColumn, resourceIdColumn, userFilter, List.of(factory(resourceColumn, StructureDefinitionDate.PARAMETER_NAME, StructureDefinitionDate::new), factory(resourceColumn, StructureDefinitionIdentifier.PARAMETER_NAME, StructureDefinitionIdentifier::new, StructureDefinitionIdentifier.getNameModifiers()), diff --git a/dsf-fhir/dsf-fhir-server/src/main/java/dev/dsf/fhir/dao/jdbc/ActivityDefinitionDaoJdbc.java b/dsf-fhir/dsf-fhir-server/src/main/java/dev/dsf/fhir/dao/jdbc/ActivityDefinitionDaoJdbc.java index 526180444..f5b37c139 100644 --- a/dsf-fhir/dsf-fhir-server/src/main/java/dev/dsf/fhir/dao/jdbc/ActivityDefinitionDaoJdbc.java +++ b/dsf-fhir/dsf-fhir-server/src/main/java/dev/dsf/fhir/dao/jdbc/ActivityDefinitionDaoJdbc.java @@ -30,6 +30,8 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import com.fasterxml.jackson.databind.ObjectMapper; + import ca.uhn.fhir.context.FhirContext; import dev.dsf.fhir.dao.ActivityDefinitionDao; import dev.dsf.fhir.search.filter.ActivityDefinitionIdentityFilter; @@ -48,10 +50,11 @@ public class ActivityDefinitionDaoJdbc extends AbstractResourceDaoJdbc readByUrl; public ActivityDefinitionDaoJdbc(DataSource dataSource, DataSource permanentDeleteDataSource, - FhirContext fhirContext) + FhirContext fhirContext, ObjectMapper objectMapper) { - super(dataSource, permanentDeleteDataSource, fhirContext, ActivityDefinition.class, "activity_definitions", - "activity_definition", "activity_definition_id", ActivityDefinitionIdentityFilter::new, + super(dataSource, permanentDeleteDataSource, fhirContext, objectMapper, ActivityDefinition.class, + "activity_definitions", "activity_definition", "activity_definition_id", + ActivityDefinitionIdentityFilter::new, List.of(factory(ActivityDefinitionDate.PARAMETER_NAME, ActivityDefinitionDate::new), factory(ActivityDefinitionIdentifier.PARAMETER_NAME, ActivityDefinitionIdentifier::new), factory(ActivityDefinitionName.PARAMETER_NAME, ActivityDefinitionName::new, diff --git a/dsf-fhir/dsf-fhir-server/src/main/java/dev/dsf/fhir/dao/jdbc/BinaryDaoJdbc.java b/dsf-fhir/dsf-fhir-server/src/main/java/dev/dsf/fhir/dao/jdbc/BinaryDaoJdbc.java index f98aa6fc3..adbccb3ed 100755 --- a/dsf-fhir/dsf-fhir-server/src/main/java/dev/dsf/fhir/dao/jdbc/BinaryDaoJdbc.java +++ b/dsf-fhir/dsf-fhir-server/src/main/java/dev/dsf/fhir/dao/jdbc/BinaryDaoJdbc.java @@ -35,6 +35,8 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import com.fasterxml.jackson.databind.ObjectMapper; + import ca.uhn.fhir.context.FhirContext; import dev.dsf.fhir.dao.BinaryDao; import dev.dsf.fhir.dao.exception.ResourceDeletedException; @@ -55,10 +57,10 @@ public class BinaryDaoJdbc extends AbstractResourceDaoJdbc implements Bi private final ExecutorService loUnlinker; public BinaryDaoJdbc(DataSource dataSource, DataSource permanentDeleteDataSource, FhirContext fhirContext, - String selectUpdateUser) + ObjectMapper objectMapper, String selectUpdateUser) { super(dataSource, permanentDeleteDataSource, Binary.class, "binaries", "binary_json", "binary_id", - new PreparedStatementFactoryBinary(fhirContext), BinaryIdentityFilter::new, + new PreparedStatementFactoryBinary(fhirContext, objectMapper), BinaryIdentityFilter::new, List.of(factory(BinaryContentType.PARAMETER_NAME, BinaryContentType::new, BinaryContentType.getNameModifiers())), List.of()); diff --git a/dsf-fhir/dsf-fhir-server/src/main/java/dev/dsf/fhir/dao/jdbc/BundleDaoJdbc.java b/dsf-fhir/dsf-fhir-server/src/main/java/dev/dsf/fhir/dao/jdbc/BundleDaoJdbc.java index 3ff1a4c1d..33a41ac36 100755 --- a/dsf-fhir/dsf-fhir-server/src/main/java/dev/dsf/fhir/dao/jdbc/BundleDaoJdbc.java +++ b/dsf-fhir/dsf-fhir-server/src/main/java/dev/dsf/fhir/dao/jdbc/BundleDaoJdbc.java @@ -21,6 +21,8 @@ import org.hl7.fhir.r4.model.Bundle; +import com.fasterxml.jackson.databind.ObjectMapper; + import ca.uhn.fhir.context.FhirContext; import dev.dsf.fhir.dao.BundleDao; import dev.dsf.fhir.search.filter.BundleIdentityFilter; @@ -28,11 +30,12 @@ public class BundleDaoJdbc extends AbstractResourceDaoJdbc implements BundleDao { - public BundleDaoJdbc(DataSource dataSource, DataSource permanentDeleteDataSource, FhirContext fhirContext) + public BundleDaoJdbc(DataSource dataSource, DataSource permanentDeleteDataSource, FhirContext fhirContext, + ObjectMapper objectMapper) { - super(dataSource, permanentDeleteDataSource, fhirContext, Bundle.class, "bundles", "bundle", "bundle_id", - BundleIdentityFilter::new, List.of(factory(BundleIdentifier.PARAMETER_NAME, BundleIdentifier::new, - BundleIdentifier.getNameModifiers())), + super(dataSource, permanentDeleteDataSource, fhirContext, objectMapper, Bundle.class, "bundles", "bundle", + "bundle_id", BundleIdentityFilter::new, List.of(factory(BundleIdentifier.PARAMETER_NAME, + BundleIdentifier::new, BundleIdentifier.getNameModifiers())), List.of()); } diff --git a/dsf-fhir/dsf-fhir-server/src/main/java/dev/dsf/fhir/dao/jdbc/CodeSystemDaoJdbc.java b/dsf-fhir/dsf-fhir-server/src/main/java/dev/dsf/fhir/dao/jdbc/CodeSystemDaoJdbc.java index b1c48def0..ea136523b 100755 --- a/dsf-fhir/dsf-fhir-server/src/main/java/dev/dsf/fhir/dao/jdbc/CodeSystemDaoJdbc.java +++ b/dsf-fhir/dsf-fhir-server/src/main/java/dev/dsf/fhir/dao/jdbc/CodeSystemDaoJdbc.java @@ -24,6 +24,8 @@ import org.hl7.fhir.r4.model.CodeSystem; +import com.fasterxml.jackson.databind.ObjectMapper; + import ca.uhn.fhir.context.FhirContext; import dev.dsf.fhir.dao.CodeSystemDao; import dev.dsf.fhir.search.filter.CodeSystemIdentityFilter; @@ -38,10 +40,11 @@ public class CodeSystemDaoJdbc extends AbstractResourceDaoJdbc imple { private final ReadByUrlDaoJdbc readByUrl; - public CodeSystemDaoJdbc(DataSource dataSource, DataSource permanentDeleteDataSource, FhirContext fhirContext) + public CodeSystemDaoJdbc(DataSource dataSource, DataSource permanentDeleteDataSource, FhirContext fhirContext, + ObjectMapper objectMapper) { - super(dataSource, permanentDeleteDataSource, fhirContext, CodeSystem.class, "code_systems", "code_system", - "code_system_id", CodeSystemIdentityFilter::new, + super(dataSource, permanentDeleteDataSource, fhirContext, objectMapper, CodeSystem.class, "code_systems", + "code_system", "code_system_id", CodeSystemIdentityFilter::new, List.of(factory(CodeSystemDate.PARAMETER_NAME, CodeSystemDate::new), factory(CodeSystemIdentifier.PARAMETER_NAME, CodeSystemIdentifier::new, CodeSystemIdentifier.getNameModifiers()), diff --git a/dsf-fhir/dsf-fhir-server/src/main/java/dev/dsf/fhir/dao/jdbc/DocumentReferenceDaoJdbc.java b/dsf-fhir/dsf-fhir-server/src/main/java/dev/dsf/fhir/dao/jdbc/DocumentReferenceDaoJdbc.java index 03dc29d8c..2e679a94d 100644 --- a/dsf-fhir/dsf-fhir-server/src/main/java/dev/dsf/fhir/dao/jdbc/DocumentReferenceDaoJdbc.java +++ b/dsf-fhir/dsf-fhir-server/src/main/java/dev/dsf/fhir/dao/jdbc/DocumentReferenceDaoJdbc.java @@ -21,6 +21,8 @@ import org.hl7.fhir.r4.model.DocumentReference; +import com.fasterxml.jackson.databind.ObjectMapper; + import ca.uhn.fhir.context.FhirContext; import dev.dsf.fhir.dao.DocumentReferenceDao; import dev.dsf.fhir.search.filter.DocumentReferenceIdentityFilter; @@ -29,12 +31,12 @@ public class DocumentReferenceDaoJdbc extends AbstractResourceDaoJdbc implements DocumentReferenceDao { public DocumentReferenceDaoJdbc(DataSource dataSource, DataSource permanentDeleteDataSource, - FhirContext fhirContext) + FhirContext fhirContext, ObjectMapper objectMapper) { - super(dataSource, permanentDeleteDataSource, fhirContext, DocumentReference.class, "document_references", - "document_reference", "document_reference_id", DocumentReferenceIdentityFilter::new, - List.of(factory(DocumentReferenceIdentifier.PARAMETER_NAME, DocumentReferenceIdentifier::new, - DocumentReferenceIdentifier.getNameModifiers())), + super(dataSource, permanentDeleteDataSource, fhirContext, objectMapper, DocumentReference.class, + "document_references", "document_reference", "document_reference_id", + DocumentReferenceIdentityFilter::new, List.of(factory(DocumentReferenceIdentifier.PARAMETER_NAME, + DocumentReferenceIdentifier::new, DocumentReferenceIdentifier.getNameModifiers())), List.of()); } diff --git a/dsf-fhir/dsf-fhir-server/src/main/java/dev/dsf/fhir/dao/jdbc/EndpointDaoJdbc.java b/dsf-fhir/dsf-fhir-server/src/main/java/dev/dsf/fhir/dao/jdbc/EndpointDaoJdbc.java index f3d9da0d4..9c6745930 100755 --- a/dsf-fhir/dsf-fhir-server/src/main/java/dev/dsf/fhir/dao/jdbc/EndpointDaoJdbc.java +++ b/dsf-fhir/dsf-fhir-server/src/main/java/dev/dsf/fhir/dao/jdbc/EndpointDaoJdbc.java @@ -28,8 +28,11 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import com.fasterxml.jackson.databind.ObjectMapper; + import ca.uhn.fhir.context.FhirContext; import dev.dsf.fhir.dao.EndpointDao; +import dev.dsf.fhir.dao.jdbc.PgObjectFactory.ExtensionParameterValueString; import dev.dsf.fhir.search.filter.EndpointIdentityFilter; import dev.dsf.fhir.search.parameters.EndpointAddress; import dev.dsf.fhir.search.parameters.EndpointIdentifier; @@ -42,9 +45,10 @@ public class EndpointDaoJdbc extends AbstractResourceDaoJdbc implement { private static final Logger logger = LoggerFactory.getLogger(EndpointDaoJdbc.class); - public EndpointDaoJdbc(DataSource dataSource, DataSource permanentDeleteDataSource, FhirContext fhirContext) + public EndpointDaoJdbc(DataSource dataSource, DataSource permanentDeleteDataSource, FhirContext fhirContext, + ObjectMapper objectMapper) { - super(dataSource, permanentDeleteDataSource, fhirContext, Endpoint.class, "endpoints", "endpoint", + super(dataSource, permanentDeleteDataSource, fhirContext, objectMapper, Endpoint.class, "endpoints", "endpoint", "endpoint_id", EndpointIdentityFilter::new, List.of(factory(EndpointAddress.PARAMETER_NAME, EndpointAddress::new, EndpointAddress.getNameModifiers()), @@ -131,11 +135,10 @@ public Optional readActiveNotDeletedByThumbprint(String thumbprintHex) try (Connection connection = getDataSource().getConnection(); PreparedStatement statement = connection.prepareStatement( - "SELECT endpoint FROM current_endpoints WHERE endpoint->'extension' @> ?::jsonb AND endpoint->>'status' = 'active'")) + "SELECT endpoint FROM current_endpoints WHERE endpoint->'extension' @> ? AND endpoint->>'status' = 'active'")) { - String search = "[{\"url\": \"http://dsf.dev/fhir/StructureDefinition/extension-certificate-thumbprint\", \"valueString\": \"" - + thumbprintHex + "\"}]"; - statement.setString(1, search); + statement.setObject(1, getPreparedStatementFactory() + .jsonParameterToPgObjectAsArray(ExtensionParameterValueString.thumbprint(thumbprintHex))); try (ResultSet result = statement.executeQuery()) { diff --git a/dsf-fhir/dsf-fhir-server/src/main/java/dev/dsf/fhir/dao/jdbc/GroupDaoJdbc.java b/dsf-fhir/dsf-fhir-server/src/main/java/dev/dsf/fhir/dao/jdbc/GroupDaoJdbc.java index c89ac4a21..34f83040a 100755 --- a/dsf-fhir/dsf-fhir-server/src/main/java/dev/dsf/fhir/dao/jdbc/GroupDaoJdbc.java +++ b/dsf-fhir/dsf-fhir-server/src/main/java/dev/dsf/fhir/dao/jdbc/GroupDaoJdbc.java @@ -21,6 +21,8 @@ import org.hl7.fhir.r4.model.Group; +import com.fasterxml.jackson.databind.ObjectMapper; + import ca.uhn.fhir.context.FhirContext; import dev.dsf.fhir.dao.GroupDao; import dev.dsf.fhir.search.filter.GroupIdentityFilter; @@ -29,10 +31,11 @@ public class GroupDaoJdbc extends AbstractResourceDaoJdbc implements GroupDao { - public GroupDaoJdbc(DataSource dataSource, DataSource permanentDeleteDataSource, FhirContext fhirContext) + public GroupDaoJdbc(DataSource dataSource, DataSource permanentDeleteDataSource, FhirContext fhirContext, + ObjectMapper objectMapper) { - super(dataSource, permanentDeleteDataSource, fhirContext, Group.class, "groups", "group_json", "group_id", - GroupIdentityFilter::new, + super(dataSource, permanentDeleteDataSource, fhirContext, objectMapper, Group.class, "groups", "group_json", + "group_id", GroupIdentityFilter::new, List.of(factory(GroupIdentifier.PARAMETER_NAME, GroupIdentifier::new, GroupIdentifier.getNameModifiers())), List.of(factory(ResearchStudyEnrollmentRevInclude::new, diff --git a/dsf-fhir/dsf-fhir-server/src/main/java/dev/dsf/fhir/dao/jdbc/HealthcareServiceDaoJdbc.java b/dsf-fhir/dsf-fhir-server/src/main/java/dev/dsf/fhir/dao/jdbc/HealthcareServiceDaoJdbc.java index fcc076d4a..c1f6af857 100755 --- a/dsf-fhir/dsf-fhir-server/src/main/java/dev/dsf/fhir/dao/jdbc/HealthcareServiceDaoJdbc.java +++ b/dsf-fhir/dsf-fhir-server/src/main/java/dev/dsf/fhir/dao/jdbc/HealthcareServiceDaoJdbc.java @@ -21,6 +21,8 @@ import org.hl7.fhir.r4.model.HealthcareService; +import com.fasterxml.jackson.databind.ObjectMapper; + import ca.uhn.fhir.context.FhirContext; import dev.dsf.fhir.dao.HealthcareServiceDao; import dev.dsf.fhir.search.filter.HealthcareServiceIdentityFilter; @@ -31,10 +33,11 @@ public class HealthcareServiceDaoJdbc extends AbstractResourceDaoJdbc implements HealthcareServiceDao { public HealthcareServiceDaoJdbc(DataSource dataSource, DataSource permanentDeleteDataSource, - FhirContext fhirContext) + FhirContext fhirContext, ObjectMapper objectMapper) { - super(dataSource, permanentDeleteDataSource, fhirContext, HealthcareService.class, "healthcare_services", - "healthcare_service", "healthcare_service_id", HealthcareServiceIdentityFilter::new, + super(dataSource, permanentDeleteDataSource, fhirContext, objectMapper, HealthcareService.class, + "healthcare_services", "healthcare_service", "healthcare_service_id", + HealthcareServiceIdentityFilter::new, List.of(factory(HealthcareServiceActive.PARAMETER_NAME, HealthcareServiceActive::new), factory(HealthcareServiceName.PARAMETER_NAME, HealthcareServiceName::new, HealthcareServiceName.getNameModifiers()), diff --git a/dsf-fhir/dsf-fhir-server/src/main/java/dev/dsf/fhir/dao/jdbc/HistroyDaoJdbc.java b/dsf-fhir/dsf-fhir-server/src/main/java/dev/dsf/fhir/dao/jdbc/HistroyDaoJdbc.java index 275f0eee7..99915704c 100644 --- a/dsf-fhir/dsf-fhir-server/src/main/java/dev/dsf/fhir/dao/jdbc/HistroyDaoJdbc.java +++ b/dsf-fhir/dsf-fhir-server/src/main/java/dev/dsf/fhir/dao/jdbc/HistroyDaoJdbc.java @@ -31,13 +31,10 @@ import org.hl7.fhir.r4.model.Binary; import org.hl7.fhir.r4.model.Resource; -import org.postgresql.util.PGobject; import org.springframework.beans.factory.InitializingBean; import ca.uhn.fhir.context.FhirContext; import ca.uhn.fhir.model.api.annotation.ResourceDef; -import ca.uhn.fhir.parser.DataFormatException; -import ca.uhn.fhir.parser.IParser; import dev.dsf.fhir.dao.HistoryDao; import dev.dsf.fhir.history.AtParameter; import dev.dsf.fhir.history.History; @@ -52,12 +49,15 @@ public class HistroyDaoJdbc implements HistoryDao, InitializingBean private final DataSource dataSource; private final FhirContext fhirContext; private final BinaryDaoJdbc binaryDao; + private final PgObjectFactory pgObjectFactory; - public HistroyDaoJdbc(DataSource dataSource, FhirContext fhirContext, BinaryDaoJdbc binaryDao) + public HistroyDaoJdbc(DataSource dataSource, FhirContext fhirContext, BinaryDaoJdbc binaryDao, + PgObjectFactory pgObjectFactory) { this.dataSource = dataSource; this.fhirContext = fhirContext; this.binaryDao = binaryDao; + this.pgObjectFactory = pgObjectFactory; } @Override @@ -66,6 +66,7 @@ public void afterPropertiesSet() throws Exception Objects.requireNonNull(dataSource, "dataSource"); Objects.requireNonNull(fhirContext, "fhirContext"); Objects.requireNonNull(binaryDao, "binaryDao"); + Objects.requireNonNull(pgObjectFactory, "pgObjectFactory"); } @Override @@ -165,40 +166,15 @@ private void modifyResource(Resource resource, Connection connection) throws SQL binaryDao.modifySearchResultResource(b, connection); } - private PGobject uuidToPgObject(UUID uuid) - { - if (uuid == null) - return null; - - try - { - PGobject o = new PGobject(); - o.setType("UUID"); - o.setValue(uuid.toString()); - return o; - } - catch (DataFormatException | SQLException e) - { - throw new RuntimeException(e); - } - } - - public IParser getJsonParser() - { - IParser p = fhirContext.newJsonParser(); - p.setStripVersionsFromReferences(false); - return p; - } - private Resource jsonToResource(String json, Class resourceType) { if (json == null) return null; if (resourceType != null) - return getJsonParser().parseResource(resourceType, json); + return pgObjectFactory.getJsonParser().parseResource(resourceType, json); else - return (Resource) getJsonParser().parseResource(json); + return (Resource) pgObjectFactory.getJsonParser().parseResource(json); } private String createCountSql(boolean forId, boolean forResource, List filter, @@ -239,7 +215,7 @@ private void configureStatement(PreparedStatement statement, UUID id, Class implements { private final ReadByUrlDaoJdbc readByUrl; - public LibraryDaoJdbc(DataSource dataSource, DataSource permanentDeleteDataSource, FhirContext fhirContext) + public LibraryDaoJdbc(DataSource dataSource, DataSource permanentDeleteDataSource, FhirContext fhirContext, + ObjectMapper objectMapper) { - super(dataSource, permanentDeleteDataSource, fhirContext, Library.class, "libraries", "library", "library_id", - LibraryIdentityFilter::new, + super(dataSource, permanentDeleteDataSource, fhirContext, objectMapper, Library.class, "libraries", "library", + "library_id", LibraryIdentityFilter::new, List.of(factory(LibraryDate.PARAMETER_NAME, LibraryDate::new), factory(LibraryIdentifier.PARAMETER_NAME, LibraryIdentifier::new, LocationIdentifier.getNameModifiers()), diff --git a/dsf-fhir/dsf-fhir-server/src/main/java/dev/dsf/fhir/dao/jdbc/LocationDaoJdbc.java b/dsf-fhir/dsf-fhir-server/src/main/java/dev/dsf/fhir/dao/jdbc/LocationDaoJdbc.java index 7c4bc5b6d..b90e349cc 100755 --- a/dsf-fhir/dsf-fhir-server/src/main/java/dev/dsf/fhir/dao/jdbc/LocationDaoJdbc.java +++ b/dsf-fhir/dsf-fhir-server/src/main/java/dev/dsf/fhir/dao/jdbc/LocationDaoJdbc.java @@ -21,6 +21,8 @@ import org.hl7.fhir.r4.model.Location; +import com.fasterxml.jackson.databind.ObjectMapper; + import ca.uhn.fhir.context.FhirContext; import dev.dsf.fhir.dao.LocationDao; import dev.dsf.fhir.search.filter.LocationIdentityFilter; @@ -29,9 +31,10 @@ public class LocationDaoJdbc extends AbstractResourceDaoJdbc implements LocationDao { - public LocationDaoJdbc(DataSource dataSource, DataSource permanentDeleteDataSource, FhirContext fhirContext) + public LocationDaoJdbc(DataSource dataSource, DataSource permanentDeleteDataSource, FhirContext fhirContext, + ObjectMapper objectMapper) { - super(dataSource, permanentDeleteDataSource, fhirContext, Location.class, "locations", "location", + super(dataSource, permanentDeleteDataSource, fhirContext, objectMapper, Location.class, "locations", "location", "location_id", LocationIdentityFilter::new, List.of(factory(LocationIdentifier.PARAMETER_NAME, LocationIdentifier::new, LocationIdentifier.getNameModifiers()), diff --git a/dsf-fhir/dsf-fhir-server/src/main/java/dev/dsf/fhir/dao/jdbc/MeasureDaoJdbc.java b/dsf-fhir/dsf-fhir-server/src/main/java/dev/dsf/fhir/dao/jdbc/MeasureDaoJdbc.java index c14f65a13..8d1b2ae0d 100755 --- a/dsf-fhir/dsf-fhir-server/src/main/java/dev/dsf/fhir/dao/jdbc/MeasureDaoJdbc.java +++ b/dsf-fhir/dsf-fhir-server/src/main/java/dev/dsf/fhir/dao/jdbc/MeasureDaoJdbc.java @@ -24,6 +24,8 @@ import org.hl7.fhir.r4.model.Measure; +import com.fasterxml.jackson.databind.ObjectMapper; + import ca.uhn.fhir.context.FhirContext; import dev.dsf.fhir.dao.MeasureDao; import dev.dsf.fhir.search.filter.MeasureIdentityFilter; @@ -39,10 +41,11 @@ public class MeasureDaoJdbc extends AbstractResourceDaoJdbc implements { private final ReadByUrlDaoJdbc readByUrl; - public MeasureDaoJdbc(DataSource dataSource, DataSource permanentDeleteDataSource, FhirContext fhirContext) + public MeasureDaoJdbc(DataSource dataSource, DataSource permanentDeleteDataSource, FhirContext fhirContext, + ObjectMapper objectMapper) { - super(dataSource, permanentDeleteDataSource, fhirContext, Measure.class, "measures", "measure", "measure_id", - MeasureIdentityFilter::new, + super(dataSource, permanentDeleteDataSource, fhirContext, objectMapper, Measure.class, "measures", "measure", + "measure_id", MeasureIdentityFilter::new, List.of(factory(MeasureDate.PARAMETER_NAME, MeasureDate::new), factory(MeasureDependsOn.PARAMETER_NAME, MeasureDependsOn::new, MeasureDependsOn.getNameModifiers(), MeasureDependsOn::new, diff --git a/dsf-fhir/dsf-fhir-server/src/main/java/dev/dsf/fhir/dao/jdbc/MeasureReportDaoJdbc.java b/dsf-fhir/dsf-fhir-server/src/main/java/dev/dsf/fhir/dao/jdbc/MeasureReportDaoJdbc.java index c6ebf562a..4ebf6d388 100755 --- a/dsf-fhir/dsf-fhir-server/src/main/java/dev/dsf/fhir/dao/jdbc/MeasureReportDaoJdbc.java +++ b/dsf-fhir/dsf-fhir-server/src/main/java/dev/dsf/fhir/dao/jdbc/MeasureReportDaoJdbc.java @@ -21,6 +21,8 @@ import org.hl7.fhir.r4.model.MeasureReport; +import com.fasterxml.jackson.databind.ObjectMapper; + import ca.uhn.fhir.context.FhirContext; import dev.dsf.fhir.dao.MeasureReportDao; import dev.dsf.fhir.search.filter.MeasureReportIdentityFilter; @@ -28,9 +30,10 @@ public class MeasureReportDaoJdbc extends AbstractResourceDaoJdbc implements MeasureReportDao { - public MeasureReportDaoJdbc(DataSource dataSource, DataSource permanentDeleteDataSource, FhirContext fhirContext) + public MeasureReportDaoJdbc(DataSource dataSource, DataSource permanentDeleteDataSource, FhirContext fhirContext, + ObjectMapper objectMapper) { - super(dataSource, permanentDeleteDataSource, fhirContext, MeasureReport.class, "measure_reports", + super(dataSource, permanentDeleteDataSource, fhirContext, objectMapper, MeasureReport.class, "measure_reports", "measure_report", "measure_report_id", MeasureReportIdentityFilter::new, List.of(factory(MeasureReportIdentifier.PARAMETER_NAME, MeasureReportIdentifier::new, MeasureReportIdentifier.getNameModifiers())), diff --git a/dsf-fhir/dsf-fhir-server/src/main/java/dev/dsf/fhir/dao/jdbc/NamingSystemDaoJdbc.java b/dsf-fhir/dsf-fhir-server/src/main/java/dev/dsf/fhir/dao/jdbc/NamingSystemDaoJdbc.java index 1b49b45e8..4f17ddc3a 100755 --- a/dsf-fhir/dsf-fhir-server/src/main/java/dev/dsf/fhir/dao/jdbc/NamingSystemDaoJdbc.java +++ b/dsf-fhir/dsf-fhir-server/src/main/java/dev/dsf/fhir/dao/jdbc/NamingSystemDaoJdbc.java @@ -25,10 +25,14 @@ import javax.sql.DataSource; +import org.hl7.fhir.r4.model.BooleanType; import org.hl7.fhir.r4.model.NamingSystem; +import org.hl7.fhir.r4.model.NamingSystem.NamingSystemIdentifierType; import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import com.fasterxml.jackson.databind.ObjectMapper; + import ca.uhn.fhir.context.FhirContext; import dev.dsf.fhir.dao.NamingSystemDao; import dev.dsf.fhir.search.filter.NamingSystemIdentityFilter; @@ -40,10 +44,11 @@ public class NamingSystemDaoJdbc extends AbstractResourceDaoJdbc i { private static final Logger logger = LoggerFactory.getLogger(NamingSystemDaoJdbc.class); - public NamingSystemDaoJdbc(DataSource dataSource, DataSource permanentDeleteDataSource, FhirContext fhirContext) + public NamingSystemDaoJdbc(DataSource dataSource, DataSource permanentDeleteDataSource, FhirContext fhirContext, + ObjectMapper objectMapper) { - super(dataSource, permanentDeleteDataSource, fhirContext, NamingSystem.class, "naming_systems", "naming_system", - "naming_system_id", NamingSystemIdentityFilter::new, + super(dataSource, permanentDeleteDataSource, fhirContext, objectMapper, NamingSystem.class, "naming_systems", + "naming_system", "naming_system_id", NamingSystemIdentityFilter::new, List.of(factory(NamingSystemDate.PARAMETER_NAME, NamingSystemDate::new), factory(NamingSystemName.PARAMETER_NAME, NamingSystemName::new, NamingSystemName.getNameModifiers()), @@ -96,12 +101,13 @@ public boolean existsWithUniqueIdUriEntry(Connection connection, String uniqueId if (uniqueIdValue == null || uniqueIdValue.isBlank()) return false; - final String namingSystem = "{\"uniqueId\":[{\"type\":\"uri\",\"value\":\"" + uniqueIdValue + "\"}]}"; + NamingSystem n = new NamingSystem(); + n.addUniqueId().setType(NamingSystemIdentifierType.URI).setValue(uniqueIdValue); try (PreparedStatement statement = connection.prepareStatement( - "SELECT count(*) FROM current_naming_systems WHERE naming_system->>'status' IN ('draft', 'active') AND naming_system @> ?::jsonb")) + "SELECT count(*) FROM current_naming_systems WHERE naming_system->>'status' IN ('draft', 'active') AND naming_system @> ?")) { - statement.setString(1, namingSystem); + statement.setObject(1, getPreparedStatementFactory().resourceToPgObject(n)); try (ResultSet result = statement.executeQuery()) { @@ -122,13 +128,14 @@ public boolean existsWithUniqueIdUriEntryResolvable(Connection connection, Strin if (uniqueIdValue == null || uniqueIdValue.isBlank()) return false; - final String namingSystem = "{\"uniqueId\":[{\"modifierExtension\":[{\"url\":\"http://dsf.dev/fhir/StructureDefinition/extension-check-logical-reference\",\"valueBoolean\":true}]," - + "\"value\":\"" + uniqueIdValue + "\"}]}"; + NamingSystem n = new NamingSystem(); + n.addUniqueId().setValue(uniqueIdValue).addModifierExtension( + "http://dsf.dev/fhir/StructureDefinition/extension-check-logical-reference", new BooleanType(true)); try (PreparedStatement statement = connection.prepareStatement( - "SELECT count(*) FROM current_naming_systems WHERE naming_system->>'status' IN ('draft', 'active') AND naming_system @> ?::jsonb")) + "SELECT count(*) FROM current_naming_systems WHERE naming_system->>'status' IN ('draft', 'active') AND naming_system @> ?")) { - statement.setString(1, namingSystem); + statement.setObject(1, getPreparedStatementFactory().resourceToPgObject(n)); try (ResultSet result = statement.executeQuery()) { diff --git a/dsf-fhir/dsf-fhir-server/src/main/java/dev/dsf/fhir/dao/jdbc/OrganizationAffiliationDaoJdbc.java b/dsf-fhir/dsf-fhir-server/src/main/java/dev/dsf/fhir/dao/jdbc/OrganizationAffiliationDaoJdbc.java index 84f7e12b9..f50615ea7 100644 --- a/dsf-fhir/dsf-fhir-server/src/main/java/dev/dsf/fhir/dao/jdbc/OrganizationAffiliationDaoJdbc.java +++ b/dsf-fhir/dsf-fhir-server/src/main/java/dev/dsf/fhir/dao/jdbc/OrganizationAffiliationDaoJdbc.java @@ -28,8 +28,12 @@ import org.hl7.fhir.r4.model.OrganizationAffiliation; +import com.fasterxml.jackson.databind.ObjectMapper; + import ca.uhn.fhir.context.FhirContext; import dev.dsf.fhir.dao.OrganizationAffiliationDao; +import dev.dsf.fhir.dao.jdbc.PgObjectFactory.CodingParameter; +import dev.dsf.fhir.dao.jdbc.PgObjectFactory.IdentifierParameter; import dev.dsf.fhir.search.filter.OrganizationAffiliationIdentityFilter; import dev.dsf.fhir.search.parameters.OrganizationAffiliationActive; import dev.dsf.fhir.search.parameters.OrganizationAffiliationEndpoint; @@ -42,9 +46,9 @@ public class OrganizationAffiliationDaoJdbc extends AbstractResourceDaoJdbc readActiveNotDeletedByMemberOrganizationIde + "AND concat('Organization/', organization->>'id') = organization_affiliation->'organization'->>'reference' LIMIT 1) AS organization_identifier " + "FROM current_organization_affiliations WHERE organization_affiliation->>'active' = 'true' AND " + "(SELECT organization->'identifier' FROM current_organizations WHERE organization->>'active' = 'true' AND " - + "concat('Organization/', organization->>'id') = organization_affiliation->'participatingOrganization'->>'reference') @> ?::jsonb"; + + "concat('Organization/', organization->>'id') = organization_affiliation->'participatingOrganization'->>'reference') @> ?"; if (endpointIdentifierValue != null && !endpointIdentifierValue.isBlank()) sql += " AND (SELECT jsonb_agg(identifier) FROM (SELECT identifier FROM current_endpoints, jsonb_array_elements(endpoint->'identifier') identifier" + " WHERE concat('Endpoint/', endpoint->>'id') IN (SELECT reference->>'reference' FROM jsonb_array_elements(organization_affiliation->'endpoint') reference)" - + " ) AS identifiers) @> ?::jsonb"; + + " ) AS identifiers) @> ?"; try (PreparedStatement statement = connection.prepareStatement(sql)) { - statement.setString(1, "[{\"system\": \"http://dsf.dev/sid/organization-identifier\", \"value\": \"" - + organizationIdentifierValue + "\"}]"); + statement.setObject(1, getPreparedStatementFactory() + .jsonParameterToPgObjectAsArray(IdentifierParameter.organization(organizationIdentifierValue))); if (endpointIdentifierValue != null && !endpointIdentifierValue.isBlank()) - statement.setString(2, "[{\"system\": \"http://dsf.dev/sid/endpoint-identifier\", \"value\": \"" - + endpointIdentifierValue + "\"}]"); + statement.setObject(2, getPreparedStatementFactory() + .jsonParameterToPgObjectAsArray(IdentifierParameter.endpoint(endpointIdentifierValue))); try (ResultSet result = statement.executeQuery()) { @@ -146,12 +150,13 @@ public boolean existsNotDeletedByParentOrganizationMemberOrganizationRoleAndNotE .prepareStatement("SELECT count(*) FROM current_organization_affiliations " + "WHERE organization_affiliation->'organization'->>'reference' = ? " + "AND organization_affiliation->'participatingOrganization'->>'reference' = ? " - + "AND (SELECT jsonb_agg(coding) FROM jsonb_array_elements(organization_affiliation->'code') AS code, jsonb_array_elements(code->'coding') AS coding) @> ?::jsonb " + + "AND (SELECT jsonb_agg(coding) FROM jsonb_array_elements(organization_affiliation->'code') AS code, jsonb_array_elements(code->'coding') AS coding) @> ? " + "AND ? NOT IN (SELECT reference->>'reference' FROM jsonb_array_elements(organization_affiliation->'endpoint') AS reference)")) { statement.setString(1, "Organization/" + parentOrganization.toString()); statement.setString(2, "Organization/" + memberOrganization.toString()); - statement.setString(3, "[{\"code\": \"" + roleCode + "\", \"system\": \"" + roleSystem + "\"}]"); + statement.setObject(3, getPreparedStatementFactory() + .jsonParameterToPgObjectAsArray(new CodingParameter(roleSystem, roleCode))); statement.setString(4, "Endpoint/" + endpoint.toString()); try (ResultSet result = statement.executeQuery()) diff --git a/dsf-fhir/dsf-fhir-server/src/main/java/dev/dsf/fhir/dao/jdbc/OrganizationDaoJdbc.java b/dsf-fhir/dsf-fhir-server/src/main/java/dev/dsf/fhir/dao/jdbc/OrganizationDaoJdbc.java index 7bf977f8d..808898ad8 100755 --- a/dsf-fhir/dsf-fhir-server/src/main/java/dev/dsf/fhir/dao/jdbc/OrganizationDaoJdbc.java +++ b/dsf-fhir/dsf-fhir-server/src/main/java/dev/dsf/fhir/dao/jdbc/OrganizationDaoJdbc.java @@ -28,8 +28,12 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import com.fasterxml.jackson.databind.ObjectMapper; + import ca.uhn.fhir.context.FhirContext; import dev.dsf.fhir.dao.OrganizationDao; +import dev.dsf.fhir.dao.jdbc.PgObjectFactory.ExtensionParameterValueString; +import dev.dsf.fhir.dao.jdbc.PgObjectFactory.IdentifierParameter; import dev.dsf.fhir.search.filter.OrganizationIdentityFilter; import dev.dsf.fhir.search.parameters.OrganizationActive; import dev.dsf.fhir.search.parameters.OrganizationEndpoint; @@ -44,10 +48,11 @@ public class OrganizationDaoJdbc extends AbstractResourceDaoJdbc i { private static final Logger logger = LoggerFactory.getLogger(OrganizationDaoJdbc.class); - public OrganizationDaoJdbc(DataSource dataSource, DataSource permanentDeleteDataSource, FhirContext fhirContext) + public OrganizationDaoJdbc(DataSource dataSource, DataSource permanentDeleteDataSource, FhirContext fhirContext, + ObjectMapper objectMapper) { - super(dataSource, permanentDeleteDataSource, fhirContext, Organization.class, "organizations", "organization", - "organization_id", OrganizationIdentityFilter::new, + super(dataSource, permanentDeleteDataSource, fhirContext, objectMapper, Organization.class, "organizations", + "organization", "organization_id", OrganizationIdentityFilter::new, List.of(factory(OrganizationActive.PARAMETER_NAME, OrganizationActive::new), factory(OrganizationEndpoint.PARAMETER_NAME, OrganizationEndpoint::new, OrganizationEndpoint.getNameModifiers(), OrganizationEndpoint::new, @@ -81,11 +86,10 @@ public Optional readActiveNotDeletedByThumbprint(String thumbprint try (Connection connection = getDataSource().getConnection(); PreparedStatement statement = connection.prepareStatement( - "SELECT organization FROM current_organizations WHERE organization->'extension' @> ?::jsonb AND organization->>'active' = 'true'")) + "SELECT organization FROM current_organizations WHERE organization->'extension' @> ? AND organization->>'active' = 'true'")) { - String search = "[{\"url\": \"http://dsf.dev/fhir/StructureDefinition/extension-certificate-thumbprint\", \"valueString\": \"" - + thumbprintHex + "\"}]"; - statement.setString(1, search); + statement.setObject(1, getPreparedStatementFactory() + .jsonParameterToPgObjectAsArray(ExtensionParameterValueString.thumbprint(thumbprintHex))); try (ResultSet result = statement.executeQuery()) { @@ -122,11 +126,10 @@ public Optional readActiveNotDeletedByIdentifier(String identifier try (Connection connection = getDataSource().getConnection(); PreparedStatement statement = connection.prepareStatement( - "SELECT organization FROM current_organizations WHERE organization->'identifier' @> ?::jsonb AND organization->>'active' = 'true'")) + "SELECT organization FROM current_organizations WHERE organization->'identifier' @> ? AND organization->>'active' = 'true'")) { - String search = "[{\"system\": \"http://dsf.dev/sid/organization-identifier\", \"value\": \"" - + identifierValue + "\"}]"; - statement.setString(1, search); + statement.setObject(1, getPreparedStatementFactory() + .jsonParameterToPgObjectAsArray(IdentifierParameter.organization(identifierValue))); try (ResultSet result = statement.executeQuery()) { @@ -163,11 +166,10 @@ public boolean existsNotDeletedByThumbprintWithTransaction(Connection connection return false; try (PreparedStatement statement = connection.prepareStatement( - "SELECT organization FROM current_organizations WHERE organization->'extension' @> ?::jsonb")) + "SELECT organization FROM current_organizations WHERE organization->'extension' @> ?")) { - String search = "[{\"url\": \"http://dsf.dev/fhir/StructureDefinition/extension-certificate-thumbprint\", \"valueString\": \"" - + thumbprintHex + "\"}]"; - statement.setString(1, search); + statement.setObject(1, getPreparedStatementFactory() + .jsonParameterToPgObjectAsArray(ExtensionParameterValueString.thumbprint(thumbprintHex))); try (ResultSet result = statement.executeQuery()) { diff --git a/dsf-fhir/dsf-fhir-server/src/main/java/dev/dsf/fhir/dao/jdbc/PatientDaoJdbc.java b/dsf-fhir/dsf-fhir-server/src/main/java/dev/dsf/fhir/dao/jdbc/PatientDaoJdbc.java index 657c7bfa0..3fa7418e3 100755 --- a/dsf-fhir/dsf-fhir-server/src/main/java/dev/dsf/fhir/dao/jdbc/PatientDaoJdbc.java +++ b/dsf-fhir/dsf-fhir-server/src/main/java/dev/dsf/fhir/dao/jdbc/PatientDaoJdbc.java @@ -21,6 +21,8 @@ import org.hl7.fhir.r4.model.Patient; +import com.fasterxml.jackson.databind.ObjectMapper; + import ca.uhn.fhir.context.FhirContext; import dev.dsf.fhir.dao.PatientDao; import dev.dsf.fhir.search.filter.PatientIdentityFilter; @@ -29,10 +31,11 @@ public class PatientDaoJdbc extends AbstractResourceDaoJdbc implements PatientDao { - public PatientDaoJdbc(DataSource dataSource, DataSource permanentDeleteDataSource, FhirContext fhirContext) + public PatientDaoJdbc(DataSource dataSource, DataSource permanentDeleteDataSource, FhirContext fhirContext, + ObjectMapper objectMapper) { - super(dataSource, permanentDeleteDataSource, fhirContext, Patient.class, "patients", "patient", "patient_id", - PatientIdentityFilter::new, + super(dataSource, permanentDeleteDataSource, fhirContext, objectMapper, Patient.class, "patients", "patient", + "patient_id", PatientIdentityFilter::new, List.of(factory(PatientActive.PARAMETER_NAME, PatientActive::new), factory(PatientIdentifier.PARAMETER_NAME, PatientIdentifier::new, PatientIdentifier.getNameModifiers())), diff --git a/dsf-fhir/dsf-fhir-server/src/main/java/dev/dsf/fhir/dao/jdbc/PgObjectFactory.java b/dsf-fhir/dsf-fhir-server/src/main/java/dev/dsf/fhir/dao/jdbc/PgObjectFactory.java new file mode 100644 index 000000000..ec11be23b --- /dev/null +++ b/dsf-fhir/dsf-fhir-server/src/main/java/dev/dsf/fhir/dao/jdbc/PgObjectFactory.java @@ -0,0 +1,84 @@ +/* + * Copyright 2018-2025 Heilbronn University of Applied Sciences + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package dev.dsf.fhir.dao.jdbc; + +import java.sql.SQLException; +import java.util.UUID; + +import org.hl7.fhir.r4.model.Coding; +import org.hl7.fhir.r4.model.Resource; +import org.postgresql.util.PGobject; + +import ca.uhn.fhir.parser.IParser; + +public interface PgObjectFactory +{ + interface JsonParameter + { + } + + IParser getJsonParser(); + + PGobject resourceToPgObject(Resource resource) throws SQLException; + + PGobject jsonParameterToPgObject(JsonParameter parameter) throws SQLException; + + PGobject jsonParameterToPgObjectAsArray(JsonParameter... parameter) throws SQLException; + + PGobject uuidToPgObject(UUID uuid) throws SQLException; + + record ExtensionParameterValueString(String url, String valueString) implements JsonParameter + { + public static ExtensionParameterValueString thumbprint(String value) + { + return new ExtensionParameterValueString( + "http://dsf.dev/fhir/StructureDefinition/extension-certificate-thumbprint", value); + } + } + + record IdentifierParameter(String system, String value) implements JsonParameter + { + public static IdentifierParameter organization(String value) + { + return new IdentifierParameter("http://dsf.dev/sid/organization-identifier", value); + } + + public static IdentifierParameter endpoint(String value) + { + return new IdentifierParameter("http://dsf.dev/sid/endpoint-identifier", value); + } + } + + record CodingParameter(String system, String code) implements JsonParameter + { + public static CodingParameter coding(Coding coding) + { + return new CodingParameter(coding.getSystem(), coding.getCode()); + } + } + + record ReferenceParameter(String reference) implements JsonParameter + { + } + + record RelatedArtifactParameter(String type, String resource) implements JsonParameter + { + public static RelatedArtifactParameter dependsOn(String resource) + { + return new RelatedArtifactParameter("depends-on", resource); + } + } +} diff --git a/dsf-fhir/dsf-fhir-server/src/main/java/dev/dsf/fhir/dao/jdbc/PgObjectFactoryImpl.java b/dsf-fhir/dsf-fhir-server/src/main/java/dev/dsf/fhir/dao/jdbc/PgObjectFactoryImpl.java new file mode 100644 index 000000000..cea7ed1c6 --- /dev/null +++ b/dsf-fhir/dsf-fhir-server/src/main/java/dev/dsf/fhir/dao/jdbc/PgObjectFactoryImpl.java @@ -0,0 +1,131 @@ +/* + * Copyright 2018-2025 Heilbronn University of Applied Sciences + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package dev.dsf.fhir.dao.jdbc; + +import java.sql.SQLException; +import java.util.Objects; +import java.util.UUID; + +import org.hl7.fhir.r4.model.Resource; +import org.postgresql.util.PGobject; +import org.springframework.beans.factory.InitializingBean; + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.ObjectMapper; + +import ca.uhn.fhir.context.FhirContext; +import ca.uhn.fhir.parser.DataFormatException; +import ca.uhn.fhir.parser.IParser; + +public class PgObjectFactoryImpl implements PgObjectFactory, InitializingBean +{ + private final FhirContext fhirContext; + private final ObjectMapper objectMapper; + + public PgObjectFactoryImpl(FhirContext fhirContext, ObjectMapper objectMapper) + { + this.fhirContext = fhirContext; + this.objectMapper = objectMapper; + } + + @Override + public void afterPropertiesSet() throws Exception + { + Objects.requireNonNull(fhirContext, "fhirContext"); + Objects.requireNonNull(objectMapper, "objectMapper"); + } + + @Override + public IParser getJsonParser() + { + IParser p = fhirContext.newJsonParser(); + p.setStripVersionsFromReferences(false); + return p; + } + + @Override + public final PGobject resourceToPgObject(Resource resource) throws SQLException + { + if (resource == null) + return null; + + try + { + return createPgObjectJsonb(getJsonParser().encodeResourceToString(resource)); + } + catch (DataFormatException e) + { + throw new SQLException(e); + } + } + + @Override + public PGobject jsonParameterToPgObject(JsonParameter parameter) throws SQLException + { + if (parameter == null) + return null; + + try + { + return createPgObjectJsonb(objectMapper.writeValueAsString(parameter)); + } + catch (JsonProcessingException e) + { + throw new SQLException(e); + } + } + + @Override + public PGobject jsonParameterToPgObjectAsArray(JsonParameter... parameter) throws SQLException + { + if (parameter == null) + return null; + + try + { + return createPgObjectJsonb(objectMapper.writeValueAsString(parameter)); + } + catch (JsonProcessingException e) + { + throw new SQLException(e); + } + } + + private PGobject createPgObjectJsonb(String value) throws SQLException + { + PGobject o = new PGobject(); + o.setType("JSONB"); + o.setValue(value); + return o; + } + + @Override + public final PGobject uuidToPgObject(UUID uuid) throws SQLException + { + if (uuid == null) + return null; + + return createPgObjectUuid(uuid.toString()); + } + + private PGobject createPgObjectUuid(String value) throws SQLException + { + PGobject o = new PGobject(); + o.setType("UUID"); + o.setValue(value); + return o; + } +} diff --git a/dsf-fhir/dsf-fhir-server/src/main/java/dev/dsf/fhir/dao/jdbc/PractitionerDaoJdbc.java b/dsf-fhir/dsf-fhir-server/src/main/java/dev/dsf/fhir/dao/jdbc/PractitionerDaoJdbc.java index 942c1da60..0b3e7d8fd 100755 --- a/dsf-fhir/dsf-fhir-server/src/main/java/dev/dsf/fhir/dao/jdbc/PractitionerDaoJdbc.java +++ b/dsf-fhir/dsf-fhir-server/src/main/java/dev/dsf/fhir/dao/jdbc/PractitionerDaoJdbc.java @@ -21,6 +21,8 @@ import org.hl7.fhir.r4.model.Practitioner; +import com.fasterxml.jackson.databind.ObjectMapper; + import ca.uhn.fhir.context.FhirContext; import dev.dsf.fhir.dao.PractitionerDao; import dev.dsf.fhir.search.filter.PractitionerIdentityFilter; @@ -29,10 +31,11 @@ public class PractitionerDaoJdbc extends AbstractResourceDaoJdbc implements PractitionerDao { - public PractitionerDaoJdbc(DataSource dataSource, DataSource permanentDeleteDataSource, FhirContext fhirContext) + public PractitionerDaoJdbc(DataSource dataSource, DataSource permanentDeleteDataSource, FhirContext fhirContext, + ObjectMapper objectMapper) { - super(dataSource, permanentDeleteDataSource, fhirContext, Practitioner.class, "practitioners", "practitioner", - "practitioner_id", PractitionerIdentityFilter::new, + super(dataSource, permanentDeleteDataSource, fhirContext, objectMapper, Practitioner.class, "practitioners", + "practitioner", "practitioner_id", PractitionerIdentityFilter::new, List.of(factory(PractitionerActive.PARAMETER_NAME, PractitionerActive::new), factory(PractitionerIdentifier.PARAMETER_NAME, PractitionerIdentifier::new, PractitionerIdentifier.getNameModifiers())), diff --git a/dsf-fhir/dsf-fhir-server/src/main/java/dev/dsf/fhir/dao/jdbc/PractitionerRoleDaoJdbc.java b/dsf-fhir/dsf-fhir-server/src/main/java/dev/dsf/fhir/dao/jdbc/PractitionerRoleDaoJdbc.java index af4a737a0..ae2cf7b58 100755 --- a/dsf-fhir/dsf-fhir-server/src/main/java/dev/dsf/fhir/dao/jdbc/PractitionerRoleDaoJdbc.java +++ b/dsf-fhir/dsf-fhir-server/src/main/java/dev/dsf/fhir/dao/jdbc/PractitionerRoleDaoJdbc.java @@ -21,6 +21,8 @@ import org.hl7.fhir.r4.model.PractitionerRole; +import com.fasterxml.jackson.databind.ObjectMapper; + import ca.uhn.fhir.context.FhirContext; import dev.dsf.fhir.dao.PractitionerRoleDao; import dev.dsf.fhir.search.filter.PractitionerRoleIdentityFilter; @@ -31,10 +33,11 @@ public class PractitionerRoleDaoJdbc extends AbstractResourceDaoJdbc implements PractitionerRoleDao { - public PractitionerRoleDaoJdbc(DataSource dataSource, DataSource permanentDeleteDataSource, FhirContext fhirContext) + public PractitionerRoleDaoJdbc(DataSource dataSource, DataSource permanentDeleteDataSource, FhirContext fhirContext, + ObjectMapper objectMapper) { - super(dataSource, permanentDeleteDataSource, fhirContext, PractitionerRole.class, "practitioner_roles", - "practitioner_role", "practitioner_role_id", PractitionerRoleIdentityFilter::new, + super(dataSource, permanentDeleteDataSource, fhirContext, objectMapper, PractitionerRole.class, + "practitioner_roles", "practitioner_role", "practitioner_role_id", PractitionerRoleIdentityFilter::new, List.of(factory(PractitionerRoleActive.PARAMETER_NAME, PractitionerRoleActive::new), factory(PractitionerRoleIdentifier.PARAMETER_NAME, PractitionerRoleIdentifier::new, PractitionerRoleIdentifier.getNameModifiers()), diff --git a/dsf-fhir/dsf-fhir-server/src/main/java/dev/dsf/fhir/dao/jdbc/PreparedStatementFactory.java b/dsf-fhir/dsf-fhir-server/src/main/java/dev/dsf/fhir/dao/jdbc/PreparedStatementFactory.java index 3e589080f..eec064dec 100755 --- a/dsf-fhir/dsf-fhir-server/src/main/java/dev/dsf/fhir/dao/jdbc/PreparedStatementFactory.java +++ b/dsf-fhir/dsf-fhir-server/src/main/java/dev/dsf/fhir/dao/jdbc/PreparedStatementFactory.java @@ -22,18 +22,9 @@ import java.util.UUID; import org.hl7.fhir.r4.model.Resource; -import org.postgresql.util.PGobject; -import ca.uhn.fhir.parser.IParser; - -interface PreparedStatementFactory +interface PreparedStatementFactory extends PgObjectFactory { - IParser getJsonParser(); - - PGobject resourceToPgObject(R resource); - - PGobject uuidToPgObject(UUID uuid); - String getCreateSql(); void configureCreateStatement(LargeObjectManager largeObjectManager, PreparedStatement statement, R resource, diff --git a/dsf-fhir/dsf-fhir-server/src/main/java/dev/dsf/fhir/dao/jdbc/PreparedStatementFactoryBinary.java b/dsf-fhir/dsf-fhir-server/src/main/java/dev/dsf/fhir/dao/jdbc/PreparedStatementFactoryBinary.java index fcf1220ab..4793e81dc 100755 --- a/dsf-fhir/dsf-fhir-server/src/main/java/dev/dsf/fhir/dao/jdbc/PreparedStatementFactoryBinary.java +++ b/dsf-fhir/dsf-fhir-server/src/main/java/dev/dsf/fhir/dao/jdbc/PreparedStatementFactoryBinary.java @@ -26,6 +26,8 @@ import org.hl7.fhir.r4.model.Base64BinaryType; import org.hl7.fhir.r4.model.Binary; +import com.fasterxml.jackson.databind.ObjectMapper; + import ca.uhn.fhir.context.FhirContext; import dev.dsf.fhir.dao.jdbc.LargeObjectManager.OidAndSize; import dev.dsf.fhir.model.StreamableBase64BinaryType; @@ -38,9 +40,9 @@ class PreparedStatementFactoryBinary extends AbstractPreparedStatementFactory extends AbstractPreparedStatementFactory { - PreparedStatementFactoryDefault(FhirContext fhirContext, Class resourceType, String resourceTable, - String resourceIdColumn, String resourceColumn) + PreparedStatementFactoryDefault(FhirContext fhirContext, ObjectMapper objectMapper, Class resourceType, + String resourceTable, String resourceIdColumn, String resourceColumn) { - super(fhirContext, resourceType, createSql(resourceTable, resourceIdColumn, resourceColumn), + super(fhirContext, objectMapper, resourceType, createSql(resourceTable, resourceIdColumn, resourceColumn), readByIdSql(resourceTable, resourceIdColumn, resourceColumn), readByIdAndVersionSql(resourceTable, resourceIdColumn, resourceColumn), updateSql(resourceTable, resourceIdColumn, resourceColumn)); diff --git a/dsf-fhir/dsf-fhir-server/src/main/java/dev/dsf/fhir/dao/jdbc/ProvenanceDaoJdbc.java b/dsf-fhir/dsf-fhir-server/src/main/java/dev/dsf/fhir/dao/jdbc/ProvenanceDaoJdbc.java index ee71cee92..529de515a 100755 --- a/dsf-fhir/dsf-fhir-server/src/main/java/dev/dsf/fhir/dao/jdbc/ProvenanceDaoJdbc.java +++ b/dsf-fhir/dsf-fhir-server/src/main/java/dev/dsf/fhir/dao/jdbc/ProvenanceDaoJdbc.java @@ -21,16 +21,19 @@ import org.hl7.fhir.r4.model.Provenance; +import com.fasterxml.jackson.databind.ObjectMapper; + import ca.uhn.fhir.context.FhirContext; import dev.dsf.fhir.dao.ProvenanceDao; import dev.dsf.fhir.search.filter.ProvenanceIdentityFilter; public class ProvenanceDaoJdbc extends AbstractResourceDaoJdbc implements ProvenanceDao { - public ProvenanceDaoJdbc(DataSource dataSource, DataSource permanentDeleteDataSource, FhirContext fhirContext) + public ProvenanceDaoJdbc(DataSource dataSource, DataSource permanentDeleteDataSource, FhirContext fhirContext, + ObjectMapper objectMapper) { - super(dataSource, permanentDeleteDataSource, fhirContext, Provenance.class, "provenances", "provenance", - "provenance_id", ProvenanceIdentityFilter::new, List.of(), List.of()); + super(dataSource, permanentDeleteDataSource, fhirContext, objectMapper, Provenance.class, "provenances", + "provenance", "provenance_id", ProvenanceIdentityFilter::new, List.of(), List.of()); } @Override diff --git a/dsf-fhir/dsf-fhir-server/src/main/java/dev/dsf/fhir/dao/jdbc/QuestionnaireDaoJdbc.java b/dsf-fhir/dsf-fhir-server/src/main/java/dev/dsf/fhir/dao/jdbc/QuestionnaireDaoJdbc.java index b9316d858..0042be304 100755 --- a/dsf-fhir/dsf-fhir-server/src/main/java/dev/dsf/fhir/dao/jdbc/QuestionnaireDaoJdbc.java +++ b/dsf-fhir/dsf-fhir-server/src/main/java/dev/dsf/fhir/dao/jdbc/QuestionnaireDaoJdbc.java @@ -28,6 +28,8 @@ import org.hl7.fhir.r4.model.Questionnaire; +import com.fasterxml.jackson.databind.ObjectMapper; + import ca.uhn.fhir.context.FhirContext; import dev.dsf.fhir.dao.QuestionnaireDao; import dev.dsf.fhir.search.filter.QuestionnaireIdentityFilter; @@ -42,9 +44,10 @@ public class QuestionnaireDaoJdbc extends AbstractResourceDaoJdbc { private final ReadByUrlDaoJdbc readByUrl; - public QuestionnaireDaoJdbc(DataSource dataSource, DataSource permanentDeleteDataSource, FhirContext fhirContext) + public QuestionnaireDaoJdbc(DataSource dataSource, DataSource permanentDeleteDataSource, FhirContext fhirContext, + ObjectMapper objectMapper) { - super(dataSource, permanentDeleteDataSource, fhirContext, Questionnaire.class, "questionnaires", + super(dataSource, permanentDeleteDataSource, fhirContext, objectMapper, Questionnaire.class, "questionnaires", "questionnaire", "questionnaire_id", QuestionnaireIdentityFilter::new, List.of(factory(QuestionnaireDate.PARAMETER_NAME, QuestionnaireDate::new), factory(QuestionnaireIdentifier.PARAMETER_NAME, QuestionnaireIdentifier::new, diff --git a/dsf-fhir/dsf-fhir-server/src/main/java/dev/dsf/fhir/dao/jdbc/QuestionnaireResponseDaoJdbc.java b/dsf-fhir/dsf-fhir-server/src/main/java/dev/dsf/fhir/dao/jdbc/QuestionnaireResponseDaoJdbc.java index eb21da4d8..669a0e633 100755 --- a/dsf-fhir/dsf-fhir-server/src/main/java/dev/dsf/fhir/dao/jdbc/QuestionnaireResponseDaoJdbc.java +++ b/dsf-fhir/dsf-fhir-server/src/main/java/dev/dsf/fhir/dao/jdbc/QuestionnaireResponseDaoJdbc.java @@ -21,6 +21,8 @@ import org.hl7.fhir.r4.model.QuestionnaireResponse; +import com.fasterxml.jackson.databind.ObjectMapper; + import ca.uhn.fhir.context.FhirContext; import dev.dsf.fhir.dao.QuestionnaireResponseDao; import dev.dsf.fhir.search.filter.QuestionnaireResponseIdentityFilter; @@ -35,9 +37,9 @@ public class QuestionnaireResponseDaoJdbc extends AbstractResourceDaoJdbc implements ResearchStudyDao { - public ResearchStudyDaoJdbc(DataSource dataSource, DataSource permanentDeleteDataSource, FhirContext fhirContext) + public ResearchStudyDaoJdbc(DataSource dataSource, DataSource permanentDeleteDataSource, FhirContext fhirContext, + ObjectMapper objectMapper) { - super(dataSource, permanentDeleteDataSource, fhirContext, ResearchStudy.class, "research_studies", + super(dataSource, permanentDeleteDataSource, fhirContext, objectMapper, ResearchStudy.class, "research_studies", "research_study", "research_study_id", ResearchStudyIdentityFilter::new, List.of(factory(ResearchStudyEnrollment.PARAMETER_NAME, ResearchStudyEnrollment::new, ResearchStudyEnrollment.getNameModifiers(), ResearchStudyEnrollment::new, diff --git a/dsf-fhir/dsf-fhir-server/src/main/java/dev/dsf/fhir/dao/jdbc/StructureDefinitionDaoJdbc.java b/dsf-fhir/dsf-fhir-server/src/main/java/dev/dsf/fhir/dao/jdbc/StructureDefinitionDaoJdbc.java index 38ebf2cff..de185409d 100755 --- a/dsf-fhir/dsf-fhir-server/src/main/java/dev/dsf/fhir/dao/jdbc/StructureDefinitionDaoJdbc.java +++ b/dsf-fhir/dsf-fhir-server/src/main/java/dev/dsf/fhir/dao/jdbc/StructureDefinitionDaoJdbc.java @@ -19,16 +19,18 @@ import org.hl7.fhir.r4.model.StructureDefinition; +import com.fasterxml.jackson.databind.ObjectMapper; + import ca.uhn.fhir.context.FhirContext; import dev.dsf.fhir.search.filter.StructureDefinitionIdentityFilter; public class StructureDefinitionDaoJdbc extends AbstractStructureDefinitionDaoJdbc { public StructureDefinitionDaoJdbc(DataSource dataSource, DataSource permanentDeleteDataSource, - FhirContext fhirContext) + FhirContext fhirContext, ObjectMapper objectMapper) { - super(dataSource, permanentDeleteDataSource, fhirContext, "structure_definitions", "structure_definition", - "structure_definition_id", StructureDefinitionIdentityFilter::new); + super(dataSource, permanentDeleteDataSource, fhirContext, objectMapper, "structure_definitions", + "structure_definition", "structure_definition_id", StructureDefinitionIdentityFilter::new); } @Override diff --git a/dsf-fhir/dsf-fhir-server/src/main/java/dev/dsf/fhir/dao/jdbc/StructureDefinitionSnapshotDaoJdbc.java b/dsf-fhir/dsf-fhir-server/src/main/java/dev/dsf/fhir/dao/jdbc/StructureDefinitionSnapshotDaoJdbc.java index d3d092fad..abaedf4f1 100755 --- a/dsf-fhir/dsf-fhir-server/src/main/java/dev/dsf/fhir/dao/jdbc/StructureDefinitionSnapshotDaoJdbc.java +++ b/dsf-fhir/dsf-fhir-server/src/main/java/dev/dsf/fhir/dao/jdbc/StructureDefinitionSnapshotDaoJdbc.java @@ -19,15 +19,17 @@ import org.hl7.fhir.r4.model.StructureDefinition; +import com.fasterxml.jackson.databind.ObjectMapper; + import ca.uhn.fhir.context.FhirContext; import dev.dsf.fhir.search.filter.StructureDefinitionSnapshotIdentityFilter; public class StructureDefinitionSnapshotDaoJdbc extends AbstractStructureDefinitionDaoJdbc { public StructureDefinitionSnapshotDaoJdbc(DataSource dataSource, DataSource permanentDeleteDataSource, - FhirContext fhirContext) + FhirContext fhirContext, ObjectMapper objectMapper) { - super(dataSource, permanentDeleteDataSource, fhirContext, "structure_definition_snapshots", + super(dataSource, permanentDeleteDataSource, fhirContext, objectMapper, "structure_definition_snapshots", "structure_definition_snapshot", "structure_definition_snapshot_id", StructureDefinitionSnapshotIdentityFilter::new); } diff --git a/dsf-fhir/dsf-fhir-server/src/main/java/dev/dsf/fhir/dao/jdbc/SubscriptionDaoJdbc.java b/dsf-fhir/dsf-fhir-server/src/main/java/dev/dsf/fhir/dao/jdbc/SubscriptionDaoJdbc.java index 915d9bc40..8fd786f0c 100755 --- a/dsf-fhir/dsf-fhir-server/src/main/java/dev/dsf/fhir/dao/jdbc/SubscriptionDaoJdbc.java +++ b/dsf-fhir/dsf-fhir-server/src/main/java/dev/dsf/fhir/dao/jdbc/SubscriptionDaoJdbc.java @@ -26,6 +26,8 @@ import org.hl7.fhir.r4.model.Subscription; +import com.fasterxml.jackson.databind.ObjectMapper; + import ca.uhn.fhir.context.FhirContext; import dev.dsf.fhir.dao.SubscriptionDao; import dev.dsf.fhir.search.filter.SubscriptionIdentityFilter; @@ -36,10 +38,11 @@ public class SubscriptionDaoJdbc extends AbstractResourceDaoJdbc implements SubscriptionDao { - public SubscriptionDaoJdbc(DataSource dataSource, DataSource permanentDeleteDataSource, FhirContext fhirContext) + public SubscriptionDaoJdbc(DataSource dataSource, DataSource permanentDeleteDataSource, FhirContext fhirContext, + ObjectMapper objectMapper) { - super(dataSource, permanentDeleteDataSource, fhirContext, Subscription.class, "subscriptions", "subscription", - "subscription_id", SubscriptionIdentityFilter::new, + super(dataSource, permanentDeleteDataSource, fhirContext, objectMapper, Subscription.class, "subscriptions", + "subscription", "subscription_id", SubscriptionIdentityFilter::new, List.of(factory(SubscriptionCriteria.PARAMETER_NAME, SubscriptionCriteria::new, SubscriptionCriteria.getNameModifiers()), factory(SubscriptionPayload.PARAMETER_NAME, SubscriptionPayload::new, diff --git a/dsf-fhir/dsf-fhir-server/src/main/java/dev/dsf/fhir/dao/jdbc/TaskDaoJdbc.java b/dsf-fhir/dsf-fhir-server/src/main/java/dev/dsf/fhir/dao/jdbc/TaskDaoJdbc.java index 66954d9fa..2fd6c977b 100755 --- a/dsf-fhir/dsf-fhir-server/src/main/java/dev/dsf/fhir/dao/jdbc/TaskDaoJdbc.java +++ b/dsf-fhir/dsf-fhir-server/src/main/java/dev/dsf/fhir/dao/jdbc/TaskDaoJdbc.java @@ -21,6 +21,8 @@ import org.hl7.fhir.r4.model.Task; +import com.fasterxml.jackson.databind.ObjectMapper; + import ca.uhn.fhir.context.FhirContext; import dev.dsf.fhir.dao.TaskDao; import dev.dsf.fhir.search.filter.TaskIdentityFilter; @@ -32,9 +34,10 @@ public class TaskDaoJdbc extends AbstractResourceDaoJdbc implements TaskDao { - public TaskDaoJdbc(DataSource dataSource, DataSource permanentDeleteDataSource, FhirContext fhirContext) + public TaskDaoJdbc(DataSource dataSource, DataSource permanentDeleteDataSource, FhirContext fhirContext, + ObjectMapper objectMapper) { - super(dataSource, permanentDeleteDataSource, fhirContext, Task.class, "tasks", "task", "task_id", + super(dataSource, permanentDeleteDataSource, fhirContext, objectMapper, Task.class, "tasks", "task", "task_id", TaskIdentityFilter::new, List.of(factory(TaskAuthoredOn.PARAMETER_NAME, TaskAuthoredOn::new), factory(TaskIdentifier.PARAMETER_NAME, TaskIdentifier::new, TaskIdentifier.getNameModifiers()), diff --git a/dsf-fhir/dsf-fhir-server/src/main/java/dev/dsf/fhir/dao/jdbc/ValueSetDaoJdbc.java b/dsf-fhir/dsf-fhir-server/src/main/java/dev/dsf/fhir/dao/jdbc/ValueSetDaoJdbc.java index 2d6cde7e1..8c8be871d 100755 --- a/dsf-fhir/dsf-fhir-server/src/main/java/dev/dsf/fhir/dao/jdbc/ValueSetDaoJdbc.java +++ b/dsf-fhir/dsf-fhir-server/src/main/java/dev/dsf/fhir/dao/jdbc/ValueSetDaoJdbc.java @@ -24,6 +24,8 @@ import org.hl7.fhir.r4.model.ValueSet; +import com.fasterxml.jackson.databind.ObjectMapper; + import ca.uhn.fhir.context.FhirContext; import dev.dsf.fhir.dao.ValueSetDao; import dev.dsf.fhir.search.filter.ValueSetIdentityFilter; @@ -38,10 +40,11 @@ public class ValueSetDaoJdbc extends AbstractResourceDaoJdbc implement { private final ReadByUrlDaoJdbc readByUrl; - public ValueSetDaoJdbc(DataSource dataSource, DataSource permanentDeleteDataSource, FhirContext fhirContext) + public ValueSetDaoJdbc(DataSource dataSource, DataSource permanentDeleteDataSource, FhirContext fhirContext, + ObjectMapper objectMapper) { - super(dataSource, permanentDeleteDataSource, fhirContext, ValueSet.class, "value_sets", "value_set", - "value_set_id", ValueSetIdentityFilter::new, + super(dataSource, permanentDeleteDataSource, fhirContext, objectMapper, ValueSet.class, "value_sets", + "value_set", "value_set_id", ValueSetIdentityFilter::new, List.of(factory(ValueSetDate.PARAMETER_NAME, ValueSetDate::new), factory(ValueSetIdentifier.PARAMETER_NAME, ValueSetIdentifier::new, ValueSetIdentifier.getNameModifiers()), diff --git a/dsf-fhir/dsf-fhir-server/src/main/java/dev/dsf/fhir/search/SearchQuery.java b/dsf-fhir/dsf-fhir-server/src/main/java/dev/dsf/fhir/search/SearchQuery.java index 58cb1eb92..56a0f274e 100755 --- a/dsf-fhir/dsf-fhir-server/src/main/java/dev/dsf/fhir/search/SearchQuery.java +++ b/dsf-fhir/dsf-fhir-server/src/main/java/dev/dsf/fhir/search/SearchQuery.java @@ -36,6 +36,7 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import dev.dsf.fhir.dao.jdbc.PgObjectFactory; import dev.dsf.fhir.dao.provider.DaoProvider; import dev.dsf.fhir.function.BiFunctionWithSqlException; import dev.dsf.fhir.search.SearchQueryParameterError.SearchQueryParameterErrorType; @@ -61,12 +62,14 @@ public class SearchQuery implements DbSearchQuery, Matcher public static class SearchQueryBuilder { - public static SearchQueryBuilder create(Class resourceType, String resourceTable, - String resourceColumn, PageAndCount pageAndCount) + public static SearchQueryBuilder create(PgObjectFactory pgObjectFactory, + Class resourceType, String resourceTable, String resourceColumn, PageAndCount pageAndCount) { - return new SearchQueryBuilder<>(resourceType, resourceTable, resourceColumn, pageAndCount); + return new SearchQueryBuilder<>(pgObjectFactory, resourceType, resourceTable, resourceColumn, pageAndCount); } + private final PgObjectFactory pgObjectFactory; + private final Class resourceType; private final String resourceTable; private final String resourceColumn; @@ -78,9 +81,11 @@ public static SearchQueryBuilder create(Class resourc private SearchQueryIdentityFilter identityFilter; // may be null - private SearchQueryBuilder(Class resourceType, String resourceTable, String resourceColumn, - PageAndCount pageAndCount) + private SearchQueryBuilder(PgObjectFactory pgObjectFactory, Class resourceType, String resourceTable, + String resourceColumn, PageAndCount pageAndCount) { + this.pgObjectFactory = pgObjectFactory; + this.resourceType = resourceType; this.resourceTable = resourceTable; this.resourceColumn = resourceColumn; @@ -125,13 +130,15 @@ public SearchQueryBuilder withRevInclude(List build() { - return new SearchQuery<>(resourceType, resourceTable, resourceColumn, identityFilter, pageAndCount, - searchParameters, revIncludeParameters); + return new SearchQuery<>(pgObjectFactory, resourceType, resourceTable, resourceColumn, identityFilter, + pageAndCount, searchParameters, revIncludeParameters); } } private static final Logger logger = LoggerFactory.getLogger(SearchQuery.class); + private final PgObjectFactory pgObjectFactory; + private final Class resourceType; private final String resourceColumn; private final String resourceTable; @@ -156,11 +163,13 @@ public SearchQuery build() private String includeSql; private String revIncludeSql; - SearchQuery(Class resourceType, String resourceTable, String resourceColumn, + SearchQuery(PgObjectFactory pgObjectFactory, Class resourceType, String resourceTable, String resourceColumn, SearchQueryIdentityFilter identityFilter, PageAndCount pageAndCount, List> searchParameterFactories, List searchRevIncludeParameterFactories) { + this.pgObjectFactory = pgObjectFactory; + this.resourceType = resourceType; this.resourceTable = resourceTable; this.resourceColumn = resourceColumn; @@ -437,13 +446,13 @@ public void modifyStatement(PreparedStatement statement, while (index < identityFilter.getSqlParameterCount()) { int i = ++index; - identityFilter.modifyStatement(i, i, statement); + identityFilter.modifyStatement(i, i, statement, pgObjectFactory); } } for (SearchQueryParameter q : filtered) for (int i = 0; i < q.getSqlParameterCount(); i++) - q.modifyStatement(++index, i + 1, statement, arrayCreator); + q.modifyStatement(++index, i + 1, statement, arrayCreator, pgObjectFactory); } catch (SQLException e) { diff --git a/dsf-fhir/dsf-fhir-server/src/main/java/dev/dsf/fhir/search/SearchQueryIdentityFilter.java b/dsf-fhir/dsf-fhir-server/src/main/java/dev/dsf/fhir/search/SearchQueryIdentityFilter.java index e74cbe57a..917f81671 100644 --- a/dsf-fhir/dsf-fhir-server/src/main/java/dev/dsf/fhir/search/SearchQueryIdentityFilter.java +++ b/dsf-fhir/dsf-fhir-server/src/main/java/dev/dsf/fhir/search/SearchQueryIdentityFilter.java @@ -18,6 +18,8 @@ import java.sql.PreparedStatement; import java.sql.SQLException; +import dev.dsf.fhir.dao.jdbc.PgObjectFactory; + public interface SearchQueryIdentityFilter { /** @@ -37,9 +39,11 @@ public interface SearchQueryIdentityFilter * [1 ... {@link #getSqlParameterCount()}] * @param statement * not null + * @param pgObjectFactory + * not null * @throws SQLException * if errors occur during modification of the statement */ - void modifyStatement(int parameterIndex, int subqueryParameterIndex, PreparedStatement statement) - throws SQLException; + void modifyStatement(int parameterIndex, int subqueryParameterIndex, PreparedStatement statement, + PgObjectFactory pgObjectFactory) throws SQLException; } diff --git a/dsf-fhir/dsf-fhir-server/src/main/java/dev/dsf/fhir/search/SearchQueryParameter.java b/dsf-fhir/dsf-fhir-server/src/main/java/dev/dsf/fhir/search/SearchQueryParameter.java index ca776361d..7a42e2c6c 100755 --- a/dsf-fhir/dsf-fhir-server/src/main/java/dev/dsf/fhir/search/SearchQueryParameter.java +++ b/dsf-fhir/dsf-fhir-server/src/main/java/dev/dsf/fhir/search/SearchQueryParameter.java @@ -28,6 +28,7 @@ import org.hl7.fhir.r4.model.Enumerations.SearchParamType; import org.hl7.fhir.r4.model.Resource; +import dev.dsf.fhir.dao.jdbc.PgObjectFactory; import dev.dsf.fhir.function.BiFunctionWithSqlException; import dev.dsf.fhir.search.parameters.SearchQuerySortParameter; @@ -66,7 +67,8 @@ SearchQueryParameter configure(List errors int getSqlParameterCount(); void modifyStatement(int parameterIndex, int subqueryParameterIndex, PreparedStatement statement, - BiFunctionWithSqlException arrayCreator) throws SQLException; + BiFunctionWithSqlException arrayCreator, PgObjectFactory pgObjectFactory) + throws SQLException; /** * Only called if {@link #isDefined()} returns true diff --git a/dsf-fhir/dsf-fhir-server/src/main/java/dev/dsf/fhir/search/filter/AbstractMetaTagAuthorizationRoleIdentityFilter.java b/dsf-fhir/dsf-fhir-server/src/main/java/dev/dsf/fhir/search/filter/AbstractMetaTagAuthorizationRoleIdentityFilter.java index ea22b0942..51c6008d7 100644 --- a/dsf-fhir/dsf-fhir-server/src/main/java/dev/dsf/fhir/search/filter/AbstractMetaTagAuthorizationRoleIdentityFilter.java +++ b/dsf-fhir/dsf-fhir-server/src/main/java/dev/dsf/fhir/search/filter/AbstractMetaTagAuthorizationRoleIdentityFilter.java @@ -22,6 +22,7 @@ import dev.dsf.common.auth.conf.Identity; import dev.dsf.fhir.authentication.FhirServerRole; +import dev.dsf.fhir.dao.jdbc.PgObjectFactory; abstract class AbstractMetaTagAuthorizationRoleIdentityFilter extends AbstractIdentityFilter { @@ -59,8 +60,8 @@ public int getSqlParameterCount() } @Override - public void modifyStatement(int parameterIndex, int subqueryParameterIndex, PreparedStatement statement) - throws SQLException + public void modifyStatement(int parameterIndex, int subqueryParameterIndex, PreparedStatement statement, + PgObjectFactory pgObjectFactory) throws SQLException { if (identity.hasDsfRole(operationRole) && identity.hasDsfRole(readRole)) { diff --git a/dsf-fhir/dsf-fhir-server/src/main/java/dev/dsf/fhir/search/filter/QuestionnaireResponseIdentityFilter.java b/dsf-fhir/dsf-fhir-server/src/main/java/dev/dsf/fhir/search/filter/QuestionnaireResponseIdentityFilter.java index c0980de93..c119363b6 100644 --- a/dsf-fhir/dsf-fhir-server/src/main/java/dev/dsf/fhir/search/filter/QuestionnaireResponseIdentityFilter.java +++ b/dsf-fhir/dsf-fhir-server/src/main/java/dev/dsf/fhir/search/filter/QuestionnaireResponseIdentityFilter.java @@ -17,10 +17,7 @@ import java.sql.PreparedStatement; import java.sql.SQLException; -import java.util.Set; -import java.util.stream.Collectors; -import org.hl7.fhir.r4.model.Coding; import org.hl7.fhir.r4.model.ResourceType; import dev.dsf.common.auth.conf.Identity; @@ -28,6 +25,9 @@ import dev.dsf.common.auth.conf.PractitionerIdentity; import dev.dsf.fhir.authentication.FhirServerRole; import dev.dsf.fhir.authentication.FhirServerRoleImpl; +import dev.dsf.fhir.dao.jdbc.PgObjectFactory; +import dev.dsf.fhir.dao.jdbc.PgObjectFactory.CodingParameter; +import dev.dsf.fhir.dao.jdbc.PgObjectFactory.JsonParameter; public class QuestionnaireResponseIdentityFilter extends AbstractIdentityFilter { @@ -66,7 +66,7 @@ else if (identity instanceof PractitionerIdentity p && p.getPractitionerIdentifi + "AND EXISTS (SELECT 1 FROM jsonb_array_elements(authExt->'extension') AS ext " + "WHERE ((ext->>'url' = 'practitioner' AND ext->'valueIdentifier'->>'value' = ?) " + "OR (ext->>'url' = 'practitioner-role' AND (" - + "SELECT COUNT(*) FROM jsonb_array_elements(?::jsonb) AS allowed_roles " + + "SELECT COUNT(*) FROM jsonb_array_elements(?) AS allowed_roles " + "WHERE allowed_roles->>'system' = ext->'valueCoding'->>'system' AND allowed_roles->>'code' = ext->'valueCoding'->>'code'" + ") > 0))))"; } @@ -86,8 +86,8 @@ public int getSqlParameterCount() } @Override - public void modifyStatement(int parameterIndex, int subqueryParameterIndex, PreparedStatement statement) - throws SQLException + public void modifyStatement(int parameterIndex, int subqueryParameterIndex, PreparedStatement statement, + PgObjectFactory pgObjectFactory) throws SQLException { if (identity.isLocalIdentity() && identity.hasDsfRole(operationRole) && identity.hasDsfRole(READ_ROLE) && identity instanceof PractitionerIdentity p && !p.hasPractionerRole("DSF_ADMIN") @@ -96,13 +96,8 @@ public void modifyStatement(int parameterIndex, int subqueryParameterIndex, Prep if (subqueryParameterIndex == 1) statement.setString(parameterIndex, p.getPractitionerIdentifierValue()); else if (subqueryParameterIndex == 2) - statement.setString(parameterIndex, toJson(p.getPractionerRoles())); + statement.setObject(parameterIndex, pgObjectFactory.jsonParameterToPgObjectAsArray( + p.getPractionerRoles().stream().map(CodingParameter::coding).toArray(JsonParameter[]::new))); } } - - private String toJson(Set roles) - { - return roles.stream().map(c -> "{\"system\":\"%s\",\"code\":\"%s\"}".formatted(c.getSystem(), c.getCode())) - .collect(Collectors.joining(",", "[", "]")); - } } diff --git a/dsf-fhir/dsf-fhir-server/src/main/java/dev/dsf/fhir/search/filter/TaskIdentityFilter.java b/dsf-fhir/dsf-fhir-server/src/main/java/dev/dsf/fhir/search/filter/TaskIdentityFilter.java index a8c31f5ac..49281333b 100644 --- a/dsf-fhir/dsf-fhir-server/src/main/java/dev/dsf/fhir/search/filter/TaskIdentityFilter.java +++ b/dsf-fhir/dsf-fhir-server/src/main/java/dev/dsf/fhir/search/filter/TaskIdentityFilter.java @@ -25,6 +25,8 @@ import dev.dsf.common.auth.conf.PractitionerIdentity; import dev.dsf.fhir.authentication.FhirServerRole; import dev.dsf.fhir.authentication.FhirServerRoleImpl; +import dev.dsf.fhir.dao.jdbc.PgObjectFactory; +import dev.dsf.fhir.dao.jdbc.PgObjectFactory.ReferenceParameter; public class TaskIdentityFilter extends AbstractIdentityFilter { @@ -57,25 +59,25 @@ public String getFilterQuery() if (identity instanceof OrganizationIdentity) { if (identity.isLocalIdentity()) - return resourceColumn + "->'restriction'->'recipient' @> ?::jsonb"; + return resourceColumn + "->'restriction'->'recipient' @> ?"; else return resourceColumn + "->'requester'->>'reference' = ?"; } else if (identity instanceof PractitionerIdentity p) { if (p.hasPractionerRole("DSF_ADMIN")) - return resourceColumn + "->'restriction'->'recipient' @> ?::jsonb"; + return resourceColumn + "->'restriction'->'recipient' @> ?"; else if (p.getPractitionerIdentifierValue() != null) { return "((" + resourceColumn + "->'requester'->'identifier'->>'system' = '" + PractitionerIdentity.PRACTITIONER_IDENTIFIER_SYSTEM + "' AND " + resourceColumn + "->'requester'->'identifier'->>'value' = ?) OR (" + resourceColumn - + "->>'status' = 'draft' AND " + resourceColumn + "->'restriction'->'recipient' @> ?::jsonb" + + "->>'status' = 'draft' AND " + resourceColumn + "->'restriction'->'recipient' @> ?" + "))"; } else return "(" + resourceColumn + "->>'status' = 'draft' AND " + resourceColumn - + "->'restriction'->'recipient' @> ?::jsonb" + ")"; + + "->'restriction'->'recipient' @> ?" + ")"; } } @@ -104,8 +106,8 @@ else if (p.getPractitionerIdentifierValue() != null) } @Override - public void modifyStatement(int parameterIndex, int subqueryParameterIndex, PreparedStatement statement) - throws SQLException + public void modifyStatement(int parameterIndex, int subqueryParameterIndex, PreparedStatement statement, + PgObjectFactory pgObjectFactory) throws SQLException { if (identity.hasDsfRole(operationRole) && identity.hasDsfRole(READ_ROLE)) { @@ -113,8 +115,9 @@ public void modifyStatement(int parameterIndex, int subqueryParameterIndex, Prep { if (identity.isLocalIdentity()) { - statement.setString(parameterIndex, "[{\"reference\": \"" - + identity.getOrganization().getIdElement().toUnqualifiedVersionless().getValue() + "\"}]"); + statement.setObject(parameterIndex, + pgObjectFactory.jsonParameterToPgObjectAsArray(new ReferenceParameter( + identity.getOrganization().getIdElement().toUnqualifiedVersionless().getValue()))); } else { @@ -126,8 +129,9 @@ else if (identity instanceof PractitionerIdentity p) { if (p.hasPractionerRole("DSF_ADMIN")) { - statement.setString(parameterIndex, "[{\"reference\": \"" - + identity.getOrganization().getIdElement().toUnqualifiedVersionless().getValue() + "\"}]"); + statement.setObject(parameterIndex, + pgObjectFactory.jsonParameterToPgObjectAsArray(new ReferenceParameter( + identity.getOrganization().getIdElement().toUnqualifiedVersionless().getValue()))); } else if (p.getPractitionerIdentifierValue() != null) { @@ -135,15 +139,16 @@ else if (p.getPractitionerIdentifierValue() != null) statement.setString(parameterIndex, p.getPractitionerIdentifierValue()); else if (subqueryParameterIndex == 2) { - statement.setString(parameterIndex, "[{\"reference\": \"" - + identity.getOrganization().getIdElement().toUnqualifiedVersionless().getValue() - + "\"}]"); + statement.setObject(parameterIndex, + pgObjectFactory.jsonParameterToPgObjectAsArray(new ReferenceParameter(identity + .getOrganization().getIdElement().toUnqualifiedVersionless().getValue()))); } } else { - statement.setString(parameterIndex, "[{\"reference\": \"" - + identity.getOrganization().getIdElement().toUnqualifiedVersionless().getValue() + "\"}]"); + statement.setObject(parameterIndex, + pgObjectFactory.jsonParameterToPgObjectAsArray(new ReferenceParameter( + identity.getOrganization().getIdElement().toUnqualifiedVersionless().getValue()))); } } } diff --git a/dsf-fhir/dsf-fhir-server/src/main/java/dev/dsf/fhir/search/parameters/BinaryContentType.java b/dsf-fhir/dsf-fhir-server/src/main/java/dev/dsf/fhir/search/parameters/BinaryContentType.java index 33d5008b5..7eac7d451 100755 --- a/dsf-fhir/dsf-fhir-server/src/main/java/dev/dsf/fhir/search/parameters/BinaryContentType.java +++ b/dsf-fhir/dsf-fhir-server/src/main/java/dev/dsf/fhir/search/parameters/BinaryContentType.java @@ -24,6 +24,7 @@ import org.hl7.fhir.r4.model.CodeType; import org.hl7.fhir.r4.model.Enumerations.SearchParamType; +import dev.dsf.fhir.dao.jdbc.PgObjectFactory; import dev.dsf.fhir.function.BiFunctionWithSqlException; import dev.dsf.fhir.search.SearchQueryParameter.SearchParameterDefinition; import dev.dsf.fhir.search.SearchQueryParameterError; @@ -86,7 +87,8 @@ public int getSqlParameterCount() @Override public void modifyStatement(int parameterIndex, int subqueryParameterIndex, PreparedStatement statement, - BiFunctionWithSqlException arrayCreator) throws SQLException + BiFunctionWithSqlException arrayCreator, PgObjectFactory pgObjectFactory) + throws SQLException { statement.setString(parameterIndex, contentType.getValue()); } diff --git a/dsf-fhir/dsf-fhir-server/src/main/java/dev/dsf/fhir/search/parameters/DocumentReferenceIdentifier.java b/dsf-fhir/dsf-fhir-server/src/main/java/dev/dsf/fhir/search/parameters/DocumentReferenceIdentifier.java index a4f3d0e89..29084a699 100644 --- a/dsf-fhir/dsf-fhir-server/src/main/java/dev/dsf/fhir/search/parameters/DocumentReferenceIdentifier.java +++ b/dsf-fhir/dsf-fhir-server/src/main/java/dev/dsf/fhir/search/parameters/DocumentReferenceIdentifier.java @@ -22,6 +22,8 @@ import org.hl7.fhir.r4.model.DocumentReference; import org.hl7.fhir.r4.model.Enumerations.SearchParamType; +import dev.dsf.fhir.dao.jdbc.PgObjectFactory; +import dev.dsf.fhir.dao.jdbc.PgObjectFactory.IdentifierParameter; import dev.dsf.fhir.function.BiFunctionWithSqlException; import dev.dsf.fhir.search.SearchQueryParameter.SearchParameterDefinition; import dev.dsf.fhir.search.parameters.basic.AbstractIdentifierParameter; @@ -43,11 +45,14 @@ protected String getPositiveFilterQuery() return switch (valueAndType.type) { case CODE -> - "(document_reference->'identifier' @> ?::jsonb OR document_reference->'masterIdentifier'->>'value' = ?)"; + "(document_reference->'identifier' @> ? OR document_reference->'masterIdentifier'->>'value' = ?)"; + case CODE_AND_SYSTEM -> - "(document_reference->'identifier' @> ?::jsonb OR (document_reference->'masterIdentifier'->>'value' = ? AND document_reference->'masterIdentifier'->>'system' = ?))"; + "(document_reference->'identifier' @> ? OR (document_reference->'masterIdentifier'->>'value' = ? AND document_reference->'masterIdentifier'->>'system' = ?))"; + case SYSTEM -> - "(document_reference->'identifier' @> ?::jsonb OR document_reference->'masterIdentifier'->>'system' = ?)"; + "(document_reference->'identifier' @> ? OR document_reference->'masterIdentifier'->>'system' = ?)"; + case CODE_AND_NO_SYSTEM_PROPERTY -> "(SELECT count(*) FROM (" + "SELECT identifier FROM jsonb_array_elements(document_reference->'identifier') AS identifier " + "UNION SELECT document_reference->'masterIdentifier') AS document_reference_identifiers " @@ -61,11 +66,14 @@ protected String getNegatedFilterQuery() return switch (valueAndType.type) { case CODE -> - "NOT (document_reference->'identifier' @> ?::jsonb OR document_reference->'masterIdentifier'->>'value' = ?)"; + "NOT (document_reference->'identifier' @> ? OR document_reference->'masterIdentifier'->>'value' = ?)"; + case CODE_AND_SYSTEM -> - "NOT (document_reference->'identifier' @> ?::jsonb OR (document_reference->'masterIdentifier'->>'value' = ? AND document_reference->'masterIdentifier'->>'system' = ?))"; + "NOT (document_reference->'identifier' @> ? OR (document_reference->'masterIdentifier'->>'value' = ? AND document_reference->'masterIdentifier'->>'system' = ?))"; + case SYSTEM -> - "NOT (document_reference->'identifier' @> ?::jsonb OR document_reference->'masterIdentifier'->>'system' = ?)"; + "NOT (document_reference->'identifier' @> ? OR document_reference->'masterIdentifier'->>'system' = ?)"; + case CODE_AND_NO_SYSTEM_PROPERTY -> "(SELECT count(*) FROM (" + "SELECT identifier FROM jsonb_array_elements(document_reference->'identifier') AS identifier " + "UNION SELECT document_reference->'masterIdentifier') AS document_reference_identifiers " @@ -87,37 +95,38 @@ public int getSqlParameterCount() @Override public void modifyStatement(int parameterIndex, int subqueryParameterIndex, PreparedStatement statement, - BiFunctionWithSqlException arrayCreator) throws SQLException + BiFunctionWithSqlException arrayCreator, PgObjectFactory pgObjectFactory) + throws SQLException { switch (valueAndType.type) { - case CODE: + case CODE -> { if (subqueryParameterIndex == 1) - statement.setString(parameterIndex, "[{\"value\": \"" + valueAndType.codeValue + "\"}]"); + statement.setObject(parameterIndex, pgObjectFactory + .jsonParameterToPgObjectAsArray(new IdentifierParameter(null, valueAndType.codeValue))); else if (subqueryParameterIndex == 2) statement.setString(parameterIndex, valueAndType.codeValue); - return; + } - case CODE_AND_SYSTEM: + case CODE_AND_SYSTEM -> { if (subqueryParameterIndex == 1) - statement.setString(parameterIndex, "[{\"value\": \"" + valueAndType.codeValue - + "\", \"system\": \"" + valueAndType.systemValue + "\"}]"); + statement.setObject(parameterIndex, pgObjectFactory.jsonParameterToPgObjectAsArray( + new IdentifierParameter(valueAndType.systemValue, valueAndType.codeValue))); else if (subqueryParameterIndex == 2) statement.setString(parameterIndex, valueAndType.codeValue); else if (subqueryParameterIndex == 3) statement.setString(parameterIndex, valueAndType.systemValue); - return; + } - case SYSTEM: + case SYSTEM -> { if (subqueryParameterIndex == 1) - statement.setString(parameterIndex, "[{\"system\": \"" + valueAndType.systemValue + "\"}]"); + statement.setObject(parameterIndex, pgObjectFactory + .jsonParameterToPgObjectAsArray(new IdentifierParameter(valueAndType.systemValue, null))); else if (subqueryParameterIndex == 2) statement.setString(parameterIndex, valueAndType.systemValue); - return; + } - case CODE_AND_NO_SYSTEM_PROPERTY: - statement.setString(parameterIndex, valueAndType.codeValue); - return; + case CODE_AND_NO_SYSTEM_PROPERTY -> statement.setString(parameterIndex, valueAndType.codeValue); } } diff --git a/dsf-fhir/dsf-fhir-server/src/main/java/dev/dsf/fhir/search/parameters/EndpointAddress.java b/dsf-fhir/dsf-fhir-server/src/main/java/dev/dsf/fhir/search/parameters/EndpointAddress.java index 7b78a9be9..c8bcc5033 100644 --- a/dsf-fhir/dsf-fhir-server/src/main/java/dev/dsf/fhir/search/parameters/EndpointAddress.java +++ b/dsf-fhir/dsf-fhir-server/src/main/java/dev/dsf/fhir/search/parameters/EndpointAddress.java @@ -23,6 +23,7 @@ import org.hl7.fhir.r4.model.Endpoint; import org.hl7.fhir.r4.model.Enumerations.SearchParamType; +import dev.dsf.fhir.dao.jdbc.PgObjectFactory; import dev.dsf.fhir.function.BiFunctionWithSqlException; import dev.dsf.fhir.search.SearchQueryParameter.SearchParameterDefinition; import dev.dsf.fhir.search.parameters.basic.AbstractCanonicalUrlParameter; @@ -55,7 +56,8 @@ public int getSqlParameterCount() @Override public void modifyStatement(int parameterIndex, int subqueryParameterIndex, PreparedStatement statement, - BiFunctionWithSqlException arrayCreator) throws SQLException + BiFunctionWithSqlException arrayCreator, PgObjectFactory pgObjectFactory) + throws SQLException { switch (valueAndType.type) { diff --git a/dsf-fhir/dsf-fhir-server/src/main/java/dev/dsf/fhir/search/parameters/EndpointOrganization.java b/dsf-fhir/dsf-fhir-server/src/main/java/dev/dsf/fhir/search/parameters/EndpointOrganization.java index d6d2ff64b..918d686ac 100755 --- a/dsf-fhir/dsf-fhir-server/src/main/java/dev/dsf/fhir/search/parameters/EndpointOrganization.java +++ b/dsf-fhir/dsf-fhir-server/src/main/java/dev/dsf/fhir/search/parameters/EndpointOrganization.java @@ -31,6 +31,8 @@ import dev.dsf.fhir.dao.OrganizationDao; import dev.dsf.fhir.dao.exception.ResourceDeletedException; +import dev.dsf.fhir.dao.jdbc.PgObjectFactory; +import dev.dsf.fhir.dao.jdbc.PgObjectFactory.IdentifierParameter; import dev.dsf.fhir.dao.provider.DaoProvider; import dev.dsf.fhir.function.BiFunctionWithSqlException; import dev.dsf.fhir.search.IncludeParameterDefinition; @@ -68,9 +70,11 @@ public String getFilterQuery() { case ID, RESOURCE_NAME_AND_ID, URL, TYPE_AND_ID, TYPE_AND_RESOURCE_NAME_AND_ID -> "endpoint->'managingOrganization'->>'reference' = ?"; + case IDENTIFIER -> switch (valueAndType.identifier.type) { - case CODE, CODE_AND_SYSTEM, SYSTEM -> IDENTIFIERS_SUBQUERY + " @> ?::jsonb"; + case CODE, CODE_AND_SYSTEM, SYSTEM -> IDENTIFIERS_SUBQUERY + " @> ?"; + case CODE_AND_NO_SYSTEM_PROPERTY -> "(SELECT count(*) FROM jsonb_array_elements(" + IDENTIFIERS_SUBQUERY + ") identifier WHERE identifier->>'value' = ? AND NOT (identifier ?? 'system')) > 0"; }; @@ -85,38 +89,31 @@ public int getSqlParameterCount() @Override public void modifyStatement(int parameterIndex, int subqueryParameterIndex, PreparedStatement statement, - BiFunctionWithSqlException arrayCreator) throws SQLException + BiFunctionWithSqlException arrayCreator, PgObjectFactory pgObjectFactory) + throws SQLException { switch (valueAndType.type) { - case ID: - case RESOURCE_NAME_AND_ID: - case TYPE_AND_ID: - case TYPE_AND_RESOURCE_NAME_AND_ID: + case ID, RESOURCE_NAME_AND_ID, TYPE_AND_ID, TYPE_AND_RESOURCE_NAME_AND_ID -> statement.setString(parameterIndex, TARGET_RESOURCE_TYPE_NAME + "/" + valueAndType.id); - break; - case URL: - statement.setString(parameterIndex, valueAndType.url); - break; - case IDENTIFIER: - { + + case URL -> statement.setString(parameterIndex, valueAndType.url); + + case IDENTIFIER -> { switch (valueAndType.identifier.type) { - case CODE: - statement.setString(parameterIndex, - "[{\"value\": \"" + valueAndType.identifier.codeValue + "\"}]"); - break; - case CODE_AND_SYSTEM: - statement.setString(parameterIndex, "[{\"value\": \"" + valueAndType.identifier.codeValue - + "\", \"system\": \"" + valueAndType.identifier.systemValue + "\"}]"); - break; - case CODE_AND_NO_SYSTEM_PROPERTY: + case CODE -> statement.setObject(parameterIndex, pgObjectFactory.jsonParameterToPgObjectAsArray( + new IdentifierParameter(null, valueAndType.identifier.codeValue))); + + case CODE_AND_SYSTEM -> statement.setObject(parameterIndex, + pgObjectFactory.jsonParameterToPgObjectAsArray(new IdentifierParameter( + valueAndType.identifier.systemValue, valueAndType.identifier.codeValue))); + + case SYSTEM -> statement.setObject(parameterIndex, pgObjectFactory.jsonParameterToPgObjectAsArray( + new IdentifierParameter(valueAndType.identifier.systemValue, null))); + + case CODE_AND_NO_SYSTEM_PROPERTY -> statement.setString(parameterIndex, valueAndType.identifier.codeValue); - break; - case SYSTEM: - statement.setString(parameterIndex, - "[{\"system\": \"" + valueAndType.identifier.systemValue + "\"}]"); - break; } } } diff --git a/dsf-fhir/dsf-fhir-server/src/main/java/dev/dsf/fhir/search/parameters/EndpointStatus.java b/dsf-fhir/dsf-fhir-server/src/main/java/dev/dsf/fhir/search/parameters/EndpointStatus.java index 44df1363b..1bd80cf25 100755 --- a/dsf-fhir/dsf-fhir-server/src/main/java/dev/dsf/fhir/search/parameters/EndpointStatus.java +++ b/dsf-fhir/dsf-fhir-server/src/main/java/dev/dsf/fhir/search/parameters/EndpointStatus.java @@ -25,6 +25,7 @@ import org.hl7.fhir.r4.model.Endpoint; import org.hl7.fhir.r4.model.Enumerations.SearchParamType; +import dev.dsf.fhir.dao.jdbc.PgObjectFactory; import dev.dsf.fhir.function.BiFunctionWithSqlException; import dev.dsf.fhir.search.SearchQueryParameter.SearchParameterDefinition; import dev.dsf.fhir.search.SearchQueryParameterError; @@ -98,7 +99,8 @@ public int getSqlParameterCount() @Override public void modifyStatement(int parameterIndex, int subqueryParameterIndex, PreparedStatement statement, - BiFunctionWithSqlException arrayCreator) throws SQLException + BiFunctionWithSqlException arrayCreator, PgObjectFactory pgObjectFactory) + throws SQLException { statement.setString(parameterIndex, status.toCode()); } diff --git a/dsf-fhir/dsf-fhir-server/src/main/java/dev/dsf/fhir/search/parameters/MeasureDependsOn.java b/dsf-fhir/dsf-fhir-server/src/main/java/dev/dsf/fhir/search/parameters/MeasureDependsOn.java index be9f323d3..80fb21d7e 100755 --- a/dsf-fhir/dsf-fhir-server/src/main/java/dev/dsf/fhir/search/parameters/MeasureDependsOn.java +++ b/dsf-fhir/dsf-fhir-server/src/main/java/dev/dsf/fhir/search/parameters/MeasureDependsOn.java @@ -27,6 +27,8 @@ import org.hl7.fhir.r4.model.Measure; import org.hl7.fhir.r4.model.Resource; +import dev.dsf.fhir.dao.jdbc.PgObjectFactory; +import dev.dsf.fhir.dao.jdbc.PgObjectFactory.RelatedArtifactParameter; import dev.dsf.fhir.dao.provider.DaoProvider; import dev.dsf.fhir.function.BiFunctionWithSqlException; import dev.dsf.fhir.search.IncludeParameterDefinition; @@ -63,7 +65,7 @@ public boolean isDefined() public String getFilterQuery() { if (ReferenceSearchType.URL.equals(valueAndType.type)) - return "(measure->'library' ?? ? OR measure->'relatedArtifact' @> ?::jsonb)"; + return "(measure->'library' ?? ? OR measure->'relatedArtifact' @> ?)"; return ""; } @@ -76,15 +78,16 @@ public int getSqlParameterCount() @Override public void modifyStatement(int parameterIndex, int subqueryParameterIndex, PreparedStatement statement, - BiFunctionWithSqlException arrayCreator) throws SQLException + BiFunctionWithSqlException arrayCreator, PgObjectFactory pgObjectFactory) + throws SQLException { if (ReferenceSearchType.URL.equals(valueAndType.type)) { if (subqueryParameterIndex == 1) statement.setString(parameterIndex, valueAndType.url); else if (subqueryParameterIndex == 2) - statement.setString(parameterIndex, - "[{\"type\": \"depends-on\", \"resource\": \"" + valueAndType.url + "\"}]"); + statement.setObject(parameterIndex, pgObjectFactory + .jsonParameterToPgObjectAsArray(RelatedArtifactParameter.dependsOn(valueAndType.url))); } } diff --git a/dsf-fhir/dsf-fhir-server/src/main/java/dev/dsf/fhir/search/parameters/OrganizationAffiliationEndpoint.java b/dsf-fhir/dsf-fhir-server/src/main/java/dev/dsf/fhir/search/parameters/OrganizationAffiliationEndpoint.java index ee154833e..64f621f99 100644 --- a/dsf-fhir/dsf-fhir-server/src/main/java/dev/dsf/fhir/search/parameters/OrganizationAffiliationEndpoint.java +++ b/dsf-fhir/dsf-fhir-server/src/main/java/dev/dsf/fhir/search/parameters/OrganizationAffiliationEndpoint.java @@ -31,6 +31,8 @@ import dev.dsf.fhir.dao.EndpointDao; import dev.dsf.fhir.dao.exception.ResourceDeletedException; +import dev.dsf.fhir.dao.jdbc.PgObjectFactory; +import dev.dsf.fhir.dao.jdbc.PgObjectFactory.IdentifierParameter; import dev.dsf.fhir.dao.provider.DaoProvider; import dev.dsf.fhir.function.BiFunctionWithSqlException; import dev.dsf.fhir.search.IncludeParameterDefinition; @@ -65,12 +67,14 @@ public String getFilterQuery() { case ID, RESOURCE_NAME_AND_ID, URL, TYPE_AND_ID, TYPE_AND_RESOURCE_NAME_AND_ID -> "? IN (SELECT reference->>'reference' FROM jsonb_array_elements(organization_affiliation->'endpoint') AS reference)"; + case IDENTIFIER -> switch (valueAndType.identifier.type) { case CODE, CODE_AND_SYSTEM, SYSTEM -> "(SELECT jsonb_agg(identifier) FROM (SELECT identifier FROM current_endpoints, jsonb_array_elements(endpoint->'identifier') identifier" + " WHERE concat('Endpoint/', endpoint->>'id') IN (SELECT reference->>'reference' FROM jsonb_array_elements(organization_affiliation->'endpoint') reference)" - + " ) AS identifiers) @> ?::jsonb"; + + " ) AS identifiers) @> ?"; + case CODE_AND_NO_SYSTEM_PROPERTY -> "(SELECT count(*) FROM (SELECT identifier FROM current_endpoints, jsonb_array_elements(endpoint->'identifier') identifier" + " WHERE concat('Endpoint/', endpoint->>'id') IN (SELECT reference->>'reference' FROM jsonb_array_elements(organization_affiliation->'endpoint') reference)" @@ -87,38 +91,31 @@ public int getSqlParameterCount() @Override public void modifyStatement(int parameterIndex, int subqueryParameterIndex, PreparedStatement statement, - BiFunctionWithSqlException arrayCreator) throws SQLException + BiFunctionWithSqlException arrayCreator, PgObjectFactory pgObjectFactory) + throws SQLException { switch (valueAndType.type) { - case ID: - case RESOURCE_NAME_AND_ID: - case TYPE_AND_ID: - case TYPE_AND_RESOURCE_NAME_AND_ID: + case ID, RESOURCE_NAME_AND_ID, TYPE_AND_ID, TYPE_AND_RESOURCE_NAME_AND_ID -> statement.setString(parameterIndex, TARGET_RESOURCE_TYPE_NAME + "/" + valueAndType.id); - break; - case URL: - statement.setString(parameterIndex, valueAndType.url); - break; - case IDENTIFIER: - { + + case URL -> statement.setString(parameterIndex, valueAndType.url); + + case IDENTIFIER -> { switch (valueAndType.identifier.type) { - case CODE: - statement.setString(parameterIndex, - "[{\"value\": \"" + valueAndType.identifier.codeValue + "\"}]"); - break; - case CODE_AND_SYSTEM: - statement.setString(parameterIndex, "[{\"value\": \"" + valueAndType.identifier.codeValue - + "\", \"system\": \"" + valueAndType.identifier.systemValue + "\"}]"); - break; - case CODE_AND_NO_SYSTEM_PROPERTY: + case CODE -> statement.setObject(parameterIndex, pgObjectFactory.jsonParameterToPgObjectAsArray( + new IdentifierParameter(null, valueAndType.identifier.codeValue))); + + case CODE_AND_SYSTEM -> statement.setObject(parameterIndex, + pgObjectFactory.jsonParameterToPgObjectAsArray(new IdentifierParameter( + valueAndType.identifier.systemValue, valueAndType.identifier.codeValue))); + + case SYSTEM -> statement.setObject(parameterIndex, pgObjectFactory.jsonParameterToPgObjectAsArray( + new IdentifierParameter(valueAndType.identifier.systemValue, null))); + + case CODE_AND_NO_SYSTEM_PROPERTY -> statement.setString(parameterIndex, valueAndType.identifier.codeValue); - break; - case SYSTEM: - statement.setString(parameterIndex, - "[{\"system\": \"" + valueAndType.identifier.systemValue + "\"}]"); - break; } } } diff --git a/dsf-fhir/dsf-fhir-server/src/main/java/dev/dsf/fhir/search/parameters/OrganizationAffiliationParticipatingOrganization.java b/dsf-fhir/dsf-fhir-server/src/main/java/dev/dsf/fhir/search/parameters/OrganizationAffiliationParticipatingOrganization.java index 9570ed7d0..53ce74e35 100644 --- a/dsf-fhir/dsf-fhir-server/src/main/java/dev/dsf/fhir/search/parameters/OrganizationAffiliationParticipatingOrganization.java +++ b/dsf-fhir/dsf-fhir-server/src/main/java/dev/dsf/fhir/search/parameters/OrganizationAffiliationParticipatingOrganization.java @@ -31,6 +31,8 @@ import dev.dsf.fhir.dao.OrganizationDao; import dev.dsf.fhir.dao.exception.ResourceDeletedException; +import dev.dsf.fhir.dao.jdbc.PgObjectFactory; +import dev.dsf.fhir.dao.jdbc.PgObjectFactory.IdentifierParameter; import dev.dsf.fhir.dao.provider.DaoProvider; import dev.dsf.fhir.function.BiFunctionWithSqlException; import dev.dsf.fhir.search.IncludeParameterDefinition; @@ -69,9 +71,11 @@ public String getFilterQuery() { case ID, RESOURCE_NAME_AND_ID, URL, TYPE_AND_ID, TYPE_AND_RESOURCE_NAME_AND_ID -> "organization_affiliation->'participatingOrganization'->>'reference' = ?"; + case IDENTIFIER -> switch (valueAndType.identifier.type) { - case CODE, CODE_AND_SYSTEM, SYSTEM -> IDENTIFIERS_SUBQUERY + " @> ?::jsonb"; + case CODE, CODE_AND_SYSTEM, SYSTEM -> IDENTIFIERS_SUBQUERY + " @> ?"; + case CODE_AND_NO_SYSTEM_PROPERTY -> "(SELECT count(*) FROM jsonb_array_elements(" + IDENTIFIERS_SUBQUERY + ") identifier WHERE identifier->>'value' = ? AND NOT (identifier ?? 'system')) > 0"; }; @@ -86,38 +90,31 @@ public int getSqlParameterCount() @Override public void modifyStatement(int parameterIndex, int subqueryParameterIndex, PreparedStatement statement, - BiFunctionWithSqlException arrayCreator) throws SQLException + BiFunctionWithSqlException arrayCreator, PgObjectFactory pgObjectFactory) + throws SQLException { switch (valueAndType.type) { - case ID: - case RESOURCE_NAME_AND_ID: - case TYPE_AND_ID: - case TYPE_AND_RESOURCE_NAME_AND_ID: + case ID, RESOURCE_NAME_AND_ID, TYPE_AND_ID, TYPE_AND_RESOURCE_NAME_AND_ID -> statement.setString(parameterIndex, TARGET_RESOURCE_TYPE_NAME + "/" + valueAndType.id); - break; - case URL: - statement.setString(parameterIndex, valueAndType.url); - break; - case IDENTIFIER: - { + + case URL -> statement.setString(parameterIndex, valueAndType.url); + + case IDENTIFIER -> { switch (valueAndType.identifier.type) { - case CODE: - statement.setString(parameterIndex, - "[{\"value\": \"" + valueAndType.identifier.codeValue + "\"}]"); - break; - case CODE_AND_SYSTEM: - statement.setString(parameterIndex, "[{\"value\": \"" + valueAndType.identifier.codeValue - + "\", \"system\": \"" + valueAndType.identifier.systemValue + "\"}]"); - break; - case CODE_AND_NO_SYSTEM_PROPERTY: + case CODE -> statement.setObject(parameterIndex, pgObjectFactory.jsonParameterToPgObjectAsArray( + new IdentifierParameter(null, valueAndType.identifier.codeValue))); + + case CODE_AND_SYSTEM -> statement.setObject(parameterIndex, + pgObjectFactory.jsonParameterToPgObjectAsArray(new IdentifierParameter( + valueAndType.identifier.systemValue, valueAndType.identifier.codeValue))); + + case SYSTEM -> statement.setObject(parameterIndex, pgObjectFactory.jsonParameterToPgObjectAsArray( + new IdentifierParameter(valueAndType.identifier.systemValue, null))); + + case CODE_AND_NO_SYSTEM_PROPERTY -> statement.setString(parameterIndex, valueAndType.identifier.codeValue); - break; - case SYSTEM: - statement.setString(parameterIndex, - "[{\"system\": \"" + valueAndType.identifier.systemValue + "\"}]"); - break; } } } diff --git a/dsf-fhir/dsf-fhir-server/src/main/java/dev/dsf/fhir/search/parameters/OrganizationAffiliationPrimaryOrganization.java b/dsf-fhir/dsf-fhir-server/src/main/java/dev/dsf/fhir/search/parameters/OrganizationAffiliationPrimaryOrganization.java index 3f37b35f4..201c43c9c 100644 --- a/dsf-fhir/dsf-fhir-server/src/main/java/dev/dsf/fhir/search/parameters/OrganizationAffiliationPrimaryOrganization.java +++ b/dsf-fhir/dsf-fhir-server/src/main/java/dev/dsf/fhir/search/parameters/OrganizationAffiliationPrimaryOrganization.java @@ -31,6 +31,8 @@ import dev.dsf.fhir.dao.OrganizationDao; import dev.dsf.fhir.dao.exception.ResourceDeletedException; +import dev.dsf.fhir.dao.jdbc.PgObjectFactory; +import dev.dsf.fhir.dao.jdbc.PgObjectFactory.IdentifierParameter; import dev.dsf.fhir.dao.provider.DaoProvider; import dev.dsf.fhir.function.BiFunctionWithSqlException; import dev.dsf.fhir.search.IncludeParameterDefinition; @@ -68,9 +70,11 @@ public String getFilterQuery() { case ID, RESOURCE_NAME_AND_ID, URL, TYPE_AND_ID, TYPE_AND_RESOURCE_NAME_AND_ID -> "organization_affiliation->'organization'->>'reference' = ?"; + case IDENTIFIER -> switch (valueAndType.identifier.type) { - case CODE, CODE_AND_SYSTEM, SYSTEM -> IDENTIFIERS_SUBQUERY + " @> ?::jsonb"; + case CODE, CODE_AND_SYSTEM, SYSTEM -> IDENTIFIERS_SUBQUERY + " @> ?"; + case CODE_AND_NO_SYSTEM_PROPERTY -> "(SELECT count(*) FROM jsonb_array_elements(" + IDENTIFIERS_SUBQUERY + ") identifier WHERE identifier->>'value' = ? AND NOT (identifier ?? 'system')) > 0"; }; @@ -85,38 +89,31 @@ public int getSqlParameterCount() @Override public void modifyStatement(int parameterIndex, int subqueryParameterIndex, PreparedStatement statement, - BiFunctionWithSqlException arrayCreator) throws SQLException + BiFunctionWithSqlException arrayCreator, PgObjectFactory pgObjectFactory) + throws SQLException { switch (valueAndType.type) { - case ID: - case RESOURCE_NAME_AND_ID: - case TYPE_AND_ID: - case TYPE_AND_RESOURCE_NAME_AND_ID: + case ID, RESOURCE_NAME_AND_ID, TYPE_AND_ID, TYPE_AND_RESOURCE_NAME_AND_ID -> statement.setString(parameterIndex, TARGET_RESOURCE_TYPE_NAME + "/" + valueAndType.id); - break; - case URL: - statement.setString(parameterIndex, valueAndType.url); - break; - case IDENTIFIER: - { + + case URL -> statement.setString(parameterIndex, valueAndType.url); + + case IDENTIFIER -> { switch (valueAndType.identifier.type) { - case CODE: - statement.setString(parameterIndex, - "[{\"value\": \"" + valueAndType.identifier.codeValue + "\"}]"); - break; - case CODE_AND_SYSTEM: - statement.setString(parameterIndex, "[{\"value\": \"" + valueAndType.identifier.codeValue - + "\", \"system\": \"" + valueAndType.identifier.systemValue + "\"}]"); - break; - case CODE_AND_NO_SYSTEM_PROPERTY: + case CODE -> statement.setObject(parameterIndex, pgObjectFactory.jsonParameterToPgObjectAsArray( + new IdentifierParameter(null, valueAndType.identifier.codeValue))); + + case CODE_AND_SYSTEM -> statement.setObject(parameterIndex, + pgObjectFactory.jsonParameterToPgObjectAsArray(new IdentifierParameter( + valueAndType.identifier.systemValue, valueAndType.identifier.codeValue))); + + case SYSTEM -> statement.setObject(parameterIndex, pgObjectFactory.jsonParameterToPgObjectAsArray( + new IdentifierParameter(valueAndType.identifier.systemValue, null))); + + case CODE_AND_NO_SYSTEM_PROPERTY -> statement.setString(parameterIndex, valueAndType.identifier.codeValue); - break; - case SYSTEM: - statement.setString(parameterIndex, - "[{\"system\": \"" + valueAndType.identifier.systemValue + "\"}]"); - break; } } } diff --git a/dsf-fhir/dsf-fhir-server/src/main/java/dev/dsf/fhir/search/parameters/OrganizationAffiliationRole.java b/dsf-fhir/dsf-fhir-server/src/main/java/dev/dsf/fhir/search/parameters/OrganizationAffiliationRole.java index 4a2e23c43..cfa322f7b 100644 --- a/dsf-fhir/dsf-fhir-server/src/main/java/dev/dsf/fhir/search/parameters/OrganizationAffiliationRole.java +++ b/dsf-fhir/dsf-fhir-server/src/main/java/dev/dsf/fhir/search/parameters/OrganizationAffiliationRole.java @@ -22,6 +22,8 @@ import org.hl7.fhir.r4.model.Enumerations.SearchParamType; import org.hl7.fhir.r4.model.OrganizationAffiliation; +import dev.dsf.fhir.dao.jdbc.PgObjectFactory; +import dev.dsf.fhir.dao.jdbc.PgObjectFactory.CodingParameter; import dev.dsf.fhir.function.BiFunctionWithSqlException; import dev.dsf.fhir.search.SearchQueryParameter.SearchParameterDefinition; import dev.dsf.fhir.search.parameters.basic.AbstractTokenParameter; @@ -42,7 +44,8 @@ protected String getPositiveFilterQuery() return switch (valueAndType.type) { case CODE, CODE_AND_SYSTEM, SYSTEM -> - "(SELECT jsonb_agg(coding) FROM jsonb_array_elements(organization_affiliation->'code') AS code, jsonb_array_elements(code->'coding') AS coding) @> ?::jsonb"; + "(SELECT jsonb_agg(coding) FROM jsonb_array_elements(organization_affiliation->'code') AS code, jsonb_array_elements(code->'coding') AS coding) @> ?"; + case CODE_AND_NO_SYSTEM_PROPERTY -> "(SELECT COUNT(*) FROM jsonb_array_elements(organization_affiliation->'code') AS code, jsonb_array_elements(code->'coding') AS coding " + "WHERE coding->>'code' = ? AND NOT (coding ?? 'system')) > 0"; @@ -55,7 +58,8 @@ protected String getNegatedFilterQuery() return switch (valueAndType.type) { case CODE, CODE_AND_SYSTEM, SYSTEM -> - "NOT ((SELECT jsonb_agg(coding) FROM jsonb_array_elements(organization_affiliation->'code') AS code, jsonb_array_elements(code->'coding') AS coding) @> ?::jsonb)"; + "NOT ((SELECT jsonb_agg(coding) FROM jsonb_array_elements(organization_affiliation->'code') AS code, jsonb_array_elements(code->'coding') AS coding) @> ?)"; + case CODE_AND_NO_SYSTEM_PROPERTY -> "(SELECT COUNT(*) FROM jsonb_array_elements(organization_affiliation->'code') AS code, jsonb_array_elements(code->'coding') AS coding " + "WHERE coding->>'code' <> ? OR (coding ?? 'system')) > 0"; @@ -70,26 +74,21 @@ public int getSqlParameterCount() @Override public void modifyStatement(int parameterIndex, int subqueryParameterIndex, PreparedStatement statement, - BiFunctionWithSqlException arrayCreator) throws SQLException + BiFunctionWithSqlException arrayCreator, PgObjectFactory pgObjectFactory) + throws SQLException { switch (valueAndType.type) { - case CODE: - statement.setString(parameterIndex, "[{\"code\": \"" + valueAndType.codeValue + "\"}]"); - return; - - case CODE_AND_SYSTEM: - statement.setString(parameterIndex, "[{\"code\": \"" + valueAndType.codeValue + "\", \"system\": \"" - + valueAndType.systemValue + "\"}]"); - return; - - case CODE_AND_NO_SYSTEM_PROPERTY: - statement.setString(parameterIndex, valueAndType.codeValue); - return; - - case SYSTEM: - statement.setString(parameterIndex, "[{\"system\": \"" + valueAndType.systemValue + "\"}]"); - return; + case CODE -> statement.setObject(parameterIndex, + pgObjectFactory.jsonParameterToPgObjectAsArray(new CodingParameter(null, valueAndType.codeValue))); + + case CODE_AND_SYSTEM -> statement.setObject(parameterIndex, pgObjectFactory.jsonParameterToPgObjectAsArray( + new CodingParameter(valueAndType.systemValue, valueAndType.codeValue))); + + case SYSTEM -> statement.setObject(parameterIndex, pgObjectFactory + .jsonParameterToPgObjectAsArray(new CodingParameter(valueAndType.systemValue, null))); + + case CODE_AND_NO_SYSTEM_PROPERTY -> statement.setString(parameterIndex, valueAndType.codeValue); } } diff --git a/dsf-fhir/dsf-fhir-server/src/main/java/dev/dsf/fhir/search/parameters/OrganizationEndpoint.java b/dsf-fhir/dsf-fhir-server/src/main/java/dev/dsf/fhir/search/parameters/OrganizationEndpoint.java index 458670b37..f20ede4ef 100755 --- a/dsf-fhir/dsf-fhir-server/src/main/java/dev/dsf/fhir/search/parameters/OrganizationEndpoint.java +++ b/dsf-fhir/dsf-fhir-server/src/main/java/dev/dsf/fhir/search/parameters/OrganizationEndpoint.java @@ -31,6 +31,8 @@ import dev.dsf.fhir.dao.EndpointDao; import dev.dsf.fhir.dao.exception.ResourceDeletedException; +import dev.dsf.fhir.dao.jdbc.PgObjectFactory; +import dev.dsf.fhir.dao.jdbc.PgObjectFactory.IdentifierParameter; import dev.dsf.fhir.dao.provider.DaoProvider; import dev.dsf.fhir.function.BiFunctionWithSqlException; import dev.dsf.fhir.search.IncludeParameterDefinition; @@ -65,12 +67,14 @@ public String getFilterQuery() { case ID, RESOURCE_NAME_AND_ID, URL, TYPE_AND_ID, TYPE_AND_RESOURCE_NAME_AND_ID -> "? IN (SELECT reference->>'reference' FROM jsonb_array_elements(organization->'endpoint') AS reference)"; + case IDENTIFIER -> switch (valueAndType.identifier.type) { case CODE, CODE_AND_SYSTEM, SYSTEM -> "(SELECT jsonb_agg(identifier) FROM (SELECT identifier FROM current_endpoints, jsonb_array_elements(endpoint->'identifier') identifier" + " WHERE concat('Endpoint/', endpoint->>'id') IN (SELECT reference->>'reference' FROM jsonb_array_elements(organization->'endpoint') reference)" - + " ) AS identifiers) @> ?::jsonb"; + + " ) AS identifiers) @> ?"; + case CODE_AND_NO_SYSTEM_PROPERTY -> "(SELECT count(*) FROM (SELECT identifier FROM current_endpoints, jsonb_array_elements(endpoint->'identifier') identifier" + " WHERE concat('Endpoint/', endpoint->>'id') IN (SELECT reference->>'reference' FROM jsonb_array_elements(organization->'endpoint') reference)" @@ -87,38 +91,31 @@ public int getSqlParameterCount() @Override public void modifyStatement(int parameterIndex, int subqueryParameterIndex, PreparedStatement statement, - BiFunctionWithSqlException arrayCreator) throws SQLException + BiFunctionWithSqlException arrayCreator, PgObjectFactory pgObjectFactory) + throws SQLException { switch (valueAndType.type) { - case ID: - case RESOURCE_NAME_AND_ID: - case TYPE_AND_ID: - case TYPE_AND_RESOURCE_NAME_AND_ID: + case ID, RESOURCE_NAME_AND_ID, TYPE_AND_ID, TYPE_AND_RESOURCE_NAME_AND_ID -> statement.setString(parameterIndex, TARGET_RESOURCE_TYPE_NAME + "/" + valueAndType.id); - break; - case URL: - statement.setString(parameterIndex, valueAndType.url); - break; - case IDENTIFIER: - { + + case URL -> statement.setString(parameterIndex, valueAndType.url); + + case IDENTIFIER -> { switch (valueAndType.identifier.type) { - case CODE: - statement.setString(parameterIndex, - "[{\"value\": \"" + valueAndType.identifier.codeValue + "\"}]"); - break; - case CODE_AND_SYSTEM: - statement.setString(parameterIndex, "[{\"value\": \"" + valueAndType.identifier.codeValue - + "\", \"system\": \"" + valueAndType.identifier.systemValue + "\"}]"); - break; - case CODE_AND_NO_SYSTEM_PROPERTY: + case CODE -> statement.setObject(parameterIndex, pgObjectFactory.jsonParameterToPgObjectAsArray( + new IdentifierParameter(null, valueAndType.identifier.codeValue))); + + case CODE_AND_SYSTEM -> statement.setObject(parameterIndex, + pgObjectFactory.jsonParameterToPgObjectAsArray(new IdentifierParameter( + valueAndType.identifier.systemValue, valueAndType.identifier.codeValue))); + + case SYSTEM -> statement.setObject(parameterIndex, pgObjectFactory.jsonParameterToPgObjectAsArray( + new IdentifierParameter(valueAndType.identifier.systemValue, null))); + + case CODE_AND_NO_SYSTEM_PROPERTY -> statement.setString(parameterIndex, valueAndType.identifier.codeValue); - break; - case SYSTEM: - statement.setString(parameterIndex, - "[{\"system\": \"" + valueAndType.identifier.systemValue + "\"}]"); - break; } } } diff --git a/dsf-fhir/dsf-fhir-server/src/main/java/dev/dsf/fhir/search/parameters/OrganizationType.java b/dsf-fhir/dsf-fhir-server/src/main/java/dev/dsf/fhir/search/parameters/OrganizationType.java index ded9f09b2..c9e81d17c 100755 --- a/dsf-fhir/dsf-fhir-server/src/main/java/dev/dsf/fhir/search/parameters/OrganizationType.java +++ b/dsf-fhir/dsf-fhir-server/src/main/java/dev/dsf/fhir/search/parameters/OrganizationType.java @@ -22,6 +22,8 @@ import org.hl7.fhir.r4.model.Enumerations.SearchParamType; import org.hl7.fhir.r4.model.Organization; +import dev.dsf.fhir.dao.jdbc.PgObjectFactory; +import dev.dsf.fhir.dao.jdbc.PgObjectFactory.CodingParameter; import dev.dsf.fhir.function.BiFunctionWithSqlException; import dev.dsf.fhir.search.SearchQueryParameter.SearchParameterDefinition; import dev.dsf.fhir.search.parameters.basic.AbstractTokenParameter; @@ -42,7 +44,8 @@ protected String getPositiveFilterQuery() return switch (valueAndType.type) { case CODE, CODE_AND_SYSTEM, SYSTEM -> - "(SELECT jsonb_agg(coding) FROM jsonb_array_elements(organization->'type') AS type, jsonb_array_elements(type->'coding') AS coding) @> ?::jsonb"; + "(SELECT jsonb_agg(coding) FROM jsonb_array_elements(organization->'type') AS type, jsonb_array_elements(type->'coding') AS coding) @> ?"; + case CODE_AND_NO_SYSTEM_PROPERTY -> "(SELECT COUNT(*) FROM jsonb_array_elements(organization->'type') AS type, jsonb_array_elements(type->'coding') AS coding " + "WHERE coding->>'code' = ? AND NOT (coding ?? 'system')) > 0"; @@ -55,7 +58,8 @@ protected String getNegatedFilterQuery() return switch (valueAndType.type) { case CODE, CODE_AND_SYSTEM, SYSTEM -> - "NOT ((SELECT jsonb_agg(coding) FROM jsonb_array_elements(organization->'type') AS type, jsonb_array_elements(type->'coding') AS coding) @> ?::jsonb)"; + "NOT ((SELECT jsonb_agg(coding) FROM jsonb_array_elements(organization->'type') AS type, jsonb_array_elements(type->'coding') AS coding) @> ?)"; + case CODE_AND_NO_SYSTEM_PROPERTY -> "(SELECT COUNT(*) FROM jsonb_array_elements(organization->'type') AS type, jsonb_array_elements(type->'coding') AS coding " + "WHERE coding->>'code' <> ? OR (coding ?? 'system')) > 0"; @@ -70,26 +74,21 @@ public int getSqlParameterCount() @Override public void modifyStatement(int parameterIndex, int subqueryParameterIndex, PreparedStatement statement, - BiFunctionWithSqlException arrayCreator) throws SQLException + BiFunctionWithSqlException arrayCreator, PgObjectFactory pgObjectFactory) + throws SQLException { switch (valueAndType.type) { - case CODE: - statement.setString(parameterIndex, "[{\"code\": \"" + valueAndType.codeValue + "\"}]"); - return; - - case CODE_AND_SYSTEM: - statement.setString(parameterIndex, "[{\"code\": \"" + valueAndType.codeValue + "\", \"system\": \"" - + valueAndType.systemValue + "\"}]"); - return; - - case CODE_AND_NO_SYSTEM_PROPERTY: - statement.setString(parameterIndex, valueAndType.codeValue); - return; - - case SYSTEM: - statement.setString(parameterIndex, "[{\"system\": \"" + valueAndType.systemValue + "\"}]"); - return; + case CODE -> statement.setObject(parameterIndex, + pgObjectFactory.jsonParameterToPgObjectAsArray(new CodingParameter(null, valueAndType.codeValue))); + + case CODE_AND_SYSTEM -> statement.setObject(parameterIndex, pgObjectFactory.jsonParameterToPgObjectAsArray( + new CodingParameter(valueAndType.systemValue, valueAndType.codeValue))); + + case SYSTEM -> statement.setObject(parameterIndex, pgObjectFactory + .jsonParameterToPgObjectAsArray(new CodingParameter(valueAndType.systemValue, null))); + + case CODE_AND_NO_SYSTEM_PROPERTY -> statement.setString(parameterIndex, valueAndType.codeValue); } } diff --git a/dsf-fhir/dsf-fhir-server/src/main/java/dev/dsf/fhir/search/parameters/PractitionerRoleOrganization.java b/dsf-fhir/dsf-fhir-server/src/main/java/dev/dsf/fhir/search/parameters/PractitionerRoleOrganization.java index 8857eeba9..7ec44a6f6 100644 --- a/dsf-fhir/dsf-fhir-server/src/main/java/dev/dsf/fhir/search/parameters/PractitionerRoleOrganization.java +++ b/dsf-fhir/dsf-fhir-server/src/main/java/dev/dsf/fhir/search/parameters/PractitionerRoleOrganization.java @@ -31,6 +31,8 @@ import dev.dsf.fhir.dao.OrganizationDao; import dev.dsf.fhir.dao.exception.ResourceDeletedException; +import dev.dsf.fhir.dao.jdbc.PgObjectFactory; +import dev.dsf.fhir.dao.jdbc.PgObjectFactory.IdentifierParameter; import dev.dsf.fhir.dao.provider.DaoProvider; import dev.dsf.fhir.function.BiFunctionWithSqlException; import dev.dsf.fhir.search.IncludeParameterDefinition; @@ -68,9 +70,11 @@ public String getFilterQuery() { case ID, RESOURCE_NAME_AND_ID, URL, TYPE_AND_ID, TYPE_AND_RESOURCE_NAME_AND_ID -> "practitioner_role->'organization'->>'reference' = ?"; + case IDENTIFIER -> switch (valueAndType.identifier.type) { - case CODE, CODE_AND_SYSTEM, SYSTEM -> PRACTITIONER_IDENTIFIERS_SUBQUERY + " @> ?::jsonb"; + case CODE, CODE_AND_SYSTEM, SYSTEM -> PRACTITIONER_IDENTIFIERS_SUBQUERY + " @> ?"; + case CODE_AND_NO_SYSTEM_PROPERTY -> "(SELECT count(*) FROM jsonb_array_elements(" + PRACTITIONER_IDENTIFIERS_SUBQUERY + ") identifier WHERE identifier->>'value' = ? AND NOT (identifier ?? 'system')) > 0"; @@ -86,38 +90,31 @@ public int getSqlParameterCount() @Override public void modifyStatement(int parameterIndex, int subqueryParameterIndex, PreparedStatement statement, - BiFunctionWithSqlException arrayCreator) throws SQLException + BiFunctionWithSqlException arrayCreator, PgObjectFactory pgObjectFactory) + throws SQLException { switch (valueAndType.type) { - case ID: - case RESOURCE_NAME_AND_ID: - case TYPE_AND_ID: - case TYPE_AND_RESOURCE_NAME_AND_ID: + case ID, RESOURCE_NAME_AND_ID, TYPE_AND_ID, TYPE_AND_RESOURCE_NAME_AND_ID -> statement.setString(parameterIndex, TARGET_RESOURCE_TYPE_NAME + "/" + valueAndType.id); - break; - case URL: - statement.setString(parameterIndex, valueAndType.url); - break; - case IDENTIFIER: - { + + case URL -> statement.setString(parameterIndex, valueAndType.url); + + case IDENTIFIER -> { switch (valueAndType.identifier.type) { - case CODE: - statement.setString(parameterIndex, - "[{\"value\": \"" + valueAndType.identifier.codeValue + "\"}]"); - break; - case CODE_AND_SYSTEM: - statement.setString(parameterIndex, "[{\"value\": \"" + valueAndType.identifier.codeValue - + "\", \"system\": \"" + valueAndType.identifier.systemValue + "\"}]"); - break; - case CODE_AND_NO_SYSTEM_PROPERTY: + case CODE -> statement.setObject(parameterIndex, pgObjectFactory.jsonParameterToPgObjectAsArray( + new IdentifierParameter(null, valueAndType.identifier.codeValue))); + + case CODE_AND_SYSTEM -> statement.setObject(parameterIndex, + pgObjectFactory.jsonParameterToPgObjectAsArray(new IdentifierParameter( + valueAndType.identifier.systemValue, valueAndType.identifier.codeValue))); + + case SYSTEM -> statement.setObject(parameterIndex, pgObjectFactory.jsonParameterToPgObjectAsArray( + new IdentifierParameter(valueAndType.identifier.systemValue, null))); + + case CODE_AND_NO_SYSTEM_PROPERTY -> statement.setString(parameterIndex, valueAndType.identifier.codeValue); - break; - case SYSTEM: - statement.setString(parameterIndex, - "[{\"system\": \"" + valueAndType.identifier.systemValue + "\"}]"); - break; } } } diff --git a/dsf-fhir/dsf-fhir-server/src/main/java/dev/dsf/fhir/search/parameters/PractitionerRolePractitioner.java b/dsf-fhir/dsf-fhir-server/src/main/java/dev/dsf/fhir/search/parameters/PractitionerRolePractitioner.java index 13574fd76..58aeef677 100644 --- a/dsf-fhir/dsf-fhir-server/src/main/java/dev/dsf/fhir/search/parameters/PractitionerRolePractitioner.java +++ b/dsf-fhir/dsf-fhir-server/src/main/java/dev/dsf/fhir/search/parameters/PractitionerRolePractitioner.java @@ -31,6 +31,8 @@ import dev.dsf.fhir.dao.PractitionerDao; import dev.dsf.fhir.dao.exception.ResourceDeletedException; +import dev.dsf.fhir.dao.jdbc.PgObjectFactory; +import dev.dsf.fhir.dao.jdbc.PgObjectFactory.IdentifierParameter; import dev.dsf.fhir.dao.provider.DaoProvider; import dev.dsf.fhir.function.BiFunctionWithSqlException; import dev.dsf.fhir.search.IncludeParameterDefinition; @@ -68,9 +70,11 @@ public String getFilterQuery() { case ID, RESOURCE_NAME_AND_ID, URL, TYPE_AND_ID, TYPE_AND_RESOURCE_NAME_AND_ID -> "practitioner_role->'practitioner'->>'reference' = ?"; + case IDENTIFIER -> switch (valueAndType.identifier.type) { - case CODE, CODE_AND_SYSTEM, SYSTEM -> PRACTITIONER_IDENTIFIERS_SUBQUERY + " @> ?::jsonb"; + case CODE, CODE_AND_SYSTEM, SYSTEM -> PRACTITIONER_IDENTIFIERS_SUBQUERY + " @> ?"; + case CODE_AND_NO_SYSTEM_PROPERTY -> "(SELECT count(*) FROM jsonb_array_elements(" + PRACTITIONER_IDENTIFIERS_SUBQUERY + ") identifier WHERE identifier->>'value' = ? AND NOT (identifier ?? 'system')) > 0"; @@ -86,38 +90,31 @@ public int getSqlParameterCount() @Override public void modifyStatement(int parameterIndex, int subqueryParameterIndex, PreparedStatement statement, - BiFunctionWithSqlException arrayCreator) throws SQLException + BiFunctionWithSqlException arrayCreator, PgObjectFactory pgObjectFactory) + throws SQLException { switch (valueAndType.type) { - case ID: - case RESOURCE_NAME_AND_ID: - case TYPE_AND_ID: - case TYPE_AND_RESOURCE_NAME_AND_ID: + case ID, RESOURCE_NAME_AND_ID, TYPE_AND_ID, TYPE_AND_RESOURCE_NAME_AND_ID -> statement.setString(parameterIndex, TARGET_RESOURCE_TYPE_NAME + "/" + valueAndType.id); - break; - case URL: - statement.setString(parameterIndex, valueAndType.url); - break; - case IDENTIFIER: - { + + case URL -> statement.setString(parameterIndex, valueAndType.url); + + case IDENTIFIER -> { switch (valueAndType.identifier.type) { - case CODE: - statement.setString(parameterIndex, - "[{\"value\": \"" + valueAndType.identifier.codeValue + "\"}]"); - break; - case CODE_AND_SYSTEM: - statement.setString(parameterIndex, "[{\"value\": \"" + valueAndType.identifier.codeValue - + "\", \"system\": \"" + valueAndType.identifier.systemValue + "\"}]"); - break; - case CODE_AND_NO_SYSTEM_PROPERTY: + case CODE -> statement.setObject(parameterIndex, pgObjectFactory.jsonParameterToPgObjectAsArray( + new IdentifierParameter(null, valueAndType.identifier.codeValue))); + + case CODE_AND_SYSTEM -> statement.setObject(parameterIndex, + pgObjectFactory.jsonParameterToPgObjectAsArray(new IdentifierParameter( + valueAndType.identifier.systemValue, valueAndType.identifier.codeValue))); + + case SYSTEM -> statement.setObject(parameterIndex, pgObjectFactory.jsonParameterToPgObjectAsArray( + new IdentifierParameter(valueAndType.identifier.systemValue, null))); + + case CODE_AND_NO_SYSTEM_PROPERTY -> statement.setString(parameterIndex, valueAndType.identifier.codeValue); - break; - case SYSTEM: - statement.setString(parameterIndex, - "[{\"system\": \"" + valueAndType.identifier.systemValue + "\"}]"); - break; } } } diff --git a/dsf-fhir/dsf-fhir-server/src/main/java/dev/dsf/fhir/search/parameters/QuestionnaireResponseAuthor.java b/dsf-fhir/dsf-fhir-server/src/main/java/dev/dsf/fhir/search/parameters/QuestionnaireResponseAuthor.java index 4b4400420..c3ce8e6ac 100644 --- a/dsf-fhir/dsf-fhir-server/src/main/java/dev/dsf/fhir/search/parameters/QuestionnaireResponseAuthor.java +++ b/dsf-fhir/dsf-fhir-server/src/main/java/dev/dsf/fhir/search/parameters/QuestionnaireResponseAuthor.java @@ -35,6 +35,8 @@ import dev.dsf.fhir.dao.ResourceDao; import dev.dsf.fhir.dao.exception.ResourceDeletedException; +import dev.dsf.fhir.dao.jdbc.PgObjectFactory; +import dev.dsf.fhir.dao.jdbc.PgObjectFactory.IdentifierParameter; import dev.dsf.fhir.dao.provider.DaoProvider; import dev.dsf.fhir.function.BiFunctionWithSqlException; import dev.dsf.fhir.search.IncludeParameterDefinition; @@ -81,16 +83,21 @@ public String getFilterQuery() { // testing all TargetResourceTypeName/ID combinations case ID -> "questionnaire_response->'author'->>'reference' = ANY (?)"; + case RESOURCE_NAME_AND_ID, URL, TYPE_AND_ID, TYPE_AND_RESOURCE_NAME_AND_ID -> "questionnaire_response->'author'->>'reference' = ?"; + case IDENTIFIER -> switch (valueAndType.identifier.type) { case CODE -> "(" + IDENTIFIERS_SUBQUERY - + " @> ?::jsonb OR questionnaire_response->'author'->'identifier'->>'value' = ?)"; + + " @> ? OR questionnaire_response->'author'->'identifier'->>'value' = ?)"; + case CODE_AND_SYSTEM -> "(" + IDENTIFIERS_SUBQUERY - + " @> ?::jsonb OR (questionnaire_response->'author'->'identifier'->>'system' = ? AND questionnaire_response->'author'->'identifier'->>'value' = ?))"; + + " @> ? OR (questionnaire_response->'author'->'identifier'->>'system' = ? AND questionnaire_response->'author'->'identifier'->>'value' = ?))"; + case SYSTEM -> "(" + IDENTIFIERS_SUBQUERY - + " @> ?::jsonb OR questionnaire_response->'author'->'identifier'->>'system' = ?)"; + + " @> ? OR questionnaire_response->'author'->'identifier'->>'system' = ?)"; + case CODE_AND_NO_SYSTEM_PROPERTY -> "((SELECT count(*) FROM jsonb_array_elements(" + IDENTIFIERS_SUBQUERY + ") identifier WHERE identifier->>'value' = ? AND NOT (identifier ?? 'system')) > 0" @@ -115,15 +122,13 @@ public int getSqlParameterCount() @Override public void modifyStatement(int parameterIndex, int subqueryParameterIndex, PreparedStatement statement, - BiFunctionWithSqlException arrayCreator) throws SQLException + BiFunctionWithSqlException arrayCreator, PgObjectFactory pgObjectFactory) + throws SQLException { switch (valueAndType.type) { - case ID -> { - Array array = arrayCreator.apply("TEXT", - Arrays.stream(TARGET_RESOURCE_TYPE_NAMES).map(n -> n + "/" + valueAndType.id).toArray()); - statement.setArray(parameterIndex, array); - } + case ID -> statement.setArray(parameterIndex, arrayCreator.apply("TEXT", + Arrays.stream(TARGET_RESOURCE_TYPE_NAMES).map(n -> n + "/" + valueAndType.id).toArray())); case RESOURCE_NAME_AND_ID, TYPE_AND_ID, TYPE_AND_RESOURCE_NAME_AND_ID -> statement.setString(parameterIndex, valueAndType.resourceName + "/" + valueAndType.id); @@ -135,16 +140,17 @@ public void modifyStatement(int parameterIndex, int subqueryParameterIndex, Prep { case CODE -> { if (subqueryParameterIndex == 1) - statement.setString(parameterIndex, - "[{\"value\": \"" + valueAndType.identifier.codeValue + "\"}]"); + statement.setObject(parameterIndex, pgObjectFactory.jsonParameterToPgObjectAsArray( + new IdentifierParameter(null, valueAndType.identifier.codeValue))); else if (subqueryParameterIndex == 2) statement.setString(parameterIndex, valueAndType.identifier.codeValue); } case CODE_AND_SYSTEM -> { if (subqueryParameterIndex == 1) - statement.setString(parameterIndex, "[{\"system\": \"" + valueAndType.identifier.systemValue - + "\", \"value\": \"" + valueAndType.identifier.codeValue + "\"}]"); + statement.setObject(parameterIndex, + pgObjectFactory.jsonParameterToPgObjectAsArray(new IdentifierParameter( + valueAndType.identifier.systemValue, valueAndType.identifier.codeValue))); else if (subqueryParameterIndex == 2) statement.setString(parameterIndex, valueAndType.identifier.systemValue); else if (subqueryParameterIndex == 3) @@ -153,8 +159,8 @@ else if (subqueryParameterIndex == 3) case SYSTEM -> { if (subqueryParameterIndex == 1) - statement.setString(parameterIndex, - "[{\"system\": \"" + valueAndType.identifier.systemValue + "\"}]"); + statement.setObject(parameterIndex, pgObjectFactory.jsonParameterToPgObjectAsArray( + new IdentifierParameter(valueAndType.identifier.systemValue, null))); else if (subqueryParameterIndex == 2) statement.setString(parameterIndex, valueAndType.identifier.systemValue); } diff --git a/dsf-fhir/dsf-fhir-server/src/main/java/dev/dsf/fhir/search/parameters/QuestionnaireResponseQuestionnaire.java b/dsf-fhir/dsf-fhir-server/src/main/java/dev/dsf/fhir/search/parameters/QuestionnaireResponseQuestionnaire.java index 4423fa2af..84479c015 100755 --- a/dsf-fhir/dsf-fhir-server/src/main/java/dev/dsf/fhir/search/parameters/QuestionnaireResponseQuestionnaire.java +++ b/dsf-fhir/dsf-fhir-server/src/main/java/dev/dsf/fhir/search/parameters/QuestionnaireResponseQuestionnaire.java @@ -26,6 +26,7 @@ import org.hl7.fhir.r4.model.QuestionnaireResponse; import org.hl7.fhir.r4.model.Resource; +import dev.dsf.fhir.dao.jdbc.PgObjectFactory; import dev.dsf.fhir.dao.provider.DaoProvider; import dev.dsf.fhir.function.BiFunctionWithSqlException; import dev.dsf.fhir.search.IncludeParameterDefinition; @@ -72,7 +73,8 @@ public int getSqlParameterCount() @Override public void modifyStatement(int parameterIndex, int subqueryParameterIndex, PreparedStatement statement, - BiFunctionWithSqlException arrayCreator) throws SQLException + BiFunctionWithSqlException arrayCreator, PgObjectFactory pgObjectFactory) + throws SQLException { statement.setString(parameterIndex, valueAndType.url); } diff --git a/dsf-fhir/dsf-fhir-server/src/main/java/dev/dsf/fhir/search/parameters/QuestionnaireResponseStatus.java b/dsf-fhir/dsf-fhir-server/src/main/java/dev/dsf/fhir/search/parameters/QuestionnaireResponseStatus.java index 68bb4e3b2..2be799f7e 100755 --- a/dsf-fhir/dsf-fhir-server/src/main/java/dev/dsf/fhir/search/parameters/QuestionnaireResponseStatus.java +++ b/dsf-fhir/dsf-fhir-server/src/main/java/dev/dsf/fhir/search/parameters/QuestionnaireResponseStatus.java @@ -25,6 +25,7 @@ import org.hl7.fhir.r4.model.Enumerations.SearchParamType; import org.hl7.fhir.r4.model.QuestionnaireResponse; +import dev.dsf.fhir.dao.jdbc.PgObjectFactory; import dev.dsf.fhir.function.BiFunctionWithSqlException; import dev.dsf.fhir.search.SearchQueryParameter.SearchParameterDefinition; import dev.dsf.fhir.search.SearchQueryParameterError; @@ -98,7 +99,8 @@ public int getSqlParameterCount() @Override public void modifyStatement(int parameterIndex, int subqueryParameterIndex, PreparedStatement statement, - BiFunctionWithSqlException arrayCreator) throws SQLException + BiFunctionWithSqlException arrayCreator, PgObjectFactory pgObjectFactory) + throws SQLException { statement.setString(parameterIndex, status.toCode()); } diff --git a/dsf-fhir/dsf-fhir-server/src/main/java/dev/dsf/fhir/search/parameters/QuestionnaireResponseSubject.java b/dsf-fhir/dsf-fhir-server/src/main/java/dev/dsf/fhir/search/parameters/QuestionnaireResponseSubject.java index 9de9a5fb7..371abbb51 100755 --- a/dsf-fhir/dsf-fhir-server/src/main/java/dev/dsf/fhir/search/parameters/QuestionnaireResponseSubject.java +++ b/dsf-fhir/dsf-fhir-server/src/main/java/dev/dsf/fhir/search/parameters/QuestionnaireResponseSubject.java @@ -34,6 +34,8 @@ import dev.dsf.fhir.dao.ResourceDao; import dev.dsf.fhir.dao.exception.ResourceDeletedException; +import dev.dsf.fhir.dao.jdbc.PgObjectFactory; +import dev.dsf.fhir.dao.jdbc.PgObjectFactory.IdentifierParameter; import dev.dsf.fhir.dao.provider.DaoProvider; import dev.dsf.fhir.function.BiFunctionWithSqlException; import dev.dsf.fhir.search.IncludeParameterDefinition; @@ -78,11 +80,14 @@ public String getFilterQuery() { // testing all TargetResourceTypeName/ID combinations case ID -> "questionnaire_response->'subject'->>'reference' = ANY (?)"; + case RESOURCE_NAME_AND_ID, URL, TYPE_AND_ID, TYPE_AND_RESOURCE_NAME_AND_ID -> "questionnaire_response->'subject'->>'reference' = ?"; + case IDENTIFIER -> switch (valueAndType.identifier.type) { - case CODE, CODE_AND_SYSTEM, SYSTEM -> IDENTIFIERS_SUBQUERY + " @> ?::jsonb"; + case CODE, CODE_AND_SYSTEM, SYSTEM -> IDENTIFIERS_SUBQUERY + " @> ?"; + case CODE_AND_NO_SYSTEM_PROPERTY -> "(SELECT count(*) FROM jsonb_array_elements(" + IDENTIFIERS_SUBQUERY + ") identifier WHERE identifier->>'value' = ? AND NOT (identifier ?? 'system')) > 0"; }; @@ -97,42 +102,34 @@ public int getSqlParameterCount() @Override public void modifyStatement(int parameterIndex, int subqueryParameterIndex, PreparedStatement statement, - BiFunctionWithSqlException arrayCreator) throws SQLException + BiFunctionWithSqlException arrayCreator, PgObjectFactory pgObjectFactory) + throws SQLException { switch (valueAndType.type) { - case ID: - Array array = arrayCreator.apply("TEXT", - Arrays.stream(TARGET_RESOURCE_TYPE_NAMES).map(n -> n + "/" + valueAndType.id).toArray()); - statement.setArray(parameterIndex, array); - break; - case RESOURCE_NAME_AND_ID: - case TYPE_AND_ID: - case TYPE_AND_RESOURCE_NAME_AND_ID: + case ID -> statement.setArray(parameterIndex, arrayCreator.apply("TEXT", + Arrays.stream(TARGET_RESOURCE_TYPE_NAMES).map(n -> n + "/" + valueAndType.id).toArray())); + + case RESOURCE_NAME_AND_ID, TYPE_AND_ID, TYPE_AND_RESOURCE_NAME_AND_ID -> statement.setString(parameterIndex, valueAndType.resourceName + "/" + valueAndType.id); - break; - case URL: - statement.setString(parameterIndex, valueAndType.url); - break; - case IDENTIFIER: - { + + case URL -> statement.setString(parameterIndex, valueAndType.url); + + case IDENTIFIER -> { switch (valueAndType.identifier.type) { - case CODE: - statement.setString(parameterIndex, - "[{\"value\": \"" + valueAndType.identifier.codeValue + "\"}]"); - break; - case CODE_AND_SYSTEM: - statement.setString(parameterIndex, "[{\"value\": \"" + valueAndType.identifier.codeValue - + "\", \"system\": \"" + valueAndType.identifier.systemValue + "\"}]"); - break; - case CODE_AND_NO_SYSTEM_PROPERTY: + case CODE -> statement.setObject(parameterIndex, pgObjectFactory.jsonParameterToPgObjectAsArray( + new IdentifierParameter(null, valueAndType.identifier.codeValue))); + + case CODE_AND_SYSTEM -> statement.setObject(parameterIndex, + pgObjectFactory.jsonParameterToPgObjectAsArray(new IdentifierParameter( + valueAndType.identifier.systemValue, valueAndType.identifier.codeValue))); + + case SYSTEM -> statement.setObject(parameterIndex, pgObjectFactory.jsonParameterToPgObjectAsArray( + new IdentifierParameter(valueAndType.identifier.systemValue, null))); + + case CODE_AND_NO_SYSTEM_PROPERTY -> statement.setString(parameterIndex, valueAndType.identifier.codeValue); - break; - case SYSTEM: - statement.setString(parameterIndex, - "[{\"system\": \"" + valueAndType.identifier.systemValue + "\"}]"); - break; } } } diff --git a/dsf-fhir/dsf-fhir-server/src/main/java/dev/dsf/fhir/search/parameters/ResearchStudyEnrollment.java b/dsf-fhir/dsf-fhir-server/src/main/java/dev/dsf/fhir/search/parameters/ResearchStudyEnrollment.java index 980517eb1..d2e47929b 100644 --- a/dsf-fhir/dsf-fhir-server/src/main/java/dev/dsf/fhir/search/parameters/ResearchStudyEnrollment.java +++ b/dsf-fhir/dsf-fhir-server/src/main/java/dev/dsf/fhir/search/parameters/ResearchStudyEnrollment.java @@ -31,6 +31,8 @@ import dev.dsf.fhir.dao.GroupDao; import dev.dsf.fhir.dao.exception.ResourceDeletedException; +import dev.dsf.fhir.dao.jdbc.PgObjectFactory; +import dev.dsf.fhir.dao.jdbc.PgObjectFactory.IdentifierParameter; import dev.dsf.fhir.dao.provider.DaoProvider; import dev.dsf.fhir.function.BiFunctionWithSqlException; import dev.dsf.fhir.search.IncludeParameterDefinition; @@ -65,12 +67,14 @@ public String getFilterQuery() { case ID, RESOURCE_NAME_AND_ID, URL, TYPE_AND_ID, TYPE_AND_RESOURCE_NAME_AND_ID -> "? IN (SELECT reference->>'reference' FROM jsonb_array_elements(research_study->'enrollment') AS reference)"; + case IDENTIFIER -> switch (valueAndType.identifier.type) { case CODE, CODE_AND_SYSTEM, SYSTEM -> "(SELECT jsonb_agg(identifier) FROM (SELECT identifier FROM current_groups, jsonb_array_elements(group_json->'identifier') identifier" + " WHERE concat('Group/', group_json->>'id') IN (SELECT reference->>'reference' FROM jsonb_array_elements(research_study->'enrollment') reference)" - + " ) AS identifiers) @> ?::jsonb"; + + " ) AS identifiers) @> ?"; + case CODE_AND_NO_SYSTEM_PROPERTY -> "(SELECT count(*) FROM (SELECT identifier FROM current_groups, jsonb_array_elements(group_json->'identifier') identifier" + " WHERE concat('Group/', group_json->>'id') IN (SELECT reference->>'reference' FROM jsonb_array_elements(research_study->'enrollment') reference)" @@ -87,38 +91,31 @@ public int getSqlParameterCount() @Override public void modifyStatement(int parameterIndex, int subqueryParameterIndex, PreparedStatement statement, - BiFunctionWithSqlException arrayCreator) throws SQLException + BiFunctionWithSqlException arrayCreator, PgObjectFactory pgObjectFactory) + throws SQLException { switch (valueAndType.type) { - case ID: - case RESOURCE_NAME_AND_ID: - case TYPE_AND_ID: - case TYPE_AND_RESOURCE_NAME_AND_ID: + case ID, RESOURCE_NAME_AND_ID, TYPE_AND_ID, TYPE_AND_RESOURCE_NAME_AND_ID -> statement.setString(parameterIndex, TARGET_RESOURCE_TYPE_NAME + "/" + valueAndType.id); - break; - case URL: - statement.setString(parameterIndex, valueAndType.url); - break; - case IDENTIFIER: - { + + case URL -> statement.setString(parameterIndex, valueAndType.url); + + case IDENTIFIER -> { switch (valueAndType.identifier.type) { - case CODE: - statement.setString(parameterIndex, - "[{\"value\": \"" + valueAndType.identifier.codeValue + "\"}]"); - break; - case CODE_AND_SYSTEM: - statement.setString(parameterIndex, "[{\"value\": \"" + valueAndType.identifier.codeValue - + "\", \"system\": \"" + valueAndType.identifier.systemValue + "\"}]"); - break; - case CODE_AND_NO_SYSTEM_PROPERTY: + case CODE -> statement.setObject(parameterIndex, pgObjectFactory.jsonParameterToPgObjectAsArray( + new IdentifierParameter(null, valueAndType.identifier.codeValue))); + + case CODE_AND_SYSTEM -> statement.setObject(parameterIndex, + pgObjectFactory.jsonParameterToPgObjectAsArray(new IdentifierParameter( + valueAndType.identifier.systemValue, valueAndType.identifier.codeValue))); + + case SYSTEM -> statement.setObject(parameterIndex, pgObjectFactory.jsonParameterToPgObjectAsArray( + new IdentifierParameter(valueAndType.identifier.systemValue, null))); + + case CODE_AND_NO_SYSTEM_PROPERTY -> statement.setString(parameterIndex, valueAndType.identifier.codeValue); - break; - case SYSTEM: - statement.setString(parameterIndex, - "[{\"system\": \"" + valueAndType.identifier.systemValue + "\"}]"); - break; } } } diff --git a/dsf-fhir/dsf-fhir-server/src/main/java/dev/dsf/fhir/search/parameters/ResearchStudyPrincipalInvestigator.java b/dsf-fhir/dsf-fhir-server/src/main/java/dev/dsf/fhir/search/parameters/ResearchStudyPrincipalInvestigator.java index b388a2899..f896d17ca 100644 --- a/dsf-fhir/dsf-fhir-server/src/main/java/dev/dsf/fhir/search/parameters/ResearchStudyPrincipalInvestigator.java +++ b/dsf-fhir/dsf-fhir-server/src/main/java/dev/dsf/fhir/search/parameters/ResearchStudyPrincipalInvestigator.java @@ -33,6 +33,8 @@ import dev.dsf.fhir.dao.ResourceDao; import dev.dsf.fhir.dao.exception.ResourceDeletedException; +import dev.dsf.fhir.dao.jdbc.PgObjectFactory; +import dev.dsf.fhir.dao.jdbc.PgObjectFactory.IdentifierParameter; import dev.dsf.fhir.dao.provider.DaoProvider; import dev.dsf.fhir.function.BiFunctionWithSqlException; import dev.dsf.fhir.search.IncludeParameterDefinition; @@ -72,11 +74,14 @@ public String getFilterQuery() return switch (valueAndType.type) { case ID -> "research_study->'principalInvestigator'->>'reference' = ANY (?)"; + case RESOURCE_NAME_AND_ID, URL, TYPE_AND_ID, TYPE_AND_RESOURCE_NAME_AND_ID -> "research_study->'principalInvestigator'->>'reference' = ?"; + case IDENTIFIER -> switch (valueAndType.identifier.type) { - case CODE, CODE_AND_SYSTEM, SYSTEM -> IDENTIFIERS_SUBQUERY + " @> ?::jsonb"; + case CODE, CODE_AND_SYSTEM, SYSTEM -> IDENTIFIERS_SUBQUERY + " @> ?"; + case CODE_AND_NO_SYSTEM_PROPERTY -> "(SELECT count(*) FROM jsonb_array_elements(" + IDENTIFIERS_SUBQUERY + ") identifier WHERE identifier->>'value' = ? AND NOT (identifier ?? 'system')) > 0"; }; @@ -91,42 +96,34 @@ public int getSqlParameterCount() @Override public void modifyStatement(int parameterIndex, int subqueryParameterIndex, PreparedStatement statement, - BiFunctionWithSqlException arrayCreator) throws SQLException + BiFunctionWithSqlException arrayCreator, PgObjectFactory pgObjectFactory) + throws SQLException { switch (valueAndType.type) { - case ID: - Array array = arrayCreator.apply("TEXT", - Arrays.stream(TARGET_RESOURCE_TYPE_NAMES).map(n -> n + "/" + valueAndType.id).toArray()); - statement.setArray(parameterIndex, array); - break; - case RESOURCE_NAME_AND_ID: - case TYPE_AND_ID: - case TYPE_AND_RESOURCE_NAME_AND_ID: + case ID -> statement.setArray(parameterIndex, arrayCreator.apply("TEXT", + Arrays.stream(TARGET_RESOURCE_TYPE_NAMES).map(n -> n + "/" + valueAndType.id).toArray())); + + case RESOURCE_NAME_AND_ID, TYPE_AND_ID, TYPE_AND_RESOURCE_NAME_AND_ID -> statement.setString(parameterIndex, valueAndType.resourceName + "/" + valueAndType.id); - break; - case URL: - statement.setString(parameterIndex, valueAndType.url); - break; - case IDENTIFIER: - { + + case URL -> statement.setString(parameterIndex, valueAndType.url); + + case IDENTIFIER -> { switch (valueAndType.identifier.type) { - case CODE: - statement.setString(parameterIndex, - "[{\"value\": \"" + valueAndType.identifier.codeValue + "\"}]"); - break; - case CODE_AND_SYSTEM: - statement.setString(parameterIndex, "[{\"value\": \"" + valueAndType.identifier.codeValue - + "\", \"system\": \"" + valueAndType.identifier.systemValue + "\"}]"); - break; - case CODE_AND_NO_SYSTEM_PROPERTY: + case CODE -> statement.setObject(parameterIndex, pgObjectFactory.jsonParameterToPgObjectAsArray( + new IdentifierParameter(null, valueAndType.identifier.codeValue))); + + case CODE_AND_SYSTEM -> statement.setObject(parameterIndex, + pgObjectFactory.jsonParameterToPgObjectAsArray(new IdentifierParameter( + valueAndType.identifier.systemValue, valueAndType.identifier.codeValue))); + + case SYSTEM -> statement.setObject(parameterIndex, pgObjectFactory.jsonParameterToPgObjectAsArray( + new IdentifierParameter(valueAndType.identifier.systemValue, null))); + + case CODE_AND_NO_SYSTEM_PROPERTY -> statement.setString(parameterIndex, valueAndType.identifier.codeValue); - break; - case SYSTEM: - statement.setString(parameterIndex, - "[{\"system\": \"" + valueAndType.identifier.systemValue + "\"}]"); - break; } } } diff --git a/dsf-fhir/dsf-fhir-server/src/main/java/dev/dsf/fhir/search/parameters/ResourceId.java b/dsf-fhir/dsf-fhir-server/src/main/java/dev/dsf/fhir/search/parameters/ResourceId.java index fb3d2d1c4..77cc81ac0 100755 --- a/dsf-fhir/dsf-fhir-server/src/main/java/dev/dsf/fhir/search/parameters/ResourceId.java +++ b/dsf-fhir/dsf-fhir-server/src/main/java/dev/dsf/fhir/search/parameters/ResourceId.java @@ -27,6 +27,7 @@ import org.postgresql.util.PGobject; import ca.uhn.fhir.parser.DataFormatException; +import dev.dsf.fhir.dao.jdbc.PgObjectFactory; import dev.dsf.fhir.function.BiFunctionWithSqlException; import dev.dsf.fhir.search.SearchQueryParameter.SearchParameterDefinition; import dev.dsf.fhir.search.SearchQueryParameterError; @@ -97,7 +98,8 @@ public int getSqlParameterCount() @Override public void modifyStatement(int parameterIndex, int subqueryParameterIndex, PreparedStatement statement, - BiFunctionWithSqlException arrayCreator) throws SQLException + BiFunctionWithSqlException arrayCreator, PgObjectFactory pgObjectFactory) + throws SQLException { statement.setObject(parameterIndex, asUuidPgObject(id)); } diff --git a/dsf-fhir/dsf-fhir-server/src/main/java/dev/dsf/fhir/search/parameters/ResourceProfile.java b/dsf-fhir/dsf-fhir-server/src/main/java/dev/dsf/fhir/search/parameters/ResourceProfile.java index ec4d27890..1d4cd2b01 100755 --- a/dsf-fhir/dsf-fhir-server/src/main/java/dev/dsf/fhir/search/parameters/ResourceProfile.java +++ b/dsf-fhir/dsf-fhir-server/src/main/java/dev/dsf/fhir/search/parameters/ResourceProfile.java @@ -22,6 +22,7 @@ import org.hl7.fhir.r4.model.Enumerations.SearchParamType; import org.hl7.fhir.r4.model.Resource; +import dev.dsf.fhir.dao.jdbc.PgObjectFactory; import dev.dsf.fhir.function.BiFunctionWithSqlException; import dev.dsf.fhir.search.SearchQueryParameter.SearchParameterDefinition; import dev.dsf.fhir.search.parameters.basic.AbstractCanonicalUrlParameter; @@ -62,7 +63,8 @@ public int getSqlParameterCount() @Override public void modifyStatement(int parameterIndex, int subqueryParameterIndex, PreparedStatement statement, - BiFunctionWithSqlException arrayCreator) throws SQLException + BiFunctionWithSqlException arrayCreator, PgObjectFactory pgObjectFactory) + throws SQLException { switch (valueAndType.type) { diff --git a/dsf-fhir/dsf-fhir-server/src/main/java/dev/dsf/fhir/search/parameters/SubscriptionCriteria.java b/dsf-fhir/dsf-fhir-server/src/main/java/dev/dsf/fhir/search/parameters/SubscriptionCriteria.java index 2dfe86e46..b05d120f1 100755 --- a/dsf-fhir/dsf-fhir-server/src/main/java/dev/dsf/fhir/search/parameters/SubscriptionCriteria.java +++ b/dsf-fhir/dsf-fhir-server/src/main/java/dev/dsf/fhir/search/parameters/SubscriptionCriteria.java @@ -23,6 +23,7 @@ import org.hl7.fhir.r4.model.Enumerations.SearchParamType; import org.hl7.fhir.r4.model.Subscription; +import dev.dsf.fhir.dao.jdbc.PgObjectFactory; import dev.dsf.fhir.function.BiFunctionWithSqlException; import dev.dsf.fhir.search.SearchQueryParameter.SearchParameterDefinition; import dev.dsf.fhir.search.parameters.basic.AbstractStringParameter; @@ -55,7 +56,8 @@ public int getSqlParameterCount() @Override public void modifyStatement(int parameterIndex, int subqueryParameterIndex, PreparedStatement statement, - BiFunctionWithSqlException arrayCreator) throws SQLException + BiFunctionWithSqlException arrayCreator, PgObjectFactory pgObjectFactory) + throws SQLException { switch (valueAndType.type) { diff --git a/dsf-fhir/dsf-fhir-server/src/main/java/dev/dsf/fhir/search/parameters/SubscriptionPayload.java b/dsf-fhir/dsf-fhir-server/src/main/java/dev/dsf/fhir/search/parameters/SubscriptionPayload.java index 8827aa3e9..a41bd627d 100755 --- a/dsf-fhir/dsf-fhir-server/src/main/java/dev/dsf/fhir/search/parameters/SubscriptionPayload.java +++ b/dsf-fhir/dsf-fhir-server/src/main/java/dev/dsf/fhir/search/parameters/SubscriptionPayload.java @@ -24,6 +24,7 @@ import org.hl7.fhir.r4.model.Enumerations.SearchParamType; import org.hl7.fhir.r4.model.Subscription; +import dev.dsf.fhir.dao.jdbc.PgObjectFactory; import dev.dsf.fhir.function.BiFunctionWithSqlException; import dev.dsf.fhir.search.SearchQueryParameter.SearchParameterDefinition; import dev.dsf.fhir.search.SearchQueryParameterError; @@ -78,7 +79,8 @@ public int getSqlParameterCount() @Override public void modifyStatement(int parameterIndex, int subqueryParameterIndex, PreparedStatement statement, - BiFunctionWithSqlException arrayCreator) throws SQLException + BiFunctionWithSqlException arrayCreator, PgObjectFactory pgObjectFactory) + throws SQLException { statement.setString(parameterIndex, payloadMimeType); } diff --git a/dsf-fhir/dsf-fhir-server/src/main/java/dev/dsf/fhir/search/parameters/SubscriptionStatus.java b/dsf-fhir/dsf-fhir-server/src/main/java/dev/dsf/fhir/search/parameters/SubscriptionStatus.java index 5ae1da29d..366f608c0 100755 --- a/dsf-fhir/dsf-fhir-server/src/main/java/dev/dsf/fhir/search/parameters/SubscriptionStatus.java +++ b/dsf-fhir/dsf-fhir-server/src/main/java/dev/dsf/fhir/search/parameters/SubscriptionStatus.java @@ -25,6 +25,7 @@ import org.hl7.fhir.r4.model.Enumerations.SearchParamType; import org.hl7.fhir.r4.model.Subscription; +import dev.dsf.fhir.dao.jdbc.PgObjectFactory; import dev.dsf.fhir.function.BiFunctionWithSqlException; import dev.dsf.fhir.search.SearchQueryParameter.SearchParameterDefinition; import dev.dsf.fhir.search.SearchQueryParameterError; @@ -98,7 +99,8 @@ public int getSqlParameterCount() @Override public void modifyStatement(int parameterIndex, int subqueryParameterIndex, PreparedStatement statement, - BiFunctionWithSqlException arrayCreator) throws SQLException + BiFunctionWithSqlException arrayCreator, PgObjectFactory pgObjectFactory) + throws SQLException { statement.setString(parameterIndex, status.toCode()); } diff --git a/dsf-fhir/dsf-fhir-server/src/main/java/dev/dsf/fhir/search/parameters/SubscriptionType.java b/dsf-fhir/dsf-fhir-server/src/main/java/dev/dsf/fhir/search/parameters/SubscriptionType.java index 4273e219e..3e48e2f86 100755 --- a/dsf-fhir/dsf-fhir-server/src/main/java/dev/dsf/fhir/search/parameters/SubscriptionType.java +++ b/dsf-fhir/dsf-fhir-server/src/main/java/dev/dsf/fhir/search/parameters/SubscriptionType.java @@ -26,6 +26,7 @@ import org.hl7.fhir.r4.model.Subscription; import org.hl7.fhir.r4.model.Subscription.SubscriptionChannelType; +import dev.dsf.fhir.dao.jdbc.PgObjectFactory; import dev.dsf.fhir.function.BiFunctionWithSqlException; import dev.dsf.fhir.search.SearchQueryParameter.SearchParameterDefinition; import dev.dsf.fhir.search.SearchQueryParameterError; @@ -99,7 +100,8 @@ public int getSqlParameterCount() @Override public void modifyStatement(int parameterIndex, int subqueryParameterIndex, PreparedStatement statement, - BiFunctionWithSqlException arrayCreator) throws SQLException + BiFunctionWithSqlException arrayCreator, PgObjectFactory pgObjectFactory) + throws SQLException { statement.setString(parameterIndex, channelType.toCode()); } diff --git a/dsf-fhir/dsf-fhir-server/src/main/java/dev/dsf/fhir/search/parameters/TaskRequester.java b/dsf-fhir/dsf-fhir-server/src/main/java/dev/dsf/fhir/search/parameters/TaskRequester.java index 83f39d4cb..2a0efc6bd 100755 --- a/dsf-fhir/dsf-fhir-server/src/main/java/dev/dsf/fhir/search/parameters/TaskRequester.java +++ b/dsf-fhir/dsf-fhir-server/src/main/java/dev/dsf/fhir/search/parameters/TaskRequester.java @@ -35,6 +35,8 @@ import dev.dsf.fhir.dao.ResourceDao; import dev.dsf.fhir.dao.exception.ResourceDeletedException; +import dev.dsf.fhir.dao.jdbc.PgObjectFactory; +import dev.dsf.fhir.dao.jdbc.PgObjectFactory.IdentifierParameter; import dev.dsf.fhir.dao.provider.DaoProvider; import dev.dsf.fhir.function.BiFunctionWithSqlException; import dev.dsf.fhir.search.IncludeParameterDefinition; @@ -81,16 +83,19 @@ public String getFilterQuery() { // testing all TargetResourceTypeName/ID combinations case ID -> "task->'requester'->>'reference' = ANY (?)"; + case RESOURCE_NAME_AND_ID, URL, TYPE_AND_ID, TYPE_AND_RESOURCE_NAME_AND_ID -> "task->'requester'->>'reference' = ?"; + case IDENTIFIER -> switch (valueAndType.identifier.type) { - case CODE -> - "(" + IDENTIFIERS_SUBQUERY + " @> ?::jsonb OR task->'requester'->'identifier'->>'value' = ?)"; + case CODE -> "(" + IDENTIFIERS_SUBQUERY + " @> ? OR task->'requester'->'identifier'->>'value' = ?)"; + case CODE_AND_SYSTEM -> "(" + IDENTIFIERS_SUBQUERY - + " @> ?::jsonb OR (task->'requester'->'identifier'->>'system' = ? AND task->'requester'->'identifier'->>'value' = ?))"; - case SYSTEM -> - "(" + IDENTIFIERS_SUBQUERY + " @> ?::jsonb OR task->'requester'->'identifier'->>'system' = ?)"; + + " @> ? OR (task->'requester'->'identifier'->>'system' = ? AND task->'requester'->'identifier'->>'value' = ?))"; + + case SYSTEM -> "(" + IDENTIFIERS_SUBQUERY + " @> ? OR task->'requester'->'identifier'->>'system' = ?)"; + case CODE_AND_NO_SYSTEM_PROPERTY -> "((SELECT count(*) FROM jsonb_array_elements(" + IDENTIFIERS_SUBQUERY + ") identifier WHERE identifier->>'value' = ? AND NOT (identifier ?? 'system')) > 0" @@ -105,6 +110,7 @@ public int getSqlParameterCount() return switch (valueAndType.type) { case ID, RESOURCE_NAME_AND_ID, URL, TYPE_AND_ID, TYPE_AND_RESOURCE_NAME_AND_ID -> 1; + case IDENTIFIER -> switch (valueAndType.identifier.type) { case CODE, SYSTEM, CODE_AND_NO_SYSTEM_PROPERTY -> 2; @@ -115,7 +121,8 @@ public int getSqlParameterCount() @Override public void modifyStatement(int parameterIndex, int subqueryParameterIndex, PreparedStatement statement, - BiFunctionWithSqlException arrayCreator) throws SQLException + BiFunctionWithSqlException arrayCreator, PgObjectFactory pgObjectFactory) + throws SQLException { switch (valueAndType.type) { @@ -135,16 +142,17 @@ public void modifyStatement(int parameterIndex, int subqueryParameterIndex, Prep { case CODE -> { if (subqueryParameterIndex == 1) - statement.setString(parameterIndex, - "[{\"value\": \"" + valueAndType.identifier.codeValue + "\"}]"); + statement.setObject(parameterIndex, pgObjectFactory.jsonParameterToPgObjectAsArray( + new IdentifierParameter(null, valueAndType.identifier.codeValue))); else if (subqueryParameterIndex == 2) statement.setString(parameterIndex, valueAndType.identifier.codeValue); } case CODE_AND_SYSTEM -> { if (subqueryParameterIndex == 1) - statement.setString(parameterIndex, "[{\"system\": \"" + valueAndType.identifier.systemValue - + "\", \"value\": \"" + valueAndType.identifier.codeValue + "\"}]"); + statement.setObject(parameterIndex, + pgObjectFactory.jsonParameterToPgObjectAsArray(new IdentifierParameter( + valueAndType.identifier.systemValue, valueAndType.identifier.codeValue))); else if (subqueryParameterIndex == 2) statement.setString(parameterIndex, valueAndType.identifier.systemValue); else if (subqueryParameterIndex == 3) @@ -153,8 +161,8 @@ else if (subqueryParameterIndex == 3) case SYSTEM -> { if (subqueryParameterIndex == 1) - statement.setString(parameterIndex, - "[{\"system\": \"" + valueAndType.identifier.systemValue + "\"}]"); + statement.setObject(parameterIndex, pgObjectFactory.jsonParameterToPgObjectAsArray( + new IdentifierParameter(valueAndType.identifier.systemValue, null))); else if (subqueryParameterIndex == 2) statement.setString(parameterIndex, valueAndType.identifier.systemValue); } diff --git a/dsf-fhir/dsf-fhir-server/src/main/java/dev/dsf/fhir/search/parameters/TaskStatus.java b/dsf-fhir/dsf-fhir-server/src/main/java/dev/dsf/fhir/search/parameters/TaskStatus.java index 96ee1ab02..361f83fbd 100755 --- a/dsf-fhir/dsf-fhir-server/src/main/java/dev/dsf/fhir/search/parameters/TaskStatus.java +++ b/dsf-fhir/dsf-fhir-server/src/main/java/dev/dsf/fhir/search/parameters/TaskStatus.java @@ -25,6 +25,7 @@ import org.hl7.fhir.r4.model.Enumerations.SearchParamType; import org.hl7.fhir.r4.model.Task; +import dev.dsf.fhir.dao.jdbc.PgObjectFactory; import dev.dsf.fhir.function.BiFunctionWithSqlException; import dev.dsf.fhir.search.SearchQueryParameter.SearchParameterDefinition; import dev.dsf.fhir.search.SearchQueryParameterError; @@ -98,7 +99,8 @@ public int getSqlParameterCount() @Override public void modifyStatement(int parameterIndex, int subqueryParameterIndex, PreparedStatement statement, - BiFunctionWithSqlException arrayCreator) throws SQLException + BiFunctionWithSqlException arrayCreator, PgObjectFactory pgObjectFactory) + throws SQLException { statement.setString(parameterIndex, status.toCode()); } diff --git a/dsf-fhir/dsf-fhir-server/src/main/java/dev/dsf/fhir/search/parameters/basic/AbstractActiveParameter.java b/dsf-fhir/dsf-fhir-server/src/main/java/dev/dsf/fhir/search/parameters/basic/AbstractActiveParameter.java index 9759bb11a..fc4de9966 100644 --- a/dsf-fhir/dsf-fhir-server/src/main/java/dev/dsf/fhir/search/parameters/basic/AbstractActiveParameter.java +++ b/dsf-fhir/dsf-fhir-server/src/main/java/dev/dsf/fhir/search/parameters/basic/AbstractActiveParameter.java @@ -23,6 +23,7 @@ import org.hl7.fhir.r4.model.Resource; +import dev.dsf.fhir.dao.jdbc.PgObjectFactory; import dev.dsf.fhir.function.BiFunctionWithSqlException; public class AbstractActiveParameter extends AbstractBooleanParameter @@ -53,7 +54,8 @@ public int getSqlParameterCount() @Override public void modifyStatement(int parameterIndex, int subqueryParameterIndex, PreparedStatement statement, - BiFunctionWithSqlException arrayCreator) throws SQLException + BiFunctionWithSqlException arrayCreator, PgObjectFactory pgObjectFactory) + throws SQLException { statement.setBoolean(parameterIndex, value); } diff --git a/dsf-fhir/dsf-fhir-server/src/main/java/dev/dsf/fhir/search/parameters/basic/AbstractDateTimeParameter.java b/dsf-fhir/dsf-fhir-server/src/main/java/dev/dsf/fhir/search/parameters/basic/AbstractDateTimeParameter.java index ce10e9700..c490ff24b 100755 --- a/dsf-fhir/dsf-fhir-server/src/main/java/dev/dsf/fhir/search/parameters/basic/AbstractDateTimeParameter.java +++ b/dsf-fhir/dsf-fhir-server/src/main/java/dev/dsf/fhir/search/parameters/basic/AbstractDateTimeParameter.java @@ -34,6 +34,7 @@ import org.hl7.fhir.r4.model.InstantType; import org.hl7.fhir.r4.model.Resource; +import dev.dsf.fhir.dao.jdbc.PgObjectFactory; import dev.dsf.fhir.function.BiFunctionWithSqlException; import dev.dsf.fhir.search.SearchQueryParameterError; import dev.dsf.fhir.search.SearchQueryParameterError.SearchQueryParameterErrorType; @@ -294,7 +295,8 @@ public int getSqlParameterCount() @Override public void modifyStatement(int parameterIndex, int subqueryParameterIndex, PreparedStatement statement, - BiFunctionWithSqlException arrayCreator) throws SQLException + BiFunctionWithSqlException arrayCreator, PgObjectFactory pgObjectFactory) + throws SQLException { switch (valueAndType.type) { diff --git a/dsf-fhir/dsf-fhir-server/src/main/java/dev/dsf/fhir/search/parameters/basic/AbstractIdentifierParameter.java b/dsf-fhir/dsf-fhir-server/src/main/java/dev/dsf/fhir/search/parameters/basic/AbstractIdentifierParameter.java index 8ff0c85e1..a2c625c41 100755 --- a/dsf-fhir/dsf-fhir-server/src/main/java/dev/dsf/fhir/search/parameters/basic/AbstractIdentifierParameter.java +++ b/dsf-fhir/dsf-fhir-server/src/main/java/dev/dsf/fhir/search/parameters/basic/AbstractIdentifierParameter.java @@ -27,6 +27,8 @@ import org.hl7.fhir.r4.model.Identifier; import org.hl7.fhir.r4.model.Resource; +import dev.dsf.fhir.dao.jdbc.PgObjectFactory; +import dev.dsf.fhir.dao.jdbc.PgObjectFactory.IdentifierParameter; import dev.dsf.fhir.function.BiFunctionWithSqlException; public abstract class AbstractIdentifierParameter extends AbstractTokenParameter @@ -87,7 +89,8 @@ protected String getPositiveFilterQuery() { return switch (valueAndType.type) { - case CODE, CODE_AND_SYSTEM, SYSTEM -> resourceColumn + "->'identifier' @> ?::jsonb"; + case CODE, CODE_AND_SYSTEM, SYSTEM -> resourceColumn + "->'identifier' @> ?"; + case CODE_AND_NO_SYSTEM_PROPERTY -> "(SELECT count(*) FROM jsonb_array_elements(" + resourceColumn + "->'identifier') identifier WHERE identifier->>'value' = ? AND NOT (identifier ?? 'system')) > 0"; }; @@ -98,7 +101,8 @@ protected String getNegatedFilterQuery() { return switch (valueAndType.type) { - case CODE, CODE_AND_SYSTEM, SYSTEM -> "NOT (" + resourceColumn + "->'identifier' @> ?::jsonb)"; + case CODE, CODE_AND_SYSTEM, SYSTEM -> "NOT (" + resourceColumn + "->'identifier' @> ?)"; + case CODE_AND_NO_SYSTEM_PROPERTY -> "(SELECT count(*) FROM jsonb_array_elements(" + resourceColumn + "->'identifier') identifier WHERE identifier->>'value' <> ? OR (identifier ?? 'system')) > 0"; }; @@ -112,23 +116,21 @@ public int getSqlParameterCount() @Override public void modifyStatement(int parameterIndex, int subqueryParameterIndex, PreparedStatement statement, - BiFunctionWithSqlException arrayCreator) throws SQLException + BiFunctionWithSqlException arrayCreator, PgObjectFactory pgObjectFactory) + throws SQLException { switch (valueAndType.type) { - case CODE: - statement.setString(parameterIndex, "[{\"value\": \"" + valueAndType.codeValue + "\"}]"); - return; - case CODE_AND_SYSTEM: - statement.setString(parameterIndex, "[{\"value\": \"" + valueAndType.codeValue + "\", \"system\": \"" - + valueAndType.systemValue + "\"}]"); - return; - case CODE_AND_NO_SYSTEM_PROPERTY: - statement.setString(parameterIndex, valueAndType.codeValue); - return; - case SYSTEM: - statement.setString(parameterIndex, "[{\"system\": \"" + valueAndType.systemValue + "\"}]"); - return; + case CODE -> statement.setObject(parameterIndex, pgObjectFactory + .jsonParameterToPgObjectAsArray(new IdentifierParameter(null, valueAndType.codeValue))); + + case CODE_AND_SYSTEM -> statement.setObject(parameterIndex, pgObjectFactory.jsonParameterToPgObjectAsArray( + new IdentifierParameter(valueAndType.systemValue, valueAndType.codeValue))); + + case CODE_AND_NO_SYSTEM_PROPERTY -> statement.setString(parameterIndex, valueAndType.codeValue); + + case SYSTEM -> statement.setObject(parameterIndex, pgObjectFactory + .jsonParameterToPgObjectAsArray(new IdentifierParameter(valueAndType.systemValue, null))); } } diff --git a/dsf-fhir/dsf-fhir-server/src/main/java/dev/dsf/fhir/search/parameters/basic/AbstractNameOrAliasParameter.java b/dsf-fhir/dsf-fhir-server/src/main/java/dev/dsf/fhir/search/parameters/basic/AbstractNameOrAliasParameter.java index 0f9e3c61f..d837d9ef9 100644 --- a/dsf-fhir/dsf-fhir-server/src/main/java/dev/dsf/fhir/search/parameters/basic/AbstractNameOrAliasParameter.java +++ b/dsf-fhir/dsf-fhir-server/src/main/java/dev/dsf/fhir/search/parameters/basic/AbstractNameOrAliasParameter.java @@ -26,6 +26,7 @@ import org.hl7.fhir.r4.model.Resource; import org.hl7.fhir.r4.model.StringType; +import dev.dsf.fhir.dao.jdbc.PgObjectFactory; import dev.dsf.fhir.function.BiFunctionWithSqlException; public class AbstractNameOrAliasParameter extends AbstractStringParameter @@ -73,7 +74,8 @@ public int getSqlParameterCount() @Override public void modifyStatement(int parameterIndex, int subqueryParameterIndex, PreparedStatement statement, - BiFunctionWithSqlException arrayCreator) throws SQLException + BiFunctionWithSqlException arrayCreator, PgObjectFactory pgObjectFactory) + throws SQLException { switch (valueAndType.type) { diff --git a/dsf-fhir/dsf-fhir-server/src/main/java/dev/dsf/fhir/search/parameters/basic/AbstractNameParameter.java b/dsf-fhir/dsf-fhir-server/src/main/java/dev/dsf/fhir/search/parameters/basic/AbstractNameParameter.java index abcd5db36..ec232efc1 100644 --- a/dsf-fhir/dsf-fhir-server/src/main/java/dev/dsf/fhir/search/parameters/basic/AbstractNameParameter.java +++ b/dsf-fhir/dsf-fhir-server/src/main/java/dev/dsf/fhir/search/parameters/basic/AbstractNameParameter.java @@ -24,6 +24,7 @@ import org.hl7.fhir.r4.model.Resource; +import dev.dsf.fhir.dao.jdbc.PgObjectFactory; import dev.dsf.fhir.function.BiFunctionWithSqlException; public class AbstractNameParameter extends AbstractStringParameter @@ -62,7 +63,8 @@ public int getSqlParameterCount() @Override public void modifyStatement(int parameterIndex, int subqueryParameterIndex, PreparedStatement statement, - BiFunctionWithSqlException arrayCreator) throws SQLException + BiFunctionWithSqlException arrayCreator, PgObjectFactory pgObjectFactory) + throws SQLException { switch (valueAndType.type) { diff --git a/dsf-fhir/dsf-fhir-server/src/main/java/dev/dsf/fhir/search/parameters/basic/AbstractSingleIdentifierParameter.java b/dsf-fhir/dsf-fhir-server/src/main/java/dev/dsf/fhir/search/parameters/basic/AbstractSingleIdentifierParameter.java index 06c30318b..762769353 100644 --- a/dsf-fhir/dsf-fhir-server/src/main/java/dev/dsf/fhir/search/parameters/basic/AbstractSingleIdentifierParameter.java +++ b/dsf-fhir/dsf-fhir-server/src/main/java/dev/dsf/fhir/search/parameters/basic/AbstractSingleIdentifierParameter.java @@ -22,6 +22,8 @@ import org.hl7.fhir.r4.model.Resource; +import dev.dsf.fhir.dao.jdbc.PgObjectFactory; +import dev.dsf.fhir.dao.jdbc.PgObjectFactory.IdentifierParameter; import dev.dsf.fhir.function.BiFunctionWithSqlException; public class AbstractSingleIdentifierParameter extends AbstractIdentifierParameter @@ -37,7 +39,7 @@ protected String getPositiveFilterQuery() { return switch (valueAndType.type) { - case CODE, CODE_AND_SYSTEM, SYSTEM -> resourceColumn + "->'identifier' = ?::jsonb"; + case CODE, CODE_AND_SYSTEM, SYSTEM -> resourceColumn + "->'identifier' = ?"; case CODE_AND_NO_SYSTEM_PROPERTY -> resourceColumn + "->'identifier'->>'value' = ? AND NOT (" + resourceColumn + "->'identifier' ?? 'system')"; }; @@ -48,7 +50,7 @@ protected String getNegatedFilterQuery() { return switch (valueAndType.type) { - case CODE, CODE_AND_SYSTEM, SYSTEM -> resourceColumn + "->'identifier' <> ?::jsonb"; + case CODE, CODE_AND_SYSTEM, SYSTEM -> resourceColumn + "->'identifier' <> ?"; case CODE_AND_NO_SYSTEM_PROPERTY -> resourceColumn + "->'identifier'->>'value' <> ? OR (" + resourceColumn + "->'identifier' ?? 'system')"; }; @@ -62,26 +64,21 @@ public int getSqlParameterCount() @Override public void modifyStatement(int parameterIndex, int subqueryParameterIndex, PreparedStatement statement, - BiFunctionWithSqlException arrayCreator) throws SQLException + BiFunctionWithSqlException arrayCreator, PgObjectFactory pgObjectFactory) + throws SQLException { switch (valueAndType.type) { - case CODE: - statement.setString(parameterIndex, "{\"value\": \"" + valueAndType.codeValue + "\"}"); - return; + case CODE -> statement.setObject(parameterIndex, + pgObjectFactory.jsonParameterToPgObject(new IdentifierParameter(null, valueAndType.codeValue))); - case CODE_AND_SYSTEM: - statement.setString(parameterIndex, "{\"value\": \"" + valueAndType.codeValue + "\", \"system\": \"" - + valueAndType.systemValue + "\"}"); - return; + case CODE_AND_SYSTEM -> statement.setObject(parameterIndex, pgObjectFactory.jsonParameterToPgObject( + new IdentifierParameter(valueAndType.systemValue, valueAndType.codeValue))); - case CODE_AND_NO_SYSTEM_PROPERTY: - statement.setString(parameterIndex, valueAndType.codeValue); - return; + case CODE_AND_NO_SYSTEM_PROPERTY -> statement.setString(parameterIndex, valueAndType.codeValue); - case SYSTEM: - statement.setString(parameterIndex, "{\"system\": \"" + valueAndType.systemValue + "\"}"); - return; + case SYSTEM -> statement.setObject(parameterIndex, + pgObjectFactory.jsonParameterToPgObject(new IdentifierParameter(valueAndType.systemValue, null))); } } diff --git a/dsf-fhir/dsf-fhir-server/src/main/java/dev/dsf/fhir/search/parameters/basic/AbstractStatusParameter.java b/dsf-fhir/dsf-fhir-server/src/main/java/dev/dsf/fhir/search/parameters/basic/AbstractStatusParameter.java index c26f21781..548fc77d1 100644 --- a/dsf-fhir/dsf-fhir-server/src/main/java/dev/dsf/fhir/search/parameters/basic/AbstractStatusParameter.java +++ b/dsf-fhir/dsf-fhir-server/src/main/java/dev/dsf/fhir/search/parameters/basic/AbstractStatusParameter.java @@ -25,6 +25,7 @@ import org.hl7.fhir.r4.model.Enumerations.PublicationStatus; import org.hl7.fhir.r4.model.MetadataResource; +import dev.dsf.fhir.dao.jdbc.PgObjectFactory; import dev.dsf.fhir.function.BiFunctionWithSqlException; import dev.dsf.fhir.search.SearchQueryParameterError; import dev.dsf.fhir.search.SearchQueryParameterError.SearchQueryParameterErrorType; @@ -98,7 +99,8 @@ public int getSqlParameterCount() @Override public void modifyStatement(int parameterIndex, int subqueryParameterIndex, PreparedStatement statement, - BiFunctionWithSqlException arrayCreator) throws SQLException + BiFunctionWithSqlException arrayCreator, PgObjectFactory pgObjectFactory) + throws SQLException { statement.setString(parameterIndex, status.toCode()); } diff --git a/dsf-fhir/dsf-fhir-server/src/main/java/dev/dsf/fhir/search/parameters/basic/AbstractUrlAndVersionParameter.java b/dsf-fhir/dsf-fhir-server/src/main/java/dev/dsf/fhir/search/parameters/basic/AbstractUrlAndVersionParameter.java index 849aa565e..e366f4238 100755 --- a/dsf-fhir/dsf-fhir-server/src/main/java/dev/dsf/fhir/search/parameters/basic/AbstractUrlAndVersionParameter.java +++ b/dsf-fhir/dsf-fhir-server/src/main/java/dev/dsf/fhir/search/parameters/basic/AbstractUrlAndVersionParameter.java @@ -22,6 +22,7 @@ import org.hl7.fhir.r4.model.MetadataResource; +import dev.dsf.fhir.dao.jdbc.PgObjectFactory; import dev.dsf.fhir.function.BiFunctionWithSqlException; public abstract class AbstractUrlAndVersionParameter @@ -58,7 +59,8 @@ public int getSqlParameterCount() @Override public void modifyStatement(int parameterIndex, int subqueryParameterIndex, PreparedStatement statement, - BiFunctionWithSqlException arrayCreator) throws SQLException + BiFunctionWithSqlException arrayCreator, PgObjectFactory pgObjectFactory) + throws SQLException { if (subqueryParameterIndex == 1) { diff --git a/dsf-fhir/dsf-fhir-server/src/main/java/dev/dsf/fhir/search/parameters/basic/AbstractVersionParameter.java b/dsf-fhir/dsf-fhir-server/src/main/java/dev/dsf/fhir/search/parameters/basic/AbstractVersionParameter.java index 43dfc80f5..431cb08bc 100755 --- a/dsf-fhir/dsf-fhir-server/src/main/java/dev/dsf/fhir/search/parameters/basic/AbstractVersionParameter.java +++ b/dsf-fhir/dsf-fhir-server/src/main/java/dev/dsf/fhir/search/parameters/basic/AbstractVersionParameter.java @@ -23,6 +23,7 @@ import org.hl7.fhir.r4.model.MetadataResource; +import dev.dsf.fhir.dao.jdbc.PgObjectFactory; import dev.dsf.fhir.function.BiFunctionWithSqlException; import dev.dsf.fhir.search.SearchQueryParameterError; import dev.dsf.fhir.search.SearchQueryParameterError.SearchQueryParameterErrorType; @@ -81,7 +82,8 @@ public int getSqlParameterCount() @Override public void modifyStatement(int parameterIndex, int subqueryParameterIndex, PreparedStatement statement, - BiFunctionWithSqlException arrayCreator) throws SQLException + BiFunctionWithSqlException arrayCreator, PgObjectFactory pgObjectFactory) + throws SQLException { statement.setString(parameterIndex, version); } diff --git a/dsf-fhir/dsf-fhir-server/src/main/java/dev/dsf/fhir/spring/config/DaoConfig.java b/dsf-fhir/dsf-fhir-server/src/main/java/dev/dsf/fhir/spring/config/DaoConfig.java index 75a5b68f5..0f9cfd75b 100755 --- a/dsf-fhir/dsf-fhir-server/src/main/java/dev/dsf/fhir/spring/config/DaoConfig.java +++ b/dsf-fhir/dsf-fhir-server/src/main/java/dev/dsf/fhir/spring/config/DaoConfig.java @@ -70,6 +70,7 @@ import dev.dsf.fhir.dao.jdbc.OrganizationAffiliationDaoJdbc; import dev.dsf.fhir.dao.jdbc.OrganizationDaoJdbc; import dev.dsf.fhir.dao.jdbc.PatientDaoJdbc; +import dev.dsf.fhir.dao.jdbc.PgObjectFactoryImpl; import dev.dsf.fhir.dao.jdbc.PractitionerDaoJdbc; import dev.dsf.fhir.dao.jdbc.PractitionerRoleDaoJdbc; import dev.dsf.fhir.dao.jdbc.ProvenanceDaoJdbc; @@ -95,6 +96,9 @@ public class DaoConfig @Autowired private FhirConfig fhirConfig; + @Autowired + private JsonConfig jsonConfig; + @Bean public DataSource dataSource() { @@ -135,165 +139,190 @@ private String toString(char[] password) @Bean public ActivityDefinitionDao activityDefinitionDao() { - return new ActivityDefinitionDaoJdbc(dataSource(), permanentDeleteDataSource(), fhirConfig.fhirContext()); + return new ActivityDefinitionDaoJdbc(dataSource(), permanentDeleteDataSource(), fhirConfig.fhirContext(), + jsonConfig.objectMapper()); } @Bean(destroyMethod = "stopLargeObjectUnlinker") public BinaryDao binaryDao() { return new BinaryDaoJdbc(dataSource(), permanentDeleteDataSource(), fhirConfig.fhirContext(), - propertiesConfig.getDbUsersGroup()); + jsonConfig.objectMapper(), propertiesConfig.getDbUsersGroup()); } @Bean public BundleDao bundleDao() { - return new BundleDaoJdbc(dataSource(), permanentDeleteDataSource(), fhirConfig.fhirContext()); + return new BundleDaoJdbc(dataSource(), permanentDeleteDataSource(), fhirConfig.fhirContext(), + jsonConfig.objectMapper()); } @Bean public CodeSystemDao codeSystemDao() { - return new CodeSystemDaoJdbc(dataSource(), permanentDeleteDataSource(), fhirConfig.fhirContext()); + return new CodeSystemDaoJdbc(dataSource(), permanentDeleteDataSource(), fhirConfig.fhirContext(), + jsonConfig.objectMapper()); } @Bean public DocumentReferenceDao documentReferenceDao() { - return new DocumentReferenceDaoJdbc(dataSource(), permanentDeleteDataSource(), fhirConfig.fhirContext()); + return new DocumentReferenceDaoJdbc(dataSource(), permanentDeleteDataSource(), fhirConfig.fhirContext(), + jsonConfig.objectMapper()); } @Bean public EndpointDao endpointDao() { - return new EndpointDaoJdbc(dataSource(), permanentDeleteDataSource(), fhirConfig.fhirContext()); + return new EndpointDaoJdbc(dataSource(), permanentDeleteDataSource(), fhirConfig.fhirContext(), + jsonConfig.objectMapper()); } @Bean public GroupDao groupDao() { - return new GroupDaoJdbc(dataSource(), permanentDeleteDataSource(), fhirConfig.fhirContext()); + return new GroupDaoJdbc(dataSource(), permanentDeleteDataSource(), fhirConfig.fhirContext(), + jsonConfig.objectMapper()); } @Bean public HealthcareServiceDao healthcareServiceDao() { - return new HealthcareServiceDaoJdbc(dataSource(), permanentDeleteDataSource(), fhirConfig.fhirContext()); + return new HealthcareServiceDaoJdbc(dataSource(), permanentDeleteDataSource(), fhirConfig.fhirContext(), + jsonConfig.objectMapper()); } @Bean public LibraryDao libraryDao() { - return new LibraryDaoJdbc(dataSource(), permanentDeleteDataSource(), fhirConfig.fhirContext()); + return new LibraryDaoJdbc(dataSource(), permanentDeleteDataSource(), fhirConfig.fhirContext(), + jsonConfig.objectMapper()); } @Bean public LocationDao locationDao() { - return new LocationDaoJdbc(dataSource(), permanentDeleteDataSource(), fhirConfig.fhirContext()); + return new LocationDaoJdbc(dataSource(), permanentDeleteDataSource(), fhirConfig.fhirContext(), + jsonConfig.objectMapper()); } @Bean public MeasureDao measureDao() { - return new MeasureDaoJdbc(dataSource(), permanentDeleteDataSource(), fhirConfig.fhirContext()); + return new MeasureDaoJdbc(dataSource(), permanentDeleteDataSource(), fhirConfig.fhirContext(), + jsonConfig.objectMapper()); } @Bean public MeasureReportDao measureReportDao() { - return new MeasureReportDaoJdbc(dataSource(), permanentDeleteDataSource(), fhirConfig.fhirContext()); + return new MeasureReportDaoJdbc(dataSource(), permanentDeleteDataSource(), fhirConfig.fhirContext(), + jsonConfig.objectMapper()); } @Bean public NamingSystemDao namingSystemDao() { - return new NamingSystemDaoJdbc(dataSource(), permanentDeleteDataSource(), fhirConfig.fhirContext()); + return new NamingSystemDaoJdbc(dataSource(), permanentDeleteDataSource(), fhirConfig.fhirContext(), + jsonConfig.objectMapper()); } @Bean public OrganizationDao organizationDao() { - return new OrganizationDaoJdbc(dataSource(), permanentDeleteDataSource(), fhirConfig.fhirContext()); + return new OrganizationDaoJdbc(dataSource(), permanentDeleteDataSource(), fhirConfig.fhirContext(), + jsonConfig.objectMapper()); } @Bean public OrganizationAffiliationDao organizationAffiliationDao() { - return new OrganizationAffiliationDaoJdbc(dataSource(), permanentDeleteDataSource(), fhirConfig.fhirContext()); + return new OrganizationAffiliationDaoJdbc(dataSource(), permanentDeleteDataSource(), fhirConfig.fhirContext(), + jsonConfig.objectMapper()); } @Bean public PatientDao patientDao() { - return new PatientDaoJdbc(dataSource(), permanentDeleteDataSource(), fhirConfig.fhirContext()); + return new PatientDaoJdbc(dataSource(), permanentDeleteDataSource(), fhirConfig.fhirContext(), + jsonConfig.objectMapper()); } @Bean public PractitionerDao practitionerDao() { - return new PractitionerDaoJdbc(dataSource(), permanentDeleteDataSource(), fhirConfig.fhirContext()); + return new PractitionerDaoJdbc(dataSource(), permanentDeleteDataSource(), fhirConfig.fhirContext(), + jsonConfig.objectMapper()); } @Bean public PractitionerRoleDao practitionerRoleDao() { - return new PractitionerRoleDaoJdbc(dataSource(), permanentDeleteDataSource(), fhirConfig.fhirContext()); + return new PractitionerRoleDaoJdbc(dataSource(), permanentDeleteDataSource(), fhirConfig.fhirContext(), + jsonConfig.objectMapper()); } @Bean public ProvenanceDao provenanceDao() { - return new ProvenanceDaoJdbc(dataSource(), permanentDeleteDataSource(), fhirConfig.fhirContext()); + return new ProvenanceDaoJdbc(dataSource(), permanentDeleteDataSource(), fhirConfig.fhirContext(), + jsonConfig.objectMapper()); } @Bean public QuestionnaireDao questionnaireDao() { - return new QuestionnaireDaoJdbc(dataSource(), permanentDeleteDataSource(), fhirConfig.fhirContext()); + return new QuestionnaireDaoJdbc(dataSource(), permanentDeleteDataSource(), fhirConfig.fhirContext(), + jsonConfig.objectMapper()); } @Bean public QuestionnaireResponseDao questionnaireResponseDao() { - return new QuestionnaireResponseDaoJdbc(dataSource(), permanentDeleteDataSource(), fhirConfig.fhirContext()); + return new QuestionnaireResponseDaoJdbc(dataSource(), permanentDeleteDataSource(), fhirConfig.fhirContext(), + jsonConfig.objectMapper()); } @Bean public ResearchStudyDao researchStudyDao() { - return new ResearchStudyDaoJdbc(dataSource(), permanentDeleteDataSource(), fhirConfig.fhirContext()); + return new ResearchStudyDaoJdbc(dataSource(), permanentDeleteDataSource(), fhirConfig.fhirContext(), + jsonConfig.objectMapper()); } @Bean public StructureDefinitionDao structureDefinitionDao() { - return new StructureDefinitionDaoJdbc(dataSource(), permanentDeleteDataSource(), fhirConfig.fhirContext()); + return new StructureDefinitionDaoJdbc(dataSource(), permanentDeleteDataSource(), fhirConfig.fhirContext(), + jsonConfig.objectMapper()); } @Bean public StructureDefinitionDao structureDefinitionSnapshotDao() { return new StructureDefinitionSnapshotDaoJdbc(dataSource(), permanentDeleteDataSource(), - fhirConfig.fhirContext()); + fhirConfig.fhirContext(), jsonConfig.objectMapper()); } @Bean public SubscriptionDao subscriptionDao() { - return new SubscriptionDaoJdbc(dataSource(), permanentDeleteDataSource(), fhirConfig.fhirContext()); + return new SubscriptionDaoJdbc(dataSource(), permanentDeleteDataSource(), fhirConfig.fhirContext(), + jsonConfig.objectMapper()); } @Bean public TaskDao taskDao() { - return new TaskDaoJdbc(dataSource(), permanentDeleteDataSource(), fhirConfig.fhirContext()); + return new TaskDaoJdbc(dataSource(), permanentDeleteDataSource(), fhirConfig.fhirContext(), + jsonConfig.objectMapper()); } @Bean public ValueSetDao valueSetDao() { - return new ValueSetDaoJdbc(dataSource(), permanentDeleteDataSource(), fhirConfig.fhirContext()); + return new ValueSetDaoJdbc(dataSource(), permanentDeleteDataSource(), fhirConfig.fhirContext(), + jsonConfig.objectMapper()); } @Bean @@ -310,7 +339,8 @@ public DaoProvider daoProvider() @Bean public HistoryDao historyDao() { - return new HistroyDaoJdbc(dataSource(), fhirConfig.fhirContext(), (BinaryDaoJdbc) binaryDao()); + return new HistroyDaoJdbc(dataSource(), fhirConfig.fhirContext(), (BinaryDaoJdbc) binaryDao(), + new PgObjectFactoryImpl(fhirConfig.fhirContext(), jsonConfig.objectMapper())); } @Bean 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 34ae1aa84..d99622a37 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 @@ -42,6 +42,8 @@ import org.junit.Test; import org.postgresql.util.PGobject; +import com.fasterxml.jackson.databind.ObjectMapper; + import ca.uhn.fhir.context.FhirContext; import dev.dsf.common.auth.conf.Identity; import dev.dsf.fhir.authorization.read.ReadAccessHelperImpl; @@ -57,7 +59,7 @@ public abstract class AbstractReadAccessDaoTest resouceClass, - TriFunction daoCreator) + QuadFunction daoCreator) { super(resouceClass, daoCreator); } @@ -156,8 +158,8 @@ public void testReadAccessTriggerOrganization() throws Exception Organization org = new Organization(); org.setActive(true); org.addIdentifier().setSystem(ORGANIZATION_IDENTIFIER_SYSTEM).setValue("org.com"); - Organization createdOrg = new OrganizationDaoJdbc(defaultDataSource, permanentDeleteDataSource, fhirContext) - .create(org); + Organization createdOrg = new OrganizationDaoJdbc(defaultDataSource, permanentDeleteDataSource, fhirContext, + objectMapper).create(org); D d = createResource(); readAccessHelper.addOrganization(d, createdOrg); @@ -184,8 +186,8 @@ public void testReadAccessTriggerOrganizationResourceFirst() throws Exception Organization org = new Organization(); org.setActive(true); org.addIdentifier().setSystem(ORGANIZATION_IDENTIFIER_SYSTEM).setValue(orgIdentifier); - Organization createdOrg = new OrganizationDaoJdbc(defaultDataSource, permanentDeleteDataSource, fhirContext) - .create(org); + Organization createdOrg = new OrganizationDaoJdbc(defaultDataSource, permanentDeleteDataSource, fhirContext, + objectMapper).create(org); assertReadAccessEntryCount(2, 1, createdD, READ_ACCESS_TAG_VALUE_LOCAL); assertReadAccessEntryCount(2, 1, createdD, READ_ACCESS_TAG_VALUE_ORGANIZATION, createdOrg); @@ -195,7 +197,7 @@ public void testReadAccessTriggerOrganizationResourceFirst() throws Exception public void testReadAccessTriggerOrganization2Organizations1Matching() throws Exception { OrganizationDaoJdbc organizationDao = new OrganizationDaoJdbc(defaultDataSource, permanentDeleteDataSource, - fhirContext); + fhirContext, objectMapper); Organization org1 = new Organization(); org1.setActive(true); @@ -221,7 +223,7 @@ public void testReadAccessTriggerOrganization2Organizations1Matching() throws Ex public void testReadAccessTriggerOrganization2Organizations2Matching() throws Exception { OrganizationDaoJdbc organizationDao = new OrganizationDaoJdbc(defaultDataSource, permanentDeleteDataSource, - fhirContext); + fhirContext, objectMapper); Organization org1 = new Organization(); org1.setActive(true); @@ -255,7 +257,8 @@ public void testReadAccessTriggerRole() throws Exception memberOrg.setActive(true); memberOrg.addIdentifier().setSystem(ORGANIZATION_IDENTIFIER_SYSTEM).setValue("member.com"); - OrganizationDao orgDao = new OrganizationDaoJdbc(defaultDataSource, permanentDeleteDataSource, fhirContext); + OrganizationDao orgDao = new OrganizationDaoJdbc(defaultDataSource, permanentDeleteDataSource, fhirContext, + objectMapper); Organization createdParentOrg = orgDao.create(parentOrg); Organization createdMemberOrg = orgDao.create(memberOrg); @@ -267,7 +270,7 @@ public void testReadAccessTriggerRole() throws Exception aff.getParticipatingOrganization().setReference("Organization/" + createdMemberOrg.getIdElement().getIdPart()); OrganizationAffiliation createdAff = new OrganizationAffiliationDaoJdbc(defaultDataSource, - permanentDeleteDataSource, fhirContext).create(aff); + permanentDeleteDataSource, fhirContext, objectMapper).create(aff); D d = createResource(); readAccessHelper.addRole(d, "parent.com", "http://dsf.dev/fhir/CodeSystem/organization-role", "DIC"); @@ -297,7 +300,8 @@ public void testReadAccessTriggerRoleResourceFirst() throws Exception memberOrg.setActive(true); memberOrg.addIdentifier().setSystem(ORGANIZATION_IDENTIFIER_SYSTEM).setValue("member.com"); - OrganizationDao orgDao = new OrganizationDaoJdbc(defaultDataSource, permanentDeleteDataSource, fhirContext); + OrganizationDao orgDao = new OrganizationDaoJdbc(defaultDataSource, permanentDeleteDataSource, fhirContext, + objectMapper); Organization createdParentOrg = orgDao.create(parentOrg); Organization createdMemberOrg = orgDao.create(memberOrg); @@ -309,7 +313,7 @@ public void testReadAccessTriggerRoleResourceFirst() throws Exception aff.getParticipatingOrganization().setReference("Organization/" + createdMemberOrg.getIdElement().getIdPart()); OrganizationAffiliation createdAff = new OrganizationAffiliationDaoJdbc(defaultDataSource, - permanentDeleteDataSource, fhirContext).create(aff); + permanentDeleteDataSource, fhirContext, objectMapper).create(aff); assertReadAccessEntryCount(2, 1, createdD, READ_ACCESS_TAG_VALUE_LOCAL); assertReadAccessEntryCount(2, 1, createdD, READ_ACCESS_TAG_VALUE_ROLE, createdMemberOrg, createdAff); @@ -330,7 +334,8 @@ public void testReadAccessTriggerRole2Organizations1Matching() throws Exception memberOrg2.setActive(true); memberOrg2.addIdentifier().setSystem(ORGANIZATION_IDENTIFIER_SYSTEM).setValue("member2.com"); - OrganizationDao orgDao = new OrganizationDaoJdbc(defaultDataSource, permanentDeleteDataSource, fhirContext); + OrganizationDao orgDao = new OrganizationDaoJdbc(defaultDataSource, permanentDeleteDataSource, fhirContext, + objectMapper); Organization createdParentOrg = orgDao.create(parentOrg); Organization createdMemberOrg1 = orgDao.create(memberOrg1); Organization createdMemberOrg2 = orgDao.create(memberOrg2); @@ -352,7 +357,7 @@ public void testReadAccessTriggerRole2Organizations1Matching() throws Exception .setReference("Organization/" + createdMemberOrg2.getIdElement().getIdPart()); OrganizationAffiliationDaoJdbc organizationAffiliationDao = new OrganizationAffiliationDaoJdbc( - defaultDataSource, permanentDeleteDataSource, fhirContext); + defaultDataSource, permanentDeleteDataSource, fhirContext, objectMapper); OrganizationAffiliation createdAff1 = organizationAffiliationDao.create(aff1); OrganizationAffiliation createdAff2 = organizationAffiliationDao.create(aff2); @@ -381,7 +386,8 @@ public void testReadAccessTriggerRole2Organizations2Matching() throws Exception memberOrg2.setActive(true); memberOrg2.addIdentifier().setSystem(ORGANIZATION_IDENTIFIER_SYSTEM).setValue("member2.com"); - OrganizationDao orgDao = new OrganizationDaoJdbc(defaultDataSource, permanentDeleteDataSource, fhirContext); + OrganizationDao orgDao = new OrganizationDaoJdbc(defaultDataSource, permanentDeleteDataSource, fhirContext, + objectMapper); Organization createdParentOrg = orgDao.create(parentOrg); Organization createdMemberOrg1 = orgDao.create(memberOrg1); Organization createdMemberOrg2 = orgDao.create(memberOrg2); @@ -403,7 +409,7 @@ public void testReadAccessTriggerRole2Organizations2Matching() throws Exception .setReference("Organization/" + createdMemberOrg2.getIdElement().getIdPart()); OrganizationAffiliationDaoJdbc organizationAffiliationDao = new OrganizationAffiliationDaoJdbc( - defaultDataSource, permanentDeleteDataSource, fhirContext); + defaultDataSource, permanentDeleteDataSource, fhirContext, objectMapper); OrganizationAffiliation createdAff1 = organizationAffiliationDao.create(aff1); OrganizationAffiliation createdAff2 = organizationAffiliationDao.create(aff2); @@ -459,7 +465,7 @@ public void testReadAccessTriggerLocalUpdate() throws Exception public void testReadAccessTriggerOrganizationUpdate() throws Exception { final OrganizationDaoJdbc organizationDao = new OrganizationDaoJdbc(defaultDataSource, - permanentDeleteDataSource, fhirContext); + permanentDeleteDataSource, fhirContext, objectMapper); Organization org = new Organization(); org.setActive(true); @@ -492,7 +498,7 @@ public void testReadAccessTriggerOrganizationUpdate() throws Exception public void testReadAccessTriggerRoleUpdate() throws Exception { final OrganizationAffiliationDaoJdbc organizationAffiliationDao = new OrganizationAffiliationDaoJdbc( - defaultDataSource, permanentDeleteDataSource, fhirContext); + defaultDataSource, permanentDeleteDataSource, fhirContext, objectMapper); Organization parentOrg = new Organization(); parentOrg.setActive(true); @@ -502,7 +508,8 @@ public void testReadAccessTriggerRoleUpdate() throws Exception memberOrg.setActive(true); memberOrg.addIdentifier().setSystem(ORGANIZATION_IDENTIFIER_SYSTEM).setValue("member.com"); - OrganizationDao orgDao = new OrganizationDaoJdbc(defaultDataSource, permanentDeleteDataSource, fhirContext); + OrganizationDao orgDao = new OrganizationDaoJdbc(defaultDataSource, permanentDeleteDataSource, fhirContext, + objectMapper); Organization createdParentOrg = orgDao.create(parentOrg); Organization createdMemberOrg = orgDao.create(memberOrg); @@ -541,7 +548,7 @@ public void testReadAccessTriggerRoleUpdate() throws Exception public void testReadAccessTriggerRoleUpdateRoleChange() throws Exception { final OrganizationAffiliationDaoJdbc organizationAffiliationDao = new OrganizationAffiliationDaoJdbc( - defaultDataSource, permanentDeleteDataSource, fhirContext); + defaultDataSource, permanentDeleteDataSource, fhirContext, objectMapper); Organization parentOrg = new Organization(); parentOrg.setActive(true); @@ -551,7 +558,8 @@ public void testReadAccessTriggerRoleUpdateRoleChange() throws Exception memberOrg.setActive(true); memberOrg.addIdentifier().setSystem(ORGANIZATION_IDENTIFIER_SYSTEM).setValue("member.com"); - OrganizationDao orgDao = new OrganizationDaoJdbc(defaultDataSource, permanentDeleteDataSource, fhirContext); + OrganizationDao orgDao = new OrganizationDaoJdbc(defaultDataSource, permanentDeleteDataSource, fhirContext, + objectMapper); Organization createdParentOrg = orgDao.create(parentOrg); Organization createdMemberOrg = orgDao.create(memberOrg); @@ -584,7 +592,7 @@ public void testReadAccessTriggerRoleUpdateRoleChange() throws Exception public void testReadAccessTriggerRoleUpdateMemberOrganizationNonActive() throws Exception { final OrganizationAffiliationDaoJdbc organizationAffiliationDao = new OrganizationAffiliationDaoJdbc( - defaultDataSource, permanentDeleteDataSource, fhirContext); + defaultDataSource, permanentDeleteDataSource, fhirContext, objectMapper); Organization parentOrg = new Organization(); parentOrg.setActive(true); @@ -594,7 +602,8 @@ public void testReadAccessTriggerRoleUpdateMemberOrganizationNonActive() throws memberOrg.setActive(true); memberOrg.addIdentifier().setSystem(ORGANIZATION_IDENTIFIER_SYSTEM).setValue("member.com"); - OrganizationDao orgDao = new OrganizationDaoJdbc(defaultDataSource, permanentDeleteDataSource, fhirContext); + OrganizationDao orgDao = new OrganizationDaoJdbc(defaultDataSource, permanentDeleteDataSource, fhirContext, + objectMapper); Organization createdParentOrg = orgDao.create(parentOrg); Organization createdMemberOrg = orgDao.create(memberOrg); @@ -633,7 +642,7 @@ public void testReadAccessTriggerRoleUpdateMemberOrganizationNonActive() throws public void testReadAccessTriggerRoleUpdateParentOrganizationNonActive() throws Exception { final OrganizationAffiliationDaoJdbc organizationAffiliationDao = new OrganizationAffiliationDaoJdbc( - defaultDataSource, permanentDeleteDataSource, fhirContext); + defaultDataSource, permanentDeleteDataSource, fhirContext, objectMapper); Organization parentOrg = new Organization(); parentOrg.setActive(true); @@ -643,7 +652,8 @@ public void testReadAccessTriggerRoleUpdateParentOrganizationNonActive() throws memberOrg.setActive(true); memberOrg.addIdentifier().setSystem(ORGANIZATION_IDENTIFIER_SYSTEM).setValue("member.com"); - OrganizationDao orgDao = new OrganizationDaoJdbc(defaultDataSource, permanentDeleteDataSource, fhirContext); + OrganizationDao orgDao = new OrganizationDaoJdbc(defaultDataSource, permanentDeleteDataSource, fhirContext, + objectMapper); Organization createdParentOrg = orgDao.create(parentOrg); Organization createdMemberOrg = orgDao.create(memberOrg); @@ -682,7 +692,7 @@ public void testReadAccessTriggerRoleUpdateParentOrganizationNonActive() throws public void testReadAccessTriggerRoleUpdateMemberAndParentOrganizationNonActive() throws Exception { final OrganizationAffiliationDaoJdbc organizationAffiliationDao = new OrganizationAffiliationDaoJdbc( - defaultDataSource, permanentDeleteDataSource, fhirContext); + defaultDataSource, permanentDeleteDataSource, fhirContext, objectMapper); Organization parentOrg = new Organization(); parentOrg.setActive(true); @@ -692,7 +702,8 @@ public void testReadAccessTriggerRoleUpdateMemberAndParentOrganizationNonActive( memberOrg.setActive(true); memberOrg.addIdentifier().setSystem(ORGANIZATION_IDENTIFIER_SYSTEM).setValue("member.com"); - OrganizationDao orgDao = new OrganizationDaoJdbc(defaultDataSource, permanentDeleteDataSource, fhirContext); + OrganizationDao orgDao = new OrganizationDaoJdbc(defaultDataSource, permanentDeleteDataSource, fhirContext, + objectMapper); Organization createdParentOrg = orgDao.create(parentOrg); Organization createdMemberOrg = orgDao.create(memberOrg); @@ -775,7 +786,7 @@ public void testReadAccessTriggerLocalDelete() throws Exception public void testReadAccessTriggerOrganizationDeleteOrganization() throws Exception { final OrganizationDaoJdbc organizationDao = new OrganizationDaoJdbc(defaultDataSource, - permanentDeleteDataSource, fhirContext); + permanentDeleteDataSource, fhirContext, objectMapper); Organization org = new Organization(); org.setActive(true); @@ -810,7 +821,7 @@ public void testReadAccessTriggerOrganizationDeleteOrganization() throws Excepti public void testReadAccessTriggerOrganizationDeleteResource() throws Exception { final OrganizationDaoJdbc organizationDao = new OrganizationDaoJdbc(defaultDataSource, - permanentDeleteDataSource, fhirContext); + permanentDeleteDataSource, fhirContext, objectMapper); Organization org = new Organization(); org.setActive(true); @@ -851,7 +862,8 @@ public void testReadAccessTriggerRoleDeleteOrganizationAffiliation() throws Exce memberOrg.setActive(true); memberOrg.addIdentifier().setSystem(ORGANIZATION_IDENTIFIER_SYSTEM).setValue("member.com"); - OrganizationDao orgDao = new OrganizationDaoJdbc(defaultDataSource, permanentDeleteDataSource, fhirContext); + OrganizationDao orgDao = new OrganizationDaoJdbc(defaultDataSource, permanentDeleteDataSource, fhirContext, + objectMapper); Organization createdParentOrg = orgDao.create(parentOrg); Organization createdMemberOrg = orgDao.create(memberOrg); @@ -863,7 +875,7 @@ public void testReadAccessTriggerRoleDeleteOrganizationAffiliation() throws Exce aff.getParticipatingOrganization().setReference("Organization/" + createdMemberOrg.getIdElement().getIdPart()); OrganizationAffiliationDaoJdbc orgAffDao = new OrganizationAffiliationDaoJdbc(defaultDataSource, - permanentDeleteDataSource, fhirContext); + permanentDeleteDataSource, fhirContext, objectMapper); OrganizationAffiliation createdAff = orgAffDao.create(aff); D d = createResource(); @@ -900,7 +912,8 @@ public void testReadAccessTriggerRoleDeleteResource() throws Exception memberOrg.setActive(true); memberOrg.addIdentifier().setSystem(ORGANIZATION_IDENTIFIER_SYSTEM).setValue("member.com"); - OrganizationDao orgDao = new OrganizationDaoJdbc(defaultDataSource, permanentDeleteDataSource, fhirContext); + OrganizationDao orgDao = new OrganizationDaoJdbc(defaultDataSource, permanentDeleteDataSource, fhirContext, + objectMapper); Organization createdParentOrg = orgDao.create(parentOrg); Organization createdMemberOrg = orgDao.create(memberOrg); @@ -912,7 +925,7 @@ public void testReadAccessTriggerRoleDeleteResource() throws Exception aff.getParticipatingOrganization().setReference("Organization/" + createdMemberOrg.getIdElement().getIdPart()); OrganizationAffiliationDaoJdbc orgAffDao = new OrganizationAffiliationDaoJdbc(defaultDataSource, - permanentDeleteDataSource, fhirContext); + permanentDeleteDataSource, fhirContext, objectMapper); OrganizationAffiliation createdAff = orgAffDao.create(aff); D d = createResource(); @@ -948,7 +961,8 @@ public void testReadAccessTriggerRoleDeleteMember() throws Exception memberOrg.setActive(true); memberOrg.addIdentifier().setSystem(ORGANIZATION_IDENTIFIER_SYSTEM).setValue("member.com"); - OrganizationDao orgDao = new OrganizationDaoJdbc(defaultDataSource, permanentDeleteDataSource, fhirContext); + OrganizationDao orgDao = new OrganizationDaoJdbc(defaultDataSource, permanentDeleteDataSource, fhirContext, + objectMapper); Organization createdParentOrg = orgDao.create(parentOrg); Organization createdMemberOrg = orgDao.create(memberOrg); @@ -960,7 +974,7 @@ public void testReadAccessTriggerRoleDeleteMember() throws Exception aff.getParticipatingOrganization().setReference("Organization/" + createdMemberOrg.getIdElement().getIdPart()); OrganizationAffiliation createdAff = new OrganizationAffiliationDaoJdbc(defaultDataSource, - permanentDeleteDataSource, fhirContext).create(aff); + permanentDeleteDataSource, fhirContext, objectMapper).create(aff); D d = createResource(); readAccessHelper.addRole(d, "parent.com", "http://dsf.dev/fhir/CodeSystem/organization-role", "DIC"); @@ -996,7 +1010,8 @@ public void testReadAccessTriggerRoleDeleteParent() throws Exception memberOrg.setActive(true); memberOrg.addIdentifier().setSystem(ORGANIZATION_IDENTIFIER_SYSTEM).setValue("member.com"); - OrganizationDao orgDao = new OrganizationDaoJdbc(defaultDataSource, permanentDeleteDataSource, fhirContext); + OrganizationDao orgDao = new OrganizationDaoJdbc(defaultDataSource, permanentDeleteDataSource, fhirContext, + objectMapper); Organization createdParentOrg = orgDao.create(parentOrg); Organization createdMemberOrg = orgDao.create(memberOrg); @@ -1008,7 +1023,7 @@ public void testReadAccessTriggerRoleDeleteParent() throws Exception aff.getParticipatingOrganization().setReference("Organization/" + createdMemberOrg.getIdElement().getIdPart()); OrganizationAffiliation createdAff = new OrganizationAffiliationDaoJdbc(defaultDataSource, - permanentDeleteDataSource, fhirContext).create(aff); + permanentDeleteDataSource, fhirContext, objectMapper).create(aff); D d = createResource(); readAccessHelper.addRole(d, "parent.com", "http://dsf.dev/fhir/CodeSystem/organization-role", "DIC"); @@ -1044,7 +1059,8 @@ public void testReadAccessTriggerRoleDeleteMemberAndParent() throws Exception memberOrg.setActive(true); memberOrg.addIdentifier().setSystem(ORGANIZATION_IDENTIFIER_SYSTEM).setValue("member.com"); - OrganizationDao orgDao = new OrganizationDaoJdbc(defaultDataSource, permanentDeleteDataSource, fhirContext); + OrganizationDao orgDao = new OrganizationDaoJdbc(defaultDataSource, permanentDeleteDataSource, fhirContext, + objectMapper); Organization createdParentOrg = orgDao.create(parentOrg); Organization createdMemberOrg = orgDao.create(memberOrg); @@ -1056,7 +1072,7 @@ public void testReadAccessTriggerRoleDeleteMemberAndParent() throws Exception aff.getParticipatingOrganization().setReference("Organization/" + createdMemberOrg.getIdElement().getIdPart()); OrganizationAffiliation createdAff = new OrganizationAffiliationDaoJdbc(defaultDataSource, - permanentDeleteDataSource, fhirContext).create(aff); + permanentDeleteDataSource, fhirContext, objectMapper).create(aff); D d = createResource(); readAccessHelper.addRole(d, "parent.com", "http://dsf.dev/fhir/CodeSystem/organization-role", "DIC"); @@ -1090,8 +1106,9 @@ private void testSearchWithUserFilterAfterReadAccessTrigger(String accessType, C Function userCreator, int expectedCount) throws Exception { OrganizationDao organizationDao = new OrganizationDaoJdbc(defaultDataSource, permanentDeleteDataSource, - fhirContext); + fhirContext, objectMapper); Organization org = new Organization(); + org.addIdentifier().setSystem(ORGANIZATION_IDENTIFIER_SYSTEM).setValue("org.com"); Organization createdOrg = organizationDao.create(org); D d = createResource(); diff --git a/dsf-fhir/dsf-fhir-server/src/test/java/dev/dsf/fhir/dao/AbstractResourceDaoTest.java b/dsf-fhir/dsf-fhir-server/src/test/java/dev/dsf/fhir/dao/AbstractResourceDaoTest.java index f98081d1a..1490bfcba 100755 --- a/dsf-fhir/dsf-fhir-server/src/test/java/dev/dsf/fhir/dao/AbstractResourceDaoTest.java +++ b/dsf-fhir/dsf-fhir-server/src/test/java/dev/dsf/fhir/dao/AbstractResourceDaoTest.java @@ -40,6 +40,13 @@ import org.slf4j.LoggerFactory; import org.testcontainers.utility.DockerImageName; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonInclude.Include; +import com.fasterxml.jackson.core.JsonGenerator.Feature; +import com.fasterxml.jackson.databind.MapperFeature; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.json.JsonMapper; + import ca.uhn.fhir.context.FhirContext; import de.hsheilbronn.mi.utils.test.PostgreSqlContainerLiquibaseTemplateClassRule; import de.hsheilbronn.mi.utils.test.PostgresTemplateRule; @@ -53,9 +60,9 @@ public abstract class AbstractResourceDaoTest + public interface QuadFunction { - R apply(A a, B b, C c); + R apply(A a, B b, C c, D d); } protected static DataSource defaultDataSource; @@ -92,13 +99,17 @@ public static void afterClass() throws Exception } protected final Class resouceClass; - protected final TriFunction daoCreator; + protected final QuadFunction daoCreator; protected final FhirContext fhirContext = FhirContext.forR4(); + protected final ObjectMapper objectMapper = JsonMapper.builder().disable(MapperFeature.DEFAULT_VIEW_INCLUSION) + .defaultPropertyInclusion(JsonInclude.Value.construct(Include.NON_NULL, Include.NON_NULL)) + .defaultPropertyInclusion(JsonInclude.Value.construct(Include.NON_EMPTY, Include.NON_EMPTY)) + .disable(Feature.AUTO_CLOSE_TARGET).build(); protected C dao; protected AbstractResourceDaoTest(Class resouceClass, - TriFunction daoCreator) + QuadFunction daoCreator) { this.resouceClass = resouceClass; this.daoCreator = daoCreator; @@ -112,7 +123,7 @@ protected boolean isSame(D d1, D d2) @Before public void before() throws Exception { - dao = daoCreator.apply(defaultDataSource, permanentDeleteDataSource, fhirContext); + dao = daoCreator.apply(defaultDataSource, permanentDeleteDataSource, fhirContext, objectMapper); } public C getDao() diff --git a/dsf-fhir/dsf-fhir-server/src/test/java/dev/dsf/fhir/dao/BinaryDaoTest.java b/dsf-fhir/dsf-fhir-server/src/test/java/dev/dsf/fhir/dao/BinaryDaoTest.java index bb1546387..7bb58c81e 100755 --- a/dsf-fhir/dsf-fhir-server/src/test/java/dev/dsf/fhir/dao/BinaryDaoTest.java +++ b/dsf-fhir/dsf-fhir-server/src/test/java/dev/dsf/fhir/dao/BinaryDaoTest.java @@ -85,17 +85,17 @@ public class BinaryDaoTest extends AbstractReadAccessDaoTest .getBytes(); private final OrganizationDao organizationDao = new OrganizationDaoJdbc(defaultDataSource, - permanentDeleteDataSource, fhirContext); + permanentDeleteDataSource, fhirContext, objectMapper); private final ResearchStudyDao researchStudyDao = new ResearchStudyDaoJdbc(defaultDataSource, - permanentDeleteDataSource, fhirContext); + permanentDeleteDataSource, fhirContext, objectMapper); private final OrganizationAffiliationDao organizationAffiliationDao = new OrganizationAffiliationDaoJdbc( - defaultDataSource, permanentDeleteDataSource, fhirContext); + defaultDataSource, permanentDeleteDataSource, fhirContext, objectMapper); public BinaryDaoTest() { super(Binary.class, - (defaultDataSource, permanentDeleteDataSource, fhirContext) -> new BinaryDaoJdbc(defaultDataSource, - permanentDeleteDataSource, fhirContext, DATABASE_USERS_GROUP)); + (defaultDataSource, permanentDeleteDataSource, fhirContext, objectMapper) -> new BinaryDaoJdbc( + defaultDataSource, permanentDeleteDataSource, fhirContext, objectMapper, DATABASE_USERS_GROUP)); } @Override @@ -375,8 +375,8 @@ private void testReadAccessTriggerSecurityContext(String accessType, Consumer readAccessModifier) throws Exception { final ResearchStudyDaoJdbc researchStudyDao = new ResearchStudyDaoJdbc(defaultDataSource, - permanentDeleteDataSource, fhirContext); + permanentDeleteDataSource, fhirContext, objectMapper); ResearchStudy rS = new ResearchStudy(); readAccessModifier.accept(rS); @@ -748,7 +751,7 @@ private void testReadAccessTriggerSecurityContextVersionSpecificUpdate(String ac Consumer readAccessModifier) throws Exception { final ResearchStudyDaoJdbc researchStudyDao = new ResearchStudyDaoJdbc(defaultDataSource, - permanentDeleteDataSource, fhirContext); + permanentDeleteDataSource, fhirContext, objectMapper); ResearchStudy rS = new ResearchStudy(); readAccessModifier.accept(rS); @@ -800,7 +803,7 @@ public void testReadAccessTriggerSecurityContextVersionSpecificLocalUpdate() thr public void testReadAccessTriggerSecurityContextOrganizationUpdate() throws Exception { final OrganizationDaoJdbc organizationDao = new OrganizationDaoJdbc(defaultDataSource, - permanentDeleteDataSource, fhirContext); + permanentDeleteDataSource, fhirContext, objectMapper); Organization org = new Organization(); org.setActive(true); @@ -809,8 +812,8 @@ public void testReadAccessTriggerSecurityContextOrganizationUpdate() throws Exce ResearchStudy rS = new ResearchStudy(); new ReadAccessHelperImpl().addOrganization(rS, createdOrg); - ResearchStudy createdRs = new ResearchStudyDaoJdbc(defaultDataSource, permanentDeleteDataSource, fhirContext) - .create(rS); + ResearchStudy createdRs = new ResearchStudyDaoJdbc(defaultDataSource, permanentDeleteDataSource, fhirContext, + objectMapper).create(rS); assertReadAccessEntryCount(2, 1, createdRs, READ_ACCESS_TAG_VALUE_LOCAL); assertReadAccessEntryCount(2, 1, createdRs, READ_ACCESS_TAG_VALUE_ORGANIZATION, createdOrg); @@ -845,7 +848,7 @@ public void testReadAccessTriggerSecurityContextOrganizationUpdate() throws Exce public void testReadAccessTriggerSecurityContextRoleUpdate() throws Exception { final OrganizationAffiliationDaoJdbc organizationAffiliationDao = new OrganizationAffiliationDaoJdbc( - defaultDataSource, permanentDeleteDataSource, fhirContext); + defaultDataSource, permanentDeleteDataSource, fhirContext, objectMapper); Organization parentOrg = new Organization(); parentOrg.setActive(true); @@ -855,7 +858,8 @@ public void testReadAccessTriggerSecurityContextRoleUpdate() throws Exception memberOrg.setActive(true); memberOrg.addIdentifier().setSystem(ORGANIZATION_IDENTIFIER_SYSTEM).setValue("member.com"); - OrganizationDao orgDao = new OrganizationDaoJdbc(defaultDataSource, permanentDeleteDataSource, fhirContext); + OrganizationDao orgDao = new OrganizationDaoJdbc(defaultDataSource, permanentDeleteDataSource, fhirContext, + objectMapper); Organization createdParentOrg = orgDao.create(parentOrg); Organization createdMemberOrg = orgDao.create(memberOrg); @@ -870,8 +874,8 @@ public void testReadAccessTriggerSecurityContextRoleUpdate() throws Exception ResearchStudy rS = new ResearchStudy(); new ReadAccessHelperImpl().addRole(rS, "parent.com", "http://dsf.dev/fhir/CodeSystem/organization-role", "DIC"); - ResearchStudy createdRs = new ResearchStudyDaoJdbc(defaultDataSource, permanentDeleteDataSource, fhirContext) - .create(rS); + ResearchStudy createdRs = new ResearchStudyDaoJdbc(defaultDataSource, permanentDeleteDataSource, fhirContext, + objectMapper).create(rS); Binary b = createResource(); b.setSecurityContext(new Reference(createdRs.getIdElement().toUnqualifiedVersionless())); @@ -906,7 +910,7 @@ public void testReadAccessTriggerSecurityContextRoleUpdate() throws Exception public void testReadAccessTriggerSecurityContextRoleUpdateMemberOrganizationNonActive() throws Exception { final OrganizationAffiliationDaoJdbc organizationAffiliationDao = new OrganizationAffiliationDaoJdbc( - defaultDataSource, permanentDeleteDataSource, fhirContext); + defaultDataSource, permanentDeleteDataSource, fhirContext, objectMapper); Organization parentOrg = new Organization(); parentOrg.setActive(true); @@ -916,7 +920,8 @@ public void testReadAccessTriggerSecurityContextRoleUpdateMemberOrganizationNonA memberOrg.setActive(true); memberOrg.addIdentifier().setSystem(ORGANIZATION_IDENTIFIER_SYSTEM).setValue("member.com"); - OrganizationDao orgDao = new OrganizationDaoJdbc(defaultDataSource, permanentDeleteDataSource, fhirContext); + OrganizationDao orgDao = new OrganizationDaoJdbc(defaultDataSource, permanentDeleteDataSource, fhirContext, + objectMapper); Organization createdParentOrg = orgDao.create(parentOrg); Organization createdMemberOrg = orgDao.create(memberOrg); @@ -931,8 +936,8 @@ public void testReadAccessTriggerSecurityContextRoleUpdateMemberOrganizationNonA ResearchStudy rS = new ResearchStudy(); new ReadAccessHelperImpl().addRole(rS, "parent.com", "http://dsf.dev/fhir/CodeSystem/organization-role", "DIC"); - ResearchStudy createdRs = new ResearchStudyDaoJdbc(defaultDataSource, permanentDeleteDataSource, fhirContext) - .create(rS); + ResearchStudy createdRs = new ResearchStudyDaoJdbc(defaultDataSource, permanentDeleteDataSource, fhirContext, + objectMapper).create(rS); Binary b = createResource(); b.setSecurityContext(new Reference(createdRs.getIdElement().toUnqualifiedVersionless())); @@ -964,7 +969,7 @@ public void testReadAccessTriggerSecurityContextRoleUpdateMemberOrganizationNonA public void testReadAccessTriggerSecurityContextRoleUpdateParentOrganizationNonActive() throws Exception { final OrganizationAffiliationDaoJdbc organizationAffiliationDao = new OrganizationAffiliationDaoJdbc( - defaultDataSource, permanentDeleteDataSource, fhirContext); + defaultDataSource, permanentDeleteDataSource, fhirContext, objectMapper); Organization parentOrg = new Organization(); parentOrg.setActive(true); @@ -974,7 +979,8 @@ public void testReadAccessTriggerSecurityContextRoleUpdateParentOrganizationNonA memberOrg.setActive(true); memberOrg.addIdentifier().setSystem(ORGANIZATION_IDENTIFIER_SYSTEM).setValue("member.com"); - OrganizationDao orgDao = new OrganizationDaoJdbc(defaultDataSource, permanentDeleteDataSource, fhirContext); + OrganizationDao orgDao = new OrganizationDaoJdbc(defaultDataSource, permanentDeleteDataSource, fhirContext, + objectMapper); Organization createdParentOrg = orgDao.create(parentOrg); Organization createdMemberOrg = orgDao.create(memberOrg); @@ -989,8 +995,8 @@ public void testReadAccessTriggerSecurityContextRoleUpdateParentOrganizationNonA ResearchStudy rS = new ResearchStudy(); new ReadAccessHelperImpl().addRole(rS, "parent.com", "http://dsf.dev/fhir/CodeSystem/organization-role", "DIC"); - ResearchStudy createdRs = new ResearchStudyDaoJdbc(defaultDataSource, permanentDeleteDataSource, fhirContext) - .create(rS); + ResearchStudy createdRs = new ResearchStudyDaoJdbc(defaultDataSource, permanentDeleteDataSource, fhirContext, + objectMapper).create(rS); Binary b = createResource(); b.setSecurityContext(new Reference(createdRs.getIdElement().toUnqualifiedVersionless())); @@ -1022,7 +1028,7 @@ public void testReadAccessTriggerSecurityContextRoleUpdateParentOrganizationNonA public void testReadAccessTriggerSecurityContextRoleUpdateMemberAndParentOrganizationNonActive() throws Exception { final OrganizationAffiliationDaoJdbc organizationAffiliationDao = new OrganizationAffiliationDaoJdbc( - defaultDataSource, permanentDeleteDataSource, fhirContext); + defaultDataSource, permanentDeleteDataSource, fhirContext, objectMapper); Organization parentOrg = new Organization(); parentOrg.setActive(true); @@ -1032,7 +1038,8 @@ public void testReadAccessTriggerSecurityContextRoleUpdateMemberAndParentOrganiz memberOrg.setActive(true); memberOrg.addIdentifier().setSystem(ORGANIZATION_IDENTIFIER_SYSTEM).setValue("member.com"); - OrganizationDao orgDao = new OrganizationDaoJdbc(defaultDataSource, permanentDeleteDataSource, fhirContext); + OrganizationDao orgDao = new OrganizationDaoJdbc(defaultDataSource, permanentDeleteDataSource, fhirContext, + objectMapper); Organization createdParentOrg = orgDao.create(parentOrg); Organization createdMemberOrg = orgDao.create(memberOrg); @@ -1047,8 +1054,8 @@ public void testReadAccessTriggerSecurityContextRoleUpdateMemberAndParentOrganiz ResearchStudy rS = new ResearchStudy(); new ReadAccessHelperImpl().addRole(rS, "parent.com", "http://dsf.dev/fhir/CodeSystem/organization-role", "DIC"); - ResearchStudy createdRs = new ResearchStudyDaoJdbc(defaultDataSource, permanentDeleteDataSource, fhirContext) - .create(rS); + ResearchStudy createdRs = new ResearchStudyDaoJdbc(defaultDataSource, permanentDeleteDataSource, fhirContext, + objectMapper).create(rS); Binary b = createResource(); b.setSecurityContext(new Reference(createdRs.getIdElement().toUnqualifiedVersionless())); @@ -1091,7 +1098,7 @@ private void testReadAccessTriggerSecurityContextDelete(String accessType, Consumer readAccessModifier) throws Exception { final ResearchStudyDaoJdbc researchStudyDao = new ResearchStudyDaoJdbc(defaultDataSource, - permanentDeleteDataSource, fhirContext); + permanentDeleteDataSource, fhirContext, objectMapper); ResearchStudy rS = new ResearchStudy(); readAccessModifier.accept(rS); @@ -1128,7 +1135,7 @@ public void testReadAccessTriggerSecurityContextLocalDelete() throws Exception public void testReadAccessTriggerSecurityContextOrganizationDelete() throws Exception { final OrganizationDaoJdbc organizationDao = new OrganizationDaoJdbc(defaultDataSource, - permanentDeleteDataSource, fhirContext); + permanentDeleteDataSource, fhirContext, objectMapper); Organization org = new Organization(); org.setActive(true); @@ -1137,8 +1144,8 @@ public void testReadAccessTriggerSecurityContextOrganizationDelete() throws Exce ResearchStudy rS = new ResearchStudy(); new ReadAccessHelperImpl().addOrganization(rS, createdOrg); - ResearchStudy createdRs = new ResearchStudyDaoJdbc(defaultDataSource, permanentDeleteDataSource, fhirContext) - .create(rS); + ResearchStudy createdRs = new ResearchStudyDaoJdbc(defaultDataSource, permanentDeleteDataSource, fhirContext, + objectMapper).create(rS); Binary b = createResource(); b.setSecurityContext(new Reference(createdRs.getIdElement().toUnqualifiedVersionless())); @@ -1168,7 +1175,8 @@ public void testReadAccessTriggerSecurityContextRoleDelete() throws Exception memberOrg.setActive(true); memberOrg.addIdentifier().setSystem(ORGANIZATION_IDENTIFIER_SYSTEM).setValue("member.com"); - OrganizationDao orgDao = new OrganizationDaoJdbc(defaultDataSource, permanentDeleteDataSource, fhirContext); + OrganizationDao orgDao = new OrganizationDaoJdbc(defaultDataSource, permanentDeleteDataSource, fhirContext, + objectMapper); Organization createdParentOrg = orgDao.create(parentOrg); Organization createdMemberOrg = orgDao.create(memberOrg); @@ -1180,14 +1188,14 @@ public void testReadAccessTriggerSecurityContextRoleDelete() throws Exception aff.getParticipatingOrganization().setReference("Organization/" + createdMemberOrg.getIdElement().getIdPart()); final OrganizationAffiliationDaoJdbc orgAffDao = new OrganizationAffiliationDaoJdbc(defaultDataSource, - permanentDeleteDataSource, fhirContext); + permanentDeleteDataSource, fhirContext, objectMapper); OrganizationAffiliation createdAff = orgAffDao.create(aff); ResearchStudy rS = new ResearchStudy(); new ReadAccessHelperImpl().addRole(rS, "parent.com", "http://dsf.dev/fhir/CodeSystem/organization-role", "DIC"); - ResearchStudy createdRs = new ResearchStudyDaoJdbc(defaultDataSource, permanentDeleteDataSource, fhirContext) - .create(rS); + ResearchStudy createdRs = new ResearchStudyDaoJdbc(defaultDataSource, permanentDeleteDataSource, fhirContext, + objectMapper).create(rS); Binary b = createResource(); b.setSecurityContext(new Reference(createdRs.getIdElement().toUnqualifiedVersionless())); @@ -1219,7 +1227,8 @@ public void testReadAccessTriggerSecurityContextRoleDeleteMember() throws Except memberOrg.setActive(true); memberOrg.addIdentifier().setSystem(ORGANIZATION_IDENTIFIER_SYSTEM).setValue("member.com"); - OrganizationDao orgDao = new OrganizationDaoJdbc(defaultDataSource, permanentDeleteDataSource, fhirContext); + OrganizationDao orgDao = new OrganizationDaoJdbc(defaultDataSource, permanentDeleteDataSource, fhirContext, + objectMapper); Organization createdParentOrg = orgDao.create(parentOrg); Organization createdMemberOrg = orgDao.create(memberOrg); @@ -1231,14 +1240,14 @@ public void testReadAccessTriggerSecurityContextRoleDeleteMember() throws Except aff.getParticipatingOrganization().setReference("Organization/" + createdMemberOrg.getIdElement().getIdPart()); final OrganizationAffiliationDaoJdbc orgAffDao = new OrganizationAffiliationDaoJdbc(defaultDataSource, - permanentDeleteDataSource, fhirContext); + permanentDeleteDataSource, fhirContext, objectMapper); OrganizationAffiliation createdAff = orgAffDao.create(aff); ResearchStudy rS = new ResearchStudy(); new ReadAccessHelperImpl().addRole(rS, "parent.com", "http://dsf.dev/fhir/CodeSystem/organization-role", "DIC"); - ResearchStudy createdRs = new ResearchStudyDaoJdbc(defaultDataSource, permanentDeleteDataSource, fhirContext) - .create(rS); + ResearchStudy createdRs = new ResearchStudyDaoJdbc(defaultDataSource, permanentDeleteDataSource, fhirContext, + objectMapper).create(rS); Binary b = createResource(); b.setSecurityContext(new Reference(createdRs.getIdElement().toUnqualifiedVersionless())); @@ -1270,7 +1279,8 @@ public void testReadAccessTriggerSecurityContextRoleDeleteParent() throws Except memberOrg.setActive(true); memberOrg.addIdentifier().setSystem(ORGANIZATION_IDENTIFIER_SYSTEM).setValue("member.com"); - OrganizationDao orgDao = new OrganizationDaoJdbc(defaultDataSource, permanentDeleteDataSource, fhirContext); + OrganizationDao orgDao = new OrganizationDaoJdbc(defaultDataSource, permanentDeleteDataSource, fhirContext, + objectMapper); Organization createdParentOrg = orgDao.create(parentOrg); Organization createdMemberOrg = orgDao.create(memberOrg); @@ -1282,14 +1292,14 @@ public void testReadAccessTriggerSecurityContextRoleDeleteParent() throws Except aff.getParticipatingOrganization().setReference("Organization/" + createdMemberOrg.getIdElement().getIdPart()); final OrganizationAffiliationDaoJdbc orgAffDao = new OrganizationAffiliationDaoJdbc(defaultDataSource, - permanentDeleteDataSource, fhirContext); + permanentDeleteDataSource, fhirContext, objectMapper); OrganizationAffiliation createdAff = orgAffDao.create(aff); ResearchStudy rS = new ResearchStudy(); new ReadAccessHelperImpl().addRole(rS, "parent.com", "http://dsf.dev/fhir/CodeSystem/organization-role", "DIC"); - ResearchStudy createdRs = new ResearchStudyDaoJdbc(defaultDataSource, permanentDeleteDataSource, fhirContext) - .create(rS); + ResearchStudy createdRs = new ResearchStudyDaoJdbc(defaultDataSource, permanentDeleteDataSource, fhirContext, + objectMapper).create(rS); Binary b = createResource(); b.setSecurityContext(new Reference(createdRs.getIdElement().toUnqualifiedVersionless())); @@ -1321,7 +1331,8 @@ public void testReadAccessTriggerSecurityContextRoleDeleteMemberAndParent() thro memberOrg.setActive(true); memberOrg.addIdentifier().setSystem(ORGANIZATION_IDENTIFIER_SYSTEM).setValue("member.com"); - OrganizationDao orgDao = new OrganizationDaoJdbc(defaultDataSource, permanentDeleteDataSource, fhirContext); + OrganizationDao orgDao = new OrganizationDaoJdbc(defaultDataSource, permanentDeleteDataSource, fhirContext, + objectMapper); Organization createdParentOrg = orgDao.create(parentOrg); Organization createdMemberOrg = orgDao.create(memberOrg); @@ -1333,14 +1344,14 @@ public void testReadAccessTriggerSecurityContextRoleDeleteMemberAndParent() thro aff.getParticipatingOrganization().setReference("Organization/" + createdMemberOrg.getIdElement().getIdPart()); final OrganizationAffiliationDaoJdbc orgAffDao = new OrganizationAffiliationDaoJdbc(defaultDataSource, - permanentDeleteDataSource, fhirContext); + permanentDeleteDataSource, fhirContext, objectMapper); OrganizationAffiliation createdAff = orgAffDao.create(aff); ResearchStudy rS = new ResearchStudy(); new ReadAccessHelperImpl().addRole(rS, "parent.com", "http://dsf.dev/fhir/CodeSystem/organization-role", "DIC"); - ResearchStudy createdRs = new ResearchStudyDaoJdbc(defaultDataSource, permanentDeleteDataSource, fhirContext) - .create(rS); + ResearchStudy createdRs = new ResearchStudyDaoJdbc(defaultDataSource, permanentDeleteDataSource, fhirContext, + objectMapper).create(rS); Binary b = createResource(); b.setSecurityContext(new Reference(createdRs.getIdElement().toUnqualifiedVersionless())); diff --git a/dsf-fhir/dsf-fhir-server/src/test/java/dev/dsf/fhir/dao/HistoryDaoTest.java b/dsf-fhir/dsf-fhir-server/src/test/java/dev/dsf/fhir/dao/HistoryDaoTest.java index f3b30b38d..150528031 100644 --- a/dsf-fhir/dsf-fhir-server/src/test/java/dev/dsf/fhir/dao/HistoryDaoTest.java +++ b/dsf-fhir/dsf-fhir-server/src/test/java/dev/dsf/fhir/dao/HistoryDaoTest.java @@ -32,12 +32,20 @@ import org.junit.Test; import org.testcontainers.utility.DockerImageName; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonInclude.Include; +import com.fasterxml.jackson.core.JsonGenerator.Feature; +import com.fasterxml.jackson.databind.MapperFeature; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.json.JsonMapper; + import ca.uhn.fhir.context.FhirContext; import de.hsheilbronn.mi.utils.test.PostgreSqlContainerLiquibaseTemplateClassRule; import de.hsheilbronn.mi.utils.test.PostgresTemplateRule; import dev.dsf.fhir.dao.jdbc.BinaryDaoJdbc; import dev.dsf.fhir.dao.jdbc.HistroyDaoJdbc; import dev.dsf.fhir.dao.jdbc.OrganizationDaoJdbc; +import dev.dsf.fhir.dao.jdbc.PgObjectFactoryImpl; import dev.dsf.fhir.history.AtParameter; import dev.dsf.fhir.history.History; import dev.dsf.fhir.history.SinceParameter; @@ -78,10 +86,16 @@ public static void afterClass() throws Exception } private final FhirContext fhirContext = FhirContext.forR4(); + private final ObjectMapper objectMapper = JsonMapper.builder().disable(MapperFeature.DEFAULT_VIEW_INCLUSION) + .defaultPropertyInclusion(JsonInclude.Value.construct(Include.NON_NULL, Include.NON_NULL)) + .defaultPropertyInclusion(JsonInclude.Value.construct(Include.NON_EMPTY, Include.NON_EMPTY)) + .disable(Feature.AUTO_CLOSE_TARGET).build(); private final OrganizationDao orgDao = new OrganizationDaoJdbc(defaultDataSource, permanentDeleteDataSource, - fhirContext); + fhirContext, objectMapper); private final HistoryDao dao = new HistroyDaoJdbc(defaultDataSource, fhirContext, - new BinaryDaoJdbc(defaultDataSource, permanentDeleteDataSource, fhirContext, DATABASE_USERS_GROUP)); + new BinaryDaoJdbc(defaultDataSource, permanentDeleteDataSource, fhirContext, objectMapper, + DATABASE_USERS_GROUP), + new PgObjectFactoryImpl(fhirContext, objectMapper)); private final HistoryIdentityFilterFactory filterFactory = new HistoryIdentityFilterFactoryImpl(); @Test 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 830b1175c..2db4c4120 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 @@ -59,9 +59,9 @@ public class OrganizationAffiliationDaoTest private static final boolean active = true; private final OrganizationDao organizationDao = new OrganizationDaoJdbc(defaultDataSource, - permanentDeleteDataSource, fhirContext); + permanentDeleteDataSource, fhirContext, objectMapper); private final EndpointDao endpointDao = new EndpointDaoJdbc(defaultDataSource, permanentDeleteDataSource, - fhirContext); + fhirContext, objectMapper); public OrganizationAffiliationDaoTest() { @@ -264,8 +264,9 @@ private OrganizationAffiliation createAndStoreOrganizationAffiliationInDb(Organi public void testUpdateWithExistingBinary() throws Exception { BinaryDaoJdbc binaryDao = new BinaryDaoJdbc(defaultDataSource, permanentDeleteDataSource, fhirContext, - DATABASE_USERS_GROUP); - OrganizationDaoJdbc orgDao = new OrganizationDaoJdbc(defaultDataSource, permanentDeleteDataSource, fhirContext); + objectMapper, DATABASE_USERS_GROUP); + OrganizationDaoJdbc orgDao = new OrganizationDaoJdbc(defaultDataSource, permanentDeleteDataSource, fhirContext, + objectMapper); Organization memberOrg = new Organization(); memberOrg.setActive(true); @@ -304,8 +305,9 @@ public void testUpdateWithExistingBinary() throws Exception public void testUpdateWithExistingBinaryUpdateMemberOrg() throws Exception { BinaryDaoJdbc binaryDao = new BinaryDaoJdbc(defaultDataSource, permanentDeleteDataSource, fhirContext, - DATABASE_USERS_GROUP); - OrganizationDaoJdbc orgDao = new OrganizationDaoJdbc(defaultDataSource, permanentDeleteDataSource, fhirContext); + objectMapper, DATABASE_USERS_GROUP); + OrganizationDaoJdbc orgDao = new OrganizationDaoJdbc(defaultDataSource, permanentDeleteDataSource, fhirContext, + objectMapper); Organization memberOrg = new Organization(); memberOrg.setActive(true); @@ -344,8 +346,9 @@ public void testUpdateWithExistingBinaryUpdateMemberOrg() throws Exception public void testUpdateWithExistingBinaryUpdateParentOrg() throws Exception { BinaryDaoJdbc binaryDao = new BinaryDaoJdbc(defaultDataSource, permanentDeleteDataSource, fhirContext, - DATABASE_USERS_GROUP); - OrganizationDaoJdbc orgDao = new OrganizationDaoJdbc(defaultDataSource, permanentDeleteDataSource, fhirContext); + objectMapper, DATABASE_USERS_GROUP); + OrganizationDaoJdbc orgDao = new OrganizationDaoJdbc(defaultDataSource, permanentDeleteDataSource, fhirContext, + objectMapper); Organization memberOrg = new Organization(); memberOrg.setActive(true); @@ -469,7 +472,8 @@ private String generateLine() @Test public void testBigUpdate() throws Exception { - OrganizationDaoJdbc orgDao = new OrganizationDaoJdbc(defaultDataSource, permanentDeleteDataSource, fhirContext); + OrganizationDaoJdbc orgDao = new OrganizationDaoJdbc(defaultDataSource, permanentDeleteDataSource, fhirContext, + objectMapper); Organization memberOrg = new Organization(); memberOrg.setActive(true); 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 7df069c53..4d1b6ca2b 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 @@ -16,6 +16,7 @@ package dev.dsf.fhir.dao; import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertNotNull; import static org.junit.Assert.assertTrue; @@ -200,8 +201,8 @@ public void testOrganizationInsertTrigger() throws Exception { CodeSystem c = new CodeSystem(); new ReadAccessHelperImpl().addOrganization(c, "organization.com"); - CodeSystem createdC = new CodeSystemDaoJdbc(defaultDataSource, permanentDeleteDataSource, fhirContext) - .create(c); + CodeSystem createdC = new CodeSystemDaoJdbc(defaultDataSource, permanentDeleteDataSource, fhirContext, + objectMapper).create(c); try (Connection connection = defaultDataSource.getConnection(); PreparedStatement statement = connection @@ -266,7 +267,7 @@ public void testUpdateWithExistingBinary() throws Exception new ReadAccessHelperImpl().addOrganization(binary, "organization.com"); BinaryDaoJdbc binaryDao = new BinaryDaoJdbc(defaultDataSource, permanentDeleteDataSource, fhirContext, - DATABASE_USERS_GROUP); + objectMapper, DATABASE_USERS_GROUP); Binary createdBinary = binaryDao.create(binary); assertNotNull(createdBinary); @@ -362,4 +363,104 @@ public void testBigUpdate() throws Exception logger.info("Organization updates executed in {} ms", t1 - t0); assertTrue("Organization updates took longer then 500 ms", t1 - t0 <= 500); } + + @Test + public void testExistsNotDeletedByThumbprintWithTransaction() throws Exception + { + final String certHex = Hex.encodeHexString("FooBarBaz".getBytes(StandardCharsets.UTF_8)); + + Organization org = new Organization(); + org.setActive(true); + org.setName("Test"); + org.addExtension().setUrl("http://dsf.dev/fhir/StructureDefinition/extension-certificate-thumbprint") + .setValue(new StringType(certHex)); + + Organization created = dao.create(org); + assertNotNull(created); + + try (Connection connection = defaultDataSource.getConnection()) + { + boolean exists = dao.existsNotDeletedByThumbprintWithTransaction(connection, certHex); + assertTrue(exists); + } + } + + @Test + public void testExistsNotDeletedByThumbprintWithTransactionNotActive() throws Exception + { + final String certHex = Hex.encodeHexString("FooBarBaz".getBytes(StandardCharsets.UTF_8)); + + Organization org = new Organization(); + org.setActive(false); + org.setName("Test"); + org.addExtension().setUrl("http://dsf.dev/fhir/StructureDefinition/extension-certificate-thumbprint") + .setValue(new StringType(certHex)); + + Organization created = dao.create(org); + assertNotNull(created); + + try (Connection connection = defaultDataSource.getConnection()) + { + boolean exists = dao.existsNotDeletedByThumbprintWithTransaction(connection, certHex); + assertTrue(exists); + } + + Optional read2 = dao.read(UUID.fromString(created.getIdElement().getIdPart())); + assertNotNull(read2); + assertTrue(read2.isPresent()); + } + + @Test + public void testExistsNotDeletedByThumbprintWithTransactionDeleted() throws Exception + { + final String certHex = Hex.encodeHexString("FooBarBaz".getBytes(StandardCharsets.UTF_8)); + + Organization org = new Organization(); + org.setActive(false); + org.setName("Test"); + org.addExtension().setUrl("http://dsf.dev/fhir/StructureDefinition/extension-certificate-thumbprint") + .setValue(new StringType(certHex)); + + Organization created = dao.create(org); + assertNotNull(created); + dao.delete(UUID.fromString(created.getIdElement().getIdPart())); + + try (Connection connection = defaultDataSource.getConnection()) + { + boolean exists = dao.existsNotDeletedByThumbprintWithTransaction(connection, certHex); + assertFalse(exists); + } + } + + @Test + public void testExistsNotDeletedByThumbprintWithTransactionNotExisting() throws Exception + { + final String certHex = Hex.encodeHexString("FooBarBaz".getBytes(StandardCharsets.UTF_8)); + + try (Connection connection = defaultDataSource.getConnection()) + { + boolean exists = dao.existsNotDeletedByThumbprintWithTransaction(connection, certHex); + assertFalse(exists); + } + } + + @Test + public void testExistsNotDeletedByThumbprintWithTransactionNull() throws Exception + { + try (Connection connection = defaultDataSource.getConnection()) + { + boolean exists = dao.existsNotDeletedByThumbprintWithTransaction(connection, null); + assertFalse(exists); + } + } + + @Test + public void testExistsNotDeletedByThumbprintWithTransactionBlank() throws Exception + { + try (Connection connection = defaultDataSource.getConnection()) + { + boolean exists = dao.existsNotDeletedByThumbprintWithTransaction(connection, " "); + assertFalse(exists); + } + } } From c6d3698289a5654a849c734acbcf00b13fa9c7d4 Mon Sep 17 00:00:00 2001 From: Hauke Hund Date: Mon, 23 Mar 2026 21:04:20 +0100 Subject: [PATCH 09/18] improved client certificate checks --- .../auth/ClientCertificateAuthenticator.java | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/dsf-common/dsf-common-jetty/src/main/java/dev/dsf/common/auth/ClientCertificateAuthenticator.java b/dsf-common/dsf-common-jetty/src/main/java/dev/dsf/common/auth/ClientCertificateAuthenticator.java index 3050d0442..8d6aa3b8c 100644 --- a/dsf-common/dsf-common-jetty/src/main/java/dev/dsf/common/auth/ClientCertificateAuthenticator.java +++ b/dsf-common/dsf-common-jetty/src/main/java/dev/dsf/common/auth/ClientCertificateAuthenticator.java @@ -20,6 +20,8 @@ import java.security.NoSuchAlgorithmException; import java.security.cert.CertificateException; import java.security.cert.X509Certificate; +import java.time.ZoneOffset; +import java.time.ZonedDateTime; import java.util.Objects; import java.util.stream.Collectors; @@ -67,13 +69,21 @@ public AuthenticationState validateRequest(Request request, Response response, C if (certificates == null || certificates.length <= 0) { - logger.warn("X509Certificate could not be retrieved, sending unauthorized"); + logger.warn( + "Client certificate could not be retrieved from jakarta request attribute, sending unauthorized"); + return null; + } + + if (ZonedDateTime.now() + .isAfter(ZonedDateTime.ofInstant(certificates[0].getNotAfter().toInstant(), ZoneOffset.UTC))) + { + logger.warn("Client certificates expired, sending unauthorized"); return null; } try { - x509TrustManager.checkClientTrusted(certificates, "RSA"); + x509TrustManager.checkClientTrusted(certificates, "UNKNOWN"); } catch (CertificateException e) { From 58edc26c5966da7c91810082f403ed438cc807cb Mon Sep 17 00:00:00 2001 From: Hauke Hund Date: Mon, 23 Mar 2026 21:08:57 +0100 Subject: [PATCH 10/18] improved handling of untrusted input --- .../src/main/resources/bpe/template/main.html | 2 +- .../adapter/ThymeleafTemplateServiceImpl.java | 28 ++++++++++++------- .../main/resources/fhir/template/main.html | 4 ++- 3 files changed, 22 insertions(+), 12 deletions(-) diff --git a/dsf-bpe/dsf-bpe-server/src/main/resources/bpe/template/main.html b/dsf-bpe/dsf-bpe-server/src/main/resources/bpe/template/main.html index 3519df25d..60fd3f9ed 100644 --- a/dsf-bpe/dsf-bpe-server/src/main/resources/bpe/template/main.html +++ b/dsf-bpe/dsf-bpe-server/src/main/resources/bpe/template/main.html @@ -42,7 +42,7 @@ -

+

diff --git a/dsf-fhir/dsf-fhir-server/src/main/java/dev/dsf/fhir/adapter/ThymeleafTemplateServiceImpl.java b/dsf-fhir/dsf-fhir-server/src/main/java/dev/dsf/fhir/adapter/ThymeleafTemplateServiceImpl.java index 4dfdaa0cc..3269d739a 100644 --- a/dsf-fhir/dsf-fhir-server/src/main/java/dev/dsf/fhir/adapter/ThymeleafTemplateServiceImpl.java +++ b/dsf-fhir/dsf-fhir-server/src/main/java/dev/dsf/fhir/adapter/ThymeleafTemplateServiceImpl.java @@ -24,6 +24,7 @@ import java.net.URISyntaxException; import java.nio.charset.StandardCharsets; import java.security.Principal; +import java.util.ArrayList; import java.util.List; import java.util.Map; import java.util.Objects; @@ -101,6 +102,10 @@ public class ThymeleafTemplateServiceImpl implements ThymeleafTemplateService, I private static final String CODE_SYSTEM_PRACTITIONER_ROLE = "http://dsf.dev/fhir/CodeSystem/practitioner-role"; + private static record Heading(String href, String title, String text) + { + } + private final String serverBaseUrl; private final Theme theme; private final FhirContext fhirContext; @@ -248,37 +253,40 @@ else if (uriInfo.getPath().endsWith("/")) return "DSF: " + HtmlUtils.htmlEscape(uriInfo.getPath()); } - private String getHeading(Resource resource, UriInfo uriInfo) + private List getHeading(Resource resource, UriInfo uriInfo) { + List headings = new ArrayList<>(); + URI uri = getResourceUri(resource, uriInfo); String[] pathSegments = uri.getPath().split("/"); String u = serverBaseUrl; - StringBuilder heading = new StringBuilder("" + u + ""); + + headings.add(new Heading(u, "Open " + u, u)); String[] basePathSegments = getServerBaseUrlPathWithLeadingSlash().split("/"); for (int i = basePathSegments.length; i < pathSegments.length; i++) { - String pathSegment = HtmlUtils.htmlEscape(pathSegments[i]); + String pathSegment = pathSegments[i]; u += "/" + pathSegment; - heading.append("/" + pathSegment + ""); + headings.add(new Heading(u, "Open " + u, "/\u200B" + pathSegment)); } if (uri.getQuery() != null) { - String queryValue = HtmlUtils.htmlEscape(uri.getQuery()); + String queryValue = uri.getQuery(); u += "?" + queryValue; - heading.append("?" - + queryValue.replace("&", "&").replace("-", "‑") + ""); + headings.add( + new Heading(u, "Open " + u, "\u200B?" + queryValue.replace("&", "\u200B&").replace("-", "\u2011"))); } else if (uriInfo.getQueryParameters().containsKey("_summary")) { - String summaryValue = HtmlUtils.htmlEscape(uriInfo.getQueryParameters().getFirst("_summary")); + String summaryValue = uriInfo.getQueryParameters().getFirst("_summary"); u += "?_summary=" + summaryValue; - heading.append("?_summary=" + summaryValue + ""); + headings.add(new Heading(u, "Open " + u, "?_summary=" + summaryValue)); } - return heading.toString(); + return headings; } private URI getResourceUri(Resource resource, UriInfo uriInfo) diff --git a/dsf-fhir/dsf-fhir-server/src/main/resources/fhir/template/main.html b/dsf-fhir/dsf-fhir-server/src/main/resources/fhir/template/main.html index c3d20af01..69eb93327 100644 --- a/dsf-fhir/dsf-fhir-server/src/main/resources/fhir/template/main.html +++ b/dsf-fhir/dsf-fhir-server/src/main/resources/fhir/template/main.html @@ -61,7 +61,9 @@

Bookmarks

Show Bookmarks -

+

+ Link text +

From e0b6cf00ab6de62c2d857190ecf50a692bea4d64 Mon Sep 17 00:00:00 2001 From: Hauke Hund Date: Tue, 24 Mar 2026 15:15:39 +0100 Subject: [PATCH 11/18] improved yaml config --- .../main/java/dev/dsf/common/auth/conf/RoleConfigReader.java | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/dsf-common/dsf-common-auth/src/main/java/dev/dsf/common/auth/conf/RoleConfigReader.java b/dsf-common/dsf-common-auth/src/main/java/dev/dsf/common/auth/conf/RoleConfigReader.java index 75db06690..7d3080deb 100644 --- a/dsf-common/dsf-common-auth/src/main/java/dev/dsf/common/auth/conf/RoleConfigReader.java +++ b/dsf-common/dsf-common-auth/src/main/java/dev/dsf/common/auth/conf/RoleConfigReader.java @@ -20,7 +20,9 @@ import java.util.function.Function; import org.hl7.fhir.r4.model.Coding; +import org.yaml.snakeyaml.LoaderOptions; import org.yaml.snakeyaml.Yaml; +import org.yaml.snakeyaml.constructor.SafeConstructor; import dev.dsf.common.auth.conf.RoleConfig.DsfRoleFactory; @@ -50,6 +52,6 @@ public RoleConfig read(InputStream config, DsfRoleFactory protected Yaml yaml() { - return new Yaml(); + return new Yaml(new SafeConstructor(new LoaderOptions())); } } From f4ecb002f7d12642f92da6b79371ed367d0140e7 Mon Sep 17 00:00:00 2001 From: Hauke Hund Date: Wed, 25 Mar 2026 01:48:34 +0100 Subject: [PATCH 12/18] improved session timeout config, new error handling code in UI If the FHIR servers answers with a redirect to the OIDC provider due to an invalidated session Task resources to be created and QuestionnaireResponse resource to be updated are stored in the browsers session storage. After the redirect returns from the OIDC provider input elements are pre-filled from the stored resources and the send button scrolled into view. The send button blinks twice to get the users attention. --- .../common/config/AbstractJettyConfig.java | 25 +- .../src/main/resources/fhir/static/form.css | 14 +- .../src/main/resources/fhir/static/form.js | 303 ++++++++++++++++-- .../src/main/resources/fhir/static/main.js | 31 +- 4 files changed, 322 insertions(+), 51 deletions(-) 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 972d96fa0..2de826323 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 @@ -212,6 +212,10 @@ public abstract class AbstractJettyConfig extends AbstractCertificateConfig @Value("${dev.dsf.server.auth.oidc.back.channel.logout.path:/back-channel-logout}") private String oidcBackChannelPath; + @Documentation(description = "Maximum inactivity period after which the server session for OIDC logins is invalidated; the access token may expire earlier, resulting in earlier session invalidation") + @Value("${dev.dsf.server.auth.oidc.session.timeout:PT30M}") + private String oidcSessionTimeout; + @Documentation(description = "Forward (http/https) proxy url, use *DEV_DSF_BPE_PROXY_NOPROXY* to list domains that do not require a forward proxy", example = "http://proxy.foo:8080") @Value("${dev.dsf.proxy.url:#{null}}") private String proxyUrl; @@ -318,6 +322,7 @@ private void configureSecurityHandler(WebAppContext webAppContext, Supplier= Integer.MAX_VALUE) + seconds = Integer.MAX_VALUE; + + return (int) seconds; + } + private Duration assertPositive(Duration duration) { if (duration != null && duration.isNegative()) diff --git a/dsf-fhir/dsf-fhir-server/src/main/resources/fhir/static/form.css b/dsf-fhir/dsf-fhir-server/src/main/resources/fhir/static/form.css index 6f84dab8f..4d38fca45 100644 --- a/dsf-fhir/dsf-fhir-server/src/main/resources/fhir/static/form.css +++ b/dsf-fhir/dsf-fhir-server/src/main/resources/fhir/static/form.css @@ -255,8 +255,8 @@ input[type=number] { } button.submit { - background-color: #326F95; - color: #fff; + background-color: var(--color-prime); + color: var(--color-background); padding: 12px 60px; border: none; border-radius: 4px; @@ -264,6 +264,16 @@ button.submit { float: left; } +@keyframes button-blink-red { + 0% { background-color: var(--color-info-red); color: var(--color-info-background-red); } + 33.3% { background-color: var(--color-prime); color: var(--color-background); } + 66.6% { background-color: var(--color-info-red); color: var(--color-info-background-red); } +} + +.button-blink { + animation: button-blink-red 0.7s steps(1); +} + .spinner-enabled { display: block; } diff --git a/dsf-fhir/dsf-fhir-server/src/main/resources/fhir/static/form.js b/dsf-fhir/dsf-fhir-server/src/main/resources/fhir/static/form.js index 9e33ac9fb..6fb710404 100644 --- a/dsf-fhir/dsf-fhir-server/src/main/resources/fhir/static/form.js +++ b/dsf-fhir/dsf-fhir-server/src/main/resources/fhir/static/form.js @@ -16,10 +16,8 @@ function startProcess() { const task = readTaskInputsFromForm() - if (task) { - const taskString = JSON.stringify(task) - createTask(taskString) - } + if (task) + createTask(task) } function readTaskInputsFromForm() { @@ -238,10 +236,8 @@ function newTaskInputQuantity(type, id, comparator, value, unit, system, code, o function completeQuestionnaireResponse() { const questionnaireResponse = readQuestionnaireResponseAnswersFromForm() - if (questionnaireResponse) { - const questionnaireResponseString = JSON.stringify(questionnaireResponse) - updateQuestionnaireResponse(questionnaireResponseString) - } + if (questionnaireResponse) + updateQuestionnaireResponse(questionnaireResponse) } function readQuestionnaireResponseAnswersFromForm() { @@ -659,36 +655,60 @@ function addError(errorListElement, message) { } function updateQuestionnaireResponse(questionnaireResponse) { + enableSpinner() + const fullUrl = window.location.origin + window.location.pathname const requestUrl = fullUrl.indexOf("/_history") < 0 ? fullUrl : fullUrl.slice(0, fullUrl.indexOf("/_history")) const resourceBaseUrlWithoutId = fullUrl.slice(0, fullUrl.indexOf("/QuestionnaireResponse") + "/QuestionnaireResponse".length) - - enableSpinner() + const questionnaireResponseString = JSON.stringify(questionnaireResponse) fetch(requestUrl, { method: "PUT", + redirect: "manual", headers: { "Content-type": "application/json", "Accept": "application/json" }, - body: questionnaireResponse - }).then(response => parseResponse(response, resourceBaseUrlWithoutId)) + body: questionnaireResponseString + }).then(response => { + if (response.type === "basic") + parseResponse(response, resourceBaseUrlWithoutId) + else if (response.type === "opaqueredirect") { + sessionStorage.setItem("QuestionnaireResponse.pending", questionnaireResponseString) + sessionStorage.setItem("QuestionnaireResponse.url", window.location.href) + + window.location.reload() + } else + console.warn("Unhandled response type", response.type) + }) } function createTask(task) { - const fullUrl = window.location.origin + window.location.pathname - const requestUrl = fullUrl.slice(0, fullUrl.indexOf("/Task") + "/Task".length) + enableSpinner() - enableSpinner() - - fetch(requestUrl, { - method: "POST", - headers: { - "Content-type": "application/json", - "Accept": "application/json" - }, - body: task - }).then(response => parseResponse(response, requestUrl)) + const fullUrl = window.location.origin + window.location.pathname + const requestUrl = fullUrl.slice(0, fullUrl.indexOf("/Task") + "/Task".length) + const taskString = JSON.stringify(task) + + fetch(requestUrl, { + method: "POST", + redirect: "manual", + headers: { + "Content-type": "application/json", + "Accept": "application/json" + }, + body: taskString + }).then(response => { + if (response.type === "basic") + parseResponse(response, requestUrl) + else if (response.type === "opaqueredirect") { + sessionStorage.setItem("Task.pending", taskString) + sessionStorage.setItem("Task.url", window.location.href) + + window.location.reload() + } else + console.warn("Unhandled response type", response.type) + }) } function parseResponse(response, resourceBaseUrlWithoutId) { @@ -750,13 +770,14 @@ function adaptQuestionnaireResponseInputsIfNotVersion1_0_0() { } } -function loadResource(url) { - return fetch(url, { +async function loadResource(url) { + const response = await fetch(url, { method: "GET", headers: { "Accept": "application/json" } - }).then(response => response.json()) + }) + return await response.json() } function parseStructureDefinition(bundle) { @@ -890,7 +911,7 @@ function appendInputRowAfter(id) { clone.querySelectorAll("[for]").forEach(e => e.setAttribute("for", id + "|" + index)) clone.querySelectorAll("input[id]").forEach(e => e.setAttribute("id", id + "|" + index)) - clone.querySelector("span[class='plus-minus-icon']").remove() + clone.querySelector("span[class='plus-minus-icon']")?.remove() clone.querySelectorAll("input").forEach(input => { input.value = '' @@ -938,4 +959,230 @@ function htmlToElement(html, innerText) { function getResourceAsJson() { const resource = document.getElementById("json").innerText return JSON.parse(resource) +} + +function normalizeUrl(url) { + return new URL(url).origin + new URL(url).pathname +} + +function getInputById(id) { + return document.querySelector(`input[id="${CSS.escape(id)}"]`) +} + +function toLocalDateTime(value) { + const date = new Date(value) + return new Date(date.getTime() - date.getTimezoneOffset() * 60000).toISOString().slice(0, 16) +} + +function handlePendingQuestionnaireResponse() { + const questionnaireResponseString = sessionStorage.getItem("QuestionnaireResponse.pending") + const url = sessionStorage.getItem("QuestionnaireResponse.url") + + sessionStorage.removeItem("QuestionnaireResponse.pending") + sessionStorage.removeItem("QuestionnaireResponse.url") + + if (!questionnaireResponseString || !url) + return + if (normalizeUrl(window.location.href) !== normalizeUrl(url)) + return + + const questionnaireResponse = JSON.parse(questionnaireResponseString) + questionnaireResponse.item.forEach(i => { + if (i.answer === undefined) + return; + + const values = { + boolean: i.answer[0]?.valueBoolean, + string: i.answer[0]?.valueString, + integer: i.answer[0]?.valueInteger, + decimal: i.answer[0]?.valueDecimal, + date: i.answer[0]?.valueDate, + time: i.answer[0]?.valueTime, + dateTime: i.answer[0]?.valueDateTime, + uri: i.answer[0]?.valueUri, + reference: i.answer[0]?.valueReference, + coding: i.answer[0]?.valueCoding, + quantity: i.answer[0]?.valueQuantity + } + + const primitiveValue = + values.boolean ?? values.string ?? values.integer ?? + values.decimal ?? values.date ?? values.time ?? + values.dateTime ?? values.uri + + if (primitiveValue != null) { + const input = getInputById(i.linkId + (values.boolean !== undefined ? `-${values.boolean}` : "")) + + if (input) { + if (values.boolean !== undefined) { + input.checked = true + } else if (values.dateTime) { + input.value = toLocalDateTime(values.dateTime) + } else { + input.value = primitiveValue + } + } + } else if (values.reference) { + const { reference, identifier } = values.reference + + if (reference) { + const input = getInputById(i.linkId) + if (input) input.value = reference + + } else if (identifier) { + const { system, value } = identifier + + const inputSystem = getInputById(i.linkId + "-system") + const inputValue = getInputById(i.linkId + "-value") + + if (inputSystem) inputSystem.value = system + if (inputValue) inputValue.value = value + } + } else if (values.coding) { + const { system, code } = values.coding + + const inputSystem = getInputById(i.linkId + "-system") + const inputCode = getInputById(i.linkId + "-code") + + if (inputSystem) inputSystem.value = system + if (inputCode) inputCode.value = code + } else if (values.quantity) { + const { comparator, value, unit, system, code } = values.quantity + + const fields = { comparator, value, unit, system, code } + + Object.entries(fields).forEach(([key, val]) => { + if (val == null) return + const input = getInputById(`${i.linkId}-${key}`) + if (input) input.value = val + }) + } + }) + + blinkButton("complete-questionnaire-response") +} + +function handlePendingTask() { + const taskString = sessionStorage.getItem("Task.pending") + const url = sessionStorage.getItem("Task.url") + + sessionStorage.removeItem("Task.pending") + sessionStorage.removeItem("Task.url") + + if (!taskString || !url) + return + if (normalizeUrl(window.location.href) !== normalizeUrl(url)) + return + + const baseIdCount = new Map() + + const task = JSON.parse(taskString) + task.input.forEach(i => { + const values = { + boolean: i.valueBoolean, + string: i.valueString, + integer: i.valueInteger, + decimal: i.valueDecimal, + date: i.valueDate, + time: i.valueTime, + dateTime: i.valueDateTime, + instant: i.valueInstant, + uri: i.valueUri, + reference: i.valueReference, + identifier: i.valueIdentifier, + coding: i.valueCoding, + quantity: i.valueQuantity + } + + i.type.coding.forEach(c => { + const baseId = `${c.system}|${c.code}` + + const count = baseIdCount.get(baseId) || 0 + baseIdCount.set(baseId, count + 1) + + const suffix = count === 0 ? "" : `|${count}` + + if (count > 0) + appendInputRowAfter(baseId) + + const fullId = (id) => id + suffix + + const primitiveValue = + values.boolean ?? values.string ?? values.integer ?? + values.decimal ?? values.date ?? values.time ?? + values.dateTime ?? values.instant ?? values.uri + + if (primitiveValue != null) { + const id = fullId(baseId + (values.boolean !== undefined ? `-${values.boolean}` : "")) + + const input = getInputById(id) + + if (input) { + if (values.boolean !== undefined) { + input.checked = true + } else if (values.dateTime || values.instant) { + input.value = toLocalDateTime(values.dateTime || values.instant) + } else { + input.value = primitiveValue + } + } + } else if (values.reference) { + const { reference, identifier } = values.reference + + if (reference) { + const input = getInputById(fullId(baseId)) + if (input) input.value = reference + + } else if (identifier) { + const { system, value } = identifier + + const inputSystem = getInputById(fullId(baseId + "-system")) + const inputValue = getInputById(fullId(baseId + "-value")) + + if (inputSystem) inputSystem.value = system + if (inputValue) inputValue.value = value + } + } else if (values.identifier) { + const { system, value } = values.identifier + + const inputSystem = getInputById(fullId(baseId + "-system")) + const inputValue = getInputById(fullId(baseId + "-value")) + + if (inputSystem) inputSystem.value = system + if (inputValue) inputValue.value = value + } else if (values.coding) { + const { system, code } = values.coding + + const inputSystem = getInputById(fullId(baseId + "-system")) + const inputCode = getInputById(fullId(baseId + "-code")) + + if (inputSystem) inputSystem.value = system + if (inputCode) inputCode.value = code + } else if (values.quantity) { + const { comparator, value, unit, system, code } = values.quantity + + const fields = { comparator, value, unit, system, code } + + Object.entries(fields).forEach(([key, val]) => { + if (val == null) return + const input = getInputById(fullId(`${baseId}-${key}`)) + if (input) input.value = val + }) + } + }) + }) + + blinkButton("start-process") +} + +function blinkButton(id) { + window.addEventListener("load", function() { + const button = document.getElementById(id); + + if (button) { + button.scrollIntoView({behavior: "instant", block: "center"}) + button.classList.add("button-blink") + setTimeout(() => button.classList.remove("button-blink") , 2000) + } + }); } \ No newline at end of file diff --git a/dsf-fhir/dsf-fhir-server/src/main/resources/fhir/static/main.js b/dsf-fhir/dsf-fhir-server/src/main/resources/fhir/static/main.js index 1e8556d4a..1874ae618 100644 --- a/dsf-fhir/dsf-fhir-server/src/main/resources/fhir/static/main.js +++ b/dsf-fhir/dsf-fhir-server/src/main/resources/fhir/static/main.js @@ -107,6 +107,9 @@ window.addEventListener('DOMContentLoaded', () => { completeQuestionnaireResponse() event.preventDefault() }) + + // pending QuestionnaireResponse + handlePendingQuestionnaireResponse() } if (resourceType != null && resourceType[1] === 'Task' && resourceType[2] && (resourceType[3] === undefined || resourceType[4])) { @@ -145,31 +148,31 @@ window.addEventListener('DOMContentLoaded', () => { startProcess() event.preventDefault() }) + + // pending Task + handlePendingTask() } document.querySelectorAll(".collapse-button").forEach(button => { button.addEventListener("click", () => { button.classList.toggle("collapse-button-rotated") - const parent = button.closest(".collapsable"); - parent.classList.toggle("collapsed"); - parent.classList.toggle("expanded"); + const parent = button.closest(".collapsable") + parent.classList.toggle("collapsed") + parent.classList.toggle("expanded") }) - }); + }) document.querySelectorAll(".collapsable").forEach(element => { - content = element.querySelector(".content-pre"); + const content = element.querySelector(".content-pre") + if (!content) + return - function checkOverflow() { - if (content.scrollHeight > element.clientHeight) { - element.classList.add("overflow"); - } else { - element.classList.add("no-overflow"); - } - } + const hasOverflow = content.scrollHeight > element.clientHeight - checkOverflow(); - }); + element.classList.toggle("overflow", hasOverflow) + element.classList.toggle("no-overflow", !hasOverflow) + }) }) window.addEventListener("popstate", (event) => { From 5103d0e9d50f5fce9d13882e27e5f70aa3c398b8 Mon Sep 17 00:00:00 2001 From: Hauke Hund Date: Wed, 25 Mar 2026 09:44:30 +0100 Subject: [PATCH 13/18] removed exception message from status service error return, improved log --- .../java/dev/dsf/common/status/webservice/StatusService.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/dsf-common/dsf-common-status/src/main/java/dev/dsf/common/status/webservice/StatusService.java b/dsf-common/dsf-common-status/src/main/java/dev/dsf/common/status/webservice/StatusService.java index 6862e05f5..37c3a3613 100644 --- a/dsf-common/dsf-common-status/src/main/java/dev/dsf/common/status/webservice/StatusService.java +++ b/dsf-common/dsf-common-status/src/main/java/dev/dsf/common/status/webservice/StatusService.java @@ -79,9 +79,9 @@ public Response status(@Context UriInfo uri, @Context HttpHeaders headers, @Cont String errorMessage = getErrorMessage(e); logger.debug("Error while accessing DB", e); - logger.error("Error while accessing DB: {}", errorMessage); + logger.error("Error while accessing DB: {} - {}", e.getClass().getName(), errorMessage); - return Response.serverError().entity(errorMessage).build(); + return Response.serverError().build(); } } From 454c1197b3444e716add2046a9ff1f9663845bf3 Mon Sep 17 00:00:00 2001 From: Hauke Hund Date: Mon, 30 Mar 2026 01:19:54 +0200 Subject: [PATCH 14/18] improved CSP for Binary resources, new inline display for Binary content * Improved Content-Security-Policy Header config for Binary resources. * New Content-Type header value sanitizer filter to remove internal parameters (pretty, summary, inline, etag) before returning the value to the user. * New inline display mode for binary resource content. HTML and Text content is displayed via iframe and open full-screen. Other media types are not displayed inline but can be displayed full-screen. Query parameter _format=inline returns binary content (if accepted by the client) without sending a "Content-Disposition: attachment ..." header. --- .../dev/dsf/fhir/adapter/ResourceBinary.java | 16 ++- .../media/InlineMediaTypePolicy.java | 23 ++++ .../media/InlineMediaTypePolicyImpl.java | 62 +++++++++ .../dev/dsf/fhir/help/ParameterConverter.java | 37 +++-- .../dev/dsf/fhir/help/ResponseGenerator.java | 3 +- .../dsf/fhir/spring/config/AdapterConfig.java | 8 +- .../spring/config/AuthorizationConfig.java | 8 ++ .../fhir/spring/config/WebserviceConfig.java | 9 +- .../BrowserPolicyHeaderResponseFilter.java | 23 +++- .../filter/ContentTypeSanitizer.java | 49 +++++++ .../impl/AbstractResourceServiceImpl.java | 8 +- .../webservice/jaxrs/BinaryServiceJaxrs.java | 129 ++++++++++-------- .../secure/AbstractResourceServiceSecure.java | 15 ++ .../secure/BinaryServiceSecure.java | 29 ++++ .../src/main/resources/fhir/static/dsf.css | 23 +++- .../src/main/resources/fhir/static/form.css | 4 +- .../src/main/resources/fhir/static/form.js | 79 ++++++----- .../src/main/resources/fhir/static/main.js | 27 ++++ .../fhir/template/resourceBinary.html | 13 +- 19 files changed, 434 insertions(+), 131 deletions(-) create mode 100644 dsf-fhir/dsf-fhir-server/src/main/java/dev/dsf/fhir/authorization/media/InlineMediaTypePolicy.java create mode 100644 dsf-fhir/dsf-fhir-server/src/main/java/dev/dsf/fhir/authorization/media/InlineMediaTypePolicyImpl.java create mode 100644 dsf-fhir/dsf-fhir-server/src/main/java/dev/dsf/fhir/webservice/filter/ContentTypeSanitizer.java diff --git a/dsf-fhir/dsf-fhir-server/src/main/java/dev/dsf/fhir/adapter/ResourceBinary.java b/dsf-fhir/dsf-fhir-server/src/main/java/dev/dsf/fhir/adapter/ResourceBinary.java index 3f4bc42b9..d00d96ec1 100644 --- a/dsf-fhir/dsf-fhir-server/src/main/java/dev/dsf/fhir/adapter/ResourceBinary.java +++ b/dsf-fhir/dsf-fhir-server/src/main/java/dev/dsf/fhir/adapter/ResourceBinary.java @@ -17,6 +17,8 @@ import org.hl7.fhir.r4.model.Binary; +import dev.dsf.fhir.authorization.media.InlineMediaTypePolicy; +import dev.dsf.fhir.help.ParameterConverter; import dev.dsf.fhir.webservice.RangeRequest; public class ResourceBinary extends AbstractResource @@ -24,17 +26,20 @@ public class ResourceBinary extends AbstractResource private static final String[] UNITS = { "Byte", "KiB", "MiB", "GiB", "TiB" }; private static final long UNIT = 1024; - private static record Element(String contentType, ElementId securityContext, String dataSize, String download) + private static record Element(String contentType, ElementId securityContext, String dataSize, String download, + String inlineDisplay, String inlineOpen) { } private final String serverBase; + private final InlineMediaTypePolicy inlineMediaTypePolicy; - public ResourceBinary(String serverBase) + public ResourceBinary(String serverBase, InlineMediaTypePolicy inlineMediaTypePolicy) { super(Binary.class, null); this.serverBase = serverBase; + this.inlineMediaTypePolicy = inlineMediaTypePolicy; } @Override @@ -49,9 +54,12 @@ protected Element toElement(Binary resource) String dataSize = resource.hasDataElement() ? toDataSize(resource) : ""; - String download = resource.getIdElement().withServerBase(serverBase, "Binary").getValue(); + String downloadUrl = resource.getIdElement().withServerBase(serverBase, "Binary").getValue(); + String inlineUrl = downloadUrl + "?_format=" + ParameterConverter.INLINE_FORMAT; + String inlineDisplay = inlineMediaTypePolicy.isInlineDisplayAllowed(contentType) ? inlineUrl : null; + String inlineOpen = inlineMediaTypePolicy.isInlineOpenAllowed(contentType) ? inlineUrl : null; - return new Element(contentType, securityContext, dataSize, download); + return new Element(contentType, securityContext, dataSize, downloadUrl, inlineDisplay, inlineOpen); } private String toDataSize(Binary resource) diff --git a/dsf-fhir/dsf-fhir-server/src/main/java/dev/dsf/fhir/authorization/media/InlineMediaTypePolicy.java b/dsf-fhir/dsf-fhir-server/src/main/java/dev/dsf/fhir/authorization/media/InlineMediaTypePolicy.java new file mode 100644 index 000000000..9eb9da0e7 --- /dev/null +++ b/dsf-fhir/dsf-fhir-server/src/main/java/dev/dsf/fhir/authorization/media/InlineMediaTypePolicy.java @@ -0,0 +1,23 @@ +/* + * Copyright 2018-2025 Heilbronn University of Applied Sciences + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package dev.dsf.fhir.authorization.media; + +public interface InlineMediaTypePolicy +{ + boolean isInlineDisplayAllowed(String mediaType); + + boolean isInlineOpenAllowed(String mediaType); +} diff --git a/dsf-fhir/dsf-fhir-server/src/main/java/dev/dsf/fhir/authorization/media/InlineMediaTypePolicyImpl.java b/dsf-fhir/dsf-fhir-server/src/main/java/dev/dsf/fhir/authorization/media/InlineMediaTypePolicyImpl.java new file mode 100644 index 000000000..9a8d59967 --- /dev/null +++ b/dsf-fhir/dsf-fhir-server/src/main/java/dev/dsf/fhir/authorization/media/InlineMediaTypePolicyImpl.java @@ -0,0 +1,62 @@ +/* + * Copyright 2018-2025 Heilbronn University of Applied Sciences + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package dev.dsf.fhir.authorization.media; + +import java.util.Arrays; +import java.util.List; + +import jakarta.ws.rs.core.MediaType; + +public class InlineMediaTypePolicyImpl implements InlineMediaTypePolicy +{ + private static final MediaType PDF = MediaType.valueOf("application/pdf"); + private static final MediaType PNG = MediaType.valueOf("image/png"); + private static final MediaType JPG = MediaType.valueOf("image/jpeg"); + private static final MediaType GIF = MediaType.valueOf("image/gif"); + private static final MediaType WEBP = MediaType.valueOf("image/webp"); + private static final MediaType SVG = MediaType.valueOf("image/svg+xml"); + private static final MediaType AVIF = MediaType.valueOf("image/avif"); + + private static final List DISPLAY_ALLOWED = Arrays.asList(MediaType.TEXT_HTML_TYPE, + MediaType.TEXT_PLAIN_TYPE); + + private static final List OPEN_ALLOWED = Arrays.asList(MediaType.TEXT_HTML_TYPE, + MediaType.TEXT_PLAIN_TYPE, PDF, PNG, JPG, GIF, WEBP, SVG, AVIF); + + @Override + public boolean isInlineDisplayAllowed(String mediaType) + { + MediaType mt = toMediaType(mediaType); + + return DISPLAY_ALLOWED.stream().anyMatch(m -> m.isCompatible(mt)); + } + + @Override + public boolean isInlineOpenAllowed(String mediaType) + { + MediaType mt = toMediaType(mediaType); + + return OPEN_ALLOWED.stream().anyMatch(m -> m.isCompatible(mt)); + } + + private MediaType toMediaType(String mediaType) + { + if (mediaType == null || mediaType.isBlank()) + return null; + else + return MediaType.valueOf(mediaType); + } +} diff --git a/dsf-fhir/dsf-fhir-server/src/main/java/dev/dsf/fhir/help/ParameterConverter.java b/dsf-fhir/dsf-fhir-server/src/main/java/dev/dsf/fhir/help/ParameterConverter.java index 23fb36d07..838f6a917 100755 --- a/dsf-fhir/dsf-fhir-server/src/main/java/dev/dsf/fhir/help/ParameterConverter.java +++ b/dsf-fhir/dsf-fhir-server/src/main/java/dev/dsf/fhir/help/ParameterConverter.java @@ -45,7 +45,11 @@ public class ParameterConverter { private static final Logger logger = LoggerFactory.getLogger(ParameterConverter.class); + public static final String MEDIA_TYPE_PARAM_INLINE = "inline"; + public static final String MEDIA_TYPE_PARAM_ETAG = "etag"; + public static final String HTML_FORMAT = "html"; + public static final String INLINE_FORMAT = "inline"; public static final String JSON_FORMAT = "json"; public static final List JSON_FORMATS = List.of(Constants.CT_FHIR_JSON, Constants.CT_FHIR_JSON_NEW, MediaType.APPLICATION_JSON); @@ -119,11 +123,13 @@ public Optional getMediaTypeIfSupported(UriInfo uri, HttpHeaders head else if (XML_FORMATS.contains(format) || JSON_FORMATS.contains(format) || MediaType.TEXT_HTML.equals(format)) return getMediaType(format, pretty, summaryMode); else if (XML_FORMAT.equals(format)) - return Optional.of(mediaType("application", "fhir+xml", pretty, summaryMode)); + return Optional.of(mediaType("application", "fhir+xml", "x", pretty, summaryMode, false)); else if (JSON_FORMAT.equals(format)) - return Optional.of(mediaType("application", "fhir+json", pretty, summaryMode)); + return Optional.of(mediaType("application", "fhir+json", "j", pretty, summaryMode, false)); else if (HTML_FORMAT.equals(format)) - return Optional.of(mediaType("text", "html", pretty, summaryMode)); + return Optional.of(mediaType("text", "html", "h", pretty, summaryMode, false)); + else if (INLINE_FORMAT.equals(format)) + return Optional.of(mediaType("*", "*", "i", pretty, summaryMode, true)); else return Optional.empty(); } @@ -134,34 +140,39 @@ private Optional getMediaType(String mediaType, boolean pretty, Summa mediaType = MediaType.WILDCARD; if (mediaType.contains(MediaType.TEXT_HTML)) - return Optional.of(mediaType("text", "html", pretty, summaryMode)); + return Optional.of(mediaType("text", "html", "h", pretty, summaryMode, false)); else if (mediaType.contains(Constants.CT_FHIR_JSON_NEW)) - return Optional.of(mediaType("application", "fhir+json", pretty, summaryMode)); + return Optional.of(mediaType("application", "fhir+json", "j", pretty, summaryMode, false)); else if (mediaType.contains(Constants.CT_FHIR_JSON)) - return Optional.of(mediaType("application", "json+fhir", pretty, summaryMode)); + return Optional.of(mediaType("application", "json+fhir", "j", pretty, summaryMode, false)); else if (mediaType.contains(MediaType.APPLICATION_JSON)) - return Optional.of(mediaType("application", "json", pretty, summaryMode)); + return Optional.of(mediaType("application", "json", "j", pretty, summaryMode, false)); else if (mediaType.contains(Constants.CT_FHIR_XML_NEW)) - return Optional.of(mediaType("application", "fhir+xml", pretty, summaryMode)); + return Optional.of(mediaType("application", "fhir+xml", "x", pretty, summaryMode, false)); else if (mediaType.contains(Constants.CT_FHIR_XML)) - return Optional.of(mediaType("application", "xml+fhir", pretty, summaryMode)); + return Optional.of(mediaType("application", "xml+fhir", "x", pretty, summaryMode, false)); else if (mediaType.contains(MediaType.APPLICATION_XML)) - return Optional.of(mediaType("application", "xml", pretty, summaryMode)); + return Optional.of(mediaType("application", "xml", "x", pretty, summaryMode, false)); else if (mediaType.contains(MediaType.TEXT_XML)) - return Optional.of(mediaType("text", "xml", pretty, summaryMode)); + return Optional.of(mediaType("text", "xml", "x", pretty, summaryMode, false)); else if (mediaType.contains(MediaType.WILDCARD)) - return Optional.of(mediaType("application", "fhir+xml", pretty, summaryMode)); + return Optional.of(mediaType("application", "fhir+xml", "x", pretty, summaryMode, false)); else return Optional.empty(); } - private MediaType mediaType(String type, String subtype, boolean pretty, SummaryMode summaryMode) + private MediaType mediaType(String type, String subtype, String etagPrefix, boolean pretty, SummaryMode summaryMode, + boolean inline) { Map parameters = new HashMap<>(); + parameters.put(MEDIA_TYPE_PARAM_ETAG, etagPrefix); + if (pretty) parameters.put(FhirAdapter.PRETTY, "true"); if (summaryMode != null) parameters.put(FhirAdapter.SUMMARY, summaryMode.toString()); + if (inline) + parameters.put(MEDIA_TYPE_PARAM_INLINE, "true"); return new MediaType(type, subtype, parameters); } diff --git a/dsf-fhir/dsf-fhir-server/src/main/java/dev/dsf/fhir/help/ResponseGenerator.java b/dsf-fhir/dsf-fhir-server/src/main/java/dev/dsf/fhir/help/ResponseGenerator.java index 905e9094b..d44287733 100755 --- a/dsf-fhir/dsf-fhir-server/src/main/java/dev/dsf/fhir/help/ResponseGenerator.java +++ b/dsf-fhir/dsf-fhir-server/src/main/java/dev/dsf/fhir/help/ResponseGenerator.java @@ -153,7 +153,8 @@ public ResponseBuilder response(Status status, Resource resource, MediaType medi && resource.getMeta().getVersionId() != null) { b = b.lastModified(resource.getMeta().getLastUpdated()); - b = b.tag(new EntityTag(resource.getMeta().getVersionId(), true)); + b = b.tag(new EntityTag(mediaType.getParameters().getOrDefault(ParameterConverter.MEDIA_TYPE_PARAM_ETAG, "") + + resource.getMeta().getVersionId(), true)); } b = b.cacheControl(PRIVATE_NO_CACHE_NO_TRANSFORM); diff --git a/dsf-fhir/dsf-fhir-server/src/main/java/dev/dsf/fhir/spring/config/AdapterConfig.java b/dsf-fhir/dsf-fhir-server/src/main/java/dev/dsf/fhir/spring/config/AdapterConfig.java index 400f96ae0..3f146eb56 100755 --- a/dsf-fhir/dsf-fhir-server/src/main/java/dev/dsf/fhir/spring/config/AdapterConfig.java +++ b/dsf-fhir/dsf-fhir-server/src/main/java/dev/dsf/fhir/spring/config/AdapterConfig.java @@ -86,6 +86,9 @@ public class AdapterConfig @Autowired private HelperConfig helperConfig; + @Autowired + private AuthorizationConfig authorizationConfig; + @Bean public FhirAdapter fhirAdapter() { @@ -96,8 +99,9 @@ public FhirAdapter fhirAdapter() public ThymeleafTemplateService thymeleafTemplateService() { List thymeleafContexts = List.of(new ResourceActivityDefinition(), - new ResourceBinary(propertiesConfig.getDsfServerBaseUrl()), new ResourceCodeSystem(), - new ResourceDocumentReference(), new ResourceEndpoint(), new ResourceLibrary(), new ResourceMeasure(), + new ResourceBinary(propertiesConfig.getDsfServerBaseUrl(), authorizationConfig.inlineMediaTypePolicy()), + new ResourceCodeSystem(), new ResourceDocumentReference(), new ResourceEndpoint(), + new ResourceLibrary(), new ResourceMeasure(), new ResourceMeasureReport(propertiesConfig.getDsfServerBaseUrl()), new ResourceNamingSystem(), new ResourceOperationOutcome(buildInfoReaderConfig.buildInfoReader(), daoConfig.statisticsDao(), helperConfig.exceptionHandler()), diff --git a/dsf-fhir/dsf-fhir-server/src/main/java/dev/dsf/fhir/spring/config/AuthorizationConfig.java b/dsf-fhir/dsf-fhir-server/src/main/java/dev/dsf/fhir/spring/config/AuthorizationConfig.java index 2abe4c6b9..098bcc515 100644 --- a/dsf-fhir/dsf-fhir-server/src/main/java/dev/dsf/fhir/spring/config/AuthorizationConfig.java +++ b/dsf-fhir/dsf-fhir-server/src/main/java/dev/dsf/fhir/spring/config/AuthorizationConfig.java @@ -76,6 +76,8 @@ import dev.dsf.fhir.authorization.SubscriptionAuthorizationRule; import dev.dsf.fhir.authorization.TaskAuthorizationRule; import dev.dsf.fhir.authorization.ValueSetAuthorizationRule; +import dev.dsf.fhir.authorization.media.InlineMediaTypePolicy; +import dev.dsf.fhir.authorization.media.InlineMediaTypePolicyImpl; import dev.dsf.fhir.authorization.process.ProcessAuthorizationHelper; import dev.dsf.fhir.authorization.process.ProcessAuthorizationHelperImpl; import dev.dsf.fhir.authorization.read.ReadAccessHelper; @@ -362,4 +364,10 @@ public AuthorizationRule rootAuthorizationRule() { return new RootAuthorizationRule(); } + + @Bean + public InlineMediaTypePolicy inlineMediaTypePolicy() + { + return new InlineMediaTypePolicyImpl(); + } } diff --git a/dsf-fhir/dsf-fhir-server/src/main/java/dev/dsf/fhir/spring/config/WebserviceConfig.java b/dsf-fhir/dsf-fhir-server/src/main/java/dev/dsf/fhir/spring/config/WebserviceConfig.java index 10e332845..2d4286c3d 100755 --- a/dsf-fhir/dsf-fhir-server/src/main/java/dev/dsf/fhir/spring/config/WebserviceConfig.java +++ b/dsf-fhir/dsf-fhir-server/src/main/java/dev/dsf/fhir/spring/config/WebserviceConfig.java @@ -24,6 +24,7 @@ import dev.dsf.common.ui.webservice.StaticResourcesService; import dev.dsf.fhir.exception.DataFormatExceptionHandler; import dev.dsf.fhir.webservice.filter.BrowserPolicyHeaderResponseFilter; +import dev.dsf.fhir.webservice.filter.ContentTypeSanitizer; import dev.dsf.fhir.webservice.impl.ActivityDefinitionServiceImpl; import dev.dsf.fhir.webservice.impl.BinaryServiceImpl; import dev.dsf.fhir.webservice.impl.BundleServiceImpl; @@ -182,6 +183,12 @@ public BrowserPolicyHeaderResponseFilter browserPolicyHeaderResponseFilter() return new BrowserPolicyHeaderResponseFilter(); } + @Bean + public ContentTypeSanitizer contentTypeSanitizer() + { + return new ContentTypeSanitizer(); + } + @Bean public DataFormatExceptionHandler dataFormatExceptionHandler() { @@ -221,7 +228,7 @@ private ActivityDefinitionServiceImpl activityDefinitionServiceImpl() public BinaryService binaryService() { return new BinaryServiceJaxrs(binaryServiceSecure(), helperConfig.parameterConverter(), - adapterConfig.fhirAdapter()); + adapterConfig.fhirAdapter(), authorizationConfig.inlineMediaTypePolicy()); } private BinaryServiceSecure binaryServiceSecure() diff --git a/dsf-fhir/dsf-fhir-server/src/main/java/dev/dsf/fhir/webservice/filter/BrowserPolicyHeaderResponseFilter.java b/dsf-fhir/dsf-fhir-server/src/main/java/dev/dsf/fhir/webservice/filter/BrowserPolicyHeaderResponseFilter.java index c762a6cec..ad4586a22 100644 --- a/dsf-fhir/dsf-fhir-server/src/main/java/dev/dsf/fhir/webservice/filter/BrowserPolicyHeaderResponseFilter.java +++ b/dsf-fhir/dsf-fhir-server/src/main/java/dev/dsf/fhir/webservice/filter/BrowserPolicyHeaderResponseFilter.java @@ -17,6 +17,7 @@ import java.io.IOException; +import dev.dsf.fhir.help.ParameterConverter; import jakarta.ws.rs.container.ContainerRequestContext; import jakarta.ws.rs.container.ContainerResponseContext; import jakarta.ws.rs.container.ContainerResponseFilter; @@ -45,17 +46,29 @@ public void filter(ContainerRequestContext requestContext, ContainerResponseCont headers.add("Cross-Origin-Resource-Policy", "same-site"); headers.add("Permissions-Policy", "geolocation=(), camera=(), microphone=()"); - // Don't send Content-Security-Policy header for non html content + // Don't send Content-Security-Policy header for non html, svg or pdf content if (!requestContext.getUriInfo().getPath().startsWith("static/") || (requestContext.getUriInfo().getPath().startsWith("static/") && (requestContext.getUriInfo().getPath().endsWith(".html") - || requestContext.getUriInfo().getPath().endsWith(".htm")))) + || requestContext.getUriInfo().getPath().endsWith(".htm") + || requestContext.getUriInfo().getPath().endsWith(".svg") + || requestContext.getUriInfo().getPath().endsWith(".pdf")))) { if (requestContext.getUriInfo() != null && requestContext.getUriInfo().getPath() != null && requestContext.getUriInfo().getPath().startsWith("Binary/")) - headers.add("Content-Security-Policy", - "base-uri 'self'; frame-ancestors 'none'; form-action 'self'; default-src 'none'; connect-src 'self'; img-src 'self';" - + " script-src 'self' 'unsafe-inline'; style-src 'self' 'unsafe-inline'"); + { + // Binary content + if (ParameterConverter.INLINE_FORMAT + .equals(requestContext.getUriInfo().getQueryParameters().getFirst("_format"))) + headers.add("Content-Security-Policy", + "base-uri 'self'; frame-ancestors 'self'; form-action 'self'; default-src 'none'; connect-src 'self'; img-src 'self';" + + " script-src 'none'; style-src 'self' 'unsafe-inline'"); + // DSF Binary UI + else + headers.add("Content-Security-Policy", + "base-uri 'self'; frame-ancestors 'none'; form-action 'self'; default-src 'none'; connect-src 'self'; img-src 'self';" + + " script-src 'self'; style-src 'self'; frame-src 'self'"); + } else headers.add("Content-Security-Policy", "base-uri 'self'; frame-ancestors 'none'; form-action 'self'; default-src 'none'; connect-src 'self'; img-src 'self';" diff --git a/dsf-fhir/dsf-fhir-server/src/main/java/dev/dsf/fhir/webservice/filter/ContentTypeSanitizer.java b/dsf-fhir/dsf-fhir-server/src/main/java/dev/dsf/fhir/webservice/filter/ContentTypeSanitizer.java new file mode 100644 index 000000000..b0b04827f --- /dev/null +++ b/dsf-fhir/dsf-fhir-server/src/main/java/dev/dsf/fhir/webservice/filter/ContentTypeSanitizer.java @@ -0,0 +1,49 @@ +/* + * Copyright 2018-2025 Heilbronn University of Applied Sciences + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package dev.dsf.fhir.webservice.filter; + +import java.io.IOException; +import java.util.Map; +import java.util.Map.Entry; +import java.util.stream.Collectors; + +import jakarta.ws.rs.container.ContainerRequestContext; +import jakarta.ws.rs.container.ContainerResponseContext; +import jakarta.ws.rs.container.ContainerResponseFilter; +import jakarta.ws.rs.core.MediaType; +import jakarta.ws.rs.ext.Provider; + +@Provider +public class ContentTypeSanitizer implements ContainerResponseFilter +{ + @Override + public void filter(ContainerRequestContext requestContext, ContainerResponseContext responseContext) + throws IOException + { + MediaType mediaType = responseContext.getMediaType(); + + if (mediaType != null) + { + Map params = mediaType.getParameters().entrySet().stream() + .filter(e -> "charset".equals(e.getKey()) || "boundary".equals(e.getKey())) + .collect(Collectors.toMap(Entry::getKey, Entry::getValue)); + + String clean = new MediaType(mediaType.getType(), mediaType.getSubtype(), params).toString(); + + responseContext.getHeaders().putSingle("Content-Type", clean); + } + } +} 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 074ae86e0..f14270501 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 @@ -440,7 +440,11 @@ protected final Response createReadResponse(UriInfo uri, HttpHeaders headers, Op { referenceCleaner.cleanLiteralReferences(resource); - EntityTag resourceTag = new EntityTag(resource.getMeta().getVersionId(), true); + MediaType mediaType = getMediaTypeForRead(uri, headers); + EntityTag resourceTag = new EntityTag( + mediaType.getParameters().getOrDefault(ParameterConverter.MEDIA_TYPE_PARAM_ETAG, "") + + resource.getMeta().getVersionId(), + true); // not conform to rfc9110 as we are evaluating against a weak ETag here if (ifMatch.map(v -> !v.equals(resource.getIdElement().getVersionIdPartAsLong())).orElse(false)) @@ -472,7 +476,7 @@ else if (ifNoneMatch.isEmpty() && ifModifiedSince else if (isSpecialCase(uri, headers, resource)) return createSpecialCaseResponse(uri, headers, resource); else - return responseGenerator.response(Status.OK, resource, getMediaTypeForRead(uri, headers)).build(); + return responseGenerator.response(Status.OK, resource, mediaType).build(); }).orElseGet(() -> { // TODO return OperationOutcome diff --git a/dsf-fhir/dsf-fhir-server/src/main/java/dev/dsf/fhir/webservice/jaxrs/BinaryServiceJaxrs.java b/dsf-fhir/dsf-fhir-server/src/main/java/dev/dsf/fhir/webservice/jaxrs/BinaryServiceJaxrs.java index 2922ec8c4..0081e88f2 100755 --- a/dsf-fhir/dsf-fhir-server/src/main/java/dev/dsf/fhir/webservice/jaxrs/BinaryServiceJaxrs.java +++ b/dsf-fhir/dsf-fhir-server/src/main/java/dev/dsf/fhir/webservice/jaxrs/BinaryServiceJaxrs.java @@ -35,6 +35,7 @@ import ca.uhn.fhir.rest.api.Constants; import dev.dsf.fhir.adapter.DeferredBase64BinaryType; import dev.dsf.fhir.adapter.FhirAdapter; +import dev.dsf.fhir.authorization.media.InlineMediaTypePolicy; import dev.dsf.fhir.help.ParameterConverter; import dev.dsf.fhir.help.ResponseGenerator; import dev.dsf.fhir.model.StreamableBase64BinaryType; @@ -93,13 +94,16 @@ public void write(OutputStream output) throws IOException, WebApplicationExcepti private final ParameterConverter parameterConverter; private final FhirAdapter fhirAdapter; + private final InlineMediaTypePolicy inlineMediaTypePolicy; - public BinaryServiceJaxrs(BinaryService delegate, ParameterConverter parameterConverter, FhirAdapter fhirAdapter) + public BinaryServiceJaxrs(BinaryService delegate, ParameterConverter parameterConverter, FhirAdapter fhirAdapter, + InlineMediaTypePolicy inlineMediaTypePolicy) { super(delegate); this.parameterConverter = parameterConverter; this.fhirAdapter = fhirAdapter; + this.inlineMediaTypePolicy = inlineMediaTypePolicy; } @Override @@ -109,6 +113,7 @@ public void afterPropertiesSet() throws Exception Objects.requireNonNull(parameterConverter, "parameterConverter"); Objects.requireNonNull(fhirAdapter, "fhirAdapter"); + Objects.requireNonNull(inlineMediaTypePolicy, "inlineMediaTypePolicy"); } @POST @@ -256,71 +261,81 @@ private Response configureReadResponse(UriInfo uri, HttpHeaders headers, boolean { Optional fhirMediaType = getValidFhirMediaType(uri, headers); - if (read.getEntity() instanceof Binary binary && fhirMediaType.isEmpty()) + boolean notFhirMediaType = fhirMediaType.isEmpty(); + + if (read.getEntity() instanceof Binary binary) { - if (mediaTypeMatches(headers, binary)) - { - long dataSize = (long) binary.getUserData(RangeRequest.USER_DATA_VALUE_DATA_SIZE); + boolean inline = (inlineMediaTypePolicy.isInlineDisplayAllowed(binary.getContentType()) + || inlineMediaTypePolicy.isInlineOpenAllowed(binary.getContentType())) + && fhirMediaType.map(m -> "true".equals( + m.getParameters().getOrDefault(ParameterConverter.MEDIA_TYPE_PARAM_INLINE, "false"))) + .orElse(false); - if (head) - { - return toStreamResponse(binary).header(HttpHeaders.CONTENT_LENGTH, dataSize).build(); - } - else + if (notFhirMediaType || inline) + { + if (mediaTypeMatches(headers, binary)) { - RangeRequest rangeRequest = (RangeRequest) binary - .getUserData(RangeRequest.USER_DATA_VALUE_RANGE_REQUEST); + long dataSize = (long) binary.getUserData(RangeRequest.USER_DATA_VALUE_DATA_SIZE); - if (rangeRequest != null && !rangeRequest.isRangeSatisfiable(dataSize)) - { - return Response.status(Status.REQUESTED_RANGE_NOT_SATISFIABLE) - .header(RangeRequest.CONTENT_RANGE_HEADER, - rangeRequest.createContentRangeHeaderValue(dataSize)) - .entity("").build(); - // empty string as content to not trigger default error handler and override header - // alternative: configure jersey.config.server.response.setStatusOverSendError = true - // via JettyServer webAppContext.getServletContext().setAttribute ... - } - - ResponseBuilder response = toStreamResponse(binary); - - // if range request - if (rangeRequest != null && !rangeRequest.isRangeNotDefined()) + if (head) + return toStreamResponse(binary, inline).header(HttpHeaders.CONTENT_LENGTH, dataSize).build(); + else { - response = response.status(Status.PARTIAL_CONTENT) - .header(RangeRequest.CONTENT_RANGE_HEADER, - rangeRequest.createRangeHeaderValue(dataSize)) - .header(HttpHeaders.CONTENT_LENGTH, rangeRequest.getRequestedLength(dataSize)); + RangeRequest rangeRequest = (RangeRequest) binary + .getUserData(RangeRequest.USER_DATA_VALUE_RANGE_REQUEST); + + if (rangeRequest != null && !rangeRequest.isRangeSatisfiable(dataSize)) + { + return Response.status(Status.REQUESTED_RANGE_NOT_SATISFIABLE) + .header(RangeRequest.CONTENT_RANGE_HEADER, + rangeRequest.createContentRangeHeaderValue(dataSize)) + .entity("").build(); + // empty string as content to not trigger default error handler and override header + // alternative: configure jersey.config.server.response.setStatusOverSendError = true + // via JettyServer webAppContext.getServletContext().setAttribute ... + } + + ResponseBuilder response = toStreamResponse(binary, inline); + + // if range request + if (rangeRequest != null && !rangeRequest.isRangeNotDefined()) + { + response = response.status(Status.PARTIAL_CONTENT) + .header(RangeRequest.CONTENT_RANGE_HEADER, + rangeRequest.createRangeHeaderValue(dataSize)) + .header(HttpHeaders.CONTENT_LENGTH, rangeRequest.getRequestedLength(dataSize)); + } + else + response = response.header(HttpHeaders.CONTENT_LENGTH, dataSize); + + return response.entity(new BinaryJaxrsOutputStream(binary)).build(); } - else - response = response.header(HttpHeaders.CONTENT_LENGTH, dataSize); - - return response.entity(new BinaryJaxrsOutputStream(binary)).build(); } + else + return Response.status(Status.NOT_ACCEPTABLE).build(); } - else - return Response.status(Status.NOT_ACCEPTABLE).build(); - } - else if (read.getEntity() instanceof Binary binary && fhirMediaType.isPresent() && head) - { - ResponseBuilder b = Response.status(Status.OK); - b.type(fhirMediaType.get()); - - if (binary.getMeta() != null && binary.getMeta().getLastUpdated() != null - && binary.getMeta().getVersionId() != null) + else if (fhirMediaType.isPresent() && head) { - b.lastModified(binary.getMeta().getLastUpdated()); - b.tag(new EntityTag(binary.getMeta().getVersionId(), true)); - } + ResponseBuilder b = Response.status(Status.OK); + b.type(fhirMediaType.get()); - b.cacheControl(ResponseGenerator.PRIVATE_NO_CACHE_NO_TRANSFORM); + if (binary.getMeta() != null && binary.getMeta().getLastUpdated() != null + && binary.getMeta().getVersionId() != null) + { + b.lastModified(binary.getMeta().getLastUpdated()); + b.tag(new EntityTag(fhirMediaType.get().getParameters().getOrDefault( + ParameterConverter.MEDIA_TYPE_PARAM_ETAG, "") + binary.getMeta().getVersionId(), true)); + } + + b.cacheControl(ResponseGenerator.PRIVATE_NO_CACHE_NO_TRANSFORM); - b.header(HttpHeaders.CONTENT_LENGTH, calculateFhirResponseSize(binary, fhirMediaType.get())); + b.header(HttpHeaders.CONTENT_LENGTH, calculateFhirResponseSize(binary, fhirMediaType.get())); - return b.build(); + return b.build(); + } } - else - return read; + + return read; } private long calculateFhirResponseSize(Binary binary, MediaType mediaType) @@ -374,7 +389,7 @@ private boolean mediaTypeMatches(HttpHeaders headers, Binary binary) .anyMatch(acceptType -> acceptType.isCompatible(binaryMediaType)); } - private ResponseBuilder toStreamResponse(Binary binary) + private ResponseBuilder toStreamResponse(Binary binary, boolean inline) { ResponseBuilder b = Response.status(Status.OK); b.type(binary.getContentType() != null ? binary.getContentType() : MediaType.APPLICATION_OCTET_STREAM); @@ -383,7 +398,7 @@ private ResponseBuilder toStreamResponse(Binary binary) && binary.getMeta().getVersionId() != null) { b.lastModified(binary.getMeta().getLastUpdated()); - b.tag(new EntityTag(binary.getMeta().getVersionId(), true)); + b.tag(new EntityTag((inline ? "i" : "b") + binary.getMeta().getVersionId(), true)); } if (binary.hasSecurityContext() && binary.getSecurityContext().hasReference()) @@ -394,7 +409,9 @@ private ResponseBuilder toStreamResponse(Binary binary) b.cacheControl(ResponseGenerator.PRIVATE_NO_CACHE_NO_TRANSFORM); b.header(RangeRequest.ACCEPT_RANGES_HEADER, RangeRequest.ACCEPT_RANGES_HEADER_VALUE); - b.header(HttpHeaders.CONTENT_DISPOSITION, "attachment; filename=" + toFileName(binary)); + + if (!inline) + b.header(HttpHeaders.CONTENT_DISPOSITION, "attachment; filename=" + toFileName(binary)); return b; } diff --git a/dsf-fhir/dsf-fhir-server/src/main/java/dev/dsf/fhir/webservice/secure/AbstractResourceServiceSecure.java b/dsf-fhir/dsf-fhir-server/src/main/java/dev/dsf/fhir/webservice/secure/AbstractResourceServiceSecure.java index 22ab44eef..c9e4a491b 100755 --- a/dsf-fhir/dsf-fhir-server/src/main/java/dev/dsf/fhir/webservice/secure/AbstractResourceServiceSecure.java +++ b/dsf-fhir/dsf-fhir-server/src/main/java/dev/dsf/fhir/webservice/secure/AbstractResourceServiceSecure.java @@ -135,8 +135,13 @@ private Response withResourceValidation(R resource, Predicate failValidationO { defaultProfileProvider.setDefaultProfile(resource); + Consumer after = modifyBeforeValidation(resource); + ValidationResult validationResult = resourceValidator.validate(resource); + if (after != null) + after.accept(resource); + if (failValidationOnErrorOrFatal.test(resource) && validationResult.getMessages().stream() .anyMatch(m -> ResultSeverityEnum.ERROR.equals(m.getSeverity()) || ResultSeverityEnum.FATAL.equals(m.getSeverity()))) @@ -164,6 +169,16 @@ private Response withResourceValidation(R resource, Predicate failValidationO } } + /** + * @param resource + * never null + * @return consumer to called applied after validation, or null + */ + protected Consumer modifyBeforeValidation(R resource) + { + return null; + } + @Override public Response create(R resource, UriInfo uri, HttpHeaders headers) { diff --git a/dsf-fhir/dsf-fhir-server/src/main/java/dev/dsf/fhir/webservice/secure/BinaryServiceSecure.java b/dsf-fhir/dsf-fhir-server/src/main/java/dev/dsf/fhir/webservice/secure/BinaryServiceSecure.java index 989f7aa5d..b6839d452 100755 --- a/dsf-fhir/dsf-fhir-server/src/main/java/dev/dsf/fhir/webservice/secure/BinaryServiceSecure.java +++ b/dsf-fhir/dsf-fhir-server/src/main/java/dev/dsf/fhir/webservice/secure/BinaryServiceSecure.java @@ -16,6 +16,9 @@ package dev.dsf.fhir.webservice.secure; import java.io.InputStream; +import java.util.Map.Entry; +import java.util.function.Consumer; +import java.util.stream.Collectors; import org.hl7.fhir.r4.model.Binary; @@ -32,6 +35,7 @@ import dev.dsf.fhir.validation.ValidationRules; import dev.dsf.fhir.webservice.specification.BinaryService; import jakarta.ws.rs.core.HttpHeaders; +import jakarta.ws.rs.core.MediaType; import jakarta.ws.rs.core.Response; import jakarta.ws.rs.core.UriInfo; @@ -62,6 +66,31 @@ public Response update(String id, InputStream in, UriInfo uri, HttpHeaders heade throw new UnsupportedOperationException("Implemented and delegated by jaxrs layer"); } + @Override + protected Consumer modifyBeforeValidation(Binary resource) + { + if (!resource.hasContentTypeElement() || !resource.getContentTypeElement().hasValue()) + return null; + + String orgContentType = resource.getContentTypeElement().getValue(); + MediaType orgMediaType = MediaType.valueOf(orgContentType); + + if (orgMediaType.getParameters().isEmpty()) + return null; + else + { + // keep not allowed parameters so that validation can reject them + MediaType tempContentType = new MediaType(orgMediaType.getType(), orgMediaType.getSubtype(), + orgMediaType.getParameters().entrySet().stream() + .filter(e -> !"charset".equals(e.getKey()) && !"boundary".equals(e.getKey())) + .collect(Collectors.toMap(Entry::getKey, Entry::getValue))); + resource.setContentType(tempContentType.toString()); + + // reset Binary resource + return r -> r.setContentType(orgContentType); + } + } + @Override public Response readHead(String id, UriInfo uri, HttpHeaders headers) { diff --git a/dsf-fhir/dsf-fhir-server/src/main/resources/fhir/static/dsf.css b/dsf-fhir/dsf-fhir-server/src/main/resources/fhir/static/dsf.css index ac5c170f3..4b8db3c71 100644 --- a/dsf-fhir/dsf-fhir-server/src/main/resources/fhir/static/dsf.css +++ b/dsf-fhir/dsf-fhir-server/src/main/resources/fhir/static/dsf.css @@ -1298,7 +1298,7 @@ div.row-text { font-size: 1em; } -a#download { +a#binary-download { background-color: #326F95; color: #fff; padding: 12px 60px; @@ -1311,6 +1311,27 @@ a#download { text-decoration: none; } +a#binary-open { + background-color: #326F95; + color: #fff; + padding: 12px 60px; + border: none; + border-radius: 4px; + cursor: pointer; + float: left; + border: 1px outset buttonborder; + border-radius: 4px; + text-decoration: none; +} + +iframe#binary-content { + width: 100%; + height: 4rem; + background-color: #fff; + border: none; + visibility: hidden; +} + .content-header { display: flex; justify-content: space-between; diff --git a/dsf-fhir/dsf-fhir-server/src/main/resources/fhir/static/form.css b/dsf-fhir/dsf-fhir-server/src/main/resources/fhir/static/form.css index 4d38fca45..d087527f6 100644 --- a/dsf-fhir/dsf-fhir-server/src/main/resources/fhir/static/form.css +++ b/dsf-fhir/dsf-fhir-server/src/main/resources/fhir/static/form.css @@ -265,9 +265,9 @@ button.submit { } @keyframes button-blink-red { - 0% { background-color: var(--color-info-red); color: var(--color-info-background-red); } + 0% { background-color: #761137; color: #fff; } 33.3% { background-color: var(--color-prime); color: var(--color-background); } - 66.6% { background-color: var(--color-info-red); color: var(--color-info-background-red); } + 66.6% { background-color: #761137; color: #fff; } } .button-blink { diff --git a/dsf-fhir/dsf-fhir-server/src/main/resources/fhir/static/form.js b/dsf-fhir/dsf-fhir-server/src/main/resources/fhir/static/form.js index 6fb710404..18ff8f4b6 100644 --- a/dsf-fhir/dsf-fhir-server/src/main/resources/fhir/static/form.js +++ b/dsf-fhir/dsf-fhir-server/src/main/resources/fhir/static/form.js @@ -93,12 +93,12 @@ function readAndValidateTaskInput(input, row) { return newTaskInputBoolean(input.type, id, htmlInputs[0].checked, htmlInputs[1].checked, optional) } else if (htmlInputs?.length === 5) { - const input0FhirType = htmlInputs[0].getAttribute("fhir-type") + const input0FhirType = htmlInputs[0].getAttribute("fhir-type") - if (input0FhirType.startsWith("Quantity")) { - return new newTaskInputQuantity(input.type, id, htmlInputs[0].value, htmlInputs[1].value, htmlInputs[2].value, htmlInputs[3].value, htmlInputs[4].value, optional) - } - } + if (input0FhirType.startsWith("Quantity")) { + return new newTaskInputQuantity(input.type, id, htmlInputs[0].value, htmlInputs[1].value, htmlInputs[2].value, htmlInputs[3].value, htmlInputs[4].value, optional) + } + } return { input: null, valid: false } } @@ -263,7 +263,7 @@ function readQuestionnaireResponseAnswersFromForm() { } } }) - + const practitionerIdentifierValue = document.querySelector('#practitionerIdentifierValue')?.value if (practitionerIdentifierValue !== undefined) { questionnaireResponse.author.type = "Practitioner" @@ -436,8 +436,8 @@ function newQuestionnaireResponseItemQuantity(text, id, comparator, value, unit, code: result.value.code } }] - } - return { item: item, valid: true } + } + return { item: item, valid: true } } else return { input: null, valid: result.valid } } @@ -583,7 +583,7 @@ function validateString(errorListElement, value, optional, valueName) { function validateStringInList(errorListElement, value, list, optional, valueName) { const valueInList = s => list.includes(s) - return validateType(errorListElement, value, optional, valueName, valueInList, "not in [" + list.toString() + "]" , v => v) + return validateType(errorListElement, value, optional, valueName, valueInList, "not in [" + list.toString() + "]", v => v) } function validateInteger(errorListElement, value, optional, valueName) { @@ -684,31 +684,31 @@ function updateQuestionnaireResponse(questionnaireResponse) { } function createTask(task) { - enableSpinner() + enableSpinner() const fullUrl = window.location.origin + window.location.pathname - const requestUrl = fullUrl.slice(0, fullUrl.indexOf("/Task") + "/Task".length) + const requestUrl = fullUrl.slice(0, fullUrl.indexOf("/Task") + "/Task".length) const taskString = JSON.stringify(task) - fetch(requestUrl, { - method: "POST", - redirect: "manual", - headers: { - "Content-type": "application/json", - "Accept": "application/json" - }, - body: taskString - }).then(response => { - if (response.type === "basic") - parseResponse(response, requestUrl) - else if (response.type === "opaqueredirect") { - sessionStorage.setItem("Task.pending", taskString) - sessionStorage.setItem("Task.url", window.location.href) - - window.location.reload() - } else - console.warn("Unhandled response type", response.type) - }) + fetch(requestUrl, { + method: "POST", + redirect: "manual", + headers: { + "Content-type": "application/json", + "Accept": "application/json" + }, + body: taskString + }).then(response => { + if (response.type === "basic") + parseResponse(response, requestUrl) + else if (response.type === "opaqueredirect") { + sessionStorage.setItem("Task.pending", taskString) + sessionStorage.setItem("Task.url", window.location.href) + + window.location.reload() + } else + console.warn("Unhandled response type", response.type) + }) } function parseResponse(response, resourceBaseUrlWithoutId) { @@ -1058,7 +1058,7 @@ function handlePendingQuestionnaireResponse() { }) } }) - + blinkButton("complete-questionnaire-response") } @@ -1171,18 +1171,15 @@ function handlePendingTask() { } }) }) - + blinkButton("start-process") } function blinkButton(id) { - window.addEventListener("load", function() { - const button = document.getElementById(id); - - if (button) { - button.scrollIntoView({behavior: "instant", block: "center"}) - button.classList.add("button-blink") - setTimeout(() => button.classList.remove("button-blink") , 2000) - } - }); + const button = document.getElementById(id); + if (button) { + button.scrollIntoView({ behavior: "instant", block: "center" }) + button.classList.add("button-blink") + setTimeout(() => button.classList.remove("button-blink"), 2000) + } } \ No newline at end of file diff --git a/dsf-fhir/dsf-fhir-server/src/main/resources/fhir/static/main.js b/dsf-fhir/dsf-fhir-server/src/main/resources/fhir/static/main.js index 1874ae618..5a027c6f8 100644 --- a/dsf-fhir/dsf-fhir-server/src/main/resources/fhir/static/main.js +++ b/dsf-fhir/dsf-fhir-server/src/main/resources/fhir/static/main.js @@ -153,6 +153,33 @@ window.addEventListener('DOMContentLoaded', () => { handlePendingTask() } + if (resourceType != null && resourceType[1] === 'Binary' && resourceType[2]) { + const iframe = document.getElementById("binary-content") + if (iframe) { + iframe.onload = (event) => { + const doc = event.target.contentDocument?.documentElement + if (doc) { + doc.setAttribute('dsf-iframe', 'true') + + const theme = document.documentElement.getAttribute('theme') + if (theme) + doc.setAttribute('dsf-theme', theme) + + const mode = getUiMode(); + if (mode) + doc.setAttribute('dsf-mode', mode) + + const height = doc.offsetHeight > 0 ? doc.offsetHeight : doc.scrollHeight + iframe.style.height = height + 'px' + iframe.style.visibility = 'visible' + } else { + iframe.style.height = '30vh' + iframe.style.visibility = 'visible' + } + } + } + } + document.querySelectorAll(".collapse-button").forEach(button => { button.addEventListener("click", () => { button.classList.toggle("collapse-button-rotated") diff --git a/dsf-fhir/dsf-fhir-server/src/main/resources/fhir/template/resourceBinary.html b/dsf-fhir/dsf-fhir-server/src/main/resources/fhir/template/resourceBinary.html index e01de56c7..d7765b400 100644 --- a/dsf-fhir/dsf-fhir-server/src/main/resources/fhir/template/resourceBinary.html +++ b/dsf-fhir/dsf-fhir-server/src/main/resources/fhir/template/resourceBinary.html @@ -20,11 +20,18 @@
- + + - foo +
+ + +
+
From 60afac79d978f614be01d25df79483aa8bb9b02d Mon Sep 17 00:00:00 2001 From: Hauke Hund Date: Wed, 8 Apr 2026 19:32:43 +0200 Subject: [PATCH 15/18] improved ETag values to fix regression - FHIR R4 specification enforces week ETags with the value being equal to the resource version number. ETag value for standard FHIR requests can not contain any other characters. ETags follow the specification again, except for frontend HTML responses. - Fixed no ETag send if resource has not lastUpdated value. HTTP header values for "Last-Modified" and "ETag" not set independently. - Added HTTP header "Vary" with value "Accept" to improve potential caching behavior for FHIR json/xml responses that use the same ETag value. --- .../dev/dsf/fhir/help/ParameterConverter.java | 22 +++++++++---------- .../dev/dsf/fhir/help/ResponseGenerator.java | 21 ++++++++++-------- .../webservice/jaxrs/BinaryServiceJaxrs.java | 11 ++++++---- 3 files changed, 30 insertions(+), 24 deletions(-) diff --git a/dsf-fhir/dsf-fhir-server/src/main/java/dev/dsf/fhir/help/ParameterConverter.java b/dsf-fhir/dsf-fhir-server/src/main/java/dev/dsf/fhir/help/ParameterConverter.java index 838f6a917..4583e0baa 100755 --- a/dsf-fhir/dsf-fhir-server/src/main/java/dev/dsf/fhir/help/ParameterConverter.java +++ b/dsf-fhir/dsf-fhir-server/src/main/java/dev/dsf/fhir/help/ParameterConverter.java @@ -123,13 +123,13 @@ public Optional getMediaTypeIfSupported(UriInfo uri, HttpHeaders head else if (XML_FORMATS.contains(format) || JSON_FORMATS.contains(format) || MediaType.TEXT_HTML.equals(format)) return getMediaType(format, pretty, summaryMode); else if (XML_FORMAT.equals(format)) - return Optional.of(mediaType("application", "fhir+xml", "x", pretty, summaryMode, false)); + return Optional.of(mediaType("application", "fhir+xml", "", pretty, summaryMode, false)); else if (JSON_FORMAT.equals(format)) - return Optional.of(mediaType("application", "fhir+json", "j", pretty, summaryMode, false)); + return Optional.of(mediaType("application", "fhir+json", "", pretty, summaryMode, false)); else if (HTML_FORMAT.equals(format)) return Optional.of(mediaType("text", "html", "h", pretty, summaryMode, false)); else if (INLINE_FORMAT.equals(format)) - return Optional.of(mediaType("*", "*", "i", pretty, summaryMode, true)); + return Optional.of(mediaType("*", "*", "", pretty, summaryMode, true)); else return Optional.empty(); } @@ -142,21 +142,21 @@ private Optional getMediaType(String mediaType, boolean pretty, Summa if (mediaType.contains(MediaType.TEXT_HTML)) return Optional.of(mediaType("text", "html", "h", pretty, summaryMode, false)); else if (mediaType.contains(Constants.CT_FHIR_JSON_NEW)) - return Optional.of(mediaType("application", "fhir+json", "j", pretty, summaryMode, false)); + return Optional.of(mediaType("application", "fhir+json", "", pretty, summaryMode, false)); else if (mediaType.contains(Constants.CT_FHIR_JSON)) - return Optional.of(mediaType("application", "json+fhir", "j", pretty, summaryMode, false)); + return Optional.of(mediaType("application", "json+fhir", "", pretty, summaryMode, false)); else if (mediaType.contains(MediaType.APPLICATION_JSON)) - return Optional.of(mediaType("application", "json", "j", pretty, summaryMode, false)); + return Optional.of(mediaType("application", "json", "", pretty, summaryMode, false)); else if (mediaType.contains(Constants.CT_FHIR_XML_NEW)) - return Optional.of(mediaType("application", "fhir+xml", "x", pretty, summaryMode, false)); + return Optional.of(mediaType("application", "fhir+xml", "", pretty, summaryMode, false)); else if (mediaType.contains(Constants.CT_FHIR_XML)) - return Optional.of(mediaType("application", "xml+fhir", "x", pretty, summaryMode, false)); + return Optional.of(mediaType("application", "xml+fhir", "", pretty, summaryMode, false)); else if (mediaType.contains(MediaType.APPLICATION_XML)) - return Optional.of(mediaType("application", "xml", "x", pretty, summaryMode, false)); + return Optional.of(mediaType("application", "xml", "", pretty, summaryMode, false)); else if (mediaType.contains(MediaType.TEXT_XML)) - return Optional.of(mediaType("text", "xml", "x", pretty, summaryMode, false)); + return Optional.of(mediaType("text", "xml", "", pretty, summaryMode, false)); else if (mediaType.contains(MediaType.WILDCARD)) - return Optional.of(mediaType("application", "fhir+xml", "x", pretty, summaryMode, false)); + return Optional.of(mediaType("application", "fhir+xml", "", pretty, summaryMode, false)); else return Optional.empty(); } diff --git a/dsf-fhir/dsf-fhir-server/src/main/java/dev/dsf/fhir/help/ResponseGenerator.java b/dsf-fhir/dsf-fhir-server/src/main/java/dev/dsf/fhir/help/ResponseGenerator.java index d44287733..96386e029 100755 --- a/dsf-fhir/dsf-fhir-server/src/main/java/dev/dsf/fhir/help/ResponseGenerator.java +++ b/dsf-fhir/dsf-fhir-server/src/main/java/dev/dsf/fhir/help/ResponseGenerator.java @@ -134,10 +134,10 @@ public ResponseBuilder response(Status status, Resource resource, MediaType medi switch (prefer) { case REPRESENTATION: - b = b.entity(resource); + b.entity(resource); break; case OPERATION_OUTCOME: - b = b.entity(operationOutcomeCreator.get()); + b.entity(operationOutcomeCreator.get()); break; case MINIMAL: // do nothing, headers only @@ -147,17 +147,20 @@ public ResponseBuilder response(Status status, Resource resource, MediaType medi } if (mediaType != null) - b = b.type(mediaType.withCharset(StandardCharsets.UTF_8.displayName())); + b.type(mediaType.withCharset(StandardCharsets.UTF_8.displayName())); - if (resource.getMeta() != null && resource.getMeta().getLastUpdated() != null - && resource.getMeta().getVersionId() != null) + if (resource.hasMeta()) { - b = b.lastModified(resource.getMeta().getLastUpdated()); - b = b.tag(new EntityTag(mediaType.getParameters().getOrDefault(ParameterConverter.MEDIA_TYPE_PARAM_ETAG, "") - + resource.getMeta().getVersionId(), true)); + if (resource.getMeta().hasLastUpdated()) + b.lastModified(resource.getMeta().getLastUpdated()); + + if (resource.getMeta().hasVersionId()) + b.tag(new EntityTag(mediaType.getParameters().getOrDefault(ParameterConverter.MEDIA_TYPE_PARAM_ETAG, "") + + resource.getMeta().getVersionId(), true)); } - b = b.cacheControl(PRIVATE_NO_CACHE_NO_TRANSFORM); + b.cacheControl(PRIVATE_NO_CACHE_NO_TRANSFORM); + b.header(HttpHeaders.VARY, HttpHeaders.ACCEPT); return b; } diff --git a/dsf-fhir/dsf-fhir-server/src/main/java/dev/dsf/fhir/webservice/jaxrs/BinaryServiceJaxrs.java b/dsf-fhir/dsf-fhir-server/src/main/java/dev/dsf/fhir/webservice/jaxrs/BinaryServiceJaxrs.java index 0081e88f2..c3016cc31 100755 --- a/dsf-fhir/dsf-fhir-server/src/main/java/dev/dsf/fhir/webservice/jaxrs/BinaryServiceJaxrs.java +++ b/dsf-fhir/dsf-fhir-server/src/main/java/dev/dsf/fhir/webservice/jaxrs/BinaryServiceJaxrs.java @@ -394,11 +394,13 @@ private ResponseBuilder toStreamResponse(Binary binary, boolean inline) ResponseBuilder b = Response.status(Status.OK); b.type(binary.getContentType() != null ? binary.getContentType() : MediaType.APPLICATION_OCTET_STREAM); - if (binary.getMeta() != null && binary.getMeta().getLastUpdated() != null - && binary.getMeta().getVersionId() != null) + if (binary.hasMeta()) { - b.lastModified(binary.getMeta().getLastUpdated()); - b.tag(new EntityTag((inline ? "i" : "b") + binary.getMeta().getVersionId(), true)); + if (binary.getMeta().hasLastUpdated()) + b.lastModified(binary.getMeta().getLastUpdated()); + + if (binary.getMeta().hasVersionId()) + b.tag(new EntityTag(binary.getMeta().getVersionId(), true)); } if (binary.hasSecurityContext() && binary.getSecurityContext().hasReference()) @@ -408,6 +410,7 @@ private ResponseBuilder toStreamResponse(Binary binary, boolean inline) } b.cacheControl(ResponseGenerator.PRIVATE_NO_CACHE_NO_TRANSFORM); + b.header(HttpHeaders.VARY, HttpHeaders.ACCEPT); b.header(RangeRequest.ACCEPT_RANGES_HEADER, RangeRequest.ACCEPT_RANGES_HEADER_VALUE); if (!inline) From d3ca59b4daccde16a006fedeccce28fd1f826908 Mon Sep 17 00:00:00 2001 From: Hauke Hund Date: Fri, 10 Apr 2026 17:44:23 +0200 Subject: [PATCH 16/18] fixed inverted token cache timeout logic --- .../main/java/dev/dsf/bpe/client/oidc/OidcClientWithCache.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dsf-bpe/dsf-bpe-server/src/main/java/dev/dsf/bpe/client/oidc/OidcClientWithCache.java b/dsf-bpe/dsf-bpe-server/src/main/java/dev/dsf/bpe/client/oidc/OidcClientWithCache.java index d0c3dbd82..edb3a61f8 100644 --- a/dsf-bpe/dsf-bpe-server/src/main/java/dev/dsf/bpe/client/oidc/OidcClientWithCache.java +++ b/dsf-bpe/dsf-bpe-server/src/main/java/dev/dsf/bpe/client/oidc/OidcClientWithCache.java @@ -115,7 +115,7 @@ public DecodedJWT getAccessTokenDecoded() throws OidcClientException @Override public DecodedJWT getAccessTokenDecoded(Configuration configuration, Jwks jwks) throws OidcClientException { - if (accessTokenCache != null && accessTokenCache.timeout.isBefore(ZonedDateTime.now())) + if (accessTokenCache != null && accessTokenCache.timeout.isAfter(ZonedDateTime.now())) return accessTokenCache.resource; else { From 7d25feafb83d66cb59985ac88568b67d937b1937 Mon Sep 17 00:00:00 2001 From: Hauke Hund Date: Sun, 12 Apr 2026 03:09:49 +0200 Subject: [PATCH 17/18] Improved websocket session handling --- .../authentication/IdentityProviderImpl.java | 6 +- .../dsf/common/auth/DsfOpenIdCredentials.java | 5 + .../common/auth/conf/AbstractIdentity.java | 6 + .../auth/conf/AbstractIdentityProvider.java | 22 +--- .../dev/dsf/common/auth/conf/Identity.java | 9 ++ .../auth/conf/PractitionerIdentityImpl.java | 9 ++ .../auth/conf/X509CertificateWrapper.java | 52 +++++++- .../auth/logging/CurrentUserMdcLogger.java | 8 +- .../common/auth/DsfOpenIdCredentialsImpl.java | 25 +++- .../common/auth/DsfOpenIdLoginService.java | 4 +- .../common/config/AbstractJettyConfig.java | 3 + .../dsf/common/jetty/SessionInvalidator.java | 40 ------ .../authentication/IdentityProviderImpl.java | 10 +- .../AbstractAuthorizationRule.java | 4 +- .../fhir/service/InitialDataLoaderImpl.java | 9 +- .../WebSocketSubscriptionManagerImpl.java | 114 ++++++++++++------ .../dsf/fhir/websocket/ServerEndpoint.java | 11 ++ .../authentication/IdentityProviderTest.java | 6 +- .../process/TestOrganizationIdentity.java | 6 + .../process/TestPractitionerIdentity.java | 6 + .../fhir/dao/TestOrganizationIdentity.java | 6 + 21 files changed, 237 insertions(+), 124 deletions(-) delete mode 100644 dsf-common/dsf-common-jetty/src/main/java/dev/dsf/common/jetty/SessionInvalidator.java diff --git a/dsf-bpe/dsf-bpe-server/src/main/java/dev/dsf/bpe/authentication/IdentityProviderImpl.java b/dsf-bpe/dsf-bpe-server/src/main/java/dev/dsf/bpe/authentication/IdentityProviderImpl.java index 845c936ea..6ec39be4a 100644 --- a/dsf-bpe/dsf-bpe-server/src/main/java/dev/dsf/bpe/authentication/IdentityProviderImpl.java +++ b/dsf-bpe/dsf-bpe-server/src/main/java/dev/dsf/bpe/authentication/IdentityProviderImpl.java @@ -80,14 +80,14 @@ public Identity getIdentity(X509Certificate[] certificates) Organization o = localOrganization.get(); Endpoint e = localEndpoint.get(); - return new PractitionerIdentityImpl(o, e, getDsfRolesFor(p, certWrapper.thumbprint(), null, null), - certWrapper, p, getPractitionerRolesFor(p, certWrapper.thumbprint(), null, null), null); + return new PractitionerIdentityImpl(o, e, getDsfRolesFor(p, certWrapper.getThumbprint(), null, null), + certWrapper, p, getPractitionerRolesFor(p, certWrapper.getThumbprint(), null, null), null); } else { logger.warn( "Certificate with thumbprint '{}' for '{}' unknown, not configured as local user or local organization unknown", - certWrapper.thumbprint(), certWrapper.subjectDn()); + certWrapper.getThumbprint(), certWrapper.getSubjectDn()); return null; } } diff --git a/dsf-common/dsf-common-auth/src/main/java/dev/dsf/common/auth/DsfOpenIdCredentials.java b/dsf-common/dsf-common-auth/src/main/java/dev/dsf/common/auth/DsfOpenIdCredentials.java index 1e26e7713..cd23cb0bc 100644 --- a/dsf-common/dsf-common-auth/src/main/java/dev/dsf/common/auth/DsfOpenIdCredentials.java +++ b/dsf-common/dsf-common-auth/src/main/java/dev/dsf/common/auth/DsfOpenIdCredentials.java @@ -42,4 +42,9 @@ public interface DsfOpenIdCredentials * @return defaultValue if no {@link String} entry with the given key in id-token */ String getStringClaimOrDefault(String key, String defaultValue); + + /** + * @return true if token not expired + */ + boolean isNotExpired(); } diff --git a/dsf-common/dsf-common-auth/src/main/java/dev/dsf/common/auth/conf/AbstractIdentity.java b/dsf-common/dsf-common-auth/src/main/java/dev/dsf/common/auth/conf/AbstractIdentity.java index f2ee39eef..f6fbd35a2 100644 --- a/dsf-common/dsf-common-auth/src/main/java/dev/dsf/common/auth/conf/AbstractIdentity.java +++ b/dsf-common/dsf-common-auth/src/main/java/dev/dsf/common/auth/conf/AbstractIdentity.java @@ -85,6 +85,12 @@ public boolean equals(Object obj) return Objects.equals(organizationIdentifierValue, ((AbstractIdentity) obj).organizationIdentifierValue); } + @Override + public boolean isNotExpired() + { + return certificate != null && certificate.isNotExpired(); + } + @Override public boolean isLocalIdentity() { diff --git a/dsf-common/dsf-common-auth/src/main/java/dev/dsf/common/auth/conf/AbstractIdentityProvider.java b/dsf-common/dsf-common-auth/src/main/java/dev/dsf/common/auth/conf/AbstractIdentityProvider.java index aa3808752..ec4098791 100644 --- a/dsf-common/dsf-common-auth/src/main/java/dev/dsf/common/auth/conf/AbstractIdentityProvider.java +++ b/dsf-common/dsf-common-auth/src/main/java/dev/dsf/common/auth/conf/AbstractIdentityProvider.java @@ -17,8 +17,6 @@ import java.net.URI; import java.net.URISyntaxException; -import java.security.cert.CertificateEncodingException; -import java.security.cert.X509Certificate; import java.util.Arrays; import java.util.List; import java.util.Map; @@ -260,26 +258,10 @@ protected final Optional toPractitioner(X509CertificateWrapper cer if (certWrapper == null) return Optional.empty(); - if (!thumbprints.contains(certWrapper.thumbprint())) + if (!thumbprints.contains(certWrapper.getThumbprint())) return Optional.empty(); - return toJcaX509CertificateHolder(certWrapper.certificate()) - .flatMap(ch -> toPractitioner(ch, certWrapper.thumbprint())); - } - - private Optional toJcaX509CertificateHolder(X509Certificate certificate) - { - try - { - return Optional.of(new JcaX509CertificateHolder(certificate)); - } - catch (CertificateEncodingException e) - { - logger.debug("Unable to decode certificate", e); - logger.warn("Unable to decode certificate: {} - {}", e.getClass().getName(), e.getMessage()); - - return Optional.empty(); - } + return toPractitioner(certWrapper.toJcaX509CertificateHolder(), certWrapper.getThumbprint()); } private Optional toPractitioner(JcaX509CertificateHolder certificate, String thumbprint) diff --git a/dsf-common/dsf-common-auth/src/main/java/dev/dsf/common/auth/conf/Identity.java b/dsf-common/dsf-common-auth/src/main/java/dev/dsf/common/auth/conf/Identity.java index e4e562102..0a4c24926 100644 --- a/dsf-common/dsf-common-auth/src/main/java/dev/dsf/common/auth/conf/Identity.java +++ b/dsf-common/dsf-common-auth/src/main/java/dev/dsf/common/auth/conf/Identity.java @@ -27,6 +27,11 @@ public interface Identity extends Principal String ORGANIZATION_IDENTIFIER_SYSTEM = "http://dsf.dev/sid/organization-identifier"; String ENDPOINT_IDENTIFIER_SYSTEM = "http://dsf.dev/sid/endpoint-identifier"; + /** + * @return true if credentials are not expired + */ + boolean isNotExpired(); + boolean isLocalIdentity(); /** @@ -38,6 +43,10 @@ public interface Identity extends Principal Set getDsfRoles(); + /** + * @param role + * @return true if Identity has the given role + */ boolean hasDsfRole(DsfRole role); /** diff --git a/dsf-common/dsf-common-auth/src/main/java/dev/dsf/common/auth/conf/PractitionerIdentityImpl.java b/dsf-common/dsf-common-auth/src/main/java/dev/dsf/common/auth/conf/PractitionerIdentityImpl.java index c1c5ba3f6..8fd1eda9e 100644 --- a/dsf-common/dsf-common-auth/src/main/java/dev/dsf/common/auth/conf/PractitionerIdentityImpl.java +++ b/dsf-common/dsf-common-auth/src/main/java/dev/dsf/common/auth/conf/PractitionerIdentityImpl.java @@ -100,6 +100,15 @@ public boolean equals(Object obj) && Objects.equals(practitionerIdentifierValue, other.practitionerIdentifierValue); } + @Override + public boolean isNotExpired() + { + if (credentials != null) + return credentials.isNotExpired(); + else + return super.isNotExpired(); + } + @Override public String getName() { diff --git a/dsf-common/dsf-common-auth/src/main/java/dev/dsf/common/auth/conf/X509CertificateWrapper.java b/dsf-common/dsf-common-auth/src/main/java/dev/dsf/common/auth/conf/X509CertificateWrapper.java index 996f6926e..4d466f262 100644 --- a/dsf-common/dsf-common-auth/src/main/java/dev/dsf/common/auth/conf/X509CertificateWrapper.java +++ b/dsf-common/dsf-common-auth/src/main/java/dev/dsf/common/auth/conf/X509CertificateWrapper.java @@ -19,19 +19,31 @@ import java.security.NoSuchAlgorithmException; import java.security.cert.CertificateEncodingException; import java.security.cert.X509Certificate; +import java.time.Instant; import javax.security.auth.x500.X500Principal; import org.apache.commons.codec.binary.Hex; +import org.bouncycastle.cert.jcajce.JcaX509CertificateHolder; -public record X509CertificateWrapper(X509Certificate certificate, String thumbprint, String subjectDn) +public class X509CertificateWrapper { + private final X509Certificate certificate; + private final String thumbprint; + private final String subjectDn; + + private final Instant expiration; + public X509CertificateWrapper(X509Certificate certificate) { - this(certificate, getThumbprint(certificate), getSubjectDn(certificate)); + this.certificate = certificate; + this.thumbprint = toThumbprint(certificate); + this.subjectDn = toSubjectDn(certificate); + + this.expiration = certificate == null ? null : certificate.getNotAfter().toInstant(); } - private static String getThumbprint(X509Certificate certificate) + private static String toThumbprint(X509Certificate certificate) { try { @@ -44,8 +56,40 @@ private static String getThumbprint(X509Certificate certificate) } } - private static String getSubjectDn(X509Certificate certificate) + private static String toSubjectDn(X509Certificate certificate) { return certificate.getSubjectX500Principal().getName(X500Principal.RFC1779); } + + public X509Certificate getCertificate() + { + return certificate; + } + + public String getThumbprint() + { + return thumbprint; + } + + public String getSubjectDn() + { + return subjectDn; + } + + public JcaX509CertificateHolder toJcaX509CertificateHolder() + { + try + { + return new JcaX509CertificateHolder(certificate); + } + catch (CertificateEncodingException e) + { + throw new RuntimeException(e); + } + } + + public boolean isNotExpired() + { + return expiration != null && Instant.now().isBefore(expiration); + } } diff --git a/dsf-common/dsf-common-auth/src/main/java/dev/dsf/common/auth/logging/CurrentUserMdcLogger.java b/dsf-common/dsf-common-auth/src/main/java/dev/dsf/common/auth/logging/CurrentUserMdcLogger.java index 6080e2414..875ddac4c 100644 --- a/dsf-common/dsf-common-auth/src/main/java/dev/dsf/common/auth/logging/CurrentUserMdcLogger.java +++ b/dsf-common/dsf-common-auth/src/main/java/dev/dsf/common/auth/logging/CurrentUserMdcLogger.java @@ -50,9 +50,9 @@ protected void before(OrganizationIdentity organization) { before((Identity) organization); - organization.getCertificate().map(X509CertificateWrapper::thumbprint) + organization.getCertificate().map(X509CertificateWrapper::getThumbprint) .ifPresent(t -> MDC.put(DSF_ORGANIZATION_THUMBPRINT, t)); - organization.getCertificate().map(X509CertificateWrapper::subjectDn) + organization.getCertificate().map(X509CertificateWrapper::getSubjectDn) .ifPresent(d -> MDC.put(DSF_ORGANIZATION_DN, d)); MDC.put(DSF_ORGANIZATION_IDENTIFIER, organization.getOrganizationIdentifierValue()); @@ -64,9 +64,9 @@ protected void before(PractitionerIdentity practitioner) { before((Identity) practitioner); - practitioner.getCertificate().map(X509CertificateWrapper::thumbprint) + practitioner.getCertificate().map(X509CertificateWrapper::getThumbprint) .ifPresent(t -> MDC.put(DSF_PRACTITIONER_THUMBPRINT, t)); - practitioner.getCertificate().map(X509CertificateWrapper::subjectDn) + practitioner.getCertificate().map(X509CertificateWrapper::getSubjectDn) .ifPresent(d -> MDC.put(DSF_PRACTITIONER_DN, d)); practitioner.getCredentials().map(DsfOpenIdCredentials::getUserId) .ifPresent(i -> MDC.put(DSF_PRACTITIONER_SUB, i)); diff --git a/dsf-common/dsf-common-jetty/src/main/java/dev/dsf/common/auth/DsfOpenIdCredentialsImpl.java b/dsf-common/dsf-common-jetty/src/main/java/dev/dsf/common/auth/DsfOpenIdCredentialsImpl.java index ebfde4b22..ed8e00a4e 100644 --- a/dsf-common/dsf-common-jetty/src/main/java/dev/dsf/common/auth/DsfOpenIdCredentialsImpl.java +++ b/dsf-common/dsf-common-jetty/src/main/java/dev/dsf/common/auth/DsfOpenIdCredentialsImpl.java @@ -15,6 +15,7 @@ */ package dev.dsf.common.auth; +import java.time.Instant; import java.util.Collections; import java.util.Map; @@ -29,16 +30,26 @@ public class DsfOpenIdCredentialsImpl implements DsfOpenIdCredentials private final Map idToken; private final Map accessToken; + private final Instant expiration; + public DsfOpenIdCredentialsImpl(OpenIdCredentials credentials) { - this.idToken = JwtDecoder.decode((String) credentials.getResponse().get(ID_TOKEN)); - this.accessToken = JwtDecoder.decode((String) credentials.getResponse().get(ACCESS_TOKEN)); + this(JwtDecoder.decode((String) credentials.getResponse().get(ID_TOKEN)), + JwtDecoder.decode((String) credentials.getResponse().get(ACCESS_TOKEN))); } public DsfOpenIdCredentialsImpl(String accessToken) { - this.idToken = Map.of(); - this.accessToken = JwtDecoder.decode(accessToken); + this(Map.of(), JwtDecoder.decode(accessToken)); + } + + private DsfOpenIdCredentialsImpl(Map idToken, Map accessToken) + { + this.idToken = idToken; + this.accessToken = accessToken; + + Long exp = getLongClaim("exp"); + expiration = exp == null ? null : Instant.ofEpochSecond(exp); } @Override @@ -72,4 +83,10 @@ public String getStringClaimOrDefault(String key, String defaultValue) Object o = getAccessToken().getOrDefault(key, defaultValue); return o instanceof String s ? s : defaultValue; } + + @Override + public boolean isNotExpired() + { + return expiration != null && Instant.now().isBefore(expiration); + } } diff --git a/dsf-common/dsf-common-jetty/src/main/java/dev/dsf/common/auth/DsfOpenIdLoginService.java b/dsf-common/dsf-common-jetty/src/main/java/dev/dsf/common/auth/DsfOpenIdLoginService.java index b7ec15986..eefb8625c 100644 --- a/dsf-common/dsf-common-jetty/src/main/java/dev/dsf/common/auth/DsfOpenIdLoginService.java +++ b/dsf-common/dsf-common-jetty/src/main/java/dev/dsf/common/auth/DsfOpenIdLoginService.java @@ -63,9 +63,7 @@ public boolean validate(UserIdentity user) return false; } - long expiry = identity.getCredentials().get().getLongClaim("exp"); - long currentTimeSeconds = (long) (System.currentTimeMillis() / 1000F); - if (currentTimeSeconds > expiry) + if (!identity.isNotExpired()) { logger.debug("ID Token has expired"); return false; 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 2de826323..b7fd7f6ce 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 @@ -323,6 +323,8 @@ private void configureSecurityHandler(WebAppContext webAppContext, Supplier organization = organizationProvider.getOrganization(certWrapper.thumbprint()); + Optional organization = organizationProvider.getOrganization(certWrapper.getThumbprint()); if (organization.isPresent()) { Organization o = organization.get(); @@ -90,7 +90,7 @@ public Identity getIdentity(X509Certificate[] certificates) boolean local = isLocalOrganization(o); Optional e = local ? getLocalEndpoint() - : endpointProvider.getEndpoint(o, certWrapper.thumbprint()); + : endpointProvider.getEndpoint(o, certWrapper.getThumbprint()); Set r = local ? FhirServerRoleImpl.LOCAL_ORGANIZATION : FhirServerRoleImpl.REMOTE_ORGANIZATION; @@ -105,14 +105,14 @@ public Identity getIdentity(X509Certificate[] certificates) Organization o = localOrganization.get(); Endpoint e = getLocalEndpoint().orElse(null); - return new PractitionerIdentityImpl(o, e, getDsfRolesFor(p, certWrapper.thumbprint(), null, null), - certWrapper, p, getPractitionerRolesFor(p, certWrapper.thumbprint(), null, null), null); + return new PractitionerIdentityImpl(o, e, getDsfRolesFor(p, certWrapper.getThumbprint(), null, null), + certWrapper, p, getPractitionerRolesFor(p, certWrapper.getThumbprint(), null, null), null); } else { logger.warn( "Certificate with thumbprint '{}' for '{}' unknown, not part of allowlist and not configured as local user or local organization", - certWrapper.thumbprint(), certWrapper.subjectDn()); + certWrapper.getThumbprint(), certWrapper.getSubjectDn()); return null; } } 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 c194f3be4..688942af0 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 @@ -324,7 +324,7 @@ protected final Optional createIfLiteralInternalOrLogicalRefe } @Override - public Optional reasonPermanentDeleteAllowed(Identity identity, R oldResource) + public final Optional reasonPermanentDeleteAllowed(Identity identity, R oldResource) { try (Connection connection = daoProvider.newReadOnlyAutoCommitTransaction()) { @@ -400,7 +400,7 @@ && reasonDeleteAllowed(connection, identity, oldResource).isPresent()) } @Override - public Optional reasonWebsocketAllowed(Identity identity, R existingResource) + public final Optional reasonWebsocketAllowed(Identity identity, R existingResource) { try (Connection connection = daoProvider.newReadOnlyAutoCommitTransaction()) { diff --git a/dsf-fhir/dsf-fhir-server/src/main/java/dev/dsf/fhir/service/InitialDataLoaderImpl.java b/dsf-fhir/dsf-fhir-server/src/main/java/dev/dsf/fhir/service/InitialDataLoaderImpl.java index bf25ee0e8..e51b68165 100644 --- a/dsf-fhir/dsf-fhir-server/src/main/java/dev/dsf/fhir/service/InitialDataLoaderImpl.java +++ b/dsf-fhir/dsf-fhir-server/src/main/java/dev/dsf/fhir/service/InitialDataLoaderImpl.java @@ -43,7 +43,14 @@ public class InitialDataLoaderImpl implements InitialDataLoader, InitializingBea org.addIdentifier().setSystem(ReadAccessHelper.ORGANIZATION_IDENTIFIER_SYSTEM).setValue("initial.data.loader"); INITIAL_DATA_LOADER = new OrganizationIdentityImpl(true, org, null, FhirServerRoleImpl.INITIAL_DATA_LOADER, - null); + null) + { + @Override + public boolean isNotExpired() + { + return true; + } + }; } private static final Logger logger = LoggerFactory.getLogger(InitialDataLoaderImpl.class); diff --git a/dsf-fhir/dsf-fhir-server/src/main/java/dev/dsf/fhir/subscription/WebSocketSubscriptionManagerImpl.java b/dsf-fhir/dsf-fhir-server/src/main/java/dev/dsf/fhir/subscription/WebSocketSubscriptionManagerImpl.java index cbfd09973..d64333c99 100755 --- a/dsf-fhir/dsf-fhir-server/src/main/java/dev/dsf/fhir/subscription/WebSocketSubscriptionManagerImpl.java +++ b/dsf-fhir/dsf-fhir-server/src/main/java/dev/dsf/fhir/subscription/WebSocketSubscriptionManagerImpl.java @@ -19,10 +19,12 @@ import java.sql.SQLException; import java.util.ArrayList; import java.util.HashMap; +import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.Objects; import java.util.Optional; +import java.util.Set; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; import java.util.concurrent.TimeUnit; @@ -89,15 +91,29 @@ boolean matches(Resource resource, DaoProvider daoProvider) private static class SessionIdAndRemoteAsync { - final Identity identity; final String sessionId; + + final Identity identity; + final Session session; + final Async remoteAsync; - SessionIdAndRemoteAsync(Identity identity, String sessionId, Async remoteAsync) + SessionIdAndRemoteAsync(String sessionId) { - this.identity = identity; this.sessionId = sessionId; - this.remoteAsync = remoteAsync; + + identity = null; + session = null; + remoteAsync = null; + } + + SessionIdAndRemoteAsync(Identity identity, Session session) + { + this.sessionId = session.getId(); + + this.identity = identity; + this.session = session; + this.remoteAsync = session.getAsyncRemote(); } @Override @@ -116,6 +132,21 @@ public boolean equals(Object obj) SessionIdAndRemoteAsync other = (SessionIdAndRemoteAsync) obj; return Objects.equals(sessionId, other.sessionId); } + + void closeCredentialsExpired() + { + try + { + if (session != null) + session.close(new CloseReason(CloseCodes.VIOLATED_POLICY, "Credentials expired")); + } + catch (IOException e) + { + logger.warn("Error while closing websocket for user {}, session {}, {}", identity.getName(), + session.getId(), e.getMessage()); + logger.debug("Error while closing websocket", e); + } + } } private final ExecutorService executor = Executors.newCachedThreadPool(); @@ -130,7 +161,7 @@ public boolean equals(Object obj) private final AtomicBoolean firstCall = new AtomicBoolean(true); private final ReadWriteMap subscriptionsByIdPart = new ReadWriteMap<>(); private final ReadWriteMap, List> matchersByResource = new ReadWriteMap<>(); - private final ReadWriteMap> asyncRemotesBySubscriptionIdPart = new ReadWriteMap<>(); + private final ReadWriteMap> asyncRemotesBySubscriptionIdPart = new ReadWriteMap<>(); public WebSocketSubscriptionManagerImpl(DaoProvider daoProvider, ExceptionHandler exceptionHandler, MatcherFactory matcherFactory, FhirContext fhirContext, AuthorizationRuleProvider authorizationRuleProvider) @@ -271,7 +302,7 @@ private void doHandleEvent(Event event) private void doHandleEventWithSubscription(Subscription s, Event event) { - Optional> optRemotes = asyncRemotesBySubscriptionIdPart + Optional> optRemotes = asyncRemotesBySubscriptionIdPart .get(s.getIdElement().getIdPart()); if (optRemotes.isEmpty()) @@ -315,27 +346,38 @@ private IParser configureParser(IParser p) private boolean userHasReadAndWebsocketAccess(SessionIdAndRemoteAsync sessionAndRemote, Event event) { - Optional> optRule = authorizationRuleProvider - .getAuthorizationRule(event.getResourceType()); - if (optRule.isPresent()) + if (sessionAndRemote.identity.isNotExpired()) { - @SuppressWarnings("unchecked") - AuthorizationRule rule = (AuthorizationRule) optRule.get(); - Optional readAllowedReason = rule.reasonReadAllowed(sessionAndRemote.identity, event.getResource()); - Optional websocketAllowedReason = rule.reasonWebsocketAllowed(sessionAndRemote.identity, - event.getResource()); - - if (readAllowedReason.isPresent() && websocketAllowedReason.isPresent()) + Optional> optRule = authorizationRuleProvider + .getAuthorizationRule(event.getResourceType()); + if (optRule.isPresent()) { - logger.info("Sending event {} to user {}, websocket access and read of {} allowed {}, {}", - event.getClass().getSimpleName(), sessionAndRemote.identity.getName(), - event.getResourceType().getSimpleName(), websocketAllowedReason.get(), - readAllowedReason.isPresent()); - return true; + @SuppressWarnings("unchecked") + AuthorizationRule rule = (AuthorizationRule) optRule.get(); + Optional readAllowedReason = rule.reasonReadAllowed(sessionAndRemote.identity, + event.getResource()); + Optional websocketAllowedReason = rule.reasonWebsocketAllowed(sessionAndRemote.identity, + event.getResource()); + + if (readAllowedReason.isPresent() && websocketAllowedReason.isPresent()) + { + logger.info("Sending event {} to user {}, websocket access and read of {} allowed {}, {}", + event.getClass().getSimpleName(), sessionAndRemote.identity.getName(), + event.getResourceType().getSimpleName(), websocketAllowedReason.get(), + readAllowedReason.isPresent()); + return true; + } + else + { + logger.warn("Skipping event {} for user {}, websocket access or read of {} not allowed", + event.getClass().getSimpleName(), sessionAndRemote.identity.getName(), + event.getResourceType().getSimpleName()); + return false; + } } else { - logger.warn("Skipping event {} for user {}, websocket access or read of {} not allowed", + logger.warn("Skipping event {} for user {}, no authorization rule for resource of type {} found", event.getClass().getSimpleName(), sessionAndRemote.identity.getName(), event.getResourceType().getSimpleName()); return false; @@ -343,9 +385,11 @@ private boolean userHasReadAndWebsocketAccess(SessionIdAndRemoteAsync sessionAnd } else { - logger.warn("Skipping event {} for user {}, no authorization rule for resource of type {} found", - event.getClass().getSimpleName(), sessionAndRemote.identity.getName(), - event.getResourceType().getSimpleName()); + logger.warn("Closing session with id {} for user {}, credentials expired", sessionAndRemote.sessionId, + sessionAndRemote.identity.getName()); + + sessionAndRemote.closeCredentialsExpired(); + return false; } } @@ -373,18 +417,18 @@ public void bind(Identity identity, Session session, String subscriptionIdPart) if (subscriptionsByIdPart.containsKey(subscriptionIdPart)) { logger.debug("Binding websocket session {} to subscription {}", session.getId(), subscriptionIdPart); - asyncRemotesBySubscriptionIdPart.replace(subscriptionIdPart, list -> + asyncRemotesBySubscriptionIdPart.replace(subscriptionIdPart, set -> { - if (list == null) + if (set == null) { - List newList = new ArrayList<>(); - newList.add(new SessionIdAndRemoteAsync(identity, session.getId(), session.getAsyncRemote())); - return newList; + Set newSet = new HashSet<>(); + newSet.add(new SessionIdAndRemoteAsync(identity, session)); + return newSet; } else { - list.add(new SessionIdAndRemoteAsync(identity, session.getId(), session.getAsyncRemote())); - return list; + set.add(new SessionIdAndRemoteAsync(identity, session)); + return set; } }); session.getAsyncRemote().sendText("bound " + subscriptionIdPart); @@ -407,7 +451,7 @@ private void closeNotFound(Identity identity, Session session, String subscripti } catch (IOException e) { - logger.warn("Error while closing websocket with user {}, session {}, {}", identity.getName(), + logger.warn("Error while closing websocket for user {}, session {}, {}", identity.getName(), session.getId(), e.getMessage()); logger.debug("Error while closing websocket", e); } @@ -417,7 +461,7 @@ private void closeNotFound(Identity identity, Session session, String subscripti public void close(String sessionId) { logger.debug("Removing websocket session {}", sessionId); - asyncRemotesBySubscriptionIdPart.removeWhereValueMatches(List::isEmpty, - list -> list.remove(new SessionIdAndRemoteAsync(null, sessionId, null))); + asyncRemotesBySubscriptionIdPart.removeWhereValueMatches(Set::isEmpty, + s -> s.remove(new SessionIdAndRemoteAsync(sessionId))); } } diff --git a/dsf-fhir/dsf-fhir-server/src/main/java/dev/dsf/fhir/websocket/ServerEndpoint.java b/dsf-fhir/dsf-fhir-server/src/main/java/dev/dsf/fhir/websocket/ServerEndpoint.java index 7abda59bd..ab44d7cd7 100755 --- a/dsf-fhir/dsf-fhir-server/src/main/java/dev/dsf/fhir/websocket/ServerEndpoint.java +++ b/dsf-fhir/dsf-fhir-server/src/main/java/dev/dsf/fhir/websocket/ServerEndpoint.java @@ -27,6 +27,7 @@ import java.util.concurrent.TimeUnit; import org.apache.commons.codec.binary.Hex; +import org.eclipse.jetty.websocket.core.exception.CloseException; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.DisposableBean; @@ -37,6 +38,7 @@ import dev.dsf.fhir.authentication.FhirServerRoleImpl; import dev.dsf.fhir.subscription.WebSocketSubscriptionManager; import jakarta.websocket.CloseReason; +import jakarta.websocket.CloseReason.CloseCode; import jakarta.websocket.CloseReason.CloseCodes; import jakarta.websocket.Endpoint; import jakarta.websocket.EndpointConfig; @@ -168,6 +170,15 @@ public void onError(Session session, Throwable throwable) { if (throwable == null) logger.info("Websocket closed with error, session {}: unknown error", session.getId()); + else if (throwable instanceof CloseException close) + { + String status = String.valueOf(close.getStatusCode()); + CloseCode c = CloseCodes.getCloseCode(close.getStatusCode()); + if (c instanceof CloseCodes cc) + status += " - " + cc.name(); + + logger.info("Websocket closed with status {}, session {}", status, session.getId()); + } else { logger.debug("Websocket closed with error, session {}", session.getId(), throwable); diff --git a/dsf-fhir/dsf-fhir-server/src/test/java/dev/dsf/fhir/authentication/IdentityProviderTest.java b/dsf-fhir/dsf-fhir-server/src/test/java/dev/dsf/fhir/authentication/IdentityProviderTest.java index d134e64fa..721d1105a 100644 --- a/dsf-fhir/dsf-fhir-server/src/test/java/dev/dsf/fhir/authentication/IdentityProviderTest.java +++ b/dsf-fhir/dsf-fhir-server/src/test/java/dev/dsf/fhir/authentication/IdentityProviderTest.java @@ -218,7 +218,7 @@ public void testGetOrganizationIdentityByX509CertificateLocalOrganization() thro assertNotNull(orgI.getCertificate()); assertTrue(orgI.getCertificate().isPresent()); assertEquals(LOCAL_ORGANIZATION_CERTIFICATE, - orgI.getCertificate().map(X509CertificateWrapper::certificate).get()); + orgI.getCertificate().map(X509CertificateWrapper::getCertificate).get()); assertEquals(LOCAL_ORGANIZATION_IDENTIFIER_VALUE, orgI.getDisplayName()); assertEquals(FhirServerRoleImpl.LOCAL_ORGANIZATION, orgI.getDsfRoles()); assertEquals(LOCAL_ORGANIZATION_IDENTIFIER_VALUE, orgI.getName()); @@ -250,7 +250,7 @@ public void testGetOrganizationIdentityByX509CertificateRemoteOrganization() thr assertNotNull(orgI.getCertificate()); assertTrue(orgI.getCertificate().isPresent()); assertEquals(REMOTE_ORGANIZATION_CERTIFICATE, - orgI.getCertificate().map(X509CertificateWrapper::certificate).get()); + orgI.getCertificate().map(X509CertificateWrapper::getCertificate).get()); assertEquals(REMOTE_ORGANIZATION_IDENTIFIER_VALUE, orgI.getDisplayName()); assertEquals(FhirServerRoleImpl.REMOTE_ORGANIZATION, orgI.getDsfRoles()); assertEquals(REMOTE_ORGANIZATION_IDENTIFIER_VALUE, orgI.getName()); @@ -315,7 +315,7 @@ public void testGetPractitionerIdentityByX509Certificate() throws Exception assertNotNull(practitionerI.getCertificate()); assertTrue(practitionerI.getCertificate().isPresent()); assertEquals(LOCAL_PRACTITIONER_CERTIFICATE, - practitionerI.getCertificate().map(X509CertificateWrapper::certificate).get()); + practitionerI.getCertificate().map(X509CertificateWrapper::getCertificate).get()); assertNotNull(practitionerI.getCredentials()); assertTrue(practitionerI.getCredentials().isEmpty()); assertEquals(LOCAL_PRACTITIONER_NAME_GIVEN + " " + LOCAL_PRACTITIONER_NAME_FAMILY, diff --git a/dsf-fhir/dsf-fhir-server/src/test/java/dev/dsf/fhir/authorization/process/TestOrganizationIdentity.java b/dsf-fhir/dsf-fhir-server/src/test/java/dev/dsf/fhir/authorization/process/TestOrganizationIdentity.java index cf31271fc..fc8ffc1f3 100644 --- a/dsf-fhir/dsf-fhir-server/src/test/java/dev/dsf/fhir/authorization/process/TestOrganizationIdentity.java +++ b/dsf-fhir/dsf-fhir-server/src/test/java/dev/dsf/fhir/authorization/process/TestOrganizationIdentity.java @@ -59,6 +59,12 @@ public String getDisplayName() throw new UnsupportedOperationException(); } + @Override + public boolean isNotExpired() + { + return false; + } + @Override public boolean isLocalIdentity() { diff --git a/dsf-fhir/dsf-fhir-server/src/test/java/dev/dsf/fhir/authorization/process/TestPractitionerIdentity.java b/dsf-fhir/dsf-fhir-server/src/test/java/dev/dsf/fhir/authorization/process/TestPractitionerIdentity.java index 0de228cca..fbbf4fbd9 100644 --- a/dsf-fhir/dsf-fhir-server/src/test/java/dev/dsf/fhir/authorization/process/TestPractitionerIdentity.java +++ b/dsf-fhir/dsf-fhir-server/src/test/java/dev/dsf/fhir/authorization/process/TestPractitionerIdentity.java @@ -61,6 +61,12 @@ public String getDisplayName() throw new UnsupportedOperationException(); } + @Override + public boolean isNotExpired() + { + return false; + } + @Override public boolean isLocalIdentity() { diff --git a/dsf-fhir/dsf-fhir-server/src/test/java/dev/dsf/fhir/dao/TestOrganizationIdentity.java b/dsf-fhir/dsf-fhir-server/src/test/java/dev/dsf/fhir/dao/TestOrganizationIdentity.java index 4143e18d4..b7929f4de 100644 --- a/dsf-fhir/dsf-fhir-server/src/test/java/dev/dsf/fhir/dao/TestOrganizationIdentity.java +++ b/dsf-fhir/dsf-fhir-server/src/test/java/dev/dsf/fhir/dao/TestOrganizationIdentity.java @@ -30,6 +30,12 @@ private TestOrganizationIdentity(boolean localIdentity, Organization organizatio super(localIdentity, organization, null, roles, null); } + @Override + public boolean isNotExpired() + { + return true; + } + public static TestOrganizationIdentity local(Organization organization) { return new TestOrganizationIdentity(true, organization, FhirServerRoleImpl.LOCAL_ORGANIZATION); From 2e0101fe956245d036e3c567954c1aa66944ea76 Mon Sep 17 00:00:00 2001 From: Hauke Hund Date: Tue, 14 Apr 2026 09:42:55 +0200 Subject: [PATCH 18/18] null guard --- .../main/java/dev/dsf/fhir/help/ResponseGenerator.java | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/dsf-fhir/dsf-fhir-server/src/main/java/dev/dsf/fhir/help/ResponseGenerator.java b/dsf-fhir/dsf-fhir-server/src/main/java/dev/dsf/fhir/help/ResponseGenerator.java index 96386e029..4dfa782c3 100755 --- a/dsf-fhir/dsf-fhir-server/src/main/java/dev/dsf/fhir/help/ResponseGenerator.java +++ b/dsf-fhir/dsf-fhir-server/src/main/java/dev/dsf/fhir/help/ResponseGenerator.java @@ -155,8 +155,12 @@ public ResponseBuilder response(Status status, Resource resource, MediaType medi b.lastModified(resource.getMeta().getLastUpdated()); if (resource.getMeta().hasVersionId()) - b.tag(new EntityTag(mediaType.getParameters().getOrDefault(ParameterConverter.MEDIA_TYPE_PARAM_ETAG, "") - + resource.getMeta().getVersionId(), true)); + { + b.tag(new EntityTag(mediaType == null ? "" + : mediaType.getParameters().getOrDefault(ParameterConverter.MEDIA_TYPE_PARAM_ETAG, "") + + resource.getMeta().getVersionId(), + true)); + } } b.cacheControl(PRIVATE_NO_CACHE_NO_TRANSFORM);