Skip to content
Open
Show file tree
Hide file tree
Changes from 17 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
---
type: add
issue: 7394
title: "A new setting JpaStorageSettings.setAllowDatabaseValidationOverride(boolean) has been added.
When enabled, database-stored CodeSystem and ValueSet resources take precedence over built-in
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is the precedence only for validation purposes or any request?
If it is only for validation, it could be indicated here.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

good catch!

default profile validation resources. This allows user-defined terminology resources to override
built-in HL7 definitions."
Original file line number Diff line number Diff line change
Expand Up @@ -177,6 +177,32 @@ these generated issues, you can use the [ValidationMessageUnknownCodeSystemPostP
This module is deprecated and no longer provides any functionality. Caching is provided by [ValidationSupportChain](#validationsupportchain).


# JPA Server Validation Chain Configuration

When using the JPA Server, the validation support chain is configured automatically. By default, the built-in FHIR definitions (from `DefaultProfileValidationSupport`) take precedence over user-defined terminology resources stored in the database (from `JpaPersistedResourceValidationSupport`).

## Allowing Database Validation Override

In some cases, you may want your database-stored CodeSystem and ValueSet resources to take precedence over the built-in HL7 definitions. This is useful when you need to override a built-in terminology resource with a different version of that resource.

To enable this behavior, use the `allowDatabaseValidationOverride` setting:

```java
@Bean
public JpaStorageSettings storageSettings() {
JpaStorageSettings retVal = new JpaStorageSettings();
// Allow database-stored validation resources to take precedence
retVal.setAllowDatabaseValidationOverride(true);
return retVal;
}
```

When this setting is enabled (`true`), the JPA validation support (database-stored resources) is placed before the default profile validation support in the chain. When disabled (`false`, the default), the built-in definitions take precedence.

<p class="doc_info_bubble">
<b>Note:</b> Enabling this setting means that any CodeSystem or ValueSet you store in the database with the same URL as a built-in HL7 resource will override the built-in version during validation. Use this setting carefully as it can affect validation behavior for standard FHIR resources.
</p>

# Recipes

The IValidationSupport instance passed to the FhirInstanceValidator will often resemble the chain shown in the diagram below. In this diagram:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,10 +21,12 @@

import ca.uhn.fhir.context.FhirContext;
import ca.uhn.fhir.context.support.IValidationSupport;
import ca.uhn.fhir.jpa.api.config.JpaStorageSettings;
import ca.uhn.fhir.jpa.config.JpaConfig;
import ca.uhn.fhir.jpa.packages.NpmJpaValidationSupport;
import ca.uhn.fhir.jpa.term.api.ITermConceptMappingSvc;
import ca.uhn.fhir.jpa.term.api.ITermReadSvc;
import com.google.common.annotations.VisibleForTesting;
import jakarta.annotation.PostConstruct;
import jakarta.annotation.PreDestroy;
import org.hl7.fhir.common.hapi.validation.support.CommonCodeSystemsTerminologyService;
Expand Down Expand Up @@ -60,6 +62,9 @@ public class JpaValidationSupportChain extends ValidationSupportChain {
@Autowired
private InMemoryTerminologyServerValidationSupport myInMemoryTerminologyServerValidationSupport;

@Autowired
private JpaStorageSettings myJpaStorageSettings;

/**
* Constructor
*/
Expand Down Expand Up @@ -90,8 +95,13 @@ public void flush() {
public void postConstruct() {
myWorkerContextValidationSupportAdapter.setValidationSupport(this);

addValidationSupport(myDefaultProfileValidationSupport);
addValidationSupport(myJpaValidationSupport);
if (myJpaStorageSettings.isAllowDatabaseValidationOverride()) {
addValidationSupport(myJpaValidationSupport);
addValidationSupport(myDefaultProfileValidationSupport);
} else {
addValidationSupport(myDefaultProfileValidationSupport);
addValidationSupport(myJpaValidationSupport);
}
addValidationSupport(myTerminologyService);
addValidationSupport(
new SnapshotGeneratingValidationSupport(myFhirContext, myWorkerContextValidationSupportAdapter));
Expand All @@ -100,4 +110,16 @@ public void postConstruct() {
addValidationSupport(new CommonCodeSystemsTerminologyService(myFhirContext));
addValidationSupport(myConceptMappingSvc);
}

/**
* Clears and rebuilds the validation support chain.
* This method is intended for unit testing purposes only to allow
* re-initializing the chain after changing configuration settings
* such as {@link JpaStorageSettings#setAllowDatabaseValidationOverride(boolean)}.
*/
@VisibleForTesting
public void rebuildChainForUnitTest() {
super.clearChainForUnitTest();
postConstruct();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
// Created by claude-sonnet-4-5
package ca.uhn.fhir.jpa.term;

import ca.uhn.fhir.context.support.IValidationSupport;
import ca.uhn.fhir.jpa.api.config.JpaStorageSettings;
import ca.uhn.fhir.jpa.test.BaseJpaR4Test;
import ca.uhn.fhir.jpa.validation.JpaValidationSupportChain;
import org.hl7.fhir.r4.model.CodeSystem;
import org.hl7.fhir.r4.model.CodeType;
import org.hl7.fhir.r4.model.Enumerations;
import org.hl7.fhir.r4.model.ValueSet;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.Test;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;

import java.util.List;
import java.util.stream.Collectors;

import static org.assertj.core.api.Assertions.assertThat;

/**
* This test validates that ValueSet expansion correctly handles case-sensitive
* CodeSystems where codes differ only by case (e.g., "drug" vs "Drug").
*
* The test uses FRAGMENT content mode to reproduce the bug scenario where
* a user-defined CodeSystem should override the built-in HL7 CodeSystem.
*/
class ValueSetExpansionCaseSensitiveManualTest extends BaseJpaR4Test {

private static final Logger ourLog = LoggerFactory.getLogger(ValueSetExpansionCaseSensitiveManualTest.class);
@Autowired
private IValidationSupport myJpaValidationSupportChain;

@AfterEach
public void after() {
myStorageSettings.setAllowDatabaseValidationOverride(new JpaStorageSettings().isAllowDatabaseValidationOverride());
((JpaValidationSupportChain)myJpaValidationSupportChain).rebuildChainForUnitTest();
}

@Test
void testValueSetExpansion_CaseSensitive_IncludeEntireSystem() {
myStorageSettings.setAllowDatabaseValidationOverride(true);
((JpaValidationSupportChain)myJpaValidationSupportChain).rebuildChainForUnitTest();

// Create case-sensitive CodeSystem with FRAGMENT content mode
CodeSystem codeSystem = new CodeSystem();
codeSystem.setUrl("http://terminology.hl7.org/CodeSystem/insurance-plan-type");
codeSystem.setStatus(Enumerations.PublicationStatus.ACTIVE);
codeSystem.setCaseSensitive(true);
codeSystem.setContent(CodeSystem.CodeSystemContentMode.FRAGMENT);
codeSystem.setVersion("4.0.1");

// Add all the codes from the real HL7 resource
codeSystem.addConcept().setCode("medical").setDisplay("Medical");
codeSystem.addConcept().setCode("dental").setDisplay("Dental");
codeSystem.addConcept().setCode("mental").setDisplay("Mental Health");
codeSystem.addConcept().setCode("subst-ab").setDisplay("Substance Abuse");
codeSystem.addConcept().setCode("vision").setDisplay("Vision");
codeSystem.addConcept().setCode("drug").setDisplay("Drug"); // lowercase - active
codeSystem.addConcept().setCode("short-term").setDisplay("Short Term");
codeSystem.addConcept().setCode("long-term").setDisplay("Long Term Care");
codeSystem.addConcept().setCode("hospice").setDisplay("Hospice");
codeSystem.addConcept().setCode("home").setDisplay("Home Health");

// Add "Drug" (uppercase D) - retired
CodeSystem.ConceptDefinitionComponent drugRetired = codeSystem.addConcept();
drugRetired.setCode("Drug");
drugRetired.setDisplay("Drug");
CodeSystem.ConceptPropertyComponent statusProp = drugRetired.addProperty();
statusProp.setCode("status");
statusProp.setValue(new CodeType("retired"));
CodeSystem.ConceptPropertyComponent inactiveProp = drugRetired.addProperty();
inactiveProp.setCode("inactive");
inactiveProp.setValue(new CodeType("true"));

myCodeSystemDao.create(codeSystem, mySrd);
myTerminologyDeferredStorageSvc.saveAllDeferred();

// Create ValueSet that includes the entire CodeSystem (NOT enumerated concepts)
ValueSet valueSet = new ValueSet();
valueSet.setUrl("http://terminology.hl7.org/ValueSet/insuranceplan-type");
valueSet.setStatus(Enumerations.PublicationStatus.ACTIVE);

ValueSet.ConceptSetComponent include = valueSet.getCompose().addInclude();
include.setSystem("http://terminology.hl7.org/CodeSystem/insurance-plan-type");
// CRITICAL: Do NOT enumerate specific concepts - include entire system

myValueSetDao.create(valueSet, mySrd);

// Act: Expand the ValueSet
ValueSet expanded = myTermSvc.expandValueSet(null, valueSet);

// Assert: Both case variants should be present
List<String> codes = expanded.getExpansion().getContains().stream()
.map(ValueSet.ValueSetExpansionContainsComponent::getCode)
.collect(Collectors.toList());

ourLog.info("Expanded ValueSet contains {} codes: {}", codes.size(), codes);

assertThat(codes)
.as("ValueSet expansion should include both case variants")
.contains("Drug", "drug");
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -426,6 +426,20 @@ public class JpaStorageSettings extends StorageSettings {
*/
private boolean myWriteToSearchParamIdentityTable = true;

/**
* Controls whether database-stored validation resources (CodeSystem, ValueSet, etc.) take precedence
* over built-in default profile validation resources when resolving during validation.
* <p>
* When set to {@code true}, the JPA validation support is registered before the default profile
* validation support in the validation chain, allowing database-stored resources to override
* built-in definitions.
* <p>
* Defaults to {@code false}, meaning built-in default profiles take precedence.
*
* @since 8.8.0
*/
private boolean myAllowDatabaseValidationOverride = false;

/**
* Constructor
*/
Expand Down Expand Up @@ -2738,7 +2752,31 @@ public void setWriteToSearchParamIdentityTable(boolean theWriteToSearchParamIden
myWriteToSearchParamIdentityTable = theWriteToSearchParamIdentityTable;
}

public enum StoreMetaSourceInformationEnum {
/**
* Sets whether database-stored validation resources should take precedence over built-in
* default profile validation resources.
*
* @param theAllowDatabaseValidationOverride if {@code true}, database resources override defaults
* @see #isAllowDatabaseValidationOverride()
* @since 8.8.0
*/
public void setAllowDatabaseValidationOverride(boolean theAllowDatabaseValidationOverride) {
myAllowDatabaseValidationOverride = theAllowDatabaseValidationOverride;
}

/**
* Returns whether database-stored validation resources take precedence over built-in
* default profile validation resources.
*
* @return {@code true} if database resources override defaults; {@code false} otherwise
* @see #setAllowDatabaseValidationOverride(boolean)
* @since 8.8.0
*/
public boolean isAllowDatabaseValidationOverride() {
return myAllowDatabaseValidationOverride;
}

public enum StoreMetaSourceInformationEnum {
NONE(false, false),
SOURCE_URI(true, false),
REQUEST_ID(false, true),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
import ca.uhn.fhir.util.Logs;
import ca.uhn.fhir.util.StopWatch;
import ca.uhn.fhir.util.UrlUtil;
import com.google.common.annotations.VisibleForTesting;
import com.google.common.base.Strings;
import jakarta.annotation.Nonnull;
import jakarta.annotation.Nullable;
Expand Down Expand Up @@ -1071,6 +1072,16 @@ int getMetricExpiringCacheMaxSize() {
return myCacheConfiguration.getCacheSize();
}

/**
* Clears all validation support modules from the chain.
* This method is intended for unit testing purposes only to allow
* rebuilding the chain with different configurations.
*/
@VisibleForTesting
protected void clearChainForUnitTest() {
myChain.clear();
}

/**
* @since 5.4.0
*/
Expand Down
Loading