diff --git a/hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/changelog/8_8_0/7338-fix-fhir-instance-validator-error-for-unknown-profiles.yaml b/hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/changelog/8_8_0/7338-fix-fhir-instance-validator-error-for-unknown-profiles.yaml new file mode 100644 index 000000000000..8f4fc47e0735 --- /dev/null +++ b/hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/changelog/8_8_0/7338-fix-fhir-instance-validator-error-for-unknown-profiles.yaml @@ -0,0 +1,6 @@ +--- +type: fix +issue: 7338 +title: "A regression in HAPI FHIR 8.2 meant that setting FhirInstanceValidator.setErrorForUnknownProfiles(false) had no effect. + This has been fixed while ensuring $validate always returns an error if the profile specified in the 'profile' parameter is unknown, as required by the FHIR specification. + Fix submitted by Tue Toft Nørgård (@ttnTrifork)" diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/BaseHapiFhirResourceDao.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/BaseHapiFhirResourceDao.java index ac259bc11ee0..403dedd11090 100644 --- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/BaseHapiFhirResourceDao.java +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/BaseHapiFhirResourceDao.java @@ -122,6 +122,7 @@ import ca.uhn.fhir.validation.IInstanceValidatorModule; import ca.uhn.fhir.validation.IValidationContext; import ca.uhn.fhir.validation.IValidatorModule; +import ca.uhn.fhir.validation.ResultSeverityEnum; import ca.uhn.fhir.validation.ValidationOptions; import ca.uhn.fhir.validation.ValidationResult; import com.google.common.annotations.VisibleForTesting; @@ -142,6 +143,7 @@ import org.hl7.fhir.instance.model.api.IPrimitiveType; import org.hl7.fhir.r4.model.Parameters; import org.hl7.fhir.r4.model.Parameters.ParametersParameterComponent; +import org.hl7.fhir.utilities.i18n.I18nConstants; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.data.domain.PageRequest; import org.springframework.data.domain.Pageable; @@ -2889,6 +2891,17 @@ public MethodOutcome validate( result = validator.validateWithResult(theResource, options); } + if (isNotBlank(theProfile)) { + // The $validate operation SHALL return error if the server cannot validate against the nominated profile. + // See https://www.hl7.org/fhir/resource-operation-validate.html + // even if FhirInstanceValidator.setErrorForUnknownProfiles(false) has been set + result.getMessages().stream() + .filter(m -> I18nConstants.VALIDATION_VAL_PROFILE_UNKNOWN.equals(m.getMessageId()) + && m.getSeverity() != ResultSeverityEnum.ERROR + && m.getSeverity() != ResultSeverityEnum.FATAL) + .forEach(m -> m.setSeverity(ResultSeverityEnum.ERROR)); + } + MethodOutcome retVal = new MethodOutcome(); retVal.setOperationOutcome(result.toOperationOutcome()); // Note an earlier version of this code returned PreconditionFailedException when the validation 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 7d068387a9f3..5f3938da0c3d 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 @@ -1577,6 +1577,45 @@ public void validateResource_withUnknownMetaProfileurl_validatesButLogsWarning() LogbackTestExtensionAssert.assertThat(myLogbackTestExtension).hasWarnMessage("Unrecognized profile uri"); } + + @Test + public void validateResource_AgainstUnknownTargetProfile_FailValidation_EvenWhenNotErrorForUnknownProfiles() { + // ARRANGE + // setup to not error on unknown profiles + FhirInstanceValidator instanceValidator = AopTestUtils.getTargetObject(myValidatorModule); + var previousSetting = instanceValidator.isErrorForUnknownProfiles(); + instanceValidator.setErrorForUnknownProfiles(false); + + // Valid Observation resource that conforms to base R4 profile + Observation observation = new Observation(); + observation.setStatus(ObservationStatus.FINAL); + observation.getCode().addCoding().setSystem("http://loinc.org").setCode("12345"); + + // ACT + final String targetProfile = "http://example.com/fhir/StructureDefinition/vitalsigns-2"; + MethodOutcome outcome = myObservationDao.validate( + /* theResource */ observation, + /* theId */ null, + /* theRawResource */ null, + /* theEncoding */ EncodingEnum.JSON, + /* theMode */ ValidationModeEnum.CREATE, + /* theProfile */ targetProfile, + /* theRequestDetails */ mySrd + ); + instanceValidator.setErrorForUnknownProfiles(previousSetting); + + // ASSERT + assertNotNull(outcome); + assertInstanceOf(OperationOutcome.class, outcome.getOperationOutcome()); + List issues = ((OperationOutcome) outcome.getOperationOutcome()).getIssue(); + assertThat(issues.stream()) + .anyMatch(i -> i.getSeverity() == OperationOutcome.IssueSeverity.ERROR + && i.getExtensionString("http://hl7.org/fhir/StructureDefinition/operationoutcome-message-id").equals(I18nConstants.VALIDATION_VAL_PROFILE_UNKNOWN) + && i.getDiagnostics().contains(targetProfile) + ); + + } + @Test public void validateResource_withMetaProfileWithVersion_validatesAsExpected() { // setup diff --git a/hapi-fhir-validation/src/main/java/org/hl7/fhir/common/hapi/validation/validator/ValidatorWrapper.java b/hapi-fhir-validation/src/main/java/org/hl7/fhir/common/hapi/validation/validator/ValidatorWrapper.java index 4fa697415f06..a72f2d736a5d 100644 --- a/hapi-fhir-validation/src/main/java/org/hl7/fhir/common/hapi/validation/validator/ValidatorWrapper.java +++ b/hapi-fhir-validation/src/main/java/org/hl7/fhir/common/hapi/validation/validator/ValidatorWrapper.java @@ -224,15 +224,14 @@ public List validate( && m.getMessage().contains("http://hl7.org/fhir/ValueSet/mimetypes")))) .collect(Collectors.toList()); - if (myErrorForUnknownProfiles) { - messages.stream() - .filter(m -> m.getMessageId() != null - && (m.getMessageId().equals(I18nConstants.VALIDATION_VAL_PROFILE_UNKNOWN) - || m.getMessageId() - .equals(I18nConstants.VALIDATION_VAL_PROFILE_UNKNOWN_NOT_POLICY))) - .filter(m -> m.getLevel() == ValidationMessage.IssueSeverity.WARNING) - .forEach(m -> m.setLevel(ValidationMessage.IssueSeverity.ERROR)); - } + messages.stream() + .filter(m -> I18nConstants.VALIDATION_VAL_PROFILE_UNKNOWN.equals(m.getMessageId()) + || I18nConstants.VALIDATION_VAL_PROFILE_UNKNOWN_NOT_POLICY.equals(m.getMessageId())) + .forEach(m -> m.setLevel( + myErrorForUnknownProfiles + ? ValidationMessage.IssueSeverity.ERROR + : ValidationMessage.IssueSeverity.WARNING)); + return messages; } 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 9549adf75da9..bd3d8c7603fc 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 @@ -1114,7 +1114,7 @@ public void testValidateResourceContainingProfileDeclaration() { } @Test - public void testValidateResourceContainingProfileDeclarationDoesntResolve() { + public void testValidateResourceContainingUnresolvedProfileDeclaration_FailsWhenErrorForUnknownProfiles() { myMockSupport.addValidConcept("http://loinc.org", "12345"); Observation input = createObservationWithDefaultSubjectPerfomerEffective(); @@ -1126,6 +1126,7 @@ public void testValidateResourceContainingProfileDeclarationDoesntResolve() { input.setStatus(ObservationStatus.FINAL); myInstanceVal.setValidationSupport(myValidationSupport); + myInstanceVal.setErrorForUnknownProfiles(true); ValidationResult output = myFhirValidator.validateWithResult(input); List errors = logResultsAndReturnNonInformationalOnes(output); @@ -1136,6 +1137,32 @@ public void testValidateResourceContainingProfileDeclarationDoesntResolve() { (r.getMessage().equals("Profile reference 'http://foo/structuredefinition/myprofile' has not been checked because it could not be found")) ); } + @Test + public void testValidateResourceContainingUnresolvedProfileDeclaration_SucceedsWhenNotErrorForUnknownProfiles() { + myMockSupport.addValidConcept("http://loinc.org", "12345"); + + Observation input = createObservationWithDefaultSubjectPerfomerEffective(); + + input.getText().setDiv(new XhtmlNode().setValue("
AA
")).setStatus(Narrative.NarrativeStatus.GENERATED); + input.getMeta().addProfile("http://foo/structuredefinition/myprofile"); + + input.getCode().addCoding().setSystem("http://loinc.org").setCode("12345"); + input.setStatus(ObservationStatus.FINAL); + + myInstanceVal.setValidationSupport(myValidationSupport); + myInstanceVal.setErrorForUnknownProfiles(false); + ValidationResult output = myFhirValidator.validateWithResult(input); + List errors = logResultsAndReturnNonInformationalOnes(output); + + assertThat(errors).hasSize(2); + assertThat(errors.stream()) + .noneMatch(r -> (r.getSeverity() == ResultSeverityEnum.ERROR)); + assertThat(errors.stream()) + .anyMatch(r -> + (r.getSeverity() == ResultSeverityEnum.WARNING) && + (r.getMessage().equals("Profile reference 'http://foo/structuredefinition/myprofile' has not been checked because it could not be found")) ); + } + @Test public void testValidateResourceFailingInvariant() { Observation input = new Observation();