diff --git a/hapi-fhir-jpaserver-test-r4/src/test/java/ca/uhn/fhir/jpa/provider/r4/ResourceProviderR4SearchContainedTest.java b/hapi-fhir-jpaserver-test-r4/src/test/java/ca/uhn/fhir/jpa/provider/r4/ResourceProviderR4SearchContainedTest.java index be955c3c401..d134679da59 100644 --- a/hapi-fhir-jpaserver-test-r4/src/test/java/ca/uhn/fhir/jpa/provider/r4/ResourceProviderR4SearchContainedTest.java +++ b/hapi-fhir-jpaserver-test-r4/src/test/java/ca/uhn/fhir/jpa/provider/r4/ResourceProviderR4SearchContainedTest.java @@ -20,6 +20,7 @@ import org.hl7.fhir.r4.model.ClinicalImpression; import org.hl7.fhir.r4.model.ClinicalImpression.ClinicalImpressionStatus; import org.hl7.fhir.r4.model.CodeableConcept; import org.hl7.fhir.r4.model.DecimalType; +import org.hl7.fhir.r4.model.DiagnosticReport; import org.hl7.fhir.r4.model.Encounter; import org.hl7.fhir.r4.model.Encounter.EncounterStatus; import org.hl7.fhir.r4.model.HumanName; @@ -41,6 +42,7 @@ import java.util.List; import static org.hamcrest.MatcherAssert.assertThat; import static org.hamcrest.Matchers.contains; +import static org.hamcrest.Matchers.containsInAnyOrder; import static org.hamcrest.Matchers.containsString; import static org.junit.jupiter.api.Assertions.assertEquals; @@ -189,6 +191,361 @@ public class ResourceProviderR4SearchContainedTest extends BaseResourceProviderR } + /** + * Unit test with multiple cases to illustrate expected behaviour of `_contained` and `_containedType` without chaining + *

+ * Although this test is in R4, the R5 specification for these parameters are much clearer: + * - _contained & _containedType + * + * @throws IOException + */ + @Test + public void testContainedParameterBehaviourWithoutChain() throws IOException { + // Some useful values + final String patientFamily = "VanHouten"; + final String patientGiven = "Milhouse"; + + // Create a discrete Patient + final IIdType discretePatientId; + { + Patient discretePatient = new Patient(); + discretePatient.addName().setFamily(patientFamily).addGiven(patientGiven); + discretePatientId = myPatientDao.create(discretePatient, mySrd).getId().toUnqualifiedVersionless(); + + ourLog.debug("\nInput - Discrete Patient:\n{}", + myFhirContext.newJsonParser().setPrettyPrint(true).encodeResourceToString(discretePatient)); + } + + /* + * Create a discrete Observation, which includes a contained Patient + * - The contained Patient is otherwise identical to the discrete Patient above + */ + final IIdType discreteObservationId; + final String containedPatientId = "contained-patient-1"; + { + Patient containedPatient = new Patient(); + containedPatient.setId(containedPatientId); + containedPatient.addName().setFamily(patientFamily).addGiven(patientGiven); + + Observation discreteObservation = new Observation(); + discreteObservation.getContained().add(containedPatient); + discreteObservation.getSubject().setReference("#" + containedPatientId); + discreteObservationId = myObservationDao.create(discreteObservation, mySrd).getId().toUnqualifiedVersionless(); + + ourLog.debug("\nInput - Discrete Observation with contained Patient:\n{}", + myFhirContext.newJsonParser().setPrettyPrint(true).encodeResourceToString(discreteObservation)); + } + + { + /* + * Case 1 + * When: we search for Patient with `_contained=false` + * - `_contained=false` means we should search and return only discrete resources; not contained resources + * - Note that we are searching by `/Patient?`, not `/Observation?` + * - `_contained=false` is the default value; same as if `_contained` were absent + * - When `_containedType` is absent, the default value of `_containedType=container` should be used + * - We should return the container resources + */ + String queryUrl = myServerBase + "/Patient?family=" + patientFamily + "&given=" + patientGiven + "&_contained=false"; + + // Then: we should get the discrete Patient + List resourceIds = searchAndReturnUnqualifiedVersionlessIdValues(queryUrl); + assertEquals(1L, resourceIds.size()); + assertThat(resourceIds, contains(discretePatientId.getValue())); + } + + { + /* + * Case 2 + * When: we search for Patient without `_contained` + * - When `_contained` is absent, the default value of `_contained=false` should be used + * - We should search and return only discrete resources; not contained resources + * - Note that we are searching by `/Patient?`, not `/Observation?` + * - When `_containedType` is absent, the default value of `_containedType=container` should be used + * - We should return the container resources + * - Case 2 is equivalent to Case 1; included to highlight default behaviour of `_contained` + */ + String queryUrl = myServerBase + "/Patient?family=" + patientFamily + "&given=" + patientGiven; + + // Then: we should get the discrete Patient + List resourceIds = searchAndReturnUnqualifiedVersionlessIdValues(queryUrl); + assertEquals(1L, resourceIds.size()); + assertThat(resourceIds, contains(discretePatientId.getValue())); + } + + { + /* + * Case 3 + * When: we search for Patient with `_contained=true` + * - `_contained=true` means we should search and return only contained resources; not discrete resources + * - Note that we are searching by `/Patient?`, not `/Observation?` + * - When `_containedType` is absent, the default value of `_containedType=container` should be used + * - We should return the container resources + */ + String queryUrl = myServerBase + "/Patient?family=" + patientFamily + "&given=" + patientGiven + "&_contained=true"; + + // Then: we should get the Observation that is containing that Patient + List resourceIds = searchAndReturnUnqualifiedVersionlessIdValues(queryUrl); + assertEquals(1L, resourceIds.size()); + // TODO Fails: we are incorrectly searching and returning the discrete Patient; should be discrete Observation with contained Patient + assertThat(resourceIds, contains(discreteObservationId.getValue())); + } + + { + /* + * Case 4 + * When: we search for Patient with `_contained=true` and `_containedType=container` + * + * - `_contained=true` means we should search and return only contained resources; not discrete resources + * - Note that we are searching by `/Patient?`, not `/Observation?` + * - `_containedType=container` is the default value; same as if `_containedType` were absent + * - `_containedType=container` means we should return the container resources + * - Case 4 is equivalent to Case 3; included to highlight default behaviour of `_containedType` + */ + String queryUrl = myServerBase + "/Patient?family=" + patientFamily + "&given=" + patientGiven + "&_contained=true&_containedType=container"; + + // Then: we should get the Observation that is containing that Patient + List resourceIds = searchAndReturnUnqualifiedVersionlessIdValues(queryUrl); + assertEquals(1L, resourceIds.size()); + // TODO Fails: we are incorrectly searching and returning the discrete Patient; should be discrete Observation with contained Patient + assertThat(resourceIds, contains(discreteObservationId.getValue())); + } + + // TODO Implementer: Note that we don't support `_containedType` at all yet. This case shows how it would look; however, implementing this behaviour is not the focus of this ticket. This is to guide your implementation for the rest of the ticket because `_contained` and `_containedType` are so closely related. We can create a new ticket for implementing `_containedType`. + { + /* + * Case 5 + * When: we search for Patient with `_contained=true` and `_containedType=contained` + * - `_contained=true` means we should search and return only contained resources; not discrete resources + * - Note that we are searching by `/Patient?`, not `/Observation?` + * - `_containedType=contained` means we should return the contained resources; not the container resources + * - `Bundle.entry.fullUrl` points to the container resource first, and includes the required resolution for the contained resource + * - e.g. "http://localhost/fhir/context/Observation/2#contained-patient-1" + * - `Bundle.entry.resource.id` includes only the required resolution for the contained resource + * - e.g. "contained-patient-1" + */ +/* + String queryUrl = myServerBase + "/Patient?family=" + patientFamily + "&given=" + patientGiven + "&_contained=true&_containedType=contained"; + + // Then: we should get just the contained Patient without the container Observation + HttpGet get = new HttpGet(queryUrl); + try (CloseableHttpResponse response = ourHttpClient.execute(get)) { + String resp = IOUtils.toString(response.getEntity().getContent(), StandardCharsets.UTF_8); + Bundle bundle = myFhirContext.newXmlParser().parseResource(Bundle.class, resp); + ourLog.debug("\nOutput - Contained Patient as discrete result:\n{}", + myFhirContext.newJsonParser().setPrettyPrint(true).encodeResourceToString(bundle)); + assertThat(bundle.getEntry(), hasSize(1)); + // TODO Fails: we are incorrectly searching and returning the discrete Patient; should be contained Patient + assertThat(bundle.getEntryFirstRep().getResource().getIdElement().getIdPart(), is(equalTo(containedPatientId))); + // TODO Fails: we are incorrectly searching and returning the discrete Patient; should be contained Patient + assertThat(bundle.getEntryFirstRep().getFullUrl(), is(containsString(discreteObservationId.getValueAsString() + "#" + containedPatientId))); + } +*/ + } + } + + /** + * Unit test with multiple cases to illustrate expected behaviour of `_contained` and `_containedType` with chaining + *

+ * Although this test is in R4, the R5 specification for these parameters are much clearer: + * - _contained & _containedType + * + * @throws IOException + */ + @Test + public void testContainedParameterBehaviourWithChain() throws IOException { + // Some useful values + final String patientFamily = "VanHouten"; + final String patientGiven = "Milhouse"; + + // Create a discrete Patient + final IIdType discretePatientId; + { + Patient discretePatient = new Patient(); + discretePatient.addName().setFamily(patientFamily).addGiven(patientGiven); + discretePatientId = myPatientDao.create(discretePatient, mySrd).getId().toUnqualifiedVersionless(); + + ourLog.info("\nInput - Discrete Patient:\n{}", + myFhirContext.newJsonParser().setPrettyPrint(true).encodeResourceToString(discretePatient)); + } + + // Create a discrete Observation, which references the discrete Patient + final IIdType discreteObservationId1; + { + Observation discreteObservation = new Observation(); + discreteObservation.getSubject().setReference(discretePatientId.getValue()); + discreteObservationId1 = myObservationDao.create(discreteObservation, mySrd).getId().toUnqualifiedVersionless(); + + ourLog.info("\nInput - Discrete Observation with reference to discrete Patient:\n{}", + myFhirContext.newJsonParser().setPrettyPrint(true).encodeResourceToString(discreteObservation)); + } + + // Create a discrete Observation, which includes a contained Patient + final IIdType discreteObservationId2; + final String containedPatientId1 = "contained-patient-1"; + { + Patient containedPatient = new Patient(); + containedPatient.setId(containedPatientId1); + containedPatient.addName().setFamily(patientFamily).addGiven(patientGiven); + + Observation discreteObservation = new Observation(); + discreteObservation.getContained().add(containedPatient); + discreteObservation.getSubject().setReference("#" + containedPatientId1); + discreteObservationId2 = myObservationDao.create(discreteObservation, mySrd).getId().toUnqualifiedVersionless(); + + ourLog.debug("\nInput - Discrete Observation with contained Patient:\n{}", + myFhirContext.newJsonParser().setPrettyPrint(true).encodeResourceToString(discreteObservation)); + } + + /* + * Create a discrete DiagnosticReport, which includes a contained Observation + * - The contained Observation itself references a contained Patient + */ + final IIdType discreteDiagnosticReportId; + final String containedObservationId = "contained-observation"; + final String containedPatientId2 = "contained-patient-2"; + { + Patient containedPatient = new Patient(); + containedPatient.setId(containedPatientId2); + containedPatient.addName().setFamily(patientFamily).addGiven(patientGiven); + + Observation containedObservation = new Observation(); + containedObservation.setId(containedObservationId); + containedObservation.getContained().add(containedPatient); + containedObservation.getSubject().setReference("#" + containedPatientId2); + + DiagnosticReport discreteDiagnosticReport = new DiagnosticReport(); + discreteDiagnosticReport.getContained().add(containedPatient); + discreteDiagnosticReport.getContained().add(containedObservation); + discreteDiagnosticReport.addResult().setReference("#" + containedObservationId); + + discreteDiagnosticReportId = myDiagnosticReportDao.create(discreteDiagnosticReport, mySrd).getId().toUnqualifiedVersionless(); + + ourLog.debug("\nInput - Discrete DiagnosticReport with contained Observation, which references a contained Patient:\n{}", + myFhirContext.newJsonParser().setPrettyPrint(true).encodeResourceToString(discreteDiagnosticReport)); + } + + { + /* + * Case 1 + * When: we search for Observation with chain and `_contained=false` + * - `_contained=false` means we should search and return only discrete resources; not contained resources + * - Note that we are searching by `/Observation?`, not `/Patient?` + * - `_contained=false` is the default value; same as if `_contained` were absent + * - When `_containedType` is absent, the default value of `_containedType=container` should be used + * - We should return the container resources + */ + String queryUrl = myServerBase + "/Observation?subject.family=" + patientFamily + "&subject.given=" + patientGiven + "&_contained=false"; + + /* + * Then: we should get the discrete Observation that is referencing that discrete Patient and + * the discrete Observation that is containing that Patient + */ + List resourceIds = searchAndReturnUnqualifiedVersionlessIdValues(queryUrl); + assertEquals(2L, resourceIds.size()); + assertThat(resourceIds, containsInAnyOrder(discreteObservationId1.getValue(), discreteObservationId2.getValue())); + } + + { + /* + * Case 2 + * When: we search for Observation with chain, and without `_contained` + * - When `_contained` is absent, the default value of `_contained=false` should be used + * - We should search and return only discrete resources; not contained resources + * - Note that we are searching by `/Observation?`, not `/Patient?` + * - When `_containedType` is absent, the default value of `_containedType=container` should be used + * - We should return the container resources + * - Case 2 is equivalent to Case 1; included to highlight chained searches do not require `_contained` + */ + String queryUrl = myServerBase + "/Observation?subject.family=" + patientFamily + "&subject.given=" + patientGiven; + + /* + * Then: we should get the discrete Observation that is referencing that discrete Patient and + * the discrete Observation that is containing that Patient + */ + List resourceIds = searchAndReturnUnqualifiedVersionlessIdValues(queryUrl); + assertEquals(2L, resourceIds.size()); + assertThat(resourceIds, containsInAnyOrder(discreteObservationId1.getValue(), discreteObservationId2.getValue())); + } + + { + /* + * Case 3 + * When: we search for Observation with chain and `_contained=true` + * - `_contained=true` means we should search and return only contained resources; not discrete resources + * - Note that we are searching by `/Observation?`, not `/Patient?` + * - When `_containedType` is absent, the default value of `_containedType=container` should be used + * - We should return the container resources + */ + String queryUrl = myServerBase + "/Observation?subject.family=" + patientFamily + "&subject.given=" + patientGiven + "&_contained=true"; + + // Then: we should get the discrete DiagnosticReport that is containing that Observation + List resourceIds = searchAndReturnUnqualifiedVersionlessIdValues(queryUrl); + // TODO Fails: we are incorrectly searching and returning the discrete Observations; should be discrete DiagnosticReport with contained Observation + assertEquals(1L, resourceIds.size()); + // TODO Fails: we are incorrectly searching and returning the discrete Observations; should be discrete DiagnosticReport with contained Observation + assertThat(resourceIds, contains(discreteDiagnosticReportId.getValue())); + } + + { + /* + * Case 4 + * When: we search for Observation with chain, `_contained=true`, and `_containedType=container` + * - `_contained=true` means we should search and return only contained resources; not discrete resources + * - Note that we are searching by `/Observation?`, not `/Patient?` + * - `_containedType=container` is the default value; same as if `_containedType` were absent + * - `_containedType=container` means we should return the container resources + * - Case 4 is equivalent to Case 3; included to highlight default behaviour of `_containedType` + */ + String queryUrl = myServerBase + "/Observation?subject.family=" + patientFamily + "&subject.given=" + patientGiven + "&_contained=true&_containedType=container"; + + // Then: we should get the discrete DiagnosticReport that is containing that Observation + List resourceIds = searchAndReturnUnqualifiedVersionlessIdValues(queryUrl); + // TODO Fails: we are incorrectly searching and returning the discrete Observations; should be discrete DiagnosticReport with contained Observation + assertEquals(1L, resourceIds.size()); + // TODO Fails: we are incorrectly searching and returning the discrete Observations; should be discrete DiagnosticReport with contained Observation + assertThat(resourceIds, contains(discreteDiagnosticReportId.getValue())); + } + + // TODO Implementer: Note that we don't support `_containedType` at all yet. This case shows how it would look; however, implementing this behaviour is not the focus of this ticket. This is to guide your implementation for the rest of the ticket because `_contained` and `_containedType` are so closely related. We can create a new ticket for implementing `_containedType`. + { + /* + * Case 5 + * When: we search for Observation with chain, `_contained=true`, and `_containedType=contained` + * - `_contained=true` means we should search and return only contained resources; not discrete resources + * - Note that we are searching by `/Observation?`, not `/Patient?` + * - `_containedType=contained` means we should return the contained resources; not the container resources + * - `Bundle.entry.fullUrl` points to the container resource first, and includes the required resolution for the contained resource + * - e.g. "http://localhost/fhir/context/DiagnosticReport/4#contained-observation" + * - `Bundle.entry.resource.id` includes only the required resolution for the contained resource + * - e.g. "contained-observation" + */ +/* + String queryUrl = myServerBase + "/Observation?subject.family=" + patientFamily + "&subject.given=" + patientGiven + "&_contained=true&_containedType=contained"; + + // Then: we should get just the contained Observation without the container DiagnosticReport + // - Note this case only makes assertions w.r.t. to contained Observation; the specification isn't clear + // about what to do about references to other contained resources. For example, if we were to add + // `&_include=Patient:patient` to the above query, what should the results be? + // - Presumably, two entries modelled similarly. The question is whether the reference from + // `Observation.subject` will still make sense within the searchset Bundle. + HttpGet get = new HttpGet(queryUrl); + try (CloseableHttpResponse response = ourHttpClient.execute(get)) { + String resp = IOUtils.toString(response.getEntity().getContent(), StandardCharsets.UTF_8); + Bundle bundle = myFhirContext.newXmlParser().parseResource(Bundle.class, resp); + ourLog.debug("\nOutput - Contained Observation as discrete result:\n{}", + myFhirContext.newJsonParser().setPrettyPrint(true).encodeResourceToString(bundle)); + // TODO Fails: we are incorrectly searching and returning the discrete Observations; should be contained Observation + assertThat(bundle.getEntry(), hasSize(1)); + // TODO Fails: we are incorrectly searching and returning the discrete Observations; should be contained Observation + assertThat(bundle.getEntryFirstRep().getResource().getIdElement().getIdPart(), is(equalTo(containedObservationId))); + // TODO Fails: we are incorrectly searching and returning the discrete Observations; should be contained Observation + assertThat(bundle.getEntryFirstRep().getFullUrl(), is(containsString(discreteDiagnosticReportId.getValueAsString() + "#" + containedObservationId))); + } + */ + } + } + @Test public void testContainedSearchByDate() throws Exception {