From b9ba57f43650e739d870f70384716ca7c276c5d9 Mon Sep 17 00:00:00 2001 From: leif stawnyczy Date: Fri, 22 Aug 2025 16:15:31 -0400 Subject: [PATCH 01/36] updating search parm registry --- .../registry/SearchParamRegistryImpl.java | 32 +++++++++++++++++++ .../SearchParamExtractorDstu3Test.java | 10 ++++++ .../server/RestfulServerConfiguration.java | 10 ++++++ .../util/FhirContextSearchParamRegistry.java | 10 ++++++ .../server/util/ISearchParamRegistry.java | 21 +++++++++++- 5 files changed, 82 insertions(+), 1 deletion(-) diff --git a/hapi-fhir-jpaserver-searchparam/src/main/java/ca/uhn/fhir/jpa/searchparam/registry/SearchParamRegistryImpl.java b/hapi-fhir-jpaserver-searchparam/src/main/java/ca/uhn/fhir/jpa/searchparam/registry/SearchParamRegistryImpl.java index 63a046b47d94..6103fb67ddb6 100644 --- a/hapi-fhir-jpaserver-searchparam/src/main/java/ca/uhn/fhir/jpa/searchparam/registry/SearchParamRegistryImpl.java +++ b/hapi-fhir-jpaserver-searchparam/src/main/java/ca/uhn/fhir/jpa/searchparam/registry/SearchParamRegistryImpl.java @@ -61,6 +61,7 @@ import java.util.ArrayList; import java.util.Collection; import java.util.Collections; +import java.util.HashMap; import java.util.HashSet; import java.util.Iterator; import java.util.List; @@ -68,6 +69,7 @@ import java.util.Objects; import java.util.Optional; import java.util.Set; +import java.util.function.Function; import java.util.stream.Collectors; import static ca.uhn.fhir.rest.server.util.ISearchParamRegistry.isAllowedForContext; @@ -121,6 +123,8 @@ public class SearchParamRegistryImpl private volatile RuntimeSearchParamCache myActiveSearchParams; private boolean myPrePopulateSearchParamIdentities = true; + private final Map> myResourceTypeToSPLocalCache = new HashMap<>(); + @VisibleForTesting public void setPopulateSearchParamIdentities(boolean myPrePopulateSearchParamIdentities) { this.myPrePopulateSearchParamIdentities = myPrePopulateSearchParamIdentities; @@ -210,6 +214,25 @@ public RuntimeSearchParam getActiveSearchParamByUrl( return null; } + @Override + public void addActiveSearchParameterToLocalCache(RuntimeSearchParam theSearchParam) { + if (!theSearchParam.hasTargets()) { + // todo - throw exception + } + for (String rt : theSearchParam.getTargets()) { + myResourceTypeToSPLocalCache.computeIfAbsent(rt, k -> { + Set set = new HashSet<>(); + set.add(theSearchParam); + return set; + }); + } + } + + @Override + public void clearLocalSearchParameterCache() { + myResourceTypeToSPLocalCache.clear(); + } + @Override public Optional getActiveComboSearchParamById( @Nonnull String theResourceName, @Nonnull IIdType theId) { @@ -249,6 +272,15 @@ private void initializeActiveSearchParams(Collection theJpaSearch long overriddenCount = overrideBuiltinSearchParamsWithActiveJpaSearchParams(searchParams, theJpaSearchParams); ourLog.trace("Have overridden {} built-in search parameters", overriddenCount); + // add the local cache search params + for (Map.Entry> entry : myResourceTypeToSPLocalCache.entrySet()) { + Set set = entry.getValue(); + for (RuntimeSearchParam rsp : set) { + searchParams.getSearchParamMap(entry.getKey()) + .put(rsp.getName(), rsp); + } + } + // Auto-register: _language if (myStorageSettings.isLanguageSearchParameterEnabled()) { registerImplicitSearchParam( diff --git a/hapi-fhir-jpaserver-searchparam/src/test/java/ca/uhn/fhir/jpa/searchparam/extractor/SearchParamExtractorDstu3Test.java b/hapi-fhir-jpaserver-searchparam/src/test/java/ca/uhn/fhir/jpa/searchparam/extractor/SearchParamExtractorDstu3Test.java index 490a5c64bfbd..71d12ac6c35d 100644 --- a/hapi-fhir-jpaserver-searchparam/src/test/java/ca/uhn/fhir/jpa/searchparam/extractor/SearchParamExtractorDstu3Test.java +++ b/hapi-fhir-jpaserver-searchparam/src/test/java/ca/uhn/fhir/jpa/searchparam/extractor/SearchParamExtractorDstu3Test.java @@ -301,6 +301,16 @@ public RuntimeSearchParam getActiveSearchParamByUrl(@Nonnull String theUrl, @Non throw new UnsupportedOperationException(); } + @Override + public void addActiveSearchParameterToLocalCache(RuntimeSearchParam theSearchParam) { + throw new UnsupportedOperationException(); + } + + @Override + public void clearLocalSearchParameterCache() { + throw new UnsupportedOperationException(); + } + @Override public List getActiveComboSearchParams(@Nonnull String theResourceName, @Nonnull SearchParamLookupContextEnum theContext) { throw new UnsupportedOperationException(); diff --git a/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/RestfulServerConfiguration.java b/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/RestfulServerConfiguration.java index 92c594289980..880efa0bf582 100644 --- a/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/RestfulServerConfiguration.java +++ b/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/RestfulServerConfiguration.java @@ -451,6 +451,16 @@ public RuntimeSearchParam getActiveSearchParamByUrl( throw new UnsupportedOperationException(Msg.code(286)); } + @Override + public void addActiveSearchParameterToLocalCache(RuntimeSearchParam theSearchParam) { + + } + + @Override + public void clearLocalSearchParameterCache() { + + } + private void createRuntimeBinding( ResourceSearchParams theMapToPopulate, SearchMethodBinding theSearchMethodBinding) { diff --git a/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/util/FhirContextSearchParamRegistry.java b/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/util/FhirContextSearchParamRegistry.java index c8611780bf91..f658f00915b3 100644 --- a/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/util/FhirContextSearchParamRegistry.java +++ b/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/util/FhirContextSearchParamRegistry.java @@ -107,6 +107,16 @@ public RuntimeSearchParam getActiveSearchParamByUrl( .orElse(null); } + @Override + public void addActiveSearchParameterToLocalCache(RuntimeSearchParam theSearchParam) { + + } + + @Override + public void clearLocalSearchParameterCache() { + + } + @Override public List getActiveComboSearchParams( @Nonnull String theResourceName, @Nonnull SearchParamLookupContextEnum theContext) { diff --git a/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/util/ISearchParamRegistry.java b/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/util/ISearchParamRegistry.java index f998b6134060..066999af7e78 100644 --- a/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/util/ISearchParamRegistry.java +++ b/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/util/ISearchParamRegistry.java @@ -204,6 +204,21 @@ default boolean hasActiveSearchParam( return getActiveSearchParam(theResourceType, theParamName, theContext) != null; } + /** + * Adds a SearchParameter to the cache so that it can be used, but does *not* + * add it to the repository (so it cannot be queried). + * + * These local cached searchparameters will remain in the cache, even after + * requests to refresh, until restart or clearLocalCache is called. + * @param theSearchParam + */ + void addActiveSearchParameterToLocalCache(RuntimeSearchParam theSearchParam); + + /** + * Clears search parameters from local cache. + */ + void clearLocalSearchParameterCache(); + /** * Describes the context for looking up individual search parameters or lists of search parameters. * These can be thought of as filter criteria - Most search parameters generally apply to all @@ -229,7 +244,11 @@ enum SearchParamLookupContextEnum { /** * Return any search parameters that are known to the system for any context */ - ALL + ALL, + /** + * + */ + ALL_EXCLUDING_LOCAL_CACHE } static boolean isAllowedForContext( From 440530843e02fc19139fa692aff3845546d3b16b Mon Sep 17 00:00:00 2001 From: leif stawnyczy Date: Mon, 25 Aug 2025 14:57:22 -0400 Subject: [PATCH 02/36] working code --- .../registry/RuntimeSearchParamCache.java | 5 ++++ .../registry/SearchParamRegistryImpl.java | 29 +++++++++---------- .../server/RestfulServerConfiguration.java | 4 +-- .../util/FhirContextSearchParamRegistry.java | 4 +-- .../server/util/ISearchParamRegistry.java | 6 +--- 5 files changed, 23 insertions(+), 25 deletions(-) diff --git a/hapi-fhir-jpaserver-searchparam/src/main/java/ca/uhn/fhir/jpa/searchparam/registry/RuntimeSearchParamCache.java b/hapi-fhir-jpaserver-searchparam/src/main/java/ca/uhn/fhir/jpa/searchparam/registry/RuntimeSearchParamCache.java index a82b412a68ad..3c17d9e1feb0 100644 --- a/hapi-fhir-jpaserver-searchparam/src/main/java/ca/uhn/fhir/jpa/searchparam/registry/RuntimeSearchParamCache.java +++ b/hapi-fhir-jpaserver-searchparam/src/main/java/ca/uhn/fhir/jpa/searchparam/registry/RuntimeSearchParamCache.java @@ -63,6 +63,11 @@ public void remove(String theResourceName, String theName) { myResourceNameToSpNameToSp.get(theResourceName).remove(theName); } + public void clear() { + myResourceNameToSpNameToSp.clear(); + myUrlToParam.clear(); + } + private void putAll(ReadOnlySearchParamCache theReadOnlySearchParamCache) { Set> builtInSps = theReadOnlySearchParamCache.myResourceNameToSpNameToSp.entrySet(); diff --git a/hapi-fhir-jpaserver-searchparam/src/main/java/ca/uhn/fhir/jpa/searchparam/registry/SearchParamRegistryImpl.java b/hapi-fhir-jpaserver-searchparam/src/main/java/ca/uhn/fhir/jpa/searchparam/registry/SearchParamRegistryImpl.java index 6103fb67ddb6..9eb346f940f4 100644 --- a/hapi-fhir-jpaserver-searchparam/src/main/java/ca/uhn/fhir/jpa/searchparam/registry/SearchParamRegistryImpl.java +++ b/hapi-fhir-jpaserver-searchparam/src/main/java/ca/uhn/fhir/jpa/searchparam/registry/SearchParamRegistryImpl.java @@ -61,7 +61,6 @@ import java.util.ArrayList; import java.util.Collection; import java.util.Collections; -import java.util.HashMap; import java.util.HashSet; import java.util.Iterator; import java.util.List; @@ -69,7 +68,6 @@ import java.util.Objects; import java.util.Optional; import java.util.Set; -import java.util.function.Function; import java.util.stream.Collectors; import static ca.uhn.fhir.rest.server.util.ISearchParamRegistry.isAllowedForContext; @@ -123,7 +121,11 @@ public class SearchParamRegistryImpl private volatile RuntimeSearchParamCache myActiveSearchParams; private boolean myPrePopulateSearchParamIdentities = true; - private final Map> myResourceTypeToSPLocalCache = new HashMap<>(); + /** + * Our local cache for search parameters stored in the system, that + * are not saved into the DB. + */ + private final RuntimeSearchParamCache myLocalSPCache = new RuntimeSearchParamCache(); @VisibleForTesting public void setPopulateSearchParamIdentities(boolean myPrePopulateSearchParamIdentities) { @@ -220,17 +222,13 @@ public void addActiveSearchParameterToLocalCache(RuntimeSearchParam theSearchPar // todo - throw exception } for (String rt : theSearchParam.getTargets()) { - myResourceTypeToSPLocalCache.computeIfAbsent(rt, k -> { - Set set = new HashSet<>(); - set.add(theSearchParam); - return set; - }); + myLocalSPCache.add(rt, theSearchParam.getName(), theSearchParam); } } @Override public void clearLocalSearchParameterCache() { - myResourceTypeToSPLocalCache.clear(); + myLocalSPCache.clear(); } @Override @@ -273,13 +271,12 @@ private void initializeActiveSearchParams(Collection theJpaSearch ourLog.trace("Have overridden {} built-in search parameters", overriddenCount); // add the local cache search params - for (Map.Entry> entry : myResourceTypeToSPLocalCache.entrySet()) { - Set set = entry.getValue(); - for (RuntimeSearchParam rsp : set) { - searchParams.getSearchParamMap(entry.getKey()) - .put(rsp.getName(), rsp); - } - } + myLocalSPCache.getSearchParamStream() + .forEach(sp -> { + for (String rt : sp.getTargets()) { + searchParams.add(rt, sp.getName(), sp); + } + }); // Auto-register: _language if (myStorageSettings.isLanguageSearchParameterEnabled()) { diff --git a/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/RestfulServerConfiguration.java b/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/RestfulServerConfiguration.java index 880efa0bf582..9e75a616bbfd 100644 --- a/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/RestfulServerConfiguration.java +++ b/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/RestfulServerConfiguration.java @@ -453,12 +453,12 @@ public RuntimeSearchParam getActiveSearchParamByUrl( @Override public void addActiveSearchParameterToLocalCache(RuntimeSearchParam theSearchParam) { - + // TODO - throw not supported exception with error code } @Override public void clearLocalSearchParameterCache() { - + // TODO - throw not supported exception with error code } private void createRuntimeBinding( diff --git a/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/util/FhirContextSearchParamRegistry.java b/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/util/FhirContextSearchParamRegistry.java index f658f00915b3..b0a642c6cb32 100644 --- a/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/util/FhirContextSearchParamRegistry.java +++ b/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/util/FhirContextSearchParamRegistry.java @@ -109,12 +109,12 @@ public RuntimeSearchParam getActiveSearchParamByUrl( @Override public void addActiveSearchParameterToLocalCache(RuntimeSearchParam theSearchParam) { - + // todo - throw unsupported exceptions } @Override public void clearLocalSearchParameterCache() { - + // todo - throw unsupported exceptions; requires updating the msg.code } @Override diff --git a/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/util/ISearchParamRegistry.java b/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/util/ISearchParamRegistry.java index 066999af7e78..e391f0a900ee 100644 --- a/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/util/ISearchParamRegistry.java +++ b/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/util/ISearchParamRegistry.java @@ -244,11 +244,7 @@ enum SearchParamLookupContextEnum { /** * Return any search parameters that are known to the system for any context */ - ALL, - /** - * - */ - ALL_EXCLUDING_LOCAL_CACHE + ALL } static boolean isAllowedForContext( From df5e976d3e3f6809cf02aa6aa277e8771673a69f Mon Sep 17 00:00:00 2001 From: leif stawnyczy Date: Tue, 26 Aug 2025 10:47:37 -0400 Subject: [PATCH 03/36] complete --- .../java/ca/uhn/fhir/util/SearchParameterUtil.java | 2 +- .../registry/SearchParamRegistryImpl.java | 13 +++++++------ 2 files changed, 8 insertions(+), 7 deletions(-) diff --git a/hapi-fhir-base/src/main/java/ca/uhn/fhir/util/SearchParameterUtil.java b/hapi-fhir-base/src/main/java/ca/uhn/fhir/util/SearchParameterUtil.java index d3f424994f46..9bfc71e869a4 100644 --- a/hapi-fhir-base/src/main/java/ca/uhn/fhir/util/SearchParameterUtil.java +++ b/hapi-fhir-base/src/main/java/ca/uhn/fhir/util/SearchParameterUtil.java @@ -301,7 +301,7 @@ public static List getAllPatientCompartmentRuntimeSearchPara return getAllPatientCompartmentRuntimeSearchParams(runtimeResourceDefinition); } - public static List getAllPatientCompartmenRuntimeSearchParams(FhirContext theFhirContext) { + public static List getAllPatientCompartmentRuntimeSearchParams(FhirContext theFhirContext) { return theFhirContext.getResourceTypes().stream() .flatMap(type -> getAllPatientCompartmentRuntimeSearchParamsForResourceType(theFhirContext, type).stream()) diff --git a/hapi-fhir-jpaserver-searchparam/src/main/java/ca/uhn/fhir/jpa/searchparam/registry/SearchParamRegistryImpl.java b/hapi-fhir-jpaserver-searchparam/src/main/java/ca/uhn/fhir/jpa/searchparam/registry/SearchParamRegistryImpl.java index 9eb346f940f4..c290f0aba877 100644 --- a/hapi-fhir-jpaserver-searchparam/src/main/java/ca/uhn/fhir/jpa/searchparam/registry/SearchParamRegistryImpl.java +++ b/hapi-fhir-jpaserver-searchparam/src/main/java/ca/uhn/fhir/jpa/searchparam/registry/SearchParamRegistryImpl.java @@ -71,6 +71,7 @@ import java.util.stream.Collectors; import static ca.uhn.fhir.rest.server.util.ISearchParamRegistry.isAllowedForContext; +import static org.apache.commons.lang3.StringUtils.isBlank; import static org.apache.commons.lang3.StringUtils.isNotBlank; public class SearchParamRegistryImpl @@ -217,11 +218,11 @@ public RuntimeSearchParam getActiveSearchParamByUrl( } @Override - public void addActiveSearchParameterToLocalCache(RuntimeSearchParam theSearchParam) { - if (!theSearchParam.hasTargets()) { - // todo - throw exception - } - for (String rt : theSearchParam.getTargets()) { + public void addActiveSearchParameterToLocalCache(@Nonnull RuntimeSearchParam theSearchParam) { + assert !isBlank(theSearchParam.getName()); + assert theSearchParam.getBase() != null && !theSearchParam.getBase().isEmpty(); + + for (String rt : theSearchParam.getBase()) { myLocalSPCache.add(rt, theSearchParam.getName(), theSearchParam); } } @@ -273,7 +274,7 @@ private void initializeActiveSearchParams(Collection theJpaSearch // add the local cache search params myLocalSPCache.getSearchParamStream() .forEach(sp -> { - for (String rt : sp.getTargets()) { + for (String rt : sp.getBase()) { searchParams.add(rt, sp.getName(), sp); } }); From dd0714c2561e24ce79caebaed8f1c1223aaed2ac Mon Sep 17 00:00:00 2001 From: leif stawnyczy Date: Tue, 26 Aug 2025 11:24:21 -0400 Subject: [PATCH 04/36] adding a test --- .../registry/SearchParamRegistryImplTest.java | 46 +++++++++++++++++++ .../server/RestfulServerConfiguration.java | 4 +- .../util/FhirContextSearchParamRegistry.java | 4 +- 3 files changed, 50 insertions(+), 4 deletions(-) diff --git a/hapi-fhir-jpaserver-searchparam/src/test/java/ca/uhn/fhir/jpa/searchparam/registry/SearchParamRegistryImplTest.java b/hapi-fhir-jpaserver-searchparam/src/test/java/ca/uhn/fhir/jpa/searchparam/registry/SearchParamRegistryImplTest.java index da9c6d0a1560..932e806f310a 100644 --- a/hapi-fhir-jpaserver-searchparam/src/test/java/ca/uhn/fhir/jpa/searchparam/registry/SearchParamRegistryImplTest.java +++ b/hapi-fhir-jpaserver-searchparam/src/test/java/ca/uhn/fhir/jpa/searchparam/registry/SearchParamRegistryImplTest.java @@ -48,6 +48,7 @@ import org.junit.jupiter.api.extension.RegisterExtension; import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.CsvSource; +import org.junit.jupiter.params.provider.ValueSource; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.mock.mockito.MockBean; import org.springframework.context.annotation.Bean; @@ -479,7 +480,52 @@ public void testContentAndTextSearchParamsCanReplaceBuiltIn(String theParamName, } else { assertNull(textSp); } + } + + @ParameterizedTest + @ValueSource(booleans = { true, false }) + public void addActiveSearchParameterToLocalCache_withValidSP_addsItToCache(boolean theIsActive) { + // setup + SearchParameter sp = new SearchParameter(); + sp.addBase("Patient"); + sp.setUrl("http://localhost/test"); + sp.setType(Enumerations.SearchParamType.STRING); + sp.setName("HelloWorld"); + sp.setDescription("description"); + sp.setExpression("Patient.name.given"); + sp.setCode("HelloWorld"); + if (theIsActive) { + sp.setStatus(Enumerations.PublicationStatus.ACTIVE); + } else { + sp.setStatus(Enumerations.PublicationStatus.RETIRED); + } + SearchParameterCanonicalizer canonicalizer = new SearchParameterCanonicalizer(FhirContext.forR4Cached()); + // test + try { + mySearchParamRegistry.addActiveSearchParameterToLocalCache(canonicalizer.canonicalizeSearchParameter(sp)); + mySearchParamRegistry.forceRefresh(); + + // validate + ReadOnlySearchParamCache cache = mySearchParamRegistry.getActiveSearchParams(); + + assertNotNull(cache); + if (theIsActive) { + assertTrue(cache.getSearchParamMap("Patient") + .containsParamName("HelloWorld")); + } else { + assertFalse(cache.getSearchParamMap("Patient") + .containsParamName("HelloWorld")); + } + + // req'd or our beforeeach will fail + mySearchParamRegistry.clearLocalSearchParameterCache(); + mySearchParamRegistry.forceRefresh(); + } finally { + // verify + ReadOnlySearchParamCache cache = mySearchParamRegistry.getActiveSearchParams(); + assertFalse(cache.getSearchParamMap("Patient").containsParamName("HelloWorld")); + } } private List resetDatabaseToOrigSearchParamsPlusNewOneWithStatus(Enumerations.PublicationStatus theStatus) { diff --git a/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/RestfulServerConfiguration.java b/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/RestfulServerConfiguration.java index 9e75a616bbfd..4c0a58b40d93 100644 --- a/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/RestfulServerConfiguration.java +++ b/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/RestfulServerConfiguration.java @@ -453,12 +453,12 @@ public RuntimeSearchParam getActiveSearchParamByUrl( @Override public void addActiveSearchParameterToLocalCache(RuntimeSearchParam theSearchParam) { - // TODO - throw not supported exception with error code + throw new UnsupportedOperationException(Msg.code(2794)); } @Override public void clearLocalSearchParameterCache() { - // TODO - throw not supported exception with error code + throw new UnsupportedOperationException(Msg.code(2795)); } private void createRuntimeBinding( diff --git a/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/util/FhirContextSearchParamRegistry.java b/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/util/FhirContextSearchParamRegistry.java index b0a642c6cb32..b68198fada9a 100644 --- a/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/util/FhirContextSearchParamRegistry.java +++ b/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/util/FhirContextSearchParamRegistry.java @@ -109,12 +109,12 @@ public RuntimeSearchParam getActiveSearchParamByUrl( @Override public void addActiveSearchParameterToLocalCache(RuntimeSearchParam theSearchParam) { - // todo - throw unsupported exceptions + throw new UnsupportedOperationException(Msg.code(2792)); } @Override public void clearLocalSearchParameterCache() { - // todo - throw unsupported exceptions; requires updating the msg.code + throw new UnsupportedOperationException(Msg.code(2793)); } @Override From 544ac53123097738f747d9f203e1ad693efcdc2e Mon Sep 17 00:00:00 2001 From: leif stawnyczy Date: Tue, 26 Aug 2025 11:27:47 -0400 Subject: [PATCH 05/36] adding changelog --- ...08-allow-searchparamregistry-to-have-a-local-cache.yaml | 7 +++++++ 1 file changed, 7 insertions(+) create mode 100644 hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/changelog/8_6_0/7208-allow-searchparamregistry-to-have-a-local-cache.yaml diff --git a/hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/changelog/8_6_0/7208-allow-searchparamregistry-to-have-a-local-cache.yaml b/hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/changelog/8_6_0/7208-allow-searchparamregistry-to-have-a-local-cache.yaml new file mode 100644 index 000000000000..e4cf58881ef3 --- /dev/null +++ b/hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/changelog/8_6_0/7208-allow-searchparamregistry-to-have-a-local-cache.yaml @@ -0,0 +1,7 @@ +--- +type: add +issue: 7208 +title: "Added the ability to add SearchParameters to a local cache + without having them added to the built in search parameters + or the database. +" From 5ba05cda1de81f2b683e866199b9f10a2c6753b0 Mon Sep 17 00:00:00 2001 From: leif stawnyczy Date: Tue, 26 Aug 2025 11:28:07 -0400 Subject: [PATCH 06/36] spotless --- .../searchparam/registry/SearchParamRegistryImpl.java | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/hapi-fhir-jpaserver-searchparam/src/main/java/ca/uhn/fhir/jpa/searchparam/registry/SearchParamRegistryImpl.java b/hapi-fhir-jpaserver-searchparam/src/main/java/ca/uhn/fhir/jpa/searchparam/registry/SearchParamRegistryImpl.java index c290f0aba877..3f024870ee0e 100644 --- a/hapi-fhir-jpaserver-searchparam/src/main/java/ca/uhn/fhir/jpa/searchparam/registry/SearchParamRegistryImpl.java +++ b/hapi-fhir-jpaserver-searchparam/src/main/java/ca/uhn/fhir/jpa/searchparam/registry/SearchParamRegistryImpl.java @@ -272,12 +272,11 @@ private void initializeActiveSearchParams(Collection theJpaSearch ourLog.trace("Have overridden {} built-in search parameters", overriddenCount); // add the local cache search params - myLocalSPCache.getSearchParamStream() - .forEach(sp -> { - for (String rt : sp.getBase()) { - searchParams.add(rt, sp.getName(), sp); - } - }); + myLocalSPCache.getSearchParamStream().forEach(sp -> { + for (String rt : sp.getBase()) { + searchParams.add(rt, sp.getName(), sp); + } + }); // Auto-register: _language if (myStorageSettings.isLanguageSearchParameterEnabled()) { From e1c1db1f46ca69bcda5c9d9b64571a9bef6c29fd Mon Sep 17 00:00:00 2001 From: leif stawnyczy Date: Thu, 28 Aug 2025 10:53:43 -0400 Subject: [PATCH 07/36] updating spvalidator --- .../SearchParameterDaoValidatorTest.java | 101 +++++++++++++++++- .../SearchParameterDaoValidator.java | 50 ++++++++- 2 files changed, 148 insertions(+), 3 deletions(-) diff --git a/hapi-fhir-jpaserver-test-utilities/src/test/java/ca/uhn/fhir/jpa/dao/validation/SearchParameterDaoValidatorTest.java b/hapi-fhir-jpaserver-test-utilities/src/test/java/ca/uhn/fhir/jpa/dao/validation/SearchParameterDaoValidatorTest.java index 763564336114..93c24fef7011 100644 --- a/hapi-fhir-jpaserver-test-utilities/src/test/java/ca/uhn/fhir/jpa/dao/validation/SearchParameterDaoValidatorTest.java +++ b/hapi-fhir-jpaserver-test-utilities/src/test/java/ca/uhn/fhir/jpa/dao/validation/SearchParameterDaoValidatorTest.java @@ -4,8 +4,10 @@ import ca.uhn.fhir.context.RuntimeSearchParam; import ca.uhn.fhir.jpa.api.config.JpaStorageSettings; import ca.uhn.fhir.jpa.searchparam.registry.SearchParameterCanonicalizer; +import ca.uhn.fhir.rest.api.RestSearchParameterTypeEnum; import ca.uhn.fhir.rest.server.exceptions.UnprocessableEntityException; import ca.uhn.fhir.rest.server.util.ISearchParamRegistry; +import ca.uhn.fhir.util.FhirTerser; import ca.uhn.fhir.util.HapiExtensions; import ca.uhn.hapi.converters.canonical.VersionCanonicalizer; import org.hl7.fhir.r5.model.BooleanType; @@ -20,11 +22,13 @@ import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.Arguments; import org.junit.jupiter.params.provider.MethodSource; +import org.junit.jupiter.params.provider.ValueSource; import org.mockito.InjectMocks; import org.mockito.Mock; import org.mockito.Spy; import org.mockito.junit.jupiter.MockitoExtension; +import java.util.Set; import java.util.stream.Stream; import static org.assertj.core.api.Assertions.assertThat; @@ -39,6 +43,7 @@ import static org.hl7.fhir.r5.model.Enumerations.SearchParamType.URI; import static org.hl7.fhir.r5.model.Enumerations.VersionIndependentResourceTypesAll.OBSERVATION; import static org.hl7.fhir.r5.model.Enumerations.VersionIndependentResourceTypesAll.PATIENT; +import static org.junit.jupiter.api.Assertions.assertTrue; import static org.junit.jupiter.api.Assertions.fail; import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.eq; @@ -85,6 +90,9 @@ private void createAndMockSearchParameter(Enumerations.SearchParamType theType, @Test public void testValidateSubscription() { SearchParameter sp = new SearchParameter(); + sp.setName("name"); + sp.setDescription("description"); + sp.setCode("name"); sp.setId("SearchParameter/patient-eyecolour"); sp.setUrl("http://example.org/SearchParameter/patient-eyecolour"); sp.addBase(PATIENT); @@ -98,6 +106,73 @@ public void testValidateSubscription() { mySvc.validate(canonicalSp); } + @Test + public void validate_overridingAllowed_doesNotThrow() { + // setup + boolean allowOverriding = myStorageSettings.isDefaultSearchParamsCanBeOverridden(); + try { + myStorageSettings.setDefaultSearchParamsCanBeOverridden(false); + + runOverrideBuiltInSPTest(new SearchParameterDaoValidator.SPValidatorOptions().setAllowOverriding(true)); + + // good + assertTrue(true); + } finally { + myStorageSettings.setDefaultSearchParamsCanBeOverridden(allowOverriding); + } + } + + @Test + public void validate_noOverridingAllowed_throws() { + // setup + boolean allowOverriding = myStorageSettings.isDefaultSearchParamsCanBeOverridden(); + try { + myStorageSettings.setDefaultSearchParamsCanBeOverridden(false); + + runOverrideBuiltInSPTest(new SearchParameterDaoValidator.SPValidatorOptions().setAllowOverriding(false)); + fail(); + } catch (UnprocessableEntityException ex) { + assertTrue(ex.getMessage().contains("Can not override built-in search parameter")); + } finally { + myStorageSettings.setDefaultSearchParamsCanBeOverridden(allowOverriding); + } + } + + private void runOverrideBuiltInSPTest(SearchParameterDaoValidator.SPValidatorOptions theOptions) { + SearchParameter sp = new SearchParameter(); + sp.setName("name"); + sp.setDescription("description"); + sp.setCode("name"); + sp.setId("SearchParameter/given-name"); + sp.setUrl("http://example.org/SearchParameter/patient-eyecolour"); + sp.addBase(PATIENT); + sp.setCode("eyecolour"); + sp.setType(TOKEN); + sp.setStatus(ACTIVE); + sp.setExpression("Patient.extension('http://foo')"); + sp.addTarget(PATIENT); + + RuntimeSearchParam rsp = new RuntimeSearchParam( + null, + "url", + "name", + "descriptoin", + "Patient.name", + RestSearchParameterTypeEnum.STRING, + null, + Set.of("Patient"), + RuntimeSearchParam.RuntimeSearchParamStatusEnum.ACTIVE, + Set.of("Patient") + ); + + // when; lenient because we might not call if overriding is allowed + lenient().when(mySearchParamRegistry.getActiveSearchParam("Patient", sp.getCode(), ISearchParamRegistry.SearchParamLookupContextEnum.ALL)) + .thenReturn(rsp); + + SearchParameter canonicalSp = myVersionCanonicalizer.searchParameterToCanonical(sp); + mySvc.validate(canonicalSp, theOptions); + } + @Test public void testValidateSubscriptionWithCustomType() { SearchParameter sp = new SearchParameter(); @@ -106,9 +181,12 @@ public void testValidateSubscriptionWithCustomType() { sp.addExtension(new Extension(HapiExtensions.EXTENSION_SEARCHPARAM_CUSTOM_BASE_RESOURCE).setValue(new StringType("Meal"))); sp.addExtension(new Extension(HapiExtensions.EXTENSION_SEARCHPARAM_CUSTOM_TARGET_RESOURCE).setValue(new StringType("Chef"))); sp.setCode("chef"); + sp.setName("chef"); + sp.setDescription("a chef thing"); sp.setType(REFERENCE); sp.setStatus(ACTIVE); sp.setExpression("Meal.chef"); + sp.addBase(PATIENT); SearchParameter canonicalSp = myVersionCanonicalizer.searchParameterToCanonical(sp); mySvc.validate(canonicalSp); @@ -132,6 +210,25 @@ public void testMethodValidate_nonUniqueComboAndCompositeSearchParamWithComponen } } + @ParameterizedTest + @ValueSource(strings = { "name", "code", "url", "description", "base" }) + public void validate_missingRequisiteField_throws(String theFieldToOmit) { + // setup + SearchParameter sp = createSearchParameter(STRING, "SearchParameter/sp", "patient-code", "Observation"); + + FhirTerser terser = myFhirContext.newTerser(); + terser.setElement(sp, theFieldToOmit, null); + + // test + try { + mySvc.validate(sp); + fail("SP missing requisite field " + theFieldToOmit + " still passes validation when it shouldn't."); + } catch (UnprocessableEntityException ex) { + assertTrue(ex.getMessage().contains( + String.format("SearchParameter.%s is missing", theFieldToOmit))); + } + } + @Test public void testMethodValidate_uniqueComboSearchParamWithComponentOfTypeReference_isValid() { SearchParameter sp = createSearchParameter(COMPOSITE, "SearchParameter/patient-code", "patient-code", "Observation"); @@ -193,8 +290,10 @@ public void testMethodValidate_allCompositeSpTypesWithComponentOfValidType_isVal } private static SearchParameter createSearchParameter(Enumerations.SearchParamType theType, String theId, String theCodeValue, String theExpression) { - SearchParameter retVal = new SearchParameter(); + retVal.setName("Name"); + retVal.setCode("Name"); + retVal.setDescription("description of sp"); retVal.setId(theId); retVal.setUrl("http://example.org/" + theId); retVal.addBase(OBSERVATION); diff --git a/hapi-fhir-storage/src/main/java/ca/uhn/fhir/jpa/dao/validation/SearchParameterDaoValidator.java b/hapi-fhir-storage/src/main/java/ca/uhn/fhir/jpa/dao/validation/SearchParameterDaoValidator.java index 7fc4cfd4e2fb..2d0d824db781 100644 --- a/hapi-fhir-storage/src/main/java/ca/uhn/fhir/jpa/dao/validation/SearchParameterDaoValidator.java +++ b/hapi-fhir-storage/src/main/java/ca/uhn/fhir/jpa/dao/validation/SearchParameterDaoValidator.java @@ -51,8 +51,28 @@ public class SearchParameterDaoValidator { + public static class SPValidatorOptions { + + public static SPValidatorOptions defaultOptions() { + return new SPValidatorOptions(); + } + + private boolean myAllowOverriding = false; + + public boolean allowOverriding() { + return myAllowOverriding; + } + + public SPValidatorOptions setAllowOverriding(boolean theAllowOverriding) { + myAllowOverriding = theAllowOverriding; + return this; + } + } + private static final Pattern REGEX_SP_EXPRESSION_HAS_PATH = Pattern.compile("[( ]*([A-Z][a-zA-Z]+\\.)?[a-z].*"); + private static final String SP_FIELD_IS_MISSING = "SearchParameter.%s is missing."; + private final FhirContext myFhirContext; private final JpaStorageSettings myStorageSettings; private final ISearchParamRegistry mySearchParamRegistry; @@ -66,12 +86,16 @@ public SearchParameterDaoValidator( mySearchParamRegistry = theSearchParamRegistry; } - public void validate(SearchParameter searchParameter) { + public void validate(SearchParameter theSearchParameter) { + validate(theSearchParameter, SPValidatorOptions.defaultOptions()); + } + + public void validate(SearchParameter searchParameter, SPValidatorOptions theOptions) { /* * If overriding built-in SPs is disabled on this server, make sure we aren't * doing that */ - if (myStorageSettings.isDefaultSearchParamsCanBeOverridden() == false) { + if (!theOptions.allowOverriding() && !myStorageSettings.isDefaultSearchParamsCanBeOverridden()) { for (IPrimitiveType nextBaseType : searchParameter.getBase()) { String nextBase = nextBaseType.getValueAsString(); RuntimeSearchParam existingSearchParam = mySearchParamRegistry.getActiveSearchParam( @@ -99,6 +123,28 @@ public void validate(SearchParameter searchParameter) { return; } + // requisite fields: base, name, description, status, url, code + + if (isBlank(searchParameter.getName())) { + throw new UnprocessableEntityException(Msg.code(2798) + String.format(SP_FIELD_IS_MISSING, "name")); + } + + if (isBlank(searchParameter.getDescription())) { + throw new UnprocessableEntityException(Msg.code(2799) + String.format(SP_FIELD_IS_MISSING, "description")); + } + + if (isBlank(searchParameter.getUrl())) { + throw new UnprocessableEntityException(Msg.code(2800) + String.format(SP_FIELD_IS_MISSING, "url")); + } + + if (isBlank(searchParameter.getCode())) { + throw new UnprocessableEntityException(Msg.code(2801) + String.format(SP_FIELD_IS_MISSING, "code")); + } + + if (searchParameter.getBase() == null || searchParameter.getBase().isEmpty()) { + throw new UnprocessableEntityException(Msg.code(2802) + String.format(SP_FIELD_IS_MISSING, "base")); + } + // Search parameters must have a base if (isCompositeWithoutBase(searchParameter)) { throw new UnprocessableEntityException(Msg.code(1113) + "SearchParameter.base is missing"); From 7cdc8b255396457d19765d1db19b3fb27c47019f Mon Sep 17 00:00:00 2001 From: leif stawnyczy Date: Fri, 29 Aug 2025 10:20:28 -0400 Subject: [PATCH 08/36] adding documentation --- .../fhir/jpa/dao/validation/SearchParameterDaoValidator.java | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/hapi-fhir-storage/src/main/java/ca/uhn/fhir/jpa/dao/validation/SearchParameterDaoValidator.java b/hapi-fhir-storage/src/main/java/ca/uhn/fhir/jpa/dao/validation/SearchParameterDaoValidator.java index 2d0d824db781..361bd90fc9e2 100644 --- a/hapi-fhir-storage/src/main/java/ca/uhn/fhir/jpa/dao/validation/SearchParameterDaoValidator.java +++ b/hapi-fhir-storage/src/main/java/ca/uhn/fhir/jpa/dao/validation/SearchParameterDaoValidator.java @@ -49,6 +49,10 @@ import static ca.uhn.fhir.rest.api.RestSearchParameterTypeEnum.URI; import static org.apache.commons.lang3.StringUtils.isBlank; +/** + * Validates SP to make sure they have all the requisite fields needed + * refer to the spec + */ public class SearchParameterDaoValidator { public static class SPValidatorOptions { From 4180ad95d1047b3cb999e0b705d94e4f709106d8 Mon Sep 17 00:00:00 2001 From: leif stawnyczy Date: Fri, 29 Aug 2025 16:00:21 -0400 Subject: [PATCH 09/36] fixing tests --- .../jpa/dao/r4/BasePartitioningR4Test.java | 34 +++++++++++++++---- .../jpa/dao/r4/PartitioningSqlR4Test.java | 5 ++- .../fhir/jpa/provider/r4/ExpungeR4Test.java | 7 ++++ .../CompositeSearchParameterTestCases.java | 2 ++ 4 files changed, 41 insertions(+), 7 deletions(-) diff --git a/hapi-fhir-jpaserver-test-r4/src/test/java/ca/uhn/fhir/jpa/dao/r4/BasePartitioningR4Test.java b/hapi-fhir-jpaserver-test-r4/src/test/java/ca/uhn/fhir/jpa/dao/r4/BasePartitioningR4Test.java index 0cd2b14d6b62..9ae8a2f546c7 100644 --- a/hapi-fhir-jpaserver-test-r4/src/test/java/ca/uhn/fhir/jpa/dao/r4/BasePartitioningR4Test.java +++ b/hapi-fhir-jpaserver-test-r4/src/test/java/ca/uhn/fhir/jpa/dao/r4/BasePartitioningR4Test.java @@ -1,11 +1,5 @@ package ca.uhn.fhir.jpa.dao.r4; -import static ca.uhn.fhir.interceptor.model.RequestPartitionId.defaultPartition; -import static ca.uhn.fhir.interceptor.model.RequestPartitionId.fromPartitionId; -import static ca.uhn.fhir.interceptor.model.RequestPartitionId.fromPartitionIds; -import static ca.uhn.fhir.interceptor.model.RequestPartitionId.fromPartitionNames; -import static ca.uhn.fhir.jpa.model.entity.ResourceTable.IDX_RES_TYPE_FHIR_ID; -import static org.junit.jupiter.api.Assertions.assertNotNull; import ca.uhn.fhir.interceptor.api.Hook; import ca.uhn.fhir.interceptor.api.Interceptor; import ca.uhn.fhir.interceptor.api.Pointcut; @@ -39,7 +33,13 @@ import java.util.Arrays; import java.util.List; +import static ca.uhn.fhir.interceptor.model.RequestPartitionId.defaultPartition; +import static ca.uhn.fhir.interceptor.model.RequestPartitionId.fromPartitionId; +import static ca.uhn.fhir.interceptor.model.RequestPartitionId.fromPartitionIds; +import static ca.uhn.fhir.interceptor.model.RequestPartitionId.fromPartitionNames; +import static ca.uhn.fhir.jpa.model.entity.ResourceTable.IDX_RES_TYPE_FHIR_ID; import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.api.Assertions.assertNotNull; import static org.mockito.Mockito.when; public abstract class BasePartitioningR4Test extends BaseJpaR4SystemTest { @@ -158,6 +158,9 @@ protected void createUniqueComboSp() { sp.setId("SearchParameter/patient-gender"); sp.setType(Enumerations.SearchParamType.TOKEN); sp.setCode("gender"); + sp.setName("pgender"); + sp.setUrl("http://localhost/" + sp.getId()); + sp.setDescription("description"); sp.setExpression("Patient.gender"); sp.setStatus(Enumerations.PublicationStatus.ACTIVE); sp.addBase("Patient"); @@ -169,17 +172,25 @@ protected void createUniqueComboSp() { sp.setId("SearchParameter/patient-family"); sp.setType(Enumerations.SearchParamType.STRING); sp.setCode("family"); + sp.setName("family"); + sp.setDescription("description"); + sp.setUrl("http://localhost/SearchParameter/patient-family"); sp.setExpression("Patient.name[0].family"); sp.setStatus(Enumerations.PublicationStatus.ACTIVE); sp.addBase("Patient"); mySearchParameterDao.update(sp, mySrd); addNextTargetPartitionForCreateWithIdDefaultPartition(); + addNextTargetPartitionForReadDefaultPartition(); // one for search param validation sp = new SearchParameter(); sp.setId("SearchParameter/patient-gender-family-unique"); sp.setType(Enumerations.SearchParamType.COMPOSITE); sp.setStatus(Enumerations.PublicationStatus.ACTIVE); sp.addBase("Patient"); + sp.setName("patient-gender-family-unique"); + sp.setCode("patient-gender-family-unique"); + sp.setUrl("http://localhost/" + sp.getId()); + sp.setDescription("description"); sp.addComponent() .setExpression("Patient") .setDefinition("SearchParameter/patient-gender"); @@ -203,6 +214,9 @@ protected void createNonUniqueComboSp() { sp.setId("SearchParameter/patient-family"); sp.setType(Enumerations.SearchParamType.STRING); sp.setCode("family"); + sp.setName("family"); + sp.setDescription("description"); + sp.setUrl("http://localhost/" + sp.getId()); sp.setExpression("Patient.name.family"); sp.setStatus(Enumerations.PublicationStatus.ACTIVE); sp.addBase("Patient"); @@ -214,16 +228,24 @@ protected void createNonUniqueComboSp() { sp.setId("SearchParameter/patient-managingorg"); sp.setType(Enumerations.SearchParamType.REFERENCE); sp.setCode(Patient.SP_ORGANIZATION); + sp.setName(Patient.SP_ORGANIZATION); + sp.setDescription("description"); + sp.setUrl("http://localhost/" + sp.getId()); sp.setExpression("Patient.managingOrganization"); sp.setStatus(Enumerations.PublicationStatus.ACTIVE); sp.addBase("Patient"); mySearchParameterDao.update(sp, mySrd); addNextTargetPartitionForCreateWithIdDefaultPartition(); + addNextTargetPartitionForReadDefaultPartition(); // one for search param validation sp = new SearchParameter(); sp.setId("SearchParameter/patient-family-and-org"); sp.setType(Enumerations.SearchParamType.COMPOSITE); sp.setStatus(Enumerations.PublicationStatus.ACTIVE); + sp.setCode("family-and-org"); + sp.setName("family-and-org"); + sp.setUrl("http://localhost/" + sp.getId()); + sp.setDescription("description"); sp.addBase("Patient"); sp.addComponent() .setExpression("Patient") diff --git a/hapi-fhir-jpaserver-test-r4/src/test/java/ca/uhn/fhir/jpa/dao/r4/PartitioningSqlR4Test.java b/hapi-fhir-jpaserver-test-r4/src/test/java/ca/uhn/fhir/jpa/dao/r4/PartitioningSqlR4Test.java index dacd11ad69c1..68396c680c7d 100644 --- a/hapi-fhir-jpaserver-test-r4/src/test/java/ca/uhn/fhir/jpa/dao/r4/PartitioningSqlR4Test.java +++ b/hapi-fhir-jpaserver-test-r4/src/test/java/ca/uhn/fhir/jpa/dao/r4/PartitioningSqlR4Test.java @@ -107,7 +107,6 @@ import static org.mockito.Mockito.verify; @SuppressWarnings({"unchecked", "ConstantConditions"}) - public class PartitioningSqlR4Test extends BasePartitioningR4Test { private static final Logger ourLog = LoggerFactory.getLogger(PartitioningSqlR4Test.class); @@ -146,6 +145,8 @@ public void testCreateSearchParameter_DefaultPartition() { sp.setType(Enumerations.SearchParamType.REFERENCE); sp.setCode("extpatorg"); sp.setName("extpatorg"); + sp.setDescription("description"); + sp.setUrl("http://localhost/extpatorg"); sp.setExpression("Patient.extension('http://patext').value.as(Reference)"); Long id = mySearchParameterDao.create(sp, mySrd).getId().getIdPartAsLong(); @@ -323,6 +324,8 @@ public void testCreateSearchParameter_DefaultPartitionWithDate() { sp.setType(Enumerations.SearchParamType.REFERENCE); sp.setCode("extpatorg"); sp.setName("extpatorg"); + sp.setDescription("description"); + sp.setUrl("http://localhost/SearchParameter/extoatorg"); sp.setExpression("Patient.extension('http://patext').value.as(Reference)"); Long id = mySearchParameterDao.create(sp, mySrd).getId().getIdPartAsLong(); diff --git a/hapi-fhir-jpaserver-test-r4/src/test/java/ca/uhn/fhir/jpa/provider/r4/ExpungeR4Test.java b/hapi-fhir-jpaserver-test-r4/src/test/java/ca/uhn/fhir/jpa/provider/r4/ExpungeR4Test.java index c970f5822045..e596f7e88832 100644 --- a/hapi-fhir-jpaserver-test-r4/src/test/java/ca/uhn/fhir/jpa/provider/r4/ExpungeR4Test.java +++ b/hapi-fhir-jpaserver-test-r4/src/test/java/ca/uhn/fhir/jpa/provider/r4/ExpungeR4Test.java @@ -143,6 +143,9 @@ public void createStandardPatients() { sp.setId("SearchParameter/patient-birthdate"); sp.setType(Enumerations.SearchParamType.DATE); sp.setCode("birthdate"); + sp.setName("birthdate"); + sp.setDescription("description"); + sp.setUrl("Http://localhost/SearchParameter/patient-birthdate"); sp.setExpression("Patient.birthDate"); sp.setStatus(Enumerations.PublicationStatus.ACTIVE); sp.addBase("Patient"); @@ -153,6 +156,10 @@ public void createStandardPatients() { sp.setType(Enumerations.SearchParamType.COMPOSITE); sp.setStatus(Enumerations.PublicationStatus.ACTIVE); sp.addBase("Patient"); + sp.setName("birthday-unique"); + sp.setCode("birthday-unique"); + sp.setDescription("description"); + sp.setUrl("http://localhost/SearchParameter/patient-birthdate-unique"); sp.addComponent() .setExpression("Patient") .setDefinition("SearchParameter/patient-birthdate"); diff --git a/hapi-fhir-jpaserver-test-utilities/src/main/java/ca/uhn/fhir/jpa/search/CompositeSearchParameterTestCases.java b/hapi-fhir-jpaserver-test-utilities/src/main/java/ca/uhn/fhir/jpa/search/CompositeSearchParameterTestCases.java index dc4b89a0b2dd..277f69556f7d 100644 --- a/hapi-fhir-jpaserver-test-utilities/src/main/java/ca/uhn/fhir/jpa/search/CompositeSearchParameterTestCases.java +++ b/hapi-fhir-jpaserver-test-utilities/src/main/java/ca/uhn/fhir/jpa/search/CompositeSearchParameterTestCases.java @@ -286,6 +286,8 @@ private static SearchParameter createCompositeSearchParameter(String theCodeValu retVal.setUrl("http://example.org/" + theCodeValue); retVal.addBase(theBase); retVal.setCode(theCodeValue); + retVal.setName(theCodeValue); + retVal.setDescription("description"); retVal.setType(Enumerations.SearchParamType.COMPOSITE); retVal.setStatus(Enumerations.PublicationStatus.ACTIVE); retVal.setExpression(theBase); From ad6a33a1da38f475781fc5af5a6069668ce9bd94 Mon Sep 17 00:00:00 2001 From: leif stawnyczy Date: Fri, 29 Aug 2025 16:24:47 -0400 Subject: [PATCH 10/36] fixing tests --- ...ourceDaoR4SearchCustomSearchParamTest.java | 119 +++++++++++++++++- .../SearchParameterDaoValidator.java | 4 - 2 files changed, 116 insertions(+), 7 deletions(-) diff --git a/hapi-fhir-jpaserver-test-r4/src/test/java/ca/uhn/fhir/jpa/dao/r4/FhirResourceDaoR4SearchCustomSearchParamTest.java b/hapi-fhir-jpaserver-test-r4/src/test/java/ca/uhn/fhir/jpa/dao/r4/FhirResourceDaoR4SearchCustomSearchParamTest.java index ac916d9ac037..323784939cab 100644 --- a/hapi-fhir-jpaserver-test-r4/src/test/java/ca/uhn/fhir/jpa/dao/r4/FhirResourceDaoR4SearchCustomSearchParamTest.java +++ b/hapi-fhir-jpaserver-test-r4/src/test/java/ca/uhn/fhir/jpa/dao/r4/FhirResourceDaoR4SearchCustomSearchParamTest.java @@ -104,6 +104,9 @@ public void testStoreSearchParamWithBracketsInExpression() { SearchParameter fooSp = new SearchParameter(); fooSp.setCode("foo"); + fooSp.setName("foo"); + fooSp.setDescription("description"); + fooSp.setUrl("http://localhost/SearchParameter/foo"); fooSp.addBase("ActivityDefinition"); fooSp.setType(Enumerations.SearchParamType.REFERENCE); fooSp.setTitle("FOO SP"); @@ -124,6 +127,9 @@ public void testStoreSearchParamWithBracketsInExpressionNormalizedQuantitySearch SearchParameter fooSp = new SearchParameter(); fooSp.setCode("foo"); + fooSp.setName("foo"); + fooSp.setDescription("description"); + fooSp.setUrl("http://localhost/SearchParameter/foo"); fooSp.addBase("ActivityDefinition"); fooSp.setType(Enumerations.SearchParamType.REFERENCE); fooSp.setTitle("FOO SP"); @@ -145,6 +151,9 @@ public void testStoreSearchParamWithBracketsInExpressionNormalizedQuantityStorag SearchParameter fooSp = new SearchParameter(); fooSp.setCode("foo"); + fooSp.setName("foo"); + fooSp.setDescription("description"); + fooSp.setUrl("http://localhost/SearchParameter/foo"); fooSp.addBase("ActivityDefinition"); fooSp.setType(Enumerations.SearchParamType.REFERENCE); fooSp.setTitle("FOO SP"); @@ -237,6 +246,9 @@ public void testCreatePhoneticSearchParameterWithOptionalCharacterLength() { // same encoder, but different lengths SearchParameter searchParam = constructSearchParameter(); searchParam.setCode("fuzzydefault"); + searchParam.setName(searchParam.getCode()); + searchParam.setDescription("description"); + searchParam.setUrl("http://localhost/SearchParameter/" + searchParam.getCode()); searchParam.addExtension() .setUrl(HapiExtensions.EXT_SEARCHPARAM_PHONETIC_ENCODER) .setValue(new StringType(PhoneticEncoderEnum.METAPHONE.name())); @@ -246,6 +258,10 @@ public void testCreatePhoneticSearchParameterWithOptionalCharacterLength() { SearchParameter searchParamModified = constructSearchParameter(); searchParamModified.setCode("fuzzymodified"); + searchParamModified.setName(searchParam.getCode()); + searchParamModified.setDescription("description"); + searchParamModified.setUrl("http://localhost/SearchParameter/" + searchParam.getCode()); + searchParamModified.setName(searchParamModified.getCode()); searchParamModified.addExtension() .setUrl(HapiExtensions.EXT_SEARCHPARAM_PHONETIC_ENCODER) .setValue(new StringType(PhoneticEncoderEnum.METAPHONE.name() + "(" + modifiedLength + ")")); @@ -309,6 +325,9 @@ public void testCreateInvalidUnquotedExtensionUrl() { SearchParameter fooSp = new SearchParameter(); fooSp.setCode("foo"); + fooSp.setName("foo"); + fooSp.setDescription("description"); + fooSp.setUrl("http://localhost/SearchParameter/foo"); fooSp.setType(Enumerations.SearchParamType.STRING); fooSp.setTitle("FOO SP"); fooSp.setExpression(invalidExpression); @@ -329,6 +348,9 @@ public void testCreateInvalidUnquotedExtensionUrl() { public void testCreateInvalidNoBase() { SearchParameter fooSp = new SearchParameter(); fooSp.setCode("foo"); + fooSp.setName("foo"); + fooSp.setDescription("description"); + fooSp.setUrl("http://localhost/SearchParameter/foo"); fooSp.setType(org.hl7.fhir.r4.model.Enumerations.SearchParamType.TOKEN); fooSp.setTitle("FOO SP"); fooSp.setExpression("Patient.gender"); @@ -367,6 +389,9 @@ public void testCreateCompositeParamNoExpressionAtRootLevel() { SearchParameter fooSp = new SearchParameter(); fooSp.addBase("Patient"); fooSp.setCode("foo"); + fooSp.setName("foo"); + fooSp.setDescription("description"); + fooSp.setUrl("http://localhost/SearchParameter/foo"); fooSp.setType(Enumerations.SearchParamType.COMPOSITE); fooSp.setTitle("FOO SP"); fooSp.setStatus(org.hl7.fhir.r4.model.Enumerations.PublicationStatus.ACTIVE); @@ -381,6 +406,9 @@ public void testCreateInvalidParamNoExpression() { SearchParameter fooSp = new SearchParameter(); fooSp.addBase("Patient"); fooSp.setCode("foo"); + fooSp.setName("foo"); + fooSp.setDescription("description"); + fooSp.setUrl("http://localhost/SearchParameter/foo"); fooSp.setType(org.hl7.fhir.r4.model.Enumerations.SearchParamType.TOKEN); fooSp.setTitle("FOO SP"); fooSp.setXpathUsage(org.hl7.fhir.r4.model.SearchParameter.XPathUsageType.NORMAL); @@ -436,6 +464,9 @@ public void testCreateInvalidParamParamNullStatus() { public void testCreateSearchParameterOnSearchParameterDoesntCauseEndlessReindexLoop() { SearchParameter fooSp = new SearchParameter(); fooSp.setCode("foo"); + fooSp.setName("foo"); + fooSp.setDescription("description"); + fooSp.setUrl("http://localhost/SearchParameter/foo"); fooSp.addBase("SearchParameter"); fooSp.setType(org.hl7.fhir.r4.model.Enumerations.SearchParamType.TOKEN); fooSp.setTitle("FOO SP"); @@ -457,6 +488,9 @@ public void testCustomReferenceParameter() throws Exception { SearchParameter sp = new SearchParameter(); sp.addBase("Patient"); sp.setCode("myDoctor"); + sp.setName("myDoctor"); + sp.setDescription("description"); + sp.setUrl("http://localhost/SearchParameter/myDoctor"); sp.setType(org.hl7.fhir.r4.model.Enumerations.SearchParamType.REFERENCE); sp.setTitle("My Doctor"); sp.setExpression("Patient.extension('http://fmcna.com/myDoctor').value.as(Reference)"); @@ -490,6 +524,9 @@ public void testIndexIntoBundle() { SearchParameter sp = new SearchParameter(); sp.addBase("Bundle"); sp.setCode("messageid"); + sp.setName(sp.getCode()); + sp.setUrl("http://localhost/SearchParameter/messageid"); + sp.setDescription("description"); sp.setType(Enumerations.SearchParamType.TOKEN); sp.setTitle("Message ID"); sp.setExpression("Bundle.entry.resource.as(MessageHeader).id"); @@ -525,6 +562,9 @@ public void testExtensionWithNoValueIndexesWithoutFailure() { SearchParameter eyeColourSp = new SearchParameter(); eyeColourSp.addBase("Patient"); eyeColourSp.setCode("eyecolour"); + eyeColourSp.setName("eyecolour"); + eyeColourSp.setDescription("description"); + eyeColourSp.setUrl("http://localhost/SearchParameter/eyecolour"); eyeColourSp.setType(org.hl7.fhir.r4.model.Enumerations.SearchParamType.TOKEN); eyeColourSp.setTitle("Eye Colour"); eyeColourSp.setExpression("Patient.extension('http://acme.org/eyecolour')"); @@ -546,6 +586,9 @@ public void testIncludeExtensionReferenceAsRecurse() { SearchParameter attendingSp = new SearchParameter(); attendingSp.addBase("Patient"); attendingSp.setCode("attending"); + attendingSp.setName("attending"); + attendingSp.setDescription("description"); + attendingSp.setUrl("http://localhost/SearchParameter/attending"); attendingSp.setType(org.hl7.fhir.r4.model.Enumerations.SearchParamType.REFERENCE); attendingSp.setTitle("Attending"); attendingSp.setExpression("Patient.extension('http://acme.org/attending')"); @@ -610,6 +653,8 @@ public void testSearchParamStringOnExtensionForVeryLongContainsSearch() { " \"status\": \"active\",\n" + " \"publisher\": \"MOH-IDMS\",\n" + " \"code\": \"ServiceRequestIndication\",\n" + + " \"name\": \"ServiceRequestIndication\",\n" + + " \"description\": \"description\",\n" + " \"base\": [\n" + " \"ServiceRequest\"\n" + " ],\n" + @@ -703,6 +748,7 @@ public void testParamWithMultipleBases() { SearchParameter sp = new SearchParameter(); sp.setUrl("http://clinicalcloud.solutions/fhir/SearchParameter/request-reason"); sp.setName("reason"); + sp.setDescription("description"); sp.setStatus(Enumerations.PublicationStatus.ACTIVE); sp.setCode("reason"); sp.addBase("MedicationRequest"); @@ -743,6 +789,8 @@ public void testParamWithMultipleBasesToken() { sp.setName("reason"); sp.setStatus(Enumerations.PublicationStatus.ACTIVE); sp.setCode("reason"); + sp.setName("reason"); + sp.setDescription("description"); sp.addBase("MedicationRequest"); sp.addBase("ServiceRequest"); sp.setType(Enumerations.SearchParamType.TOKEN); @@ -770,6 +818,9 @@ public void testRejectSearchParamWithInvalidExpression() { SearchParameter threadIdSp = new SearchParameter(); threadIdSp.addBase("Communication"); threadIdSp.setCode("has-attachments"); + threadIdSp.setName("has-attachments"); + threadIdSp.setDescription("description"); + threadIdSp.setUrl("http://localhost/SearchParameter/has-attachments"); threadIdSp.setType(Enumerations.SearchParamType.REFERENCE); threadIdSp.setExpression("Communication.payload[1].contentAttachment is not null"); threadIdSp.setXpathUsage(SearchParameter.XPathUsageType.NORMAL); @@ -787,6 +838,9 @@ public void testSearchForExtensionReferenceWithNonMatchingTarget() { SearchParameter siblingSp = new SearchParameter(); siblingSp.addBase("Patient"); siblingSp.setCode("sibling"); + siblingSp.setName("sibling"); + siblingSp.setDescription("description"); + siblingSp.setUrl("http://localhost/SearchParameter/sibling"); siblingSp.setType(org.hl7.fhir.r4.model.Enumerations.SearchParamType.REFERENCE); siblingSp.setTitle("Sibling"); siblingSp.setExpression("Patient.extension('http://acme.org/sibling')"); @@ -839,6 +893,9 @@ public void testSearchForExtensionReferenceWithTwoPaths() { SearchParameter sp = new SearchParameter(); sp.addBase("DiagnosticReport"); sp.setCode("fooBar"); + sp.setName(sp.getCode()); + sp.setDescription("description"); + sp.setUrl("http://localhost/SearchParameter/foobar"); sp.setType(org.hl7.fhir.r4.model.Enumerations.SearchParamType.REFERENCE); sp.setTitle("FOO AND BAR"); sp.setExpression("DiagnosticReport.extension('http://foo') | DiagnosticReport.extension('http://bar')"); @@ -881,6 +938,9 @@ public void testSearchForExtensionReferenceWithTarget() { SearchParameter siblingSp = new SearchParameter(); siblingSp.addBase("Patient"); siblingSp.setCode("sibling"); + siblingSp.setName("sibling"); + siblingSp.setDescription("description"); + siblingSp.setUrl("http://localhost/SearchParameter/sibling"); siblingSp.setType(org.hl7.fhir.r4.model.Enumerations.SearchParamType.REFERENCE); siblingSp.setTitle("Sibling"); siblingSp.setExpression("Patient.extension('http://acme.org/sibling')"); @@ -936,6 +996,9 @@ public void testSearchForExtensionReferenceWithoutTarget() { SearchParameter siblingSp = new SearchParameter(); siblingSp.addBase("Patient"); siblingSp.setCode("sibling"); + siblingSp.setName("sibling"); + siblingSp.setDescription("description"); + siblingSp.setUrl("http://localhost/SearchParameter/sibling"); siblingSp.setType(org.hl7.fhir.r4.model.Enumerations.SearchParamType.REFERENCE); siblingSp.setTitle("Sibling"); siblingSp.setExpression("Patient.extension('http://acme.org/sibling')"); @@ -1009,6 +1072,9 @@ public void testSearchForExtensionToken() { SearchParameter eyeColourSp = new SearchParameter(); eyeColourSp.addBase("Patient"); eyeColourSp.setCode("eyecolour"); + eyeColourSp.setName("eyecolour"); + eyeColourSp.setDescription("description"); + eyeColourSp.setUrl("http://localhost/SearchParameter/eyecolour"); eyeColourSp.setType(org.hl7.fhir.r4.model.Enumerations.SearchParamType.TOKEN); eyeColourSp.setTitle("Eye Colour"); eyeColourSp.setExpression("Patient.extension('http://acme.org/eyecolour')"); @@ -1042,6 +1108,9 @@ public void testSearchForExtensionTwoDeepCodeableConcept() { SearchParameter siblingSp = new SearchParameter(); siblingSp.addBase("Patient"); siblingSp.setCode("foobar"); + siblingSp.setName("foobar"); + siblingSp.setDescription("Description"); + siblingSp.setUrl("http://localhost/SearchParameter/foobar"); siblingSp.setType(org.hl7.fhir.r4.model.Enumerations.SearchParamType.TOKEN); siblingSp.setTitle("FooBar"); siblingSp.setExpression("Patient.extension('http://acme.org/foo').extension('http://acme.org/bar')"); @@ -1080,6 +1149,9 @@ public void testSearchForExtensionTwoDeepCoding() { SearchParameter siblingSp = new SearchParameter(); siblingSp.addBase("Patient"); siblingSp.setCode("foobar"); + siblingSp.setName("foobar"); + siblingSp.setUrl("http://localhost/SearchParameter/foobar"); + siblingSp.setDescription("description"); siblingSp.setType(org.hl7.fhir.r4.model.Enumerations.SearchParamType.TOKEN); siblingSp.setTitle("FooBar"); siblingSp.setExpression("Patient.extension('http://acme.org/foo').extension('http://acme.org/bar')"); @@ -1118,6 +1190,9 @@ public void testSearchForExtensionTwoDeepDate() { SearchParameter siblingSp = new SearchParameter(); siblingSp.addBase("Patient"); siblingSp.setCode("foobar"); + siblingSp.setName("foobar"); + siblingSp.setDescription("description"); + siblingSp.setUrl("http://localhost/SearchParameter/foobar"); siblingSp.setType(org.hl7.fhir.r4.model.Enumerations.SearchParamType.DATE); siblingSp.setTitle("FooBar"); siblingSp.setExpression("Patient.extension('http://acme.org/foo').extension('http://acme.org/bar')"); @@ -1160,6 +1235,9 @@ public void testSearchForExtensionTwoDeepDecimalR4() { final SearchParameter siblingSp = new SearchParameter(); siblingSp.addBase("Patient"); siblingSp.setCode("foobar"); + siblingSp.setName("foobar"); + siblingSp.setDescription("description"); + siblingSp.setUrl("http://localhost/SearchParameter/foobar"); siblingSp.setType(org.hl7.fhir.r4.model.Enumerations.SearchParamType.NUMBER); siblingSp.setTitle("FooBar"); siblingSp.setExpression("Patient.extension('http://acme.org/foo').extension('http://acme.org/bar')"); @@ -1215,6 +1293,9 @@ public void testSearchForExtensionTwoDeepNumber() { SearchParameter siblingSp = new SearchParameter(); siblingSp.addBase("Patient"); siblingSp.setCode("foobar"); + siblingSp.setName("foobar"); + siblingSp.setDescription("description"); + siblingSp.setUrl("http://localhost/SearchParameter/foobar"); siblingSp.setType(org.hl7.fhir.r4.model.Enumerations.SearchParamType.NUMBER); siblingSp.setTitle("FooBar"); siblingSp.setExpression("Patient.extension('http://acme.org/foo').extension('http://acme.org/bar')"); @@ -1252,6 +1333,9 @@ public void testSearchForExtensionTwoDeepReference() { SearchParameter siblingSp = new SearchParameter(); siblingSp.addBase("Patient"); siblingSp.setCode("foobar"); + siblingSp.setName("foobar"); + siblingSp.setDescription("Description"); + siblingSp.setUrl("http://localhost/SearchParameter/foobar"); siblingSp.setType(org.hl7.fhir.r4.model.Enumerations.SearchParamType.REFERENCE); siblingSp.setTitle("FooBar"); siblingSp.setExpression("Patient.extension('http://acme.org/foo').extension('http://acme.org/bar')"); @@ -1295,6 +1379,9 @@ public void testSearchForExtensionTwoDeepReferenceWithoutType() { SearchParameter siblingSp = new SearchParameter(); siblingSp.addBase("Patient"); siblingSp.setCode("foobar"); + siblingSp.setName("foobar"); + siblingSp.setUrl("http://localhost/SearchParameter/foobar"); + siblingSp.setDescription("desdription"); siblingSp.setType(org.hl7.fhir.r4.model.Enumerations.SearchParamType.REFERENCE); siblingSp.setTitle("FooBar"); siblingSp.setExpression("Patient.extension('http://acme.org/foo').extension('http://acme.org/bar')"); @@ -1337,6 +1424,9 @@ public void testSearchForExtensionTwoDeepReferenceWrongType() { SearchParameter siblingSp = new SearchParameter(); siblingSp.addBase("Patient"); siblingSp.setCode("foobar"); + siblingSp.setName("foobar"); + siblingSp.setDescription("description"); + siblingSp.setUrl("http://localhost/SearchParameter/foobar"); siblingSp.setType(org.hl7.fhir.r4.model.Enumerations.SearchParamType.REFERENCE); siblingSp.setTitle("FooBar"); siblingSp.setExpression("Patient.extension('http://acme.org/foo').extension('http://acme.org/bar')"); @@ -1380,6 +1470,9 @@ public void testSearchForExtensionTwoDeepString() { SearchParameter siblingSp = new SearchParameter(); siblingSp.addBase("Patient"); siblingSp.setCode("foobar"); + siblingSp.setName("foobar"); + siblingSp.setDescription("description"); + siblingSp.setUrl("http://localhost/SearchParameter/foobar"); siblingSp.setType(org.hl7.fhir.r4.model.Enumerations.SearchParamType.STRING); siblingSp.setTitle("FooBar"); siblingSp.setExpression("Patient.extension('http://acme.org/foo').extension('http://acme.org/bar')"); @@ -1422,6 +1515,9 @@ public void testSearchForStringOnIdentifier() { SearchParameter fooSp = new SearchParameter(); fooSp.addBase("Patient"); fooSp.setCode("foo"); + fooSp.setName("foo"); + fooSp.setDescription("description"); + fooSp.setUrl("http://localhost/SearchParameter/foo"); fooSp.setType(org.hl7.fhir.r4.model.Enumerations.SearchParamType.STRING); fooSp.setTitle("FOO SP"); fooSp.setExpression("Patient.identifier.value"); @@ -1466,6 +1562,9 @@ public void testSearchForStringOnIdentifierWithSpecificSystem() { SearchParameter fooSp = new SearchParameter(); fooSp.addBase("Patient"); fooSp.setCode("foo"); + fooSp.setName("foo"); + fooSp.setDescription("description"); + fooSp.setUrl("http://localhost/SearchParameter/foo"); fooSp.setType(org.hl7.fhir.r4.model.Enumerations.SearchParamType.STRING); fooSp.setTitle("FOO SP"); fooSp.setExpression("Patient.identifier.where(system = 'http://AAA').value"); @@ -1510,6 +1609,9 @@ public void testSearchParameterDescendsIntoContainedResource() { SearchParameter sp = new SearchParameter(); sp.addBase("Observation"); sp.setCode("specimencollectedtime"); + sp.setName(sp.getCode()); + sp.setUrl("http://localhost/SearchParameter/" + sp.getName()); + sp.setDescription("description"); sp.setType(Enumerations.SearchParamType.DATE); sp.setTitle("Observation Specimen Collected Time"); sp.setExpression("Observation.specimen.resolve().receivedTime"); @@ -1555,6 +1657,9 @@ public void testSearchWithCustomParam() { SearchParameter fooSp = new SearchParameter(); fooSp.addBase("Patient"); fooSp.setCode("foo"); + fooSp.setName("foo"); + fooSp.setDescription("description"); + fooSp.setUrl("http://localhost/SearchParameter/foo"); fooSp.setType(org.hl7.fhir.r4.model.Enumerations.SearchParamType.TOKEN); fooSp.setTitle("FOO SP"); fooSp.setExpression("Patient.gender"); @@ -1636,6 +1741,9 @@ public void testSearchParametersWithVeryLongFhirPathExpressionsAreAccepted() { searchParameter.addBase("PractitionerRole"); searchParameter.setId("random-extension-sp"); searchParameter.setCode("random-extension"); + searchParameter.setName("random-extension"); + searchParameter.setDescription("Description"); + searchParameter.setUrl("http://localhost/SearchParameter/random-extension-sp"); searchParameter.setStatus(Enumerations.PublicationStatus.ACTIVE); searchParameter.setType(Enumerations.SearchParamType.REFERENCE); mySearchParameterDao.update(searchParameter); @@ -1671,6 +1779,9 @@ public void testSearchWithCustomParamDraft() { SearchParameter fooSp = new SearchParameter(); fooSp.addBase("Patient"); fooSp.setCode("foo"); + fooSp.setName("foo"); + fooSp.setDescription("description"); + fooSp.setUrl("http://localhost/SearchParameter/foo-2"); fooSp.setType(org.hl7.fhir.r4.model.Enumerations.SearchParamType.TOKEN); fooSp.setTitle("FOO SP"); fooSp.setExpression("Patient.gender"); @@ -1717,6 +1828,8 @@ public void testCustomCodeableConcept() { fooSp.addBase("ChargeItem"); fooSp.setName("Product"); fooSp.setCode("product"); + fooSp.setUrl("http://localhost/SearchParameter/product-p"); + fooSp.setDescription("description"); fooSp.setType(org.hl7.fhir.r4.model.Enumerations.SearchParamType.TOKEN); fooSp.setTitle("Product within a ChargeItem"); fooSp.setExpression("ChargeItem.product.as(CodeableConcept)"); @@ -1735,17 +1848,17 @@ public void testCustomCodeableConcept() { map.add("product", new TokenParam(null, "1")); IBundleProvider results = myChargeItemDao.search(map); assertEquals(1, results.size().intValue()); - - } - @Test public void testCompositeWithInvalidTarget() { // Setup SearchParameter sp = new SearchParameter(); sp.addBase("Patient"); sp.setCode("myDoctor"); + sp.setName("myDoctor"); + sp.setDescription("description"); + sp.setUrl("http://localhost/SearchParameter/myDoctor"); sp.setType(Enumerations.SearchParamType.COMPOSITE); sp.setTitle("My Doctor"); sp.setStatus(org.hl7.fhir.r4.model.Enumerations.PublicationStatus.ACTIVE); diff --git a/hapi-fhir-storage/src/main/java/ca/uhn/fhir/jpa/dao/validation/SearchParameterDaoValidator.java b/hapi-fhir-storage/src/main/java/ca/uhn/fhir/jpa/dao/validation/SearchParameterDaoValidator.java index 361bd90fc9e2..8efc753f1464 100644 --- a/hapi-fhir-storage/src/main/java/ca/uhn/fhir/jpa/dao/validation/SearchParameterDaoValidator.java +++ b/hapi-fhir-storage/src/main/java/ca/uhn/fhir/jpa/dao/validation/SearchParameterDaoValidator.java @@ -145,10 +145,6 @@ public void validate(SearchParameter searchParameter, SPValidatorOptions theOpti throw new UnprocessableEntityException(Msg.code(2801) + String.format(SP_FIELD_IS_MISSING, "code")); } - if (searchParameter.getBase() == null || searchParameter.getBase().isEmpty()) { - throw new UnprocessableEntityException(Msg.code(2802) + String.format(SP_FIELD_IS_MISSING, "base")); - } - // Search parameters must have a base if (isCompositeWithoutBase(searchParameter)) { throw new UnprocessableEntityException(Msg.code(1113) + "SearchParameter.base is missing"); From 7aebfd99c349e8597e8a00746a4dad28f2646d04 Mon Sep 17 00:00:00 2001 From: leif stawnyczy Date: Tue, 2 Sep 2025 09:59:51 -0400 Subject: [PATCH 11/36] review fixes --- .../SearchParameterCanonicalizer.java | 22 +++++++++++++++++++ 1 file changed, 22 insertions(+) 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 c27533888955..d29736d6a6da 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 @@ -52,6 +52,7 @@ import java.util.Collections; import java.util.HashSet; import java.util.List; +import java.util.Optional; import java.util.Set; import java.util.stream.Collectors; @@ -216,6 +217,27 @@ private RuntimeSearchParam canonicalizeSearchParameterDstu2( baseResource); } + /** + * Takes an IBaseResource SearchParameter (ie, fhir version independent) + * and returns true if the SP has a status = ACTIVE; false otherwise + * @param theSP the search parameter + */ + public boolean isSPActive(IBaseResource theSP) { + FhirTerser terser = myFhirContext.newTerser(); + // "status" has a cardinality of at least 0 and at most 1 for + // all versions; so more than 1 is not expected and < 1 we'll + // treat as "not active" + Optional statusOp = terser.getSinglePrimitiveValue(theSP, "status"); + + if (statusOp.isEmpty()) { + // not active? + return false; + } + String statusStr = statusOp.get(); + + return statusStr.equalsIgnoreCase("active"); + } + private RuntimeSearchParam canonicalizeSearchParameterDstu3(org.hl7.fhir.dstu3.model.SearchParameter theNextSp) { String name = theNextSp.getCode(); String description = theNextSp.getDescription(); From af039b97b40f8b0ee720d62074025002df4302cc Mon Sep 17 00:00:00 2001 From: leif stawnyczy Date: Tue, 2 Sep 2025 11:02:18 -0400 Subject: [PATCH 12/36] fixing more tests --- ...rResourceDaoR5SearchFtSelectiveEnablingTest.java | 1 + .../UpliftedRefchainsAndChainedSortingR5Test.java | 2 ++ .../SearchParameterDisabledForQueryingR5Test.java | 2 ++ .../reindex/InstanceReindexServiceImplR5Test.java | 13 +++++++++++++ .../test/util/ComboSearchParameterTestHelper.java | 10 ++++++++++ 5 files changed, 28 insertions(+) diff --git a/hapi-fhir-jpaserver-test-r5/src/test/java/ca/uhn/fhir/jpa/dao/r5/FhirResourceDaoR5SearchFtSelectiveEnablingTest.java b/hapi-fhir-jpaserver-test-r5/src/test/java/ca/uhn/fhir/jpa/dao/r5/FhirResourceDaoR5SearchFtSelectiveEnablingTest.java index a7491ec76186..e874057f6d93 100644 --- a/hapi-fhir-jpaserver-test-r5/src/test/java/ca/uhn/fhir/jpa/dao/r5/FhirResourceDaoR5SearchFtSelectiveEnablingTest.java +++ b/hapi-fhir-jpaserver-test-r5/src/test/java/ca/uhn/fhir/jpa/dao/r5/FhirResourceDaoR5SearchFtSelectiveEnablingTest.java @@ -346,6 +346,7 @@ private void createSearchParameter(String theParam, String theSpBase, String the } sp.setName(theParam); sp.setCode(theParam); + sp.setDescription("description"); sp.setStatus(Enumerations.PublicationStatus.fromCode(theSpStatus)); sp.addBase(Enumerations.VersionIndependentResourceTypesAll.fromCode(theSpBase)); sp.setType(Enumerations.SearchParamType.STRING); diff --git a/hapi-fhir-jpaserver-test-r5/src/test/java/ca/uhn/fhir/jpa/dao/r5/UpliftedRefchainsAndChainedSortingR5Test.java b/hapi-fhir-jpaserver-test-r5/src/test/java/ca/uhn/fhir/jpa/dao/r5/UpliftedRefchainsAndChainedSortingR5Test.java index 0a4db527f2a3..e7ae53f3c4db 100644 --- a/hapi-fhir-jpaserver-test-r5/src/test/java/ca/uhn/fhir/jpa/dao/r5/UpliftedRefchainsAndChainedSortingR5Test.java +++ b/hapi-fhir-jpaserver-test-r5/src/test/java/ca/uhn/fhir/jpa/dao/r5/UpliftedRefchainsAndChainedSortingR5Test.java @@ -148,6 +148,7 @@ public void testCreate_BundleWithComposition_UsingSimpleUplift() { sp.setCode(subjectSp.getName()); sp.setName(subjectSp.getName()); sp.setUrl(subjectSp.getUri()); + sp.setDescription("Description"); sp.setStatus(Enumerations.PublicationStatus.ACTIVE); sp.setType(Enumerations.SearchParamType.REFERENCE); sp.setExpression("Bundle.entry[0].resource.as(Composition)"); @@ -1024,6 +1025,7 @@ private void createSearchParam_EncounterSubject_WithUpliftOnName() { sp.setCode(subjectSp.getName()); sp.setName(subjectSp.getName()); sp.setUrl(subjectSp.getUri()); + sp.setDescription("Description"); sp.setStatus(Enumerations.PublicationStatus.ACTIVE); sp.setType(Enumerations.SearchParamType.REFERENCE); sp.setExpression(subjectSp.getPath()); diff --git a/hapi-fhir-jpaserver-test-r5/src/test/java/ca/uhn/fhir/jpa/provider/r5/SearchParameterDisabledForQueryingR5Test.java b/hapi-fhir-jpaserver-test-r5/src/test/java/ca/uhn/fhir/jpa/provider/r5/SearchParameterDisabledForQueryingR5Test.java index c0d10885321f..a89469691826 100644 --- a/hapi-fhir-jpaserver-test-r5/src/test/java/ca/uhn/fhir/jpa/provider/r5/SearchParameterDisabledForQueryingR5Test.java +++ b/hapi-fhir-jpaserver-test-r5/src/test/java/ca/uhn/fhir/jpa/provider/r5/SearchParameterDisabledForQueryingR5Test.java @@ -80,6 +80,8 @@ private static SearchParameter createSearchParameter(Boolean theEnabledForSearch sp.setId(code); sp.setName(code); sp.setCode(code); + sp.setUrl("http://localhost/" + sp.getId()); + sp.setDescription("description"); sp.setType(Enumerations.SearchParamType.STRING); sp.setStatus(Enumerations.PublicationStatus.ACTIVE); sp.setExpression(expression); diff --git a/hapi-fhir-jpaserver-test-r5/src/test/java/ca/uhn/fhir/jpa/search/reindex/InstanceReindexServiceImplR5Test.java b/hapi-fhir-jpaserver-test-r5/src/test/java/ca/uhn/fhir/jpa/search/reindex/InstanceReindexServiceImplR5Test.java index a69451365b5b..baa362672509 100644 --- a/hapi-fhir-jpaserver-test-r5/src/test/java/ca/uhn/fhir/jpa/search/reindex/InstanceReindexServiceImplR5Test.java +++ b/hapi-fhir-jpaserver-test-r5/src/test/java/ca/uhn/fhir/jpa/search/reindex/InstanceReindexServiceImplR5Test.java @@ -276,6 +276,9 @@ public void testReindexInstance() { SearchParameter eyeColourSp = new SearchParameter(); eyeColourSp.addBase(Enumerations.VersionIndependentResourceTypesAll.PATIENT); eyeColourSp.setCode("eyecolour"); + eyeColourSp.setName("eyecolour"); + eyeColourSp.setDescription("description"); + eyeColourSp.setUrl("http://localhost/SearchParameter/eyecolour"); eyeColourSp.setType(Enumerations.SearchParamType.STRING); eyeColourSp.setTitle("Eye Colour"); eyeColourSp.setExpression("Patient.extension('http://acme.org/eyecolour')"); @@ -304,6 +307,9 @@ private void createNamesAndGenderSp(boolean theUnique) { sp.setId("SearchParameter/patient-family"); sp.setType(Enumerations.SearchParamType.STRING); sp.setCode("family"); + sp.setName("family"); + sp.setDescription("description"); + sp.setUrl("http://localhost/" + sp.getId()); sp.setExpression("Patient.name.family + '|'"); sp.setStatus(Enumerations.PublicationStatus.ACTIVE); sp.addBase(Enumerations.VersionIndependentResourceTypesAll.PATIENT); @@ -313,6 +319,9 @@ private void createNamesAndGenderSp(boolean theUnique) { sp.setId("SearchParameter/patient-given"); sp.setType(Enumerations.SearchParamType.STRING); sp.setCode("given"); + sp.setName("given"); + sp.setDescription("description"); + sp.setUrl("http://localhost" + sp.getId()); sp.setExpression("Patient.name.given"); sp.setStatus(Enumerations.PublicationStatus.ACTIVE); sp.addBase(Enumerations.VersionIndependentResourceTypesAll.PATIENT); @@ -321,6 +330,10 @@ private void createNamesAndGenderSp(boolean theUnique) { sp = new SearchParameter(); sp.setId("SearchParameter/patient-names-and-gender"); sp.setType(Enumerations.SearchParamType.COMPOSITE); + sp.setName("patient-names-and-gender"); + sp.setCode("patient-names-and-gender"); + sp.setDescription("description"); + sp.setUrl("http://localhost" + sp.getId()); sp.setStatus(Enumerations.PublicationStatus.ACTIVE); sp.addBase(Enumerations.VersionIndependentResourceTypesAll.PATIENT); sp.addComponent() diff --git a/hapi-fhir-jpaserver-test-utilities/src/main/java/ca/uhn/fhir/jpa/test/util/ComboSearchParameterTestHelper.java b/hapi-fhir-jpaserver-test-utilities/src/main/java/ca/uhn/fhir/jpa/test/util/ComboSearchParameterTestHelper.java index 8df6916f28ea..e0bd71eef917 100644 --- a/hapi-fhir-jpaserver-test-utilities/src/main/java/ca/uhn/fhir/jpa/test/util/ComboSearchParameterTestHelper.java +++ b/hapi-fhir-jpaserver-test-utilities/src/main/java/ca/uhn/fhir/jpa/test/util/ComboSearchParameterTestHelper.java @@ -98,6 +98,9 @@ public void createFamilyAndGenderSps(boolean theUnique, ISearchParamCustomizer.. sp.setId("SearchParameter/patient-family"); sp.setType(Enumerations.SearchParamType.STRING); sp.setCode("family"); + sp.setName("family"); + sp.setDescription("description"); + sp.setUrl("http://localhost/" + sp.getId()); sp.setExpression("Patient.name.family"); sp.setStatus(Enumerations.PublicationStatus.ACTIVE); sp.addBase(Enumerations.VersionIndependentResourceTypesAll.PATIENT); @@ -107,6 +110,9 @@ public void createFamilyAndGenderSps(boolean theUnique, ISearchParamCustomizer.. sp.setId("SearchParameter/patient-gender"); sp.setType(Enumerations.SearchParamType.TOKEN); sp.setCode("gender"); + sp.setName("gender"); + sp.setDescription("description"); + sp.setUrl("http://localhost/" + sp.getId()); sp.setExpression("Patient.gender"); sp.setStatus(Enumerations.PublicationStatus.ACTIVE); sp.addBase(Enumerations.VersionIndependentResourceTypesAll.PATIENT); @@ -116,6 +122,10 @@ public void createFamilyAndGenderSps(boolean theUnique, ISearchParamCustomizer.. sp.setId("SearchParameter/patient-family-gender"); sp.setType(Enumerations.SearchParamType.COMPOSITE); sp.setStatus(Enumerations.PublicationStatus.ACTIVE); + sp.setName("patient-family-gender"); + sp.setCode("patient-family-gender"); + sp.setDescription("description"); + sp.setUrl("http://localhost/" + sp.getId()); sp.addBase(Enumerations.VersionIndependentResourceTypesAll.PATIENT); sp.addComponent() .setExpression("Patient") From 8ab343ca12c7d6eb11f2d73996dcb11e4bdf9e98 Mon Sep 17 00:00:00 2001 From: leif stawnyczy Date: Tue, 2 Sep 2025 14:06:23 -0400 Subject: [PATCH 13/36] fixing tests --- .../JpaResourceDaoSearchParameterTest.java | 8 ++ ...FhirResourceDaoR4ComboUniqueParamTest.java | 93 ++++++++++++++++++- .../provider/r4/ResourceProviderR4Test.java | 30 +++++- .../util/ComboSearchParameterTestHelper.java | 12 ++- 4 files changed, 133 insertions(+), 10 deletions(-) diff --git a/hapi-fhir-jpaserver-test-r4/src/test/java/ca/uhn/fhir/jpa/dao/JpaResourceDaoSearchParameterTest.java b/hapi-fhir-jpaserver-test-r4/src/test/java/ca/uhn/fhir/jpa/dao/JpaResourceDaoSearchParameterTest.java index 3c31e3a4faef..cb670a8d856c 100644 --- a/hapi-fhir-jpaserver-test-r4/src/test/java/ca/uhn/fhir/jpa/dao/JpaResourceDaoSearchParameterTest.java +++ b/hapi-fhir-jpaserver-test-r4/src/test/java/ca/uhn/fhir/jpa/dao/JpaResourceDaoSearchParameterTest.java @@ -69,6 +69,10 @@ public void testValidateAllBuiltInSearchParams() { SearchParameter nextSearchParameter = new SearchParameter(); nextSearchParameter.setExpression(nextp.getPath()); + nextSearchParameter.setName(nextp.getName()); + nextSearchParameter.setCode(nextp.getName()); + nextSearchParameter.setDescription(nextp.getDescription()); + nextSearchParameter.setUrl(nextp.getUri()); nextSearchParameter.setStatus(Enumerations.PublicationStatus.ACTIVE); nextSearchParameter.setType(Enumerations.SearchParamType.fromCode(nextp.getParamType().getCode())); nextp.getBase().forEach(nextSearchParameter::addBase); @@ -86,6 +90,10 @@ public void testValidateAllBuiltInSearchParams() { public void testValidateInvalidExpression() { SearchParameter nextSearchParameter = new SearchParameter(); nextSearchParameter.setExpression("Patient.ex[[["); + nextSearchParameter.setName("name"); + nextSearchParameter.setUrl("http://localhost/SearchParameter/a"); + nextSearchParameter.setDescription("description"); + nextSearchParameter.setCode("name"); nextSearchParameter.setStatus(Enumerations.PublicationStatus.ACTIVE); nextSearchParameter.setType(Enumerations.SearchParamType.STRING); nextSearchParameter.addBase("Patient"); diff --git a/hapi-fhir-jpaserver-test-r4/src/test/java/ca/uhn/fhir/jpa/dao/r4/FhirResourceDaoR4ComboUniqueParamTest.java b/hapi-fhir-jpaserver-test-r4/src/test/java/ca/uhn/fhir/jpa/dao/r4/FhirResourceDaoR4ComboUniqueParamTest.java index 89f1f90da48a..f60a98d26d36 100644 --- a/hapi-fhir-jpaserver-test-r4/src/test/java/ca/uhn/fhir/jpa/dao/r4/FhirResourceDaoR4ComboUniqueParamTest.java +++ b/hapi-fhir-jpaserver-test-r4/src/test/java/ca/uhn/fhir/jpa/dao/r4/FhirResourceDaoR4ComboUniqueParamTest.java @@ -81,6 +81,9 @@ private void createUniqueGenderFamilyComboSp() { sp.setId("SearchParameter/patient-gender"); sp.setType(Enumerations.SearchParamType.TOKEN); sp.setCode("gender"); + sp.setName("gender"); + sp.setDescription("description"); + sp.setUrl("http://localhost/" + sp.getId()); sp.setExpression("Patient.gender"); sp.setStatus(PublicationStatus.ACTIVE); sp.addBase("Patient"); @@ -90,6 +93,9 @@ private void createUniqueGenderFamilyComboSp() { sp.setId("SearchParameter/patient-family"); sp.setType(Enumerations.SearchParamType.STRING); sp.setCode("family"); + sp.setName("family"); + sp.setDescription("description"); + sp.setUrl("http://localhost/" + sp.getId()); sp.setExpression("Patient.name.family"); sp.setStatus(Enumerations.PublicationStatus.ACTIVE); sp.addBase("Patient"); @@ -100,6 +106,10 @@ private void createUniqueGenderFamilyComboSp() { sp.setType(Enumerations.SearchParamType.COMPOSITE); sp.setStatus(PublicationStatus.ACTIVE); sp.addBase("Patient"); + sp.setUrl("http://localhost/" + sp.getId()); + sp.setCode("patient-gender-family"); + sp.setName("patient-gender-family"); + sp.setDescription("description"); sp.addComponent() .setExpression("Patient") .setDefinition("SearchParameter/patient-gender"); @@ -120,6 +130,9 @@ private void createUniqueIndexCoverageBeneficiary() { SearchParameter sp = new SearchParameter(); sp.setId("SearchParameter/coverage-beneficiary"); sp.setCode("beneficiary"); + sp.setName("beneficiary"); + sp.setDescription("Description"); + sp.setUrl("http://localhost/" + sp.getId()); sp.setExpression("Coverage.beneficiary"); sp.setType(Enumerations.SearchParamType.REFERENCE); sp.setStatus(PublicationStatus.ACTIVE); @@ -129,6 +142,9 @@ private void createUniqueIndexCoverageBeneficiary() { sp = new SearchParameter(); sp.setId("SearchParameter/coverage-identifier"); sp.setCode("identifier"); + sp.setName("identifier"); + sp.setDescription("description"); + sp.setUrl("http://localhost/" + sp.getId()); sp.setExpression("Coverage.identifier"); sp.setType(Enumerations.SearchParamType.TOKEN); sp.setStatus(PublicationStatus.ACTIVE); @@ -138,6 +154,9 @@ private void createUniqueIndexCoverageBeneficiary() { sp = new SearchParameter(); sp.setId("SearchParameter/coverage-beneficiary-identifier"); sp.setCode("coverage-beneficiary-identifier"); + sp.setName("coverage-beneficiary-identifier"); + sp.setUrl("http://localhost/" + sp.getId()); + sp.setDescription("description"); sp.setExpression("Coverage.beneficiary"); sp.setType(Enumerations.SearchParamType.COMPOSITE); sp.setStatus(PublicationStatus.ACTIVE); @@ -160,6 +179,9 @@ private void createUniqueIndexObservationSubject() { SearchParameter sp = new SearchParameter(); sp.setId("SearchParameter/observation-subject"); sp.setCode("observation-subject"); + sp.setName(sp.getCode()); + sp.setUrl("http://localhost/" + sp.getId()); + sp.setDescription("description"); sp.setExpression("Observation.subject"); sp.setType(Enumerations.SearchParamType.REFERENCE); sp.setStatus(PublicationStatus.ACTIVE); @@ -169,6 +191,9 @@ private void createUniqueIndexObservationSubject() { sp = new SearchParameter(); sp.setId("SearchParameter/observation-uniq-subject"); sp.setCode("observation-uniq-subject"); + sp.setName(sp.getCode()); + sp.setUrl("http://localhost/" + sp.getId()); + sp.setDescription("description"); sp.setExpression("Observation.subject"); sp.setType(Enumerations.SearchParamType.COMPOSITE); sp.setStatus(PublicationStatus.ACTIVE); @@ -188,6 +213,9 @@ private void createUniqueIndexPatientIdentifier() { SearchParameter sp = new SearchParameter(); sp.setId("SearchParameter/patient-identifier"); sp.setCode("identifier"); + sp.setName("identifier"); + sp.setDescription("description"); + sp.setUrl("http://localhost/" + sp.getId()); sp.setExpression("Patient.identifier"); sp.setType(Enumerations.SearchParamType.TOKEN); sp.setStatus(PublicationStatus.ACTIVE); @@ -197,6 +225,9 @@ private void createUniqueIndexPatientIdentifier() { sp = new SearchParameter(); sp.setId("SearchParameter/patient-uniq-identifier"); sp.setCode("patient-uniq-identifier"); + sp.setName("patient-uniq-identifier"); + sp.setDescription("description"); + sp.setUrl("http//localhost/" + sp.getId()); sp.setExpression("Patient"); sp.setType(Enumerations.SearchParamType.COMPOSITE); sp.setStatus(PublicationStatus.ACTIVE); @@ -216,6 +247,9 @@ private void createUniqueIndexPatientIdentifierCount1() { SearchParameter sp = new SearchParameter(); sp.setId("SearchParameter/patient-identifier"); sp.setCode("first-identifier"); + sp.setName("first-identifier"); + sp.setDescription("description"); + sp.setUrl("http://localhost/" + sp.getId()); sp.setExpression("Patient.identifier.first()"); sp.setType(Enumerations.SearchParamType.TOKEN); sp.setStatus(PublicationStatus.ACTIVE); @@ -225,6 +259,9 @@ private void createUniqueIndexPatientIdentifierCount1() { sp = new SearchParameter(); sp.setId("SearchParameter/patient-uniq-identifier"); sp.setCode("patient-uniq-identifier"); + sp.setName(sp.getCode()); + sp.setUrl("http://localhost/" + sp.getId()); + sp.setDescription("description"); sp.setExpression("Patient.identifier"); sp.setType(Enumerations.SearchParamType.COMPOSITE); sp.setStatus(PublicationStatus.ACTIVE); @@ -244,6 +281,9 @@ private void createUniqueNameAndManagingOrganizationSps() { sp.setId("SearchParameter/patient-name"); sp.setType(Enumerations.SearchParamType.STRING); sp.setCode("name"); + sp.setName(sp.getCode()); + sp.setUrl("http://localhost/" + sp.getId()); + sp.setDescription("description"); sp.setExpression("Patient.name"); sp.setStatus(PublicationStatus.ACTIVE); sp.addBase("Patient"); @@ -253,6 +293,9 @@ private void createUniqueNameAndManagingOrganizationSps() { sp.setId("SearchParameter/patient-organization"); sp.setType(Enumerations.SearchParamType.REFERENCE); sp.setCode("organization"); + sp.setName(sp.getCode()); + sp.setUrl("http://localhost/" + sp.getId()); + sp.setDescription("description"); sp.setExpression("Patient.managingOrganization"); sp.setStatus(PublicationStatus.ACTIVE); sp.addBase("Patient"); @@ -263,6 +306,10 @@ private void createUniqueNameAndManagingOrganizationSps() { sp.setType(Enumerations.SearchParamType.COMPOSITE); sp.setStatus(PublicationStatus.ACTIVE); sp.addBase("Patient"); + sp.setCode("patient-name-organization"); + sp.setName(sp.getCode()); + sp.setUrl("http://localhost/" + sp.getId()); + sp.setDescription("description"); sp.addComponent() .setExpression("Patient") .setDefinition("SearchParameter/patient-name"); @@ -282,6 +329,9 @@ private void createUniqueObservationDateCode() { sp.setId("SearchParameter/obs-effective"); sp.setType(Enumerations.SearchParamType.DATE); sp.setCode("date"); + sp.setName(sp.getCode()); + sp.setUrl("http://localhost/" + sp.getId()); + sp.setDescription("description"); sp.setExpression("Observation.effective"); sp.setStatus(PublicationStatus.ACTIVE); sp.addBase("Observation"); @@ -291,6 +341,9 @@ private void createUniqueObservationDateCode() { sp.setId("SearchParameter/obs-code"); sp.setType(Enumerations.SearchParamType.TOKEN); sp.setCode("code"); + sp.setName(sp.getCode()); + sp.setUrl("http://localhost/" + sp.getId()); + sp.setDescription("description"); sp.setExpression("Observation.code"); sp.setStatus(PublicationStatus.ACTIVE); sp.addBase("Observation"); @@ -301,6 +354,10 @@ private void createUniqueObservationDateCode() { sp.setType(Enumerations.SearchParamType.COMPOSITE); sp.setStatus(PublicationStatus.ACTIVE); sp.addBase("Observation"); + sp.setCode("observation-date-code"); + sp.setName(sp.getCode()); + sp.setUrl("http://localhost/" + sp.getId()); + sp.setDescription("description"); sp.setExpression("Observation.code"); sp.addComponent() .setExpression("Observation") @@ -321,6 +378,9 @@ private void createUniqueObservationSubjectDateCode() { sp.setId("SearchParameter/obs-subject"); sp.setType(Enumerations.SearchParamType.REFERENCE); sp.setCode("subject"); + sp.setName("subject"); + sp.setDescription("description"); + sp.setUrl("http://localhost/" + sp.getId()); sp.setExpression("Observation.subject"); sp.setStatus(PublicationStatus.ACTIVE); sp.addBase("Observation"); @@ -331,6 +391,9 @@ private void createUniqueObservationSubjectDateCode() { sp.setId("SearchParameter/obs-effective"); sp.setType(Enumerations.SearchParamType.DATE); sp.setCode("date"); + sp.setName("date"); + sp.setUrl("http://localhost/" + sp.getId()); + sp.setDescription("description"); sp.setExpression("Observation.effective"); sp.setStatus(PublicationStatus.ACTIVE); sp.addBase("Observation"); @@ -340,6 +403,9 @@ private void createUniqueObservationSubjectDateCode() { sp.setId("SearchParameter/obs-code"); sp.setType(Enumerations.SearchParamType.TOKEN); sp.setCode("code"); + sp.setName("code"); + sp.setDescription("description"); + sp.setUrl("http://localhost/" + sp.getId()); sp.setExpression("Observation.code"); sp.setStatus(PublicationStatus.ACTIVE); sp.addBase("Observation"); @@ -349,6 +415,10 @@ private void createUniqueObservationSubjectDateCode() { sp.setId("SearchParameter/observation-subject-date-code"); sp.setType(Enumerations.SearchParamType.COMPOSITE); sp.setStatus(PublicationStatus.ACTIVE); + sp.setName("observation-subject-date-code"); + sp.setCode("observation-subject-date-code"); + sp.setDescription("description"); + sp.setUrl("http://localhost/" + sp.getId()); sp.addBase("Observation"); sp.setExpression("Observation.code"); sp.addComponent() @@ -373,6 +443,9 @@ public void testCreateUniqueSpWithNoComponent() { SearchParameter sp = new SearchParameter(); sp.setId("SearchParameter/patient-uniq-identifier"); sp.setCode("patient-uniq-identifier"); + sp.setName("patient-uniq-identifier"); + sp.setDescription("description"); + sp.setUrl("http://localhost/" + sp.getId()); sp.setExpression("Patient"); sp.setType(Enumerations.SearchParamType.COMPOSITE); sp.setStatus(PublicationStatus.ACTIVE); @@ -394,6 +467,9 @@ public void testCreateUniqueSpWithNoComponentDefinition() { SearchParameter sp = new SearchParameter(); sp.setId("SearchParameter/patient-uniq-identifier"); sp.setCode("patient-uniq-identifier"); + sp.setName("patient-uniq-identifier"); + sp.setDescription("description"); + sp.setUrl("http://localhost/" + sp.getId()); sp.setExpression("Patient"); sp.setType(Enumerations.SearchParamType.COMPOSITE); sp.setStatus(PublicationStatus.ACTIVE); @@ -599,7 +675,9 @@ public void testDoubleMatchingOnAnd_Search2() { sp = new SearchParameter(); sp.setStatus(PublicationStatus.ACTIVE); sp.setCode("patient"); - sp.setName("patient"); + sp.setName(sp.getCode()); + sp.setUrl("http://localhost/SearchParameter/patient"); + sp.setDescription("description"); sp.setType(Enumerations.SearchParamType.REFERENCE); sp.addBase(ServiceRequest.class.getSimpleName()); sp.setExpression("ServiceRequest.subject.where(resolve() is Patient)"); @@ -608,7 +686,9 @@ public void testDoubleMatchingOnAnd_Search2() { sp = new SearchParameter(); sp.setStatus(PublicationStatus.ACTIVE); sp.setCode("performer"); - sp.setName("performer"); + sp.setName(sp.getCode()); + sp.setUrl("http://localhost/SearchParameter/performer"); + sp.setDescription("description"); sp.setType(Enumerations.SearchParamType.REFERENCE); sp.addBase(ServiceRequest.class.getSimpleName()); sp.setExpression("ServiceRequest.performer"); @@ -617,7 +697,9 @@ public void testDoubleMatchingOnAnd_Search2() { sp = new SearchParameter(); sp.setStatus(PublicationStatus.ACTIVE); sp.setCode("identifier"); - sp.setName("identifier"); + sp.setName(sp.getCode()); + sp.setUrl("http://localhost/SearchParameter/identifier"); + sp.setDescription("description"); sp.setType(Enumerations.SearchParamType.TOKEN); sp.addBase(ServiceRequest.class.getSimpleName()); sp.setExpression("ServiceRequest.identifier"); @@ -626,6 +708,9 @@ public void testDoubleMatchingOnAnd_Search2() { sp = new SearchParameter(); sp.setId("SearchParameter/patient-uniq-identifier"); sp.setCode("procreq-patient-performer-identifier"); + sp.setName(sp.getCode()); + sp.setUrl("http://localhost/" + sp.getId()); + sp.setDescription("description"); sp.setExpression("ServiceRequest.patient"); sp.setType(Enumerations.SearchParamType.COMPOSITE); sp.setStatus(PublicationStatus.ACTIVE); @@ -1339,10 +1424,8 @@ public void testSearchUsingUniqueComposite() { logCapturedMessages(); assertThat(myMessages.toString()).doesNotContain("unique index"); myMessages.clear(); - } - @Test public void testUniqueValuesAreIndexed_DateAndToken() { createBirthdateAndGenderSps(true); diff --git a/hapi-fhir-jpaserver-test-r4/src/test/java/ca/uhn/fhir/jpa/provider/r4/ResourceProviderR4Test.java b/hapi-fhir-jpaserver-test-r4/src/test/java/ca/uhn/fhir/jpa/provider/r4/ResourceProviderR4Test.java index c780b8ee67fa..fa48f4a2137b 100644 --- a/hapi-fhir-jpaserver-test-r4/src/test/java/ca/uhn/fhir/jpa/provider/r4/ResourceProviderR4Test.java +++ b/hapi-fhir-jpaserver-test-r4/src/test/java/ca/uhn/fhir/jpa/provider/r4/ResourceProviderR4Test.java @@ -297,6 +297,8 @@ public void testSearchParameterValidation() { sp.setUrl("http://example.com/name"); sp.setId("name"); sp.setCode("name"); + sp.setName("name"); + sp.setDescription("description"); sp.setType(Enumerations.SearchParamType.STRING); sp.setStatus(Enumerations.PublicationStatus.RETIRED); sp.addBase("Patient"); @@ -318,6 +320,9 @@ public void testParameterWithNoValueThrowsError_InvalidChainOnCustomSearch() thr SearchParameter searchParameter = new SearchParameter(); searchParameter.addBase("BodyStructure").addBase("Procedure"); searchParameter.setCode("focalAccess"); + searchParameter.setName("focalAccess"); + searchParameter.setUrl("http://localhost/SearchParameter/focalAccess"); + searchParameter.setDescription("description"); searchParameter.setType(Enumerations.SearchParamType.REFERENCE); searchParameter.setExpression("Procedure.extension('Procedure#focalAccess')"); searchParameter.setXpathUsage(SearchParameter.XPathUsageType.NORMAL); @@ -341,6 +346,8 @@ public void createResourceSearchParameter_withExpressionMetaSecurity_succeeds() searchParameter.setStatus(Enumerations.PublicationStatus.ACTIVE); searchParameter.setName("Security"); searchParameter.setCode("_security"); + searchParameter.setDescription("description"); + searchParameter.setUrl("http://localhost/SearchParameter/resource-security"); searchParameter.addBase("Patient").addBase("Account"); searchParameter.setType(Enumerations.SearchParamType.TOKEN); searchParameter.setExpression("meta.security"); @@ -357,6 +364,9 @@ public void createSearchParameter_with2Expressions_succeeds() { searchParameter.setStatus(Enumerations.PublicationStatus.ACTIVE); searchParameter.setCode("myGender"); + searchParameter.setName("myGender"); + searchParameter.setDescription("description"); + searchParameter.setUrl("http://localhost/SearchParameter/myGender"); searchParameter.addBase("Patient").addBase("Person"); searchParameter.setType(Enumerations.SearchParamType.TOKEN); searchParameter.setExpression("Patient.gender|Person.gender"); @@ -371,6 +381,9 @@ public void testParameterWithNoValueThrowsError_InvalidRootParam() throws IOExce SearchParameter searchParameter = new SearchParameter(); searchParameter.addBase("BodyStructure").addBase("Procedure"); searchParameter.setCode("focalAccess"); + searchParameter.setName("focalAccess"); + searchParameter.setUrl("http://localhost/SearchParameter/SearchParameter/focalAccess"); + searchParameter.setDescription("description"); searchParameter.setType(Enumerations.SearchParamType.REFERENCE); searchParameter.setExpression("Procedure.extension('Procedure#focalAccess')"); searchParameter.setXpathUsage(SearchParameter.XPathUsageType.NORMAL); @@ -396,6 +409,9 @@ public void testSearchResourcesOnProfile_whenProfileIsMissing_returnsResourcesWi SearchParameter searchParameter = new SearchParameter(); searchParameter.addBase("Organization"); searchParameter.setCode("_profile"); + searchParameter.setName("_profile"); + searchParameter.setDescription("description"); + searchParameter.setUrl("http://localhost/SearchParameter/_profile"); searchParameter.setType(Enumerations.SearchParamType.URI); searchParameter.setExpression("meta.profile"); searchParameter.setStatus(Enumerations.PublicationStatus.ACTIVE); @@ -839,13 +855,14 @@ private int createPatientsWithReferences(Map theRefCountToReso @Test public void testSearchWithDeepChain() throws IOException { - SearchParameter sp = new SearchParameter(); sp.addBase("Patient"); sp.setStatus(Enumerations.PublicationStatus.ACTIVE); sp.setType(Enumerations.SearchParamType.REFERENCE); sp.setCode("extpatorg"); sp.setName("extpatorg"); + sp.setDescription("Description"); + sp.setUrl("http://localhost/SearchParameter/extpatorg"); sp.setExpression("Patient.extension('http://patext').value.as(Reference)"); myClient.create().resource(sp).execute(); @@ -855,6 +872,8 @@ public void testSearchWithDeepChain() throws IOException { sp.setType(Enumerations.SearchParamType.REFERENCE); sp.setCode("extorgorg"); sp.setName("extorgorg"); + sp.setDescription("description"); + sp.setUrl("http://localhost/SearchParameter/extorgorg"); sp.setExpression("Organization.extension('http://orgext').value.as(Reference)"); myClient.create().resource(sp).execute(); @@ -7783,7 +7802,7 @@ public void testMissingReferenceClientParameterOnIndexedContainedResources(Missi @ParameterizedTest @MethodSource("provideParameters") - public void testMissingURLParameter(MissingSearchTestParameters theParams) { + public void testMissingSPSearch(MissingSearchTestParameters theParams) { runTest(theParams, hasField -> { String methodName = new Exception().getStackTrace()[0].getMethodName(); @@ -7791,15 +7810,18 @@ public void testMissingURLParameter(MissingSearchTestParameters theParams) { SearchParameter sp = new SearchParameter(); sp.addBase("MolecularSequence"); sp.setCode(methodName); + sp.setName(methodName); + sp.setDescription("description"); + sp.setUrl("http://localhost/SearchParameter/" + methodName); sp.setType(Enumerations.SearchParamType.NUMBER); sp.setExpression("MolecularSequence.variant-end"); sp.setXpathUsage(SearchParameter.XPathUsageType.NORMAL); sp.setStatus(Enumerations.PublicationStatus.ACTIVE); if (hasField) { - sp.setUrl("http://example.com"); + sp.setPublisher("smile"); } return sp; - }, isMissing -> doSearch(SearchParameter.class, SearchParameter.URL.isMissing(isMissing))); + }, isMissing -> doSearch(SearchParameter.class, SearchParameter.PUBLISHER.isMissing(isMissing))); } @ParameterizedTest diff --git a/hapi-fhir-jpaserver-test-utilities/src/main/java/ca/uhn/fhir/jpa/test/util/ComboSearchParameterTestHelper.java b/hapi-fhir-jpaserver-test-utilities/src/main/java/ca/uhn/fhir/jpa/test/util/ComboSearchParameterTestHelper.java index e0bd71eef917..53469fa1e8a2 100644 --- a/hapi-fhir-jpaserver-test-utilities/src/main/java/ca/uhn/fhir/jpa/test/util/ComboSearchParameterTestHelper.java +++ b/hapi-fhir-jpaserver-test-utilities/src/main/java/ca/uhn/fhir/jpa/test/util/ComboSearchParameterTestHelper.java @@ -56,6 +56,9 @@ public void createBirthdateAndGenderSps(boolean theUnique, ISearchParamCustomize sp.setId("SearchParameter/patient-gender"); sp.setType(Enumerations.SearchParamType.TOKEN); sp.setCode("gender"); + sp.setName("gender"); + sp.setDescription("Description"); + sp.setUrl("http://localhost/" + sp.getId()); sp.setExpression("Patient.gender"); sp.setStatus(Enumerations.PublicationStatus.ACTIVE); sp.addBase(Enumerations.VersionIndependentResourceTypesAll.PATIENT); @@ -65,6 +68,9 @@ public void createBirthdateAndGenderSps(boolean theUnique, ISearchParamCustomize sp.setId("SearchParameter/patient-birthdate"); sp.setType(Enumerations.SearchParamType.DATE); sp.setCode("birthdate"); + sp.setName("birthdate"); + sp.setUrl("http://localhost/" + sp.getId()); + sp.setDescription("description"); sp.setExpression("Patient.birthDate"); sp.setStatus(Enumerations.PublicationStatus.ACTIVE); sp.addBase(Enumerations.VersionIndependentResourceTypesAll.PATIENT); @@ -72,6 +78,10 @@ public void createBirthdateAndGenderSps(boolean theUnique, ISearchParamCustomize sp = new SearchParameter(); sp.setId("SearchParameter/patient-gender-birthdate"); + sp.setName("patient-gender-birthdate"); + sp.setCode("patient-gender-birthdate"); + sp.setDescription("description"); + sp.setUrl("http://localhost/" + sp.getId()); sp.setType(Enumerations.SearchParamType.COMPOSITE); sp.setStatus(Enumerations.PublicationStatus.ACTIVE); sp.addBase(Enumerations.VersionIndependentResourceTypesAll.PATIENT); @@ -111,8 +121,8 @@ public void createFamilyAndGenderSps(boolean theUnique, ISearchParamCustomizer.. sp.setType(Enumerations.SearchParamType.TOKEN); sp.setCode("gender"); sp.setName("gender"); - sp.setDescription("description"); sp.setUrl("http://localhost/" + sp.getId()); + sp.setDescription("description"); sp.setExpression("Patient.gender"); sp.setStatus(Enumerations.PublicationStatus.ACTIVE); sp.addBase(Enumerations.VersionIndependentResourceTypesAll.PATIENT); From ba0c5e9c16050b8f11e8cfec860b1a5493cb6c8f Mon Sep 17 00:00:00 2001 From: leif stawnyczy Date: Tue, 2 Sep 2025 15:10:41 -0400 Subject: [PATCH 14/36] adding a default that isn't a valid validation to preserve existing behaviour --- .../SearchParameterDaoValidatorTest.java | 87 ++++++++++++++++++- .../SearchParameterDaoValidator.java | 54 ++++++++++-- 2 files changed, 135 insertions(+), 6 deletions(-) diff --git a/hapi-fhir-jpaserver-test-utilities/src/test/java/ca/uhn/fhir/jpa/dao/validation/SearchParameterDaoValidatorTest.java b/hapi-fhir-jpaserver-test-utilities/src/test/java/ca/uhn/fhir/jpa/dao/validation/SearchParameterDaoValidatorTest.java index 93c24fef7011..f2b26564bcab 100644 --- a/hapi-fhir-jpaserver-test-utilities/src/test/java/ca/uhn/fhir/jpa/dao/validation/SearchParameterDaoValidatorTest.java +++ b/hapi-fhir-jpaserver-test-utilities/src/test/java/ca/uhn/fhir/jpa/dao/validation/SearchParameterDaoValidatorTest.java @@ -21,6 +21,7 @@ import org.junit.jupiter.api.extension.ExtendWith; import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.EnumSource; import org.junit.jupiter.params.provider.MethodSource; import org.junit.jupiter.params.provider.ValueSource; import org.mockito.InjectMocks; @@ -138,6 +139,90 @@ public void validate_noOverridingAllowed_throws() { } } + @ParameterizedTest + @EnumSource(SearchParameterDaoValidator.RequisiteFields.class) + public void validate_noOmittedFields_throwsIfNotThere(SearchParameterDaoValidator.RequisiteFields theFieldToOmit) { + // setup + SearchParameter sp = new SearchParameter(); + sp.setName("name"); + sp.setCode("code"); + sp.setDescription("description"); + sp.setUrl("http://localhost/SearchParameter/name"); + sp.addBase(PATIENT); + sp.setType(TOKEN); + sp.setStatus(ACTIVE); + sp.setExpression("Patient.extension('http://foo')"); + sp.addTarget(PATIENT); + + switch (theFieldToOmit) { + case URL -> { + sp.setUrl(null); + } + case CODE -> { + sp.setCode(null); + } + case NAME -> { + sp.setName(null); + } + case DESCRIPTION -> { + sp.setDescription(null); + } + } + + // test + try { + SearchParameterDaoValidator.SPValidatorOptions options = new SearchParameterDaoValidator.SPValidatorOptions(); + for (SearchParameterDaoValidator.RequisiteFields f : SearchParameterDaoValidator.RequisiteFields.values()) { + if (f != theFieldToOmit) { + options.addOmittedField(f); + } + } + mySvc.validate(sp, options); + } catch (UnprocessableEntityException ex) { + assertTrue( + ex.getLocalizedMessage().contains("SearchParameter." + theFieldToOmit.getFieldName() + " is missing") + ); + } + } + + @ParameterizedTest + @EnumSource(SearchParameterDaoValidator.RequisiteFields.class) + public void validate_withAllowedOmitFields_works(SearchParameterDaoValidator.RequisiteFields theFieldToOmit) { + SearchParameter sp = new SearchParameter(); + sp.setName("name"); + sp.setCode("code"); + sp.setDescription("description"); + sp.setUrl("http://localhost/SearchParameter/name"); + sp.addBase(PATIENT); + sp.setType(TOKEN); + sp.setStatus(ACTIVE); + sp.setExpression("Patient.extension('http://foo')"); + sp.addTarget(PATIENT); + + switch (theFieldToOmit) { + case URL -> { + sp.setUrl(null); + } + case CODE -> { + sp.setCode(null); + } + case NAME -> { + sp.setName(null); + } + case DESCRIPTION -> { + sp.setDescription(null); + } + } + + // test + SearchParameterDaoValidator.SPValidatorOptions options = new SearchParameterDaoValidator.SPValidatorOptions(); + options.addOmittedField(theFieldToOmit); + mySvc.validate(sp, options); + + // validate - we should get here + assertTrue(true); + } + private void runOverrideBuiltInSPTest(SearchParameterDaoValidator.SPValidatorOptions theOptions) { SearchParameter sp = new SearchParameter(); sp.setName("name"); @@ -221,7 +306,7 @@ public void validate_missingRequisiteField_throws(String theFieldToOmit) { // test try { - mySvc.validate(sp); + mySvc.validate(sp, new SearchParameterDaoValidator.SPValidatorOptions()); fail("SP missing requisite field " + theFieldToOmit + " still passes validation when it shouldn't."); } catch (UnprocessableEntityException ex) { assertTrue(ex.getMessage().contains( diff --git a/hapi-fhir-storage/src/main/java/ca/uhn/fhir/jpa/dao/validation/SearchParameterDaoValidator.java b/hapi-fhir-storage/src/main/java/ca/uhn/fhir/jpa/dao/validation/SearchParameterDaoValidator.java index 8efc753f1464..a3819cc6e812 100644 --- a/hapi-fhir-storage/src/main/java/ca/uhn/fhir/jpa/dao/validation/SearchParameterDaoValidator.java +++ b/hapi-fhir-storage/src/main/java/ca/uhn/fhir/jpa/dao/validation/SearchParameterDaoValidator.java @@ -34,7 +34,10 @@ import org.hl7.fhir.r5.model.Enumerations; import org.hl7.fhir.r5.model.SearchParameter; +import java.util.ArrayList; import java.util.Collection; +import java.util.HashSet; +import java.util.List; import java.util.Objects; import java.util.Set; import java.util.regex.Pattern; @@ -55,14 +58,47 @@ */ public class SearchParameterDaoValidator { + /** + * These are fields that are required on a SearchParameter resource. + * But due to backwards compatibility, some checks were not ensuring these + * were populated. So they can be optionally omitted. + */ + public enum RequisiteFields { + NAME("name"), + DESCRIPTION("description"), + URL("url"), + CODE("code"); + + private final String myFieldName; + + RequisiteFields(String theFieldName) { + myFieldName = theFieldName; + } + + public String getFieldName() { + return myFieldName; + } + } + public static class SPValidatorOptions { public static SPValidatorOptions defaultOptions() { - return new SPValidatorOptions(); + SPValidatorOptions options = new SPValidatorOptions(); + for (RequisiteFields f : RequisiteFields.values()) { + options.addOmittedField(f); + } + return options; } private boolean myAllowOverriding = false; + /** + * Collection of fields that should be omitted during validation. + * These are fields that are, otherwise, required on earchParameter + * resources (ie, have a cardinality >= 1) + */ + private final Set myOmittedFields = new HashSet<>(); + public boolean allowOverriding() { return myAllowOverriding; } @@ -71,6 +107,14 @@ public SPValidatorOptions setAllowOverriding(boolean theAllowOverriding) { myAllowOverriding = theAllowOverriding; return this; } + + public void addOmittedField(RequisiteFields theField) { + myOmittedFields.add(theField); + } + + public Set getOmittedFields() { + return myOmittedFields; + } } private static final Pattern REGEX_SP_EXPRESSION_HAS_PATH = Pattern.compile("[( ]*([A-Z][a-zA-Z]+\\.)?[a-z].*"); @@ -129,19 +173,19 @@ public void validate(SearchParameter searchParameter, SPValidatorOptions theOpti // requisite fields: base, name, description, status, url, code - if (isBlank(searchParameter.getName())) { + if (!theOptions.getOmittedFields().contains(RequisiteFields.NAME) && isBlank(searchParameter.getName())) { throw new UnprocessableEntityException(Msg.code(2798) + String.format(SP_FIELD_IS_MISSING, "name")); } - if (isBlank(searchParameter.getDescription())) { + if (!theOptions.getOmittedFields().contains(RequisiteFields.DESCRIPTION) && isBlank(searchParameter.getDescription())) { throw new UnprocessableEntityException(Msg.code(2799) + String.format(SP_FIELD_IS_MISSING, "description")); } - if (isBlank(searchParameter.getUrl())) { + if (!theOptions.getOmittedFields().contains(RequisiteFields.URL) && isBlank(searchParameter.getUrl())) { throw new UnprocessableEntityException(Msg.code(2800) + String.format(SP_FIELD_IS_MISSING, "url")); } - if (isBlank(searchParameter.getCode())) { + if (!theOptions.getOmittedFields().contains(RequisiteFields.CODE) && isBlank(searchParameter.getCode())) { throw new UnprocessableEntityException(Msg.code(2801) + String.format(SP_FIELD_IS_MISSING, "code")); } From 0e11dc897d0b3fc6d1ad28d4fc448790c7bc549a Mon Sep 17 00:00:00 2001 From: leif stawnyczy Date: Tue, 2 Sep 2025 15:11:07 -0400 Subject: [PATCH 15/36] spotless --- .../fhir/jpa/dao/validation/SearchParameterDaoValidator.java | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/hapi-fhir-storage/src/main/java/ca/uhn/fhir/jpa/dao/validation/SearchParameterDaoValidator.java b/hapi-fhir-storage/src/main/java/ca/uhn/fhir/jpa/dao/validation/SearchParameterDaoValidator.java index a3819cc6e812..783ebab14fa4 100644 --- a/hapi-fhir-storage/src/main/java/ca/uhn/fhir/jpa/dao/validation/SearchParameterDaoValidator.java +++ b/hapi-fhir-storage/src/main/java/ca/uhn/fhir/jpa/dao/validation/SearchParameterDaoValidator.java @@ -34,10 +34,8 @@ import org.hl7.fhir.r5.model.Enumerations; import org.hl7.fhir.r5.model.SearchParameter; -import java.util.ArrayList; import java.util.Collection; import java.util.HashSet; -import java.util.List; import java.util.Objects; import java.util.Set; import java.util.regex.Pattern; @@ -177,7 +175,8 @@ public void validate(SearchParameter searchParameter, SPValidatorOptions theOpti throw new UnprocessableEntityException(Msg.code(2798) + String.format(SP_FIELD_IS_MISSING, "name")); } - if (!theOptions.getOmittedFields().contains(RequisiteFields.DESCRIPTION) && isBlank(searchParameter.getDescription())) { + if (!theOptions.getOmittedFields().contains(RequisiteFields.DESCRIPTION) + && isBlank(searchParameter.getDescription())) { throw new UnprocessableEntityException(Msg.code(2799) + String.format(SP_FIELD_IS_MISSING, "description")); } From 9d2c65a405c178e1274ec1d0e02ac0d65c10f5db Mon Sep 17 00:00:00 2001 From: leif stawnyczy Date: Wed, 3 Sep 2025 16:32:39 -0400 Subject: [PATCH 16/36] allow overwriting builtin sp --- .../jpa/searchparam/registry/RuntimeSearchParamCache.java | 6 +++++- .../jpa/searchparam/registry/SearchParamRegistryImpl.java | 3 ++- 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/hapi-fhir-jpaserver-searchparam/src/main/java/ca/uhn/fhir/jpa/searchparam/registry/RuntimeSearchParamCache.java b/hapi-fhir-jpaserver-searchparam/src/main/java/ca/uhn/fhir/jpa/searchparam/registry/RuntimeSearchParamCache.java index 3c17d9e1feb0..a70ab2736d96 100644 --- a/hapi-fhir-jpaserver-searchparam/src/main/java/ca/uhn/fhir/jpa/searchparam/registry/RuntimeSearchParamCache.java +++ b/hapi-fhir-jpaserver-searchparam/src/main/java/ca/uhn/fhir/jpa/searchparam/registry/RuntimeSearchParamCache.java @@ -35,6 +35,10 @@ public class RuntimeSearchParamCache extends ReadOnlySearchParamCache { protected RuntimeSearchParamCache() {} public void add(String theResourceName, String theName, RuntimeSearchParam theSearchParam) { + add(theResourceName, theName, theSearchParam, false); + } + + public void add(String theResourceName, String theName, RuntimeSearchParam theSearchParam, boolean theAllowOverrideExisting) { ResourceSearchParams resourceSearchParams = getSearchParamMap(theResourceName); resourceSearchParams.put(theName, theSearchParam); String uri = theSearchParam.getUri(); @@ -44,7 +48,7 @@ public void add(String theResourceName, String theName, RuntimeSearchParam theSe // This is expected, since the same SP can span multiple resource types // so it may get added more than once by this method ourLog.trace("Search param was previously registered for url: {}", uri); - } else if (existingForUrl != null) { + } else if (existingForUrl != null && !theAllowOverrideExisting) { ourLog.debug("Multiple search parameters have URL: {}", uri); } else { myUrlToParam.put(uri, theSearchParam); diff --git a/hapi-fhir-jpaserver-searchparam/src/main/java/ca/uhn/fhir/jpa/searchparam/registry/SearchParamRegistryImpl.java b/hapi-fhir-jpaserver-searchparam/src/main/java/ca/uhn/fhir/jpa/searchparam/registry/SearchParamRegistryImpl.java index 3f024870ee0e..4a3a6d3d2d70 100644 --- a/hapi-fhir-jpaserver-searchparam/src/main/java/ca/uhn/fhir/jpa/searchparam/registry/SearchParamRegistryImpl.java +++ b/hapi-fhir-jpaserver-searchparam/src/main/java/ca/uhn/fhir/jpa/searchparam/registry/SearchParamRegistryImpl.java @@ -274,7 +274,8 @@ private void initializeActiveSearchParams(Collection theJpaSearch // add the local cache search params myLocalSPCache.getSearchParamStream().forEach(sp -> { for (String rt : sp.getBase()) { - searchParams.add(rt, sp.getName(), sp); + // we will allow local cache SPs to override existing ones + searchParams.add(rt, sp.getName(), sp, true); } }); From e9d5ac603e79f92828dda6a88227cd64d1144fb5 Mon Sep 17 00:00:00 2001 From: leif stawnyczy Date: Wed, 3 Sep 2025 16:33:10 -0400 Subject: [PATCH 17/36] spotless --- .../jpa/searchparam/registry/RuntimeSearchParamCache.java | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/hapi-fhir-jpaserver-searchparam/src/main/java/ca/uhn/fhir/jpa/searchparam/registry/RuntimeSearchParamCache.java b/hapi-fhir-jpaserver-searchparam/src/main/java/ca/uhn/fhir/jpa/searchparam/registry/RuntimeSearchParamCache.java index a70ab2736d96..e8ad05ffa488 100644 --- a/hapi-fhir-jpaserver-searchparam/src/main/java/ca/uhn/fhir/jpa/searchparam/registry/RuntimeSearchParamCache.java +++ b/hapi-fhir-jpaserver-searchparam/src/main/java/ca/uhn/fhir/jpa/searchparam/registry/RuntimeSearchParamCache.java @@ -38,7 +38,11 @@ public void add(String theResourceName, String theName, RuntimeSearchParam theSe add(theResourceName, theName, theSearchParam, false); } - public void add(String theResourceName, String theName, RuntimeSearchParam theSearchParam, boolean theAllowOverrideExisting) { + public void add( + String theResourceName, + String theName, + RuntimeSearchParam theSearchParam, + boolean theAllowOverrideExisting) { ResourceSearchParams resourceSearchParams = getSearchParamMap(theResourceName); resourceSearchParams.put(theName, theSearchParam); String uri = theSearchParam.getUri(); From 8167799cf11395ea68596d49b1d764785ce239bd Mon Sep 17 00:00:00 2001 From: leif stawnyczy Date: Thu, 4 Sep 2025 16:00:55 -0400 Subject: [PATCH 18/36] fxiing a bug --- .../hl7/fhir/instance/model/api/IBase.java | 2 +- .../registry/RuntimeSearchParamCache.java | 3 +- .../registry/SearchParamRegistryImpl.java | 11 +-- .../registry/RuntimeSearchParamCacheTest.java | 69 +++++++++++++++++++ 4 files changed, 79 insertions(+), 6 deletions(-) create mode 100644 hapi-fhir-jpaserver-searchparam/src/test/java/ca/uhn/fhir/jpa/searchparam/registry/RuntimeSearchParamCacheTest.java diff --git a/hapi-fhir-base/src/main/java/org/hl7/fhir/instance/model/api/IBase.java b/hapi-fhir-base/src/main/java/org/hl7/fhir/instance/model/api/IBase.java index 105e4265e61e..80df14aa2daf 100644 --- a/hapi-fhir-base/src/main/java/org/hl7/fhir/instance/model/api/IBase.java +++ b/hapi-fhir-base/src/main/java/org/hl7/fhir/instance/model/api/IBase.java @@ -63,7 +63,7 @@ default String fhirType() { } /** - * Retrieves any user suplied data in this element + * Retrieves any user supplied data in this element */ Object getUserData(String theName); diff --git a/hapi-fhir-jpaserver-searchparam/src/main/java/ca/uhn/fhir/jpa/searchparam/registry/RuntimeSearchParamCache.java b/hapi-fhir-jpaserver-searchparam/src/main/java/ca/uhn/fhir/jpa/searchparam/registry/RuntimeSearchParamCache.java index e8ad05ffa488..2cf02450c077 100644 --- a/hapi-fhir-jpaserver-searchparam/src/main/java/ca/uhn/fhir/jpa/searchparam/registry/RuntimeSearchParamCache.java +++ b/hapi-fhir-jpaserver-searchparam/src/main/java/ca/uhn/fhir/jpa/searchparam/registry/RuntimeSearchParamCache.java @@ -64,11 +64,12 @@ public void add( } } - public void remove(String theResourceName, String theName) { + public void remove(String theResourceName, String theName, String theURL) { if (!myResourceNameToSpNameToSp.containsKey(theResourceName)) { return; } myResourceNameToSpNameToSp.get(theResourceName).remove(theName); + myUrlToParam.remove(theURL); } public void clear() { diff --git a/hapi-fhir-jpaserver-searchparam/src/main/java/ca/uhn/fhir/jpa/searchparam/registry/SearchParamRegistryImpl.java b/hapi-fhir-jpaserver-searchparam/src/main/java/ca/uhn/fhir/jpa/searchparam/registry/SearchParamRegistryImpl.java index 4a3a6d3d2d70..060e0a8d7031 100644 --- a/hapi-fhir-jpaserver-searchparam/src/main/java/ca/uhn/fhir/jpa/searchparam/registry/SearchParamRegistryImpl.java +++ b/hapi-fhir-jpaserver-searchparam/src/main/java/ca/uhn/fhir/jpa/searchparam/registry/SearchParamRegistryImpl.java @@ -268,8 +268,6 @@ private void initializeActiveSearchParams(Collection theJpaSearch ReadOnlySearchParamCache builtInSearchParams = getBuiltInSearchParams(); RuntimeSearchParamCache searchParams = RuntimeSearchParamCache.fromReadOnlySearchParamCache(builtInSearchParams); - long overriddenCount = overrideBuiltinSearchParamsWithActiveJpaSearchParams(searchParams, theJpaSearchParams); - ourLog.trace("Have overridden {} built-in search parameters", overriddenCount); // add the local cache search params myLocalSPCache.getSearchParamStream().forEach(sp -> { @@ -279,6 +277,9 @@ private void initializeActiveSearchParams(Collection theJpaSearch } }); + long overriddenCount = overrideBuiltinSearchParamsWithActiveJpaSearchParams(searchParams, theJpaSearchParams); + ourLog.trace("Have overridden {} built-in search parameters", overriddenCount); + // Auto-register: _language if (myStorageSettings.isLanguageSearchParameterEnabled()) { registerImplicitSearchParam( @@ -325,7 +326,9 @@ private void initializeActiveSearchParams(Collection theJpaSearch private void unregisterImplicitSearchParam(RuntimeSearchParamCache theSearchParams, String theParamName) { for (String resourceType : theSearchParams.getResourceNameKeys()) { - theSearchParams.remove(resourceType, theParamName); + RuntimeSearchParam rsp = theSearchParams.get(resourceType, theParamName); + String uri = rsp == null ? null : rsp.getUri(); + theSearchParams.remove(resourceType, theParamName, uri); } } @@ -466,7 +469,7 @@ private long overrideSearchParam(RuntimeSearchParamCache theSearchParams, IBaseR } else { Set expandedBases = expandBaseList(existingParam.getBase()); for (String base : expandedBases) { - theSearchParams.remove(base, existingParam.getName()); + theSearchParams.remove(base, existingParam.getName(), existingParam.getUri()); } } } diff --git a/hapi-fhir-jpaserver-searchparam/src/test/java/ca/uhn/fhir/jpa/searchparam/registry/RuntimeSearchParamCacheTest.java b/hapi-fhir-jpaserver-searchparam/src/test/java/ca/uhn/fhir/jpa/searchparam/registry/RuntimeSearchParamCacheTest.java new file mode 100644 index 000000000000..8d1dec63b91e --- /dev/null +++ b/hapi-fhir-jpaserver-searchparam/src/test/java/ca/uhn/fhir/jpa/searchparam/registry/RuntimeSearchParamCacheTest.java @@ -0,0 +1,69 @@ +package ca.uhn.fhir.jpa.searchparam.registry; + +import ca.uhn.fhir.context.RuntimeSearchParam; +import ca.uhn.fhir.rest.api.RestSearchParameterTypeEnum; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import java.util.Set; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertNull; + +public class RuntimeSearchParamCacheTest { + + private RuntimeSearchParamCache myCache; + + @BeforeEach + public void before() { + myCache = new RuntimeSearchParamCache(); + } + + + @Test + public void remove_anExistingSP_removesAllInstances() { + // setup + RuntimeSearchParam rtsp = new RuntimeSearchParam( + null, + "http://example.com", + "name", + "description", + "Patient.name", + RestSearchParameterTypeEnum.STRING, + null, + Set.of("Patient"), + RuntimeSearchParam.RuntimeSearchParamStatusEnum.ACTIVE, + Set.of("Patient") + ); + + // set our sp + myCache.add("Patient", rtsp.getName(), rtsp); + + // make sure it's there + { + // by name + RuntimeSearchParam param = myCache.get("Patient", "name"); + assertNotNull(param); + assertEquals(rtsp.getName(), param.getName()); + assertEquals(rtsp.getUri(), param.getUri()); + } + { + // by uri + RuntimeSearchParam param = myCache.getByUrl(rtsp.getUri()); + assertNotNull(param); + assertEquals(rtsp.getName(), param.getName()); + assertEquals(rtsp.getUri(), param.getUri()); + } + + // test + // first remove + myCache.remove("Patient", rtsp.getName(), rtsp.getUri()); + + // verify + // by name + assertNull(myCache.get("Patient", rtsp.getName())); + // by url + assertNull(myCache.getByUrl(rtsp.getUri())); + } +} From 1fb663976bd00023b9d2a646379bb967f437bd12 Mon Sep 17 00:00:00 2001 From: leif stawnyczy Date: Fri, 5 Sep 2025 15:37:27 -0400 Subject: [PATCH 19/36] init --- .../ca/uhn/fhir/jpa/config/JpaConfig.java | 15 -- .../fhir/jpa/config/PackageLoaderConfig.java | 20 ++ .../jpa/packages/NpmJpaValidationSupport.java | 66 ++++- .../packages/NpmPackageMetadataLiteJson.java | 39 +++ .../fhir/jpa/packages/util/PackageUtils.java | 35 +++ .../jpa/model/entity/NpmPackageEntity.java | 4 + .../packages/NpmJpaValidationSupportIT.java | 194 ++++++++++++++ .../implementationguide/GZipCreatorUtil.java | 61 +++++ .../ImplementationGuideCreator.java | 246 ++++++++++++++++++ 9 files changed, 664 insertions(+), 16 deletions(-) create mode 100644 hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/packages/NpmPackageMetadataLiteJson.java create mode 100644 hapi-fhir-jpaserver-test-r4/src/test/java/ca/uhn/fhir/jpa/packages/NpmJpaValidationSupportIT.java create mode 100644 hapi-fhir-test-utilities/src/main/java/ca/uhn/fhir/implementationguide/GZipCreatorUtil.java create mode 100644 hapi-fhir-test-utilities/src/main/java/ca/uhn/fhir/implementationguide/ImplementationGuideCreator.java diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/config/JpaConfig.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/config/JpaConfig.java index dbe5b3c1fcb3..b188c2f8bcf9 100644 --- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/config/JpaConfig.java +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/config/JpaConfig.java @@ -411,21 +411,6 @@ public IResourceLinkResolver daoResourceLinkResolver() { return new JpaDaoResourceLinkResolver(); } - @Bean(name = PackageUtils.LOADER_WITH_CACHE) - public IHapiPackageCacheManager packageCacheManager() { - return new JpaPackageCache(); - } - - @Bean - public NpmJpaValidationSupport npmJpaValidationSupport() { - return new NpmJpaValidationSupport(); - } - - @Bean - public ValidationSettings validationSettings() { - return new ValidationSettings(); - } - @Bean public ISearchCacheSvc searchCacheSvc() { return new DatabaseSearchCacheSvcImpl(); diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/config/PackageLoaderConfig.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/config/PackageLoaderConfig.java index 5a0ee359b6c0..b875aa92a2d1 100644 --- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/config/PackageLoaderConfig.java +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/config/PackageLoaderConfig.java @@ -20,8 +20,13 @@ package ca.uhn.fhir.jpa.config; import ca.uhn.fhir.context.FhirContext; +import ca.uhn.fhir.jpa.packages.IHapiPackageCacheManager; +import ca.uhn.fhir.jpa.packages.JpaPackageCache; +import ca.uhn.fhir.jpa.packages.NpmJpaValidationSupport; import ca.uhn.fhir.jpa.packages.loader.PackageLoaderSvc; import ca.uhn.fhir.jpa.packages.loader.PackageResourceParsingSvc; +import ca.uhn.fhir.jpa.packages.util.PackageUtils; +import ca.uhn.fhir.jpa.validation.ValidationSettings; import org.hl7.fhir.utilities.npm.PackageServer; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; @@ -42,4 +47,19 @@ public PackageLoaderSvc packageLoaderSvc() { public PackageResourceParsingSvc resourceParsingSvc(FhirContext theContext) { return new PackageResourceParsingSvc(theContext); } + + @Bean(name = PackageUtils.LOADER_WITH_CACHE) + public IHapiPackageCacheManager packageCacheManager() { + return new JpaPackageCache(); + } + + @Bean + public NpmJpaValidationSupport npmJpaValidationSupport() { + return new NpmJpaValidationSupport(); + } + + @Bean + public ValidationSettings validationSettings() { + return new ValidationSettings(); + } } diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/packages/NpmJpaValidationSupport.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/packages/NpmJpaValidationSupport.java index a6a761f0a794..592866cafc96 100644 --- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/packages/NpmJpaValidationSupport.java +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/packages/NpmJpaValidationSupport.java @@ -22,11 +22,22 @@ import ca.uhn.fhir.context.FhirContext; import ca.uhn.fhir.context.FhirVersionEnum; import ca.uhn.fhir.context.support.IValidationSupport; +import ca.uhn.fhir.jpa.dao.data.INpmPackageVersionDao; +import ca.uhn.fhir.jpa.model.entity.NpmPackageVersionEntity; +import ca.uhn.fhir.jpa.packages.loader.PackageResourceParsingSvc; +import ca.uhn.fhir.jpa.packages.util.PackageUtils; +import ca.uhn.fhir.model.api.PagingIterator; +import ca.uhn.fhir.rest.annotation.Transaction; import jakarta.annotation.Nullable; import org.hl7.fhir.instance.model.api.IBaseResource; +import org.hl7.fhir.utilities.npm.NpmPackage; import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.data.domain.Pageable; +import java.io.IOException; +import java.util.ArrayList; import java.util.List; +import java.util.function.Consumer; public class NpmJpaValidationSupport implements IValidationSupport { @@ -34,7 +45,13 @@ public class NpmJpaValidationSupport implements IValidationSupport { private FhirContext myFhirContext; @Autowired - private IHapiPackageCacheManager myHapiPackageCacheManager; + private IHapiPackageCacheManager myHapiPackageCacheManager; // this is JpaPackageCache + + @Autowired + private INpmPackageVersionDao myPackageVersionDao; + + @Autowired + private PackageResourceParsingSvc myPackageResourceParsingSvc; @Override public FhirContext getFhirContext() { @@ -77,4 +94,51 @@ public List fetchAllStructureDefinitions() { FhirVersionEnum fhirVersion = myFhirContext.getVersion().getVersion(); return (List) myHapiPackageCacheManager.loadPackageAssetsByType(fhirVersion, "StructureDefinition"); } + + @Override + @Transaction + public List fetchAllSearchParameters() { + /* + * default findall is probably fine + * there's probably not a lot and even if there are + * we don't reorder them + */ + PagingIterator iterator = new PagingIterator<>(1000, new PagingIterator.PageFetcher() { + @Override + public void fetchNextPage(int thePageIndex, int theBatchSize, Consumer theConsumer) { + myPackageVersionDao.findAll(Pageable.ofSize(theBatchSize).withPage(thePageIndex)) + .stream().forEach(theConsumer); + } + }); + + // do we want to cache this? + List sps = new ArrayList<>(); + while (iterator.hasNext()) { + NpmPackageVersionEntity entity = iterator.next(); + getAllSP(entity, sps); + } + return sps; + } + + private void getAllSP(NpmPackageVersionEntity theNpmPackageEntity, List theSps) { + if (theNpmPackageEntity.getFhirVersion() != myFhirContext.getVersion().getVersion()) { + // not the same fhir version as this class is dependent on, + // so we cannot create these objects + return; + } + + NpmPackage pkg; + try { + pkg = myHapiPackageCacheManager.loadPackage(theNpmPackageEntity.getPackageId(), theNpmPackageEntity.getVersionId()); + } catch (IOException ex) { + // TODO - better handling + throw new RuntimeException(ex); + } + + List pkgSps = myPackageResourceParsingSvc.parseResourcesOfType("SearchParameter", pkg); + for (IBaseResource sp : pkgSps) { + PackageUtils.addPackageMetadata(sp, pkg); + theSps.add((T) sp); + } + } } diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/packages/NpmPackageMetadataLiteJson.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/packages/NpmPackageMetadataLiteJson.java new file mode 100644 index 000000000000..35f67a2af81d --- /dev/null +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/packages/NpmPackageMetadataLiteJson.java @@ -0,0 +1,39 @@ +package ca.uhn.fhir.jpa.packages; + +import com.fasterxml.jackson.annotation.JsonAutoDetect; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import io.swagger.v3.oas.annotations.media.Schema; + +@Schema(description = "Contains the bare minimum of NpmPackage metadata.") +@JsonInclude(JsonInclude.Include.NON_NULL) +@JsonAutoDetect( + creatorVisibility = JsonAutoDetect.Visibility.NONE, + fieldVisibility = JsonAutoDetect.Visibility.NONE, + getterVisibility = JsonAutoDetect.Visibility.NONE, + isGetterVisibility = JsonAutoDetect.Visibility.NONE, + setterVisibility = JsonAutoDetect.Visibility.NONE) +public class NpmPackageMetadataLiteJson { + + @JsonProperty("name") + private String myName; + + @JsonProperty("version") + private String myVersion; + + public String getName() { + return myName; + } + + public void setName(String theName) { + myName = theName; + } + + public String getVersion() { + return myVersion; + } + + public void setVersion(String theVersion) { + myVersion = theVersion; + } +} diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/packages/util/PackageUtils.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/packages/util/PackageUtils.java index 855e15b18c15..efa6845bd6c0 100644 --- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/packages/util/PackageUtils.java +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/packages/util/PackageUtils.java @@ -19,11 +19,17 @@ */ package ca.uhn.fhir.jpa.packages.util; +import ca.uhn.fhir.jpa.packages.NpmPackageMetadataLiteJson; +import ca.uhn.fhir.util.JsonUtil; import com.google.common.collect.Lists; +import org.hl7.fhir.instance.model.api.IBaseResource; +import org.hl7.fhir.utilities.npm.NpmPackage; import java.util.Collections; import java.util.List; +import static org.apache.commons.lang3.StringUtils.isBlank; + public class PackageUtils { public static final String LOADER_WITH_CACHE = "loaderWithCache"; @@ -39,4 +45,33 @@ public class PackageUtils { "ConceptMap", "SearchParameter", "Subscription")); + + public static final String PKG_METADATA_KEY = "PKG_METADATA"; + + /** + * Adds package metadata to the resource for identification purposes downstream. + * @param theResource the resource + * @param thePkg the npm package (IG) it is from + */ + public static void addPackageMetadata(IBaseResource theResource, NpmPackage thePkg) { + NpmPackageMetadataLiteJson metadata = new NpmPackageMetadataLiteJson(); + metadata.setName(thePkg.name()); + metadata.setVersion(thePkg.version()); + + theResource.setUserData(PKG_METADATA_KEY, JsonUtil.serialize(metadata)); + } + + /** + * Retrieves whatever npm pkg metadata is on this resource, or null if none is found. + * @param theResource resource that originated from an npm package + * @return the metadata about the pkg (or null if not available) + */ + public static NpmPackageMetadataLiteJson getPackageMetadata(IBaseResource theResource) { + String metadataJson = (String) theResource.getUserData(PKG_METADATA_KEY); + if (isBlank(metadataJson)) { + // metadata was not on this resource + return null; + } + return JsonUtil.deserialize(metadataJson, NpmPackageMetadataLiteJson.class); + } } diff --git a/hapi-fhir-jpaserver-model/src/main/java/ca/uhn/fhir/jpa/model/entity/NpmPackageEntity.java b/hapi-fhir-jpaserver-model/src/main/java/ca/uhn/fhir/jpa/model/entity/NpmPackageEntity.java index be1b053ea7d8..938e2ebd63e9 100644 --- a/hapi-fhir-jpaserver-model/src/main/java/ca/uhn/fhir/jpa/model/entity/NpmPackageEntity.java +++ b/hapi-fhir-jpaserver-model/src/main/java/ca/uhn/fhir/jpa/model/entity/NpmPackageEntity.java @@ -111,4 +111,8 @@ public String getCurrentVersionId() { public void setCurrentVersionId(String theCurrentVersionId) { myCurrentVersionId = theCurrentVersionId; } + + public List getVersions() { + return myVersions; + } } diff --git a/hapi-fhir-jpaserver-test-r4/src/test/java/ca/uhn/fhir/jpa/packages/NpmJpaValidationSupportIT.java b/hapi-fhir-jpaserver-test-r4/src/test/java/ca/uhn/fhir/jpa/packages/NpmJpaValidationSupportIT.java new file mode 100644 index 000000000000..f4847114fea2 --- /dev/null +++ b/hapi-fhir-jpaserver-test-r4/src/test/java/ca/uhn/fhir/jpa/packages/NpmJpaValidationSupportIT.java @@ -0,0 +1,194 @@ +package ca.uhn.fhir.jpa.packages; + +import ca.uhn.fhir.context.FhirContext; +import ca.uhn.fhir.implementationguide.ImplementationGuideCreator; +import ca.uhn.fhir.jpa.packages.util.PackageUtils; +import ca.uhn.fhir.jpa.test.BaseJpaR4Test; +import org.hl7.fhir.instance.model.api.IBaseResource; +import org.hl7.fhir.r4.model.Enumerations; +import org.hl7.fhir.r4.model.SearchParameter; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; + +import java.io.File; +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertTrue; + +public class NpmJpaValidationSupportIT extends BaseJpaR4Test { + @Autowired + private NpmJpaValidationSupport mySvc; + + @Autowired + private IPackageInstallerSvc myPackageInstallerSvc; + + @Test + public void fetchAllSearchParameters_withStoredIG_returnsSPsFromIG() throws IOException { + String name = "ig-test-dir"; + Path tempDirPath = createTempDir(name); + + ImplementationGuideCreator igCreator = new ImplementationGuideCreator(myFhirContext); + igCreator.setDirectory(tempDirPath); + + SearchParameter generated = generateSP(); + igCreator.addSPToIG(generated); + + PackageInstallationSpec spec = createAndInstallPackageSpec(igCreator); + + // test + List sps = mySvc.fetchAllSearchParameters(); + + // verify + assertNotNull(sps); + assertEquals(1, sps.size()); + IBaseResource r = sps.get(0); + assertTrue(r instanceof SearchParameter); + SearchParameter sp = (SearchParameter)r; + assertEquals(generated.getName(), sp.getName()); + assertEquals(generated.getUrl(), sp.getUrl()); + NpmPackageMetadataLiteJson metadata = PackageUtils.getPackageMetadata(sp); + assertNotNull(metadata); + assertEquals(spec.getName(), metadata.getName()); + assertEquals(spec.getVersion(), metadata.getVersion()); + } + + @Test + public void fetchAllSearchParameters_multipleVersion_returnsAllSPs() throws IOException { + String name = "ig-test-dir"; + String[] versions = new String[] { "1.1.1", "1.2.2" }; + + SearchParameter generated = generateSP(); + for (String version : versions) { + Path tempDirPath = createTempDir(name + version); + + ImplementationGuideCreator igCreator = new ImplementationGuideCreator(myFhirContext, name, version); + igCreator.setDirectory(tempDirPath); + igCreator.addSPToIG(generated); + + PackageInstallationSpec spec = createAndInstallPackageSpec(igCreator); + } + + // test + List sps = mySvc.fetchAllSearchParameters(); + + // verify + assertNotNull(sps); + assertEquals(2, sps.size()); + + Map map = new HashMap<>(); + for (IBaseResource r : sps) { + assertTrue(r instanceof SearchParameter); + SearchParameter sp = (SearchParameter) r; + + assertEquals(generated.getUrl(), sp.getUrl()); + assertEquals(generated.getName(), sp.getName()); + NpmPackageMetadataLiteJson metadataLiteJson = PackageUtils.getPackageMetadata(sp); + assertNotNull(metadataLiteJson); + + map.put(metadataLiteJson.getVersion(), metadataLiteJson); + } + assertEquals(versions.length, map.size()); + for (String version : versions) { + assertTrue(map.containsKey(version)); + NpmPackageMetadataLiteJson metadataLiteJson = map.get(version); + assertEquals(name, metadataLiteJson.getName()); + } + } + + @Test + public void fetchAllSearchParameters_multipleFhirVersions_returnsOnlySPsMatchingFhirVersion() throws IOException { + FhirContext dstu3 = FhirContext.forDstu3(); + Path path1 = createTempDir("pkg1"); + Path path2 = createTempDir("pkg2"); + + ImplementationGuideCreator igFhir4 = new ImplementationGuideCreator(myFhirContext, "r4.test.ig", "1.1.1"); + igFhir4.setDirectory(path1); + ImplementationGuideCreator igFhir3dstu = new ImplementationGuideCreator(dstu3, "dstu3.test.ig", "1.1.1"); + igFhir3dstu.setDirectory(path2); + + { + SearchParameter spr4 = new SearchParameter(); + spr4.setStatus(Enumerations.PublicationStatus.ACTIVE); + spr4.setType(Enumerations.SearchParamType.STRING); + spr4.setExpression("Patient.name.given"); + spr4.setName("helloWorld"); + spr4.setCode("helloWorld"); + spr4.setDescription("description"); + spr4.addBase("Patient"); + spr4.setUrl("http://localhost/ig-test-dir/spr4"); + igFhir4.addSPToIG(spr4); + } + { + org.hl7.fhir.dstu3.model.SearchParameter spdstu3 = new org.hl7.fhir.dstu3.model.SearchParameter(); + spdstu3.setStatus(org.hl7.fhir.dstu3.model.Enumerations.PublicationStatus.ACTIVE); + spdstu3.setType(org.hl7.fhir.dstu3.model.Enumerations.SearchParamType.STRING); + spdstu3.setExpression("Patient.name.given"); + spdstu3.setName("helloWorld"); + spdstu3.setCode("helloWorld"); + spdstu3.setDescription("description"); + spdstu3.addBase("Patient"); + spdstu3.setUrl("http://localhost/ig-test-dir/spdstu3"); + igFhir3dstu.addSPToIG(spdstu3); + } + + createAndInstallPackageSpec(igFhir4); + createAndInstallPackageSpec(igFhir3dstu); + + // test + List sps = mySvc.fetchAllSearchParameters(); + assertNotNull(sps); + assertEquals(1, sps.size()); + assertTrue(sps.stream() + .anyMatch(sp -> sp instanceof SearchParameter)); + assertFalse(sps.stream() + .anyMatch(sp -> sp instanceof org.hl7.fhir.dstu3.model.SearchParameter)); + } + + private SearchParameter generateSP() { + SearchParameter sp = new SearchParameter(); + sp.setUrl("http://localhost/ig-test-dir/sp1"); + sp.setCode("helloWorld"); + sp.setName(sp.getCode()); + sp.setDescription("description"); + sp.setType(Enumerations.SearchParamType.STRING); + sp.addBase("Patient"); + sp.setExpression("Patient.name.given"); + sp.setStatus(Enumerations.PublicationStatus.ACTIVE); + return sp; + } + + private PackageInstallationSpec createAndInstallPackageSpec(ImplementationGuideCreator theIgCreator) throws IOException { + // create a source directory + Path outputFileName = theIgCreator.createTestIG(); + + // add some NPM package + PackageInstallationSpec spec = new PackageInstallationSpec() + .setName(theIgCreator.getPackageName()) + .setVersion(theIgCreator.getPackageVersion()) + .setInstallMode(PackageInstallationSpec.InstallModeEnum.STORE_ONLY) + .setPackageContents(Files.readAllBytes(outputFileName)) + ; + PackageInstallOutcomeJson outcome = myPackageInstallerSvc.install(spec); + + assertNotNull(outcome); + assertTrue(outcome.getMessage() + .stream().anyMatch(m -> m.contains("Successfully added package")), + String.join(", ", outcome.getMessage())); + return spec; + } + + private Path createTempDir(String theName) throws IOException { + Path tempDirPath = Files.createTempDirectory(theName); + File tempFile = new File(tempDirPath.toString()); + tempFile.deleteOnExit(); + return tempDirPath; + } +} diff --git a/hapi-fhir-test-utilities/src/main/java/ca/uhn/fhir/implementationguide/GZipCreatorUtil.java b/hapi-fhir-test-utilities/src/main/java/ca/uhn/fhir/implementationguide/GZipCreatorUtil.java new file mode 100644 index 000000000000..b138dab4c6dc --- /dev/null +++ b/hapi-fhir-test-utilities/src/main/java/ca/uhn/fhir/implementationguide/GZipCreatorUtil.java @@ -0,0 +1,61 @@ +package ca.uhn.fhir.implementationguide; + +import org.apache.commons.compress.archivers.tar.TarArchiveEntry; +import org.apache.commons.compress.archivers.tar.TarArchiveOutputStream; +import org.apache.commons.compress.compressors.gzip.GzipCompressorOutputStream; + +import java.io.BufferedOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.nio.file.DirectoryStream; +import java.nio.file.Files; +import java.nio.file.Path; + +public class GZipCreatorUtil { + + /** + * Create a tarball of the provided input and saves it to the provided output. + * + * @param theSource - the input path to the directory containing all source files to be zipped + * @param theOutput - the output path to the gzip file. + */ + public static void createTarGz(Path theSource, Path theOutput) throws IOException { + try (OutputStream fos = Files.newOutputStream(theOutput); + BufferedOutputStream bos = new BufferedOutputStream(fos); + GzipCompressorOutputStream gzos = new GzipCompressorOutputStream(bos); + TarArchiveOutputStream taos = new TarArchiveOutputStream(gzos)) { + addFilesToTarGz(theSource, "", taos); + } + } + + private static void addFilesToTarGz(Path thePath, + String theParent, + TarArchiveOutputStream theTarballOutputStream) + throws IOException { + String entryName = theParent + thePath.getFileName().toString(); + TarArchiveEntry entry = new TarArchiveEntry(thePath.toFile(), entryName); + theTarballOutputStream.putArchiveEntry(entry); + + if (Files.isRegularFile(thePath)) { + // add file + try (InputStream fis = Files.newInputStream(thePath)) { + byte[] buffer = new byte[1024]; + int len; + while ((len = fis.read(buffer)) > 0) { + theTarballOutputStream.write(buffer, 0, len); + } + } + theTarballOutputStream.closeArchiveEntry(); + } else { + theTarballOutputStream.closeArchiveEntry(); + // walk directory + try (DirectoryStream stream = Files.newDirectoryStream(thePath)) { + for (Path child : stream) { + addFilesToTarGz(child, entryName + "/", theTarballOutputStream); + } + } + } + } + +} diff --git a/hapi-fhir-test-utilities/src/main/java/ca/uhn/fhir/implementationguide/ImplementationGuideCreator.java b/hapi-fhir-test-utilities/src/main/java/ca/uhn/fhir/implementationguide/ImplementationGuideCreator.java new file mode 100644 index 000000000000..88b376d1619a --- /dev/null +++ b/hapi-fhir-test-utilities/src/main/java/ca/uhn/fhir/implementationguide/ImplementationGuideCreator.java @@ -0,0 +1,246 @@ +package ca.uhn.fhir.implementationguide; + +import ca.uhn.fhir.context.FhirContext; +import ca.uhn.fhir.parser.IParser; +import ca.uhn.fhir.util.FhirTerser; +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.SerializationFeature; +import jakarta.annotation.Nonnull; +import org.hl7.fhir.instance.model.api.IBase; +import org.hl7.fhir.instance.model.api.IBaseResource; +import org.intellij.lang.annotations.Language; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.io.ByteArrayInputStream; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import java.util.Optional; + +import static org.apache.commons.lang3.StringUtils.isNotBlank; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertTrue; + +/** + * A helper class to allow creation of Implementation Guides with custom values for + * use in tests. + */ +public class ImplementationGuideCreator { + + private static final Logger ourLog = LoggerFactory.getLogger(ImplementationGuideCreator.class); + + @Language("JSON") + private static final String PACKAGE_JSON_BASE = """ + { + "name": "test.fhir.ca.com", + "version": "1.2.3", + "tools-version": 3, + "type": "fhir.ig", + "date": "20200831134427", + "license": "not-open-source", + "canonical": "http://test-ig.com/fhir/us/providerdataexchange", + "url": "file://C:\\\\dev\\\\test-exchange\\\\fsh\\\\build\\\\output", + "title": "Test Implementation Guide", + "description": "Test Implementation Guide", + "fhirVersions": [ + "4.0.1" + ], + "dependencies": { + }, + "author": "SmileCDR", + "maintainers": [ + { + "name": "Smile", + "email": "smilecdr@smiledigitalhealth.com", + "url": "https://www.smilecdr.com" + } + ], + "directories": { + "lib": "package", + "example": "example" + } + } + """; + + private Path myDir; + private final FhirContext myFhirContext; + + private final FhirTerser myTerser; + private final IParser myParser; + + private final String myPackageJson; + + private final String myPackageName; + private final String myPackageVersion; + + private final List mySPToInclude = new ArrayList<>(); + + /** + * If set true, lenient IG creator will forego all "assertions" in creation. + * This can be useful for testing failure cases where we are constructing + * IGs that we know are broken. + */ + private boolean myLenientSetting; + + public ImplementationGuideCreator(@Nonnull FhirContext theFhirContext) throws JsonProcessingException { + this(theFhirContext, "test.fhir.ca.com", "1.2.3"); + } + + public ImplementationGuideCreator(@Nonnull FhirContext theFhirContext, String thePackageName, String thePackageVersion) throws JsonProcessingException { + this(theFhirContext, theFhirContext.getVersion().getVersion().getFhirVersionString(), thePackageName, thePackageVersion); + } + + /** + * Constructor + * @param theFhirContext - FhirContext to use + * @param theFhirVersion - fhir version to use (provided to allow setting a custom value different form the FhirContext) + * @param theName - name to set in package.json's name field + * @param theVersion - version to set in package.json's version field + */ + @SuppressWarnings("unchecked") + public ImplementationGuideCreator( + @Nonnull FhirContext theFhirContext, + String theFhirVersion, + String theName, + String theVersion + ) throws JsonProcessingException { + myFhirContext = theFhirContext; + myTerser = myFhirContext.newTerser(); + myParser = myFhirContext.newJsonParser(); + myPackageName = theName; + myPackageVersion = theVersion; + + ObjectMapper mapper = new ObjectMapper(); + mapper.enable(SerializationFeature.INDENT_OUTPUT); + + Map mapJson = mapper.readValue(PACKAGE_JSON_BASE, Map.class); + + // update provided values + List versions = (List) mapJson.get("fhirVersions"); + versions.clear(); + versions.add(theFhirVersion); + mapJson.replace("name", myPackageName); + mapJson.replace("version", myPackageVersion); + + myPackageJson = mapper.writerWithDefaultPrettyPrinter() + .writeValueAsString(mapJson); + + ourLog.info(myPackageJson); + } + + /** + * Sets the directory where files will be created. + * Should be a temp dir for tests. + */ + public ImplementationGuideCreator setDirectory(Path thePath) { + myDir = thePath; + return this; + } + + /** + * If set true, this creator will forego all validation on construction + * (except for directory existence, since files need to be created somewhere) + * and create the IG as requested, even if the included resources are invalid + * or broken in some way. + * + * By default this is set to false. + */ + public ImplementationGuideCreator setLenient(boolean theLenientSetting) { + myLenientSetting = theLenientSetting; + return this; + } + + public String getPackageName() { + return myPackageName; + } + + public String getPackageVersion() { + return myPackageVersion; + } + + /** + * Adds a SearchParameter to the ImplementationGuide. + * Not to be used for other resource types. + */ + public void addSPToIG(IBaseResource theSP) { + for (String reqdField : new String[] {"name", "status", "url", "code", "expression", "type"}) { + Optional fieldOp = myTerser.getSinglePrimitiveValue(theSP, reqdField); + if (!myLenientSetting) { + assertTrue(fieldOp.isPresent(), String.format("%s is a required field for IG SearchParameters", reqdField)); + } + } + List base = myTerser.getValues(theSP, "base"); + if (!myLenientSetting) { + assertTrue(base != null && !base.isEmpty(), "SP.base is a required field (should be the same as targets)."); + } + + mySPToInclude.add(theSP); + } + + private void verifyDir() { + String msg = "Directory must be set first."; + + assertNotNull(myDir, msg); + assertTrue(isNotBlank(myDir.toString()), msg); + } + + /** + * Creates an the IG from all the provided SearchParameters, + * zips them up, and provides the path to the newly created gzip file. + */ + public Path createTestIG() throws IOException { + verifyDir(); + + Path sourceDir = Files.createDirectory(Path.of(myDir.toString(), "package")); + + // add the package.json + addFileToDir(myPackageJson, "package.json", sourceDir); + + // add search parameters + int index = 0; + for (IBaseResource sp : mySPToInclude) { + // iterating over SPs and include them + Optional nameOp = myTerser.getSinglePrimitiveValue(sp, "name"); + + String name = null; + if (!myLenientSetting) { + assertTrue(nameOp.isPresent()); + } else if (nameOp.isPresent()) { + name = nameOp.get(); + } else { + name = "sp_" + index; + } + + addFileToDir(myParser.encodeResourceToString(sp), name + ".json", sourceDir); + index++; + } + + // we can add other resources here (not req'd for now) + + Path outputFileName = Files.createFile(Path.of(myDir.toString(), myPackageName + ".gz.tar")); + GZipCreatorUtil.createTarGz(sourceDir, outputFileName); + return outputFileName; + } + + private void addFileToDir(String theContent, String theFileName, Path theOutputPath) throws IOException { + byte[] bytes = new byte[1024]; + int length = 0; + + try (FileOutputStream outputStream = new FileOutputStream(theOutputPath.toString() + "/" + theFileName)) { + try (InputStream stream = new ByteArrayInputStream(theContent.getBytes(StandardCharsets.UTF_8))) { + while ((length = stream.read(bytes)) >= 0) { + outputStream.write(bytes, 0, length); + } + } + } + } + +} From a979b9027d09d4b3b4a1783dd066d01de56db4e0 Mon Sep 17 00:00:00 2001 From: leif stawnyczy Date: Fri, 5 Sep 2025 16:28:29 -0400 Subject: [PATCH 20/36] updates --- .../fhir/jpa/searchparam/registry/SearchParamRegistryImpl.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/hapi-fhir-jpaserver-searchparam/src/main/java/ca/uhn/fhir/jpa/searchparam/registry/SearchParamRegistryImpl.java b/hapi-fhir-jpaserver-searchparam/src/main/java/ca/uhn/fhir/jpa/searchparam/registry/SearchParamRegistryImpl.java index 060e0a8d7031..bf3775fd0d31 100644 --- a/hapi-fhir-jpaserver-searchparam/src/main/java/ca/uhn/fhir/jpa/searchparam/registry/SearchParamRegistryImpl.java +++ b/hapi-fhir-jpaserver-searchparam/src/main/java/ca/uhn/fhir/jpa/searchparam/registry/SearchParamRegistryImpl.java @@ -223,7 +223,7 @@ public void addActiveSearchParameterToLocalCache(@Nonnull RuntimeSearchParam the assert theSearchParam.getBase() != null && !theSearchParam.getBase().isEmpty(); for (String rt : theSearchParam.getBase()) { - myLocalSPCache.add(rt, theSearchParam.getName(), theSearchParam); + myLocalSPCache.add(rt, theSearchParam.getName(), theSearchParam, true); } } From 68fe4eca6f5a8c8f54621b7d3f6421b4f49b9c57 Mon Sep 17 00:00:00 2001 From: leif stawnyczy Date: Fri, 5 Sep 2025 16:34:23 -0400 Subject: [PATCH 21/36] spotless --- .../ca/uhn/fhir/jpa/config/JpaConfig.java | 5 -- .../jpa/packages/NpmJpaValidationSupport.java | 21 +++-- .../packages/NpmPackageMetadataLiteJson.java | 10 +-- hapi-fhir-test-utilities/pom.xml | 7 ++ .../implementationguide/GZipCreatorUtil.java | 13 ++- .../ImplementationGuideCreator.java | 82 ++++++++++--------- 6 files changed, 73 insertions(+), 65 deletions(-) diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/config/JpaConfig.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/config/JpaConfig.java index b188c2f8bcf9..07fb47a24871 100644 --- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/config/JpaConfig.java +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/config/JpaConfig.java @@ -94,12 +94,8 @@ import ca.uhn.fhir.jpa.model.config.PartitionSettings; import ca.uhn.fhir.jpa.model.dao.JpaPid; import ca.uhn.fhir.jpa.model.sched.ISchedulerService; -import ca.uhn.fhir.jpa.packages.IHapiPackageCacheManager; import ca.uhn.fhir.jpa.packages.IPackageInstallerSvc; -import ca.uhn.fhir.jpa.packages.JpaPackageCache; -import ca.uhn.fhir.jpa.packages.NpmJpaValidationSupport; import ca.uhn.fhir.jpa.packages.PackageInstallerSvcImpl; -import ca.uhn.fhir.jpa.packages.util.PackageUtils; import ca.uhn.fhir.jpa.partition.IPartitionLookupSvc; import ca.uhn.fhir.jpa.partition.IRequestPartitionHelperSvc; import ca.uhn.fhir.jpa.partition.PartitionLookupSvcImpl; @@ -184,7 +180,6 @@ import ca.uhn.fhir.jpa.util.PersistenceContextProvider; import ca.uhn.fhir.jpa.validation.JpaValidationSupportChain; import ca.uhn.fhir.jpa.validation.ResourceLoaderImpl; -import ca.uhn.fhir.jpa.validation.ValidationSettings; import ca.uhn.fhir.model.api.IPrimitiveDatatype; import ca.uhn.fhir.replacereferences.PreviousResourceVersionRestorer; import ca.uhn.fhir.replacereferences.ReplaceReferencesPatchBundleSvc; diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/packages/NpmJpaValidationSupport.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/packages/NpmJpaValidationSupport.java index 592866cafc96..a82abb5a32dd 100644 --- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/packages/NpmJpaValidationSupport.java +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/packages/NpmJpaValidationSupport.java @@ -103,13 +103,17 @@ public List fetchAllSearchParameters() { * there's probably not a lot and even if there are * we don't reorder them */ - PagingIterator iterator = new PagingIterator<>(1000, new PagingIterator.PageFetcher() { - @Override - public void fetchNextPage(int thePageIndex, int theBatchSize, Consumer theConsumer) { - myPackageVersionDao.findAll(Pageable.ofSize(theBatchSize).withPage(thePageIndex)) - .stream().forEach(theConsumer); - } - }); + PagingIterator iterator = + new PagingIterator<>(1000, new PagingIterator.PageFetcher() { + @Override + public void fetchNextPage( + int thePageIndex, int theBatchSize, Consumer theConsumer) { + myPackageVersionDao + .findAll(Pageable.ofSize(theBatchSize).withPage(thePageIndex)) + .stream() + .forEach(theConsumer); + } + }); // do we want to cache this? List sps = new ArrayList<>(); @@ -129,7 +133,8 @@ private void getAllSP(NpmPackageVersionEntity theNpmPa NpmPackage pkg; try { - pkg = myHapiPackageCacheManager.loadPackage(theNpmPackageEntity.getPackageId(), theNpmPackageEntity.getVersionId()); + pkg = myHapiPackageCacheManager.loadPackage( + theNpmPackageEntity.getPackageId(), theNpmPackageEntity.getVersionId()); } catch (IOException ex) { // TODO - better handling throw new RuntimeException(ex); diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/packages/NpmPackageMetadataLiteJson.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/packages/NpmPackageMetadataLiteJson.java index 35f67a2af81d..d5ea2001e936 100644 --- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/packages/NpmPackageMetadataLiteJson.java +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/packages/NpmPackageMetadataLiteJson.java @@ -8,11 +8,11 @@ @Schema(description = "Contains the bare minimum of NpmPackage metadata.") @JsonInclude(JsonInclude.Include.NON_NULL) @JsonAutoDetect( - creatorVisibility = JsonAutoDetect.Visibility.NONE, - fieldVisibility = JsonAutoDetect.Visibility.NONE, - getterVisibility = JsonAutoDetect.Visibility.NONE, - isGetterVisibility = JsonAutoDetect.Visibility.NONE, - setterVisibility = JsonAutoDetect.Visibility.NONE) + creatorVisibility = JsonAutoDetect.Visibility.NONE, + fieldVisibility = JsonAutoDetect.Visibility.NONE, + getterVisibility = JsonAutoDetect.Visibility.NONE, + isGetterVisibility = JsonAutoDetect.Visibility.NONE, + setterVisibility = JsonAutoDetect.Visibility.NONE) public class NpmPackageMetadataLiteJson { @JsonProperty("name") diff --git a/hapi-fhir-test-utilities/pom.xml b/hapi-fhir-test-utilities/pom.xml index 0dbc8c2aa655..f18935a921c2 100644 --- a/hapi-fhir-test-utilities/pom.xml +++ b/hapi-fhir-test-utilities/pom.xml @@ -209,6 +209,13 @@ org.assertj assertj-core + + + + org.apache.commons + commons-compress + 1.28.0 + diff --git a/hapi-fhir-test-utilities/src/main/java/ca/uhn/fhir/implementationguide/GZipCreatorUtil.java b/hapi-fhir-test-utilities/src/main/java/ca/uhn/fhir/implementationguide/GZipCreatorUtil.java index b138dab4c6dc..fa7f677dac55 100644 --- a/hapi-fhir-test-utilities/src/main/java/ca/uhn/fhir/implementationguide/GZipCreatorUtil.java +++ b/hapi-fhir-test-utilities/src/main/java/ca/uhn/fhir/implementationguide/GZipCreatorUtil.java @@ -22,17 +22,15 @@ public class GZipCreatorUtil { */ public static void createTarGz(Path theSource, Path theOutput) throws IOException { try (OutputStream fos = Files.newOutputStream(theOutput); - BufferedOutputStream bos = new BufferedOutputStream(fos); - GzipCompressorOutputStream gzos = new GzipCompressorOutputStream(bos); - TarArchiveOutputStream taos = new TarArchiveOutputStream(gzos)) { + BufferedOutputStream bos = new BufferedOutputStream(fos); + GzipCompressorOutputStream gzos = new GzipCompressorOutputStream(bos); + TarArchiveOutputStream taos = new TarArchiveOutputStream(gzos)) { addFilesToTarGz(theSource, "", taos); } } - private static void addFilesToTarGz(Path thePath, - String theParent, - TarArchiveOutputStream theTarballOutputStream) - throws IOException { + private static void addFilesToTarGz(Path thePath, String theParent, TarArchiveOutputStream theTarballOutputStream) + throws IOException { String entryName = theParent + thePath.getFileName().toString(); TarArchiveEntry entry = new TarArchiveEntry(thePath.toFile(), entryName); theTarballOutputStream.putArchiveEntry(entry); @@ -57,5 +55,4 @@ private static void addFilesToTarGz(Path thePath, } } } - } diff --git a/hapi-fhir-test-utilities/src/main/java/ca/uhn/fhir/implementationguide/ImplementationGuideCreator.java b/hapi-fhir-test-utilities/src/main/java/ca/uhn/fhir/implementationguide/ImplementationGuideCreator.java index 88b376d1619a..3180d7b9055d 100644 --- a/hapi-fhir-test-utilities/src/main/java/ca/uhn/fhir/implementationguide/ImplementationGuideCreator.java +++ b/hapi-fhir-test-utilities/src/main/java/ca/uhn/fhir/implementationguide/ImplementationGuideCreator.java @@ -38,35 +38,36 @@ public class ImplementationGuideCreator { private static final Logger ourLog = LoggerFactory.getLogger(ImplementationGuideCreator.class); @Language("JSON") - private static final String PACKAGE_JSON_BASE = """ + private static final String PACKAGE_JSON_BASE = + """ { - "name": "test.fhir.ca.com", - "version": "1.2.3", - "tools-version": 3, - "type": "fhir.ig", - "date": "20200831134427", - "license": "not-open-source", - "canonical": "http://test-ig.com/fhir/us/providerdataexchange", - "url": "file://C:\\\\dev\\\\test-exchange\\\\fsh\\\\build\\\\output", - "title": "Test Implementation Guide", - "description": "Test Implementation Guide", - "fhirVersions": [ - "4.0.1" - ], - "dependencies": { - }, - "author": "SmileCDR", - "maintainers": [ - { - "name": "Smile", - "email": "smilecdr@smiledigitalhealth.com", - "url": "https://www.smilecdr.com" - } - ], - "directories": { - "lib": "package", - "example": "example" - } + "name": "test.fhir.ca.com", + "version": "1.2.3", + "tools-version": 3, + "type": "fhir.ig", + "date": "20200831134427", + "license": "not-open-source", + "canonical": "http://test-ig.com/fhir/us/providerdataexchange", + "url": "file://C:\\\\dev\\\\test-exchange\\\\fsh\\\\build\\\\output", + "title": "Test Implementation Guide", + "description": "Test Implementation Guide", + "fhirVersions": [ + "4.0.1" + ], + "dependencies": { + }, + "author": "SmileCDR", + "maintainers": [ + { + "name": "Smile", + "email": "smilecdr@smiledigitalhealth.com", + "url": "https://www.smilecdr.com" + } + ], + "directories": { + "lib": "package", + "example": "example" + } } """; @@ -94,8 +95,14 @@ public ImplementationGuideCreator(@Nonnull FhirContext theFhirContext) throws Js this(theFhirContext, "test.fhir.ca.com", "1.2.3"); } - public ImplementationGuideCreator(@Nonnull FhirContext theFhirContext, String thePackageName, String thePackageVersion) throws JsonProcessingException { - this(theFhirContext, theFhirContext.getVersion().getVersion().getFhirVersionString(), thePackageName, thePackageVersion); + public ImplementationGuideCreator( + @Nonnull FhirContext theFhirContext, String thePackageName, String thePackageVersion) + throws JsonProcessingException { + this( + theFhirContext, + theFhirContext.getVersion().getVersion().getFhirVersionString(), + thePackageName, + thePackageVersion); } /** @@ -107,11 +114,8 @@ public ImplementationGuideCreator(@Nonnull FhirContext theFhirContext, String th */ @SuppressWarnings("unchecked") public ImplementationGuideCreator( - @Nonnull FhirContext theFhirContext, - String theFhirVersion, - String theName, - String theVersion - ) throws JsonProcessingException { + @Nonnull FhirContext theFhirContext, String theFhirVersion, String theName, String theVersion) + throws JsonProcessingException { myFhirContext = theFhirContext; myTerser = myFhirContext.newTerser(); myParser = myFhirContext.newJsonParser(); @@ -130,8 +134,7 @@ public ImplementationGuideCreator( mapJson.replace("name", myPackageName); mapJson.replace("version", myPackageVersion); - myPackageJson = mapper.writerWithDefaultPrettyPrinter() - .writeValueAsString(mapJson); + myPackageJson = mapper.writerWithDefaultPrettyPrinter().writeValueAsString(mapJson); ourLog.info(myPackageJson); } @@ -174,7 +177,9 @@ public void addSPToIG(IBaseResource theSP) { for (String reqdField : new String[] {"name", "status", "url", "code", "expression", "type"}) { Optional fieldOp = myTerser.getSinglePrimitiveValue(theSP, reqdField); if (!myLenientSetting) { - assertTrue(fieldOp.isPresent(), String.format("%s is a required field for IG SearchParameters", reqdField)); + assertTrue( + fieldOp.isPresent(), + String.format("%s is a required field for IG SearchParameters", reqdField)); } } List base = myTerser.getValues(theSP, "base"); @@ -242,5 +247,4 @@ private void addFileToDir(String theContent, String theFileName, Path theOutputP } } } - } From 6914bc5ae5dc06c7bcb5d46409e528234a09e8ff Mon Sep 17 00:00:00 2001 From: leif stawnyczy Date: Fri, 5 Sep 2025 16:36:30 -0400 Subject: [PATCH 22/36] documentation --- .../uhn/fhir/jpa/packages/NpmPackageMetadataLiteJson.java | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/packages/NpmPackageMetadataLiteJson.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/packages/NpmPackageMetadataLiteJson.java index d5ea2001e936..d0384db60038 100644 --- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/packages/NpmPackageMetadataLiteJson.java +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/packages/NpmPackageMetadataLiteJson.java @@ -14,10 +14,14 @@ isGetterVisibility = JsonAutoDetect.Visibility.NONE, setterVisibility = JsonAutoDetect.Visibility.NONE) public class NpmPackageMetadataLiteJson { - + /** + * The npm package name + */ @JsonProperty("name") private String myName; - + /** + * The npm package version + */ @JsonProperty("version") private String myVersion; From d0e5226caf17857efba825ffef28538f05ac5563 Mon Sep 17 00:00:00 2001 From: leif stawnyczy Date: Mon, 8 Sep 2025 08:24:43 -0400 Subject: [PATCH 23/36] updating changelog --- .../7208-allow-searchparamregistry-to-have-a-local-cache.yaml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/changelog/8_6_0/7208-allow-searchparamregistry-to-have-a-local-cache.yaml b/hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/changelog/8_6_0/7208-allow-searchparamregistry-to-have-a-local-cache.yaml index e4cf58881ef3..3b0d868847fe 100644 --- a/hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/changelog/8_6_0/7208-allow-searchparamregistry-to-have-a-local-cache.yaml +++ b/hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/changelog/8_6_0/7208-allow-searchparamregistry-to-have-a-local-cache.yaml @@ -4,4 +4,6 @@ issue: 7208 title: "Added the ability to add SearchParameters to a local cache without having them added to the built in search parameters or the database. + IValidationSupport can now also be used to read all SearchParameters + out of Npm packages (Implementation Guides). " From da5da3ecf08fe2c0ea8b01bda603c0a31d69cee1 Mon Sep 17 00:00:00 2001 From: leif stawnyczy Date: Mon, 8 Sep 2025 08:37:00 -0400 Subject: [PATCH 24/36] adding some logging --- .../uhn/fhir/jpa/packages/NpmJpaValidationSupport.java | 7 +++++++ .../fhir/jpa/packages/NpmJpaValidationSupportIT.java | 10 ++++++++++ 2 files changed, 17 insertions(+) diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/packages/NpmJpaValidationSupport.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/packages/NpmJpaValidationSupport.java index a82abb5a32dd..256929b0e5b2 100644 --- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/packages/NpmJpaValidationSupport.java +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/packages/NpmJpaValidationSupport.java @@ -31,6 +31,8 @@ import jakarta.annotation.Nullable; import org.hl7.fhir.instance.model.api.IBaseResource; import org.hl7.fhir.utilities.npm.NpmPackage; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.data.domain.Pageable; @@ -40,6 +42,7 @@ import java.util.function.Consumer; public class NpmJpaValidationSupport implements IValidationSupport { + private static final Logger ourLog = LoggerFactory.getLogger(NpmJpaValidationSupport.class); @Autowired private FhirContext myFhirContext; @@ -128,6 +131,10 @@ private void getAllSP(NpmPackageVersionEntity theNpmPa if (theNpmPackageEntity.getFhirVersion() != myFhirContext.getVersion().getVersion()) { // not the same fhir version as this class is dependent on, // so we cannot create these objects + ourLog.info( + "Encountered an NPM package with an incompatible fhir version: {} when expecting {}", + theNpmPackageEntity.getFhirVersion().getFhirVersionString(), + myFhirContext.getVersion().getVersion().getFhirVersionString()); return; } diff --git a/hapi-fhir-jpaserver-test-r4/src/test/java/ca/uhn/fhir/jpa/packages/NpmJpaValidationSupportIT.java b/hapi-fhir-jpaserver-test-r4/src/test/java/ca/uhn/fhir/jpa/packages/NpmJpaValidationSupportIT.java index f4847114fea2..9ae212909514 100644 --- a/hapi-fhir-jpaserver-test-r4/src/test/java/ca/uhn/fhir/jpa/packages/NpmJpaValidationSupportIT.java +++ b/hapi-fhir-jpaserver-test-r4/src/test/java/ca/uhn/fhir/jpa/packages/NpmJpaValidationSupportIT.java @@ -4,10 +4,12 @@ import ca.uhn.fhir.implementationguide.ImplementationGuideCreator; import ca.uhn.fhir.jpa.packages.util.PackageUtils; import ca.uhn.fhir.jpa.test.BaseJpaR4Test; +import ca.uhn.test.util.LogbackTestExtension; import org.hl7.fhir.instance.model.api.IBaseResource; import org.hl7.fhir.r4.model.Enumerations; import org.hl7.fhir.r4.model.SearchParameter; import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; import org.springframework.beans.factory.annotation.Autowired; import java.io.File; @@ -30,6 +32,9 @@ public class NpmJpaValidationSupportIT extends BaseJpaR4Test { @Autowired private IPackageInstallerSvc myPackageInstallerSvc; + @RegisterExtension + private final LogbackTestExtension myLogbackTestExtension = new LogbackTestExtension(NpmJpaValidationSupport.class); + @Test public void fetchAllSearchParameters_withStoredIG_returnsSPsFromIG() throws IOException { String name = "ig-test-dir"; @@ -144,12 +149,17 @@ public void fetchAllSearchParameters_multipleFhirVersions_returnsOnlySPsMatching // test List sps = mySvc.fetchAllSearchParameters(); + + // verify assertNotNull(sps); assertEquals(1, sps.size()); assertTrue(sps.stream() .anyMatch(sp -> sp instanceof SearchParameter)); assertFalse(sps.stream() .anyMatch(sp -> sp instanceof org.hl7.fhir.dstu3.model.SearchParameter)); + + assertTrue(myLogbackTestExtension.getLogEvents() + .stream().anyMatch(e -> e.getFormattedMessage().contains("Encountered an NPM package with an incompatible fhir version"))); } private SearchParameter generateSP() { From 3c3bf576474a75ab8e1818585f7b59fff50c84f3 Mon Sep 17 00:00:00 2001 From: leif stawnyczy Date: Mon, 8 Sep 2025 09:21:34 -0400 Subject: [PATCH 25/36] fixing it --- .../ca/uhn/fhir/jpa/packages/NpmJpaValidationSupportIT.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/hapi-fhir-jpaserver-test-r4/src/test/java/ca/uhn/fhir/jpa/packages/NpmJpaValidationSupportIT.java b/hapi-fhir-jpaserver-test-r4/src/test/java/ca/uhn/fhir/jpa/packages/NpmJpaValidationSupportIT.java index 9ae212909514..f545633dbcb3 100644 --- a/hapi-fhir-jpaserver-test-r4/src/test/java/ca/uhn/fhir/jpa/packages/NpmJpaValidationSupportIT.java +++ b/hapi-fhir-jpaserver-test-r4/src/test/java/ca/uhn/fhir/jpa/packages/NpmJpaValidationSupportIT.java @@ -33,7 +33,7 @@ public class NpmJpaValidationSupportIT extends BaseJpaR4Test { private IPackageInstallerSvc myPackageInstallerSvc; @RegisterExtension - private final LogbackTestExtension myLogbackTestExtension = new LogbackTestExtension(NpmJpaValidationSupport.class); + public final LogbackTestExtension myLogbackTestExtension = new LogbackTestExtension(NpmJpaValidationSupport.class); @Test public void fetchAllSearchParameters_withStoredIG_returnsSPsFromIG() throws IOException { From 7e319c0a7f0a78c19b457a4331fa9d4d26ad5134 Mon Sep 17 00:00:00 2001 From: leif stawnyczy Date: Mon, 8 Sep 2025 10:47:03 -0400 Subject: [PATCH 26/36] updating api for hapi package cache manager --- .../jpa/packages/FindPackageAssetRequest.java | 24 +++++ .../packages/IHapiPackageCacheManager.java | 29 ++++++ .../fhir/jpa/packages/JpaPackageCache.java | 95 +++++++++++-------- ...JpaPackageCacheDuplicateResourcesTest.java | 35 ++++++- 4 files changed, 142 insertions(+), 41 deletions(-) diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/packages/FindPackageAssetRequest.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/packages/FindPackageAssetRequest.java index 2d8006f8d24c..187b2d1ce95b 100644 --- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/packages/FindPackageAssetRequest.java +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/packages/FindPackageAssetRequest.java @@ -21,6 +21,7 @@ import ca.uhn.fhir.context.FhirVersionEnum; import jakarta.annotation.Nullable; +import org.springframework.data.domain.PageRequest; import java.util.Objects; import java.util.StringJoiner; @@ -34,6 +35,15 @@ public class FindPackageAssetRequest { private final String myCanonicalUrl; private final String myPackageId; + /** + * The default page number + */ + private int myPageNumber = 0; + /** + * The default page size + */ + private int myPageSize = 10; + @Nullable private final String myVersion; @@ -72,6 +82,20 @@ public String getVersion() { return myVersion; } + public PageRequest getPageRequest() { + return PageRequest.of(myPageNumber, myPageSize); + } + + public FindPackageAssetRequest setPageNumber(int thePageNumber) { + myPageNumber = thePageNumber; + return this; + } + + public FindPackageAssetRequest setPageSize(int thePageSize) { + myPageSize = thePageSize; + return this; + } + @Override public boolean equals(Object object) { if (object == null || getClass() != object.getClass()) { diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/packages/IHapiPackageCacheManager.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/packages/IHapiPackageCacheManager.java index b4e570da3c9c..0600d9b0bc92 100644 --- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/packages/IHapiPackageCacheManager.java +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/packages/IHapiPackageCacheManager.java @@ -24,6 +24,7 @@ import org.hl7.fhir.instance.model.api.IBaseResource; import org.hl7.fhir.utilities.npm.IPackageCacheManager; import org.hl7.fhir.utilities.npm.NpmPackage; +import org.springframework.data.domain.PageRequest; import java.io.IOException; import java.util.Date; @@ -33,12 +34,40 @@ public interface IHapiPackageCacheManager extends IPackageCacheManager { NpmPackage installPackage(PackageInstallationSpec theInstallationSpec) throws IOException; + + /** + * Returns the first instance of a resource by fhir version and canonical url + * @param theFhirVersion the fhir version + * @param theCanonicalUrl the canonical url + * @return the first instance of a resource matching these conditions, or null if none found. + * @deprecated Use loadPackageAssetsByUrl. + */ + @Deprecated(forRemoval = true, since = "8.5") IBaseResource loadPackageAssetByUrl(FhirVersionEnum theFhirVersion, String theCanonicalUrl); + /** + * Returns all possible resources by the provided url and fhir version. + */ + List loadPackageAssetsByUrl(FhirVersionEnum theFhirVersionEnum, String theCanonicalUrl, PageRequest thePageRequest); + List findPackageAssetInfoByUrl(FhirVersionEnum theFhirVersion, String theCanonicalUrl); + /** + * Returns the first instance of a resource by fhir version and canonical url + * @param theFindPackageAssetRequest The request parameters + * @return the first instance of a resource matching these conditions, or null if none found. + * @deprecated Use findsPackageAssets with a proper page size/page number. + */ + @Deprecated(forRemoval = true, since = "8.5") IBaseResource findPackageAsset(FindPackageAssetRequest theFindPackageAssetRequest); + /** + * Returns all package assets matching the request object. + * @param theRequest the request object + * @return a list of package assets + */ + List findPackageAssets(FindPackageAssetRequest theRequest); + NpmPackageMetadataJson loadPackageMetadata(String thePackageId) throws ResourceNotFoundException; PackageContents loadPackageContents(String thePackageId, String theVersion); diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/packages/JpaPackageCache.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/packages/JpaPackageCache.java index 6dccee7829d2..0493b8e094db 100644 --- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/packages/JpaPackageCache.java +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/packages/JpaPackageCache.java @@ -581,53 +581,70 @@ public NpmPackage installPackage(PackageInstallationSpec theInstallationSpec) th @Override @Transactional(readOnly = true) public IBaseResource loadPackageAssetByUrl(FhirVersionEnum theFhirVersion, String theCanonicalUrl) { + List resources = loadPackageAssetsByUrl(theFhirVersion, theCanonicalUrl, PageRequest.of(0, 2)); + if (resources.size() > 1) { + ourLog.warn( + "Found multiple package versions for FHIR version: {} and canonical URL: {}", + theFhirVersion, + theCanonicalUrl); + } else if (resources.isEmpty()) { + return null; + } + return resources.get(0); + } + @Override + @Transactional(readOnly = true) + public List loadPackageAssetsByUrl(FhirVersionEnum theFhirVersion, String theCanonicalUrl, PageRequest thePageRequest) { final List npmPackageVersionResourceEntities = - loadPackageInfoByCanonicalUrl(theFhirVersion, theCanonicalUrl, 2, null, null); + loadPackageInfoByCanonicalUrl(theFhirVersion, theCanonicalUrl, thePageRequest, null, null); if (npmPackageVersionResourceEntities.isEmpty()) { - return null; + return List.of(); } else { - if (npmPackageVersionResourceEntities.size() > 1) { - ourLog.warn( - "Found multiple package versions for FHIR version: {} and canonical URL: {}", - theFhirVersion, - theCanonicalUrl); - } - final NpmPackageVersionResourceEntity contents = npmPackageVersionResourceEntities.get(0); - return loadPackageEntity(contents); + return npmPackageVersionResourceEntities.stream() + .map(this::loadPackageEntity) + .collect(Collectors.toList()); } } @Override @Transactional(readOnly = true) public IBaseResource findPackageAsset(FindPackageAssetRequest theRequest) { + List assets = findPackageAssets(theRequest); + if (assets.size() > 1) { + ourLog.warn( + "Found multiple package versions for FHIR version: {} and canonical URL: {}", + theRequest.getFhirVersion(), + theRequest.getCanonicalUrl()); + } + // assets will always have a single element because findPackageAssets throws if nothing is found + return assets.get(0); + } + @Override + @Transactional(readOnly = true) + public List findPackageAssets(FindPackageAssetRequest theRequest) { final List npmPackageVersionResourceEntities = loadPackageInfoByCanonicalUrl( - theRequest.getFhirVersion(), - theRequest.getCanonicalUrl(), - 2, // We set it to 2 so that if we get more than one we can warn - theRequest.getPackageId(), - theRequest.getVersion()); + theRequest.getFhirVersion(), + theRequest.getCanonicalUrl(), + theRequest.getPageRequest(), + theRequest.getPackageId(), + theRequest.getVersion()); if (npmPackageVersionResourceEntities.isEmpty()) { throw new ResourceNotFoundException( - "%s Could not find asset for FHIR version: %s, canonical URL: %s, package ID: %s and package version: %s" - .formatted( - Msg.code(2644), - theRequest.getFhirVersion(), - theRequest.getCanonicalUrl(), - theRequest.getPackageId(), - Optional.ofNullable(theRequest.getVersion()).orElse("[none]"))); - } else { - if (npmPackageVersionResourceEntities.size() > 1) { - ourLog.warn( - "Found multiple package versions for FHIR version: {} and canonical URL: {}", + "%s Could not find assets for FHIR version: %s, canonical URL: %s, package ID: %s and package version: %s" + .formatted( + Msg.code(2644), theRequest.getFhirVersion(), - theRequest.getCanonicalUrl()); - } - final NpmPackageVersionResourceEntity contents = npmPackageVersionResourceEntities.get(0); - return loadPackageEntity(contents); + theRequest.getCanonicalUrl(), + theRequest.getPackageId(), + Optional.ofNullable(theRequest.getVersion()).orElse("[none]"))); + } else { + return npmPackageVersionResourceEntities.stream() + .map(this::loadPackageEntity) + .collect(Collectors.toList()); } } @@ -636,7 +653,7 @@ public IBaseResource findPackageAsset(FindPackageAssetRequest theRequest) { public List findPackageAssetInfoByUrl( FhirVersionEnum theFhirVersion, String theCanonicalUrl) { final List npmPackageVersionResourceEntities = - loadPackageInfoByCanonicalUrl(theFhirVersion, theCanonicalUrl, 20, null, null); + loadPackageInfoByCanonicalUrl(theFhirVersion, theCanonicalUrl, PageRequest.of(0, 20), null, null); return npmPackageVersionResourceEntities.stream() .map(entity -> new NpmPackageAssetInfoJson( @@ -651,7 +668,7 @@ public List findPackageAssetInfoByUrl( private List loadPackageInfoByCanonicalUrl( FhirVersionEnum theFhirVersion, String theCanonicalUrl, - int thePageSize, + PageRequest thePageRequest, @Nullable String thePackageId, @Nullable String theVersionId) { String canonicalUrl = theCanonicalUrl; @@ -659,8 +676,6 @@ private List loadPackageInfoByCanonicalUrl( int versionSeparator = canonicalUrl.lastIndexOf('|'); Slice slice; - final PageRequest pageRequest = PageRequest.of(0, thePageSize); - if (versionSeparator != -1) { String canonicalVersion = canonicalUrl.substring(versionSeparator + 1); canonicalUrl = canonicalUrl.substring(0, versionSeparator); @@ -670,7 +685,7 @@ private List loadPackageInfoByCanonicalUrl( slice = myPackageVersionResourceDao .findCurrentVersionByCanonicalUrlAndVersionAndPackageIdAndVersion( - pageRequest, + thePageRequest, theFhirVersion, canonicalUrl, canonicalVersion, @@ -678,25 +693,25 @@ private List loadPackageInfoByCanonicalUrl( theVersionId); } else { slice = myPackageVersionResourceDao.findCurrentVersionByCanonicalUrlAndVersionAndPackageId( - pageRequest, theFhirVersion, canonicalUrl, canonicalVersion, thePackageId); + thePageRequest, theFhirVersion, canonicalUrl, canonicalVersion, thePackageId); } } else { slice = myPackageVersionResourceDao.findCurrentVersionByCanonicalUrlAndVersion( - pageRequest, theFhirVersion, canonicalUrl, canonicalVersion); + thePageRequest, theFhirVersion, canonicalUrl, canonicalVersion); } } else { if (thePackageId != null) { if (theVersionId != null) { slice = myPackageVersionResourceDao.findCurrentVersionByCanonicalUrlAndPackageIdAndVersion( - pageRequest, theFhirVersion, canonicalUrl, thePackageId, theVersionId); + thePageRequest, theFhirVersion, canonicalUrl, thePackageId, theVersionId); } else { slice = myPackageVersionResourceDao.findCurrentVersionByCanonicalUrlAndPackageId( - pageRequest, theFhirVersion, canonicalUrl, thePackageId); + thePageRequest, theFhirVersion, canonicalUrl, thePackageId); } } else { slice = myPackageVersionResourceDao.findCurrentVersionByCanonicalUrl( - pageRequest, theFhirVersion, canonicalUrl); + thePageRequest, theFhirVersion, canonicalUrl); } } diff --git a/hapi-fhir-jpaserver-test-r4/src/test/java/ca/uhn/fhir/jpa/packages/JpaPackageCacheDuplicateResourcesTest.java b/hapi-fhir-jpaserver-test-r4/src/test/java/ca/uhn/fhir/jpa/packages/JpaPackageCacheDuplicateResourcesTest.java index 71ad07218e0c..ccf55ab41094 100644 --- a/hapi-fhir-jpaserver-test-r4/src/test/java/ca/uhn/fhir/jpa/packages/JpaPackageCacheDuplicateResourcesTest.java +++ b/hapi-fhir-jpaserver-test-r4/src/test/java/ca/uhn/fhir/jpa/packages/JpaPackageCacheDuplicateResourcesTest.java @@ -15,6 +15,7 @@ import org.junit.jupiter.params.provider.Arguments; import org.junit.jupiter.params.provider.MethodSource; import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.data.domain.PageRequest; import java.io.IOException; import java.util.List; @@ -23,6 +24,9 @@ import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatExceptionOfType; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertTrue; import static org.junit.jupiter.api.Assertions.fail; /** @@ -83,6 +87,19 @@ void beforeEach() throws IOException { .setPackageUrl("classpath://packages/simple-alpha-dupe.tgz")); } + @Test + public void findPackageAssetsByUrl_withDuplicateResources_returnsAll() { + // test + List resources = myPackageCacheManager.loadPackageAssetsByUrl(FhirVersionEnum.R4, MEASURE_URL, PageRequest.of(0, 10)); + + // validation + assertNotNull(resources); + assertEquals(2, resources.size()); + for (IBaseResource resource : resources) { + assertTrue(resource instanceof Measure); + } + } + @Test void findPackageAssetInfoByUrl_duplicateResources() { @@ -224,6 +241,22 @@ void findPackageAsset_duplicateResources(String theCanonicalUrl, String thePacka } } + @Test + void findPackageAssets_duplicateResources() { + // test + FindPackageAssetRequest request = + FindPackageAssetRequest.withVersion( + FhirVersionEnum.R4, + MEASURE_URL, + null, + null); + List resources = myPackageCacheManager.findPackageAssets(request); + + // validate + assertNotNull(resources); + assertEquals(2, resources.size()); + } + private static Stream findPackageAsset_duplicateResources_badInputParams() { return Stream.of( Arguments.of( @@ -277,7 +310,7 @@ void findPackageAsset_duplicateResources_badInput(String theCanonicalUrl, String assertThatExceptionOfType(ResourceNotFoundException.class) .isThrownBy(() -> myPackageCacheManager.findPackageAsset(request)) - .withMessage("HAPI-2644: Could not find asset for FHIR version: R4, canonical URL: %s, package ID: %s and package version: %s" + .withMessage("HAPI-2644: Could not find assets for FHIR version: R4, canonical URL: %s, package ID: %s and package version: %s" .formatted(theCanonicalUrl, thePackageId, Optional.ofNullable(theVersionId).orElse("[none]"))); } } From ea16c2e53648316b5c0a4c297bcaafa50a0f5415 Mon Sep 17 00:00:00 2001 From: leif stawnyczy Date: Mon, 8 Sep 2025 10:47:23 -0400 Subject: [PATCH 27/36] spotless --- .../packages/IHapiPackageCacheManager.java | 4 +- .../fhir/jpa/packages/JpaPackageCache.java | 57 ++++++++++--------- 2 files changed, 31 insertions(+), 30 deletions(-) diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/packages/IHapiPackageCacheManager.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/packages/IHapiPackageCacheManager.java index 0600d9b0bc92..c0e07c8eeb77 100644 --- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/packages/IHapiPackageCacheManager.java +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/packages/IHapiPackageCacheManager.java @@ -34,7 +34,6 @@ public interface IHapiPackageCacheManager extends IPackageCacheManager { NpmPackage installPackage(PackageInstallationSpec theInstallationSpec) throws IOException; - /** * Returns the first instance of a resource by fhir version and canonical url * @param theFhirVersion the fhir version @@ -48,7 +47,8 @@ public interface IHapiPackageCacheManager extends IPackageCacheManager { /** * Returns all possible resources by the provided url and fhir version. */ - List loadPackageAssetsByUrl(FhirVersionEnum theFhirVersionEnum, String theCanonicalUrl, PageRequest thePageRequest); + List loadPackageAssetsByUrl( + FhirVersionEnum theFhirVersionEnum, String theCanonicalUrl, PageRequest thePageRequest); List findPackageAssetInfoByUrl(FhirVersionEnum theFhirVersion, String theCanonicalUrl); diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/packages/JpaPackageCache.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/packages/JpaPackageCache.java index 0493b8e094db..45b2cafa07f1 100644 --- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/packages/JpaPackageCache.java +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/packages/JpaPackageCache.java @@ -584,9 +584,9 @@ public IBaseResource loadPackageAssetByUrl(FhirVersionEnum theFhirVersion, Strin List resources = loadPackageAssetsByUrl(theFhirVersion, theCanonicalUrl, PageRequest.of(0, 2)); if (resources.size() > 1) { ourLog.warn( - "Found multiple package versions for FHIR version: {} and canonical URL: {}", - theFhirVersion, - theCanonicalUrl); + "Found multiple package versions for FHIR version: {} and canonical URL: {}", + theFhirVersion, + theCanonicalUrl); } else if (resources.isEmpty()) { return null; } @@ -595,16 +595,17 @@ public IBaseResource loadPackageAssetByUrl(FhirVersionEnum theFhirVersion, Strin @Override @Transactional(readOnly = true) - public List loadPackageAssetsByUrl(FhirVersionEnum theFhirVersion, String theCanonicalUrl, PageRequest thePageRequest) { + public List loadPackageAssetsByUrl( + FhirVersionEnum theFhirVersion, String theCanonicalUrl, PageRequest thePageRequest) { final List npmPackageVersionResourceEntities = - loadPackageInfoByCanonicalUrl(theFhirVersion, theCanonicalUrl, thePageRequest, null, null); + loadPackageInfoByCanonicalUrl(theFhirVersion, theCanonicalUrl, thePageRequest, null, null); if (npmPackageVersionResourceEntities.isEmpty()) { return List.of(); } else { return npmPackageVersionResourceEntities.stream() - .map(this::loadPackageEntity) - .collect(Collectors.toList()); + .map(this::loadPackageEntity) + .collect(Collectors.toList()); } } @@ -614,9 +615,9 @@ public IBaseResource findPackageAsset(FindPackageAssetRequest theRequest) { List assets = findPackageAssets(theRequest); if (assets.size() > 1) { ourLog.warn( - "Found multiple package versions for FHIR version: {} and canonical URL: {}", - theRequest.getFhirVersion(), - theRequest.getCanonicalUrl()); + "Found multiple package versions for FHIR version: {} and canonical URL: {}", + theRequest.getFhirVersion(), + theRequest.getCanonicalUrl()); } // assets will always have a single element because findPackageAssets throws if nothing is found return assets.get(0); @@ -626,25 +627,25 @@ public IBaseResource findPackageAsset(FindPackageAssetRequest theRequest) { @Transactional(readOnly = true) public List findPackageAssets(FindPackageAssetRequest theRequest) { final List npmPackageVersionResourceEntities = loadPackageInfoByCanonicalUrl( - theRequest.getFhirVersion(), - theRequest.getCanonicalUrl(), - theRequest.getPageRequest(), - theRequest.getPackageId(), - theRequest.getVersion()); + theRequest.getFhirVersion(), + theRequest.getCanonicalUrl(), + theRequest.getPageRequest(), + theRequest.getPackageId(), + theRequest.getVersion()); if (npmPackageVersionResourceEntities.isEmpty()) { throw new ResourceNotFoundException( - "%s Could not find assets for FHIR version: %s, canonical URL: %s, package ID: %s and package version: %s" - .formatted( - Msg.code(2644), - theRequest.getFhirVersion(), - theRequest.getCanonicalUrl(), - theRequest.getPackageId(), - Optional.ofNullable(theRequest.getVersion()).orElse("[none]"))); + "%s Could not find assets for FHIR version: %s, canonical URL: %s, package ID: %s and package version: %s" + .formatted( + Msg.code(2644), + theRequest.getFhirVersion(), + theRequest.getCanonicalUrl(), + theRequest.getPackageId(), + Optional.ofNullable(theRequest.getVersion()).orElse("[none]"))); } else { return npmPackageVersionResourceEntities.stream() - .map(this::loadPackageEntity) - .collect(Collectors.toList()); + .map(this::loadPackageEntity) + .collect(Collectors.toList()); } } @@ -697,21 +698,21 @@ private List loadPackageInfoByCanonicalUrl( } } else { slice = myPackageVersionResourceDao.findCurrentVersionByCanonicalUrlAndVersion( - thePageRequest, theFhirVersion, canonicalUrl, canonicalVersion); + thePageRequest, theFhirVersion, canonicalUrl, canonicalVersion); } } else { if (thePackageId != null) { if (theVersionId != null) { slice = myPackageVersionResourceDao.findCurrentVersionByCanonicalUrlAndPackageIdAndVersion( - thePageRequest, theFhirVersion, canonicalUrl, thePackageId, theVersionId); + thePageRequest, theFhirVersion, canonicalUrl, thePackageId, theVersionId); } else { slice = myPackageVersionResourceDao.findCurrentVersionByCanonicalUrlAndPackageId( - thePageRequest, theFhirVersion, canonicalUrl, thePackageId); + thePageRequest, theFhirVersion, canonicalUrl, thePackageId); } } else { slice = myPackageVersionResourceDao.findCurrentVersionByCanonicalUrl( - thePageRequest, theFhirVersion, canonicalUrl); + thePageRequest, theFhirVersion, canonicalUrl); } } From 59d1b4bafcf75b79d50be65153c886bf1f2ce284 Mon Sep 17 00:00:00 2001 From: leif stawnyczy Date: Mon, 8 Sep 2025 16:19:04 -0400 Subject: [PATCH 28/36] allowing some sorting --- .../jpa/packages/NpmJpaValidationSupport.java | 15 ++---- .../packages/NpmJpaValidationSupportIT.java | 53 ++++++++++++++++--- 2 files changed, 52 insertions(+), 16 deletions(-) diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/packages/NpmJpaValidationSupport.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/packages/NpmJpaValidationSupport.java index 256929b0e5b2..458367718233 100644 --- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/packages/NpmJpaValidationSupport.java +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/packages/NpmJpaValidationSupport.java @@ -35,11 +35,11 @@ import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.data.domain.Pageable; +import org.springframework.data.domain.Sort; import java.io.IOException; import java.util.ArrayList; import java.util.List; -import java.util.function.Consumer; public class NpmJpaValidationSupport implements IValidationSupport { private static final Logger ourLog = LoggerFactory.getLogger(NpmJpaValidationSupport.class); @@ -107,15 +107,10 @@ public List fetchAllSearchParameters() { * we don't reorder them */ PagingIterator iterator = - new PagingIterator<>(1000, new PagingIterator.PageFetcher() { - @Override - public void fetchNextPage( - int thePageIndex, int theBatchSize, Consumer theConsumer) { - myPackageVersionDao - .findAll(Pageable.ofSize(theBatchSize).withPage(thePageIndex)) - .stream() - .forEach(theConsumer); - } + new PagingIterator<>(1000, (thePageIndex, theBatchSize, theConsumer) -> { + myPackageVersionDao + .findAll(Pageable.ofSize(theBatchSize).withPage(thePageIndex).getSortOr(Sort.by(Sort.Direction.ASC, "myPackageId", "myVersionId"))) + .forEach(theConsumer); }); // do we want to cache this? diff --git a/hapi-fhir-jpaserver-test-r4/src/test/java/ca/uhn/fhir/jpa/packages/NpmJpaValidationSupportIT.java b/hapi-fhir-jpaserver-test-r4/src/test/java/ca/uhn/fhir/jpa/packages/NpmJpaValidationSupportIT.java index f545633dbcb3..df22767e3c25 100644 --- a/hapi-fhir-jpaserver-test-r4/src/test/java/ca/uhn/fhir/jpa/packages/NpmJpaValidationSupportIT.java +++ b/hapi-fhir-jpaserver-test-r4/src/test/java/ca/uhn/fhir/jpa/packages/NpmJpaValidationSupportIT.java @@ -16,6 +16,8 @@ import java.io.IOException; import java.nio.file.Files; import java.nio.file.Path; +import java.util.ArrayList; +import java.util.Arrays; import java.util.HashMap; import java.util.List; import java.util.Map; @@ -46,7 +48,7 @@ public void fetchAllSearchParameters_withStoredIG_returnsSPsFromIG() throws IOEx SearchParameter generated = generateSP(); igCreator.addSPToIG(generated); - PackageInstallationSpec spec = createAndInstallPackageSpec(igCreator); + PackageInstallationSpec spec = createAndInstallPackageSpec(igCreator, PackageInstallationSpec.InstallModeEnum.STORE_ONLY); // test List sps = mySvc.fetchAllSearchParameters(); @@ -65,6 +67,44 @@ public void fetchAllSearchParameters_withStoredIG_returnsSPsFromIG() throws IOEx assertEquals(spec.getVersion(), metadata.getVersion()); } + @Test + public void fetchAllSearchParameters_multipleVersions_returnVersionsHigherToLower() throws IOException { + String name = "ig-test-dir"; + // deliberately not in order to ensure the loading order + // is always in order + String[] versions = new String[] { "1.1.1", "3.1.2", "2.2.2" }; + + List orderedVersions = new ArrayList<>(); + SearchParameter generated = generateSP(); + for (String version : versions) { + Path tempDirPath = createTempDir(name + version); + orderedVersions.add(version); + + ImplementationGuideCreator igCreator = new ImplementationGuideCreator(myFhirContext, name, version); + igCreator.setDirectory(tempDirPath); + generated.setDescription("My SP for " + version); + igCreator.addSPToIG(generated); + + PackageInstallationSpec spec = createAndInstallPackageSpec(igCreator, PackageInstallationSpec.InstallModeEnum.STORE_ONLY); + } + orderedVersions.sort(String::compareTo); + + // test + List sps = mySvc.fetchAllSearchParameters(); + + // verify + assertNotNull(sps); + assertEquals(orderedVersions.size(), sps.size()); + for (int i = 0; i < sps.size(); i++) { + SearchParameter sp = (SearchParameter) sps.get(i); + String version = orderedVersions.get(i); + + assertTrue(sp.getDescription() + .contains(version), + String.format("Expected %s but found %s", version, sp.getDescription())); + } + } + @Test public void fetchAllSearchParameters_multipleVersion_returnsAllSPs() throws IOException { String name = "ig-test-dir"; @@ -78,7 +118,7 @@ public void fetchAllSearchParameters_multipleVersion_returnsAllSPs() throws IOEx igCreator.setDirectory(tempDirPath); igCreator.addSPToIG(generated); - PackageInstallationSpec spec = createAndInstallPackageSpec(igCreator); + PackageInstallationSpec spec = createAndInstallPackageSpec(igCreator, PackageInstallationSpec.InstallModeEnum.STORE_ONLY); } // test @@ -144,8 +184,8 @@ public void fetchAllSearchParameters_multipleFhirVersions_returnsOnlySPsMatching igFhir3dstu.addSPToIG(spdstu3); } - createAndInstallPackageSpec(igFhir4); - createAndInstallPackageSpec(igFhir3dstu); + createAndInstallPackageSpec(igFhir4, PackageInstallationSpec.InstallModeEnum.STORE_ONLY); + createAndInstallPackageSpec(igFhir3dstu, PackageInstallationSpec.InstallModeEnum.STORE_ONLY); // test List sps = mySvc.fetchAllSearchParameters(); @@ -175,7 +215,8 @@ private SearchParameter generateSP() { return sp; } - private PackageInstallationSpec createAndInstallPackageSpec(ImplementationGuideCreator theIgCreator) throws IOException { + private PackageInstallationSpec createAndInstallPackageSpec(ImplementationGuideCreator theIgCreator, + PackageInstallationSpec.InstallModeEnum theInstallModeEnum) throws IOException { // create a source directory Path outputFileName = theIgCreator.createTestIG(); @@ -183,7 +224,7 @@ private PackageInstallationSpec createAndInstallPackageSpec(ImplementationGuideC PackageInstallationSpec spec = new PackageInstallationSpec() .setName(theIgCreator.getPackageName()) .setVersion(theIgCreator.getPackageVersion()) - .setInstallMode(PackageInstallationSpec.InstallModeEnum.STORE_ONLY) + .setInstallMode(theInstallModeEnum) .setPackageContents(Files.readAllBytes(outputFileName)) ; PackageInstallOutcomeJson outcome = myPackageInstallerSvc.install(spec); From 8a6618d09519c4c150883622cb8ad352dc277564 Mon Sep 17 00:00:00 2001 From: leif stawnyczy Date: Tue, 9 Sep 2025 08:56:06 -0400 Subject: [PATCH 29/36] adding checked error --- .../uhn/fhir/jpa/packages/NpmJpaValidationSupport.java | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/packages/NpmJpaValidationSupport.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/packages/NpmJpaValidationSupport.java index 458367718233..5362d275dad0 100644 --- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/packages/NpmJpaValidationSupport.java +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/packages/NpmJpaValidationSupport.java @@ -19,9 +19,11 @@ */ package ca.uhn.fhir.jpa.packages; +import ca.uhn.fhir.context.ConfigurationException; import ca.uhn.fhir.context.FhirContext; import ca.uhn.fhir.context.FhirVersionEnum; import ca.uhn.fhir.context.support.IValidationSupport; +import ca.uhn.fhir.i18n.Msg; import ca.uhn.fhir.jpa.dao.data.INpmPackageVersionDao; import ca.uhn.fhir.jpa.model.entity.NpmPackageVersionEntity; import ca.uhn.fhir.jpa.packages.loader.PackageResourceParsingSvc; @@ -138,8 +140,11 @@ private void getAllSP(NpmPackageVersionEntity theNpmPa pkg = myHapiPackageCacheManager.loadPackage( theNpmPackageEntity.getPackageId(), theNpmPackageEntity.getVersionId()); } catch (IOException ex) { - // TODO - better handling - throw new RuntimeException(ex); + // we shouldn't ever really see this + String msg = String.format("Failed to load package %s|%s", + theNpmPackageEntity.getPackageId(), theNpmPackageEntity.getVersionId()); + ourLog.error(msg); + throw new ConfigurationException(Msg.code(2813) + msg, ex); } List pkgSps = myPackageResourceParsingSvc.parseResourcesOfType("SearchParameter", pkg); From f475040809a3a813a0d6b4899c8f02a560b6a6d8 Mon Sep 17 00:00:00 2001 From: leif stawnyczy Date: Tue, 9 Sep 2025 09:25:07 -0400 Subject: [PATCH 30/36] updating a test --- .../fhir/jpa/packages/NpmJpaValidationSupport.java | 11 +++++++---- .../fhir/jpa/packages/NpmJpaValidationSupportIT.java | 4 ++-- 2 files changed, 9 insertions(+), 6 deletions(-) diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/packages/NpmJpaValidationSupport.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/packages/NpmJpaValidationSupport.java index 5362d275dad0..b95a7b037c6b 100644 --- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/packages/NpmJpaValidationSupport.java +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/packages/NpmJpaValidationSupport.java @@ -111,8 +111,10 @@ public List fetchAllSearchParameters() { PagingIterator iterator = new PagingIterator<>(1000, (thePageIndex, theBatchSize, theConsumer) -> { myPackageVersionDao - .findAll(Pageable.ofSize(theBatchSize).withPage(thePageIndex).getSortOr(Sort.by(Sort.Direction.ASC, "myPackageId", "myVersionId"))) - .forEach(theConsumer); + .findAll(Pageable.ofSize(theBatchSize) + .withPage(thePageIndex) + .getSortOr(Sort.by(Sort.Direction.ASC, "myPackageId", "myVersionId"))) + .forEach(theConsumer); }); // do we want to cache this? @@ -141,8 +143,9 @@ private void getAllSP(NpmPackageVersionEntity theNpmPa theNpmPackageEntity.getPackageId(), theNpmPackageEntity.getVersionId()); } catch (IOException ex) { // we shouldn't ever really see this - String msg = String.format("Failed to load package %s|%s", - theNpmPackageEntity.getPackageId(), theNpmPackageEntity.getVersionId()); + String msg = String.format( + "Failed to load package %s|%s", + theNpmPackageEntity.getPackageId(), theNpmPackageEntity.getVersionId()); ourLog.error(msg); throw new ConfigurationException(Msg.code(2813) + msg, ex); } diff --git a/hapi-fhir-jpaserver-test-r4/src/test/java/ca/uhn/fhir/jpa/packages/NpmJpaValidationSupportIT.java b/hapi-fhir-jpaserver-test-r4/src/test/java/ca/uhn/fhir/jpa/packages/NpmJpaValidationSupportIT.java index df22767e3c25..4f8f056c2c99 100644 --- a/hapi-fhir-jpaserver-test-r4/src/test/java/ca/uhn/fhir/jpa/packages/NpmJpaValidationSupportIT.java +++ b/hapi-fhir-jpaserver-test-r4/src/test/java/ca/uhn/fhir/jpa/packages/NpmJpaValidationSupportIT.java @@ -17,7 +17,6 @@ import java.nio.file.Files; import java.nio.file.Path; import java.util.ArrayList; -import java.util.Arrays; import java.util.HashMap; import java.util.List; import java.util.Map; @@ -72,7 +71,7 @@ public void fetchAllSearchParameters_multipleVersions_returnVersionsHigherToLowe String name = "ig-test-dir"; // deliberately not in order to ensure the loading order // is always in order - String[] versions = new String[] { "1.1.1", "3.1.2", "2.2.2" }; + String[] versions = new String[] { "1.1.1", "3.3.3", "2.2.2" }; List orderedVersions = new ArrayList<>(); SearchParameter generated = generateSP(); @@ -95,6 +94,7 @@ public void fetchAllSearchParameters_multipleVersions_returnVersionsHigherToLowe // verify assertNotNull(sps); assertEquals(orderedVersions.size(), sps.size()); + ourLog.info(String.join(", ", orderedVersions)); for (int i = 0; i < sps.size(); i++) { SearchParameter sp = (SearchParameter) sps.get(i); String version = orderedVersions.get(i); From e62c99f3b4b31ba7b0068f446b7331550c872751 Mon Sep 17 00:00:00 2001 From: leif stawnyczy Date: Tue, 9 Sep 2025 10:53:31 -0400 Subject: [PATCH 31/36] all the new stuff --- .../uhn/fhir/context/RuntimeSearchParam.java | 76 +++++++++++++++++++ .../ca/uhn/fhir/util/SearchParameterUtil.java | 17 +++++ .../registry/ReadOnlySearchParamCache.java | 6 +- .../registry/SearchParamRegistryImpl.java | 43 ++++++++++- .../SearchParamExtractorDstu3Test.java | 6 ++ .../registry/RuntimeSearchParamCacheTest.java | 1 - .../registry/SearchParamRegistryImplTest.java | 76 ++++++++++++++++++- .../server/RestfulServerConfiguration.java | 5 ++ .../util/FhirContextSearchParamRegistry.java | 5 ++ .../server/util/ISearchParamRegistry.java | 8 ++ 10 files changed, 234 insertions(+), 9 deletions(-) diff --git a/hapi-fhir-base/src/main/java/ca/uhn/fhir/context/RuntimeSearchParam.java b/hapi-fhir-base/src/main/java/ca/uhn/fhir/context/RuntimeSearchParam.java index 876136b2f717..23ace0a89910 100644 --- a/hapi-fhir-base/src/main/java/ca/uhn/fhir/context/RuntimeSearchParam.java +++ b/hapi-fhir-base/src/main/java/ca/uhn/fhir/context/RuntimeSearchParam.java @@ -44,6 +44,31 @@ import static org.apache.commons.lang3.StringUtils.trim; public class RuntimeSearchParam { + /** + * Enum defining the source of the SearchParameter + * (ie, where it's from) + */ + public enum Source { + /** + * Unknown source; a source should always be provided, + * but if one cannot be, specify UNKNOWN. + */ + UNKNOWN, + /** + * The SP is built-into the base FHIR models. + */ + BUILT_IN, + /** + * The originating source is the DB (this was saved + * via an API call or the like). + */ + DATABASE, + /** + * The originating source is an IG. + */ + IMPLEMENTATION_GUIDE; + } + private final IIdType myId; private final Set myBase; private final String myDescription; @@ -62,6 +87,28 @@ public class RuntimeSearchParam { private IPhoneticEncoder myPhoneticEncoder; private boolean myEnabledForSearching = true; + /** + * The originating source of this RuntimeSearchParameter. + */ + private Source myOriginatingSource = Source.UNKNOWN; + + /** + * The name of the source Implementation Guide + */ + private String myIGName; + + /** + * The version of the IG + */ + private String myIGVersion; + + /** + * The version of the SP itself. + * If this SP comes from a builtin fhircontext, + * this will be the FHIR version. + */ + private String mySPVersion; + /** * Constructor */ @@ -185,6 +232,35 @@ public void setEnabledForSearching(boolean theEnabledForSearching) { myEnabledForSearching = theEnabledForSearching; } + public Source getOriginatingSource() { + return myOriginatingSource; + } + + public void setOriginatingSource(Source theOriginatingSource) { + myOriginatingSource = theOriginatingSource; + } + + public String getIGName() { + return myIGName; + } + + public String getIGVersion() { + return myIGVersion; + } + + public void setIGUrlAndVersion(String theIGUrl, String theVersion) { + myIGName = theIGUrl; + myIGVersion = theVersion; + } + + public String getSPVersion() { + return mySPVersion; + } + + public void setSPVersion(String theSPVersion) { + mySPVersion = theSPVersion; + } + public List getComponents() { return myComponents; } diff --git a/hapi-fhir-base/src/main/java/ca/uhn/fhir/util/SearchParameterUtil.java b/hapi-fhir-base/src/main/java/ca/uhn/fhir/util/SearchParameterUtil.java index 9bfc71e869a4..ed67eb3e43b1 100644 --- a/hapi-fhir-base/src/main/java/ca/uhn/fhir/util/SearchParameterUtil.java +++ b/hapi-fhir-base/src/main/java/ca/uhn/fhir/util/SearchParameterUtil.java @@ -49,6 +49,7 @@ import java.util.Map; import java.util.Optional; import java.util.Set; +import java.util.function.Function; import java.util.stream.Collectors; import static org.apache.commons.lang3.StringUtils.isBlank; @@ -420,6 +421,22 @@ private static boolean allParensHaveBeenClosed(String thePaths) { return open == close; } + /** + * Retrieves the first value in the provided field, or null or no such field exists or isn't populated. + * @param theTerser + * @param theSP + * @param theField + * @return + */ + public static IBase getFirstFieldValueOrNull(FhirTerser theTerser, IBaseResource theSP, String theField) { + List fieldValues = theTerser.getValues(theSP, theField); + + if (fieldValues != null && !fieldValues.isEmpty()) { + return fieldValues.get(0); + } + return null; + } + /** * Given a FHIRPath expression which presumably addresses a FHIR reference or * canonical reference element (i.e. a FHIRPath expression used in a "reference" diff --git a/hapi-fhir-jpaserver-searchparam/src/main/java/ca/uhn/fhir/jpa/searchparam/registry/ReadOnlySearchParamCache.java b/hapi-fhir-jpaserver-searchparam/src/main/java/ca/uhn/fhir/jpa/searchparam/registry/ReadOnlySearchParamCache.java index a974d1b33cc3..40207649b004 100644 --- a/hapi-fhir-jpaserver-searchparam/src/main/java/ca/uhn/fhir/jpa/searchparam/registry/ReadOnlySearchParamCache.java +++ b/hapi-fhir-jpaserver-searchparam/src/main/java/ca/uhn/fhir/jpa/searchparam/registry/ReadOnlySearchParamCache.java @@ -25,6 +25,7 @@ import ca.uhn.fhir.rest.server.util.ResourceSearchParams; import jakarta.annotation.Nonnull; import jakarta.annotation.Nullable; +import org.apache.commons.lang3.ObjectUtils; import org.apache.commons.lang3.Validate; import org.hl7.fhir.instance.model.api.IBaseResource; @@ -36,8 +37,6 @@ import java.util.Set; import java.util.stream.Stream; -import static org.apache.commons.lang3.ObjectUtils.defaultIfNull; - public class ReadOnlySearchParamCache { // resourceName -> searchParamName -> searchparam @@ -101,7 +100,7 @@ public static ReadOnlySearchParamCache fromFhirContext( FhirContext.forCached(theFhirContext.getVersion().getVersion()); List searchParams = cachedCtx.getValidationSupport().fetchAllSearchParameters(); - searchParams = defaultIfNull(searchParams, Collections.emptyList()); + searchParams = ObjectUtils.getIfNull(searchParams, Collections.emptyList()); for (IBaseResource next : searchParams) { RuntimeSearchParam nextCanonical = theCanonicalizer.canonicalizeSearchParameter(next); @@ -123,6 +122,7 @@ public static ReadOnlySearchParamCache fromFhirContext( nextCanonical.getComboSearchParamType(), nextCanonical.getComponents(), nextCanonical.getBase()); + nextCanonical.setOriginatingSource(RuntimeSearchParam.Source.BUILT_IN); Collection base = nextCanonical.getBase(); if (base.contains("Resource") || base.contains("DomainResource")) { diff --git a/hapi-fhir-jpaserver-searchparam/src/main/java/ca/uhn/fhir/jpa/searchparam/registry/SearchParamRegistryImpl.java b/hapi-fhir-jpaserver-searchparam/src/main/java/ca/uhn/fhir/jpa/searchparam/registry/SearchParamRegistryImpl.java index bf3775fd0d31..994cb8647b4a 100644 --- a/hapi-fhir-jpaserver-searchparam/src/main/java/ca/uhn/fhir/jpa/searchparam/registry/SearchParamRegistryImpl.java +++ b/hapi-fhir-jpaserver-searchparam/src/main/java/ca/uhn/fhir/jpa/searchparam/registry/SearchParamRegistryImpl.java @@ -41,6 +41,7 @@ import ca.uhn.fhir.rest.server.util.ISearchParamRegistry; import ca.uhn.fhir.rest.server.util.IndexedSearchParam; import ca.uhn.fhir.rest.server.util.ResourceSearchParams; +import ca.uhn.fhir.util.FhirTerser; import ca.uhn.fhir.util.SearchParameterUtil; import ca.uhn.fhir.util.StopWatch; import com.google.common.annotations.VisibleForTesting; @@ -51,8 +52,10 @@ import jakarta.annotation.PreDestroy; import org.apache.commons.lang3.StringUtils; import org.apache.commons.lang3.time.DateUtils; +import org.hl7.fhir.instance.model.api.IBase; import org.hl7.fhir.instance.model.api.IBaseResource; import org.hl7.fhir.instance.model.api.IIdType; +import org.jetbrains.annotations.NotNull; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.ObjectProvider; @@ -68,6 +71,7 @@ import java.util.Objects; import java.util.Optional; import java.util.Set; +import java.util.function.Function; import java.util.stream.Collectors; import static ca.uhn.fhir.rest.server.util.ISearchParamRegistry.isAllowedForContext; @@ -217,10 +221,29 @@ public RuntimeSearchParam getActiveSearchParamByUrl( return null; } + @Override + public List getActiveSearchParamsByName(@NotNull String theName, @NotNull ISearchParamRegistry.SearchParamLookupContextEnum theContext) { + List sps = new ArrayList<>(); + if (myActiveSearchParams != null) { + myActiveSearchParams.getSearchParamStream() + .forEach(sp -> { + if (sp.getName().equals(theName)) { + sps.add(sp); + } + }); + } + return sps; + } + @Override public void addActiveSearchParameterToLocalCache(@Nonnull RuntimeSearchParam theSearchParam) { assert !isBlank(theSearchParam.getName()); assert theSearchParam.getBase() != null && !theSearchParam.getBase().isEmpty(); + assert theSearchParam.getOriginatingSource() != null; + + if (theSearchParam.getOriginatingSource() == RuntimeSearchParam.Source.UNKNOWN) { + ourLog.warn("SearchParameter added with unknown source."); + } for (String rt : theSearchParam.getBase()) { myLocalSPCache.add(rt, theSearchParam.getName(), theSearchParam, true); @@ -415,9 +438,19 @@ private long overrideBuiltinSearchParamsWithActiveJpaSearchParams( return 0; } + FhirTerser terser = myFhirContext.newTerser(); long retval = 0; + Function conversion = (spResource) -> { + RuntimeSearchParam rtsp = mySearchParameterCanonicalizer.canonicalizeSearchParameter(spResource); + rtsp.setOriginatingSource(RuntimeSearchParam.Source.DATABASE); + IBase versionField = SearchParameterUtil.getFirstFieldValueOrNull(terser, spResource, "version"); + if (versionField != null) { + rtsp.setSPVersion(versionField.toString()); + } + return rtsp; + }; for (IBaseResource searchParam : theSearchParams) { - retval += overrideSearchParam(theSearchParamCache, searchParam); + retval += overrideSearchParam(theSearchParamCache, searchParam, conversion); } return retval; } @@ -429,13 +462,16 @@ private long overrideBuiltinSearchParamsWithActiveJpaSearchParams( * * @param theSearchParams The cache to populate * @param theSearchParameter The SearchParameter to insert into the cache and potentially replace existing params + * @param theConvertFn A function used to convert an IBaseResource SearchParameter resource into a RuntimeSearchParameter + * to be added to the cache. */ - private long overrideSearchParam(RuntimeSearchParamCache theSearchParams, IBaseResource theSearchParameter) { + private long overrideSearchParam(RuntimeSearchParamCache theSearchParams, IBaseResource theSearchParameter, Function theConvertFn) { if (theSearchParameter == null) { return 0; } - RuntimeSearchParam runtimeSp = mySearchParameterCanonicalizer.canonicalizeSearchParameter(theSearchParameter); + // convert the SP into a RuntimeSearchParameter + RuntimeSearchParam runtimeSp = theConvertFn.apply(theSearchParameter); if (runtimeSp == null) { return 0; } @@ -479,6 +515,7 @@ private long overrideSearchParam(RuntimeSearchParamCache theSearchParams, IBaseR expandBaseList(SearchParameterUtil.getBaseAsStrings(myFhirContext, theSearchParameter))) { String name = runtimeSp.getName(); theSearchParams.add(nextBaseName, name, runtimeSp); + ourLog.debug( "Adding search parameter {}.{} to SearchParamRegistry", nextBaseName, diff --git a/hapi-fhir-jpaserver-searchparam/src/test/java/ca/uhn/fhir/jpa/searchparam/extractor/SearchParamExtractorDstu3Test.java b/hapi-fhir-jpaserver-searchparam/src/test/java/ca/uhn/fhir/jpa/searchparam/extractor/SearchParamExtractorDstu3Test.java index 71d12ac6c35d..ab0af4e857aa 100644 --- a/hapi-fhir-jpaserver-searchparam/src/test/java/ca/uhn/fhir/jpa/searchparam/extractor/SearchParamExtractorDstu3Test.java +++ b/hapi-fhir-jpaserver-searchparam/src/test/java/ca/uhn/fhir/jpa/searchparam/extractor/SearchParamExtractorDstu3Test.java @@ -36,6 +36,7 @@ import org.hl7.fhir.dstu3.model.Patient; import org.hl7.fhir.dstu3.model.Questionnaire; import org.hl7.fhir.instance.model.api.IIdType; +import org.jetbrains.annotations.NotNull; import org.junit.jupiter.api.AfterAll; import org.junit.jupiter.api.Test; @@ -301,6 +302,11 @@ public RuntimeSearchParam getActiveSearchParamByUrl(@Nonnull String theUrl, @Non throw new UnsupportedOperationException(); } + @Override + public List getActiveSearchParamsByName(@NotNull String theName, @NotNull ISearchParamRegistry.SearchParamLookupContextEnum theContext) { + throw new UnsupportedOperationException(); + } + @Override public void addActiveSearchParameterToLocalCache(RuntimeSearchParam theSearchParam) { throw new UnsupportedOperationException(); diff --git a/hapi-fhir-jpaserver-searchparam/src/test/java/ca/uhn/fhir/jpa/searchparam/registry/RuntimeSearchParamCacheTest.java b/hapi-fhir-jpaserver-searchparam/src/test/java/ca/uhn/fhir/jpa/searchparam/registry/RuntimeSearchParamCacheTest.java index 8d1dec63b91e..913996a1d8c4 100644 --- a/hapi-fhir-jpaserver-searchparam/src/test/java/ca/uhn/fhir/jpa/searchparam/registry/RuntimeSearchParamCacheTest.java +++ b/hapi-fhir-jpaserver-searchparam/src/test/java/ca/uhn/fhir/jpa/searchparam/registry/RuntimeSearchParamCacheTest.java @@ -20,7 +20,6 @@ public void before() { myCache = new RuntimeSearchParamCache(); } - @Test public void remove_anExistingSP_removesAllInstances() { // setup diff --git a/hapi-fhir-jpaserver-searchparam/src/test/java/ca/uhn/fhir/jpa/searchparam/registry/SearchParamRegistryImplTest.java b/hapi-fhir-jpaserver-searchparam/src/test/java/ca/uhn/fhir/jpa/searchparam/registry/SearchParamRegistryImplTest.java index 932e806f310a..f6c3554ee23b 100644 --- a/hapi-fhir-jpaserver-searchparam/src/test/java/ca/uhn/fhir/jpa/searchparam/registry/SearchParamRegistryImplTest.java +++ b/hapi-fhir-jpaserver-searchparam/src/test/java/ca/uhn/fhir/jpa/searchparam/registry/SearchParamRegistryImplTest.java @@ -16,6 +16,7 @@ import ca.uhn.fhir.jpa.model.entity.ResourceTable; import ca.uhn.fhir.jpa.model.entity.StorageSettings; import ca.uhn.fhir.jpa.searchparam.MatchUrlService; +import ca.uhn.fhir.jpa.searchparam.SearchParameterMap; import ca.uhn.fhir.jpa.searchparam.extractor.SearchParamExtractorService; import ca.uhn.fhir.jpa.searchparam.matcher.InMemoryMatchResult; import ca.uhn.fhir.jpa.searchparam.matcher.InMemoryResourceMatcher; @@ -24,6 +25,7 @@ import ca.uhn.fhir.model.primitive.IdDt; import ca.uhn.fhir.rest.api.Constants; import ca.uhn.fhir.rest.api.RestSearchParameterTypeEnum; +import ca.uhn.fhir.rest.api.server.IBundleProvider; import ca.uhn.fhir.rest.server.SimpleBundleProvider; import ca.uhn.fhir.rest.server.exceptions.InternalErrorException; import ca.uhn.fhir.rest.server.exceptions.ResourceNotFoundException; @@ -48,6 +50,7 @@ import org.junit.jupiter.api.extension.RegisterExtension; import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.CsvSource; +import org.junit.jupiter.params.provider.EnumSource; import org.junit.jupiter.params.provider.ValueSource; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.mock.mockito.MockBean; @@ -103,7 +106,6 @@ public class SearchParamRegistryImplTest { SearchParamRegistryImpl mySearchParamRegistry; @Autowired private ResourceChangeListenerRegistryImpl myResourceChangeListenerRegistry; - @MockBean private PartitionSettings myPartitionSettings; @MockBean @@ -291,6 +293,27 @@ public void testBuiltInSearchParameter_Text() { assertSame(patientAddress, personAddress); } + @Test + public void getActiveSearchParamsByName_withValidName_returnsListOfParams() { + // test + List sps = mySearchParamRegistry.getActiveSearchParamsByName("name", ISearchParamRegistry.SearchParamLookupContextEnum.ALL); + + // verify + assertFalse(sps.isEmpty()); + assertTrue(sps.size() > 1); + for (RuntimeSearchParam sp : sps) { + assertEquals("name", sp.getName()); + } + } + + @Test + public void getActiveSearchParamsByName_withInvalidName_returnsEmptyList() { + // test + List sps = mySearchParamRegistry.getActiveSearchParamsByName("abcdefghijklmnopqrstuvwxyz", ISearchParamRegistry.SearchParamLookupContextEnum.ALL); + + // verify + assertTrue(sps.isEmpty()); + } @Test public void testGetActiveUniqueSearchParams_Empty() { @@ -331,7 +354,7 @@ public void testGetActiveSearchParamsRetries() { } @Test - public void testAddActiveSearchparam() { + public void testAddActiveSearchParams() { // Initialize the registry mySearchParamRegistry.forceRefresh(); @@ -482,6 +505,55 @@ public void testContentAndTextSearchParamsCanReplaceBuiltIn(String theParamName, } } + @Test + public void getActiveSearchParams_builtIn_haveProperSource() { + // setup + mySearchParamRegistry.forceRefresh(); + + // test + ReadOnlySearchParamCache params = mySearchParamRegistry.getActiveSearchParams(); + + // verify + params.getSearchParamStream() + .forEach(sp -> { + assertEquals(RuntimeSearchParam.Source.BUILT_IN, sp.getOriginatingSource()); + }); + } + + @Test + public void getActiveSearchParams_SPsFromDB_haveProperSource() { + // setup + SearchParameter sp = new SearchParameter(); + sp.addBase("Patient"); + sp.setUrl("http://localhost/test"); + sp.setType(Enumerations.SearchParamType.STRING); + sp.setName("HelloWorld"); + sp.setDescription("description"); + sp.setExpression("Patient.name.given"); + sp.setCode("HelloWorld"); + sp.setStatus(Enumerations.PublicationStatus.ACTIVE); + + IBundleProvider provider = new SimpleBundleProvider( + List.of(sp) + ); + + // when + when(mySearchParamProvider.search(any(SearchParameterMap.class))) + .thenReturn(provider); + + // test + mySearchParamRegistry.forceRefresh(); + + RuntimeSearchParam result = mySearchParamRegistry.getActiveSearchParam("Patient", "HelloWorld", ISearchParamRegistry.SearchParamLookupContextEnum.ALL); + assertNotNull(result); + assertEquals("HelloWorld", result.getName()); + assertEquals(RuntimeSearchParam.Source.DATABASE, result.getOriginatingSource()); + assertNull(result.getIGName()); + assertNull(result.getIGVersion()); + } + + + @ParameterizedTest @ValueSource(booleans = { true, false }) public void addActiveSearchParameterToLocalCache_withValidSP_addsItToCache(boolean theIsActive) { diff --git a/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/RestfulServerConfiguration.java b/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/RestfulServerConfiguration.java index 4c0a58b40d93..3ca8d01e1527 100644 --- a/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/RestfulServerConfiguration.java +++ b/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/RestfulServerConfiguration.java @@ -451,6 +451,11 @@ public RuntimeSearchParam getActiveSearchParamByUrl( throw new UnsupportedOperationException(Msg.code(286)); } + @Override + public List getActiveSearchParamsByName(@Nonnull String theName, @Nonnull SearchParamLookupContextEnum theContext) { + throw new UnsupportedOperationException(Msg.code(2803)); + } + @Override public void addActiveSearchParameterToLocalCache(RuntimeSearchParam theSearchParam) { throw new UnsupportedOperationException(Msg.code(2794)); diff --git a/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/util/FhirContextSearchParamRegistry.java b/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/util/FhirContextSearchParamRegistry.java index b68198fada9a..7fc9268cab68 100644 --- a/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/util/FhirContextSearchParamRegistry.java +++ b/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/util/FhirContextSearchParamRegistry.java @@ -107,6 +107,11 @@ public RuntimeSearchParam getActiveSearchParamByUrl( .orElse(null); } + @Override + public List getActiveSearchParamsByName(@Nonnull String theName, @Nonnull SearchParamLookupContextEnum theContext) { + throw new UnsupportedOperationException(Msg.code(2804)); + } + @Override public void addActiveSearchParameterToLocalCache(RuntimeSearchParam theSearchParam) { throw new UnsupportedOperationException(Msg.code(2792)); diff --git a/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/util/ISearchParamRegistry.java b/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/util/ISearchParamRegistry.java index e391f0a900ee..721d25d6ffab 100644 --- a/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/util/ISearchParamRegistry.java +++ b/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/util/ISearchParamRegistry.java @@ -153,6 +153,14 @@ default Collection getValidSearchParameterNamesIncludingMeta( RuntimeSearchParam getActiveSearchParamByUrl( @Nonnull String theUrl, @Nonnull SearchParamLookupContextEnum theContext); + /** + * Gets a list of RuntimeSearchParam objects by name. + * @param theName the name of the SP to look for. + * @param theContext Context + * @return + */ + List getActiveSearchParamsByName(@Nonnull String theName, @Nonnull SearchParamLookupContextEnum theContext); + /** * Find a search param for a resource. First, check the resource itself, then check the top-level `Resource` resource. * From 69a3c286b3dced0d81929239c131745cf3db5cb9 Mon Sep 17 00:00:00 2001 From: leif stawnyczy Date: Tue, 9 Sep 2025 14:03:51 -0400 Subject: [PATCH 32/36] review points --- .../uhn/fhir/context/RuntimeSearchParam.java | 77 ++------------- .../context/RuntimeSearchParamSource.java | 98 +++++++++++++++++++ ...rofileValidationSupportBundleStrategy.java | 2 - .../ca/uhn/fhir/util/SearchParameterUtil.java | 1 - .../registry/ReadOnlySearchParamCache.java | 3 +- .../registry/SearchParamRegistryImpl.java | 30 ++---- .../SearchParamExtractorDstu3Test.java | 6 -- .../registry/SearchParamRegistryImplTest.java | 34 +------ .../server/RestfulServerConfiguration.java | 5 - .../util/FhirContextSearchParamRegistry.java | 5 - .../server/util/ISearchParamRegistry.java | 8 -- .../support/ValidationSupportChain.java | 3 + 12 files changed, 126 insertions(+), 146 deletions(-) create mode 100644 hapi-fhir-base/src/main/java/ca/uhn/fhir/context/RuntimeSearchParamSource.java diff --git a/hapi-fhir-base/src/main/java/ca/uhn/fhir/context/RuntimeSearchParam.java b/hapi-fhir-base/src/main/java/ca/uhn/fhir/context/RuntimeSearchParam.java index 23ace0a89910..77258cf84f58 100644 --- a/hapi-fhir-base/src/main/java/ca/uhn/fhir/context/RuntimeSearchParam.java +++ b/hapi-fhir-base/src/main/java/ca/uhn/fhir/context/RuntimeSearchParam.java @@ -44,30 +44,6 @@ import static org.apache.commons.lang3.StringUtils.trim; public class RuntimeSearchParam { - /** - * Enum defining the source of the SearchParameter - * (ie, where it's from) - */ - public enum Source { - /** - * Unknown source; a source should always be provided, - * but if one cannot be, specify UNKNOWN. - */ - UNKNOWN, - /** - * The SP is built-into the base FHIR models. - */ - BUILT_IN, - /** - * The originating source is the DB (this was saved - * via an API call or the like). - */ - DATABASE, - /** - * The originating source is an IG. - */ - IMPLEMENTATION_GUIDE; - } private final IIdType myId; private final Set myBase; @@ -88,27 +64,11 @@ public enum Source { private boolean myEnabledForSearching = true; /** - * The originating source of this RuntimeSearchParameter. - */ - private Source myOriginatingSource = Source.UNKNOWN; - - /** - * The name of the source Implementation Guide + * The source of this SP. + * We have it initialized so it's never null; but seeding services + * should populate this value */ - private String myIGName; - - /** - * The version of the IG - */ - private String myIGVersion; - - /** - * The version of the SP itself. - * If this SP comes from a builtin fhircontext, - * this will be the FHIR version. - */ - private String mySPVersion; - + private RuntimeSearchParamSource mySource = new RuntimeSearchParamSource(); /** * Constructor */ @@ -232,33 +192,12 @@ public void setEnabledForSearching(boolean theEnabledForSearching) { myEnabledForSearching = theEnabledForSearching; } - public Source getOriginatingSource() { - return myOriginatingSource; - } - - public void setOriginatingSource(Source theOriginatingSource) { - myOriginatingSource = theOriginatingSource; - } - - public String getIGName() { - return myIGName; - } - - public String getIGVersion() { - return myIGVersion; - } - - public void setIGUrlAndVersion(String theIGUrl, String theVersion) { - myIGName = theIGUrl; - myIGVersion = theVersion; - } - - public String getSPVersion() { - return mySPVersion; + public RuntimeSearchParamSource getSource() { + return mySource; } - public void setSPVersion(String theSPVersion) { - mySPVersion = theSPVersion; + public void setSource(RuntimeSearchParamSource theSource) { + mySource = theSource; } public List getComponents() { diff --git a/hapi-fhir-base/src/main/java/ca/uhn/fhir/context/RuntimeSearchParamSource.java b/hapi-fhir-base/src/main/java/ca/uhn/fhir/context/RuntimeSearchParamSource.java new file mode 100644 index 000000000000..452ff08c443a --- /dev/null +++ b/hapi-fhir-base/src/main/java/ca/uhn/fhir/context/RuntimeSearchParamSource.java @@ -0,0 +1,98 @@ +package ca.uhn.fhir.context; + +public class RuntimeSearchParamSource { + /** + * Enum defining the source of the SearchParameter + * (ie, where it's from) + */ + public enum SourceType { + /** + * Unknown source; a source should always be provided, + * but if one cannot be, specify UNKNOWN. + */ + UNKNOWN, + /** + * The SP is built-into the base FHIR models. + */ + BUILT_IN, + /** + * The originating source is the DB (this was saved + * via an API call or the like). + */ + DATABASE, + /** + * The originating source is an IG. + */ + IMPLEMENTATION_GUIDE; + } + + public static RuntimeSearchParamSource builtinSource() { + RuntimeSearchParamSource source = new RuntimeSearchParamSource(); + source.setOriginatingSource(SourceType.BUILT_IN); + return source; + } + + public static RuntimeSearchParamSource databaseSource() { + RuntimeSearchParamSource source = new RuntimeSearchParamSource(); + source.setOriginatingSource(SourceType.DATABASE); + return source; + } + + public static RuntimeSearchParamSource implementationGuid(String theIG, String theIGVersion) { + RuntimeSearchParamSource source = new RuntimeSearchParamSource(); + source.setOriginatingSource(SourceType.IMPLEMENTATION_GUIDE); + source.setIGUrlAndVersion(theIG, theIGVersion); + return source; + } + + /** + * The originating source of this RuntimeSearchParameter. + */ + private SourceType myOriginatingSource = SourceType.UNKNOWN; + + /** + * The name of the source Implementation Guide + */ + private String myIGName; + + /** + * The version of the IG + */ + private String myIGVersion; + + /** + * The version of the SP itself. + * If this SP comes from a builtin fhircontext, + * this will be the FHIR version. + */ + private String mySPVersion; + + public SourceType getOriginatingSource() { + return myOriginatingSource; + } + + public void setOriginatingSource(SourceType theOriginatingSource) { + myOriginatingSource = theOriginatingSource; + } + + public String getIGName() { + return myIGName; + } + + public String getIGVersion() { + return myIGVersion; + } + + public void setIGUrlAndVersion(String theIGUrl, String theVersion) { + myIGName = theIGUrl; + myIGVersion = theVersion; + } + + public String getSPVersion() { + return mySPVersion; + } + + public void setSPVersion(String theSPVersion) { + mySPVersion = theSPVersion; + } +} diff --git a/hapi-fhir-base/src/main/java/ca/uhn/fhir/context/support/DefaultProfileValidationSupportBundleStrategy.java b/hapi-fhir-base/src/main/java/ca/uhn/fhir/context/support/DefaultProfileValidationSupportBundleStrategy.java index e8938a48802c..0fe1711d5092 100644 --- a/hapi-fhir-base/src/main/java/ca/uhn/fhir/context/support/DefaultProfileValidationSupportBundleStrategy.java +++ b/hapi-fhir-base/src/main/java/ca/uhn/fhir/context/support/DefaultProfileValidationSupportBundleStrategy.java @@ -29,7 +29,6 @@ import ca.uhn.fhir.rest.api.EncodingEnum; import ca.uhn.fhir.util.BundleUtil; import ca.uhn.fhir.util.ClasspathUtil; -import ca.uhn.fhir.util.FhirTerser; import jakarta.annotation.Nullable; import org.apache.commons.lang3.StringUtils; import org.hl7.fhir.instance.model.api.IBaseBundle; @@ -187,7 +186,6 @@ public List fetchAllSearchParameters() { retVal = new ArrayList<>(); for (String searchParameterResource : mySearchParameterResources) { try (InputStream inputStream = ClasspathUtil.loadResourceAsStream(searchParameterResource)) { - FhirTerser terser = myCtx.newTerser(); EncodingEnum encoding = searchParameterResource.endsWith("json") ? EncodingEnum.JSON : EncodingEnum.XML; List resources = diff --git a/hapi-fhir-base/src/main/java/ca/uhn/fhir/util/SearchParameterUtil.java b/hapi-fhir-base/src/main/java/ca/uhn/fhir/util/SearchParameterUtil.java index ed67eb3e43b1..162aed1ece29 100644 --- a/hapi-fhir-base/src/main/java/ca/uhn/fhir/util/SearchParameterUtil.java +++ b/hapi-fhir-base/src/main/java/ca/uhn/fhir/util/SearchParameterUtil.java @@ -49,7 +49,6 @@ import java.util.Map; import java.util.Optional; import java.util.Set; -import java.util.function.Function; import java.util.stream.Collectors; import static org.apache.commons.lang3.StringUtils.isBlank; diff --git a/hapi-fhir-jpaserver-searchparam/src/main/java/ca/uhn/fhir/jpa/searchparam/registry/ReadOnlySearchParamCache.java b/hapi-fhir-jpaserver-searchparam/src/main/java/ca/uhn/fhir/jpa/searchparam/registry/ReadOnlySearchParamCache.java index 40207649b004..5a2a9ead2e8e 100644 --- a/hapi-fhir-jpaserver-searchparam/src/main/java/ca/uhn/fhir/jpa/searchparam/registry/ReadOnlySearchParamCache.java +++ b/hapi-fhir-jpaserver-searchparam/src/main/java/ca/uhn/fhir/jpa/searchparam/registry/ReadOnlySearchParamCache.java @@ -22,6 +22,7 @@ import ca.uhn.fhir.context.FhirContext; import ca.uhn.fhir.context.RuntimeResourceDefinition; import ca.uhn.fhir.context.RuntimeSearchParam; +import ca.uhn.fhir.context.RuntimeSearchParamSource; import ca.uhn.fhir.rest.server.util.ResourceSearchParams; import jakarta.annotation.Nonnull; import jakarta.annotation.Nullable; @@ -122,7 +123,7 @@ public static ReadOnlySearchParamCache fromFhirContext( nextCanonical.getComboSearchParamType(), nextCanonical.getComponents(), nextCanonical.getBase()); - nextCanonical.setOriginatingSource(RuntimeSearchParam.Source.BUILT_IN); + nextCanonical.setSource(RuntimeSearchParamSource.builtinSource()); Collection base = nextCanonical.getBase(); if (base.contains("Resource") || base.contains("DomainResource")) { diff --git a/hapi-fhir-jpaserver-searchparam/src/main/java/ca/uhn/fhir/jpa/searchparam/registry/SearchParamRegistryImpl.java b/hapi-fhir-jpaserver-searchparam/src/main/java/ca/uhn/fhir/jpa/searchparam/registry/SearchParamRegistryImpl.java index 994cb8647b4a..6e6a9fc65400 100644 --- a/hapi-fhir-jpaserver-searchparam/src/main/java/ca/uhn/fhir/jpa/searchparam/registry/SearchParamRegistryImpl.java +++ b/hapi-fhir-jpaserver-searchparam/src/main/java/ca/uhn/fhir/jpa/searchparam/registry/SearchParamRegistryImpl.java @@ -22,6 +22,7 @@ import ca.uhn.fhir.context.ComboSearchParamType; import ca.uhn.fhir.context.FhirContext; import ca.uhn.fhir.context.RuntimeSearchParam; +import ca.uhn.fhir.context.RuntimeSearchParamSource; import ca.uhn.fhir.context.phonetic.IPhoneticEncoder; import ca.uhn.fhir.i18n.Msg; import ca.uhn.fhir.interceptor.api.IInterceptorService; @@ -55,7 +56,6 @@ import org.hl7.fhir.instance.model.api.IBase; import org.hl7.fhir.instance.model.api.IBaseResource; import org.hl7.fhir.instance.model.api.IIdType; -import org.jetbrains.annotations.NotNull; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.ObjectProvider; @@ -221,27 +221,13 @@ public RuntimeSearchParam getActiveSearchParamByUrl( return null; } - @Override - public List getActiveSearchParamsByName(@NotNull String theName, @NotNull ISearchParamRegistry.SearchParamLookupContextEnum theContext) { - List sps = new ArrayList<>(); - if (myActiveSearchParams != null) { - myActiveSearchParams.getSearchParamStream() - .forEach(sp -> { - if (sp.getName().equals(theName)) { - sps.add(sp); - } - }); - } - return sps; - } - @Override public void addActiveSearchParameterToLocalCache(@Nonnull RuntimeSearchParam theSearchParam) { assert !isBlank(theSearchParam.getName()); assert theSearchParam.getBase() != null && !theSearchParam.getBase().isEmpty(); - assert theSearchParam.getOriginatingSource() != null; + assert theSearchParam.getSource() != null; - if (theSearchParam.getOriginatingSource() == RuntimeSearchParam.Source.UNKNOWN) { + if (theSearchParam.getSource().getOriginatingSource() == RuntimeSearchParamSource.SourceType.UNKNOWN) { ourLog.warn("SearchParameter added with unknown source."); } @@ -442,10 +428,11 @@ private long overrideBuiltinSearchParamsWithActiveJpaSearchParams( long retval = 0; Function conversion = (spResource) -> { RuntimeSearchParam rtsp = mySearchParameterCanonicalizer.canonicalizeSearchParameter(spResource); - rtsp.setOriginatingSource(RuntimeSearchParam.Source.DATABASE); + RuntimeSearchParamSource source = RuntimeSearchParamSource.databaseSource(); + rtsp.setSource(source); IBase versionField = SearchParameterUtil.getFirstFieldValueOrNull(terser, spResource, "version"); if (versionField != null) { - rtsp.setSPVersion(versionField.toString()); + source.setSPVersion(versionField.toString()); } return rtsp; }; @@ -465,7 +452,10 @@ private long overrideBuiltinSearchParamsWithActiveJpaSearchParams( * @param theConvertFn A function used to convert an IBaseResource SearchParameter resource into a RuntimeSearchParameter * to be added to the cache. */ - private long overrideSearchParam(RuntimeSearchParamCache theSearchParams, IBaseResource theSearchParameter, Function theConvertFn) { + private long overrideSearchParam( + RuntimeSearchParamCache theSearchParams, + IBaseResource theSearchParameter, + Function theConvertFn) { if (theSearchParameter == null) { return 0; } diff --git a/hapi-fhir-jpaserver-searchparam/src/test/java/ca/uhn/fhir/jpa/searchparam/extractor/SearchParamExtractorDstu3Test.java b/hapi-fhir-jpaserver-searchparam/src/test/java/ca/uhn/fhir/jpa/searchparam/extractor/SearchParamExtractorDstu3Test.java index ab0af4e857aa..71d12ac6c35d 100644 --- a/hapi-fhir-jpaserver-searchparam/src/test/java/ca/uhn/fhir/jpa/searchparam/extractor/SearchParamExtractorDstu3Test.java +++ b/hapi-fhir-jpaserver-searchparam/src/test/java/ca/uhn/fhir/jpa/searchparam/extractor/SearchParamExtractorDstu3Test.java @@ -36,7 +36,6 @@ import org.hl7.fhir.dstu3.model.Patient; import org.hl7.fhir.dstu3.model.Questionnaire; import org.hl7.fhir.instance.model.api.IIdType; -import org.jetbrains.annotations.NotNull; import org.junit.jupiter.api.AfterAll; import org.junit.jupiter.api.Test; @@ -302,11 +301,6 @@ public RuntimeSearchParam getActiveSearchParamByUrl(@Nonnull String theUrl, @Non throw new UnsupportedOperationException(); } - @Override - public List getActiveSearchParamsByName(@NotNull String theName, @NotNull ISearchParamRegistry.SearchParamLookupContextEnum theContext) { - throw new UnsupportedOperationException(); - } - @Override public void addActiveSearchParameterToLocalCache(RuntimeSearchParam theSearchParam) { throw new UnsupportedOperationException(); diff --git a/hapi-fhir-jpaserver-searchparam/src/test/java/ca/uhn/fhir/jpa/searchparam/registry/SearchParamRegistryImplTest.java b/hapi-fhir-jpaserver-searchparam/src/test/java/ca/uhn/fhir/jpa/searchparam/registry/SearchParamRegistryImplTest.java index f6c3554ee23b..9cec0084c22a 100644 --- a/hapi-fhir-jpaserver-searchparam/src/test/java/ca/uhn/fhir/jpa/searchparam/registry/SearchParamRegistryImplTest.java +++ b/hapi-fhir-jpaserver-searchparam/src/test/java/ca/uhn/fhir/jpa/searchparam/registry/SearchParamRegistryImplTest.java @@ -2,6 +2,7 @@ import ca.uhn.fhir.context.FhirContext; import ca.uhn.fhir.context.RuntimeSearchParam; +import ca.uhn.fhir.context.RuntimeSearchParamSource; import ca.uhn.fhir.context.support.IValidationSupport; import ca.uhn.fhir.interceptor.api.IInterceptorService; import ca.uhn.fhir.jpa.cache.IResourceChangeListenerRegistry; @@ -50,7 +51,6 @@ import org.junit.jupiter.api.extension.RegisterExtension; import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.CsvSource; -import org.junit.jupiter.params.provider.EnumSource; import org.junit.jupiter.params.provider.ValueSource; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.mock.mockito.MockBean; @@ -293,28 +293,6 @@ public void testBuiltInSearchParameter_Text() { assertSame(patientAddress, personAddress); } - @Test - public void getActiveSearchParamsByName_withValidName_returnsListOfParams() { - // test - List sps = mySearchParamRegistry.getActiveSearchParamsByName("name", ISearchParamRegistry.SearchParamLookupContextEnum.ALL); - - // verify - assertFalse(sps.isEmpty()); - assertTrue(sps.size() > 1); - for (RuntimeSearchParam sp : sps) { - assertEquals("name", sp.getName()); - } - } - - @Test - public void getActiveSearchParamsByName_withInvalidName_returnsEmptyList() { - // test - List sps = mySearchParamRegistry.getActiveSearchParamsByName("abcdefghijklmnopqrstuvwxyz", ISearchParamRegistry.SearchParamLookupContextEnum.ALL); - - // verify - assertTrue(sps.isEmpty()); - } - @Test public void testGetActiveUniqueSearchParams_Empty() { assertThat(mySearchParamRegistry.getActiveComboSearchParams("Patient", null)).isEmpty(); @@ -516,7 +494,7 @@ public void getActiveSearchParams_builtIn_haveProperSource() { // verify params.getSearchParamStream() .forEach(sp -> { - assertEquals(RuntimeSearchParam.Source.BUILT_IN, sp.getOriginatingSource()); + assertEquals(RuntimeSearchParamSource.SourceType.BUILT_IN, sp.getSource().getOriginatingSource()); }); } @@ -547,13 +525,11 @@ public void getActiveSearchParams_SPsFromDB_haveProperSource() { RuntimeSearchParam result = mySearchParamRegistry.getActiveSearchParam("Patient", "HelloWorld", ISearchParamRegistry.SearchParamLookupContextEnum.ALL); assertNotNull(result); assertEquals("HelloWorld", result.getName()); - assertEquals(RuntimeSearchParam.Source.DATABASE, result.getOriginatingSource()); - assertNull(result.getIGName()); - assertNull(result.getIGVersion()); + assertEquals(RuntimeSearchParamSource.SourceType.DATABASE, result.getSource().getOriginatingSource()); + assertNull(result.getSource().getIGName()); + assertNull(result.getSource().getIGVersion()); } - - @ParameterizedTest @ValueSource(booleans = { true, false }) public void addActiveSearchParameterToLocalCache_withValidSP_addsItToCache(boolean theIsActive) { diff --git a/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/RestfulServerConfiguration.java b/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/RestfulServerConfiguration.java index 3ca8d01e1527..4c0a58b40d93 100644 --- a/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/RestfulServerConfiguration.java +++ b/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/RestfulServerConfiguration.java @@ -451,11 +451,6 @@ public RuntimeSearchParam getActiveSearchParamByUrl( throw new UnsupportedOperationException(Msg.code(286)); } - @Override - public List getActiveSearchParamsByName(@Nonnull String theName, @Nonnull SearchParamLookupContextEnum theContext) { - throw new UnsupportedOperationException(Msg.code(2803)); - } - @Override public void addActiveSearchParameterToLocalCache(RuntimeSearchParam theSearchParam) { throw new UnsupportedOperationException(Msg.code(2794)); diff --git a/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/util/FhirContextSearchParamRegistry.java b/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/util/FhirContextSearchParamRegistry.java index 7fc9268cab68..b68198fada9a 100644 --- a/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/util/FhirContextSearchParamRegistry.java +++ b/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/util/FhirContextSearchParamRegistry.java @@ -107,11 +107,6 @@ public RuntimeSearchParam getActiveSearchParamByUrl( .orElse(null); } - @Override - public List getActiveSearchParamsByName(@Nonnull String theName, @Nonnull SearchParamLookupContextEnum theContext) { - throw new UnsupportedOperationException(Msg.code(2804)); - } - @Override public void addActiveSearchParameterToLocalCache(RuntimeSearchParam theSearchParam) { throw new UnsupportedOperationException(Msg.code(2792)); diff --git a/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/util/ISearchParamRegistry.java b/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/util/ISearchParamRegistry.java index 721d25d6ffab..e391f0a900ee 100644 --- a/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/util/ISearchParamRegistry.java +++ b/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/util/ISearchParamRegistry.java @@ -153,14 +153,6 @@ default Collection getValidSearchParameterNamesIncludingMeta( RuntimeSearchParam getActiveSearchParamByUrl( @Nonnull String theUrl, @Nonnull SearchParamLookupContextEnum theContext); - /** - * Gets a list of RuntimeSearchParam objects by name. - * @param theName the name of the SP to look for. - * @param theContext Context - * @return - */ - List getActiveSearchParamsByName(@Nonnull String theName, @Nonnull SearchParamLookupContextEnum theContext); - /** * Find a search param for a resource. First, check the resource itself, then check the top-level `Resource` resource. * diff --git a/hapi-fhir-validation/src/main/java/org/hl7/fhir/common/hapi/validation/support/ValidationSupportChain.java b/hapi-fhir-validation/src/main/java/org/hl7/fhir/common/hapi/validation/support/ValidationSupportChain.java index 8b4ecc0159cb..d0349196a1ad 100644 --- a/hapi-fhir-validation/src/main/java/org/hl7/fhir/common/hapi/validation/support/ValidationSupportChain.java +++ b/hapi-fhir-validation/src/main/java/org/hl7/fhir/common/hapi/validation/support/ValidationSupportChain.java @@ -627,6 +627,8 @@ public List fetchAllNonBaseStructureDefinitions() { @Nullable @Override public List fetchAllSearchParameters() { + // TODO - this is not a good order for SPs (since it has the fhir context first, + // and then blocks future ones with the same canonical) FetchAllKey key = new FetchAllKey(FetchAllKey.TypeEnum.ALL_SEARCHPARAMETERS); Supplier> loader = () -> doFetchStructureDefinitions(IValidationSupport::fetchAllSearchParameters); @@ -642,6 +644,7 @@ private List doFetchStructureDefinitions( if (allStructureDefinitions != null) { for (IBaseResource next : allStructureDefinitions) { + // TODO - ask why first in wins? IPrimitiveType urlType = getFhirContext().newTerser().getSingleValueOrNull(next, "url", IPrimitiveType.class); if (urlType == null From 56b5378a1c05b8ad1325cfaa099dd09ac7a02669 Mon Sep 17 00:00:00 2001 From: leif stawnyczy Date: Tue, 9 Sep 2025 16:34:08 -0400 Subject: [PATCH 33/36] review fixes --- ...Source.java => RuntimeResourceSource.java} | 44 ++++++-------- .../uhn/fhir/context/RuntimeSearchParam.java | 7 ++- .../npm}/NpmPackageMetadataLiteJson.java | 14 ++--- .../ca/uhn/fhir/util/NpmPackageUtils.java | 59 ++++++++----------- .../jpa/packages/NpmJpaValidationSupport.java | 4 +- .../registry/ReadOnlySearchParamCache.java | 5 +- .../registry/SearchParamRegistryImpl.java | 14 ++--- .../SearchParameterCanonicalizer.java | 14 +++++ .../registry/SearchParamRegistryImplTest.java | 8 ++- .../packages/NpmJpaValidationSupportIT.java | 7 ++- 10 files changed, 84 insertions(+), 92 deletions(-) rename hapi-fhir-base/src/main/java/ca/uhn/fhir/context/{RuntimeSearchParamSource.java => RuntimeResourceSource.java} (58%) rename {hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/packages => hapi-fhir-base/src/main/java/ca/uhn/fhir/model/npm}/NpmPackageMetadataLiteJson.java (61%) rename hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/packages/util/PackageUtils.java => hapi-fhir-base/src/main/java/ca/uhn/fhir/util/NpmPackageUtils.java (56%) diff --git a/hapi-fhir-base/src/main/java/ca/uhn/fhir/context/RuntimeSearchParamSource.java b/hapi-fhir-base/src/main/java/ca/uhn/fhir/context/RuntimeResourceSource.java similarity index 58% rename from hapi-fhir-base/src/main/java/ca/uhn/fhir/context/RuntimeSearchParamSource.java rename to hapi-fhir-base/src/main/java/ca/uhn/fhir/context/RuntimeResourceSource.java index 452ff08c443a..d0c2c8bf420c 100644 --- a/hapi-fhir-base/src/main/java/ca/uhn/fhir/context/RuntimeSearchParamSource.java +++ b/hapi-fhir-base/src/main/java/ca/uhn/fhir/context/RuntimeResourceSource.java @@ -1,6 +1,8 @@ package ca.uhn.fhir.context; -public class RuntimeSearchParamSource { +import ca.uhn.fhir.model.api.IFhirVersion; + +public class RuntimeResourceSource { /** * Enum defining the source of the SearchParameter * (ie, where it's from) @@ -12,9 +14,9 @@ public enum SourceType { */ UNKNOWN, /** - * The SP is built-into the base FHIR models. + * The Resource is built-into the base FHIR context. */ - BUILT_IN, + FHIR_CONTEXT, /** * The originating source is the DB (this was saved * via an API call or the like). @@ -22,25 +24,28 @@ public enum SourceType { DATABASE, /** * The originating source is an IG. + * These are Implementation Guides from NPM */ - IMPLEMENTATION_GUIDE; + NPM; } - public static RuntimeSearchParamSource builtinSource() { - RuntimeSearchParamSource source = new RuntimeSearchParamSource(); - source.setOriginatingSource(SourceType.BUILT_IN); + public static RuntimeResourceSource fhirContextSource(FhirContext theFhirContext) { + RuntimeResourceSource source = new RuntimeResourceSource(); + source.setOriginatingSource(SourceType.FHIR_CONTEXT); + IFhirVersion fhirVersion = theFhirContext.getVersion(); + source.setIGUrlAndVersion("FhirContext", fhirVersion.getVersion().getFhirVersionString()); return source; } - public static RuntimeSearchParamSource databaseSource() { - RuntimeSearchParamSource source = new RuntimeSearchParamSource(); + public static RuntimeResourceSource databaseSource() { + RuntimeResourceSource source = new RuntimeResourceSource(); source.setOriginatingSource(SourceType.DATABASE); return source; } - public static RuntimeSearchParamSource implementationGuid(String theIG, String theIGVersion) { - RuntimeSearchParamSource source = new RuntimeSearchParamSource(); - source.setOriginatingSource(SourceType.IMPLEMENTATION_GUIDE); + public static RuntimeResourceSource npmSource(String theIG, String theIGVersion) { + RuntimeResourceSource source = new RuntimeResourceSource(); + source.setOriginatingSource(SourceType.NPM); source.setIGUrlAndVersion(theIG, theIGVersion); return source; } @@ -60,13 +65,6 @@ public static RuntimeSearchParamSource implementationGuid(String theIG, String t */ private String myIGVersion; - /** - * The version of the SP itself. - * If this SP comes from a builtin fhircontext, - * this will be the FHIR version. - */ - private String mySPVersion; - public SourceType getOriginatingSource() { return myOriginatingSource; } @@ -87,12 +85,4 @@ public void setIGUrlAndVersion(String theIGUrl, String theVersion) { myIGName = theIGUrl; myIGVersion = theVersion; } - - public String getSPVersion() { - return mySPVersion; - } - - public void setSPVersion(String theSPVersion) { - mySPVersion = theSPVersion; - } } diff --git a/hapi-fhir-base/src/main/java/ca/uhn/fhir/context/RuntimeSearchParam.java b/hapi-fhir-base/src/main/java/ca/uhn/fhir/context/RuntimeSearchParam.java index 77258cf84f58..c6868134f821 100644 --- a/hapi-fhir-base/src/main/java/ca/uhn/fhir/context/RuntimeSearchParam.java +++ b/hapi-fhir-base/src/main/java/ca/uhn/fhir/context/RuntimeSearchParam.java @@ -55,6 +55,7 @@ public class RuntimeSearchParam { private final Set myProvidesMembershipInCompartments; private final RuntimeSearchParamStatusEnum myStatus; private final String myUri; + private String myVersion; private final Map>> myExtensions = new HashMap<>(); private final Map myUpliftRefchains = new HashMap<>(); private final ComboSearchParamType myComboSearchParamType; @@ -68,7 +69,7 @@ public class RuntimeSearchParam { * We have it initialized so it's never null; but seeding services * should populate this value */ - private RuntimeSearchParamSource mySource = new RuntimeSearchParamSource(); + private RuntimeResourceSource mySource = new RuntimeResourceSource(); /** * Constructor */ @@ -192,11 +193,11 @@ public void setEnabledForSearching(boolean theEnabledForSearching) { myEnabledForSearching = theEnabledForSearching; } - public RuntimeSearchParamSource getSource() { + public RuntimeResourceSource getSource() { return mySource; } - public void setSource(RuntimeSearchParamSource theSource) { + public void setSource(RuntimeResourceSource theSource) { mySource = theSource; } diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/packages/NpmPackageMetadataLiteJson.java b/hapi-fhir-base/src/main/java/ca/uhn/fhir/model/npm/NpmPackageMetadataLiteJson.java similarity index 61% rename from hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/packages/NpmPackageMetadataLiteJson.java rename to hapi-fhir-base/src/main/java/ca/uhn/fhir/model/npm/NpmPackageMetadataLiteJson.java index d0384db60038..e460c16da89e 100644 --- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/packages/NpmPackageMetadataLiteJson.java +++ b/hapi-fhir-base/src/main/java/ca/uhn/fhir/model/npm/NpmPackageMetadataLiteJson.java @@ -1,18 +1,16 @@ -package ca.uhn.fhir.jpa.packages; +package ca.uhn.fhir.model.npm; import com.fasterxml.jackson.annotation.JsonAutoDetect; import com.fasterxml.jackson.annotation.JsonInclude; import com.fasterxml.jackson.annotation.JsonProperty; -import io.swagger.v3.oas.annotations.media.Schema; -@Schema(description = "Contains the bare minimum of NpmPackage metadata.") @JsonInclude(JsonInclude.Include.NON_NULL) @JsonAutoDetect( - creatorVisibility = JsonAutoDetect.Visibility.NONE, - fieldVisibility = JsonAutoDetect.Visibility.NONE, - getterVisibility = JsonAutoDetect.Visibility.NONE, - isGetterVisibility = JsonAutoDetect.Visibility.NONE, - setterVisibility = JsonAutoDetect.Visibility.NONE) + creatorVisibility = JsonAutoDetect.Visibility.NONE, + fieldVisibility = JsonAutoDetect.Visibility.NONE, + getterVisibility = JsonAutoDetect.Visibility.NONE, + isGetterVisibility = JsonAutoDetect.Visibility.NONE, + setterVisibility = JsonAutoDetect.Visibility.NONE) public class NpmPackageMetadataLiteJson { /** * The npm package name diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/packages/util/PackageUtils.java b/hapi-fhir-base/src/main/java/ca/uhn/fhir/util/NpmPackageUtils.java similarity index 56% rename from hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/packages/util/PackageUtils.java rename to hapi-fhir-base/src/main/java/ca/uhn/fhir/util/NpmPackageUtils.java index efa6845bd6c0..3912c9f4182f 100644 --- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/packages/util/PackageUtils.java +++ b/hapi-fhir-base/src/main/java/ca/uhn/fhir/util/NpmPackageUtils.java @@ -1,36 +1,17 @@ -/*- - * #%L - * HAPI FHIR JPA Server - * %% - * Copyright (C) 2014 - 2025 Smile CDR, Inc. - * %% - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - * #L% - */ -package ca.uhn.fhir.jpa.packages.util; +package ca.uhn.fhir.util; -import ca.uhn.fhir.jpa.packages.NpmPackageMetadataLiteJson; -import ca.uhn.fhir.util.JsonUtil; +import ca.uhn.fhir.context.RuntimeResourceSource; +import ca.uhn.fhir.context.RuntimeSearchParam; +import ca.uhn.fhir.model.npm.NpmPackageMetadataLiteJson; import com.google.common.collect.Lists; import org.hl7.fhir.instance.model.api.IBaseResource; -import org.hl7.fhir.utilities.npm.NpmPackage; import java.util.Collections; import java.util.List; import static org.apache.commons.lang3.StringUtils.isBlank; -public class PackageUtils { +public class NpmPackageUtils { public static final String LOADER_WITH_CACHE = "loaderWithCache"; @@ -38,13 +19,13 @@ public class PackageUtils { * Default install types */ public static List DEFAULT_INSTALL_TYPES = Collections.unmodifiableList(Lists.newArrayList( - "NamingSystem", - "CodeSystem", - "ValueSet", - "StructureDefinition", - "ConceptMap", - "SearchParameter", - "Subscription")); + "NamingSystem", + "CodeSystem", + "ValueSet", + "StructureDefinition", + "ConceptMap", + "SearchParameter", + "Subscription")); public static final String PKG_METADATA_KEY = "PKG_METADATA"; @@ -53,10 +34,10 @@ public class PackageUtils { * @param theResource the resource * @param thePkg the npm package (IG) it is from */ - public static void addPackageMetadata(IBaseResource theResource, NpmPackage thePkg) { + public static void addPackageMetadata(IBaseResource theResource, String thePkgName, String thePkgVersion) { NpmPackageMetadataLiteJson metadata = new NpmPackageMetadataLiteJson(); - metadata.setName(thePkg.name()); - metadata.setVersion(thePkg.version()); + metadata.setName(thePkgName); + metadata.setVersion(thePkgVersion); theResource.setUserData(PKG_METADATA_KEY, JsonUtil.serialize(metadata)); } @@ -74,4 +55,14 @@ public static NpmPackageMetadataLiteJson getPackageMetadata(IBaseResource theRes } return JsonUtil.deserialize(metadataJson, NpmPackageMetadataLiteJson.class); } + + public static boolean isFromNpmPackage(IBaseResource theResource) { + return theResource.getUserData(PKG_METADATA_KEY) != null; + } + + public static void setSourceForSP(IBaseResource theResource, RuntimeSearchParam theSearchParam) { + assert isFromNpmPackage(theResource); + NpmPackageMetadataLiteJson metadata = getPackageMetadata(theResource); + theSearchParam.setSource(RuntimeResourceSource.npmSource(metadata.getName(), metadata.getVersion())); + } } diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/packages/NpmJpaValidationSupport.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/packages/NpmJpaValidationSupport.java index b95a7b037c6b..bb2a43f7af27 100644 --- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/packages/NpmJpaValidationSupport.java +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/packages/NpmJpaValidationSupport.java @@ -27,9 +27,9 @@ import ca.uhn.fhir.jpa.dao.data.INpmPackageVersionDao; import ca.uhn.fhir.jpa.model.entity.NpmPackageVersionEntity; import ca.uhn.fhir.jpa.packages.loader.PackageResourceParsingSvc; -import ca.uhn.fhir.jpa.packages.util.PackageUtils; import ca.uhn.fhir.model.api.PagingIterator; import ca.uhn.fhir.rest.annotation.Transaction; +import ca.uhn.fhir.util.NpmPackageUtils; import jakarta.annotation.Nullable; import org.hl7.fhir.instance.model.api.IBaseResource; import org.hl7.fhir.utilities.npm.NpmPackage; @@ -152,7 +152,7 @@ private void getAllSP(NpmPackageVersionEntity theNpmPa List pkgSps = myPackageResourceParsingSvc.parseResourcesOfType("SearchParameter", pkg); for (IBaseResource sp : pkgSps) { - PackageUtils.addPackageMetadata(sp, pkg); + NpmPackageUtils.addPackageMetadata(sp, pkg.name(), pkg.version()); theSps.add((T) sp); } } diff --git a/hapi-fhir-jpaserver-searchparam/src/main/java/ca/uhn/fhir/jpa/searchparam/registry/ReadOnlySearchParamCache.java b/hapi-fhir-jpaserver-searchparam/src/main/java/ca/uhn/fhir/jpa/searchparam/registry/ReadOnlySearchParamCache.java index 5a2a9ead2e8e..0d2f1be50222 100644 --- a/hapi-fhir-jpaserver-searchparam/src/main/java/ca/uhn/fhir/jpa/searchparam/registry/ReadOnlySearchParamCache.java +++ b/hapi-fhir-jpaserver-searchparam/src/main/java/ca/uhn/fhir/jpa/searchparam/registry/ReadOnlySearchParamCache.java @@ -21,8 +21,8 @@ import ca.uhn.fhir.context.FhirContext; import ca.uhn.fhir.context.RuntimeResourceDefinition; +import ca.uhn.fhir.context.RuntimeResourceSource; import ca.uhn.fhir.context.RuntimeSearchParam; -import ca.uhn.fhir.context.RuntimeSearchParamSource; import ca.uhn.fhir.rest.server.util.ResourceSearchParams; import jakarta.annotation.Nonnull; import jakarta.annotation.Nullable; @@ -106,6 +106,7 @@ public static ReadOnlySearchParamCache fromFhirContext( RuntimeSearchParam nextCanonical = theCanonicalizer.canonicalizeSearchParameter(next); if (nextCanonical != null) { + RuntimeResourceSource source = nextCanonical.getSource(); // Force status to ACTIVE - For whatever reason the R5 draft SPs ship with // a status of DRAFT which means the server doesn't actually apply them. @@ -123,7 +124,7 @@ public static ReadOnlySearchParamCache fromFhirContext( nextCanonical.getComboSearchParamType(), nextCanonical.getComponents(), nextCanonical.getBase()); - nextCanonical.setSource(RuntimeSearchParamSource.builtinSource()); + nextCanonical.setSource(source); Collection base = nextCanonical.getBase(); if (base.contains("Resource") || base.contains("DomainResource")) { diff --git a/hapi-fhir-jpaserver-searchparam/src/main/java/ca/uhn/fhir/jpa/searchparam/registry/SearchParamRegistryImpl.java b/hapi-fhir-jpaserver-searchparam/src/main/java/ca/uhn/fhir/jpa/searchparam/registry/SearchParamRegistryImpl.java index 6e6a9fc65400..3b21ce65b340 100644 --- a/hapi-fhir-jpaserver-searchparam/src/main/java/ca/uhn/fhir/jpa/searchparam/registry/SearchParamRegistryImpl.java +++ b/hapi-fhir-jpaserver-searchparam/src/main/java/ca/uhn/fhir/jpa/searchparam/registry/SearchParamRegistryImpl.java @@ -22,7 +22,7 @@ import ca.uhn.fhir.context.ComboSearchParamType; import ca.uhn.fhir.context.FhirContext; import ca.uhn.fhir.context.RuntimeSearchParam; -import ca.uhn.fhir.context.RuntimeSearchParamSource; +import ca.uhn.fhir.context.RuntimeResourceSource; import ca.uhn.fhir.context.phonetic.IPhoneticEncoder; import ca.uhn.fhir.i18n.Msg; import ca.uhn.fhir.interceptor.api.IInterceptorService; @@ -53,7 +53,6 @@ import jakarta.annotation.PreDestroy; import org.apache.commons.lang3.StringUtils; import org.apache.commons.lang3.time.DateUtils; -import org.hl7.fhir.instance.model.api.IBase; import org.hl7.fhir.instance.model.api.IBaseResource; import org.hl7.fhir.instance.model.api.IIdType; import org.slf4j.Logger; @@ -227,7 +226,7 @@ public void addActiveSearchParameterToLocalCache(@Nonnull RuntimeSearchParam the assert theSearchParam.getBase() != null && !theSearchParam.getBase().isEmpty(); assert theSearchParam.getSource() != null; - if (theSearchParam.getSource().getOriginatingSource() == RuntimeSearchParamSource.SourceType.UNKNOWN) { + if (theSearchParam.getSource().getOriginatingSource() == RuntimeResourceSource.SourceType.UNKNOWN) { ourLog.warn("SearchParameter added with unknown source."); } @@ -424,16 +423,11 @@ private long overrideBuiltinSearchParamsWithActiveJpaSearchParams( return 0; } - FhirTerser terser = myFhirContext.newTerser(); long retval = 0; Function conversion = (spResource) -> { RuntimeSearchParam rtsp = mySearchParameterCanonicalizer.canonicalizeSearchParameter(spResource); - RuntimeSearchParamSource source = RuntimeSearchParamSource.databaseSource(); - rtsp.setSource(source); - IBase versionField = SearchParameterUtil.getFirstFieldValueOrNull(terser, spResource, "version"); - if (versionField != null) { - source.setSPVersion(versionField.toString()); - } +// RuntimeResourceSource source = RuntimeResourceSource.databaseSource(); +// rtsp.setSource(source); return rtsp; }; for (IBaseResource searchParam : theSearchParams) { 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 d29736d6a6da..be60b7b1fdff 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 @@ -21,6 +21,7 @@ import ca.uhn.fhir.context.ComboSearchParamType; import ca.uhn.fhir.context.FhirContext; +import ca.uhn.fhir.context.RuntimeResourceSource; import ca.uhn.fhir.context.RuntimeSearchParam; import ca.uhn.fhir.context.phonetic.IPhoneticEncoder; import ca.uhn.fhir.i18n.Msg; @@ -31,6 +32,7 @@ import ca.uhn.fhir.util.ExtensionUtil; import ca.uhn.fhir.util.FhirTerser; import ca.uhn.fhir.util.HapiExtensions; +import ca.uhn.fhir.util.NpmPackageUtils; import ca.uhn.fhir.util.PhoneticEncoderUtil; import org.apache.commons.lang3.StringUtils; import org.hl7.fhir.dstu3.model.Extension; @@ -110,6 +112,18 @@ public RuntimeSearchParam canonicalizeSearchParameter(IBaseResource theSearchPar if (retVal != null) { extractExtensions(theSearchParameter, retVal); + + if (theSearchParameter.getIdElement() != null && theSearchParameter.getIdElement().hasVersionIdPart()) { + retVal.setSource(RuntimeResourceSource.databaseSource()); + } else if (NpmPackageUtils.isFromNpmPackage(theSearchParameter)) { + NpmPackageUtils.setSourceForSP(theSearchParameter, retVal); + } else if (theSearchParameter.getIdElement() == null + || !theSearchParameter.getIdElement().hasVersionIdPart()) { + retVal.setSource(RuntimeResourceSource.fhirContextSource(myFhirContext)); + } else { + // unknown + retVal.setSource(new RuntimeResourceSource()); + } } return retVal; diff --git a/hapi-fhir-jpaserver-searchparam/src/test/java/ca/uhn/fhir/jpa/searchparam/registry/SearchParamRegistryImplTest.java b/hapi-fhir-jpaserver-searchparam/src/test/java/ca/uhn/fhir/jpa/searchparam/registry/SearchParamRegistryImplTest.java index 9cec0084c22a..b80dd656b3eb 100644 --- a/hapi-fhir-jpaserver-searchparam/src/test/java/ca/uhn/fhir/jpa/searchparam/registry/SearchParamRegistryImplTest.java +++ b/hapi-fhir-jpaserver-searchparam/src/test/java/ca/uhn/fhir/jpa/searchparam/registry/SearchParamRegistryImplTest.java @@ -2,7 +2,7 @@ import ca.uhn.fhir.context.FhirContext; import ca.uhn.fhir.context.RuntimeSearchParam; -import ca.uhn.fhir.context.RuntimeSearchParamSource; +import ca.uhn.fhir.context.RuntimeResourceSource; import ca.uhn.fhir.context.support.IValidationSupport; import ca.uhn.fhir.interceptor.api.IInterceptorService; import ca.uhn.fhir.jpa.cache.IResourceChangeListenerRegistry; @@ -42,6 +42,7 @@ import org.hl7.fhir.r4.model.CodeType; import org.hl7.fhir.r4.model.Enumerations; import org.hl7.fhir.r4.model.Extension; +import org.hl7.fhir.r4.model.IdType; import org.hl7.fhir.r4.model.SearchParameter; import org.hl7.fhir.r4.model.StringType; import org.junit.jupiter.api.AfterEach; @@ -494,7 +495,7 @@ public void getActiveSearchParams_builtIn_haveProperSource() { // verify params.getSearchParamStream() .forEach(sp -> { - assertEquals(RuntimeSearchParamSource.SourceType.BUILT_IN, sp.getSource().getOriginatingSource()); + assertEquals(RuntimeResourceSource.SourceType.FHIR_CONTEXT, sp.getSource().getOriginatingSource()); }); } @@ -510,6 +511,7 @@ public void getActiveSearchParams_SPsFromDB_haveProperSource() { sp.setExpression("Patient.name.given"); sp.setCode("HelloWorld"); sp.setStatus(Enumerations.PublicationStatus.ACTIVE); + sp.setId(new IdType("SearchParameter/1/_history/1")); IBundleProvider provider = new SimpleBundleProvider( List.of(sp) @@ -525,7 +527,7 @@ public void getActiveSearchParams_SPsFromDB_haveProperSource() { RuntimeSearchParam result = mySearchParamRegistry.getActiveSearchParam("Patient", "HelloWorld", ISearchParamRegistry.SearchParamLookupContextEnum.ALL); assertNotNull(result); assertEquals("HelloWorld", result.getName()); - assertEquals(RuntimeSearchParamSource.SourceType.DATABASE, result.getSource().getOriginatingSource()); + assertEquals(RuntimeResourceSource.SourceType.DATABASE, result.getSource().getOriginatingSource()); assertNull(result.getSource().getIGName()); assertNull(result.getSource().getIGVersion()); } diff --git a/hapi-fhir-jpaserver-test-r4/src/test/java/ca/uhn/fhir/jpa/packages/NpmJpaValidationSupportIT.java b/hapi-fhir-jpaserver-test-r4/src/test/java/ca/uhn/fhir/jpa/packages/NpmJpaValidationSupportIT.java index 4f8f056c2c99..3cd30b8d0a47 100644 --- a/hapi-fhir-jpaserver-test-r4/src/test/java/ca/uhn/fhir/jpa/packages/NpmJpaValidationSupportIT.java +++ b/hapi-fhir-jpaserver-test-r4/src/test/java/ca/uhn/fhir/jpa/packages/NpmJpaValidationSupportIT.java @@ -2,8 +2,9 @@ import ca.uhn.fhir.context.FhirContext; import ca.uhn.fhir.implementationguide.ImplementationGuideCreator; -import ca.uhn.fhir.jpa.packages.util.PackageUtils; import ca.uhn.fhir.jpa.test.BaseJpaR4Test; +import ca.uhn.fhir.model.npm.NpmPackageMetadataLiteJson; +import ca.uhn.fhir.util.NpmPackageUtils; import ca.uhn.test.util.LogbackTestExtension; import org.hl7.fhir.instance.model.api.IBaseResource; import org.hl7.fhir.r4.model.Enumerations; @@ -60,7 +61,7 @@ public void fetchAllSearchParameters_withStoredIG_returnsSPsFromIG() throws IOEx SearchParameter sp = (SearchParameter)r; assertEquals(generated.getName(), sp.getName()); assertEquals(generated.getUrl(), sp.getUrl()); - NpmPackageMetadataLiteJson metadata = PackageUtils.getPackageMetadata(sp); + NpmPackageMetadataLiteJson metadata = NpmPackageUtils.getPackageMetadata(sp); assertNotNull(metadata); assertEquals(spec.getName(), metadata.getName()); assertEquals(spec.getVersion(), metadata.getVersion()); @@ -135,7 +136,7 @@ public void fetchAllSearchParameters_multipleVersion_returnsAllSPs() throws IOEx assertEquals(generated.getUrl(), sp.getUrl()); assertEquals(generated.getName(), sp.getName()); - NpmPackageMetadataLiteJson metadataLiteJson = PackageUtils.getPackageMetadata(sp); + NpmPackageMetadataLiteJson metadataLiteJson = NpmPackageUtils.getPackageMetadata(sp); assertNotNull(metadataLiteJson); map.put(metadataLiteJson.getVersion(), metadataLiteJson); From 25a2d122b0f5b889961d6864bf1b882c2cac6134 Mon Sep 17 00:00:00 2001 From: leif stawnyczy Date: Tue, 9 Sep 2025 16:34:21 -0400 Subject: [PATCH 34/36] spotless --- .../ca/uhn/fhir/context/RuntimeSearchParam.java | 2 +- .../fhir/model/npm/NpmPackageMetadataLiteJson.java | 10 +++++----- .../java/ca/uhn/fhir/util/NpmPackageUtils.java | 14 +++++++------- .../uhn/fhir/jpa/config/PackageLoaderConfig.java | 4 ++-- .../fhir/jpa/packages/PackageInstallerSvcImpl.java | 2 +- .../registry/SearchParamRegistryImpl.java | 5 +---- .../registry/SearchParameterCanonicalizer.java | 3 ++- .../jpa/packages/loader/PackageLoaderSvcIT.java | 9 ++++----- 8 files changed, 23 insertions(+), 26 deletions(-) diff --git a/hapi-fhir-base/src/main/java/ca/uhn/fhir/context/RuntimeSearchParam.java b/hapi-fhir-base/src/main/java/ca/uhn/fhir/context/RuntimeSearchParam.java index c6868134f821..d36d4fd07fab 100644 --- a/hapi-fhir-base/src/main/java/ca/uhn/fhir/context/RuntimeSearchParam.java +++ b/hapi-fhir-base/src/main/java/ca/uhn/fhir/context/RuntimeSearchParam.java @@ -55,7 +55,7 @@ public class RuntimeSearchParam { private final Set myProvidesMembershipInCompartments; private final RuntimeSearchParamStatusEnum myStatus; private final String myUri; - private String myVersion; + private String myVersion; private final Map>> myExtensions = new HashMap<>(); private final Map myUpliftRefchains = new HashMap<>(); private final ComboSearchParamType myComboSearchParamType; diff --git a/hapi-fhir-base/src/main/java/ca/uhn/fhir/model/npm/NpmPackageMetadataLiteJson.java b/hapi-fhir-base/src/main/java/ca/uhn/fhir/model/npm/NpmPackageMetadataLiteJson.java index e460c16da89e..c4cef64debb9 100644 --- a/hapi-fhir-base/src/main/java/ca/uhn/fhir/model/npm/NpmPackageMetadataLiteJson.java +++ b/hapi-fhir-base/src/main/java/ca/uhn/fhir/model/npm/NpmPackageMetadataLiteJson.java @@ -6,11 +6,11 @@ @JsonInclude(JsonInclude.Include.NON_NULL) @JsonAutoDetect( - creatorVisibility = JsonAutoDetect.Visibility.NONE, - fieldVisibility = JsonAutoDetect.Visibility.NONE, - getterVisibility = JsonAutoDetect.Visibility.NONE, - isGetterVisibility = JsonAutoDetect.Visibility.NONE, - setterVisibility = JsonAutoDetect.Visibility.NONE) + creatorVisibility = JsonAutoDetect.Visibility.NONE, + fieldVisibility = JsonAutoDetect.Visibility.NONE, + getterVisibility = JsonAutoDetect.Visibility.NONE, + isGetterVisibility = JsonAutoDetect.Visibility.NONE, + setterVisibility = JsonAutoDetect.Visibility.NONE) public class NpmPackageMetadataLiteJson { /** * The npm package name diff --git a/hapi-fhir-base/src/main/java/ca/uhn/fhir/util/NpmPackageUtils.java b/hapi-fhir-base/src/main/java/ca/uhn/fhir/util/NpmPackageUtils.java index 3912c9f4182f..59d214c5e4b1 100644 --- a/hapi-fhir-base/src/main/java/ca/uhn/fhir/util/NpmPackageUtils.java +++ b/hapi-fhir-base/src/main/java/ca/uhn/fhir/util/NpmPackageUtils.java @@ -19,13 +19,13 @@ public class NpmPackageUtils { * Default install types */ public static List DEFAULT_INSTALL_TYPES = Collections.unmodifiableList(Lists.newArrayList( - "NamingSystem", - "CodeSystem", - "ValueSet", - "StructureDefinition", - "ConceptMap", - "SearchParameter", - "Subscription")); + "NamingSystem", + "CodeSystem", + "ValueSet", + "StructureDefinition", + "ConceptMap", + "SearchParameter", + "Subscription")); public static final String PKG_METADATA_KEY = "PKG_METADATA"; diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/config/PackageLoaderConfig.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/config/PackageLoaderConfig.java index b875aa92a2d1..c1064150b8f4 100644 --- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/config/PackageLoaderConfig.java +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/config/PackageLoaderConfig.java @@ -25,8 +25,8 @@ import ca.uhn.fhir.jpa.packages.NpmJpaValidationSupport; import ca.uhn.fhir.jpa.packages.loader.PackageLoaderSvc; import ca.uhn.fhir.jpa.packages.loader.PackageResourceParsingSvc; -import ca.uhn.fhir.jpa.packages.util.PackageUtils; import ca.uhn.fhir.jpa.validation.ValidationSettings; +import ca.uhn.fhir.util.NpmPackageUtils; import org.hl7.fhir.utilities.npm.PackageServer; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; @@ -48,7 +48,7 @@ public PackageResourceParsingSvc resourceParsingSvc(FhirContext theContext) { return new PackageResourceParsingSvc(theContext); } - @Bean(name = PackageUtils.LOADER_WITH_CACHE) + @Bean(name = NpmPackageUtils.LOADER_WITH_CACHE) public IHapiPackageCacheManager packageCacheManager() { return new JpaPackageCache(); } diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/packages/PackageInstallerSvcImpl.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/packages/PackageInstallerSvcImpl.java index 54a247aa59f4..85003e18e21f 100644 --- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/packages/PackageInstallerSvcImpl.java +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/packages/PackageInstallerSvcImpl.java @@ -74,7 +74,7 @@ import java.util.List; import java.util.Optional; -import static ca.uhn.fhir.jpa.packages.util.PackageUtils.DEFAULT_INSTALL_TYPES; +import static ca.uhn.fhir.util.NpmPackageUtils.DEFAULT_INSTALL_TYPES; import static ca.uhn.fhir.util.SearchParameterUtil.getBaseAsStrings; /** diff --git a/hapi-fhir-jpaserver-searchparam/src/main/java/ca/uhn/fhir/jpa/searchparam/registry/SearchParamRegistryImpl.java b/hapi-fhir-jpaserver-searchparam/src/main/java/ca/uhn/fhir/jpa/searchparam/registry/SearchParamRegistryImpl.java index 3b21ce65b340..de4b8348f9df 100644 --- a/hapi-fhir-jpaserver-searchparam/src/main/java/ca/uhn/fhir/jpa/searchparam/registry/SearchParamRegistryImpl.java +++ b/hapi-fhir-jpaserver-searchparam/src/main/java/ca/uhn/fhir/jpa/searchparam/registry/SearchParamRegistryImpl.java @@ -21,8 +21,8 @@ import ca.uhn.fhir.context.ComboSearchParamType; import ca.uhn.fhir.context.FhirContext; -import ca.uhn.fhir.context.RuntimeSearchParam; import ca.uhn.fhir.context.RuntimeResourceSource; +import ca.uhn.fhir.context.RuntimeSearchParam; import ca.uhn.fhir.context.phonetic.IPhoneticEncoder; import ca.uhn.fhir.i18n.Msg; import ca.uhn.fhir.interceptor.api.IInterceptorService; @@ -42,7 +42,6 @@ import ca.uhn.fhir.rest.server.util.ISearchParamRegistry; import ca.uhn.fhir.rest.server.util.IndexedSearchParam; import ca.uhn.fhir.rest.server.util.ResourceSearchParams; -import ca.uhn.fhir.util.FhirTerser; import ca.uhn.fhir.util.SearchParameterUtil; import ca.uhn.fhir.util.StopWatch; import com.google.common.annotations.VisibleForTesting; @@ -426,8 +425,6 @@ private long overrideBuiltinSearchParamsWithActiveJpaSearchParams( long retval = 0; Function conversion = (spResource) -> { RuntimeSearchParam rtsp = mySearchParameterCanonicalizer.canonicalizeSearchParameter(spResource); -// RuntimeResourceSource source = RuntimeResourceSource.databaseSource(); -// rtsp.setSource(source); return rtsp; }; for (IBaseResource searchParam : theSearchParams) { 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 be60b7b1fdff..2bd531534904 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 @@ -113,7 +113,8 @@ public RuntimeSearchParam canonicalizeSearchParameter(IBaseResource theSearchPar if (retVal != null) { extractExtensions(theSearchParameter, retVal); - if (theSearchParameter.getIdElement() != null && theSearchParameter.getIdElement().hasVersionIdPart()) { + if (theSearchParameter.getIdElement() != null + && theSearchParameter.getIdElement().hasVersionIdPart()) { retVal.setSource(RuntimeResourceSource.databaseSource()); } else if (NpmPackageUtils.isFromNpmPackage(theSearchParameter)) { NpmPackageUtils.setSourceForSP(theSearchParameter, retVal); diff --git a/hapi-fhir-jpaserver-test-r4/src/test/java/ca/uhn/fhir/jpa/packages/loader/PackageLoaderSvcIT.java b/hapi-fhir-jpaserver-test-r4/src/test/java/ca/uhn/fhir/jpa/packages/loader/PackageLoaderSvcIT.java index b8fabb07f1a8..3a960568f5c4 100644 --- a/hapi-fhir-jpaserver-test-r4/src/test/java/ca/uhn/fhir/jpa/packages/loader/PackageLoaderSvcIT.java +++ b/hapi-fhir-jpaserver-test-r4/src/test/java/ca/uhn/fhir/jpa/packages/loader/PackageLoaderSvcIT.java @@ -1,12 +1,10 @@ package ca.uhn.fhir.jpa.packages.loader; -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertNotNull; import ca.uhn.fhir.context.FhirContext; import ca.uhn.fhir.jpa.packages.FakeNpmServlet; -import ca.uhn.fhir.jpa.packages.util.PackageUtils; import ca.uhn.fhir.test.utilities.server.HttpServletExtension; import ca.uhn.fhir.util.ClasspathUtil; +import ca.uhn.fhir.util.NpmPackageUtils; import org.hl7.fhir.instance.model.api.IBaseResource; import org.hl7.fhir.utilities.npm.NpmPackage; import org.hl7.fhir.utilities.npm.PackageServer; @@ -22,7 +20,8 @@ import java.util.List; import static org.assertj.core.api.Assertions.assertThat; -import static org.junit.jupiter.api.Assertions.fail; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; import static org.junit.jupiter.api.Assertions.fail; @@ -66,7 +65,7 @@ public void fetchPackageFromServer_thenParseoutResources_inMemory() throws IOExc // test parse resources List resources = new ArrayList<>(); - List resourcesToParse = PackageUtils.DEFAULT_INSTALL_TYPES; + List resourcesToParse = NpmPackageUtils.DEFAULT_INSTALL_TYPES; for (String resourceType : resourcesToParse) { resources.addAll( myResourceParsingSvc.parseResourcesOfType(resourceType, npmPackage) From 1b63e07fa6571d83e424e3a01e94424da36927d0 Mon Sep 17 00:00:00 2001 From: leif stawnyczy Date: Wed, 10 Sep 2025 15:13:20 -0400 Subject: [PATCH 35/36] added alidation fixmes --- .../jpa/validation/JpaValidationSupportChain.java | 3 +++ .../searchparam/registry/SearchParamRegistryImpl.java | 11 +++++++++++ .../registry/SearchParameterCanonicalizer.java | 6 ++++++ .../validation/support/ValidationSupportChain.java | 9 +++++++-- 4 files changed, 27 insertions(+), 2 deletions(-) diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/validation/JpaValidationSupportChain.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/validation/JpaValidationSupportChain.java index 48873a3ad845..df9965e1e6bf 100644 --- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/validation/JpaValidationSupportChain.java +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/validation/JpaValidationSupportChain.java @@ -94,6 +94,9 @@ public void flush() { public void postConstruct() { myWorkerContextValidationSupportAdapter.setValidationSupport(this); + // FIXME LS - SPOrder + // This ordering is valid for StructureDefinitions + // but not addValidationSupport(myDefaultProfileValidationSupport); addValidationSupport(myJpaValidationSupport); addValidationSupport(myTerminologyService); diff --git a/hapi-fhir-jpaserver-searchparam/src/main/java/ca/uhn/fhir/jpa/searchparam/registry/SearchParamRegistryImpl.java b/hapi-fhir-jpaserver-searchparam/src/main/java/ca/uhn/fhir/jpa/searchparam/registry/SearchParamRegistryImpl.java index de4b8348f9df..dc3453ac3124 100644 --- a/hapi-fhir-jpaserver-searchparam/src/main/java/ca/uhn/fhir/jpa/searchparam/registry/SearchParamRegistryImpl.java +++ b/hapi-fhir-jpaserver-searchparam/src/main/java/ca/uhn/fhir/jpa/searchparam/registry/SearchParamRegistryImpl.java @@ -164,6 +164,10 @@ public RuntimeSearchParam getActiveSearchParam( } } + if (theContext == SearchParamLookupContextEnum.ALL) { + return myLocalSPCache.get(theResourceName, theParamName); + } + return null; } @@ -216,6 +220,11 @@ public RuntimeSearchParam getActiveSearchParamByUrl( return param; } } + + if (theContext == SearchParamLookupContextEnum.ALL) { + return myLocalSPCache.getByUrl(theUrl); + } + return null; } @@ -272,6 +281,7 @@ private void rebuildActiveSearchParams() { private void initializeActiveSearchParams(Collection theJpaSearchParams) { StopWatch sw = new StopWatch(); + // add built-in SPs ReadOnlySearchParamCache builtInSearchParams = getBuiltInSearchParams(); RuntimeSearchParamCache searchParams = RuntimeSearchParamCache.fromReadOnlySearchParamCache(builtInSearchParams); @@ -284,6 +294,7 @@ private void initializeActiveSearchParams(Collection theJpaSearch } }); + // add / override with SPs from the DB (these should always come last) long overriddenCount = overrideBuiltinSearchParamsWithActiveJpaSearchParams(searchParams, theJpaSearchParams); ourLog.trace("Have overridden {} built-in search parameters", overriddenCount); 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 2bd531534904..4e159df091aa 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 @@ -113,6 +113,12 @@ public RuntimeSearchParam canonicalizeSearchParameter(IBaseResource theSearchPar if (retVal != null) { extractExtensions(theSearchParameter, retVal); + // set the source of the RuntimeSearchParam + // Assumptions here are: + // * DB SPs have a version (they are in the db) + // * NpmPackage provided SPs have metadata in UserData + // * SPs that are built in have no Id or no version + // * otherwise, we list them as unknown if (theSearchParameter.getIdElement() != null && theSearchParameter.getIdElement().hasVersionIdPart()) { retVal.setSource(RuntimeResourceSource.databaseSource()); diff --git a/hapi-fhir-validation/src/main/java/org/hl7/fhir/common/hapi/validation/support/ValidationSupportChain.java b/hapi-fhir-validation/src/main/java/org/hl7/fhir/common/hapi/validation/support/ValidationSupportChain.java index d0349196a1ad..5cd99ea261c0 100644 --- a/hapi-fhir-validation/src/main/java/org/hl7/fhir/common/hapi/validation/support/ValidationSupportChain.java +++ b/hapi-fhir-validation/src/main/java/org/hl7/fhir/common/hapi/validation/support/ValidationSupportChain.java @@ -627,8 +627,13 @@ public List fetchAllNonBaseStructureDefinitions() { @Nullable @Override public List fetchAllSearchParameters() { - // TODO - this is not a good order for SPs (since it has the fhir context first, - // and then blocks future ones with the same canonical) + // FIXME LS - SPOrder + // this is not a good order for SPs. + // The ordering (set forth in {@link JpaValidationSupportChain}) has + // fhir context first, and DB last (the exact opposite of what we want). + // We might want to throw an exception here and require + // callers use the correct validation support chain impl, + // as is the case in {@link ReadOnlySearchParamCache} FetchAllKey key = new FetchAllKey(FetchAllKey.TypeEnum.ALL_SEARCHPARAMETERS); Supplier> loader = () -> doFetchStructureDefinitions(IValidationSupport::fetchAllSearchParameters); From 7f7484e2fd158e2dafcc46bdc8632756d46dc898 Mon Sep 17 00:00:00 2001 From: leif stawnyczy Date: Wed, 10 Sep 2025 15:45:28 -0400 Subject: [PATCH 36/36] fixing a bug --- .../ca/uhn/fhir/jpa/packages/IgInstallerDstu3Test.java | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/hapi-fhir-jpaserver-test-dstu3/src/test/java/ca/uhn/fhir/jpa/packages/IgInstallerDstu3Test.java b/hapi-fhir-jpaserver-test-dstu3/src/test/java/ca/uhn/fhir/jpa/packages/IgInstallerDstu3Test.java index c8878aa852ed..c448033943ed 100644 --- a/hapi-fhir-jpaserver-test-dstu3/src/test/java/ca/uhn/fhir/jpa/packages/IgInstallerDstu3Test.java +++ b/hapi-fhir-jpaserver-test-dstu3/src/test/java/ca/uhn/fhir/jpa/packages/IgInstallerDstu3Test.java @@ -1,16 +1,15 @@ package ca.uhn.fhir.jpa.packages; -import static org.junit.jupiter.api.Assertions.assertEquals; import ca.uhn.fhir.i18n.Msg; import ca.uhn.fhir.interceptor.api.Pointcut; import ca.uhn.fhir.jpa.api.config.JpaStorageSettings; import ca.uhn.fhir.jpa.dao.data.INpmPackageVersionDao; -import ca.uhn.fhir.jpa.packages.util.PackageUtils; import ca.uhn.fhir.jpa.test.BaseJpaDstu3Test; import ca.uhn.fhir.rest.server.exceptions.InvalidRequestException; import ca.uhn.fhir.rest.server.exceptions.ResourceNotFoundException; import ca.uhn.fhir.test.utilities.ProxyUtil; import ca.uhn.fhir.test.utilities.server.HttpServletExtension; +import ca.uhn.fhir.util.NpmPackageUtils; import org.hl7.fhir.instance.model.api.IBaseBinary; import org.hl7.fhir.instance.model.api.IBaseResource; import org.hl7.fhir.utilities.npm.PackageServer; @@ -28,9 +27,10 @@ import static ca.uhn.fhir.util.ClasspathUtil.loadResourceAsByteArray; import static org.assertj.core.api.Assertions.assertThat; -import static org.junit.jupiter.api.Assertions.fail; +import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertFalse; import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.junit.jupiter.api.Assertions.fail; public class IgInstallerDstu3Test extends BaseJpaDstu3Test { @@ -38,7 +38,7 @@ public class IgInstallerDstu3Test extends BaseJpaDstu3Test { @Autowired private PackageInstallerSvcImpl igInstaller; @Autowired - @Qualifier(PackageUtils.LOADER_WITH_CACHE) + @Qualifier(NpmPackageUtils.LOADER_WITH_CACHE) private IHapiPackageCacheManager myPackageCacheManager; @Autowired private INpmPackageVersionDao myPackageVersionDao;