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:
Jens Kristian Villadsen
2026-02-05 15:28:02 +01:00
committed by GitHub
parent fc395b613a
commit 63256fe1d2
4 changed files with 599 additions and 0 deletions

View File

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

View File

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