Skip to content

Commit

Permalink
Check Availability of gICS and gPAS During Startup
Browse files Browse the repository at this point in the history
  • Loading branch information
trobanga committed Feb 28, 2025
1 parent b46e1ec commit 717f5bb
Show file tree
Hide file tree
Showing 29 changed files with 624 additions and 157 deletions.
2 changes: 1 addition & 1 deletion .github/test/tc-agent/compose.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ services:
depends_on:
keystore:
condition: service_healthy
gics: # CI_ONLY
gics: # CI_ONLY
condition: service_healthy # CI_ONLY
gpas: # CI_ONLY
condition: service_healthy # CI_ONLY
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,16 +2,16 @@

import static care.smith.fts.util.MediaTypes.APPLICATION_FHIR_JSON;
import static care.smith.fts.util.RetryStrategies.defaultRetryStrategy;
import static care.smith.fts.util.error.FhirErrorResponseUtil.operationOutcomeWithIssue;
import static java.util.Map.entry;
import static java.util.Optional.ofNullable;
import static org.springframework.http.HttpStatus.BAD_REQUEST;
import static org.springframework.http.MediaType.APPLICATION_JSON;

import care.smith.fts.api.ConsentedPatient;
import care.smith.fts.api.cda.CohortSelector;
import care.smith.fts.util.ConsentedPatientExtractor;
import care.smith.fts.util.error.FhirErrorResponseUtil;
import care.smith.fts.util.error.TransferProcessException;
import care.smith.fts.util.error.fhir.FhirException;
import io.micrometer.core.instrument.MeterRegistry;
import jakarta.validation.constraints.NotNull;
import java.time.Duration;
Expand All @@ -22,7 +22,6 @@
import org.hl7.fhir.r4.model.Bundle;
import org.hl7.fhir.r4.model.Bundle.BundleLinkComponent;
import org.hl7.fhir.r4.model.OperationOutcome;
import org.springframework.http.HttpStatus;
import org.springframework.web.reactive.function.client.ClientResponse;
import org.springframework.web.reactive.function.client.WebClient;
import org.springframework.web.reactive.function.client.WebClientException;
Expand Down Expand Up @@ -74,7 +73,7 @@ private Mono<Bundle> fetchBundle(String uri, List<String> pids) {
.headers(h -> h.setContentType(APPLICATION_JSON))
.headers(h -> h.setAccept(List.of(APPLICATION_FHIR_JSON)))
.retrieve()
.onStatus(r -> r.equals(HttpStatus.BAD_REQUEST), TCACohortSelector::handleBadRequest)
.onStatus(r -> r.equals(BAD_REQUEST), TCACohortSelector::handleBadRequest)
.bodyToMono(Bundle.class)
.retryWhen(defaultRetryStrategy(meterRegistry, "fetchBundle"));
}
Expand Down Expand Up @@ -131,7 +130,8 @@ private static Mono<Throwable> handleBadRequest(ClientResponse r) {
.onErrorResume(
e -> {
log.error("Failed to parse error response", e);
return Mono.just(operationOutcomeWithIssue(e));
return Mono.just(
new FhirException(BAD_REQUEST, e.getMessage()).getOperationOutcome());
})
.flatMap(
outcome ->
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
import static care.smith.fts.test.MockServerUtil.fhirResponse;
import static care.smith.fts.test.MockServerUtil.jsonResponse;
import static care.smith.fts.util.FhirUtils.toBundle;
import static care.smith.fts.util.error.FhirErrorResponseUtil.operationOutcomeWithIssue;
import static care.smith.fts.util.error.fhir.FhirErrorResponseUtil.operationOutcomeWithIssue;
import static com.github.tomakehurst.wiremock.client.WireMock.equalTo;
import static com.github.tomakehurst.wiremock.client.WireMock.ok;
import static com.github.tomakehurst.wiremock.client.WireMock.post;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,25 +3,33 @@
import static care.smith.fts.tca.consent.GicsFhirUtil.filterOuterBundle;
import static care.smith.fts.util.MediaTypes.APPLICATION_FHIR_JSON;
import static care.smith.fts.util.RetryStrategies.defaultRetryStrategy;
import static org.springframework.http.HttpStatus.BAD_REQUEST;
import static org.springframework.http.HttpStatus.INTERNAL_SERVER_ERROR;
import static org.springframework.http.HttpStatus.NOT_FOUND;
import static org.springframework.http.MediaType.APPLICATION_JSON;

import care.smith.fts.util.error.UnknownDomainException;
import care.smith.fts.util.error.fhir.FhirException;
import care.smith.fts.util.error.fhir.FhirUnknownDomainException;
import care.smith.fts.util.error.fhir.NoFhirServerException;
import care.smith.fts.util.tca.ConsentFetchAllRequest;
import care.smith.fts.util.tca.ConsentFetchRequest;
import care.smith.fts.util.tca.ConsentRequest;
import io.micrometer.core.instrument.MeterRegistry;
import java.util.List;
import java.util.Set;
import lombok.extern.slf4j.Slf4j;
import org.hl7.fhir.r4.model.*;
import org.springframework.http.HttpStatus;
import org.springframework.http.HttpStatusCode;
import org.springframework.web.reactive.function.client.ClientResponse;
import org.springframework.web.reactive.function.client.WebClient;
import org.springframework.web.reactive.function.client.WebClientRequestException;
import org.springframework.web.reactive.function.client.WebClientResponseException;
import org.springframework.web.util.UriComponentsBuilder;
import reactor.core.publisher.Mono;

/** This class provides functionalities for handling FHIR consents using an HTTP client. */
@Slf4j
public class FhirConsentedPatientsProvider implements ConsentedPatientsProvider {
public class GicsFhirConsentedPatientsProvider implements ConsentedPatientsProvider {
private final WebClient gicsClient;
private final MeterRegistry meterRegistry;

Expand All @@ -30,7 +38,7 @@ public class FhirConsentedPatientsProvider implements ConsentedPatientsProvider
*
* @param gicsClient the WebClient used for HTTP requests
*/
public FhirConsentedPatientsProvider(WebClient gicsClient, MeterRegistry meterRegistry) {
public GicsFhirConsentedPatientsProvider(WebClient gicsClient, MeterRegistry meterRegistry) {
this.gicsClient = gicsClient;
this.meterRegistry = meterRegistry;
}
Expand Down Expand Up @@ -76,30 +84,48 @@ private <C extends ConsentRequest> Mono<Bundle> doFetch(
.headers(h -> h.setContentType(APPLICATION_FHIR_JSON))
.headers(h -> h.setAccept(List.of(APPLICATION_FHIR_JSON, APPLICATION_JSON)))
.retrieve()
.onStatus(
r -> r.equals(HttpStatus.NOT_FOUND), FhirConsentedPatientsProvider::handleGicsNotFound)
.onStatus(HttpStatusCode::is4xxClientError, this::handle4xxError)
.bodyToMono(Bundle.class)
.doOnNext(b -> log.trace("body(n: {})", b.getEntry().size()))
.retryWhen(defaultRetryStrategy(meterRegistry, helper.requestName()))
.onErrorResume(
e -> {
if (e.getCause() instanceof WebClientRequestException) {
return Mono.error(new NoFhirServerException("No connection to gICS server"));
} else if (e.getCause() instanceof WebClientResponseException) {
return Mono.error(new FhirException(INTERNAL_SERVER_ERROR, e.getMessage()));
} else {
return Mono.error(e);
}
})
.doOnError(b -> log.error("Unable to fetch consent from gICS", b))
.map(outerBundle -> filterOuterBundle(req.policySystem(), req.policies(), outerBundle))
.map(bundle -> helper.processResponse(bundle, req, requestUrl, paging));
}

private static Mono<Throwable> handleGicsNotFound(ClientResponse r) {
private Mono<Throwable> handle4xxError(ClientResponse r) {
log.trace("response headers: {}", r.headers().asHttpHeaders());
return r.bodyToMono(OperationOutcome.class)
.doOnNext(re -> log.info("{}", re))
.flatMap(
b -> {
log.info("issue: {}", b.getIssueFirstRep());
var diagnostics = b.getIssueFirstRep().getDiagnostics();
log.error(diagnostics);
if (diagnostics != null && diagnostics.startsWith("No consents found for domain")) {
return Mono.error(new UnknownDomainException(diagnostics));
} else {
return Mono.error(new IllegalArgumentException());
}
});
if (Set.of(400, 401, 404, 422).contains(r.statusCode().value())) {
log.debug("Status code: {}", r.statusCode().value());
return r.bodyToMono(OperationOutcome.class)
.onErrorResume(
e -> {
log.error("Cannot parse OperationOutcome expected from gICS", e);
return Mono.error(
new NoFhirServerException("Cannot parse OperationOutcome received from gICS"));
})
.flatMap(
b -> {
var diagnostics = b.getIssueFirstRep().getDiagnostics();
log.error(diagnostics);
if (r.statusCode() == NOT_FOUND) {
return Mono.error(new FhirUnknownDomainException(b));
} else {
return Mono.error(new FhirException(BAD_REQUEST, b));
}
});
} else {
return Mono.error(new NoFhirServerException("Unexpected error connecting to gICS"));
}
}
}
Original file line number Diff line number Diff line change
@@ -1,17 +1,27 @@
package care.smith.fts.tca.consent.configuration;

import care.smith.fts.tca.consent.FhirConsentedPatientsProvider;
import static care.smith.fts.util.FhirClientUtils.fetchCapabilityStatement;
import static care.smith.fts.util.FhirClientUtils.verifyOperationsExist;

import care.smith.fts.tca.consent.GicsFhirConsentedPatientsProvider;
import care.smith.fts.util.HttpClientConfig;
import care.smith.fts.util.WebClientFactory;
import care.smith.fts.util.auth.HttpClientAuth;
import care.smith.fts.util.error.fhir.FhirConnectException;
import io.micrometer.core.instrument.MeterRegistry;
import jakarta.validation.constraints.NotBlank;
import java.util.List;
import lombok.Data;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.boot.ApplicationRunner;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.reactive.function.client.WebClient;
import reactor.core.publisher.Mono;

@Slf4j
@Configuration
@ConfigurationProperties(prefix = "consent.gics.fhir")
@Data
Expand All @@ -31,11 +41,32 @@ public WebClient gicsClient(WebClientFactory clientFactory) {
return clientFactory.create(config);
}

@Bean("gicsApplicationRunner")
ApplicationRunner runner(@Qualifier("gicsFhirHttpClient") WebClient gicsClient) {
return args ->
fetchCapabilityStatement(gicsClient)
.flatMap(
c ->
verifyOperationsExist(
c, List.of("allConsentsForDomain", "allConsentsForPerson"))
? Mono.just(c)
: Mono.error(new FhirConnectException("Server is missing capabilities")))
.doOnNext(i -> log.info("gCIS available"))
.doOnError(
e -> {
log.warn(
"Connection to gICS could not be established on agent startup. The agent will continue startup anyway, in case gICS connection will be available later on.");
log.debug("", e);
})
.onErrorComplete()
.block();
}

@Bean
FhirConsentedPatientsProvider fhirConsentedPatientsProvider(
GicsFhirConsentedPatientsProvider fhirConsentedPatientsProvider(
WebClientFactory clientFactory, MeterRegistry meterRegistry) {
var config = new HttpClientConfig(baseUrl, auth);
var client = clientFactory.create(config);
return new FhirConsentedPatientsProvider(client, meterRegistry);
return new GicsFhirConsentedPatientsProvider(client, meterRegistry);
}
}
Original file line number Diff line number Diff line change
@@ -1,15 +1,17 @@
package care.smith.fts.tca.deidentification;

import static org.springframework.http.HttpStatus.OK;

import care.smith.fts.util.MediaTypes;
import care.smith.fts.util.RetryStrategies;
import care.smith.fts.util.error.UnknownDomainException;
import care.smith.fts.util.error.fhir.FhirUnknownDomainException;
import care.smith.fts.util.error.fhir.NoFhirServerException;
import io.micrometer.core.instrument.MeterRegistry;
import java.util.List;
import java.util.Map;
import lombok.extern.slf4j.Slf4j;
import org.hl7.fhir.r4.model.OperationOutcome;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.http.HttpStatus;
import org.springframework.stereotype.Component;
import org.springframework.web.reactive.function.client.ClientResponse;
import org.springframework.web.reactive.function.client.WebClient;
Expand Down Expand Up @@ -49,7 +51,7 @@ public Mono<String> fetchOrCreatePseudonyms(String domain, String id) {
.bodyValue(params)
.headers(h -> h.setAccept(List.of(MediaTypes.APPLICATION_FHIR_JSON)))
.retrieve()
.onStatus(r1 -> r1.equals(HttpStatus.BAD_REQUEST), GpasClient::handleGpasBadRequest)
.onStatus(r -> !r.equals(OK), this::handleError)
.bodyToMono(GpasParameterResponse.class)
.retryWhen(
RetryStrategies.defaultRetryStrategy(meterRegistry, "fetchOrCreatePseudonymsOnGpas"))
Expand All @@ -59,14 +61,20 @@ public Mono<String> fetchOrCreatePseudonyms(String domain, String id) {
.map(map -> map.get(id));
}

private static Mono<Throwable> handleGpasBadRequest(ClientResponse r) {
private Mono<Throwable> handleError(ClientResponse r) {
return r.bodyToMono(OperationOutcome.class)
.onErrorResume(
e -> {
log.error("Cannot parse OperationOutcome expected from gPAS", e);
return Mono.error(
new NoFhirServerException("Cannot parse OperationOutcome received from gPAS"));
})
.flatMap(
b -> {
var diagnostics = b.getIssueFirstRep().getDiagnostics();
log.error("Bad Request: {}", diagnostics);
if (diagnostics != null && diagnostics.startsWith("Unknown domain")) {
return Mono.error(new UnknownDomainException(diagnostics));
if (diagnostics.startsWith("Unknown domain")) {
return Mono.error(new FhirUnknownDomainException(b));
} else {
return Mono.error(new IllegalArgumentException(diagnostics));
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,18 +1,28 @@
package care.smith.fts.tca.deidentification.configuration;

import static care.smith.fts.util.FhirClientUtils.fetchCapabilityStatement;
import static care.smith.fts.util.FhirClientUtils.verifyOperationsExist;

import care.smith.fts.util.HttpClientConfig;
import care.smith.fts.util.WebClientFactory;
import care.smith.fts.util.auth.HttpClientAuth;
import care.smith.fts.util.error.fhir.FhirConnectException;
import com.fasterxml.jackson.databind.ObjectMapper;
import jakarta.validation.constraints.NotBlank;
import java.security.SecureRandom;
import java.util.List;
import java.util.random.RandomGenerator;
import lombok.Data;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.boot.ApplicationRunner;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.reactive.function.client.WebClient;
import reactor.core.publisher.Mono;

@Slf4j
@Configuration
@ConfigurationProperties(prefix = "de-identification.gpas.fhir")
@Data
Expand All @@ -30,6 +40,26 @@ public WebClient gpasClient(WebClientFactory clientFactory) {
return clientFactory.create(new HttpClientConfig(baseUrl, auth));
}

@Bean("gpasApplicationRunner")
ApplicationRunner runner(@Qualifier("gpasFhirHttpClient") WebClient gpasClient) {
return args ->
fetchCapabilityStatement(gpasClient)
.flatMap(
c ->
verifyOperationsExist(c, List.of("pseudonymizeAllowCreate"))
? Mono.just(c)
: Mono.error(new FhirConnectException("Server is missing capabilities")))
.doOnNext(i -> log.info("gPAS available"))
.doOnError(
e -> {
log.warn(
"Connection to gPAS could not be established on agent startup. The agent will continue startup anyway, in case gPAS connection will be available later on.");
log.debug("", e);
})
.onErrorComplete()
.block();
}

@Bean
public RandomGenerator secureRandom() {
return new SecureRandom();
Expand Down
Original file line number Diff line number Diff line change
@@ -1,11 +1,13 @@
package care.smith.fts.tca.rest;

import static care.smith.fts.util.error.fhir.FhirErrorResponseUtil.fromFhirException;
import static care.smith.fts.util.error.fhir.FhirErrorResponseUtil.internalServerError;

import care.smith.fts.tca.consent.ConsentedPatientsProvider;
import care.smith.fts.tca.consent.ConsentedPatientsProvider.PagingParams;
import care.smith.fts.util.MediaTypes;
import care.smith.fts.util.error.ErrorResponseUtil;
import care.smith.fts.util.error.FhirErrorResponseUtil;
import care.smith.fts.util.error.UnknownDomainException;
import care.smith.fts.util.error.fhir.FhirErrorResponseUtil;
import care.smith.fts.util.error.fhir.FhirException;
import care.smith.fts.util.tca.ConsentFetchAllRequest;
import care.smith.fts.util.tca.ConsentFetchRequest;
import io.swagger.v3.oas.annotations.Operation;
Expand Down Expand Up @@ -123,10 +125,10 @@ public Mono<ResponseEntity<Bundle>> fetch(
}

private static Mono<ResponseEntity<Bundle>> errorResponse(Throwable e) {
if (e instanceof UnknownDomainException) {
return FhirErrorResponseUtil.badRequest(e);
if (e instanceof FhirException) {
return fromFhirException((FhirException) e);
} else {
return FhirErrorResponseUtil.internalServerError(e);
return internalServerError(e);
}
}
}
Original file line number Diff line number Diff line change
@@ -1,8 +1,12 @@
package care.smith.fts.tca.rest;

import static care.smith.fts.util.error.fhir.FhirErrorResponseUtil.fromFhirException;
import static care.smith.fts.util.error.fhir.FhirErrorResponseUtil.internalServerError;

import care.smith.fts.tca.deidentification.MappingProvider;
import care.smith.fts.util.error.ErrorResponseUtil;
import care.smith.fts.util.error.UnknownDomainException;
import care.smith.fts.util.error.fhir.FhirException;
import care.smith.fts.util.tca.*;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter;
Expand All @@ -13,6 +17,7 @@
import jakarta.validation.constraints.NotNull;
import jakarta.validation.constraints.Pattern;
import lombok.extern.slf4j.Slf4j;
import org.hl7.fhir.r4.model.Bundle;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
Expand Down
Loading

0 comments on commit 717f5bb

Please sign in to comment.