Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Check Availability of gICS and gPAS During Startup #630

Open
wants to merge 2 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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 @@
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")))

Check warning on line 53 in trust-center-agent/src/main/java/care/smith/fts/tca/consent/configuration/GicsFhirConfiguration.java

View check run for this annotation

Codecov / codecov/patch

trust-center-agent/src/main/java/care/smith/fts/tca/consent/configuration/GicsFhirConfiguration.java#L51-L53

Added lines #L51 - L53 were not covered by tests
.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 @@
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")))

Check warning on line 51 in trust-center-agent/src/main/java/care/smith/fts/tca/deidentification/configuration/GpasFhirDeIdentificationConfiguration.java

View check run for this annotation

Codecov / codecov/patch

trust-center-agent/src/main/java/care/smith/fts/tca/deidentification/configuration/GpasFhirDeIdentificationConfiguration.java#L50-L51

Added lines #L50 - L51 were not covered by tests
.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