legalUrls = new ArrayList<>();
for (CanonicalType url : urls) {
StructureDefinition definition = getDefinition(url.getValue());
if (definition != null) {
- return definition;
+ legalUrls.add(url);
}
}
- return null;
+ return legalUrls;
}
public StructureDefinition.StructureDefinitionSnapshotComponent getSnapshot(String url) {
@@ -79,9 +93,6 @@ public StructureDefinition.StructureDefinitionSnapshotComponent getSnapshot(Stri
/**
* Reads all JSON files in a directory and stores their StructureDefinitions in the definitionsMap
- *
- * @param directoryPath the path to the directory containing JSON files
- * @throws IOException
*/
private void processDirectory(String directoryPath) throws IOException {
File directory = new File(directoryPath);
diff --git a/src/main/java/de/medizininformatikinitiative/torch/ConsentHandler.java b/src/main/java/de/medizininformatikinitiative/torch/ConsentHandler.java
index 4f780d7..c09d185 100644
--- a/src/main/java/de/medizininformatikinitiative/torch/ConsentHandler.java
+++ b/src/main/java/de/medizininformatikinitiative/torch/ConsentHandler.java
@@ -3,7 +3,13 @@
import ca.uhn.fhir.context.FhirContext;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
+import de.medizininformatikinitiative.torch.exceptions.ConsentViolatedException;
import de.medizininformatikinitiative.torch.exceptions.PatientIdNotFoundException;
+import de.medizininformatikinitiative.torch.model.PatientBatch;
+import de.medizininformatikinitiative.torch.model.consent.ConsentInfo;
+import de.medizininformatikinitiative.torch.model.consent.NonContinuousPeriod;
+import de.medizininformatikinitiative.torch.model.consent.PatientConsentInfo;
+import de.medizininformatikinitiative.torch.model.consent.Provisions;
import de.medizininformatikinitiative.torch.model.fhir.Query;
import de.medizininformatikinitiative.torch.model.fhir.QueryParams;
import de.medizininformatikinitiative.torch.service.DataStore;
@@ -11,17 +17,17 @@
import org.hl7.fhir.r4.model.*;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
-import org.springframework.beans.factory.annotation.Autowired;
-import org.springframework.stereotype.Component;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
-import reactor.core.scheduler.Schedulers;
+import reactor.util.function.Tuple2;
+import reactor.util.function.Tuples;
import java.io.File;
import java.io.IOException;
import java.util.*;
+import java.util.stream.Collectors;
-import static de.medizininformatikinitiative.torch.model.fhir.QueryParams.EMPTY;
+import static de.medizininformatikinitiative.torch.model.consent.Provisions.merge;
import static de.medizininformatikinitiative.torch.model.fhir.QueryParams.stringValue;
/**
@@ -31,17 +37,18 @@
*
* - Checking patient consent based on FHIR resources.
* - Building consent information for a batch of patients.
- * - Updating consent periods based on patient encounters.
+ * - Updating consent provisions based on patient encounters.
*
*
* @see DataStore
* @see ConsentCodeMapper
* @see ConsentProcessor
*/
-@Component
public class ConsentHandler {
private static final Logger logger = LoggerFactory.getLogger(ConsentHandler.class);
+ public static final String CDS_CONSENT_PROFILE_URL = "https://www.medizininformatik-initiative.de/fhir/modul-consent/StructureDefinition/mii-pr-consent-einwilligung";
+ public static final String CDS_ENCOUNTER_PROFILE_URL = "https://www.medizininformatik-initiative.de/fhir/core/modul-fall/StructureDefinition/KontaktGesundheitseinrichtung";
private final DataStore dataStore;
private final ConsentCodeMapper mapper;
private final JsonNode mappingProfiletoDateField;
@@ -57,32 +64,46 @@ public class ConsentHandler {
* @param mapper The {@link ConsentCodeMapper} for mapping consent codes.
* @param profilePath The file system path to the consent profile mapping configuration.
* @param cdsStructureDefinitionHandler The {@link CdsStructureDefinitionHandler} for handling structure definitions.
- * @param objectMapper
* @throws IOException If an error occurs while reading the mapping profile file.
*/
- @Autowired
public ConsentHandler(DataStore dataStore, ConsentCodeMapper mapper, String profilePath, CdsStructureDefinitionHandler cdsStructureDefinitionHandler, FhirContext ctx, ObjectMapper objectMapper) throws IOException {
this.dataStore = dataStore;
this.mapper = mapper;
this.ctx = ctx;
- this.fhirPathBuilder = new FhirPathBuilder(new Slicing(cdsStructureDefinitionHandler, ctx));
+ this.fhirPathBuilder = new FhirPathBuilder(new Slicing(ctx));
this.cdsStructureDefinitionHandler = cdsStructureDefinitionHandler;
this.consentProcessor = new ConsentProcessor(ctx);
mappingProfiletoDateField = objectMapper.readTree(new File(profilePath).getAbsoluteFile());
}
+ /**
+ * Returns a Mono which will emit a {@code ConsentInfo} for given consent key and batch.
+ *
+ * This methods fetches Consent resources from a FHIR server and extracts provisions according to consent key.
+ *
+ * @param consentKey Consent consentKey for which the ConsentInfo should be build
+ * @param batch Batch of patient IDs
+ * @return Consentinfo containing all required provisions by Patient with valid times
+ */
+ public Mono fetchAndBuildConsentInfo(String consentKey, PatientBatch batch) {
+ Flux consentInfoFlux = buildingConsentInfo(consentKey, batch);
+ Mono> collectedConsentInfo = collectConsentInfo(consentInfoFlux);
+ return updateConsentPeriodsByPatientEncounters(collectedConsentInfo, batch)
+ .map(consentInfos -> new ConsentInfo(true, consentInfos.stream().collect(Collectors.toMap(PatientConsentInfo::patientId, PatientConsentInfo::provisions))));
+ }
+
/**
* Checks whether the provided {@link DomainResource} complies with the patient's consents.
*
* This method evaluates the resource against the consent information to determine if access
- * should be granted based on the defined consent periods.
+ * should be granted based on the defined consent provisions.
*
* @param resource The FHIR {@link DomainResource} to check for consent compliance.
* @param consentInfo A map containing consent information structured by patient ID and consent codes.
* @return {@code true} if the resource complies with the consents; {@code false} otherwise.
*/
- public Boolean checkConsent(DomainResource resource, Map>> consentInfo) {
- logger.trace("Checking Consent for {} {}", resource.getResourceType(), resource.getId());
+ public boolean checkConsent(DomainResource resource, ConsentInfo consentInfo) {
+ logger.trace("Checking consent for {} {}", resource.getResourceType(), resource.getId());
Iterator profileIterator = resource.getMeta().getProfile().iterator();
JsonNode fieldValue = null;
StructureDefinition.StructureDefinitionSnapshotComponent snapshot = null;
@@ -107,72 +128,49 @@ public Boolean checkConsent(DomainResource resource, Map values = ctx.newFhirPath().evaluate(resource, fhirPathBuilder.handleSlicingForFhirPath(fieldValue.asText(), snapshot), Base.class);
+
+ String fhirPath = fhirPathBuilder.handleSlicingForFhirPath(fieldValue.asText(), snapshot)[0];
+ List values = ctx.newFhirPath().evaluate(resource, fhirPath, Base.class);
+
logger.trace("Evaluated FHIRPath expression, found {} values.", values.size());
for (Base value : values) {
- DateTimeType resourceStart;
- DateTimeType resourceEnd;
-
- if (value instanceof DateTimeType) {
- resourceStart = (DateTimeType) value;
- resourceEnd = (DateTimeType) value;
- logger.trace("Evaluated value is DateTimeType: start {}, end {}", resourceStart, resourceEnd);
- } else if (value instanceof Period) {
- resourceStart = ((Period) value).getStartElement();
- resourceEnd = ((Period) value).getEndElement();
- logger.trace("Evaluated value is Period: start {}, end {}", resourceStart, resourceEnd);
+ de.medizininformatikinitiative.torch.model.consent.Period period;
+ if (value instanceof Period) {
+ period = de.medizininformatikinitiative.torch.model.consent.Period.fromHapi((Period) value);
+ } else if (value instanceof DateTimeType) {
+ period = de.medizininformatikinitiative.torch.model.consent.Period.fromHapi((DateTimeType) value);
} else {
- logger.error("No valid Date Time Value found. Value: {}", value);
throw new IllegalArgumentException("No valid Date Time Value found");
}
-
- String patientID = null;
+ String patientID;
try {
- patientID = ResourceUtils.getPatientId(resource);
+ patientID = ResourceUtils.patientId(resource);
} catch (PatientIdNotFoundException e) {
- logger.warn("Resource {} does not Contain any Patient Reference ", resource.getIdElement());
return false;
}
- logger.trace("Patient ID {} and Set {} ", patientID, consentInfo.keySet());
- logger.trace("Get Result {}", consentInfo.get(patientID));
-
- boolean hasValidConsent = Optional.ofNullable(consentInfo.get(patientID))
- .map(consentPeriodMap -> consentPeriodMap.entrySet().stream()
+ boolean hasValidConsent = Optional.ofNullable(consentInfo.provisions().get(patientID))
+ .map(provisions -> provisions.periods().entrySet().stream()
.allMatch(innerEntry -> {
- String code = innerEntry.getKey();
- List consentPeriods = innerEntry.getValue();
- logger.debug("Checking {} consent periods for code: {}", consentPeriods.size(), code);
-
- // Check if at least one consent period is valid for the current code
- return consentPeriods.stream()
- .anyMatch(period -> {
- logger.trace("Evaluating ConsentPeriod: start {}, end {} vs {} and {}",
- resourceStart, resourceEnd, period.getStart(), period.getEnd());
- logger.debug("Result: {}", resourceStart.after(period.getStartElement()) && resourceEnd.before(period.getEndElement()));
- return resourceStart.after(period.getStartElement()) && resourceEnd.before(period.getEndElement());
- });
+ NonContinuousPeriod consentPeriods = innerEntry.getValue();
+ return consentPeriods.within(period);
}))
.orElse(false);
-
if (hasValidConsent) {
- logger.debug("Valid consent period found for evaluated values.");
return true;
}
}
- logger.warn("No valid consent period found for any value.");
- logger.debug("No valid consent period found for any value in Resource {}", resource.getIdElement());
return false; // No matching consent period found
}
}
+
/**
* Builds consent information for a batch of patients based on the provided key and patient IDs.
*
@@ -183,159 +181,98 @@ public Boolean checkConsent(DomainResource resource, Map>>> buildingConsentInfo(String key, List batch) {
+ public Flux buildingConsentInfo(String key, PatientBatch batch) {
Objects.requireNonNull(batch, "Patient batch cannot be null");
// Retrieve the relevant codes for the given key
Set codes = mapper.getRelevantCodes(key);
- logger.trace("Starting to build consent info for key: {} with batch size: {}", key, batch.size());
- QueryParams consentParams = EMPTY.appendParam("_profile", stringValue("https://www.medizininformatik-initiative.de/fhir/modul-consent/StructureDefinition/mii-pr-consent-einwilligung"));
- consentParams = consentParams.appendParam(BatchUtils.queryElements("Consent"), QueryParams.stringValue(String.join(",", batch)));
- // Fetch resources using a bounded elastic scheduler for offloading blocking HTTP I/O
- Query query = new Query("Consent", consentParams);
- return dataStore.getResources(query)
- .subscribeOn(Schedulers.boundedElastic()) // Offload the HTTP requests
- .doOnSubscribe(subscription -> logger.debug("Fetching resources for batch: {}", batch))
- .doOnNext(resource -> logger.trace("Resource fetched for ConsentBuild: {}", resource.getIdElement().getIdPart()))
- .onErrorResume(e -> {
- logger.error("Error fetching resources for parameters: {}", query, e);
- return Flux.empty();
- })
+ logger.trace("Starting to build consent info for key: {} with batch size: {}", key, batch.ids().size());
+ String type = "Consent";
+ Query query = Query.of(type, QueryParams.of("_profile", stringValue(CDS_CONSENT_PROFILE_URL))
+ .appendParams(batch.compartmentSearchParam(type)));
- .map(resource -> {
+ return dataStore.search(query)
+ .cast(Consent.class)
+ .doOnSubscribe(subscription -> logger.debug("Fetching resources for batch: {}", batch.ids()))
+ .doOnNext(resource -> logger.trace("consent resource with id {} fetched for ConsentBuild", resource.getIdPart()))
+ .flatMap(consent -> {
try {
- DomainResource domainResource = (DomainResource) resource;
- String patient = ResourceUtils.getPatientId(domainResource);
-
- logger.trace("Processing resource for patient: {} {}", patient, resource.getResourceType());
-
- Map> consents = consentProcessor.transformToConsentPeriodByCode(domainResource, codes);
-
- Map>> patientConsentMap = new HashMap<>();
- patientConsentMap.put(patient, new HashMap<>());
-
- // Log consent periods transformation
- logger.trace("Transformed resource into {} consent periods for patient: {}", consents.size(), patient);
-
- // Iterate over the consent periods and add them to the patient's map
- consents.forEach((code, newConsentPeriods) -> {
- patientConsentMap.get(patient)
- .computeIfAbsent(code, k -> new ArrayList<>())
- .addAll(newConsentPeriods);
- });
-
- logger.trace("Consent periods updated for patient: {} with {} codes", patient, consents.size());
-
- // Return the map containing the patient's consent periods
- return patientConsentMap;
- } catch (Exception e) {
- logger.error("Error processing resource", e);
- throw new RuntimeException(e);
+ String patientId = ResourceUtils.patientId(consent);
+ logger.trace("Processing consent for patient {}", patientId);
+ Provisions consents = consentProcessor.transformToConsentPeriodByCode(consent, codes);
+ return Mono.just(new PatientConsentInfo(patientId, consents));
+ } catch (ConsentViolatedException | PatientIdNotFoundException e) {
+ return Mono.error(e);
}
- })
-
- .collectList()
- .doOnSuccess(list -> logger.trace("Successfully processed {} resources for buildingConsentInfo", list.size()))
-
- .flatMapMany(Flux::fromIterable);
+ });
}
/**
- * Updates consent periods based on patient encounters for a given batch.
+ * Updates consent provisions based on patient encounters for a given batch.
*
* This method retrieves all encounters associated with the patients in the batch and updates
- * their consent periods accordingly. It ensures that consents are valid in the context of the
+ * their consent provisions accordingly. It ensures that consents are valid in the context of the
* patient's encounters.
*
- * @param consentInfoFlux A {@link Flux} emitting maps of consent information structured by patient ID and consent codes.
- * @param batch A list of patient IDs to process in this batch.
+ * @param consentInfos A {@link Flux} emitting maps of consent information structured by patient ID and consent codes.
+ * @param batch A list of patient IDs to process in this batch.
* @return A {@link Flux} emitting updated maps of consent information.
*/
- public Flux