Skip to content
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
import com.lantanagroup.link.shared.entities.ReportScheduleModel;
import com.lantanagroup.link.shared.exceptions.ValidationException;
import com.lantanagroup.link.shared.services.ReportClient;
import org.hl7.fhir.instance.model.api.IIdType;
import org.hl7.fhir.r4.model.IdType;
import org.hl7.fhir.r4.model.MeasureReport;
import org.hl7.fhir.r4.model.Reference;
Expand All @@ -19,10 +20,10 @@

import java.net.URI;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.UUID;
import java.util.function.Function;
import java.util.stream.Collectors;

public class BlobStorageService {
Expand Down Expand Up @@ -114,9 +115,18 @@ private List<Resource> normalize(MeasureReport measureReport) {
}

List<Resource> contained = measureReport.getContained();
Map<String, Resource> containedByIdPart = contained.stream().collect(Collectors.toMap(
resource -> stripHash(resource.getIdPart()),
Function.identity()));

// Contained resource IDs are only guaranteed unique within a resource type (e.g. Condition/A and
// Observation/A can legally coexist), so the primary lookup is type-qualified. A secondary,
// id-part-only index is kept to resolve untyped "#id" references, but only when unambiguous.
Map<String, Resource> containedByTypedId = new HashMap<>();
Map<String, List<Resource>> containedByIdPart = new HashMap<>();
for (Resource resource : contained) {
String idPart = stripHash(resource.getIdPart());
containedByTypedId.put(resource.getResourceType().name() + "/" + idPart, resource);
containedByIdPart.computeIfAbsent(idPart, key -> new ArrayList<>()).add(resource);
}

measureReport.setContained(null);
measureReport.setEvaluatedResource(null);
for (Resource resource : contained) {
Expand All @@ -127,8 +137,26 @@ private List<Resource> normalize(MeasureReport measureReport) {
}
for (Reference reference :
fhirContext.newTerser().getAllPopulatedChildElementsOfType(measureReport, Reference.class)) {
String idPart = stripHash(reference.getReferenceElement().getIdPart());
Resource resource = containedByIdPart.get(idPart);
IIdType referenceElement = reference.getReferenceElement();
String idPart = stripHash(referenceElement.getIdPart());
String resourceType = referenceElement.getResourceType();

Resource resource;
if (resourceType != null) {
resource = containedByTypedId.get(resourceType + "/" + idPart);
} else {
List<Resource> candidates = containedByIdPart.getOrDefault(idPart, List.of());
if (candidates.size() > 1) {
String candidateTypes = candidates.stream()
.map(candidate -> candidate.getResourceType().name())
.collect(Collectors.joining(", "));
logger.warn("Contained reference '#{}' is ambiguous between {} resources ({}); leaving reference unresolved",
idPart, candidates.size(), candidateTypes);
Comment thread
arianamihailescu marked this conversation as resolved.
Outdated
continue;
}
resource = candidates.isEmpty() ? null : candidates.get(0);
}

if (resource == null) {
continue;
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,19 +1,28 @@
package com.lantanagroup.link.measureeval.services;

import ca.uhn.fhir.context.FhirContext;
import ch.qos.logback.classic.Level;
import ch.qos.logback.classic.Logger;
import ch.qos.logback.classic.LoggerContext;
import ch.qos.logback.classic.spi.ILoggingEvent;
import ch.qos.logback.core.read.ListAppender;
import com.azure.core.util.BinaryData;
import com.azure.storage.blob.BlobClient;
import com.azure.storage.blob.BlobContainerClient;
import com.lantanagroup.link.measureeval.entities.PatientReportingEvaluationStatus;
import com.lantanagroup.link.shared.entities.ReportScheduleModel;
import com.lantanagroup.link.shared.exceptions.ValidationException;
import com.lantanagroup.link.shared.services.ReportClient;
import org.hl7.fhir.r4.model.Condition;
import org.hl7.fhir.r4.model.MeasureReport;
import org.hl7.fhir.r4.model.Observation;
import org.hl7.fhir.r4.model.Patient;
import org.hl7.fhir.r4.model.Reference;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.mockito.ArgumentCaptor;
import org.slf4j.LoggerFactory;

import java.util.List;

Expand Down Expand Up @@ -142,6 +151,99 @@ void testStorePatientInBlobStorageWithContainedResources() {
assertTrue(content.contains("MeasureReport/mr1"));
}

@Test
void testStorePatientInBlobStorageWithDuplicateIdPartAcrossTypes() {
PatientReportingEvaluationStatus status = new PatientReportingEvaluationStatus();
status.setPatientId("patient1");
PatientReportingEvaluationStatus.Report report = new PatientReportingEvaluationStatus.Report();
report.setReportTrackingId("track1");
report.setReportType("type1");
status.setReports(List.of(report));

ReportScheduleModel schedule = new ReportScheduleModel();
schedule.setPayloadRootUri("https://storage.com/test-container/root/");
when(reportClient.getReportSchedule("track1")).thenReturn(schedule);

MeasureReport measureReport = new MeasureReport();
measureReport.setId("mr1");

// Two contained resources of different types sharing the same ID part.
// Previously this threw IllegalStateException from Collectors.toMap.
Condition condition = new Condition();
condition.setId("#LCR-A");
measureReport.addContained(condition);

Observation observation = new Observation();
observation.setId("#LCR-A");
measureReport.addContained(observation);

blobStorageService.storePatientInBlobStorage(status, report, measureReport);

ArgumentCaptor<BinaryData> contentCaptor = ArgumentCaptor.forClass(BinaryData.class);
verify(blobClient).upload(contentCaptor.capture(), eq(true));
String content = contentCaptor.getValue().toString();

assertTrue(content.contains("Condition/A"));
assertTrue(content.contains("Observation/A"));
}

@Test
void testStorePatientInBlobStorageWithAmbiguousUntypedReferenceIsSkippedAndLogged() {
Logger blobStorageServiceLogger =
(Logger) LoggerFactory.getLogger(BlobStorageService.class);
ListAppender<ILoggingEvent> logAppender = new ListAppender<>();
logAppender.setContext((LoggerContext) LoggerFactory.getILoggerFactory());
logAppender.start();
blobStorageServiceLogger.addAppender(logAppender);

try {
PatientReportingEvaluationStatus status = new PatientReportingEvaluationStatus();
status.setPatientId("patient1");
PatientReportingEvaluationStatus.Report report = new PatientReportingEvaluationStatus.Report();
report.setReportTrackingId("track1");
report.setReportType("type1");
status.setReports(List.of(report));

ReportScheduleModel schedule = new ReportScheduleModel();
schedule.setPayloadRootUri("https://storage.com/test-container/root/");
when(reportClient.getReportSchedule("track1")).thenReturn(schedule);

MeasureReport measureReport = new MeasureReport();
measureReport.setId("mr1");

Condition condition = new Condition();
condition.setId("#LCR-A");
measureReport.addContained(condition);

Observation observation = new Observation();
observation.setId("#LCR-A");
measureReport.addContained(observation);

// Untyped contained reference to the ambiguous ID part - cannot be resolved deterministically.
measureReport.setSubject(new Reference("#LCR-A"));

blobStorageService.storePatientInBlobStorage(status, report, measureReport);

ArgumentCaptor<BinaryData> contentCaptor = ArgumentCaptor.forClass(BinaryData.class);
verify(blobClient).upload(contentCaptor.capture(), eq(true));
String content = contentCaptor.getValue().toString();

// First line is the "MeasureReport/<id>" marker, second is the MeasureReport JSON itself.
String measureReportJson = content.split("\n")[1];
MeasureReport parsed = fhirContext.newJsonParser().parseResource(MeasureReport.class, measureReportJson);

// The ambiguous reference is left unresolved rather than guessing which resource it means.
assertEquals("#LCR-A", parsed.getSubject().getReference());

boolean warningLogged = logAppender.list.stream()
.anyMatch(event -> event.getLevel() == Level.WARN
&& event.getFormattedMessage().contains("LCR-A"));
assertTrue(warningLogged, "Expected a warning to be logged for the ambiguous contained reference");
} finally {
blobStorageServiceLogger.detachAppender(logAppender);
}
}

@Test
void testStorePatientInBlobStoragePayloadUriNull() {
PatientReportingEvaluationStatus status = new PatientReportingEvaluationStatus();
Expand Down
Loading