Jr 20211018 chained references 2 (#3099)
* Create index entries for outbound references of contained resources * build query for chained reference * fix case where the contained reference is an explicit id rather than a continued chain * fix contained index to use path names not search param names * make qualified search work * cleanup and changelog * recurse while creating indexes on contained resources * double link both contained * longer contained subchains * adding some failing test cases to illustrate the limitations of qualified searches * clean up merge cruft * changelog
This commit is contained in:
parent
d94284b2e8
commit
b267fdb752
|
@ -0,0 +1,8 @@
|
||||||
|
---
|
||||||
|
type: add
|
||||||
|
issue: 3100
|
||||||
|
jira: SMILE-3151
|
||||||
|
title: "Previously, only contained resources that are referenced directly by the containing resource were being indexed.
|
||||||
|
This enhancement indexes the fields of contained resources that are referenced by other contained resources and
|
||||||
|
uses these new indices in chained searches. Note: in order to make use of this new capability, it must be enabled via
|
||||||
|
a configuration parameter and the repository must be re-indexed."
|
|
@ -685,7 +685,7 @@ public class QueryStack {
|
||||||
}
|
}
|
||||||
|
|
||||||
public Condition createPredicateReferenceForContainedResource(@Nullable DbColumn theSourceJoinColumn,
|
public Condition createPredicateReferenceForContainedResource(@Nullable DbColumn theSourceJoinColumn,
|
||||||
String theResourceName, String theParamName, RuntimeSearchParam theSearchParam,
|
String theResourceName, String theParamName, List<String> theQualifiers, RuntimeSearchParam theSearchParam,
|
||||||
List<? extends IQueryParameterType> theList, SearchFilterParser.CompareOperation theOperation,
|
List<? extends IQueryParameterType> theList, SearchFilterParser.CompareOperation theOperation,
|
||||||
RequestDetails theRequest, RequestPartitionId theRequestPartitionId) {
|
RequestDetails theRequest, RequestPartitionId theRequestPartitionId) {
|
||||||
|
|
||||||
|
@ -760,7 +760,7 @@ public class QueryStack {
|
||||||
throw new InvalidRequestException("Unknown search parameter name: " + theSearchParam.getName() + ".");
|
throw new InvalidRequestException("Unknown search parameter name: " + theSearchParam.getName() + ".");
|
||||||
}
|
}
|
||||||
|
|
||||||
List<String> qualifiers= Collections.singletonList(headQualifier);
|
theQualifiers.add(headQualifier);
|
||||||
|
|
||||||
// 3. create the query
|
// 3. create the query
|
||||||
Condition containedCondition = null;
|
Condition containedCondition = null;
|
||||||
|
@ -796,7 +796,11 @@ public class QueryStack {
|
||||||
break;
|
break;
|
||||||
case REFERENCE:
|
case REFERENCE:
|
||||||
String chainedParamName = theParamName + "." + targetParamName;
|
String chainedParamName = theParamName + "." + targetParamName;
|
||||||
containedCondition = createPredicateReference(theSourceJoinColumn, theResourceName, chainedParamName, qualifiers, trimmedParameters, theOperation, theRequest, theRequestPartitionId);
|
containedCondition = createPredicateReference(theSourceJoinColumn, theResourceName, chainedParamName, theQualifiers, trimmedParameters, theOperation, theRequest, theRequestPartitionId);
|
||||||
|
if (myModelConfig.isIndexOnContainedResourcesRecursively()) {
|
||||||
|
containedCondition = toOrPredicate(containedCondition,
|
||||||
|
createPredicateReferenceForContainedResource(theSourceJoinColumn, theResourceName, chainedParamName, theQualifiers, theSearchParam, trimmedParameters, theOperation, theRequest, theRequestPartitionId));
|
||||||
|
}
|
||||||
break;
|
break;
|
||||||
case HAS:
|
case HAS:
|
||||||
case SPECIAL:
|
case SPECIAL:
|
||||||
|
@ -1140,11 +1144,11 @@ public class QueryStack {
|
||||||
// See SMILE-2898 for details.
|
// See SMILE-2898 for details.
|
||||||
// For now, leave the incorrect implementation alone, just in case someone is relying on it,
|
// For now, leave the incorrect implementation alone, just in case someone is relying on it,
|
||||||
// until the complete fix is available.
|
// until the complete fix is available.
|
||||||
andPredicates.add(createPredicateReferenceForContainedResource(null, theResourceName, theParamName, nextParamDef, nextAnd, null, theRequest, theRequestPartitionId));
|
andPredicates.add(createPredicateReferenceForContainedResource(null, theResourceName, theParamName, new ArrayList<>(), nextParamDef, nextAnd, null, theRequest, theRequestPartitionId));
|
||||||
} else if (isEligibleForContainedResourceSearch(nextAnd)) {
|
} else if (isEligibleForContainedResourceSearch(nextAnd)) {
|
||||||
andPredicates.add(toOrPredicate(
|
andPredicates.add(toOrPredicate(
|
||||||
createPredicateReference(theSourceJoinColumn, theResourceName, theParamName, new ArrayList<>(), nextAnd, null, theRequest, theRequestPartitionId),
|
createPredicateReference(theSourceJoinColumn, theResourceName, theParamName, new ArrayList<>(), nextAnd, null, theRequest, theRequestPartitionId),
|
||||||
createPredicateReferenceForContainedResource(theSourceJoinColumn, theResourceName, theParamName, nextParamDef, nextAnd, null, theRequest, theRequestPartitionId)
|
createPredicateReferenceForContainedResource(theSourceJoinColumn, theResourceName, theParamName, new ArrayList<>(), nextParamDef, nextAnd, null, theRequest, theRequestPartitionId)
|
||||||
));
|
));
|
||||||
} else {
|
} else {
|
||||||
andPredicates.add(createPredicateReference(theSourceJoinColumn, theResourceName, theParamName, new ArrayList<>(), nextAnd, null, theRequest, theRequestPartitionId));
|
andPredicates.add(createPredicateReference(theSourceJoinColumn, theResourceName, theParamName, new ArrayList<>(), nextAnd, null, theRequest, theRequestPartitionId));
|
||||||
|
|
|
@ -569,6 +569,10 @@ public class ResourceLinkPredicateBuilder extends BaseJoiningPredicateBuilder {
|
||||||
String qualifier = theParamQualifiers.get(0);
|
String qualifier = theParamQualifiers.get(0);
|
||||||
|
|
||||||
RuntimeSearchParam param = mySearchParamRegistry.getActiveSearchParam(theResourceName, paramNameHead);
|
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<>();
|
||||||
|
}
|
||||||
Set<String> tailPaths = param.getTargets().stream()
|
Set<String> tailPaths = param.getTargets().stream()
|
||||||
.filter(t -> isBlank(qualifier) || qualifier.equals(t))
|
.filter(t -> isBlank(qualifier) || qualifier.equals(t))
|
||||||
.map(t -> createResourceLinkPaths(t, paramNameTail, theParamQualifiers.subList(1, theParamQualifiers.size())))
|
.map(t -> createResourceLinkPaths(t, paramNameTail, theParamQualifiers.subList(1, theParamQualifiers.size())))
|
||||||
|
|
|
@ -10,12 +10,14 @@ import ca.uhn.fhir.rest.api.server.IBundleProvider;
|
||||||
import org.hl7.fhir.instance.model.api.IIdType;
|
import org.hl7.fhir.instance.model.api.IIdType;
|
||||||
import org.hl7.fhir.r4.model.Device;
|
import org.hl7.fhir.r4.model.Device;
|
||||||
import org.hl7.fhir.r4.model.IdType;
|
import org.hl7.fhir.r4.model.IdType;
|
||||||
|
import org.hl7.fhir.r4.model.Location;
|
||||||
import org.hl7.fhir.r4.model.Observation;
|
import org.hl7.fhir.r4.model.Observation;
|
||||||
import org.hl7.fhir.r4.model.Organization;
|
import org.hl7.fhir.r4.model.Organization;
|
||||||
import org.hl7.fhir.r4.model.Patient;
|
import org.hl7.fhir.r4.model.Patient;
|
||||||
import org.hl7.fhir.r4.model.StringType;
|
import org.hl7.fhir.r4.model.StringType;
|
||||||
import org.junit.jupiter.api.AfterEach;
|
import org.junit.jupiter.api.AfterEach;
|
||||||
import org.junit.jupiter.api.BeforeEach;
|
import org.junit.jupiter.api.BeforeEach;
|
||||||
|
import org.junit.jupiter.api.Disabled;
|
||||||
import org.junit.jupiter.api.Test;
|
import org.junit.jupiter.api.Test;
|
||||||
import org.springframework.beans.factory.annotation.Autowired;
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
|
|
||||||
|
@ -46,6 +48,7 @@ public class ChainingR4SearchTest extends BaseJpaR4Test {
|
||||||
|
|
||||||
myModelConfig.setIndexOnContainedResources(false);
|
myModelConfig.setIndexOnContainedResources(false);
|
||||||
myModelConfig.setIndexOnContainedResources(new ModelConfig().isIndexOnContainedResources());
|
myModelConfig.setIndexOnContainedResources(new ModelConfig().isIndexOnContainedResources());
|
||||||
|
myModelConfig.setIndexOnContainedResourcesRecursively(new ModelConfig().isIndexOnContainedResourcesRecursively());
|
||||||
}
|
}
|
||||||
|
|
||||||
@BeforeEach
|
@BeforeEach
|
||||||
|
@ -116,6 +119,50 @@ public class ChainingR4SearchTest extends BaseJpaR4Test {
|
||||||
assertThat(oids, contains(oid1.getIdPart()));
|
assertThat(oids, contains(oid1.getIdPart()));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@Disabled
|
||||||
|
public void testShouldResolveATwoLinkChainWithQualifiersWithAContainedResource() throws Exception {
|
||||||
|
// TODO: This test fails because of a known limitation in qualified searches over contained resources.
|
||||||
|
// Type information for intermediate resources in the chain is not being retained in the indexes.
|
||||||
|
// setup
|
||||||
|
IIdType oid1;
|
||||||
|
|
||||||
|
{
|
||||||
|
Patient p = new Patient();
|
||||||
|
p.setId("pat");
|
||||||
|
p.addName().setFamily("Smith").addGiven("John");
|
||||||
|
|
||||||
|
Observation obs = new Observation();
|
||||||
|
obs.getContained().add(p);
|
||||||
|
obs.getCode().setText("Observation 1");
|
||||||
|
obs.setValue(new StringType("Test"));
|
||||||
|
obs.getSubject().setReference("#pat");
|
||||||
|
|
||||||
|
oid1 = myObservationDao.create(obs, mySrd).getId().toUnqualifiedVersionless();
|
||||||
|
|
||||||
|
Location loc = new Location();
|
||||||
|
loc.setId("loc");
|
||||||
|
loc.setName("Smith");
|
||||||
|
|
||||||
|
Observation obs2 = new Observation();
|
||||||
|
obs2.getContained().add(loc);
|
||||||
|
obs2.getCode().setText("Observation 2");
|
||||||
|
obs2.setValue(new StringType("Test"));
|
||||||
|
obs2.getSubject().setReference("#loc");
|
||||||
|
|
||||||
|
myObservationDao.create(obs2, mySrd);
|
||||||
|
}
|
||||||
|
|
||||||
|
String url = "/Observation?subject:Patient.name=Smith";
|
||||||
|
|
||||||
|
// execute
|
||||||
|
List<String> oids = searchAndReturnUnqualifiedVersionlessIdValues(url);
|
||||||
|
|
||||||
|
// validate
|
||||||
|
assertEquals(1L, oids.size());
|
||||||
|
assertThat(oids, contains(oid1.getIdPart()));
|
||||||
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void testShouldResolveATwoLinkChainToAContainedReference() throws Exception {
|
public void testShouldResolveATwoLinkChainToAContainedReference() throws Exception {
|
||||||
// Adding support for this case in SMILE-3151
|
// Adding support for this case in SMILE-3151
|
||||||
|
@ -260,6 +307,45 @@ public class ChainingR4SearchTest extends BaseJpaR4Test {
|
||||||
assertThat(oids, contains(oid1.getIdPart()));
|
assertThat(oids, contains(oid1.getIdPart()));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testShouldResolveAThreeLinkChainWithAllContainedResources() throws Exception {
|
||||||
|
|
||||||
|
// setup
|
||||||
|
myModelConfig.setIndexOnContainedResourcesRecursively(true);
|
||||||
|
|
||||||
|
IIdType oid1;
|
||||||
|
|
||||||
|
{
|
||||||
|
Organization org = new Organization();
|
||||||
|
org.setId("org");
|
||||||
|
org.setName("HealthCo");
|
||||||
|
|
||||||
|
Patient p = new Patient();
|
||||||
|
p.setId("pat");
|
||||||
|
p.addName().setFamily("Smith").addGiven("John");
|
||||||
|
p.getManagingOrganization().setReference("#org");
|
||||||
|
|
||||||
|
Observation obs = new Observation();
|
||||||
|
obs.getContained().add(p);
|
||||||
|
obs.getContained().add(org);
|
||||||
|
obs.getCode().setText("Observation 1");
|
||||||
|
obs.getSubject().setReference("#pat");
|
||||||
|
|
||||||
|
oid1 = myObservationDao.create(obs, mySrd).getId().toUnqualifiedVersionless();
|
||||||
|
}
|
||||||
|
|
||||||
|
String url = "/Observation?subject.organization.name=HealthCo";
|
||||||
|
|
||||||
|
// execute
|
||||||
|
myCaptureQueriesListener.clear();
|
||||||
|
List<String> oids = searchAndReturnUnqualifiedVersionlessIdValues(url);
|
||||||
|
myCaptureQueriesListener.logSelectQueries();
|
||||||
|
|
||||||
|
// validate
|
||||||
|
assertEquals(1L, oids.size());
|
||||||
|
assertThat(oids, contains(oid1.getIdPart()));
|
||||||
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void testShouldResolveAThreeLinkChainWithQualifiersWhereAllResourcesStandAlone() throws Exception {
|
public void testShouldResolveAThreeLinkChainWithQualifiersWhereAllResourcesStandAlone() throws Exception {
|
||||||
|
|
||||||
|
@ -405,6 +491,117 @@ public class ChainingR4SearchTest extends BaseJpaR4Test {
|
||||||
assertThat(oids, contains(oid1.getIdPart()));
|
assertThat(oids, contains(oid1.getIdPart()));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@Disabled
|
||||||
|
public void testShouldResolveAThreeLinkChainWithQualifiersWithAContainedResourceAtTheBeginning_NotDistinctSourcePaths() throws Exception {
|
||||||
|
// TODO: This test fails because of a known limitation in qualified searches over contained resources.
|
||||||
|
// Type information for intermediate resources in the chain is not being retained in the indexes.
|
||||||
|
|
||||||
|
// Adding support for this case in SMILE-3151
|
||||||
|
|
||||||
|
// setup
|
||||||
|
IIdType oid1;
|
||||||
|
|
||||||
|
{
|
||||||
|
Organization org = new Organization();
|
||||||
|
org.setId(IdType.newRandomUuid());
|
||||||
|
org.setName("HealthCo");
|
||||||
|
myOrganizationDao.create(org, mySrd);
|
||||||
|
|
||||||
|
Patient p = new Patient();
|
||||||
|
p.setId("pat");
|
||||||
|
p.addName().setFamily("Smith").addGiven("John");
|
||||||
|
p.getManagingOrganization().setReference(org.getId());
|
||||||
|
|
||||||
|
Observation obs = new Observation();
|
||||||
|
obs.getContained().add(p);
|
||||||
|
obs.getCode().setText("Observation 1");
|
||||||
|
obs.getSubject().setReference("#pat");
|
||||||
|
|
||||||
|
oid1 = myObservationDao.create(obs, mySrd).getId().toUnqualifiedVersionless();
|
||||||
|
|
||||||
|
Location loc = new Location();
|
||||||
|
loc.setId("loc");
|
||||||
|
loc.getManagingOrganization().setReference(org.getId());
|
||||||
|
|
||||||
|
Observation obs2 = new Observation();
|
||||||
|
obs2.getContained().add(loc);
|
||||||
|
obs2.getCode().setText("Observation 2");
|
||||||
|
obs2.getSubject().setReference("#loc");
|
||||||
|
|
||||||
|
myObservationDao.create(obs2, mySrd);
|
||||||
|
}
|
||||||
|
|
||||||
|
String url = "/Observation?subject:Patient.organization:Organization.name=HealthCo";
|
||||||
|
|
||||||
|
// execute
|
||||||
|
myCaptureQueriesListener.clear();
|
||||||
|
List<String> oids = searchAndReturnUnqualifiedVersionlessIdValues(url);
|
||||||
|
myCaptureQueriesListener.logSelectQueries();
|
||||||
|
|
||||||
|
// validate
|
||||||
|
assertEquals(1L, oids.size());
|
||||||
|
assertThat(oids, contains(oid1.getIdPart()));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@Disabled
|
||||||
|
public void testShouldResolveAThreeLinkChainWithQualifiersWithAllContainedResources() throws Exception {
|
||||||
|
// TODO: This test fails because of a known limitation in qualified searches over contained resources.
|
||||||
|
// Type information for intermediate resources in the chain is not being retained in the indexes.
|
||||||
|
|
||||||
|
// setup
|
||||||
|
myModelConfig.setIndexOnContainedResourcesRecursively(true);
|
||||||
|
|
||||||
|
IIdType oid1;
|
||||||
|
|
||||||
|
{
|
||||||
|
Organization org = new Organization();
|
||||||
|
org.setId("org");
|
||||||
|
org.setName("HealthCo");
|
||||||
|
|
||||||
|
Patient p = new Patient();
|
||||||
|
p.setId("pat");
|
||||||
|
p.addName().setFamily("Smith").addGiven("John");
|
||||||
|
p.getManagingOrganization().setReference("#org");
|
||||||
|
|
||||||
|
Observation obs = new Observation();
|
||||||
|
obs.getContained().add(p);
|
||||||
|
obs.getContained().add(org);
|
||||||
|
obs.getCode().setText("Observation 1");
|
||||||
|
obs.getSubject().setReference("#pat");
|
||||||
|
|
||||||
|
oid1 = myObservationDao.create(obs, mySrd).getId().toUnqualifiedVersionless();
|
||||||
|
|
||||||
|
Organization org2 = new Organization();
|
||||||
|
org2.setId("org");
|
||||||
|
org2.setName("HealthCo");
|
||||||
|
|
||||||
|
Device d = new Device();
|
||||||
|
d.setId("dev");
|
||||||
|
d.getOwner().setReference("#org");
|
||||||
|
|
||||||
|
Observation obs2 = new Observation();
|
||||||
|
obs2.getContained().add(d);
|
||||||
|
obs2.getContained().add(org2);
|
||||||
|
obs2.getCode().setText("Observation 2");
|
||||||
|
obs2.getSubject().setReference("#dev");
|
||||||
|
|
||||||
|
myObservationDao.create(obs2, mySrd);
|
||||||
|
}
|
||||||
|
|
||||||
|
String url = "/Observation?subject:Patient.organization:Organization.name=HealthCo";
|
||||||
|
|
||||||
|
// execute
|
||||||
|
myCaptureQueriesListener.clear();
|
||||||
|
List<String> oids = searchAndReturnUnqualifiedVersionlessIdValues(url);
|
||||||
|
myCaptureQueriesListener.logSelectQueries();
|
||||||
|
|
||||||
|
// validate
|
||||||
|
assertEquals(1L, oids.size());
|
||||||
|
assertThat(oids, contains(oid1.getIdPart()));
|
||||||
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void testShouldResolveAFourLinkChainWhereAllResourcesStandAlone() throws Exception {
|
public void testShouldResolveAFourLinkChainWhereAllResourcesStandAlone() throws Exception {
|
||||||
|
|
||||||
|
@ -485,6 +682,47 @@ public class ChainingR4SearchTest extends BaseJpaR4Test {
|
||||||
assertThat(oids, contains(oid1.getIdPart()));
|
assertThat(oids, contains(oid1.getIdPart()));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testShouldResolveAFourLinkChainWhereTheLastTwoReferencesAreContained() throws Exception {
|
||||||
|
|
||||||
|
// setup
|
||||||
|
myModelConfig.setIndexOnContainedResourcesRecursively(true);
|
||||||
|
IIdType oid1;
|
||||||
|
|
||||||
|
{
|
||||||
|
Organization org = new Organization();
|
||||||
|
org.setId("parent");
|
||||||
|
org.setName("HealthCo");
|
||||||
|
|
||||||
|
Organization partOfOrg = new Organization();
|
||||||
|
partOfOrg.setId("child");
|
||||||
|
partOfOrg.getPartOf().setReference("#parent");
|
||||||
|
|
||||||
|
Patient p = new Patient();
|
||||||
|
p.getContained().add(org);
|
||||||
|
p.getContained().add(partOfOrg);
|
||||||
|
p.setId(IdType.newRandomUuid());
|
||||||
|
p.addName().setFamily("Smith").addGiven("John");
|
||||||
|
p.getManagingOrganization().setReference("#child");
|
||||||
|
myPatientDao.create(p, mySrd);
|
||||||
|
|
||||||
|
Observation obs = new Observation();
|
||||||
|
obs.getCode().setText("Observation 1");
|
||||||
|
obs.getSubject().setReference(p.getId());
|
||||||
|
|
||||||
|
oid1 = myObservationDao.create(obs, mySrd).getId().toUnqualifiedVersionless();
|
||||||
|
}
|
||||||
|
|
||||||
|
String url = "/Observation?subject.organization.partof.name=HealthCo";
|
||||||
|
|
||||||
|
// execute
|
||||||
|
List<String> oids = searchAndReturnUnqualifiedVersionlessIdValues(url);
|
||||||
|
|
||||||
|
// validate
|
||||||
|
assertEquals(1L, oids.size());
|
||||||
|
assertThat(oids, contains(oid1.getIdPart()));
|
||||||
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void testShouldResolveAFourLinkChainWithAContainedResourceInTheMiddle() throws Exception {
|
public void testShouldResolveAFourLinkChainWithAContainedResourceInTheMiddle() throws Exception {
|
||||||
|
|
||||||
|
@ -531,6 +769,47 @@ public class ChainingR4SearchTest extends BaseJpaR4Test {
|
||||||
assertThat(oids, contains(oid1.getIdPart()));
|
assertThat(oids, contains(oid1.getIdPart()));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testShouldResolveAFourLinkChainWhereAllReferencesAreContained() throws Exception {
|
||||||
|
|
||||||
|
// setup
|
||||||
|
myModelConfig.setIndexOnContainedResourcesRecursively(true);
|
||||||
|
IIdType oid1;
|
||||||
|
|
||||||
|
{
|
||||||
|
Organization org = new Organization();
|
||||||
|
org.setId("parent");
|
||||||
|
org.setName("HealthCo");
|
||||||
|
|
||||||
|
Organization partOfOrg = new Organization();
|
||||||
|
partOfOrg.setId("child");
|
||||||
|
partOfOrg.getPartOf().setReference("#parent");
|
||||||
|
|
||||||
|
Patient p = new Patient();
|
||||||
|
p.setId("pat");
|
||||||
|
p.addName().setFamily("Smith").addGiven("John");
|
||||||
|
p.getManagingOrganization().setReference("#child");
|
||||||
|
|
||||||
|
Observation obs = new Observation();
|
||||||
|
obs.getContained().add(org);
|
||||||
|
obs.getContained().add(partOfOrg);
|
||||||
|
obs.getContained().add(p);
|
||||||
|
obs.getCode().setText("Observation 1");
|
||||||
|
obs.getSubject().setReference("#pat");
|
||||||
|
|
||||||
|
oid1 = myObservationDao.create(obs, mySrd).getId().toUnqualifiedVersionless();
|
||||||
|
}
|
||||||
|
|
||||||
|
String url = "/Observation?subject.organization.partof.name=HealthCo";
|
||||||
|
|
||||||
|
// execute
|
||||||
|
List<String> oids = searchAndReturnUnqualifiedVersionlessIdValues(url);
|
||||||
|
|
||||||
|
// validate
|
||||||
|
assertEquals(1L, oids.size());
|
||||||
|
assertThat(oids, contains(oid1.getIdPart()));
|
||||||
|
}
|
||||||
|
|
||||||
private List<String> searchAndReturnUnqualifiedVersionlessIdValues(String theUrl) throws IOException {
|
private List<String> searchAndReturnUnqualifiedVersionlessIdValues(String theUrl) throws IOException {
|
||||||
List<String> ids = new ArrayList<>();
|
List<String> ids = new ArrayList<>();
|
||||||
|
|
||||||
|
|
|
@ -6,6 +6,7 @@ import ca.uhn.fhir.jpa.model.entity.ModelConfig;
|
||||||
import ca.uhn.fhir.jpa.model.entity.NormalizedQuantitySearchLevel;
|
import ca.uhn.fhir.jpa.model.entity.NormalizedQuantitySearchLevel;
|
||||||
import ca.uhn.fhir.jpa.model.entity.ResourceIndexedSearchParamQuantity;
|
import ca.uhn.fhir.jpa.model.entity.ResourceIndexedSearchParamQuantity;
|
||||||
import ca.uhn.fhir.jpa.model.entity.ResourceIndexedSearchParamQuantityNormalized;
|
import ca.uhn.fhir.jpa.model.entity.ResourceIndexedSearchParamQuantityNormalized;
|
||||||
|
import ca.uhn.fhir.jpa.model.entity.ResourceIndexedSearchParamString;
|
||||||
import ca.uhn.fhir.jpa.model.entity.ResourceLink;
|
import ca.uhn.fhir.jpa.model.entity.ResourceLink;
|
||||||
import ca.uhn.fhir.jpa.model.util.UcumServiceUtil;
|
import ca.uhn.fhir.jpa.model.util.UcumServiceUtil;
|
||||||
import ca.uhn.fhir.jpa.partition.SystemRequestDetails;
|
import ca.uhn.fhir.jpa.partition.SystemRequestDetails;
|
||||||
|
@ -67,6 +68,7 @@ public class FhirResourceDaoR4CreateTest extends BaseJpaR4Test {
|
||||||
myDaoConfig.setDefaultSearchParamsCanBeOverridden(new DaoConfig().isDefaultSearchParamsCanBeOverridden());
|
myDaoConfig.setDefaultSearchParamsCanBeOverridden(new DaoConfig().isDefaultSearchParamsCanBeOverridden());
|
||||||
myModelConfig.setNormalizedQuantitySearchLevel(NormalizedQuantitySearchLevel.NORMALIZED_QUANTITY_SEARCH_NOT_SUPPORTED);
|
myModelConfig.setNormalizedQuantitySearchLevel(NormalizedQuantitySearchLevel.NORMALIZED_QUANTITY_SEARCH_NOT_SUPPORTED);
|
||||||
myModelConfig.setIndexOnContainedResources(new ModelConfig().isIndexOnContainedResources());
|
myModelConfig.setIndexOnContainedResources(new ModelConfig().isIndexOnContainedResources());
|
||||||
|
myModelConfig.setIndexOnContainedResourcesRecursively(new ModelConfig().isIndexOnContainedResourcesRecursively());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@ -122,6 +124,87 @@ public class FhirResourceDaoR4CreateTest extends BaseJpaR4Test {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testCreateLinkCreatesAppropriatePaths_ContainedResourceRecursive() {
|
||||||
|
myModelConfig.setIndexOnContainedResources(true);
|
||||||
|
myModelConfig.setIndexOnContainedResourcesRecursively(true);
|
||||||
|
|
||||||
|
Patient p = new Patient();
|
||||||
|
p.setId("pat");
|
||||||
|
p.setActive(true);
|
||||||
|
p.getNameFirstRep().setFamily("Smith");
|
||||||
|
|
||||||
|
Observation containedObs = new Observation();
|
||||||
|
containedObs.setId("#obs");
|
||||||
|
containedObs.setSubject(new Reference("#pat"));
|
||||||
|
|
||||||
|
Encounter enc = new Encounter();
|
||||||
|
enc.getContained().add(containedObs);
|
||||||
|
enc.getContained().add(p);
|
||||||
|
enc.addReasonReference(new Reference("#obs"));
|
||||||
|
myEncounterDao.create(enc, mySrd);
|
||||||
|
|
||||||
|
runInTransaction(() ->{
|
||||||
|
List<ResourceIndexedSearchParamString> allParams = myResourceIndexedSearchParamStringDao.findAll();
|
||||||
|
Optional<ResourceIndexedSearchParamString> link = allParams
|
||||||
|
.stream()
|
||||||
|
.filter(t -> "reason-reference.subject.family".equals(t.getParamName()))
|
||||||
|
.findFirst();
|
||||||
|
assertTrue(link.isPresent());
|
||||||
|
assertEquals("Smith", link.get().getValueExact());
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testCreateLinkCreatesAppropriatePaths_ContainedResourceRecursive_DoesNotLoop() {
|
||||||
|
myModelConfig.setIndexOnContainedResources(true);
|
||||||
|
myModelConfig.setIndexOnContainedResourcesRecursively(true);
|
||||||
|
|
||||||
|
Organization org1 = new Organization();
|
||||||
|
org1.setId("org1");
|
||||||
|
org1.setName("EscherCorp");
|
||||||
|
org1.setPartOf(new Reference("#org2"));
|
||||||
|
|
||||||
|
Organization org2 = new Organization();
|
||||||
|
org2.setId("org2");
|
||||||
|
org2.setName("M.C.Escher Unlimited");
|
||||||
|
org2.setPartOf(new Reference("#org1"));
|
||||||
|
|
||||||
|
Observation containedObs = new Observation();
|
||||||
|
containedObs.setId("#obs");
|
||||||
|
containedObs.addPerformer(new Reference("#org1"));
|
||||||
|
|
||||||
|
Encounter enc = new Encounter();
|
||||||
|
enc.getContained().add(containedObs);
|
||||||
|
enc.getContained().add(org1);
|
||||||
|
enc.getContained().add(org2);
|
||||||
|
enc.addReasonReference(new Reference("#obs"));
|
||||||
|
myEncounterDao.create(enc, mySrd);
|
||||||
|
|
||||||
|
runInTransaction(() ->{
|
||||||
|
List<ResourceIndexedSearchParamString> allParams = myResourceIndexedSearchParamStringDao.findAll();
|
||||||
|
Optional<ResourceIndexedSearchParamString> firstOrg = allParams
|
||||||
|
.stream()
|
||||||
|
.filter(t -> "reason-reference.performer.name".equals(t.getParamName()))
|
||||||
|
.findFirst();
|
||||||
|
assertTrue(firstOrg.isPresent());
|
||||||
|
assertEquals("EscherCorp", firstOrg.get().getValueExact());
|
||||||
|
|
||||||
|
Optional<ResourceIndexedSearchParamString> secondOrg = allParams
|
||||||
|
.stream()
|
||||||
|
.filter(t -> "reason-reference.performer.partof.name".equals(t.getParamName()))
|
||||||
|
.findFirst();
|
||||||
|
assertTrue(secondOrg.isPresent());
|
||||||
|
assertEquals("M.C.Escher Unlimited", secondOrg.get().getValueExact());
|
||||||
|
|
||||||
|
Optional<ResourceIndexedSearchParamString> thirdOrg = allParams
|
||||||
|
.stream()
|
||||||
|
.filter(t -> "reason-reference.performer.partof.partof.name".equals(t.getParamName()))
|
||||||
|
.findFirst();
|
||||||
|
assertFalse(thirdOrg.isPresent());
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void testConditionalCreateWithPlusInUrl() {
|
public void testConditionalCreateWithPlusInUrl() {
|
||||||
|
|
|
@ -100,6 +100,7 @@ public class ModelConfig {
|
||||||
private Map<String, Set<String>> myTypeToAutoVersionReferenceAtPaths = Collections.emptyMap();
|
private Map<String, Set<String>> myTypeToAutoVersionReferenceAtPaths = Collections.emptyMap();
|
||||||
private boolean myRespectVersionsForSearchIncludes;
|
private boolean myRespectVersionsForSearchIncludes;
|
||||||
private boolean myIndexOnContainedResources = false;
|
private boolean myIndexOnContainedResources = false;
|
||||||
|
private boolean myIndexOnContainedResourcesRecursively = false;
|
||||||
private boolean myAllowMdmExpansion = false;
|
private boolean myAllowMdmExpansion = false;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -785,6 +786,26 @@ public class ModelConfig {
|
||||||
myIndexOnContainedResources = theIndexOnContainedResources;
|
myIndexOnContainedResources = theIndexOnContainedResources;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Should recursive indexing and searching on contained resources be enabled on this server.
|
||||||
|
* This may have performance impacts, and should be enabled only if it is needed. Default is <code>false</code>.
|
||||||
|
*
|
||||||
|
* @since 5.6.0
|
||||||
|
*/
|
||||||
|
public boolean isIndexOnContainedResourcesRecursively() {
|
||||||
|
return myIndexOnContainedResourcesRecursively;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Should indexing and searching on contained resources be enabled on this server.
|
||||||
|
* This may have performance impacts, and should be enabled only if it is needed. Default is <code>false</code>.
|
||||||
|
*
|
||||||
|
* @since 5.6.0
|
||||||
|
*/
|
||||||
|
public void setIndexOnContainedResourcesRecursively(boolean theIndexOnContainedResourcesRecursively) {
|
||||||
|
myIndexOnContainedResourcesRecursively = theIndexOnContainedResourcesRecursively;
|
||||||
|
}
|
||||||
|
|
||||||
private static void validateTreatBaseUrlsAsLocal(String theUrl) {
|
private static void validateTreatBaseUrlsAsLocal(String theUrl) {
|
||||||
Validate.notBlank(theUrl, "Base URL must not be null or empty");
|
Validate.notBlank(theUrl, "Base URL must not be null or empty");
|
||||||
|
|
||||||
|
|
|
@ -64,6 +64,7 @@ import javax.annotation.Nonnull;
|
||||||
import javax.validation.constraints.NotNull;
|
import javax.validation.constraints.NotNull;
|
||||||
import java.util.Collection;
|
import java.util.Collection;
|
||||||
import java.util.Date;
|
import java.util.Date;
|
||||||
|
import java.util.HashSet;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
|
||||||
import static org.apache.commons.lang3.StringUtils.isBlank;
|
import static org.apache.commons.lang3.StringUtils.isBlank;
|
||||||
|
@ -133,41 +134,57 @@ public class SearchParamExtractorService {
|
||||||
// 1. get all contained resources
|
// 1. get all contained resources
|
||||||
Collection<IBaseResource> containedResources = terser.getAllEmbeddedResources(theResource, false);
|
Collection<IBaseResource> containedResources = terser.getAllEmbeddedResources(theResource, false);
|
||||||
|
|
||||||
|
extractSearchIndexParametersForContainedResources(theRequestDetails, theParams, theResource, theEntity, containedResources, new HashSet<>());
|
||||||
|
}
|
||||||
|
|
||||||
|
private void extractSearchIndexParametersForContainedResources(RequestDetails theRequestDetails, ResourceIndexedSearchParams theParams, IBaseResource theResource, ResourceTable theEntity, Collection<IBaseResource> containedResources, Collection<IBaseResource> theAlreadySeenResources) {
|
||||||
// 2. Find referenced search parameters
|
// 2. Find referenced search parameters
|
||||||
ISearchParamExtractor.SearchParamSet<PathAndRef> referencedSearchParamSet = mySearchParamExtractor.extractResourceLinks(theResource, true);
|
ISearchParamExtractor.SearchParamSet<PathAndRef> referencedSearchParamSet = mySearchParamExtractor.extractResourceLinks(theResource, true);
|
||||||
|
|
||||||
String spnamePrefix = null;
|
String spnamePrefix = null;
|
||||||
ResourceIndexedSearchParams currParams;
|
ResourceIndexedSearchParams currParams;
|
||||||
// 3. for each referenced search parameter, create an index
|
// 3. for each referenced search parameter, create an index
|
||||||
for (PathAndRef nextPathAndRef : referencedSearchParamSet) {
|
for (PathAndRef nextPathAndRef : referencedSearchParamSet) {
|
||||||
|
|
||||||
// 3.1 get the search parameter name as spname prefix
|
// 3.1 get the search parameter name as spname prefix
|
||||||
spnamePrefix = nextPathAndRef.getSearchParamName();
|
spnamePrefix = nextPathAndRef.getSearchParamName();
|
||||||
|
|
||||||
if (spnamePrefix == null || nextPathAndRef.getRef() == null)
|
if (spnamePrefix == null || nextPathAndRef.getRef() == null)
|
||||||
continue;
|
continue;
|
||||||
|
|
||||||
// 3.2 find the contained resource
|
// 3.2 find the contained resource
|
||||||
IBaseResource containedResource = findContainedResource(containedResources, nextPathAndRef.getRef());
|
IBaseResource containedResource = findContainedResource(containedResources, nextPathAndRef.getRef());
|
||||||
if (containedResource == null)
|
if (containedResource == null)
|
||||||
continue;
|
continue;
|
||||||
|
|
||||||
|
// 3.2.1 if we've already processed this resource upstream, do not process it again, to prevent infinite loops
|
||||||
|
if (theAlreadySeenResources.contains(containedResource)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
currParams = new ResourceIndexedSearchParams();
|
currParams = new ResourceIndexedSearchParams();
|
||||||
|
|
||||||
// 3.3 create indexes for the current contained resource
|
// 3.3 create indexes for the current contained resource
|
||||||
extractSearchIndexParameters(theRequestDetails, currParams, containedResource, theEntity);
|
extractSearchIndexParameters(theRequestDetails, currParams, containedResource, theEntity);
|
||||||
|
|
||||||
// 3.4 added reference name as a prefix for the contained resource if any
|
// 3.4 recurse to process any other contained resources referenced by this one
|
||||||
|
if (myModelConfig.isIndexOnContainedResourcesRecursively()) {
|
||||||
|
HashSet<IBaseResource> nextAlreadySeenResources = new HashSet<>(theAlreadySeenResources);
|
||||||
|
nextAlreadySeenResources.add(containedResource);
|
||||||
|
extractSearchIndexParametersForContainedResources(theRequestDetails, currParams, containedResource, theEntity, containedResources, nextAlreadySeenResources);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3.5 added reference name as a prefix for the contained resource if any
|
||||||
// e.g. for Observation.subject contained reference
|
// e.g. for Observation.subject contained reference
|
||||||
// the SP_NAME = subject.family
|
// the SP_NAME = subject.family
|
||||||
currParams.updateSpnamePrefixForIndexedOnContainedResource(spnamePrefix);
|
currParams.updateSpnamePrefixForIndexedOnContainedResource(spnamePrefix);
|
||||||
|
|
||||||
// 3.5 merge to the mainParams
|
// 3.6 merge to the mainParams
|
||||||
// NOTE: the spname prefix is different
|
// NOTE: the spname prefix is different
|
||||||
mergeParams(currParams, theParams);
|
mergeParams(currParams, theParams);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private IBaseResource findContainedResource(Collection<IBaseResource> resources, IBaseReference reference) {
|
private IBaseResource findContainedResource(Collection<IBaseResource> resources, IBaseReference reference) {
|
||||||
for (IBaseResource resource : resources) {
|
for (IBaseResource resource : resources) {
|
||||||
if (resource.getIdElement().equals(reference.getReferenceElement()))
|
if (resource.getIdElement().equals(reference.getReferenceElement()))
|
||||||
|
|
Loading…
Reference in New Issue