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 4f3ff3825ab..6ae1939f611 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 @@ -691,8 +691,9 @@ public interface IValidationSupport { private boolean myFound; private String mySearchedForCode; private String mySearchedForSystem; - private List myProperties; + private List myProperties; private List myDesignations; + private String myErrorMessage; /** * Constructor @@ -708,7 +709,7 @@ public interface IValidationSupport { return myProperties; } - public void setProperties(List theProperties) { + public void setProperties(List theProperties) { myProperties = theProperties; } @@ -808,7 +809,7 @@ public interface IValidationSupport { .collect(Collectors.toSet()); } - for (IValidationSupport.BaseConceptProperty next : myProperties) { + for (BaseConceptProperty next : myProperties) { if (!properties.isEmpty()) { if (!properties.contains(next.getPropertyName())) { @@ -819,11 +820,11 @@ public interface IValidationSupport { IBase property = ParametersUtil.addParameterToParameters(theContext, retVal, "property"); ParametersUtil.addPartCode(theContext, property, "code", next.getPropertyName()); - if (next instanceof IValidationSupport.StringConceptProperty) { - IValidationSupport.StringConceptProperty prop = (IValidationSupport.StringConceptProperty) next; + if (next instanceof StringConceptProperty) { + StringConceptProperty prop = (StringConceptProperty) next; ParametersUtil.addPartString(theContext, property, "value", prop.getValue()); - } else if (next instanceof IValidationSupport.CodingConceptProperty) { - IValidationSupport.CodingConceptProperty prop = (IValidationSupport.CodingConceptProperty) next; + } else if (next instanceof CodingConceptProperty) { + CodingConceptProperty prop = (CodingConceptProperty) next; ParametersUtil.addPartCoding( theContext, property, "value", prop.getCodeSystem(), prop.getCode(), prop.getDisplay()); } else { @@ -846,6 +847,14 @@ public interface IValidationSupport { return retVal; } + public void setErrorMessage(String theErrorMessage) { + myErrorMessage = theErrorMessage; + } + + public String getErrorMessage() { + return myErrorMessage; + } + public static LookupCodeResult notFound(String theSearchedForSystem, String theSearchedForCode) { return new LookupCodeResult() .setFound(false) diff --git a/hapi-fhir-base/src/main/java/ca/uhn/fhir/util/OperationOutcomeUtil.java b/hapi-fhir-base/src/main/java/ca/uhn/fhir/util/OperationOutcomeUtil.java index ccb2b588169..eadf61d44f2 100644 --- a/hapi-fhir-base/src/main/java/ca/uhn/fhir/util/OperationOutcomeUtil.java +++ b/hapi-fhir-base/src/main/java/ca/uhn/fhir/util/OperationOutcomeUtil.java @@ -23,6 +23,7 @@ import ca.uhn.fhir.context.BaseRuntimeChildDefinition; import ca.uhn.fhir.context.BaseRuntimeElementCompositeDefinition; import ca.uhn.fhir.context.BaseRuntimeElementDefinition; import ca.uhn.fhir.context.FhirContext; +import ca.uhn.fhir.context.FhirVersionEnum; import ca.uhn.fhir.context.RuntimeResourceDefinition; import ca.uhn.fhir.i18n.Msg; import ca.uhn.fhir.rest.api.Constants; @@ -250,34 +251,77 @@ public class OperationOutcomeUtil { public static IBase addIssueWithMessageId( FhirContext myCtx, IBaseOperationOutcome theOperationOutcome, - String severity, - String message, - String messageId, - String location, + String theSeverity, + String theMessage, + String theMessageId, + String theLocation, String theCode) { - IBase issue = addIssue(myCtx, theOperationOutcome, severity, message, location, theCode); - BaseRuntimeElementCompositeDefinition issueElement = - (BaseRuntimeElementCompositeDefinition) myCtx.getElementDefinition(issue.getClass()); - BaseRuntimeChildDefinition detailsChildDef = issueElement.getChildByName("details"); + IBase issue = addIssue(myCtx, theOperationOutcome, theSeverity, theMessage, theLocation, theCode); + if (isNotBlank(theMessageId)) { + addDetailsToIssue(myCtx, issue, Constants.JAVA_VALIDATOR_DETAILS_SYSTEM, theMessageId); + } - IPrimitiveType system = - (IPrimitiveType) myCtx.getElementDefinition("uri").newInstance(); - system.setValueAsString(Constants.JAVA_VALIDATOR_DETAILS_SYSTEM); - IPrimitiveType code = - (IPrimitiveType) myCtx.getElementDefinition("code").newInstance(); - code.setValueAsString(messageId); - - BaseRuntimeElementCompositeDefinition codingDef = - (BaseRuntimeElementCompositeDefinition) myCtx.getElementDefinition("Coding"); - ICompositeType coding = (ICompositeType) codingDef.newInstance(); - codingDef.getChildByName("system").getMutator().addValue(coding, system); - codingDef.getChildByName("code").getMutator().addValue(coding, code); - BaseRuntimeElementCompositeDefinition ccDef = - (BaseRuntimeElementCompositeDefinition) myCtx.getElementDefinition("CodeableConcept"); - ICompositeType codeableConcept = (ICompositeType) ccDef.newInstance(); - ccDef.getChildByName("coding").getMutator().addValue(codeableConcept, coding); - - detailsChildDef.getMutator().addValue(issue, codeableConcept); return issue; } + + public static void addDetailsToIssue(FhirContext theFhirContext, IBase theIssue, String theSystem, String theCode) { + BaseRuntimeElementCompositeDefinition issueElement = + (BaseRuntimeElementCompositeDefinition) theFhirContext.getElementDefinition(theIssue.getClass()); + BaseRuntimeChildDefinition detailsChildDef = issueElement.getChildByName("details"); + + BaseRuntimeElementCompositeDefinition codingDef = + (BaseRuntimeElementCompositeDefinition) theFhirContext.getElementDefinition("Coding"); + ICompositeType coding = (ICompositeType) codingDef.newInstance(); + + // System + IPrimitiveType system = + (IPrimitiveType) theFhirContext.getElementDefinition("uri").newInstance(); + system.setValueAsString(theSystem); + codingDef.getChildByName("system").getMutator().addValue(coding, system); + + // Code + IPrimitiveType code = + (IPrimitiveType) theFhirContext.getElementDefinition("code").newInstance(); + code.setValueAsString(theCode); + codingDef.getChildByName("code").getMutator().addValue(coding, code); + BaseRuntimeElementCompositeDefinition ccDef = + (BaseRuntimeElementCompositeDefinition) theFhirContext.getElementDefinition("CodeableConcept"); + + ICompositeType codeableConcept = (ICompositeType) ccDef.newInstance(); + ccDef.getChildByName("coding").getMutator().addValue(codeableConcept, coding); + detailsChildDef.getMutator().addValue(theIssue, codeableConcept); + } + + public static void addIssueLineExtensionToIssue(FhirContext theCtx, IBase theIssue, String theLine) { + if (theCtx.getVersion().getVersion() != FhirVersionEnum.DSTU2) { + ExtensionUtil.setExtension( + theCtx, + theIssue, + "http://hl7.org/fhir/StructureDefinition/operationoutcome-issue-line", + "integer", + theLine); + } + } + + public static void addIssueColExtensionToIssue(FhirContext theCtx, IBase theIssue, String theColumn) { + if (theCtx.getVersion().getVersion() != FhirVersionEnum.DSTU2) { + ExtensionUtil.setExtension( + theCtx, + theIssue, + "http://hl7.org/fhir/StructureDefinition/operationoutcome-issue-col", + "integer", + theColumn); + } + } + + public static void addMessageIdExtensionToIssue(FhirContext theCtx, IBase theIssue, String theMessageId) { + if (theCtx.getVersion().getVersion() != FhirVersionEnum.DSTU2) { + ExtensionUtil.setExtension( + theCtx, + theIssue, + "http://hl7.org/fhir/StructureDefinition/operationoutcome-message-id", + "string", + theMessageId); + } + } } diff --git a/hapi-fhir-base/src/main/java/ca/uhn/fhir/validation/SingleValidationMessage.java b/hapi-fhir-base/src/main/java/ca/uhn/fhir/validation/SingleValidationMessage.java index 7d9e827d493..569a2f5e9d1 100644 --- a/hapi-fhir-base/src/main/java/ca/uhn/fhir/validation/SingleValidationMessage.java +++ b/hapi-fhir-base/src/main/java/ca/uhn/fhir/validation/SingleValidationMessage.java @@ -24,6 +24,8 @@ import org.apache.commons.lang3.builder.HashCodeBuilder; import org.apache.commons.lang3.builder.ToStringBuilder; import org.apache.commons.lang3.builder.ToStringStyle; +import java.util.List; + public class SingleValidationMessage { private Integer myLocationCol; @@ -32,6 +34,7 @@ public class SingleValidationMessage { private String myMessage; private String myMessageId; private ResultSeverityEnum mySeverity; + private List mySliceMessages; /** * Constructor @@ -58,6 +61,7 @@ public class SingleValidationMessage { b.append(myLocationString, other.myLocationString); b.append(myMessage, other.myMessage); b.append(mySeverity, other.mySeverity); + b.append(mySliceMessages, other.mySliceMessages); return b.isEquals(); } @@ -93,6 +97,7 @@ public class SingleValidationMessage { b.append(myLocationString); b.append(myMessage); b.append(mySeverity); + b.append(mySliceMessages); return b.toHashCode(); } @@ -137,6 +142,17 @@ public class SingleValidationMessage { if (mySeverity != null) { b.append("severity", mySeverity.getCode()); } + if (mySliceMessages != null) { + b.append("sliceMessages", mySliceMessages); + } return b.toString(); } + + public void setSliceMessages(List theSliceMessages) { + mySliceMessages = theSliceMessages; + } + + public List getSliceMessages() { + return mySliceMessages; + } } diff --git a/hapi-fhir-base/src/main/java/ca/uhn/fhir/validation/ValidationResult.java b/hapi-fhir-base/src/main/java/ca/uhn/fhir/validation/ValidationResult.java index 0eca0cc8a61..f95f993637e 100644 --- a/hapi-fhir-base/src/main/java/ca/uhn/fhir/validation/ValidationResult.java +++ b/hapi-fhir-base/src/main/java/ca/uhn/fhir/validation/ValidationResult.java @@ -28,6 +28,7 @@ import org.hl7.fhir.instance.model.api.IBaseOperationOutcome; import java.util.Collections; import java.util.List; +import static org.apache.commons.lang3.StringUtils.isBlank; import static org.apache.commons.lang3.StringUtils.isNotBlank; /** @@ -38,11 +39,11 @@ import static org.apache.commons.lang3.StringUtils.isNotBlank; */ public class ValidationResult { public static final int ERROR_DISPLAY_LIMIT_DEFAULT = 1; - + public static final String UNKNOWN = "(unknown)"; + private static final String ourNewLine = System.getProperty("line.separator"); private final FhirContext myCtx; private final boolean myIsSuccessful; private final List myMessages; - private int myErrorDisplayLimit = ERROR_DISPLAY_LIMIT_DEFAULT; public ValidationResult(FhirContext theCtx, List theMessages) { @@ -108,8 +109,8 @@ public class ValidationResult { /** * @deprecated Use {@link #toOperationOutcome()} instead since this method returns a view. - * {@link #toOperationOutcome()} is identical to this method, but has a more suitable name so this method - * will be removed at some point. + * {@link #toOperationOutcome()} is identical to this method, but has a more suitable name so this method + * will be removed at some point. */ @Deprecated public IBaseOperationOutcome getOperationOutcome() { @@ -131,39 +132,36 @@ public class ValidationResult { */ public void populateOperationOutcome(IBaseOperationOutcome theOperationOutcome) { for (SingleValidationMessage next : myMessages) { - String location; - if (isNotBlank(next.getLocationString())) { - location = next.getLocationString(); - } else if (next.getLocationLine() != null || next.getLocationCol() != null) { - location = "Line[" + next.getLocationLine() + "] Col[" + next.getLocationCol() + "]"; - } else { - location = null; - } - String severity = next.getSeverity() != null ? next.getSeverity().getCode() : null; - IBase issue = OperationOutcomeUtil.addIssueWithMessageId( - myCtx, - theOperationOutcome, - severity, - next.getMessage(), - next.getMessageId(), - location, - Constants.OO_INFOSTATUS_PROCESSING); + Integer locationLine = next.getLocationLine(); + Integer locationCol = next.getLocationCol(); + String location = next.getLocationString(); + ResultSeverityEnum issueSeverity = next.getSeverity(); + String message = next.getMessage(); + String messageId = next.getMessageId(); - if (next.getLocationLine() != null || next.getLocationCol() != null) { - String unknown = "(unknown)"; - String line = unknown; - if (next.getLocationLine() != null && next.getLocationLine() != -1) { - line = next.getLocationLine().toString(); - } - String col = unknown; - if (next.getLocationCol() != null && next.getLocationCol() != -1) { - col = next.getLocationCol().toString(); - } - if (!unknown.equals(line) || !unknown.equals(col)) { - OperationOutcomeUtil.addLocationToIssue(myCtx, issue, "Line " + line + ", Col " + col); - } + if (next.getSliceMessages() == null) { + addIssueToOperationOutcome( + theOperationOutcome, location, locationLine, locationCol, issueSeverity, message, messageId); + continue; } - } + + /* + * Occasionally the validator will return these lists of "slice messages" + * which happen when validating rules associated with a specific slice in + * a profile. + */ + for (String nextSliceMessage : next.getSliceMessages()) { + String combinedMessage = message + " - " + nextSliceMessage; + addIssueToOperationOutcome( + theOperationOutcome, + location, + locationLine, + locationCol, + issueSeverity, + combinedMessage, + messageId); + } + } // for if (myMessages.isEmpty()) { String message = myCtx.getLocalizer().getMessage(ValidationResult.class, "noIssuesDetected"); @@ -171,6 +169,44 @@ public class ValidationResult { } } + private void addIssueToOperationOutcome( + IBaseOperationOutcome theOperationOutcome, + String location, + Integer locationLine, + Integer locationCol, + ResultSeverityEnum issueSeverity, + String message, + String messageId) { + if (isBlank(location) && locationLine != null && locationCol != null) { + location = "Line[" + locationLine + "] Col[" + locationCol + "]"; + } + String severity = issueSeverity != null ? issueSeverity.getCode() : null; + IBase issue = OperationOutcomeUtil.addIssueWithMessageId( + myCtx, theOperationOutcome, severity, message, messageId, location, Constants.OO_INFOSTATUS_PROCESSING); + + if (locationLine != null || locationCol != null) { + String unknown = UNKNOWN; + String line = unknown; + if (locationLine != null && locationLine != -1) { + line = locationLine.toString(); + } + String col = unknown; + if (locationCol != null && locationCol != -1) { + col = locationCol.toString(); + } + if (!unknown.equals(line) || !unknown.equals(col)) { + OperationOutcomeUtil.addIssueLineExtensionToIssue(myCtx, issue, line); + OperationOutcomeUtil.addIssueColExtensionToIssue(myCtx, issue, col); + String locationString = "Line[" + line + "] Col[" + col + "]"; + OperationOutcomeUtil.addLocationToIssue(myCtx, issue, locationString); + } + } + + if (isNotBlank(messageId)) { + OperationOutcomeUtil.addMessageIdExtensionToIssue(myCtx, issue, messageId); + } + } + @Override public String toString() { return "ValidationResult{" + "messageCount=" + myMessages.size() + ", isSuccessful=" + myIsSuccessful @@ -191,6 +227,4 @@ public class ValidationResult { public void setErrorDisplayLimit(int theErrorDisplayLimit) { myErrorDisplayLimit = theErrorDisplayLimit; } - - private static final String ourNewLine = System.getProperty("line.separator"); } diff --git a/hapi-fhir-base/src/main/resources/ca/uhn/fhir/i18n/hapi-messages.properties b/hapi-fhir-base/src/main/resources/ca/uhn/fhir/i18n/hapi-messages.properties index 5cbae62faa5..17632574a82 100644 --- a/hapi-fhir-base/src/main/resources/ca/uhn/fhir/i18n/hapi-messages.properties +++ b/hapi-fhir-base/src/main/resources/ca/uhn/fhir/i18n/hapi-messages.properties @@ -8,6 +8,7 @@ ca.uhn.fhir.jpa.term.TermReadSvcImpl.validationPerformedAgainstPreExpansion=Code ca.uhn.fhir.jpa.term.TermReadSvcImpl.valueSetNotFoundInTerminologyDatabase=ValueSet can not be found in terminology database: {0} ca.uhn.fhir.jpa.term.TermReadSvcImpl.valueSetPreExpansionInvalidated=ValueSet with URL "{0}" precaluclated expansion with {1} concept(s) has been invalidated ca.uhn.fhir.jpa.term.TermReadSvcImpl.valueSetCantInvalidateNotYetPrecalculated=ValueSet with URL "{0}" already has status: {1} +ca.uhn.fhir.jpa.term.TermReadSvcImpl.unknownCodeInSystem=Unknown code "{0}#{1}" # Core Library Messages diff --git a/hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/changelog/7_0_0/5271-improve-code-validation-error-messages.yaml b/hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/changelog/7_0_0/5271-improve-code-validation-error-messages.yaml new file mode 100644 index 00000000000..9d194939a3c --- /dev/null +++ b/hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/changelog/7_0_0/5271-improve-code-validation-error-messages.yaml @@ -0,0 +1,8 @@ +--- +type: add +issue: 5271 +title: "The error messages returned in an OperationOutcome when validating terminology codes + as a part of resource profile validation have been improved. Machine processable location + (line/col) information is now available through a pair of dedicated extensions, and + error messages such as UCUM parsing issues are now returned to the client (previously + they were swallowed and a generic error message was returned)." 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 6e8df461d85..3ace1fbb072 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 @@ -2093,7 +2093,10 @@ public class TermReadSvcImpl implements ITermReadSvc, IHasScheduledJobs { if (outcome.size() == 0) { append = " - No codes in ValueSet belong to CodeSystem with URL " + theSystem; } else { - append = " - Unknown code " + theSystem + "#" + theCode + ". " + msg; + String unknownCodeMessage = myContext + .getLocalizer() + .getMessage(TermReadSvcImpl.class, "unknownCodeInSystem", theSystem, theCode); + append = " - " + unknownCodeMessage + ". " + msg; } return createFailureCodeValidationResult(theSystem, theCode, null, append); 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 9d4073343ec..b8ece1bac31 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 @@ -23,6 +23,7 @@ import ca.uhn.fhir.parser.StrictErrorHandler; import ca.uhn.fhir.rest.api.EncodingEnum; import ca.uhn.fhir.rest.api.MethodOutcome; import ca.uhn.fhir.rest.api.ValidationModeEnum; +import ca.uhn.fhir.rest.api.server.SystemRequestDetails; import ca.uhn.fhir.rest.server.exceptions.PreconditionFailedException; import ca.uhn.fhir.rest.server.exceptions.ResourceGoneException; import ca.uhn.fhir.rest.server.exceptions.ResourceVersionConflictException; @@ -30,48 +31,21 @@ import ca.uhn.fhir.rest.server.exceptions.UnprocessableEntityException; import ca.uhn.fhir.util.OperationOutcomeUtil; import ca.uhn.fhir.util.StopWatch; import ca.uhn.fhir.validation.IValidatorModule; +import ca.uhn.fhir.validation.ResultSeverityEnum; +import ca.uhn.fhir.validation.ValidationResult; import org.apache.commons.io.IOUtils; import org.hl7.fhir.common.hapi.validation.support.UnknownCodeSystemWarningValidationSupport; 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.IIdType; -import org.hl7.fhir.r4.model.AllergyIntolerance; -import org.hl7.fhir.r4.model.Binary; -import org.hl7.fhir.r4.model.Bundle; +import org.hl7.fhir.r4.model.*; import org.hl7.fhir.r4.model.Bundle.BundleEntryComponent; -import org.hl7.fhir.r4.model.CanonicalType; -import org.hl7.fhir.r4.model.CapabilityStatement; -import org.hl7.fhir.r4.model.CodeSystem; -import org.hl7.fhir.r4.model.CodeType; -import org.hl7.fhir.r4.model.Coding; -import org.hl7.fhir.r4.model.Condition; -import org.hl7.fhir.r4.model.DateTimeType; -import org.hl7.fhir.r4.model.ElementDefinition; -import org.hl7.fhir.r4.model.Enumerations; -import org.hl7.fhir.r4.model.Group; -import org.hl7.fhir.r4.model.IdType; -import org.hl7.fhir.r4.model.Location; -import org.hl7.fhir.r4.model.Narrative; -import org.hl7.fhir.r4.model.Observation; import org.hl7.fhir.r4.model.Observation.ObservationStatus; -import org.hl7.fhir.r4.model.OperationOutcome; -import org.hl7.fhir.r4.model.Organization; -import org.hl7.fhir.r4.model.Parameters; -import org.hl7.fhir.r4.model.Patient; -import org.hl7.fhir.r4.model.Practitioner; -import org.hl7.fhir.r4.model.Quantity; -import org.hl7.fhir.r4.model.Questionnaire; -import org.hl7.fhir.r4.model.QuestionnaireResponse; -import org.hl7.fhir.r4.model.Reference; -import org.hl7.fhir.r4.model.SearchParameter; -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.hl7.fhir.r5.utils.validation.constants.ReferenceValidationPolicy; import org.hl7.fhir.utilities.i18n.I18nConstants; +import org.hl7.fhir.utilities.xhtml.XhtmlNode; import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.Disabled; import org.junit.jupiter.api.Test; @@ -86,9 +60,8 @@ import java.util.stream.Collectors; import static ca.uhn.fhir.rest.api.Constants.JAVA_VALIDATOR_DETAILS_SYSTEM; import static org.awaitility.Awaitility.await; import static org.hamcrest.MatcherAssert.assertThat; -import static org.hamcrest.Matchers.contains; -import static org.hamcrest.Matchers.containsString; -import static org.hamcrest.Matchers.not; +import static org.hamcrest.Matchers.*; +import static org.hl7.fhir.common.hapi.validation.support.CommonCodeSystemsTerminologyService.CURRENCIES_CODESYSTEM_URL; import static org.hl7.fhir.common.hapi.validation.support.ValidationConstants.LOINC_LOW; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertFalse; @@ -368,6 +341,14 @@ public class FhirResourceDaoR4ValidateTest extends BaseJpaR4Test { assertThat(oo.getIssue().get(0).getDiagnostics(), containsString("HAPI-0702: Unable to expand ValueSet because CodeSystem could not be found: http://cs")); assertEquals(OperationOutcome.IssueSeverity.ERROR, oo.getIssueFirstRep().getSeverity()); + assertEquals(27, ((IntegerType)oo.getIssue().get(0).getExtensionByUrl("http://hl7.org/fhir/StructureDefinition/operationoutcome-issue-line").getValue()).getValue()); + assertEquals(4, ((IntegerType)oo.getIssue().get(0).getExtensionByUrl("http://hl7.org/fhir/StructureDefinition/operationoutcome-issue-col").getValue()).getValue()); + assertEquals("Terminology_TX_Confirm_4a", ((StringType)oo.getIssue().get(0).getExtensionByUrl("http://hl7.org/fhir/StructureDefinition/operationoutcome-message-id").getValue()).getValue()); + assertEquals(OperationOutcome.IssueType.PROCESSING, oo.getIssue().get(0).getCode()); + assertEquals(OperationOutcome.IssueSeverity.ERROR, oo.getIssue().get(0).getSeverity()); + assertEquals(2, oo.getIssue().get(0).getLocation().size()); + assertEquals("Observation.value.ofType(Quantity)", oo.getIssue().get(0).getLocation().get(0).getValue()); + assertEquals("Line[27] Col[4]", oo.getIssue().get(0).getLocation().get(1).getValue()); } @@ -1272,7 +1253,8 @@ public class FhirResourceDaoR4ValidateTest extends BaseJpaR4Test { private OperationOutcome validateAndReturnOutcome(T theObs, Boolean theWantError) { IFhirResourceDao dao = (IFhirResourceDao) myDaoRegistry.getResourceDao(theObs.getClass()); - MethodOutcome outcome = dao.validate(theObs, null, null, null, ValidationModeEnum.CREATE, null, mySrd); + String encoded = myFhirContext.newJsonParser().setPrettyPrint(true).encodeResourceToString(theObs); + MethodOutcome outcome = dao.validate(theObs, null, encoded, EncodingEnum.JSON, ValidationModeEnum.CREATE, null, mySrd); OperationOutcome oo = (OperationOutcome) outcome.getOperationOutcome(); if (theWantError) { assertHasErrors(oo); @@ -1596,6 +1578,66 @@ public class FhirResourceDaoR4ValidateTest extends BaseJpaR4Test { } + @Test + void testValidateCommonCodes_Ucum_ErrorMessageIsPreserved() { + Observation input = new Observation(); + input.getText().setDiv(new XhtmlNode().setValue("
AA
")).setStatus(Narrative.NarrativeStatus.GENERATED); + input.setStatus(ObservationStatus.AMENDED); + input.getCode().addCoding().setSystem("http://loinc.org").setCode("1234").setDisplay("FOO"); + input.setValue(new Quantity( + null, + 123, + "http://unitsofmeasure.org", + "MG/DL", + "MG/DL" + )); + + String inputString = myFhirContext.newJsonParser().setPrettyPrint(true).encodeResourceToString(input); + MethodOutcome result = myObservationDao.validate(input, null, inputString, EncodingEnum.JSON, ValidationModeEnum.CREATE, null, mySrd); + OperationOutcome oo = (OperationOutcome) result.getOperationOutcome(); + assertHasErrors(oo); + + assertEquals(15, ((IntegerType)oo.getIssue().get(0).getExtensionByUrl("http://hl7.org/fhir/StructureDefinition/operationoutcome-issue-line").getValue()).getValue()); + assertEquals(4, ((IntegerType)oo.getIssue().get(0).getExtensionByUrl("http://hl7.org/fhir/StructureDefinition/operationoutcome-issue-col").getValue()).getValue()); + assertEquals("Terminology_PassThrough_TX_Message", ((StringType)oo.getIssue().get(0).getExtensionByUrl("http://hl7.org/fhir/StructureDefinition/operationoutcome-message-id").getValue()).getValue()); + assertEquals("Error processing unit 'MG/DL': The unit 'DL' is unknown' at position 3 for 'http://unitsofmeasure.org#MG/DL'", oo.getIssue().get(0).getDiagnostics()); + assertEquals(OperationOutcome.IssueType.PROCESSING, oo.getIssue().get(0).getCode()); + assertEquals(OperationOutcome.IssueSeverity.ERROR, oo.getIssue().get(0).getSeverity()); + assertEquals(2, oo.getIssue().get(0).getLocation().size()); + assertEquals("Observation.value.ofType(Quantity)", oo.getIssue().get(0).getLocation().get(0).getValue()); + assertEquals("Line[15] Col[4]", oo.getIssue().get(0).getLocation().get(1).getValue()); + } + + @Test + void testValidateCommonCodes_Currency_ErrorMessageIsPreserved() { + Observation input = new Observation(); + input.getText().setDiv(new XhtmlNode().setValue("
AA
")).setStatus(Narrative.NarrativeStatus.GENERATED); + input.setStatus(ObservationStatus.AMENDED); + input.getCode().addCoding().setSystem("http://loinc.org").setCode("1234").setDisplay("FOO"); + input.setValue(new Quantity( + null, + 123, + CURRENCIES_CODESYSTEM_URL, + "blah", + "blah" + )); + + String inputString = myFhirContext.newJsonParser().setPrettyPrint(true).encodeResourceToString(input); + MethodOutcome result = myObservationDao.validate(input, null, inputString, EncodingEnum.JSON, ValidationModeEnum.CREATE, null, mySrd); + OperationOutcome oo = (OperationOutcome) result.getOperationOutcome(); + assertHasErrors(oo); + + assertEquals(15, ((IntegerType)oo.getIssue().get(0).getExtensionByUrl("http://hl7.org/fhir/StructureDefinition/operationoutcome-issue-line").getValue()).getValue()); + assertEquals(4, ((IntegerType)oo.getIssue().get(0).getExtensionByUrl("http://hl7.org/fhir/StructureDefinition/operationoutcome-issue-col").getValue()).getValue()); + assertEquals("Terminology_PassThrough_TX_Message", ((StringType)oo.getIssue().get(0).getExtensionByUrl("http://hl7.org/fhir/StructureDefinition/operationoutcome-message-id").getValue()).getValue()); + assertEquals("Unknown code 'urn:iso:std:iso:4217#blah' for 'urn:iso:std:iso:4217#blah'", oo.getIssue().get(0).getDiagnostics()); + assertEquals(OperationOutcome.IssueType.PROCESSING, oo.getIssue().get(0).getCode()); + assertEquals(OperationOutcome.IssueSeverity.ERROR, oo.getIssue().get(0).getSeverity()); + assertEquals(2, oo.getIssue().get(0).getLocation().size()); + assertEquals("Observation.value.ofType(Quantity)", oo.getIssue().get(0).getLocation().get(0).getValue()); + assertEquals("Line[15] Col[4]", oo.getIssue().get(0).getLocation().get(1).getValue()); + } + @Test public void testValidateForCreate() { String methodName = "testValidateForCreate"; 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 bc9d9b61364..a95aa057d15 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 @@ -182,7 +182,7 @@ public class FhirResourceDaoR4ValueSetTest extends BaseJpaR4Test { outcome = myValidationSupport.validateCode(ctx, options, "http://cs", "childX", null, "http://vs"); assertNotNull(outcome); assertFalse(outcome.isOk()); - assertThat(outcome.getMessage(), containsString("Unknown code http://cs#childX")); + assertThat(outcome.getMessage(), containsString("Unknown code \"http://cs#childX\"")); assertThat(outcome.getMessage(), containsString("Code validation occurred using a ValueSet expansion that was pre-calculated at ")); // Precalculated - Enumerated in non-present CS @@ -195,7 +195,7 @@ public class FhirResourceDaoR4ValueSetTest extends BaseJpaR4Test { outcome = myValidationSupport.validateCode(ctx, options, "http://cs-np", "codeX", null, "http://vs"); assertNotNull(outcome); assertFalse(outcome.isOk()); - assertThat(outcome.getMessage(), containsString("Unknown code http://cs-np#codeX")); + assertThat(outcome.getMessage(), containsString("Unknown code \"http://cs-np#codeX\"")); assertThat(outcome.getMessage(), containsString("Code validation occurred using a ValueSet expansion that was pre-calculated at ")); } @@ -285,7 +285,7 @@ public class FhirResourceDaoR4ValueSetTest extends BaseJpaR4Test { outcome = myValidationSupport.validateCode(ctx, options, "http://cs", "childX", null, "http://vs"); assertNotNull(outcome); assertFalse(outcome.isOk()); - assertThat(outcome.getMessage(), containsString("Unknown code http://cs#childX")); + assertThat(outcome.getMessage(), containsString("Unknown code \"http://cs#childX\"")); assertThat(outcome.getMessage(), containsString("Code validation occurred using a ValueSet expansion that was pre-calculated at ")); // Precalculated - Enumerated in non-present CS @@ -298,7 +298,7 @@ public class FhirResourceDaoR4ValueSetTest extends BaseJpaR4Test { outcome = myValidationSupport.validateCode(ctx, options, "http://cs-np", "codeX", null, "http://vs"); assertNotNull(outcome); assertFalse(outcome.isOk()); - assertThat(outcome.getMessage(), containsString("Unknown code http://cs-np#codeX")); + assertThat(outcome.getMessage(), containsString("Unknown code \"http://cs-np#codeX\"")); assertThat(outcome.getMessage(), containsString("Code validation occurred using a ValueSet expansion that was pre-calculated at ")); } diff --git a/hapi-fhir-validation/src/main/java/org/hl7/fhir/common/hapi/validation/support/CommonCodeSystemsTerminologyService.java b/hapi-fhir-validation/src/main/java/org/hl7/fhir/common/hapi/validation/support/CommonCodeSystemsTerminologyService.java index d680996f74a..67c114e8cf0 100644 --- a/hapi-fhir-validation/src/main/java/org/hl7/fhir/common/hapi/validation/support/CommonCodeSystemsTerminologyService.java +++ b/hapi-fhir-validation/src/main/java/org/hl7/fhir/common/hapi/validation/support/CommonCodeSystemsTerminologyService.java @@ -38,6 +38,7 @@ import java.util.Optional; import javax.annotation.Nonnull; import javax.annotation.Nullable; +import static org.apache.commons.lang3.StringUtils.defaultIfBlank; import static org.apache.commons.lang3.StringUtils.defaultString; import static org.apache.commons.lang3.StringUtils.isBlank; import static org.apache.commons.lang3.StringUtils.isNotBlank; @@ -124,8 +125,8 @@ public class CommonCodeSystemsTerminologyService implements IValidationSupport { break; case LANGUAGES_VALUESET_URL: - if (!LANGUAGES_CODESYSTEM_URL.equals(theCodeSystem) - && !(theCodeSystem == null && theOptions.isInferSystem())) { + expectSystem = LANGUAGES_CODESYSTEM_URL; + if (!expectSystem.equals(theCodeSystem) && !(theCodeSystem == null && theOptions.isInferSystem())) { return new CodeValidationResult() .setSeverity(IssueSeverity.ERROR) .setMessage("Inappropriate CodeSystem URL \"" + theCodeSystem + "\" for ValueSet: " @@ -155,8 +156,8 @@ public class CommonCodeSystemsTerminologyService implements IValidationSupport { } case ALL_LANGUAGES_VALUESET_URL: - if (!LANGUAGES_CODESYSTEM_URL.equals(theCodeSystem) - && !(theCodeSystem == null && theOptions.isInferSystem())) { + expectSystem = LANGUAGES_CODESYSTEM_URL; + if (!expectSystem.equals(theCodeSystem) && !(theCodeSystem == null && theOptions.isInferSystem())) { return new CodeValidationResult() .setSeverity(IssueSeverity.ERROR) .setMessage("Inappropriate CodeSystem URL \"" + theCodeSystem + "\" for ValueSet: " @@ -178,8 +179,9 @@ public class CommonCodeSystemsTerminologyService implements IValidationSupport { case UCUM_VALUESET_URL: { String system = theCodeSystem; + expectSystem = UCUM_CODESYSTEM_URL; if (system == null && theOptions.isInferSystem()) { - system = UCUM_CODESYSTEM_URL; + system = expectSystem; } CodeValidationResult validationResult = validateLookupCode(theValidationSupportContext, theCode, system); @@ -197,9 +199,11 @@ public class CommonCodeSystemsTerminologyService implements IValidationSupport { } } - return new CodeValidationResult() - .setSeverity(IssueSeverity.ERROR) - .setMessage("Code \"" + theCode + "\" is not in system: " + USPS_CODESYSTEM_URL); + String actualSystem = defaultIfBlank(theCodeSystem, expectSystem); + String unknownCodeMessage = myFhirContext + .getLocalizer() + .getMessage("ca.uhn.fhir.jpa.term.TermReadSvcImpl.unknownCodeInSystem", actualSystem, theCode); + return new CodeValidationResult().setSeverity(IssueSeverity.ERROR).setMessage(unknownCodeMessage); } if (isBlank(theValueSetUrl)) { @@ -219,6 +223,10 @@ public class CommonCodeSystemsTerminologyService implements IValidationSupport { validationResult = new CodeValidationResult() .setCode(lookupResult.getSearchedForCode()) .setDisplay(lookupResult.getCodeDisplay()); + } else if (lookupResult.getErrorMessage() != null) { + validationResult = new CodeValidationResult() + .setSeverity(IssueSeverity.ERROR) + .setMessage(lookupResult.getErrorMessage()); } } @@ -267,6 +275,7 @@ public class CommonCodeSystemsTerminologyService implements IValidationSupport { retVal.setSearchedForCode(theCode); retVal.setSearchedForSystem(theSystem); retVal.setFound(false); + retVal.setErrorMessage("Code '" + theCode + "' is not valid for system: " + theSystem); return retVal; } @@ -390,20 +399,22 @@ public class CommonCodeSystemsTerminologyService implements IValidationSupport { private LookupCodeResult lookupUcumCode(String theCode) { InputStream input = ClasspathUtil.loadResourceAsStream("/ucum-essence.xml"); String outcome = null; - try { - UcumEssenceService svc = new UcumEssenceService(input); - outcome = svc.analyse(theCode); - } catch (UcumException e) { - ourLog.warn("Failed parse UCUM code: {}", theCode, e); - } finally { - ClasspathUtil.close(input); - } LookupCodeResult retVal = new LookupCodeResult(); retVal.setSearchedForCode(theCode); retVal.setSearchedForSystem(UCUM_CODESYSTEM_URL); - if (outcome != null) { - retVal.setFound(true); - retVal.setCodeDisplay(outcome); + + try { + UcumEssenceService svc = new UcumEssenceService(input); + outcome = svc.analyse(theCode); + if (outcome != null) { + retVal.setFound(true); + retVal.setCodeDisplay(outcome); + } + } catch (UcumException e) { + ourLog.debug("Failed parse UCUM code: {}", theCode, e); + retVal.setErrorMessage(e.getMessage()); + } finally { + ClasspathUtil.close(input); } return retVal; } diff --git a/hapi-fhir-validation/src/main/java/org/hl7/fhir/common/hapi/validation/validator/BaseValidatorBridge.java b/hapi-fhir-validation/src/main/java/org/hl7/fhir/common/hapi/validation/validator/BaseValidatorBridge.java index 0985a37cbf9..3f6e13c80ea 100644 --- a/hapi-fhir-validation/src/main/java/org/hl7/fhir/common/hapi/validation/validator/BaseValidatorBridge.java +++ b/hapi-fhir-validation/src/main/java/org/hl7/fhir/common/hapi/validation/validator/BaseValidatorBridge.java @@ -7,6 +7,7 @@ import ca.uhn.fhir.validation.SingleValidationMessage; import org.hl7.fhir.instance.model.api.IBaseResource; import org.hl7.fhir.utilities.validation.ValidationMessage; +import java.util.Arrays; import java.util.List; /** @@ -38,6 +39,9 @@ abstract class BaseValidatorBridge implements IValidatorModule { if (riMessage.getMessageId() != null) { hapiMessage.setMessageId(riMessage.getMessageId()); } + if (riMessage.sliceText != null && riMessage.sliceText.length > 0) { + hapiMessage.setSliceMessages(Arrays.asList(riMessage.sliceText)); + } theCtx.addValidationMessage(hapiMessage); } } diff --git a/hapi-fhir-validation/src/test/java/org/hl7/fhir/common/hapi/validation/support/CommonCodeSystemsTerminologyServiceTest.java b/hapi-fhir-validation/src/test/java/org/hl7/fhir/common/hapi/validation/support/CommonCodeSystemsTerminologyServiceTest.java index 475fe4f84ae..dc0d72961a0 100644 --- a/hapi-fhir-validation/src/test/java/org/hl7/fhir/common/hapi/validation/support/CommonCodeSystemsTerminologyServiceTest.java +++ b/hapi-fhir-validation/src/test/java/org/hl7/fhir/common/hapi/validation/support/CommonCodeSystemsTerminologyServiceTest.java @@ -33,21 +33,21 @@ public class CommonCodeSystemsTerminologyServiceTest { public void testUcum_LookupCode_Good() { IValidationSupport.LookupCodeResult outcome = mySvc.lookupCode(newSupport(), "http://unitsofmeasure.org", "Cel"); assert outcome != null; - assertEquals(true, outcome.isFound()); + assertTrue(outcome.isFound()); } @Test public void testUcum_LookupCode_Good2() { IValidationSupport.LookupCodeResult outcome = mySvc.lookupCode(newSupport(), "http://unitsofmeasure.org", "kg/m2"); assert outcome != null; - assertEquals(true, outcome.isFound()); + assertTrue(outcome.isFound()); } @Test public void testUcum_LookupCode_Bad() { IValidationSupport.LookupCodeResult outcome = mySvc.lookupCode(newSupport(), "http://unitsofmeasure.org", "AAAAA"); assert outcome != null; - assertEquals(false, outcome.isFound()); + assertFalse(outcome.isFound()); } @Test @@ -82,7 +82,7 @@ public class CommonCodeSystemsTerminologyServiceTest { vs.setUrl("http://hl7.org/fhir/ValueSet/ucum-units"); IValidationSupport.CodeValidationResult outcome = mySvc.validateCodeInValueSet(newSupport(), newOptions(), "http://unitsofmeasure.org", "mg", null, vs); assert outcome != null; - assertEquals(true, outcome.isOk()); + assertTrue(outcome.isOk()); assertEquals("(milligram)", outcome.getDisplay()); } @@ -92,7 +92,7 @@ public class CommonCodeSystemsTerminologyServiceTest { vs.setUrl("http://hl7.org/fhir/ValueSet/ucum-units"); IValidationSupport.CodeValidationResult outcome = mySvc.validateCodeInValueSet(newSupport(), newOptions().setInferSystem(true), null, "mg", null, vs); assert outcome != null; - assertEquals(true, outcome.isOk()); + assertTrue(outcome.isOk()); assertEquals("(milligram)", outcome.getDisplay()); } @@ -101,7 +101,10 @@ public class CommonCodeSystemsTerminologyServiceTest { ValueSet vs = new ValueSet(); vs.setUrl("http://hl7.org/fhir/ValueSet/ucum-units"); IValidationSupport.CodeValidationResult outcome = mySvc.validateCodeInValueSet(newSupport(), newOptions(), "http://unitsofmeasure.org", "aaaaa", null, vs); - assertNull(outcome); + assertNotNull(outcome); + assertFalse(outcome.isOk()); + assertEquals("Error processing unit 'aaaaa': The unit 'aaaaa' is unknown' at position 0", outcome.getMessage()); + assertEquals("error", outcome.getSeverityCode()); } @Test @@ -207,7 +210,7 @@ public class CommonCodeSystemsTerminologyServiceTest { public void testFetchCodeSystemBuiltIn_Iso3166_DSTU2() { CommonCodeSystemsTerminologyService svc = new CommonCodeSystemsTerminologyService(FhirContext.forDstu2Cached()); IBaseResource cs = svc.fetchCodeSystem(CommonCodeSystemsTerminologyService.COUNTRIES_CODESYSTEM_URL); - assertEquals(null, cs); + assertNull(cs); } @Test @@ -220,7 +223,7 @@ public class CommonCodeSystemsTerminologyServiceTest { @Test public void testFetchCodeSystemBuiltIn_Unknown() { CodeSystem cs = (CodeSystem) mySvc.fetchCodeSystem("http://foo"); - assertEquals(null, cs); + assertNull(cs); } @Test 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 df0c1fbafe5..44c1307f146 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 @@ -16,9 +16,11 @@ import ca.uhn.fhir.validation.FhirValidator; import ca.uhn.fhir.validation.ResultSeverityEnum; import ca.uhn.fhir.validation.SingleValidationMessage; import ca.uhn.fhir.validation.ValidationResult; +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.hamcrest.Matchers; 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; @@ -26,6 +28,7 @@ import org.hl7.fhir.common.hapi.validation.support.PrePopulatedValidationSupport 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.common.hapi.validation.validator.VersionSpecificWorkerContextWrapper; import org.hl7.fhir.exceptions.FHIRException; import org.hl7.fhir.instance.model.api.IBaseResource; import org.hl7.fhir.r4.conformance.ProfileUtilities; @@ -43,6 +46,7 @@ import org.hl7.fhir.r4.model.Consent; import org.hl7.fhir.r4.model.ContactPoint; import org.hl7.fhir.r4.model.DateTimeType; import org.hl7.fhir.r4.model.Extension; +import org.hl7.fhir.r4.model.IntegerType; import org.hl7.fhir.r4.model.Media; import org.hl7.fhir.r4.model.Narrative; import org.hl7.fhir.r4.model.Observation; @@ -51,7 +55,9 @@ import org.hl7.fhir.r4.model.OperationOutcome; import org.hl7.fhir.r4.model.Patient; import org.hl7.fhir.r4.model.Period; import org.hl7.fhir.r4.model.Practitioner; +import org.hl7.fhir.r4.model.PrimitiveType; import org.hl7.fhir.r4.model.Procedure; +import org.hl7.fhir.r4.model.Quantity; import org.hl7.fhir.r4.model.QuestionnaireResponse; import org.hl7.fhir.r4.model.Reference; import org.hl7.fhir.r4.model.RelatedPerson; @@ -62,6 +68,7 @@ import org.hl7.fhir.r4.model.ValueSet; import org.hl7.fhir.r4.model.ValueSet.ValueSetExpansionComponent; import org.hl7.fhir.r4.terminologies.ValueSetExpander; import org.hl7.fhir.r4.utils.FHIRPathEngine; +import org.hl7.fhir.r5.elementmodel.JsonParser; import org.hl7.fhir.r5.test.utils.ClassesLoadedFlags; import org.hl7.fhir.r5.utils.validation.IValidationPolicyAdvisor; import org.hl7.fhir.r5.utils.validation.IValidatorResourceFetcher; @@ -98,6 +105,7 @@ import static org.hamcrest.Matchers.hasSize; import static org.hamcrest.Matchers.lessThan; import static org.hamcrest.Matchers.not; import static org.hamcrest.Matchers.startsWith; +import static org.hl7.fhir.common.hapi.validation.support.CommonCodeSystemsTerminologyService.CURRENCIES_CODESYSTEM_URL; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertFalse; import static org.junit.jupiter.api.Assertions.assertTrue; @@ -108,6 +116,7 @@ import static org.mockito.Mockito.mock; 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 FhirInstanceValidatorR4Test extends BaseTest { @@ -923,7 +932,7 @@ public class FhirInstanceValidatorR4Test extends BaseTest { ourLog.debug(ourCtx.newJsonParser().setPrettyPrint(true).encodeResourceToString(operationOutcome)); assertEquals("Unrecognized property 'foo'", operationOutcome.getIssue().get(0).getDiagnostics()); assertEquals("Patient", operationOutcome.getIssue().get(0).getLocation().get(0).getValue()); - assertEquals("Line 5, Col 23", operationOutcome.getIssue().get(0).getLocation().get(1).getValue()); + assertEquals("Line[5] Col[23]", operationOutcome.getIssue().get(0).getLocation().get(1).getValue()); } @Test @@ -1314,6 +1323,44 @@ public class FhirInstanceValidatorR4Test extends BaseTest { } + @Test + public void testSliceValidation() throws IOException { + Patient patient = new Patient(); + patient.getText().setDiv(new XhtmlNode().setValue("
")).setStatus(Narrative.NarrativeStatus.GENERATED); + + Observation input = new Observation(); + input.setSubject(new Reference("Patient/A")); + input.getText().setDiv(new XhtmlNode().setValue("
AA
")).setStatus(Narrative.NarrativeStatus.GENERATED); + input.setStatus(ObservationStatus.FINAL); + input.getCode().addCoding().setSystem("http://acme.org").setCode("12345"); + + addValidConcept("http://acme.org", "12345"); + + IValidationPolicyAdvisor policy = mock(IValidationPolicyAdvisor.class, withSettings().verboseLogging()); + when(policy.policyForReference(any(), any(), any(), any())).thenReturn(ReferenceValidationPolicy.CHECK_VALID); + myInstanceVal.setValidatorPolicyAdvisor(policy); + + IValidatorResourceFetcher fetcher = mock(IValidatorResourceFetcher.class, withSettings().verboseLogging()); + when(fetcher.fetch(any(), any(), any())).thenAnswer(t -> { + return new JsonParser(new VersionSpecificWorkerContextWrapper(new ValidationSupportContext(myValidationSupport), new VersionCanonicalizer(ourCtx))) + .parse(ourCtx.newJsonParser().encodeResourceToString(patient), "Patient"); + }); + myInstanceVal.setValidatorResourceFetcher(fetcher); + myInstanceVal.setValidationSupport(myValidationSupport); + myInstanceVal.setBestPracticeWarningLevel(BestPracticeWarningLevel.Ignore); + + ValidationResult output = myFhirValidator.validateWithResult(input); + OperationOutcome oo = (OperationOutcome) output.toOperationOutcome(); + ourLog.info("Outcome: {}", ourCtx.newJsonParser().setPrettyPrint(true).encodeResourceToString(oo)); + assertEquals(1, oo.getIssue().size()); + assertEquals(OperationOutcome.IssueType.PROCESSING, oo.getIssue().get(0).getCode()); + assertEquals(OperationOutcome.IssueSeverity.INFORMATION, oo.getIssue().get(0).getSeverity()); + assertEquals("Details for Patient/A matching against profile http://hl7.org/fhir/StructureDefinition/Patient|4.0.1 - Observation.subject->Patient.text: Narrative.div: minimum required = 1, but only found 0 (from http://hl7.org/fhir/StructureDefinition/Narrative|4.0.1)", oo.getIssue().get(0).getDiagnostics()); + assertThat(oo.getIssue().get(0).getLocation().stream().map(PrimitiveType::getValue).collect(Collectors.toList()), Matchers.contains( + "Observation.subject", "Line[1] Col[238]" + )); + } + @Test public void testValidateResourceWithExampleBindingCodeValidationFailingNonLoinc() { Observation input = new Observation(); @@ -1416,7 +1463,7 @@ public class FhirInstanceValidatorR4Test extends BaseTest { output = myFhirValidator.validateWithResult(input); all = logResultsAndReturnNonInformationalOnes(output); assertEquals(2, all.size()); - assertThat(all.get(0).getMessage(), containsString("Validation failed for 'http://unitsofmeasure.org#Heck'")); + assertThat(all.get(0).getMessage(), containsString("The unit 'Heck' is unknown' at position 0 for 'http://unitsofmeasure.org#Heck'")); assertThat(all.get(1).getMessage(), containsString("The value provided ('Heck') is not in the value set 'Body Temperature Units'")); } @@ -1518,7 +1565,8 @@ public class FhirInstanceValidatorR4Test extends BaseTest { ValidationResult output = myFhirValidator.validateWithResult(input); List errors = logResultsAndReturnNonInformationalOnes(output); assertEquals(1, errors.size(), errors.toString()); - assertThat(errors.get(0).getMessage(), containsString("The value provided ('BLAH') is not in the value set 'CurrencyCode' (http://hl7.org/fhir/ValueSet/currencies|4.0.1), and a code is required from this value set) (error message = Unknown code 'BLAH' for in-memory expansion of ValueSet 'http://hl7.org/fhir/ValueSet/currencies')")); + assertThat(errors.get(0).getMessage(), containsString("The value provided ('BLAH') is not in the value set 'CurrencyCode' (http://hl7.org/fhir/ValueSet/currencies|4.0.1)")); + assertThat(errors.get(0).getMessage(), containsString("error message = Unknown code \"urn:iso:std:iso:4217#BLAH\"")); } @@ -1602,6 +1650,90 @@ public class FhirInstanceValidatorR4Test extends BaseTest { } } + + @Test + void testValidateCommonCodes_Ucum_ErrorMessageIsPreserved() { + buildValidationSupportWithLogicalAndSupport(false); + + Observation input = new Observation(); + input.getText().setDiv(new XhtmlNode().setValue("
AA
")).setStatus(Narrative.NarrativeStatus.GENERATED); + input.setStatus(ObservationStatus.AMENDED); + input.getCode().addCoding().setSystem("http://loinc.org").setCode("1234").setDisplay("FOO"); + input.setValue(new Quantity( + null, + 123, + "http://unitsofmeasure.org", + "MG/DL", + "MG/DL" + )); + String inputString = ourCtx.newJsonParser().setPrettyPrint(true).encodeResourceToString(input); + ourLog.info("Input:\n{}", inputString); + + ValidationResult result = myFhirValidator.validateWithResult(inputString); + + OperationOutcome oo = (OperationOutcome) result.toOperationOutcome(); + ourLog.info(ourCtx.newJsonParser().setPrettyPrint(true).encodeResourceToString(oo)); + + assertEquals("Error processing unit 'MG/DL': The unit 'DL' is unknown' at position 3 for 'http://unitsofmeasure.org#MG/DL'", result.getMessages().get(0).getMessage()); + assertEquals(ResultSeverityEnum.ERROR, result.getMessages().get(0).getSeverity()); + assertEquals(15, result.getMessages().get(0).getLocationLine()); + assertEquals(4, result.getMessages().get(0).getLocationCol()); + assertEquals("Observation.value.ofType(Quantity)", result.getMessages().get(0).getLocationString()); + assertEquals("Terminology_PassThrough_TX_Message", result.getMessages().get(0).getMessageId()); + + assertEquals(15, ((IntegerType) oo.getIssue().get(0).getExtensionByUrl("http://hl7.org/fhir/StructureDefinition/operationoutcome-issue-line").getValue()).getValue()); + assertEquals(4, ((IntegerType) oo.getIssue().get(0).getExtensionByUrl("http://hl7.org/fhir/StructureDefinition/operationoutcome-issue-col").getValue()).getValue()); + assertEquals("Terminology_PassThrough_TX_Message", ((StringType) oo.getIssue().get(0).getExtensionByUrl("http://hl7.org/fhir/StructureDefinition/operationoutcome-message-id").getValue()).getValue()); + assertEquals("Error processing unit 'MG/DL': The unit 'DL' is unknown' at position 3 for 'http://unitsofmeasure.org#MG/DL'", oo.getIssue().get(0).getDiagnostics()); + assertEquals(OperationOutcome.IssueType.PROCESSING, oo.getIssue().get(0).getCode()); + assertEquals(OperationOutcome.IssueSeverity.ERROR, oo.getIssue().get(0).getSeverity()); + assertEquals(2, oo.getIssue().get(0).getLocation().size()); + assertEquals("Observation.value.ofType(Quantity)", oo.getIssue().get(0).getLocation().get(0).getValue()); + assertEquals("Line[15] Col[4]", oo.getIssue().get(0).getLocation().get(1).getValue()); + } + + @Test + void testValidateCommonCodes_Currency_ErrorMessageIsPreserved() { + buildValidationSupportWithLogicalAndSupport(false); + + Observation input = new Observation(); + input.getText().setDiv(new XhtmlNode().setValue("
AA
")).setStatus(Narrative.NarrativeStatus.GENERATED); + input.setStatus(ObservationStatus.AMENDED); + input.getCode().addCoding().setSystem("http://loinc.org").setCode("1234").setDisplay("FOO"); + input.setValue(new Quantity( + null, + 123, + CURRENCIES_CODESYSTEM_URL, + "blah", + "blah" + )); + String inputString = ourCtx.newJsonParser().setPrettyPrint(true).encodeResourceToString(input); + ourLog.info("Results:\n{}", inputString); + + ValidationResult result = myFhirValidator.validateWithResult(inputString); + + OperationOutcome oo = (OperationOutcome) result.toOperationOutcome(); + ourLog.info(ourCtx.newJsonParser().setPrettyPrint(true).encodeResourceToString(oo)); + + assertEquals("Unknown code 'urn:iso:std:iso:4217#blah' for 'urn:iso:std:iso:4217#blah'", result.getMessages().get(0).getMessage()); + assertEquals(ResultSeverityEnum.ERROR, result.getMessages().get(0).getSeverity()); + assertEquals(15, result.getMessages().get(0).getLocationLine()); + assertEquals(4, result.getMessages().get(0).getLocationCol()); + assertEquals("Observation.value.ofType(Quantity)", result.getMessages().get(0).getLocationString()); + assertEquals("Terminology_PassThrough_TX_Message", result.getMessages().get(0).getMessageId()); + + assertEquals(15, ((IntegerType) oo.getIssue().get(0).getExtensionByUrl("http://hl7.org/fhir/StructureDefinition/operationoutcome-issue-line").getValue()).getValue()); + assertEquals(4, ((IntegerType) oo.getIssue().get(0).getExtensionByUrl("http://hl7.org/fhir/StructureDefinition/operationoutcome-issue-col").getValue()).getValue()); + assertEquals("Terminology_PassThrough_TX_Message", ((StringType) oo.getIssue().get(0).getExtensionByUrl("http://hl7.org/fhir/StructureDefinition/operationoutcome-message-id").getValue()).getValue()); + assertEquals("Unknown code 'urn:iso:std:iso:4217#blah' for 'urn:iso:std:iso:4217#blah'", oo.getIssue().get(0).getDiagnostics()); + assertEquals(OperationOutcome.IssueType.PROCESSING, oo.getIssue().get(0).getCode()); + assertEquals(OperationOutcome.IssueSeverity.ERROR, oo.getIssue().get(0).getSeverity()); + assertEquals(2, oo.getIssue().get(0).getLocation().size()); + assertEquals("Observation.value.ofType(Quantity)", oo.getIssue().get(0).getLocation().get(0).getValue()); + assertEquals("Line[15] Col[4]", oo.getIssue().get(0).getLocation().get(1).getValue()); + } + + @Test public void testPatientSingleCommunicationLanguage_en() throws IOException { final String encoded = loadResource("patient-with-single-comm-lang-en.json"); @@ -1678,6 +1810,25 @@ public class FhirInstanceValidatorR4Test extends BaseTest { } } + private void buildValidationSupportWithLogicalAndSupport(boolean theLogicalAnd) { + myFhirValidator = ourCtx.newValidator(); + myFhirValidator.setValidateAgainstStandardSchema(false); + myFhirValidator.setValidateAgainstStandardSchematron(false); + // This is only used if the validation is performed with validationOptions.isConcurrentBundleValidation = true + myFhirValidator.setExecutorService(Executors.newFixedThreadPool(4)); + + myMockSupport = mock(IValidationSupport.class); + when(myMockSupport.getFhirContext()).thenReturn(ourCtx); + ValidationSupportChain chain = new ValidationSupportChain( + myDefaultValidationSupport, + myMockSupport, + new CommonCodeSystemsTerminologyService(ourCtx), + new InMemoryTerminologyServerValidationSupport(ourCtx), + new SnapshotGeneratingValidationSupport(ourCtx)); + myValidationSupport = new CachingValidationSupport(chain, theLogicalAnd); + myInstanceVal = new FhirInstanceValidator(myValidationSupport); + myFhirValidator.registerValidatorModule(myInstanceVal); + } @AfterAll public static void verifyFormatParsersNotLoaded() throws Exception { @@ -1695,7 +1846,6 @@ public class FhirInstanceValidatorR4Test extends BaseTest { assertFalse(ClassesLoadedFlags.ourXmlParserBaseLoaded); } - @AfterAll public static void afterClassClearContext() throws IOException, NoSuchFieldException { myDefaultValidationSupport.flush(); @@ -1703,19 +1853,4 @@ public class FhirInstanceValidatorR4Test extends BaseTest { TestUtil.randomizeLocaleAndTimezone(); } - private void buildValidationSupportWithLogicalAndSupport(boolean theLogicalAnd) { - myFhirValidator = ourCtx.newValidator(); - myFhirValidator.setValidateAgainstStandardSchema(false); - myFhirValidator.setValidateAgainstStandardSchematron(false); - // This is only used if the validation is performed with validationOptions.isConcurrentBundleValidation = true - myFhirValidator.setExecutorService(Executors.newFixedThreadPool(4)); - - myMockSupport = mock(IValidationSupport.class); - when(myMockSupport.getFhirContext()).thenReturn(ourCtx); - ValidationSupportChain chain = new ValidationSupportChain(myDefaultValidationSupport, myMockSupport, new InMemoryTerminologyServerValidationSupport(ourCtx), new CommonCodeSystemsTerminologyService(ourCtx), new SnapshotGeneratingValidationSupport(ourCtx)); - myValidationSupport = new CachingValidationSupport(chain, theLogicalAnd); - myInstanceVal = new FhirInstanceValidator(myValidationSupport); - myFhirValidator.registerValidatorModule(myInstanceVal); - } - } 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 cb935d72f75..7a0f0073cbd 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 @@ -868,7 +868,7 @@ public class FhirInstanceValidatorR4BTest extends BaseTest { ourLog.debug(ourCtx.newJsonParser().setPrettyPrint(true).encodeResourceToString(operationOutcome)); assertEquals("Unrecognized property 'foo'", operationOutcome.getIssue().get(0).getDiagnostics()); assertEquals("Patient", operationOutcome.getIssue().get(0).getLocation().get(0).getValue()); - assertEquals("Line 5, Col 23", operationOutcome.getIssue().get(0).getLocation().get(1).getValue()); + assertEquals("Line[5] Col[23]", operationOutcome.getIssue().get(0).getLocation().get(1).getValue()); } @Test @@ -1329,7 +1329,7 @@ public class FhirInstanceValidatorR4BTest extends BaseTest { output = myFhirValidator.validateWithResult(input); all = logResultsAndReturnNonInformationalOnes(output); assertEquals(2, all.size()); - assertThat(all.get(0).getMessage(), containsString("Validation failed for 'http://unitsofmeasure.org#Heck'")); + assertThat(all.get(0).getMessage(), containsString("The unit 'Heck' is unknown' at position 0 for 'http://unitsofmeasure.org#Heck'")); assertThat(all.get(1).getMessage(), containsString("The value provided ('Heck') is not in the value set 'Body Temperature Units'")); }