diff --git a/hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/changelog/7_8_0/6534-support-versioned-urls-better-in-valsupport.yaml b/hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/changelog/7_8_0/6534-support-versioned-urls-better-in-valsupport.yaml new file mode 100644 index 00000000000..fcff5f1144b --- /dev/null +++ b/hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/changelog/7_8_0/6534-support-versioned-urls-better-in-valsupport.yaml @@ -0,0 +1,6 @@ +--- +type: add +issue: 6534 +title: "The JpaPersistedValidationSupport module which is used to fetch + conformance resources from the JPA repository for validation purposes can + now support versioned URLs. Thanks to Mangala Ekanayake for the contribution!" diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/JpaPersistedResourceValidationSupport.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/JpaPersistedResourceValidationSupport.java index b2a9f45ffa4..61230093b92 100644 --- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/JpaPersistedResourceValidationSupport.java +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/JpaPersistedResourceValidationSupport.java @@ -235,7 +235,12 @@ public class JpaPersistedResourceValidationSupport implements IValidationSupport } SearchParameterMap params = new SearchParameterMap(); params.setLoadSynchronousUpTo(1); - params.add(StructureDefinition.SP_URL, new UriParam(theUri)); + int versionSeparator = theUri.lastIndexOf('|'); + if (versionSeparator != -1) {params.add(StructureDefinition.SP_VERSION, new TokenParam(theUri.substring(versionSeparator + 1))); + params.add(StructureDefinition.SP_URL, new UriParam(theUri.substring(0, versionSeparator))); + } else { + params.add(StructureDefinition.SP_URL, new UriParam(theUri)); + } search = myDaoRegistry.getResourceDao("StructureDefinition").search(params); break; } diff --git a/hapi-fhir-jpaserver-test-r4/src/test/java/ca/uhn/fhir/jpa/dao/JpaPersistedResourceValidationSupportTest.java b/hapi-fhir-jpaserver-test-r4/src/test/java/ca/uhn/fhir/jpa/dao/JpaPersistedResourceValidationSupportTest.java new file mode 100644 index 00000000000..eb226a0877c --- /dev/null +++ b/hapi-fhir-jpaserver-test-r4/src/test/java/ca/uhn/fhir/jpa/dao/JpaPersistedResourceValidationSupportTest.java @@ -0,0 +1,119 @@ +package ca.uhn.fhir.jpa.dao; + +/*- + * #%L + * HAPI FHIR JPA Server + * %% + * Copyright (C) 2014 - 2024 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% + */ + +import ca.uhn.fhir.context.FhirContext; +import ca.uhn.fhir.context.support.IValidationSupport; +import ca.uhn.fhir.jpa.api.dao.DaoRegistry; +import ca.uhn.fhir.jpa.api.dao.IFhirResourceDao; +import ca.uhn.fhir.jpa.searchparam.SearchParameterMap; +import ca.uhn.fhir.rest.api.server.IBundleProvider; +import ca.uhn.fhir.rest.param.TokenParam; +import ca.uhn.fhir.rest.param.UriParam; +import org.hl7.fhir.r4.model.StructureDefinition; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.ArgumentCaptor; +import org.mockito.Captor; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; + +import static org.assertj.core.api.AssertionsForClassTypes.assertThat; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +@ExtendWith(MockitoExtension.class) +class JpaPersistedResourceValidationSupportTest { + + private FhirContext theFhirContext = FhirContext.forR4(); + + @Nested + class FetchStructureDefinitionTests { + + @Mock + private DaoRegistry myDaoRegistry; + + @InjectMocks + private final JpaPersistedResourceValidationSupport testClass = new JpaPersistedResourceValidationSupport(theFhirContext); + + @Captor + ArgumentCaptor searchParameterMapCaptor; + + @Test + @DisplayName("fetch StructureDefinition by version less url") + void fetchStructureDefinitionForUrl() { + final String profileUrl = "http://example.com/fhir/StructureDefinition/exampleProfile"; + IFhirResourceDao mockDao = mock(IFhirResourceDao.class); + when(mockDao.search(any())).thenReturn(mock(IBundleProvider.class)); + when(myDaoRegistry.getResourceDao(anyString())).thenReturn(mockDao); + + testClass.fetchResource(StructureDefinition.class, profileUrl); + + verify(mockDao).search(searchParameterMapCaptor.capture()); + SearchParameterMap searchParams = searchParameterMapCaptor.getValue(); + String uriParam = searchParams.get(StructureDefinition.SP_URL) + .get(0) + .stream() + .map(UriParam.class::cast) + .map(UriParam::getValue) + .findFirst() + .orElse(null); + assertThat(uriParam).isEqualTo(profileUrl); + } + + @Test + @DisplayName("fetch StructureDefinition by versioned url") + void fetchStructureDefinitionForVersionedUrl() { + final String profileUrl = "http://example.com/fhir/StructureDefinition/exampleProfile|1.1.0"; + IFhirResourceDao mockDao = mock(IFhirResourceDao.class); + when(mockDao.search(any())).thenReturn(mock(IBundleProvider.class)); + when(myDaoRegistry.getResourceDao(anyString())).thenReturn(mockDao); + + testClass.fetchResource(StructureDefinition.class, profileUrl); + + verify(mockDao).search(searchParameterMapCaptor.capture()); + SearchParameterMap searchParams = searchParameterMapCaptor.getValue(); + String uriParam = searchParams.get(StructureDefinition.SP_URL) + .get(0) + .stream() + .map(UriParam.class::cast) + .map(UriParam::getValue) + .findFirst() + .orElse(null); + assertThat(uriParam).isEqualTo("http://example.com/fhir/StructureDefinition/exampleProfile"); + + String versionParam = searchParams.get(StructureDefinition.SP_VERSION) + .get(0) + .stream() + .map(TokenParam.class::cast) + .map(TokenParam::getValue) + .findFirst() + .orElse(null); + assertThat(versionParam).isEqualTo("1.1.0"); + } + } +} diff --git a/hapi-fhir-jpaserver-test-r4/src/test/java/ca/uhn/fhir/jpa/interceptor/RemoteTerminologyServiceJpaR4Test.java b/hapi-fhir-jpaserver-test-r4/src/test/java/ca/uhn/fhir/jpa/interceptor/RemoteTerminologyServiceJpaR4Test.java index 11d7984baff..773e65eddd3 100644 --- a/hapi-fhir-jpaserver-test-r4/src/test/java/ca/uhn/fhir/jpa/interceptor/RemoteTerminologyServiceJpaR4Test.java +++ b/hapi-fhir-jpaserver-test-r4/src/test/java/ca/uhn/fhir/jpa/interceptor/RemoteTerminologyServiceJpaR4Test.java @@ -123,8 +123,8 @@ public class RemoteTerminologyServiceJpaR4Test extends BaseJpaR4Test { // Verify 1 Assertions.assertEquals(2, myCaptureQueriesListener.countGetConnections()); assertThat(ourValueSetProvider.mySearchUrls).asList().containsExactlyInAnyOrder( - "http://hl7.org/fhir/ValueSet/administrative-gender", - "http://hl7.org/fhir/ValueSet/administrative-gender" + "http://hl7.org/fhir/ValueSet/administrative-gender|4.0.1", + "http://hl7.org/fhir/ValueSet/administrative-gender","http://hl7.org/fhir/ValueSet/administrative-gender" ); assertThat(ourCodeSystemProvider.mySearchUrls).asList().containsExactlyInAnyOrder( "http://hl7.org/fhir/administrative-gender", @@ -162,7 +162,7 @@ public class RemoteTerminologyServiceJpaR4Test extends BaseJpaR4Test { // Verify 1 Assertions.assertEquals(0, myCaptureQueriesListener.countGetConnections()); assertThat(ourValueSetProvider.mySearchUrls).asList().containsExactlyInAnyOrder( - "http://hl7.org/fhir/ValueSet/administrative-gender", + "http://hl7.org/fhir/ValueSet/administrative-gender|4.0.1", "http://hl7.org/fhir/ValueSet/administrative-gender" ); assertThat(ourValueSetProvider.myValidatedCodes).asList().containsExactlyInAnyOrder( @@ -215,14 +215,18 @@ public class RemoteTerminologyServiceJpaR4Test extends BaseJpaR4Test { // Verify 1 Assertions.assertEquals(0, myCaptureQueriesListener.countGetConnections()); assertThat(ourValueSetProvider.mySearchUrls).asList().containsExactlyInAnyOrder( - "http://hl7.org/fhir/ValueSet/administrative-gender", + "http://hl7.org/fhir/ValueSet/administrative-gender|4.0.1", "http://hl7.org/fhir/ValueSet/administrative-gender" ); assertThat(ourValueSetProvider.myValidatedCodes).asList().containsExactlyInAnyOrder( - "http://hl7.org/fhir/ValueSet/administrative-gender#null#female" + "http://hl7.org/fhir/ValueSet/administrative-gender#http://hl7.org/fhir/administrative-gender#female" + ); + assertThat(ourCodeSystemProvider.mySearchUrls).asList().containsExactlyInAnyOrder( + "http://hl7.org/fhir/administrative-gender" + ); + assertThat(ourCodeSystemProvider.myValidatedCodes).asList().containsExactlyInAnyOrder( + "http://hl7.org/fhir/administrative-gender#female#null" ); - assertThat(ourCodeSystemProvider.mySearchUrls).asList().isEmpty(); - assertThat(ourCodeSystemProvider.myValidatedCodes).asList().isEmpty(); // Test 2 (should rely on caches) ourCodeSystemProvider.clearCalls(); diff --git a/hapi-fhir-jpaserver-test-r4/src/test/java/ca/uhn/fhir/jpa/validation/ValidateWithRemoteTerminologyTest.java b/hapi-fhir-jpaserver-test-r4/src/test/java/ca/uhn/fhir/jpa/validation/ValidateWithRemoteTerminologyTest.java index 79a656db39c..f36199d8202 100644 --- a/hapi-fhir-jpaserver-test-r4/src/test/java/ca/uhn/fhir/jpa/validation/ValidateWithRemoteTerminologyTest.java +++ b/hapi-fhir-jpaserver-test-r4/src/test/java/ca/uhn/fhir/jpa/validation/ValidateWithRemoteTerminologyTest.java @@ -94,7 +94,7 @@ public class ValidateWithRemoteTerminologyTest extends BaseResourceProviderR4Tes final String classSystem = "http://terminology.hl7.org/CodeSystem/v3-ActCode"; final String identifierTypeSystem = "http://terminology.hl7.org/CodeSystem/v2-0203"; - setupValueSetValidateCode("http://hl7.org/fhir/ValueSet/encounter-status", "http://hl7.org/fhir/encounter-status", statusCode, "validation/encounter/validateCode-ValueSet-encounter-status.json"); + setupValueSetValidateCode("http://hl7.org/fhir/ValueSet/encounter-status", "4.0.1","http://hl7.org/fhir/encounter-status", statusCode, "validation/encounter/validateCode-ValueSet-encounter-status.json"); setupValueSetValidateCode("http://terminology.hl7.org/ValueSet/v3-ActEncounterCode", "http://terminology.hl7.org/CodeSystem/v3-ActCode", classCode, "validation/encounter/validateCode-ValueSet-v3-ActEncounterCode.json"); setupValueSetValidateCode("http://hl7.org/fhir/ValueSet/identifier-type", "http://hl7.org/fhir/identifier-type", identifierTypeCode, "validation/encounter/validateCode-ValueSet-identifier-type.json"); @@ -138,7 +138,7 @@ public class ValidateWithRemoteTerminologyTest extends BaseResourceProviderR4Tes final String loincSystem = "http://loinc.org"; final String system = "http://fhir.infoway-inforoute.ca/io/psca/CodeSystem/ICD9CM"; - setupValueSetValidateCode("http://hl7.org/fhir/ValueSet/observation-status", statusSystem, statusCode, "validation/observation/validateCode-ValueSet-observation-status.json"); + setupValueSetValidateCode("http://hl7.org/fhir/ValueSet/observation-status", "4.0.1", statusSystem, statusCode, "validation/observation/validateCode-ValueSet-observation-status.json"); setupValueSetValidateCode("http://hl7.org/fhir/ValueSet/observation-codes", loincSystem, statusCode, "validation/observation/validateCode-ValueSet-codes.json"); setupCodeSystemValidateCode(statusSystem, statusCode, "validation/observation/validateCode-CodeSystem-observation-status.json"); @@ -171,7 +171,7 @@ public class ValidateWithRemoteTerminologyTest extends BaseResourceProviderR4Tes final String statusSystem = "http://hl7.org/fhir/event-status"; final String snomedSystem = "http://snomed.info/sct"; - setupValueSetValidateCode("http://hl7.org/fhir/ValueSet/event-status", statusSystem, statusCode, "validation/procedure/validateCode-ValueSet-event-status.json"); + setupValueSetValidateCode("http://hl7.org/fhir/ValueSet/event-status", "4.0.1", statusSystem, statusCode, "validation/procedure/validateCode-ValueSet-event-status.json"); setupValueSetValidateCode("http://hl7.org/fhir/ValueSet/procedure-code", snomedSystem, procedureCode1, "validation/procedure/validateCode-ValueSet-procedure-code-valid.json"); setupValueSetValidateCode("http://hl7.org/fhir/ValueSet/procedure-code", snomedSystem, procedureCode2, "validation/procedure/validateCode-ValueSet-procedure-code-invalid.json"); @@ -213,7 +213,7 @@ public class ValidateWithRemoteTerminologyTest extends BaseResourceProviderR4Tes final String snomedSystem = "http://snomed.info/sct"; final String absentUnknownSystem = "http://hl7.org/fhir/uv/ips/CodeSystem/absent-unknown-uv-ips"; - setupValueSetValidateCode("http://hl7.org/fhir/ValueSet/event-status", statusSystem, statusCode, "validation/procedure/validateCode-ValueSet-event-status.json"); + setupValueSetValidateCode("http://hl7.org/fhir/ValueSet/event-status", "4.0.1", statusSystem, statusCode, "validation/procedure/validateCode-ValueSet-event-status.json"); setupValueSetValidateCode("http://hl7.org/fhir/ValueSet/procedure-code", snomedSystem, procedureCode, "validation/procedure/validateCode-ValueSet-procedure-code-invalid-slice.json"); setupValueSetValidateCode("http://hl7.org/fhir/uv/ips/ValueSet/absent-or-unknown-procedures-uv-ips", absentUnknownSystem, procedureCode, "validation/procedure/validateCode-ValueSet-absent-or-unknown-procedure.json"); @@ -245,6 +245,13 @@ public class ValidateWithRemoteTerminologyTest extends BaseResourceProviderR4Tes // which also attempts a validateCode against the CodeSystem after the validateCode against the ValueSet } + private void setupValueSetValidateCode(String theUrl, String theVersion, String theSystem, String theCode, String theTerminologyResponseFile) { + ValueSet valueSet = myValueSetProvider.addTerminologyResource(theUrl, theVersion); + myValueSetProvider.addTerminologyResource(theSystem, theVersion); + myValueSetProvider.addTerminologyResponse(OPERATION_VALIDATE_CODE, valueSet.getUrl(), theCode, ourCtx, theTerminologyResponseFile); + + valueSet.getCompose().addInclude().setSystem(theSystem); + } private void setupCodeSystemValidateCode(String theUrl, String theCode, String theTerminologyResponseFile) { CodeSystem codeSystem = myCodeSystemProvider.addTerminologyResource(theUrl); myCodeSystemProvider.addTerminologyResponse(OPERATION_VALIDATE_CODE, codeSystem.getUrl(), theCode, ourCtx, theTerminologyResponseFile); diff --git a/hapi-fhir-jpaserver-test-r5/src/test/java/ca/uhn/fhir/jpa/provider/r5/ResourceProviderR5Test.java b/hapi-fhir-jpaserver-test-r5/src/test/java/ca/uhn/fhir/jpa/provider/r5/ResourceProviderR5Test.java index 4ee5ea481c4..bf2d0f12509 100644 --- a/hapi-fhir-jpaserver-test-r5/src/test/java/ca/uhn/fhir/jpa/provider/r5/ResourceProviderR5Test.java +++ b/hapi-fhir-jpaserver-test-r5/src/test/java/ca/uhn/fhir/jpa/provider/r5/ResourceProviderR5Test.java @@ -285,7 +285,6 @@ public class ResourceProviderR5Test extends BaseResourceProviderR5Test { "source": "#384dd6bccaeafa6c" }, "url": "https://health.gov.on.ca/idms/fhir/SearchParameter/MedicinalProductDefinition-SearchableString", - "version": "1.0.0", "name": "MedicinalProductDefinitionSearchableString", "status": "active", "publisher": "MOH-IDMS", diff --git a/hapi-fhir-test-utilities/src/main/java/ca/uhn/fhir/test/utilities/validation/IValidationProviders.java b/hapi-fhir-test-utilities/src/main/java/ca/uhn/fhir/test/utilities/validation/IValidationProviders.java index d9933708e47..2aba77a6176 100644 --- a/hapi-fhir-test-utilities/src/main/java/ca/uhn/fhir/test/utilities/validation/IValidationProviders.java +++ b/hapi-fhir-test-utilities/src/main/java/ca/uhn/fhir/test/utilities/validation/IValidationProviders.java @@ -85,8 +85,12 @@ public interface IValidationProviders { myTerminologyResourceMap.put(theUrl, theResource); } + protected void addVersionedTerminologyResource(String theUrl, String theVersion, T theResource) { + myTerminologyResourceMap.put(theUrl + "|" + theVersion, theResource); + } public abstract T addTerminologyResource(String theUrl); + public abstract T addTerminologyResource(String theUrl, String theVersion); protected IBaseParameters getTerminologyResponse(String theOperation, String theUrl, String theCode) throws Exception { String inputKey = getInputKey(theOperation, theUrl, theCode); if (myExceptionMap.containsKey(inputKey)) { diff --git a/hapi-fhir-test-utilities/src/main/java/ca/uhn/fhir/test/utilities/validation/IValidationProvidersDstu3.java b/hapi-fhir-test-utilities/src/main/java/ca/uhn/fhir/test/utilities/validation/IValidationProvidersDstu3.java index 11c5244df41..19e1b28e09f 100644 --- a/hapi-fhir-test-utilities/src/main/java/ca/uhn/fhir/test/utilities/validation/IValidationProvidersDstu3.java +++ b/hapi-fhir-test-utilities/src/main/java/ca/uhn/fhir/test/utilities/validation/IValidationProvidersDstu3.java @@ -95,6 +95,10 @@ public interface IValidationProvidersDstu3 { addTerminologyResource(theUrl, codeSystem); return codeSystem; } + @Override + public CodeSystem addTerminologyResource(String theUrl, String theVersion) { + return addTerminologyResource(theUrl); + } } @SuppressWarnings("unused") @@ -133,5 +137,9 @@ public interface IValidationProvidersDstu3 { addTerminologyResource(theUrl, valueSet); return valueSet; } + @Override + public ValueSet addTerminologyResource(String theUrl, String theVersion) { + return addTerminologyResource(theUrl); + } } } diff --git a/hapi-fhir-test-utilities/src/main/java/ca/uhn/fhir/test/utilities/validation/IValidationProvidersR4.java b/hapi-fhir-test-utilities/src/main/java/ca/uhn/fhir/test/utilities/validation/IValidationProvidersR4.java index 87a3dacb1fc..ec7789664ed 100644 --- a/hapi-fhir-test-utilities/src/main/java/ca/uhn/fhir/test/utilities/validation/IValidationProvidersR4.java +++ b/hapi-fhir-test-utilities/src/main/java/ca/uhn/fhir/test/utilities/validation/IValidationProvidersR4.java @@ -99,6 +99,14 @@ public interface IValidationProvidersR4 { addTerminologyResource(theUrl, codeSystem); return codeSystem; } + + @Override + public CodeSystem addTerminologyResource(String theUrl, String theVersion) { + CodeSystem codeSystem = addTerminologyResource(theUrl); + codeSystem.setVersion(theVersion); + addVersionedTerminologyResource(theUrl, theVersion, codeSystem); + return codeSystem; + } } @SuppressWarnings("unused") @@ -138,5 +146,12 @@ public interface IValidationProvidersR4 { addTerminologyResource(theUrl, valueSet); return valueSet; } + + @Override + public ValueSet addTerminologyResource(String theUrl, String theVersion) { + ValueSet valueSet = addTerminologyResource(theUrl); + addVersionedTerminologyResource(theUrl, theVersion, valueSet); + return valueSet; + } } } diff --git a/hapi-fhir-validation/src/main/java/org/hl7/fhir/common/hapi/validation/validator/VersionSpecificWorkerContextWrapper.java b/hapi-fhir-validation/src/main/java/org/hl7/fhir/common/hapi/validation/validator/VersionSpecificWorkerContextWrapper.java index ac4eb92581c..45d71cecb9d 100644 --- a/hapi-fhir-validation/src/main/java/org/hl7/fhir/common/hapi/validation/validator/VersionSpecificWorkerContextWrapper.java +++ b/hapi-fhir-validation/src/main/java/org/hl7/fhir/common/hapi/validation/validator/VersionSpecificWorkerContextWrapper.java @@ -12,6 +12,7 @@ import ca.uhn.fhir.rest.server.exceptions.InternalErrorException; import ca.uhn.hapi.converters.canonical.VersionCanonicalizer; import jakarta.annotation.Nonnull; import jakarta.annotation.Nullable; +import org.apache.commons.lang3.StringUtils; import org.apache.commons.lang3.Validate; import org.apache.commons.lang3.builder.EqualsBuilder; import org.apache.commons.lang3.builder.HashCodeBuilder; @@ -460,20 +461,13 @@ public class VersionSpecificWorkerContextWrapper extends I18nBase implements IWo return null; } - String uri = theUri; - // handle profile version, if present - if (theUri.contains("|")) { - String[] parts = theUri.split("\\|"); - if (parts.length == 2) { - uri = parts[0]; - } else { - ourLog.warn("Unrecognized profile uri: {}", theUri); - } + if (StringUtils.countMatches(theUri, "|") > 1) { + ourLog.warn("Unrecognized profile uri: {}", theUri); } String resourceType = getResourceType(class_); @SuppressWarnings("unchecked") - T retVal = (T) fetchResource(resourceType, uri); + T retVal = (T) fetchResource(resourceType, theUri); return retVal; } diff --git a/pom.xml b/pom.xml index 366d1dd2f21..b65926a9a26 100644 --- a/pom.xml +++ b/pom.xml @@ -959,6 +959,11 @@ Ibrahim Tallouzi Trifork A/S + + MangalaEkanayake + Mangala Ekanayake + Cambio +