Add fallback for versioned base FHIR StructureDefinition lookups (#911)
* Add fallback for versioned base FHIR StructureDefinition lookups Problem: Implementation Guides (e.g., ch-core) may reference base FHIR resources with explicit versions like "http://hl7.org/fhir/StructureDefinition/Organization|4.0.1". These lookups fail because base FHIR StructureDefinitions are intentionally cached without version (see hapi-fhir PR #7236). This causes validation errors like: - "Unable to resolve the profile reference '...|4.0.1'" - "Invalid Resource target type. Found Organization, but expected one of ([])" Solution: Add VersionedUrlFallbackValidationSupport that intercepts versioned lookups for base FHIR StructureDefinitions (http://hl7.org/fhir/StructureDefinition/*) and falls back to: 1. Major.minor version (e.g., 4.0.1 -> 4.0) 2. Non-versioned URL The fallback logs a warning when triggered, allowing visibility into which IGs reference versioned base resources. Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> * Remove duplicates * formatting * removing unneeded thread local * corrected according to review * cleanup * removed not needed const * tests include some chain now * Update src/test/java/ca/uhn/fhir/jpa/starter/validation/VersionedUrlFallbackValidationSupportTest.java Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> * updated doc * adding tests * Add TODO comment for core fix in validation support Added a comment indicating a TODO for future improvement. * Update TODO comment for core fix in validation support --------- Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com> Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
This commit is contained in:
committed by
GitHub
parent
fc395b613a
commit
63256fe1d2
@@ -0,0 +1,29 @@
|
||||
package ca.uhn.fhir.jpa.starter.validation;
|
||||
|
||||
import ca.uhn.fhir.context.FhirContext;
|
||||
import org.hl7.fhir.common.hapi.validation.support.ValidationSupportChain;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
|
||||
/**
|
||||
* Configuration that enables versioned URL fallback behavior for FHIR validation.
|
||||
*
|
||||
* This wraps the validation support chain to add fallback logic for versioned canonical URLs.
|
||||
* When a versioned URL like "http://hl7.org/fhir/StructureDefinition/Organization|4.0.1"
|
||||
* cannot be found, it will automatically fall back to Non-versioned URL (without the |version suffix)
|
||||
*
|
||||
* This is useful when Implementation Guides reference versioned base FHIR resources
|
||||
* that aren't loaded with exact version matching.
|
||||
*/
|
||||
@Configuration
|
||||
public class VersionedUrlFallbackConfig {
|
||||
|
||||
private static final Logger ourLog = LoggerFactory.getLogger(VersionedUrlFallbackConfig.class);
|
||||
|
||||
public VersionedUrlFallbackConfig(FhirContext theFhirContext, ValidationSupportChain theValidationSupportChain) {
|
||||
ourLog.info("Adding VersionedUrlFallbackValidationSupport to validation chain");
|
||||
theValidationSupportChain.addValidationSupport(
|
||||
0, new VersionedUrlFallbackValidationSupport(theFhirContext, theValidationSupportChain));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,122 @@
|
||||
package ca.uhn.fhir.jpa.starter.validation;
|
||||
|
||||
import ca.uhn.fhir.context.FhirContext;
|
||||
import ca.uhn.fhir.context.support.IValidationSupport;
|
||||
import org.hl7.fhir.instance.model.api.IBaseResource;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import java.util.Set;
|
||||
import java.util.function.Function;
|
||||
|
||||
/**
|
||||
* A validation support that provides fallback behavior for versioned canonical URLs.
|
||||
*
|
||||
* When a versioned URL like "http://hl7.org/fhir/StructureDefinition/Organization|4.0.1"
|
||||
* is requested, this support first tries the exact versioned URL, then falls back to
|
||||
* the non-versioned URL if not found.
|
||||
*
|
||||
* For non-versioned URLs or URLs not matching the configured prefixes, this support
|
||||
* returns null to let other supports in the chain handle the request.
|
||||
*
|
||||
* This addresses issues where profiles reference versioned base FHIR resources that
|
||||
* aren't available with exact version matching in the validation context.
|
||||
*/
|
||||
// TODO: this should be fixed in core
|
||||
public class VersionedUrlFallbackValidationSupport implements IValidationSupport {
|
||||
|
||||
private static final Logger ourLog = LoggerFactory.getLogger(VersionedUrlFallbackValidationSupport.class);
|
||||
|
||||
private final FhirContext myFhirContext;
|
||||
private final IValidationSupport myChain;
|
||||
private final Set<String> myUrlPrefixes;
|
||||
|
||||
/**
|
||||
* Creates a fallback validation support that only applies to URLs starting with the default prefix
|
||||
* (http://hl7.org/fhir/StructureDefinition/).
|
||||
*/
|
||||
public VersionedUrlFallbackValidationSupport(FhirContext theFhirContext, IValidationSupport theChain) {
|
||||
this(theFhirContext, theChain, Set.of(URL_PREFIX_STRUCTURE_DEFINITION));
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a fallback validation support that only applies to URLs starting with the specified prefixes.
|
||||
*
|
||||
* @param theFhirContext the FHIR context
|
||||
* @param theChain the validation support chain to delegate fallback lookups to
|
||||
* @param theUrlPrefixes the URL prefixes to apply fallback logic to (e.g., "http://hl7.org/fhir/StructureDefinition/").
|
||||
* Pass an empty set to apply to all URLs.
|
||||
*/
|
||||
public VersionedUrlFallbackValidationSupport(
|
||||
FhirContext theFhirContext, IValidationSupport theChain, Set<String> theUrlPrefixes) {
|
||||
myFhirContext = theFhirContext;
|
||||
myChain = theChain;
|
||||
myUrlPrefixes = theUrlPrefixes;
|
||||
}
|
||||
|
||||
@Override
|
||||
public FhirContext getFhirContext() {
|
||||
return myFhirContext;
|
||||
}
|
||||
|
||||
@Override
|
||||
public <T extends IBaseResource> T fetchResource(Class<T> theClass, String theUri) {
|
||||
return doFetchWithFallback(theUri, uri -> myChain.fetchResource(theClass, uri));
|
||||
}
|
||||
|
||||
@Override
|
||||
public IBaseResource fetchStructureDefinition(String theUrl) {
|
||||
return doFetchWithFallback(theUrl, myChain::fetchStructureDefinition);
|
||||
}
|
||||
|
||||
private <T extends IBaseResource> T doFetchWithFallback(String theUrl, Function<String, T> theFetcher) {
|
||||
// Check if this is a versioned URL (contains |)
|
||||
int pipeIndex = theUrl.indexOf('|');
|
||||
if (pipeIndex <= 0) {
|
||||
// Not a versioned URL, let other supports handle it
|
||||
return null;
|
||||
}
|
||||
|
||||
String baseUrl = theUrl.substring(0, pipeIndex);
|
||||
|
||||
// Check if this URL matches our configured prefixes
|
||||
if (!matchesPrefix(baseUrl)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// Try exact versioned URL first
|
||||
T result = theFetcher.apply(theUrl);
|
||||
if (result != null) {
|
||||
return result;
|
||||
}
|
||||
|
||||
// Try non-versioned URL fallback
|
||||
result = theFetcher.apply(baseUrl);
|
||||
if (result != null) {
|
||||
ourLog.warn(
|
||||
"Requested versioned canonical '{}' not found, falling back to non-versioned '{}'",
|
||||
theUrl,
|
||||
baseUrl);
|
||||
return result;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
private boolean matchesPrefix(String theUrl) {
|
||||
if (myUrlPrefixes.isEmpty()) {
|
||||
return true;
|
||||
}
|
||||
for (String prefix : myUrlPrefixes) {
|
||||
if (theUrl.startsWith(prefix)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getName() {
|
||||
return "VersionedUrlFallbackValidationSupport";
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,447 @@
|
||||
package ca.uhn.fhir.jpa.starter.validation;
|
||||
|
||||
import ca.uhn.fhir.context.FhirContext;
|
||||
import ca.uhn.fhir.context.support.DefaultProfileValidationSupport;
|
||||
import ca.uhn.fhir.context.support.IValidationSupport;
|
||||
import ca.uhn.fhir.validation.FhirValidator;
|
||||
import ca.uhn.fhir.validation.ValidationResult;
|
||||
import org.hl7.fhir.common.hapi.validation.support.CommonCodeSystemsTerminologyService;
|
||||
import org.hl7.fhir.common.hapi.validation.support.InMemoryTerminologyServerValidationSupport;
|
||||
import org.hl7.fhir.common.hapi.validation.support.ValidationSupportChain;
|
||||
import org.hl7.fhir.common.hapi.validation.validator.FhirInstanceValidator;
|
||||
import org.hl7.fhir.r4.model.Observation;
|
||||
import org.hl7.fhir.r4.model.StructureDefinition;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Nested;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.extension.ExtendWith;
|
||||
import org.mockito.Mock;
|
||||
import org.mockito.junit.jupiter.MockitoExtension;
|
||||
|
||||
import java.util.Set;
|
||||
|
||||
import static ca.uhn.fhir.context.support.IValidationSupport.URL_PREFIX_STRUCTURE_DEFINITION;
|
||||
import static org.junit.jupiter.api.Assertions.*;
|
||||
import static org.mockito.Mockito.*;
|
||||
|
||||
@ExtendWith(MockitoExtension.class)
|
||||
class VersionedUrlFallbackValidationSupportTest {
|
||||
|
||||
private static final String BASE_FHIR_SD_PREFIX = "http://hl7.org/fhir/StructureDefinition/";
|
||||
private static final String ORGANIZATION_URL = BASE_FHIR_SD_PREFIX + "Organization";
|
||||
private static final String ORGANIZATION_URL_VERSIONED = ORGANIZATION_URL + "|4.0.1";
|
||||
|
||||
private static final String CUSTOM_SD_URL = "http://example.com/StructureDefinition/MyProfile";
|
||||
private static final String CUSTOM_SD_URL_VERSIONED = CUSTOM_SD_URL + "|1.0.0";
|
||||
|
||||
private FhirContext myFhirContext;
|
||||
|
||||
@Mock
|
||||
private IValidationSupport myChain;
|
||||
|
||||
private VersionedUrlFallbackValidationSupport mySvc;
|
||||
|
||||
@BeforeEach
|
||||
void setUp() {
|
||||
myFhirContext = FhirContext.forR4Cached();
|
||||
mySvc = new VersionedUrlFallbackValidationSupport(myFhirContext, myChain);
|
||||
}
|
||||
|
||||
@Test
|
||||
void testExactVersionedUrl_ReturnedWithoutFallback() {
|
||||
// Setup: exact versioned URL is available
|
||||
StructureDefinition sd = new StructureDefinition();
|
||||
sd.setUrl(ORGANIZATION_URL);
|
||||
sd.setVersion("4.0.1");
|
||||
|
||||
when(myChain.fetchStructureDefinition(ORGANIZATION_URL_VERSIONED)).thenReturn(sd);
|
||||
|
||||
// Execute
|
||||
var result = mySvc.fetchStructureDefinition(ORGANIZATION_URL_VERSIONED);
|
||||
|
||||
// Verify: returns exact match, no fallback attempted
|
||||
assertNotNull(result);
|
||||
assertSame(sd, result);
|
||||
verify(myChain).fetchStructureDefinition(ORGANIZATION_URL_VERSIONED);
|
||||
verify(myChain, never()).fetchStructureDefinition(ORGANIZATION_URL);
|
||||
}
|
||||
|
||||
@Test
|
||||
void testFallbackToNonVersionedUrl() {
|
||||
// Setup: exact versioned URL not found, non-versioned returns a resource
|
||||
StructureDefinition sd = new StructureDefinition();
|
||||
sd.setUrl(ORGANIZATION_URL);
|
||||
|
||||
when(myChain.fetchStructureDefinition(ORGANIZATION_URL_VERSIONED)).thenReturn(null);
|
||||
when(myChain.fetchStructureDefinition(ORGANIZATION_URL)).thenReturn(sd);
|
||||
|
||||
// Execute
|
||||
var result = mySvc.fetchStructureDefinition(ORGANIZATION_URL_VERSIONED);
|
||||
|
||||
// Verify: fallback to non-versioned succeeds
|
||||
assertNotNull(result);
|
||||
assertSame(sd, result);
|
||||
verify(myChain).fetchStructureDefinition(ORGANIZATION_URL_VERSIONED);
|
||||
verify(myChain).fetchStructureDefinition(ORGANIZATION_URL);
|
||||
}
|
||||
|
||||
@Test
|
||||
void testNoFallback_ForNonVersionedUrl() {
|
||||
// Execute: non-versioned URL should pass through without any chain calls
|
||||
var result = mySvc.fetchStructureDefinition(ORGANIZATION_URL);
|
||||
|
||||
// Verify: returns null immediately, lets other chain supports handle it
|
||||
assertNull(result);
|
||||
verifyNoInteractions(myChain);
|
||||
}
|
||||
|
||||
@Test
|
||||
void testNoFallback_ForCustomUrlNotMatchingDefaultPrefix() {
|
||||
// Execute: custom URL doesn't match the default prefix filter
|
||||
var result = mySvc.fetchStructureDefinition(CUSTOM_SD_URL_VERSIONED);
|
||||
|
||||
// Verify: returns null, doesn't attempt fallback (not in prefix list)
|
||||
assertNull(result);
|
||||
verifyNoInteractions(myChain);
|
||||
}
|
||||
|
||||
@Test
|
||||
void testFallback_ForCustomUrl_WhenPrefixConfigured() {
|
||||
// Setup: configure to also handle custom URLs
|
||||
mySvc = new VersionedUrlFallbackValidationSupport(myFhirContext, myChain,
|
||||
Set.of(BASE_FHIR_SD_PREFIX, "http://example.com/StructureDefinition/"));
|
||||
|
||||
StructureDefinition sd = new StructureDefinition();
|
||||
sd.setUrl(CUSTOM_SD_URL);
|
||||
|
||||
when(myChain.fetchStructureDefinition(CUSTOM_SD_URL_VERSIONED)).thenReturn(null);
|
||||
when(myChain.fetchStructureDefinition(CUSTOM_SD_URL)).thenReturn(sd);
|
||||
|
||||
// Execute
|
||||
var result = mySvc.fetchStructureDefinition(CUSTOM_SD_URL_VERSIONED);
|
||||
|
||||
// Verify
|
||||
assertNotNull(result);
|
||||
assertSame(sd, result);
|
||||
}
|
||||
|
||||
@Test
|
||||
void testFallback_ForAllUrls_WhenEmptyPrefixSet() {
|
||||
// Setup: empty prefix set means apply to all URLs
|
||||
mySvc = new VersionedUrlFallbackValidationSupport(myFhirContext, myChain, Set.of());
|
||||
|
||||
StructureDefinition sd = new StructureDefinition();
|
||||
sd.setUrl(CUSTOM_SD_URL);
|
||||
|
||||
when(myChain.fetchStructureDefinition(CUSTOM_SD_URL_VERSIONED)).thenReturn(null);
|
||||
when(myChain.fetchStructureDefinition(CUSTOM_SD_URL)).thenReturn(sd);
|
||||
|
||||
// Execute
|
||||
var result = mySvc.fetchStructureDefinition(CUSTOM_SD_URL_VERSIONED);
|
||||
|
||||
// Verify
|
||||
assertNotNull(result);
|
||||
assertSame(sd, result);
|
||||
}
|
||||
|
||||
@Test
|
||||
void testFetchResource_FallbackToNonVersioned() {
|
||||
// Setup
|
||||
StructureDefinition sd = new StructureDefinition();
|
||||
sd.setUrl(ORGANIZATION_URL);
|
||||
|
||||
when(myChain.fetchResource(StructureDefinition.class, ORGANIZATION_URL_VERSIONED)).thenReturn(null);
|
||||
when(myChain.fetchResource(StructureDefinition.class, ORGANIZATION_URL)).thenReturn(sd);
|
||||
|
||||
// Execute
|
||||
var result = mySvc.fetchResource(StructureDefinition.class, ORGANIZATION_URL_VERSIONED);
|
||||
|
||||
// Verify
|
||||
assertNotNull(result);
|
||||
assertSame(sd, result);
|
||||
}
|
||||
|
||||
@Test
|
||||
void testFetchResource_NoFallbackForNonMatchingPrefix() {
|
||||
// Execute
|
||||
var result = mySvc.fetchResource(StructureDefinition.class, CUSTOM_SD_URL_VERSIONED);
|
||||
|
||||
// Verify
|
||||
assertNull(result);
|
||||
verifyNoInteractions(myChain);
|
||||
}
|
||||
|
||||
@Test
|
||||
void testFetchResource_NoFallbackForNonVersionedUrl() {
|
||||
// Execute
|
||||
var result = mySvc.fetchResource(StructureDefinition.class, ORGANIZATION_URL);
|
||||
|
||||
// Verify
|
||||
assertNull(result);
|
||||
verifyNoInteractions(myChain);
|
||||
}
|
||||
|
||||
@Test
|
||||
void testReturnsNull_WhenNoFallbackSucceeds() {
|
||||
// Setup: nothing found in any lookup
|
||||
when(myChain.fetchStructureDefinition(ORGANIZATION_URL_VERSIONED)).thenReturn(null);
|
||||
when(myChain.fetchStructureDefinition(ORGANIZATION_URL)).thenReturn(null);
|
||||
|
||||
// Execute
|
||||
var result = mySvc.fetchStructureDefinition(ORGANIZATION_URL_VERSIONED);
|
||||
|
||||
// Verify
|
||||
assertNull(result);
|
||||
verify(myChain).fetchStructureDefinition(ORGANIZATION_URL_VERSIONED);
|
||||
verify(myChain).fetchStructureDefinition(ORGANIZATION_URL);
|
||||
}
|
||||
|
||||
@Test
|
||||
void testGetName() {
|
||||
assertEquals("VersionedUrlFallbackValidationSupport", mySvc.getName());
|
||||
}
|
||||
|
||||
@Test
|
||||
void testGetFhirContext() {
|
||||
assertSame(myFhirContext, mySvc.getFhirContext());
|
||||
}
|
||||
|
||||
@Test
|
||||
void testDefaultUrlPrefix() {
|
||||
assertEquals("http://hl7.org/fhir/StructureDefinition/",
|
||||
URL_PREFIX_STRUCTURE_DEFINITION);
|
||||
}
|
||||
|
||||
/**
|
||||
* Integration tests using real DefaultProfileValidationSupport instead of mocks.
|
||||
* This tests the actual fallback behavior with FHIR's built-in profiles.
|
||||
*/
|
||||
@Nested
|
||||
class WithRealValidationChain {
|
||||
|
||||
private FhirContext myFhirContext;
|
||||
private ValidationSupportChain myValidationChain;
|
||||
private VersionedUrlFallbackValidationSupport mySvc;
|
||||
|
||||
@BeforeEach
|
||||
void setUp() {
|
||||
myFhirContext = FhirContext.forR4Cached();
|
||||
|
||||
// Create a validation chain with the real DefaultProfileValidationSupport
|
||||
// which contains all built-in FHIR R4 StructureDefinitions
|
||||
myValidationChain = new ValidationSupportChain(new DefaultProfileValidationSupport(myFhirContext));
|
||||
|
||||
// Wrap the chain with our fallback support, similar to production setup
|
||||
mySvc = new VersionedUrlFallbackValidationSupport(myFhirContext, myValidationChain);
|
||||
}
|
||||
|
||||
@Test
|
||||
void testFallbackToNonVersionedUrl_WithRealDefaultProfile() {
|
||||
// The DefaultProfileValidationSupport has Organization without version suffix.
|
||||
// When we request versioned URL, it should fall back and find it.
|
||||
String versionedUrl = "http://hl7.org/fhir/StructureDefinition/Organization|4.0.1";
|
||||
|
||||
var result = mySvc.fetchStructureDefinition(versionedUrl);
|
||||
|
||||
assertNotNull(result, "Should find Organization via fallback to non-versioned URL");
|
||||
assertInstanceOf(StructureDefinition.class, result);
|
||||
StructureDefinition sd = (StructureDefinition) result;
|
||||
assertEquals("http://hl7.org/fhir/StructureDefinition/Organization", sd.getUrl());
|
||||
assertEquals("Organization", sd.getName());
|
||||
}
|
||||
|
||||
@Test
|
||||
void testFallbackForPatient_WithRealDefaultProfile() {
|
||||
String versionedUrl = "http://hl7.org/fhir/StructureDefinition/Patient|4.0.1";
|
||||
|
||||
var result = mySvc.fetchStructureDefinition(versionedUrl);
|
||||
|
||||
assertNotNull(result, "Should find Patient via fallback");
|
||||
assertInstanceOf(StructureDefinition.class, result);
|
||||
StructureDefinition sd = (StructureDefinition) result;
|
||||
assertEquals("http://hl7.org/fhir/StructureDefinition/Patient", sd.getUrl());
|
||||
}
|
||||
|
||||
@Test
|
||||
void testFetchResource_WithRealDefaultProfile() {
|
||||
String versionedUrl = "http://hl7.org/fhir/StructureDefinition/Observation|4.0.1";
|
||||
|
||||
var result = mySvc.fetchResource(StructureDefinition.class, versionedUrl);
|
||||
|
||||
assertNotNull(result, "Should find Observation via fetchResource fallback");
|
||||
assertEquals("http://hl7.org/fhir/StructureDefinition/Observation", result.getUrl());
|
||||
}
|
||||
|
||||
@Test
|
||||
void testNonExistentResource_ReturnsNull() {
|
||||
String versionedUrl = "http://hl7.org/fhir/StructureDefinition/NonExistentResource|1.0.0";
|
||||
|
||||
var result = mySvc.fetchStructureDefinition(versionedUrl);
|
||||
|
||||
assertNull(result, "Should return null for non-existent resource");
|
||||
}
|
||||
|
||||
@Test
|
||||
void testNonVersionedUrl_PassesThrough() {
|
||||
// Non-versioned URLs should return null from the fallback support
|
||||
// (they're handled by DefaultProfileValidationSupport directly in a real chain)
|
||||
String nonVersionedUrl = "http://hl7.org/fhir/StructureDefinition/Patient";
|
||||
|
||||
var result = mySvc.fetchStructureDefinition(nonVersionedUrl);
|
||||
|
||||
// The fallback support returns null for non-versioned URLs
|
||||
// In a real setup, the chain would handle this
|
||||
assertNull(result);
|
||||
}
|
||||
|
||||
@Test
|
||||
void testDataTypeProfiles_WithRealDefaultProfile() {
|
||||
// Test that data type StructureDefinitions also work
|
||||
String versionedUrl = "http://hl7.org/fhir/StructureDefinition/HumanName|4.0.1";
|
||||
|
||||
var result = mySvc.fetchStructureDefinition(versionedUrl);
|
||||
|
||||
assertNotNull(result, "Should find HumanName data type via fallback");
|
||||
assertInstanceOf(StructureDefinition.class, result);
|
||||
assertEquals("http://hl7.org/fhir/StructureDefinition/HumanName",
|
||||
((StructureDefinition) result).getUrl());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Integration tests where VersionedUrlFallbackValidationSupport is part of the
|
||||
* ValidationSupportChain (as in production) rather than wrapping it.
|
||||
*/
|
||||
@Nested
|
||||
class WithFallbackInChain {
|
||||
|
||||
private FhirContext myFhirContext;
|
||||
private ValidationSupportChain myValidationChain;
|
||||
|
||||
@BeforeEach
|
||||
void setUp() {
|
||||
myFhirContext = FhirContext.forR4Cached();
|
||||
DefaultProfileValidationSupport defaultSupport = new DefaultProfileValidationSupport(myFhirContext);
|
||||
|
||||
// Create a chain where VersionedUrlFallbackValidationSupport is a member
|
||||
// This mimics production setup where the fallback is part of the chain
|
||||
myValidationChain = new ValidationSupportChain(defaultSupport);
|
||||
VersionedUrlFallbackValidationSupport fallbackSupport =
|
||||
new VersionedUrlFallbackValidationSupport(myFhirContext, myValidationChain);
|
||||
|
||||
// Rebuild chain with fallback support first (higher priority)
|
||||
myValidationChain = new ValidationSupportChain(fallbackSupport, defaultSupport);
|
||||
}
|
||||
|
||||
@Test
|
||||
void testChainResolvesVersionedUrl() {
|
||||
String versionedUrl = "http://hl7.org/fhir/StructureDefinition/Patient|4.0.1";
|
||||
|
||||
var result = myValidationChain.fetchStructureDefinition(versionedUrl);
|
||||
|
||||
assertNotNull(result, "Chain should resolve versioned URL via fallback");
|
||||
assertInstanceOf(StructureDefinition.class, result);
|
||||
assertEquals("http://hl7.org/fhir/StructureDefinition/Patient",
|
||||
((StructureDefinition) result).getUrl());
|
||||
}
|
||||
|
||||
@Test
|
||||
void testChainResolvesNonVersionedUrl() {
|
||||
// Non-versioned URLs should still work (handled by DefaultProfileValidationSupport)
|
||||
String nonVersionedUrl = "http://hl7.org/fhir/StructureDefinition/Patient";
|
||||
|
||||
var result = myValidationChain.fetchStructureDefinition(nonVersionedUrl);
|
||||
|
||||
assertNotNull(result, "Chain should resolve non-versioned URL directly");
|
||||
assertInstanceOf(StructureDefinition.class, result);
|
||||
}
|
||||
|
||||
@Test
|
||||
void testChainFetchResource() {
|
||||
String versionedUrl = "http://hl7.org/fhir/StructureDefinition/Encounter|4.0.1";
|
||||
|
||||
var result = myValidationChain.fetchResource(StructureDefinition.class, versionedUrl);
|
||||
|
||||
assertNotNull(result, "fetchResource should work through chain with fallback");
|
||||
assertEquals("http://hl7.org/fhir/StructureDefinition/Encounter", result.getUrl());
|
||||
}
|
||||
|
||||
@Test
|
||||
void testMultipleResourceTypes() {
|
||||
// Verify fallback works for various resource types
|
||||
String[] versionedUrls = {
|
||||
"http://hl7.org/fhir/StructureDefinition/Condition|4.0.1",
|
||||
"http://hl7.org/fhir/StructureDefinition/Medication|4.0.1",
|
||||
"http://hl7.org/fhir/StructureDefinition/DiagnosticReport|4.0.1"
|
||||
};
|
||||
|
||||
for (String versionedUrl : versionedUrls) {
|
||||
var result = myValidationChain.fetchStructureDefinition(versionedUrl);
|
||||
assertNotNull(result, "Should resolve " + versionedUrl);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Full validation integration tests using FhirInstanceValidator to prove
|
||||
* the fallback support works in actual resource validation scenarios.
|
||||
*/
|
||||
@Nested
|
||||
class WithFhirInstanceValidator {
|
||||
|
||||
private FhirContext myFhirContext;
|
||||
private FhirValidator myValidator;
|
||||
|
||||
@BeforeEach
|
||||
void setUp() {
|
||||
myFhirContext = FhirContext.forR4Cached();
|
||||
DefaultProfileValidationSupport defaultSupport = new DefaultProfileValidationSupport(myFhirContext);
|
||||
InMemoryTerminologyServerValidationSupport terminologySupport =
|
||||
new InMemoryTerminologyServerValidationSupport(myFhirContext);
|
||||
CommonCodeSystemsTerminologyService commonCodeSystems =
|
||||
new CommonCodeSystemsTerminologyService(myFhirContext);
|
||||
|
||||
// Build production-like validation chain with fallback support
|
||||
ValidationSupportChain baseChain = new ValidationSupportChain(
|
||||
defaultSupport,
|
||||
terminologySupport,
|
||||
commonCodeSystems
|
||||
);
|
||||
|
||||
VersionedUrlFallbackValidationSupport fallbackSupport =
|
||||
new VersionedUrlFallbackValidationSupport(myFhirContext, baseChain);
|
||||
|
||||
// ValidationSupportChain now handles caching internally (since HAPI FHIR 8.0.0)
|
||||
ValidationSupportChain fullChain = new ValidationSupportChain(
|
||||
fallbackSupport,
|
||||
defaultSupport,
|
||||
terminologySupport,
|
||||
commonCodeSystems
|
||||
);
|
||||
|
||||
FhirInstanceValidator instanceValidator = new FhirInstanceValidator(fullChain);
|
||||
myValidator = myFhirContext.newValidator();
|
||||
myValidator.registerValidatorModule(instanceValidator);
|
||||
}
|
||||
|
||||
@Test
|
||||
void testValidateSimpleObservation() {
|
||||
Observation observation = new Observation();
|
||||
observation.setStatus(Observation.ObservationStatus.FINAL);
|
||||
observation.getCode().addCoding()
|
||||
.setSystem("http://loinc.org")
|
||||
.setCode("12345-6")
|
||||
.setDisplay("Test");
|
||||
|
||||
ValidationResult result = myValidator.validateWithResult(observation);
|
||||
|
||||
// The validation should complete without errors related to unresolved versioned URLs
|
||||
assertNotNull(result);
|
||||
// We don't require the resource to be fully valid (may have other issues)
|
||||
// but it should not fail due to missing versioned profile resolution
|
||||
assertTrue(result.getMessages().stream()
|
||||
.noneMatch(m -> m.getMessage().contains("Unable to locate profile")),
|
||||
"Should not have profile resolution errors");
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user