From f6c1451588f46597bd4fd42d0d4ad142e4b71f54 Mon Sep 17 00:00:00 2001 From: tyner Date: Fri, 12 May 2023 12:32:13 -0400 Subject: [PATCH 1/9] Modified canonicalizeSearchParameterDstu2 and 3, now correctly detect search parameters for custom resources --- .../SearchParameterCanonicalizer.java | 44 +++++++++++++++++-- 1 file changed, 41 insertions(+), 3 deletions(-) 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..e8f10459b6f0 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 @@ -95,6 +95,17 @@ private RuntimeSearchParam canonicalizeSearchParameterDstu2(ca.uhn.fhir.model.ds String name = theNextSp.getCode(); String description = theNextSp.getDescription(); String path = theNextSp.getXpath(); + + Collection base = toStrings(Collections.singletonList(theNextSp.getBaseElement())); + // add extensions as base if the SP is for a custom resource type + List customSPBase = theNextSp.getUndeclaredExtensionsByUrl(HapiExtensions.EXTENSION_SEARCHPARAM_CUSTOM_BASE_RESOURCE); + for (ExtensionDt e : customSPBase) { + String eStr = e.getValueAsPrimitive().getValueAsString(); + if (StringUtils.isNotBlank(eStr)) { + base.add(eStr); + } + } + RestSearchParameterTypeEnum paramType = null; RuntimeSearchParam.RuntimeSearchParamStatusEnum status = null; if (theNextSp.getTypeElement().getValueAsEnum() != null) { @@ -140,6 +151,14 @@ private RuntimeSearchParam canonicalizeSearchParameterDstu2(ca.uhn.fhir.model.ds } Set providesMembershipInCompartments = Collections.emptySet(); Set targets = DatatypeUtil.toStringSet(theNextSp.getTarget()); + // add extensions as target if the SP is for a custom resource type + List customSPTargets = theNextSp.getUndeclaredExtensionsByUrl(HapiExtensions.EXTENSION_SEARCHPARAM_CUSTOM_TARGET_RESOURCE); + for (ExtensionDt e : customSPTargets) { + String eStr = e.getValueAsPrimitive().getValueAsString(); + if (StringUtils.isNotBlank(eStr)) { + targets.add(eStr); + } + } if (isBlank(name) || isBlank(path)) { if (paramType != RestSearchParameterTypeEnum.COMPOSITE) { @@ -164,14 +183,24 @@ 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, providesMembershipInCompartments, targets, status, unique, components, base); } private RuntimeSearchParam canonicalizeSearchParameterDstu3(org.hl7.fhir.dstu3.model.SearchParameter theNextSp) { String name = theNextSp.getCode(); String description = theNextSp.getDescription(); String path = theNextSp.getExpression(); + + List base = new ArrayList<>(toStrings(theNextSp.getBase())); + // add extensions as base if the SP is for a custom resource type + List customSPBase = theNextSp.getExtensionsByUrl(HapiExtensions.EXTENSION_SEARCHPARAM_CUSTOM_BASE_RESOURCE); + for (Extension e : customSPBase){ + String eStr = e.getValueAsPrimitive().getValueAsString(); + if (StringUtils.isNotBlank(eStr)) { + base.add(eStr); + } + } + RestSearchParameterTypeEnum paramType = null; RuntimeSearchParam.RuntimeSearchParamStatusEnum status = null; if (theNextSp.getType() != null) { @@ -222,8 +251,17 @@ private RuntimeSearchParam canonicalizeSearchParameterDstu3(org.hl7.fhir.dstu3.m break; } } + Set providesMembershipInCompartments = Collections.emptySet(); Set targets = DatatypeUtil.toStringSet(theNextSp.getTarget()); + // add extensions as target if the SP is for a custom resource type + List customSPTargets = theNextSp.getExtensionsByUrl(HapiExtensions.EXTENSION_SEARCHPARAM_CUSTOM_TARGET_RESOURCE); + for (Extension e : customSPTargets) { + String eStr = e.getValueAsPrimitive().getValueAsString(); + if (StringUtils.isNotBlank(eStr)) { + targets.add(eStr); + } + } if (isBlank(name) || isBlank(path) || paramType == null) { if (paramType != RestSearchParameterTypeEnum.COMPOSITE) { @@ -252,7 +290,7 @@ 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, providesMembershipInCompartments, targets, status, unique, components, base); } private RuntimeSearchParam canonicalizeSearchParameterR4Plus(IBaseResource theNextSp) { From d229f9f02a44d32feebbc75feb7017ede1ec8699 Mon Sep 17 00:00:00 2001 From: tyner Date: Mon, 15 May 2023 11:20:36 -0400 Subject: [PATCH 2/9] Canonicalizers now correctly handle search parameters for custom resources --- .../SearchParameterCanonicalizer.java | 24 ++++++++++++++++--- 1 file changed, 21 insertions(+), 3 deletions(-) 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 e8f10459b6f0..f42dfe8e05df 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 @@ -99,12 +99,17 @@ private RuntimeSearchParam canonicalizeSearchParameterDstu2(ca.uhn.fhir.model.ds Collection base = toStrings(Collections.singletonList(theNextSp.getBaseElement())); // add extensions as base if the SP is for a custom resource type List customSPBase = theNextSp.getUndeclaredExtensionsByUrl(HapiExtensions.EXTENSION_SEARCHPARAM_CUSTOM_BASE_RESOURCE); + ArrayList baseFromExtensions = new ArrayList<>(); for (ExtensionDt e : customSPBase) { String eStr = e.getValueAsPrimitive().getValueAsString(); if (StringUtils.isNotBlank(eStr)) { - base.add(eStr); + baseFromExtensions.add(eStr); } } + if (!baseFromExtensions.isEmpty()){ + base.remove("Resource"); + base.addAll(baseFromExtensions); + } RestSearchParameterTypeEnum paramType = null; RuntimeSearchParam.RuntimeSearchParamStatusEnum status = null; @@ -194,12 +199,17 @@ private RuntimeSearchParam canonicalizeSearchParameterDstu3(org.hl7.fhir.dstu3.m List base = new ArrayList<>(toStrings(theNextSp.getBase())); // add extensions as base if the SP is for a custom resource type List customSPBase = theNextSp.getExtensionsByUrl(HapiExtensions.EXTENSION_SEARCHPARAM_CUSTOM_BASE_RESOURCE); + ArrayList baseFromExtensions = new ArrayList<>(); for (Extension e : customSPBase){ String eStr = e.getValueAsPrimitive().getValueAsString(); if (StringUtils.isNotBlank(eStr)) { - base.add(eStr); + baseFromExtensions.add(eStr); } } + if (!baseFromExtensions.isEmpty()){ + base.remove("Resource"); + base.addAll(baseFromExtensions); + } RestSearchParameterTypeEnum paramType = null; RuntimeSearchParam.RuntimeSearchParamStatusEnum status = null; @@ -299,11 +309,14 @@ private RuntimeSearchParam canonicalizeSearchParameterR4Plus(IBaseResource theNe 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()); + + ArrayList baseFromExtension = new ArrayList<>(); if (theNextSp instanceof IBaseHasExtensions) { ((IBaseHasExtensions) theNextSp) .getExtension() @@ -313,7 +326,12 @@ private RuntimeSearchParam canonicalizeSearchParameterR4Plus(IBaseResource theNe .map(t -> ((IPrimitiveType) t.getValue())) .map(IPrimitiveType::getValueAsString) .filter(StringUtils::isNotBlank) - .forEach(base::add); + .forEach(baseFromExtension::add); + } + + if (!baseFromExtension.isEmpty()){ + base.remove("Resource"); + base.addAll(baseFromExtension); } RestSearchParameterTypeEnum paramType = null; From e58d111b7a4849012f7d80f14454a12d0e23e9c2 Mon Sep 17 00:00:00 2001 From: tyner Date: Mon, 15 May 2023 14:52:13 -0400 Subject: [PATCH 3/9] created changelog --- ...types-when-converting-dstu23-into-runtimesearchparam.yaml | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 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 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..a4ad99ba37b7 --- /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: "The SearchParameterCanonicalizer does not correctly convert DSTU2 and DSTU3 custom resources search parameters +into RuntimeSearchParam. This is now fixed." From 1d684c5f80e56c9876886648cd5530eb17d303e0 Mon Sep 17 00:00:00 2001 From: tyner Date: Wed, 17 May 2023 10:16:06 -0400 Subject: [PATCH 4/9] Modification based on comments: - remove Resource from target field when there are custom resource types - fixed changelog typo - removed unnecessary variable providesMembershipInCompartments --- ...erting-dstu23-into-runtimesearchparam.yaml | 2 +- .../SearchParameterCanonicalizer.java | 30 +++++++++++++------ 2 files changed, 22 insertions(+), 10 deletions(-) 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 index a4ad99ba37b7..a377b98c80ca 100644 --- 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 @@ -1,5 +1,5 @@ --- type: fix issue: 4863 -title: "The SearchParameterCanonicalizer does not correctly convert DSTU2 and DSTU3 custom resources search parameters +title: "The SearchParameterCanonicalizer does 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 f42dfe8e05df..09dca892bee0 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 @@ -154,16 +154,20 @@ private RuntimeSearchParam canonicalizeSearchParameterDstu2(ca.uhn.fhir.model.ds break; } } - Set providesMembershipInCompartments = Collections.emptySet(); Set targets = DatatypeUtil.toStringSet(theNextSp.getTarget()); // add extensions as target if the SP is for a custom resource type + Set targetsFromExtensions = new HashSet<>(); List customSPTargets = theNextSp.getUndeclaredExtensionsByUrl(HapiExtensions.EXTENSION_SEARCHPARAM_CUSTOM_TARGET_RESOURCE); for (ExtensionDt e : customSPTargets) { String eStr = e.getValueAsPrimitive().getValueAsString(); if (StringUtils.isNotBlank(eStr)) { - targets.add(eStr); + targetsFromExtensions.add(eStr); } } + if (!targetsFromExtensions.isEmpty()){ + targets.remove("String"); + targets.addAll(targetsFromExtensions); + } if (isBlank(name) || isBlank(path)) { if (paramType != RestSearchParameterTypeEnum.COMPOSITE) { @@ -188,7 +192,7 @@ private RuntimeSearchParam canonicalizeSearchParameterDstu2(ca.uhn.fhir.model.ds } List components = Collections.emptyList(); - 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(), targets, status, unique, components, base); } private RuntimeSearchParam canonicalizeSearchParameterDstu3(org.hl7.fhir.dstu3.model.SearchParameter theNextSp) { @@ -262,16 +266,20 @@ private RuntimeSearchParam canonicalizeSearchParameterDstu3(org.hl7.fhir.dstu3.m } } - Set providesMembershipInCompartments = Collections.emptySet(); Set targets = DatatypeUtil.toStringSet(theNextSp.getTarget()); // add extensions as target if the SP is for a custom resource type + Set targetsFromExtensions = new HashSet<>(); List customSPTargets = theNextSp.getExtensionsByUrl(HapiExtensions.EXTENSION_SEARCHPARAM_CUSTOM_TARGET_RESOURCE); for (Extension e : customSPTargets) { String eStr = e.getValueAsPrimitive().getValueAsString(); if (StringUtils.isNotBlank(eStr)) { - targets.add(eStr); + targetsFromExtensions.add(eStr); } } + if (!targetsFromExtensions.isEmpty()){ + targets.remove("Resource"); + targets.addAll(targetsFromExtensions); + } if (isBlank(name) || isBlank(path) || paramType == null) { if (paramType != RestSearchParameterTypeEnum.COMPOSITE) { @@ -300,7 +308,7 @@ 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, base); + return new RuntimeSearchParam(id, uri, name, description, path, paramType, Collections.emptySet(), targets, status, unique, components, base); } private RuntimeSearchParam canonicalizeSearchParameterR4Plus(IBaseResource theNextSp) { @@ -379,13 +387,13 @@ 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()); + Set targetsFromExtensions = new HashSet<>(); if (theNextSp instanceof IBaseHasExtensions) { ((IBaseHasExtensions) theNextSp) .getExtension() @@ -395,7 +403,11 @@ private RuntimeSearchParam canonicalizeSearchParameterR4Plus(IBaseResource theNe .map(t -> ((IPrimitiveType) t.getValue())) .map(IPrimitiveType::getValueAsString) .filter(StringUtils::isNotBlank) - .forEach(targets::add); + .forEach(targetsFromExtensions::add); + } + if(!targetsFromExtensions.isEmpty()){ + targets.remove("Resource"); + targets.addAll(targetsFromExtensions); } if (isBlank(name) || isBlank(path) || paramType == null) { @@ -435,7 +447,7 @@ 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(), targets, status, unique, components, base); } /** From 44b6926a8f0c7cb64595bb90dfa1dfc03d8ca4fa Mon Sep 17 00:00:00 2001 From: tyner Date: Wed, 17 May 2023 13:37:59 -0400 Subject: [PATCH 5/9] Added tests for the SearchParameterCanonicalizer to test if base and target of RuntimeSearchParam is set as expected for DSTU2, DSTU3, R4, R4B, and R5 resources --- .../SearchParameterCanonicalizer.java | 15 +- .../SearchParameterCanonicalizerTest.java | 168 +++++++++++++++++- 2 files changed, 180 insertions(+), 3 deletions(-) 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 09dca892bee0..2322f7b232d1 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 @@ -106,6 +106,8 @@ private RuntimeSearchParam canonicalizeSearchParameterDstu2(ca.uhn.fhir.model.ds 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); @@ -164,8 +166,10 @@ private RuntimeSearchParam canonicalizeSearchParameterDstu2(ca.uhn.fhir.model.ds 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("String"); + targets.remove("Resource"); targets.addAll(targetsFromExtensions); } @@ -210,6 +214,8 @@ private RuntimeSearchParam canonicalizeSearchParameterDstu3(org.hl7.fhir.dstu3.m 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); @@ -276,6 +282,8 @@ private RuntimeSearchParam canonicalizeSearchParameterDstu3(org.hl7.fhir.dstu3.m 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); @@ -336,7 +344,8 @@ private RuntimeSearchParam canonicalizeSearchParameterR4Plus(IBaseResource theNe .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); @@ -405,6 +414,8 @@ private RuntimeSearchParam canonicalizeSearchParameterR4Plus(IBaseResource theNe .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); 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..e9bc52472929 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,130 @@ 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); +// SearchParameter initSearchParam(String version){ +// switch (version){ +// case "Dstu2": +// ca.uhn.fhir.model.dstu2.resource.SearchParameter sp = new ca.uhn.fhir.model.dstu2.resource.SearchParameter(); +// sp.setBase(ResourceTypeEnum.RESOURCE); +// sp.setType(SearchParamTypeEnum.REFERENCE); +// sp.setStatus(ConformanceResourceStatusEnum.ACTIVE); +// sp.setXpath("Meal.chef | Observation.subject"); +// break; +// case "Dstu3": +// org.hl7.fhir.dstu3.model.SearchParameter sp = new org.hl7.fhir.dstu3.model.SearchParameter(); +// sp.addBase("Resource"); +// sp.addBase("Patient"); +// 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"); +// break; +// case "R4": +// SearchParameter sp = new SearchParameter(); +// sp.addBase("Resource"); +// sp.addBase("Patient"); +// sp.setType(Enumerations.SearchParamType.REFERENCE); +// sp.setStatus(Enumerations.PublicationStatus.ACTIVE); +// sp.setExpression("Meal.chef | Observation.subject"); +// } +// } + + 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; + } + + 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; + } + + 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; + } + @ParameterizedTest @ValueSource(booleans = {false, true}) public void testCanonicalizeSearchParameterWithCustomType(boolean theConvertToR5) { @@ -37,7 +160,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 +179,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 has 1 base + if ("Dstu2".equals(version)){ + assertThat(output.getBase(), containsInAnyOrder("Meal")); + } else { + assertThat(output.getBase(), containsInAnyOrder("Meal", "Patient")); + } + assertThat(output.getTargets(), contains("Chef", "Observation")); + assertThat(output.getBase(), not(contains("Resource"))); + assertThat(output.getTargets(), not(contains("Resource"))); } } From c82447a8ac0de558f0754f340b69c9f1a53bba4d Mon Sep 17 00:00:00 2001 From: tyner Date: Wed, 17 May 2023 13:41:14 -0400 Subject: [PATCH 6/9] Fixed typo and removed commented code --- .../SearchParameterCanonicalizerTest.java | 29 +------------------ 1 file changed, 1 insertion(+), 28 deletions(-) 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 e9bc52472929..ec85673896e0 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 @@ -35,33 +35,6 @@ public class SearchParameterCanonicalizerTest { private static final Logger ourLog = LoggerFactory.getLogger(SearchParameterCanonicalizerTest.class); -// SearchParameter initSearchParam(String version){ -// switch (version){ -// case "Dstu2": -// ca.uhn.fhir.model.dstu2.resource.SearchParameter sp = new ca.uhn.fhir.model.dstu2.resource.SearchParameter(); -// sp.setBase(ResourceTypeEnum.RESOURCE); -// sp.setType(SearchParamTypeEnum.REFERENCE); -// sp.setStatus(ConformanceResourceStatusEnum.ACTIVE); -// sp.setXpath("Meal.chef | Observation.subject"); -// break; -// case "Dstu3": -// org.hl7.fhir.dstu3.model.SearchParameter sp = new org.hl7.fhir.dstu3.model.SearchParameter(); -// sp.addBase("Resource"); -// sp.addBase("Patient"); -// 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"); -// break; -// case "R4": -// SearchParameter sp = new SearchParameter(); -// sp.addBase("Resource"); -// sp.addBase("Patient"); -// sp.setType(Enumerations.SearchParamType.REFERENCE); -// sp.setStatus(Enumerations.PublicationStatus.ACTIVE); -// sp.setExpression("Meal.chef | Observation.subject"); -// } -// } - IBaseResource initSearchParamR4(){ SearchParameter sp = new SearchParameter(); sp.setId("SearchParameter/meal-chef"); @@ -215,7 +188,7 @@ public void testCanonicalizeSearchParameterWithCustomTypeAllVersion(String versi assertEquals(RestSearchParameterTypeEnum.REFERENCE, output.getParamType()); assertEquals(RuntimeSearchParam.RuntimeSearchParamStatusEnum.ACTIVE, output.getStatus()); assertThat(output.getPathsSplit(), containsInAnyOrder("Meal.chef", "Observation.subject")); - // DSTU2 Resources must only has 1 base + // DSTU2 Resources must only have 1 base if ("Dstu2".equals(version)){ assertThat(output.getBase(), containsInAnyOrder("Meal")); } else { From 51f7b954f2a1a4708ccf2be1dcf1afc700a16446 Mon Sep 17 00:00:00 2001 From: tyner Date: Wed, 17 May 2023 13:46:12 -0400 Subject: [PATCH 7/9] re-ordered init methods --- .../SearchParameterCanonicalizerTest.java | 67 ++++++++++--------- 1 file changed, 34 insertions(+), 33 deletions(-) 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 ec85673896e0..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 @@ -35,6 +35,39 @@ 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"); @@ -86,38 +119,6 @@ IBaseResource initSearchParamR5(){ 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; - } - - 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; - } @ParameterizedTest @ValueSource(booleans = {false, true}) @@ -194,7 +195,7 @@ public void testCanonicalizeSearchParameterWithCustomTypeAllVersion(String versi } else { assertThat(output.getBase(), containsInAnyOrder("Meal", "Patient")); } - assertThat(output.getTargets(), contains("Chef", "Observation")); + assertThat(output.getTargets(), containsInAnyOrder("Chef", "Observation")); assertThat(output.getBase(), not(contains("Resource"))); assertThat(output.getTargets(), not(contains("Resource"))); } From d0a20f82eeac98def8db140a9d5e067ee7c08574 Mon Sep 17 00:00:00 2001 From: TynerGjs <132295567+TynerGjs@users.noreply.github.com> Date: Wed, 17 May 2023 16:09:27 -0400 Subject: [PATCH 8/9] Update changelog Co-authored-by: Tadgh --- ...es-types-when-converting-dstu23-into-runtimesearchparam.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 index a377b98c80ca..7cb575aa8aa9 100644 --- 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 @@ -1,5 +1,5 @@ --- type: fix issue: 4863 -title: "The SearchParameterCanonicalizer does not correctly convert DSTU2 and DSTU3 custom resources SearchParameters +title: "Previously the SearchParameterCanonicalizer did not correctly convert DSTU2 and DSTU3 custom resources SearchParameters into RuntimeSearchParam. This is now fixed." From c1142ea385859c43aecbc8fa8e7986e7e24f00a0 Mon Sep 17 00:00:00 2001 From: peartree Date: Thu, 18 May 2023 10:39:24 -0400 Subject: [PATCH 9/9] modifications following first code review. --- .../SearchParameterCanonicalizer.java | 252 +++++++++--------- 1 file changed, 125 insertions(+), 127 deletions(-) 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 2322f7b232d1..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) { @@ -96,21 +114,11 @@ private RuntimeSearchParam canonicalizeSearchParameterDstu2(ca.uhn.fhir.model.ds String description = theNextSp.getDescription(); String path = theNextSp.getXpath(); - Collection base = toStrings(Collections.singletonList(theNextSp.getBaseElement())); - // add extensions as base if the SP is for a custom resource type - List customSPBase = theNextSp.getUndeclaredExtensionsByUrl(HapiExtensions.EXTENSION_SEARCHPARAM_CUSTOM_BASE_RESOURCE); - ArrayList 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 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; @@ -156,22 +164,11 @@ private RuntimeSearchParam canonicalizeSearchParameterDstu2(ca.uhn.fhir.model.ds break; } } - Set targets = DatatypeUtil.toStringSet(theNextSp.getTarget()); - // add extensions as target if the SP is for a custom resource type - Set targetsFromExtensions = new HashSet<>(); - List 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 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) { @@ -196,7 +193,7 @@ private RuntimeSearchParam canonicalizeSearchParameterDstu2(ca.uhn.fhir.model.ds } List 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) { @@ -204,22 +201,10 @@ private RuntimeSearchParam canonicalizeSearchParameterDstu3(org.hl7.fhir.dstu3.m String description = theNextSp.getDescription(); String path = theNextSp.getExpression(); - List base = new ArrayList<>(toStrings(theNextSp.getBase())); - // add extensions as base if the SP is for a custom resource type - List customSPBase = theNextSp.getExtensionsByUrl(HapiExtensions.EXTENSION_SEARCHPARAM_CUSTOM_BASE_RESOURCE); - ArrayList 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 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; @@ -272,22 +257,10 @@ private RuntimeSearchParam canonicalizeSearchParameterDstu3(org.hl7.fhir.dstu3.m } } - Set targets = DatatypeUtil.toStringSet(theNextSp.getTarget()); - // add extensions as target if the SP is for a custom resource type - Set targetsFromExtensions = new HashSet<>(); - List 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 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) { @@ -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 base = terser - .getValues(theNextSp, "base", IPrimitiveType.class) - .stream() - .map(IPrimitiveType::getValueAsString) - .collect(Collectors.toList()); + Set baseResources = extractR4PlusResources("base", theNextSp); + List baseCustomResources = extractR4PlusCustomResourcesFromExtensions(theNextSp, HapiExtensions.EXTENSION_SEARCHPARAM_CUSTOM_BASE_RESOURCE); - ArrayList 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; @@ -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; @@ -397,29 +349,10 @@ private RuntimeSearchParam canonicalizeSearchParameterR4Plus(IBaseResource theNe break; } - Set targets = terser - .getValues(theNextSp, "target", IPrimitiveType.class) - .stream() - .map(IPrimitiveType::getValueAsString) - .collect(Collectors.toSet()); - Set 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 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)) { @@ -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() @@ -448,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); } @@ -458,7 +391,15 @@ private RuntimeSearchParam canonicalizeSearchParameterR4Plus(IBaseResource theNe 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 extractR4PlusResources(String thePath, IBaseResource theNextSp) { + return myTerser + .getValues(theNextSp, thePath, IPrimitiveType.class) + .stream() + .map(IPrimitiveType::getValueAsString) + .collect(Collectors.toSet()); } /** @@ -506,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); + } + + } }