[6463] use version in canonical url of StructureDefinition to identify it (#6534)

* [6463] use version in canonical url of StructureDefinition to identify it

* Add credit for #6534

---------

Co-authored-by: James Agnew <jamesagnew@gmail.com>
This commit is contained in:
Mangala Ekanayake 2024-12-07 02:30:23 +05:30 committed by GitHub
parent 06580742d4
commit f1318915fe
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
11 changed files with 189 additions and 23 deletions

View File

@ -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!"

View File

@ -235,7 +235,12 @@ public class JpaPersistedResourceValidationSupport implements IValidationSupport
} }
SearchParameterMap params = new SearchParameterMap(); SearchParameterMap params = new SearchParameterMap();
params.setLoadSynchronousUpTo(1); 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); search = myDaoRegistry.getResourceDao("StructureDefinition").search(params);
break; break;
} }

View File

@ -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<SearchParameterMap> 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");
}
}
}

View File

@ -123,8 +123,8 @@ public class RemoteTerminologyServiceJpaR4Test extends BaseJpaR4Test {
// Verify 1 // Verify 1
Assertions.assertEquals(2, myCaptureQueriesListener.countGetConnections()); Assertions.assertEquals(2, myCaptureQueriesListener.countGetConnections());
assertThat(ourValueSetProvider.mySearchUrls).asList().containsExactlyInAnyOrder( 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" "http://hl7.org/fhir/ValueSet/administrative-gender","http://hl7.org/fhir/ValueSet/administrative-gender"
); );
assertThat(ourCodeSystemProvider.mySearchUrls).asList().containsExactlyInAnyOrder( assertThat(ourCodeSystemProvider.mySearchUrls).asList().containsExactlyInAnyOrder(
"http://hl7.org/fhir/administrative-gender", "http://hl7.org/fhir/administrative-gender",
@ -162,7 +162,7 @@ public class RemoteTerminologyServiceJpaR4Test extends BaseJpaR4Test {
// Verify 1 // Verify 1
Assertions.assertEquals(0, myCaptureQueriesListener.countGetConnections()); Assertions.assertEquals(0, myCaptureQueriesListener.countGetConnections());
assertThat(ourValueSetProvider.mySearchUrls).asList().containsExactlyInAnyOrder( 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" "http://hl7.org/fhir/ValueSet/administrative-gender"
); );
assertThat(ourValueSetProvider.myValidatedCodes).asList().containsExactlyInAnyOrder( assertThat(ourValueSetProvider.myValidatedCodes).asList().containsExactlyInAnyOrder(
@ -215,14 +215,18 @@ public class RemoteTerminologyServiceJpaR4Test extends BaseJpaR4Test {
// Verify 1 // Verify 1
Assertions.assertEquals(0, myCaptureQueriesListener.countGetConnections()); Assertions.assertEquals(0, myCaptureQueriesListener.countGetConnections());
assertThat(ourValueSetProvider.mySearchUrls).asList().containsExactlyInAnyOrder( 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" "http://hl7.org/fhir/ValueSet/administrative-gender"
); );
assertThat(ourValueSetProvider.myValidatedCodes).asList().containsExactlyInAnyOrder( 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) // Test 2 (should rely on caches)
ourCodeSystemProvider.clearCalls(); ourCodeSystemProvider.clearCalls();

View File

@ -94,7 +94,7 @@ public class ValidateWithRemoteTerminologyTest extends BaseResourceProviderR4Tes
final String classSystem = "http://terminology.hl7.org/CodeSystem/v3-ActCode"; final String classSystem = "http://terminology.hl7.org/CodeSystem/v3-ActCode";
final String identifierTypeSystem = "http://terminology.hl7.org/CodeSystem/v2-0203"; 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://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"); 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 loincSystem = "http://loinc.org";
final String system = "http://fhir.infoway-inforoute.ca/io/psca/CodeSystem/ICD9CM"; 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"); 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"); 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 statusSystem = "http://hl7.org/fhir/event-status";
final String snomedSystem = "http://snomed.info/sct"; 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, 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"); 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 snomedSystem = "http://snomed.info/sct";
final String absentUnknownSystem = "http://hl7.org/fhir/uv/ips/CodeSystem/absent-unknown-uv-ips"; 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/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"); 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 // 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) { private void setupCodeSystemValidateCode(String theUrl, String theCode, String theTerminologyResponseFile) {
CodeSystem codeSystem = myCodeSystemProvider.addTerminologyResource(theUrl); CodeSystem codeSystem = myCodeSystemProvider.addTerminologyResource(theUrl);
myCodeSystemProvider.addTerminologyResponse(OPERATION_VALIDATE_CODE, codeSystem.getUrl(), theCode, ourCtx, theTerminologyResponseFile); myCodeSystemProvider.addTerminologyResponse(OPERATION_VALIDATE_CODE, codeSystem.getUrl(), theCode, ourCtx, theTerminologyResponseFile);

View File

@ -285,7 +285,6 @@ public class ResourceProviderR5Test extends BaseResourceProviderR5Test {
"source": "#384dd6bccaeafa6c" "source": "#384dd6bccaeafa6c"
}, },
"url": "https://health.gov.on.ca/idms/fhir/SearchParameter/MedicinalProductDefinition-SearchableString", "url": "https://health.gov.on.ca/idms/fhir/SearchParameter/MedicinalProductDefinition-SearchableString",
"version": "1.0.0",
"name": "MedicinalProductDefinitionSearchableString", "name": "MedicinalProductDefinitionSearchableString",
"status": "active", "status": "active",
"publisher": "MOH-IDMS", "publisher": "MOH-IDMS",

View File

@ -85,8 +85,12 @@ public interface IValidationProviders {
myTerminologyResourceMap.put(theUrl, theResource); 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);
public abstract T addTerminologyResource(String theUrl, String theVersion);
protected IBaseParameters getTerminologyResponse(String theOperation, String theUrl, String theCode) throws Exception { protected IBaseParameters getTerminologyResponse(String theOperation, String theUrl, String theCode) throws Exception {
String inputKey = getInputKey(theOperation, theUrl, theCode); String inputKey = getInputKey(theOperation, theUrl, theCode);
if (myExceptionMap.containsKey(inputKey)) { if (myExceptionMap.containsKey(inputKey)) {

View File

@ -95,6 +95,10 @@ public interface IValidationProvidersDstu3 {
addTerminologyResource(theUrl, codeSystem); addTerminologyResource(theUrl, codeSystem);
return codeSystem; return codeSystem;
} }
@Override
public CodeSystem addTerminologyResource(String theUrl, String theVersion) {
return addTerminologyResource(theUrl);
}
} }
@SuppressWarnings("unused") @SuppressWarnings("unused")
@ -133,5 +137,9 @@ public interface IValidationProvidersDstu3 {
addTerminologyResource(theUrl, valueSet); addTerminologyResource(theUrl, valueSet);
return valueSet; return valueSet;
} }
@Override
public ValueSet addTerminologyResource(String theUrl, String theVersion) {
return addTerminologyResource(theUrl);
}
} }
} }

View File

@ -99,6 +99,14 @@ public interface IValidationProvidersR4 {
addTerminologyResource(theUrl, codeSystem); addTerminologyResource(theUrl, codeSystem);
return 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") @SuppressWarnings("unused")
@ -138,5 +146,12 @@ public interface IValidationProvidersR4 {
addTerminologyResource(theUrl, valueSet); addTerminologyResource(theUrl, valueSet);
return valueSet; return valueSet;
} }
@Override
public ValueSet addTerminologyResource(String theUrl, String theVersion) {
ValueSet valueSet = addTerminologyResource(theUrl);
addVersionedTerminologyResource(theUrl, theVersion, valueSet);
return valueSet;
}
} }
} }

View File

@ -12,6 +12,7 @@ import ca.uhn.fhir.rest.server.exceptions.InternalErrorException;
import ca.uhn.hapi.converters.canonical.VersionCanonicalizer; import ca.uhn.hapi.converters.canonical.VersionCanonicalizer;
import jakarta.annotation.Nonnull; import jakarta.annotation.Nonnull;
import jakarta.annotation.Nullable; import jakarta.annotation.Nullable;
import org.apache.commons.lang3.StringUtils;
import org.apache.commons.lang3.Validate; import org.apache.commons.lang3.Validate;
import org.apache.commons.lang3.builder.EqualsBuilder; import org.apache.commons.lang3.builder.EqualsBuilder;
import org.apache.commons.lang3.builder.HashCodeBuilder; import org.apache.commons.lang3.builder.HashCodeBuilder;
@ -460,20 +461,13 @@ public class VersionSpecificWorkerContextWrapper extends I18nBase implements IWo
return null; return null;
} }
String uri = theUri; if (StringUtils.countMatches(theUri, "|") > 1) {
// handle profile version, if present ourLog.warn("Unrecognized profile uri: {}", theUri);
if (theUri.contains("|")) {
String[] parts = theUri.split("\\|");
if (parts.length == 2) {
uri = parts[0];
} else {
ourLog.warn("Unrecognized profile uri: {}", theUri);
}
} }
String resourceType = getResourceType(class_); String resourceType = getResourceType(class_);
@SuppressWarnings("unchecked") @SuppressWarnings("unchecked")
T retVal = (T) fetchResource(resourceType, uri); T retVal = (T) fetchResource(resourceType, theUri);
return retVal; return retVal;
} }

View File

@ -959,6 +959,11 @@
<name>Ibrahim Tallouzi</name> <name>Ibrahim Tallouzi</name>
<organization>Trifork A/S</organization> <organization>Trifork A/S</organization>
</developer> </developer>
<developer>
<id>MangalaEkanayake</id>
<name>Mangala Ekanayake</name>
<organization>Cambio</organization>
</developer>
</developers> </developers>
<licenses> <licenses>