Jr 20211013 chained references (#3079)
* 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 * code review * fix broken tests
This commit is contained in:
parent
34010fa78d
commit
d07764e8e5
|
@ -0,0 +1,6 @@
|
|||
---
|
||||
type: add
|
||||
issue: 3088
|
||||
jira: SMILE-3151
|
||||
title: "Previously, chained searches were not able to traverse reference fields within contained resources.
|
||||
This enhancement adds the ability to traverse the reference fields of contained resources when those fields refer to discrete resources."
|
|
@ -93,6 +93,7 @@ import com.healthmarketscience.sqlbuilder.dbspec.basic.DbColumn;
|
|||
import org.apache.commons.collections4.BidiMap;
|
||||
import org.apache.commons.collections4.bidimap.DualHashBidiMap;
|
||||
import org.apache.commons.collections4.bidimap.UnmodifiableBidiMap;
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import org.apache.commons.lang3.builder.EqualsBuilder;
|
||||
import org.apache.commons.lang3.builder.HashCodeBuilder;
|
||||
import org.apache.commons.lang3.tuple.Pair;
|
||||
|
@ -109,7 +110,6 @@ import java.util.Collection;
|
|||
import java.util.Collections;
|
||||
import java.util.EnumSet;
|
||||
import java.util.HashMap;
|
||||
import java.util.Iterator;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.function.Supplier;
|
||||
|
@ -231,7 +231,7 @@ public class QueryStack {
|
|||
BaseJoiningPredicateBuilder firstPredicateBuilder = mySqlBuilder.getOrCreateFirstPredicateBuilder();
|
||||
ResourceLinkPredicateBuilder sortPredicateBuilder = mySqlBuilder.addReferencePredicateBuilder(this, firstPredicateBuilder.getResourceIdColumn());
|
||||
|
||||
Condition pathPredicate = sortPredicateBuilder.createPredicateSourcePaths(theResourceName, theParamName);
|
||||
Condition pathPredicate = sortPredicateBuilder.createPredicateSourcePaths(theResourceName, theParamName, new ArrayList<>());
|
||||
mySqlBuilder.addPredicate(pathPredicate);
|
||||
mySqlBuilder.addSortNumeric(sortPredicateBuilder.getColumnTargetResourceId(), theAscending);
|
||||
}
|
||||
|
@ -459,7 +459,7 @@ public class QueryStack {
|
|||
String chain = (theFilter.getParamPath().getNext() != null) ? theFilter.getParamPath().getNext().toString() : null;
|
||||
String value = theFilter.getValue();
|
||||
ReferenceParam referenceParam = new ReferenceParam(resourceType, chain, value);
|
||||
return theQueryStack3.createPredicateReference(null, theResourceName, paramName, Collections.singletonList(referenceParam), operation, theRequest, theRequestPartitionId);
|
||||
return theQueryStack3.createPredicateReference(null, theResourceName, paramName, new ArrayList<>(), Collections.singletonList(referenceParam), operation, theRequest, theRequestPartitionId);
|
||||
} else if (typeEnum == RestSearchParameterTypeEnum.QUANTITY) {
|
||||
return theQueryStack3.createPredicateQuantity(null, theResourceName, null, searchParam, Collections.singletonList(new QuantityParam(theFilter.getValue())), theFilter.getOperation(), theRequestPartitionId);
|
||||
} else if (typeEnum == RestSearchParameterTypeEnum.COMPOSITE) {
|
||||
|
@ -564,7 +564,7 @@ public class QueryStack {
|
|||
ResourceLinkPredicateBuilder join = mySqlBuilder.addReferencePredicateBuilderReversed(this, theSourceJoinColumn);
|
||||
Condition partitionPredicate = join.createPartitionIdPredicate(theRequestPartitionId);
|
||||
|
||||
List<String> paths = join.createResourceLinkPaths(targetResourceType, paramReference);
|
||||
List<String> paths = join.createResourceLinkPaths(targetResourceType, paramReference, new ArrayList<>());
|
||||
Condition typePredicate = BinaryCondition.equalTo(join.getColumnTargetResourceType(), mySqlBuilder.generatePlaceholder(theResourceType));
|
||||
Condition pathPredicate = toEqualToOrInPredicate(join.getColumnSourcePath(), mySqlBuilder.generatePlaceholders(paths));
|
||||
Condition linkedPredicate = searchForIdsWithAndOr(join.getColumnSrcResourceId(), targetResourceType, parameterName, Collections.singletonList(orValues), theRequest, theRequestPartitionId, SearchContainedModeEnum.FALSE);
|
||||
|
@ -662,14 +662,12 @@ public class QueryStack {
|
|||
public Condition createPredicateReference(@Nullable DbColumn theSourceJoinColumn,
|
||||
String theResourceName,
|
||||
String theParamName,
|
||||
List<String> theQualifiers,
|
||||
List<? extends IQueryParameterType> theList,
|
||||
SearchFilterParser.CompareOperation theOperation,
|
||||
RequestDetails theRequest,
|
||||
RequestPartitionId theRequestPartitionId) {
|
||||
|
||||
// This just to ensure the chain has been split correctly
|
||||
assert theParamName.contains(".") == false;
|
||||
|
||||
if ((theOperation != null) &&
|
||||
(theOperation != SearchFilterParser.CompareOperation.eq) &&
|
||||
(theOperation != SearchFilterParser.CompareOperation.ne)) {
|
||||
|
@ -683,7 +681,7 @@ public class QueryStack {
|
|||
}
|
||||
|
||||
ResourceLinkPredicateBuilder predicateBuilder = createOrReusePredicateBuilder(PredicateBuilderTypeEnum.REFERENCE, theSourceJoinColumn, theParamName, () -> mySqlBuilder.addReferencePredicateBuilder(this, theSourceJoinColumn)).getResult();
|
||||
return predicateBuilder.createPredicate(theRequest, theResourceName, theParamName, theList, theOperation, theRequestPartitionId);
|
||||
return predicateBuilder.createPredicate(theRequest, theResourceName, theParamName, theQualifiers, theList, theOperation, theRequestPartitionId);
|
||||
}
|
||||
|
||||
public Condition createPredicateReferenceForContainedResource(@Nullable DbColumn theSourceJoinColumn,
|
||||
|
@ -695,12 +693,14 @@ public class QueryStack {
|
|||
|
||||
String targetChain = null;
|
||||
String targetParamName = null;
|
||||
String headQualifier = null;
|
||||
String targetQualifier = null;
|
||||
String targetValue = null;
|
||||
|
||||
RuntimeSearchParam targetParamDefinition = null;
|
||||
|
||||
ArrayList<IQueryParameterType> orValues = Lists.newArrayList();
|
||||
List<IQueryParameterType> trimmedParameters = Lists.newArrayList();
|
||||
IQueryParameterType qp = null;
|
||||
|
||||
for (int orIdx = 0; orIdx < theList.size(); orIdx++) {
|
||||
|
@ -715,18 +715,28 @@ public class QueryStack {
|
|||
targetChain = referenceParam.getChain();
|
||||
targetParamName = targetChain;
|
||||
targetValue = nextOr.getValueAsQueryToken(myFhirContext);
|
||||
headQualifier = referenceParam.getResourceType();
|
||||
|
||||
int qualifierIndex = targetChain.indexOf(':');
|
||||
if (qualifierIndex != -1) {
|
||||
targetParamName = targetChain.substring(0, qualifierIndex);
|
||||
targetQualifier = targetChain.substring(qualifierIndex);
|
||||
String targetNextChain = null;
|
||||
int linkIndex = targetChain.indexOf('.');
|
||||
if (linkIndex != -1) {
|
||||
targetParamName = targetChain.substring(0, linkIndex);
|
||||
targetNextChain = targetChain.substring(linkIndex+1);
|
||||
}
|
||||
|
||||
int qualifierIndex = targetParamName.indexOf(':');
|
||||
if (qualifierIndex != -1) {
|
||||
targetParamName = targetParamName.substring(0, qualifierIndex);
|
||||
targetQualifier = targetParamName.substring(qualifierIndex);
|
||||
}
|
||||
trimmedParameters.add(new ReferenceParam(targetQualifier, targetNextChain, referenceParam.getValue()));
|
||||
|
||||
// 2. find out the data type
|
||||
if (targetParamDefinition == null) {
|
||||
Iterator<String> it = theSearchParam.getTargets().iterator();
|
||||
while (it.hasNext()) {
|
||||
targetParamDefinition = mySearchParamRegistry.getActiveSearchParam(it.next(), targetParamName);
|
||||
for (String nextTarget : theSearchParam.getTargets()) {
|
||||
if (!referenceParam.hasResourceType() || referenceParam.getResourceType().equals(nextTarget)) {
|
||||
targetParamDefinition = mySearchParamRegistry.getActiveSearchParam(nextTarget, targetParamName);
|
||||
}
|
||||
if (targetParamDefinition != null)
|
||||
break;
|
||||
}
|
||||
|
@ -736,6 +746,10 @@ public class QueryStack {
|
|||
throw new InvalidRequestException("Unknown search parameter name: " + theSearchParam.getName() + '.' + targetParamName + ".");
|
||||
}
|
||||
|
||||
if (RestSearchParameterTypeEnum.REFERENCE.equals(targetParamDefinition.getParamType())) {
|
||||
continue;
|
||||
}
|
||||
|
||||
qp = toParameterType(targetParamDefinition);
|
||||
qp.setValueAsQueryToken(myFhirContext, targetParamName, targetQualifier, targetValue);
|
||||
orValues.add(qp);
|
||||
|
@ -746,6 +760,8 @@ public class QueryStack {
|
|||
throw new InvalidRequestException("Unknown search parameter name: " + theSearchParam.getName() + ".");
|
||||
}
|
||||
|
||||
List<String> qualifiers= Collections.singletonList(headQualifier);
|
||||
|
||||
// 3. create the query
|
||||
Condition containedCondition = null;
|
||||
|
||||
|
@ -778,8 +794,11 @@ public class QueryStack {
|
|||
containedCondition = createPredicateUri(theSourceJoinColumn, theResourceName, spnamePrefix, targetParamDefinition,
|
||||
orValues, theOperation, theRequest, theRequestPartitionId);
|
||||
break;
|
||||
case HAS:
|
||||
case REFERENCE:
|
||||
String chainedParamName = theParamName + "." + targetParamName;
|
||||
containedCondition = createPredicateReference(theSourceJoinColumn, theResourceName, chainedParamName, qualifiers, trimmedParameters, theOperation, theRequest, theRequestPartitionId);
|
||||
break;
|
||||
case HAS:
|
||||
case SPECIAL:
|
||||
default:
|
||||
throw new InvalidRequestException(
|
||||
|
@ -1123,16 +1142,12 @@ public class QueryStack {
|
|||
// until the complete fix is available.
|
||||
andPredicates.add(createPredicateReferenceForContainedResource(null, theResourceName, theParamName, nextParamDef, nextAnd, null, theRequest, theRequestPartitionId));
|
||||
} else if (isEligibleForContainedResourceSearch(nextAnd)) {
|
||||
// TODO for now, restrict contained reference traversal to the last reference in the chain
|
||||
// We don't seem to be indexing the outbound references of a contained resource, so we can't
|
||||
// include them in search chains.
|
||||
// It would be nice to eventually relax this constraint, but no client seems to be asking for it.
|
||||
andPredicates.add(toOrPredicate(
|
||||
createPredicateReference(theSourceJoinColumn, theResourceName, theParamName, nextAnd, null, theRequest, theRequestPartitionId),
|
||||
createPredicateReference(theSourceJoinColumn, theResourceName, theParamName, new ArrayList<>(), nextAnd, null, theRequest, theRequestPartitionId),
|
||||
createPredicateReferenceForContainedResource(theSourceJoinColumn, theResourceName, theParamName, nextParamDef, nextAnd, null, theRequest, theRequestPartitionId)
|
||||
));
|
||||
} else {
|
||||
andPredicates.add(createPredicateReference(theSourceJoinColumn, theResourceName, theParamName, nextAnd, null, theRequest, theRequestPartitionId));
|
||||
andPredicates.add(createPredicateReference(theSourceJoinColumn, theResourceName, theParamName, new ArrayList<>(), nextAnd, null, theRequest, theRequestPartitionId));
|
||||
}
|
||||
}
|
||||
break;
|
||||
|
@ -1215,8 +1230,8 @@ public class QueryStack {
|
|||
return myModelConfig.isIndexOnContainedResources() &&
|
||||
nextAnd.stream()
|
||||
.filter(t -> t instanceof ReferenceParam)
|
||||
.map(t -> (ReferenceParam) t)
|
||||
.noneMatch(t -> t.getChain().contains("."));
|
||||
.map(t -> ((ReferenceParam) t).getChain())
|
||||
.anyMatch(StringUtils::isNotBlank);
|
||||
}
|
||||
|
||||
public void addPredicateCompositeUnique(String theIndexString, RequestPartitionId theRequestPartitionId) {
|
||||
|
|
|
@ -81,7 +81,9 @@ import javax.annotation.Nonnull;
|
|||
import javax.annotation.Nullable;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.Collection;
|
||||
import java.util.Collections;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.ListIterator;
|
||||
import java.util.Set;
|
||||
|
@ -159,7 +161,7 @@ public class ResourceLinkPredicateBuilder extends BaseJoiningPredicateBuilder {
|
|||
}
|
||||
}
|
||||
|
||||
public Condition createPredicate(RequestDetails theRequest, String theResourceType, String theParamName, List<? extends IQueryParameterType> theReferenceOrParamList, SearchFilterParser.CompareOperation theOperation, RequestPartitionId theRequestPartitionId) {
|
||||
public Condition createPredicate(RequestDetails theRequest, String theResourceType, String theParamName, List<String> theQualifiers, List<? extends IQueryParameterType> theReferenceOrParamList, SearchFilterParser.CompareOperation theOperation, RequestPartitionId theRequestPartitionId) {
|
||||
|
||||
List<IIdType> targetIds = new ArrayList<>();
|
||||
List<String> targetQualifiedUrls = new ArrayList<>();
|
||||
|
@ -195,7 +197,7 @@ public class ResourceLinkPredicateBuilder extends BaseJoiningPredicateBuilder {
|
|||
* Handle chained search, e.g. Patient?organization.name=Kwik-e-mart
|
||||
*/
|
||||
|
||||
return addPredicateReferenceWithChain(theResourceType, theParamName, theReferenceOrParamList, ref, theRequest, theRequestPartitionId);
|
||||
return addPredicateReferenceWithChain(theResourceType, theParamName, theQualifiers, theReferenceOrParamList, ref, theRequest, theRequestPartitionId);
|
||||
|
||||
}
|
||||
|
||||
|
@ -211,7 +213,7 @@ public class ResourceLinkPredicateBuilder extends BaseJoiningPredicateBuilder {
|
|||
}
|
||||
}
|
||||
|
||||
List<String> pathsToMatch = createResourceLinkPaths(theResourceType, theParamName);
|
||||
List<String> pathsToMatch = createResourceLinkPaths(theResourceType, theParamName, theQualifiers);
|
||||
boolean inverse;
|
||||
if ((theOperation == null) || (theOperation == SearchFilterParser.CompareOperation.eq)) {
|
||||
inverse = false;
|
||||
|
@ -266,8 +268,8 @@ public class ResourceLinkPredicateBuilder extends BaseJoiningPredicateBuilder {
|
|||
return toEqualToOrInPredicate(myColumnSrcPath, generatePlaceholders(thePathsToMatch));
|
||||
}
|
||||
|
||||
public Condition createPredicateSourcePaths(String theResourceName, String theParamName) {
|
||||
List<String> pathsToMatch = createResourceLinkPaths(theResourceName, theParamName);
|
||||
public Condition createPredicateSourcePaths(String theResourceName, String theParamName, List<String> theQualifiers) {
|
||||
List<String> pathsToMatch = createResourceLinkPaths(theResourceName, theParamName, theQualifiers);
|
||||
return createPredicateSourcePaths(pathsToMatch);
|
||||
}
|
||||
|
||||
|
@ -301,7 +303,7 @@ public class ResourceLinkPredicateBuilder extends BaseJoiningPredicateBuilder {
|
|||
* This is for handling queries like the following: /Observation?device.identifier=urn:system|foo in which we use a chain
|
||||
* on the device.
|
||||
*/
|
||||
private Condition addPredicateReferenceWithChain(String theResourceName, String theParamName, List<? extends IQueryParameterType> theList, ReferenceParam theReferenceParam, RequestDetails theRequest, RequestPartitionId theRequestPartitionId) {
|
||||
private Condition addPredicateReferenceWithChain(String theResourceName, String theParamName, List<String> theQualifiers, List<? extends IQueryParameterType> theList, ReferenceParam theReferenceParam, RequestDetails theRequest, RequestPartitionId theRequestPartitionId) {
|
||||
|
||||
/*
|
||||
* Which resource types can the given chained parameter actually link to? This might be a list
|
||||
|
@ -318,7 +320,7 @@ public class ResourceLinkPredicateBuilder extends BaseJoiningPredicateBuilder {
|
|||
*/
|
||||
if (Constants.PARAM_TYPE.equals(theReferenceParam.getChain())) {
|
||||
|
||||
List<String> pathsToMatch = createResourceLinkPaths(theResourceName, theParamName);
|
||||
List<String> pathsToMatch = createResourceLinkPaths(theResourceName, theParamName, theQualifiers);
|
||||
Condition typeCondition = createPredicateSourcePaths(pathsToMatch);
|
||||
|
||||
String typeValue = theReferenceParam.getValue();
|
||||
|
@ -430,7 +432,7 @@ public class ResourceLinkPredicateBuilder extends BaseJoiningPredicateBuilder {
|
|||
multiTypePredicate = toOrPredicate(orPredicates);
|
||||
}
|
||||
|
||||
List<String> pathsToMatch = createResourceLinkPaths(theResourceName, theParamName);
|
||||
List<String> pathsToMatch = createResourceLinkPaths(theResourceName, theParamName, theQualifiers);
|
||||
Condition pathPredicate = createPredicateSourcePaths(pathsToMatch);
|
||||
return toAndPredicate(pathPredicate, multiTypePredicate);
|
||||
}
|
||||
|
@ -440,15 +442,7 @@ public class ResourceLinkPredicateBuilder extends BaseJoiningPredicateBuilder {
|
|||
final List<Class<? extends IBaseResource>> resourceTypes;
|
||||
if (!theReferenceParam.hasResourceType()) {
|
||||
|
||||
RuntimeSearchParam param = mySearchParamRegistry.getActiveSearchParam(theResourceName, theParamName);
|
||||
resourceTypes = new ArrayList<>();
|
||||
|
||||
if (param.hasTargets()) {
|
||||
Set<String> targetTypes = param.getTargets();
|
||||
for (String next : targetTypes) {
|
||||
resourceTypes.add(getFhirContext().getResourceDefinition(next).getImplementingClass());
|
||||
}
|
||||
}
|
||||
resourceTypes = determineResourceTypes(Collections.singleton(theResourceName), theParamName);
|
||||
|
||||
if (resourceTypes.isEmpty()) {
|
||||
RuntimeSearchParam searchParamByName = mySearchParamRegistry.getActiveSearchParam(theResourceName, theParamName);
|
||||
|
@ -513,25 +507,89 @@ public class ResourceLinkPredicateBuilder extends BaseJoiningPredicateBuilder {
|
|||
.collect(Collectors.toList());
|
||||
}
|
||||
|
||||
public List<String> createResourceLinkPaths(String theResourceName, String theParamName) {
|
||||
RuntimeSearchParam param = mySearchParamRegistry.getActiveSearchParam(theResourceName, theParamName);
|
||||
List<String> path = param.getPathsSplit();
|
||||
private List<Class<? extends IBaseResource>> determineResourceTypes(Set<String> theResourceNames, String theParamNameChain) {
|
||||
int linkIndex = theParamNameChain.indexOf('.');
|
||||
if (linkIndex == -1) {
|
||||
Set<Class<? extends IBaseResource>> resourceTypes = new HashSet<>();
|
||||
for (String resourceName : theResourceNames) {
|
||||
RuntimeSearchParam param = mySearchParamRegistry.getActiveSearchParam(resourceName, theParamNameChain);
|
||||
|
||||
/*
|
||||
* SearchParameters can declare paths on multiple resource
|
||||
* types. Here we only want the ones that actually apply.
|
||||
*/
|
||||
path = new ArrayList<>(path);
|
||||
|
||||
ListIterator<String> iter = path.listIterator();
|
||||
while (iter.hasNext()) {
|
||||
String nextPath = trim(iter.next());
|
||||
if (!nextPath.contains(theResourceName + ".")) {
|
||||
iter.remove();
|
||||
if (param != null && param.hasTargets()) {
|
||||
Set<String> targetTypes = param.getTargets();
|
||||
for (String next : targetTypes) {
|
||||
resourceTypes.add(getFhirContext().getResourceDefinition(next).getImplementingClass());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return new ArrayList<>(resourceTypes);
|
||||
} else {
|
||||
String paramNameHead = theParamNameChain.substring(0, linkIndex);
|
||||
String paramNameTail = theParamNameChain.substring(linkIndex+1);
|
||||
Set<String> targetResourceTypeNames = new HashSet<>();
|
||||
for (String resourceName : theResourceNames) {
|
||||
RuntimeSearchParam param = mySearchParamRegistry.getActiveSearchParam(resourceName, paramNameHead);
|
||||
|
||||
return path;
|
||||
if (param != null && param.hasTargets()) {
|
||||
targetResourceTypeNames.addAll(param.getTargets());
|
||||
}
|
||||
}
|
||||
return determineResourceTypes(targetResourceTypeNames, paramNameTail);
|
||||
}
|
||||
}
|
||||
|
||||
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<>();
|
||||
}
|
||||
List<String> path = param.getPathsSplit();
|
||||
|
||||
/*
|
||||
* SearchParameters can declare paths on multiple resource
|
||||
* types. Here we only want the ones that actually apply.
|
||||
*/
|
||||
path = new ArrayList<>(path);
|
||||
|
||||
ListIterator<String> iter = path.listIterator();
|
||||
while (iter.hasNext()) {
|
||||
String nextPath = trim(iter.next());
|
||||
if (!nextPath.contains(theResourceName + ".")) {
|
||||
iter.remove();
|
||||
}
|
||||
}
|
||||
|
||||
return path;
|
||||
} else {
|
||||
String paramNameHead = theParamName.substring(0, linkIndex);
|
||||
String paramNameTail = theParamName.substring(linkIndex + 1);
|
||||
String qualifier = theParamQualifiers.get(0);
|
||||
|
||||
RuntimeSearchParam param = mySearchParamRegistry.getActiveSearchParam(theResourceName, paramNameHead);
|
||||
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());
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
|
|
@ -8,6 +8,7 @@ import ca.uhn.fhir.jpa.searchparam.SearchParameterMap;
|
|||
import ca.uhn.fhir.parser.StrictErrorHandler;
|
||||
import ca.uhn.fhir.rest.api.server.IBundleProvider;
|
||||
import org.hl7.fhir.instance.model.api.IIdType;
|
||||
import org.hl7.fhir.r4.model.Device;
|
||||
import org.hl7.fhir.r4.model.IdType;
|
||||
import org.hl7.fhir.r4.model.Observation;
|
||||
import org.hl7.fhir.r4.model.Organization;
|
||||
|
@ -15,7 +16,6 @@ import org.hl7.fhir.r4.model.Patient;
|
|||
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.springframework.beans.factory.annotation.Autowired;
|
||||
|
||||
|
@ -116,6 +116,43 @@ public class ChainingR4SearchTest extends BaseJpaR4Test {
|
|||
assertThat(oids, contains(oid1.getIdPart()));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testShouldResolveATwoLinkChainToAContainedReference() throws Exception {
|
||||
// Adding support for this case in SMILE-3151
|
||||
|
||||
// setup
|
||||
IIdType oid1;
|
||||
IIdType orgId;
|
||||
|
||||
{
|
||||
Organization org = new Organization();
|
||||
org.setId(IdType.newRandomUuid());
|
||||
org.setName("HealthCo");
|
||||
orgId = myOrganizationDao.create(org, mySrd).getId();
|
||||
|
||||
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();
|
||||
}
|
||||
|
||||
String url = "/Observation?subject.organization=" + orgId.getValueAsString();
|
||||
|
||||
// execute
|
||||
List<String> oids = searchAndReturnUnqualifiedVersionlessIdValues(url);
|
||||
|
||||
// validate
|
||||
assertEquals(1L, oids.size());
|
||||
assertThat(oids, contains(oid1.getIdPart()));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testShouldResolveAThreeLinkChainWhereAllResourcesStandAlone() throws Exception {
|
||||
|
||||
|
@ -188,9 +225,8 @@ public class ChainingR4SearchTest extends BaseJpaR4Test {
|
|||
}
|
||||
|
||||
@Test
|
||||
@Disabled
|
||||
public void testShouldResolveAThreeLinkChainWithAContainedResourceAtTheBeginningOfTheChain() throws Exception {
|
||||
// We do not currently support this case - we may not be indexing the references of contained resources
|
||||
// Adding support for this case in SMILE-3151
|
||||
|
||||
// setup
|
||||
IIdType oid1;
|
||||
|
@ -242,11 +278,21 @@ public class ChainingR4SearchTest extends BaseJpaR4Test {
|
|||
p.getManagingOrganization().setReference(org.getId());
|
||||
myPatientDao.create(p, mySrd);
|
||||
|
||||
Observation obs = new Observation();
|
||||
obs.getCode().setText("Observation 1");
|
||||
obs.getSubject().setReference(p.getId());
|
||||
Device d = new Device();
|
||||
d.setId(IdType.newRandomUuid());
|
||||
d.getOwner().setReference(org.getId());
|
||||
myDeviceDao.create(d, mySrd);
|
||||
|
||||
oid1 = myObservationDao.create(obs, mySrd).getId().toUnqualifiedVersionless();
|
||||
Observation obs1 = new Observation();
|
||||
obs1.getCode().setText("Observation 1");
|
||||
obs1.getSubject().setReference(p.getId());
|
||||
|
||||
Observation obs2 = new Observation();
|
||||
obs2.getCode().setText("Observation 2");
|
||||
obs2.getSubject().setReference(d.getId());
|
||||
|
||||
oid1 = myObservationDao.create(obs1, mySrd).getId().toUnqualifiedVersionless();
|
||||
myObservationDao.create(obs2, mySrd);
|
||||
}
|
||||
|
||||
String url = "/Observation?subject:Patient.organization:Organization.name=HealthCo";
|
||||
|
@ -278,11 +324,26 @@ public class ChainingR4SearchTest extends BaseJpaR4Test {
|
|||
p.getManagingOrganization().setReference("#org");
|
||||
myPatientDao.create(p, mySrd);
|
||||
|
||||
Organization org2 = new Organization();
|
||||
org2.setId("org");
|
||||
org2.setName("HealthCo");
|
||||
|
||||
Device d = new Device();
|
||||
d.setId(IdType.newRandomUuid());
|
||||
d.getContained().add(org2);
|
||||
d.getOwner().setReference("#org");
|
||||
myDeviceDao.create(d, mySrd);
|
||||
|
||||
Observation obs = new Observation();
|
||||
obs.getCode().setText("Observation 1");
|
||||
obs.getSubject().setReference(p.getId());
|
||||
|
||||
oid1 = myObservationDao.create(obs, mySrd).getId().toUnqualifiedVersionless();
|
||||
|
||||
Observation obs2 = new Observation();
|
||||
obs2.getCode().setText("Observation 2");
|
||||
obs2.getSubject().setReference(d.getId());
|
||||
myObservationDao.create(obs2, mySrd);
|
||||
}
|
||||
|
||||
String url = "/Observation?subject:Patient.organization:Organization.name=HealthCo";
|
||||
|
@ -295,6 +356,55 @@ public class ChainingR4SearchTest extends BaseJpaR4Test {
|
|||
assertThat(oids, contains(oid1.getIdPart()));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testShouldResolveAThreeLinkChainWithQualifiersWithAContainedResourceAtTheBeginning() throws Exception {
|
||||
// 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();
|
||||
|
||||
Device d = new Device();
|
||||
d.setId("dev");
|
||||
d.getOwner().setReference(org.getId());
|
||||
|
||||
Observation obs2 = new Observation();
|
||||
obs2.getContained().add(d);
|
||||
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
|
||||
public void testShouldResolveAFourLinkChainWhereAllResourcesStandAlone() throws Exception {
|
||||
|
||||
|
@ -375,6 +485,52 @@ public class ChainingR4SearchTest extends BaseJpaR4Test {
|
|||
assertThat(oids, contains(oid1.getIdPart()));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testShouldResolveAFourLinkChainWithAContainedResourceInTheMiddle() throws Exception {
|
||||
|
||||
// setup
|
||||
IIdType oid1;
|
||||
|
||||
{
|
||||
myCaptureQueriesListener.clear();
|
||||
|
||||
Organization org = new Organization();
|
||||
org.setId(IdType.newRandomUuid());
|
||||
org.setName("HealthCo");
|
||||
myOrganizationDao.create(org, mySrd);
|
||||
|
||||
Organization partOfOrg = new Organization();
|
||||
partOfOrg.setId("org");
|
||||
partOfOrg.getPartOf().setReference(org.getId());
|
||||
|
||||
Patient p = new Patient();
|
||||
p.setId(IdType.newRandomUuid());
|
||||
p.addName().setFamily("Smith").addGiven("John");
|
||||
p.getContained().add(partOfOrg);
|
||||
p.getManagingOrganization().setReference("#org");
|
||||
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();
|
||||
|
||||
myCaptureQueriesListener.logInsertQueries();
|
||||
}
|
||||
|
||||
String url = "/Observation?subject.organization.partof.name=HealthCo";
|
||||
|
||||
// execute
|
||||
myCaptureQueriesListener.clear();
|
||||
List<String> oids = searchAndReturnUnqualifiedVersionlessIdValues(url);
|
||||
myCaptureQueriesListener.logSelectQueries();
|
||||
|
||||
// validate
|
||||
assertEquals(1L, oids.size());
|
||||
assertThat(oids, contains(oid1.getIdPart()));
|
||||
}
|
||||
|
||||
private List<String> searchAndReturnUnqualifiedVersionlessIdValues(String theUrl) throws IOException {
|
||||
List<String> ids = new ArrayList<>();
|
||||
|
||||
|
|
|
@ -2,6 +2,7 @@ package ca.uhn.fhir.jpa.dao.r4;
|
|||
|
||||
import ca.uhn.fhir.jpa.api.config.DaoConfig;
|
||||
import ca.uhn.fhir.jpa.api.model.DaoMethodOutcome;
|
||||
import ca.uhn.fhir.jpa.model.entity.ModelConfig;
|
||||
import ca.uhn.fhir.jpa.model.entity.NormalizedQuantitySearchLevel;
|
||||
import ca.uhn.fhir.jpa.model.entity.ResourceIndexedSearchParamQuantity;
|
||||
import ca.uhn.fhir.jpa.model.entity.ResourceIndexedSearchParamQuantityNormalized;
|
||||
|
@ -23,6 +24,7 @@ import org.hl7.fhir.instance.model.api.IIdType;
|
|||
import org.hl7.fhir.r4.model.Bundle;
|
||||
import org.hl7.fhir.r4.model.DateType;
|
||||
import org.hl7.fhir.r4.model.DecimalType;
|
||||
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.Observation;
|
||||
|
@ -42,6 +44,7 @@ import java.io.IOException;
|
|||
import java.math.BigDecimal;
|
||||
import java.util.Date;
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import static org.hamcrest.MatcherAssert.assertThat;
|
||||
|
@ -63,6 +66,7 @@ public class FhirResourceDaoR4CreateTest extends BaseJpaR4Test {
|
|||
myDaoConfig.setResourceClientIdStrategy(new DaoConfig().getResourceClientIdStrategy());
|
||||
myDaoConfig.setDefaultSearchParamsCanBeOverridden(new DaoConfig().isDefaultSearchParamsCanBeOverridden());
|
||||
myModelConfig.setNormalizedQuantitySearchLevel(NormalizedQuantitySearchLevel.NORMALIZED_QUANTITY_SEARCH_NOT_SUPPORTED);
|
||||
myModelConfig.setIndexOnContainedResources(new ModelConfig().isIndexOnContainedResources());
|
||||
}
|
||||
|
||||
|
||||
|
@ -81,13 +85,43 @@ public class FhirResourceDaoR4CreateTest extends BaseJpaR4Test {
|
|||
List<ResourceLink> allLinks = myResourceLinkDao.findAll();
|
||||
List<String> paths = allLinks
|
||||
.stream()
|
||||
.map(t -> t.getSourcePath())
|
||||
.map(ResourceLink::getSourcePath)
|
||||
.sorted()
|
||||
.collect(Collectors.toList());
|
||||
assertThat(paths.toString(), paths, contains("Observation.subject", "Observation.subject.where(resolve() is Patient)"));
|
||||
});
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testCreateLinkCreatesAppropriatePaths_ContainedResource() {
|
||||
myModelConfig.setIndexOnContainedResources(true);
|
||||
|
||||
Patient p = new Patient();
|
||||
p.setId("Patient/A");
|
||||
p.setActive(true);
|
||||
myPatientDao.update(p, mySrd);
|
||||
|
||||
Observation containedObs = new Observation();
|
||||
containedObs.setId("#cont");
|
||||
containedObs.setSubject(new Reference("Patient/A"));
|
||||
|
||||
Encounter enc = new Encounter();
|
||||
enc.getContained().add(containedObs);
|
||||
enc.addReasonReference(new Reference("#cont"));
|
||||
myEncounterDao.create(enc, mySrd);
|
||||
|
||||
runInTransaction(() ->{
|
||||
List<ResourceLink> allLinks = myResourceLinkDao.findAll();
|
||||
Optional<ResourceLink> link = allLinks
|
||||
.stream()
|
||||
.filter(t -> "Encounter.reasonReference.subject".equals(t.getSourcePath()))
|
||||
.findFirst();
|
||||
assertTrue(link.isPresent());
|
||||
assertEquals("Patient", link.get().getTargetResourceType());
|
||||
assertEquals("A", link.get().getTargetResourceId());
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@Test
|
||||
public void testConditionalCreateWithPlusInUrl() {
|
||||
|
@ -245,7 +279,7 @@ public class FhirResourceDaoR4CreateTest extends BaseJpaR4Test {
|
|||
|
||||
// Read it back
|
||||
p = myPatientDao.read(new IdType("Patient/" + firstClientAssignedId));
|
||||
assertEquals(true, p.getActive());
|
||||
assertTrue(p.getActive());
|
||||
|
||||
// Now create a client assigned numeric ID
|
||||
p = new Patient();
|
||||
|
@ -298,7 +332,7 @@ public class FhirResourceDaoR4CreateTest extends BaseJpaR4Test {
|
|||
|
||||
// Read it back
|
||||
p = myPatientDao.read(id0.toUnqualifiedVersionless());
|
||||
assertEquals(true, p.getActive());
|
||||
assertTrue(p.getActive());
|
||||
|
||||
// Pick an ID that was already used as an internal PID
|
||||
Long newId = runInTransaction(() -> myResourceTableDao.findIdsOfResourcesWithinUpdatedRangeOrderedFromNewest(
|
||||
|
|
|
@ -158,6 +158,21 @@ public final class ResourceIndexedSearchParams {
|
|||
updateSpnamePrefixForIndexedOnContainedResource(myCoordsParams, theSpnamePrefix);
|
||||
}
|
||||
|
||||
public void updateSpnamePrefixForLinksOnContainedResource(String theSpNamePrefix) {
|
||||
for (ResourceLink param : myLinks) {
|
||||
// The resource link already has the resource type of the contained resource at the head of the path.
|
||||
// We need to replace this with the name of the containing type, and extend the search path.
|
||||
int index = param.getSourcePath().indexOf('.');
|
||||
if (index > -1) {
|
||||
param.setSourcePath(theSpNamePrefix + param.getSourcePath().substring(index));
|
||||
} else {
|
||||
// Can this ever happen?
|
||||
param.setSourcePath(theSpNamePrefix + "." + param.getSourcePath());
|
||||
}
|
||||
param.calculateHashes(); // re-calculateHashes
|
||||
}
|
||||
}
|
||||
|
||||
void setUpdatedTime(Date theUpdateTime) {
|
||||
setUpdatedTime(myStringParams, theUpdateTime);
|
||||
setUpdatedTime(myNumberParams, theUpdateTime);
|
||||
|
|
|
@ -114,6 +114,10 @@ public class SearchParamExtractorService {
|
|||
// Reference search parameters
|
||||
extractResourceLinks(theRequestPartitionId, theParams, theEntity, theResource, theTransactionDetails, theFailOnInvalidReference, theRequestDetails);
|
||||
|
||||
if (myModelConfig.isIndexOnContainedResources()) {
|
||||
extractResourceLinksForContainedResources(theRequestPartitionId, theParams, theEntity, theResource, theTransactionDetails, theFailOnInvalidReference, theRequestDetails);
|
||||
}
|
||||
|
||||
theParams.setUpdatedTime(theTransactionDetails.getTransactionDate());
|
||||
}
|
||||
|
||||
|
@ -402,6 +406,48 @@ public class SearchParamExtractorService {
|
|||
theParams.myLinks.add(resourceLink);
|
||||
}
|
||||
|
||||
private void extractResourceLinksForContainedResources(RequestPartitionId theRequestPartitionId, ResourceIndexedSearchParams theParams, ResourceTable theEntity, IBaseResource theResource, TransactionDetails theTransactionDetails, boolean theFailOnInvalidReference, RequestDetails theRequest) {
|
||||
|
||||
FhirTerser terser = myContext.newTerser();
|
||||
|
||||
// 1. get all contained resources
|
||||
Collection<IBaseResource> containedResources = terser.getAllEmbeddedResources(theResource, false);
|
||||
|
||||
// 2. Find referenced search parameters
|
||||
ISearchParamExtractor.SearchParamSet<PathAndRef> referencedSearchParamSet = mySearchParamExtractor.extractResourceLinks(theResource, true);
|
||||
|
||||
String spNamePrefix = null;
|
||||
ResourceIndexedSearchParams currParams;
|
||||
// 3. for each referenced search parameter, create an index
|
||||
for (PathAndRef nextPathAndRef : referencedSearchParamSet) {
|
||||
|
||||
// 3.1 get the search parameter name as spname prefix
|
||||
spNamePrefix = nextPathAndRef.getSearchParamName();
|
||||
|
||||
if (spNamePrefix == null || nextPathAndRef.getRef() == null)
|
||||
continue;
|
||||
|
||||
// 3.2 find the contained resource
|
||||
IBaseResource containedResource = findContainedResource(containedResources, nextPathAndRef.getRef());
|
||||
if (containedResource == null)
|
||||
continue;
|
||||
|
||||
currParams = new ResourceIndexedSearchParams();
|
||||
|
||||
// 3.3 create indexes for the current contained resource
|
||||
extractResourceLinks(theRequestPartitionId, currParams, theEntity, containedResource, theTransactionDetails, theFailOnInvalidReference, theRequest);
|
||||
|
||||
// 3.4 added reference name as a prefix for the contained resource if any
|
||||
// e.g. for Observation.subject contained reference
|
||||
// the SP_NAME = subject.family
|
||||
currParams.updateSpnamePrefixForLinksOnContainedResource(nextPathAndRef.getPath());
|
||||
|
||||
// 3.5 merge to the mainParams
|
||||
// NOTE: the spname prefix is different
|
||||
theParams.getResourceLinks().addAll(currParams.getResourceLinks());
|
||||
}
|
||||
}
|
||||
|
||||
private ResourceLink resolveTargetAndCreateResourceLinkOrReturnNull(@Nonnull RequestPartitionId theRequestPartitionId, ResourceTable theEntity, Date theUpdateTime, RuntimeSearchParam nextSpDef, String theNextPathsUnsplit, PathAndRef nextPathAndRef, IIdType theNextId, String theTypeString, Class<? extends IBaseResource> theType, IBaseReference theReference, RequestDetails theRequest, TransactionDetails theTransactionDetails) {
|
||||
|
||||
ResourcePersistentId resolvedResourceId = theTransactionDetails.getResolvedResourceId(theNextId);
|
||||
|
|
Loading…
Reference in New Issue