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/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-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..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 @@ -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) @@ -164,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 @@ -191,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 { @@ -226,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..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 @@ -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 { @@ -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 { 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-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-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-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 c3079bd49..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 @@ -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,34 @@ 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 + public boolean isNotExpired() + { + return certificate != null && certificate.isNotExpired(); } @Override @@ -74,9 +104,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/AbstractIdentityProvider.java b/dsf-common/dsf-common-auth/src/main/java/dev/dsf/common/auth/conf/AbstractIdentityProvider.java index 68c22b425..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; @@ -139,12 +137,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 +150,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() @@ -277,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 61337528b..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(); /** @@ -34,10 +39,14 @@ public interface Identity extends Principal */ Organization getOrganization(); - Optional getOrganizationIdentifierValue(); + String getOrganizationIdentifierValue(); 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/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..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 @@ -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,51 @@ 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 boolean isNotExpired() + { + if (credentials != null) + return credentials.isNotExpired(); + else + return super.isNotExpired(); } @Override public String getName() { - return getOrganizationIdentifierValue().orElse("?") + "/" + getPractitionerIdentifierValue().orElse("?"); + return getOrganizationIdentifierValue() + "/" + practitionerIdentifierValue; } @Override @@ -87,9 +128,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/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())); } } 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 ee179acb0..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,12 +50,12 @@ 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)); - 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)); } @@ -64,16 +64,16 @@ 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)); - 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-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-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) { 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) { 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 032c595cd..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 @@ -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); } @@ -76,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 425e1de6c..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 @@ -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) @@ -162,6 +164,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; @@ -202,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; @@ -307,6 +321,15 @@ private KeyStore clientCertificateTrustStore() private void configureSecurityHandler(WebAppContext webAppContext, Supplier statusPortSupplier) { SessionHandler sessionHandler = webAppContext.getSessionHandler(); + sessionHandler.setSameSite(SameSite.LAX); + sessionHandler.setMaxInactiveInterval(oidcSessionTimeout()); + sessionHandler.setSessionIdPathParameterName(null); + sessionHandler.setRefreshCookieAge(Math.min(oidcSessionTimeout() / 2, 600)); + + SessionCookieConfig sessionCookieConfig = sessionHandler.getSessionCookieConfig(); + sessionCookieConfig.setSecure(true); + sessionCookieConfig.setHttpOnly(true); + DsfLoginService dsfLoginService = new DsfLoginService(webAppContext); OpenIdConfiguration openIdConfiguration = null; @@ -318,7 +341,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()) + 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/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()) { 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()); } } 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(); } } 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/adapter/ResourceBinary.java b/dsf-fhir/dsf-fhir-server/src/main/java/dev/dsf/fhir/adapter/ResourceBinary.java index d9af98bfd..b04b8ed70 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/adapter/ThymeleafTemplateServiceImpl.java b/dsf-fhir/dsf-fhir-server/src/main/java/dev/dsf/fhir/adapter/ThymeleafTemplateServiceImpl.java index e13f36f22..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; @@ -34,6 +35,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 +65,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 { @@ -99,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; @@ -106,7 +113,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 +146,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 @@ -165,9 +186,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() @@ -179,7 +200,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())); @@ -232,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/java/dev/dsf/fhir/authentication/IdentityProviderImpl.java b/dsf-fhir/dsf-fhir-server/src/main/java/dev/dsf/fhir/authentication/IdentityProviderImpl.java index 9ce3a23b0..7164c8a7a 100644 --- a/dsf-fhir/dsf-fhir-server/src/main/java/dev/dsf/fhir/authentication/IdentityProviderImpl.java +++ b/dsf-fhir/dsf-fhir-server/src/main/java/dev/dsf/fhir/authentication/IdentityProviderImpl.java @@ -82,7 +82,7 @@ public Identity getIdentity(X509Certificate[] certificates) X509CertificateWrapper certWrapper = new X509CertificateWrapper(certificates[0]); - Optional 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/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-common/dsf-common-jetty/src/main/java/dev/dsf/common/jetty/SessionInvalidator.java b/dsf-fhir/dsf-fhir-server/src/main/java/dev/dsf/fhir/authorization/media/InlineMediaTypePolicy.java similarity index 50% rename from dsf-common/dsf-common-jetty/src/main/java/dev/dsf/common/jetty/SessionInvalidator.java rename to dsf-fhir/dsf-fhir-server/src/main/java/dev/dsf/fhir/authorization/media/InlineMediaTypePolicy.java index f1c40baf2..9eb9da0e7 100644 --- a/dsf-common/dsf-common-jetty/src/main/java/dev/dsf/common/jetty/SessionInvalidator.java +++ b/dsf-fhir/dsf-fhir-server/src/main/java/dev/dsf/fhir/authorization/media/InlineMediaTypePolicy.java @@ -13,28 +13,11 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package dev.dsf.common.jetty; +package dev.dsf.fhir.authorization.media; -import jakarta.servlet.ServletRequestEvent; -import jakarta.servlet.ServletRequestListener; -import jakarta.servlet.http.HttpServletRequest; -import jakarta.servlet.http.HttpSession; - -public class SessionInvalidator implements ServletRequestListener +public interface InlineMediaTypePolicy { - @Override - public void requestInitialized(ServletRequestEvent sre) - { - // nothing to do - } - - @Override - public void requestDestroyed(ServletRequestEvent sre) - { - HttpServletRequest servletRequest = (HttpServletRequest) sre.getServletRequest(); - HttpSession session = servletRequest.getSession(false); + boolean isInlineDisplayAllowed(String mediaType); - if (session != null) - session.invalidate(); - } + 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/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 86370fe2c..f5ff2763a 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); } @@ -977,7 +978,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 2b6a5a119..41e328b6e 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.ReadByUrlDao; @@ -63,12 +65,12 @@ 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, ReadByUrlDaoFactory readByUrlDaoFactory) { - 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 229845143..8b1416fef 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.dao.ReadByUrlDao; @@ -49,10 +51,12 @@ public class ActivityDefinitionDaoJdbc extends AbstractResourceDaoJdbc readByUrl; public ActivityDefinitionDaoJdbc(DataSource dataSource, DataSource permanentDeleteDataSource, - FhirContext fhirContext, ReadByUrlDaoFactory readByUrlDaoFactory) + FhirContext fhirContext, ObjectMapper objectMapper, + ReadByUrlDaoFactory readByUrlDaoFactory) { - 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 08b5a26a8..a045d138f 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 1710b72fb..d86a81f83 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.dao.ReadByUrlDao; @@ -40,10 +42,10 @@ public class CodeSystemDaoJdbc extends AbstractResourceDaoJdbc imple private final ReadByUrlDao readByUrl; public CodeSystemDaoJdbc(DataSource dataSource, DataSource permanentDeleteDataSource, FhirContext fhirContext, - ReadByUrlDaoFactory readByUrlDaoFactory) + ObjectMapper objectMapper, ReadByUrlDaoFactory readByUrlDaoFactory) { - 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 LibraryDao { private final ReadByUrlDao readByUrl; public LibraryDaoJdbc(DataSource dataSource, DataSource permanentDeleteDataSource, FhirContext fhirContext, - ReadByUrlDaoFactory readByUrlDaoFactory) + ObjectMapper objectMapper, ReadByUrlDaoFactory readByUrlDaoFactory) { - 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()), + LibraryIdentifier.getNameModifiers()), factory(LibraryName.PARAMETER_NAME, LibraryName::new, LibraryName.getNameModifiers()), factory(LibraryStatus.PARAMETER_NAME, LibraryStatus::new, LibraryStatus.getNameModifiers()), factory(LibraryUrl.PARAMETER_NAME, LibraryUrl::new, LibraryUrl.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 0d1861a4f..32924abb7 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.dao.ReadByUrlDao; @@ -41,10 +43,10 @@ public class MeasureDaoJdbc extends AbstractResourceDaoJdbc implements private final ReadByUrlDao readByUrl; public MeasureDaoJdbc(DataSource dataSource, DataSource permanentDeleteDataSource, FhirContext fhirContext, - ReadByUrlDaoFactory readByUrlDaoFactory) + ObjectMapper objectMapper, ReadByUrlDaoFactory readByUrlDaoFactory) { - 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 a8175a7db..8d39a5cfa 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 ('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 " - + "('Organization/' || (organization->>'id')) = organization_affiliation->'participatingOrganization'->>'reference') @> ?::jsonb"; + + "('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 ('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 cf840bf96..5df0a1ae5 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 ebbf510f5..ad9399846 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.dao.ReadByUrlDao; @@ -44,9 +46,9 @@ public class QuestionnaireDaoJdbc extends AbstractResourceDaoJdbc private final ReadByUrlDao readByUrl; public QuestionnaireDaoJdbc(DataSource dataSource, DataSource permanentDeleteDataSource, FhirContext fhirContext, - ReadByUrlDaoFactory readByUrlDaoFactory) + ObjectMapper objectMapper, ReadByUrlDaoFactory readByUrlDaoFactory) { - 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 4e0ccd28b..e4b787a2a 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,20 @@ 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, ReadByUrlDaoFactory readByUrlDaoFactory) + FhirContext fhirContext, ObjectMapper objectMapper, + ReadByUrlDaoFactory readByUrlDaoFactory) { - super(dataSource, permanentDeleteDataSource, fhirContext, "structure_definitions", "structure_definition", - "structure_definition_id", StructureDefinitionIdentityFilter::new, readByUrlDaoFactory); + super(dataSource, permanentDeleteDataSource, fhirContext, objectMapper, "structure_definitions", + "structure_definition", "structure_definition_id", StructureDefinitionIdentityFilter::new, + readByUrlDaoFactory); } @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 dc5b01bc8..a1a94299a 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,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.StructureDefinitionSnapshotIdentityFilter; public class StructureDefinitionSnapshotDaoJdbc extends AbstractStructureDefinitionDaoJdbc { public StructureDefinitionSnapshotDaoJdbc(DataSource dataSource, DataSource permanentDeleteDataSource, - FhirContext fhirContext, ReadByUrlDaoFactory readByUrlDaoFactory) + FhirContext fhirContext, ObjectMapper objectMapper, + ReadByUrlDaoFactory readByUrlDaoFactory) { - super(dataSource, permanentDeleteDataSource, fhirContext, "structure_definition_snapshots", + super(dataSource, permanentDeleteDataSource, fhirContext, objectMapper, "structure_definition_snapshots", "structure_definition_snapshot", "structure_definition_snapshot_id", StructureDefinitionSnapshotIdentityFilter::new, readByUrlDaoFactory); } 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 cb9394c7e..2385953be 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.ReadByUrlDao; import dev.dsf.fhir.dao.ValueSetDao; @@ -40,10 +42,10 @@ public class ValueSetDaoJdbc extends AbstractResourceDaoJdbc implement private final ReadByUrlDao readByUrl; public ValueSetDaoJdbc(DataSource dataSource, DataSource permanentDeleteDataSource, FhirContext fhirContext, - ReadByUrlDaoFactory readByUrlDaoFactory) + ObjectMapper objectMapper, ReadByUrlDaoFactory readByUrlDaoFactory) { - 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/help/ParameterConverter.java b/dsf-fhir/dsf-fhir-server/src/main/java/dev/dsf/fhir/help/ParameterConverter.java index 23fb36d07..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 @@ -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", "", 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", "", 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("*", "*", "", 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", "", 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", "", pretty, summaryMode, false)); else if (mediaType.contains(MediaType.APPLICATION_JSON)) - return Optional.of(mediaType("application", "json", pretty, summaryMode)); + 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", pretty, summaryMode)); + 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", pretty, summaryMode)); + return Optional.of(mediaType("application", "xml+fhir", "", pretty, summaryMode, false)); else if (mediaType.contains(MediaType.APPLICATION_XML)) - return Optional.of(mediaType("application", "xml", pretty, summaryMode)); + return Optional.of(mediaType("application", "xml", "", pretty, summaryMode, false)); else if (mediaType.contains(MediaType.TEXT_XML)) - return Optional.of(mediaType("text", "xml", pretty, summaryMode)); + return Optional.of(mediaType("text", "xml", "", 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", "", 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..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 @@ -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,16 +147,24 @@ 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(resource.getMeta().getVersionId(), true)); + if (resource.getMeta().hasLastUpdated()) + b.lastModified(resource.getMeta().getLastUpdated()); + + if (resource.getMeta().hasVersionId()) + { + b.tag(new EntityTag(mediaType == null ? "" + : 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/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 63e90b788..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 { @@ -60,13 +60,13 @@ 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 " + "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))))"; } @@ -79,30 +79,25 @@ 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; } @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") - && 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())); + 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 5cd2945a7..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"; - else if (p.getPractitionerIdentifierValue().isPresent()) + 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' @> ?" + ")"; } } @@ -93,7 +95,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; @@ -104,8 +106,8 @@ else if (p.getPractitionerIdentifierValue().isPresent()) } @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,24 +129,26 @@ 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().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\": \"" - + 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 4bd5177a3..c5c9a450a 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 4775a67db..a6e3d6c12 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,13 @@ 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 ('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 ('Endpoint/' || (endpoint->>'id')) IN (SELECT reference->>'reference' FROM jsonb_array_elements(organization_affiliation->'endpoint') reference)" @@ -87,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/OrganizationAffiliationParticipatingOrganization.java b/dsf-fhir/dsf-fhir-server/src/main/java/dev/dsf/fhir/search/parameters/OrganizationAffiliationParticipatingOrganization.java index 1c6769c27..1b60b110f 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 541534872..dd972a4b5 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 cf2c4ed37..f485b2ed0 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,13 @@ 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 ('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 ('Endpoint/' || (endpoint->>'id')) IN (SELECT reference->>'reference' FROM jsonb_array_elements(organization->'endpoint') reference)" @@ -87,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/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 c57f843a7..cfd598f24 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 110acabd6..fec272bf7 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 f2b720b55..d6c147199 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 3e97c9392..e1e5b1e35 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 47deb47dd..9df3a6fd1 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,13 @@ 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 ('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 ('Group/' || (group_json->>'id')) IN (SELECT reference->>'reference' FROM jsonb_array_elements(research_study->'enrollment') reference)" @@ -87,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/ResearchStudyPrincipalInvestigator.java b/dsf-fhir/dsf-fhir-server/src/main/java/dev/dsf/fhir/search/parameters/ResearchStudyPrincipalInvestigator.java index 5d0db56e4..4399f8f89 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 b3061123a..840ec894f 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/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/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/DaoConfig.java b/dsf-fhir/dsf-fhir-server/src/main/java/dev/dsf/fhir/spring/config/DaoConfig.java index b3f3d6fd9..e680a9351 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 @@ -73,6 +73,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; @@ -99,6 +100,9 @@ public class DaoConfig @Autowired private FhirConfig fhirConfig; + @Autowired + private JsonConfig jsonConfig; + @Bean public DataSource dataSource() { @@ -151,26 +155,28 @@ private ReadByUrlDaoFactory readByUrlDaoFactory( public ActivityDefinitionDao activityDefinitionDao() { return new ActivityDefinitionDaoJdbc(dataSource(), permanentDeleteDataSource(), fhirConfig.fhirContext(), - readByUrlDaoFactory()); + jsonConfig.objectMapper(), readByUrlDaoFactory()); } @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(), + jsonConfig.objectMapper(), (dataSourceSupplier, resourceExtractor, resourceTable, resourceColumn) -> new ReadByUrlDaoNotFoundCache<>(new ReadByUrlDaoJdbc<>(dataSourceSupplier, resourceExtractor, resourceTable, resourceColumn))); @@ -179,145 +185,162 @@ public CodeSystemDao codeSystemDao() @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(), - readByUrlDaoFactory()); + jsonConfig.objectMapper(), readByUrlDaoFactory()); } @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(), - readByUrlDaoFactory()); + jsonConfig.objectMapper(), readByUrlDaoFactory()); } @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(), - readByUrlDaoFactory()); + jsonConfig.objectMapper(), readByUrlDaoFactory()); } @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(), - readByUrlDaoFactory()); + jsonConfig.objectMapper(), readByUrlDaoFactory()); } @Bean public StructureDefinitionDao structureDefinitionSnapshotDao() { return new StructureDefinitionSnapshotDaoJdbc(dataSource(), permanentDeleteDataSource(), - fhirConfig.fhirContext(), readByUrlDaoFactory()); + fhirConfig.fhirContext(), jsonConfig.objectMapper(), readByUrlDaoFactory()); } @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(), - readByUrlDaoFactory()); + jsonConfig.objectMapper(), readByUrlDaoFactory()); } @Bean @@ -334,7 +357,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/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())) { 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/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/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..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 @@ -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); - - 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); + long dataSize = (long) binary.getUserData(RangeRequest.USER_DATA_VALUE_DATA_SIZE); - // 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()); + + 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.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,16 +389,18 @@ 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); - if (binary.getMeta() != null && binary.getMeta().getLastUpdated() != null - && binary.getMeta().getVersionId() != null) + if (binary.hasMeta()) { - b.lastModified(binary.getMeta().getLastUpdated()); - b.tag(new EntityTag(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()) @@ -393,8 +410,11 @@ private ResponseBuilder toStreamResponse(Binary binary) } 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); - 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/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/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 6f84dab8f..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 @@ -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: #761137; color: #fff; } + 33.3% { background-color: var(--color-prime); color: var(--color-background); } + 66.6% { background-color: #761137; color: #fff; } +} + +.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 6e14400ea..0d96348e3 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() { @@ -240,10 +238,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() { @@ -661,36 +657,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) { + enableSpinner() + const fullUrl = window.location.origin + window.location.pathname const requestUrl = fullUrl.slice(0, fullUrl.indexOf("/Task") + "/Task".length) - - enableSpinner() + const taskString = JSON.stringify(task) fetch(requestUrl, { method: "POST", + redirect: "manual", headers: { "Content-type": "application/json", "Accept": "application/json" }, - body: task - }).then(response => parseResponse(response, requestUrl)) + 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) { @@ -752,13 +772,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) { @@ -898,7 +919,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 = '' @@ -946,4 +967,227 @@ 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) { + 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..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 @@ -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,58 @@ window.addEventListener('DOMContentLoaded', () => { startProcess() event.preventDefault() }) + + // pending Task + 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") - 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) => { 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 +

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 +
+ + +
+
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..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,12 +218,12 @@ 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()); 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()); @@ -250,12 +250,12 @@ 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()); 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()); @@ -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, @@ -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..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() { @@ -72,7 +78,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..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() { @@ -74,7 +80,7 @@ public Organization getOrganization() } @Override - public Optional getOrganizationIdentifierValue() + public String getOrganizationIdentifierValue() { throw new UnsupportedOperationException(); } @@ -129,8 +135,8 @@ public Optional getEndpointIdentifierValue() } @Override - public Optional getPractitionerIdentifierValue() + public String getPractitionerIdentifierValue() { - return Optional.empty(); + throw new UnsupportedOperationException(); } } 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/ActivityDefinitionDaoTest.java b/dsf-fhir/dsf-fhir-server/src/test/java/dev/dsf/fhir/dao/ActivityDefinitionDaoTest.java index 5208107a0..e35dd9b87 100644 --- a/dsf-fhir/dsf-fhir-server/src/test/java/dev/dsf/fhir/dao/ActivityDefinitionDaoTest.java +++ b/dsf-fhir/dsf-fhir-server/src/test/java/dev/dsf/fhir/dao/ActivityDefinitionDaoTest.java @@ -31,8 +31,9 @@ public class ActivityDefinitionDaoTest extends AbstractReadAccessDaoTest new ActivityDefinitionDaoJdbc(dataSource, - permanentDeleteDataSource, fhirContext, ReadByUrlDaoTest.createReadByUrlDao())); + (dataSource, permanentDeleteDataSource, fhirContext, objectMapper) -> new ActivityDefinitionDaoJdbc( + dataSource, permanentDeleteDataSource, fhirContext, objectMapper, + ReadByUrlDaoTest.createReadByUrlDao())); } @Override 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/CodeSystemDaoTest.java b/dsf-fhir/dsf-fhir-server/src/test/java/dev/dsf/fhir/dao/CodeSystemDaoTest.java index 919cfc6b9..81c097fa8 100755 --- a/dsf-fhir/dsf-fhir-server/src/test/java/dev/dsf/fhir/dao/CodeSystemDaoTest.java +++ b/dsf-fhir/dsf-fhir-server/src/test/java/dev/dsf/fhir/dao/CodeSystemDaoTest.java @@ -31,8 +31,8 @@ public class CodeSystemDaoTest extends AbstractReadAccessDaoTest new CodeSystemDaoJdbc(dataSource, - permanentDeleteDataSource, fhirContext, ReadByUrlDaoTest.createReadByUrlDao())); + (dataSource, permanentDeleteDataSource, fhirContext, objectMapper) -> new CodeSystemDaoJdbc(dataSource, + permanentDeleteDataSource, fhirContext, objectMapper, ReadByUrlDaoTest.createReadByUrlDao())); } @Override 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/LibraryDaoTest.java b/dsf-fhir/dsf-fhir-server/src/test/java/dev/dsf/fhir/dao/LibraryDaoTest.java index 9a3f79d09..74db330f0 100755 --- a/dsf-fhir/dsf-fhir-server/src/test/java/dev/dsf/fhir/dao/LibraryDaoTest.java +++ b/dsf-fhir/dsf-fhir-server/src/test/java/dev/dsf/fhir/dao/LibraryDaoTest.java @@ -29,8 +29,9 @@ public class LibraryDaoTest extends AbstractReadAccessDaoTest new LibraryDaoJdbc(dataSource, - permanentDeleteDataSource, fhirContext, ReadByUrlDaoTest.createReadByUrlDao())); + super(Library.class, + (dataSource, permanentDeleteDataSource, fhirContext, objectMapper) -> new LibraryDaoJdbc(dataSource, + permanentDeleteDataSource, fhirContext, objectMapper, ReadByUrlDaoTest.createReadByUrlDao())); } @Override diff --git a/dsf-fhir/dsf-fhir-server/src/test/java/dev/dsf/fhir/dao/MeasureDaoTest.java b/dsf-fhir/dsf-fhir-server/src/test/java/dev/dsf/fhir/dao/MeasureDaoTest.java index ee5c73ac5..e34579318 100755 --- a/dsf-fhir/dsf-fhir-server/src/test/java/dev/dsf/fhir/dao/MeasureDaoTest.java +++ b/dsf-fhir/dsf-fhir-server/src/test/java/dev/dsf/fhir/dao/MeasureDaoTest.java @@ -29,8 +29,9 @@ public class MeasureDaoTest extends AbstractReadAccessDaoTest new MeasureDaoJdbc(dataSource, - permanentDeleteDataSource, fhirContext, ReadByUrlDaoTest.createReadByUrlDao())); + super(Measure.class, + (dataSource, permanentDeleteDataSource, fhirContext, objectMapper) -> new MeasureDaoJdbc(dataSource, + permanentDeleteDataSource, fhirContext, objectMapper, ReadByUrlDaoTest.createReadByUrlDao())); } @Override 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 096e5b5f2..a6946b09d 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 898a89125..f0f2a32de 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; @@ -201,7 +202,7 @@ public void testOrganizationInsertTrigger() throws Exception CodeSystem c = new CodeSystem(); new ReadAccessHelperImpl().addOrganization(c, "organization.com"); CodeSystem createdC = new CodeSystemDaoJdbc(defaultDataSource, permanentDeleteDataSource, fhirContext, - ReadByUrlDaoTest.createReadByUrlDao()).create(c); + objectMapper, ReadByUrlDaoTest.createReadByUrlDao()).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); + } + } } diff --git a/dsf-fhir/dsf-fhir-server/src/test/java/dev/dsf/fhir/dao/QuestionnaireDaoTest.java b/dsf-fhir/dsf-fhir-server/src/test/java/dev/dsf/fhir/dao/QuestionnaireDaoTest.java index a07e3812f..9b0c89e5a 100755 --- a/dsf-fhir/dsf-fhir-server/src/test/java/dev/dsf/fhir/dao/QuestionnaireDaoTest.java +++ b/dsf-fhir/dsf-fhir-server/src/test/java/dev/dsf/fhir/dao/QuestionnaireDaoTest.java @@ -31,8 +31,9 @@ public class QuestionnaireDaoTest extends AbstractReadAccessDaoTest new QuestionnaireDaoJdbc(dataSource, - permanentDeleteDataSource, fhirContext, ReadByUrlDaoTest.createReadByUrlDao())); + (dataSource, permanentDeleteDataSource, fhirContext, objectMapper) -> new QuestionnaireDaoJdbc( + dataSource, permanentDeleteDataSource, fhirContext, objectMapper, + ReadByUrlDaoTest.createReadByUrlDao())); } @Override diff --git a/dsf-fhir/dsf-fhir-server/src/test/java/dev/dsf/fhir/dao/StructureDefinitionDaoTest.java b/dsf-fhir/dsf-fhir-server/src/test/java/dev/dsf/fhir/dao/StructureDefinitionDaoTest.java index 268c7e4f3..fc7337624 100755 --- a/dsf-fhir/dsf-fhir-server/src/test/java/dev/dsf/fhir/dao/StructureDefinitionDaoTest.java +++ b/dsf-fhir/dsf-fhir-server/src/test/java/dev/dsf/fhir/dao/StructureDefinitionDaoTest.java @@ -31,8 +31,9 @@ public class StructureDefinitionDaoTest extends AbstractReadAccessDaoTest new StructureDefinitionDaoJdbc(dataSource, - permanentDeleteDataSource, fhirContext, ReadByUrlDaoTest.createReadByUrlDao())); + (dataSource, permanentDeleteDataSource, fhirContext, objectMapper) -> new StructureDefinitionDaoJdbc( + dataSource, permanentDeleteDataSource, fhirContext, objectMapper, + ReadByUrlDaoTest.createReadByUrlDao())); } @Override diff --git a/dsf-fhir/dsf-fhir-server/src/test/java/dev/dsf/fhir/dao/StructureDefinitionSnapshotDaoTest.java b/dsf-fhir/dsf-fhir-server/src/test/java/dev/dsf/fhir/dao/StructureDefinitionSnapshotDaoTest.java index cf476b014..5488dbce7 100755 --- a/dsf-fhir/dsf-fhir-server/src/test/java/dev/dsf/fhir/dao/StructureDefinitionSnapshotDaoTest.java +++ b/dsf-fhir/dsf-fhir-server/src/test/java/dev/dsf/fhir/dao/StructureDefinitionSnapshotDaoTest.java @@ -32,8 +32,9 @@ public class StructureDefinitionSnapshotDaoTest public StructureDefinitionSnapshotDaoTest() { super(StructureDefinition.class, - (dataSource, permanentDeleteDataSource, fhirContext) -> new StructureDefinitionSnapshotDaoJdbc( - dataSource, permanentDeleteDataSource, fhirContext, ReadByUrlDaoTest.createReadByUrlDao())); + (dataSource, permanentDeleteDataSource, fhirContext, + objectMapper) -> new StructureDefinitionSnapshotDaoJdbc(dataSource, permanentDeleteDataSource, + fhirContext, objectMapper, ReadByUrlDaoTest.createReadByUrlDao())); } @Override 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); diff --git a/dsf-fhir/dsf-fhir-server/src/test/java/dev/dsf/fhir/dao/ValueSetDaoTest.java b/dsf-fhir/dsf-fhir-server/src/test/java/dev/dsf/fhir/dao/ValueSetDaoTest.java index d22bf1b01..8b60794ff 100755 --- a/dsf-fhir/dsf-fhir-server/src/test/java/dev/dsf/fhir/dao/ValueSetDaoTest.java +++ b/dsf-fhir/dsf-fhir-server/src/test/java/dev/dsf/fhir/dao/ValueSetDaoTest.java @@ -30,8 +30,9 @@ public class ValueSetDaoTest extends AbstractReadAccessDaoTest new ValueSetDaoJdbc(dataSource, - permanentDeleteDataSource, fhirContext, ReadByUrlDaoTest.createReadByUrlDao())); + super(ValueSet.class, + (dataSource, permanentDeleteDataSource, fhirContext, objectMapper) -> new ValueSetDaoJdbc(dataSource, + permanentDeleteDataSource, fhirContext, objectMapper, ReadByUrlDaoTest.createReadByUrlDao())); } @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 2ff170308..1af4169be 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 { @@ -386,6 +389,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");