Merge pull request #5978

* Fix authorization handling for Bundle resources in the output. When t…

* Merge remote-tracking branch 'origin/master' into mm-20240529-test-se…

* Add test case for the described scenario

* Merge remote-tracking branch 'origin/master'

* Add Bundle search test with custom search parameters.

* Update test to simpler one to illustrate the problem

* SearchParameter for Bundle document referencing resource through comp…

* Merge remote-tracking branch 'origin/master' into mm-20240605-search-…

* Merge another test into the new class

* Remove unused json file. Small formatting fix in a test.

* Add more use cases and adjust an existing one
This commit is contained in:
Martha Mitran 2024-06-06 10:09:30 -07:00 committed by GitHub
parent 5302a7da52
commit 00a6591586
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
8 changed files with 382 additions and 280 deletions

View File

@ -776,7 +776,7 @@ public class BundleUtil {
continue;
}
if (isPlaceholderReference) {
if (theUrl.equals(next.getUrl())
if (theUrl.equals(next.getFullUrl())
|| theUrl.equals(nextResource.getIdElement().getValue())) {
return nextResource;
}

View File

@ -0,0 +1,9 @@
---
type: fix
issue: 5988
title: "Previously, when creating a custom SearchParameter for a document Bundle that references an entry resource
through the composition (e.g. Bundle.entry[0].resource.as(Composition).subject.resolve().as(Patient).identifier),
the search using that parameter would not return the matching document Bundle resources, despite the fullUrl of the entry matching the composite reference.
This problem does not exist if the reference match can be done against the entry id.
However an id is not required so the match should be done against the fullUrl.
This issue has been fixed."

View File

@ -655,15 +655,8 @@ public class ResourceLinkPredicateBuilder extends BaseJoiningPredicateBuilder im
public List<String> createResourceLinkPaths(
String theResourceName, String theParamName, List<String> theParamQualifiers) {
int linkIndex = theParamName.indexOf('.');
if (linkIndex == -1) {
RuntimeSearchParam param = mySearchParamRegistry.getActiveSearchParam(theResourceName, theParamName);
if (param == null) {
// This can happen during recursion, if not all the possible target types of one link in the chain
// support the next link
return new ArrayList<>();
}
RuntimeSearchParam param = mySearchParamRegistry.getActiveSearchParam(theResourceName, theParamName);
if (param != null) {
List<String> path = param.getPathsSplit();
/*
@ -681,40 +674,48 @@ public class ResourceLinkPredicateBuilder extends BaseJoiningPredicateBuilder im
}
return path;
} else {
}
boolean containsChain = theParamName.contains(".");
if (containsChain) {
int linkIndex = theParamName.indexOf('.');
String paramNameHead = theParamName.substring(0, linkIndex);
String paramNameTail = theParamName.substring(linkIndex + 1);
String qualifier = theParamQualifiers.get(0);
String qualifier = !theParamQualifiers.isEmpty() ? theParamQualifiers.get(0) : null;
List<String> nextQualifiersList = !theParamQualifiers.isEmpty()
? theParamQualifiers.subList(1, theParamQualifiers.size())
: List.of();
RuntimeSearchParam param = mySearchParamRegistry.getActiveSearchParam(theResourceName, paramNameHead);
if (param == null) {
// This can happen during recursion, if not all the possible target types of one link in the chain
// support the next link
return new ArrayList<>();
param = mySearchParamRegistry.getActiveSearchParam(theResourceName, paramNameHead);
if (param != null) {
Set<String> tailPaths = param.getTargets().stream()
.filter(t -> isBlank(qualifier) || qualifier.equals(t))
.map(t -> createResourceLinkPaths(t, paramNameTail, nextQualifiersList))
.flatMap(Collection::stream)
.map(t -> t.substring(t.indexOf('.') + 1))
.collect(Collectors.toSet());
List<String> path = param.getPathsSplit();
/*
* SearchParameters can declare paths on multiple resource
* types. Here we only want the ones that actually apply.
* Then append all the tail paths to each of the applicable head paths
*/
return path.stream()
.map(String::trim)
.filter(t -> t.startsWith(theResourceName + "."))
.map(head -> tailPaths.stream()
.map(tail -> head + "." + tail)
.collect(Collectors.toSet()))
.flatMap(Collection::stream)
.collect(Collectors.toList());
}
Set<String> tailPaths = param.getTargets().stream()
.filter(t -> isBlank(qualifier) || qualifier.equals(t))
.map(t -> createResourceLinkPaths(
t, paramNameTail, theParamQualifiers.subList(1, theParamQualifiers.size())))
.flatMap(Collection::stream)
.map(t -> t.substring(t.indexOf('.') + 1))
.collect(Collectors.toSet());
List<String> path = param.getPathsSplit();
/*
* SearchParameters can declare paths on multiple resource
* types. Here we only want the ones that actually apply.
* Then append all the tail paths to each of the applicable head paths
*/
return path.stream()
.map(String::trim)
.filter(t -> t.startsWith(theResourceName + "."))
.map(head ->
tailPaths.stream().map(tail -> head + "." + tail).collect(Collectors.toSet()))
.flatMap(Collection::stream)
.collect(Collectors.toList());
}
// This can happen during recursion, if not all the possible target types of one link in the chain
// support the next link
return new ArrayList<>();
}
private IQueryParameterType mapReferenceChainToRawParamType(

View File

@ -3,9 +3,6 @@ package ca.uhn.fhir.jpa.dao.r4;
import ca.uhn.fhir.context.RuntimeSearchParam;
import ca.uhn.fhir.i18n.Msg;
import ca.uhn.fhir.jpa.api.config.JpaStorageSettings;
import ca.uhn.fhir.jpa.api.dao.IFhirResourceDao;
import ca.uhn.fhir.jpa.searchparam.MatchUrlService;
import ca.uhn.fhir.jpa.searchparam.SearchParameterMap;
import ca.uhn.fhir.jpa.test.BaseJpaR4Test;
import ca.uhn.fhir.jpa.util.SqlQuery;
import ca.uhn.fhir.parser.StrictErrorHandler;
@ -13,12 +10,9 @@ import ca.uhn.fhir.rest.server.exceptions.InvalidRequestException;
import org.hl7.fhir.instance.model.api.IIdType;
import org.hl7.fhir.r4.model.AuditEvent;
import org.hl7.fhir.r4.model.Bundle;
import org.hl7.fhir.r4.model.CodeableConcept;
import org.hl7.fhir.r4.model.Coding;
import org.hl7.fhir.r4.model.Composition;
import org.hl7.fhir.r4.model.Device;
import org.hl7.fhir.r4.model.Encounter;
import org.hl7.fhir.r4.model.Enumerations;
import org.hl7.fhir.r4.model.IdType;
import org.hl7.fhir.r4.model.Location;
import org.hl7.fhir.r4.model.MessageHeader;
@ -27,32 +21,22 @@ import org.hl7.fhir.r4.model.Organization;
import org.hl7.fhir.r4.model.Patient;
import org.hl7.fhir.r4.model.Quantity;
import org.hl7.fhir.r4.model.Reference;
import org.hl7.fhir.r4.model.SearchParameter;
import org.hl7.fhir.r4.model.StringType;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Disabled;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.CsvSource;
import org.springframework.beans.factory.annotation.Autowired;
import java.sql.Date;
import java.util.List;
import static org.apache.commons.lang3.StringUtils.countMatches;
import static org.assertj.core.api.Assertions.assertThat;
import static org.junit.jupiter.api.Assertions.fail;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.fail;
import static org.junit.jupiter.api.Assertions.assertNotNull;
public class ChainingR4SearchTest extends BaseJpaR4Test {
@Autowired
MatchUrlService myMatchUrlService;
@AfterEach
public void after() throws Exception {
@ -359,7 +343,6 @@ public class ChainingR4SearchTest extends BaseJpaR4Test {
@Test
public void testShouldNotResolveATwoLinkChainWithAContainedResourceWhenContainedResourceIndexingIsTurnedOff() {
// setup
IIdType oid1;
{
Patient p = new Patient();
@ -843,8 +826,6 @@ public class ChainingR4SearchTest extends BaseJpaR4Test {
// setup
myStorageSettings.setIndexOnContainedResources(true);
IIdType oid1;
{
Organization org = new Organization();
org.setId("org");
@ -1575,79 +1556,6 @@ public class ChainingR4SearchTest extends BaseJpaR4Test {
countUnionStatementsInGeneratedQuery("/Observation?subject:Location.name=Smith", 1);
}
@ParameterizedTest
@CsvSource({
// search url expected count
"/Bundle?composition.patient.identifier=system|value-1&composition.patient.birthdate=1980-01-01, 1, correct identifier correct birthdate",
"/Bundle?composition.patient.birthdate=1980-01-01&composition.patient.identifier=system|value-1, 1, correct birthdate correct identifier",
"/Bundle?composition.patient.identifier=system|value-1&composition.patient.birthdate=2000-01-01, 0, correct identifier incorrect birthdate",
"/Bundle?composition.patient.birthdate=2000-01-01&composition.patient.identifier=system|value-1, 0, incorrect birthdate correct identifier",
"/Bundle?composition.patient.identifier=system|value-2&composition.patient.birthdate=1980-01-01, 0, incorrect identifier correct birthdate",
"/Bundle?composition.patient.birthdate=1980-01-01&composition.patient.identifier=system|value-2, 0, correct birthdate incorrect identifier",
"/Bundle?composition.patient.identifier=system|value-2&composition.patient.birthdate=2000-01-01, 0, incorrect identifier incorrect birthdate",
"/Bundle?composition.patient.birthdate=2000-01-01&composition.patient.identifier=system|value-2, 0, incorrect birthdate incorrect identifier",
// try sort by composition sp
"/Bundle?composition.patient.identifier=system|value-1&_sort=composition.patient.birthdate, 1, correct identifier sort by birthdate",
})
public void testMultipleChainedBundleCompositionSearchParameters(String theSearchUrl, int theExpectedCount, String theMessage) {
createSearchParameter("bundle-composition-patient-birthdate",
"composition.patient.birthdate",
"Bundle",
"Bundle.entry.resource.ofType(Patient).birthDate",
Enumerations.SearchParamType.DATE
);
createSearchParameter("bundle-composition-patient-identifier",
"composition.patient.identifier",
"Bundle",
"Bundle.entry.resource.ofType(Patient).identifier",
Enumerations.SearchParamType.TOKEN
);
createDocumentBundleWithPatientDetails("1980-01-01", "system", "value-1");
List<String> ids = myTestDaoSearch.searchForIds(theSearchUrl);
assertThat(ids).as(theMessage).hasSize(theExpectedCount);
}
private void createSearchParameter(String theId, String theCode, String theBase, String theExpression, Enumerations.SearchParamType theType) {
SearchParameter searchParameter = new SearchParameter();
searchParameter.setId(theId);
searchParameter.setCode(theCode);
searchParameter.setName(theCode);
searchParameter.setUrl("http://example.org/SearchParameter/" + theId);
searchParameter.setStatus(Enumerations.PublicationStatus.ACTIVE);
searchParameter.addBase(theBase);
searchParameter.setType(theType);
searchParameter.setExpression(theExpression);
searchParameter = (SearchParameter) mySearchParameterDao.update(searchParameter, mySrd).getResource();
mySearchParamRegistry.forceRefresh();
assertNotNull(mySearchParamRegistry.getActiveSearchParam(theBase, searchParameter.getName()));
}
private void createDocumentBundleWithPatientDetails(String theBirthDate, String theIdentifierSystem, String theIdentifierValue) {
Patient patient = new Patient();
patient.setBirthDate(Date.valueOf(theBirthDate));
patient.addIdentifier().setSystem(theIdentifierSystem).setValue(theIdentifierValue);
patient = (Patient) myPatientDao.create(patient, mySrd).getResource();
assertSearchReturns(myPatientDao, SearchParameterMap.newSynchronous(), 1);
Bundle bundle = new Bundle();
bundle.setType(Bundle.BundleType.DOCUMENT);
Composition composition = new Composition();
composition.setType(new CodeableConcept().addCoding(new Coding().setCode("code").setSystem("http://example.org")));
bundle.addEntry().setResource(composition);
composition.getSubject().setReference(patient.getIdElement().getValue());
bundle.addEntry().setResource(patient);
myBundleDao.create(bundle, mySrd);
assertSearchReturns(myBundleDao, SearchParameterMap.newSynchronous(), 1);
}
private void assertSearchReturns(IFhirResourceDao<?> theDao, SearchParameterMap theSearchParams, int theExpectedCount){
assertEquals(theExpectedCount, theDao.search(theSearchParams, mySrd).size());
}
private void countUnionStatementsInGeneratedQuery(String theUrl, int theExpectedNumberOfUnions) {
myCaptureQueriesListener.clear();
myTestDaoSearch.searchForIds(theUrl);

View File

@ -0,0 +1,246 @@
package ca.uhn.fhir.jpa.dao.r4;
import ca.uhn.fhir.jpa.api.model.DaoMethodOutcome;
import ca.uhn.fhir.jpa.searchparam.SearchParameterMap;
import ca.uhn.fhir.jpa.test.BaseJpaR4Test;
import ca.uhn.fhir.jpa.test.config.TestHSearchAddInConfig;
import ca.uhn.fhir.rest.api.server.IBundleProvider;
import ca.uhn.fhir.rest.param.ReferenceParam;
import ca.uhn.fhir.rest.param.TokenParam;
import org.hl7.fhir.instance.model.api.IBaseResource;
import org.hl7.fhir.instance.model.api.IIdType;
import org.hl7.fhir.r4.model.Bundle;
import org.hl7.fhir.r4.model.Composition;
import org.hl7.fhir.r4.model.Enumerations;
import org.hl7.fhir.r4.model.Patient;
import org.hl7.fhir.r4.model.Reference;
import org.hl7.fhir.r4.model.SearchParameter;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.CsvSource;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit.jupiter.SpringExtension;
import java.sql.Date;
import java.util.List;
import java.util.UUID;
import static org.assertj.core.api.Assertions.assertThat;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.junit.jupiter.api.Assertions.assertTrue;
@ExtendWith(SpringExtension.class)
@ContextConfiguration(classes = {TestHSearchAddInConfig.NoFT.class})
public class FhirResourceDaoR4SearchBundleNoFTTest extends BaseJpaR4Test {
@Test
public void searchDocumentBundle_withLocalReferenceUsingId_returnsCorrectly() {
createBundleSearchParameter("Bundle-composition-patient-identifier",
Enumerations.SearchParamType.TOKEN,
"composition.patient.identifier",
"Bundle.entry[0].resource.as(Composition).subject.resolve().as(Patient).identifier");
String patientId = "Patient/ABC";
String identifierSystem = "http://foo";
String identifierValue = "bar";
String patientUrl = "http://example.com/fhir/" + patientId;
Composition composition = new Composition();
composition.setSubject(new Reference(patientUrl));
Patient patient = new Patient();
patient.setId(patientId);
patient.addIdentifier().setSystem(identifierSystem).setValue(identifierValue);
Bundle bundle = new Bundle();
bundle.setType(Bundle.BundleType.DOCUMENT);
bundle.addEntry().setResource(composition);
bundle.addEntry().setResource(patient);
DaoMethodOutcome createOutcome = myBundleDao.create(bundle, mySrd);
assertTrue(createOutcome.getCreated());
IIdType bundleId = createOutcome.getId();
verifySearchCompositionPatientReturnsBundle(identifierSystem, identifierValue, bundleId);
}
@Test
public void searchDocumentBundle_withPlaceholderReferenceUsingFullUrl_returnsCorrectly() {
createBundleSearchParameter("Bundle-composition-patient-identifier",
Enumerations.SearchParamType.TOKEN,
"composition.patient.identifier",
"Bundle.entry[0].resource.as(Composition).subject.resolve().as(Patient).identifier");
String patientUrl = "urn:uuid:" + UUID.randomUUID();
String identifierSystem = "http://foo";
String identifierValue = "bar";
Composition composition = new Composition();
composition.setSubject(new Reference(patientUrl));
Patient patient = new Patient();
patient.addIdentifier().setSystem(identifierSystem).setValue(identifierValue);
Bundle bundle = new Bundle();
bundle.setType(Bundle.BundleType.DOCUMENT);
bundle.addEntry().setResource(composition);
bundle.addEntry().setFullUrl(patientUrl).setResource(patient);
DaoMethodOutcome createOutcome = myBundleDao.create(bundle, mySrd);
assertTrue(createOutcome.getCreated());
IIdType bundleId = createOutcome.getId();
verifySearchCompositionPatientReturnsBundle(identifierSystem, identifierValue, bundleId);
}
@Test
public void searchDocumentBundle_withPlaceholderReferenceUsingId_returnsCorrectly() {
createBundleSearchParameter("Bundle-composition-patient-identifier",
Enumerations.SearchParamType.TOKEN,
"composition.patient.identifier",
"Bundle.entry[0].resource.as(Composition).subject.resolve().as(Patient).identifier");
String patientId = "urn:uuid:" + UUID.randomUUID();
String identifierSystem = "http://foo";
String identifierValue = "bar";
Composition composition = new Composition();
composition.setSubject(new Reference(patientId));
Patient patient = new Patient();
patient.setId(patientId);
patient.addIdentifier().setSystem(identifierSystem).setValue(identifierValue);
Bundle bundle = new Bundle();
bundle.setType(Bundle.BundleType.DOCUMENT);
bundle.addEntry().setResource(composition);
bundle.addEntry().setResource(patient);
DaoMethodOutcome createOutcome = myBundleDao.create(bundle, mySrd);
assertTrue(createOutcome.getCreated());
IIdType bundleId = createOutcome.getId();
verifySearchCompositionPatientReturnsBundle(identifierSystem, identifierValue, bundleId);
}
@Test
public void searchDocumentBundle_withExternalReference_returnsCorrectly() {
String searchParamCode = "composition.subject";
createBundleSearchParameter("Bundle-composition-subject",
Enumerations.SearchParamType.REFERENCE,
searchParamCode,
"Bundle.entry[0].resource.as(Composition).subject");
String patientId = "Patient/ABC";
String identifierSystem = "http://foo";
String identifierValue = "bar";
Patient patient = new Patient();
patient.setId(patientId);
patient.addIdentifier().setSystem(identifierSystem).setValue(identifierValue);
DaoMethodOutcome createPatientOutcome = myPatientDao.update(patient, mySrd);
assertTrue(createPatientOutcome.getCreated());
Composition composition = new Composition();
composition.getSubject().setReference(createPatientOutcome.getId().toUnqualifiedVersionless().getValue());
Bundle bundle = new Bundle();
bundle.setType(Bundle.BundleType.DOCUMENT);
bundle.addEntry().setResource(composition);
DaoMethodOutcome createBundleOutcome = myBundleDao.create(bundle, mySrd);
assertTrue(createBundleOutcome.getCreated());
IIdType bundleId = createBundleOutcome.getId();
verifySearchReturnsBundle(SearchParameterMap.newSynchronous(searchParamCode, new ReferenceParam(patientId)), bundleId);
}
private void verifySearchCompositionPatientReturnsBundle(String theIdentifierSystem, String theIdentifierValue, IIdType theBundleId) {
final String systemAndValue = theIdentifierSystem + "|" + theIdentifierValue;
verifySearchReturnsBundle(SearchParameterMap.newSynchronous("composition.patient.identifier", new TokenParam(theIdentifierValue)), theBundleId);
verifySearchReturnsBundle(SearchParameterMap.newSynchronous("composition.patient.identifier", new TokenParam(theIdentifierSystem, theIdentifierValue)), theBundleId);
verifySearchReturnsBundle(SearchParameterMap.newSynchronous("composition", new ReferenceParam("patient.identifier", theIdentifierValue)), theBundleId);
verifySearchReturnsBundle(SearchParameterMap.newSynchronous("composition", new ReferenceParam("patient.identifier", systemAndValue)), theBundleId);
}
@ParameterizedTest
@CsvSource({
"/Bundle?composition.patient.identifier=system|value-1&composition.patient.birthdate=1980-01-01, true, correct identifier correct birthdate",
"/Bundle?composition.patient.birthdate=1980-01-01&composition.patient.identifier=system|value-1, true, correct birthdate correct identifier",
"/Bundle?composition.patient.identifier=system|value-1&composition.patient.birthdate=2000-01-01, false, correct identifier incorrect birthdate",
"/Bundle?composition.patient.birthdate=2000-01-01&composition.patient.identifier=system|value-1, false, incorrect birthdate correct identifier",
"/Bundle?composition.patient.identifier=system|value-2&composition.patient.birthdate=1980-01-01, false, incorrect identifier correct birthdate",
"/Bundle?composition.patient.birthdate=1980-01-01&composition.patient.identifier=system|value-2, false, correct birthdate incorrect identifier",
"/Bundle?composition.patient.identifier=system|value-2&composition.patient.birthdate=2000-01-01, false, incorrect identifier incorrect birthdate",
"/Bundle?composition.patient.birthdate=2000-01-01&composition.patient.identifier=system|value-2, false, incorrect birthdate incorrect identifier",
// try sort by composition sp
"/Bundle?composition.patient.identifier=system|value-1&_sort=composition.patient.birthdate, true, correct identifier sort by birthdate",
})
public void searchDocumentBundle_withExternalReferenceAndEntryCopy_returnsCorrectly(String theSearchUrl, boolean theShouldMatch, String theMessage) {
createBundleSearchParameter("bundle-composition-patient-birthdate",
Enumerations.SearchParamType.DATE,
"composition.patient.birthdate",
"Bundle.entry.resource.ofType(Patient).birthDate"
);
createBundleSearchParameter("bundle-composition-patient-identifier",
Enumerations.SearchParamType.TOKEN,
"composition.patient.identifier",
"Bundle.entry.resource.ofType(Patient).identifier"
);
String identifierSystem = "system";
String identifierValue = "value-1";
String birthDateString = "1980-01-01";
Patient patient = new Patient();
patient.setBirthDate(Date.valueOf(birthDateString));
patient.addIdentifier().setSystem(identifierSystem).setValue(identifierValue);
DaoMethodOutcome createPatientOutcome = myPatientDao.create(patient, mySrd);
assertTrue(createPatientOutcome.getCreated());
Composition composition = new Composition();
composition.setSubject(new Reference(createPatientOutcome.getId().getValue()));
Bundle bundle = new Bundle();
bundle.setType(Bundle.BundleType.DOCUMENT);
bundle.addEntry().setResource(composition);
bundle.addEntry().setResource(patient);
DaoMethodOutcome createBundleOutcome = myBundleDao.create(bundle, mySrd);
assertTrue(createBundleOutcome.getCreated());
IIdType bundleId = createBundleOutcome.getId();
List<String> ids = myTestDaoSearch.searchForIds(theSearchUrl);
if (theShouldMatch) {
assertThat(ids).as(theMessage).containsExactlyInAnyOrder(bundleId.getIdPart());
} else {
assertThat(ids).as(theMessage).hasSize(0);
}
}
private void createBundleSearchParameter(String id, Enumerations.SearchParamType theType, String theCode, String theExpression) {
SearchParameter sp = new SearchParameter()
.setCode(theCode)
.addBase("Bundle")
.setType(theType)
.setExpression(theExpression)
.setXpathUsage(SearchParameter.XPathUsageType.NORMAL)
.setStatus(Enumerations.PublicationStatus.ACTIVE);
sp.setId("SearchParameter/" + id);
sp.setUrl("http://example.com/fhir/" + sp.getId());
ourLog.info("SP: {}", myFhirContext.newJsonParser().setPrettyPrint(true).encodeResourceToString(sp));
IBaseResource resource = mySearchParameterDao.update(sp, mySrd).getResource();
assertNotNull(resource);
}
private void verifySearchReturnsBundle(SearchParameterMap theSearchParameterMap, IIdType theBundleId) {
IBundleProvider searchOutcome = myBundleDao.search(theSearchParameterMap, mySrd);
assertEquals(1, searchOutcome.size());
assertEquals(theBundleId, searchOutcome.getAllResources().get(0).getIdElement());
}
}

View File

@ -1,6 +1,5 @@
package ca.uhn.fhir.jpa.dao.r4;
import static org.junit.jupiter.api.Assertions.assertEquals;
import ca.uhn.fhir.batch2.jobs.reindex.ReindexAppCtx;
import ca.uhn.fhir.batch2.model.JobInstance;
import ca.uhn.fhir.context.RuntimeSearchParam;
@ -27,7 +26,6 @@ import ca.uhn.fhir.rest.server.exceptions.InvalidRequestException;
import ca.uhn.fhir.rest.server.exceptions.UnprocessableEntityException;
import ca.uhn.fhir.util.ClasspathUtil;
import ca.uhn.fhir.util.HapiExtensions;
import org.hamcrest.Matchers;
import org.hl7.fhir.instance.model.api.IIdType;
import org.hl7.fhir.r4.model.Appointment;
import org.hl7.fhir.r4.model.Appointment.AppointmentStatus;
@ -36,13 +34,11 @@ import org.hl7.fhir.r4.model.ChargeItem;
import org.hl7.fhir.r4.model.CodeType;
import org.hl7.fhir.r4.model.CodeableConcept;
import org.hl7.fhir.r4.model.Coding;
import org.hl7.fhir.r4.model.Composition;
import org.hl7.fhir.r4.model.Condition;
import org.hl7.fhir.r4.model.DateTimeType;
import org.hl7.fhir.r4.model.DateType;
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.Enumerations;
import org.hl7.fhir.r4.model.Enumerations.AdministrativeGender;
import org.hl7.fhir.r4.model.Extension;
@ -74,9 +70,8 @@ import java.util.stream.Collectors;
import static org.apache.commons.lang3.StringUtils.countMatches;
import static org.assertj.core.api.Assertions.assertThat;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.fail;
import static org.junit.jupiter.api.Assertions.fail;
import static org.mockito.Mockito.any;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.times;
@ -304,48 +299,6 @@ public class FhirResourceDaoR4SearchCustomSearchParamTest extends BaseJpaR4Test
mySearchParamRegistry.forceRefresh();
}
@Test
public void testBundleComposition() {
SearchParameter fooSp = new SearchParameter();
fooSp.setCode("foo");
fooSp.addBase("Bundle");
fooSp.setType(Enumerations.SearchParamType.REFERENCE);
fooSp.setTitle("FOO SP");
fooSp.setExpression("Bundle.entry[0].resource.as(Composition).encounter");
fooSp.setXpathUsage(org.hl7.fhir.r4.model.SearchParameter.XPathUsageType.NORMAL);
fooSp.setStatus(org.hl7.fhir.r4.model.Enumerations.PublicationStatus.ACTIVE);
ourLog.debug(myFhirContext.newJsonParser().setPrettyPrint(true).encodeResourceToString(fooSp));
mySearchParameterDao.create(fooSp, mySrd);
mySearchParamRegistry.forceRefresh();
Encounter enc = new Encounter();
enc.setStatus(Encounter.EncounterStatus.ARRIVED);
String encId = myEncounterDao.create(enc).getId().toUnqualifiedVersionless().getValue();
Composition composition = new Composition();
composition.getEncounter().setReference(encId);
Bundle bundle = new Bundle();
bundle.setType(Bundle.BundleType.DOCUMENT);
bundle.addEntry().setResource(composition);
ourLog.debug(myFhirContext.newJsonParser().setPrettyPrint(true).encodeResourceToString(bundle));
String bundleId = myBundleDao.create(bundle).getId().toUnqualifiedVersionless().getValue();
SearchParameterMap map;
map = new SearchParameterMap();
map.setLoadSynchronous(true);
map.add("foo", new ReferenceParam(encId));
IBundleProvider results = myBundleDao.search(map);
assertThat(toUnqualifiedVersionlessIdValues(results)).contains(bundleId);
}
@Test
public void testCreateInvalidUnquotedExtensionUrl() {
String invalidExpression = "Patient.extension.where(url=http://foo).value";

View File

@ -1,9 +1,5 @@
package ca.uhn.fhir.jpa.dao.r4;
import static org.assertj.core.api.Assertions.assertThatThrownBy;
import static org.junit.jupiter.api.Assertions.assertTrue;
import static org.junit.jupiter.api.Assertions.assertFalse;
import ca.uhn.fhir.context.RuntimeResourceDefinition;
import ca.uhn.fhir.i18n.Msg;
import ca.uhn.fhir.interceptor.api.HookParams;
@ -89,7 +85,6 @@ import org.hl7.fhir.r4.model.CodeableConcept;
import org.hl7.fhir.r4.model.Coding;
import org.hl7.fhir.r4.model.Communication;
import org.hl7.fhir.r4.model.CommunicationRequest;
import org.hl7.fhir.r4.model.Composition;
import org.hl7.fhir.r4.model.Condition;
import org.hl7.fhir.r4.model.ContactPoint.ContactPointSystem;
import org.hl7.fhir.r4.model.DateTimeType;
@ -144,8 +139,6 @@ import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Nested;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.CsvSource;
import org.mockito.ArgumentCaptor;
import org.mockito.ArgumentMatchers;
import org.springframework.beans.factory.annotation.Autowired;
@ -184,12 +177,13 @@ import static ca.uhn.fhir.util.DateUtils.convertDateToIso8601String;
import static org.apache.commons.lang3.StringUtils.countMatches;
import static org.apache.commons.lang3.StringUtils.leftPad;
import static org.assertj.core.api.Assertions.assertThat;
import static org.junit.jupiter.api.Assertions.fail;
import static org.assertj.core.api.Assertions.assertThatThrownBy;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.junit.jupiter.api.Assertions.assertNull;
import static org.junit.jupiter.api.Assertions.assertTrue;
import static org.junit.jupiter.api.Assertions.fail;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
@ -5733,95 +5727,6 @@ public class FhirResourceDaoR4SearchNoFtTest extends BaseJpaR4Test {
}
}
/**
* Index for
* [base]/Bundle?composition.patient.identifier=foo
*/
@ParameterizedTest
@CsvSource({
"true , urn:uuid:5c34dc2c-9b5d-4ec1-b30b-3e2d4371508b , urn:uuid:5c34dc2c-9b5d-4ec1-b30b-3e2d4371508b",
"false, urn:uuid:5c34dc2c-9b5d-4ec1-b30b-3e2d4371508b , urn:uuid:5c34dc2c-9b5d-4ec1-b30b-3e2d4371508b",
"true , Patient/ABC , Patient/ABC ",
"false, Patient/ABC , Patient/ABC ",
"true , Patient/ABC , http://example.com/fhir/Patient/ABC ",
"false, Patient/ABC , http://example.com/fhir/Patient/ABC ",
})
public void testCreateAndSearchForFullyChainedSearchParameter(boolean theUseFullChainInName, String thePatientId, String theFullUrl) {
// Setup 1
myStorageSettings.setIndexMissingFields(JpaStorageSettings.IndexEnabledEnum.DISABLED);
SearchParameter sp = new SearchParameter();
sp.setId("SearchParameter/Bundle-composition-patient-identifier");
sp.setCode("composition.patient.identifier");
sp.setName("composition.patient.identifier");
sp.setUrl("http://example.org/SearchParameter/Bundle-composition-patient-identifier");
sp.setStatus(Enumerations.PublicationStatus.ACTIVE);
sp.setType(Enumerations.SearchParamType.TOKEN);
sp.setExpression("Bundle.entry[0].resource.as(Composition).subject.resolve().as(Patient).identifier");
sp.addBase("Bundle");
ourLog.info("SP: {}", myFhirContext.newJsonParser().setPrettyPrint(true).encodeResourceToString(sp));
mySearchParameterDao.update(sp, mySrd);
mySearchParamRegistry.forceRefresh();
// Test 1
Composition composition = new Composition();
composition.setSubject(new Reference(thePatientId));
Patient patient = new Patient();
patient.setId(new IdType(theFullUrl));
patient.addIdentifier().setSystem("http://foo").setValue("bar");
Bundle bundle = new Bundle();
bundle.setType(Bundle.BundleType.DOCUMENT);
bundle
.addEntry()
.setResource(composition);
bundle
.addEntry()
.setFullUrl(theFullUrl)
.setResource(patient);
myBundleDao.create(bundle, mySrd);
Bundle bundle2 = new Bundle();
bundle2.setType(Bundle.BundleType.DOCUMENT);
myBundleDao.create(bundle2, mySrd);
// Test
SearchParameterMap map;
if (theUseFullChainInName) {
map = SearchParameterMap.newSynchronous("composition.patient.identifier", new TokenParam("http://foo", "bar"));
} else {
map = SearchParameterMap.newSynchronous("composition", new ReferenceParam("patient.identifier", "http://foo|bar"));
}
IBundleProvider outcome = myBundleDao.search(map, mySrd);
// Verify
List<String> params = extractAllTokenIndexes();
assertThat(params).as(params.toString()).containsExactlyInAnyOrder("composition.patient.identifier http://foo|bar");
assertEquals(1, outcome.size());
}
private List<String> extractAllTokenIndexes() {
List<String> params = runInTransaction(() -> {
logAllTokenIndexes();
return myResourceIndexedSearchParamTokenDao
.findAll()
.stream()
.filter(t -> t.getParamName().contains("."))
.map(t -> t.getParamName() + " " + t.getSystem() + "|" + t.getValue())
.toList();
});
return params;
}
@Nested
public class TagBelowTests {

View File

@ -1,6 +1,7 @@
package ca.uhn.fhir.jpa.search.builder.predicate;
import ca.uhn.fhir.context.FhirContext;
import ca.uhn.fhir.context.RuntimeSearchParam;
import ca.uhn.fhir.interceptor.model.RequestPartitionId;
import ca.uhn.fhir.jpa.api.svc.IIdHelperService;
import ca.uhn.fhir.jpa.search.builder.sql.SearchQueryBuilder;
@ -15,6 +16,8 @@ import com.healthmarketscience.sqlbuilder.InCondition;
import com.healthmarketscience.sqlbuilder.dbspec.basic.DbSchema;
import com.healthmarketscience.sqlbuilder.dbspec.basic.DbSpec;
import com.healthmarketscience.sqlbuilder.dbspec.basic.DbTable;
import org.hamcrest.MatcherAssert;
import org.hamcrest.Matchers;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
@ -27,11 +30,13 @@ import java.util.Collections;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.UUID;
import static org.assertj.core.api.Assertions.assertThatThrownBy;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.mockito.ArgumentMatchers.anyCollection;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;
@ExtendWith(MockitoExtension.class)
@ -48,7 +53,7 @@ public class ResourceLinkPredicateBuilderTest {
private ISearchParamRegistry mySearchParamRegistry;
@Mock
private IIdHelperService myIdHelperService;
private IIdHelperService<?> myIdHelperService;
@BeforeEach
public void init() {
@ -64,14 +69,14 @@ public class ResourceLinkPredicateBuilderTest {
@Test
public void createEverythingPredicate_withListOfPids_returnsInPredicate() {
when(myResourceLinkPredicateBuilder.generatePlaceholders(anyCollection())).thenReturn(List.of(PLACEHOLDER_BASE + "1", PLACEHOLDER_BASE + "2"));
Condition condition = myResourceLinkPredicateBuilder.createEverythingPredicate("Patient", new ArrayList<>(), 1l, 2l);
Condition condition = myResourceLinkPredicateBuilder.createEverythingPredicate("Patient", new ArrayList<>(), 1L, 2L);
assertEquals(InCondition.class, condition.getClass());
}
@Test
public void createEverythingPredicate_withSinglePid_returnsInCondition() {
when(myResourceLinkPredicateBuilder.generatePlaceholders(anyCollection())).thenReturn(List.of(PLACEHOLDER_BASE + "1"));
Condition condition = myResourceLinkPredicateBuilder.createEverythingPredicate("Patient", new ArrayList<>(), 1l);
Condition condition = myResourceLinkPredicateBuilder.createEverythingPredicate("Patient", new ArrayList<>(), 1L);
assertEquals(BinaryCondition.class, condition.getClass());
}
@ -100,4 +105,79 @@ public class ResourceLinkPredicateBuilderTest {
.hasMessage("HAPI-2498: Unsupported search modifier(s): \"[:identifier, :x, :y]\" for resource type \"Observation\". Valid search modifiers are: [:contains, :exact, :in, :iterate, :missing, :not-in, :of-type, :recurse, :text]");
}
@Test
public void createResourceLinkPaths_withoutChainAndSearchParameterFoundNoQualifiers_returnsFilteredPaths() {
String paramName = "param.name";
String resourceType = "Bundle";
RuntimeSearchParam mockSearchParam = mock(RuntimeSearchParam.class);
when(mockSearchParam.getPathsSplit()).thenReturn(List.of("Patient.given", "Bundle.composition.subject", "Bundle.type"));
when(mySearchParamRegistry.getActiveSearchParam(resourceType, paramName)).thenReturn(mockSearchParam);
List<String> result = myResourceLinkPredicateBuilder.createResourceLinkPaths(resourceType, paramName, List.of());
MatcherAssert.assertThat(result, Matchers.containsInAnyOrder("Bundle.composition.subject", "Bundle.type"));
}
@Test
public void createResourceLinkPaths_withoutChainAndSearchParameterNotFoundNoQualifiers_returnsEmpty() {
String paramName = "param.name";
String resourceType = "Bundle";
List<String> result = myResourceLinkPredicateBuilder.createResourceLinkPaths(resourceType, paramName, List.of());
MatcherAssert.assertThat(result, Matchers.empty());
}
@Test
public void createResourceLinkPaths_withChainAndSearchParameterFoundNoQualifiers_returnsPath() {
String paramName = "subject.identifier";
String resourceType = "Observation";
when(mySearchParamRegistry.getActiveSearchParam("Observation", "subject.identifier")).thenReturn(null);
RuntimeSearchParam observationSubjectSP = mock(RuntimeSearchParam.class);
when(observationSubjectSP.getPathsSplit()).thenReturn(List.of("Observation.subject"));
when(observationSubjectSP.getTargets()).thenReturn(Set.of("Patient"));
when(mySearchParamRegistry.getActiveSearchParam("Observation", "subject")).thenReturn(observationSubjectSP);
RuntimeSearchParam patientIdentifierSP = mock(RuntimeSearchParam.class);
when(patientIdentifierSP.getPathsSplit()).thenReturn(List.of("Patient.identifier"));
when(mySearchParamRegistry.getActiveSearchParam("Patient", "identifier")).thenReturn(patientIdentifierSP);
List<String> result = myResourceLinkPredicateBuilder.createResourceLinkPaths(resourceType, paramName, List.of());
MatcherAssert.assertThat(result, Matchers.containsInAnyOrder("Observation.subject.identifier"));
}
@Test
public void createResourceLinkPaths_withChainAndSearchParameterFoundWithQualifiers_returnsPath() {
String paramName = "subject.managingOrganization.identifier";
String resourceType = "Observation";
when(mySearchParamRegistry.getActiveSearchParam("Observation", "subject.managingOrganization.identifier")).thenReturn(null);
RuntimeSearchParam observationSubjectSP = mock(RuntimeSearchParam.class);
when(observationSubjectSP.getPathsSplit()).thenReturn(List.of("Observation.subject"));
when(observationSubjectSP.getTargets()).thenReturn(Set.of("Patient"));
when(mySearchParamRegistry.getActiveSearchParam("Observation", "subject")).thenReturn(observationSubjectSP);
when(mySearchParamRegistry.getActiveSearchParam("Patient", "managingOrganization.identifier")).thenReturn(null);
RuntimeSearchParam organizationSP = mock(RuntimeSearchParam.class);
when(organizationSP.getPathsSplit()).thenReturn(List.of("Patient.managingOrganization"));
when(organizationSP.getTargets()).thenReturn(Set.of("Organization"));
when(mySearchParamRegistry.getActiveSearchParam("Patient", "managingOrganization")).thenReturn(organizationSP);
RuntimeSearchParam organizationIdentifierSP = mock(RuntimeSearchParam.class);
when(organizationIdentifierSP.getPathsSplit()).thenReturn(List.of("Organization.identifier"));
when(mySearchParamRegistry.getActiveSearchParam("Organization", "identifier")).thenReturn(organizationIdentifierSP);
List<String> result = myResourceLinkPredicateBuilder.createResourceLinkPaths(resourceType, paramName, List.of("Patient", "Organization"));
MatcherAssert.assertThat(result, Matchers.containsInAnyOrder("Observation.subject.managingOrganization.identifier"));
}
@Test
public void createResourceLinkPaths_withChainAndSearchParameterFoundWithNonMatchingQualifier_returnsEmpty() {
String paramName = "subject.identifier";
String resourceType = "Observation";
when(mySearchParamRegistry.getActiveSearchParam("Observation", "subject.identifier")).thenReturn(null);
RuntimeSearchParam observationSubjectSP = mock(RuntimeSearchParam.class);
when(observationSubjectSP.getPathsSplit()).thenReturn(List.of("Observation.subject"));
when(observationSubjectSP.getTargets()).thenReturn(Set.of("Patient"));
when(mySearchParamRegistry.getActiveSearchParam("Observation", "subject")).thenReturn(observationSubjectSP);
List<String> result = myResourceLinkPredicateBuilder.createResourceLinkPaths(resourceType, paramName, List.of("Group"));
MatcherAssert.assertThat(result, Matchers.empty());
}
}