Skip to content
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
type: fix
issue: 4863
title: "Previously the SearchParameterCanonicalizer did not correctly convert DSTU2 and DSTU3 custom resources SearchParameters
into RuntimeSearchParam. This is now fixed."
Original file line number Diff line number Diff line change
Expand Up @@ -27,30 +27,48 @@
import ca.uhn.fhir.model.api.ExtensionDt;
import ca.uhn.fhir.rest.api.RestSearchParameterTypeEnum;
import ca.uhn.fhir.rest.server.exceptions.InternalErrorException;
import ca.uhn.fhir.util.*;
import ca.uhn.fhir.util.DatatypeUtil;
import ca.uhn.fhir.util.ExtensionUtil;
import ca.uhn.fhir.util.FhirTerser;
import ca.uhn.fhir.util.HapiExtensions;
import ca.uhn.fhir.util.PhoneticEncoderUtil;
import org.apache.commons.lang3.StringUtils;
import org.hl7.fhir.dstu3.model.Extension;
import org.hl7.fhir.dstu3.model.SearchParameter;
import org.hl7.fhir.instance.model.api.*;
import org.hl7.fhir.instance.model.api.IBase;
import org.hl7.fhir.instance.model.api.IBaseDatatype;
import org.hl7.fhir.instance.model.api.IBaseExtension;
import org.hl7.fhir.instance.model.api.IBaseHasExtensions;
import org.hl7.fhir.instance.model.api.IBaseResource;
import org.hl7.fhir.instance.model.api.IIdType;
import org.hl7.fhir.instance.model.api.IPrimitiveType;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

import java.util.*;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import java.util.stream.Collectors;

import static org.apache.commons.lang3.StringUtils.*;
import static org.apache.commons.lang3.StringUtils.isBlank;
import static org.apache.commons.lang3.StringUtils.isNotBlank;
import static org.apache.commons.lang3.StringUtils.startsWith;

@Service
public class SearchParameterCanonicalizer {
private static final Logger ourLog = LoggerFactory.getLogger(SearchParameterCanonicalizer.class);

private final FhirContext myFhirContext;

private final FhirTerser myTerser;
@Autowired
public SearchParameterCanonicalizer(FhirContext theFhirContext) {
myFhirContext = theFhirContext;
myTerser = myFhirContext.newTerser();
}

private static Collection<String> toStrings(Collection<? extends IPrimitiveType<String>> theBase) {
Expand Down Expand Up @@ -95,6 +113,14 @@ private RuntimeSearchParam canonicalizeSearchParameterDstu2(ca.uhn.fhir.model.ds
String name = theNextSp.getCode();
String description = theNextSp.getDescription();
String path = theNextSp.getXpath();

Collection<String> baseResource = toStrings(Collections.singletonList(theNextSp.getBaseElement()));
List<String> baseCustomResources = extractDstu2CustomResourcesFromExtensions(theNextSp, HapiExtensions.EXTENSION_SEARCHPARAM_CUSTOM_BASE_RESOURCE);

if(!baseCustomResources.isEmpty()){
baseResource = Collections.singleton(baseCustomResources.get(0));
}

RestSearchParameterTypeEnum paramType = null;
RuntimeSearchParam.RuntimeSearchParamStatusEnum status = null;
if (theNextSp.getTypeElement().getValueAsEnum() != null) {
Expand Down Expand Up @@ -138,8 +164,11 @@ private RuntimeSearchParam canonicalizeSearchParameterDstu2(ca.uhn.fhir.model.ds
break;
}
}
Set<String> providesMembershipInCompartments = Collections.emptySet();
Set<String> targets = DatatypeUtil.toStringSet(theNextSp.getTarget());

Set<String> targetResources = DatatypeUtil.toStringSet(theNextSp.getTarget());
List<String> targetCustomResources = extractDstu2CustomResourcesFromExtensions(theNextSp, HapiExtensions.EXTENSION_SEARCHPARAM_CUSTOM_TARGET_RESOURCE);

maybeAddCustomResourcesToResources(targetResources, targetCustomResources);

if (isBlank(name) || isBlank(path)) {
if (paramType != RestSearchParameterTypeEnum.COMPOSITE) {
Expand All @@ -164,14 +193,19 @@ private RuntimeSearchParam canonicalizeSearchParameterDstu2(ca.uhn.fhir.model.ds
}

List<RuntimeSearchParam.Component> components = Collections.emptyList();
Collection<? extends IPrimitiveType<String>> base = Collections.singletonList(theNextSp.getBaseElement());
return new RuntimeSearchParam(id, uri, name, description, path, paramType, providesMembershipInCompartments, targets, status, unique, components, toStrings(base));
return new RuntimeSearchParam(id, uri, name, description, path, paramType, Collections.emptySet(), targetResources, status, unique, components, baseResource);
}

private RuntimeSearchParam canonicalizeSearchParameterDstu3(org.hl7.fhir.dstu3.model.SearchParameter theNextSp) {
String name = theNextSp.getCode();
String description = theNextSp.getDescription();
String path = theNextSp.getExpression();

List<String> baseResources = new ArrayList<>(toStrings(theNextSp.getBase()));
List<String> baseCustomResources = extractDstu3CustomResourcesFromExtensions(theNextSp, HapiExtensions.EXTENSION_SEARCHPARAM_CUSTOM_BASE_RESOURCE);

maybeAddCustomResourcesToResources(baseResources, baseCustomResources);

RestSearchParameterTypeEnum paramType = null;
RuntimeSearchParam.RuntimeSearchParamStatusEnum status = null;
if (theNextSp.getType() != null) {
Expand Down Expand Up @@ -222,8 +256,11 @@ private RuntimeSearchParam canonicalizeSearchParameterDstu3(org.hl7.fhir.dstu3.m
break;
}
}
Set<String> providesMembershipInCompartments = Collections.emptySet();
Set<String> targets = DatatypeUtil.toStringSet(theNextSp.getTarget());

Set<String> targetResources = DatatypeUtil.toStringSet(theNextSp.getTarget());
List<String> targetCustomResources = extractDstu3CustomResourcesFromExtensions(theNextSp, HapiExtensions.EXTENSION_SEARCHPARAM_CUSTOM_TARGET_RESOURCE);

maybeAddCustomResourcesToResources(targetResources, targetCustomResources);

if (isBlank(name) || isBlank(path) || paramType == null) {
if (paramType != RestSearchParameterTypeEnum.COMPOSITE) {
Expand Down Expand Up @@ -252,35 +289,23 @@ private RuntimeSearchParam canonicalizeSearchParameterDstu3(org.hl7.fhir.dstu3.m
components.add(new RuntimeSearchParam.Component(next.getExpression(), next.getDefinition().getReferenceElement().toUnqualifiedVersionless().getValue()));
}

return new RuntimeSearchParam(id, uri, name, description, path, paramType, providesMembershipInCompartments, targets, status, unique, components, toStrings(theNextSp.getBase()));
return new RuntimeSearchParam(id, uri, name, description, path, paramType, Collections.emptySet(), targetResources, status, unique, components, baseResources);
}

private RuntimeSearchParam canonicalizeSearchParameterR4Plus(IBaseResource theNextSp) {
FhirTerser terser = myFhirContext.newTerser();
String name = terser.getSinglePrimitiveValueOrNull(theNextSp, "code");
String description = terser.getSinglePrimitiveValueOrNull(theNextSp, "description");
String path = terser.getSinglePrimitiveValueOrNull(theNextSp, "expression");

List<String> base = terser
.getValues(theNextSp, "base", IPrimitiveType.class)
.stream()
.map(IPrimitiveType::getValueAsString)
.collect(Collectors.toList());
if (theNextSp instanceof IBaseHasExtensions) {
((IBaseHasExtensions) theNextSp)
.getExtension()
.stream()
.filter(t -> HapiExtensions.EXTENSION_SEARCHPARAM_CUSTOM_BASE_RESOURCE.equals(t.getUrl()))
.filter(t -> t.getValue() instanceof IPrimitiveType)
.map(t -> ((IPrimitiveType<?>) t.getValue()))
.map(IPrimitiveType::getValueAsString)
.filter(StringUtils::isNotBlank)
.forEach(base::add);
}
String name = myTerser.getSinglePrimitiveValueOrNull(theNextSp, "code");
String description = myTerser.getSinglePrimitiveValueOrNull(theNextSp, "description");
String path = myTerser.getSinglePrimitiveValueOrNull(theNextSp, "expression");

Set<String> baseResources = extractR4PlusResources("base", theNextSp);
List<String> baseCustomResources = extractR4PlusCustomResourcesFromExtensions(theNextSp, HapiExtensions.EXTENSION_SEARCHPARAM_CUSTOM_BASE_RESOURCE);

maybeAddCustomResourcesToResources(baseResources, baseCustomResources);

RestSearchParameterTypeEnum paramType = null;
RuntimeSearchParam.RuntimeSearchParamStatusEnum status = null;
switch (terser.getSinglePrimitiveValue(theNextSp, "type").orElse("")) {
switch (myTerser.getSinglePrimitiveValue(theNextSp, "type").orElse("")) {
case "composite":
paramType = RestSearchParameterTypeEnum.COMPOSITE;
break;
Expand Down Expand Up @@ -309,7 +334,7 @@ private RuntimeSearchParam canonicalizeSearchParameterR4Plus(IBaseResource theNe
paramType = RestSearchParameterTypeEnum.SPECIAL;
break;
}
switch (terser.getSinglePrimitiveValue(theNextSp, "status").orElse("")) {
switch (myTerser.getSinglePrimitiveValue(theNextSp, "status").orElse("")) {
case "active":
status = RuntimeSearchParam.RuntimeSearchParamStatusEnum.ACTIVE;
break;
Expand All @@ -323,24 +348,11 @@ private RuntimeSearchParam canonicalizeSearchParameterR4Plus(IBaseResource theNe
status = RuntimeSearchParam.RuntimeSearchParamStatusEnum.UNKNOWN;
break;
}
Set<String> providesMembershipInCompartments = Collections.emptySet();

Set<String> targets = terser
.getValues(theNextSp, "target", IPrimitiveType.class)
.stream()
.map(IPrimitiveType::getValueAsString)
.collect(Collectors.toSet());
if (theNextSp instanceof IBaseHasExtensions) {
((IBaseHasExtensions) theNextSp)
.getExtension()
.stream()
.filter(t -> HapiExtensions.EXTENSION_SEARCHPARAM_CUSTOM_TARGET_RESOURCE.equals(t.getUrl()))
.filter(t -> t.getValue() instanceof IPrimitiveType)
.map(t -> ((IPrimitiveType<?>) t.getValue()))
.map(IPrimitiveType::getValueAsString)
.filter(StringUtils::isNotBlank)
.forEach(targets::add);
}
Set<String> targetResources = extractR4PlusResources("target", theNextSp);
List<String> targetCustomResources = extractR4PlusCustomResourcesFromExtensions(theNextSp, HapiExtensions.EXTENSION_SEARCHPARAM_CUSTOM_TARGET_RESOURCE);

maybeAddCustomResourcesToResources(targetResources, targetCustomResources);

if (isBlank(name) || isBlank(path) || paramType == null) {
if ("_text".equals(name) || "_content".equals(name)) {
Expand All @@ -351,7 +363,7 @@ private RuntimeSearchParam canonicalizeSearchParameterR4Plus(IBaseResource theNe
}

IIdType id = theNextSp.getIdElement();
String uri = terser.getSinglePrimitiveValueOrNull(theNextSp, "url");
String uri = myTerser.getSinglePrimitiveValueOrNull(theNextSp, "url");
ComboSearchParamType unique = null;

String value = ((IBaseHasExtensions) theNextSp).getExtension()
Expand All @@ -369,17 +381,25 @@ private RuntimeSearchParam canonicalizeSearchParameterR4Plus(IBaseResource theNe
}

List<RuntimeSearchParam.Component> components = new ArrayList<>();
for (IBase next : terser.getValues(theNextSp, "component")) {
String expression = terser.getSinglePrimitiveValueOrNull(next, "expression");
String definition = terser.getSinglePrimitiveValueOrNull(next, "definition");
for (IBase next : myTerser.getValues(theNextSp, "component")) {
String expression = myTerser.getSinglePrimitiveValueOrNull(next, "expression");
String definition = myTerser.getSinglePrimitiveValueOrNull(next, "definition");
if (startsWith(definition, "/SearchParameter/")) {
definition = definition.substring(1);
}

components.add(new RuntimeSearchParam.Component(expression, definition));
}

return new RuntimeSearchParam(id, uri, name, description, path, paramType, providesMembershipInCompartments, targets, status, unique, components, base);
return new RuntimeSearchParam(id, uri, name, description, path, paramType, Collections.emptySet(), targetResources, status, unique, components, baseResources);
}

private Set<String> extractR4PlusResources(String thePath, IBaseResource theNextSp) {
return myTerser
.getValues(theNextSp, thePath, IPrimitiveType.class)
.stream()
.map(IPrimitiveType::getValueAsString)
.collect(Collectors.toSet());
}

/**
Expand Down Expand Up @@ -427,5 +447,62 @@ private void setEncoder(RuntimeSearchParam theRuntimeSearchParam, IBaseDatatype
}
}

private List<String> extractDstu2CustomResourcesFromExtensions(ca.uhn.fhir.model.dstu2.resource.SearchParameter theSearchParameter, String theExtensionUrl) {

List<ExtensionDt> customSpExtensionDt = theSearchParameter.getUndeclaredExtensionsByUrl(theExtensionUrl);

return customSpExtensionDt.stream()
.map(theExtensionDt -> theExtensionDt.getValueAsPrimitive().getValueAsString())
.filter(StringUtils::isNotBlank)
.collect(Collectors.toList());
}

private List<String> extractDstu3CustomResourcesFromExtensions(org.hl7.fhir.dstu3.model.SearchParameter theSearchParameter, String theExtensionUrl) {

List<Extension> customSpExtensions = theSearchParameter.getExtensionsByUrl(theExtensionUrl);

return customSpExtensions.stream()
.map(theExtension -> theExtension.getValueAsPrimitive().getValueAsString())
.filter(StringUtils::isNotBlank)
.collect(Collectors.toList());

}

private List<String> extractR4PlusCustomResourcesFromExtensions(IBaseResource theSearchParameter, String theExtensionUrl) {

List<String> retVal = new ArrayList<>();

if (theSearchParameter instanceof IBaseHasExtensions) {
((IBaseHasExtensions) theSearchParameter)
.getExtension()
.stream()
.filter(t -> theExtensionUrl.equals(t.getUrl()))
.filter(t -> t.getValue() instanceof IPrimitiveType)
.map(t -> ((IPrimitiveType<?>) t.getValue()))
.map(IPrimitiveType::getValueAsString)
.filter(StringUtils::isNotBlank)
.forEach(retVal::add);
}

return retVal;
}

private <T extends Collection<String>> void maybeAddCustomResourcesToResources(T theResources, List<String> theCustomResources) {
// SearchParameter base and target components require strict binding to ResourceType for dstu[2|3], R4, R4B
// and to Version Independent Resource Types for R5.
//
// To handle custom resources, we set a placeholder of type 'Resource' in the base or target component and define
// the custom resource by adding a corresponding extension with url HapiExtensions.EXTENSION_SEARCHPARAM_CUSTOM_BASE_RESOURCE
// or HapiExtensions.EXTENSION_SEARCHPARAM_CUSTOM_TARGET_RESOURCE with the name of the custom resource.
//
// To provide a base/target list that contains both the resources and customResources, we need to remove the placeholders
// from the theResources and add theCustomResources.

if (!theCustomResources.isEmpty()){
theResources.removeAll(Collections.singleton("Resource"));
theResources.addAll(theCustomResources);
}

}

}
Loading