Skip to content
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 @@ -96,21 +114,11 @@ private RuntimeSearchParam canonicalizeSearchParameterDstu2(ca.uhn.fhir.model.ds
String description = theNextSp.getDescription();
String path = theNextSp.getXpath();

Collection<String> base = toStrings(Collections.singletonList(theNextSp.getBaseElement()));
// add extensions as base if the SP is for a custom resource type
List<ExtensionDt> customSPBase = theNextSp.getUndeclaredExtensionsByUrl(HapiExtensions.EXTENSION_SEARCHPARAM_CUSTOM_BASE_RESOURCE);
ArrayList<String> baseFromExtensions = new ArrayList<>();
for (ExtensionDt e : customSPBase) {
String eStr = e.getValueAsPrimitive().getValueAsString();
if (StringUtils.isNotBlank(eStr)) {
baseFromExtensions.add(eStr);
}
}
// when base contains custom resources, "Resource" is present to satisfy 1...* cardinality requirement of FHIR
// removed it for RuntimeSearchParam
if (!baseFromExtensions.isEmpty()){
base.remove("Resource");
base.addAll(baseFromExtensions);
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;
Expand Down Expand Up @@ -156,22 +164,11 @@ private RuntimeSearchParam canonicalizeSearchParameterDstu2(ca.uhn.fhir.model.ds
break;
}
}
Set<String> targets = DatatypeUtil.toStringSet(theNextSp.getTarget());
// add extensions as target if the SP is for a custom resource type
Set<String> targetsFromExtensions = new HashSet<>();
List<ExtensionDt> customSPTargets = theNextSp.getUndeclaredExtensionsByUrl(HapiExtensions.EXTENSION_SEARCHPARAM_CUSTOM_TARGET_RESOURCE);
for (ExtensionDt e : customSPTargets) {
String eStr = e.getValueAsPrimitive().getValueAsString();
if (StringUtils.isNotBlank(eStr)) {
targetsFromExtensions.add(eStr);
}
}
// when targets contains custom resources, "Resource" is present to satisfy 1...* cardinality requirement of FHIR
// removed it for RuntimeSearchParam
if (!targetsFromExtensions.isEmpty()){
targets.remove("Resource");
targets.addAll(targetsFromExtensions);
}

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 @@ -196,30 +193,18 @@ private RuntimeSearchParam canonicalizeSearchParameterDstu2(ca.uhn.fhir.model.ds
}

List<RuntimeSearchParam.Component> components = Collections.emptyList();
return new RuntimeSearchParam(id, uri, name, description, path, paramType, Collections.emptySet(), targets, status, unique, components, 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> base = new ArrayList<>(toStrings(theNextSp.getBase()));
// add extensions as base if the SP is for a custom resource type
List<Extension> customSPBase = theNextSp.getExtensionsByUrl(HapiExtensions.EXTENSION_SEARCHPARAM_CUSTOM_BASE_RESOURCE);
ArrayList<String> baseFromExtensions = new ArrayList<>();
for (Extension e : customSPBase){
String eStr = e.getValueAsPrimitive().getValueAsString();
if (StringUtils.isNotBlank(eStr)) {
baseFromExtensions.add(eStr);
}
}
// when base contains custom resources, "Resource" is present to satisfy 1...* cardinality requirement of FHIR
// removed it for RuntimeSearchParam
if (!baseFromExtensions.isEmpty()){
base.remove("Resource");
base.addAll(baseFromExtensions);
}
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;
Expand Down Expand Up @@ -272,22 +257,10 @@ private RuntimeSearchParam canonicalizeSearchParameterDstu3(org.hl7.fhir.dstu3.m
}
}

Set<String> targets = DatatypeUtil.toStringSet(theNextSp.getTarget());
// add extensions as target if the SP is for a custom resource type
Set<String> targetsFromExtensions = new HashSet<>();
List<Extension> customSPTargets = theNextSp.getExtensionsByUrl(HapiExtensions.EXTENSION_SEARCHPARAM_CUSTOM_TARGET_RESOURCE);
for (Extension e : customSPTargets) {
String eStr = e.getValueAsPrimitive().getValueAsString();
if (StringUtils.isNotBlank(eStr)) {
targetsFromExtensions.add(eStr);
}
}
// when targets contains custom resources, "Resource" is present to satisfy 1...* cardinality requirement of FHIR
// removed it for RuntimeSearchParam
if (!targetsFromExtensions.isEmpty()){
targets.remove("Resource");
targets.addAll(targetsFromExtensions);
}
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 @@ -316,44 +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, Collections.emptySet(), targets, status, unique, components, base);
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");

String name = myTerser.getSinglePrimitiveValueOrNull(theNextSp, "code");
String description = myTerser.getSinglePrimitiveValueOrNull(theNextSp, "description");
String path = myTerser.getSinglePrimitiveValueOrNull(theNextSp, "expression");

List<String> base = terser
.getValues(theNextSp, "base", IPrimitiveType.class)
.stream()
.map(IPrimitiveType::getValueAsString)
.collect(Collectors.toList());
Set<String> baseResources = extractR4PlusResources("base", theNextSp);
List<String> baseCustomResources = extractR4PlusCustomResourcesFromExtensions(theNextSp, HapiExtensions.EXTENSION_SEARCHPARAM_CUSTOM_BASE_RESOURCE);

ArrayList<String> baseFromExtension = new ArrayList<>();
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(baseFromExtension::add);
}
// when base contains custom resources, "Resource" is present to satisfy 1...* cardinality requirement of FHIR
// removed it for RuntimeSearchParam
if (!baseFromExtension.isEmpty()){
base.remove("Resource");
base.addAll(baseFromExtension);
}
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 @@ -382,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 @@ -397,29 +349,10 @@ private RuntimeSearchParam canonicalizeSearchParameterR4Plus(IBaseResource theNe
break;
}

Set<String> targets = terser
.getValues(theNextSp, "target", IPrimitiveType.class)
.stream()
.map(IPrimitiveType::getValueAsString)
.collect(Collectors.toSet());
Set<String> targetsFromExtensions = new HashSet<>();
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(targetsFromExtensions::add);
}
// when targets contains custom resources, "Resource" is present to satisfy 1...* cardinality requirement of FHIR
// removed it for RuntimeSearchParam
if(!targetsFromExtensions.isEmpty()){
targets.remove("Resource");
targets.addAll(targetsFromExtensions);
}
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 @@ -430,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 @@ -448,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, Collections.emptySet(), 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 @@ -506,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);
}

}

}