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;
|
||||||
import org.apache.commons.collections4.bidimap.DualHashBidiMap;
|
import org.apache.commons.collections4.bidimap.DualHashBidiMap;
|
||||||
import org.apache.commons.collections4.bidimap.UnmodifiableBidiMap;
|
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.EqualsBuilder;
|
||||||
import org.apache.commons.lang3.builder.HashCodeBuilder;
|
import org.apache.commons.lang3.builder.HashCodeBuilder;
|
||||||
import org.apache.commons.lang3.tuple.Pair;
|
import org.apache.commons.lang3.tuple.Pair;
|
||||||
|
@ -109,7 +110,6 @@ import java.util.Collection;
|
||||||
import java.util.Collections;
|
import java.util.Collections;
|
||||||
import java.util.EnumSet;
|
import java.util.EnumSet;
|
||||||
import java.util.HashMap;
|
import java.util.HashMap;
|
||||||
import java.util.Iterator;
|
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
import java.util.function.Supplier;
|
import java.util.function.Supplier;
|
||||||
|
@ -231,7 +231,7 @@ public class QueryStack {
|
||||||
BaseJoiningPredicateBuilder firstPredicateBuilder = mySqlBuilder.getOrCreateFirstPredicateBuilder();
|
BaseJoiningPredicateBuilder firstPredicateBuilder = mySqlBuilder.getOrCreateFirstPredicateBuilder();
|
||||||
ResourceLinkPredicateBuilder sortPredicateBuilder = mySqlBuilder.addReferencePredicateBuilder(this, firstPredicateBuilder.getResourceIdColumn());
|
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.addPredicate(pathPredicate);
|
||||||
mySqlBuilder.addSortNumeric(sortPredicateBuilder.getColumnTargetResourceId(), theAscending);
|
mySqlBuilder.addSortNumeric(sortPredicateBuilder.getColumnTargetResourceId(), theAscending);
|
||||||
}
|
}
|
||||||
|
@ -459,7 +459,7 @@ public class QueryStack {
|
||||||
String chain = (theFilter.getParamPath().getNext() != null) ? theFilter.getParamPath().getNext().toString() : null;
|
String chain = (theFilter.getParamPath().getNext() != null) ? theFilter.getParamPath().getNext().toString() : null;
|
||||||
String value = theFilter.getValue();
|
String value = theFilter.getValue();
|
||||||
ReferenceParam referenceParam = new ReferenceParam(resourceType, chain, value);
|
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) {
|
} else if (typeEnum == RestSearchParameterTypeEnum.QUANTITY) {
|
||||||
return theQueryStack3.createPredicateQuantity(null, theResourceName, null, searchParam, Collections.singletonList(new QuantityParam(theFilter.getValue())), theFilter.getOperation(), theRequestPartitionId);
|
return theQueryStack3.createPredicateQuantity(null, theResourceName, null, searchParam, Collections.singletonList(new QuantityParam(theFilter.getValue())), theFilter.getOperation(), theRequestPartitionId);
|
||||||
} else if (typeEnum == RestSearchParameterTypeEnum.COMPOSITE) {
|
} else if (typeEnum == RestSearchParameterTypeEnum.COMPOSITE) {
|
||||||
|
@ -564,7 +564,7 @@ public class QueryStack {
|
||||||
ResourceLinkPredicateBuilder join = mySqlBuilder.addReferencePredicateBuilderReversed(this, theSourceJoinColumn);
|
ResourceLinkPredicateBuilder join = mySqlBuilder.addReferencePredicateBuilderReversed(this, theSourceJoinColumn);
|
||||||
Condition partitionPredicate = join.createPartitionIdPredicate(theRequestPartitionId);
|
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 typePredicate = BinaryCondition.equalTo(join.getColumnTargetResourceType(), mySqlBuilder.generatePlaceholder(theResourceType));
|
||||||
Condition pathPredicate = toEqualToOrInPredicate(join.getColumnSourcePath(), mySqlBuilder.generatePlaceholders(paths));
|
Condition pathPredicate = toEqualToOrInPredicate(join.getColumnSourcePath(), mySqlBuilder.generatePlaceholders(paths));
|
||||||
Condition linkedPredicate = searchForIdsWithAndOr(join.getColumnSrcResourceId(), targetResourceType, parameterName, Collections.singletonList(orValues), theRequest, theRequestPartitionId, SearchContainedModeEnum.FALSE);
|
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,
|
public Condition createPredicateReference(@Nullable DbColumn theSourceJoinColumn,
|
||||||
String theResourceName,
|
String theResourceName,
|
||||||
String theParamName,
|
String theParamName,
|
||||||
|
List<String> theQualifiers,
|
||||||
List<? extends IQueryParameterType> theList,
|
List<? extends IQueryParameterType> theList,
|
||||||
SearchFilterParser.CompareOperation theOperation,
|
SearchFilterParser.CompareOperation theOperation,
|
||||||
RequestDetails theRequest,
|
RequestDetails theRequest,
|
||||||
RequestPartitionId theRequestPartitionId) {
|
RequestPartitionId theRequestPartitionId) {
|
||||||
|
|
||||||
// This just to ensure the chain has been split correctly
|
|
||||||
assert theParamName.contains(".") == false;
|
|
||||||
|
|
||||||
if ((theOperation != null) &&
|
if ((theOperation != null) &&
|
||||||
(theOperation != SearchFilterParser.CompareOperation.eq) &&
|
(theOperation != SearchFilterParser.CompareOperation.eq) &&
|
||||||
(theOperation != SearchFilterParser.CompareOperation.ne)) {
|
(theOperation != SearchFilterParser.CompareOperation.ne)) {
|
||||||
|
@ -683,7 +681,7 @@ public class QueryStack {
|
||||||
}
|
}
|
||||||
|
|
||||||
ResourceLinkPredicateBuilder predicateBuilder = createOrReusePredicateBuilder(PredicateBuilderTypeEnum.REFERENCE, theSourceJoinColumn, theParamName, () -> mySqlBuilder.addReferencePredicateBuilder(this, theSourceJoinColumn)).getResult();
|
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,
|
public Condition createPredicateReferenceForContainedResource(@Nullable DbColumn theSourceJoinColumn,
|
||||||
|
@ -695,12 +693,14 @@ public class QueryStack {
|
||||||
|
|
||||||
String targetChain = null;
|
String targetChain = null;
|
||||||
String targetParamName = null;
|
String targetParamName = null;
|
||||||
|
String headQualifier = null;
|
||||||
String targetQualifier = null;
|
String targetQualifier = null;
|
||||||
String targetValue = null;
|
String targetValue = null;
|
||||||
|
|
||||||
RuntimeSearchParam targetParamDefinition = null;
|
RuntimeSearchParam targetParamDefinition = null;
|
||||||
|
|
||||||
ArrayList<IQueryParameterType> orValues = Lists.newArrayList();
|
ArrayList<IQueryParameterType> orValues = Lists.newArrayList();
|
||||||
|
List<IQueryParameterType> trimmedParameters = Lists.newArrayList();
|
||||||
IQueryParameterType qp = null;
|
IQueryParameterType qp = null;
|
||||||
|
|
||||||
for (int orIdx = 0; orIdx < theList.size(); orIdx++) {
|
for (int orIdx = 0; orIdx < theList.size(); orIdx++) {
|
||||||
|
@ -715,18 +715,28 @@ public class QueryStack {
|
||||||
targetChain = referenceParam.getChain();
|
targetChain = referenceParam.getChain();
|
||||||
targetParamName = targetChain;
|
targetParamName = targetChain;
|
||||||
targetValue = nextOr.getValueAsQueryToken(myFhirContext);
|
targetValue = nextOr.getValueAsQueryToken(myFhirContext);
|
||||||
|
headQualifier = referenceParam.getResourceType();
|
||||||
|
|
||||||
int qualifierIndex = targetChain.indexOf(':');
|
String targetNextChain = null;
|
||||||
if (qualifierIndex != -1) {
|
int linkIndex = targetChain.indexOf('.');
|
||||||
targetParamName = targetChain.substring(0, qualifierIndex);
|
if (linkIndex != -1) {
|
||||||
targetQualifier = targetChain.substring(qualifierIndex);
|
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
|
// 2. find out the data type
|
||||||
if (targetParamDefinition == null) {
|
if (targetParamDefinition == null) {
|
||||||
Iterator<String> it = theSearchParam.getTargets().iterator();
|
for (String nextTarget : theSearchParam.getTargets()) {
|
||||||
while (it.hasNext()) {
|
if (!referenceParam.hasResourceType() || referenceParam.getResourceType().equals(nextTarget)) {
|
||||||
targetParamDefinition = mySearchParamRegistry.getActiveSearchParam(it.next(), targetParamName);
|
targetParamDefinition = mySearchParamRegistry.getActiveSearchParam(nextTarget, targetParamName);
|
||||||
|
}
|
||||||
if (targetParamDefinition != null)
|
if (targetParamDefinition != null)
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
@ -736,6 +746,10 @@ public class QueryStack {
|
||||||
throw new InvalidRequestException("Unknown search parameter name: " + theSearchParam.getName() + '.' + targetParamName + ".");
|
throw new InvalidRequestException("Unknown search parameter name: " + theSearchParam.getName() + '.' + targetParamName + ".");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (RestSearchParameterTypeEnum.REFERENCE.equals(targetParamDefinition.getParamType())) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
qp = toParameterType(targetParamDefinition);
|
qp = toParameterType(targetParamDefinition);
|
||||||
qp.setValueAsQueryToken(myFhirContext, targetParamName, targetQualifier, targetValue);
|
qp.setValueAsQueryToken(myFhirContext, targetParamName, targetQualifier, targetValue);
|
||||||
orValues.add(qp);
|
orValues.add(qp);
|
||||||
|
@ -746,6 +760,8 @@ 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);
|
||||||
|
|
||||||
// 3. create the query
|
// 3. create the query
|
||||||
Condition containedCondition = null;
|
Condition containedCondition = null;
|
||||||
|
|
||||||
|
@ -778,8 +794,11 @@ public class QueryStack {
|
||||||
containedCondition = createPredicateUri(theSourceJoinColumn, theResourceName, spnamePrefix, targetParamDefinition,
|
containedCondition = createPredicateUri(theSourceJoinColumn, theResourceName, spnamePrefix, targetParamDefinition,
|
||||||
orValues, theOperation, theRequest, theRequestPartitionId);
|
orValues, theOperation, theRequest, theRequestPartitionId);
|
||||||
break;
|
break;
|
||||||
case HAS:
|
|
||||||
case REFERENCE:
|
case REFERENCE:
|
||||||
|
String chainedParamName = theParamName + "." + targetParamName;
|
||||||
|
containedCondition = createPredicateReference(theSourceJoinColumn, theResourceName, chainedParamName, qualifiers, trimmedParameters, theOperation, theRequest, theRequestPartitionId);
|
||||||
|
break;
|
||||||
|
case HAS:
|
||||||
case SPECIAL:
|
case SPECIAL:
|
||||||
default:
|
default:
|
||||||
throw new InvalidRequestException(
|
throw new InvalidRequestException(
|
||||||
|
@ -1123,16 +1142,12 @@ public class QueryStack {
|
||||||
// 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, nextParamDef, nextAnd, null, theRequest, theRequestPartitionId));
|
||||||
} else if (isEligibleForContainedResourceSearch(nextAnd)) {
|
} 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(
|
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)
|
createPredicateReferenceForContainedResource(theSourceJoinColumn, theResourceName, theParamName, nextParamDef, nextAnd, null, theRequest, theRequestPartitionId)
|
||||||
));
|
));
|
||||||
} else {
|
} else {
|
||||||
andPredicates.add(createPredicateReference(theSourceJoinColumn, theResourceName, theParamName, nextAnd, null, theRequest, theRequestPartitionId));
|
andPredicates.add(createPredicateReference(theSourceJoinColumn, theResourceName, theParamName, new ArrayList<>(), nextAnd, null, theRequest, theRequestPartitionId));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
|
@ -1215,8 +1230,8 @@ public class QueryStack {
|
||||||
return myModelConfig.isIndexOnContainedResources() &&
|
return myModelConfig.isIndexOnContainedResources() &&
|
||||||
nextAnd.stream()
|
nextAnd.stream()
|
||||||
.filter(t -> t instanceof ReferenceParam)
|
.filter(t -> t instanceof ReferenceParam)
|
||||||
.map(t -> (ReferenceParam) t)
|
.map(t -> ((ReferenceParam) t).getChain())
|
||||||
.noneMatch(t -> t.getChain().contains("."));
|
.anyMatch(StringUtils::isNotBlank);
|
||||||
}
|
}
|
||||||
|
|
||||||
public void addPredicateCompositeUnique(String theIndexString, RequestPartitionId theRequestPartitionId) {
|
public void addPredicateCompositeUnique(String theIndexString, RequestPartitionId theRequestPartitionId) {
|
||||||
|
|
|
@ -81,7 +81,9 @@ import javax.annotation.Nonnull;
|
||||||
import javax.annotation.Nullable;
|
import javax.annotation.Nullable;
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.Arrays;
|
import java.util.Arrays;
|
||||||
|
import java.util.Collection;
|
||||||
import java.util.Collections;
|
import java.util.Collections;
|
||||||
|
import java.util.HashSet;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.ListIterator;
|
import java.util.ListIterator;
|
||||||
import java.util.Set;
|
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<IIdType> targetIds = new ArrayList<>();
|
||||||
List<String> targetQualifiedUrls = 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
|
* 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;
|
boolean inverse;
|
||||||
if ((theOperation == null) || (theOperation == SearchFilterParser.CompareOperation.eq)) {
|
if ((theOperation == null) || (theOperation == SearchFilterParser.CompareOperation.eq)) {
|
||||||
inverse = false;
|
inverse = false;
|
||||||
|
@ -266,8 +268,8 @@ public class ResourceLinkPredicateBuilder extends BaseJoiningPredicateBuilder {
|
||||||
return toEqualToOrInPredicate(myColumnSrcPath, generatePlaceholders(thePathsToMatch));
|
return toEqualToOrInPredicate(myColumnSrcPath, generatePlaceholders(thePathsToMatch));
|
||||||
}
|
}
|
||||||
|
|
||||||
public Condition createPredicateSourcePaths(String theResourceName, String theParamName) {
|
public Condition createPredicateSourcePaths(String theResourceName, String theParamName, List<String> theQualifiers) {
|
||||||
List<String> pathsToMatch = createResourceLinkPaths(theResourceName, theParamName);
|
List<String> pathsToMatch = createResourceLinkPaths(theResourceName, theParamName, theQualifiers);
|
||||||
return createPredicateSourcePaths(pathsToMatch);
|
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
|
* This is for handling queries like the following: /Observation?device.identifier=urn:system|foo in which we use a chain
|
||||||
* on the device.
|
* 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
|
* 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())) {
|
if (Constants.PARAM_TYPE.equals(theReferenceParam.getChain())) {
|
||||||
|
|
||||||
List<String> pathsToMatch = createResourceLinkPaths(theResourceName, theParamName);
|
List<String> pathsToMatch = createResourceLinkPaths(theResourceName, theParamName, theQualifiers);
|
||||||
Condition typeCondition = createPredicateSourcePaths(pathsToMatch);
|
Condition typeCondition = createPredicateSourcePaths(pathsToMatch);
|
||||||
|
|
||||||
String typeValue = theReferenceParam.getValue();
|
String typeValue = theReferenceParam.getValue();
|
||||||
|
@ -430,7 +432,7 @@ public class ResourceLinkPredicateBuilder extends BaseJoiningPredicateBuilder {
|
||||||
multiTypePredicate = toOrPredicate(orPredicates);
|
multiTypePredicate = toOrPredicate(orPredicates);
|
||||||
}
|
}
|
||||||
|
|
||||||
List<String> pathsToMatch = createResourceLinkPaths(theResourceName, theParamName);
|
List<String> pathsToMatch = createResourceLinkPaths(theResourceName, theParamName, theQualifiers);
|
||||||
Condition pathPredicate = createPredicateSourcePaths(pathsToMatch);
|
Condition pathPredicate = createPredicateSourcePaths(pathsToMatch);
|
||||||
return toAndPredicate(pathPredicate, multiTypePredicate);
|
return toAndPredicate(pathPredicate, multiTypePredicate);
|
||||||
}
|
}
|
||||||
|
@ -440,15 +442,7 @@ public class ResourceLinkPredicateBuilder extends BaseJoiningPredicateBuilder {
|
||||||
final List<Class<? extends IBaseResource>> resourceTypes;
|
final List<Class<? extends IBaseResource>> resourceTypes;
|
||||||
if (!theReferenceParam.hasResourceType()) {
|
if (!theReferenceParam.hasResourceType()) {
|
||||||
|
|
||||||
RuntimeSearchParam param = mySearchParamRegistry.getActiveSearchParam(theResourceName, theParamName);
|
resourceTypes = determineResourceTypes(Collections.singleton(theResourceName), theParamName);
|
||||||
resourceTypes = new ArrayList<>();
|
|
||||||
|
|
||||||
if (param.hasTargets()) {
|
|
||||||
Set<String> targetTypes = param.getTargets();
|
|
||||||
for (String next : targetTypes) {
|
|
||||||
resourceTypes.add(getFhirContext().getResourceDefinition(next).getImplementingClass());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (resourceTypes.isEmpty()) {
|
if (resourceTypes.isEmpty()) {
|
||||||
RuntimeSearchParam searchParamByName = mySearchParamRegistry.getActiveSearchParam(theResourceName, theParamName);
|
RuntimeSearchParam searchParamByName = mySearchParamRegistry.getActiveSearchParam(theResourceName, theParamName);
|
||||||
|
@ -513,25 +507,89 @@ public class ResourceLinkPredicateBuilder extends BaseJoiningPredicateBuilder {
|
||||||
.collect(Collectors.toList());
|
.collect(Collectors.toList());
|
||||||
}
|
}
|
||||||
|
|
||||||
public List<String> createResourceLinkPaths(String theResourceName, String theParamName) {
|
private List<Class<? extends IBaseResource>> determineResourceTypes(Set<String> theResourceNames, String theParamNameChain) {
|
||||||
RuntimeSearchParam param = mySearchParamRegistry.getActiveSearchParam(theResourceName, theParamName);
|
int linkIndex = theParamNameChain.indexOf('.');
|
||||||
List<String> path = param.getPathsSplit();
|
if (linkIndex == -1) {
|
||||||
|
Set<Class<? extends IBaseResource>> resourceTypes = new HashSet<>();
|
||||||
|
for (String resourceName : theResourceNames) {
|
||||||
|
RuntimeSearchParam param = mySearchParamRegistry.getActiveSearchParam(resourceName, theParamNameChain);
|
||||||
|
|
||||||
/*
|
if (param != null && param.hasTargets()) {
|
||||||
* SearchParameters can declare paths on multiple resource
|
Set<String> targetTypes = param.getTargets();
|
||||||
* types. Here we only want the ones that actually apply.
|
for (String next : targetTypes) {
|
||||||
*/
|
resourceTypes.add(getFhirContext().getResourceDefinition(next).getImplementingClass());
|
||||||
path = new ArrayList<>(path);
|
}
|
||||||
|
}
|
||||||
ListIterator<String> iter = path.listIterator();
|
|
||||||
while (iter.hasNext()) {
|
|
||||||
String nextPath = trim(iter.next());
|
|
||||||
if (!nextPath.contains(theResourceName + ".")) {
|
|
||||||
iter.remove();
|
|
||||||
}
|
}
|
||||||
}
|
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.parser.StrictErrorHandler;
|
||||||
import ca.uhn.fhir.rest.api.server.IBundleProvider;
|
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.IdType;
|
import org.hl7.fhir.r4.model.IdType;
|
||||||
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;
|
||||||
|
@ -15,7 +16,6 @@ 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;
|
||||||
|
|
||||||
|
@ -116,6 +116,43 @@ public class ChainingR4SearchTest extends BaseJpaR4Test {
|
||||||
assertThat(oids, contains(oid1.getIdPart()));
|
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
|
@Test
|
||||||
public void testShouldResolveAThreeLinkChainWhereAllResourcesStandAlone() throws Exception {
|
public void testShouldResolveAThreeLinkChainWhereAllResourcesStandAlone() throws Exception {
|
||||||
|
|
||||||
|
@ -188,9 +225,8 @@ public class ChainingR4SearchTest extends BaseJpaR4Test {
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
@Disabled
|
|
||||||
public void testShouldResolveAThreeLinkChainWithAContainedResourceAtTheBeginningOfTheChain() throws Exception {
|
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
|
// setup
|
||||||
IIdType oid1;
|
IIdType oid1;
|
||||||
|
@ -242,11 +278,21 @@ public class ChainingR4SearchTest extends BaseJpaR4Test {
|
||||||
p.getManagingOrganization().setReference(org.getId());
|
p.getManagingOrganization().setReference(org.getId());
|
||||||
myPatientDao.create(p, mySrd);
|
myPatientDao.create(p, mySrd);
|
||||||
|
|
||||||
Observation obs = new Observation();
|
Device d = new Device();
|
||||||
obs.getCode().setText("Observation 1");
|
d.setId(IdType.newRandomUuid());
|
||||||
obs.getSubject().setReference(p.getId());
|
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";
|
String url = "/Observation?subject:Patient.organization:Organization.name=HealthCo";
|
||||||
|
@ -278,11 +324,26 @@ public class ChainingR4SearchTest extends BaseJpaR4Test {
|
||||||
p.getManagingOrganization().setReference("#org");
|
p.getManagingOrganization().setReference("#org");
|
||||||
myPatientDao.create(p, mySrd);
|
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();
|
Observation obs = new Observation();
|
||||||
obs.getCode().setText("Observation 1");
|
obs.getCode().setText("Observation 1");
|
||||||
obs.getSubject().setReference(p.getId());
|
obs.getSubject().setReference(p.getId());
|
||||||
|
|
||||||
oid1 = myObservationDao.create(obs, mySrd).getId().toUnqualifiedVersionless();
|
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";
|
String url = "/Observation?subject:Patient.organization:Organization.name=HealthCo";
|
||||||
|
@ -295,6 +356,55 @@ public class ChainingR4SearchTest extends BaseJpaR4Test {
|
||||||
assertThat(oids, contains(oid1.getIdPart()));
|
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
|
@Test
|
||||||
public void testShouldResolveAFourLinkChainWhereAllResourcesStandAlone() throws Exception {
|
public void testShouldResolveAFourLinkChainWhereAllResourcesStandAlone() throws Exception {
|
||||||
|
|
||||||
|
@ -375,6 +485,52 @@ public class ChainingR4SearchTest extends BaseJpaR4Test {
|
||||||
assertThat(oids, contains(oid1.getIdPart()));
|
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 {
|
private List<String> searchAndReturnUnqualifiedVersionlessIdValues(String theUrl) throws IOException {
|
||||||
List<String> ids = new ArrayList<>();
|
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.config.DaoConfig;
|
||||||
import ca.uhn.fhir.jpa.api.model.DaoMethodOutcome;
|
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.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;
|
||||||
|
@ -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.Bundle;
|
||||||
import org.hl7.fhir.r4.model.DateType;
|
import org.hl7.fhir.r4.model.DateType;
|
||||||
import org.hl7.fhir.r4.model.DecimalType;
|
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.Enumerations;
|
||||||
import org.hl7.fhir.r4.model.IdType;
|
import org.hl7.fhir.r4.model.IdType;
|
||||||
import org.hl7.fhir.r4.model.Observation;
|
import org.hl7.fhir.r4.model.Observation;
|
||||||
|
@ -42,6 +44,7 @@ import java.io.IOException;
|
||||||
import java.math.BigDecimal;
|
import java.math.BigDecimal;
|
||||||
import java.util.Date;
|
import java.util.Date;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
import java.util.Optional;
|
||||||
import java.util.stream.Collectors;
|
import java.util.stream.Collectors;
|
||||||
|
|
||||||
import static org.hamcrest.MatcherAssert.assertThat;
|
import static org.hamcrest.MatcherAssert.assertThat;
|
||||||
|
@ -63,6 +66,7 @@ public class FhirResourceDaoR4CreateTest extends BaseJpaR4Test {
|
||||||
myDaoConfig.setResourceClientIdStrategy(new DaoConfig().getResourceClientIdStrategy());
|
myDaoConfig.setResourceClientIdStrategy(new DaoConfig().getResourceClientIdStrategy());
|
||||||
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());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@ -81,13 +85,43 @@ public class FhirResourceDaoR4CreateTest extends BaseJpaR4Test {
|
||||||
List<ResourceLink> allLinks = myResourceLinkDao.findAll();
|
List<ResourceLink> allLinks = myResourceLinkDao.findAll();
|
||||||
List<String> paths = allLinks
|
List<String> paths = allLinks
|
||||||
.stream()
|
.stream()
|
||||||
.map(t -> t.getSourcePath())
|
.map(ResourceLink::getSourcePath)
|
||||||
.sorted()
|
.sorted()
|
||||||
.collect(Collectors.toList());
|
.collect(Collectors.toList());
|
||||||
assertThat(paths.toString(), paths, contains("Observation.subject", "Observation.subject.where(resolve() is Patient)"));
|
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
|
@Test
|
||||||
public void testConditionalCreateWithPlusInUrl() {
|
public void testConditionalCreateWithPlusInUrl() {
|
||||||
|
@ -245,7 +279,7 @@ public class FhirResourceDaoR4CreateTest extends BaseJpaR4Test {
|
||||||
|
|
||||||
// Read it back
|
// Read it back
|
||||||
p = myPatientDao.read(new IdType("Patient/" + firstClientAssignedId));
|
p = myPatientDao.read(new IdType("Patient/" + firstClientAssignedId));
|
||||||
assertEquals(true, p.getActive());
|
assertTrue(p.getActive());
|
||||||
|
|
||||||
// Now create a client assigned numeric ID
|
// Now create a client assigned numeric ID
|
||||||
p = new Patient();
|
p = new Patient();
|
||||||
|
@ -298,7 +332,7 @@ public class FhirResourceDaoR4CreateTest extends BaseJpaR4Test {
|
||||||
|
|
||||||
// Read it back
|
// Read it back
|
||||||
p = myPatientDao.read(id0.toUnqualifiedVersionless());
|
p = myPatientDao.read(id0.toUnqualifiedVersionless());
|
||||||
assertEquals(true, p.getActive());
|
assertTrue(p.getActive());
|
||||||
|
|
||||||
// Pick an ID that was already used as an internal PID
|
// Pick an ID that was already used as an internal PID
|
||||||
Long newId = runInTransaction(() -> myResourceTableDao.findIdsOfResourcesWithinUpdatedRangeOrderedFromNewest(
|
Long newId = runInTransaction(() -> myResourceTableDao.findIdsOfResourcesWithinUpdatedRangeOrderedFromNewest(
|
||||||
|
|
|
@ -158,6 +158,21 @@ public final class ResourceIndexedSearchParams {
|
||||||
updateSpnamePrefixForIndexedOnContainedResource(myCoordsParams, theSpnamePrefix);
|
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) {
|
void setUpdatedTime(Date theUpdateTime) {
|
||||||
setUpdatedTime(myStringParams, theUpdateTime);
|
setUpdatedTime(myStringParams, theUpdateTime);
|
||||||
setUpdatedTime(myNumberParams, theUpdateTime);
|
setUpdatedTime(myNumberParams, theUpdateTime);
|
||||||
|
|
|
@ -114,6 +114,10 @@ public class SearchParamExtractorService {
|
||||||
// Reference search parameters
|
// Reference search parameters
|
||||||
extractResourceLinks(theRequestPartitionId, theParams, theEntity, theResource, theTransactionDetails, theFailOnInvalidReference, theRequestDetails);
|
extractResourceLinks(theRequestPartitionId, theParams, theEntity, theResource, theTransactionDetails, theFailOnInvalidReference, theRequestDetails);
|
||||||
|
|
||||||
|
if (myModelConfig.isIndexOnContainedResources()) {
|
||||||
|
extractResourceLinksForContainedResources(theRequestPartitionId, theParams, theEntity, theResource, theTransactionDetails, theFailOnInvalidReference, theRequestDetails);
|
||||||
|
}
|
||||||
|
|
||||||
theParams.setUpdatedTime(theTransactionDetails.getTransactionDate());
|
theParams.setUpdatedTime(theTransactionDetails.getTransactionDate());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -402,6 +406,48 @@ public class SearchParamExtractorService {
|
||||||
theParams.myLinks.add(resourceLink);
|
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) {
|
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);
|
ResourcePersistentId resolvedResourceId = theTransactionDetails.getResolvedResourceId(theNextId);
|
||||||
|
|
Loading…
Reference in New Issue