diff --git a/hapi-fhir-structures-r4/src/main/java/org/hl7/fhir/r4/elementmodel/Property.java b/hapi-fhir-structures-r4/src/main/java/org/hl7/fhir/r4/elementmodel/Property.java index 60f4d26334b..62421ae9e3e 100644 --- a/hapi-fhir-structures-r4/src/main/java/org/hl7/fhir/r4/elementmodel/Property.java +++ b/hapi-fhir-structures-r4/src/main/java/org/hl7/fhir/r4/elementmodel/Property.java @@ -13,6 +13,7 @@ import org.hl7.fhir.r4.model.StructureDefinition; import org.hl7.fhir.r4.model.StructureDefinition.StructureDefinitionKind; import org.hl7.fhir.r4.model.TypeDetails; import org.hl7.fhir.r4.utils.ToolingExtensions; +import org.apache.commons.lang3.StringUtils; import org.hl7.fhir.exceptions.DefinitionException; public class Property { @@ -163,6 +164,20 @@ public class Property { return FormatUtilities.FHIR_NS; } + private boolean isElementWithOnlyExtension(final ElementDefinition ed, final List children) { + boolean result = false; + if (!ed.getType().isEmpty()) { + result = true; + for (final ElementDefinition ele : children) { + if (!ele.getPath().contains("extension")) { + result = false; + break; + } + } + } + return result; + } + public boolean IsLogicalAndHasPrimitiveValue(String name) { // if (canBePrimitive!= null) // return canBePrimitive; @@ -203,7 +218,7 @@ public class Property { ElementDefinition ed = definition; StructureDefinition sd = structure; List children = ProfileUtilities.getChildMap(sd, ed); - if (children.isEmpty()) { + if (children.isEmpty() || isElementWithOnlyExtension(ed, children)) { // ok, find the right definitions String t = null; if (ed.getType().size() == 1) @@ -240,7 +255,13 @@ public class Property { } } if (!"xhtml".equals(t)) { - sd = context.fetchResource(StructureDefinition.class, "http://hl7.org/fhir/StructureDefinition/"+t); + final String url; + if (StringUtils.isNotBlank(ed.getType().get(0).getProfile())) { + url = ed.getType().get(0).getProfile(); + } else { + url = "http://hl7.org/fhir/StructureDefinition/" + t; + } + sd = context.fetchResource(StructureDefinition.class, url); if (sd == null) throw new DefinitionException("Unable to find type '"+t+"' for name '"+elementName+"' on property "+definition.getPath()); children = ProfileUtilities.getChildMap(sd, sd.getSnapshot().getElement().get(0)); diff --git a/hapi-fhir-structures-r4/src/test/java/org/hl7/fhir/r4/elementmodel/PropertyTest.java b/hapi-fhir-structures-r4/src/test/java/org/hl7/fhir/r4/elementmodel/PropertyTest.java new file mode 100644 index 00000000000..e60e14d91e8 --- /dev/null +++ b/hapi-fhir-structures-r4/src/test/java/org/hl7/fhir/r4/elementmodel/PropertyTest.java @@ -0,0 +1,66 @@ +package org.hl7.fhir.r4.elementmodel; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; + +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.util.List; + +import org.apache.commons.io.IOUtils; +import org.hl7.fhir.exceptions.DefinitionException; +import org.hl7.fhir.r4.hapi.ctx.DefaultProfileValidationSupport; +import org.hl7.fhir.r4.hapi.ctx.HapiWorkerContext; +import org.hl7.fhir.r4.model.ElementDefinition; +import org.hl7.fhir.r4.model.StructureDefinition; +import org.junit.Before; +import org.junit.Test; + +import ca.uhn.fhir.context.FhirContext; +import ca.uhn.fhir.parser.IParser; + +/** + * Created by axemj on 14/07/2017. + */ +public class PropertyTest { + + private static final FhirContext ourCtx = FhirContext.forR4(); + private Property property; + private StructureDefinition sd; + private HapiWorkerContext workerContext; + + @Before + public void setUp() throws IOException { + final String sdString = IOUtils.toString(getClass().getResourceAsStream("/customPatientSd.xml"), StandardCharsets.UTF_8); + final IParser parser = ourCtx.newXmlParser(); + sd = parser.parseResource(StructureDefinition.class, sdString); + workerContext = new HapiWorkerContext(ourCtx, new DefaultProfileValidationSupport()); + } + + @Test + public void getChildPropertiesPrimitiveTest() throws DefinitionException { + final ElementDefinition ed = sd.getSnapshot().getElement().get(1); + property = new Property(workerContext, ed, sd); + final List result = property.getChildProperties("id", null); + assertFalse(result.isEmpty()); + assertEquals(3, result.size()); + assertEquals("id.id", result.get(0).getDefinition().getPath()); + } + + @Test + public void getChildPropertiesOnlyExtensionElementTest() throws DefinitionException { + final ElementDefinition ed = sd.getSnapshot().getElement().get(23); + property = new Property(workerContext, ed, sd); + final List result = property.getChildProperties("birthdate", null); + assertFalse(result.isEmpty()); + assertEquals(3, result.size()); + assertEquals("date.id", result.get(0).getDefinition().getPath()); + } + + @Test(expected = Error.class) + public void getChildPropertiesErrorTest() throws DefinitionException { + final ElementDefinition ed = sd.getSnapshot().getElement().get(7); + property = new Property(workerContext, ed, sd); + property.getChildProperties("birthdate", null); + } +} diff --git a/hapi-fhir-structures-r4/src/test/resource/customPatientSd.xml b/hapi-fhir-structures-r4/src/test/resource/customPatientSd.xml new file mode 100644 index 00000000000..592badb62ab --- /dev/null +++ b/hapi-fhir-structures-r4/src/test/resource/customPatientSd.xml @@ -0,0 +1,447 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + <status value="draft"/> + <experimental value="false"/> + <date value="2017-07-14T08:37:31+02:00"/> + <publisher value="Me"/> + <contact> + <name value="test"/> + <telecom> + <system value="email"/> + <value value="test@test.com"/> + <use value="work"/> + </telecom> + </contact> + <description value="Information about an individual receiving health care services"/> + <useContext> + <code> + <system value="urn:iso:std:iso:3166"/> + <code value="FR"/> + <display value="France"/> + </code> + </useContext> + <fhirVersion value="3.0.1"/> + <kind value="resource"/> + <abstract value="false"/> + <type value="Patient"/> + <baseDefinition value="http://hl7.org/fhir/StructureDefinition/patient"/> + <derivation value="constraint"/> + <snapshot> + <element> + <path value="Patient"/> + </element> + <element> + <path value="Patient.id"/> + <short value="Logical id of this artifact"/> + <definition value="The logical id of the resource, as used in the URL for the resource. Once assigned, this value never changes."/> + <min value="0"/> + <max value="1"/> + <type> + <code value="id"/> + </type> + <isSummary value="true"/> + </element> + <element> + <path value="Patient.meta"/> + <short value="Metadata about the resource"/> + <definition value="The metadata about the resource. This is content that is maintained by the infrastructure. Changes to the content may not always be associated with version changes to the resource."/> + <min value="0"/> + <max value="1"/> + <type> + <code value="Meta"/> + </type> + <isSummary value="true"/> + </element> + <element> + <path value="Patient.text"/> + <max value="0"/> + </element> + <element> + <path value="Patient.contained"/> + <max value="0"/> + </element> + <element> + <path value="Patient.implicitRules"/> + <max value="0"/> + </element> + <element> + <path value="Patient.language"/> + <short value="Language of the resource content"/> + <definition value="The base language in which the resource is written."/> + <min value="0"/> + <max value="1"/> + <type> + <code value="code"/> + </type> + <isModifier value="false"/> + <isSummary value="false"/> + </element> + <element> + <path value="Patient.extension"/> + <slicing> + <discriminator> + <type value="value"/> + <path value="url"/> + </discriminator> + <rules value="open"/> + </slicing> + <definition value="May be used to represent additional information that is not part of the basic definition of the element."/> + </element> + <element> + <path value="Patient.extension"/> + <short value="identityReliabilityCode"/> + <definition value="The patient identity reliability code"/> + <min value="0"/> + <max value="1"/> + <type> + <code value="Extension"/> + <profile value="http://www.myServer.com/fhir/StructureDefinition/identityReliabilityCode"/> + </type> + <isModifier value="false"/> + <isSummary value="true"/> + <binding> + <strength value="required"/> + <valueSetReference> + <reference value="ValueSet/IDENTITYSTATUS"/> + </valueSetReference> + </binding> + </element> + <element> + <path value="Patient.extension"/> + <short value="lunarBirthDate"/> + <definition value="The patient lunar's birth date"/> + <min value="0"/> + <max value="1"/> + <type> + <code value="Extension"/> + <profile value="http://www.myServer.com/fhir/StructureDefinition/lunarBirthDate"/> + </type> + <isModifier value="false"/> + <isSummary value="false"/> + </element> + <element> + <path value="Patient.extension"/> + <short value="legalStatus"/> + <definition value="The patient legal status"/> + <min value="0"/> + <max value="1"/> + <type> + <code value="Extension"/> + <profile value="http://www.myServer.com/fhir/StructureDefinition/legalStatus"/> + </type> + <isModifier value="false"/> + <isSummary value="false"/> + <binding> + <strength value="required"/> + <valueSetReference> + <reference value="ValueSet/ADT_LEGALSTATUS"/> + </valueSetReference> + </binding> + </element> + <element> + <path value="Patient.extension"/> + <short value="familyStatus"/> + <definition value="The patient family status"/> + <min value="0"/> + <max value="1"/> + <type> + <code value="Extension"/> + <profile value="http://www.myServer.com/fhir/StructureDefinition/familyStatus"/> + </type> + <isModifier value="false"/> + <isSummary value="false"/> + <binding> + <strength value="required"/> + <valueSetReference> + <reference value="ValueSet/FAMILYSTATUS"/> + </valueSetReference> + </binding> + </element> + <element> + <path value="Patient.extension"/> + <short value="birthPlace"/> + <definition value="The patient birth place"/> + <min value="0"/> + <max value="1"/> + <type> + <code value="Extension"/> + <profile value="http://hl7.org/fhir/StructureDefinition/birthPlace"/> + </type> + <isModifier value="false"/> + <isSummary value="false"/> + </element> + <element> + <path value="Patient.extension"/> + <short value="homeless"/> + <definition value="The patient being homeless, true if homeless"/> + <min value="0"/> + <max value="1"/> + <type> + <code value="Extension"/> + <profile value="http://www.myServer.com/fhir/StructureDefinition/homeless"/> + </type> + <isModifier value="false"/> + <isSummary value="false"/> + </element> + <element> + <path value="Patient.extension"/> + <short value="phoneConsent"/> + <definition value="The patient consent on phone level"/> + <min value="0"/> + <max value="1"/> + <type> + <code value="Extension"/> + <profile value="http://www.myServer.com/fhir/StructureDefinition/phoneConsent"/> + </type> + <isModifier value="false"/> + <isSummary value="false"/> + <binding> + <strength value="required"/> + <valueSetReference> + <reference value="ValueSet/ADT_CONTACT_CONSENT_MOBILE"/> + </valueSetReference> + </binding> + </element> + <element> + <path value="Patient.extension"/> + <short value="emailConsent"/> + <definition value="The patient consent on email level"/> + <min value="0"/> + <max value="1"/> + <type> + <code value="Extension"/> + <profile value="http://www.myServer.com/fhir/StructureDefinition/emailConsent"/> + </type> + <isModifier value="false"/> + <isSummary value="false"/> + <binding> + <strength value="required"/> + <valueSetReference> + <reference value="ValueSet/ADT_CONTACT_CONSENT_EMAIL"/> + </valueSetReference> + </binding> + </element> + <element> + <path value="Patient.extension"/> + <short value="comments"/> + <definition value="The patient comments"/> + <min value="0"/> + <max value="1"/> + <type> + <code value="Extension"/> + <profile value="http://www.myServer.com/fhir/StructureDefinition/comments"/> + </type> + <isModifier value="false"/> + <isSummary value="false"/> + </element> + <element> + <path value="Patient.extension"/> + <short value="nationality"/> + <definition value="The patient nationality"/> + <min value="0"/> + <max value="1"/> + <type> + <code value="Extension"/> + <profile value="http://hl7.org/fhir/StructureDefinition/patient-nationality"/> + </type> + <isModifier value="false"/> + </element> + <element> + <path value="Patient.identifier"/> + <short value="An identifier for this patient"/> + <definition value="An identifier for this patient."/> + <min value="0"/> + <max value="*"/> + <type> + <code value="Identifier"/> + <profile value="http://www.myServer.com/fhir/StructureDefinition/Identifier"/> + </type> + <isModifier value="false"/> + <isSummary value="true"/> + </element> + <element> + <path value="Patient.active"/> + <max value="0"/> + </element> + <element> + <path value="Patient.name"/> + <short value="A name associated with the patient"/> + <definition value="A name associated with the individual."/> + <min value="0"/> + <max value="*"/> + <type> + <code value="HumanName"/> + <profile value="http://www.myServer.com/fhir/StructureDefinition/HumanName"/> + </type> + <isModifier value="false"/> + <isSummary value="true"/> + </element> + <element> + <path value="Patient.telecom"/> + <short value="A contact detail for the individual"/> + <definition value="A contact detail (e.g. a telephone number or an email address) by which the individual may be contacted."/> + <min value="0"/> + <max value="*"/> + <type> + <code value="ContactPoint"/> + <profile value="http://www.myServer.com/fhir/StructureDefinition/ContactPoint"/> + </type> + <isModifier value="false"/> + <isSummary value="true"/> + </element> + <element> + <path value="Patient.gender"/> + <short value="male | female | other | unknown"/> + <definition value="Administrative Gender - the gender that the patient is considered to have for administration and record keeping purposes."/> + <min value="0"/> + <max value="1"/> + <type> + <code value="code"/> + </type> + <isModifier value="false"/> + <isSummary value="true"/> + </element> + <element> + <path value="Patient.birthDate"/> + <short value="The date of birth for the individual"/> + <definition value="The date of birth for the individual."/> + <min value="0"/> + <max value="1"/> + <type> + <code value="date"/> + </type> + <isModifier value="false"/> + <isSummary value="true"/> + </element> + <element> + <path value="Patient.birthDate.extension"/> + <slicing> + <discriminator> + <type value="value"/> + <path value="url"/> + </discriminator> + <rules value="open"/> + </slicing> + </element> + <element> + <path value="Patient.birthDate.extension"/> + <short value="approximateBirthDate"/> + <definition value="Flag to indicate if the birthdate is approximative or not"/> + <min value="0"/> + <max value="1"/> + <type> + <code value="Extension"/> + <profile value="http://www.myServer.com/fhir/StructureDefinition/birthdate-approximative"/> + </type> + <isModifier value="false"/> + </element> + <element> + <path value="Patient.deceased[x]"/> + <short value="Indicates if the individual is deceased or not"/> + <definition value="Indicates if the individual is deceased or not."/> + <min value="0"/> + <max value="1"/> + <type> + <code value="boolean"/> + </type> + <type> + <code value="dateTime"/> + </type> + <isModifier value="true"/> + <isSummary value="true"/> + </element> + <element> + <path value="Patient.address"/> + <short value="Addresses for the individual"/> + <definition value="Addresses for the individual."/> + <min value="0"/> + <max value="*"/> + <type> + <code value="Address"/> + <profile value="http://www.myServer.com/fhir/StructureDefinition/Address"/> + </type> + <isModifier value="false"/> + <isSummary value="true"/> + </element> + <element> + <path value="Patient.maritalStatus"/> + <max value="0"/> + </element> + <element> + <path value="Patient.multipleBirth[x]"/> + <short value="Whether patient is part of a multiple birth"/> + <definition value="Indicates whether the patient is part of a multiple (bool) or indicates the actual birth order (integer)."/> + <min value="0"/> + <max value="1"/> + <type> + <code value="boolean"/> + </type> + <type> + <code value="integer"/> + </type> + <isModifier value="false"/> + <isSummary value="false"/> + </element> + <element> + <path value="Patient.photo"/> + <max value="0"/> + </element> + <element> + <path value="Patient.contact"/> + <max value="0"/> + </element> + <element> + <path value="Patient.animal"/> + <max value="0"/> + </element> + <element> + <path value="Patient.communication"/> + <max value="0"/> + </element> + <element> + <path value="Patient.generalPractitioner"/> + <max value="0"/> + </element> + <element> + <path value="Patient.managingOrganization"/> + <max value="0"/> + </element> + <element> + <path value="Patient.link"/> + <max value="0"/> + </element> + </snapshot> +</StructureDefinition> + diff --git a/hapi-fhir-validation/src/main/java/org/hl7/fhir/r4/hapi/validation/FhirInstanceValidator.java b/hapi-fhir-validation/src/main/java/org/hl7/fhir/r4/hapi/validation/FhirInstanceValidator.java index 852ee862377..03a718dd3a9 100644 --- a/hapi-fhir-validation/src/main/java/org/hl7/fhir/r4/hapi/validation/FhirInstanceValidator.java +++ b/hapi-fhir-validation/src/main/java/org/hl7/fhir/r4/hapi/validation/FhirInstanceValidator.java @@ -32,235 +32,252 @@ import ca.uhn.fhir.validation.IValidatorModule; public class FhirInstanceValidator extends BaseValidatorBridge implements IValidatorModule { - private static final org.slf4j.Logger ourLog = org.slf4j.LoggerFactory.getLogger(FhirInstanceValidator.class); + private static final org.slf4j.Logger ourLog = org.slf4j.LoggerFactory.getLogger(FhirInstanceValidator.class); - private boolean myAnyExtensionsAllowed = true; - private BestPracticeWarningLevel myBestPracticeWarningLevel; - private DocumentBuilderFactory myDocBuilderFactory; - private StructureDefinition myStructureDefintion; - private IValidationSupport myValidationSupport; + private boolean myAnyExtensionsAllowed = true; + private BestPracticeWarningLevel myBestPracticeWarningLevel; + private DocumentBuilderFactory myDocBuilderFactory; + private boolean myNoTerminologyChecks; + private StructureDefinition myStructureDefintion; - /** - * Constructor - * - * Uses {@link DefaultProfileValidationSupport} for {@link IValidationSupport validation support} - */ - public FhirInstanceValidator() { - this(new DefaultProfileValidationSupport()); - } + private IValidationSupport myValidationSupport; - /** - * Constructor which uses the given validation support - * - * @param theValidationSupport - * The validation support - */ - public FhirInstanceValidator(IValidationSupport theValidationSupport) { - myDocBuilderFactory = DocumentBuilderFactory.newInstance(); - myDocBuilderFactory.setNamespaceAware(true); - myValidationSupport = theValidationSupport; - } + /** + * Constructor + * + * Uses {@link DefaultProfileValidationSupport} for {@link IValidationSupport validation support} + */ + public FhirInstanceValidator() { + this(new DefaultProfileValidationSupport()); + } - private String determineResourceName(Document theDocument) { - Element root = null; + /** + * Constructor which uses the given validation support + * + * @param theValidationSupport + * The validation support + */ + public FhirInstanceValidator(IValidationSupport theValidationSupport) { + myDocBuilderFactory = DocumentBuilderFactory.newInstance(); + myDocBuilderFactory.setNamespaceAware(true); + myValidationSupport = theValidationSupport; + } - NodeList list = theDocument.getChildNodes(); - for (int i = 0; i < list.getLength(); i++) { - if (list.item(i) instanceof Element) { - root = (Element) list.item(i); - break; - } - } - root = theDocument.getDocumentElement(); - return root.getLocalName(); - } + private String determineResourceName(Document theDocument) { + Element root = null; - private StructureDefinition findStructureDefinitionForResourceName(final FhirContext theCtx, String resourceName) { - String sdName = "http://hl7.org/fhir/StructureDefinition/" + resourceName; - StructureDefinition profile = myStructureDefintion != null ? myStructureDefintion : myValidationSupport.fetchStructureDefinition(theCtx, sdName); - return profile; - } + NodeList list = theDocument.getChildNodes(); + for (int i = 0; i < list.getLength(); i++) { + if (list.item(i) instanceof Element) { + root = (Element) list.item(i); + break; + } + } + root = theDocument.getDocumentElement(); + return root.getLocalName(); + } - /** - * Returns the "best practice" warning level (default is {@link BestPracticeWarningLevel#Hint}). - * <p> - * The FHIR Instance Validator has a number of checks for best practices in terms of FHIR usage. If this setting is - * set to {@link BestPracticeWarningLevel#Error}, any resource data which does not meet these best practices will be - * reported at the ERROR level. If this setting is set to {@link BestPracticeWarningLevel#Ignore}, best practice - * guielines will be ignored. - * </p> - * - * @see {@link #setBestPracticeWarningLevel(BestPracticeWarningLevel)} - */ - public BestPracticeWarningLevel getBestPracticeWarningLevel() { - return myBestPracticeWarningLevel; - } + private StructureDefinition findStructureDefinitionForResourceName(final FhirContext theCtx, String resourceName) { + String sdName = "http://hl7.org/fhir/StructureDefinition/" + resourceName; + StructureDefinition profile = myStructureDefintion != null ? myStructureDefintion : myValidationSupport.fetchStructureDefinition(theCtx, sdName); + return profile; + } - /** - * Returns the {@link IValidationSupport validation support} in use by this validator. Default is an instance of - * {@link DefaultProfileValidationSupport} if the no-arguments constructor for this object was used. - */ - public IValidationSupport getValidationSupport() { - return myValidationSupport; - } + /** + * Returns the "best practice" warning level (default is {@link BestPracticeWarningLevel#Hint}). + * <p> + * The FHIR Instance Validator has a number of checks for best practices in terms of FHIR usage. If this setting is + * set to {@link BestPracticeWarningLevel#Error}, any resource data which does not meet these best practices will be + * reported at the ERROR level. If this setting is set to {@link BestPracticeWarningLevel#Ignore}, best practice + * guielines will be ignored. + * </p> + * + * @see {@link #setBestPracticeWarningLevel(BestPracticeWarningLevel)} + */ + public BestPracticeWarningLevel getBestPracticeWarningLevel() { + return myBestPracticeWarningLevel; + } - /** - * If set to {@literal true} (default is true) extensions which are not known to the - * validator (e.g. because they have not been explicitly declared in a profile) will - * be validated but will not cause an error. - */ - public boolean isAnyExtensionsAllowed() { - return myAnyExtensionsAllowed; - } + /** + * Returns the {@link IValidationSupport validation support} in use by this validator. Default is an instance of + * {@link DefaultProfileValidationSupport} if the no-arguments constructor for this object was used. + */ + public IValidationSupport getValidationSupport() { + return myValidationSupport; + } - /** - * If set to {@literal true} (default is true) extensions which are not known to the - * validator (e.g. because they have not been explicitly declared in a profile) will - * be validated but will not cause an error. - */ - public void setAnyExtensionsAllowed(boolean theAnyExtensionsAllowed) { - myAnyExtensionsAllowed = theAnyExtensionsAllowed; - } + /** + * If set to {@literal true} (default is true) extensions which are not known to the + * validator (e.g. because they have not been explicitly declared in a profile) will + * be validated but will not cause an error. + */ + public boolean isAnyExtensionsAllowed() { + return myAnyExtensionsAllowed; + } - /** - * Sets the "best practice warning level". When validating, any deviations from best practices will be reported at - * this level. - * <p> - * The FHIR Instance Validator has a number of checks for best practices in terms of FHIR usage. If this setting is - * set to {@link BestPracticeWarningLevel#Error}, any resource data which does not meet these best practices will be - * reported at the ERROR level. If this setting is set to {@link BestPracticeWarningLevel#Ignore}, best practice - * guielines will be ignored. - * </p> - * - * @param theBestPracticeWarningLevel - * The level, must not be <code>null</code> - */ - public void setBestPracticeWarningLevel(BestPracticeWarningLevel theBestPracticeWarningLevel) { - Validate.notNull(theBestPracticeWarningLevel); - myBestPracticeWarningLevel = theBestPracticeWarningLevel; - } + /** + * If set to {@literal true} (default is false) the valueSet will not be validate + */ + public boolean isNoTerminologyChecks() { + return myNoTerminologyChecks; + } - public void setStructureDefintion(StructureDefinition theStructureDefintion) { - myStructureDefintion = theStructureDefintion; - } + /** + * If set to {@literal true} (default is true) extensions which are not known to the + * validator (e.g. because they have not been explicitly declared in a profile) will + * be validated but will not cause an error. + */ + public void setAnyExtensionsAllowed(boolean theAnyExtensionsAllowed) { + myAnyExtensionsAllowed = theAnyExtensionsAllowed; + } - /** - * Sets the {@link IValidationSupport validation support} in use by this validator. Default is an instance of - * {@link DefaultProfileValidationSupport} if the no-arguments constructor for this object was used. - */ - public void setValidationSupport(IValidationSupport theValidationSupport) { - myValidationSupport = theValidationSupport; - } + /** + * Sets the "best practice warning level". When validating, any deviations from best practices will be reported at + * this level. + * <p> + * The FHIR Instance Validator has a number of checks for best practices in terms of FHIR usage. If this setting is + * set to {@link BestPracticeWarningLevel#Error}, any resource data which does not meet these best practices will be + * reported at the ERROR level. If this setting is set to {@link BestPracticeWarningLevel#Ignore}, best practice + * guielines will be ignored. + * </p> + * + * @param theBestPracticeWarningLevel + * The level, must not be <code>null</code> + */ + public void setBestPracticeWarningLevel(BestPracticeWarningLevel theBestPracticeWarningLevel) { + Validate.notNull(theBestPracticeWarningLevel); + myBestPracticeWarningLevel = theBestPracticeWarningLevel; + } - protected List<ValidationMessage> validate(final FhirContext theCtx, String theInput, EncodingEnum theEncoding) { - HapiWorkerContext workerContext = new HapiWorkerContext(theCtx, myValidationSupport); + /** + * If set to {@literal true} (default is false) the valueSet will not be validate + */ + public void setNoTerminologyChecks(final boolean theNoTerminologyChecks) { + myNoTerminologyChecks = theNoTerminologyChecks; + } - InstanceValidator v; - IEvaluationContext evaluationCtx = new NullEvaluationContext(); - try { - v = new InstanceValidator(workerContext, evaluationCtx); - } catch (Exception e) { - throw new ConfigurationException(e); - } + public void setStructureDefintion(StructureDefinition theStructureDefintion) { + myStructureDefintion = theStructureDefintion; + } - v.setBestPracticeWarningLevel(getBestPracticeWarningLevel()); - v.setAnyExtensionsAllowed(isAnyExtensionsAllowed()); - v.setResourceIdRule(IdStatus.OPTIONAL); + /** + * Sets the {@link IValidationSupport validation support} in use by this validator. Default is an instance of + * {@link DefaultProfileValidationSupport} if the no-arguments constructor for this object was used. + */ + public void setValidationSupport(IValidationSupport theValidationSupport) { + myValidationSupport = theValidationSupport; + } - List<ValidationMessage> messages = new ArrayList<ValidationMessage>(); + protected List<ValidationMessage> validate(final FhirContext theCtx, String theInput, EncodingEnum theEncoding) { + HapiWorkerContext workerContext = new HapiWorkerContext(theCtx, myValidationSupport); - if (theEncoding == EncodingEnum.XML) { - Document document; - try { - DocumentBuilder builder = myDocBuilderFactory.newDocumentBuilder(); - InputSource src = new InputSource(new StringReader(theInput)); - document = builder.parse(src); - } catch (Exception e2) { - ourLog.error("Failure to parse XML input", e2); - ValidationMessage m = new ValidationMessage(); - m.setLevel(IssueSeverity.FATAL); - m.setMessage("Failed to parse input, it does not appear to be valid XML:" + e2.getMessage()); - return Collections.singletonList(m); - } + InstanceValidator v; + IEvaluationContext evaluationCtx = new NullEvaluationContext(); + try { + v = new InstanceValidator(workerContext, evaluationCtx); + } catch (Exception e) { + throw new ConfigurationException(e); + } - String resourceName = determineResourceName(document); - StructureDefinition profile = findStructureDefinitionForResourceName(theCtx, resourceName); - if (profile != null) { - try { - v.validate(null, messages, document, profile); - } catch (Exception e) { - throw new InternalErrorException("Unexpected failure while validating resource", e); - } - } - } else if (theEncoding == EncodingEnum.JSON) { - Gson gson = new GsonBuilder().create(); - JsonObject json = gson.fromJson(theInput, JsonObject.class); + v.setBestPracticeWarningLevel(getBestPracticeWarningLevel()); + v.setAnyExtensionsAllowed(isAnyExtensionsAllowed()); + v.setResourceIdRule(IdStatus.OPTIONAL); + v.setNoTerminologyChecks(isNoTerminologyChecks()); - String resourceName = json.get("resourceType").getAsString(); - StructureDefinition profile = findStructureDefinitionForResourceName(theCtx, resourceName); - if (profile != null) { - try { - v.validate(null, messages, json, profile); - } catch (Exception e) { - throw new InternalErrorException("Unexpected failure while validating resource", e); - } - } - } else { - throw new IllegalArgumentException("Unknown encoding: " + theEncoding); - } + List<ValidationMessage> messages = new ArrayList<ValidationMessage>(); - for (int i = 0; i < messages.size(); i++) { - ValidationMessage next = messages.get(i); - if ("Binding has no source, so can't be checked".equals(next.getMessage())) { - messages.remove(i); - i--; - } - } - return messages; - } + if (theEncoding == EncodingEnum.XML) { + Document document; + try { + DocumentBuilder builder = myDocBuilderFactory.newDocumentBuilder(); + InputSource src = new InputSource(new StringReader(theInput)); + document = builder.parse(src); + } catch (Exception e2) { + ourLog.error("Failure to parse XML input", e2); + ValidationMessage m = new ValidationMessage(); + m.setLevel(IssueSeverity.FATAL); + m.setMessage("Failed to parse input, it does not appear to be valid XML:" + e2.getMessage()); + return Collections.singletonList(m); + } - @Override - protected List<ValidationMessage> validate(IValidationContext<?> theCtx) { - return validate(theCtx.getFhirContext(), theCtx.getResourceAsString(), theCtx.getResourceAsStringEncoding()); - } + String resourceName = determineResourceName(document); + StructureDefinition profile = findStructureDefinitionForResourceName(theCtx, resourceName); + if (profile != null) { + try { + v.validate(null, messages, document, profile); + } catch (Exception e) { + throw new InternalErrorException("Unexpected failure while validating resource", e); + } + } + } else if (theEncoding == EncodingEnum.JSON) { + Gson gson = new GsonBuilder().create(); + JsonObject json = gson.fromJson(theInput, JsonObject.class); - public class NullEvaluationContext implements IEvaluationContext { + String resourceName = json.get("resourceType").getAsString(); + StructureDefinition profile = findStructureDefinitionForResourceName(theCtx, resourceName); + if (profile != null) { + try { + v.validate(null, messages, json, profile); + } catch (Exception e) { + throw new InternalErrorException("Unexpected failure while validating resource", e); + } + } + } else { + throw new IllegalArgumentException("Unknown encoding: " + theEncoding); + } - @Override - public TypeDetails checkFunction(Object theAppContext, String theFunctionName, List<TypeDetails> theParameters) throws PathEngineException { - return null; - } + for (int i = 0; i < messages.size(); i++) { + ValidationMessage next = messages.get(i); + if ("Binding has no source, so can't be checked".equals(next.getMessage())) { + messages.remove(i); + i--; + } + } + return messages; + } - @Override - public List<Base> executeFunction(Object theAppContext, String theFunctionName, List<List<Base>> theParameters) { - return null; - } + @Override + protected List<ValidationMessage> validate(IValidationContext<?> theCtx) { + return validate(theCtx.getFhirContext(), theCtx.getResourceAsString(), theCtx.getResourceAsStringEncoding()); + } - @Override - public boolean log(String theArgument, List<Base> theFocus) { - return false; - } + public class NullEvaluationContext implements IEvaluationContext { - @Override - public Base resolveConstant(Object theAppContext, String theName) throws PathEngineException { - return null; - } + @Override + public TypeDetails checkFunction(Object theAppContext, String theFunctionName, List<TypeDetails> theParameters) throws PathEngineException { + return null; + } - @Override - public TypeDetails resolveConstantType(Object theAppContext, String theName) throws PathEngineException { - return null; - } + @Override + public List<Base> executeFunction(Object theAppContext, String theFunctionName, List<List<Base>> theParameters) { + return null; + } - @Override - public FunctionDetails resolveFunction(String theFunctionName) { - return null; - } + @Override + public boolean log(String theArgument, List<Base> theFocus) { + return false; + } - @Override - public Base resolveReference(Object theAppContext, String theUrl) { - return null; - } + @Override + public Base resolveConstant(Object theAppContext, String theName) throws PathEngineException { + return null; + } - } + @Override + public TypeDetails resolveConstantType(Object theAppContext, String theName) throws PathEngineException { + return null; + } + + @Override + public FunctionDetails resolveFunction(String theFunctionName) { + return null; + } + + @Override + public Base resolveReference(Object theAppContext, String theUrl) { + return null; + } + + } } diff --git a/hapi-fhir-validation/src/test/java/ca/uhn/fhir/validation/FhirInstanceValidatorR4Test.java b/hapi-fhir-validation/src/test/java/ca/uhn/fhir/validation/FhirInstanceValidatorR4Test.java new file mode 100644 index 00000000000..f16271d19f4 --- /dev/null +++ b/hapi-fhir-validation/src/test/java/ca/uhn/fhir/validation/FhirInstanceValidatorR4Test.java @@ -0,0 +1,844 @@ +package ca.uhn.fhir.validation; + +import static org.hamcrest.Matchers.containsString; +import static org.hamcrest.Matchers.empty; +import static org.hamcrest.Matchers.greaterThan; +import static org.hamcrest.Matchers.not; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertThat; +import static org.junit.Assert.assertTrue; +import static org.mockito.Matchers.any; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +import java.io.IOException; +import java.io.InputStream; +import java.util.*; +import java.util.zip.GZIPInputStream; + +import org.apache.commons.io.IOUtils; +import org.hl7.fhir.instance.model.api.IBaseResource; +import org.hl7.fhir.r4.hapi.ctx.*; +import org.hl7.fhir.r4.hapi.ctx.IValidationSupport.CodeValidationResult; +import org.hl7.fhir.r4.hapi.validation.FhirInstanceValidator; +import org.hl7.fhir.r4.model.*; +import org.hl7.fhir.r4.model.Bundle.BundleEntryComponent; +import org.hl7.fhir.r4.model.CodeSystem.ConceptDefinitionComponent; +import org.hl7.fhir.r4.model.Observation.ObservationStatus; +import org.hl7.fhir.r4.model.StructureDefinition.StructureDefinitionKind; +import org.hl7.fhir.r4.model.ValueSet.ConceptSetComponent; +import org.hl7.fhir.r4.model.ValueSet.ValueSetExpansionComponent; +import org.hl7.fhir.r4.utils.FHIRPathEngine; +import org.junit.*; +import org.junit.rules.TestRule; +import org.junit.rules.TestWatcher; +import org.junit.runner.Description; +import org.mockito.invocation.InvocationOnMock; +import org.mockito.stubbing.Answer; + +import ca.uhn.fhir.context.FhirContext; +import ca.uhn.fhir.util.TestUtil; + +public class FhirInstanceValidatorR4Test { + + private static DefaultProfileValidationSupport myDefaultValidationSupport = new DefaultProfileValidationSupport(); + private static FhirContext ourCtx = FhirContext.forDstu3(); + private static final org.slf4j.Logger ourLog = org.slf4j.LoggerFactory.getLogger(FhirInstanceValidatorR4Test.class); + private FhirInstanceValidator myInstanceVal; + private IValidationSupport myMockSupport; + + private Map<String, ValueSetExpansionComponent> mySupportedCodeSystemsForExpansion; + private FhirValidator myVal; + private ArrayList<String> myValidConcepts; + private Set<String> myValidSystems = new HashSet<String>(); + @Rule + public TestRule watcher = new TestWatcher() { + protected void starting(Description description) { + ourLog.info("Starting test: " + description.getMethodName()); + } + }; + + private void addValidConcept(String theSystem, String theCode) { + myValidSystems.add(theSystem); + myValidConcepts.add(theSystem + "___" + theCode); + } + + /** + * See #531 + */ + @Test + public void testContactPointSystemUrlWorks() { + Patient p = new Patient(); + ContactPoint t = p.addTelecom(); + t.setSystem(org.hl7.fhir.r4.model.ContactPoint.ContactPointSystem.URL); + t.setValue("http://infoway-inforoute.ca"); + + ValidationResult results = myVal.validateWithResult(p); + List<SingleValidationMessage> outcome = logResultsAndReturnNonInformationalOnes(results); + assertThat(outcome, empty()); + + } + + /** + * See #370 + */ + @Test + public void testValidateRelatedPerson() { + + /* + * Try with a code that is in http://hl7.org/fhir/ValueSet/relatedperson-relationshiptype + * and therefore should validate + */ + RelatedPerson rp = new RelatedPerson(); + rp.getPatient().setReference("Patient/1"); + rp.getRelationship().addCoding().setSystem("http://hl7.org/fhir/v2/0131").setCode("c"); + + ValidationResult results = myVal.validateWithResult(rp); + List<SingleValidationMessage> outcome = logResultsAndReturnNonInformationalOnes(results); + assertThat(outcome, empty()); + + /* + * Code system is case insensitive, so try with capital C + */ + rp = new RelatedPerson(); + rp.getPatient().setReference("Patient/1"); + rp.getRelationship().addCoding().setSystem("http://hl7.org/fhir/v2/0131").setCode("C"); + + results = myVal.validateWithResult(rp); + outcome = logResultsAndReturnNonInformationalOnes(results); + assertThat(outcome, empty()); + + /* + * Now a bad code + */ + rp = new RelatedPerson(); + rp.getPatient().setReference("Patient/1"); + rp.getRelationship().addCoding().setSystem("http://hl7.org/fhir/v2/0131").setCode("GAGAGAGA"); + + results = myVal.validateWithResult(rp); + outcome = logResultsAndReturnNonInformationalOnes(results); + assertThat(outcome, not(empty())); + + } + + @Test + // @Ignore + public void testValidateBuiltInProfiles() throws Exception { + org.hl7.fhir.r4.model.Bundle bundle; + String name = "profiles-resources"; + ourLog.info("Uploading " + name); + String vsContents; + vsContents = IOUtils.toString(FhirInstanceValidatorR4Test.class.getResourceAsStream("/org/hl7/fhir/r4/model/profile/" + name + ".xml"), "UTF-8"); + + TreeSet<String> ids = new TreeSet<String>(); + + bundle = ourCtx.newXmlParser().parseResource(org.hl7.fhir.r4.model.Bundle.class, vsContents); + for (BundleEntryComponent i : bundle.getEntry()) { + org.hl7.fhir.r4.model.Resource next = i.getResource(); + ids.add(next.getId()); + + if (next instanceof StructureDefinition) { + StructureDefinition sd = (StructureDefinition) next; + if (sd.getKind() == StructureDefinitionKind.LOGICAL) { + ourLog.info("Skipping logical type: {}", next.getId()); + continue; + } + } + + ourLog.info("Validating {}", next.getId()); + ourLog.trace(ourCtx.newXmlParser().setPrettyPrint(true).encodeResourceToString(next)); + + ValidationResult output = myVal.validateWithResult(next); + List<SingleValidationMessage> errors = logResultsAndReturnNonInformationalOnes(output); + assertThat("Failed to validate " + i.getFullUrl() + " - " + errors, errors, empty()); + } + + ourLog.info("Validated the following:\n{}", ids); + } + + /** + * FHIRPathEngine was throwing Error... + */ + @Test + public void testValidateCrucibleCarePlan() throws Exception { + org.hl7.fhir.r4.model.Bundle bundle; + String name = "profiles-resources"; + ourLog.info("Uploading " + name); + String vsContents; + vsContents = IOUtils.toString(FhirInstanceValidatorR4Test.class.getResourceAsStream("/crucible-condition.xml"), "UTF-8"); + + ValidationResult output = myVal.validateWithResult(vsContents); + List<SingleValidationMessage> errors = logResultsAndReturnNonInformationalOnes(output); + } + + @Test + @Ignore + public void testValidateBundleWithObservations() throws Exception { + String name = "profiles-resources"; + ourLog.info("Uploading " + name); + String inputString; + inputString = IOUtils.toString(FhirInstanceValidatorR4Test.class.getResourceAsStream("/brian_reinhold_bundle.json"), "UTF-8"); + Bundle bundle = ourCtx.newJsonParser().parseResource(Bundle.class, inputString); + + FHIRPathEngine fp = new FHIRPathEngine(new HapiWorkerContext(ourCtx, myDefaultValidationSupport)); + List<Base> fpOutput; + BooleanType bool; + + fpOutput = fp.evaluate(bundle.getEntry().get(0).getResource(), "component.where(code = %resource.code).empty()"); + assertEquals(1, fpOutput.size()); + bool = (BooleanType) fpOutput.get(0); + assertTrue(bool.getValue()); + // + // fpOutput = fp.evaluate(bundle, "component.where(code = %resource.code).empty()"); + // assertEquals(1, fpOutput.size()); + // bool = (BooleanType) fpOutput.get(0); + // assertTrue(bool.getValue()); + + ValidationResult output = myVal.validateWithResult(inputString); + List<SingleValidationMessage> errors = logResultsAndReturnNonInformationalOnes(output); + assertThat(errors, empty()); + + } + + @Test + public void testValidateDocument() throws Exception { + String vsContents = IOUtils.toString(FhirInstanceValidatorR4Test.class.getResourceAsStream("/sample-document.xml"), "UTF-8"); + + ValidationResult output = myVal.validateWithResult(vsContents); + logResultsAndReturnNonInformationalOnes(output); + assertTrue(output.isSuccessful()); + } + + /** + * A reference with only an identifier should be valid + */ + @Test + public void testValidateReferenceWithIdentifierValid() throws Exception { + Patient p = new Patient(); + p.getManagingOrganization().getIdentifier().setSystem("http://acme.org"); + p.getManagingOrganization().getIdentifier().setValue("foo"); + + ValidationResult output = myVal.validateWithResult(p); + List<SingleValidationMessage> nonInfo = logResultsAndReturnNonInformationalOnes(output); + assertThat(nonInfo, empty()); + } + + /** + * A reference with only an identifier should be valid + */ + @Test + public void testValidateReferenceWithDisplayValid() throws Exception { + Patient p = new Patient(); + p.getManagingOrganization().setDisplay("HELLO"); + + ValidationResult output = myVal.validateWithResult(p); + List<SingleValidationMessage> nonInfo = logResultsAndReturnNonInformationalOnes(output); + assertThat(nonInfo, empty()); + } + + @SuppressWarnings("unchecked") + @Before + public void before() { + myVal = ourCtx.newValidator(); + myVal.setValidateAgainstStandardSchema(false); + myVal.setValidateAgainstStandardSchematron(false); + + myMockSupport = mock(IValidationSupport.class); + ValidationSupportChain validationSupport = new ValidationSupportChain(myMockSupport, myDefaultValidationSupport); + myInstanceVal = new FhirInstanceValidator(validationSupport); + + myVal.registerValidatorModule(myInstanceVal); + + mySupportedCodeSystemsForExpansion = new HashMap<String, ValueSet.ValueSetExpansionComponent>(); + + myValidConcepts = new ArrayList<String>(); + + when(myMockSupport.expandValueSet(any(FhirContext.class), any(ConceptSetComponent.class))).thenAnswer(new Answer<ValueSetExpansionComponent>() { + @Override + public ValueSetExpansionComponent answer(InvocationOnMock theInvocation) throws Throwable { + ConceptSetComponent arg = (ConceptSetComponent) theInvocation.getArguments()[0]; + ValueSetExpansionComponent retVal = mySupportedCodeSystemsForExpansion.get(arg.getSystem()); + if (retVal == null) { + retVal = myDefaultValidationSupport.expandValueSet(any(FhirContext.class), arg); + } + ourLog.debug("expandValueSet({}) : {}", new Object[] { theInvocation.getArguments()[0], retVal }); + return retVal; + } + }); + when(myMockSupport.isCodeSystemSupported(any(FhirContext.class), any(String.class))).thenAnswer(new Answer<Boolean>() { + @Override + public Boolean answer(InvocationOnMock theInvocation) throws Throwable { + boolean retVal = myValidSystems.contains(theInvocation.getArguments()[1]); + ourLog.debug("isCodeSystemSupported({}) : {}", new Object[] { theInvocation.getArguments()[1], retVal }); + return retVal; + } + }); + when(myMockSupport.fetchResource(any(FhirContext.class), any(Class.class), any(String.class))).thenAnswer(new Answer<IBaseResource>() { + @Override + public IBaseResource answer(InvocationOnMock theInvocation) throws Throwable { + IBaseResource retVal; + String id = (String) theInvocation.getArguments()[2]; + if ("Questionnaire/q_jon".equals(id)) { + retVal = ourCtx.newJsonParser().parseResource(IOUtils.toString(FhirInstanceValidatorR4Test.class.getResourceAsStream("/q_jon.json"))); + } else { + retVal = myDefaultValidationSupport.fetchResource((FhirContext) theInvocation.getArguments()[0], (Class<IBaseResource>) theInvocation.getArguments()[1], id); + } + ourLog.debug("fetchResource({}, {}) : {}", new Object[] { theInvocation.getArguments()[1], id, retVal }); + return retVal; + } + }); + when(myMockSupport.validateCode(any(FhirContext.class), any(String.class), any(String.class), any(String.class))).thenAnswer(new Answer<CodeValidationResult>() { + @Override + public CodeValidationResult answer(InvocationOnMock theInvocation) throws Throwable { + FhirContext ctx = (FhirContext) theInvocation.getArguments()[0]; + String system = (String) theInvocation.getArguments()[1]; + String code = (String) theInvocation.getArguments()[2]; + CodeValidationResult retVal; + if (myValidConcepts.contains(system + "___" + code)) { + retVal = new CodeValidationResult(new ConceptDefinitionComponent(new CodeType(code))); + } else { + retVal = myDefaultValidationSupport.validateCode(ctx, system, code, (String) theInvocation.getArguments()[2]); + } + ourLog.debug("validateCode({}, {}, {}) : {}", new Object[] { system, code, (String) theInvocation.getArguments()[2], retVal }); + return retVal; + } + }); + when(myMockSupport.fetchCodeSystem(any(FhirContext.class), any(String.class))).thenAnswer(new Answer<CodeSystem>() { + @Override + public CodeSystem answer(InvocationOnMock theInvocation) throws Throwable { + CodeSystem retVal = myDefaultValidationSupport.fetchCodeSystem((FhirContext) theInvocation.getArguments()[0], (String) theInvocation.getArguments()[1]); + ourLog.debug("fetchCodeSystem({}) : {}", new Object[] { (String) theInvocation.getArguments()[1], retVal }); + return retVal; + } + }); + when(myMockSupport.fetchStructureDefinition(any(FhirContext.class), any(String.class))).thenAnswer(new Answer<StructureDefinition>() { + @Override + public StructureDefinition answer(InvocationOnMock theInvocation) throws Throwable { + StructureDefinition retVal = myDefaultValidationSupport.fetchStructureDefinition((FhirContext) theInvocation.getArguments()[0], (String) theInvocation.getArguments()[1]); + ourLog.debug("fetchStructureDefinition({}) : {}", new Object[] { (String) theInvocation.getArguments()[1], retVal }); + return retVal; + } + }); + when(myMockSupport.fetchAllStructureDefinitions(any(FhirContext.class))).thenAnswer(new Answer<List<StructureDefinition>>() { + @Override + public List<StructureDefinition> answer(InvocationOnMock theInvocation) throws Throwable { + List<StructureDefinition> retVal = myDefaultValidationSupport.fetchAllStructureDefinitions((FhirContext) theInvocation.getArguments()[0]); + ourLog.debug("fetchAllStructureDefinitions()", new Object[] {}); + return retVal; + } + }); + + } + + private Object defaultString(Integer theLocationLine) { + return theLocationLine != null ? theLocationLine.toString() : ""; + } + + private List<SingleValidationMessage> logResultsAndReturnAll(ValidationResult theOutput) { + List<SingleValidationMessage> retVal = new ArrayList<SingleValidationMessage>(); + + int index = 0; + for (SingleValidationMessage next : theOutput.getMessages()) { + ourLog.info("Result {}: {} - {}:{} {} - {}", + new Object[] { index, next.getSeverity(), defaultString(next.getLocationLine()), defaultString(next.getLocationCol()), next.getLocationString(), next.getMessage() }); + index++; + + retVal.add(next); + } + + return retVal; + } + + private List<SingleValidationMessage> logResultsAndReturnNonInformationalOnes(ValidationResult theOutput) { + List<SingleValidationMessage> retVal = new ArrayList<SingleValidationMessage>(); + + int index = 0; + for (SingleValidationMessage next : theOutput.getMessages()) { + ourLog.info("Result {}: {} - {} - {}", new Object[] { index, next.getSeverity(), next.getLocationString(), next.getMessage() }); + index++; + + if (next.getSeverity() != ResultSeverityEnum.INFORMATION) { + retVal.add(next); + } + } + + return retVal; + } + + @Test + public void testValidateBigRawJsonResource() throws Exception { + InputStream stream = FhirInstanceValidatorR4Test.class.getResourceAsStream("/conformance.json.gz"); + stream = new GZIPInputStream(stream); + String input = IOUtils.toString(stream); + + long start = System.currentTimeMillis(); + ValidationResult output = null; + int passes = 1; + for (int i = 0; i < passes; i++) { + ourLog.info("Pass {}", i + 1); + output = myVal.validateWithResult(input); + } + + long delay = System.currentTimeMillis() - start; + long per = delay / passes; + + logResultsAndReturnAll(output); + + ourLog.info("Took {} ms -- {}ms / pass", delay, per); + } + + @Test + public void testValidateQuestionnaireResponse() throws IOException { + String input = IOUtils.toString(FhirInstanceValidatorR4Test.class.getResourceAsStream("/qr_jon.xml")); + + ValidationResult output = myVal.validateWithResult(input); + logResultsAndReturnAll(output); + + assertThat(output.getMessages().toString(), containsString("Items not of type group should not have items - Item with linkId 5.1 of type BOOLEAN has 1 item(s)")); + } + + @Test + public void testValidateRawJsonResource() { + //@formatter:off + String input = "{" + "\"resourceType\":\"Patient\"," + "\"id\":\"123\"" + "}"; + //@formatter:on + + ValidationResult output = myVal.validateWithResult(input); + assertEquals(output.toString(), 0, output.getMessages().size()); + } + + @Test + public void testValidateRawJsonResourceWithUnknownExtension() { + + Patient patient = new Patient(); + patient.setId("1"); + + Extension ext = patient.addExtension(); + ext.setUrl("http://hl7.org/fhir/v3/ethnicity"); + ext.setValue(new CodeType("Hispanic or Latino")); + + String encoded = ourCtx.newJsonParser().setPrettyPrint(true).encodeResourceToString(patient); + ourLog.info(encoded); + + /* + * { + * "resourceType": "Patient", + * "id": "1", + * "extension": [ + * { + * "url": "http://hl7.org/fhir/v3/ethnicity", + * "valueCode": "Hispanic or Latino" + * } + * ] + * } + */ + + ValidationResult output = myVal.validateWithResult(encoded); + assertEquals(output.toString(), 1, output.getMessages().size()); + + assertEquals("Unknown extension http://hl7.org/fhir/v3/ethnicity", output.getMessages().get(0).getMessage()); + assertEquals(ResultSeverityEnum.INFORMATION, output.getMessages().get(0).getSeverity()); + } + + @Test + public void testValidateRawJsonResourceWithUnknownExtensionNotAllowed() { + + Patient patient = new Patient(); + patient.setId("1"); + + Extension ext = patient.addExtension(); + ext.setUrl("http://hl7.org/fhir/v3/ethnicity"); + ext.setValue(new CodeType("Hispanic or Latino")); + + String encoded = ourCtx.newJsonParser().setPrettyPrint(true).encodeResourceToString(patient); + ourLog.info(encoded); + + /* + * { + * "resourceType": "Patient", + * "id": "1", + * "extension": [ + * { + * "url": "http://hl7.org/fhir/v3/ethnicity", + * "valueCode": "Hispanic or Latino" + * } + * ] + * } + */ + + myInstanceVal.setAnyExtensionsAllowed(false); + ValidationResult output = myVal.validateWithResult(encoded); + assertEquals(output.toString(), 1, output.getMessages().size()); + + assertEquals("The extension http://hl7.org/fhir/v3/ethnicity is unknown, and not allowed here", output.getMessages().get(0).getMessage()); + assertEquals(ResultSeverityEnum.ERROR, output.getMessages().get(0).getSeverity()); + } + + @Test + public void testValidateRawXmlWithMissingRootNamespace() { + //@formatter:off + String input = "" + + "<Patient>" + + " <text>" + + " <status value=\"generated\"/>" + + " <div xmlns=\"http://www.w3.org/1999/xhtml\">Some narrative</div>" + + " </text>" + + " <name>" + + " <use value=\"official\"/>" + + " <family value=\"Doe\"/>" + + " <given value=\"John\"/>" + + " </name>" + + " <gender value=\"male\"/>" + + " <birthDate value=\"1974-12-25\"/>" + + "</Patient>"; + //@formatter:on + + ValidationResult output = myVal.validateWithResult(input); + assertEquals(output.toString(), 1, output.getMessages().size()); + assertEquals("This cannot be parsed as a FHIR object (no namespace)", output.getMessages().get(0).getMessage()); + ourLog.info(output.getMessages().get(0).getLocationString()); + } + + @Test + public void testValidateRawJsonResourceBadAttributes() { + //@formatter:off + String input = + "{" + + "\"resourceType\":\"Patient\"," + + "\"id\":\"123\"," + + "\"foo\":\"123\"" + + "}"; + //@formatter:on + + ValidationResult output = myVal.validateWithResult(input); + assertEquals(output.toString(), 1, output.getMessages().size()); + ourLog.info(output.getMessages().get(0).getLocationString()); + ourLog.info(output.getMessages().get(0).getMessage()); + assertEquals("/Patient", output.getMessages().get(0).getLocationString()); + assertEquals("Unrecognised property '@foo'", output.getMessages().get(0).getMessage()); + } + + @Test + public void testValidateRawJsonResourceFromExamples() throws Exception { + // @formatter:off + String input = IOUtils.toString(FhirInstanceValidator.class.getResourceAsStream("/testscript-search.json")); + // @formatter:on + + ValidationResult output = myVal.validateWithResult(input); + logResultsAndReturnNonInformationalOnes(output); + // assertEquals(output.toString(), 1, output.getMessages().size()); + // ourLog.info(output.getMessages().get(0).getLocationString()); + // ourLog.info(output.getMessages().get(0).getMessage()); + // assertEquals("/foo", output.getMessages().get(0).getLocationString()); + // assertEquals("Element is unknown or does not match any slice", output.getMessages().get(0).getMessage()); + } + + @Test + public void testValidateRawXmlResource() { + // @formatter:off + String input = "<Patient xmlns=\"http://hl7.org/fhir\">" + "<id value=\"123\"/>" + "</Patient>"; + // @formatter:on + + ValidationResult output = myVal.validateWithResult(input); + assertEquals(output.toString(), 0, output.getMessages().size()); + } + + @Test + public void testValidateRawXmlResourceWithEmptyPrimitive() { + // @formatter:off + String input = "<Patient xmlns=\"http://hl7.org/fhir\"><name><given/></name></Patient>"; + // @formatter:on + + ValidationResult output = myVal.validateWithResult(input); + assertEquals(output.toString(), 2, output.getMessages().size()); + assertThat(output.getMessages().get(0).getMessage(), containsString("Element must have some content")); + assertThat(output.getMessages().get(1).getMessage(), containsString("primitive types must have a value or must have child extensions")); + } + + @Test + public void testValidateRawXmlResourceWithPrimitiveContainingOnlyAnExtension() { + // @formatter:off + String input = "<ActivityDefinition xmlns=\"http://hl7.org/fhir\">\n" + + " <id value=\"referralToMentalHealthCare\"/>\n" + + " <status value=\"draft\"/>\n" + + " <description value=\"refer to primary care mental-health integrated care program for evaluation and treatment of mental health conditions now\"/>\n" + + " <code>\n" + + " <coding>\n" + + " <!-- Error: Connection to http://localhost:960 refused -->\n" + + " <!--<system value=\"http://snomed.info/sct\"/>-->\n" + + " <code value=\"306206005\"/>\n" + + " </coding>\n" + + " </code>\n" + + " <!-- Specifying this this way results in a null reference exception in the validator -->\n" + + " <timingTiming>\n" + + " <event>\n" + + " <extension url=\"http://fhir.org/cql-expression\">\n" + + " <valueString value=\"Now()\"/>\n" + + " </extension>\n" + + " </event>\n" + + " </timingTiming>\n" + + " </ActivityDefinition>"; + // @formatter:on + + ValidationResult output = myVal.validateWithResult(input); + List<SingleValidationMessage> res = logResultsAndReturnNonInformationalOnes(output); + assertEquals(output.toString(), 0, res.size()); + } + + @Test + public void testValidateRawXmlResourceBadAttributes() { + //@formatter:off + String input = "<Patient xmlns=\"http://hl7.org/fhir\">" + "<id value=\"123\"/>" + "<foo value=\"222\"/>" + + "</Patient>"; + //@formatter:on + + ValidationResult output = myVal.validateWithResult(input); + assertEquals(output.toString(), 1, output.getMessages().size()); + ourLog.info(output.getMessages().get(0).getLocationString()); + ourLog.info(output.getMessages().get(0).getMessage()); + assertEquals("/f:Patient", output.getMessages().get(0).getLocationString()); + assertEquals("Undefined element 'foo\"", output.getMessages().get(0).getMessage()); + } + + @Test + public void testValidateResourceContainingLoincCode() { + addValidConcept("http://loinc.org", "1234567"); + + Observation input = new Observation(); + // input.getMeta().addProfile("http://hl7.org/fhir/StructureDefinition/devicemetricobservation"); + + input.addIdentifier().setSystem("http://acme").setValue("12345"); + input.getContext().setReference("http://foo.com/Encounter/9"); + input.setStatus(ObservationStatus.FINAL); + input.getCode().addCoding().setSystem("http://loinc.org").setCode("12345"); + + myInstanceVal.setValidationSupport(myMockSupport); + ValidationResult output = myVal.validateWithResult(input); + List<SingleValidationMessage> errors = logResultsAndReturnAll(output); + + assertThat(errors.toString(), containsString("warning")); + assertThat(errors.toString(), containsString("Unknown code: http://loinc.org / 12345")); + } + + @Test + public void testValidateResourceContainingProfileDeclaration() { + addValidConcept("http://loinc.org", "12345"); + + Observation input = new Observation(); + input.getMeta().addProfile("http://hl7.org/fhir/StructureDefinition/devicemetricobservation"); + + input.addIdentifier().setSystem("http://acme").setValue("12345"); + input.getContext().setReference("http://foo.com/Encounter/9"); + input.setStatus(ObservationStatus.FINAL); + input.getCode().addCoding().setSystem("http://loinc.org").setCode("12345"); + + myInstanceVal.setValidationSupport(myMockSupport); + ValidationResult output = myVal.validateWithResult(input); + List<SingleValidationMessage> errors = logResultsAndReturnNonInformationalOnes(output); + + assertThat(errors.toString(), containsString("Element 'Observation.subject': minimum required = 1, but only found 0")); + assertThat(errors.toString(), containsString("Element 'Observation.context: max allowed = 0, but found 1")); + assertThat(errors.toString(), containsString("Element 'Observation.device': minimum required = 1, but only found 0")); + assertThat(errors.toString(), containsString("")); + } + + @Test + public void testValidateResourceContainingProfileDeclarationDoesntResolve() { + addValidConcept("http://loinc.org", "12345"); + + Observation input = new Observation(); + input.getMeta().addProfile("http://foo/myprofile"); + + input.getCode().addCoding().setSystem("http://loinc.org").setCode("12345"); + input.setStatus(ObservationStatus.FINAL); + + myInstanceVal.setValidationSupport(myMockSupport); + ValidationResult output = myVal.validateWithResult(input); + List<SingleValidationMessage> errors = logResultsAndReturnNonInformationalOnes(output); + assertEquals(errors.toString(), 1, errors.size()); + assertEquals("StructureDefinition reference \"http://foo/myprofile\" could not be resolved", errors.get(0).getMessage()); + } + + @Test + public void testValidateResourceFailingInvariant() { + Observation input = new Observation(); + + // Has a value, but not a status (which is required) + input.getCode().addCoding().setSystem("http://loinc.org").setCode("12345"); + input.setValue(new StringType("AAA")); + + ValidationResult output = myVal.validateWithResult(input); + assertThat(output.getMessages().size(), greaterThan(0)); + assertEquals("Profile http://hl7.org/fhir/StructureDefinition/Observation, Element 'Observation.status': minimum required = 1, but only found 0", output.getMessages().get(0).getMessage()); + + } + + @Test + public void testValidateResourceWithDefaultValueset() { + Observation input = new Observation(); + + input.setStatus(ObservationStatus.FINAL); + input.getCode().setText("No code here!"); + + ourLog.info(ourCtx.newXmlParser().setPrettyPrint(true).encodeResourceToString(input)); + + ValidationResult output = myVal.validateWithResult(input); + assertEquals(output.getMessages().size(), 0); + } + + @Test + public void testValidateResourceWithDefaultValuesetBadCode() { + //@formatter:off + String input = + "<Observation xmlns=\"http://hl7.org/fhir\">\n" + + " <status value=\"notvalidcode\"/>\n" + + " <code>\n" + + " <text value=\"No code here!\"/>\n" + + " </code>\n" + + "</Observation>"; + //@formatter:on + ValidationResult output = myVal.validateWithResult(input); + logResultsAndReturnAll(output); + assertEquals( + "The value provided ('notvalidcode') is not in the value set http://hl7.org/fhir/ValueSet/observation-status (http://hl7.org/fhir/ValueSet/observation-status, and a code is required from this value set) (error message = Unknown code[notvalidcode] in system[null])", + output.getMessages().get(0).getMessage()); + } + + @Test + public void testValidateResourceWithExampleBindingCodeValidationFailing() { + Observation input = new Observation(); + + myInstanceVal.setValidationSupport(myMockSupport); + + input.setStatus(ObservationStatus.FINAL); + input.getCode().addCoding().setSystem("http://loinc.org").setCode("12345"); + + ValidationResult output = myVal.validateWithResult(input); + List<SingleValidationMessage> errors = logResultsAndReturnNonInformationalOnes(output); + assertEquals(errors.toString(), 0, errors.size()); + + } + + @Test + public void testValidateResourceWithExampleBindingCodeValidationPassingLoinc() { + Observation input = new Observation(); + + myInstanceVal.setValidationSupport(myMockSupport); + addValidConcept("http://loinc.org", "12345"); + + input.setStatus(ObservationStatus.FINAL); + input.getCode().addCoding().setSystem("http://loinc.org").setCode("12345"); + + ValidationResult output = myVal.validateWithResult(input); + List<SingleValidationMessage> errors = logResultsAndReturnNonInformationalOnes(output); + assertEquals(errors.toString(), 0, errors.size()); + } + + @Test + public void testValidateResourceWithExampleBindingCodeValidationPassingLoincWithExpansion() { + Observation input = new Observation(); + + ValueSetExpansionComponent expansionComponent = new ValueSetExpansionComponent(); + expansionComponent.addContains().setSystem("http://loinc.org").setCode("12345").setDisplay("Some display code"); + + mySupportedCodeSystemsForExpansion.put("http://loinc.org", expansionComponent); + myInstanceVal.setValidationSupport(myMockSupport); + addValidConcept("http://loinc.org", "12345"); + + input.setStatus(ObservationStatus.FINAL); + input.getCode().addCoding().setSystem("http://loinc.org").setCode("1234"); + + ValidationResult output = myVal.validateWithResult(input); + List<SingleValidationMessage> errors = logResultsAndReturnNonInformationalOnes(output); + assertEquals(1, errors.size()); + assertEquals("Unknown code: http://loinc.org / 1234", errors.get(0).getMessage()); + } + + @Test + public void testValidateResourceWithExampleBindingCodeValidationPassingNonLoinc() { + Observation input = new Observation(); + + myInstanceVal.setValidationSupport(myMockSupport); + addValidConcept("http://acme.org", "12345"); + + input.setStatus(ObservationStatus.FINAL); + input.getCode().addCoding().setSystem("http://acme.org").setCode("12345"); + + ValidationResult output = myVal.validateWithResult(input); + List<SingleValidationMessage> errors = logResultsAndReturnAll(output); + assertEquals(errors.toString(), 0, errors.size()); + } + + @Test + public void testValidateResourceWithExampleBindingCodeValidationFailingNonLoinc() { + Observation input = new Observation(); + + myInstanceVal.setValidationSupport(myMockSupport); + addValidConcept("http://acme.org", "12345"); + + input.setStatus(ObservationStatus.FINAL); + input.getCode().addCoding().setSystem("http://acme.org").setCode("9988877"); + + ValidationResult output = myVal.validateWithResult(input); + List<SingleValidationMessage> errors = logResultsAndReturnAll(output); + assertThat(errors.toString(), errors.size(), greaterThan(0)); + assertEquals("Unknown code: http://acme.org / 9988877", errors.get(0).getMessage()); + + } + + @Test + public void testValidateResourceWithValuesetExpansionBad() { + + Patient patient = new Patient(); + patient.addIdentifier().setSystem("http://example.com/").setValue("12345").getType().addCoding().setSystem("http://example.com/foo/bar").setCode("bar"); + + ValidationResult output = myVal.validateWithResult(patient); + List<SingleValidationMessage> all = logResultsAndReturnAll(output); + assertEquals(1, all.size()); + assertEquals("Patient.identifier.type", all.get(0).getLocationString()); + assertEquals( + "None of the codes provided are in the value set http://hl7.org/fhir/ValueSet/identifier-type (http://hl7.org/fhir/ValueSet/identifier-type, and a code should come from this value set unless it has no suitable code) (codes = http://example.com/foo/bar#bar)", + all.get(0).getMessage()); + assertEquals(ResultSeverityEnum.WARNING, all.get(0).getSeverity()); + + } + + @Test + public void testValidateResourceWithValuesetExpansionGood() { + Patient patient = new Patient(); + patient.addIdentifier().setSystem("http://system").setValue("12345").getType().addCoding().setSystem("http://hl7.org/fhir/v2/0203").setCode("MR"); + + ValidationResult output = myVal.validateWithResult(patient); + List<SingleValidationMessage> all = logResultsAndReturnAll(output); + assertEquals(0, all.size()); + } + + @Test + public void testIsNoTerminologyChecks() { + assertFalse(myInstanceVal.isNoTerminologyChecks()); + myInstanceVal.setNoTerminologyChecks(true); + assertTrue(myInstanceVal.isNoTerminologyChecks()); + } + + @Test + @Ignore + public void testValidateStructureDefinition() throws IOException { + String input = IOUtils.toString(FhirInstanceValidatorR4Test.class.getResourceAsStream("/sdc-questionnaire.profile.xml")); + + ValidationResult output = myVal.validateWithResult(input); + logResultsAndReturnAll(output); + + assertEquals(output.toString(), 3, output.getMessages().size()); + ourLog.info(output.getMessages().get(0).getLocationString()); + ourLog.info(output.getMessages().get(0).getMessage()); + } + + @AfterClass + public static void afterClassClearContext() { + myDefaultValidationSupport.flush(); + myDefaultValidationSupport = null; + TestUtil.clearAllStaticFieldsForUnitTest(); + } + +} diff --git a/hapi-fhir-validation/src/test/java/ca/uhn/fhir/validation/QuestionnaireResponseValidatorR4Test.java b/hapi-fhir-validation/src/test/java/ca/uhn/fhir/validation/QuestionnaireResponseValidatorR4Test.java new file mode 100644 index 00000000000..4f890678915 --- /dev/null +++ b/hapi-fhir-validation/src/test/java/ca/uhn/fhir/validation/QuestionnaireResponseValidatorR4Test.java @@ -0,0 +1,420 @@ +package ca.uhn.fhir.validation; + +import static org.hamcrest.Matchers.containsString; +import static org.hamcrest.Matchers.empty; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertThat; +import static org.mockito.Matchers.any; +import static org.mockito.Matchers.eq; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +import java.util.ArrayList; +import java.util.List; + +import org.apache.commons.io.IOUtils; +import org.hl7.fhir.dstu3.context.IWorkerContext; +import org.hl7.fhir.dstu3.hapi.validation.DefaultProfileValidationSupport; +import org.hl7.fhir.dstu3.hapi.validation.FhirInstanceValidator; +import org.hl7.fhir.dstu3.hapi.validation.HapiWorkerContext; +import org.hl7.fhir.dstu3.hapi.validation.IValidationSupport; +import org.hl7.fhir.dstu3.hapi.validation.IValidationSupport.CodeValidationResult; +import org.hl7.fhir.dstu3.hapi.validation.ValidationSupportChain; +import org.hl7.fhir.dstu3.model.CodeSystem; +import org.hl7.fhir.dstu3.model.CodeSystem.CodeSystemContentMode; +import org.hl7.fhir.dstu3.model.CodeSystem.ConceptDefinitionComponent; +import org.hl7.fhir.dstu3.model.CodeType; +import org.hl7.fhir.dstu3.model.Coding; +import org.hl7.fhir.dstu3.model.IntegerType; +import org.hl7.fhir.dstu3.model.Questionnaire; +import org.hl7.fhir.dstu3.model.Questionnaire.QuestionnaireItemComponent; +import org.hl7.fhir.dstu3.model.Questionnaire.QuestionnaireItemType; +import org.hl7.fhir.dstu3.model.QuestionnaireResponse; +import org.hl7.fhir.dstu3.model.QuestionnaireResponse.QuestionnaireResponseItemComponent; +import org.hl7.fhir.dstu3.model.QuestionnaireResponse.QuestionnaireResponseStatus; +import org.hl7.fhir.dstu3.model.Reference; +import org.hl7.fhir.dstu3.model.StringType; +import org.hl7.fhir.dstu3.model.ValueSet; +import org.junit.AfterClass; +import org.junit.Before; +import org.junit.Test; + +import ca.uhn.fhir.context.FhirContext; +import ca.uhn.fhir.util.TestUtil; + +public class QuestionnaireResponseValidatorR4Test { + private static DefaultProfileValidationSupport myDefaultValidationSupport = new DefaultProfileValidationSupport(); + + private static FhirContext ourCtx = FhirContext.forDstu3(); + private static final org.slf4j.Logger ourLog = org.slf4j.LoggerFactory.getLogger(QuestionnaireResponseValidatorR4Test.class); + + private FhirInstanceValidator myInstanceVal; + private FhirValidator myVal; + private IValidationSupport myValSupport; + + private IWorkerContext myWorkerCtx; + + @Before + public void before() { + myValSupport = mock(IValidationSupport.class); + // new DefaultProfileValidationSupport(); + myWorkerCtx = new HapiWorkerContext(ourCtx, myValSupport); + + myVal = ourCtx.newValidator(); + myVal.setValidateAgainstStandardSchema(false); + myVal.setValidateAgainstStandardSchematron(false); + + ValidationSupportChain validationSupport = new ValidationSupportChain(myValSupport, myDefaultValidationSupport); + myInstanceVal = new FhirInstanceValidator(validationSupport); + + myVal.registerValidatorModule(myInstanceVal); + + } + private ValidationResult stripBindingHasNoSourceMessage(ValidationResult theErrors) { + List<SingleValidationMessage> messages = new ArrayList<SingleValidationMessage>(theErrors.getMessages()); + for (int i = 0; i < messages.size(); i++) { + if (messages.get(i).getMessage().contains("has no source, so can't")) { + messages.remove(i); + i--; + } + } + + return new ValidationResult(ourCtx, messages); + } + @Test + public void testAnswerWithWrongType() { + Questionnaire q = new Questionnaire(); + q.addItem().setLinkId("link0").setRequired(true).setType(QuestionnaireItemType.BOOLEAN); + + QuestionnaireResponse qa = new QuestionnaireResponse(); + qa.setStatus(QuestionnaireResponseStatus.COMPLETED); + qa.getQuestionnaire().setReference("http://example.com/Questionnaire/q1"); + qa.addItem().setLinkId("link0").addAnswer().setValue(new StringType("FOO")); + + when(myValSupport.fetchResource(any(FhirContext.class), eq(Questionnaire.class), eq(qa.getQuestionnaire().getReference()))).thenReturn(q); + + ValidationResult errors = myVal.validateWithResult(qa); + + ourLog.info(errors.toString()); + assertThat(errors.toString(), containsString("Answer value must be of type boolean")); + } + + @Test + public void testCodedAnswer() { + String questionnaireRef = "http://example.com/Questionnaire/q1"; + + Questionnaire q = new Questionnaire(); + q.addItem().setLinkId("link0").setRequired(false).setType(QuestionnaireItemType.CHOICE).setOptions(new Reference("http://somevalueset")); + when(myValSupport.fetchResource(any(FhirContext.class), eq(Questionnaire.class), eq("http://example.com/Questionnaire/q1"))).thenReturn(q); + + CodeSystem codeSystem = new CodeSystem(); + codeSystem.setContent(CodeSystemContentMode.COMPLETE); + codeSystem.setUrl("http://codesystems.com/system"); + codeSystem.addConcept().setCode("code0"); + when(myValSupport.fetchCodeSystem(any(FhirContext.class), eq("http://codesystems.com/system"))).thenReturn(codeSystem); + + CodeSystem codeSystem2 = new CodeSystem(); + codeSystem2.setContent(CodeSystemContentMode.COMPLETE); + codeSystem2.setUrl("http://codesystems.com/system2"); + codeSystem2.addConcept().setCode("code2"); + when(myValSupport.fetchCodeSystem(any(FhirContext.class), eq("http://codesystems.com/system2"))).thenReturn(codeSystem2); + + ValueSet options = new ValueSet(); + options.getCompose().addInclude().setSystem("http://codesystems.com/system").addConcept().setCode("code0"); + options.getCompose().addInclude().setSystem("http://codesystems.com/system2").addConcept().setCode("code2"); + when(myValSupport.fetchResource(any(FhirContext.class), eq(ValueSet.class), eq("http://somevalueset"))).thenReturn(options); + + QuestionnaireResponse qa; + ValidationResult errors; + + // Good code + + qa = new QuestionnaireResponse(); + qa.setStatus(QuestionnaireResponseStatus.COMPLETED); + qa.getQuestionnaire().setReference(questionnaireRef); + qa.addItem().setLinkId("link0").addAnswer().setValue(new Coding().setSystem("http://codesystems.com/system").setCode("code0")); + errors = myVal.validateWithResult(qa); + errors = stripBindingHasNoSourceMessage(errors); + assertEquals(errors.toString(), 0, errors.getMessages().size()); + + // Bad code + + qa = new QuestionnaireResponse(); + qa.setStatus(QuestionnaireResponseStatus.COMPLETED); + qa.getQuestionnaire().setReference(questionnaireRef); + qa.addItem().setLinkId("link0").addAnswer().setValue(new Coding().setSystem("http://codesystems.com/system").setCode("code1")); + errors = myVal.validateWithResult(qa); + errors = stripBindingHasNoSourceMessage(errors); + ourLog.info(errors.toString()); + assertThat(errors.toString(), containsString("The value provided (http://codesystems.com/system::code1) is not in the options value set in the questionnaire")); + assertThat(errors.toString(), containsString("QuestionnaireResponse.item.answer")); + + qa = new QuestionnaireResponse(); + qa.setStatus(QuestionnaireResponseStatus.COMPLETED); + qa.getQuestionnaire().setReference(questionnaireRef); + qa.addItem().setLinkId("link0").addAnswer().setValue(new Coding().setSystem("http://codesystems.com/system2").setCode("code3")); + errors = myVal.validateWithResult(qa); + errors = stripBindingHasNoSourceMessage(errors); + ourLog.info(errors.toString()); + assertThat(errors.toString(), containsString("The value provided (http://codesystems.com/system2::code3) is not in the options value set in the questionnaire")); + assertThat(errors.toString(), containsString("QuestionnaireResponse.item.answer")); + + } + + @Test + public void testGroupWithNoLinkIdInQuestionnaireResponse() { + Questionnaire q = new Questionnaire(); + QuestionnaireItemComponent qGroup = q.addItem().setType(QuestionnaireItemType.GROUP); + qGroup.addItem().setLinkId("link0").setRequired(true).setType(QuestionnaireItemType.BOOLEAN); + + QuestionnaireResponse qa = new QuestionnaireResponse(); + qa.setStatus(QuestionnaireResponseStatus.COMPLETED); + qa.getQuestionnaire().setReference("http://example.com/Questionnaire/q1"); + QuestionnaireResponseItemComponent qaGroup = qa.addItem(); + qaGroup.addItem().setLinkId("link0").addAnswer().setValue(new StringType("FOO")); + + when(myValSupport.fetchResource(any(FhirContext.class), eq(Questionnaire.class), eq(qa.getQuestionnaire().getReference()))).thenReturn(q); + ValidationResult errors = myVal.validateWithResult(qa); + + ourLog.info(errors.toString()); + assertThat(errors.toString(), containsString("minimum required = 1, but only found 0 - QuestionnaireResponse.item")); + } + + @Test + public void testItemWithNoType() { + Questionnaire q = new Questionnaire(); + QuestionnaireItemComponent qGroup = q.addItem(); + qGroup.setLinkId("link0"); + qGroup.addItem().setLinkId("link1").setType(QuestionnaireItemType.STRING); + + QuestionnaireResponse qa = new QuestionnaireResponse(); + qa.setStatus(QuestionnaireResponseStatus.COMPLETED); + qa.getQuestionnaire().setReference("http://example.com/Questionnaire/q1"); + QuestionnaireResponseItemComponent qaGroup = qa.addItem().setLinkId("link0"); + qaGroup.addItem().setLinkId("link1").addAnswer().setValue(new StringType("FOO")); + + when(myValSupport.fetchResource(any(FhirContext.class), eq(Questionnaire.class), eq(qa.getQuestionnaire().getReference()))).thenReturn(q); + ValidationResult errors = myVal.validateWithResult(qa); + + ourLog.info(errors.toString()); + assertThat(errors.toString(), containsString("Definition for item link0 does not contain a type")); + assertEquals(1, errors.getMessages().size()); + } + + @Test + public void testMissingRequiredQuestion() { + + Questionnaire q = new Questionnaire(); + q.addItem().setLinkId("link0").setRequired(true).setType(QuestionnaireItemType.STRING); + q.addItem().setLinkId("link1").setRequired(true).setType(QuestionnaireItemType.STRING); + + QuestionnaireResponse qa = new QuestionnaireResponse(); + qa.setStatus(QuestionnaireResponseStatus.COMPLETED); + qa.getQuestionnaire().setReference("http://example.com/Questionnaire/q1"); + qa.addItem().setLinkId("link1").addAnswer().setValue(new StringType("FOO")); + + String reference = qa.getQuestionnaire().getReference(); + when(myValSupport.fetchResource(any(FhirContext.class), eq(Questionnaire.class), eq(reference))).thenReturn(q); + ValidationResult errors = myVal.validateWithResult(qa); + + ourLog.info(errors.toString()); + assertThat(errors.toString(), containsString("No response found for required item link0")); + } + + @Test + public void testOpenchoiceAnswer() { + String questionnaireRef = "http://example.com/Questionnaire/q1"; + + Questionnaire q = new Questionnaire(); + QuestionnaireItemComponent item = q.addItem(); + item.setLinkId("link0").setRequired(true).setType(QuestionnaireItemType.OPENCHOICE).setOptions(new Reference("http://somevalueset")); + when(myValSupport.fetchResource(any(FhirContext.class), eq(Questionnaire.class), eq(questionnaireRef))).thenReturn(q); + + CodeSystem codeSystem = new CodeSystem(); + codeSystem.setContent(CodeSystemContentMode.COMPLETE); + codeSystem.setUrl("http://codesystems.com/system"); + codeSystem.addConcept().setCode("code0"); + when(myValSupport.fetchCodeSystem(any(FhirContext.class), eq("http://codesystems.com/system"))).thenReturn(codeSystem); + + CodeSystem codeSystem2 = new CodeSystem(); + codeSystem2.setContent(CodeSystemContentMode.COMPLETE); + codeSystem2.setUrl("http://codesystems.com/system2"); + codeSystem2.addConcept().setCode("code2"); + when(myValSupport.fetchCodeSystem(any(FhirContext.class), eq("http://codesystems.com/system2"))).thenReturn(codeSystem2); + + ValueSet options = new ValueSet(); + options.getCompose().addInclude().setSystem("http://codesystems.com/system").addConcept().setCode("code0"); + options.getCompose().addInclude().setSystem("http://codesystems.com/system2").addConcept().setCode("code2"); + when(myValSupport.fetchResource(any(FhirContext.class), eq(ValueSet.class), eq("http://somevalueset"))).thenReturn(options); + + when(myValSupport.validateCode(any(FhirContext.class), eq("http://codesystems.com/system"), eq("code0"), any(String.class))).thenReturn(new CodeValidationResult(new ConceptDefinitionComponent(new CodeType("code0")))); + + QuestionnaireResponse qa; + ValidationResult errors; + + // Good code + + qa = new QuestionnaireResponse(); + qa.setStatus(QuestionnaireResponseStatus.COMPLETED); + qa.getQuestionnaire().setReference(questionnaireRef); + qa.addItem().setLinkId("link0").addAnswer().setValue(new Coding().setSystem("http://codesystems.com/system").setCode("code0")); + errors = myVal.validateWithResult(qa); + errors = stripBindingHasNoSourceMessage(errors); + assertEquals(errors.toString(), 0, errors.getMessages().size()); + + // Bad code + + qa = new QuestionnaireResponse(); + qa.setStatus(QuestionnaireResponseStatus.COMPLETED); + qa.getQuestionnaire().setReference(questionnaireRef); + qa.addItem().setLinkId("link0").addAnswer().setValue(new Coding().setSystem("http://codesystems.com/system").setCode("code1")); + errors = myVal.validateWithResult(qa); + errors = stripBindingHasNoSourceMessage(errors); + ourLog.info(errors.toString()); + assertThat(errors.toString(), containsString("The value provided (http://codesystems.com/system::code1) is not in the options value set in the questionnaire")); + assertThat(errors.toString(), containsString("QuestionnaireResponse.item.answer")); + + // Partial code + + qa = new QuestionnaireResponse(); + qa.setStatus(QuestionnaireResponseStatus.COMPLETED); + qa.getQuestionnaire().setReference(questionnaireRef); + qa.addItem().setLinkId("link0").addAnswer().setValue(new Coding().setSystem(null).setCode("code1")); + errors = myVal.validateWithResult(qa); + errors = stripBindingHasNoSourceMessage(errors); + ourLog.info(errors.toString()); + assertThat(errors.toString(), containsString("The value provided (null::code1) is not in the options value set in the questionnaire")); + assertThat(errors.toString(), containsString("QuestionnaireResponse.item.answer")); + + qa = new QuestionnaireResponse(); + qa.setStatus(QuestionnaireResponseStatus.COMPLETED); + qa.getQuestionnaire().setReference(questionnaireRef); + qa.addItem().setLinkId("link0").addAnswer().setValue(new Coding().setSystem("").setCode("code1")); + errors = myVal.validateWithResult(qa); + errors = stripBindingHasNoSourceMessage(errors); + ourLog.info(errors.toString()); + assertThat(errors.toString(), containsString("The value provided (null::code1) is not in the options value set in the questionnaire")); + assertThat(errors.toString(), containsString("QuestionnaireResponse.item.answer")); + + qa = new QuestionnaireResponse(); + qa.setStatus(QuestionnaireResponseStatus.COMPLETED); + qa.getQuestionnaire().setReference(questionnaireRef); + qa.addItem().setLinkId("link0").addAnswer().setValue(new Coding().setSystem("http://system").setCode(null)); + errors = myVal.validateWithResult(qa); + ourLog.info(errors.toString()); + assertThat(errors.toString(), containsString("The value provided (http://system::null) is not in the options value set in the questionnaire")); + assertThat(errors.toString(), containsString("QuestionnaireResponse.item.answer")); + + // Wrong type + + qa = new QuestionnaireResponse(); + qa.setStatus(QuestionnaireResponseStatus.COMPLETED); + qa.getQuestionnaire().setReference(questionnaireRef); + qa.addItem().setLinkId("link0").addAnswer().setValue(new IntegerType(123)); + errors = myVal.validateWithResult(qa); + ourLog.info(errors.toString()); + assertThat(errors.toString(), containsString("Cannot validate integer answer option because no option list is provided")); + assertThat(errors.toString(), containsString("QuestionnaireResponse.item.answer")); + + // String answer + + qa = new QuestionnaireResponse(); + qa.setStatus(QuestionnaireResponseStatus.COMPLETED); + qa.getQuestionnaire().setReference(questionnaireRef); + qa.addItem().setLinkId("link0").addAnswer().setValue(new Coding().setDisplay("Hello")); + errors = myVal.validateWithResult(qa); + ourLog.info(errors.toString()); + assertThat(errors.getMessages(), empty()); + + // Missing String answer + + qa = new QuestionnaireResponse(); + qa.setStatus(QuestionnaireResponseStatus.COMPLETED); + qa.getQuestionnaire().setReference(questionnaireRef); + qa.addItem().setLinkId("link0").addAnswer().setValue(new Coding().setDisplay("")); + errors = myVal.validateWithResult(qa); + ourLog.info(errors.toString()); + assertThat(errors.toString(), containsString("No response answer found for required item link0")); + assertThat(errors.toString(), containsString("QuestionnaireResponse.item")); + + } + + @Test + public void testUnexpectedAnswer() { + Questionnaire q = new Questionnaire(); + q.addItem().setLinkId("link0").setRequired(false).setType(QuestionnaireItemType.BOOLEAN); + + QuestionnaireResponse qa = new QuestionnaireResponse(); + qa.setStatus(QuestionnaireResponseStatus.COMPLETED); + qa.getQuestionnaire().setReference("http://example.com/Questionnaire/q1"); + qa.addItem().setLinkId("link1").addAnswer().setValue(new StringType("FOO")); + + when(myValSupport.fetchResource(any(FhirContext.class), eq(Questionnaire.class), eq(qa.getQuestionnaire().getReference()))).thenReturn(q); + ValidationResult errors = myVal.validateWithResult(qa); + + ourLog.info(errors.toString()); + assertThat(errors.toString(), containsString(" - QuestionnaireResponse")); + assertThat(errors.toString(), containsString("LinkId \"link1\" not found in questionnaire")); + } + + @Test + public void testUnexpectedGroup() { + Questionnaire q = new Questionnaire(); + q.addItem().setLinkId("link0").setRequired(false).setType(QuestionnaireItemType.BOOLEAN); + + QuestionnaireResponse qa = new QuestionnaireResponse(); + qa.setStatus(QuestionnaireResponseStatus.COMPLETED); + qa.getQuestionnaire().setReference("http://example.com/Questionnaire/q1"); + qa.addItem().setLinkId("link1").addItem().setLinkId("link2"); + + when(myValSupport.fetchResource(any(FhirContext.class), eq(Questionnaire.class), eq(qa.getQuestionnaire().getReference()))).thenReturn(q); + ValidationResult errors = myVal.validateWithResult(qa); + + ourLog.info(errors.toString()); + assertThat(errors.toString(), containsString(" - QuestionnaireResponse")); + assertThat(errors.toString(), containsString("LinkId \"link1\" not found in questionnaire")); + } + + // @Test + public void validateHealthConnexExample() throws Exception { + String input = IOUtils.toString(QuestionnaireResponseValidatorR4Test.class.getResourceAsStream("/questionnaireanswers-0f431c50ddbe4fff8e0dd6b7323625fc.xml")); + + QuestionnaireResponse qa = ourCtx.newXmlParser().parseResource(QuestionnaireResponse.class, input); + ValidationResult errors = myVal.validateWithResult(qa); + assertEquals(errors.toString(), 0, errors.getMessages().size()); + + /* + * Now change a coded value + */ + //@formatter:off + input = input.replaceAll( + "<answer>\n" + + " <valueCoding>\n" + + " <system value=\"f69573b8-cb63-4d31-85a4-23ac784735ab\"/>\n" + + " <code value=\"2\"/>\n" + + " <display value=\"Once/twice\"/>\n" + + " </valueCoding>\n" + + " </answer>", + "<answer>\n" + + " <valueCoding>\n" + + " <system value=\"f69573b8-cb63-4d31-85a4-23ac784735ab\"/>\n" + + " <code value=\"GGG\"/>\n" + + " <display value=\"Once/twice\"/>\n" + + " </valueCoding>\n" + + " </answer>"); + assertThat(input, containsString("GGG")); + //@formatter:on + + qa = ourCtx.newXmlParser().parseResource(QuestionnaireResponse.class, input); + errors = myVal.validateWithResult(qa); + assertEquals(errors.toString(), 10, errors.getMessages().size()); + } + + @AfterClass + public static void afterClassClearContext() { + myDefaultValidationSupport.flush(); + myDefaultValidationSupport = null; + TestUtil.clearAllStaticFieldsForUnitTest(); + } + +} diff --git a/hapi-fhir-validation/src/test/java/ca/uhn/fhir/validation/SchemaValidationR4Test.java b/hapi-fhir-validation/src/test/java/ca/uhn/fhir/validation/SchemaValidationR4Test.java new file mode 100644 index 00000000000..5df6a832cc7 --- /dev/null +++ b/hapi-fhir-validation/src/test/java/ca/uhn/fhir/validation/SchemaValidationR4Test.java @@ -0,0 +1,60 @@ +package ca.uhn.fhir.validation; + +import static org.hamcrest.Matchers.containsString; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertThat; + +import org.junit.AfterClass; +import org.junit.Test; + +import ca.uhn.fhir.context.FhirContext; +import ca.uhn.fhir.util.TestUtil; + +public class SchemaValidationR4Test { + + private static FhirContext ourCtx = FhirContext.forDstu3(); + + private static final org.slf4j.Logger ourLog = org.slf4j.LoggerFactory.getLogger(SchemaValidationR4Test.class); + + /** + * See #339 + * + * https://www.owasp.org/index.php/XML_External_Entity_(XXE)_Processing + */ + @Test + public void testXxe() { + //@formatter:off + String input = + "<?xml version=\"1.0\" encoding=\"ISO-8859-1\"?>\n" + + "<!DOCTYPE foo [ \n" + + "<!ELEMENT foo ANY >\n" + + "<!ENTITY xxe SYSTEM \"file:///etc/passwd\" >]>" + + "<Patient xmlns=\"http://hl7.org/fhir\">" + + "<text>" + + "<status value=\"generated\"/>" + + "<div xmlns=\"http://www.w3.org/1999/xhtml\">TEXT &xxe; TEXT</div>\n" + + "</text>" + + "<address>" + + "<line value=\"FOO\"/>" + + "</address>" + + "</Patient>"; + //@formatter:on + + FhirValidator val = ourCtx.newValidator(); + val.setValidateAgainstStandardSchema(true); + val.setValidateAgainstStandardSchematron(false); + ValidationResult result = val.validateWithResult(input); + + String encoded = ourCtx.newXmlParser().setPrettyPrint(true).encodeResourceToString(result.toOperationOutcome()); + ourLog.info(encoded); + + assertFalse(result.isSuccessful()); + assertThat(encoded, containsString("passwd")); + assertThat(encoded, containsString("accessExternalDTD")); + } + + @AfterClass + public static void afterClassClearContext() { + TestUtil.clearAllStaticFieldsForUnitTest(); + } +} diff --git a/src/changes/changes.xml b/src/changes/changes.xml index bed3dd234fd..ccea8c30f05 100644 --- a/src/changes/changes.xml +++ b/src/changes/changes.xml @@ -22,7 +22,7 @@ Schematron validator now applies invariants to resources within a Bundle, not just to the outer Bundle resource itself </action> - </release + </release> <release version="2.6" date="TBD"> <action type="add"> <ul> @@ -240,6 +240,11 @@ Fix an unfortunate typo in the custom structures documentation. Thanks to Jason Owen for the PR! </action> + <action type="fix" issue="686"> + Correct an issue in the validator (DSTU3/R4) where elements were not always + correctly validated if the element contained only a profiled extension. Thanks + to Sébastien Rivière for the pull request! + </action> </release> <release version="2.5" date="2017-06-08"> <action type="fix">