diff --git a/hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/changelog/6_6_0/4863-resolve-searchparametercanonicalizer-does-not-account-for-search-parameters-for-custom-resources-types-when-converting-dstu23-into-runtimesearchparam.yaml b/hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/changelog/6_6_0/4863-resolve-searchparametercanonicalizer-does-not-account-for-search-parameters-for-custom-resources-types-when-converting-dstu23-into-runtimesearchparam.yaml new file mode 100644 index 000000000000..7cb575aa8aa9 --- /dev/null +++ b/hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/changelog/6_6_0/4863-resolve-searchparametercanonicalizer-does-not-account-for-search-parameters-for-custom-resources-types-when-converting-dstu23-into-runtimesearchparam.yaml @@ -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." diff --git a/hapi-fhir-jpaserver-searchparam/src/main/java/ca/uhn/fhir/jpa/searchparam/registry/SearchParameterCanonicalizer.java b/hapi-fhir-jpaserver-searchparam/src/main/java/ca/uhn/fhir/jpa/searchparam/registry/SearchParameterCanonicalizer.java index c136ead47858..e51c7f608a36 100644 --- a/hapi-fhir-jpaserver-searchparam/src/main/java/ca/uhn/fhir/jpa/searchparam/registry/SearchParameterCanonicalizer.java +++ b/hapi-fhir-jpaserver-searchparam/src/main/java/ca/uhn/fhir/jpa/searchparam/registry/SearchParameterCanonicalizer.java @@ -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 toStrings(Collection> theBase) { @@ -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 baseResource = toStrings(Collections.singletonList(theNextSp.getBaseElement())); + List 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) { @@ -138,8 +164,11 @@ private RuntimeSearchParam canonicalizeSearchParameterDstu2(ca.uhn.fhir.model.ds break; } } - Set providesMembershipInCompartments = Collections.emptySet(); - Set targets = DatatypeUtil.toStringSet(theNextSp.getTarget()); + + Set targetResources = DatatypeUtil.toStringSet(theNextSp.getTarget()); + List targetCustomResources = extractDstu2CustomResourcesFromExtensions(theNextSp, HapiExtensions.EXTENSION_SEARCHPARAM_CUSTOM_TARGET_RESOURCE); + + maybeAddCustomResourcesToResources(targetResources, targetCustomResources); if (isBlank(name) || isBlank(path)) { if (paramType != RestSearchParameterTypeEnum.COMPOSITE) { @@ -164,14 +193,19 @@ private RuntimeSearchParam canonicalizeSearchParameterDstu2(ca.uhn.fhir.model.ds } List components = Collections.emptyList(); - Collection> 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 baseResources = new ArrayList<>(toStrings(theNextSp.getBase())); + List baseCustomResources = extractDstu3CustomResourcesFromExtensions(theNextSp, HapiExtensions.EXTENSION_SEARCHPARAM_CUSTOM_BASE_RESOURCE); + + maybeAddCustomResourcesToResources(baseResources, baseCustomResources); + RestSearchParameterTypeEnum paramType = null; RuntimeSearchParam.RuntimeSearchParamStatusEnum status = null; if (theNextSp.getType() != null) { @@ -222,8 +256,11 @@ private RuntimeSearchParam canonicalizeSearchParameterDstu3(org.hl7.fhir.dstu3.m break; } } - Set providesMembershipInCompartments = Collections.emptySet(); - Set targets = DatatypeUtil.toStringSet(theNextSp.getTarget()); + + Set targetResources = DatatypeUtil.toStringSet(theNextSp.getTarget()); + List targetCustomResources = extractDstu3CustomResourcesFromExtensions(theNextSp, HapiExtensions.EXTENSION_SEARCHPARAM_CUSTOM_TARGET_RESOURCE); + + maybeAddCustomResourcesToResources(targetResources, targetCustomResources); if (isBlank(name) || isBlank(path) || paramType == null) { if (paramType != RestSearchParameterTypeEnum.COMPOSITE) { @@ -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 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 baseResources = extractR4PlusResources("base", theNextSp); + List 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; @@ -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; @@ -323,24 +348,11 @@ private RuntimeSearchParam canonicalizeSearchParameterR4Plus(IBaseResource theNe status = RuntimeSearchParam.RuntimeSearchParamStatusEnum.UNKNOWN; break; } - Set providesMembershipInCompartments = Collections.emptySet(); - Set 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 targetResources = extractR4PlusResources("target", theNextSp); + List 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)) { @@ -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() @@ -369,9 +381,9 @@ private RuntimeSearchParam canonicalizeSearchParameterR4Plus(IBaseResource theNe } List 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); } @@ -379,7 +391,15 @@ private RuntimeSearchParam canonicalizeSearchParameterR4Plus(IBaseResource theNe 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 extractR4PlusResources(String thePath, IBaseResource theNextSp) { + return myTerser + .getValues(theNextSp, thePath, IPrimitiveType.class) + .stream() + .map(IPrimitiveType::getValueAsString) + .collect(Collectors.toSet()); } /** @@ -427,5 +447,62 @@ private void setEncoder(RuntimeSearchParam theRuntimeSearchParam, IBaseDatatype } } + private List extractDstu2CustomResourcesFromExtensions(ca.uhn.fhir.model.dstu2.resource.SearchParameter theSearchParameter, String theExtensionUrl) { + + List customSpExtensionDt = theSearchParameter.getUndeclaredExtensionsByUrl(theExtensionUrl); + + return customSpExtensionDt.stream() + .map(theExtensionDt -> theExtensionDt.getValueAsPrimitive().getValueAsString()) + .filter(StringUtils::isNotBlank) + .collect(Collectors.toList()); + } + + private List extractDstu3CustomResourcesFromExtensions(org.hl7.fhir.dstu3.model.SearchParameter theSearchParameter, String theExtensionUrl) { + + List customSpExtensions = theSearchParameter.getExtensionsByUrl(theExtensionUrl); + + return customSpExtensions.stream() + .map(theExtension -> theExtension.getValueAsPrimitive().getValueAsString()) + .filter(StringUtils::isNotBlank) + .collect(Collectors.toList()); + + } + + private List extractR4PlusCustomResourcesFromExtensions(IBaseResource theSearchParameter, String theExtensionUrl) { + + List 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 > void maybeAddCustomResourcesToResources(T theResources, List 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); + } + + } } diff --git a/hapi-fhir-jpaserver-searchparam/src/test/java/ca/uhn/fhir/jpa/searchparam/registry/SearchParameterCanonicalizerTest.java b/hapi-fhir-jpaserver-searchparam/src/test/java/ca/uhn/fhir/jpa/searchparam/registry/SearchParameterCanonicalizerTest.java index 5fdceb4ca1a8..2e2ed172c244 100644 --- a/hapi-fhir-jpaserver-searchparam/src/test/java/ca/uhn/fhir/jpa/searchparam/registry/SearchParameterCanonicalizerTest.java +++ b/hapi-fhir-jpaserver-searchparam/src/test/java/ca/uhn/fhir/jpa/searchparam/registry/SearchParameterCanonicalizerTest.java @@ -2,11 +2,19 @@ import ca.uhn.fhir.context.FhirContext; import ca.uhn.fhir.context.RuntimeSearchParam; +import ca.uhn.fhir.model.api.ExtensionDt; +import ca.uhn.fhir.model.dstu2.valueset.ConformanceResourceStatusEnum; +import ca.uhn.fhir.model.dstu2.valueset.ResourceTypeEnum; +import ca.uhn.fhir.model.dstu2.valueset.SearchParamTypeEnum; +import ca.uhn.fhir.model.primitive.StringDt; import ca.uhn.fhir.rest.api.RestSearchParameterTypeEnum; import ca.uhn.hapi.converters.canonical.VersionCanonicalizer; import org.hl7.fhir.instance.model.api.IBaseResource; +import org.hl7.fhir.r4.model.BaseResource; import org.hl7.fhir.r4.model.Enumerations; import org.hl7.fhir.r4.model.SearchParameter; +import org.hl7.fhir.r4.model.StringType; +import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.extension.ExtendWith; import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.ValueSource; @@ -14,15 +22,104 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import static ca.uhn.fhir.util.HapiExtensions.EXTENSION_SEARCHPARAM_CUSTOM_BASE_RESOURCE; +import static ca.uhn.fhir.util.HapiExtensions.EXTENSION_SEARCHPARAM_CUSTOM_TARGET_RESOURCE; import static org.hamcrest.MatcherAssert.assertThat; import static org.hamcrest.Matchers.contains; import static org.hamcrest.Matchers.containsInAnyOrder; +import static org.hamcrest.Matchers.is; +import static org.hamcrest.Matchers.not; import static org.junit.jupiter.api.Assertions.assertEquals; @ExtendWith(MockitoExtension.class) public class SearchParameterCanonicalizerTest { private static final Logger ourLog = LoggerFactory.getLogger(SearchParameterCanonicalizerTest.class); + ca.uhn.fhir.model.dstu2.resource.SearchParameter initSearchParamDstu2(){ + ca.uhn.fhir.model.dstu2.resource.SearchParameter sp = new ca.uhn.fhir.model.dstu2.resource.SearchParameter(); + sp.setId("SearchParameter/meal-chef"); + sp.setUrl("http://example.org/SearchParameter/meal-chef"); + sp.setBase(ResourceTypeEnum.RESOURCE); + sp.setCode("chef"); + sp.setType(SearchParamTypeEnum.REFERENCE); + sp.setStatus(ConformanceResourceStatusEnum.ACTIVE); + sp.setXpath("Meal.chef | Observation.subject"); + sp.addTarget(ResourceTypeEnum.RESOURCE); + sp.addTarget(ResourceTypeEnum.OBSERVATION); + sp.addUndeclaredExtension(new ExtensionDt(false, EXTENSION_SEARCHPARAM_CUSTOM_BASE_RESOURCE, new StringDt("Meal"))); + sp.addUndeclaredExtension(new ExtensionDt(false, EXTENSION_SEARCHPARAM_CUSTOM_TARGET_RESOURCE, new StringDt("Chef"))); + return sp; + } + + org.hl7.fhir.dstu3.model.SearchParameter initSearchParamDstu3(){ + org.hl7.fhir.dstu3.model.SearchParameter sp = new org.hl7.fhir.dstu3.model.SearchParameter(); + sp.setId("SearchParameter/meal-chef"); + sp.setUrl("http://example.org/SearchParameter/meal-chef"); + sp.addBase("Resource"); + sp.addBase("Patient"); + sp.setCode("chef"); + sp.setType(org.hl7.fhir.dstu3.model.Enumerations.SearchParamType.REFERENCE); + sp.setStatus(org.hl7.fhir.dstu3.model.Enumerations.PublicationStatus.ACTIVE); + sp.setExpression("Meal.chef | Observation.subject"); + sp.addTarget("Resource"); + sp.addTarget("Observation"); + sp.addExtension(EXTENSION_SEARCHPARAM_CUSTOM_BASE_RESOURCE, new org.hl7.fhir.dstu3.model.StringType("Meal")); + sp.addExtension(EXTENSION_SEARCHPARAM_CUSTOM_TARGET_RESOURCE, new org.hl7.fhir.dstu3.model.StringType("Chef")); + return sp; + } + + IBaseResource initSearchParamR4(){ + SearchParameter sp = new SearchParameter(); + sp.setId("SearchParameter/meal-chef"); + sp.setUrl("http://example.org/SearchParameter/meal-chef"); + sp.addBase("Resource"); + sp.addBase("Patient"); + sp.setCode("chef"); + sp.setType(Enumerations.SearchParamType.REFERENCE); + sp.setStatus(Enumerations.PublicationStatus.ACTIVE); + sp.setExpression("Meal.chef | Observation.subject"); + sp.addTarget("Resource"); + sp.addTarget("Observation"); + sp.addExtension(EXTENSION_SEARCHPARAM_CUSTOM_BASE_RESOURCE, new StringType("Meal")); + sp.addExtension(EXTENSION_SEARCHPARAM_CUSTOM_TARGET_RESOURCE, new StringType("Chef")); + return sp; + } + + IBaseResource initSearchParamR4B(){ + org.hl7.fhir.r4b.model.SearchParameter sp = new org.hl7.fhir.r4b.model.SearchParameter(); + sp.setId("SearchParameter/meal-chef"); + sp.setUrl("http://example.org/SearchParameter/meal-chef"); + sp.addBase("Resource"); + sp.addBase("Patient"); + sp.setCode("chef"); + sp.setType(org.hl7.fhir.r4b.model.Enumerations.SearchParamType.REFERENCE); + sp.setStatus(org.hl7.fhir.r4b.model.Enumerations.PublicationStatus.ACTIVE); + sp.setExpression("Meal.chef | Observation.subject"); + sp.addTarget("Resource"); + sp.addTarget("Observation"); + sp.addExtension(EXTENSION_SEARCHPARAM_CUSTOM_BASE_RESOURCE, new org.hl7.fhir.r4b.model.StringType("Meal")); + sp.addExtension(EXTENSION_SEARCHPARAM_CUSTOM_TARGET_RESOURCE, new org.hl7.fhir.r4b.model.StringType("Chef")); + return sp; + } + + IBaseResource initSearchParamR5(){ + org.hl7.fhir.r5.model.SearchParameter sp = new org.hl7.fhir.r5.model.SearchParameter(); + sp.setId("SearchParameter/meal-chef"); + sp.setUrl("http://example.org/SearchParameter/meal-chef"); + sp.addBase(org.hl7.fhir.r5.model.Enumerations.VersionIndependentResourceTypesAll.RESOURCE); + sp.addBase(org.hl7.fhir.r5.model.Enumerations.VersionIndependentResourceTypesAll.PATIENT); + sp.setCode("chef"); + sp.setType(org.hl7.fhir.r5.model.Enumerations.SearchParamType.REFERENCE); + sp.setStatus(org.hl7.fhir.r5.model.Enumerations.PublicationStatus.ACTIVE); + sp.setExpression("Meal.chef | Observation.subject"); + sp.addTarget(org.hl7.fhir.r5.model.Enumerations.VersionIndependentResourceTypesAll.RESOURCE); + sp.addTarget(org.hl7.fhir.r5.model.Enumerations.VersionIndependentResourceTypesAll.OBSERVATION); + sp.addExtension(EXTENSION_SEARCHPARAM_CUSTOM_BASE_RESOURCE, new org.hl7.fhir.r5.model.StringType("Meal")); + sp.addExtension(EXTENSION_SEARCHPARAM_CUSTOM_TARGET_RESOURCE, new org.hl7.fhir.r5.model.StringType("Chef")); + return sp; + } + + @ParameterizedTest @ValueSource(booleans = {false, true}) public void testCanonicalizeSearchParameterWithCustomType(boolean theConvertToR5) { @@ -37,7 +134,6 @@ public void testCanonicalizeSearchParameterWithCustomType(boolean theConvertToR5 sp.setExpression("Meal.chef | Observation.subject"); sp.addTarget("Chef"); sp.addTarget("Observation"); - IBaseResource searchParamToCanonicalize = sp; SearchParameterCanonicalizer svc; if (theConvertToR5) { @@ -57,7 +153,51 @@ public void testCanonicalizeSearchParameterWithCustomType(boolean theConvertToR5 assertThat(output.getPathsSplit(), containsInAnyOrder("Meal.chef", "Observation.subject")); assertThat(output.getBase(), containsInAnyOrder("Meal", "Patient")); assertThat(output.getTargets(), contains("Chef", "Observation")); + } + + @ParameterizedTest + @ValueSource(strings = {"Dstu2", "Dstu3", "R4", "R4B", "R5"}) + public void testCanonicalizeSearchParameterWithCustomTypeAllVersion(String version) { + SearchParameterCanonicalizer svc; + IBaseResource searchParamToCanonicalize; + switch (version){ + case "Dstu2": + searchParamToCanonicalize = initSearchParamDstu2(); + svc = new SearchParameterCanonicalizer(FhirContext.forDstu2Cached()); + break; + case "Dstu3": + searchParamToCanonicalize = initSearchParamDstu3(); + svc = new SearchParameterCanonicalizer(FhirContext.forDstu3Cached()); + break; + case "R4": + searchParamToCanonicalize = initSearchParamR4(); + svc = new SearchParameterCanonicalizer(FhirContext.forR4Cached()); + break; + case "R4B": + searchParamToCanonicalize = initSearchParamR4B(); + svc = new SearchParameterCanonicalizer(FhirContext.forR4BCached()); + break; + default: + searchParamToCanonicalize = initSearchParamR5(); + svc = new SearchParameterCanonicalizer(FhirContext.forR5Cached()); + break; + } + + RuntimeSearchParam output = svc.canonicalizeSearchParameter(searchParamToCanonicalize); + assertEquals("chef", output.getName()); + assertEquals(RestSearchParameterTypeEnum.REFERENCE, output.getParamType()); + assertEquals(RuntimeSearchParam.RuntimeSearchParamStatusEnum.ACTIVE, output.getStatus()); + assertThat(output.getPathsSplit(), containsInAnyOrder("Meal.chef", "Observation.subject")); + // DSTU2 Resources must only have 1 base + if ("Dstu2".equals(version)){ + assertThat(output.getBase(), containsInAnyOrder("Meal")); + } else { + assertThat(output.getBase(), containsInAnyOrder("Meal", "Patient")); + } + assertThat(output.getTargets(), containsInAnyOrder("Chef", "Observation")); + assertThat(output.getBase(), not(contains("Resource"))); + assertThat(output.getTargets(), not(contains("Resource"))); } }