Skip to content
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 @@ -11,6 +11,7 @@
import de.medizininformatikinitiative.torch.consent.ConsentValidator;
import de.medizininformatikinitiative.torch.cql.CqlClient;
import de.medizininformatikinitiative.torch.cql.FhirHelper;
import de.medizininformatikinitiative.torch.exceptions.ValidationException;
import de.medizininformatikinitiative.torch.management.CompartmentManager;
import de.medizininformatikinitiative.torch.management.ProcessedGroupFactory;
import de.medizininformatikinitiative.torch.management.StructureDefinitionHandler;
Expand Down Expand Up @@ -257,7 +258,7 @@ public ObjectMapper objectMapper() {
}

@Bean
public ConsentCodeMapper consentCodeMapper(ObjectMapper objectMapper) throws IOException {
public ConsentCodeMapper consentCodeMapper(ObjectMapper objectMapper) throws IOException, ValidationException {
return new ConsentCodeMapper(torchProperties.mapping().consent(), objectMapper);
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,35 +2,52 @@

import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import de.medizininformatikinitiative.torch.exceptions.ValidationException;
import de.medizininformatikinitiative.torch.model.mapping.ConsentKey;

import java.io.File;
import java.io.IOException;
import java.util.*;
import java.util.ArrayList;
import java.util.Collections;
import java.util.EnumMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Set;


/**
* Provides a Map of all consent codes belonging to a consent key e.g. "yes-yes-yes-yes"
* Provides a Map of all consent codes belonging to a consent key e.g. "yes-yes-yes-yes" as
* defined in the mapping file.
*/
public class ConsentCodeMapper {

private final Map<String, List<String>> consentMap;
private final Map<ConsentKey, List<String>> consentMap;
private final ObjectMapper objectMapper;


public ConsentCodeMapper(String consentFilePath, ObjectMapper objectMapper) throws IOException {
this.consentMap = new HashMap<>();
/**
* @param consentFilePath Path to the mapping file e.g. consent-mappings_fhir.json
* @param objectMapper Objectmapper for Json Processing
* @throws IOException
* @throws ValidationException
*/
public ConsentCodeMapper(String consentFilePath, ObjectMapper objectMapper) throws IOException, ValidationException {
this.consentMap = new EnumMap<>(ConsentKey.class);
Objects.requireNonNull(objectMapper);
this.objectMapper = objectMapper;
buildConsentMap(consentFilePath);
}

// Method to build the map based on the JSON file
private void buildConsentMap(String filePath) throws IOException {
//Class get Resource as Stream
//init method
private void buildConsentMap(String filePath) throws IOException, ValidationException {
File file = new File(filePath);
JsonNode consentMappingData = objectMapper.readTree(file.getAbsoluteFile());
for (JsonNode consent : consentMappingData) {
String keyCode = consent.get("key").get("code").asText();

ConsentKey keyCode = ConsentKey.fromString(consent.get("key").get("code").asText());
List<String> relevantCodes = new ArrayList<>();

JsonNode fixedCriteria = consent.get("fixedCriteria");
Expand All @@ -43,12 +60,23 @@ private void buildConsentMap(String filePath) throws IOException {
}
}
}

consentMap.put(keyCode, relevantCodes);
}
if (consentMap.size() != ConsentKey.values().length) {
throw new ValidationException("Consent map size does not match ConsentKey enum size");
}
}

public Set<String> getRelevantCodes(String key) {
/**
* @param key Consentkey to be handled
* @return All codes associated with that key
* e.g. for "no-no-no-no" in the current version of the codesystem
* "urn:oid:2.16.840.1.113883.3.1937.777.24.5.3" the codes
* "2.16.840.1.113883.3.1937.777.24.5.3.47"
* "2.16.840.1.113883.3.1937.777.24.5.3.49"
* "2.16.840.1.113883.3.1937.777.24.5.3.9" will be returned
*/
public Set<String> getRelevantCodes(ConsentKey key) {
return new HashSet<>(consentMap.getOrDefault(key, Collections.emptyList()));
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
import de.medizininformatikinitiative.torch.model.fhir.Query;
import de.medizininformatikinitiative.torch.model.management.PatientBatch;
import de.medizininformatikinitiative.torch.model.management.PatientResourceBundle;
import de.medizininformatikinitiative.torch.model.mapping.ConsentKey;
import de.medizininformatikinitiative.torch.service.DataStore;
import de.medizininformatikinitiative.torch.util.ResourceUtils;
import org.hl7.fhir.r4.model.Consent;
Expand Down Expand Up @@ -98,7 +99,7 @@ private static Map<String, Provisions> mergeAllProvisions(Map<String, Collection
* @param batch A list of patient IDs to process in this batch.
* @return A {@link Flux} emitting maps containing consent information structured by patient ID and consent codes.
*/
public Mono<PatientBatchWithConsent> buildConsentInfo(String key, PatientBatch batch) {
public Mono<PatientBatchWithConsent> buildConsentInfo(ConsentKey key, PatientBatch batch) {
logger.debug("Starting to build consent info for key {} and {} patients", key, batch.ids().size());

Set<String> codes = mapper.getRelevantCodes(key);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
import de.medizininformatikinitiative.torch.model.consent.PatientBatchWithConsent;
import de.medizininformatikinitiative.torch.model.fhir.Query;
import de.medizininformatikinitiative.torch.model.management.PatientBatch;
import de.medizininformatikinitiative.torch.model.mapping.ConsentKey;
import de.medizininformatikinitiative.torch.service.DataStore;
import de.medizininformatikinitiative.torch.util.ResourceUtils;
import org.hl7.fhir.r4.model.Encounter;
Expand Down Expand Up @@ -73,7 +74,7 @@ private static Mono<Map<String, Collection<Encounter>>> groupEncounterByPatient(
* @param batch Batch of patient IDs.
* @return {@link Mono<PatientBatchWithConsent>} containing all required provisions by patient with valid times.
*/
public Mono<PatientBatchWithConsent> fetchAndBuildConsentInfo(String consentKey, PatientBatch batch) {
public Mono<PatientBatchWithConsent> fetchAndBuildConsentInfo(ConsentKey consentKey, PatientBatch batch) {
return consentFetcher.buildConsentInfo(consentKey, batch)
.flatMap(this::adjustConsentPeriodsByPatientEncounters);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,8 @@
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
import com.fasterxml.jackson.annotation.JsonProperty;
import com.fasterxml.jackson.databind.JsonNode;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import de.medizininformatikinitiative.torch.exceptions.ValidationException;
import de.medizininformatikinitiative.torch.model.mapping.ConsentKey;

import java.util.Optional;

Expand All @@ -17,32 +17,28 @@ public record Crtdl(
@JsonProperty(required = true)
DataExtraction dataExtraction
) {
private static final Logger logger = LoggerFactory.getLogger(Crtdl.class);

public Crtdl {
requireNonNull(cohortDefinition);
requireNonNull(dataExtraction);
}

public Optional<String> consentKey() {
public Optional<ConsentKey> consentKey() throws ValidationException {
JsonNode inclusionCriteria = cohortDefinition.get("inclusionCriteria");
if (inclusionCriteria != null && inclusionCriteria.isArray()) {
for (JsonNode criteriaGroup : inclusionCriteria) {
for (JsonNode criteria : criteriaGroup) {
JsonNode context = criteria.get("context");
if (context != null && "Einwilligung".equals(context.get("code").asText())) {
JsonNode termcodes = criteria.get("termCodes");
if (termcodes != null && termcodes.isArray()) {
JsonNode firstTermcode = termcodes.get(0);
if (firstTermcode != null && firstTermcode.has("code")) {
return Optional.of(firstTermcode.get("code").asText());
}
JsonNode firstTermcode = criteria.get("termCodes").get(0);
if (firstTermcode != null && firstTermcode.has("code")) {
String code = firstTermcode.get("code").asText();
return Optional.of(ConsentKey.fromString(code));
}
}
}
}
}
return Optional.empty();
}

}
Original file line number Diff line number Diff line change
@@ -1,38 +1,18 @@
package de.medizininformatikinitiative.torch.model.crtdl.annotated;

import com.fasterxml.jackson.databind.JsonNode;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import de.medizininformatikinitiative.torch.model.mapping.ConsentKey;

import java.util.Optional;

import static java.util.Objects.requireNonNull;

public record AnnotatedCrtdl(JsonNode cohortDefinition, AnnotatedDataExtraction dataExtraction) {
public record AnnotatedCrtdl(JsonNode cohortDefinition, AnnotatedDataExtraction dataExtraction,
Optional<ConsentKey> consentKey) {

public AnnotatedCrtdl {
requireNonNull(cohortDefinition);
requireNonNull(dataExtraction);
}

public Optional<String> consentKey() {
JsonNode inclusionCriteria = cohortDefinition.get("inclusionCriteria");
if (inclusionCriteria != null && inclusionCriteria.isArray()) {
for (JsonNode criteriaGroup : inclusionCriteria) {
for (JsonNode criteria : criteriaGroup) {
JsonNode context = criteria.get("context");
if (context != null && "Einwilligung".equals(context.get("code").asText())) {
JsonNode termcodes = criteria.get("termCodes");
if (termcodes != null && termcodes.isArray()) {
JsonNode firstTermcode = termcodes.get(0);
if (firstTermcode != null && firstTermcode.has("code")) {
return Optional.of(firstTermcode.get("code").asText());
}
}
}
}
}
}
return Optional.empty();
requireNonNull(consentKey);
}
}
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
package de.medizininformatikinitiative.torch.model.mapping;

import de.medizininformatikinitiative.torch.exceptions.ValidationException;

import static java.util.Objects.requireNonNull;

public enum ConsentKey {
Expand Down Expand Up @@ -31,4 +33,13 @@ public enum ConsentKey {
public String toString() {
return s;
}

public static ConsentKey fromString(String value) throws ValidationException {
for (ConsentKey key : values()) {
if (key.toString().equalsIgnoreCase(value)) {
return key;
}
}
throw new ValidationException("Unknown consent key: " + value);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
import de.medizininformatikinitiative.torch.model.crtdl.annotated.AnnotatedAttributeGroup;
import de.medizininformatikinitiative.torch.model.crtdl.annotated.AnnotatedCrtdl;
import de.medizininformatikinitiative.torch.model.crtdl.annotated.AnnotatedDataExtraction;
import de.medizininformatikinitiative.torch.model.mapping.ConsentKey;
import de.medizininformatikinitiative.torch.util.CompiledStructureDefinition;
import de.medizininformatikinitiative.torch.util.FhirPathBuilder;
import org.hl7.fhir.r4.model.ElementDefinition;
Expand All @@ -20,6 +21,7 @@
import java.util.HashSet;
import java.util.List;
import java.util.Objects;
import java.util.Optional;
import java.util.Set;
import java.util.function.Predicate;

Expand All @@ -45,26 +47,27 @@ public CrtdlValidatorService(StructureDefinitionHandler profileHandler, Standard
* @return the validated Crtdl or an error signal with ValidationException if a profile is unknown.
*/
public AnnotatedCrtdl validate(Crtdl crtdl) throws ValidationException {
Optional<ConsentKey> consentKey = crtdl.consentKey();
List<AnnotatedAttributeGroup> annotatedAttributeGroups = new ArrayList<>();
Set<String> linkedGroups = new HashSet<>();
Set<String> successfullyAnnotatedGroups = new HashSet<>();
boolean exactlyOnePatientGroup = false;
boolean patientGroupFound = false;
String patientAttributeGroupId = "";

for (AttributeGroup attributeGroup : crtdl.dataExtraction().attributeGroups()) {
CompiledStructureDefinition definition = profileHandler.getDefinition(attributeGroup.groupReference())
.orElseThrow(() -> new ValidationException("Unknown Profile: " + attributeGroup.groupReference()));
if (Objects.equals(definition.type(), "Patient")) {
if (exactlyOnePatientGroup) {
if (patientGroupFound) {
throw new ValidationException(" More than one Patient Attribute Group");
} else {
exactlyOnePatientGroup = true;
patientGroupFound = true;
patientAttributeGroupId = attributeGroup.id();
logger.debug("Found Patient Attribute Group {}", patientAttributeGroupId);
}
}
}
if (!exactlyOnePatientGroup) {
if (!patientGroupFound) {
throw new ValidationException("No Patient Attribute Group");
}

Expand All @@ -86,7 +89,7 @@ public AnnotatedCrtdl validate(Crtdl crtdl) throws ValidationException {
}


return new AnnotatedCrtdl(crtdl.cohortDefinition(), new AnnotatedDataExtraction(annotatedAttributeGroups));
return new AnnotatedCrtdl(crtdl.cohortDefinition(), new AnnotatedDataExtraction(annotatedAttributeGroups), consentKey);
}

private AnnotatedAttributeGroup annotateGroup(AttributeGroup attributeGroup, CompiledStructureDefinition
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
import de.medizininformatikinitiative.torch.exceptions.ConsentViolatedException;
import de.medizininformatikinitiative.torch.model.consent.PatientBatchWithConsent;
import de.medizininformatikinitiative.torch.model.management.PatientBatch;
import de.medizininformatikinitiative.torch.model.mapping.ConsentKey;
import org.hl7.fhir.r4.model.DateTimeType;
import org.hl7.fhir.r4.model.Observation;
import org.hl7.fhir.r4.model.Reference;
Expand Down Expand Up @@ -69,7 +70,7 @@ private void assertConsentFalse(PatientBatchWithConsent batch, String patientId,

@Test
void failsOnNoPatientMatchesConsentKeyBuildingConsent() {
var resultBatch = consentFetcher.buildConsentInfo("yes-no-no-yes", BATCH);
var resultBatch = consentFetcher.buildConsentInfo(ConsentKey.YES_NO_NO_YES, BATCH);

StepVerifier.create(resultBatch)
.expectErrorSatisfies(error -> assertThat(error)
Expand All @@ -80,7 +81,7 @@ void failsOnNoPatientMatchesConsentKeyBuildingConsent() {

@Test
void failsOnUnknownPatientBuildingConsent() {
var resultBatch = consentFetcher.buildConsentInfo("yes-yes-yes-yes", BATCH_UNKNOWN);
var resultBatch = consentFetcher.buildConsentInfo(ConsentKey.YES_YES_YES_YES, BATCH_UNKNOWN);

StepVerifier.create(resultBatch)
.expectErrorSatisfies(error -> assertThat(error)
Expand All @@ -91,7 +92,7 @@ void failsOnUnknownPatientBuildingConsent() {

@Test
void successBuildingConsent() {
var resultBatch = consentFetcher.buildConsentInfo("yes-yes-yes-yes", BATCH);
var resultBatch = consentFetcher.buildConsentInfo(ConsentKey.YES_YES_YES_YES, BATCH);

StepVerifier.create(resultBatch)
.assertNext(batch -> {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
import de.medizininformatikinitiative.torch.consent.ConsentValidator;
import de.medizininformatikinitiative.torch.model.consent.PatientBatchWithConsent;
import de.medizininformatikinitiative.torch.model.management.PatientBatch;
import de.medizininformatikinitiative.torch.model.mapping.ConsentKey;
import org.hl7.fhir.r4.model.DateTimeType;
import org.hl7.fhir.r4.model.Observation;
import org.hl7.fhir.r4.model.Reference;
Expand Down Expand Up @@ -67,7 +68,7 @@ private void assertConsentFalse(PatientBatchWithConsent batch, String patientId,

@Test
void successAfterEncounterUpdatesProvisions() {
var resultBatch = consentHandler.fetchAndBuildConsentInfo("yes-yes-yes-yes", BATCH);
var resultBatch = consentHandler.fetchAndBuildConsentInfo(ConsentKey.YES_YES_YES_YES, BATCH);

StepVerifier.create(resultBatch)
.assertNext(batch -> {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
import de.medizininformatikinitiative.torch.consent.ConsentHandler;
import de.medizininformatikinitiative.torch.exceptions.ConsentViolatedException;
import de.medizininformatikinitiative.torch.model.management.PatientBatch;
import de.medizininformatikinitiative.torch.model.mapping.ConsentKey;
import de.medizininformatikinitiative.torch.service.DataStore;
import org.junit.jupiter.api.Test;
import org.mockito.Mockito;
Expand All @@ -26,10 +27,10 @@ public class ConsentHandlerTest {

@Test
void failsOnNoPatientMatchesConsentKeyBuildingConsent() {
when(consentFetcher.buildConsentInfo("yes-no-no-yes", BATCH))
when(consentFetcher.buildConsentInfo(ConsentKey.YES_NO_NO_YES, BATCH))
.thenReturn(Mono.error(new ConsentViolatedException("No valid provisions found for any patients in batch")));

var resultBatch = consentHandler.fetchAndBuildConsentInfo("yes-no-no-yes", BATCH);
var resultBatch = consentHandler.fetchAndBuildConsentInfo(ConsentKey.YES_NO_NO_YES, BATCH);

StepVerifier.create(resultBatch)
.expectErrorSatisfies(error -> assertThat(error)
Expand All @@ -40,10 +41,10 @@ void failsOnNoPatientMatchesConsentKeyBuildingConsent() {

@Test
void failsOnUnknownPatientBuildingConsent() {
when(consentFetcher.buildConsentInfo("yes-yes-yes-yes", BATCH_UNKNOWN))
when(consentFetcher.buildConsentInfo(ConsentKey.YES_YES_YES_YES, BATCH_UNKNOWN))
.thenReturn(Mono.error(new ConsentViolatedException("No valid provisions found for any patients in batch")));

var resultBatch = consentHandler.fetchAndBuildConsentInfo("yes-yes-yes-yes", BATCH_UNKNOWN);
var resultBatch = consentHandler.fetchAndBuildConsentInfo(ConsentKey.YES_YES_YES_YES, BATCH_UNKNOWN);

StepVerifier.create(resultBatch)
.expectErrorSatisfies(error -> assertThat(error)
Expand Down
Loading