-
Notifications
You must be signed in to change notification settings - Fork 5
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- append "flare" to method name "DirectSpringConfig.directWebClientFlare" to clarify that it is only associated with the flare client - replace custom "internal/json" media type from mock server response in DirectBrokerClientFlareIT and replace it with standard application/json header - replace HashMap with EnumMap in DirectBrokerClient.DirectQuery - rename SITE_1_NAME to SITE_NAME_LOCAL to be consistent with SITE_ID_LOCAL (and make both constants final) - make the fhirClient a local var in the setup method in DirectBrokerClientCqlIT - don't return anything from DirectBrokerClientCqlIT.createDummyPatient - catch and wrap BaseServerResponseException when trying to read measure report from fhir server - catch BaseServerResponseException instead of just FhirClientConnectionException when creating library and measure - throw SiteNotFoundException instead of returning an empty string when the site name for an invalid id is requested - add constructor for QueryDefinitionNotFoundException without QueryMediaType parameter to be thrown in DSFQueryManager, and change the queryMediaType Parameter from String to the respective enum - move Measure.json and Library.json (as well as the 2 cql "query" files) to the direct broker package and use getResourceAsStream - remove backendquery mapping from DirectBrokerClient and move the backendquery id directly to DirectBrokerClient.DirectQuery - extract fhir connection from DirectBrokerClientCql to separate class FhirConnector
- Loading branch information
1 parent
ccdb060
commit 89f0742
Showing
17 changed files
with
373 additions
and
203 deletions.
There are no files selected for viewing
9 changes: 8 additions & 1 deletion
9
...va/de/numcodex/feasibility_gui_backend/query/broker/QueryDefinitionNotFoundException.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
149 changes: 13 additions & 136 deletions
149
...n/java/de/numcodex/feasibility_gui_backend/query/broker/direct/DirectBrokerClientCql.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,184 +1,61 @@ | ||
package de.numcodex.feasibility_gui_backend.query.broker.direct; | ||
|
||
import ca.uhn.fhir.context.FhirContext; | ||
import ca.uhn.fhir.rest.client.api.IGenericClient; | ||
import ca.uhn.fhir.rest.client.exceptions.FhirClientConnectionException; | ||
import ca.uhn.fhir.rest.param.DateParam; | ||
import ca.uhn.fhir.rest.param.StringParam; | ||
import de.numcodex.feasibility_gui_backend.query.broker.BrokerClient; | ||
import de.numcodex.feasibility_gui_backend.query.broker.QueryDefinitionNotFoundException; | ||
import de.numcodex.feasibility_gui_backend.query.collect.QueryStatus; | ||
import de.numcodex.feasibility_gui_backend.query.broker.QueryNotFoundException; | ||
import java.io.InputStream; | ||
import lombok.extern.slf4j.Slf4j; | ||
import org.hl7.fhir.instance.model.api.IBaseResource; | ||
import org.hl7.fhir.r4.model.Bundle; | ||
import org.hl7.fhir.r4.model.Library; | ||
import org.hl7.fhir.r4.model.Measure; | ||
import org.hl7.fhir.r4.model.MeasureReport; | ||
import org.hl7.fhir.r4.model.Parameters; | ||
|
||
import java.io.IOException; | ||
import java.util.*; | ||
|
||
import static de.numcodex.feasibility_gui_backend.query.QueryMediaType.CQL; | ||
import static de.numcodex.feasibility_gui_backend.query.collect.QueryStatus.COMPLETED; | ||
import static de.numcodex.feasibility_gui_backend.query.collect.QueryStatus.FAILED; | ||
import static java.nio.charset.StandardCharsets.UTF_8; | ||
import static org.hl7.fhir.r4.model.Bundle.BundleType.TRANSACTION; | ||
import static org.hl7.fhir.r4.model.Bundle.HTTPVerb.POST; | ||
|
||
/** | ||
* A {@link BrokerClient} to be used to directly communicate with a CQL-capable FHIR Server instance | ||
* without the need for using any middleware (Aktin or DSF). | ||
*/ | ||
@Slf4j | ||
public class DirectBrokerClientCql extends DirectBrokerClient { | ||
private final FhirContext fhirContext; | ||
private final IGenericClient fhirClient; | ||
private final FhirConnector fhirConnector; | ||
|
||
/** | ||
* Creates a new {@link DirectBrokerClientCql} instance that uses the given web client to | ||
* communicate with a CQL capable FHIR server instance. | ||
* | ||
* @param fhirContext A FHIR context. | ||
* @param fhirClient A FHIR client, configured with the correct url | ||
* @param fhirConnector A FHIR connector. | ||
*/ | ||
public DirectBrokerClientCql(FhirContext fhirContext, IGenericClient fhirClient) { | ||
this.fhirContext = Objects.requireNonNull(fhirContext); | ||
this.fhirClient = Objects.requireNonNull(fhirClient); | ||
public DirectBrokerClientCql(FhirConnector fhirConnector) { | ||
this.fhirConnector = Objects.requireNonNull(fhirConnector); | ||
listeners = new ArrayList<>(); | ||
brokerQueries = new HashMap<>(); | ||
brokerToBackendQueryIdMapping = new HashMap<>(); | ||
} | ||
|
||
@Override | ||
public void publishQuery(String brokerQueryId) | ||
throws QueryNotFoundException, IOException, QueryDefinitionNotFoundException { | ||
var query = findQuery(brokerQueryId); | ||
var queryContent = Optional.ofNullable(query.getQueryDefinition(CQL)) | ||
.orElseThrow(() -> new QueryDefinitionNotFoundException(query.getQueryId(), | ||
CQL.getRepresentation()) | ||
); | ||
var queryContent = query.getQueryDefinition(CQL); | ||
|
||
updateQueryStatus(query, QueryStatus.EXECUTING); | ||
String measureUri; | ||
var libraryUri = "urn:uuid" + UUID.randomUUID(); | ||
var measureUri = "urn:uuid" + UUID.randomUUID(); | ||
MeasureReport measureReport; | ||
try { | ||
measureUri = createMeasureAndLibrary(queryContent); | ||
Bundle bundle = fhirConnector.createBundle(queryContent, libraryUri, measureUri); | ||
fhirConnector.transmitBundle(bundle); | ||
measureReport = fhirConnector.evaluateMeasure(measureUri); | ||
} catch (IOException e) { | ||
updateQueryStatus(query, FAILED); | ||
throw new IOException( | ||
"An error occurred while publishing the query with ID: " + query.getQueryId() | ||
+ " while trying to create measure and library", e); | ||
throw (e); | ||
} | ||
var measureReport = evaluateMeasure(measureUri); | ||
|
||
var resultCount = measureReport.getGroupFirstRep().getPopulationFirstRep().getCount(); | ||
query.setResult(obfuscateResultCount ? obfuscate(resultCount) : resultCount); | ||
updateQueryStatus(query, COMPLETED); | ||
} | ||
|
||
/** | ||
* Create FHIR {@link Measure} and {@link Library} resources and transmit them in a bundled transaction. | ||
* @param cql the plaintext cql definition | ||
* @return the randomly generated identifier of the {@link Measure} resource | ||
*/ | ||
private String createMeasureAndLibrary(String cql) throws IOException { | ||
var libraryUri = "urn:uuid" + UUID.randomUUID(); | ||
var library = appendCql(parseResource(Library.class, | ||
getResourceFileAsString("query/cql/Library.json")).setUrl(libraryUri), cql); | ||
var measureUri = "urn:uuid" + UUID.randomUUID(); | ||
var measure = parseResource(Measure.class, | ||
getResourceFileAsString("query/cql/Measure.json")) | ||
.setUrl(measureUri) | ||
.addLibrary(libraryUri); | ||
var bundle = createBundle(library, measure); | ||
|
||
try { | ||
fhirClient.transaction().withBundle(bundle).execute(); | ||
} catch (FhirClientConnectionException e) { | ||
throw new IOException(e); | ||
} | ||
|
||
return measureUri; | ||
} | ||
|
||
/** | ||
* Get the {@link MeasureReport} for a previously transmitted {@link Measure} | ||
* @param measureUri the identifier of the {@link Measure} | ||
* @return the retrieved {@link MeasureReport} from the server | ||
*/ | ||
private MeasureReport evaluateMeasure(String measureUri) { | ||
return fhirClient.operation() | ||
.onType(Measure.class) | ||
.named("evaluate-measure") | ||
.withSearchParameter(Parameters.class, "measure", new StringParam(measureUri)) | ||
.andSearchParameter("periodStart", new DateParam("1900")) | ||
.andSearchParameter("periodEnd", new DateParam("2100")) | ||
.useHttpGet() | ||
.returnResourceType(MeasureReport.class) | ||
.execute(); | ||
} | ||
|
||
/** | ||
* Read file contents as String | ||
* @param fileName name of the resource file | ||
* @return the String contents of the file | ||
*/ | ||
public static String getResourceFileAsString(String fileName) throws IOException { | ||
InputStream is = getResourceFileAsInputStream(fileName); | ||
if (is != null) { | ||
return new String(is.readAllBytes(), UTF_8); | ||
} else { | ||
throw new RuntimeException("File not found in classpath: " + fileName); | ||
} | ||
} | ||
|
||
/** | ||
* Read file contents as {@link InputStream} | ||
* @param fileName name of the resource file | ||
* @return an {@link InputStream} of the file | ||
*/ | ||
public static InputStream getResourceFileAsInputStream(String fileName) { | ||
ClassLoader classLoader = DirectBrokerClientCql.class.getClassLoader(); | ||
return classLoader.getResourceAsStream(fileName); | ||
} | ||
|
||
/** | ||
* Parse a String as an {@link IBaseResource} implementation | ||
* @param type the concrete {@link IBaseResource} implementation class to parse to | ||
* @param input the {@link String} to parse | ||
* @return the wanted {@link IBaseResource} implementation object | ||
* @param <T> any implementation of {@link IBaseResource} | ||
*/ | ||
private <T extends IBaseResource> T parseResource(Class<T> type, String input) { | ||
var parser = fhirContext.newJsonParser(); | ||
return type.cast(parser.parseResource(input)); | ||
} | ||
|
||
/** | ||
* Add the CQL query to a {@link Library} | ||
* @param library the {@link Library} to add the CQL string to | ||
* @param cql the CQL string to add | ||
* @return the {@link Library} with the added CQL | ||
*/ | ||
private Library appendCql(Library library, String cql) { | ||
library.getContentFirstRep().setContentType(CQL.getRepresentation()); | ||
library.getContentFirstRep().setData(cql.getBytes(UTF_8)); | ||
return library; | ||
} | ||
|
||
/** | ||
* Create a {@link Bundle} of a {@link Library} and a {@link Measure} | ||
* @param library the {@link Library} to add to the {@link Bundle} | ||
* @param measure the {@link Measure} to add to the {@link Bundle} | ||
* @return the {@link Bundle}, consisting of the given {@link Library} and {@link Measure} | ||
*/ | ||
private static Bundle createBundle(Library library, Measure measure) { | ||
var bundle = new Bundle(); | ||
bundle.setType(TRANSACTION); | ||
bundle.addEntry().setResource(library).getRequest().setMethod(POST).setUrl("Library"); | ||
bundle.addEntry().setResource(measure).getRequest().setMethod(POST).setUrl("Measure"); | ||
return bundle; | ||
} | ||
|
||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.