diff --git a/org.hl7.fhir.r5/src/main/java/org/hl7/fhir/r5/context/BaseWorkerContext.java b/org.hl7.fhir.r5/src/main/java/org/hl7/fhir/r5/context/BaseWorkerContext.java index f05f4ca3e..ab8f72c8e 100644 --- a/org.hl7.fhir.r5/src/main/java/org/hl7/fhir/r5/context/BaseWorkerContext.java +++ b/org.hl7.fhir.r5/src/main/java/org/hl7/fhir/r5/context/BaseWorkerContext.java @@ -1286,7 +1286,7 @@ public abstract class BaseWorkerContext extends I18nBase implements IWorkerConte } if (res != null) { updateUnsupportedCodeSystems(res, code, getCodeKey(code)); - return res; + return new ValidationResult(res); } List issues = new ArrayList<>(); @@ -1566,7 +1566,7 @@ public abstract class BaseWorkerContext extends I18nBase implements IWorkerConte if (cachingAllowed) { res = txCache.getValidation(cacheToken); if (res != null) { - return res; + return new ValidationResult(res); } } for (Coding c : code.getCoding()) { diff --git a/org.hl7.fhir.r5/src/main/java/org/hl7/fhir/r5/terminologies/utilities/ValidationResult.java b/org.hl7.fhir.r5/src/main/java/org/hl7/fhir/r5/terminologies/utilities/ValidationResult.java index 2264de156..de92af6fb 100644 --- a/org.hl7.fhir.r5/src/main/java/org/hl7/fhir/r5/terminologies/utilities/ValidationResult.java +++ b/org.hl7.fhir.r5/src/main/java/org/hl7/fhir/r5/terminologies/utilities/ValidationResult.java @@ -1,9 +1,6 @@ package org.hl7.fhir.r5.terminologies.utilities; -import java.util.ArrayList; -import java.util.Collections; -import java.util.List; -import java.util.Set; +import java.util.*; import org.hl7.fhir.r5.model.CodeableConcept; import org.hl7.fhir.r5.model.Coding; @@ -35,6 +32,30 @@ public class ValidationResult { + errorClass + ", txLink=" + txLink + "]"; } + public ValidationResult(ValidationResult validationResult) { + this.definition = validationResult.definition == null ? null : validationResult.definition.copy(); + this.preferredDisplay = validationResult.preferredDisplay; + this.system = validationResult.system; + this.version = validationResult.version; + this.severity = validationResult.severity; + if (validationResult.messages != null) { + this.messages.addAll(validationResult.messages); + } + this.errorClass = validationResult.errorClass; + this.txLink = validationResult.txLink; + this.diagnostics = validationResult.diagnostics; + if (validationResult.issues != null) { + for (OperationOutcomeIssueComponent issue : validationResult.issues) { + this.issues.add(issue.copy()); + } + } + this.codeableConcept = validationResult.codeableConcept == null ? null : validationResult.codeableConcept.copy(); + this.unknownSystems = validationResult.unknownSystems == null ? null : new HashSet<>(validationResult.unknownSystems); + this.inactive = validationResult.inactive; + this.status = validationResult.status; + this.server = validationResult.server; + } + public ValidationResult(IssueSeverity severity, String message, List issues) { this.severity = severity; if (message != null) { @@ -334,4 +355,57 @@ public class ValidationResult { this.server = server; } + public boolean equals(Object otherObject) { + if (!(otherObject instanceof ValidationResult)) { + return false; + } + + ValidationResult other = (ValidationResult) otherObject; + if (!Objects.equals(this.system, other.system)) { + return false; + } + if (!Objects.equals(this.version, other.version)) { + return false; + } + if (!Objects.equals(this.preferredDisplay, other.preferredDisplay)) { + return false; + } + if (!Objects.equals(this.severity, other.severity)) { + return false; + } + if (!Objects.equals(this.definition, other.definition)) { + return false; + } + if (!Objects.equals(this.messages, other.messages)) { + return false; + } + if (!Objects.equals(this.errorClass, other.errorClass)) { + return false; + } + if (!Objects.equals(this.txLink, other.txLink)) { + return false; + } + if (!Objects.equals(this.diagnostics, other.diagnostics)) { + return false; + } + if (!Objects.equals(this.issues, other.issues)) { + return false; + } + if (!Objects.equals(this.codeableConcept, other.codeableConcept)) { + return false; + } + if (!Objects.equals(this.unknownSystems, other.unknownSystems)) { + return false; + } + if (this.inactive != other.inactive) { + return false; + } + if (!Objects.equals(this.status, other.status)) { + return false; + } + if (!Objects.equals(this.server, other.server)) { + return false; + } + return true; + } } \ No newline at end of file diff --git a/org.hl7.fhir.r5/src/test/java/org/hl7/fhir/r5/context/BaseWorkerContextTests.java b/org.hl7.fhir.r5/src/test/java/org/hl7/fhir/r5/context/BaseWorkerContextTests.java index da99ea473..3592bd6e5 100644 --- a/org.hl7.fhir.r5/src/test/java/org/hl7/fhir/r5/context/BaseWorkerContextTests.java +++ b/org.hl7.fhir.r5/src/test/java/org/hl7/fhir/r5/context/BaseWorkerContextTests.java @@ -1,31 +1,224 @@ package org.hl7.fhir.r5.context; import org.hl7.fhir.exceptions.FHIRException; -import org.hl7.fhir.r5.model.CanonicalResource; -import org.hl7.fhir.r5.model.PackageInformation; -import org.hl7.fhir.r5.model.Parameters; -import org.hl7.fhir.r5.model.Resource; -import org.hl7.fhir.r5.model.ValueSet; +import org.hl7.fhir.r5.model.*; +import org.hl7.fhir.r5.terminologies.client.ITerminologyClient; +import org.hl7.fhir.r5.terminologies.client.TerminologyClientContext; +import org.hl7.fhir.r5.terminologies.expansion.ValueSetExpander; +import org.hl7.fhir.r5.terminologies.expansion.ValueSetExpansionOutcome; +import org.hl7.fhir.r5.terminologies.utilities.TerminologyCache; +import org.hl7.fhir.r5.terminologies.utilities.ValidationResult; +import org.hl7.fhir.r5.terminologies.validation.ValueSetValidator; import org.hl7.fhir.r5.utils.validation.IResourceValidator; +import org.hl7.fhir.r5.utils.validation.ValidationContextCarrier; import org.hl7.fhir.utilities.FhirPublication; +import org.hl7.fhir.utilities.ToolingClientLogger; import org.hl7.fhir.utilities.npm.BasePackageCacheManager; import org.hl7.fhir.utilities.npm.NpmPackage; +import org.hl7.fhir.utilities.validation.ValidationMessage; import org.hl7.fhir.utilities.validation.ValidationOptions; +import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; -import net.sourceforge.plantuml.tim.stdlib.GetVariableValue; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.ArgumentMatcher; +import org.mockito.Mock; +import org.mockito.Mockito; +import org.mockito.Spy; +import org.mockito.junit.jupiter.MockitoExtension; import java.io.FileNotFoundException; import java.io.IOException; import java.util.ArrayList; -import java.util.HashSet; +import java.util.Arrays; import java.util.List; -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.ArgumentMatchers.*; +import static org.mockito.ArgumentMatchers.argThat; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.times; +@ExtendWith(MockitoExtension.class) public class BaseWorkerContextTests { + private static final String DUMMY_URL = "dummyUrl"; + + @Spy + BaseWorkerContext context = new BaseWorkerContext(){ + @Override + public String getVersion() { + return "4.0.1"; + } + + @Override + public IResourceValidator newValidator() throws FHIRException { + return null; + } + + @Override + public T fetchResourceRaw(Class class_, String uri) { + return null; + } + + @Override + public void cachePackage(PackageInformation packageInfo) { + + } + + @Override + public List getResourceNames() { + return List.of(); + } + + @Override + public int loadFromPackage(NpmPackage pi, IContextResourceLoader loader) throws FileNotFoundException, IOException, FHIRException { + return 0; + } + + @Override + public int loadFromPackage(NpmPackage pi, IContextResourceLoader loader, List types) throws FileNotFoundException, IOException, FHIRException { + return 0; + } + + @Override + public int loadFromPackageAndDependencies(NpmPackage pi, IContextResourceLoader loader, BasePackageCacheManager pcm) throws FileNotFoundException, IOException, FHIRException { + return 0; + } + + @Override + public boolean hasPackage(String id, String ver) { + return false; + } + + @Override + public boolean hasPackage(PackageInformation pack) { + return false; + } + + @Override + public PackageInformation getPackage(String id, String ver) { + return null; + } + + @Override + public String getSpecUrl() { + return ""; + } + }; + + @Mock + TerminologyCache terminologyCache; + + @Mock + ToolingClientLogger txLog; + + @Mock + ITerminologyClient terminologyClient; + + @Mock + TerminologyCache.CacheToken cacheToken; + + ValidationResult cachedValidationResult = new ValidationResult(ValidationMessage.IssueSeverity.INFORMATION, "dummyMessageForCached", List.of()); + + ValidationResult createdValidationResult = new ValidationResult(ValidationMessage.IssueSeverity.INFORMATION, "dummyMessageForCreated", List.of()); + + @Mock + ValueSetExpansionOutcome expectedExpansionResult; + + @Mock + ValueSetValidator valueSetCheckerSimple; + + @Mock + ValueSetExpander valueSetExpanderSimple; + + @Mock + Parameters pIn; + + @Mock + Parameters expParameters; + + public static final TerminologyCapabilities terminologyCapabilities = new TerminologyCapabilities(); + static { terminologyCapabilities.getExpansion().setParameter(Arrays.asList());} + + public static final CapabilityStatement.CapabilityStatementSoftwareComponent software = new CapabilityStatement.CapabilityStatementSoftwareComponent(); + static { software.setVersion("dummyVersion"); } + + public static final CapabilityStatement capabilitiesStatement = new CapabilityStatement(); + static { capabilitiesStatement.setSoftware(software);} + + private final static Parameters pInWithDependentResources = new Parameters(); + static { + pInWithDependentResources.addParameter("includeDefinition", false); + pInWithDependentResources.addParameter("excludeNester", false); + pInWithDependentResources.addParameter("incomplete-ok", true); + } + + public static class ValueSetMatcher implements ArgumentMatcher { + + private ValueSet left; + + ValueSetMatcher(ValueSet left) { + this.left = left; + } + + @Override + public boolean matches(ValueSet right) { + return left.getStatus().equals(right.getStatus()) + && left.getCompose().equalsDeep(right.getCompose()); + } + } + + public static class CodingMatcher implements ArgumentMatcher { + final private Coding left; + + CodingMatcher(Coding left) { this.left = left; } + + public boolean matches(Coding right) { + return left.equalsShallow(right); + } + } + + public static class ParametersMatcher implements ArgumentMatcher { + final private Parameters left; + + ParametersMatcher(Parameters left) { + this.left = left; + } + + @Override + public boolean matches(Parameters right) { + return left.equalsShallow(right); + } + } + + public static class TerminologyClientContextMatcher implements ArgumentMatcher { + + final private TerminologyClientContext left; + + TerminologyClientContextMatcher(TerminologyClientContext left) { + this.left = left; + } + + @Override + public boolean matches(TerminologyClientContext argument) { + return left.getAddress().equals(argument.getAddress()); + } + } + + @BeforeEach + public void beforeEach() { + + Mockito.doReturn(DUMMY_URL).when(terminologyClient).getAddress(); + context.initTxCache(terminologyCache); + context.expParameters = expParameters; + context.terminologyClientManager.setMasterClient(terminologyClient, false); + context.txLog = txLog; + } + + public BaseWorkerContextTests() throws IOException { + } + private BaseWorkerContext getBaseWorkerContext() throws IOException { BaseWorkerContext baseWorkerContext = new BaseWorkerContext() { @Override @@ -123,4 +316,273 @@ public class BaseWorkerContextTests { baseWorkerContext.addServerValidationParameters(baseWorkerContext.getTxClientManager().getMaster(), new ValueSet(), pin, new ValidationOptions(FhirPublication.fromCode(baseWorkerContext.getVersion()))); assertNull(pin.getParameter("mode")); } + + @Test + public void testValidateCodingWithCache() throws IOException { + ValidationOptions validationOptions = new ValidationOptions(FhirPublication.R5).withGuessSystem().withVersionFlexible(false); + ValueSet valueSet = new ValueSet(); + Coding coding = new Coding(); + + Mockito.doReturn(cacheToken).when(terminologyCache).generateValidationToken(validationOptions, coding, valueSet, expParameters); + Mockito.doReturn(cachedValidationResult).when(terminologyCache).getValidation(cacheToken); + + ValidationContextCarrier ctxt = mock(ValidationContextCarrier.class); + + ValidationResult actualValidationResult = context.validateCode(validationOptions, coding, valueSet, ctxt); + + assertNotSame(cachedValidationResult, actualValidationResult); + assertEquals(cachedValidationResult, actualValidationResult); + + Mockito.verify(valueSetCheckerSimple, times(0)).validateCode("Coding", coding); + Mockito.verify(terminologyCache).getValidation(cacheToken); + Mockito.verify(terminologyCache, times(0)).cacheValidation(any(), any(), anyBoolean()); + } + + @Test + public void testValidateCodingWithValueSetChecker() throws IOException { + ValidationOptions validationOptions = new ValidationOptions(FhirPublication.R5).withGuessSystem().withVersionFlexible(false); + ValueSet valueSet = new ValueSet(); + Coding coding = new Coding(); + + Mockito.doReturn(cacheToken).when(terminologyCache).generateValidationToken(validationOptions, coding, valueSet, expParameters); + + Mockito.doReturn(valueSetCheckerSimple).when(context).constructValueSetCheckerSimple(any(), any(), any()); + Mockito.doReturn(createdValidationResult).when(valueSetCheckerSimple).validateCode(eq("Coding"), any(Coding.class)); + + ValidationContextCarrier ctxt = mock(ValidationContextCarrier.class); + + ValidationResult actualValidationResult = context.validateCode(validationOptions, coding, valueSet, ctxt); + + assertSame(createdValidationResult, actualValidationResult); + + Mockito.verify(valueSetCheckerSimple).validateCode(eq("Coding"), argThat(new CodingMatcher(coding))); + Mockito.verify(terminologyCache).getValidation(cacheToken); + Mockito.verify(terminologyCache).cacheValidation(eq(cacheToken), same(createdValidationResult),eq(false)); + + } + + + @Test + public void testValidateCodingWithServer() throws IOException { + ValidationOptions validationOptions = new ValidationOptions(FhirPublication.R5).withGuessSystem().withVersionFlexible(false).withNoClient(); + ValueSet valueSet = new ValueSet(); + Coding coding = new Coding(); + + Mockito.doReturn(cacheToken).when(terminologyCache).generateValidationToken(validationOptions, coding, valueSet, expParameters); + Mockito.doReturn(pIn).when(context).constructParameters(validationOptions, coding); + + TerminologyClientContext terminologyClientContext = context.getTxClientManager().getMaster(); + + Mockito.doReturn(createdValidationResult).when(context).validateOnServer(terminologyClientContext, valueSet, pIn, validationOptions); + + ValidationContextCarrier ctxt = mock(ValidationContextCarrier.class); + + ValidationResult actualValidationResult = context.validateCode(validationOptions, coding, valueSet, ctxt); + + assertSame(createdValidationResult, actualValidationResult); + + Mockito.verify(valueSetCheckerSimple, times(0)).validateCode("Coding", coding); + Mockito.verify(terminologyCache).getValidation(cacheToken); + Mockito.verify(terminologyCache).cacheValidation(eq(cacheToken), same(createdValidationResult),eq(true)); + } + + @Test + public void testValidateCodableConceptWithCache() throws IOException { + CodeableConcept codeableConcept = new CodeableConcept(); + ValueSet valueSet = new ValueSet(); + + Mockito.doReturn(cacheToken).when(terminologyCache).generateValidationToken(CacheTestUtils.validationOptions, codeableConcept, valueSet, expParameters); + Mockito.doReturn(cachedValidationResult).when(terminologyCache).getValidation(cacheToken); + + ValidationResult actualValidationResult = context.validateCode(CacheTestUtils.validationOptions, codeableConcept, valueSet); + assertNotSame(cachedValidationResult, actualValidationResult); + assertEquals(cachedValidationResult, actualValidationResult); + + Mockito.verify(valueSetCheckerSimple, times(0)).validateCode("CodeableConcept", codeableConcept); + Mockito.verify(terminologyCache).getValidation(cacheToken); + Mockito.verify(terminologyCache, times(0)).cacheValidation(any(), any(), anyBoolean()); + } + + @Test + public void testValidateCodableConceptWithValueSetChecker() throws IOException { + Mockito.doReturn(valueSetCheckerSimple).when(context).constructValueSetCheckerSimple(any(), any()); + Mockito.doReturn(createdValidationResult).when(valueSetCheckerSimple).validateCode(eq("CodeableConcept"),any(CodeableConcept.class)); + + CodeableConcept codeableConcept = new CodeableConcept(); + ValueSet valueSet = new ValueSet(); + + Mockito.doReturn(cacheToken).when(terminologyCache).generateValidationToken(CacheTestUtils.validationOptions, codeableConcept, valueSet, expParameters); + + ValidationResult validationResultB = context.validateCode(CacheTestUtils.validationOptions, codeableConcept, valueSet); + assertSame(createdValidationResult, validationResultB); + + Mockito.verify(valueSetCheckerSimple).validateCode("CodeableConcept", codeableConcept); + Mockito.verify(terminologyCache).cacheValidation(eq(cacheToken), same(createdValidationResult), eq(false)); + Mockito.verify(context, times(0)).validateOnServer(any(), any(), any(), any()); + } + + + @Test + public void testValidateCodableConceptWithServer() throws IOException { + + CodeableConcept codeableConcept = new CodeableConcept(); + ValueSet valueSet = new ValueSet(); + + ValidationOptions validationOptions = CacheTestUtils.validationOptions.withNoClient(); + Mockito.doReturn(pIn).when(context).constructParameters(validationOptions, codeableConcept); + + TerminologyClientContext terminologyClientContext = context.getTxClientManager().getMaster(); + + Mockito.doReturn(createdValidationResult).when(context).validateOnServer(terminologyClientContext, valueSet, pIn, validationOptions); + + Mockito.doReturn(cacheToken).when(terminologyCache).generateValidationToken(validationOptions, codeableConcept, valueSet, expParameters); + + ValidationResult validationResultB = context.validateCode(validationOptions, codeableConcept, valueSet); + + assertSame(createdValidationResult, validationResultB); + + Mockito.verify(valueSetCheckerSimple, times(0)).validateCode("CodeableConcept", codeableConcept); + Mockito.verify(terminologyCache).cacheValidation(eq(cacheToken), same(createdValidationResult), eq(true)); + Mockito.verify(context).validateOnServer(terminologyClientContext, valueSet, pIn, validationOptions); + } + + @Test + public void testExpandValueSetWithCache() throws IOException { + + ValueSet.ConceptSetComponent inc = new ValueSet.ConceptSetComponent(); + + ValueSet vs = new ValueSet(); + vs.setStatus(Enumerations.PublicationStatus.ACTIVE); + vs.setCompose(new ValueSet.ValueSetComposeComponent()); + vs.getCompose().setInactive(true); + vs.getCompose().getInclude().add(inc); + + Mockito.doReturn(cacheToken).when(terminologyCache).generateExpandToken(argThat(new ValueSetMatcher(vs)),eq(true)); + Mockito.doReturn(expectedExpansionResult).when(terminologyCache).getExpansion(cacheToken); + + ValueSetExpansionOutcome actualExpansionResult = context.expandVS(inc, true, false); + + assertSame(expectedExpansionResult, actualExpansionResult); + + Mockito.verify(terminologyCache).getExpansion(cacheToken); + Mockito.verify(terminologyCache, times(0)).cacheExpansion(any(), any(), anyBoolean()); + Mockito.verify(terminologyClient, times(0)).expandValueset(any(), any()); + } + + @Test + public void testExpandValueSetWithClient() throws IOException { + + ValueSet.ConceptSetComponent inc = new ValueSet.ConceptSetComponent(); + + ValueSet vs = new ValueSet(); + vs.setStatus(Enumerations.PublicationStatus.ACTIVE); + vs.setCompose(new ValueSet.ValueSetComposeComponent()); + vs.getCompose().setInactive(true); + vs.getCompose().getInclude().add(inc); + + Mockito.doReturn(cacheToken).when(terminologyCache).generateExpandToken(argThat(new ValueSetMatcher(vs)),eq(true)); + + TerminologyClientContext terminologyClientContext = context.getTxClientManager().getMaster(); + + Mockito.doReturn(expParameters).when(context).constructParameters(argThat(new TerminologyClientContextMatcher(terminologyClientContext)),argThat(new ValueSetMatcher(vs)), eq(true)); + + ValueSet expectedValueSet = new ValueSet(); + + Mockito.doReturn(expectedValueSet).when(terminologyClient).expandValueset(argThat(new ValueSetMatcher(vs)), + argThat(new ParametersMatcher(pInWithDependentResources))); + + ValueSetExpansionOutcome actualExpansionResult = context.expandVS(inc, true, false); + + assertEquals(expectedValueSet, actualExpansionResult.getValueset()); + + Mockito.verify(terminologyCache).getExpansion(cacheToken); + Mockito.verify(terminologyCache).cacheExpansion(cacheToken, actualExpansionResult,true); + } + + @Test + public void testExpandValueSet4ArgsWithCache() throws IOException { + + ValueSet vs = new ValueSet(); + vs.setUrl(DUMMY_URL); + + Mockito.doReturn(cacheToken).when(terminologyCache).generateExpandToken(vs,true); + Mockito.doReturn(expectedExpansionResult).when(terminologyCache).getExpansion(cacheToken); + + Parameters pIn = new Parameters(); + + ValueSetExpansionOutcome actualExpansionResult = context.expandVS(vs, true, true, true, pIn); + + assertEquals(expectedExpansionResult, actualExpansionResult); + + Mockito.verify(terminologyCache).getExpansion(cacheToken); + Mockito.verify(terminologyCache, times(0)).cacheExpansion(any(), any(), anyBoolean()); + Mockito.verify(terminologyClient, times(0)).expandValueset(any(), any()); + } + + private static class ValidationOptionsFhirPublicationMatcher implements ArgumentMatcher { + + final FhirPublication fhirPublication; + + ValidationOptionsFhirPublicationMatcher(FhirPublication fhirPublication) { + this.fhirPublication = fhirPublication; + } + @Override + public boolean matches(ValidationOptions argument) { + return fhirPublication.toCode().equals(argument.getFhirVersion().toCode()); + } + } + + @Test + public void testExpandValueSet4ArgsWithValueSetExpanderSimple() throws IOException { + + ValueSet vs = new ValueSet(); + vs.setUrl(DUMMY_URL); + + Mockito.doReturn(cacheToken).when(terminologyCache).generateExpandToken(vs,true); + + Parameters pIn = new Parameters(); + + Mockito.doReturn(vs).when(expectedExpansionResult).getValueset(); + + Mockito.doReturn(expectedExpansionResult).when(valueSetExpanderSimple).expand(eq(vs), + argThat(new ParametersMatcher(pInWithDependentResources))); + + Mockito.doReturn(valueSetExpanderSimple).when(context).constructValueSetExpanderSimple(argThat(new ValidationOptionsFhirPublicationMatcher(vs.getFHIRPublicationVersion()))); + + ValueSetExpansionOutcome actualExpansionResult = context.expandVS(vs, true, true, true, pIn); + + assertEquals(expectedExpansionResult, actualExpansionResult); + + Mockito.verify(terminologyCache).getExpansion(cacheToken); + Mockito.verify(terminologyCache).cacheExpansion(cacheToken, actualExpansionResult, false); + Mockito.verify(terminologyClient, times(0)).expandValueset(any(), any()); + } + + @Test + public void testExpandValueSet4ArgsWithClient() throws IOException { + + ValueSet vs = new ValueSet(); + vs.setUrl(DUMMY_URL); + + Mockito.doReturn(cacheToken).when(terminologyCache).generateExpandToken(vs,true); + + Parameters pIn = new Parameters(); + + ValueSet expectedValueSet = new ValueSet(); + expectedValueSet.setUrl("dummyUrl2"); + + Mockito.doReturn(expectedExpansionResult).when(valueSetExpanderSimple).expand(eq(vs), + argThat(new ParametersMatcher(pInWithDependentResources))); + + Mockito.doReturn(valueSetExpanderSimple).when(context).constructValueSetExpanderSimple(argThat(new ValidationOptionsFhirPublicationMatcher(vs.getFHIRPublicationVersion()))); + + Mockito.doReturn(expectedValueSet).when(terminologyClient).expandValueset(eq(vs), argThat(new ParametersMatcher(pInWithDependentResources))); + + ValueSetExpansionOutcome actualExpansionResult = context.expandVS(vs, true, true, true, pIn, false); + + assertEquals(expectedValueSet, actualExpansionResult.getValueset()); + + Mockito.verify(terminologyCache).getExpansion(cacheToken); + Mockito.verify(terminologyCache).cacheExpansion(cacheToken, actualExpansionResult, true); + } } diff --git a/org.hl7.fhir.r5/src/test/java/org/hl7/fhir/r5/context/SimpleWorkerContextTests.java b/org.hl7.fhir.r5/src/test/java/org/hl7/fhir/r5/context/SimpleWorkerContextTests.java index 4dc9b0279..677663c9f 100644 --- a/org.hl7.fhir.r5/src/test/java/org/hl7/fhir/r5/context/SimpleWorkerContextTests.java +++ b/org.hl7.fhir.r5/src/test/java/org/hl7/fhir/r5/context/SimpleWorkerContextTests.java @@ -31,6 +31,7 @@ import org.hl7.fhir.r5.terminologies.validation.ValueSetValidator; import org.hl7.fhir.r5.utils.validation.ValidationContextCarrier; import org.hl7.fhir.utilities.FhirPublication; import org.hl7.fhir.utilities.ToolingClientLogger; +import org.hl7.fhir.utilities.validation.ValidationMessage; import org.hl7.fhir.utilities.validation.ValidationOptions; import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.BeforeEach; @@ -61,27 +62,6 @@ public class SimpleWorkerContextTests { @Mock ITerminologyClient terminologyClient; - @Mock - TerminologyCache.CacheToken cacheToken; - - @Mock - ValidationResult expectedValidationResult; - - @Mock - ValueSetExpansionOutcome expectedExpansionResult; - - @Mock - ValueSetValidator valueSetCheckerSimple; - - @Mock - ValueSetExpander valueSetExpanderSimple; - - @Mock - Parameters pIn; - - @Mock - Parameters expParameters; - public static final TerminologyCapabilities terminologyCapabilities = new TerminologyCapabilities(); static { terminologyCapabilities.getExpansion().setParameter(Arrays.asList());} @@ -93,342 +73,11 @@ public class SimpleWorkerContextTests { @BeforeEach public void beforeEach() { - Mockito.doReturn(DUMMY_URL).when(terminologyClient).getAddress(); context.initTxCache(terminologyCache); - context.expParameters = expParameters; context.terminologyClientManager.setMasterClient(terminologyClient, false); - context.txLog = txLog; } - private final static Parameters pInWithDependentResources = new Parameters(); - static { - pInWithDependentResources.addParameter("includeDefinition", false); - pInWithDependentResources.addParameter("excludeNester", false); - pInWithDependentResources.addParameter("incomplete-ok", true); - } - - public class ValueSetMatcher implements ArgumentMatcher { - - private ValueSet left; - - ValueSetMatcher(ValueSet left) { - this.left = left; - } - - @Override - public boolean matches(ValueSet right) { - return left.getStatus().equals(right.getStatus()) - && left.getCompose().equalsDeep(right.getCompose()); - } - } - - public class CodingMatcher implements ArgumentMatcher { - final private Coding left; - - CodingMatcher(Coding left) { this.left = left; } - - public boolean matches(Coding right) { - return left.equalsShallow(right); - } - } - - public class ParametersMatcher implements ArgumentMatcher { - final private Parameters left; - - ParametersMatcher(Parameters left) { - this.left = left; - } - - @Override - public boolean matches(Parameters right) { - return left.equalsShallow(right); - } - } - - public class TerminologyClientContextMatcher implements ArgumentMatcher { - - final private TerminologyClientContext left; - - TerminologyClientContextMatcher(TerminologyClientContext left) { - this.left = left; - } - - @Override - public boolean matches(TerminologyClientContext argument) { - return left.getAddress().equals(argument.getAddress()); - } - } - - @Test - public void testValidateCodingWithCache() throws IOException { - ValidationOptions validationOptions = new ValidationOptions(FhirPublication.R5).withGuessSystem().withVersionFlexible(false); - ValueSet valueSet = new ValueSet(); - Coding coding = new Coding(); - - Mockito.doReturn(cacheToken).when(terminologyCache).generateValidationToken(validationOptions, coding, valueSet, expParameters); - Mockito.doReturn(expectedValidationResult).when(terminologyCache).getValidation(cacheToken); - - ValidationContextCarrier ctxt = mock(ValidationContextCarrier.class); - - ValidationResult actualValidationResult = context.validateCode(validationOptions, coding, valueSet, ctxt); - - assertEquals(expectedValidationResult, actualValidationResult); - - Mockito.verify(valueSetCheckerSimple, times(0)).validateCode("Coding", coding); - Mockito.verify(terminologyCache).getValidation(cacheToken); - Mockito.verify(terminologyCache, times(0)).cacheValidation(any(), any(), anyBoolean()); - } - - @Test - public void testValidateCodingWithValueSetChecker() throws IOException { - ValidationOptions validationOptions = new ValidationOptions(FhirPublication.R5).withGuessSystem().withVersionFlexible(false); - ValueSet valueSet = new ValueSet(); - Coding coding = new Coding(); - - Mockito.doReturn(cacheToken).when(terminologyCache).generateValidationToken(validationOptions, coding, valueSet, expParameters); - - Mockito.doReturn(valueSetCheckerSimple).when(context).constructValueSetCheckerSimple(any(), any(), any()); - Mockito.doReturn(expectedValidationResult).when(valueSetCheckerSimple).validateCode(eq("Coding"), any(Coding.class)); - - ValidationContextCarrier ctxt = mock(ValidationContextCarrier.class); - - ValidationResult actualValidationResult = context.validateCode(validationOptions, coding, valueSet, ctxt); - - assertEquals(expectedValidationResult, actualValidationResult); - - Mockito.verify(valueSetCheckerSimple).validateCode(eq("Coding"), argThat(new CodingMatcher(coding))); - Mockito.verify(terminologyCache).getValidation(cacheToken); - Mockito.verify(terminologyCache).cacheValidation(cacheToken, expectedValidationResult,false); - } - - - @Test - public void testValidateCodingWithServer() throws IOException { - ValidationOptions validationOptions = new ValidationOptions(FhirPublication.R5).withGuessSystem().withVersionFlexible(false).withNoClient(); - ValueSet valueSet = new ValueSet(); - Coding coding = new Coding(); - - Mockito.doReturn(cacheToken).when(terminologyCache).generateValidationToken(validationOptions, coding, valueSet, expParameters); - Mockito.doReturn(pIn).when(context).constructParameters(validationOptions, coding); - - TerminologyClientContext terminologyClientContext = context.getTxClientManager().getMaster(); - - Mockito.doReturn(expectedValidationResult).when(context).validateOnServer(terminologyClientContext, valueSet, pIn, validationOptions); - - ValidationContextCarrier ctxt = mock(ValidationContextCarrier.class); - - ValidationResult actualValidationResult = context.validateCode(validationOptions, coding, valueSet, ctxt); - - assertEquals(expectedValidationResult, actualValidationResult); - - Mockito.verify(valueSetCheckerSimple, times(0)).validateCode("Coding", coding); - Mockito.verify(terminologyCache).getValidation(cacheToken); - Mockito.verify(terminologyCache).cacheValidation(cacheToken, expectedValidationResult,true); - } - - @Test - public void testValidateCodableConceptWithCache() throws IOException { - CodeableConcept codeableConcept = new CodeableConcept(); - ValueSet valueSet = new ValueSet(); - - Mockito.doReturn(cacheToken).when(terminologyCache).generateValidationToken(CacheTestUtils.validationOptions, codeableConcept, valueSet, expParameters); - Mockito.doReturn(expectedValidationResult).when(terminologyCache).getValidation(cacheToken); - - ValidationResult actualValidationResult = context.validateCode(CacheTestUtils.validationOptions, codeableConcept, valueSet); - assertEquals(expectedValidationResult, actualValidationResult); - - Mockito.verify(valueSetCheckerSimple, times(0)).validateCode("CodeableConcept", codeableConcept); - Mockito.verify(terminologyCache).getValidation(cacheToken); - Mockito.verify(terminologyCache, times(0)).cacheValidation(any(), any(), anyBoolean()); - } - - @Test - public void testValidateCodableConceptWithValueSetChecker() throws IOException { - Mockito.doReturn(valueSetCheckerSimple).when(context).constructValueSetCheckerSimple(any(), any()); - Mockito.doReturn(expectedValidationResult).when(valueSetCheckerSimple).validateCode(eq("CodeableConcept"),any(CodeableConcept.class)); - - CodeableConcept codeableConcept = new CodeableConcept(); - ValueSet valueSet = new ValueSet(); - - Mockito.doReturn(cacheToken).when(terminologyCache).generateValidationToken(CacheTestUtils.validationOptions, codeableConcept, valueSet, expParameters); - - ValidationResult validationResultB = context.validateCode(CacheTestUtils.validationOptions, codeableConcept, valueSet); - assertEquals(expectedValidationResult, validationResultB); - - Mockito.verify(valueSetCheckerSimple).validateCode("CodeableConcept", codeableConcept); - Mockito.verify(terminologyCache).cacheValidation(cacheToken, expectedValidationResult, false); - Mockito.verify(context, times(0)).validateOnServer(any(), any(), any(), any()); - } - - - @Test - public void testValidateCodableConceptWithServer() throws IOException { - - CodeableConcept codeableConcept = new CodeableConcept(); - ValueSet valueSet = new ValueSet(); - - ValidationOptions validationOptions = CacheTestUtils.validationOptions.withNoClient(); - Mockito.doReturn(pIn).when(context).constructParameters(validationOptions, codeableConcept); - - TerminologyClientContext terminologyClientContext = context.getTxClientManager().getMaster(); - - Mockito.doReturn(expectedValidationResult).when(context).validateOnServer(terminologyClientContext, valueSet, pIn, validationOptions); - - Mockito.doReturn(cacheToken).when(terminologyCache).generateValidationToken(validationOptions, codeableConcept, valueSet, expParameters); - - ValidationResult validationResultB = context.validateCode(validationOptions, codeableConcept, valueSet); - - assertEquals(expectedValidationResult, validationResultB); - - Mockito.verify(valueSetCheckerSimple, times(0)).validateCode("CodeableConcept", codeableConcept); - Mockito.verify(terminologyCache).cacheValidation(cacheToken, expectedValidationResult, true); - Mockito.verify(context).validateOnServer(terminologyClientContext, valueSet, pIn, validationOptions); - } - - @Test - public void testExpandValueSetWithCache() throws IOException { - - ValueSet.ConceptSetComponent inc = new ValueSet.ConceptSetComponent(); - - ValueSet vs = new ValueSet(); - vs.setStatus(Enumerations.PublicationStatus.ACTIVE); - vs.setCompose(new ValueSet.ValueSetComposeComponent()); - vs.getCompose().setInactive(true); - vs.getCompose().getInclude().add(inc); - - Mockito.doReturn(cacheToken).when(terminologyCache).generateExpandToken(argThat(new ValueSetMatcher(vs)),eq(true)); - Mockito.doReturn(expectedExpansionResult).when(terminologyCache).getExpansion(cacheToken); - - ValueSetExpansionOutcome actualExpansionResult = context.expandVS(inc, true, false); - - assertEquals(expectedExpansionResult, actualExpansionResult); - - Mockito.verify(terminologyCache).getExpansion(cacheToken); - Mockito.verify(terminologyCache, times(0)).cacheExpansion(any(), any(), anyBoolean()); - Mockito.verify(terminologyClient, times(0)).expandValueset(any(), any()); - } - - @Test - public void testExpandValueSetWithClient() throws IOException { - - ValueSet.ConceptSetComponent inc = new ValueSet.ConceptSetComponent(); - - ValueSet vs = new ValueSet(); - vs.setStatus(Enumerations.PublicationStatus.ACTIVE); - vs.setCompose(new ValueSet.ValueSetComposeComponent()); - vs.getCompose().setInactive(true); - vs.getCompose().getInclude().add(inc); - - Mockito.doReturn(cacheToken).when(terminologyCache).generateExpandToken(argThat(new ValueSetMatcher(vs)),eq(true)); - - TerminologyClientContext terminologyClientContext = context.getTxClientManager().getMaster(); - - - Mockito.doReturn(expParameters).when(context).constructParameters(argThat(new TerminologyClientContextMatcher(terminologyClientContext)),argThat(new ValueSetMatcher(vs)), eq(true)); - - ValueSet expectedValueSet = new ValueSet(); - - - Mockito.doReturn(expectedValueSet).when(terminologyClient).expandValueset(argThat(new ValueSetMatcher(vs)), - argThat(new ParametersMatcher(pInWithDependentResources))); - - ValueSetExpansionOutcome actualExpansionResult = context.expandVS(inc, true, false); - - assertEquals(expectedValueSet, actualExpansionResult.getValueset()); - - Mockito.verify(terminologyCache).getExpansion(cacheToken); - Mockito.verify(terminologyCache).cacheExpansion(cacheToken, actualExpansionResult,true); - } - - @Test - public void testExpandValueSet4ArgsWithCache() throws IOException { - - ValueSet vs = new ValueSet(); - vs.setUrl(DUMMY_URL); - - Mockito.doReturn(cacheToken).when(terminologyCache).generateExpandToken(vs,true); - Mockito.doReturn(expectedExpansionResult).when(terminologyCache).getExpansion(cacheToken); - - Parameters pIn = new Parameters(); - - ValueSetExpansionOutcome actualExpansionResult = context.expandVS(vs, true, true, true, pIn); - - assertEquals(expectedExpansionResult, actualExpansionResult); - - Mockito.verify(terminologyCache).getExpansion(cacheToken); - Mockito.verify(terminologyCache, times(0)).cacheExpansion(any(), any(), anyBoolean()); - Mockito.verify(terminologyClient, times(0)).expandValueset(any(), any()); - } - - private class ValidationOptionsFhirPublicationMatcher implements ArgumentMatcher { - - final FhirPublication fhirPublication; - - ValidationOptionsFhirPublicationMatcher(FhirPublication fhirPublication) { - this.fhirPublication = fhirPublication; - } - @Override - public boolean matches(ValidationOptions argument) { - return fhirPublication.toCode().equals(argument.getFhirVersion().toCode()); - } - } - - @Test - public void testExpandValueSet4ArgsWithValueSetExpanderSimple() throws IOException { - - ValueSet vs = new ValueSet(); - vs.setUrl(DUMMY_URL); - - Mockito.doReturn(cacheToken).when(terminologyCache).generateExpandToken(vs,true); - - Parameters pIn = new Parameters(); - - Mockito.doReturn(vs).when(expectedExpansionResult).getValueset(); - - Mockito.doReturn(expectedExpansionResult).when(valueSetExpanderSimple).expand(eq(vs), - argThat(new ParametersMatcher(pInWithDependentResources))); - - Mockito.doReturn(valueSetExpanderSimple).when(context).constructValueSetExpanderSimple(argThat(new ValidationOptionsFhirPublicationMatcher(vs.getFHIRPublicationVersion()))); - - ValueSetExpansionOutcome actualExpansionResult = context.expandVS(vs, true, true, true, pIn); - - assertEquals(expectedExpansionResult, actualExpansionResult); - - Mockito.verify(terminologyCache).getExpansion(cacheToken); - Mockito.verify(terminologyCache).cacheExpansion(cacheToken, actualExpansionResult, false); - Mockito.verify(terminologyClient, times(0)).expandValueset(any(), any()); - } - - @Test - public void testExpandValueSet4ArgsWithClient() throws IOException { - - ValueSet vs = new ValueSet(); - vs.setUrl(DUMMY_URL); - - Mockito.doReturn(cacheToken).when(terminologyCache).generateExpandToken(vs,true); - - Parameters pIn = new Parameters(); - - ValueSet expectedValueSet = new ValueSet(); - expectedValueSet.setUrl("dummyUrl2"); - - Mockito.doReturn(expectedExpansionResult).when(valueSetExpanderSimple).expand(eq(vs), - argThat(new ParametersMatcher(pInWithDependentResources))); - - Mockito.doReturn(valueSetExpanderSimple).when(context).constructValueSetExpanderSimple(argThat(new ValidationOptionsFhirPublicationMatcher(vs.getFHIRPublicationVersion()))); - - Mockito.doReturn(expectedValueSet).when(terminologyClient).expandValueset(eq(vs), argThat(new ParametersMatcher(pInWithDependentResources))); - - ValueSetExpansionOutcome actualExpansionResult = context.expandVS(vs, true, true, true, pIn, false); - - assertEquals(expectedValueSet, actualExpansionResult.getValueset()); - - Mockito.verify(terminologyCache).getExpansion(cacheToken); - Mockito.verify(terminologyCache).cacheExpansion(cacheToken, actualExpansionResult, true); - } - - @Test public void testInitializationWithCache() { String address = "dummyUrl"; diff --git a/org.hl7.fhir.utilities/src/main/java/org/hl7/fhir/utilities/validation/ValidationMessage.java b/org.hl7.fhir.utilities/src/main/java/org/hl7/fhir/utilities/validation/ValidationMessage.java index 61471be66..330489bab 100644 --- a/org.hl7.fhir.utilities/src/main/java/org/hl7/fhir/utilities/validation/ValidationMessage.java +++ b/org.hl7.fhir.utilities/src/main/java/org/hl7/fhir/utilities/validation/ValidationMessage.java @@ -757,7 +757,9 @@ public class ValidationMessage implements Comparator, Compara @Override public boolean equals(Object o) { - return (this.getMessage() != null && this.getMessage().equals(((ValidationMessage)o).getMessage())) && (this.getLocation() != null && this.getLocation().equals(((ValidationMessage)o).getLocation())); + return ( + this.getMessage() != null && this.getMessage().equals(((ValidationMessage)o).getMessage())) + && (this.getLocation() != null && this.getLocation().equals(((ValidationMessage)o).getLocation())); } @Override diff --git a/org.hl7.fhir.validation/src/main/java/org/hl7/fhir/validation/instance/InstanceValidator.java b/org.hl7.fhir.validation/src/main/java/org/hl7/fhir/validation/instance/InstanceValidator.java index 6993f11c0..0a723957c 100644 --- a/org.hl7.fhir.validation/src/main/java/org/hl7/fhir/validation/instance/InstanceValidator.java +++ b/org.hl7.fhir.validation/src/main/java/org/hl7/fhir/validation/instance/InstanceValidator.java @@ -5612,6 +5612,31 @@ public class InstanceValidator extends BaseValidator implements IResourceValidat } } + private static void addMessagesReplaceExistingIfMoreSevere(List errors, List newErrors) { + for (ValidationMessage newError : newErrors) { + int index = indexOfMatchingMessageAndLocation(errors, newError); + if (index == -1) { + errors.add(newError); + } else { + ValidationMessage existingError = errors.get(index); + if (newError.getLevel().ordinal() < existingError.getLevel().ordinal()) { + errors.set(index, newError); + } + } + } + } + + private static int indexOfMatchingMessageAndLocation(List messages, ValidationMessage message) { + for (int i = 0; i < messages.size(); i++) { + ValidationMessage iMessage = messages.get(i); + if (message.getMessage() != null && message.getMessage().equals(iMessage.getMessage()) + && message.getLocation() != null && message.getLocation().equals(iMessage.getLocation())) { + return i; + } + } + return -1; + } + public boolean startInner(ValidationContext valContext, List errors, Element resource, Element element, StructureDefinition defn, NodeStack stack, boolean checkSpecials, PercentageTracker pct, ValidationMode mode, boolean fromContained) { // the first piece of business is to see if we've validated this resource against this profile before. // if we have (*or if we still are*), then we'll just return our existing errors @@ -5633,11 +5658,7 @@ public class InstanceValidator extends BaseValidator implements IResourceValidat trackUsage(defn, valContext, element); ok = validateElement(valContext, localErrors, defn, defn.getSnapshot().getElement().get(0), null, null, resource, element, element.getName(), stack, false, true, null, pct, mode) && ok; resTracker.storeOutcomes(defn, localErrors); - for (ValidationMessage vm : localErrors) { - if (!errors.contains(vm)) { - errors.add(vm); - } - } + addMessagesReplaceExistingIfMoreSevere(errors, localErrors); } else { ok = false; } diff --git a/org.hl7.fhir.validation/src/test/java/org/hl7/fhir/validation/tests/ValidationEngineTests.java b/org.hl7.fhir.validation/src/test/java/org/hl7/fhir/validation/tests/ValidationEngineTests.java index 55510b284..aec35db23 100644 --- a/org.hl7.fhir.validation/src/test/java/org/hl7/fhir/validation/tests/ValidationEngineTests.java +++ b/org.hl7.fhir.validation/src/test/java/org/hl7/fhir/validation/tests/ValidationEngineTests.java @@ -283,12 +283,13 @@ public class ValidationEngineTests { OperationOutcome op = ve.validate(FhirFormat.JSON, TestingUtilities.loadTestResourceStream("validator", "observation401_ucum.json"), profiles); Assertions.assertTrue(checkOutcomes("test401USCore", op, "Observation null information/informational: Validate Observation against the Body weight profile (http://hl7.org/fhir/StructureDefinition/bodyweight) which is required by the FHIR specification because the LOINC code 29463-7 was found\n"+ - "Observation.code.coding[0].system null information/not-found: A definition for CodeSystem 'http://loinc.org' could not be found, so the code cannot be validated\n"+ "Observation.value.ofType(Quantity) null warning/business-rule: Unable to validate code 'kg' in system 'http://unitsofmeasure.org' because the validator is running without terminology services\n"+ "Observation.value.ofType(Quantity).code null warning/informational: Unable to validate code without using server because: Resolved system http://unitsofmeasure.org (v3.0.1), but the definition doesn't include any codes, so the code has not been validated\n"+ // "Observation.code null warning/code-invalid: None of the codings provided are in the value set 'Vital Signs' (http://hl7.org/fhir/ValueSet/observation-vitalsignresult|4.0.1), and a coding should come from this value set unless it has no suitable code (note that the validator cannot judge what is suitable) (codes = http://loinc.org#29463-7)\n"+ "Observation null warning/invalid: Best Practice Recommendation: In general, all observations should have a performer\n"+ - "Observation.code null warning/not-found: Unable to check whether the code is in the value set 'http://hl7.org/fhir/ValueSet/observation-vitalsignresult|4.0.1' because the code system http://loinc.org was not found\n"+ + "Observation.code.coding[0].system null warning/not-found: A definition for CodeSystem 'http://loinc.org' could not be found, so the code cannot be validated\n"+ + + "Observation.code null warning/not-found: Unable to check whether the code is in the value set 'http://hl7.org/fhir/ValueSet/observation-vitalsignresult|4.0.1' because the code system http://loinc.org was not found\n"+ "Observation null warning/invariant: Constraint failed: dom-6: 'A resource should have narrative for robust management' (defined in http://hl7.org/fhir/StructureDefinition/DomainResource) (Best Practice Recommendation)")); assertTrue(logger.verifyHasNoRequests(), "Unexpected request to TX server"); }