diff --git a/hapi-deployable-pom/pom.xml b/hapi-deployable-pom/pom.xml index 2146bb70fd8..26d51925270 100644 --- a/hapi-deployable-pom/pom.xml +++ b/hapi-deployable-pom/pom.xml @@ -5,7 +5,7 @@ ca.uhn.hapi.fhir hapi-fhir - 7.7.7-SNAPSHOT + 7.7.8-SNAPSHOT ../pom.xml diff --git a/hapi-fhir-android/pom.xml b/hapi-fhir-android/pom.xml index 6c1b44706b1..9e0e3e40cdc 100644 --- a/hapi-fhir-android/pom.xml +++ b/hapi-fhir-android/pom.xml @@ -5,7 +5,7 @@ ca.uhn.hapi.fhir hapi-deployable-pom - 7.7.7-SNAPSHOT + 7.7.8-SNAPSHOT ../hapi-deployable-pom/pom.xml diff --git a/hapi-fhir-base/pom.xml b/hapi-fhir-base/pom.xml index 9a173eed3eb..0f624a5a381 100644 --- a/hapi-fhir-base/pom.xml +++ b/hapi-fhir-base/pom.xml @@ -5,7 +5,7 @@ ca.uhn.hapi.fhir hapi-deployable-pom - 7.7.7-SNAPSHOT + 7.7.8-SNAPSHOT ../hapi-deployable-pom/pom.xml diff --git a/hapi-fhir-base/src/main/java/ca/uhn/fhir/context/FhirContext.java b/hapi-fhir-base/src/main/java/ca/uhn/fhir/context/FhirContext.java index 796c9f7392e..a66d8cc2db6 100644 --- a/hapi-fhir-base/src/main/java/ca/uhn/fhir/context/FhirContext.java +++ b/hapi-fhir-base/src/main/java/ca/uhn/fhir/context/FhirContext.java @@ -713,6 +713,8 @@ public class FhirContext { "org.hl7.fhir.common.hapi.validation.support.InMemoryTerminologyServerValidationSupport"; String commonCodeSystemsSupportType = "org.hl7.fhir.common.hapi.validation.support.CommonCodeSystemsTerminologyService"; + String snapshotGeneratingType = + "org.hl7.fhir.common.hapi.validation.support.SnapshotGeneratingValidationSupport"; if (ReflectionUtil.typeExists(inMemoryTermSvcType)) { IValidationSupport inMemoryTermSvc = ReflectionUtil.newInstanceOrReturnNull( inMemoryTermSvcType, @@ -724,11 +726,23 @@ public class FhirContext { IValidationSupport.class, new Class[] {FhirContext.class}, new Object[] {this}); + IValidationSupport snapshotGeneratingSupport = null; + if (getVersion().getVersion().isEqualOrNewerThan(FhirVersionEnum.DSTU3)) { + snapshotGeneratingSupport = ReflectionUtil.newInstanceOrReturnNull( + snapshotGeneratingType, + IValidationSupport.class, + new Class[] {FhirContext.class}, + new Object[] {this}); + } retVal = ReflectionUtil.newInstanceOrReturnNull( "org.hl7.fhir.common.hapi.validation.support.ValidationSupportChain", IValidationSupport.class, new Class[] {IValidationSupport[].class}, - new Object[] {new IValidationSupport[] {retVal, inMemoryTermSvc, commonCodeSystemsSupport}}); + new Object[] { + new IValidationSupport[] { + retVal, inMemoryTermSvc, commonCodeSystemsSupport, snapshotGeneratingSupport + } + }); assert retVal != null : "Failed to instantiate " + "org.hl7.fhir.common.hapi.validation.support.ValidationSupportChain"; diff --git a/hapi-fhir-base/src/main/java/ca/uhn/fhir/context/support/ConceptValidationOptions.java b/hapi-fhir-base/src/main/java/ca/uhn/fhir/context/support/ConceptValidationOptions.java index 6e2e19e2d22..d1693a4652b 100644 --- a/hapi-fhir-base/src/main/java/ca/uhn/fhir/context/support/ConceptValidationOptions.java +++ b/hapi-fhir-base/src/main/java/ca/uhn/fhir/context/support/ConceptValidationOptions.java @@ -22,11 +22,26 @@ package ca.uhn.fhir.context.support; import org.apache.commons.lang3.builder.ToStringBuilder; import org.apache.commons.lang3.builder.ToStringStyle; +import java.util.Objects; + public class ConceptValidationOptions { private boolean myValidateDisplay; private boolean myInferSystem; + @Override + public boolean equals(Object theO) { + if (this == theO) return true; + if (!(theO instanceof ConceptValidationOptions)) return false; + ConceptValidationOptions that = (ConceptValidationOptions) theO; + return myValidateDisplay == that.myValidateDisplay && myInferSystem == that.myInferSystem; + } + + @Override + public int hashCode() { + return Objects.hash(myValidateDisplay, myInferSystem); + } + public boolean isInferSystem() { return myInferSystem; } diff --git a/hapi-fhir-base/src/main/java/ca/uhn/fhir/context/support/DefaultProfileValidationSupport.java b/hapi-fhir-base/src/main/java/ca/uhn/fhir/context/support/DefaultProfileValidationSupport.java index 40f3baa355d..e7e95ea22fd 100644 --- a/hapi-fhir-base/src/main/java/ca/uhn/fhir/context/support/DefaultProfileValidationSupport.java +++ b/hapi-fhir-base/src/main/java/ca/uhn/fhir/context/support/DefaultProfileValidationSupport.java @@ -23,7 +23,9 @@ import ca.uhn.fhir.context.FhirContext; import ca.uhn.fhir.context.FhirVersionEnum; import ca.uhn.fhir.util.ILockable; import ca.uhn.fhir.util.ReflectionUtil; +import jakarta.annotation.Nonnull; import jakarta.annotation.Nullable; +import org.apache.commons.lang3.Validate; import org.hl7.fhir.instance.model.api.IBase; import org.hl7.fhir.instance.model.api.IBaseResource; import org.hl7.fhir.instance.model.api.IPrimitiveType; @@ -48,6 +50,13 @@ public class DefaultProfileValidationSupport implements IValidationSupport { private static final Map ourImplementations = Collections.synchronizedMap(new HashMap<>()); + + /** + * Userdata key indicating the source package ID for this package + */ + public static final String SOURCE_PACKAGE_ID = + DefaultProfileValidationSupport.class.getName() + "_SOURCE_PACKAGE_ID"; + private final FhirContext myCtx; /** * This module just delegates all calls to a concrete implementation which will @@ -62,7 +71,8 @@ public class DefaultProfileValidationSupport implements IValidationSupport { * * @param theFhirContext The context to use */ - public DefaultProfileValidationSupport(FhirContext theFhirContext) { + public DefaultProfileValidationSupport(@Nonnull FhirContext theFhirContext) { + Validate.notNull(theFhirContext, "FhirContext must not be null"); myCtx = theFhirContext; IValidationSupport strategy; @@ -106,33 +116,45 @@ public class DefaultProfileValidationSupport implements IValidationSupport { @Override public List fetchAllConformanceResources() { - return myDelegate.fetchAllConformanceResources(); + List retVal = myDelegate.fetchAllConformanceResources(); + addPackageInformation(retVal); + return retVal; } @Override public List fetchAllStructureDefinitions() { - return myDelegate.fetchAllStructureDefinitions(); + List retVal = myDelegate.fetchAllStructureDefinitions(); + addPackageInformation(retVal); + return retVal; } @Nullable @Override public List fetchAllNonBaseStructureDefinitions() { - return myDelegate.fetchAllNonBaseStructureDefinitions(); + List retVal = myDelegate.fetchAllNonBaseStructureDefinitions(); + addPackageInformation(retVal); + return retVal; } @Override public IBaseResource fetchCodeSystem(String theSystem) { - return myDelegate.fetchCodeSystem(theSystem); + IBaseResource retVal = myDelegate.fetchCodeSystem(theSystem); + addPackageInformation(retVal); + return retVal; } @Override public IBaseResource fetchStructureDefinition(String theUrl) { - return myDelegate.fetchStructureDefinition(theUrl); + IBaseResource retVal = myDelegate.fetchStructureDefinition(theUrl); + addPackageInformation(retVal); + return retVal; } @Override public IBaseResource fetchValueSet(String theUrl) { - return myDelegate.fetchValueSet(theUrl); + IBaseResource retVal = myDelegate.fetchValueSet(theUrl); + addPackageInformation(retVal); + return retVal; } public void flush() { @@ -158,4 +180,43 @@ public class DefaultProfileValidationSupport implements IValidationSupport { } return urlValueString; } + + private void addPackageInformation(List theResources) { + if (theResources != null) { + theResources.forEach(this::addPackageInformation); + } + } + + private void addPackageInformation(IBaseResource theResource) { + if (theResource != null) { + String sourcePackageId = null; + switch (myCtx.getVersion().getVersion()) { + case DSTU2: + case DSTU2_HL7ORG: + sourcePackageId = "hl7.fhir.r2.core"; + break; + case DSTU2_1: + return; + case DSTU3: + sourcePackageId = "hl7.fhir.r3.core"; + break; + case R4: + sourcePackageId = "hl7.fhir.r4.core"; + break; + case R4B: + sourcePackageId = "hl7.fhir.r4b.core"; + break; + case R5: + sourcePackageId = "hl7.fhir.r5.core"; + break; + } + + Validate.notNull( + sourcePackageId, + "Don't know how to handle package ID: %s", + myCtx.getVersion().getVersion()); + + theResource.setUserData(SOURCE_PACKAGE_ID, sourcePackageId); + } + } } diff --git a/hapi-fhir-base/src/main/java/ca/uhn/fhir/context/support/IValidationSupport.java b/hapi-fhir-base/src/main/java/ca/uhn/fhir/context/support/IValidationSupport.java index fdbc4ca31d7..41091c28ae9 100644 --- a/hapi-fhir-base/src/main/java/ca/uhn/fhir/context/support/IValidationSupport.java +++ b/hapi-fhir-base/src/main/java/ca/uhn/fhir/context/support/IValidationSupport.java @@ -41,6 +41,7 @@ import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; import java.util.List; +import java.util.Objects; import java.util.Set; import java.util.function.Supplier; import java.util.stream.Collectors; @@ -116,7 +117,8 @@ public interface IValidationSupport { @Nonnull String theValueSetUrlToExpand) throws ResourceNotFoundException { Validate.notBlank(theValueSetUrlToExpand, "theValueSetUrlToExpand must not be null or blank"); - IBaseResource valueSet = fetchValueSet(theValueSetUrlToExpand); + IBaseResource valueSet = + theValidationSupportContext.getRootValidationSupport().fetchValueSet(theValueSetUrlToExpand); if (valueSet == null) { throw new ResourceNotFoundException( Msg.code(2024) + "Unknown ValueSet: " + UrlUtil.escapeUrlParam(theValueSetUrlToExpand)); @@ -212,8 +214,8 @@ public interface IValidationSupport { () -> fetchStructureDefinition(theUri), () -> fetchValueSet(theUri), () -> fetchCodeSystem(theUri) }; return (T) Arrays.stream(sources) - .map(t -> t.get()) - .filter(t -> t != null) + .map(Supplier::get) + .filter(Objects::nonNull) .findFirst() .orElse(null); } @@ -792,6 +794,7 @@ public interface IValidationSupport { return myValue; } + @Override public String getType() { return TYPE_STRING; } @@ -826,6 +829,7 @@ public interface IValidationSupport { return myDisplay; } + @Override public String getType() { return TYPE_CODING; } @@ -1431,10 +1435,9 @@ public interface IValidationSupport { } /** - *

* See VersionSpecificWorkerContextWrapper#validateCode in hapi-fhir-validation, and the refer to the values below * for the behaviour associated with each value. @@ -1449,7 +1452,7 @@ public interface IValidationSupport { *

* @return true or false depending on the desired coding validation behaviour. */ - default boolean isEnabledValidationForCodingsLogicalAnd() { + default boolean isCodeableConceptValidationSuccessfulIfNotAllCodingsAreValid() { return false; } } diff --git a/hapi-fhir-base/src/main/java/ca/uhn/fhir/context/support/LookupCodeRequest.java b/hapi-fhir-base/src/main/java/ca/uhn/fhir/context/support/LookupCodeRequest.java index 55adec92a7f..46891633f51 100644 --- a/hapi-fhir-base/src/main/java/ca/uhn/fhir/context/support/LookupCodeRequest.java +++ b/hapi-fhir-base/src/main/java/ca/uhn/fhir/context/support/LookupCodeRequest.java @@ -21,6 +21,7 @@ package ca.uhn.fhir.context.support; import java.util.Collection; import java.util.Collections; +import java.util.Objects; /** * Represents parameters which can be passed to the $lookup operation for codes. @@ -72,4 +73,20 @@ public class LookupCodeRequest { } return myPropertyNames; } + + @Override + public boolean equals(Object theO) { + if (this == theO) return true; + if (!(theO instanceof LookupCodeRequest)) return false; + LookupCodeRequest that = (LookupCodeRequest) theO; + return Objects.equals(mySystem, that.mySystem) + && Objects.equals(myCode, that.myCode) + && Objects.equals(myDisplayLanguage, that.myDisplayLanguage) + && Objects.equals(myPropertyNames, that.myPropertyNames); + } + + @Override + public int hashCode() { + return Objects.hash(mySystem, myCode, myDisplayLanguage, myPropertyNames); + } } diff --git a/hapi-fhir-base/src/main/java/ca/uhn/fhir/context/support/ValidationSupportContext.java b/hapi-fhir-base/src/main/java/ca/uhn/fhir/context/support/ValidationSupportContext.java index 8f4b8a2a52d..e1e8381230c 100644 --- a/hapi-fhir-base/src/main/java/ca/uhn/fhir/context/support/ValidationSupportContext.java +++ b/hapi-fhir-base/src/main/java/ca/uhn/fhir/context/support/ValidationSupportContext.java @@ -42,7 +42,7 @@ public class ValidationSupportContext { return myCurrentlyGeneratingSnapshots; } - public boolean isEnabledValidationForCodingsLogicalAnd() { - return myRootValidationSupport.isEnabledValidationForCodingsLogicalAnd(); + public boolean isCodeableConceptValidationSuccessfulIfNotAllCodingsAreValid() { + return myRootValidationSupport.isCodeableConceptValidationSuccessfulIfNotAllCodingsAreValid(); } } diff --git a/hapi-fhir-base/src/main/java/ca/uhn/fhir/context/support/ValueSetExpansionOptions.java b/hapi-fhir-base/src/main/java/ca/uhn/fhir/context/support/ValueSetExpansionOptions.java index 34f2122d705..6dcd46db56b 100644 --- a/hapi-fhir-base/src/main/java/ca/uhn/fhir/context/support/ValueSetExpansionOptions.java +++ b/hapi-fhir-base/src/main/java/ca/uhn/fhir/context/support/ValueSetExpansionOptions.java @@ -21,6 +21,8 @@ package ca.uhn.fhir.context.support; import org.apache.commons.lang3.Validate; +import java.util.Objects; + /** * Options for ValueSet expansion * @@ -126,4 +128,23 @@ public class ValueSetExpansionOptions { myDisplayLanguage = theDisplayLanguage; return this; } + + @Override + public boolean equals(Object theO) { + if (this == theO) return true; + if (!(theO instanceof ValueSetExpansionOptions)) return false; + ValueSetExpansionOptions that = (ValueSetExpansionOptions) theO; + return myFailOnMissingCodeSystem == that.myFailOnMissingCodeSystem + && myCount == that.myCount + && myOffset == that.myOffset + && myIncludeHierarchy == that.myIncludeHierarchy + && Objects.equals(myFilter, that.myFilter) + && Objects.equals(myDisplayLanguage, that.myDisplayLanguage); + } + + @Override + public int hashCode() { + return Objects.hash( + myFailOnMissingCodeSystem, myCount, myOffset, myIncludeHierarchy, myFilter, myDisplayLanguage); + } } diff --git a/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/api/Constants.java b/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/api/Constants.java index db4dc9cb027..b99ed890a21 100644 --- a/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/api/Constants.java +++ b/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/api/Constants.java @@ -342,6 +342,8 @@ public class Constants { */ public static final String HIBERNATE_INTEGRATION_ENVERS_ENABLED = "hibernate.integration.envers.enabled"; + public static final String OPENTELEMETRY_BASE_NAME = "io.hapifhir"; + static { CHARSET_UTF8 = StandardCharsets.UTF_8; CHARSET_US_ASCII = StandardCharsets.ISO_8859_1; diff --git a/hapi-fhir-base/src/main/java/ca/uhn/fhir/util/SleepUtil.java b/hapi-fhir-base/src/main/java/ca/uhn/fhir/util/SleepUtil.java index e0037782d55..439ff6e7fba 100644 --- a/hapi-fhir-base/src/main/java/ca/uhn/fhir/util/SleepUtil.java +++ b/hapi-fhir-base/src/main/java/ca/uhn/fhir/util/SleepUtil.java @@ -33,7 +33,7 @@ public class SleepUtil { @SuppressWarnings("BusyWait") public void sleepAtLeast(long theMillis, boolean theLogProgress) { long start = System.currentTimeMillis(); - while (System.currentTimeMillis() <= start + theMillis) { + while (System.currentTimeMillis() < start + theMillis) { try { long timeSinceStarted = System.currentTimeMillis() - start; long timeToSleep = Math.max(0, theMillis - timeSinceStarted); diff --git a/hapi-fhir-bom/pom.xml b/hapi-fhir-bom/pom.xml index db25bc4f2ba..615928fa422 100644 --- a/hapi-fhir-bom/pom.xml +++ b/hapi-fhir-bom/pom.xml @@ -4,7 +4,7 @@ 4.0.0 ca.uhn.hapi.fhir hapi-fhir-bom - 7.7.7-SNAPSHOT + 7.7.8-SNAPSHOT pom HAPI FHIR BOM @@ -12,7 +12,7 @@ ca.uhn.hapi.fhir hapi-deployable-pom - 7.7.7-SNAPSHOT + 7.7.8-SNAPSHOT ../hapi-deployable-pom/pom.xml diff --git a/hapi-fhir-checkstyle/pom.xml b/hapi-fhir-checkstyle/pom.xml index 6edc883df32..db4657026c1 100644 --- a/hapi-fhir-checkstyle/pom.xml +++ b/hapi-fhir-checkstyle/pom.xml @@ -5,7 +5,7 @@ ca.uhn.hapi.fhir hapi-fhir - 7.7.7-SNAPSHOT + 7.7.8-SNAPSHOT ../pom.xml diff --git a/hapi-fhir-cli/hapi-fhir-cli-api/pom.xml b/hapi-fhir-cli/hapi-fhir-cli-api/pom.xml index d4a3f519428..33782915ddc 100644 --- a/hapi-fhir-cli/hapi-fhir-cli-api/pom.xml +++ b/hapi-fhir-cli/hapi-fhir-cli-api/pom.xml @@ -4,7 +4,7 @@ ca.uhn.hapi.fhir hapi-deployable-pom - 7.7.7-SNAPSHOT + 7.7.8-SNAPSHOT ../../hapi-deployable-pom/pom.xml diff --git a/hapi-fhir-cli/hapi-fhir-cli-app/pom.xml b/hapi-fhir-cli/hapi-fhir-cli-app/pom.xml index e402e75a1e3..301353ac697 100644 --- a/hapi-fhir-cli/hapi-fhir-cli-app/pom.xml +++ b/hapi-fhir-cli/hapi-fhir-cli-app/pom.xml @@ -6,7 +6,7 @@ ca.uhn.hapi.fhir hapi-fhir-cli - 7.7.7-SNAPSHOT + 7.7.8-SNAPSHOT ../pom.xml diff --git a/hapi-fhir-cli/pom.xml b/hapi-fhir-cli/pom.xml index 1c93cfa5375..746014589d2 100644 --- a/hapi-fhir-cli/pom.xml +++ b/hapi-fhir-cli/pom.xml @@ -5,7 +5,7 @@ ca.uhn.hapi.fhir hapi-fhir - 7.7.7-SNAPSHOT + 7.7.8-SNAPSHOT ../pom.xml diff --git a/hapi-fhir-client-okhttp/pom.xml b/hapi-fhir-client-okhttp/pom.xml index eb1663c660b..f6dcd6b81a2 100644 --- a/hapi-fhir-client-okhttp/pom.xml +++ b/hapi-fhir-client-okhttp/pom.xml @@ -4,7 +4,7 @@ ca.uhn.hapi.fhir hapi-deployable-pom - 7.7.7-SNAPSHOT + 7.7.8-SNAPSHOT ../hapi-deployable-pom/pom.xml diff --git a/hapi-fhir-client/pom.xml b/hapi-fhir-client/pom.xml index c743cad6eb5..f9b12517d25 100644 --- a/hapi-fhir-client/pom.xml +++ b/hapi-fhir-client/pom.xml @@ -4,7 +4,7 @@ ca.uhn.hapi.fhir hapi-deployable-pom - 7.7.7-SNAPSHOT + 7.7.8-SNAPSHOT ../hapi-deployable-pom/pom.xml diff --git a/hapi-fhir-converter/pom.xml b/hapi-fhir-converter/pom.xml index 16105895f4c..28d3078c087 100644 --- a/hapi-fhir-converter/pom.xml +++ b/hapi-fhir-converter/pom.xml @@ -5,7 +5,7 @@ ca.uhn.hapi.fhir hapi-deployable-pom - 7.7.7-SNAPSHOT + 7.7.8-SNAPSHOT ../hapi-deployable-pom/pom.xml diff --git a/hapi-fhir-dist/pom.xml b/hapi-fhir-dist/pom.xml index bc99a2f88c7..0749e3faaff 100644 --- a/hapi-fhir-dist/pom.xml +++ b/hapi-fhir-dist/pom.xml @@ -5,7 +5,7 @@ ca.uhn.hapi.fhir hapi-fhir - 7.7.7-SNAPSHOT + 7.7.8-SNAPSHOT ../pom.xml diff --git a/hapi-fhir-docs/pom.xml b/hapi-fhir-docs/pom.xml index 27a94cebe28..a8d553276c6 100644 --- a/hapi-fhir-docs/pom.xml +++ b/hapi-fhir-docs/pom.xml @@ -5,7 +5,7 @@ ca.uhn.hapi.fhir hapi-deployable-pom - 7.7.7-SNAPSHOT + 7.7.8-SNAPSHOT ../hapi-deployable-pom/pom.xml diff --git a/hapi-fhir-docs/src/main/java/ca/uhn/hapi/fhir/docs/ValidatorExamples.java b/hapi-fhir-docs/src/main/java/ca/uhn/hapi/fhir/docs/ValidatorExamples.java index 9b11ad82d50..b2b3a3e935e 100644 --- a/hapi-fhir-docs/src/main/java/ca/uhn/hapi/fhir/docs/ValidatorExamples.java +++ b/hapi-fhir-docs/src/main/java/ca/uhn/hapi/fhir/docs/ValidatorExamples.java @@ -40,7 +40,6 @@ import jakarta.annotation.Nonnull; import jakarta.servlet.ServletException; import org.apache.commons.io.IOUtils; import org.apache.commons.io.filefilter.WildcardFileFilter; -import org.hl7.fhir.common.hapi.validation.support.CachingValidationSupport; 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.NpmPackageValidationSupport; @@ -343,7 +342,8 @@ public class ValidatorExamples { // START SNIPPET: validateSupplyProfiles FhirContext ctx = FhirContext.forR4(); - // Create a chain that will hold our modules + // Create a chain that will hold our modules and caches the + // values they supply ValidationSupportChain supportChain = new ValidationSupportChain(); // DefaultProfileValidationSupport supplies base FHIR definitions. This is generally required @@ -368,12 +368,9 @@ public class ValidatorExamples { // Add the custom definitions to the chain supportChain.addValidationSupport(prePopulatedSupport); - // Wrap the chain in a cache to improve performance - CachingValidationSupport cache = new CachingValidationSupport(supportChain); - // Create a validator using the FhirInstanceValidator module. We can use this // validator to perform validation - FhirInstanceValidator validatorModule = new FhirInstanceValidator(cache); + FhirInstanceValidator validatorModule = new FhirInstanceValidator(supportChain); FhirValidator validator = ctx.newValidator().registerValidatorModule(validatorModule); ValidationResult result = validator.validateWithResult(input); // END SNIPPET: validateSupplyProfiles @@ -403,12 +400,9 @@ public class ValidatorExamples { remoteTermSvc.setBaseUrl("http://hapi.fhir.org/baseR4"); supportChain.addValidationSupport(remoteTermSvc); - // Wrap the chain in a cache to improve performance - CachingValidationSupport cache = new CachingValidationSupport(supportChain); - // Create a validator using the FhirInstanceValidator module. We can use this // validator to perform validation - FhirInstanceValidator validatorModule = new FhirInstanceValidator(cache); + FhirInstanceValidator validatorModule = new FhirInstanceValidator(supportChain); FhirValidator validator = ctx.newValidator().registerValidatorModule(validatorModule); ValidationResult result = validator.validateWithResult(input); // END SNIPPET: validateUsingRemoteTermSvr @@ -462,12 +456,11 @@ public class ValidatorExamples { new CommonCodeSystemsTerminologyService(ctx), new InMemoryTerminologyServerValidationSupport(ctx), new SnapshotGeneratingValidationSupport(ctx)); - CachingValidationSupport validationSupport = new CachingValidationSupport(validationSupportChain); // Create a validator. Note that for good performance you can create as many validator objects // as you like, but you should reuse the same validation support object in all of the,. FhirValidator validator = ctx.newValidator(); - FhirInstanceValidator instanceValidator = new FhirInstanceValidator(validationSupport); + FhirInstanceValidator instanceValidator = new FhirInstanceValidator(validationSupportChain); validator.registerValidatorModule(instanceValidator); // Create a test patient to validate diff --git a/hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/changelog/7_8_0/6508-validation-support-chain-perf-improvements.yaml b/hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/changelog/7_8_0/6508-validation-support-chain-perf-improvements.yaml new file mode 100644 index 00000000000..701bbfd0d60 --- /dev/null +++ b/hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/changelog/7_8_0/6508-validation-support-chain-perf-improvements.yaml @@ -0,0 +1,13 @@ +--- +type: perf +issue: 6508 +title: "The ValidationSupportChain module has been rewritten to improve validator performance. This change: +* Adds new caching capabilities to ValidationSupportChain. This is an improvement over the previous separate caching module because the chain can now remember which entries in the cache responded affirmative to `isValueSetSupported()` and will therefore be more efficient about trying entries in the chain. It also makes debugging much less confusing as there is less recursion and the caches don't use loadingCache. +* Importantly, the caching in ValidationSupportChain caches negative lookups (i.e. items that could not be found by URL) as well as positive lookups. This is a change from the historical caching behaviour. +* Changes ValidationSupportChain to never expire StructureDefinition entries in the cache, which is needed because the validator makes assumptions about structuredefinitions never changing. Fixes #6424. +* Modifies `VersionSpecificWorkerContextWrapper` so that it doesn't use a separate cache and instead relies on the caching provided by ValidationSupportChain. This class previously used a cache because it converts arbitrary versions of FHIR StructureDefinitions into the canonical version required by the validator (R5), but these converted versions are now stored in the userdata map of objects returned by and cached by ValidationSupportChain. This makes the caching more predictable since there is only one cache to track. +* Adds OpenTelemetry support to ValidationSupportChain, with metrics for tracking the cache size. +* Deprecates CachingValidationSupport since caching is now provided by ValidationSupportChain. CachingValidationSupport is now just a passthrough and should be removed from applications. It will be removed from the library in a future release. +* Removes ConceptMap caching from TermReachSvcImpl, as this caching is both redundant and inefficient as it operates within a database transaction. + +These changes result in very significant performance improvements when performing validation in the JPA server. Throughput improvements of 1000% have been recorded in benchmarking use cases involving large profiles and remote terminology services enabled. Many other validation use cases should see significant improvements as well." diff --git a/hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/docs/validation/validation_support_modules.md b/hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/docs/validation/validation_support_modules.md index 05da89a6993..33b4e5fe109 100644 --- a/hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/docs/validation/validation_support_modules.md +++ b/hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/docs/validation/validation_support_modules.md @@ -16,6 +16,21 @@ There are a several implementations of the [IValidationSupport](/hapi-fhir/apido This module can be used to combine multiple implementations together so that for every request, each support class instance in the chain is tried in sequence. Note that nearly all methods in the [IValidationSupport](/hapi-fhir/apidocs/hapi-fhir-base/ca/uhn/fhir/context/support/IValidationSupport.html) interface are permitted to return `null` if they are not able to service a particular method call. So for example, if a call to the [`validateCode`](/hapi-fhir/apidocs/hapi-fhir-base/ca/uhn/fhir/context/support/IValidationSupport.html#validateCode(ca.uhn.fhir.context.support.ValidationSupportContext,ca.uhn.fhir.context.support.ConceptValidationOptions,java.lang.String,java.lang.String,java.lang.String,java.lang.String)) method is made, the validator will try each module in the chain until one of them returns a non-null response. +The following chaining logic is used: + +* Calls to `fetchAll...` methods such as `fetchAllConformanceResources()` and `fetchAllStructureDefinitions()` will call every method in the chain in order, and aggregate the results into a single list to return. +* Calls to fetch or validate codes, such as `validateCode(...)` and `lookupCode(...)` will first test each module in the chain using the`isCodeSystemSupported(...)` or `isValueSetSupported(...)` methods (depending on whether a ValueSet URL is present in the method parameters) and will invoke any methods in the chain which return that they can handle the given CodeSystem/ValueSet URL. The first non-null value returned by a method in the chain that can support the URL will be returned to the caller. +* All other methods will invoke the method in the chain in order, and will return immediately as soon as a non-null value is returned. + +The following caching logic is used if caching is enabled using `CacheConfiguration`. You can use `CacheConfiguration.disabled()` if you want to disable caching. + +* Calls to fetch StructureDefinitions including `fetchAllStructureDefinitions()` and `fetchStructureDefinition(...)` are cached in a non-expiring cache. This is because the `FhirInstanceValidator` module makes assumptions that these objects will not change for the lifetime of the validator for performance reasons. +* Calls to all other `fetchAll...` methods including `fetchAllConformanceResources()` and `fetchAllSearchParameters()` cache their results in an expiring cache, but will refresh that cache asynchronously. +* Results of `generateSnapshot(...)` are not cached, as this method is generally called in contexts where the results are cached. +* Results of all other methods are stored in an expiring cache. + +Note that caching functionality used to be provided by a separate provider called {@literal CachingValidationSupport} but that functionality has been moved into this class as of HAPI FHIR 8.0.0, because it is possible to provide a more efficient chain when these functions are combined. + # DefaultProfileValidationSupport [JavaDoc](/hapi-fhir/apidocs/hapi-fhir-base/undefined/ca/uhn/fhir/context/support/DefaultProfileValidationSupport.html) / [Source](https://github.com/hapifhir/hapi-fhir/blob/master/hapi-fhir-base/src/main/java/ca/uhn/fhir/context/support/DefaultProfileValidationSupport.java) @@ -44,12 +59,6 @@ This module contains a series of HashMaps that store loaded conformance resource This module can be used to load FHIR NPM Packages and supply the conformance resources within them to the validator. See [Validating Using Packages](./instance_validator.html#packages) for am example of how to use this module. -# CachingValidationSupport - -[JavaDoc](/hapi-fhir/apidocs/hapi-fhir-validation/org/hl7/fhir/common/hapi/validation/support/CachingValidationSupport.html) / [Source](https://github.com/jamesagnew/hapi-fhir/blob/master/hapi-fhir-validation/src/main/java/org/hl7/fhir/common/hapi/validation/support/CachingValidationSupport.java) - -This module caches results of calls to a wrapped service implementation for a period of time. This class can be a significant help in terms of performance if you are loading conformance resources or performing terminology operations from a database or disk, but it also has value even for purely in-memory validation since validating codes against a ValueSet can require the expansion of that ValueSet. - # SnapshotGeneratingValidationSupport [JavaDoc](/hapi-fhir/apidocs/hapi-fhir-validation/org/hl7/fhir/common/hapi/validation/support/SnapshotGeneratingValidationSupport.html) / [Source](https://github.com/jamesagnew/hapi-fhir/blob/master/hapi-fhir-validation/src/main/java/org/hl7/fhir/common/hapi/validation/support/SnapshotGeneratingValidationSupport.java) @@ -161,6 +170,12 @@ This validation support module may be placed at the end of a ValidationSupportCh Note that this module must also be activated by calling [setAllowNonExistentCodeSystem(true)](/hapi-fhir/apidocs/hapi-fhir-validation/org/hl7/fhir/common/hapi/validation/support/UnknownCodeSystemWarningValidationSupport.html#setAllowNonExistentCodeSystem(boolean)) in order to specify that unknown code systems should be allowed. +# CachingValidationSupport + +[JavaDoc](/hapi-fhir/apidocs/hapi-fhir-validation/org/hl7/fhir/common/hapi/validation/support/CachingValidationSupport.html) / [Source](https://github.com/jamesagnew/hapi-fhir/blob/master/hapi-fhir-validation/src/main/java/org/hl7/fhir/common/hapi/validation/support/CachingValidationSupport.java) + +This module is deprecated and no longer provides any functionality. Caching is provided by [ValidationSupportChain](#validationsupportchain). + # Recipes diff --git a/hapi-fhir-jacoco/pom.xml b/hapi-fhir-jacoco/pom.xml index 1e106aaa69f..e6a3f64d67d 100644 --- a/hapi-fhir-jacoco/pom.xml +++ b/hapi-fhir-jacoco/pom.xml @@ -11,7 +11,7 @@ ca.uhn.hapi.fhir hapi-deployable-pom - 7.7.7-SNAPSHOT + 7.7.8-SNAPSHOT ../hapi-deployable-pom/pom.xml diff --git a/hapi-fhir-jaxrsserver-base/pom.xml b/hapi-fhir-jaxrsserver-base/pom.xml index fe17d290684..f1ce7ec0ce8 100644 --- a/hapi-fhir-jaxrsserver-base/pom.xml +++ b/hapi-fhir-jaxrsserver-base/pom.xml @@ -4,7 +4,7 @@ ca.uhn.hapi.fhir hapi-deployable-pom - 7.7.7-SNAPSHOT + 7.7.8-SNAPSHOT ../hapi-deployable-pom/pom.xml diff --git a/hapi-fhir-jpa/pom.xml b/hapi-fhir-jpa/pom.xml index 3d91a9e4086..e894f987778 100644 --- a/hapi-fhir-jpa/pom.xml +++ b/hapi-fhir-jpa/pom.xml @@ -5,7 +5,7 @@ ca.uhn.hapi.fhir hapi-deployable-pom - 7.7.7-SNAPSHOT + 7.7.8-SNAPSHOT ../hapi-deployable-pom/pom.xml diff --git a/hapi-fhir-jpaserver-base/pom.xml b/hapi-fhir-jpaserver-base/pom.xml index b7db9a6a5a5..02f29b40e6e 100644 --- a/hapi-fhir-jpaserver-base/pom.xml +++ b/hapi-fhir-jpaserver-base/pom.xml @@ -5,7 +5,7 @@ ca.uhn.hapi.fhir hapi-deployable-pom - 7.7.7-SNAPSHOT + 7.7.8-SNAPSHOT ../hapi-deployable-pom/pom.xml diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/config/HapiJpaConfig.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/config/HapiJpaConfig.java index 08095145225..e13b75e4c67 100644 --- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/config/HapiJpaConfig.java +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/config/HapiJpaConfig.java @@ -22,7 +22,6 @@ package ca.uhn.fhir.jpa.config; import ca.uhn.fhir.jpa.api.IDaoRegistry; import ca.uhn.fhir.jpa.api.dao.IFhirSystemDao; import ca.uhn.fhir.jpa.config.util.ResourceCountCacheUtil; -import ca.uhn.fhir.jpa.config.util.ValidationSupportConfigUtil; import ca.uhn.fhir.jpa.dao.FulltextSearchSvcImpl; import ca.uhn.fhir.jpa.dao.IFulltextSearchSvc; import ca.uhn.fhir.jpa.dao.search.HSearchSortHelperImpl; @@ -32,15 +31,12 @@ import ca.uhn.fhir.jpa.search.DatabaseBackedPagingProvider; import ca.uhn.fhir.jpa.search.IStaleSearchDeletingSvc; import ca.uhn.fhir.jpa.search.StaleSearchDeletingSvcImpl; import ca.uhn.fhir.jpa.util.ResourceCountCache; -import ca.uhn.fhir.jpa.validation.JpaValidationSupportChain; import ca.uhn.fhir.rest.api.IResourceSupportedSvc; import ca.uhn.fhir.rest.server.util.ISearchParamRegistry; -import org.hl7.fhir.common.hapi.validation.support.CachingValidationSupport; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Import; -import org.springframework.context.annotation.Primary; @Configuration @Import({JpaConfig.class}) @@ -64,12 +60,6 @@ public class HapiJpaConfig { return new StaleSearchDeletingSvcImpl(); } - @Primary - @Bean - public CachingValidationSupport validationSupportChain(JpaValidationSupportChain theJpaValidationSupportChain) { - return ValidationSupportConfigUtil.newCachingValidationSupport(theJpaValidationSupportChain); - } - @Bean public DatabaseBackedPagingProvider databaseBackedPagingProvider() { return new DatabaseBackedPagingProvider(); diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/config/JpaConfig.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/config/JpaConfig.java index 9a066b9dddb..a17e3425f46 100644 --- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/config/JpaConfig.java +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/config/JpaConfig.java @@ -168,6 +168,7 @@ import ca.uhn.fhir.jpa.term.config.TermCodeSystemConfig; import ca.uhn.fhir.jpa.util.JpaHapiTransactionService; import ca.uhn.fhir.jpa.util.MemoryCacheService; import ca.uhn.fhir.jpa.util.PersistenceContextProvider; +import ca.uhn.fhir.jpa.validation.JpaValidationSupportChain; import ca.uhn.fhir.jpa.validation.ResourceLoaderImpl; import ca.uhn.fhir.jpa.validation.ValidationSettings; import ca.uhn.fhir.model.api.IPrimitiveDatatype; @@ -185,6 +186,7 @@ import ca.uhn.fhir.util.MetaTagSorterAlphabetical; import ca.uhn.hapi.converters.canonical.VersionCanonicalizer; import jakarta.annotation.Nullable; import org.hl7.fhir.common.hapi.validation.support.UnknownCodeSystemWarningValidationSupport; +import org.hl7.fhir.common.hapi.validation.support.ValidationSupportChain; import org.hl7.fhir.utilities.graphql.IGraphQLStorageServices; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.annotation.Bean; @@ -230,11 +232,26 @@ public class JpaConfig { public static final String PERSISTED_JPA_SEARCH_FIRST_PAGE_BUNDLE_PROVIDER = "PersistedJpaSearchFirstPageBundleProvider"; public static final String HISTORY_BUILDER = "HistoryBuilder"; + public static final String DEFAULT_PROFILE_VALIDATION_SUPPORT = "myDefaultProfileValidationSupport"; private static final String HAPI_DEFAULT_SCHEDULER_GROUP = "HAPI"; @Autowired public JpaStorageSettings myStorageSettings; + @Autowired + private FhirContext myFhirContext; + + @Bean + public ValidationSupportChain.CacheConfiguration validationSupportChainCacheConfiguration() { + return ValidationSupportChain.CacheConfiguration.defaultValues(); + } + + @Bean(name = JpaConfig.JPA_VALIDATION_SUPPORT_CHAIN) + @Primary + public IValidationSupport jpaValidationSupportChain() { + return new JpaValidationSupportChain(myFhirContext, validationSupportChainCacheConfiguration()); + } + @Bean("myDaoRegistry") public DaoRegistry daoRegistry() { return new DaoRegistry(); diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/config/ValidationSupportConfig.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/config/ValidationSupportConfig.java index 8400b65232b..2c4c97640b5 100644 --- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/config/ValidationSupportConfig.java +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/config/ValidationSupportConfig.java @@ -20,31 +20,31 @@ package ca.uhn.fhir.jpa.config; import ca.uhn.fhir.context.FhirContext; -import ca.uhn.fhir.context.FhirVersionEnum; import ca.uhn.fhir.context.support.DefaultProfileValidationSupport; import ca.uhn.fhir.context.support.IValidationSupport; import ca.uhn.fhir.jpa.api.config.JpaStorageSettings; import ca.uhn.fhir.jpa.api.dao.DaoRegistry; import ca.uhn.fhir.jpa.dao.JpaPersistedResourceValidationSupport; -import ca.uhn.fhir.jpa.validation.JpaValidationSupportChain; import ca.uhn.fhir.jpa.validation.ValidatorPolicyAdvisor; import ca.uhn.fhir.jpa.validation.ValidatorResourceFetcher; import ca.uhn.fhir.validation.IInstanceValidatorModule; -import org.hl7.fhir.common.hapi.validation.support.CachingValidationSupport; 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.common.hapi.validation.validator.HapiToHl7OrgDstu2ValidatingSupportWrapper; import org.hl7.fhir.r5.utils.validation.constants.BestPracticeWarningLevel; +import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Lazy; @Configuration public class ValidationSupportConfig { - @Bean(name = "myDefaultProfileValidationSupport") - public DefaultProfileValidationSupport defaultProfileValidationSupport(FhirContext theFhirContext) { - return new DefaultProfileValidationSupport(theFhirContext); + + @Autowired + private FhirContext myFhirContext; + + @Bean(name = JpaConfig.DEFAULT_PROFILE_VALIDATION_SUPPORT) + public DefaultProfileValidationSupport defaultProfileValidationSupport() { + return new DefaultProfileValidationSupport(myFhirContext); } @Bean @@ -56,11 +56,6 @@ public class ValidationSupportConfig { return retVal; } - @Bean(name = JpaConfig.JPA_VALIDATION_SUPPORT_CHAIN) - public JpaValidationSupportChain jpaValidationSupportChain(FhirContext theFhirContext) { - return new JpaValidationSupportChain(theFhirContext); - } - @Bean(name = JpaConfig.JPA_VALIDATION_SUPPORT) public IValidationSupport jpaValidationSupport(FhirContext theFhirContext) { return new JpaPersistedResourceValidationSupport(theFhirContext); @@ -68,26 +63,13 @@ public class ValidationSupportConfig { @Bean(name = "myInstanceValidator") public IInstanceValidatorModule instanceValidator( - FhirContext theFhirContext, - CachingValidationSupport theCachingValidationSupport, - ValidationSupportChain theValidationSupportChain, - IValidationSupport theValidationSupport, - DaoRegistry theDaoRegistry) { - if (theFhirContext.getVersion().getVersion().isEqualOrNewerThan(FhirVersionEnum.DSTU3)) { - FhirInstanceValidator val = new FhirInstanceValidator(theCachingValidationSupport); - val.setValidatorResourceFetcher( - jpaValidatorResourceFetcher(theFhirContext, theValidationSupport, theDaoRegistry)); - val.setValidatorPolicyAdvisor(jpaValidatorPolicyAdvisor()); - val.setBestPracticeWarningLevel(BestPracticeWarningLevel.Warning); - val.setValidationSupport(theCachingValidationSupport); - return val; - } else { - CachingValidationSupport cachingValidationSupport = new CachingValidationSupport( - new HapiToHl7OrgDstu2ValidatingSupportWrapper(theValidationSupportChain)); - FhirInstanceValidator retVal = new FhirInstanceValidator(cachingValidationSupport); - retVal.setBestPracticeWarningLevel(BestPracticeWarningLevel.Warning); - return retVal; - } + FhirContext theFhirContext, IValidationSupport theValidationSupportChain, DaoRegistry theDaoRegistry) { + FhirInstanceValidator val = new FhirInstanceValidator(theValidationSupportChain); + val.setValidatorResourceFetcher( + jpaValidatorResourceFetcher(theFhirContext, theValidationSupportChain, theDaoRegistry)); + val.setValidatorPolicyAdvisor(jpaValidatorPolicyAdvisor()); + val.setBestPracticeWarningLevel(BestPracticeWarningLevel.Warning); + return val; } @Bean diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/config/util/ValidationSupportConfigUtil.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/config/util/ValidationSupportConfigUtil.java deleted file mode 100644 index c05f9b7edd6..00000000000 --- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/config/util/ValidationSupportConfigUtil.java +++ /dev/null @@ -1,43 +0,0 @@ -/*- - * #%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% - */ -package ca.uhn.fhir.jpa.config.util; - -import ca.uhn.fhir.jpa.validation.JpaValidationSupportChain; -import org.hl7.fhir.common.hapi.validation.support.CachingValidationSupport; - -public final class ValidationSupportConfigUtil { - private ValidationSupportConfigUtil() {} - - public static CachingValidationSupport newCachingValidationSupport( - JpaValidationSupportChain theJpaValidationSupportChain) { - return newCachingValidationSupport(theJpaValidationSupportChain, false); - } - - public static CachingValidationSupport newCachingValidationSupport( - JpaValidationSupportChain theJpaValidationSupportChain, - boolean theIsEnabledValidationForCodingsLogicalAnd) { - // Short timeout for code translation because TermConceptMappingSvcImpl has its own caching - CachingValidationSupport.CacheTimeouts cacheTimeouts = - CachingValidationSupport.CacheTimeouts.defaultValues().setTranslateCodeMillis(1000); - - return new CachingValidationSupport( - theJpaValidationSupportChain, cacheTimeouts, theIsEnabledValidationForCodingsLogicalAnd); - } -} diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/HistoryBuilder.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/HistoryBuilder.java index 8255daf5486..dcf578ee5c9 100644 --- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/HistoryBuilder.java +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/HistoryBuilder.java @@ -48,6 +48,9 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; +import java.time.Instant; +import java.time.ZoneId; +import java.time.ZonedDateTime; import java.util.ArrayList; import java.util.Date; import java.util.List; @@ -243,8 +246,23 @@ public class HistoryBuilder { Subquery pastDateSubQuery = theQuery.subquery(Date.class); Root subQueryResourceHistory = pastDateSubQuery.from(ResourceHistoryTable.class); Expression myUpdatedMostRecent = theCriteriaBuilder.max(subQueryResourceHistory.get("myUpdated")); + + /* + * This conversion from the Date in myRangeEndInclusive into a ZonedDateTime is an experiment - + * There is an intermittent test failure in testSearchHistoryWithAtAndGtParameters() that I can't + * figure out. But I've added a ton of logging to the error it fails with and I noticed that + * we emit SQL along the lines of + * select coalesce(max(rht2_0.RES_UPDATED), timestamp with time zone '2024-10-05 18:24:48.172000000Z') + * for this date, and all other dates are in GMT so this is an experiment. If nothing changes, + * we can roll this back to + * theCriteriaBuilder.literal(myRangeStartInclusive) + * JA 20241005 + */ + ZonedDateTime rangeStart = + ZonedDateTime.ofInstant(Instant.ofEpochMilli(myRangeStartInclusive.getTime()), ZoneId.of("GMT")); + Expression myUpdatedMostRecentOrDefault = - theCriteriaBuilder.coalesce(myUpdatedMostRecent, theCriteriaBuilder.literal(myRangeStartInclusive)); + theCriteriaBuilder.coalesce(myUpdatedMostRecent, theCriteriaBuilder.literal(rangeStart)); pastDateSubQuery .select(myUpdatedMostRecentOrDefault) 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 f9b62ad95e9..48a04cf38fe 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 @@ -164,6 +164,7 @@ public class JpaPersistedResourceValidationSupport implements IValidationSupport @Override public IBaseResource fetchStructureDefinition(String theUrl) { + assert myStructureDefinitionType != null; return fetchResource(myStructureDefinitionType, theUrl); } diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/term/TermReadSvcImpl.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/term/TermReadSvcImpl.java index 972255d9376..d52f493632b 100644 --- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/term/TermReadSvcImpl.java +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/term/TermReadSvcImpl.java @@ -117,7 +117,6 @@ import org.hibernate.search.mapper.orm.Search; import org.hibernate.search.mapper.orm.common.EntityReference; import org.hibernate.search.mapper.orm.session.SearchSession; import org.hibernate.search.mapper.pojo.massindexing.impl.PojoMassIndexingLoggingMonitor; -import org.hl7.fhir.common.hapi.validation.support.CachingValidationSupport; import org.hl7.fhir.common.hapi.validation.support.InMemoryTerminologyServerValidationSupport; import org.hl7.fhir.convertors.advisors.impl.BaseAdvisor_40_50; import org.hl7.fhir.convertors.context.ConversionContext40_50; @@ -284,9 +283,6 @@ public class TermReadSvcImpl implements ITermReadSvc, IHasScheduledJobs { @Autowired private HibernatePropertiesProvider myHibernatePropertiesProvider; - @Autowired - private CachingValidationSupport myCachingValidationSupport; - @Autowired private VersionCanonicalizer myVersionCanonicalizer; @@ -2509,9 +2505,7 @@ public class TermReadSvcImpl implements ITermReadSvc, IHasScheduledJobs { * results while they test changes, which is probably a worthwhile sacrifice */ private void afterValueSetExpansionStatusChange() { - // TODO: JA2 - Move this caching into the memorycacheservice, and only purge the - // relevant individual cache - myCachingValidationSupport.invalidateCaches(); + provideValidationSupport().invalidateCaches(); } private synchronized boolean isPreExpandingValueSets() { diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/util/SubscriptionsRequireManualActivationInterceptorDstu2.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/util/SubscriptionsRequireManualActivationInterceptorDstu2.java index 096af297509..892776c9d9d 100644 --- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/util/SubscriptionsRequireManualActivationInterceptorDstu2.java +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/util/SubscriptionsRequireManualActivationInterceptorDstu2.java @@ -20,6 +20,9 @@ package ca.uhn.fhir.jpa.util; import ca.uhn.fhir.i18n.Msg; +import ca.uhn.fhir.interceptor.api.Hook; +import ca.uhn.fhir.interceptor.api.Interceptor; +import ca.uhn.fhir.interceptor.api.Pointcut; import ca.uhn.fhir.jpa.api.dao.IFhirResourceDao; import ca.uhn.fhir.model.dstu2.resource.Subscription; import ca.uhn.fhir.model.dstu2.valueset.ResourceTypeEnum; @@ -29,31 +32,32 @@ import ca.uhn.fhir.rest.api.RestOperationTypeEnum; import ca.uhn.fhir.rest.api.server.RequestDetails; import ca.uhn.fhir.rest.server.exceptions.ResourceNotFoundException; import ca.uhn.fhir.rest.server.exceptions.UnprocessableEntityException; -import ca.uhn.fhir.rest.server.interceptor.ServerOperationInterceptorAdapter; import org.hl7.fhir.instance.model.api.IBaseResource; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Qualifier; +import static ca.uhn.fhir.subscription.SubscriptionConstants.ORDER_SUBSCRIPTION_ACTIVATING; import static org.apache.commons.lang3.StringUtils.isNotBlank; /** * Interceptor which requires newly created {@link Subscription subscriptions} to be in * {@link SubscriptionStatusEnum#REQUESTED} state and prevents clients from changing the status. */ -public class SubscriptionsRequireManualActivationInterceptorDstu2 extends ServerOperationInterceptorAdapter { +@Interceptor +public class SubscriptionsRequireManualActivationInterceptorDstu2 { @Autowired @Qualifier("mySubscriptionDaoDstu2") private IFhirResourceDao myDao; - @Override + @Hook(value = Pointcut.STORAGE_PRESTORAGE_RESOURCE_CREATED, order = ORDER_SUBSCRIPTION_ACTIVATING) public void resourceCreated(RequestDetails theRequest, IBaseResource theResource) { if (myDao.getContext().getResourceType(theResource).equals(ResourceTypeEnum.SUBSCRIPTION.getCode())) { verifyStatusOk(RestOperationTypeEnum.CREATE, null, theResource); } } - @Override + @Hook(value = Pointcut.STORAGE_PRESTORAGE_RESOURCE_UPDATED, order = ORDER_SUBSCRIPTION_ACTIVATING) public void resourceUpdated(RequestDetails theRequest, IBaseResource theOldResource, IBaseResource theNewResource) { if (myDao.getContext().getResourceType(theNewResource).equals(ResourceTypeEnum.SUBSCRIPTION.getCode())) { verifyStatusOk(RestOperationTypeEnum.UPDATE, theOldResource, theNewResource); diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/util/SubscriptionsRequireManualActivationInterceptorDstu3.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/util/SubscriptionsRequireManualActivationInterceptorDstu3.java index 9c93bd5dcf7..df670af2e93 100644 --- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/util/SubscriptionsRequireManualActivationInterceptorDstu3.java +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/util/SubscriptionsRequireManualActivationInterceptorDstu3.java @@ -20,13 +20,15 @@ package ca.uhn.fhir.jpa.util; import ca.uhn.fhir.i18n.Msg; +import ca.uhn.fhir.interceptor.api.Hook; +import ca.uhn.fhir.interceptor.api.Interceptor; +import ca.uhn.fhir.interceptor.api.Pointcut; import ca.uhn.fhir.jpa.api.dao.IFhirResourceDao; import ca.uhn.fhir.model.dstu2.valueset.ResourceTypeEnum; import ca.uhn.fhir.rest.api.RestOperationTypeEnum; import ca.uhn.fhir.rest.api.server.RequestDetails; import ca.uhn.fhir.rest.server.exceptions.ResourceNotFoundException; import ca.uhn.fhir.rest.server.exceptions.UnprocessableEntityException; -import ca.uhn.fhir.rest.server.interceptor.ServerOperationInterceptorAdapter; import org.hl7.fhir.dstu3.model.Subscription; import org.hl7.fhir.dstu3.model.Subscription.SubscriptionChannelType; import org.hl7.fhir.dstu3.model.Subscription.SubscriptionStatus; @@ -34,26 +36,28 @@ import org.hl7.fhir.instance.model.api.IBaseResource; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Qualifier; +import static ca.uhn.fhir.subscription.SubscriptionConstants.ORDER_SUBSCRIPTION_ACTIVATING; import static org.apache.commons.lang3.StringUtils.isNotBlank; /** * Interceptor which requires newly created {@link Subscription subscriptions} to be in * {@link SubscriptionStatus#REQUESTED} state and prevents clients from changing the status. */ -public class SubscriptionsRequireManualActivationInterceptorDstu3 extends ServerOperationInterceptorAdapter { +@Interceptor +public class SubscriptionsRequireManualActivationInterceptorDstu3 { @Autowired @Qualifier("mySubscriptionDaoDstu3") private IFhirResourceDao myDao; - @Override + @Hook(value = Pointcut.STORAGE_PRESTORAGE_RESOURCE_CREATED, order = ORDER_SUBSCRIPTION_ACTIVATING) public void resourceCreated(RequestDetails theRequest, IBaseResource theResource) { if (myDao.getContext().getResourceType(theResource).equals(ResourceTypeEnum.SUBSCRIPTION.getCode())) { verifyStatusOk(RestOperationTypeEnum.CREATE, null, theResource); } } - @Override + @Hook(value = Pointcut.STORAGE_PRESTORAGE_RESOURCE_UPDATED, order = ORDER_SUBSCRIPTION_ACTIVATING) public void resourceUpdated(RequestDetails theRequest, IBaseResource theOldResource, IBaseResource theNewResource) { if (myDao.getContext().getResourceType(theNewResource).equals(ResourceTypeEnum.SUBSCRIPTION.getCode())) { verifyStatusOk(RestOperationTypeEnum.UPDATE, theOldResource, theNewResource); diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/util/SubscriptionsRequireManualActivationInterceptorR4.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/util/SubscriptionsRequireManualActivationInterceptorR4.java index 2cb14efdd18..3d6cf30cfb5 100644 --- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/util/SubscriptionsRequireManualActivationInterceptorR4.java +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/util/SubscriptionsRequireManualActivationInterceptorR4.java @@ -20,13 +20,15 @@ package ca.uhn.fhir.jpa.util; import ca.uhn.fhir.i18n.Msg; +import ca.uhn.fhir.interceptor.api.Hook; +import ca.uhn.fhir.interceptor.api.Interceptor; +import ca.uhn.fhir.interceptor.api.Pointcut; import ca.uhn.fhir.jpa.api.dao.IFhirResourceDao; import ca.uhn.fhir.model.dstu2.valueset.ResourceTypeEnum; import ca.uhn.fhir.rest.api.RestOperationTypeEnum; import ca.uhn.fhir.rest.api.server.RequestDetails; import ca.uhn.fhir.rest.server.exceptions.ResourceNotFoundException; import ca.uhn.fhir.rest.server.exceptions.UnprocessableEntityException; -import ca.uhn.fhir.rest.server.interceptor.ServerOperationInterceptorAdapter; import org.hl7.fhir.instance.model.api.IBaseResource; import org.hl7.fhir.r4.model.Subscription; import org.hl7.fhir.r4.model.Subscription.SubscriptionChannelType; @@ -34,26 +36,28 @@ import org.hl7.fhir.r4.model.Subscription.SubscriptionStatus; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Qualifier; +import static ca.uhn.fhir.subscription.SubscriptionConstants.ORDER_SUBSCRIPTION_ACTIVATING; import static org.apache.commons.lang3.StringUtils.isNotBlank; /** * Interceptor which requires newly created {@link Subscription subscriptions} to be in * {@link SubscriptionStatus#REQUESTED} state and prevents clients from changing the status. */ -public class SubscriptionsRequireManualActivationInterceptorR4 extends ServerOperationInterceptorAdapter { +@Interceptor +public class SubscriptionsRequireManualActivationInterceptorR4 { @Autowired @Qualifier("mySubscriptionDaoR4") private IFhirResourceDao myDao; - @Override + @Hook(value = Pointcut.STORAGE_PRESTORAGE_RESOURCE_CREATED, order = ORDER_SUBSCRIPTION_ACTIVATING) public void resourceCreated(RequestDetails theRequest, IBaseResource theResource) { if (myDao.getContext().getResourceType(theResource).equals(ResourceTypeEnum.SUBSCRIPTION.getCode())) { verifyStatusOk(RestOperationTypeEnum.CREATE, null, theResource); } } - @Override + @Hook(value = Pointcut.STORAGE_PRESTORAGE_RESOURCE_UPDATED, order = ORDER_SUBSCRIPTION_ACTIVATING) public void resourceUpdated(RequestDetails theRequest, IBaseResource theOldResource, IBaseResource theNewResource) { if (myDao.getContext().getResourceType(theNewResource).equals(ResourceTypeEnum.SUBSCRIPTION.getCode())) { verifyStatusOk(RestOperationTypeEnum.UPDATE, theOldResource, theNewResource); diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/validation/JpaValidationSupportChain.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/validation/JpaValidationSupportChain.java index 25fda432e36..51791e0a051 100644 --- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/validation/JpaValidationSupportChain.java +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/validation/JpaValidationSupportChain.java @@ -21,6 +21,7 @@ package ca.uhn.fhir.jpa.validation; import ca.uhn.fhir.context.FhirContext; import ca.uhn.fhir.context.support.IValidationSupport; +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; @@ -39,7 +40,7 @@ public class JpaValidationSupportChain extends ValidationSupportChain { private final FhirContext myFhirContext; @Autowired - @Qualifier("myJpaValidationSupport") + @Qualifier(JpaConfig.JPA_VALIDATION_SUPPORT) public IValidationSupport myJpaValidationSupport; @Qualifier("myDefaultProfileValidationSupport") @@ -64,7 +65,13 @@ public class JpaValidationSupportChain extends ValidationSupportChain { /** * Constructor */ - public JpaValidationSupportChain(FhirContext theFhirContext) { + public JpaValidationSupportChain( + FhirContext theFhirContext, ValidationSupportChain.CacheConfiguration theCacheConfiguration) { + super(theCacheConfiguration); + + assert theFhirContext != null; + assert theCacheConfiguration != null; + myFhirContext = theFhirContext; } diff --git a/hapi-fhir-jpaserver-elastic-test-utilities/pom.xml b/hapi-fhir-jpaserver-elastic-test-utilities/pom.xml index 02632b55cbd..e945e1951d9 100644 --- a/hapi-fhir-jpaserver-elastic-test-utilities/pom.xml +++ b/hapi-fhir-jpaserver-elastic-test-utilities/pom.xml @@ -6,7 +6,7 @@ ca.uhn.hapi.fhir hapi-deployable-pom - 7.7.7-SNAPSHOT + 7.7.8-SNAPSHOT ../hapi-deployable-pom/pom.xml diff --git a/hapi-fhir-jpaserver-hfql/pom.xml b/hapi-fhir-jpaserver-hfql/pom.xml index 7325bd0e32f..6ebfca9d291 100644 --- a/hapi-fhir-jpaserver-hfql/pom.xml +++ b/hapi-fhir-jpaserver-hfql/pom.xml @@ -3,7 +3,7 @@ ca.uhn.hapi.fhir hapi-deployable-pom - 7.7.7-SNAPSHOT + 7.7.8-SNAPSHOT ../hapi-deployable-pom/pom.xml diff --git a/hapi-fhir-jpaserver-ips/pom.xml b/hapi-fhir-jpaserver-ips/pom.xml index 8b8d77344cc..bccea89af6f 100644 --- a/hapi-fhir-jpaserver-ips/pom.xml +++ b/hapi-fhir-jpaserver-ips/pom.xml @@ -3,7 +3,7 @@ ca.uhn.hapi.fhir hapi-deployable-pom - 7.7.7-SNAPSHOT + 7.7.8-SNAPSHOT ../hapi-deployable-pom/pom.xml diff --git a/hapi-fhir-jpaserver-mdm/pom.xml b/hapi-fhir-jpaserver-mdm/pom.xml index f07832756c1..6d73b51dc28 100644 --- a/hapi-fhir-jpaserver-mdm/pom.xml +++ b/hapi-fhir-jpaserver-mdm/pom.xml @@ -6,7 +6,7 @@ ca.uhn.hapi.fhir hapi-deployable-pom - 7.7.7-SNAPSHOT + 7.7.8-SNAPSHOT ../hapi-deployable-pom/pom.xml diff --git a/hapi-fhir-jpaserver-model/pom.xml b/hapi-fhir-jpaserver-model/pom.xml index 4c77735cfce..71d5378a536 100644 --- a/hapi-fhir-jpaserver-model/pom.xml +++ b/hapi-fhir-jpaserver-model/pom.xml @@ -5,7 +5,7 @@ ca.uhn.hapi.fhir hapi-deployable-pom - 7.7.7-SNAPSHOT + 7.7.8-SNAPSHOT ../hapi-deployable-pom/pom.xml diff --git a/hapi-fhir-jpaserver-searchparam/pom.xml b/hapi-fhir-jpaserver-searchparam/pom.xml index 3b5fa244225..17961eca9d5 100755 --- a/hapi-fhir-jpaserver-searchparam/pom.xml +++ b/hapi-fhir-jpaserver-searchparam/pom.xml @@ -5,7 +5,7 @@ ca.uhn.hapi.fhir hapi-deployable-pom - 7.7.7-SNAPSHOT + 7.7.8-SNAPSHOT ../hapi-deployable-pom/pom.xml diff --git a/hapi-fhir-jpaserver-subscription/pom.xml b/hapi-fhir-jpaserver-subscription/pom.xml index 4f1146e7b01..01b2efc2643 100644 --- a/hapi-fhir-jpaserver-subscription/pom.xml +++ b/hapi-fhir-jpaserver-subscription/pom.xml @@ -5,7 +5,7 @@ ca.uhn.hapi.fhir hapi-deployable-pom - 7.7.7-SNAPSHOT + 7.7.8-SNAPSHOT ../hapi-deployable-pom/pom.xml diff --git a/hapi-fhir-jpaserver-subscription/src/main/java/ca/uhn/fhir/jpa/subscription/submit/interceptor/SubscriptionValidatingInterceptor.java b/hapi-fhir-jpaserver-subscription/src/main/java/ca/uhn/fhir/jpa/subscription/submit/interceptor/SubscriptionValidatingInterceptor.java index c57dc761665..de49da049b2 100644 --- a/hapi-fhir-jpaserver-subscription/src/main/java/ca/uhn/fhir/jpa/subscription/submit/interceptor/SubscriptionValidatingInterceptor.java +++ b/hapi-fhir-jpaserver-subscription/src/main/java/ca/uhn/fhir/jpa/subscription/submit/interceptor/SubscriptionValidatingInterceptor.java @@ -64,6 +64,7 @@ import java.util.ArrayList; import java.util.List; import java.util.Optional; +import static ca.uhn.fhir.subscription.SubscriptionConstants.ORDER_SUBSCRIPTION_VALIDATING; import static org.apache.commons.lang3.StringUtils.isBlank; @Interceptor @@ -92,14 +93,14 @@ public class SubscriptionValidatingInterceptor { @Autowired private SubscriptionChannelTypeValidatorFactory mySubscriptionChannelTypeValidatorFactory; - @Hook(Pointcut.STORAGE_PRESTORAGE_RESOURCE_CREATED) + @Hook(value = Pointcut.STORAGE_PRESTORAGE_RESOURCE_CREATED, order = ORDER_SUBSCRIPTION_VALIDATING) public void resourcePreCreate( IBaseResource theResource, RequestDetails theRequestDetails, RequestPartitionId theRequestPartitionId) { validateSubmittedSubscription( theResource, theRequestDetails, theRequestPartitionId, Pointcut.STORAGE_PRESTORAGE_RESOURCE_CREATED); } - @Hook(Pointcut.STORAGE_PRESTORAGE_RESOURCE_UPDATED) + @Hook(value = Pointcut.STORAGE_PRESTORAGE_RESOURCE_UPDATED, order = ORDER_SUBSCRIPTION_VALIDATING) public void resourceUpdated( IBaseResource theOldResource, IBaseResource theResource, diff --git a/hapi-fhir-jpaserver-test-dstu2/pom.xml b/hapi-fhir-jpaserver-test-dstu2/pom.xml index c9701f08e4b..9b3c42420e3 100644 --- a/hapi-fhir-jpaserver-test-dstu2/pom.xml +++ b/hapi-fhir-jpaserver-test-dstu2/pom.xml @@ -6,7 +6,7 @@ ca.uhn.hapi.fhir hapi-deployable-pom - 7.7.7-SNAPSHOT + 7.7.8-SNAPSHOT ../hapi-deployable-pom/pom.xml diff --git a/hapi-fhir-jpaserver-test-dstu3/pom.xml b/hapi-fhir-jpaserver-test-dstu3/pom.xml index 99000fbb047..575740e0752 100644 --- a/hapi-fhir-jpaserver-test-dstu3/pom.xml +++ b/hapi-fhir-jpaserver-test-dstu3/pom.xml @@ -6,7 +6,7 @@ ca.uhn.hapi.fhir hapi-deployable-pom - 7.7.7-SNAPSHOT + 7.7.8-SNAPSHOT ../hapi-deployable-pom/pom.xml diff --git a/hapi-fhir-jpaserver-test-dstu3/src/test/java/ca/uhn/fhir/jpa/dao/dstu3/FhirResourceDaoDstu3TerminologyTest.java b/hapi-fhir-jpaserver-test-dstu3/src/test/java/ca/uhn/fhir/jpa/dao/dstu3/FhirResourceDaoDstu3TerminologyTest.java index e591027c62d..bccc40e1fd3 100644 --- a/hapi-fhir-jpaserver-test-dstu3/src/test/java/ca/uhn/fhir/jpa/dao/dstu3/FhirResourceDaoDstu3TerminologyTest.java +++ b/hapi-fhir-jpaserver-test-dstu3/src/test/java/ca/uhn/fhir/jpa/dao/dstu3/FhirResourceDaoDstu3TerminologyTest.java @@ -58,8 +58,6 @@ public class FhirResourceDaoDstu3TerminologyTest extends BaseJpaDstu3Test { public static final String URL_MY_VALUE_SET = "http://example.com/my_value_set"; private static final org.slf4j.Logger ourLog = org.slf4j.LoggerFactory.getLogger(FhirResourceDaoDstu3TerminologyTest.class); @Autowired - private CachingValidationSupport myCachingValidationSupport; - @Autowired private ITermDeferredStorageSvc myTermDeferredStorageSvc; @AfterEach @@ -69,10 +67,12 @@ public class FhirResourceDaoDstu3TerminologyTest extends BaseJpaDstu3Test { TermReindexingSvcImpl.setForceSaveDeferredAlwaysForUnitTest(false); } + @Override @BeforeEach - public void before() { + public void before() throws Exception { + super.before(); myStorageSettings.setMaximumExpansionSize(5000); - myCachingValidationSupport.invalidateCaches(); + myValidationSupport.invalidateCaches(); } private CodeSystem createExternalCs() { diff --git a/hapi-fhir-jpaserver-test-dstu3/src/test/java/ca/uhn/fhir/jpa/dao/dstu3/FhirResourceDaoDstu3ValidateTest.java b/hapi-fhir-jpaserver-test-dstu3/src/test/java/ca/uhn/fhir/jpa/dao/dstu3/FhirResourceDaoDstu3ValidateTest.java index d8c0a4f99e0..47ac1ba2f36 100644 --- a/hapi-fhir-jpaserver-test-dstu3/src/test/java/ca/uhn/fhir/jpa/dao/dstu3/FhirResourceDaoDstu3ValidateTest.java +++ b/hapi-fhir-jpaserver-test-dstu3/src/test/java/ca/uhn/fhir/jpa/dao/dstu3/FhirResourceDaoDstu3ValidateTest.java @@ -14,7 +14,6 @@ import ca.uhn.fhir.test.utilities.ProxyUtil; import ca.uhn.fhir.util.StopWatch; import ca.uhn.fhir.validation.IValidatorModule; import org.apache.commons.io.IOUtils; -import org.hl7.fhir.common.hapi.validation.support.CachingValidationSupport; import org.hl7.fhir.common.hapi.validation.validator.FhirInstanceValidator; import org.hl7.fhir.dstu3.model.Bundle; import org.hl7.fhir.dstu3.model.Bundle.BundleEntryComponent; @@ -55,8 +54,6 @@ public class FhirResourceDaoDstu3ValidateTest extends BaseJpaDstu3Test { @Autowired private IValidatorModule myValidatorModule; @Autowired - private CachingValidationSupport myValidationSupport; - @Autowired private FhirInstanceValidator myFhirInstanceValidator; @Autowired @Qualifier(JpaConfig.JPA_VALIDATION_SUPPORT) diff --git a/hapi-fhir-jpaserver-test-r4/pom.xml b/hapi-fhir-jpaserver-test-r4/pom.xml index 8a9353386f4..fc415fbd3c1 100644 --- a/hapi-fhir-jpaserver-test-r4/pom.xml +++ b/hapi-fhir-jpaserver-test-r4/pom.xml @@ -6,7 +6,7 @@ ca.uhn.hapi.fhir hapi-deployable-pom - 7.7.7-SNAPSHOT + 7.7.8-SNAPSHOT ../hapi-deployable-pom/pom.xml diff --git a/hapi-fhir-jpaserver-test-r4/src/test/java/ca/uhn/fhir/jpa/dao/JpaPersistedResourceValidationSupportFromValidationChainTest.java b/hapi-fhir-jpaserver-test-r4/src/test/java/ca/uhn/fhir/jpa/dao/JpaPersistedResourceValidationSupportFromValidationChainTest.java index ef9c28688e1..6163dea83f8 100644 --- a/hapi-fhir-jpaserver-test-r4/src/test/java/ca/uhn/fhir/jpa/dao/JpaPersistedResourceValidationSupportFromValidationChainTest.java +++ b/hapi-fhir-jpaserver-test-r4/src/test/java/ca/uhn/fhir/jpa/dao/JpaPersistedResourceValidationSupportFromValidationChainTest.java @@ -18,6 +18,7 @@ import org.hl7.fhir.r4.model.Bundle; import org.hl7.fhir.r4.model.Library; import org.hl7.fhir.r4.model.Measure; import org.hl7.fhir.r4.model.StructureDefinition; +import org.hl7.fhir.r4.model.ValueSet; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Disabled; import org.junit.jupiter.api.Test; @@ -38,7 +39,7 @@ import static org.mockito.Mockito.when; public class JpaPersistedResourceValidationSupportFromValidationChainTest { private static final FhirContext ourCtx = FhirContext.forR4(); - private IValidationSupport jpaValidator; + private JpaPersistedResourceValidationSupport jpaValidator; @Mock private DaoRegistry myDaoRegistry; @@ -58,6 +59,7 @@ public class JpaPersistedResourceValidationSupportFromValidationChainTest { @BeforeEach public void setUp() { jpaValidator = new JpaPersistedResourceValidationSupport(ourCtx); + jpaValidator.start(); ReflectionTestUtils.setField(jpaValidator, "myDaoRegistry", myDaoRegistry); } diff --git a/hapi-fhir-jpaserver-test-r4/src/test/java/ca/uhn/fhir/jpa/dao/r4/FhirResourceDaoR4TerminologyTest.java b/hapi-fhir-jpaserver-test-r4/src/test/java/ca/uhn/fhir/jpa/dao/r4/FhirResourceDaoR4TerminologyTest.java index fac5f68ef0c..ab8bd3a14b5 100644 --- a/hapi-fhir-jpaserver-test-r4/src/test/java/ca/uhn/fhir/jpa/dao/r4/FhirResourceDaoR4TerminologyTest.java +++ b/hapi-fhir-jpaserver-test-r4/src/test/java/ca/uhn/fhir/jpa/dao/r4/FhirResourceDaoR4TerminologyTest.java @@ -75,7 +75,7 @@ public class FhirResourceDaoR4TerminologyTest extends BaseJpaR4Test { public void before() throws Exception { super.before(); myStorageSettings.setMaximumExpansionSize(5000); - myCachingValidationSupport.invalidateCaches(); + myValidationSupport.invalidateCaches(); } private CodeSystem createExternalCs() { diff --git a/hapi-fhir-jpaserver-test-r4/src/test/java/ca/uhn/fhir/jpa/dao/r4/FhirResourceDaoR4ValidateTest.java b/hapi-fhir-jpaserver-test-r4/src/test/java/ca/uhn/fhir/jpa/dao/r4/FhirResourceDaoR4ValidateTest.java index 7cd70ab49e6..f007aa2fcf9 100644 --- a/hapi-fhir-jpaserver-test-r4/src/test/java/ca/uhn/fhir/jpa/dao/r4/FhirResourceDaoR4ValidateTest.java +++ b/hapi-fhir-jpaserver-test-r4/src/test/java/ca/uhn/fhir/jpa/dao/r4/FhirResourceDaoR4ValidateTest.java @@ -38,6 +38,7 @@ import ch.qos.logback.classic.Level; import org.apache.commons.io.IOUtils; import org.hl7.fhir.common.hapi.validation.support.InMemoryTerminologyServerValidationSupport; import org.hl7.fhir.common.hapi.validation.support.UnknownCodeSystemWarningValidationSupport; +import org.hl7.fhir.common.hapi.validation.support.ValidationSupportChain; import org.hl7.fhir.common.hapi.validation.validator.FhirInstanceValidator; import org.hl7.fhir.instance.model.api.IBaseOperationOutcome; import org.hl7.fhir.instance.model.api.IBaseResource; @@ -2237,6 +2238,7 @@ public class FhirResourceDaoR4ValidateTest extends BaseJpaR4Test { createStructureDefinitionInDao(); // execute + ((ValidationSupportChain)myValidationSupport).invalidateExpiringCaches(); final String outcomePatientValidateAfterStructDef = validate(PATIENT_WITH_REAL_URL); // verify diff --git a/hapi-fhir-jpaserver-test-r4/src/test/java/ca/uhn/fhir/jpa/dao/r4/FhirResourceDaoR4ValueSetTest.java b/hapi-fhir-jpaserver-test-r4/src/test/java/ca/uhn/fhir/jpa/dao/r4/FhirResourceDaoR4ValueSetTest.java index 7b5e063681c..ac590617758 100644 --- a/hapi-fhir-jpaserver-test-r4/src/test/java/ca/uhn/fhir/jpa/dao/r4/FhirResourceDaoR4ValueSetTest.java +++ b/hapi-fhir-jpaserver-test-r4/src/test/java/ca/uhn/fhir/jpa/dao/r4/FhirResourceDaoR4ValueSetTest.java @@ -169,7 +169,7 @@ public class FhirResourceDaoR4ValueSetTest extends BaseJpaR4Test { myTerminologyDeferredStorageSvc.saveAllDeferred(); myTermSvc.preExpandDeferredValueSetsToTerminologyTables(); logAllValueSets(); - myCachingValidationSupport.invalidateCaches(); + myValidationSupport.invalidateCaches(); outcome = myValidationSupport.validateCode(ctx, options, "http://cs", "child10", null, "http://vs"); assertNotNull(outcome); @@ -272,7 +272,7 @@ public class FhirResourceDaoR4ValueSetTest extends BaseJpaR4Test { myTerminologyDeferredStorageSvc.saveAllDeferred(); myTermSvc.preExpandDeferredValueSetsToTerminologyTables(); logAllValueSets(); - myCachingValidationSupport.invalidateCaches(); + myValidationSupport.invalidateCaches(); outcome = myValidationSupport.validateCode(ctx, options, "http://cs", "child10", null, "http://vs"); assertNotNull(outcome); 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 new file mode 100644 index 00000000000..8ed166482e0 --- /dev/null +++ b/hapi-fhir-jpaserver-test-r4/src/test/java/ca/uhn/fhir/jpa/interceptor/RemoteTerminologyServiceJpaR4Test.java @@ -0,0 +1,496 @@ +package ca.uhn.fhir.jpa.interceptor; + +import ca.uhn.fhir.context.FhirContext; +import ca.uhn.fhir.context.support.IValidationSupport; +import ca.uhn.fhir.jpa.test.BaseJpaR4Test; +import ca.uhn.fhir.rest.annotation.IdParam; +import ca.uhn.fhir.rest.annotation.Operation; +import ca.uhn.fhir.rest.annotation.OperationParam; +import ca.uhn.fhir.rest.annotation.OptionalParam; +import ca.uhn.fhir.rest.annotation.RequiredParam; +import ca.uhn.fhir.rest.annotation.Search; +import ca.uhn.fhir.rest.api.EncodingEnum; +import ca.uhn.fhir.rest.api.ValidationModeEnum; +import ca.uhn.fhir.rest.param.UriParam; +import ca.uhn.fhir.rest.server.IResourceProvider; +import ca.uhn.fhir.test.utilities.server.RestfulServerExtension; +import com.google.common.collect.ListMultimap; +import com.google.common.collect.MultimapBuilder; +import jakarta.annotation.Nonnull; +import jakarta.servlet.http.HttpServletRequest; +import org.hl7.fhir.common.hapi.validation.support.RemoteTerminologyServiceValidationSupport; +import org.hl7.fhir.common.hapi.validation.support.ValidationSupportChain; +import org.hl7.fhir.common.hapi.validation.validator.FhirInstanceValidator; +import org.hl7.fhir.instance.model.api.IBaseOperationOutcome; +import org.hl7.fhir.instance.model.api.IBaseResource; +import org.hl7.fhir.instance.model.api.IPrimitiveType; +import org.hl7.fhir.r4.model.BooleanType; +import org.hl7.fhir.r4.model.CodeSystem; +import org.hl7.fhir.r4.model.CodeType; +import org.hl7.fhir.r4.model.CodeableConcept; +import org.hl7.fhir.r4.model.Coding; +import org.hl7.fhir.r4.model.ElementDefinition; +import org.hl7.fhir.r4.model.Enumerations; +import org.hl7.fhir.r4.model.IdType; +import org.hl7.fhir.r4.model.OperationOutcome; +import org.hl7.fhir.r4.model.Parameters; +import org.hl7.fhir.r4.model.Patient; +import org.hl7.fhir.r4.model.StringType; +import org.hl7.fhir.r4.model.StructureDefinition; +import org.hl7.fhir.r4.model.UriType; +import org.hl7.fhir.r4.model.ValueSet; +import org.hl7.fhir.r5.utils.validation.constants.BestPracticeWarningLevel; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; +import org.springframework.beans.factory.annotation.Autowired; + +import java.util.ArrayList; +import java.util.List; + +import static java.util.Objects.requireNonNull; +import static org.apache.commons.lang3.StringUtils.defaultString; +import static org.apache.commons.lang3.StringUtils.isNotBlank; +import static org.assertj.core.api.AssertionsForClassTypes.assertThat; +import static org.junit.jupiter.api.Assertions.assertEquals; + +/** + * This test verifies how {@link RemoteTerminologyServiceValidationSupport} interacts with + * the rest of the ValidationSupportChain. The aim here is that we should perform as few + * interactions across the network as we can, so any caching that avoids a lookup through + * the remote module is a good thing. We're also testing that we don't open more database + * connections than we need to, since every connection is a delay. + */ +public class RemoteTerminologyServiceJpaR4Test extends BaseJpaR4Test { + + private static final MyCodeSystemProvider ourCodeSystemProvider = new MyCodeSystemProvider(); + private static final MyValueSetProvider ourValueSetProvider = new MyValueSetProvider(); + @RegisterExtension + private static final RestfulServerExtension ourTerminologyServer = new RestfulServerExtension(FhirContext.forR4Cached()) + .registerProvider(ourCodeSystemProvider) + .registerProvider(ourValueSetProvider); + @Autowired + private ValidationSupportChain myValidationSupportChain; + @Autowired + private FhirInstanceValidator myFhirInstanceValidator; + private RemoteTerminologyServiceValidationSupport myRemoteTerminologyService; + private ValidationSupportChain myInternalValidationSupport; + + @Override + @BeforeEach + public void before() throws Exception { + super.before(); + + myFhirInstanceValidator.setBestPracticeWarningLevel(BestPracticeWarningLevel.Ignore); + + List original = myValidationSupportChain.getValidationSupports(); + myInternalValidationSupport = new ValidationSupportChain(original); + + myRemoteTerminologyService = new RemoteTerminologyServiceValidationSupport(myFhirContext, ourTerminologyServer.getBaseUrl()); + myValidationSupportChain.addValidationSupport(0, myRemoteTerminologyService); + myValidationSupportChain.invalidateCaches(); + + // Warm this as it's needed once by the FhirPath evaluator on startup + // so this avoids having different connection counts depending on + // which test method is called first. This is a non-expiring cache, so + // pre-warming here isn't affecting anything meaningful. + myValidationSupportChain.fetchAllStructureDefinitions(); + } + + @AfterEach + public void after() throws Exception { + myValidationSupportChain.logCacheSizes(); + myValidationSupportChain.removeValidationSupport(myRemoteTerminologyService); + + ourValueSetProvider.clearAll(); + ourCodeSystemProvider.clearAll(); + } + + @Test + public void testValidateSimpleCode() { + Patient p = new Patient(); + p.setGender(Enumerations.AdministrativeGender.FEMALE); + + // Test 1 + ourCodeSystemProvider.clearCalls(); + ourValueSetProvider.clearCalls(); + myCaptureQueriesListener.clear(); + IBaseOperationOutcome outcome = validate(p); + assertSuccess(outcome); + + // 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" + ); + assertThat(ourCodeSystemProvider.mySearchUrls).asList().containsExactlyInAnyOrder( + "http://hl7.org/fhir/administrative-gender", + "http://hl7.org/fhir/administrative-gender" + ); + + // Test 2 (should rely on caches) + ourCodeSystemProvider.clearCalls(); + ourValueSetProvider.clearCalls(); + myCaptureQueriesListener.clear(); + outcome = validate(p); + assertSuccess(outcome); + + // Verify 2 + Assertions.assertEquals(0, myCaptureQueriesListener.countGetConnections()); + assertThat(ourValueSetProvider.mySearchUrls).asList().isEmpty(); + assertThat(ourCodeSystemProvider.mySearchUrls).asList().isEmpty(); + + } + + @Test + public void testValidateSimpleCode_SupportedByRemoteService() { + Patient p = new Patient(); + p.setGender(Enumerations.AdministrativeGender.FEMALE); + ourValueSetProvider.add((ValueSet) requireNonNull(myInternalValidationSupport.fetchValueSet("http://hl7.org/fhir/ValueSet/administrative-gender"))); + ourCodeSystemProvider.add((CodeSystem) requireNonNull(myInternalValidationSupport.fetchCodeSystem("http://hl7.org/fhir/administrative-gender"))); + + // Test 1 + ourCodeSystemProvider.clearCalls(); + ourValueSetProvider.clearCalls(); + myCaptureQueriesListener.clear(); + IBaseOperationOutcome outcome = validate(p); + assertSuccess(outcome); + + // 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" + ); + assertThat(ourValueSetProvider.myValidatedCodes).asList().containsExactlyInAnyOrder( + "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" + ); + + // Test 2 (should rely on caches) + ourCodeSystemProvider.clearCalls(); + ourValueSetProvider.clearCalls(); + myCaptureQueriesListener.clear(); + outcome = validate(p); + assertSuccess(outcome); + + // Verify 2 + Assertions.assertEquals(0, myCaptureQueriesListener.countGetConnections()); + assertThat(ourValueSetProvider.mySearchUrls).asList().isEmpty(); + assertThat(ourValueSetProvider.myValidatedCodes).asList().isEmpty(); + assertThat(ourCodeSystemProvider.mySearchUrls).asList().isEmpty(); + assertThat(ourCodeSystemProvider.myValidatedCodes).asList().isEmpty(); + + } + + /** + * If the remote terminology service is serving up stub ValueSet and CodeSystem + * resources, make sure we still behave in a sane way. This probably wouldn't + * happen exactly like this, but the idea here is that the server could + * serve up weird contents where our internal services couldn't determine + * the implicit system from the ValueSet. + */ + @Test + public void testValidateSimpleCode_SupportedByRemoteService_EmptyValueSet() { + Patient p = new Patient(); + p.setGender(Enumerations.AdministrativeGender.FEMALE); + ourValueSetProvider.add((ValueSet) new ValueSet().setUrl("http://hl7.org/fhir/ValueSet/administrative-gender").setId("gender")); + ourCodeSystemProvider.add((CodeSystem) new CodeSystem().setUrl("http://hl7.org/fhir/administrative-gender").setId("gender")); + + // Test 1 + ourCodeSystemProvider.clearCalls(); + ourValueSetProvider.clearCalls(); + myCaptureQueriesListener.clear(); + IBaseOperationOutcome outcome = validate(p); + assertSuccess(outcome); + + // 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" + ); + assertThat(ourValueSetProvider.myValidatedCodes).asList().containsExactlyInAnyOrder( + "http://hl7.org/fhir/ValueSet/administrative-gender#null#female" + ); + assertThat(ourCodeSystemProvider.mySearchUrls).asList().isEmpty(); + assertThat(ourCodeSystemProvider.myValidatedCodes).asList().isEmpty(); + + // Test 2 (should rely on caches) + ourCodeSystemProvider.clearCalls(); + ourValueSetProvider.clearCalls(); + myCaptureQueriesListener.clear(); + outcome = validate(p); + assertSuccess(outcome); + + // Verify 2 + Assertions.assertEquals(0, myCaptureQueriesListener.countGetConnections()); + assertThat(ourValueSetProvider.mySearchUrls).asList().isEmpty(); + assertThat(ourValueSetProvider.myValidatedCodes).asList().isEmpty(); + assertThat(ourCodeSystemProvider.mySearchUrls).asList().isEmpty(); + assertThat(ourCodeSystemProvider.myValidatedCodes).asList().isEmpty(); + + myValidationSupportChain.logCacheSizes(); + } + + @Test + public void testValidateSimpleExtension() { + // Setup + myStructureDefinitionDao.create(createFooExtensionStructureDefinition(), mySrd); + Patient p = new Patient(); + p.addExtension("http://foo", new StringType("BAR")); + + // Test 1 + ourCodeSystemProvider.clearCalls(); + ourValueSetProvider.clearCalls(); + myCaptureQueriesListener.clear(); + IBaseOperationOutcome outcome = validate(p); + assertSuccess(outcome); + + // Verify 1 + myCaptureQueriesListener.logSelectQueries(); + Assertions.assertEquals(4, myCaptureQueriesListener.countGetConnections()); + assertThat(ourValueSetProvider.mySearchUrls).asList().isEmpty(); + assertThat(ourCodeSystemProvider.mySearchUrls).asList().isEmpty(); + + // Test 2 (should rely on caches) + ourCodeSystemProvider.clearCalls(); + ourValueSetProvider.clearCalls(); + myCaptureQueriesListener.clear(); + outcome = validate(p); + assertSuccess(outcome); + + // Verify 2 + Assertions.assertEquals(0, myCaptureQueriesListener.countGetConnections()); + assertThat(ourValueSetProvider.mySearchUrls).asList().isEmpty(); + assertThat(ourCodeSystemProvider.mySearchUrls).asList().isEmpty(); + + } + + @Test + public void testValidateMultipleCodings() { + // Setup + Patient p = new Patient(); + p.addIdentifier() + // Valid type + .setType(new CodeableConcept().addCoding(new Coding("http://terminology.hl7.org/CodeSystem/v2-0203", "DL", null))) + .setSystem("http://my-system-1") + .setValue("1"); + p.addIdentifier() + // Valid type + .setType(new CodeableConcept().addCoding(new Coding("http://terminology.hl7.org/CodeSystem/v2-0203", "PPN", null))) + .setSystem("http://my-system-2") + .setValue("2"); + p.addIdentifier() + // Invalid type + .setType(new CodeableConcept().addCoding(new Coding("http://terminology.hl7.org/CodeSystem/v2-0203", "FOO", null))) + .setSystem("http://my-system-3") + .setValue("3"); + + // Test 1 + ourCodeSystemProvider.clearCalls(); + ourValueSetProvider.clearCalls(); + myCaptureQueriesListener.clear(); + IBaseOperationOutcome outcome = validate(p); + assertHasIssuesContainingMessages(outcome, + "Unknown code 'http://terminology.hl7.org/CodeSystem/v2-0203#FOO'", + "None of the codings provided are in the value set 'IdentifierType'"); + + // Verify 1 + Assertions.assertEquals(2, myCaptureQueriesListener.countGetConnections()); + assertThat(ourValueSetProvider.mySearchUrls).asList().containsExactlyInAnyOrder( + "http://hl7.org/fhir/ValueSet/identifier-type", + "http://hl7.org/fhir/ValueSet/identifier-type" + ); + assertThat(ourCodeSystemProvider.mySearchUrls).asList().containsExactlyInAnyOrder( + "http://terminology.hl7.org/CodeSystem/v2-0203", + "http://terminology.hl7.org/CodeSystem/v2-0203" + ); + assertEquals(0, ourValueSetProvider.myValidatedCodes.size()); + assertEquals(0, ourCodeSystemProvider.myValidatedCodes.size()); + + // Test 2 (should rely on caches) + ourCodeSystemProvider.clearCalls(); + ourValueSetProvider.clearCalls(); + myCaptureQueriesListener.clear(); + validate(p); + + // Verify 2 + Assertions.assertEquals(0, myCaptureQueriesListener.countGetConnections()); + assertThat(ourValueSetProvider.mySearchUrls).asList().isEmpty(); + assertThat(ourCodeSystemProvider.mySearchUrls).asList().isEmpty(); + assertEquals(0, ourValueSetProvider.myValidatedCodes.size()); + assertEquals(0, ourCodeSystemProvider.myValidatedCodes.size()); + + } + + private void assertSuccess(IBaseOperationOutcome theOutcome) { + OperationOutcome oo = (OperationOutcome) theOutcome; + assertEquals(1, oo.getIssue().size(), () -> encode(oo)); + assertThat(oo.getIssue().get(0).getDiagnostics()).as(() -> encode(oo)).contains("No issues detected"); + } + + private void assertHasIssuesContainingMessages(IBaseOperationOutcome theOutcome, String... theDiagnosticMessageFragments) { + OperationOutcome oo = (OperationOutcome) theOutcome; + assertEquals(theDiagnosticMessageFragments.length, oo.getIssue().size(), () -> encode(oo)); + for (int i = 0; i < theDiagnosticMessageFragments.length; i++) { + assertThat(oo.getIssue().get(i).getDiagnostics()).as(() -> encode(oo)).contains(theDiagnosticMessageFragments[i]); + } + } + + private IBaseOperationOutcome validate(Patient p) { + return myPatientDao.validate(p, null, myFhirContext.newJsonParser().setPrettyPrint(true).encodeResourceToString(p), EncodingEnum.JSON, ValidationModeEnum.CREATE, null, mySrd).getOperationOutcome(); + } + + /** + * Create a StructureDefinition for an extension with URL http://foo + */ + @Nonnull + private static StructureDefinition createFooExtensionStructureDefinition() { + StructureDefinition sd = new StructureDefinition(); + sd.setUrl("http://foo"); + sd.setFhirVersion(Enumerations.FHIRVersion._4_0_1); + sd.setKind(StructureDefinition.StructureDefinitionKind.COMPLEXTYPE); + sd.setAbstract(false); + sd.addContext().setType(StructureDefinition.ExtensionContextType.ELEMENT).setExpression("Patient"); + sd.setType("Extension"); + sd.setBaseDefinition("http://hl7.org/fhir/StructureDefinition/Extension"); + sd.setDerivation(StructureDefinition.TypeDerivationRule.CONSTRAINT); + ElementDefinition e0 = sd.getDifferential().addElement(); + e0.setId("Extension"); + e0.setPath("Extension"); + ElementDefinition e1 = sd.getDifferential().addElement(); + e1.setId("Extension.url"); + e1.setPath("Extension.url"); + e1.setFixed(new UriType("http://foo")); + ElementDefinition e2 = sd.getDifferential().addElement(); + e2.setId("Extension.value[x]"); + e2.setPath("Extension.value[x]"); + e2.addType().setCode("string"); + return sd; + } + + private static String toValue(IPrimitiveType theUrlType) { + return theUrlType != null ? theUrlType.getValue() : null; + } + + private static class MyCodeSystemProvider implements IResourceProvider { + private final ListMultimap myUrlToCodeSystems = MultimapBuilder.hashKeys().arrayListValues().build(); + private final List mySearchUrls = new ArrayList<>(); + private final List myValidatedCodes = new ArrayList<>(); + + public void clearAll() { + myUrlToCodeSystems.clear(); + clearCalls(); + } + + public void clearCalls() { + mySearchUrls.clear(); + myValidatedCodes.clear(); + } + + @Operation(name = "validate-code", idempotent = true, returnParameters = { + @OperationParam(name = "result", type = BooleanType.class, min = 1), + @OperationParam(name = "message", type = StringType.class), + @OperationParam(name = "display", type = StringType.class) + }) + public Parameters validateCode( + HttpServletRequest theServletRequest, + @IdParam(optional = true) IdType theId, + @OperationParam(name = "url", min = 0, max = 1) UriType theCodeSystemUrl, + @OperationParam(name = "code", min = 0, max = 1) CodeType theCode, + @OperationParam(name = "display", min = 0, max = 1) StringType theDisplay + ) { + myValidatedCodes.add(toValue(theCodeSystemUrl) + "#" + toValue(theCode) + "#" + toValue(theDisplay)); + + Parameters retVal = new Parameters(); + retVal.addParameter("result", new BooleanType(true)); + return retVal; + } + + @Search + public List find(@RequiredParam(name = "url") UriParam theUrlParam) { + String url = theUrlParam != null ? theUrlParam.getValue() : null; + mySearchUrls.add(url); + return myUrlToCodeSystems.get(defaultString(url)); + } + + @Override + public Class getResourceType() { + return CodeSystem.class; + } + + public void add(CodeSystem theCs) { + assert theCs != null; + assert isNotBlank(theCs.getUrl()); + myUrlToCodeSystems.put(theCs.getUrl(), theCs); + } + } + + @SuppressWarnings("unused") + private static class MyValueSetProvider implements IResourceProvider { + + private final ListMultimap myUrlToValueSets = MultimapBuilder.hashKeys().arrayListValues().build(); + private final List mySearchUrls = new ArrayList<>(); + private final List myValidatedCodes = new ArrayList<>(); + + public void clearAll() { + myUrlToValueSets.clear(); + clearCalls(); + } + + public void clearCalls() { + mySearchUrls.clear(); + myValidatedCodes.clear(); + } + + @Operation(name = "validate-code", idempotent = true, returnParameters = { + @OperationParam(name = "result", type = BooleanType.class, min = 1), + @OperationParam(name = "message", type = StringType.class), + @OperationParam(name = "display", type = StringType.class) + }) + public Parameters validateCode( + HttpServletRequest theServletRequest, + @OperationParam(name = "url", min = 0, max = 1) UriType theValueSetUrl, + @OperationParam(name = "code", min = 0, max = 1) CodeType theCode, + @OperationParam(name = "system", min = 0, max = 1) UriType theSystem, + @OperationParam(name = "display", min = 0, max = 1) StringType theDisplay, + @OperationParam(name = "valueSet") ValueSet theValueSet + ) { + myValidatedCodes.add(toValue(theValueSetUrl) + "#" + toValue(theSystem) + "#" + toValue(theCode)); + + Parameters retVal = new Parameters(); + retVal.addParameter("result", new BooleanType(true)); + return retVal; + } + + @Search + public List find(@OptionalParam(name = "url") UriParam theUrlParam) { + String url = theUrlParam != null ? theUrlParam.getValue() : null; + mySearchUrls.add(url); + List retVal = myUrlToValueSets.get(defaultString(url)); + ourLog.info("Remote terminology fetch ValueSet[{}] - Found: {}", url, !retVal.isEmpty()); + return retVal; + } + + @Override + public Class getResourceType() { + return ValueSet.class; + } + + public void add(ValueSet theVs) { + assert theVs != null; + assert isNotBlank(theVs.getUrl()); + myUrlToValueSets.put(theVs.getUrl(), theVs); + } + } + +} diff --git a/hapi-fhir-jpaserver-test-r4/src/test/java/ca/uhn/fhir/jpa/provider/r4/ResourceProviderR4Test.java b/hapi-fhir-jpaserver-test-r4/src/test/java/ca/uhn/fhir/jpa/provider/r4/ResourceProviderR4Test.java index 8f3e849719f..1da8eb0e6db 100644 --- a/hapi-fhir-jpaserver-test-r4/src/test/java/ca/uhn/fhir/jpa/provider/r4/ResourceProviderR4Test.java +++ b/hapi-fhir-jpaserver-test-r4/src/test/java/ca/uhn/fhir/jpa/provider/r4/ResourceProviderR4Test.java @@ -2366,18 +2366,19 @@ public class ResourceProviderR4Test extends BaseResourceProviderR4Test { IIdType id = idCreated.toUnqualifiedVersionless(); for (int i = 0; i < 10; i++) { - sleepOneClick(); + sleepAtLeast(100); preDates.add(new Date()); - sleepOneClick(); + sleepAtLeast(100); patient.setId(id); patient.getName().get(0).getFamilyElement().setValue(methodName + "_i" + i); ids.add(myPatientDao.update(patient, mySrd).getId().toUnqualified().getValue()); - sleepOneClick(); } List idValues; + myCaptureQueriesListener.clear(); idValues = searchAndReturnUnqualifiedIdValues(myServerBase + "/Patient/" + id.getIdPart() + "/_history?_at=gt" + toStr(preDates.get(0)) + "&_at=lt" + toStr(preDates.get(3))); + myCaptureQueriesListener.logSelectQueries(); assertThat(idValues).as(idValues.toString()).containsExactly(ids.get(3), ids.get(2), ids.get(1), ids.get(0)); idValues = searchAndReturnUnqualifiedIdValues(myServerBase + "/Patient/_history?_at=gt" + toStr(preDates.get(0)) + "&_at=lt" + toStr(preDates.get(3))); diff --git a/hapi-fhir-jpaserver-test-r4b/pom.xml b/hapi-fhir-jpaserver-test-r4b/pom.xml index e84136c59f7..bb5bfc312e1 100644 --- a/hapi-fhir-jpaserver-test-r4b/pom.xml +++ b/hapi-fhir-jpaserver-test-r4b/pom.xml @@ -6,7 +6,7 @@ ca.uhn.hapi.fhir hapi-deployable-pom - 7.7.7-SNAPSHOT + 7.7.8-SNAPSHOT ../hapi-deployable-pom/pom.xml diff --git a/hapi-fhir-jpaserver-test-r5/pom.xml b/hapi-fhir-jpaserver-test-r5/pom.xml index 9ebac21f0b6..d17bb5b2f29 100644 --- a/hapi-fhir-jpaserver-test-r5/pom.xml +++ b/hapi-fhir-jpaserver-test-r5/pom.xml @@ -6,7 +6,7 @@ ca.uhn.hapi.fhir hapi-deployable-pom - 7.7.7-SNAPSHOT + 7.7.8-SNAPSHOT ../hapi-deployable-pom/pom.xml diff --git a/hapi-fhir-jpaserver-test-r5/src/test/java/ca/uhn/fhir/jpa/dao/r5/FhirResourceDaoR5ValueSetTest.java b/hapi-fhir-jpaserver-test-r5/src/test/java/ca/uhn/fhir/jpa/dao/r5/FhirResourceDaoR5ValueSetTest.java index 8d96a32e00d..b1ed8242826 100644 --- a/hapi-fhir-jpaserver-test-r5/src/test/java/ca/uhn/fhir/jpa/dao/r5/FhirResourceDaoR5ValueSetTest.java +++ b/hapi-fhir-jpaserver-test-r5/src/test/java/ca/uhn/fhir/jpa/dao/r5/FhirResourceDaoR5ValueSetTest.java @@ -1,7 +1,5 @@ package ca.uhn.fhir.jpa.dao.r5; -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertNotNull; import ca.uhn.fhir.context.FhirVersionEnum; import ca.uhn.fhir.context.support.ConceptValidationOptions; import ca.uhn.fhir.context.support.IValidationSupport; @@ -17,7 +15,6 @@ import ca.uhn.fhir.jpa.util.ValueSetTestUtil; import ca.uhn.fhir.rest.server.exceptions.InternalErrorException; import ca.uhn.fhir.rest.server.exceptions.InvalidRequestException; import ca.uhn.fhir.rest.server.exceptions.ResourceGoneException; -import org.hl7.fhir.common.hapi.validation.support.CachingValidationSupport; import org.hl7.fhir.instance.model.api.IIdType; import org.hl7.fhir.instance.model.api.IPrimitiveType; import org.hl7.fhir.r5.model.CodeSystem; @@ -38,10 +35,12 @@ import java.io.IOException; import java.util.Optional; import static org.assertj.core.api.Assertions.assertThat; -import static org.junit.jupiter.api.Assertions.fail; import static org.assertj.core.api.AssertionsForClassTypes.assertThatExceptionOfType; +import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNotNull; import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.junit.jupiter.api.Assertions.fail; public class FhirResourceDaoR5ValueSetTest extends BaseJpaR5Test { @@ -51,7 +50,6 @@ public class FhirResourceDaoR5ValueSetTest extends BaseJpaR5Test { @Autowired protected ITermDeferredStorageSvc myTerminologyDeferredStorageSvc; - private IIdType myExtensionalVsId; @AfterEach @@ -61,7 +59,6 @@ public class FhirResourceDaoR5ValueSetTest extends BaseJpaR5Test { myStorageSettings.setExpungeEnabled(new JpaStorageSettings().isExpungeEnabled()); } - @BeforeEach @Transactional public void before02() throws IOException { @@ -251,7 +248,6 @@ public class FhirResourceDaoR5ValueSetTest extends BaseJpaR5Test { } - /** * See #4305 */ @@ -267,7 +263,7 @@ public class FhirResourceDaoR5ValueSetTest extends BaseJpaR5Test { // Update valueset vs.setName("Hello"); assertEquals("2", myValueSetDao.update(vs, mySrd).getId().getVersionIdPart()); - runInTransaction(()->{ + runInTransaction(() -> { Optional resource = myResourceTableDao.findById(id.getIdPartAsLong()); assertTrue(resource.isPresent()); }); @@ -287,13 +283,12 @@ public class FhirResourceDaoR5ValueSetTest extends BaseJpaR5Test { myValueSetDao.expunge(id, new ExpungeOptions().setExpungeDeletedResources(true).setExpungeOldVersions(true), mySrd); // Verify expunged - runInTransaction(()->{ + runInTransaction(() -> { Optional resource = myResourceTableDao.findById(id.getIdPartAsLong()); assertFalse(resource.isPresent()); }); } - @Test public void testExpandByValueSet_ExceedsMaxSize() { // Add a bunch of codes @@ -336,12 +331,12 @@ public class FhirResourceDaoR5ValueSetTest extends BaseJpaR5Test { myTerminologyDeferredStorageSvc.saveAllDeferred(); myTermSvc.preExpandDeferredValueSetsToTerminologyTables(); logAllValueSets(); - myCachingValidationSupport.invalidateCaches(); + myValidationSupport.invalidateCaches(); // Validate code ValidationSupportContext ctx = new ValidationSupportContext(myValidationSupport); - ConceptValidationOptions options= new ConceptValidationOptions(); + ConceptValidationOptions options = new ConceptValidationOptions(); IValidationSupport.CodeValidationResult outcome = myValidationSupport.validateCode(ctx, options, "http://cs", "CODE4", null, "http://vs"); assertNotNull(outcome); assertTrue(outcome.isOk()); @@ -354,9 +349,6 @@ public class FhirResourceDaoR5ValueSetTest extends BaseJpaR5Test { assertThat(expansionMessage).startsWith("ValueSet was expanded using an expansion that was pre-calculated"); } - @Autowired - protected CachingValidationSupport myCachingValidationSupport; - @Test public void testValidateCodeAgainstBuiltInValueSetAndCodeSystemWithValidCode() { IPrimitiveType display = null; diff --git a/hapi-fhir-jpaserver-test-utilities/pom.xml b/hapi-fhir-jpaserver-test-utilities/pom.xml index 1ac2a59d4e2..c942a7a4ab9 100644 --- a/hapi-fhir-jpaserver-test-utilities/pom.xml +++ b/hapi-fhir-jpaserver-test-utilities/pom.xml @@ -6,7 +6,7 @@ ca.uhn.hapi.fhir hapi-deployable-pom - 7.7.7-SNAPSHOT + 7.7.8-SNAPSHOT ../hapi-deployable-pom/pom.xml diff --git a/hapi-fhir-jpaserver-test-utilities/src/main/java/ca/uhn/fhir/jpa/test/BaseJpaR4Test.java b/hapi-fhir-jpaserver-test-utilities/src/main/java/ca/uhn/fhir/jpa/test/BaseJpaR4Test.java index f379ee2cf0a..b29c4a76ea2 100644 --- a/hapi-fhir-jpaserver-test-utilities/src/main/java/ca/uhn/fhir/jpa/test/BaseJpaR4Test.java +++ b/hapi-fhir-jpaserver-test-utilities/src/main/java/ca/uhn/fhir/jpa/test/BaseJpaR4Test.java @@ -118,7 +118,6 @@ import ca.uhn.fhir.validation.FhirValidator; import ca.uhn.fhir.validation.ValidationResult; import ca.uhn.test.util.LogbackTestExtension; import jakarta.persistence.EntityManager; -import org.hl7.fhir.common.hapi.validation.support.CachingValidationSupport; import org.hl7.fhir.common.hapi.validation.validator.FhirInstanceValidator; import org.hl7.fhir.instance.model.api.IBaseResource; import org.hl7.fhir.instance.model.api.IIdType; @@ -246,8 +245,6 @@ public abstract class BaseJpaR4Test extends BaseJpaTest implements ITestDataBuil @Autowired protected ITermReadSvc myHapiTerminologySvc; @Autowired - protected CachingValidationSupport myCachingValidationSupport; - @Autowired protected ITermCodeSystemStorageSvc myTermCodeSystemStorageSvc; @Autowired protected ISearchDao mySearchEntityDao; diff --git a/hapi-fhir-jpaserver-test-utilities/src/main/java/ca/uhn/fhir/jpa/test/BaseJpaTest.java b/hapi-fhir-jpaserver-test-utilities/src/main/java/ca/uhn/fhir/jpa/test/BaseJpaTest.java index dba57255f05..8423f9e899a 100644 --- a/hapi-fhir-jpaserver-test-utilities/src/main/java/ca/uhn/fhir/jpa/test/BaseJpaTest.java +++ b/hapi-fhir-jpaserver-test-utilities/src/main/java/ca/uhn/fhir/jpa/test/BaseJpaTest.java @@ -271,6 +271,8 @@ public abstract class BaseJpaTest extends BaseTest { @Autowired private IValidationSupport myJpaPersistedValidationSupport; @Autowired + private IValidationSupport myValidationSupport; + @Autowired private FhirInstanceValidator myFhirInstanceValidator; @Autowired private IResourceTableDao myResourceTableDao; @@ -398,6 +400,10 @@ public abstract class BaseJpaTest extends BaseTest { if (myFhirInstanceValidator != null) { myFhirInstanceValidator.invalidateCaches(); } + if (myValidationSupport != null) { + myValidationSupport.invalidateCaches(); + } + JpaStorageSettings defaultConfig = new JpaStorageSettings(); myStorageSettings.setAdvancedHSearchIndexing(defaultConfig.isAdvancedHSearchIndexing()); myStorageSettings.setAllowContainsSearches(defaultConfig.isAllowContainsSearches()); diff --git a/hapi-fhir-jpaserver-uhnfhirtest/pom.xml b/hapi-fhir-jpaserver-uhnfhirtest/pom.xml index 37b62d52c3c..c0b49b0e380 100644 --- a/hapi-fhir-jpaserver-uhnfhirtest/pom.xml +++ b/hapi-fhir-jpaserver-uhnfhirtest/pom.xml @@ -5,7 +5,7 @@ ca.uhn.hapi.fhir hapi-fhir - 7.7.7-SNAPSHOT + 7.7.8-SNAPSHOT ../pom.xml diff --git a/hapi-fhir-server-cds-hooks/pom.xml b/hapi-fhir-server-cds-hooks/pom.xml index 0c3871b2a8c..cca741e8e44 100644 --- a/hapi-fhir-server-cds-hooks/pom.xml +++ b/hapi-fhir-server-cds-hooks/pom.xml @@ -7,7 +7,7 @@ ca.uhn.hapi.fhir hapi-deployable-pom - 7.7.7-SNAPSHOT + 7.7.8-SNAPSHOT ../hapi-deployable-pom/pom.xml diff --git a/hapi-fhir-server-mdm/pom.xml b/hapi-fhir-server-mdm/pom.xml index 00258dc7ebd..8aa2a2d33a9 100644 --- a/hapi-fhir-server-mdm/pom.xml +++ b/hapi-fhir-server-mdm/pom.xml @@ -7,7 +7,7 @@ ca.uhn.hapi.fhir hapi-deployable-pom - 7.7.7-SNAPSHOT + 7.7.8-SNAPSHOT ../hapi-deployable-pom/pom.xml diff --git a/hapi-fhir-server-openapi/pom.xml b/hapi-fhir-server-openapi/pom.xml index d0671ee96a5..052fc447623 100644 --- a/hapi-fhir-server-openapi/pom.xml +++ b/hapi-fhir-server-openapi/pom.xml @@ -5,7 +5,7 @@ ca.uhn.hapi.fhir hapi-deployable-pom - 7.7.7-SNAPSHOT + 7.7.8-SNAPSHOT ../hapi-deployable-pom/pom.xml diff --git a/hapi-fhir-server/pom.xml b/hapi-fhir-server/pom.xml index cb990bf5cfa..642311c8ff5 100644 --- a/hapi-fhir-server/pom.xml +++ b/hapi-fhir-server/pom.xml @@ -5,7 +5,7 @@ ca.uhn.hapi.fhir hapi-deployable-pom - 7.7.7-SNAPSHOT + 7.7.8-SNAPSHOT ../hapi-deployable-pom/pom.xml diff --git a/hapi-fhir-server/src/main/java/ca/uhn/fhir/subscription/SubscriptionConstants.java b/hapi-fhir-server/src/main/java/ca/uhn/fhir/subscription/SubscriptionConstants.java index 56dabb1d52e..27a8cc0396d 100644 --- a/hapi-fhir-server/src/main/java/ca/uhn/fhir/subscription/SubscriptionConstants.java +++ b/hapi-fhir-server/src/main/java/ca/uhn/fhir/subscription/SubscriptionConstants.java @@ -58,4 +58,7 @@ public class SubscriptionConstants { "http://hl7.org/fhir/uv/subscriptions-backport/StructureDefinition/backport-payload-content"; public static final String SUBSCRIPTION_TOPIC_STATUS = "http://hl7.org/fhir/uv/subscriptions-backport/StructureDefinition/backport-subscription-status-r4"; + + public static final int ORDER_SUBSCRIPTION_VALIDATING = 100; + public static final int ORDER_SUBSCRIPTION_ACTIVATING = 200; } diff --git a/hapi-fhir-serviceloaders/hapi-fhir-caching-api/pom.xml b/hapi-fhir-serviceloaders/hapi-fhir-caching-api/pom.xml index acedc81e55a..334ada1b88a 100644 --- a/hapi-fhir-serviceloaders/hapi-fhir-caching-api/pom.xml +++ b/hapi-fhir-serviceloaders/hapi-fhir-caching-api/pom.xml @@ -7,7 +7,7 @@ hapi-fhir-serviceloaders ca.uhn.hapi.fhir - 7.7.7-SNAPSHOT + 7.7.8-SNAPSHOT ../pom.xml diff --git a/hapi-fhir-serviceloaders/hapi-fhir-caching-caffeine/pom.xml b/hapi-fhir-serviceloaders/hapi-fhir-caching-caffeine/pom.xml index e7f7d5b7212..55c95b5b01e 100644 --- a/hapi-fhir-serviceloaders/hapi-fhir-caching-caffeine/pom.xml +++ b/hapi-fhir-serviceloaders/hapi-fhir-caching-caffeine/pom.xml @@ -7,7 +7,7 @@ hapi-fhir-serviceloaders ca.uhn.hapi.fhir - 7.7.7-SNAPSHOT + 7.7.8-SNAPSHOT ../pom.xml @@ -21,7 +21,7 @@ ca.uhn.hapi.fhir hapi-fhir-caching-api - 7.7.7-SNAPSHOT + 7.7.8-SNAPSHOT diff --git a/hapi-fhir-serviceloaders/hapi-fhir-caching-guava/pom.xml b/hapi-fhir-serviceloaders/hapi-fhir-caching-guava/pom.xml index 79741373a8a..8bdee9dffc1 100644 --- a/hapi-fhir-serviceloaders/hapi-fhir-caching-guava/pom.xml +++ b/hapi-fhir-serviceloaders/hapi-fhir-caching-guava/pom.xml @@ -7,7 +7,7 @@ hapi-fhir-serviceloaders ca.uhn.hapi.fhir - 7.7.7-SNAPSHOT + 7.7.8-SNAPSHOT ../pom.xml diff --git a/hapi-fhir-serviceloaders/hapi-fhir-caching-testing/pom.xml b/hapi-fhir-serviceloaders/hapi-fhir-caching-testing/pom.xml index c54fc19604a..6a6f3b5a624 100644 --- a/hapi-fhir-serviceloaders/hapi-fhir-caching-testing/pom.xml +++ b/hapi-fhir-serviceloaders/hapi-fhir-caching-testing/pom.xml @@ -7,7 +7,7 @@ hapi-fhir ca.uhn.hapi.fhir - 7.7.7-SNAPSHOT + 7.7.8-SNAPSHOT ../../pom.xml diff --git a/hapi-fhir-serviceloaders/pom.xml b/hapi-fhir-serviceloaders/pom.xml index 314519bf752..4eaa08227ee 100644 --- a/hapi-fhir-serviceloaders/pom.xml +++ b/hapi-fhir-serviceloaders/pom.xml @@ -5,7 +5,7 @@ hapi-deployable-pom ca.uhn.hapi.fhir - 7.7.7-SNAPSHOT + 7.7.8-SNAPSHOT ../hapi-deployable-pom/pom.xml diff --git a/hapi-fhir-spring-boot/hapi-fhir-spring-boot-autoconfigure/pom.xml b/hapi-fhir-spring-boot/hapi-fhir-spring-boot-autoconfigure/pom.xml index 6afde2bfd71..9fb961a7de3 100644 --- a/hapi-fhir-spring-boot/hapi-fhir-spring-boot-autoconfigure/pom.xml +++ b/hapi-fhir-spring-boot/hapi-fhir-spring-boot-autoconfigure/pom.xml @@ -5,7 +5,7 @@ ca.uhn.hapi.fhir hapi-deployable-pom - 7.7.7-SNAPSHOT + 7.7.8-SNAPSHOT ../../hapi-deployable-pom/pom.xml diff --git a/hapi-fhir-spring-boot/hapi-fhir-spring-boot-samples/hapi-fhir-spring-boot-sample-client-apache/pom.xml b/hapi-fhir-spring-boot/hapi-fhir-spring-boot-samples/hapi-fhir-spring-boot-sample-client-apache/pom.xml index 018576bb564..c0404daee27 100644 --- a/hapi-fhir-spring-boot/hapi-fhir-spring-boot-samples/hapi-fhir-spring-boot-sample-client-apache/pom.xml +++ b/hapi-fhir-spring-boot/hapi-fhir-spring-boot-samples/hapi-fhir-spring-boot-sample-client-apache/pom.xml @@ -5,7 +5,7 @@ ca.uhn.hapi.fhir hapi-fhir-spring-boot-samples - 7.7.7-SNAPSHOT + 7.7.8-SNAPSHOT hapi-fhir-spring-boot-sample-client-apache diff --git a/hapi-fhir-spring-boot/hapi-fhir-spring-boot-samples/hapi-fhir-spring-boot-sample-client-okhttp/pom.xml b/hapi-fhir-spring-boot/hapi-fhir-spring-boot-samples/hapi-fhir-spring-boot-sample-client-okhttp/pom.xml index 59b7c769df0..a03857982c3 100644 --- a/hapi-fhir-spring-boot/hapi-fhir-spring-boot-samples/hapi-fhir-spring-boot-sample-client-okhttp/pom.xml +++ b/hapi-fhir-spring-boot/hapi-fhir-spring-boot-samples/hapi-fhir-spring-boot-sample-client-okhttp/pom.xml @@ -5,7 +5,7 @@ ca.uhn.hapi.fhir hapi-fhir-spring-boot-samples - 7.7.7-SNAPSHOT + 7.7.8-SNAPSHOT diff --git a/hapi-fhir-spring-boot/hapi-fhir-spring-boot-samples/hapi-fhir-spring-boot-sample-server-jersey/pom.xml b/hapi-fhir-spring-boot/hapi-fhir-spring-boot-samples/hapi-fhir-spring-boot-sample-server-jersey/pom.xml index 2ed7be33b1d..97caf2be8ae 100644 --- a/hapi-fhir-spring-boot/hapi-fhir-spring-boot-samples/hapi-fhir-spring-boot-sample-server-jersey/pom.xml +++ b/hapi-fhir-spring-boot/hapi-fhir-spring-boot-samples/hapi-fhir-spring-boot-sample-server-jersey/pom.xml @@ -5,7 +5,7 @@ ca.uhn.hapi.fhir hapi-fhir-spring-boot-samples - 7.7.7-SNAPSHOT + 7.7.8-SNAPSHOT diff --git a/hapi-fhir-spring-boot/hapi-fhir-spring-boot-samples/pom.xml b/hapi-fhir-spring-boot/hapi-fhir-spring-boot-samples/pom.xml index 02a7740e4c9..00431db20ca 100644 --- a/hapi-fhir-spring-boot/hapi-fhir-spring-boot-samples/pom.xml +++ b/hapi-fhir-spring-boot/hapi-fhir-spring-boot-samples/pom.xml @@ -5,7 +5,7 @@ ca.uhn.hapi.fhir hapi-fhir-spring-boot - 7.7.7-SNAPSHOT + 7.7.8-SNAPSHOT diff --git a/hapi-fhir-spring-boot/hapi-fhir-spring-boot-starter/pom.xml b/hapi-fhir-spring-boot/hapi-fhir-spring-boot-starter/pom.xml index ee4b04cf6e7..1cb18798c2a 100644 --- a/hapi-fhir-spring-boot/hapi-fhir-spring-boot-starter/pom.xml +++ b/hapi-fhir-spring-boot/hapi-fhir-spring-boot-starter/pom.xml @@ -5,7 +5,7 @@ ca.uhn.hapi.fhir hapi-deployable-pom - 7.7.7-SNAPSHOT + 7.7.8-SNAPSHOT ../../hapi-deployable-pom/pom.xml diff --git a/hapi-fhir-spring-boot/pom.xml b/hapi-fhir-spring-boot/pom.xml index 51d5c49944f..8485992ca06 100644 --- a/hapi-fhir-spring-boot/pom.xml +++ b/hapi-fhir-spring-boot/pom.xml @@ -5,7 +5,7 @@ ca.uhn.hapi.fhir hapi-fhir - 7.7.7-SNAPSHOT + 7.7.8-SNAPSHOT ../pom.xml diff --git a/hapi-fhir-sql-migrate/pom.xml b/hapi-fhir-sql-migrate/pom.xml index acca99e04de..9db01829031 100644 --- a/hapi-fhir-sql-migrate/pom.xml +++ b/hapi-fhir-sql-migrate/pom.xml @@ -5,7 +5,7 @@ ca.uhn.hapi.fhir hapi-deployable-pom - 7.7.7-SNAPSHOT + 7.7.8-SNAPSHOT ../hapi-deployable-pom/pom.xml diff --git a/hapi-fhir-storage-batch2-jobs/pom.xml b/hapi-fhir-storage-batch2-jobs/pom.xml index 5421f5e2793..61df69802a8 100644 --- a/hapi-fhir-storage-batch2-jobs/pom.xml +++ b/hapi-fhir-storage-batch2-jobs/pom.xml @@ -5,7 +5,7 @@ ca.uhn.hapi.fhir hapi-deployable-pom - 7.7.7-SNAPSHOT + 7.7.8-SNAPSHOT ../hapi-deployable-pom/pom.xml diff --git a/hapi-fhir-storage-batch2-test-utilities/pom.xml b/hapi-fhir-storage-batch2-test-utilities/pom.xml index 93b435fc026..1c4f8874056 100644 --- a/hapi-fhir-storage-batch2-test-utilities/pom.xml +++ b/hapi-fhir-storage-batch2-test-utilities/pom.xml @@ -7,7 +7,7 @@ ca.uhn.hapi.fhir hapi-deployable-pom - 7.7.7-SNAPSHOT + 7.7.8-SNAPSHOT ../hapi-deployable-pom/pom.xml diff --git a/hapi-fhir-storage-batch2/pom.xml b/hapi-fhir-storage-batch2/pom.xml index d662c258d6e..7c8c0512358 100644 --- a/hapi-fhir-storage-batch2/pom.xml +++ b/hapi-fhir-storage-batch2/pom.xml @@ -7,7 +7,7 @@ ca.uhn.hapi.fhir hapi-deployable-pom - 7.7.7-SNAPSHOT + 7.7.8-SNAPSHOT ../hapi-deployable-pom/pom.xml diff --git a/hapi-fhir-storage-cr/pom.xml b/hapi-fhir-storage-cr/pom.xml index 6f31b62b9ee..5f7d299e4a1 100644 --- a/hapi-fhir-storage-cr/pom.xml +++ b/hapi-fhir-storage-cr/pom.xml @@ -7,7 +7,7 @@ ca.uhn.hapi.fhir hapi-deployable-pom - 7.7.7-SNAPSHOT + 7.7.8-SNAPSHOT ../hapi-deployable-pom/pom.xml diff --git a/hapi-fhir-storage-mdm/pom.xml b/hapi-fhir-storage-mdm/pom.xml index 4cf64576707..5a1e19e54e9 100644 --- a/hapi-fhir-storage-mdm/pom.xml +++ b/hapi-fhir-storage-mdm/pom.xml @@ -5,7 +5,7 @@ ca.uhn.hapi.fhir hapi-deployable-pom - 7.7.7-SNAPSHOT + 7.7.8-SNAPSHOT ../hapi-deployable-pom/pom.xml diff --git a/hapi-fhir-storage-test-utilities/pom.xml b/hapi-fhir-storage-test-utilities/pom.xml index 305ca40e896..e76bfbc8bbd 100644 --- a/hapi-fhir-storage-test-utilities/pom.xml +++ b/hapi-fhir-storage-test-utilities/pom.xml @@ -5,7 +5,7 @@ ca.uhn.hapi.fhir hapi-deployable-pom - 7.7.7-SNAPSHOT + 7.7.8-SNAPSHOT ../hapi-deployable-pom/pom.xml diff --git a/hapi-fhir-storage/pom.xml b/hapi-fhir-storage/pom.xml index d90d02dfa75..804588a7555 100644 --- a/hapi-fhir-storage/pom.xml +++ b/hapi-fhir-storage/pom.xml @@ -5,7 +5,7 @@ ca.uhn.hapi.fhir hapi-deployable-pom - 7.7.7-SNAPSHOT + 7.7.8-SNAPSHOT ../hapi-deployable-pom/pom.xml diff --git a/hapi-fhir-storage/src/main/java/ca/uhn/fhir/jpa/util/BaseCaptureQueriesListener.java b/hapi-fhir-storage/src/main/java/ca/uhn/fhir/jpa/util/BaseCaptureQueriesListener.java index f2c8d92ec93..5d56284f450 100644 --- a/hapi-fhir-storage/src/main/java/ca/uhn/fhir/jpa/util/BaseCaptureQueriesListener.java +++ b/hapi-fhir-storage/src/main/java/ca/uhn/fhir/jpa/util/BaseCaptureQueriesListener.java @@ -27,6 +27,8 @@ import net.ttddyy.dsproxy.QueryInfo; import net.ttddyy.dsproxy.listener.MethodExecutionContext; import net.ttddyy.dsproxy.proxy.ParameterSetOperation; import net.ttddyy.dsproxy.support.ProxyDataSourceBuilder; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; import java.util.Collections; import java.util.List; @@ -38,7 +40,7 @@ import static org.apache.commons.lang3.StringUtils.trim; public abstract class BaseCaptureQueriesListener implements ProxyDataSourceBuilder.SingleQueryExecution, ProxyDataSourceBuilder.SingleMethodExecution { - + private static final Logger ourLog = LoggerFactory.getLogger(BaseCaptureQueriesListener.class); private boolean myCaptureQueryStackTrace = false; /** @@ -112,6 +114,9 @@ public abstract class BaseCaptureQueriesListener @Nullable protected abstract AtomicInteger provideCommitCounter(); + @Nullable + protected abstract AtomicInteger provideGetConnectionCounter(); + @Nullable protected abstract AtomicInteger provideRollbackCounter(); @@ -125,6 +130,9 @@ public abstract class BaseCaptureQueriesListener case "rollback": counter = provideRollbackCounter(); break; + case "getConnection": + counter = provideGetConnectionCounter(); + break; } if (counter != null) { @@ -132,10 +140,23 @@ public abstract class BaseCaptureQueriesListener } } + /** + * @return Returns the number of times the connection pool was asked for a new connection + */ + public int countGetConnections() { + return provideGetConnectionCounter().get(); + } + + /** + * @return Returns the number of DB commits which have happened against connections from the pool + */ public int countCommits() { return provideCommitCounter().get(); } + /** + * @return Returns the number of DB rollbacks which have happened against connections from the pool + */ public int countRollbacks() { return provideRollbackCounter().get(); } diff --git a/hapi-fhir-storage/src/main/java/ca/uhn/fhir/jpa/util/CircularQueueCaptureQueriesListener.java b/hapi-fhir-storage/src/main/java/ca/uhn/fhir/jpa/util/CircularQueueCaptureQueriesListener.java index 4739b26afde..5714b97fd04 100644 --- a/hapi-fhir-storage/src/main/java/ca/uhn/fhir/jpa/util/CircularQueueCaptureQueriesListener.java +++ b/hapi-fhir-storage/src/main/java/ca/uhn/fhir/jpa/util/CircularQueueCaptureQueriesListener.java @@ -22,6 +22,7 @@ package ca.uhn.fhir.jpa.util; import ca.uhn.fhir.util.StopWatch; import com.google.common.collect.Queues; import jakarta.annotation.Nonnull; +import jakarta.annotation.Nullable; import net.ttddyy.dsproxy.support.ProxyDataSourceBuilder; import org.apache.commons.collections4.queue.CircularFifoQueue; import org.hl7.fhir.r4.model.InstantType; @@ -58,6 +59,7 @@ public class CircularQueueCaptureQueriesListener extends BaseCaptureQueriesListe private static final int CAPACITY = 10000; private static final Logger ourLog = LoggerFactory.getLogger(CircularQueueCaptureQueriesListener.class); private Queue myQueries; + private AtomicInteger myGetConnectionCounter; private AtomicInteger myCommitCounter; private AtomicInteger myRollbackCounter; @@ -91,6 +93,12 @@ public class CircularQueueCaptureQueriesListener extends BaseCaptureQueriesListe return myCommitCounter; } + @Nullable + @Override + protected AtomicInteger provideGetConnectionCounter() { + return myGetConnectionCounter; + } + @Override protected AtomicInteger provideRollbackCounter() { return myRollbackCounter; @@ -101,6 +109,7 @@ public class CircularQueueCaptureQueriesListener extends BaseCaptureQueriesListe */ public void clear() { myQueries.clear(); + myGetConnectionCounter.set(0); myCommitCounter.set(0); myRollbackCounter.set(0); } @@ -110,6 +119,7 @@ public class CircularQueueCaptureQueriesListener extends BaseCaptureQueriesListe */ public void startCollecting() { myQueries = Queues.synchronizedQueue(new CircularFifoQueue<>(CAPACITY)); + myGetConnectionCounter = new AtomicInteger(0); myCommitCounter = new AtomicInteger(0); myRollbackCounter = new AtomicInteger(0); } @@ -167,10 +177,18 @@ public class CircularQueueCaptureQueriesListener extends BaseCaptureQueriesListe return getQueriesMatching(thePredicate, threadName); } + /** + * @deprecated Use {@link #countCommits()} + */ + @Deprecated public int getCommitCount() { return myCommitCounter.get(); } + /** + * @deprecated Use {@link #countRollbacks()} + */ + @Deprecated public int getRollbackCount() { return myRollbackCounter.get(); } diff --git a/hapi-fhir-storage/src/main/java/ca/uhn/fhir/jpa/util/CurrentThreadCaptureQueriesListener.java b/hapi-fhir-storage/src/main/java/ca/uhn/fhir/jpa/util/CurrentThreadCaptureQueriesListener.java index a9a211b009f..391ba1be456 100644 --- a/hapi-fhir-storage/src/main/java/ca/uhn/fhir/jpa/util/CurrentThreadCaptureQueriesListener.java +++ b/hapi-fhir-storage/src/main/java/ca/uhn/fhir/jpa/util/CurrentThreadCaptureQueriesListener.java @@ -19,6 +19,7 @@ */ package ca.uhn.fhir.jpa.util; +import jakarta.annotation.Nullable; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -32,6 +33,7 @@ import java.util.stream.Collectors; public class CurrentThreadCaptureQueriesListener extends BaseCaptureQueriesListener { private static final ThreadLocal> ourQueues = new ThreadLocal<>(); + private static final ThreadLocal ourGetConnections = new ThreadLocal<>(); private static final ThreadLocal ourCommits = new ThreadLocal<>(); private static final ThreadLocal ourRollbacks = new ThreadLocal<>(); private static final Logger ourLog = LoggerFactory.getLogger(CurrentThreadCaptureQueriesListener.class); @@ -46,6 +48,12 @@ public class CurrentThreadCaptureQueriesListener extends BaseCaptureQueriesListe return ourCommits.get(); } + @Nullable + @Override + protected AtomicInteger provideGetConnectionCounter() { + return ourGetConnections.get(); + } + @Override protected AtomicInteger provideRollbackCounter() { return ourRollbacks.get(); @@ -57,6 +65,7 @@ public class CurrentThreadCaptureQueriesListener extends BaseCaptureQueriesListe public static SqlQueryList getCurrentQueueAndStopCapturing() { Queue retVal = ourQueues.get(); ourQueues.remove(); + ourGetConnections.remove(); ourCommits.remove(); ourRollbacks.remove(); if (retVal == null) { @@ -76,6 +85,7 @@ public class CurrentThreadCaptureQueriesListener extends BaseCaptureQueriesListe */ public static void startCapturing() { ourQueues.set(new ArrayDeque<>()); + ourGetConnections.set(new AtomicInteger(0)); ourCommits.set(new AtomicInteger(0)); ourRollbacks.set(new AtomicInteger(0)); } diff --git a/hapi-fhir-storage/src/main/java/ca/uhn/fhir/jpa/validation/ValidatorResourceFetcher.java b/hapi-fhir-storage/src/main/java/ca/uhn/fhir/jpa/validation/ValidatorResourceFetcher.java index 14813f8b394..db777db0c33 100644 --- a/hapi-fhir-storage/src/main/java/ca/uhn/fhir/jpa/validation/ValidatorResourceFetcher.java +++ b/hapi-fhir-storage/src/main/java/ca/uhn/fhir/jpa/validation/ValidatorResourceFetcher.java @@ -20,6 +20,7 @@ package ca.uhn.fhir.jpa.validation; import ca.uhn.fhir.context.FhirContext; +import ca.uhn.fhir.context.RuntimeResourceDefinition; import ca.uhn.fhir.context.support.IValidationSupport; import ca.uhn.fhir.i18n.Msg; import ca.uhn.fhir.jpa.api.dao.DaoRegistry; @@ -75,10 +76,16 @@ public class ValidatorResourceFetcher implements IValidatorResourceFetcher { target = dao.read(id, (RequestDetails) appContext); } catch (ResourceNotFoundException e) { ourLog.info("Failed to resolve local reference: {}", theUrl); - try { - target = fetchByUrl(theUrl, dao, (RequestDetails) appContext); - } catch (ResourceNotFoundException e2) { - ourLog.info("Failed to find resource by URL: {}", theUrl); + + RuntimeResourceDefinition def = myFhirContext.getResourceDefinition(resourceType); + if (def.getChildByName("url") != null) { + try { + target = fetchByUrl(theUrl, dao, (RequestDetails) appContext); + } catch (ResourceNotFoundException e2) { + ourLog.info("Failed to find resource by URL: {}", theUrl); + return null; + } + } else { return null; } } @@ -86,7 +93,7 @@ public class ValidatorResourceFetcher implements IValidatorResourceFetcher { return new JsonParser(myVersionSpecificContextWrapper) .parse(myFhirContext.newJsonParser().encodeResourceToString(target), resourceType); } catch (Exception e) { - throw new FHIRException(Msg.code(576) + e); + throw new FHIRException(Msg.code(576) + e, e); } } diff --git a/hapi-fhir-structures-dstu2.1/pom.xml b/hapi-fhir-structures-dstu2.1/pom.xml index d8d55d0d193..c14dc3a7001 100644 --- a/hapi-fhir-structures-dstu2.1/pom.xml +++ b/hapi-fhir-structures-dstu2.1/pom.xml @@ -5,7 +5,7 @@ ca.uhn.hapi.fhir hapi-deployable-pom - 7.7.7-SNAPSHOT + 7.7.8-SNAPSHOT ../hapi-deployable-pom/pom.xml diff --git a/hapi-fhir-structures-dstu2/pom.xml b/hapi-fhir-structures-dstu2/pom.xml index 12ef01f1c22..7cc77115a18 100644 --- a/hapi-fhir-structures-dstu2/pom.xml +++ b/hapi-fhir-structures-dstu2/pom.xml @@ -4,7 +4,7 @@ ca.uhn.hapi.fhir hapi-deployable-pom - 7.7.7-SNAPSHOT + 7.7.8-SNAPSHOT ../hapi-deployable-pom/pom.xml diff --git a/hapi-fhir-structures-dstu3/pom.xml b/hapi-fhir-structures-dstu3/pom.xml index eb06ff89d41..6d1abc83f12 100644 --- a/hapi-fhir-structures-dstu3/pom.xml +++ b/hapi-fhir-structures-dstu3/pom.xml @@ -5,7 +5,7 @@ ca.uhn.hapi.fhir hapi-deployable-pom - 7.7.7-SNAPSHOT + 7.7.8-SNAPSHOT ../hapi-deployable-pom/pom.xml diff --git a/hapi-fhir-structures-hl7org-dstu2/pom.xml b/hapi-fhir-structures-hl7org-dstu2/pom.xml index adc0100a995..2d678f794fc 100644 --- a/hapi-fhir-structures-hl7org-dstu2/pom.xml +++ b/hapi-fhir-structures-hl7org-dstu2/pom.xml @@ -5,7 +5,7 @@ ca.uhn.hapi.fhir hapi-deployable-pom - 7.7.7-SNAPSHOT + 7.7.8-SNAPSHOT ../hapi-deployable-pom/pom.xml diff --git a/hapi-fhir-structures-r4/pom.xml b/hapi-fhir-structures-r4/pom.xml index b862ed06928..81332f0b285 100644 --- a/hapi-fhir-structures-r4/pom.xml +++ b/hapi-fhir-structures-r4/pom.xml @@ -5,7 +5,7 @@ ca.uhn.hapi.fhir hapi-deployable-pom - 7.7.7-SNAPSHOT + 7.7.8-SNAPSHOT ../hapi-deployable-pom/pom.xml diff --git a/hapi-fhir-structures-r4b/pom.xml b/hapi-fhir-structures-r4b/pom.xml index 0ecd3520d9b..e9bb68a0ced 100644 --- a/hapi-fhir-structures-r4b/pom.xml +++ b/hapi-fhir-structures-r4b/pom.xml @@ -5,7 +5,7 @@ ca.uhn.hapi.fhir hapi-deployable-pom - 7.7.7-SNAPSHOT + 7.7.8-SNAPSHOT ../hapi-deployable-pom/pom.xml diff --git a/hapi-fhir-structures-r5/pom.xml b/hapi-fhir-structures-r5/pom.xml index fb636ac3592..20fc936b45e 100644 --- a/hapi-fhir-structures-r5/pom.xml +++ b/hapi-fhir-structures-r5/pom.xml @@ -5,7 +5,7 @@ ca.uhn.hapi.fhir hapi-deployable-pom - 7.7.7-SNAPSHOT + 7.7.8-SNAPSHOT ../hapi-deployable-pom/pom.xml diff --git a/hapi-fhir-test-utilities/pom.xml b/hapi-fhir-test-utilities/pom.xml index 21ac27a7bfa..8f3810005ca 100644 --- a/hapi-fhir-test-utilities/pom.xml +++ b/hapi-fhir-test-utilities/pom.xml @@ -5,7 +5,7 @@ ca.uhn.hapi.fhir hapi-deployable-pom - 7.7.7-SNAPSHOT + 7.7.8-SNAPSHOT ../hapi-deployable-pom/pom.xml diff --git a/hapi-fhir-testpage-overlay/pom.xml b/hapi-fhir-testpage-overlay/pom.xml index 7cca45e4626..b726d4f06b9 100644 --- a/hapi-fhir-testpage-overlay/pom.xml +++ b/hapi-fhir-testpage-overlay/pom.xml @@ -5,7 +5,7 @@ ca.uhn.hapi.fhir hapi-fhir - 7.7.7-SNAPSHOT + 7.7.8-SNAPSHOT ../pom.xml diff --git a/hapi-fhir-validation-resources-dstu2.1/pom.xml b/hapi-fhir-validation-resources-dstu2.1/pom.xml index c96a4fa6d72..771c5dfce90 100644 --- a/hapi-fhir-validation-resources-dstu2.1/pom.xml +++ b/hapi-fhir-validation-resources-dstu2.1/pom.xml @@ -4,7 +4,7 @@ ca.uhn.hapi.fhir hapi-deployable-pom - 7.7.7-SNAPSHOT + 7.7.8-SNAPSHOT ../hapi-deployable-pom/pom.xml diff --git a/hapi-fhir-validation-resources-dstu2/pom.xml b/hapi-fhir-validation-resources-dstu2/pom.xml index 9e1cb753cc3..97d409315ea 100644 --- a/hapi-fhir-validation-resources-dstu2/pom.xml +++ b/hapi-fhir-validation-resources-dstu2/pom.xml @@ -4,7 +4,7 @@ ca.uhn.hapi.fhir hapi-deployable-pom - 7.7.7-SNAPSHOT + 7.7.8-SNAPSHOT ../hapi-deployable-pom/pom.xml diff --git a/hapi-fhir-validation-resources-dstu3/pom.xml b/hapi-fhir-validation-resources-dstu3/pom.xml index 9aa79b6c5bd..a049c6e0326 100644 --- a/hapi-fhir-validation-resources-dstu3/pom.xml +++ b/hapi-fhir-validation-resources-dstu3/pom.xml @@ -4,7 +4,7 @@ ca.uhn.hapi.fhir hapi-deployable-pom - 7.7.7-SNAPSHOT + 7.7.8-SNAPSHOT ../hapi-deployable-pom/pom.xml diff --git a/hapi-fhir-validation-resources-r4/pom.xml b/hapi-fhir-validation-resources-r4/pom.xml index 65bc10b13f4..8758c8a3f27 100644 --- a/hapi-fhir-validation-resources-r4/pom.xml +++ b/hapi-fhir-validation-resources-r4/pom.xml @@ -4,7 +4,7 @@ ca.uhn.hapi.fhir hapi-deployable-pom - 7.7.7-SNAPSHOT + 7.7.8-SNAPSHOT ../hapi-deployable-pom/pom.xml diff --git a/hapi-fhir-validation-resources-r4b/pom.xml b/hapi-fhir-validation-resources-r4b/pom.xml index 69417a1d0eb..70968ba423b 100644 --- a/hapi-fhir-validation-resources-r4b/pom.xml +++ b/hapi-fhir-validation-resources-r4b/pom.xml @@ -4,7 +4,7 @@ ca.uhn.hapi.fhir hapi-deployable-pom - 7.7.7-SNAPSHOT + 7.7.8-SNAPSHOT ../hapi-deployable-pom/pom.xml diff --git a/hapi-fhir-validation-resources-r5/pom.xml b/hapi-fhir-validation-resources-r5/pom.xml index e0a2ac82d76..a08bea7582d 100644 --- a/hapi-fhir-validation-resources-r5/pom.xml +++ b/hapi-fhir-validation-resources-r5/pom.xml @@ -4,7 +4,7 @@ ca.uhn.hapi.fhir hapi-deployable-pom - 7.7.7-SNAPSHOT + 7.7.8-SNAPSHOT ../hapi-deployable-pom/pom.xml diff --git a/hapi-fhir-validation/pom.xml b/hapi-fhir-validation/pom.xml index a4067215f26..7991424e435 100644 --- a/hapi-fhir-validation/pom.xml +++ b/hapi-fhir-validation/pom.xml @@ -5,7 +5,7 @@ ca.uhn.hapi.fhir hapi-deployable-pom - 7.7.7-SNAPSHOT + 7.7.8-SNAPSHOT ../hapi-deployable-pom/pom.xml @@ -256,6 +256,16 @@ ${project.version} test + + io.opentelemetry.javaagent + opentelemetry-testing-common + test + + + io.opentelemetry.javaagent + opentelemetry-agent-for-testing + test + commons-lang diff --git a/hapi-fhir-validation/src/main/java/org/hl7/fhir/common/hapi/validation/support/CachingValidationSupport.java b/hapi-fhir-validation/src/main/java/org/hl7/fhir/common/hapi/validation/support/CachingValidationSupport.java index 7edb7effd39..d62b19d2ecc 100644 --- a/hapi-fhir-validation/src/main/java/org/hl7/fhir/common/hapi/validation/support/CachingValidationSupport.java +++ b/hapi-fhir-validation/src/main/java/org/hl7/fhir/common/hapi/validation/support/CachingValidationSupport.java @@ -1,51 +1,14 @@ package org.hl7.fhir.common.hapi.validation.support; -import ca.uhn.fhir.context.BaseRuntimeChildDefinition; -import ca.uhn.fhir.context.support.ConceptValidationOptions; import ca.uhn.fhir.context.support.IValidationSupport; -import ca.uhn.fhir.context.support.LookupCodeRequest; -import ca.uhn.fhir.context.support.TranslateConceptResults; -import ca.uhn.fhir.context.support.ValidationSupportContext; -import ca.uhn.fhir.context.support.ValueSetExpansionOptions; -import ca.uhn.fhir.sl.cache.Cache; -import ca.uhn.fhir.sl.cache.CacheFactory; -import jakarta.annotation.Nonnull; -import jakarta.annotation.Nullable; -import org.apache.commons.lang3.concurrent.BasicThreadFactory; import org.apache.commons.lang3.time.DateUtils; -import org.hl7.fhir.instance.model.api.IBaseResource; -import org.hl7.fhir.instance.model.api.IPrimitiveType; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; -import java.util.Collections; -import java.util.HashMap; -import java.util.List; -import java.util.Map; -import java.util.Optional; -import java.util.concurrent.LinkedBlockingQueue; -import java.util.concurrent.ThreadPoolExecutor; -import java.util.concurrent.TimeUnit; -import java.util.function.Function; - -import static org.apache.commons.lang3.ObjectUtils.defaultIfNull; -import static org.apache.commons.lang3.StringUtils.defaultIfBlank; -import static org.apache.commons.lang3.StringUtils.defaultString; -import static org.apache.commons.lang3.StringUtils.isNotBlank; - -@SuppressWarnings("unchecked") +/** + * @deprecated This should no longer be used, caching functionality is provided by {@link ValidationSupportChain} + */ +@Deprecated(since = "8.0.0", forRemoval = true) public class CachingValidationSupport extends BaseValidationSupportWrapper implements IValidationSupport { - private static final Logger ourLog = LoggerFactory.getLogger(CachingValidationSupport.class); - public static final ValueSetExpansionOptions EMPTY_EXPANSION_OPTIONS = new ValueSetExpansionOptions(); - - private final Cache myCache; - private final Cache myValidateCodeCache; - private final Cache myTranslateCodeCache; - private final Cache myLookupCodeCache; - private final ThreadPoolExecutor myBackgroundExecutor; - private final Map myNonExpiringCache; - private final Cache myExpandValueSetCache; private final boolean myIsEnabledValidationForCodingsLogicalAnd; /** @@ -76,312 +39,53 @@ public class CachingValidationSupport extends BaseValidationSupportWrapper imple CacheTimeouts theCacheTimeouts, boolean theIsEnabledValidationForCodingsLogicalAnd) { super(theWrap.getFhirContext(), theWrap); - myExpandValueSetCache = CacheFactory.build(theCacheTimeouts.getExpandValueSetMillis(), 100); - myValidateCodeCache = CacheFactory.build(theCacheTimeouts.getValidateCodeMillis(), 5000); - myLookupCodeCache = CacheFactory.build(theCacheTimeouts.getLookupCodeMillis(), 5000); - myTranslateCodeCache = CacheFactory.build(theCacheTimeouts.getTranslateCodeMillis(), 5000); - myCache = CacheFactory.build(theCacheTimeouts.getMiscMillis(), 5000); - myNonExpiringCache = Collections.synchronizedMap(new HashMap<>()); - - LinkedBlockingQueue executorQueue = new LinkedBlockingQueue<>(1000); - BasicThreadFactory threadFactory = new BasicThreadFactory.Builder() - .namingPattern("CachingValidationSupport-%d") - .daemon(false) - .priority(Thread.NORM_PRIORITY) - .build(); - myBackgroundExecutor = new ThreadPoolExecutor( - 1, 1, 0L, TimeUnit.MILLISECONDS, executorQueue, threadFactory, new ThreadPoolExecutor.DiscardPolicy()); - myIsEnabledValidationForCodingsLogicalAnd = theIsEnabledValidationForCodingsLogicalAnd; } - @Override - public List fetchAllConformanceResources() { - String key = "fetchAllConformanceResources"; - return loadFromCacheWithAsyncRefresh(myCache, key, t -> super.fetchAllConformanceResources()); - } - - @Override - public List fetchAllStructureDefinitions() { - String key = "fetchAllStructureDefinitions"; - return loadFromCacheWithAsyncRefresh(myCache, key, t -> super.fetchAllStructureDefinitions()); - } - - @Nullable - @Override - public List fetchAllSearchParameters() { - String key = "fetchAllSearchParameters"; - return loadFromCacheWithAsyncRefresh(myCache, key, t -> super.fetchAllSearchParameters()); - } - - @Override - public List fetchAllNonBaseStructureDefinitions() { - String key = "fetchAllNonBaseStructureDefinitions"; - return loadFromCacheWithAsyncRefresh(myCache, key, t -> super.fetchAllNonBaseStructureDefinitions()); - } - - @Override - public IBaseResource fetchCodeSystem(String theSystem) { - return loadFromCache(myCache, "fetchCodeSystem " + theSystem, t -> super.fetchCodeSystem(theSystem)); - } - - @Override - public IBaseResource fetchValueSet(String theUri) { - return loadFromCache(myCache, "fetchValueSet " + theUri, t -> super.fetchValueSet(theUri)); - } - - @Override - public IBaseResource fetchStructureDefinition(String theUrl) { - return loadFromCache( - myCache, "fetchStructureDefinition " + theUrl, t -> super.fetchStructureDefinition(theUrl)); - } - - @Override - public byte[] fetchBinary(String theBinaryKey) { - return loadFromCache(myCache, "fetchBinary " + theBinaryKey, t -> super.fetchBinary(theBinaryKey)); - } - - @Override - public T fetchResource(@Nullable Class theClass, String theUri) { - return loadFromCache( - myCache, "fetchResource " + theClass + " " + theUri, t -> super.fetchResource(theClass, theUri)); - } - - @Override - public boolean isCodeSystemSupported(ValidationSupportContext theValidationSupportContext, String theSystem) { - String key = "isCodeSystemSupported " + theSystem; - Boolean retVal = loadFromCacheReentrantSafe( - myCache, key, t -> super.isCodeSystemSupported(theValidationSupportContext, theSystem)); - assert retVal != null; - return retVal; - } - - @Override - public ValueSetExpansionOutcome expandValueSet( - ValidationSupportContext theValidationSupportContext, - ValueSetExpansionOptions theExpansionOptions, - @Nonnull IBaseResource theValueSetToExpand) { - if (!theValueSetToExpand.getIdElement().hasIdPart()) { - return super.expandValueSet(theValidationSupportContext, theExpansionOptions, theValueSetToExpand); - } - - ValueSetExpansionOptions expansionOptions = defaultIfNull(theExpansionOptions, EMPTY_EXPANSION_OPTIONS); - String key = "expandValueSet " + theValueSetToExpand.getIdElement().getValue() - + " " + expansionOptions.isIncludeHierarchy() - + " " + expansionOptions.getFilter() - + " " + expansionOptions.getOffset() - + " " + expansionOptions.getCount(); - return loadFromCache( - myExpandValueSetCache, - key, - t -> super.expandValueSet(theValidationSupportContext, theExpansionOptions, theValueSetToExpand)); - } - - @Override - public CodeValidationResult validateCode( - @Nonnull ValidationSupportContext theValidationSupportContext, - @Nonnull ConceptValidationOptions theOptions, - String theCodeSystem, - String theCode, - String theDisplay, - String theValueSetUrl) { - String key = "validateCode " + theCodeSystem + " " + theCode + " " + defaultString(theDisplay) + " " - + defaultIfBlank(theValueSetUrl, "NO_VS"); - return loadFromCache( - myValidateCodeCache, - key, - t -> super.validateCode( - theValidationSupportContext, theOptions, theCodeSystem, theCode, theDisplay, theValueSetUrl)); - } - - @Override - public LookupCodeResult lookupCode( - ValidationSupportContext theValidationSupportContext, @Nonnull LookupCodeRequest theLookupCodeRequest) { - String key = "lookupCode " + theLookupCodeRequest.getSystem() + " " - + theLookupCodeRequest.getCode() - + " " + defaultIfBlank(theLookupCodeRequest.getDisplayLanguage(), "NO_LANG") - + " " + theLookupCodeRequest.getPropertyNames().toString(); - return loadFromCache( - myLookupCodeCache, key, t -> super.lookupCode(theValidationSupportContext, theLookupCodeRequest)); - } - - @Override - public IValidationSupport.CodeValidationResult validateCodeInValueSet( - ValidationSupportContext theValidationSupportContext, - ConceptValidationOptions theValidationOptions, - String theCodeSystem, - String theCode, - String theDisplay, - @Nonnull IBaseResource theValueSet) { - - BaseRuntimeChildDefinition urlChild = - myCtx.getResourceDefinition(theValueSet).getChildByName("url"); - Optional valueSetUrl = urlChild.getAccessor().getValues(theValueSet).stream() - .map(t -> ((IPrimitiveType) t).getValueAsString()) - .filter(t -> isNotBlank(t)) - .findFirst(); - if (valueSetUrl.isPresent()) { - String key = - "validateCodeInValueSet " + theValidationOptions.toString() + " " + defaultString(theCodeSystem) - + " " + defaultString(theCode) + " " + defaultString(theDisplay) + " " + valueSetUrl.get(); - return loadFromCache( - myValidateCodeCache, - key, - t -> super.validateCodeInValueSet( - theValidationSupportContext, - theValidationOptions, - theCodeSystem, - theCode, - theDisplay, - theValueSet)); - } - - return super.validateCodeInValueSet( - theValidationSupportContext, theValidationOptions, theCodeSystem, theCode, theDisplay, theValueSet); - } - - @Override - public TranslateConceptResults translateConcept(TranslateCodeRequest theRequest) { - return loadFromCache(myTranslateCodeCache, theRequest, k -> super.translateConcept(theRequest)); - } - - @SuppressWarnings("OptionalAssignedToNull") - @Nullable - private T loadFromCache(Cache theCache, S theKey, Function theLoader) { - ourLog.trace("Fetching from cache: {}", theKey); - - Function> loaderWrapper = key -> Optional.ofNullable(theLoader.apply(theKey)); - Optional result = (Optional) theCache.get(theKey, loaderWrapper); - assert result != null; - - // UGH! Animal sniffer :( - if (!result.isPresent()) { - ourLog.debug( - "Invalidating cache entry for key: {} since the result of the underlying query is empty", theKey); - theCache.invalidate(theKey); - } - - return result.orElse(null); - } - - /** - * The Caffeine cache uses ConcurrentHashMap which is not reentrant, so if we get unlucky and the hashtable - * needs to grow at the same time as we are in a reentrant cache lookup, the thread will deadlock. Use this - * method in place of loadFromCache in situations where a cache lookup calls another cache lookup within its lambda - */ - @Nullable - private T loadFromCacheReentrantSafe(Cache theCache, S theKey, Function theLoader) { - ourLog.trace("Reentrant fetch from cache: {}", theKey); - - Optional result = (Optional) theCache.getIfPresent(theKey); - if (result != null && result.isPresent()) { - return result.get(); - } - T value = theLoader.apply(theKey); - assert value != null; - - theCache.put(theKey, Optional.of(value)); - - return value; - } - - private T loadFromCacheWithAsyncRefresh(Cache theCache, S theKey, Function theLoader) { - T retVal = (T) theCache.getIfPresent(theKey); - if (retVal == null) { - retVal = (T) myNonExpiringCache.get(theKey); - if (retVal != null) { - - Runnable loaderTask = () -> { - T loadedItem = loadFromCache(theCache, theKey, theLoader); - myNonExpiringCache.put(theKey, loadedItem); - }; - myBackgroundExecutor.execute(loaderTask); - - return retVal; - } - } - - retVal = loadFromCache(theCache, theKey, theLoader); - myNonExpiringCache.put(theKey, retVal); - return retVal; - } - - @Override - public void invalidateCaches() { - myExpandValueSetCache.invalidateAll(); - myLookupCodeCache.invalidateAll(); - myCache.invalidateAll(); - myValidateCodeCache.invalidateAll(); - myNonExpiringCache.clear(); - } - /** * @since 5.4.0 + * @deprecated */ + @Deprecated public static class CacheTimeouts { - private long myTranslateCodeMillis; - private long myLookupCodeMillis; - private long myValidateCodeMillis; - private long myMiscMillis; - private long myExpandValueSetMillis; - - public long getExpandValueSetMillis() { - return myExpandValueSetMillis; - } - public CacheTimeouts setExpandValueSetMillis(long theExpandValueSetMillis) { - myExpandValueSetMillis = theExpandValueSetMillis; + // nothing return this; } - public long getTranslateCodeMillis() { - return myTranslateCodeMillis; - } - public CacheTimeouts setTranslateCodeMillis(long theTranslateCodeMillis) { - myTranslateCodeMillis = theTranslateCodeMillis; + // nothing return this; } - public long getLookupCodeMillis() { - return myLookupCodeMillis; - } - public CacheTimeouts setLookupCodeMillis(long theLookupCodeMillis) { - myLookupCodeMillis = theLookupCodeMillis; + // nothibng return this; } - public long getValidateCodeMillis() { - return myValidateCodeMillis; - } - public CacheTimeouts setValidateCodeMillis(long theValidateCodeMillis) { - myValidateCodeMillis = theValidateCodeMillis; + // nothing return this; } - public long getMiscMillis() { - return myMiscMillis; - } - public CacheTimeouts setMiscMillis(long theMiscMillis) { - myMiscMillis = theMiscMillis; + // nothing return this; } public static CacheTimeouts defaultValues() { return new CacheTimeouts() .setLookupCodeMillis(10 * DateUtils.MILLIS_PER_MINUTE) - .setExpandValueSetMillis(1 * DateUtils.MILLIS_PER_MINUTE) + .setExpandValueSetMillis(DateUtils.MILLIS_PER_MINUTE) .setTranslateCodeMillis(10 * DateUtils.MILLIS_PER_MINUTE) .setValidateCodeMillis(10 * DateUtils.MILLIS_PER_MINUTE) .setMiscMillis(10 * DateUtils.MILLIS_PER_MINUTE); } } - public boolean isEnabledValidationForCodingsLogicalAnd() { + @Override + public boolean isCodeableConceptValidationSuccessfulIfNotAllCodingsAreValid() { return myIsEnabledValidationForCodingsLogicalAnd; } } diff --git a/hapi-fhir-validation/src/main/java/org/hl7/fhir/common/hapi/validation/support/SnapshotGeneratingValidationSupport.java b/hapi-fhir-validation/src/main/java/org/hl7/fhir/common/hapi/validation/support/SnapshotGeneratingValidationSupport.java index a4cf3642b0a..1b4e3ccb2f7 100644 --- a/hapi-fhir-validation/src/main/java/org/hl7/fhir/common/hapi/validation/support/SnapshotGeneratingValidationSupport.java +++ b/hapi-fhir-validation/src/main/java/org/hl7/fhir/common/hapi/validation/support/SnapshotGeneratingValidationSupport.java @@ -16,6 +16,7 @@ import org.hl7.fhir.instance.model.api.IBaseResource; import org.hl7.fhir.r5.conformance.profile.ProfileKnowledgeProvider; import org.hl7.fhir.r5.conformance.profile.ProfileUtilities; import org.hl7.fhir.r5.context.IWorkerContext; +import org.hl7.fhir.r5.fhirpath.FHIRPathEngine; import org.hl7.fhir.utilities.validation.ValidationMessage; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -39,14 +40,23 @@ public class SnapshotGeneratingValidationSupport implements IValidationSupport { private static final Logger ourLog = LoggerFactory.getLogger(SnapshotGeneratingValidationSupport.class); private final FhirContext myCtx; private final VersionCanonicalizer myVersionCanonicalizer; + private final IWorkerContext myWorkerContext; + private final FHIRPathEngine myFHIRPathEngine; /** * Constructor */ - public SnapshotGeneratingValidationSupport(FhirContext theCtx) { - Validate.notNull(theCtx); - myCtx = theCtx; - myVersionCanonicalizer = new VersionCanonicalizer(theCtx); + public SnapshotGeneratingValidationSupport(FhirContext theFhirContext) { + this(theFhirContext, null, null); + } + + public SnapshotGeneratingValidationSupport( + FhirContext theFhirContext, IWorkerContext theWorkerContext, FHIRPathEngine theFHIRPathEngine) { + Validate.notNull(theFhirContext); + myCtx = theFhirContext; + myVersionCanonicalizer = new VersionCanonicalizer(theFhirContext); + myWorkerContext = theWorkerContext; + myFHIRPathEngine = theFHIRPathEngine; } @SuppressWarnings("EnhancedSwitchMigration") @@ -97,9 +107,17 @@ public class SnapshotGeneratingValidationSupport implements IValidationSupport { ArrayList messages = new ArrayList<>(); ProfileKnowledgeProvider profileKnowledgeProvider = new ProfileKnowledgeWorkerR5(myCtx); - IWorkerContext context = - new VersionSpecificWorkerContextWrapper(theValidationSupportContext, myVersionCanonicalizer); - ProfileUtilities profileUtilities = new ProfileUtilities(context, messages, profileKnowledgeProvider); + + ProfileUtilities profileUtilities; + if (myWorkerContext == null) { + IWorkerContext context = + new VersionSpecificWorkerContextWrapper(theValidationSupportContext, myVersionCanonicalizer); + profileUtilities = new ProfileUtilities(context, messages, profileKnowledgeProvider); + } else { + profileUtilities = + new ProfileUtilities(myWorkerContext, messages, profileKnowledgeProvider, myFHIRPathEngine); + } + profileUtilities.generateSnapshot(baseCanonical, inputCanonical, theUrl, theWebUrl, theProfileName); switch (getFhirVersionEnum( diff --git a/hapi-fhir-validation/src/main/java/org/hl7/fhir/common/hapi/validation/support/ValidationSupportChain.java b/hapi-fhir-validation/src/main/java/org/hl7/fhir/common/hapi/validation/support/ValidationSupportChain.java index 4eea50efc11..16b3dadb3da 100644 --- a/hapi-fhir-validation/src/main/java/org/hl7/fhir/common/hapi/validation/support/ValidationSupportChain.java +++ b/hapi-fhir-validation/src/main/java/org/hl7/fhir/common/hapi/validation/support/ValidationSupportChain.java @@ -1,5 +1,6 @@ package org.hl7.fhir.common.hapi.validation.support; +import ca.uhn.fhir.context.BaseRuntimeElementDefinition; import ca.uhn.fhir.context.ConfigurationException; import ca.uhn.fhir.context.FhirContext; import ca.uhn.fhir.context.FhirVersionEnum; @@ -10,40 +11,231 @@ import ca.uhn.fhir.context.support.TranslateConceptResults; import ca.uhn.fhir.context.support.ValidationSupportContext; import ca.uhn.fhir.context.support.ValueSetExpansionOptions; import ca.uhn.fhir.i18n.Msg; +import ca.uhn.fhir.rest.server.exceptions.ResourceNotFoundException; +import ca.uhn.fhir.sl.cache.Cache; +import ca.uhn.fhir.sl.cache.CacheFactory; +import ca.uhn.fhir.util.FhirTerser; import ca.uhn.fhir.util.Logs; import jakarta.annotation.Nonnull; +import jakarta.annotation.Nullable; +import jakarta.annotation.PostConstruct; +import jakarta.annotation.PreDestroy; import org.apache.commons.lang3.Validate; +import org.apache.commons.lang3.concurrent.BasicThreadFactory; import org.hl7.fhir.instance.model.api.IBaseResource; import org.hl7.fhir.instance.model.api.IPrimitiveType; import org.slf4j.Logger; +import java.time.Duration; import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.HashMap; import java.util.HashSet; import java.util.List; -import java.util.Optional; +import java.util.Map; +import java.util.Objects; import java.util.Set; +import java.util.UUID; +import java.util.concurrent.LinkedBlockingQueue; +import java.util.concurrent.ThreadPoolExecutor; +import java.util.concurrent.TimeUnit; import java.util.function.Function; +import java.util.function.Supplier; +import static org.apache.commons.lang3.ObjectUtils.defaultIfNull; +import static org.apache.commons.lang3.StringUtils.defaultIfBlank; import static org.apache.commons.lang3.StringUtils.isBlank; import static org.apache.commons.lang3.StringUtils.isNotBlank; +/** + * This validation support module has two primary purposes: It can be used to + * chain multiple backing modules together, and it can optionally cache the + * results. + *

+ * The following chaining logic is used: + *

    + *
  • + * Calls to {@literal fetchAll...} methods such as {@link #fetchAllConformanceResources()} + * and {@link #fetchAllStructureDefinitions()} will call every method in the chain in + * order, and aggregate the results into a single list to return. + *
  • + *
  • + * Calls to fetch or validate codes, such as {@link #validateCode(ValidationSupportContext, ConceptValidationOptions, String, String, String, String)} + * and {@link #lookupCode(ValidationSupportContext, LookupCodeRequest)} will first test + * each module in the chain using the {@link #isCodeSystemSupported(ValidationSupportContext, String)} + * or {@link #isValueSetSupported(ValidationSupportContext, String)} + * methods (depending on whether a ValueSet URL is present in the method parameters) + * and will invoke any methods in the chain which return that they can handle the given + * CodeSystem/ValueSet URL. The first non-null value returned by a method in the chain + * that can support the URL will be returned to the caller. + *
  • + *
  • + * All other methods will invoke each method in the chain in order, and will stop processing and return + * immediately as soon as the first non-null value is returned. + *
  • + *
+ *

+ *

+ * The following caching logic is used if caching is enabled using {@link CacheConfiguration}. + * You can use {@link CacheConfiguration#disabled()} if you want to disable caching. + *

    + *
  • + * Calls to fetch StructureDefinitions including {@link #fetchAllStructureDefinitions()} + * and {@link #fetchStructureDefinition(String)} are cached in a non-expiring cache. + * This is because the {@link org.hl7.fhir.common.hapi.validation.validator.FhirInstanceValidator} + * module makes assumptions that these objects will not change for the lifetime + * of the validator for performance reasons. + *
  • + *
  • + * Calls to all other {@literal fetchAll...} methods including + * {@link #fetchAllConformanceResources()} and {@link #fetchAllSearchParameters()} + * cache their results in an expiring cache, but will refresh that cache asynchronously. + *
  • + *
  • + * Results of {@link #generateSnapshot(ValidationSupportContext, IBaseResource, String, String, String)} + * are not cached, since this method is generally called in contexts where the results + * are cached. + *
  • + *
  • + * Results of all other methods are stored in an expiring cache. + *
  • + *
+ *

+ *

+ * Note that caching functionality used to be provided by a separate provider + * called {@literal CachingValidationSupport} but that functionality has been + * moved into this class as of HAPI FHIR 8.0.0, because it is possible to + * provide a more efficient chain when these functions are combined. + *

+ */ public class ValidationSupportChain implements IValidationSupport { + public static final ValueSetExpansionOptions EMPTY_EXPANSION_OPTIONS = new ValueSetExpansionOptions(); static Logger ourLog = Logs.getTerminologyTroubleshootingLog(); + private final List myChain = new ArrayList<>(); - private List myChain; + @Nullable + private final Cache, Object> myExpiringCache; + + @Nullable + private final Map, Object> myNonExpiringCache; /** - * Constructor + * See class documentation for an explanation of why this is separate + * and non-expiring. Note that this field is non-synchronized. If you + * access it, you should first wrap the call in + * synchronized(myStructureDefinitionsByUrl). + */ + @Nonnull + private final Map myStructureDefinitionsByUrl = new HashMap<>(); + /** + * See class documentation for an explanation of why this is separate + * and non-expiring. Note that this field is non-synchronized. If you + * access it, you should first wrap the call in + * synchronized(myStructureDefinitionsByUrl) (synchronize on + * the other field because both collections are expected to be modified + * at the same time). + */ + @Nonnull + private final List myStructureDefinitionsAsList = new ArrayList<>(); + + private final ThreadPoolExecutor myBackgroundExecutor; + private final CacheConfiguration myCacheConfiguration; + private boolean myEnabledValidationForCodingsLogicalAnd; + private String myName = getClass().getSimpleName(); + private ValidationSupportChainMetrics myMetrics; + private volatile boolean myHaveFetchedAllStructureDefinitions = false; + + /** + * Constructor which initializes the chain with no modules (modules + * must subsequently be registered using {@link #addValidationSupport(IValidationSupport)}). + * The cache will be enabled using {@link CacheConfiguration#defaultValues()}. */ public ValidationSupportChain() { - myChain = new ArrayList<>(); + /* + * Note, this constructor is called by default when + * FhirContext#getValidationSupport() is called, so it should + * provide sensible defaults. + */ + this(Collections.emptyList()); + } + + /** + * Constructor which initializes the chain with the given modules. + * The cache will be enabled using {@link CacheConfiguration#defaultValues()}. + */ + public ValidationSupportChain(IValidationSupport... theValidationSupportModules) { + this( + theValidationSupportModules != null + ? Arrays.asList(theValidationSupportModules) + : Collections.emptyList()); + } + + /** + * Constructor which initializes the chain with the given modules. + * The cache will be enabled using {@link CacheConfiguration#defaultValues()}. + */ + public ValidationSupportChain(List theValidationSupportModules) { + this(CacheConfiguration.defaultValues(), theValidationSupportModules); } /** * Constructor + * + * @param theCacheConfiguration The caching configuration + * @param theValidationSupportModules The initial modules to add to the chain */ - public ValidationSupportChain(IValidationSupport... theValidationSupportModules) { - this(); + public ValidationSupportChain( + @Nonnull CacheConfiguration theCacheConfiguration, IValidationSupport... theValidationSupportModules) { + this( + theCacheConfiguration, + theValidationSupportModules != null + ? Arrays.asList(theValidationSupportModules) + : Collections.emptyList()); + } + + /** + * Constructor + * + * @param theCacheConfiguration The caching configuration + * @param theValidationSupportModules The initial modules to add to the chain + */ + public ValidationSupportChain( + @Nonnull CacheConfiguration theCacheConfiguration, + @Nonnull List theValidationSupportModules) { + + Validate.notNull(theCacheConfiguration, "theCacheConfiguration must not be null"); + Validate.notNull(theValidationSupportModules, "theValidationSupportModules must not be null"); + + myCacheConfiguration = theCacheConfiguration; + if (theCacheConfiguration.getCacheSize() == 0 || theCacheConfiguration.getCacheTimeout() == 0) { + myExpiringCache = null; + myNonExpiringCache = null; + myBackgroundExecutor = null; + } else { + myExpiringCache = + CacheFactory.build(theCacheConfiguration.getCacheTimeout(), theCacheConfiguration.getCacheSize()); + myNonExpiringCache = Collections.synchronizedMap(new HashMap<>()); + + LinkedBlockingQueue executorQueue = new LinkedBlockingQueue<>(1000); + BasicThreadFactory threadFactory = new BasicThreadFactory.Builder() + .namingPattern("CachingValidationSupport-%d") + .daemon(false) + .priority(Thread.NORM_PRIORITY) + .build(); + + // NOTE: We're not using ThreadPoolUtil here, because that class depends on Spring and + // we want the validator infrastructure to not require spring dependencies. + myBackgroundExecutor = new ThreadPoolExecutor( + 1, + 1, + 0L, + TimeUnit.MILLISECONDS, + executorQueue, + threadFactory, + new ThreadPoolExecutor.DiscardPolicy()); + } + for (IValidationSupport next : theValidationSupportModules) { if (next != null) { addValidationSupport(next); @@ -51,63 +243,174 @@ public class ValidationSupportChain implements IValidationSupport { } } + @Override + public String getName() { + return myName; + } + + /** + * Sets a name for this chain. This name will be returned by + * {@link #getName()} and used by OpenTelemetry. + */ + public void setName(String theName) { + Validate.notBlank(theName, "theName must not be blank"); + myName = theName; + } + + @PostConstruct + public void start() { + if (myMetrics == null) { + myMetrics = new ValidationSupportChainMetrics(this); + myMetrics.start(); + } + } + + @PreDestroy + public void stop() { + if (myMetrics != null) { + myMetrics.stop(); + myMetrics = null; + } + } + + @Override + public boolean isCodeableConceptValidationSuccessfulIfNotAllCodingsAreValid() { + return myEnabledValidationForCodingsLogicalAnd; + } + + /** + * When validating a CodeableConcept containing multiple codings, this method can be used to control whether + * the validator requires all codings in the CodeableConcept to be valid in order to consider the + * CodeableConcept valid. + *

+ * See VersionSpecificWorkerContextWrapper#validateCode in hapi-fhir-validation, and the refer to the values below + * for the behaviour associated with each value. + *

+ *

+ *

    + *
  • If false (default setting) the validation for codings will return a positive result only if + * ALL codings are valid.
  • + *
  • If true the validation for codings will return a positive result if ANY codings are valid. + *
  • + *
+ *

+ * + * @return true or false depending on the desired coding validation behaviour. + */ + public ValidationSupportChain setCodeableConceptValidationSuccessfulIfNotAllCodingsAreValid( + boolean theEnabledValidationForCodingsLogicalAnd) { + myEnabledValidationForCodingsLogicalAnd = theEnabledValidationForCodingsLogicalAnd; + return this; + } + @Override public TranslateConceptResults translateConcept(TranslateCodeRequest theRequest) { - TranslateConceptResults retVal = null; - for (IValidationSupport next : myChain) { - TranslateConceptResults translations = next.translateConcept(theRequest); - if (translations != null) { - if (retVal == null) { - retVal = new TranslateConceptResults(); - } + TranslateConceptKey key = new TranslateConceptKey(theRequest); + CacheValue retVal = getFromCache(key); + if (retVal == null) { - if (retVal.getMessage() == null) { - retVal.setMessage(translations.getMessage()); - } + /* + * The chain behaviour for this method is to call every element in the + * chain and aggregate the results (as opposed to just using the first + * module which provides a response). + */ + retVal = CacheValue.empty(); - if (translations.getResult() && !retVal.getResult()) { - retVal.setResult(translations.getResult()); - retVal.setMessage(translations.getMessage()); - } + TranslateConceptResults outcome = null; + for (IValidationSupport next : myChain) { + TranslateConceptResults translations = next.translateConcept(theRequest); + if (translations != null) { + if (outcome == null) { + outcome = new TranslateConceptResults(); + } - if (!translations.isEmpty()) { - if (ourLog.isDebugEnabled()) { + if (outcome.getMessage() == null) { + outcome.setMessage(translations.getMessage()); + } + + if (translations.getResult() && !outcome.getResult()) { + outcome.setResult(translations.getResult()); + outcome.setMessage(translations.getMessage()); + } + + if (!translations.isEmpty()) { ourLog.debug( "{} found {} concept translation{} for {}", next.getName(), translations.size(), translations.size() > 1 ? "s" : "", theRequest); + outcome.getResults().addAll(translations.getResults()); } - retVal.getResults().addAll(translations.getResults()); } } + + if (outcome != null) { + retVal = new CacheValue<>(outcome); + } + + putInCache(key, retVal); } - return retVal; + + return retVal.getValue(); } @Override public void invalidateCaches() { ourLog.debug("Invalidating caches in {} validation support modules", myChain.size()); + myHaveFetchedAllStructureDefinitions = false; for (IValidationSupport next : myChain) { next.invalidateCaches(); } + if (myNonExpiringCache != null) { + myNonExpiringCache.clear(); + } + if (myExpiringCache != null) { + myExpiringCache.invalidateAll(); + } + synchronized (myStructureDefinitionsByUrl) { + myStructureDefinitionsByUrl.clear(); + myStructureDefinitionsAsList.clear(); + } + } + + /** + * Invalidate the expiring cache, but not the permanent StructureDefinition cache + * + * @since 8.0.0 + */ + public void invalidateExpiringCaches() { + if (myExpiringCache != null) { + myExpiringCache.invalidateAll(); + } } @Override public boolean isValueSetSupported(ValidationSupportContext theValidationSupportContext, String theValueSetUrl) { for (IValidationSupport next : myChain) { - boolean retVal = next.isValueSetSupported(theValidationSupportContext, theValueSetUrl); + boolean retVal = isValueSetSupported(theValidationSupportContext, next, theValueSetUrl); if (retVal) { - if (ourLog.isDebugEnabled()) { - ourLog.debug("ValueSet {} found in {}", theValueSetUrl, next.getName()); - } + ourLog.debug("ValueSet {} found in {}", theValueSetUrl, next.getName()); return true; } } return false; } + private boolean isValueSetSupported( + ValidationSupportContext theValidationSupportContext, + IValidationSupport theValidationSupport, + String theValueSetUrl) { + IsValueSetSupportedKey key = new IsValueSetSupportedKey(theValidationSupport, theValueSetUrl); + CacheValue value = getFromCache(key); + if (value == null) { + value = new CacheValue<>( + theValidationSupport.isValueSetSupported(theValidationSupportContext, theValueSetUrl)); + putInCache(key, value); + } + return value.getValue(); + } + @Override public IBaseResource generateSnapshot( ValidationSupportContext theValidationSupportContext, @@ -115,13 +418,22 @@ public class ValidationSupportChain implements IValidationSupport { String theUrl, String theWebUrl, String theProfileName) { + + /* + * No caching for this method because we typically cache the results anyhow. + * If this ever changes, make sure to update the class javadocs and the + * HAPI FHIR documentation which indicate that this isn't cached. + */ + for (IValidationSupport next : myChain) { IBaseResource retVal = next.generateSnapshot(theValidationSupportContext, theInput, theUrl, theWebUrl, theProfileName); if (retVal != null) { - if (ourLog.isDebugEnabled()) { - ourLog.debug("Profile snapshot for {} generated by {}", theInput.getIdElement(), next.getName()); - } + ourLog.atDebug() + .setMessage("Profile snapshot for {} generated by {}") + .addArgument(() -> theInput.getIdElement()) + .addArgument(() -> next.getName()) + .log(); return retVal; } } @@ -130,7 +442,7 @@ public class ValidationSupportChain implements IValidationSupport { @Override public FhirContext getFhirContext() { - if (myChain.size() == 0) { + if (myChain.isEmpty()) { return null; } return myChain.get(0).getFhirContext(); @@ -160,6 +472,7 @@ public class ValidationSupportChain implements IValidationSupport { */ public void addValidationSupport(int theIndex, IValidationSupport theValidationSupport) { Validate.notNull(theValidationSupport, "theValidationSupport must not be null"); + invalidateCaches(); if (theValidationSupport.getFhirContext() == null) { String message = "Can not add validation support: getFhirContext() returns null"; @@ -189,58 +502,134 @@ public class ValidationSupportChain implements IValidationSupport { myChain.remove(theValidationSupport); } + @Nullable + @Override + public ValueSetExpansionOutcome expandValueSet( + ValidationSupportContext theValidationSupportContext, + @Nullable ValueSetExpansionOptions theExpansionOptions, + @Nonnull String theValueSetUrlToExpand) + throws ResourceNotFoundException { + ValueSetExpansionOptions expansionOptions = defaultIfNull(theExpansionOptions, EMPTY_EXPANSION_OPTIONS); + ExpandValueSetKey key = new ExpandValueSetKey(expansionOptions, null, theValueSetUrlToExpand); + CacheValue retVal = getFromCache(key); + + if (retVal == null) { + retVal = CacheValue.empty(); + for (IValidationSupport next : myChain) { + if (isValueSetSupported(theValidationSupportContext, next, theValueSetUrlToExpand)) { + ValueSetExpansionOutcome expanded = + next.expandValueSet(theValidationSupportContext, expansionOptions, theValueSetUrlToExpand); + if (expanded != null) { + ourLog.debug("ValueSet {} expanded by URL by {}", theValueSetUrlToExpand, next.getName()); + retVal = new CacheValue<>(expanded); + break; + } + } + } + + putInCache(key, retVal); + } + + return retVal.getValue(); + } + @Override public ValueSetExpansionOutcome expandValueSet( ValidationSupportContext theValidationSupportContext, ValueSetExpansionOptions theExpansionOptions, @Nonnull IBaseResource theValueSetToExpand) { - for (IValidationSupport next : myChain) { - // TODO: test if code system is supported? - ValueSetExpansionOutcome expanded = - next.expandValueSet(theValidationSupportContext, theExpansionOptions, theValueSetToExpand); - if (expanded != null) { - if (ourLog.isDebugEnabled()) { + + ValueSetExpansionOptions expansionOptions = defaultIfNull(theExpansionOptions, EMPTY_EXPANSION_OPTIONS); + String id = theValueSetToExpand.getIdElement().getValue(); + ExpandValueSetKey key = null; + CacheValue retVal = null; + if (isNotBlank(id)) { + key = new ExpandValueSetKey(expansionOptions, id, null); + retVal = getFromCache(key); + } + if (retVal == null) { + retVal = CacheValue.empty(); + for (IValidationSupport next : myChain) { + ValueSetExpansionOutcome expanded = + next.expandValueSet(theValidationSupportContext, expansionOptions, theValueSetToExpand); + if (expanded != null) { ourLog.debug("ValueSet {} expanded by {}", theValueSetToExpand.getIdElement(), next.getName()); + retVal = new CacheValue<>(expanded); + break; } - return expanded; + } + + if (key != null) { + putInCache(key, retVal); } } - return null; + + return retVal.getValue(); } @Override public boolean isRemoteTerminologyServiceConfigured() { - if (myChain != null) { - Optional remoteTerminologyService = myChain.stream() - .filter(RemoteTerminologyServiceValidationSupport.class::isInstance) - .findFirst(); - if (remoteTerminologyService.isPresent()) { - return true; - } - } - return false; + return myChain.stream().anyMatch(RemoteTerminologyServiceValidationSupport.class::isInstance); } @Override public List fetchAllConformanceResources() { - List retVal = new ArrayList<>(); - for (IValidationSupport next : myChain) { - List candidates = next.fetchAllConformanceResources(); - if (candidates != null) { - retVal.addAll(candidates); + FetchAllKey key = new FetchAllKey(FetchAllKey.TypeEnum.ALL); + Supplier> loader = () -> { + List allCandidates = new ArrayList<>(); + for (IValidationSupport next : myChain) { + List candidates = next.fetchAllConformanceResources(); + if (candidates != null) { + allCandidates.addAll(candidates); + } } - } - return retVal; + return allCandidates; + }; + + return getFromCacheWithAsyncRefresh(key, loader); } + @SuppressWarnings("unchecked") @Override + @Nonnull public List fetchAllStructureDefinitions() { - return doFetchStructureDefinitions(t -> t.fetchAllStructureDefinitions()); + if (!myHaveFetchedAllStructureDefinitions) { + FhirTerser terser = getFhirContext().newTerser(); + List allStructureDefinitions = + doFetchStructureDefinitions(IValidationSupport::fetchAllStructureDefinitions); + if (myExpiringCache != null) { + synchronized (myStructureDefinitionsByUrl) { + for (IBaseResource structureDefinition : allStructureDefinitions) { + String url = terser.getSinglePrimitiveValueOrNull(structureDefinition, "url"); + url = defaultIfBlank(url, UUID.randomUUID().toString()); + if (myStructureDefinitionsByUrl.putIfAbsent(url, structureDefinition) == null) { + myStructureDefinitionsAsList.add(structureDefinition); + } + } + } + } + myHaveFetchedAllStructureDefinitions = true; + } + return Collections.unmodifiableList(new ArrayList<>(myStructureDefinitionsAsList)); } + @SuppressWarnings("unchecked") @Override public List fetchAllNonBaseStructureDefinitions() { - return doFetchStructureDefinitions(t -> t.fetchAllNonBaseStructureDefinitions()); + FetchAllKey key = new FetchAllKey(FetchAllKey.TypeEnum.ALL_NON_BASE_STRUCTUREDEFINITIONS); + Supplier> loader = + () -> doFetchStructureDefinitions(IValidationSupport::fetchAllNonBaseStructureDefinitions); + return getFromCacheWithAsyncRefresh(key, loader); + } + + @SuppressWarnings("unchecked") + @Nullable + @Override + public List fetchAllSearchParameters() { + FetchAllKey key = new FetchAllKey(FetchAllKey.TypeEnum.ALL_SEARCHPARAMETERS); + Supplier> loader = + () -> doFetchStructureDefinitions(IValidationSupport::fetchAllSearchParameters); + return (List) getFromCacheWithAsyncRefresh(key, loader); } private List doFetchStructureDefinitions( @@ -267,84 +656,98 @@ public class ValidationSupportChain implements IValidationSupport { @Override public IBaseResource fetchCodeSystem(String theSystem) { - for (IValidationSupport next : myChain) { - IBaseResource retVal = next.fetchCodeSystem(theSystem); - if (retVal != null) { - if (ourLog.isDebugEnabled()) { - ourLog.debug( - "CodeSystem {} with System {} fetched by {}", - retVal.getIdElement(), - theSystem, - next.getName()); + Function invoker = v -> v.fetchCodeSystem(theSystem); + ResourceByUrlKey key = new ResourceByUrlKey<>(ResourceByUrlKey.TypeEnum.CODESYSTEM, theSystem); + return fetchValue(key, invoker, theSystem); + } + + private T fetchValue(ResourceByUrlKey theKey, Function theInvoker, String theUrl) { + CacheValue retVal = getFromCache(theKey); + + if (retVal == null) { + retVal = CacheValue.empty(); + for (IValidationSupport next : myChain) { + T outcome = theInvoker.apply(next); + if (outcome != null) { + ourLog.debug("{} {} with URL {} fetched by {}", theKey.myType, outcome, theUrl, next.getName()); + retVal = new CacheValue<>(outcome); + break; } - return retVal; } + putInCache(theKey, retVal); } - return null; + + return retVal.getValue(); } @Override public IBaseResource fetchValueSet(String theUrl) { - for (IValidationSupport next : myChain) { - IBaseResource retVal = next.fetchValueSet(theUrl); - if (retVal != null) { - if (ourLog.isDebugEnabled()) { - ourLog.debug( - "ValueSet {} with URL {} fetched by {}", retVal.getIdElement(), theUrl, next.getName()); - } - return retVal; - } - } - return null; + Function invoker = v -> v.fetchValueSet(theUrl); + ResourceByUrlKey key = new ResourceByUrlKey<>(ResourceByUrlKey.TypeEnum.VALUESET, theUrl); + return fetchValue(key, invoker, theUrl); } + @SuppressWarnings("unchecked") @Override public T fetchResource(Class theClass, String theUri) { - for (IValidationSupport next : myChain) { - T retVal = next.fetchResource(theClass, theUri); - if (retVal != null) { - if (ourLog.isDebugEnabled()) { - ourLog.debug( - "Resource {} with URI {} fetched by {}", retVal.getIdElement(), theUri, next.getName()); + + /* + * If we're looking for a common type with a dedicated fetch method, use that + * so that we can use a common cache location for lookups wanting a given + * URL on both methods (the validator will call both paths when looking for a + * specific URL so this improves cache efficiency). + */ + if (theClass != null) { + BaseRuntimeElementDefinition elementDefinition = getFhirContext().getElementDefinition(theClass); + if (elementDefinition != null) { + switch (elementDefinition.getName()) { + case "ValueSet": + return (T) fetchValueSet(theUri); + case "CodeSystem": + return (T) fetchCodeSystem(theUri); + case "StructureDefinition": + return (T) fetchStructureDefinition(theUri); } - return retVal; } } - return null; + + Function invoker = v -> v.fetchResource(theClass, theUri); + TypedResourceByUrlKey key = new TypedResourceByUrlKey<>(theClass, theUri); + return fetchValue(key, invoker, theUri); } @Override - public byte[] fetchBinary(String key) { - for (IValidationSupport next : myChain) { - byte[] retVal = next.fetchBinary(key); - if (retVal != null) { - if (ourLog.isDebugEnabled()) { - ourLog.debug("Binary with key {} fetched by {}", key, next.getName()); - } - return retVal; - } - } - return null; + public byte[] fetchBinary(String theKey) { + Function invoker = v -> v.fetchBinary(theKey); + ResourceByUrlKey key = new ResourceByUrlKey<>(ResourceByUrlKey.TypeEnum.BINARY, theKey); + return fetchValue(key, invoker, theKey); } @Override public IBaseResource fetchStructureDefinition(String theUrl) { - for (IValidationSupport next : myChain) { - IBaseResource retVal = next.fetchStructureDefinition(theUrl); - if (retVal != null) { - if (ourLog.isDebugEnabled()) { - ourLog.debug("StructureDefinition with URL {} fetched by {}", theUrl, next.getName()); + synchronized (myStructureDefinitionsByUrl) { + IBaseResource candidate = myStructureDefinitionsByUrl.get(theUrl); + if (candidate == null) { + Function invoker = v -> v.fetchStructureDefinition(theUrl); + ResourceByUrlKey key = + new ResourceByUrlKey<>(ResourceByUrlKey.TypeEnum.STRUCTUREDEFINITION, theUrl); + candidate = fetchValue(key, invoker, theUrl); + if (myExpiringCache != null) { + if (candidate != null) { + if (myStructureDefinitionsByUrl.putIfAbsent(theUrl, candidate) == null) { + myStructureDefinitionsAsList.add(candidate); + } + } } - return retVal; } + return candidate; } - return null; } @Override public boolean isCodeSystemSupported(ValidationSupportContext theValidationSupportContext, String theSystem) { for (IValidationSupport next : myChain) { - if (next.isCodeSystemSupported(theValidationSupportContext, theSystem)) { + if (isCodeSystemSupported(theValidationSupportContext, next, theSystem)) { if (ourLog.isDebugEnabled()) { ourLog.debug("CodeSystem with System {} is supported by {}", theSystem, next.getName()); } @@ -354,6 +757,20 @@ public class ValidationSupportChain implements IValidationSupport { return false; } + private boolean isCodeSystemSupported( + ValidationSupportContext theValidationSupportContext, + IValidationSupport theValidationSupport, + String theCodeSystemUrl) { + IsCodeSystemSupportedKey key = new IsCodeSystemSupportedKey(theValidationSupport, theCodeSystemUrl); + CacheValue value = getFromCache(key); + if (value == null) { + value = new CacheValue<>( + theValidationSupport.isCodeSystemSupported(theValidationSupportContext, theCodeSystemUrl)); + putInCache(key, value); + } + return value.getValue(); + } + @Override public CodeValidationResult validateCode( @Nonnull ValidationSupportContext theValidationSupportContext, @@ -362,14 +779,24 @@ public class ValidationSupportChain implements IValidationSupport { String theCode, String theDisplay, String theValueSetUrl) { - for (IValidationSupport next : myChain) { - if ((isBlank(theValueSetUrl) && next.isCodeSystemSupported(theValidationSupportContext, theCodeSystem)) - || (isNotBlank(theValueSetUrl) - && next.isValueSetSupported(theValidationSupportContext, theValueSetUrl))) { - CodeValidationResult retVal = next.validateCode( - theValidationSupportContext, theOptions, theCodeSystem, theCode, theDisplay, theValueSetUrl); - if (retVal != null) { - if (ourLog.isDebugEnabled()) { + + ValidateCodeKey key = new ValidateCodeKey(theOptions, theCodeSystem, theCode, theDisplay, theValueSetUrl); + CacheValue retVal = getFromCache(key); + if (retVal == null) { + retVal = CacheValue.empty(); + + for (IValidationSupport next : myChain) { + if ((isBlank(theValueSetUrl) && isCodeSystemSupported(theValidationSupportContext, next, theCodeSystem)) + || (isNotBlank(theValueSetUrl) + && isValueSetSupported(theValidationSupportContext, next, theValueSetUrl))) { + CodeValidationResult outcome = next.validateCode( + theValidationSupportContext, + theOptions, + theCodeSystem, + theCode, + theDisplay, + theValueSetUrl); + if (outcome != null) { ourLog.debug( "Code {}|{} '{}' in ValueSet {} validated by {}", theCodeSystem, @@ -377,12 +804,16 @@ public class ValidationSupportChain implements IValidationSupport { theDisplay, theValueSetUrl, next.getName()); + retVal = new CacheValue<>(outcome); + break; } - return retVal; } } + + putInCache(key, retVal); } - return null; + + return retVal.getValue(); } @Override @@ -393,56 +824,522 @@ public class ValidationSupportChain implements IValidationSupport { String theCode, String theDisplay, @Nonnull IBaseResource theValueSet) { + String url = CommonCodeSystemsTerminologyService.getValueSetUrl(getFhirContext(), theValueSet); + + ValidateCodeKey key = null; + CacheValue retVal = null; + if (isNotBlank(url)) { + key = new ValidateCodeKey(theOptions, theCodeSystem, theCode, theDisplay, url); + retVal = getFromCache(key); + } + if (retVal != null) { + return retVal.getValue(); + } + + retVal = CacheValue.empty(); for (IValidationSupport next : myChain) { - String url = CommonCodeSystemsTerminologyService.getValueSetUrl(getFhirContext(), theValueSet); - if (isBlank(url) || next.isValueSetSupported(theValidationSupportContext, url)) { - CodeValidationResult retVal = next.validateCodeInValueSet( + if (isBlank(url) || isValueSetSupported(theValidationSupportContext, next, url)) { + CodeValidationResult outcome = next.validateCodeInValueSet( theValidationSupportContext, theOptions, theCodeSystem, theCode, theDisplay, theValueSet); - if (retVal != null) { - if (ourLog.isDebugEnabled()) { - ourLog.debug( - "Code {}|{} '{}' in ValueSet {} validated by {}", - theCodeSystem, - theCode, - theDisplay, - theValueSet.getIdElement(), - next.getName()); - } - return retVal; + if (outcome != null) { + ourLog.debug( + "Code {}|{} '{}' in ValueSet {} validated by {}", + theCodeSystem, + theCode, + theDisplay, + theValueSet.getIdElement(), + next.getName()); + retVal = new CacheValue<>(outcome); + break; } } } - return null; + + if (key != null) { + putInCache(key, retVal); + } + + return retVal.getValue(); } @Override public LookupCodeResult lookupCode( ValidationSupportContext theValidationSupportContext, @Nonnull LookupCodeRequest theLookupCodeRequest) { - for (IValidationSupport next : myChain) { - final String system = theLookupCodeRequest.getSystem(); - final String code = theLookupCodeRequest.getCode(); - final String displayLanguage = theLookupCodeRequest.getDisplayLanguage(); - if (next.isCodeSystemSupported(theValidationSupportContext, system)) { - LookupCodeResult lookupCodeResult = next.lookupCode(theValidationSupportContext, theLookupCodeRequest); - if (lookupCodeResult == null) { - /* - This branch has been added as a fall-back mechanism for supporting lookupCode - methods marked as deprecated in interface IValidationSupport. - */ - lookupCodeResult = next.lookupCode(theValidationSupportContext, system, code, displayLanguage); + + LookupCodeKey key = new LookupCodeKey(theLookupCodeRequest); + CacheValue retVal = getFromCache(key); + if (retVal == null) { + + retVal = CacheValue.empty(); + for (IValidationSupport next : myChain) { + final String system = theLookupCodeRequest.getSystem(); + final String code = theLookupCodeRequest.getCode(); + final String displayLanguage = theLookupCodeRequest.getDisplayLanguage(); + if (isCodeSystemSupported(theValidationSupportContext, next, system)) { + LookupCodeResult lookupCodeResult = + next.lookupCode(theValidationSupportContext, theLookupCodeRequest); + if (lookupCodeResult == null) { + /* + This branch has been added as a fall-back mechanism for supporting lookupCode + methods marked as deprecated in interface IValidationSupport. + */ + //noinspection deprecation + lookupCodeResult = next.lookupCode(theValidationSupportContext, system, code, displayLanguage); + } + if (lookupCodeResult != null) { + ourLog.debug( + "Code {}|{}{} {} by {}", + system, + code, + isBlank(displayLanguage) ? "" : " (" + theLookupCodeRequest.getDisplayLanguage() + ")", + lookupCodeResult.isFound() ? "found" : "not found", + next.getName()); + retVal = new CacheValue<>(lookupCodeResult); + break; + } } - if (ourLog.isDebugEnabled()) { - ourLog.debug( - "Code {}|{}{} {} by {}", - system, - code, - isBlank(displayLanguage) ? "" : " (" + theLookupCodeRequest.getDisplayLanguage() + ")", - lookupCodeResult != null && lookupCodeResult.isFound() ? "found" : "not found", - next.getName()); - } - return lookupCodeResult; + } + + putInCache(key, retVal); + } + + return retVal.getValue(); + } + + /** + * Returns a view of the {@link IValidationSupport} modules within + * this chain. The returned collection is unmodifiable and will reflect + * changes to the underlying list. + * + * @since 8.0.0 + */ + public List getValidationSupports() { + return Collections.unmodifiableList(myChain); + } + + private void putInCache(BaseKey key, CacheValue theValue) { + if (myExpiringCache != null) { + myExpiringCache.put(key, theValue); + } + } + + @SuppressWarnings("unchecked") + private CacheValue getFromCache(BaseKey key) { + if (myExpiringCache != null) { + return (CacheValue) myExpiringCache.getIfPresent(key); + } else { + return null; + } + } + + @SuppressWarnings("unchecked") + private List getFromCacheWithAsyncRefresh( + FetchAllKey theKey, Supplier> theLoader) { + if (myExpiringCache == null || myNonExpiringCache == null) { + return theLoader.get(); + } + + CacheValue> retVal = getFromCache(theKey); + if (retVal == null) { + retVal = (CacheValue>) myNonExpiringCache.get(theKey); + if (retVal != null) { + Runnable loaderTask = () -> { + List loadedItem = theLoader.get(); + CacheValue> value = new CacheValue<>(loadedItem); + myNonExpiringCache.put(theKey, value); + putInCache(theKey, value); + }; + List returnValue = retVal.getValue(); + + myBackgroundExecutor.execute(loaderTask); + + return returnValue; + } else { + retVal = new CacheValue<>(theLoader.get()); + myNonExpiringCache.put(theKey, retVal); + putInCache(theKey, retVal); } } - return null; + + return retVal.getValue(); + } + + public void logCacheSizes() { + String b = "Cache sizes:" + "\n * Expiring: " + + (myExpiringCache != null ? myExpiringCache.estimatedSize() : "(disabled)") + + "\n * Non-Expiring: " + + (myNonExpiringCache != null ? myNonExpiringCache.size() : "(disabled)"); + ourLog.info(b); + } + + long getMetricExpiringCacheEntries() { + if (myExpiringCache != null) { + return myExpiringCache.estimatedSize(); + } else { + return 0; + } + } + + int getMetricNonExpiringCacheEntries() { + synchronized (myStructureDefinitionsByUrl) { + int size = myNonExpiringCache != null ? myNonExpiringCache.size() : 0; + return size + myStructureDefinitionsAsList.size(); + } + } + + int getMetricExpiringCacheMaxSize() { + return myCacheConfiguration.getCacheSize(); + } + + /** + * @since 5.4.0 + */ + public static class CacheConfiguration { + + private long myCacheTimeout; + private int myCacheSize; + + /** + * Non-instantiable. Use the factory methods. + */ + private CacheConfiguration() { + super(); + } + + public long getCacheTimeout() { + return myCacheTimeout; + } + + public CacheConfiguration setCacheTimeout(Duration theCacheTimeout) { + Validate.isTrue(theCacheTimeout.toMillis() >= 0, "Cache timeout must not be negative"); + myCacheTimeout = theCacheTimeout.toMillis(); + return this; + } + + public int getCacheSize() { + return myCacheSize; + } + + public CacheConfiguration setCacheSize(int theCacheSize) { + Validate.isTrue(theCacheSize >= 0, "Cache size must not be negative"); + myCacheSize = theCacheSize; + return this; + } + + /** + * Creates a cache configuration with sensible default values: + * 10 minutes expiry, and 5000 cache entries. + */ + public static CacheConfiguration defaultValues() { + return new CacheConfiguration() + .setCacheTimeout(Duration.ofMinutes(10)) + .setCacheSize(5000); + } + + public static CacheConfiguration disabled() { + return new CacheConfiguration().setCacheSize(0).setCacheTimeout(Duration.ofMillis(0)); + } + } + + /** + * @param The value type associated with this key + */ + @SuppressWarnings("unused") + abstract static class BaseKey { + + @Override + public abstract boolean equals(Object theO); + + @Override + public abstract int hashCode(); + } + + static class ExpandValueSetKey extends BaseKey { + + private final ValueSetExpansionOptions myOptions; + private final String myId; + private final String myUrl; + private final int myHashCode; + + private ExpandValueSetKey(ValueSetExpansionOptions theOptions, String theId, String theUrl) { + myOptions = theOptions; + myId = theId; + myUrl = theUrl; + myHashCode = Objects.hash(myOptions, myId, myUrl); + } + + @Override + public boolean equals(Object theO) { + if (this == theO) return true; + if (!(theO instanceof ExpandValueSetKey)) return false; + ExpandValueSetKey that = (ExpandValueSetKey) theO; + return Objects.equals(myOptions, that.myOptions) + && Objects.equals(myId, that.myId) + && Objects.equals(myUrl, that.myUrl); + } + + @Override + public int hashCode() { + return myHashCode; + } + } + + static class FetchAllKey extends BaseKey> { + + private final TypeEnum myType; + private final int myHashCode; + + private FetchAllKey(TypeEnum theType) { + myType = theType; + myHashCode = Objects.hash(myType); + } + + @Override + public boolean equals(Object theO) { + if (this == theO) return true; + if (!(theO instanceof FetchAllKey)) return false; + FetchAllKey that = (FetchAllKey) theO; + return myType == that.myType; + } + + @Override + public int hashCode() { + return myHashCode; + } + + private enum TypeEnum { + ALL, + ALL_STRUCTUREDEFINITIONS, + ALL_NON_BASE_STRUCTUREDEFINITIONS, + ALL_SEARCHPARAMETERS + } + } + + static class ResourceByUrlKey extends BaseKey { + + private final TypeEnum myType; + private final String myUrl; + private final int myHashCode; + + private ResourceByUrlKey(TypeEnum theType, String theUrl) { + this(theType, theUrl, Objects.hash("ResourceByUrl", theType, theUrl)); + } + + private ResourceByUrlKey(TypeEnum theType, String theUrl, int theHashCode) { + myType = theType; + myUrl = theUrl; + myHashCode = theHashCode; + } + + @Override + public boolean equals(Object theO) { + if (this == theO) return true; + if (!(theO instanceof ResourceByUrlKey)) return false; + ResourceByUrlKey that = (ResourceByUrlKey) theO; + return myType == that.myType && Objects.equals(myUrl, that.myUrl); + } + + @Override + public int hashCode() { + return myHashCode; + } + + private enum TypeEnum { + CODESYSTEM, + VALUESET, + RESOURCE, + BINARY, + STRUCTUREDEFINITION + } + } + + static class TypedResourceByUrlKey extends ResourceByUrlKey { + + private final Class myType; + + private TypedResourceByUrlKey(Class theType, String theUrl) { + super(ResourceByUrlKey.TypeEnum.RESOURCE, theUrl, Objects.hash("TypedResourceByUrl", theType, theUrl)); + myType = theType; + } + + @Override + public boolean equals(Object theO) { + if (this == theO) return true; + if (!(theO instanceof TypedResourceByUrlKey)) return false; + if (!super.equals(theO)) return false; + TypedResourceByUrlKey that = (TypedResourceByUrlKey) theO; + return Objects.equals(myType, that.myType); + } + + @Override + public int hashCode() { + return Objects.hash(super.hashCode(), myType); + } + } + + static class IsValueSetSupportedKey extends BaseKey { + + private final String myValueSetUrl; + private final IValidationSupport myValidationSupport; + private final int myHashCode; + + private IsValueSetSupportedKey(IValidationSupport theValidationSupport, String theValueSetUrl) { + myValidationSupport = theValidationSupport; + myValueSetUrl = theValueSetUrl; + myHashCode = Objects.hash("IsValueSetSupported", theValidationSupport, myValueSetUrl); + } + + @Override + public boolean equals(Object theO) { + if (this == theO) return true; + if (!(theO instanceof IsValueSetSupportedKey)) return false; + IsValueSetSupportedKey that = (IsValueSetSupportedKey) theO; + return myValidationSupport == that.myValidationSupport && Objects.equals(myValueSetUrl, that.myValueSetUrl); + } + + @Override + public int hashCode() { + return myHashCode; + } + } + + static class IsCodeSystemSupportedKey extends BaseKey { + + private final String myCodeSystemUrl; + private final IValidationSupport myValidationSupport; + private final int myHashCode; + + private IsCodeSystemSupportedKey(IValidationSupport theValidationSupport, String theCodeSystemUrl) { + myValidationSupport = theValidationSupport; + myCodeSystemUrl = theCodeSystemUrl; + myHashCode = Objects.hash("IsCodeSystemSupported", theValidationSupport, myCodeSystemUrl); + } + + @Override + public boolean equals(Object theO) { + if (this == theO) return true; + if (!(theO instanceof IsCodeSystemSupportedKey)) return false; + IsCodeSystemSupportedKey that = (IsCodeSystemSupportedKey) theO; + return myValidationSupport == that.myValidationSupport + && Objects.equals(myCodeSystemUrl, that.myCodeSystemUrl); + } + + @Override + public int hashCode() { + return myHashCode; + } + } + + static class LookupCodeKey extends BaseKey { + + private final LookupCodeRequest myRequest; + private final int myHashCode; + + private LookupCodeKey(LookupCodeRequest theRequest) { + myRequest = theRequest; + myHashCode = Objects.hash("LookupCode", myRequest); + } + + @Override + public boolean equals(Object theO) { + if (this == theO) return true; + if (!(theO instanceof LookupCodeKey)) return false; + LookupCodeKey that = (LookupCodeKey) theO; + return Objects.equals(myRequest, that.myRequest); + } + + @Override + public int hashCode() { + return myHashCode; + } + } + + static class TranslateConceptKey extends BaseKey { + + private final TranslateCodeRequest myRequest; + private final int myHashCode; + + private TranslateConceptKey(TranslateCodeRequest theRequest) { + myRequest = theRequest; + myHashCode = Objects.hash("TranslateConcept", myRequest); + } + + @Override + public boolean equals(Object theO) { + if (this == theO) return true; + if (!(theO instanceof TranslateConceptKey)) return false; + TranslateConceptKey that = (TranslateConceptKey) theO; + return Objects.equals(myRequest, that.myRequest); + } + + @Override + public int hashCode() { + return myHashCode; + } + } + + static class ValidateCodeKey extends BaseKey { + private final String mySystem; + private final String myCode; + private final String myDisplay; + private final String myValueSetUrl; + private final int myHashCode; + private final ConceptValidationOptions myOptions; + + private ValidateCodeKey( + ConceptValidationOptions theOptions, + String theSystem, + String theCode, + String theDisplay, + String theValueSetUrl) { + myOptions = theOptions; + mySystem = theSystem; + myCode = theCode; + myDisplay = theDisplay; + myValueSetUrl = theValueSetUrl; + myHashCode = Objects.hash("ValidateCodeKey", myOptions, mySystem, myCode, myDisplay, myValueSetUrl); + } + + @Override + public boolean equals(Object theO) { + if (this == theO) return true; + if (!(theO instanceof ValidateCodeKey)) return false; + ValidateCodeKey that = (ValidateCodeKey) theO; + return Objects.equals(myOptions, that.myOptions) + && Objects.equals(mySystem, that.mySystem) + && Objects.equals(myCode, that.myCode) + && Objects.equals(myDisplay, that.myDisplay) + && Objects.equals(myValueSetUrl, that.myValueSetUrl); + } + + @Override + public int hashCode() { + return myHashCode; + } + } + + /** + * This class is basically the same thing as Optional, but is a distinct thing + * because we want to use it as a method parameter value, and compare instances of + * it with null. Both of these things generate warnings in various linters. + */ + private static class CacheValue { + + private static final CacheValue EMPTY = new CacheValue<>(null); + + private final T myValue; + + private CacheValue(T theValue) { + myValue = theValue; + } + + public T getValue() { + return myValue; + } + + @SuppressWarnings("unchecked") + public static CacheValue empty() { + return (CacheValue) EMPTY; + } } } diff --git a/hapi-fhir-validation/src/main/java/org/hl7/fhir/common/hapi/validation/support/ValidationSupportChainMetrics.java b/hapi-fhir-validation/src/main/java/org/hl7/fhir/common/hapi/validation/support/ValidationSupportChainMetrics.java new file mode 100644 index 00000000000..05fb8229541 --- /dev/null +++ b/hapi-fhir-validation/src/main/java/org/hl7/fhir/common/hapi/validation/support/ValidationSupportChainMetrics.java @@ -0,0 +1,93 @@ +package org.hl7.fhir.common.hapi.validation.support; + +import ca.uhn.fhir.rest.api.Constants; +import io.opentelemetry.api.GlobalOpenTelemetry; +import io.opentelemetry.api.OpenTelemetry; +import io.opentelemetry.api.common.AttributeKey; +import io.opentelemetry.api.common.Attributes; +import io.opentelemetry.api.metrics.BatchCallback; +import io.opentelemetry.api.metrics.Meter; +import io.opentelemetry.api.metrics.MeterBuilder; +import io.opentelemetry.api.metrics.ObservableLongMeasurement; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import static io.opentelemetry.api.common.AttributeKey.stringKey; + +/** + * This class provides OpenTelemetry metrics for the {@link ValidationSupportChain} cache. + */ +public class ValidationSupportChainMetrics { + + /* + * See ValidationSupportChainTest#testMetrics for a unit test + * which exercises the functionality in this class. + */ + + public static final String CLASS_OPENTELEMETRY_BASE_NAME = + Constants.OPENTELEMETRY_BASE_NAME + ".validation_support_chain"; + static final String INSTRUMENTATION_NAME = CLASS_OPENTELEMETRY_BASE_NAME; + private static final AttributeKey INSTANCE_NAME = stringKey(INSTRUMENTATION_NAME + ".instance_name"); + public static final String EXPIRING_CACHE_MAXIMUM_SIZE = + CLASS_OPENTELEMETRY_BASE_NAME + ".expiring_cache.maximum_size"; + public static final String EXPIRING_CACHE_CURRENT_ENTRIES = + CLASS_OPENTELEMETRY_BASE_NAME + ".expiring_cache.current_entries"; + public static final String NON_EXPIRING_CACHE_CURRENT_ENTRIES = + CLASS_OPENTELEMETRY_BASE_NAME + ".non_expiring_cache.current_entries"; + private static final Logger ourLog = LoggerFactory.getLogger(ValidationSupportChainMetrics.class); + private final ValidationSupportChain myValidationSupportChain; + private BatchCallback myBatchCallback; + + public ValidationSupportChainMetrics(ValidationSupportChain theValidationSupportChain) { + myValidationSupportChain = theValidationSupportChain; + } + + public void start() { + OpenTelemetry openTelemetry = GlobalOpenTelemetry.get(); + MeterBuilder meterBuilder = openTelemetry.getMeterProvider().meterBuilder(INSTRUMENTATION_NAME); + Meter meter = meterBuilder.build(); + + Attributes baseAttribute = Attributes.of(INSTANCE_NAME, myValidationSupportChain.getName()); + + ObservableLongMeasurement expiringCacheMaxSize = meter.gaugeBuilder(EXPIRING_CACHE_MAXIMUM_SIZE) + .ofLongs() + .setUnit("{entries}") + .setDescription("The maximum number of cache entries in the expiring cache.") + .buildObserver(); + ObservableLongMeasurement expiringCacheCurrentEntries = meter.gaugeBuilder(EXPIRING_CACHE_CURRENT_ENTRIES) + .ofLongs() + .setUnit("{entries}") + .setDescription("The current number of cache entries in the expiring cache.") + .buildObserver(); + ObservableLongMeasurement nonExpiringCacheCurrentEntries = meter.gaugeBuilder( + NON_EXPIRING_CACHE_CURRENT_ENTRIES) + .ofLongs() + .setUnit("{entries}") + .setDescription("The current number of cache entries in the non-expiring cache.") + .buildObserver(); + + myBatchCallback = meter.batchCallback( + () -> { + long expiringCacheEntries = myValidationSupportChain.getMetricExpiringCacheEntries(); + int expiringCacheMaxSizeValue = myValidationSupportChain.getMetricExpiringCacheMaxSize(); + int nonExpiringCacheEntries = myValidationSupportChain.getMetricNonExpiringCacheEntries(); + ourLog.trace( + "ExpiringMax[{}] ExpiringEntries[{}] NonExpiringEntries[{}]", + expiringCacheMaxSizeValue, + expiringCacheEntries, + nonExpiringCacheEntries); + expiringCacheMaxSize.record(expiringCacheMaxSizeValue, baseAttribute); + expiringCacheCurrentEntries.record(expiringCacheEntries, baseAttribute); + nonExpiringCacheCurrentEntries.record(nonExpiringCacheEntries, baseAttribute); + }, + expiringCacheMaxSize, + expiringCacheCurrentEntries, + nonExpiringCacheCurrentEntries); + } + + public void stop() { + if (myBatchCallback != null) { + myBatchCallback.close(); + } + } +} 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 393d8ce1dc5..ac4eb92581c 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 @@ -1,21 +1,23 @@ package org.hl7.fhir.common.hapi.validation.validator; +import ca.uhn.fhir.context.FhirContext; import ca.uhn.fhir.context.FhirVersionEnum; import ca.uhn.fhir.context.support.ConceptValidationOptions; +import ca.uhn.fhir.context.support.DefaultProfileValidationSupport; import ca.uhn.fhir.context.support.IValidationSupport; import ca.uhn.fhir.context.support.ValidationSupportContext; import ca.uhn.fhir.i18n.Msg; +import ca.uhn.fhir.rest.server.exceptions.BaseServerResponseException; import ca.uhn.fhir.rest.server.exceptions.InternalErrorException; -import ca.uhn.fhir.sl.cache.CacheFactory; -import ca.uhn.fhir.sl.cache.LoadingCache; -import ca.uhn.fhir.system.HapiSystemProperties; import ca.uhn.hapi.converters.canonical.VersionCanonicalizer; import jakarta.annotation.Nonnull; import jakarta.annotation.Nullable; import org.apache.commons.lang3.Validate; import org.apache.commons.lang3.builder.EqualsBuilder; import org.apache.commons.lang3.builder.HashCodeBuilder; +import org.apache.commons.lang3.exception.ExceptionUtils; import org.fhir.ucum.UcumService; +import org.hl7.fhir.common.hapi.validation.support.SnapshotGeneratingValidationSupport; import org.hl7.fhir.common.hapi.validation.support.ValidationSupportUtils; import org.hl7.fhir.exceptions.FHIRException; import org.hl7.fhir.exceptions.TerminologyServiceException; @@ -23,6 +25,7 @@ import org.hl7.fhir.instance.model.api.IBaseResource; import org.hl7.fhir.r5.context.IContextResourceLoader; import org.hl7.fhir.r5.context.IWorkerContext; import org.hl7.fhir.r5.context.IWorkerContextManager; +import org.hl7.fhir.r5.fhirpath.FHIRPathEngine; import org.hl7.fhir.r5.model.CodeSystem; import org.hl7.fhir.r5.model.CodeableConcept; import org.hl7.fhir.r5.model.Coding; @@ -56,11 +59,13 @@ import java.io.FileNotFoundException; import java.io.IOException; import java.util.ArrayList; import java.util.Collections; +import java.util.Date; import java.util.List; import java.util.Locale; import java.util.Map; import java.util.Objects; import java.util.Set; +import java.util.stream.Collectors; import static ca.uhn.fhir.context.support.IValidationSupport.CodeValidationIssueCoding.INVALID_DISPLAY; import static java.util.stream.Collectors.collectingAndThen; @@ -70,68 +75,33 @@ import static org.apache.commons.lang3.StringUtils.isNotBlank; public class VersionSpecificWorkerContextWrapper extends I18nBase implements IWorkerContext { private static final Logger ourLog = LoggerFactory.getLogger(VersionSpecificWorkerContextWrapper.class); + + /** + * When we fetch conformance resources such as StructureDefinitions from {@link IValidationSupport} + * they will be returned using whatever version of FHIR the underlying infrastructure is + * configured to support. But we need to convert it to R5 since that's what the validator + * uses. In order to avoid repeated conversions, we put the converted version of the resource + * in the {@link org.hl7.fhir.instance.model.api.IAnyResource#getUserData(String)} map + * using this key. Since conformance resources returned by validation support are typically + * cached by {@link org.hl7.fhir.common.hapi.validation.support.ValidationSupportChain}, + * the converted version gets cached too. + */ + private static final String CANONICAL_USERDATA_KEY = + VersionSpecificWorkerContextWrapper.class.getName() + "_CANONICAL_USERDATA_KEY"; + + public static final FhirContext FHIR_CONTEXT_R5 = FhirContext.forR5(); private final ValidationSupportContext myValidationSupportContext; private final VersionCanonicalizer myVersionCanonicalizer; - private final LoadingCache myFetchResourceCache; private volatile List myAllStructures; private volatile Set myAllPrimitiveTypes; private Parameters myExpansionProfile; + private volatile FHIRPathEngine myFHIRPathEngine; public VersionSpecificWorkerContextWrapper( ValidationSupportContext theValidationSupportContext, VersionCanonicalizer theVersionCanonicalizer) { myValidationSupportContext = theValidationSupportContext; myVersionCanonicalizer = theVersionCanonicalizer; - long timeoutMillis = HapiSystemProperties.getTestValidationResourceCachesMs(); - - myFetchResourceCache = CacheFactory.build(timeoutMillis, 10000, key -> { - String fetchResourceName = key.getResourceName(); - if (myValidationSupportContext - .getRootValidationSupport() - .getFhirContext() - .getVersion() - .getVersion() - == FhirVersionEnum.DSTU2) { - if ("CodeSystem".equals(fetchResourceName)) { - fetchResourceName = "ValueSet"; - } - } - - Class fetchResourceType; - if (fetchResourceName.equals("Resource")) { - fetchResourceType = null; - } else { - fetchResourceType = myValidationSupportContext - .getRootValidationSupport() - .getFhirContext() - .getResourceDefinition(fetchResourceName) - .getImplementingClass(); - } - - IBaseResource fetched = myValidationSupportContext - .getRootValidationSupport() - .fetchResource(fetchResourceType, key.getUri()); - - Resource canonical = myVersionCanonicalizer.resourceToValidatorCanonical(fetched); - - if (canonical instanceof StructureDefinition) { - StructureDefinition canonicalSd = (StructureDefinition) canonical; - if (canonicalSd.getSnapshot().isEmpty()) { - ourLog.info("Generating snapshot for StructureDefinition: {}", canonicalSd.getUrl()); - fetched = myValidationSupportContext - .getRootValidationSupport() - .generateSnapshot(theValidationSupportContext, fetched, "", null, ""); - Validate.isTrue( - fetched != null, - "StructureDefinition %s has no snapshot, and no snapshot generator is configured", - key.getUri()); - canonical = myVersionCanonicalizer.resourceToValidatorCanonical(fetched); - } - } - - return canonical; - }); - setValidationMessageLanguage(getLocale()); } @@ -237,21 +207,45 @@ public class VersionSpecificWorkerContextWrapper extends I18nBase implements IWo myExpansionProfile = expParameters; } - private List allStructures() { + private List allStructureDefinitions() { List retVal = myAllStructures; if (retVal == null) { - retVal = new ArrayList<>(); - for (IBaseResource next : - myValidationSupportContext.getRootValidationSupport().fetchAllStructureDefinitions()) { - try { - StructureDefinition converted = myVersionCanonicalizer.structureDefinitionToCanonical(next); - retVal.add(converted); - } catch (FHIRException e) { - throw new InternalErrorException(Msg.code(659) + e); - } - } + List allStructureDefinitions = + myValidationSupportContext.getRootValidationSupport().fetchAllStructureDefinitions(); + assert allStructureDefinitions != null; + + /* + * This method (allStructureDefinitions()) gets called recursively - As we + * try to return all StructureDefinitions, we want to generate snapshots for + * any that don't already have a snapshot. But the snapshot generator in turn + * also calls allStructureDefinitions() - That specific call doesn't require + * that the returned SDs have snapshots generated though. + * + * So, we first just convert to canonical version and store a list containing + * the canonical versions. That way any recursive calls will return the + * stored list. But after that we'll generate all the snapshots and + * store that list instead. If the canonicalization fails with an + * unexpected exception, we wipe the stored list. This is probably an + * unrecoverable failure since this method will probably always + * fail if it fails once. But at least this way we're likley to + * generate useful error messages for the user. + */ + retVal = allStructureDefinitions.stream() + .map(t -> myVersionCanonicalizer.structureDefinitionToCanonical(t)) + .collect(Collectors.toList()); myAllStructures = retVal; + + try { + for (IBaseResource next : allStructureDefinitions) { + Resource converted = convertToCanonicalVersionAndGenerateSnapshot(next, false); + retVal.add((StructureDefinition) converted); + } + myAllStructures = retVal; + } catch (Exception e) { + ourLog.error("Failure during snapshot generation", e); + myAllStructures = null; + } } return retVal; @@ -477,9 +471,9 @@ public class VersionSpecificWorkerContextWrapper extends I18nBase implements IWo } } - ResourceKey key = new ResourceKey(class_.getSimpleName(), uri); + String resourceType = getResourceType(class_); @SuppressWarnings("unchecked") - T retVal = (T) myFetchResourceCache.get(key); + T retVal = (T) fetchResource(resourceType, uri); return retVal; } @@ -574,7 +568,7 @@ public class VersionSpecificWorkerContextWrapper extends I18nBase implements IWo @Override public List fetchTypeDefinitions(String theTypeName) { - List allStructures = new ArrayList<>(allStructures()); + List allStructures = new ArrayList<>(allStructureDefinitions()); allStructures.removeIf(sd -> !sd.hasType() || !sd.getType().equals(theTypeName)); return allStructures; } @@ -593,7 +587,7 @@ public class VersionSpecificWorkerContextWrapper extends I18nBase implements IWo Set retVal = myAllPrimitiveTypes; if (retVal == null) { // Collector may be changed to Collectors.toUnmodifiableSet() when switching to Android API level >= 33 - retVal = allStructures().stream() + retVal = allStructureDefinitions().stream() .filter(structureDefinition -> structureDefinition.getKind() == StructureDefinition.StructureDefinitionKind.PRIMITIVETYPE) .map(StructureDefinition::getName) @@ -636,8 +630,15 @@ public class VersionSpecificWorkerContextWrapper extends I18nBase implements IWo return false; } - ResourceKey key = new ResourceKey(class_.getSimpleName(), uri); - return myFetchResourceCache.get(key) != null; + String resourceType = getResourceType(class_); + return fetchResource(resourceType, uri) != null; + } + + private static String getResourceType(Class theClass) { + if (theClass.getSimpleName().equals("Resource")) { + return "Resource"; + } + return FHIR_CONTEXT_R5.getResourceType(theClass); } @Override @@ -815,7 +816,7 @@ public class VersionSpecificWorkerContextWrapper extends I18nBase implements IWo .getRootValidationSupport() .validateCodeInValueSet( myValidationSupportContext, theValidationOptions, theSystem, theCode, theDisplay, theValueSet); - if (result != null && theSystem != null) { + if (result != null && isNotBlank(theSystem)) { /* We got a value set result, which could be successful, or could contain errors/warnings. The code might also be invalid in the code system, so we will check that as well and add those issues to our result. @@ -878,7 +879,7 @@ public class VersionSpecificWorkerContextWrapper extends I18nBase implements IWo } if (code.getCoding().size() > 0) { - if (!myValidationSupportContext.isEnabledValidationForCodingsLogicalAnd()) { + if (!myValidationSupportContext.isCodeableConceptValidationSuccessfulIfNotAllCodingsAreValid()) { if (validationResultsOk.size() == code.getCoding().size()) { return validationResultsOk.get(0); } @@ -905,13 +906,13 @@ public class VersionSpecificWorkerContextWrapper extends I18nBase implements IWo } public void invalidateCaches() { - myFetchResourceCache.invalidateAll(); + // nothing for now } @Override public List fetchResourcesByType(Class theClass) { if (theClass.equals(StructureDefinition.class)) { - return (List) allStructures(); + return (List) allStructureDefinitions(); } throw new UnsupportedOperationException(Msg.code(650) + "Unable to fetch resources of type: " + theClass); } @@ -1033,4 +1034,109 @@ public class VersionSpecificWorkerContextWrapper extends I18nBase implements IWo public boolean isServerSideSystem(String url) { return false; } + + private IBaseResource fetchResource(String theResourceType, String theUrl) { + String fetchResourceName = theResourceType; + if (myValidationSupportContext + .getRootValidationSupport() + .getFhirContext() + .getVersion() + .getVersion() + == FhirVersionEnum.DSTU2) { + if ("CodeSystem".equals(fetchResourceName)) { + fetchResourceName = "ValueSet"; + } + } + + Class fetchResourceType; + if (fetchResourceName.equals("Resource")) { + fetchResourceType = null; + } else { + fetchResourceType = myValidationSupportContext + .getRootValidationSupport() + .getFhirContext() + .getResourceDefinition(fetchResourceName) + .getImplementingClass(); + } + + IBaseResource fetched = + myValidationSupportContext.getRootValidationSupport().fetchResource(fetchResourceType, theUrl); + + if (fetched == null) { + return null; + } + + return convertToCanonicalVersionAndGenerateSnapshot(fetched, true); + } + + private Resource convertToCanonicalVersionAndGenerateSnapshot( + @Nonnull IBaseResource theResource, boolean thePropagateSnapshotException) { + Resource canonical; + synchronized (theResource) { + canonical = (Resource) theResource.getUserData(CANONICAL_USERDATA_KEY); + if (canonical == null) { + boolean storeCanonical = true; + canonical = myVersionCanonicalizer.resourceToValidatorCanonical(theResource); + + if (canonical instanceof StructureDefinition) { + StructureDefinition canonicalSd = (StructureDefinition) canonical; + if (canonicalSd.getSnapshot().isEmpty()) { + ourLog.info("Generating snapshot for StructureDefinition: {}", canonicalSd.getUrl()); + IBaseResource resource = theResource; + try { + + FhirContext fhirContext = myValidationSupportContext + .getRootValidationSupport() + .getFhirContext(); + SnapshotGeneratingValidationSupport snapshotGenerator = + new SnapshotGeneratingValidationSupport(fhirContext, this, getFHIRPathEngine()); + resource = snapshotGenerator.generateSnapshot( + myValidationSupportContext, resource, "", null, ""); + Validate.isTrue( + resource != null, + "StructureDefinition %s has no snapshot, and no snapshot generator is configured", + canonicalSd.getUrl()); + + } catch (BaseServerResponseException e) { + if (thePropagateSnapshotException) { + throw e; + } + String message = e.toString(); + Throwable rootCause = ExceptionUtils.getRootCause(e); + if (rootCause != null) { + message = rootCause.getMessage(); + } + ourLog.warn( + "Failed to generate snapshot for profile with URL[{}]: {}", + canonicalSd.getUrl(), + message); + storeCanonical = false; + } + + canonical = myVersionCanonicalizer.resourceToValidatorCanonical(resource); + } + } + + String sourcePackageId = + (String) theResource.getUserData(DefaultProfileValidationSupport.SOURCE_PACKAGE_ID); + if (sourcePackageId != null) { + canonical.setSourcePackage(new PackageInformation(sourcePackageId, null, null, new Date())); + } + + if (storeCanonical) { + theResource.setUserData(CANONICAL_USERDATA_KEY, canonical); + } + } + } + return canonical; + } + + private FHIRPathEngine getFHIRPathEngine() { + FHIRPathEngine retVal = myFHIRPathEngine; + if (retVal == null) { + retVal = new FHIRPathEngine(this); + myFHIRPathEngine = retVal; + } + return retVal; + } } diff --git a/hapi-fhir-validation/src/test/java/org/hl7/fhir/common/hapi/validation/support/CachingValidationSupportTest.java b/hapi-fhir-validation/src/test/java/org/hl7/fhir/common/hapi/validation/support/CachingValidationSupportTest.java index 6eb2894630e..d59fe708c0d 100644 --- a/hapi-fhir-validation/src/test/java/org/hl7/fhir/common/hapi/validation/support/CachingValidationSupportTest.java +++ b/hapi-fhir-validation/src/test/java/org/hl7/fhir/common/hapi/validation/support/CachingValidationSupportTest.java @@ -7,6 +7,7 @@ import jakarta.annotation.Nonnull; import jakarta.annotation.Nullable; import org.hl7.fhir.instance.model.api.IBaseResource; import org.hl7.fhir.r4.model.StructureDefinition; +import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.NullSource; @@ -21,98 +22,43 @@ import static ca.uhn.fhir.util.TestUtil.sleepAtLeast; import static org.assertj.core.api.Assertions.assertThat; import static org.awaitility.Awaitility.await; import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotSame; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.mockito.ArgumentMatchers.any; import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; +/** + * CachingValidationSupport is deprecated and is just a passthrough now. This + * test verifies that it works that way. + */ +@SuppressWarnings("removal") @ExtendWith(MockitoExtension.class) public class CachingValidationSupportTest { private static final FhirContext ourCtx = FhirContext.forR4Cached(); @Mock - private IValidationSupport myValidationSupport; + private IValidationSupport myValidationSupport0; - @ParameterizedTest - @NullSource - @ValueSource(booleans = {true, false}) - public void testAsyncBackgroundLoading(Boolean theIsEnabledValidationForCodingsLogicalAnd) { - StructureDefinition sd0 = (StructureDefinition) new StructureDefinition().setId("SD0"); - StructureDefinition sd1 = (StructureDefinition) new StructureDefinition().setId("SD1"); - StructureDefinition sd2 = (StructureDefinition) new StructureDefinition().setId("SD2"); - List responses = Collections.synchronizedList(Lists.newArrayList( - sd0, sd1, sd2 - )); + @Test + public void testNoCaching() { + when(myValidationSupport0.getFhirContext()).thenReturn(ourCtx); + when(myValidationSupport0.fetchStructureDefinition(any())).thenAnswer(t->new StructureDefinition()); - when(myValidationSupport.getFhirContext()).thenReturn(ourCtx); - when(myValidationSupport.fetchAllNonBaseStructureDefinitions()).thenAnswer(t -> { - Thread.sleep(2000); - return Collections.singletonList(responses.remove(0)); - }); + CachingValidationSupport support = new CachingValidationSupport(myValidationSupport0); - final CachingValidationSupport.CacheTimeouts cacheTimeouts = CachingValidationSupport.CacheTimeouts - .defaultValues() - .setMiscMillis(1000); - final CachingValidationSupport support = getSupport(cacheTimeouts, theIsEnabledValidationForCodingsLogicalAnd); - - assertThat(responses).hasSize(3); - List fetched = support.fetchAllNonBaseStructureDefinitions(); - assert fetched != null; - assertThat(fetched.get(0)).isSameAs(sd0); - assertThat(responses).hasSize(2); - - sleepAtLeast(1200); - fetched = support.fetchAllNonBaseStructureDefinitions(); - assert fetched != null; - assertThat(fetched.get(0)).isSameAs(sd0); - assertThat(responses).hasSize(2); - - await().until(() -> responses.size() == 1); - assertThat(responses).hasSize(1); - fetched = support.fetchAllNonBaseStructureDefinitions(); - assert fetched != null; - assertThat(fetched.get(0)).isSameAs(sd1); - assertThat(responses).hasSize(1); - - assertEquals(theIsEnabledValidationForCodingsLogicalAnd != null && theIsEnabledValidationForCodingsLogicalAnd, support.isEnabledValidationForCodingsLogicalAnd()); + IBaseResource actual0 = support.fetchStructureDefinition("http://foo"); + IBaseResource actual1 = support.fetchStructureDefinition("http://foo"); + assertNotSame(actual0, actual1); } - @ParameterizedTest - @NullSource - @ValueSource(booleans = {true, false}) - public void fetchBinary_normally_accessesSuperOnlyOnce(Boolean theIsEnabledValidationForCodingsLogicalAnd) { - final byte[] EXPECTED_BINARY = "dummyBinaryContent".getBytes(); - final String EXPECTED_BINARY_KEY = "dummyBinaryKey"; - when(myValidationSupport.getFhirContext()).thenReturn(ourCtx); - when(myValidationSupport.fetchBinary(EXPECTED_BINARY_KEY)).thenReturn(EXPECTED_BINARY); - - final CachingValidationSupport support = getSupport(null, theIsEnabledValidationForCodingsLogicalAnd); - - final byte[] firstActualBinary = support.fetchBinary(EXPECTED_BINARY_KEY); - assertEquals(EXPECTED_BINARY, firstActualBinary); - verify(myValidationSupport, times(1)).fetchBinary(EXPECTED_BINARY_KEY); - - final byte[] secondActualBinary = support.fetchBinary(EXPECTED_BINARY_KEY); - assertEquals(EXPECTED_BINARY, secondActualBinary); - verify(myValidationSupport, times(1)).fetchBinary(EXPECTED_BINARY_KEY); - - assertEquals(theIsEnabledValidationForCodingsLogicalAnd != null && theIsEnabledValidationForCodingsLogicalAnd, support.isEnabledValidationForCodingsLogicalAnd()); + @Test + public void testEnabledValidationForCodingsLogicalAnd() { + when(myValidationSupport0.getFhirContext()).thenReturn(ourCtx); + CachingValidationSupport support = new CachingValidationSupport(myValidationSupport0, true); + assertTrue(support.isCodeableConceptValidationSuccessfulIfNotAllCodingsAreValid()); } - @Nonnull - private CachingValidationSupport getSupport(@Nullable CachingValidationSupport.CacheTimeouts theCacheTimeouts, @Nullable Boolean theIsEnabledValidationForCodingsLogicalAnd) { - if (theCacheTimeouts == null) { - if (theIsEnabledValidationForCodingsLogicalAnd == null) { - return new CachingValidationSupport(myValidationSupport); - } - - return new CachingValidationSupport(myValidationSupport, theIsEnabledValidationForCodingsLogicalAnd); - } - - if (theIsEnabledValidationForCodingsLogicalAnd == null) { - return new CachingValidationSupport(myValidationSupport, theCacheTimeouts); - } - - return new CachingValidationSupport(myValidationSupport, theCacheTimeouts, theIsEnabledValidationForCodingsLogicalAnd); - } } diff --git a/hapi-fhir-validation/src/test/java/org/hl7/fhir/common/hapi/validation/support/ValidationSupportChainTest.java b/hapi-fhir-validation/src/test/java/org/hl7/fhir/common/hapi/validation/support/ValidationSupportChainTest.java index cbb8f87ac4d..c04c9af3376 100644 --- a/hapi-fhir-validation/src/test/java/org/hl7/fhir/common/hapi/validation/support/ValidationSupportChainTest.java +++ b/hapi-fhir-validation/src/test/java/org/hl7/fhir/common/hapi/validation/support/ValidationSupportChainTest.java @@ -2,41 +2,108 @@ package org.hl7.fhir.common.hapi.validation.support; import ca.uhn.fhir.context.ConfigurationException; import ca.uhn.fhir.context.FhirContext; -import ca.uhn.fhir.context.FhirVersionEnum; +import ca.uhn.fhir.context.support.ConceptValidationOptions; import ca.uhn.fhir.context.support.DefaultProfileValidationSupport; import ca.uhn.fhir.context.support.IValidationSupport; -import ca.uhn.fhir.fhirpath.BaseValidationTestWithInlineMocks; +import ca.uhn.fhir.context.support.LookupCodeRequest; +import ca.uhn.fhir.context.support.TranslateConceptResult; +import ca.uhn.fhir.context.support.TranslateConceptResults; +import ca.uhn.fhir.context.support.ValidationSupportContext; +import ca.uhn.fhir.context.support.ValueSetExpansionOptions; import ca.uhn.fhir.i18n.Msg; +import ca.uhn.fhir.test.BaseTest; +import ca.uhn.fhir.util.TestUtil; +import com.google.common.collect.Lists; +import io.opentelemetry.instrumentation.testing.LibraryTestRunner; +import io.opentelemetry.sdk.metrics.data.Data; +import io.opentelemetry.sdk.metrics.data.LongPointData; +import io.opentelemetry.sdk.metrics.data.MetricData; +import jakarta.annotation.Nonnull; +import org.hl7.fhir.instance.model.api.IBaseResource; +import org.hl7.fhir.r4.model.ListResource; +import org.hl7.fhir.r4.model.SearchParameter; +import org.hl7.fhir.r4.model.StructureDefinition; +import org.hl7.fhir.r4.model.ValueSet; import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.CsvSource; +import org.junit.jupiter.params.provider.ValueSource; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.time.Duration; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.stream.Collectors; import static org.assertj.core.api.Assertions.assertThat; +import static org.awaitility.Awaitility.await; import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertNotSame; import static org.junit.jupiter.api.Assertions.assertNull; -import static org.mockito.Mockito.mock; +import static org.junit.jupiter.api.Assertions.assertSame; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.reset; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.verifyNoInteractions; +import static org.mockito.Mockito.verifyNoMoreInteractions; import static org.mockito.Mockito.when; -public class ValidationSupportChainTest extends BaseValidationTestWithInlineMocks { +@ExtendWith(MockitoExtension.class) +public class ValidationSupportChainTest extends BaseTest { + public static final String CODE_SYSTEM_URL_0 = "http://code-system-url-0"; + public static final String CODE_0 = "code-0"; + public static final String DISPLAY_0 = "display-0"; + public static final String VALUE_SET_URL_0 = "http://value-set-url-0"; + private static final Logger ourLog = LoggerFactory.getLogger(ValidationSupportChainTest.class); + @Mock(strictness = Mock.Strictness.LENIENT) + private IValidationSupport myValidationSupport0; + @Mock(strictness = Mock.Strictness.LENIENT) + private IValidationSupport myValidationSupport1; + @Mock(strictness = Mock.Strictness.LENIENT) + private IValidationSupport myValidationSupport2; @Test public void testVersionCheck() { - - DefaultProfileValidationSupport ctx3 = new DefaultProfileValidationSupport(FhirContext.forDstu3()); - DefaultProfileValidationSupport ctx4 = new DefaultProfileValidationSupport(FhirContext.forR4()); + DefaultProfileValidationSupport ctx3 = new DefaultProfileValidationSupport(FhirContext.forDstu3Cached()); + DefaultProfileValidationSupport ctx4 = new DefaultProfileValidationSupport(FhirContext.forR4Cached()); try { new ValidationSupportChain(ctx3, ctx4); } catch (ConfigurationException e) { assertEquals(Msg.code(709) + "Trying to add validation support of version R4 to chain with 1 entries of version DSTU3", e.getMessage()); } + } + + + @Test + public void testFetchIndividualStructureDefinitionThenAll() { + DefaultProfileValidationSupport ctx = new DefaultProfileValidationSupport(FhirContext.forR4Cached()); + ValidationSupportChain chain = new ValidationSupportChain(ctx); + + assertNotNull(chain.fetchStructureDefinition("http://hl7.org/fhir/StructureDefinition/Patient")); + assertEquals(649, chain.fetchAllStructureDefinitions().size()); } + @Test public void testMissingContext() { - IValidationSupport ctx = mock(IValidationSupport.class); + when(myValidationSupport0.getFhirContext()).thenReturn(null); + try { - new ValidationSupportChain(ctx); + new ValidationSupportChain(myValidationSupport0); } catch (ConfigurationException e) { assertEquals(Msg.code(708) + "Can not add validation support: getFhirContext() returns null", e.getMessage()); } @@ -44,39 +111,657 @@ public class ValidationSupportChainTest extends BaseValidationTestWithInlineMock @Test public void fetchBinary_normally_returnsExpectedBinaries() { - + // Setup final byte[] EXPECTED_BINARY_CONTENT_1 = "dummyBinaryContent1".getBytes(); final byte[] EXPECTED_BINARY_CONTENT_2 = "dummyBinaryContent2".getBytes(); final String EXPECTED_BINARY_KEY_1 = "dummyBinaryKey1"; final String EXPECTED_BINARY_KEY_2 = "dummyBinaryKey2"; + prepareMock(myValidationSupport0); + prepareMock(myValidationSupport1); + createMockValidationSupportWithSingleBinary(myValidationSupport0, EXPECTED_BINARY_KEY_1, EXPECTED_BINARY_CONTENT_1); + createMockValidationSupportWithSingleBinary(myValidationSupport1, EXPECTED_BINARY_KEY_2, EXPECTED_BINARY_CONTENT_2); - IValidationSupport validationSupport1 = createMockValidationSupportWithSingleBinary(EXPECTED_BINARY_KEY_1, EXPECTED_BINARY_CONTENT_1); - IValidationSupport validationSupport2 = createMockValidationSupportWithSingleBinary(EXPECTED_BINARY_KEY_2, EXPECTED_BINARY_CONTENT_2); - - ValidationSupportChain validationSupportChain = new ValidationSupportChain(validationSupport1, validationSupport2); - - final byte[] actualBinaryContent1 = validationSupportChain.fetchBinary(EXPECTED_BINARY_KEY_1 ); - final byte[] actualBinaryContent2 = validationSupportChain.fetchBinary(EXPECTED_BINARY_KEY_2 ); + // Test + ValidationSupportChain validationSupportChain = new ValidationSupportChain(myValidationSupport0, myValidationSupport1); + final byte[] actualBinaryContent1 = validationSupportChain.fetchBinary(EXPECTED_BINARY_KEY_1); + final byte[] actualBinaryContent2 = validationSupportChain.fetchBinary(EXPECTED_BINARY_KEY_2); + // Verify assertThat(actualBinaryContent1).containsExactly(EXPECTED_BINARY_CONTENT_1); assertThat(actualBinaryContent2).containsExactly(EXPECTED_BINARY_CONTENT_2); assertNull(validationSupportChain.fetchBinary("nonExistentKey")); } + @ParameterizedTest + @ValueSource(booleans = {true, false}) + public void testValidateCode_WithValueSetUrl(boolean theUseCache) { + // Setup + prepareMock(myValidationSupport0, myValidationSupport1, myValidationSupport2); + ValidationSupportChain chain = new ValidationSupportChain(newCacheConfiguration(theUseCache), myValidationSupport0, myValidationSupport1, myValidationSupport2); - private static IValidationSupport createMockValidationSupport() { - IValidationSupport validationSupport; - validationSupport = mock(IValidationSupport.class); - FhirContext mockContext = mock(FhirContext.class); - when(mockContext.getVersion()).thenReturn(FhirVersionEnum.R4.getVersionImplementation()); - when(validationSupport.getFhirContext()).thenReturn(mockContext); - return validationSupport; + when(myValidationSupport0.isValueSetSupported(any(), eq(VALUE_SET_URL_0))).thenReturn(false); + when(myValidationSupport1.isValueSetSupported(any(), eq(VALUE_SET_URL_0))).thenReturn(true); + when(myValidationSupport1.validateCode(any(), any(), any(), any(), any(), any())).thenAnswer(t -> new IValidationSupport.CodeValidationResult()); + + // Test + IValidationSupport.CodeValidationResult result = chain.validateCode(newValidationCtx(chain), new ConceptValidationOptions(), CODE_SYSTEM_URL_0, CODE_0, DISPLAY_0, VALUE_SET_URL_0); + + // Verify + verify(myValidationSupport0, times(1)).isValueSetSupported(any(), eq(VALUE_SET_URL_0)); + verify(myValidationSupport1, times(1)).isValueSetSupported(any(), eq(VALUE_SET_URL_0)); + verify(myValidationSupport2, never()).isValueSetSupported(any(), eq(VALUE_SET_URL_0)); + verify(myValidationSupport0, never()).validateCode(any(), any(), any(), any(), any(), any()); + verify(myValidationSupport1, times(1)).validateCode(any(), any(), any(), any(), any(), any()); + verify(myValidationSupport2, never()).validateCode(any(), any(), any(), any(), any(), any()); + + // Setup for second execution (should use cache this time) + prepareMock(myValidationSupport0, myValidationSupport1, myValidationSupport2); + when(myValidationSupport0.isValueSetSupported(any(), eq(VALUE_SET_URL_0))).thenReturn(false); + when(myValidationSupport1.isValueSetSupported(any(), eq(VALUE_SET_URL_0))).thenReturn(true); + when(myValidationSupport1.validateCode(any(), any(), any(), any(), any(), any())).thenAnswer(t -> new IValidationSupport.CodeValidationResult()); + + // Test again (should use cache) + IValidationSupport.CodeValidationResult result2 = chain.validateCode(newValidationCtx(chain), new ConceptValidationOptions(), CODE_SYSTEM_URL_0, CODE_0, DISPLAY_0, VALUE_SET_URL_0); + + // Verify + if (theUseCache) { + assertSame(result, result2); + verifyNoInteractions(myValidationSupport0, myValidationSupport1, myValidationSupport2); + } else { + assertNotSame(result, result2); + verify(myValidationSupport0, times(1)).isValueSetSupported(any(), eq(VALUE_SET_URL_0)); + verify(myValidationSupport1, times(1)).isValueSetSupported(any(), eq(VALUE_SET_URL_0)); + verify(myValidationSupport2, never()).isValueSetSupported(any(), eq(VALUE_SET_URL_0)); + verify(myValidationSupport0, never()).validateCode(any(), any(), any(), any(), any(), any()); + verify(myValidationSupport1, times(1)).validateCode(any(), any(), any(), any(), any(), any()); + verify(myValidationSupport2, never()).validateCode(any(), any(), any(), any(), any(), any()); + } + } + + @ParameterizedTest + @ValueSource(booleans = {true, false}) + public void testValidateCode_WithoutValueSetUrl(boolean theUseCache) { + // Setup + prepareMock(myValidationSupport0, myValidationSupport1, myValidationSupport2); + ValidationSupportChain chain = new ValidationSupportChain(newCacheConfiguration(theUseCache), myValidationSupport0, myValidationSupport1, myValidationSupport2); + + when(myValidationSupport0.isCodeSystemSupported(any(), eq(CODE_SYSTEM_URL_0))).thenReturn(false); + when(myValidationSupport1.isCodeSystemSupported(any(), eq(CODE_SYSTEM_URL_0))).thenReturn(true); + when(myValidationSupport1.validateCode(any(), any(), any(), any(), any(), any())).thenAnswer(t -> new IValidationSupport.CodeValidationResult()); + + // Test + IValidationSupport.CodeValidationResult result = chain.validateCode(newValidationCtx(chain), new ConceptValidationOptions(), CODE_SYSTEM_URL_0, CODE_0, DISPLAY_0, null); + + // Verify + verify(myValidationSupport0, times(1)).isCodeSystemSupported(any(), eq(CODE_SYSTEM_URL_0)); + verify(myValidationSupport1, times(1)).isCodeSystemSupported(any(), eq(CODE_SYSTEM_URL_0)); + verify(myValidationSupport2, never()).isCodeSystemSupported(any(), any()); + verify(myValidationSupport0, never()).validateCode(any(), any(), any(), any(), any(), any()); + verify(myValidationSupport1, times(1)).validateCode(any(), any(), any(), any(), any(), any()); + verify(myValidationSupport2, never()).validateCode(any(), any(), any(), any(), any(), any()); + + // Setup for second execution (should use cache this time) + prepareMock(myValidationSupport0, myValidationSupport1, myValidationSupport2); + when(myValidationSupport0.isCodeSystemSupported(any(), eq(CODE_SYSTEM_URL_0))).thenReturn(false); + when(myValidationSupport1.isCodeSystemSupported(any(), eq(CODE_SYSTEM_URL_0))).thenReturn(true); + when(myValidationSupport1.validateCode(any(), any(), any(), any(), any(), any())).thenAnswer(t -> new IValidationSupport.CodeValidationResult()); + + // Test again (should use cache) + IValidationSupport.CodeValidationResult result2 = chain.validateCode(newValidationCtx(chain), new ConceptValidationOptions(), CODE_SYSTEM_URL_0, CODE_0, DISPLAY_0, null); + + // Verify + if (theUseCache) { + assertSame(result, result2); + verifyNoInteractions(myValidationSupport0, myValidationSupport1, myValidationSupport2); + } else { + assertNotSame(result, result2); + verify(myValidationSupport0, times(1)).isCodeSystemSupported(any(), eq(CODE_SYSTEM_URL_0)); + verify(myValidationSupport1, times(1)).isCodeSystemSupported(any(), eq(CODE_SYSTEM_URL_0)); + verify(myValidationSupport2, never()).isCodeSystemSupported(any(), any()); + verify(myValidationSupport0, never()).validateCode(any(), any(), any(), any(), any(), any()); + verify(myValidationSupport1, times(1)).validateCode(any(), any(), any(), any(), any(), any()); + verify(myValidationSupport2, never()).validateCode(any(), any(), any(), any(), any(), any()); + } + } + + @ParameterizedTest + @CsvSource({ + "true, true", + "true, false", + "false, true", + "false, false", + }) + public void testValidateCodeInValueSet(boolean theUseCache, boolean theValueSetHasUrl) { + // Setup + prepareMock(myValidationSupport0, myValidationSupport1, myValidationSupport2); + ValidationSupportChain chain = new ValidationSupportChain(newCacheConfiguration(theUseCache), myValidationSupport0, myValidationSupport1, myValidationSupport2); + + when(myValidationSupport0.isValueSetSupported(any(), eq(VALUE_SET_URL_0))).thenReturn(false); + when(myValidationSupport1.isValueSetSupported(any(), eq(VALUE_SET_URL_0))).thenReturn(true); + when(myValidationSupport1.validateCodeInValueSet(any(), any(), any(), any(), any(), any())).thenAnswer(t -> new IValidationSupport.CodeValidationResult()); + + ValueSet inputValueSet = new ValueSet(); + if (theValueSetHasUrl) { + inputValueSet.setUrl(VALUE_SET_URL_0); + } + + // Test + IValidationSupport.CodeValidationResult result = chain.validateCodeInValueSet(newValidationCtx(chain), new ConceptValidationOptions(), CODE_SYSTEM_URL_0, CODE_0, DISPLAY_0, inputValueSet); + + // Verify + if (theValueSetHasUrl) { + verify(myValidationSupport0, times(1)).isValueSetSupported(any(), eq(VALUE_SET_URL_0)); + verify(myValidationSupport1, times(1)).isValueSetSupported(any(), eq(VALUE_SET_URL_0)); + verify(myValidationSupport2, never()).isValueSetSupported(any(), eq(VALUE_SET_URL_0)); + verify(myValidationSupport0, never()).validateCodeInValueSet(any(), any(), any(), any(), any(), any()); + verify(myValidationSupport1, times(1)).validateCodeInValueSet(any(), any(), any(), any(), any(), any()); + verify(myValidationSupport2, never()).validateCodeInValueSet(any(), any(), any(), any(), any(), any()); + } else { + verify(myValidationSupport0, never()).isValueSetSupported(any(), eq(VALUE_SET_URL_0)); + verify(myValidationSupport1, never()).isValueSetSupported(any(), eq(VALUE_SET_URL_0)); + verify(myValidationSupport2, never()).isValueSetSupported(any(), eq(VALUE_SET_URL_0)); + verify(myValidationSupport0, times(1)).validateCodeInValueSet(any(), any(), any(), any(), any(), any()); + verify(myValidationSupport1, times(1)).validateCodeInValueSet(any(), any(), any(), any(), any(), any()); + verify(myValidationSupport2, never()).validateCodeInValueSet(any(), any(), any(), any(), any(), any()); + } + + // Setup for second execution (should use cache this time) + prepareMock(myValidationSupport0, myValidationSupport1, myValidationSupport2); + when(myValidationSupport0.isValueSetSupported(any(), eq(VALUE_SET_URL_0))).thenReturn(false); + when(myValidationSupport1.isValueSetSupported(any(), eq(VALUE_SET_URL_0))).thenReturn(true); + when(myValidationSupport1.validateCodeInValueSet(any(), any(), any(), any(), any(), any())).thenAnswer(t -> new IValidationSupport.CodeValidationResult()); + + // Test again (should use cache) + IValidationSupport.CodeValidationResult result2 = chain.validateCodeInValueSet(newValidationCtx(chain), new ConceptValidationOptions(), CODE_SYSTEM_URL_0, CODE_0, DISPLAY_0, inputValueSet); + + // Verify + if (theUseCache && theValueSetHasUrl) { + assertSame(result, result2); + if (theValueSetHasUrl) { + verify(myValidationSupport0, times(1)).getFhirContext(); + } + verifyNoMoreInteractions(myValidationSupport0, myValidationSupport1, myValidationSupport2); + } else { + assertNotSame(result, result2); + if (theValueSetHasUrl) { + verify(myValidationSupport0, times(1)).isValueSetSupported(any(), eq(VALUE_SET_URL_0)); + verify(myValidationSupport1, times(1)).isValueSetSupported(any(), eq(VALUE_SET_URL_0)); + verify(myValidationSupport2, never()).isValueSetSupported(any(), eq(VALUE_SET_URL_0)); + verify(myValidationSupport0, never()).validateCodeInValueSet(any(), any(), any(), any(), any(), any()); + } else { + verify(myValidationSupport0, never()).isValueSetSupported(any(), eq(VALUE_SET_URL_0)); + verify(myValidationSupport1, never()).isValueSetSupported(any(), eq(VALUE_SET_URL_0)); + verify(myValidationSupport2, never()).isValueSetSupported(any(), eq(VALUE_SET_URL_0)); + verify(myValidationSupport0, times(1)).validateCodeInValueSet(any(), any(), any(), any(), any(), any()); + } + + verify(myValidationSupport1, times(1)).validateCodeInValueSet(any(), any(), any(), any(), any(), any()); + verify(myValidationSupport2, never()).validateCodeInValueSet(any(), any(), any(), any(), any(), any()); + } + } + + @ParameterizedTest + @CsvSource({ + "true, true", + "true, false", + "false, true", + "false, false", + }) + public void testExpandValueSet_ValueSetParam(boolean theUseCache, boolean theValueSetHasUrl) { + // Setup + prepareMock(myValidationSupport0, myValidationSupport1, myValidationSupport2); + ValidationSupportChain chain = new ValidationSupportChain(newCacheConfiguration(theUseCache), myValidationSupport0, myValidationSupport1, myValidationSupport2); + + when(myValidationSupport0.isValueSetSupported(any(), eq(VALUE_SET_URL_0))).thenReturn(true); + when(myValidationSupport0.expandValueSet(any(), any(), any(IBaseResource.class))).thenReturn(null); + when(myValidationSupport1.isValueSetSupported(any(), eq(VALUE_SET_URL_0))).thenReturn(true); + when(myValidationSupport1.expandValueSet(any(), any(), any(IBaseResource.class))).thenAnswer(t -> new IValidationSupport.ValueSetExpansionOutcome(new ValueSet())); + + ValueSet valueSetToExpand = new ValueSet(); + if (theValueSetHasUrl) { + valueSetToExpand.setId("123"); + valueSetToExpand.setUrl("http://foo"); + } + + // Test + IValidationSupport.ValueSetExpansionOutcome result = chain.expandValueSet(newValidationCtx(chain), new ValueSetExpansionOptions(), valueSetToExpand); + + // Verify + verify(myValidationSupport0, times(1)).expandValueSet(any(), any(), any(IBaseResource.class)); + verify(myValidationSupport1, times(1)).expandValueSet(any(), any(), any(IBaseResource.class)); + verify(myValidationSupport2, times(0)).expandValueSet(any(), any(), any(IBaseResource.class)); + + // Test again (should use cache) + IValidationSupport.ValueSetExpansionOutcome result2 = chain.expandValueSet(newValidationCtx(chain), new ValueSetExpansionOptions(), valueSetToExpand); + + // Verify + if (theUseCache && theValueSetHasUrl) { + assertSame(result, result2); + verify(myValidationSupport0, times(1)).expandValueSet(any(), any(), any(IBaseResource.class)); + verify(myValidationSupport1, times(1)).expandValueSet(any(), any(), any(IBaseResource.class)); + verify(myValidationSupport2, times(0)).expandValueSet(any(), any(), any(IBaseResource.class)); + } else { + verify(myValidationSupport0, times(2)).expandValueSet(any(), any(), any(IBaseResource.class)); + verify(myValidationSupport1, times(2)).expandValueSet(any(), any(), any(IBaseResource.class)); + verify(myValidationSupport2, times(0)).expandValueSet(any(), any(), any(IBaseResource.class)); + } + } + + @ParameterizedTest + @ValueSource(booleans = {true, false}) + public void testExpandValueSet_StringParam(boolean theUseCache) { + // Setup + prepareMock(myValidationSupport0, myValidationSupport1, myValidationSupport2); + ValidationSupportChain chain = new ValidationSupportChain(newCacheConfiguration(theUseCache), myValidationSupport0, myValidationSupport1, myValidationSupport2); + + when(myValidationSupport0.isValueSetSupported(any(), eq(VALUE_SET_URL_0))).thenReturn(true); + when(myValidationSupport0.expandValueSet(any(), any(), any(String.class))).thenReturn(null); + when(myValidationSupport1.isValueSetSupported(any(), eq(VALUE_SET_URL_0))).thenReturn(true); + when(myValidationSupport1.expandValueSet(any(), any(), any(String.class))).thenAnswer(t -> new IValidationSupport.ValueSetExpansionOutcome(new ValueSet())); + + // Test + IValidationSupport.ValueSetExpansionOutcome result = chain.expandValueSet(newValidationCtx(chain), new ValueSetExpansionOptions(), VALUE_SET_URL_0); + + // Verify + verify(myValidationSupport0, times(1)).expandValueSet(any(), any(), any(String.class)); + verify(myValidationSupport1, times(1)).expandValueSet(any(), any(), any(String.class)); + verify(myValidationSupport2, times(0)).expandValueSet(any(), any(), any(String.class)); + + // Test again (should use cache) + IValidationSupport.ValueSetExpansionOutcome result2 = chain.expandValueSet(newValidationCtx(chain), new ValueSetExpansionOptions(), VALUE_SET_URL_0); + + // Verify + if (theUseCache) { + assertSame(result, result2); + verify(myValidationSupport0, times(1)).expandValueSet(any(), any(), any(String.class)); + verify(myValidationSupport1, times(1)).expandValueSet(any(), any(), any(String.class)); + verify(myValidationSupport2, times(0)).expandValueSet(any(), any(), any(String.class)); + } else { + verify(myValidationSupport0, times(2)).expandValueSet(any(), any(), any(String.class)); + verify(myValidationSupport1, times(2)).expandValueSet(any(), any(), any(String.class)); + verify(myValidationSupport2, times(0)).expandValueSet(any(), any(), any(String.class)); + } + } + + @Test + public void testFetchAllNonBaseStructureDefinitions() { + // Setup + prepareMock(myValidationSupport0); + + StructureDefinition sd0 = (StructureDefinition) new StructureDefinition().setId("SD0"); + StructureDefinition sd1 = (StructureDefinition) new StructureDefinition().setId("SD1"); + StructureDefinition sd2 = (StructureDefinition) new StructureDefinition().setId("SD2"); + List responses = Collections.synchronizedList(Lists.newArrayList( + sd0, sd1, sd2 + )); + + // Each time this is called it will return a slightly shorter list + when(myValidationSupport0.fetchAllNonBaseStructureDefinitions()).thenAnswer(t -> { + Thread.sleep(1000); + return new ArrayList<>(responses); + }); + + final ValidationSupportChain.CacheConfiguration cacheTimeouts = ValidationSupportChain.CacheConfiguration + .defaultValues() + .setCacheTimeout(Duration.ofMillis(500)); + ValidationSupportChain chain = new ValidationSupportChain(cacheTimeouts, myValidationSupport0); + + // First call should return the full list + assertEquals(3, chain.fetchAllNonBaseStructureDefinitions().size()); + assertEquals(3, chain.fetchAllNonBaseStructureDefinitions().size()); + + // Remove one from the backing list and wait for the cache to expire + responses.remove(0); + TestUtil.sleepAtLeast(750); + + // The cache is expired, but we should still return the old list and + // start a background job to update the backing list + assertEquals(3, chain.fetchAllNonBaseStructureDefinitions().size()); + + // Eventually we should refresh + await().until(() -> chain.fetchAllNonBaseStructureDefinitions().size(), t -> t == 2); + } + + @ParameterizedTest + @ValueSource(booleans = {true, false}) + public void testFetchAllSearchParameters(boolean theUseCache) { + // Setup + prepareMock(myValidationSupport0, myValidationSupport1, myValidationSupport2); + when(myValidationSupport0.fetchAllSearchParameters()).thenReturn(List.of( + new SearchParameter().setId("01"), + new SearchParameter().setId("02") + )); + when(myValidationSupport1.fetchAllSearchParameters()).thenReturn(List.of( + new SearchParameter().setId("11"), + new SearchParameter().setId("12") + )); + when(myValidationSupport2.fetchAllSearchParameters()).thenReturn(null); + ValidationSupportChain.CacheConfiguration cache = theUseCache ? ValidationSupportChain.CacheConfiguration.defaultValues() : ValidationSupportChain.CacheConfiguration.disabled(); + ValidationSupportChain chain = new ValidationSupportChain(cache, myValidationSupport0, myValidationSupport1, myValidationSupport2); + + // Test + List actual = chain.fetchAllSearchParameters(); + + // Verify + assert actual != null; + assertThat(actual.stream().map(t -> t.getIdElement().getIdPart()).toList()).asList().containsExactly( + "01", "02", "11", "12" + ); + verify(myValidationSupport0, times(1)).fetchAllSearchParameters(); + verify(myValidationSupport1, times(1)).fetchAllSearchParameters(); + verify(myValidationSupport2, times(1)).fetchAllSearchParameters(); + + // Test a second time + actual = chain.fetchAllSearchParameters(); + + // Verify + assert actual != null; + assertThat(actual.stream().map(t -> t.getIdElement().getIdPart()).toList()).asList().containsExactly( + "01", "02", "11", "12" + ); + if (theUseCache) { + verify(myValidationSupport0, times(1)).fetchAllSearchParameters(); + verify(myValidationSupport1, times(1)).fetchAllSearchParameters(); + verify(myValidationSupport2, times(1)).fetchAllSearchParameters(); + } else { + verify(myValidationSupport0, times(2)).fetchAllSearchParameters(); + verify(myValidationSupport1, times(2)).fetchAllSearchParameters(); + verify(myValidationSupport2, times(2)).fetchAllSearchParameters(); + } + + } + + @ParameterizedTest + @ValueSource(booleans = {true, false}) + public void testLookupCode(boolean theUseCache) { + // Setup + prepareMock(myValidationSupport0, myValidationSupport1, myValidationSupport2); + ValidationSupportChain chain = new ValidationSupportChain(newCacheConfiguration(theUseCache), myValidationSupport0, myValidationSupport1, myValidationSupport2); + + when(myValidationSupport0.isCodeSystemSupported(any(), eq(CODE_SYSTEM_URL_0))).thenReturn(true); + when(myValidationSupport1.isCodeSystemSupported(any(), eq(CODE_SYSTEM_URL_0))).thenReturn(true); + when(myValidationSupport0.lookupCode(any(), any(LookupCodeRequest.class))).thenReturn(null); + when(myValidationSupport1.lookupCode(any(), any(LookupCodeRequest.class))).thenAnswer(t -> new IValidationSupport.LookupCodeResult()); + + // Test + IValidationSupport.LookupCodeResult result = chain.lookupCode(newValidationCtx(chain), new LookupCodeRequest(CODE_SYSTEM_URL_0, CODE_0)); + + // Verify + verify(myValidationSupport0, times(1)).lookupCode(any(), any(LookupCodeRequest.class)); + verify(myValidationSupport1, times(1)).lookupCode(any(), any(LookupCodeRequest.class)); + verify(myValidationSupport2, times(0)).lookupCode(any(), any(LookupCodeRequest.class)); + + // Test again (should use cache) + IValidationSupport.LookupCodeResult result2 = chain.lookupCode(newValidationCtx(chain), new LookupCodeRequest(CODE_SYSTEM_URL_0, CODE_0)); + + // Verify + if (theUseCache) { + assertSame(result, result2); + verify(myValidationSupport0, times(1)).lookupCode(any(), any(LookupCodeRequest.class)); + verify(myValidationSupport1, times(1)).lookupCode(any(), any(LookupCodeRequest.class)); + verify(myValidationSupport2, times(0)).lookupCode(any(), any(LookupCodeRequest.class)); + } else { + verify(myValidationSupport0, times(2)).lookupCode(any(), any(LookupCodeRequest.class)); + verify(myValidationSupport1, times(2)).lookupCode(any(), any(LookupCodeRequest.class)); + verify(myValidationSupport2, times(0)).lookupCode(any(), any(LookupCodeRequest.class)); + } + } + + @ParameterizedTest + @ValueSource(booleans = {true, false}) + public void testFetchValueSet(boolean theUseCache) { + // Setup + prepareMock(myValidationSupport0, myValidationSupport1, myValidationSupport2); + ValidationSupportChain chain = new ValidationSupportChain(newCacheConfiguration(theUseCache), myValidationSupport0, myValidationSupport1, myValidationSupport2); + + when(myValidationSupport0.fetchValueSet(any())).thenReturn(null); + when(myValidationSupport1.fetchValueSet(any())).thenAnswer(t -> new ValueSet()); + + // Test + IBaseResource result = chain.fetchValueSet(VALUE_SET_URL_0); + + // Verify + verify(myValidationSupport0, times(1)).fetchValueSet(any()); + verify(myValidationSupport1, times(1)).fetchValueSet(any()); + verify(myValidationSupport2, times(0)).fetchValueSet(any()); + + // Test again (should use cache) + IBaseResource result2 = chain.fetchValueSet(VALUE_SET_URL_0); + + // Verify + if (theUseCache) { + assertSame(result, result2); + verify(myValidationSupport0, times(1)).fetchValueSet(any()); + verify(myValidationSupport1, times(1)).fetchValueSet(any()); + verify(myValidationSupport2, times(0)).fetchValueSet(any()); + } else { + verify(myValidationSupport0, times(2)).fetchValueSet(any()); + verify(myValidationSupport1, times(2)).fetchValueSet(any()); + verify(myValidationSupport2, times(0)).fetchValueSet(any()); + } + } + + @ParameterizedTest + @ValueSource(booleans = {true, false}) + public void testResource_ValueSet(boolean theUseCache) { + // Setup + prepareMock(myValidationSupport0, myValidationSupport1, myValidationSupport2); + ValidationSupportChain chain = new ValidationSupportChain(newCacheConfiguration(theUseCache), myValidationSupport0, myValidationSupport1, myValidationSupport2); + + when(myValidationSupport0.fetchValueSet(any())).thenReturn(null); + when(myValidationSupport1.fetchValueSet(any())).thenAnswer(t -> new ValueSet()); + + // Test + IBaseResource result = chain.fetchResource(ValueSet.class, VALUE_SET_URL_0); + + // Verify + verify(myValidationSupport0, times(1)).fetchValueSet( any()); + verify(myValidationSupport1, times(1)).fetchValueSet( any()); + verify(myValidationSupport2, times(0)).fetchValueSet(any()); + + // Test again (should use cache) + IBaseResource result2 = chain.fetchResource(ValueSet.class, VALUE_SET_URL_0); + + // Verify + if (theUseCache) { + assertSame(result, result2); + verify(myValidationSupport0, times(1)).fetchValueSet( any()); + verify(myValidationSupport1, times(1)).fetchValueSet( any()); + verify(myValidationSupport2, times(0)).fetchValueSet( any()); + } else { + verify(myValidationSupport0, times(2)).fetchValueSet( any()); + verify(myValidationSupport1, times(2)).fetchValueSet( any()); + verify(myValidationSupport2, times(0)).fetchValueSet(any()); + } + } + + @ParameterizedTest + @ValueSource(booleans = {true, false}) + public void testResource_Arbitrary(boolean theUseCache) { + // Setup + prepareMock(myValidationSupport0, myValidationSupport1, myValidationSupport2); + ValidationSupportChain chain = new ValidationSupportChain(newCacheConfiguration(theUseCache), myValidationSupport0, myValidationSupport1, myValidationSupport2); + + when(myValidationSupport0.fetchResource(any(), any())).thenReturn(null); + when(myValidationSupport1.fetchResource(any(), any())).thenAnswer(t -> new ListResource()); + + // Test + IBaseResource result = chain.fetchResource(ListResource.class, "http://foo"); + + // Verify + verify(myValidationSupport0, times(1)).fetchResource(any(), any()); + verify(myValidationSupport1, times(1)).fetchResource(any(), any()); + verify(myValidationSupport2, times(0)).fetchResource(any(), any()); + + // Test again (should use cache) + IBaseResource result2 = chain.fetchResource(ListResource.class, "http://foo"); + + // Verify + if (theUseCache) { + assertSame(result, result2); + verify(myValidationSupport0, times(1)).fetchResource(any(), any()); + verify(myValidationSupport1, times(1)).fetchResource(any(), any()); + verify(myValidationSupport2, times(0)).fetchResource(any(), any()); + } else { + verify(myValidationSupport0, times(2)).fetchResource(any(), any()); + verify(myValidationSupport1, times(2)).fetchResource(any(), any()); + verify(myValidationSupport2, times(0)).fetchResource(any(), any()); + } + } + + @ParameterizedTest + @ValueSource(booleans = {true, false}) + public void testTranslateCode(boolean theUseCache) { + // Setup + prepareMock(myValidationSupport0, myValidationSupport1, myValidationSupport2); + ValidationSupportChain chain = new ValidationSupportChain(newCacheConfiguration(theUseCache), myValidationSupport0, myValidationSupport1, myValidationSupport2); + + when(myValidationSupport0.translateConcept(any())).thenReturn(null); + TranslateConceptResults backingResult1 = new TranslateConceptResults(); + backingResult1.setMessage("Message 1"); + backingResult1.setResult(true); + backingResult1.getResults().add(new TranslateConceptResult().setCode("A")); + when(myValidationSupport1.translateConcept(any())).thenReturn(backingResult1); + TranslateConceptResults backingResult2 = new TranslateConceptResults(); + backingResult2.setMessage("Message 2"); + backingResult2.setResult(true); + backingResult2.getResults().add(new TranslateConceptResult().setCode("B")); + when(myValidationSupport2.translateConcept(any())).thenReturn(backingResult2); + + // Test + TranslateConceptResults result = chain.translateConcept(new IValidationSupport.TranslateCodeRequest(List.of(), CODE_SYSTEM_URL_0)); + + // Verify + assertEquals("Message 1", result.getMessage()); + assertTrue(result.getResult()); + assertEquals(2, result.getResults().size()); + verify(myValidationSupport0, times(1)).translateConcept(any()); + verify(myValidationSupport1, times(1)).translateConcept(any()); + verify(myValidationSupport2, times(1)).translateConcept(any()); + + // Test again (should use cache) + TranslateConceptResults result2 = chain.translateConcept(new IValidationSupport.TranslateCodeRequest(List.of(), CODE_SYSTEM_URL_0)); + + // Verify + if (theUseCache) { + assertSame(result, result2); + verify(myValidationSupport0, times(1)).translateConcept(any()); + verify(myValidationSupport1, times(1)).translateConcept(any()); + verify(myValidationSupport2, times(1)).translateConcept(any()); + } else { + assertNotSame(result, result2); + verify(myValidationSupport0, times(2)).translateConcept(any()); + verify(myValidationSupport1, times(2)).translateConcept(any()); + verify(myValidationSupport2, times(2)).translateConcept(any()); + } + } + + /** + * Verify that OpenTelemetry metrics are generated correctly + */ + @ParameterizedTest + @ValueSource(booleans = {true, false}) + public void testMetrics(boolean theUseCache) { + LibraryTestRunner libraryTestRunner = LibraryTestRunner.instance(); + + /* + * As of version 2.10.0 of the opentelemetry-testing-common library, + * the following doesn't actually clear the stored metrics. Hopefully + * this will be fixed in a future release. + */ + libraryTestRunner.clearAllExportedData(); + + prepareMock(myValidationSupport0, myValidationSupport1, myValidationSupport2); + when(myValidationSupport0.fetchStructureDefinition("http://foo")).thenReturn(new StructureDefinition().setUrl("http://foo")); + ValidationSupportChain chain = new ValidationSupportChain(newCacheConfiguration(theUseCache), myValidationSupport0, myValidationSupport1, myValidationSupport2); + chain.setName("FOO_NAME"); + + chain.start(); + try { + if (theUseCache) { + assertEquals(5000L, getLastMetricValue(libraryTestRunner, ValidationSupportChainMetrics.EXPIRING_CACHE_MAXIMUM_SIZE)); + assertEquals(0L, getLastMetricValue(libraryTestRunner, ValidationSupportChainMetrics.EXPIRING_CACHE_CURRENT_ENTRIES)); + assertEquals(0L, getLastMetricValue(libraryTestRunner, ValidationSupportChainMetrics.NON_EXPIRING_CACHE_CURRENT_ENTRIES)); + } else { + assertEquals(0L, getLastMetricValue(libraryTestRunner, ValidationSupportChainMetrics.EXPIRING_CACHE_MAXIMUM_SIZE)); + assertEquals(0L, getLastMetricValue(libraryTestRunner, ValidationSupportChainMetrics.EXPIRING_CACHE_CURRENT_ENTRIES)); + assertEquals(0L, getLastMetricValue(libraryTestRunner, ValidationSupportChainMetrics.NON_EXPIRING_CACHE_CURRENT_ENTRIES)); + } + + // Test + assertNotNull(chain.fetchStructureDefinition("http://foo")); + + // Verify + if (theUseCache) { + assertEquals(5000L, getLastMetricValue(libraryTestRunner, "io.hapifhir.validation_support_chain.expiring_cache.maximum_size")); + assertEquals(1L, getLastMetricValue(libraryTestRunner, "io.hapifhir.validation_support_chain.expiring_cache.current_entries")); + assertEquals(1L, getLastMetricValue(libraryTestRunner, "io.hapifhir.validation_support_chain.non_expiring_cache.current_entries")); + } else { + assertEquals(0L, getLastMetricValue(libraryTestRunner, "io.hapifhir.validation_support_chain.expiring_cache.maximum_size")); + assertEquals(0L, getLastMetricValue(libraryTestRunner, "io.hapifhir.validation_support_chain.expiring_cache.current_entries")); + assertEquals(0L, getLastMetricValue(libraryTestRunner, "io.hapifhir.validation_support_chain.non_expiring_cache.current_entries")); + } + } finally { + chain.stop(); + } } - private static IValidationSupport createMockValidationSupportWithSingleBinary(String expected_binary_key, byte[] expected_binary_content) { - IValidationSupport validationSupport1 = createMockValidationSupport(); - when(validationSupport1.fetchBinary(expected_binary_key)).thenReturn(expected_binary_content); - return validationSupport1; + @Test + public void testModifyingServiceInvalidatesCache() { + // Setup + prepareMock(myValidationSupport0, myValidationSupport1, myValidationSupport2); + when(myValidationSupport0.isCodeSystemSupported(any(), eq("http://foo"))).thenReturn(true); + when(myValidationSupport1.isCodeSystemSupported(any(), eq("http://foo"))).thenReturn(true); + + ValidationSupportChain svc = new ValidationSupportChain(myValidationSupport0, myValidationSupport1); + assertTrue(svc.isCodeSystemSupported(newValidationCtx(svc), "http://foo")); + + // Test + svc.addValidationSupport(myValidationSupport2); + when(myValidationSupport0.isCodeSystemSupported(any(), eq("http://foo"))).thenReturn(false); + when(myValidationSupport1.isCodeSystemSupported(any(), eq("http://foo"))).thenReturn(false); + when(myValidationSupport2.isCodeSystemSupported(any(), eq("http://foo"))).thenReturn(false); + boolean actual = svc.isCodeSystemSupported(newValidationCtx(svc), "http://foo"); + + // Verify + assertFalse(actual); } + + + private static long getLastMetricValue(LibraryTestRunner libraryTestRunner, String metricName) { + List metrics = libraryTestRunner.getExportedMetrics(); + List metricsList = metrics.stream().filter(t -> t.getName().equals(metricName)).toList(); + ourLog.info("Have metrics {}\n * {}", metricName, metricsList.stream().map(t -> t.getData().getPoints().toString()).collect(Collectors.joining("\n * "))); + MetricData metric = metricsList.get(metricsList.size() - 1); + assertEquals("io.hapifhir.validation_support_chain", metric.getInstrumentationScopeInfo().getName()); + Data data = metric.getData(); + ArrayList dataPoints = new ArrayList<>(data.getPoints()); + assertEquals(1, dataPoints.size()); + LongPointData pointData = (LongPointData) dataPoints.get(0); + return pointData.getValue(); + } + + + private static ValidationSupportChain.CacheConfiguration newCacheConfiguration(boolean theUseCache) { + return theUseCache ? ValidationSupportChain.CacheConfiguration.defaultValues() : ValidationSupportChain.CacheConfiguration.disabled(); + } + + @Nonnull + private static ValidationSupportContext newValidationCtx(ValidationSupportChain validationSupportChain) { + return new ValidationSupportContext(validationSupportChain); + } + + + private static void prepareMock(IValidationSupport... theMock) { + reset(theMock); + for (var mock : theMock) { + when(mock.getFhirContext()).thenReturn(FhirContext.forR4Cached()); + } + } + + private static void createMockValidationSupportWithSingleBinary(IValidationSupport theValidationSupport, String theExpectedBinaryKey, byte[] theExpectedBinaryContent) { + when(theValidationSupport.fetchBinary(theExpectedBinaryKey)).thenReturn(theExpectedBinaryContent); + } + } diff --git a/hapi-fhir-validation/src/test/java/org/hl7/fhir/common/hapi/validation/validator/VersionSpecificWorkerContextWrapperCoreTest.java b/hapi-fhir-validation/src/test/java/org/hl7/fhir/common/hapi/validation/validator/VersionSpecificWorkerContextWrapperCoreTest.java index 10548169c0f..9a03a81d262 100644 --- a/hapi-fhir-validation/src/test/java/org/hl7/fhir/common/hapi/validation/validator/VersionSpecificWorkerContextWrapperCoreTest.java +++ b/hapi-fhir-validation/src/test/java/org/hl7/fhir/common/hapi/validation/validator/VersionSpecificWorkerContextWrapperCoreTest.java @@ -2,16 +2,13 @@ package org.hl7.fhir.common.hapi.validation.validator; import ca.uhn.fhir.context.FhirContext; import ca.uhn.fhir.context.support.DefaultProfileValidationSupport; - import ca.uhn.fhir.context.support.IValidationSupport; import ca.uhn.fhir.context.support.ValidationSupportContext; import ca.uhn.fhir.test.utilities.LoggingExtension; import ca.uhn.fhir.validation.FhirValidator; import ca.uhn.hapi.converters.canonical.VersionCanonicalizer; import com.google.common.base.Charsets; - import org.apache.commons.io.IOUtils; -import org.hl7.fhir.common.hapi.validation.support.CachingValidationSupport; 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.UnknownCodeSystemWarningValidationSupport; @@ -24,6 +21,8 @@ import org.hl7.fhir.exceptions.DefinitionException; import org.hl7.fhir.exceptions.FHIRException; import org.hl7.fhir.exceptions.FHIRFormatError; import org.hl7.fhir.instance.model.api.IBaseResource; +import org.hl7.fhir.r5.formats.JsonParser; +import org.hl7.fhir.r5.formats.XmlParser; import org.hl7.fhir.r5.model.CodeSystem; import org.hl7.fhir.r5.model.Constants; import org.hl7.fhir.r5.model.Parameters; @@ -41,6 +40,9 @@ import org.junit.jupiter.api.extension.RegisterExtension; import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.Arguments; import org.junit.jupiter.params.provider.MethodSource; +import org.mockito.invocation.InvocationOnMock; +import org.mockito.quality.Strictness; +import org.mockito.stubbing.Answer; import java.io.File; import java.io.FileNotFoundException; @@ -50,14 +52,7 @@ import java.util.HashMap; import java.util.Map; import java.util.stream.Stream; -import org.hl7.fhir.r5.formats.JsonParser; -import org.hl7.fhir.r5.formats.XmlParser; -import org.mockito.invocation.InvocationOnMock; -import org.mockito.quality.Strictness; -import org.mockito.stubbing.Answer; - import static org.junit.jupiter.api.Assertions.assertNull; - import static org.mockito.ArgumentMatchers.eq; import static org.mockito.ArgumentMatchers.nullable; import static org.mockito.Mockito.mock; @@ -114,7 +109,7 @@ public class VersionSpecificWorkerContextWrapperCoreTest { private FhirInstanceValidator myInstanceVal; private Map mySupportedCodeSystemsForExpansion; private FhirValidator myVal; - private CachingValidationSupport myValidationSupport; + private ValidationSupportChain myValidationSupport; private static final String VALIDATE_CODE_OPERATION = "validate-code"; @@ -176,14 +171,13 @@ public class VersionSpecificWorkerContextWrapperCoreTest { UnknownCodeSystemWarningValidationSupport unknownCodeSystemWarningValidationSupport = new UnknownCodeSystemWarningValidationSupport(ourCtx); unknownCodeSystemWarningValidationSupport.setNonExistentCodeSystemSeverity(IValidationSupport.IssueSeverity.WARNING); - myValidationSupport = new CachingValidationSupport( + myValidationSupport = new ValidationSupportChain( mockSupport, myDefaultValidationSupport, new InMemoryTerminologyServerValidationSupport(ourCtx), new CommonCodeSystemsTerminologyService(ourCtx), - unknownCodeSystemWarningValidationSupport) - ); + unknownCodeSystemWarningValidationSupport); myInstanceVal = new FhirInstanceValidator(myValidationSupport); wrapper = new VersionSpecificWorkerContextWrapper(new ValidationSupportContext(myValidationSupport), versionCanonicalizer); diff --git a/hapi-fhir-validation/src/test/java/org/hl7/fhir/common/hapi/validation/validator/VersionSpecificWorkerContextWrapperTest.java b/hapi-fhir-validation/src/test/java/org/hl7/fhir/common/hapi/validation/validator/VersionSpecificWorkerContextWrapperTest.java index cbc79fadc98..a6162c2b67c 100644 --- a/hapi-fhir-validation/src/test/java/org/hl7/fhir/common/hapi/validation/validator/VersionSpecificWorkerContextWrapperTest.java +++ b/hapi-fhir-validation/src/test/java/org/hl7/fhir/common/hapi/validation/validator/VersionSpecificWorkerContextWrapperTest.java @@ -1,39 +1,50 @@ package org.hl7.fhir.common.hapi.validation.validator; import ca.uhn.fhir.context.FhirContext; -import ca.uhn.fhir.context.FhirVersionEnum; +import ca.uhn.fhir.context.support.DefaultProfileValidationSupport; import ca.uhn.fhir.context.support.IValidationSupport; import ca.uhn.fhir.context.support.ValidationSupportContext; -import ca.uhn.fhir.fhirpath.BaseValidationTestWithInlineMocks; -import ca.uhn.fhir.i18n.HapiLocalizer; +import ca.uhn.fhir.test.BaseTest; import ca.uhn.hapi.converters.canonical.VersionCanonicalizer; +import org.hl7.fhir.r5.model.PackageInformation; import org.hl7.fhir.r5.model.Resource; import org.hl7.fhir.r5.model.StructureDefinition; import org.hl7.fhir.r5.model.StructureDefinition.StructureDefinitionKind; import org.hl7.fhir.r5.model.ValueSet; import org.hl7.fhir.utilities.validation.ValidationOptions; import org.junit.jupiter.api.Test; -import org.mockito.quality.Strictness; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; + +import java.util.List; import java.util.List; import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertSame; import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.ArgumentMatchers.isNull; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.never; import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; -import static org.mockito.Mockito.withSettings; -public class VersionSpecificWorkerContextWrapperTest extends BaseValidationTestWithInlineMocks { +@ExtendWith(MockitoExtension.class) +public class VersionSpecificWorkerContextWrapperTest extends BaseTest { final byte[] EXPECTED_BINARY_CONTENT_1 = "dummyBinaryContent1".getBytes(); final byte[] EXPECTED_BINARY_CONTENT_2 = "dummyBinaryContent2".getBytes(); final String EXPECTED_BINARY_KEY_1 = "dummyBinaryKey1"; final String EXPECTED_BINARY_KEY_2 = "dummyBinaryKey2"; final String NON_EXISTENT_BINARY_KEY = "nonExistentBinaryKey"; + @Mock + private ValidationSupportContext myValidationSupportContext; + @Mock + private IValidationSupport myValidationSupport; @Test public void hasBinaryKey_normally_returnsExpected() { @@ -90,7 +101,7 @@ public class VersionSpecificWorkerContextWrapperTest extends BaseValidationTestW ValueSet valueSet = new ValueSet(); valueSet.getCompose().addInclude().setSystem("http://codesystems.com/system").addConcept().setCode("code0"); valueSet.getCompose().addInclude().setSystem("http://codesystems.com/system2").addConcept().setCode("code2"); - when(validationSupport.validateCodeInValueSet(any(), any(), any(), any(), any(), any())).thenReturn(mock(IValidationSupport.CodeValidationResult.class)); + when(validationSupport.validateCodeInValueSet(any(), any(), any(), any(), any(), any())).thenReturn(new IValidationSupport.CodeValidationResult()); // execute wrapper.validateCode(new ValidationOptions(), "code0", valueSet); @@ -157,6 +168,49 @@ public class VersionSpecificWorkerContextWrapperTest extends BaseValidationTestW assertThat(wrapper.isPrimitiveType("Unknown")).isFalse(); } + @Test + public void testFetchResource_ResourceParameter() { + // setup + IValidationSupport validationSupport = mockValidationSupport(); + ValidationSupportContext mockContext = mockValidationSupportContext(validationSupport); + VersionCanonicalizer versionCanonicalizer = new VersionCanonicalizer(validationSupport.getFhirContext()); + VersionSpecificWorkerContextWrapper wrapper = new VersionSpecificWorkerContextWrapper(mockContext, versionCanonicalizer); + + org.hl7.fhir.r4.model.StructureDefinition expected = new org.hl7.fhir.r4.model.StructureDefinition(); + expected.setUrl("http://foo"); + expected.getSnapshot().addElement().setId("FOO"); + when(mockContext.getRootValidationSupport().fetchResource(isNull(), eq("http://foo"))).thenReturn(expected); + + // Test + StructureDefinition actual = (StructureDefinition) wrapper.fetchResource(Resource.class, "http://foo"); + + // Verify + assertEquals("FOO", actual.getSnapshot().getElementFirstRep().getId()); + } + + @Test + public void testFetchResource_StructureDefinitionParameter() { + // setup + IValidationSupport validationSupport = mockValidationSupport(); + ValidationSupportContext mockContext = mockValidationSupportContext(validationSupport); + VersionCanonicalizer versionCanonicalizer = new VersionCanonicalizer(validationSupport.getFhirContext()); + VersionSpecificWorkerContextWrapper wrapper = new VersionSpecificWorkerContextWrapper(mockContext, versionCanonicalizer); + + org.hl7.fhir.r4.model.StructureDefinition expected = new org.hl7.fhir.r4.model.StructureDefinition(); + expected.setUrl("http://foo"); + expected.getSnapshot().addElement().setId("FOO"); + expected.setUserData(DefaultProfileValidationSupport.SOURCE_PACKAGE_ID, "hl7.fhir.r999.core"); + when(mockContext.getRootValidationSupport().fetchResource(eq(org.hl7.fhir.r4.model.StructureDefinition.class), eq("http://foo"))).thenReturn(expected); + + // Test + StructureDefinition actual = wrapper.fetchResource(StructureDefinition.class, "http://foo"); + + // Verify + assertEquals("FOO", actual.getSnapshot().getElementFirstRep().getId()); + PackageInformation sourcePackage = actual.getSourcePackage(); + assertEquals("hl7.fhir.r999.core", sourcePackage.getId()); + } + private List createStructureDefinitions() { StructureDefinition stringType = createPrimitive("string"); StructureDefinition boolType = createPrimitive("boolean"); @@ -166,20 +220,25 @@ public class VersionSpecificWorkerContextWrapperTest extends BaseValidationTestW return List.of(personType, boolType, orgType, stringType); } - private StructureDefinition createComplex(String name){ + private StructureDefinition createComplex(String name) { return createStructureDefinition(name).setKind(StructureDefinitionKind.COMPLEXTYPE); } - private StructureDefinition createPrimitive(String name){ + private StructureDefinition createPrimitive(String name) { return createStructureDefinition(name).setKind(StructureDefinitionKind.PRIMITIVETYPE); } private StructureDefinition createStructureDefinition(String name) { StructureDefinition sd = new StructureDefinition(); - sd.setUrl("http://hl7.org/fhir/StructureDefinition/"+name).setName(name); + sd.setUrl("http://hl7.org/fhir/StructureDefinition/" + name).setName(name); + addFakeSnapshot(sd); return sd; } + private static void addFakeSnapshot(StructureDefinition sd) { + sd.getSnapshot().addElement().setId("FOO"); + } + private IValidationSupport mockValidationSupportWithTwoBinaries() { IValidationSupport validationSupport; validationSupport = mockValidationSupport(); @@ -188,22 +247,14 @@ public class VersionSpecificWorkerContextWrapperTest extends BaseValidationTestW return validationSupport; } - - private static ValidationSupportContext mockValidationSupportContext(IValidationSupport validationSupport) { - ValidationSupportContext mockContext; - mockContext = mock(ValidationSupportContext.class); + private ValidationSupportContext mockValidationSupportContext(IValidationSupport validationSupport) { + ValidationSupportContext mockContext = myValidationSupportContext; when(mockContext.getRootValidationSupport()).thenReturn(validationSupport); return mockContext; } - - private static IValidationSupport mockValidationSupport() { - IValidationSupport mockValidationSupport; - mockValidationSupport = mock(IValidationSupport.class); - FhirContext mockFhirContext = mock(FhirContext.class, withSettings().strictness(Strictness.LENIENT)); - when(mockFhirContext.getLocalizer()).thenReturn(new HapiLocalizer()); - when(mockFhirContext.getVersion()).thenReturn(FhirVersionEnum.R4.getVersionImplementation()); - when(mockValidationSupport.getFhirContext()).thenReturn(mockFhirContext); - return mockValidationSupport; + private IValidationSupport mockValidationSupport() { + when(myValidationSupport.getFhirContext()).thenReturn(FhirContext.forR4Cached()); + return myValidationSupport; } } diff --git a/hapi-fhir-validation/src/test/java/org/hl7/fhir/dstu3/hapi/validation/FhirInstanceValidatorDstu3Test.java b/hapi-fhir-validation/src/test/java/org/hl7/fhir/dstu3/hapi/validation/FhirInstanceValidatorDstu3Test.java index cfbeca68626..34fc831525f 100644 --- a/hapi-fhir-validation/src/test/java/org/hl7/fhir/dstu3/hapi/validation/FhirInstanceValidatorDstu3Test.java +++ b/hapi-fhir-validation/src/test/java/org/hl7/fhir/dstu3/hapi/validation/FhirInstanceValidatorDstu3Test.java @@ -15,7 +15,6 @@ import ca.uhn.fhir.validation.SingleValidationMessage; import ca.uhn.fhir.validation.ValidationResult; import com.google.common.base.Charsets; import org.apache.commons.io.IOUtils; -import org.hl7.fhir.common.hapi.validation.support.CachingValidationSupport; 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.SnapshotGeneratingValidationSupport; @@ -115,7 +114,7 @@ public class FhirInstanceValidatorDstu3Test extends BaseValidationTestWithInline private HashMap myCodeSystems; private HashMap myValueSets; private HashMap myQuestionnaires; - private CachingValidationSupport myValidationSupport; + private ValidationSupportChain myValidationSupport; private void addValidConcept(String theSystem, String theCode) { addValidConcept(theSystem, theCode, true); @@ -139,12 +138,12 @@ public class FhirInstanceValidatorDstu3Test extends BaseValidationTestWithInline IValidationSupport mockSupport = mock(IValidationSupport.class, withSettings().strictness(Strictness.LENIENT)); when(mockSupport.getFhirContext()).thenReturn(ourCtx); - myValidationSupport = new CachingValidationSupport(new ValidationSupportChain( + myValidationSupport = new ValidationSupportChain( mockSupport, myDefaultValidationSupport, new InMemoryTerminologyServerValidationSupport(ourCtx), new CommonCodeSystemsTerminologyService(ourCtx), - new SnapshotGeneratingValidationSupport(ourCtx))); + new SnapshotGeneratingValidationSupport(ourCtx)); myInstanceVal = new FhirInstanceValidator(myValidationSupport); myVal.registerValidatorModule(myInstanceVal); diff --git a/hapi-fhir-validation/src/test/java/org/hl7/fhir/dstu3/hapi/validation/QuestionnaireResponseValidatorDstu3Test.java b/hapi-fhir-validation/src/test/java/org/hl7/fhir/dstu3/hapi/validation/QuestionnaireResponseValidatorDstu3Test.java index 57591b31e76..4c7d0749585 100644 --- a/hapi-fhir-validation/src/test/java/org/hl7/fhir/dstu3/hapi/validation/QuestionnaireResponseValidatorDstu3Test.java +++ b/hapi-fhir-validation/src/test/java/org/hl7/fhir/dstu3/hapi/validation/QuestionnaireResponseValidatorDstu3Test.java @@ -169,7 +169,7 @@ public class QuestionnaireResponseValidatorDstu3Test { when(myValSupport.fetchResource(eq(Questionnaire.class), eq(QUESTIONNAIRE_URL))).thenReturn(q); when(myValSupport.fetchCodeSystem(eq("http://codesystems.com/system"))).thenReturn(codeSystem); - when(myValSupport.fetchResource(eq(ValueSet.class), eq("http://somevalueset"))).thenReturn(options); + when(myValSupport.fetchValueSet(eq("http://somevalueset"))).thenReturn(options); when(myValSupport.validateCodeInValueSet(any(), any(), eq("http://codesystems.com/system"), eq("code0"), any(), nullable(ValueSet.class))) .thenReturn(new IValidationSupport.CodeValidationResult().setCode("code0")); @@ -242,7 +242,7 @@ public class QuestionnaireResponseValidatorDstu3Test { ValueSet options = new ValueSet(); options.getCompose().addInclude().setSystem("http://codesystems.com/system").addConcept().setCode("code0"); options.getCompose().addInclude().setSystem("http://codesystems.com/system2").addConcept().setCode("code2"); - when(myValSupport.fetchResource(eq(ValueSet.class), eq("http://somevalueset"))).thenReturn(options); + when(myValSupport.fetchValueSet(eq("http://somevalueset"))).thenReturn(options); when(myValSupport.validateCode(any(), any(), eq("http://codesystems.com/system"), eq("code0"), any(), nullable(String.class))) .thenReturn(new IValidationSupport.CodeValidationResult().setCode(CODE_ICC_SCHOOLTYPE_PT)); @@ -802,7 +802,7 @@ public class QuestionnaireResponseValidatorDstu3Test { ValueSet options = new ValueSet(); options.getCompose().addInclude().setSystem(codeSystemUrl).addConcept().setCode(codeValue); - when(myValSupport.fetchResource(eq(ValueSet.class), eq(valueSetRef))) + when(myValSupport.fetchValueSet(eq(valueSetRef))) .thenReturn(options); when(myValSupport.validateCode(any(), any(), eq(codeSystemUrl), eq(codeValue), any(String.class), anyString())) .thenReturn(new IValidationSupport.CodeValidationResult().setCode(codeValue)); @@ -858,7 +858,7 @@ public class QuestionnaireResponseValidatorDstu3Test { ValueSet options = new ValueSet(); options.getCompose().addInclude().setSystem(codeSystemUrl).addConcept().setCode(codeValue); - when(myValSupport.fetchResource(eq(ValueSet.class), eq(valueSetRef))) + when(myValSupport.fetchValueSet(eq(valueSetRef))) .thenReturn(options); when(myValSupport.validateCode(any(), any(), eq(codeSystemUrl), eq(codeValue), any(String.class), anyString())) .thenReturn(new IValidationSupport.CodeValidationResult().setCode(codeValue)); @@ -981,7 +981,7 @@ public class QuestionnaireResponseValidatorDstu3Test { .setValue(new Coding(SYSTEMURI_ICC_SCHOOLTYPE, CODE_ICC_SCHOOLTYPE_PT, "")); when(myValSupport.fetchResource(eq(Questionnaire.class), eq(questionnaireResponse.getQuestionnaire().getReference()))).thenReturn(questionnaire); - when(myValSupport.fetchResource(eq(ValueSet.class), eq(ID_VS_SCHOOLTYPE.getValue()))).thenReturn(iccSchoolTypeVs); + when(myValSupport.fetchValueSet(eq(ID_VS_SCHOOLTYPE.getValue()))).thenReturn(iccSchoolTypeVs); when(myValSupport.validateCodeInValueSet(any(), any(), eq(SYSTEMURI_ICC_SCHOOLTYPE), eq(CODE_ICC_SCHOOLTYPE_PT), any(), nullable(ValueSet.class))) .thenReturn(new IValidationSupport.CodeValidationResult().setCode(CODE_ICC_SCHOOLTYPE_PT)); when(myValSupport.fetchCodeSystem(eq(SYSTEMURI_ICC_SCHOOLTYPE))).thenReturn(codeSystem); @@ -1053,7 +1053,7 @@ public class QuestionnaireResponseValidatorDstu3Test { options.setUrl("http://somevalueset"); options.getCompose().addInclude().setSystem("http://codesystems.com/system").addConcept().setCode("code0"); options.getCompose().addInclude().setSystem("http://codesystems.com/system2").addConcept().setCode("code2"); - when(myValSupport.fetchResource(eq(ValueSet.class), eq("http://somevalueset"))).thenReturn(options); + when(myValSupport.fetchValueSet(eq("http://somevalueset"))).thenReturn(options); when(myValSupport.isValueSetSupported(any(), eq("http://somevalueset"))).thenReturn(true); @@ -1095,7 +1095,7 @@ public class QuestionnaireResponseValidatorDstu3Test { assertThat(errors.getMessages()).hasSize(2); assertThat(errors.getMessages().get(0).getMessage()).contains("A code with no system has no defined meaning, and it cannot be validated. A system should be provided"); assertThat(errors.getMessages().get(0).getLocationString()).contains("QuestionnaireResponse.item[0].answer[0]"); - assertThat(errors.getMessages().get(1).getMessage()).contains("The code 'code1' in the system 'null' is not in the options value set (ValueSet[http://somevalueset]) specified by the questionnaire. Terminology Error: Validation failed"); + assertThat(errors.getMessages().get(1).getMessage()).contains("The code 'code1' in the system 'null' is not in the options value set (ValueSet[http://somevalueset]) specified by the questionnaire. Terminology Error: Unknown code 'code1' for in-memory expansion of ValueSet 'http://somevalueset'"); assertThat(errors.getMessages().get(1).getLocationString()).contains("QuestionnaireResponse.item[0].answer[0]"); qa = new QuestionnaireResponse(); @@ -1108,7 +1108,7 @@ public class QuestionnaireResponseValidatorDstu3Test { assertThat(errors.getMessages()).hasSize(2); assertThat(errors.getMessages().get(0).getMessage()).contains("A code with no system has no defined meaning, and it cannot be validated. A system should be provided"); assertThat(errors.getMessages().get(0).getLocationString()).contains("QuestionnaireResponse.item[0].answer[0]"); - assertThat(errors.getMessages().get(1).getMessage()).contains("The code 'code1' in the system 'null' is not in the options value set (ValueSet[http://somevalueset]) specified by the questionnaire. Terminology Error: Validation failed"); + assertThat(errors.getMessages().get(1).getMessage()).contains("The code 'code1' in the system 'null' is not in the options value set (ValueSet[http://somevalueset]) specified by the questionnaire. Terminology Error: Unknown code 'code1' for in-memory expansion of ValueSet 'http://somevalueset'"); assertThat(errors.getMessages().get(1).getLocationString()).contains("QuestionnaireResponse.item[0].answer[0]"); qa = new QuestionnaireResponse(); diff --git a/hapi-fhir-validation/src/test/java/org/hl7/fhir/r4/validation/FhirInstanceValidatorR4Test.java b/hapi-fhir-validation/src/test/java/org/hl7/fhir/r4/validation/FhirInstanceValidatorR4Test.java index fb46f7f8008..ff5d8e2e1fe 100644 --- a/hapi-fhir-validation/src/test/java/org/hl7/fhir/r4/validation/FhirInstanceValidatorR4Test.java +++ b/hapi-fhir-validation/src/test/java/org/hl7/fhir/r4/validation/FhirInstanceValidatorR4Test.java @@ -21,7 +21,6 @@ import ca.uhn.hapi.converters.canonical.VersionCanonicalizer; import com.google.common.base.Charsets; import org.apache.commons.io.IOUtils; import org.apache.commons.lang3.Validate; -import org.hl7.fhir.common.hapi.validation.support.CachingValidationSupport; 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.PrePopulatedValidationSupport; @@ -133,7 +132,7 @@ public class FhirInstanceValidatorR4Test extends BaseValidationTestWithInlineMoc private Set myValidSystemsNotReturningIssues = new HashSet<>(); private Set myValidValueSets = new HashSet<>(); private Map myStructureDefinitionMap = new HashMap<>(); - private CachingValidationSupport myValidationSupport; + private IValidationSupport myValidationSupport; private IValidationSupport myMockSupport; private void addValidConcept(String theSystem, String theCode) { @@ -909,7 +908,7 @@ public class FhirInstanceValidatorR4Test extends BaseValidationTestWithInlineMoc public void testValidateProfileWithExtension() throws IOException, FHIRException { PrePopulatedValidationSupport valSupport = new PrePopulatedValidationSupport(ourCtx); DefaultProfileValidationSupport defaultSupport = new DefaultProfileValidationSupport(ourCtx); - CachingValidationSupport support = new CachingValidationSupport(new ValidationSupportChain(defaultSupport, valSupport, new InMemoryTerminologyServerValidationSupport(ourCtx)), false); + ValidationSupportChain support = new ValidationSupportChain(defaultSupport, valSupport, new InMemoryTerminologyServerValidationSupport(ourCtx)).setCodeableConceptValidationSuccessfulIfNotAllCodingsAreValid(false); // Prepopulate SDs valSupport.addStructureDefinition(loadStructureDefinition(defaultSupport, "/r4/myconsent-profile.xml")); @@ -1929,7 +1928,7 @@ public class FhirInstanceValidatorR4Test extends BaseValidationTestWithInlineMoc new CommonCodeSystemsTerminologyService(ourCtx), new InMemoryTerminologyServerValidationSupport(ourCtx), new SnapshotGeneratingValidationSupport(ourCtx)); - myValidationSupport = new CachingValidationSupport(chain, theLogicalAnd); + myValidationSupport = chain.setCodeableConceptValidationSuccessfulIfNotAllCodingsAreValid(theLogicalAnd); myInstanceVal = new FhirInstanceValidator(myValidationSupport); myFhirValidator.registerValidatorModule(myInstanceVal); } diff --git a/hapi-fhir-validation/src/test/java/org/hl7/fhir/r4/validation/QuestionnaireResponseValidatorR4Test.java b/hapi-fhir-validation/src/test/java/org/hl7/fhir/r4/validation/QuestionnaireResponseValidatorR4Test.java index 4a5b10e6cb3..17c347740b4 100644 --- a/hapi-fhir-validation/src/test/java/org/hl7/fhir/r4/validation/QuestionnaireResponseValidatorR4Test.java +++ b/hapi-fhir-validation/src/test/java/org/hl7/fhir/r4/validation/QuestionnaireResponseValidatorR4Test.java @@ -110,7 +110,7 @@ public class QuestionnaireResponseValidatorR4Test { ValueSet options = new ValueSet(); options.getCompose().addInclude().setSystem("http://codesystems.com/system").addConcept().setCode("code0"); - when(myValSupport.fetchResource(eq(ValueSet.class), eq("http://somevalueset"))).thenReturn(options); + when(myValSupport.fetchValueSet(eq("http://somevalueset"))).thenReturn(options); int itemCnt = 16; QuestionnaireItemType[] questionnaireItemTypes = new QuestionnaireItemType[itemCnt]; @@ -241,7 +241,7 @@ public class QuestionnaireResponseValidatorR4Test { ValueSet options = new ValueSet(); options.getCompose().addInclude().setSystem("http://codesystems.com/system").addConcept().setCode("code0"); options.getCompose().addInclude().setSystem("http://codesystems.com/system2").addConcept().setCode("code2"); - when(myValSupport.fetchResource(eq(ValueSet.class), eq("http://somevalueset"))).thenReturn(options); + when(myValSupport.fetchValueSet(eq("http://somevalueset"))).thenReturn(options); QuestionnaireResponse qa; ValidationResult errors; @@ -376,7 +376,7 @@ public class QuestionnaireResponseValidatorR4Test { ValueSet options = new ValueSet(); options.getCompose().addInclude().setSystem(codeSystemUrl).addConcept().setCode(codeValue); - when(myValSupport.fetchResource(eq(ValueSet.class), eq(valueSetRef))) + when(myValSupport.fetchValueSet(eq(valueSetRef))) .thenReturn(options); when(myValSupport.validateCode(any(), any(), eq(codeSystemUrl), eq(codeValue), any(String.class), anyString())) .thenReturn(new IValidationSupport.CodeValidationResult().setCode(codeValue)); @@ -433,7 +433,7 @@ public class QuestionnaireResponseValidatorR4Test { ValueSet options = new ValueSet(); options.getCompose().addInclude().setSystem(codeSystemUrl).addConcept().setCode(codeValue); - when(myValSupport.fetchResource(eq(ValueSet.class), eq(valueSetRef))) + when(myValSupport.fetchValueSet(eq(valueSetRef))) .thenReturn(options); when(myValSupport.validateCode(any(), any(), eq(codeSystemUrl), eq(codeValue), any(String.class), anyString())) .thenReturn(new IValidationSupport.CodeValidationResult().setCode(codeValue)); @@ -553,7 +553,7 @@ public class QuestionnaireResponseValidatorR4Test { ValueSet options = new ValueSet(); options.getCompose().addInclude().setSystem("http://codesystems.com/system").addConcept().setCode("code0"); options.getCompose().addInclude().setSystem("http://codesystems.com/system2").addConcept().setCode("code2"); - when(myValSupport.fetchResource(eq(ValueSet.class), eq("http://somevalueset"))).thenReturn(options); + when(myValSupport.fetchValueSet(eq("http://somevalueset"))).thenReturn(options); when(myValSupport.validateCode(any(), any(), eq("http://codesystems.com/system"), eq("code0"), any(), nullable(String.class))) .thenReturn(new IValidationSupport.CodeValidationResult().setCode("code0")); when(myValSupport.validateCode(any(), any(), eq("http://codesystems.com/system"), eq("code1"), any(), nullable(String.class))) @@ -833,7 +833,7 @@ public class QuestionnaireResponseValidatorR4Test { .setValue(new Coding(SYSTEMURI_ICC_SCHOOLTYPE, CODE_ICC_SCHOOLTYPE_PT, "")); when(myValSupport.fetchResource(eq(Questionnaire.class), eq(qa.getQuestionnaire()))).thenReturn(questionnaire); - when(myValSupport.fetchResource(eq(ValueSet.class), eq(ID_VS_SCHOOLTYPE))).thenReturn(iccSchoolTypeVs); + when(myValSupport.fetchValueSet(eq(ID_VS_SCHOOLTYPE))).thenReturn(iccSchoolTypeVs); when(myValSupport.validateCodeInValueSet(any(), any(), any(), any(), any(), any(ValueSet.class))).thenReturn(new IValidationSupport.CodeValidationResult().setCode(CODE_ICC_SCHOOLTYPE_PT)); when(myValSupport.fetchCodeSystem(eq(SYSTEMURI_ICC_SCHOOLTYPE))).thenReturn(codeSystem); ValidationResult errors = myVal.validateWithResult(qa); diff --git a/hapi-fhir-validation/src/test/java/org/hl7/fhir/r4b/validation/FhirInstanceValidatorR4BTest.java b/hapi-fhir-validation/src/test/java/org/hl7/fhir/r4b/validation/FhirInstanceValidatorR4BTest.java index d5035a8048e..8b1d4df1051 100644 --- a/hapi-fhir-validation/src/test/java/org/hl7/fhir/r4b/validation/FhirInstanceValidatorR4BTest.java +++ b/hapi-fhir-validation/src/test/java/org/hl7/fhir/r4b/validation/FhirInstanceValidatorR4BTest.java @@ -20,7 +20,6 @@ import ca.uhn.fhir.validation.ValidationResult; import com.google.common.base.Charsets; import org.apache.commons.io.IOUtils; import org.apache.commons.lang3.Validate; -import org.hl7.fhir.common.hapi.validation.support.CachingValidationSupport; 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.PrePopulatedValidationSupport; @@ -118,7 +117,7 @@ public class FhirInstanceValidatorR4BTest extends BaseValidationTestWithInlineMo private ArrayList myValidConcepts; private Set myValidSystems = new HashSet<>(); private Map myStructureDefinitionMap = new HashMap<>(); - private CachingValidationSupport myValidationSupport; + private IValidationSupport myValidationSupport; private IValidationSupport myMockSupport; private void addValidConcept(String theSystem, String theCode) { @@ -812,7 +811,7 @@ public class FhirInstanceValidatorR4BTest extends BaseValidationTestWithInlineMo public void testValidateProfileWithExtension() throws IOException, FHIRException { PrePopulatedValidationSupport valSupport = new PrePopulatedValidationSupport(ourCtx); DefaultProfileValidationSupport defaultSupport = new DefaultProfileValidationSupport(ourCtx); - CachingValidationSupport support = new CachingValidationSupport(new ValidationSupportChain(defaultSupport, valSupport, new InMemoryTerminologyServerValidationSupport(ourCtx)), false); + ValidationSupportChain support = new ValidationSupportChain(defaultSupport, valSupport, new InMemoryTerminologyServerValidationSupport(ourCtx)).setCodeableConceptValidationSuccessfulIfNotAllCodingsAreValid(false); // Prepopulate SDs valSupport.addStructureDefinition(loadStructureDefinition(defaultSupport, "/r4/myconsent-profile.xml")); @@ -1694,7 +1693,7 @@ public class FhirInstanceValidatorR4BTest extends BaseValidationTestWithInlineMo new CommonCodeSystemsTerminologyService(ourCtx), new InMemoryTerminologyServerValidationSupport(ourCtx), new SnapshotGeneratingValidationSupport(ourCtx)); - myValidationSupport = new CachingValidationSupport(chain, theLogicalAnd); + myValidationSupport = chain.setCodeableConceptValidationSuccessfulIfNotAllCodingsAreValid(theLogicalAnd); myInstanceVal = new FhirInstanceValidator(myValidationSupport); myFhirValidator.registerValidatorModule(myInstanceVal); } diff --git a/hapi-fhir-validation/src/test/java/org/hl7/fhir/r5/validation/FhirInstanceValidatorR5Test.java b/hapi-fhir-validation/src/test/java/org/hl7/fhir/r5/validation/FhirInstanceValidatorR5Test.java index abcb0f94704..7a58d4f7d25 100644 --- a/hapi-fhir-validation/src/test/java/org/hl7/fhir/r5/validation/FhirInstanceValidatorR5Test.java +++ b/hapi-fhir-validation/src/test/java/org/hl7/fhir/r5/validation/FhirInstanceValidatorR5Test.java @@ -16,9 +16,9 @@ import ca.uhn.fhir.validation.SingleValidationMessage; import ca.uhn.fhir.validation.ValidationResult; import com.google.common.base.Charsets; import org.apache.commons.io.IOUtils; -import org.hl7.fhir.common.hapi.validation.support.CachingValidationSupport; 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.SnapshotGeneratingValidationSupport; import org.hl7.fhir.common.hapi.validation.support.ValidationSupportChain; import org.hl7.fhir.common.hapi.validation.validator.FhirInstanceValidator; import org.hl7.fhir.instance.model.api.IBaseResource; @@ -100,7 +100,7 @@ public class FhirInstanceValidatorR5Test extends BaseValidationTestWithInlineMoc private Set mySupportedValueSets = new HashSet<>(); private Set myValidSystems = new HashSet<>(); private Set myValidSystemsNotReturningIssues = new HashSet<>(); - private CachingValidationSupport myValidationSupport; + private IValidationSupport myValidationSupport; private void addValidConcept(String theSystem, String theCode) { addValidConcept(theSystem, theCode, true); @@ -141,7 +141,7 @@ public class FhirInstanceValidatorR5Test extends BaseValidationTestWithInlineMoc myMockSupport = mock(IValidationSupport.class); when(myMockSupport.getFhirContext()).thenReturn(ourCtx); - myValidationSupport = new CachingValidationSupport(new ValidationSupportChain(myMockSupport, myDefaultValidationSupport, new InMemoryTerminologyServerValidationSupport(ourCtx), new CommonCodeSystemsTerminologyService(ourCtx))); + myValidationSupport = new ValidationSupportChain(myMockSupport, myDefaultValidationSupport, new InMemoryTerminologyServerValidationSupport(ourCtx), new CommonCodeSystemsTerminologyService(ourCtx), new SnapshotGeneratingValidationSupport(ourCtx)); myInstanceVal = new FhirInstanceValidator(myValidationSupport); myVal.registerValidatorModule(myInstanceVal); @@ -222,7 +222,7 @@ public class FhirInstanceValidatorR5Test extends BaseValidationTestWithInlineMoc when(myMockSupport.fetchStructureDefinition(nullable(String.class))).thenAnswer(new Answer() { @Override public StructureDefinition answer(InvocationOnMock theInvocation) { - StructureDefinition retVal = (StructureDefinition) myDefaultValidationSupport.fetchStructureDefinition((String) theInvocation.getArguments()[1]); + StructureDefinition retVal = (StructureDefinition) myDefaultValidationSupport.fetchStructureDefinition((String) theInvocation.getArguments()[0]); ourLog.debug("fetchStructureDefinition({}) : {}", new Object[]{theInvocation.getArguments()[0], retVal}); return retVal; } diff --git a/hapi-fhir-validation/src/test/java/org/hl7/fhir/r5/validation/QuestionnaireResponseValidatorR5Test.java b/hapi-fhir-validation/src/test/java/org/hl7/fhir/r5/validation/QuestionnaireResponseValidatorR5Test.java index ebd1679278b..fbd25fbad33 100644 --- a/hapi-fhir-validation/src/test/java/org/hl7/fhir/r5/validation/QuestionnaireResponseValidatorR5Test.java +++ b/hapi-fhir-validation/src/test/java/org/hl7/fhir/r5/validation/QuestionnaireResponseValidatorR5Test.java @@ -112,7 +112,7 @@ public class QuestionnaireResponseValidatorR5Test { ValueSet options = new ValueSet(); options.getCompose().addInclude().setSystem("http://codesystems.com/system").addConcept().setCode("code0"); - when(myValSupport.fetchResource(eq(ValueSet.class), eq("http://somevalueset"))).thenReturn(options); + when(myValSupport.fetchValueSet(eq("http://somevalueset"))).thenReturn(options); when(myValSupport.validateCode(any(), any(), any(), any(), any(), nullable(String.class))) .thenReturn(new IValidationSupport.CodeValidationResult().setSeverity(IValidationSupport.IssueSeverity.ERROR).setMessage("Unknown code")); @@ -241,7 +241,7 @@ public class QuestionnaireResponseValidatorR5Test { ValueSet options = new ValueSet(); options.getCompose().addInclude().setSystem("http://codesystems.com/system").addConcept().setCode("code0"); options.getCompose().addInclude().setSystem("http://codesystems.com/system2").addConcept().setCode("code2"); - when(myValSupport.fetchResource(eq(ValueSet.class), eq("http://somevalueset"))).thenReturn(options); + when(myValSupport.fetchValueSet(eq("http://somevalueset"))).thenReturn(options); QuestionnaireResponse qa; ValidationResult errors; @@ -374,7 +374,7 @@ public class QuestionnaireResponseValidatorR5Test { ValueSet options = new ValueSet(); options.getCompose().addInclude().setSystem(codeSystemUrl).addConcept().setCode(codeValue); - when(myValSupport.fetchResource(eq(ValueSet.class), eq(valueSetRef))) + when(myValSupport.fetchValueSet(eq(valueSetRef))) .thenReturn(options); when(myValSupport.validateCode(any(), any(), eq(codeSystemUrl), eq(codeValue), any(String.class), anyString())) .thenReturn(new IValidationSupport.CodeValidationResult().setCode(codeValue)); @@ -435,7 +435,8 @@ public class QuestionnaireResponseValidatorR5Test { ValueSet options = new ValueSet(); options.getCompose().addInclude().setSystem(codeSystemUrl).addConcept().setCode(codeValue); - when(myValSupport.fetchResource(eq(ValueSet.class), eq(valueSetRef))) + when(myValSupport.isValueSetSupported(any(), eq(valueSetRef))).thenReturn(true); + when(myValSupport.fetchValueSet(eq(valueSetRef))) .thenReturn(options); when(myValSupport.validateCode(any(), any(), eq(codeSystemUrl), eq(codeValue), any(String.class), anyString())) .thenReturn(new IValidationSupport.CodeValidationResult().setCode(codeValue)); @@ -564,7 +565,7 @@ public class QuestionnaireResponseValidatorR5Test { ValueSet options = new ValueSet(); options.getCompose().addInclude().setSystem("http://codesystems.com/system").addConcept().setCode("code0"); options.getCompose().addInclude().setSystem("http://codesystems.com/system2").addConcept().setCode("code2"); - when(myValSupport.fetchResource(eq(ValueSet.class), eq("http://somevalueset"))).thenReturn(options); + when(myValSupport.fetchValueSet(eq("http://somevalueset"))).thenReturn(options); when(myValSupport.validateCode(any(), any(), eq("http://codesystems.com/system"), eq("code0"), any(), nullable(String.class))) .thenReturn(new IValidationSupport.CodeValidationResult().setCode("code0")); @@ -721,7 +722,7 @@ public class QuestionnaireResponseValidatorR5Test { .setValue(new Coding(SYSTEMURI_ICC_SCHOOLTYPE, CODE_ICC_SCHOOLTYPE_PT, "")); when(myValSupport.fetchResource(eq(Questionnaire.class), eq(qa.getQuestionnaire()))).thenReturn(questionnaire); - when(myValSupport.fetchResource(eq(ValueSet.class), eq(ID_VS_SCHOOLTYPE))).thenReturn(iccSchoolTypeVs); + when(myValSupport.fetchValueSet(eq(ID_VS_SCHOOLTYPE))).thenReturn(iccSchoolTypeVs); when(myValSupport.validateCodeInValueSet(any(), any(), any(), any(), any(), any(ValueSet.class))).thenReturn(new IValidationSupport.CodeValidationResult().setCode(CODE_ICC_SCHOOLTYPE_PT)); when(myValSupport.fetchCodeSystem(eq(SYSTEMURI_ICC_SCHOOLTYPE))).thenReturn(codeSystem); ValidationResult errors = myVal.validateWithResult(qa); diff --git a/hapi-tinder-plugin/pom.xml b/hapi-tinder-plugin/pom.xml index d3fa30406ab..93b151848b7 100644 --- a/hapi-tinder-plugin/pom.xml +++ b/hapi-tinder-plugin/pom.xml @@ -5,7 +5,7 @@ ca.uhn.hapi.fhir hapi-fhir - 7.7.7-SNAPSHOT + 7.7.8-SNAPSHOT ../pom.xml diff --git a/hapi-tinder-test/pom.xml b/hapi-tinder-test/pom.xml index db18a5dad1f..15b14e92ac8 100644 --- a/hapi-tinder-test/pom.xml +++ b/hapi-tinder-test/pom.xml @@ -4,7 +4,7 @@ ca.uhn.hapi.fhir hapi-fhir - 7.7.7-SNAPSHOT + 7.7.8-SNAPSHOT ../pom.xml diff --git a/pom.xml b/pom.xml index 9b19056b43d..05227daf415 100644 --- a/pom.xml +++ b/pom.xml @@ -8,7 +8,7 @@ ca.uhn.hapi.fhir hapi-fhir pom - 7.7.7-SNAPSHOT + 7.7.8-SNAPSHOT HAPI-FHIR An open-source implementation of the FHIR specification in Java. @@ -1033,7 +1033,13 @@ 3.3.0 1.8 4.12.0 + 2.8.0 + ${otel_instrumentation.version}-alpha 4.1.2 1.4 6.2.9.Final @@ -2306,6 +2312,16 @@ pom import
+ + io.opentelemetry.javaagent + opentelemetry-testing-common + ${otel_agent_for_testing.version} + + + io.opentelemetry.javaagent + opentelemetry-agent-for-testing + ${otel_agent_for_testing.version} + org.assertj @@ -2623,7 +2639,7 @@ ca.uhn.hapi.fhir hapi-tinder-plugin - 7.7.7-SNAPSHOT + 7.7.8-SNAPSHOT diff --git a/tests/hapi-fhir-base-test-jaxrsserver-kotlin/pom.xml b/tests/hapi-fhir-base-test-jaxrsserver-kotlin/pom.xml index 2618b695119..7310102bfc5 100644 --- a/tests/hapi-fhir-base-test-jaxrsserver-kotlin/pom.xml +++ b/tests/hapi-fhir-base-test-jaxrsserver-kotlin/pom.xml @@ -7,7 +7,7 @@ ca.uhn.hapi.fhir hapi-fhir - 7.7.7-SNAPSHOT + 7.7.8-SNAPSHOT ../../pom.xml diff --git a/tests/hapi-fhir-base-test-mindeps-client/pom.xml b/tests/hapi-fhir-base-test-mindeps-client/pom.xml index 1f3098c2673..df98eac11cc 100644 --- a/tests/hapi-fhir-base-test-mindeps-client/pom.xml +++ b/tests/hapi-fhir-base-test-mindeps-client/pom.xml @@ -4,7 +4,7 @@ ca.uhn.hapi.fhir hapi-fhir - 7.7.7-SNAPSHOT + 7.7.8-SNAPSHOT ../../pom.xml diff --git a/tests/hapi-fhir-base-test-mindeps-server/pom.xml b/tests/hapi-fhir-base-test-mindeps-server/pom.xml index f4c44792a09..180c4f6347f 100644 --- a/tests/hapi-fhir-base-test-mindeps-server/pom.xml +++ b/tests/hapi-fhir-base-test-mindeps-server/pom.xml @@ -5,7 +5,7 @@ ca.uhn.hapi.fhir hapi-fhir - 7.7.7-SNAPSHOT + 7.7.8-SNAPSHOT ../../pom.xml