Credit for #686 and forward port the fix to R4 validator

This commit is contained in:
James Agnew 2017-07-31 17:36:38 -04:00
parent 96543c3992
commit e326a7b0cd
8 changed files with 2082 additions and 202 deletions

View File

@ -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.StructureDefinition.StructureDefinitionKind;
import org.hl7.fhir.r4.model.TypeDetails; import org.hl7.fhir.r4.model.TypeDetails;
import org.hl7.fhir.r4.utils.ToolingExtensions; import org.hl7.fhir.r4.utils.ToolingExtensions;
import org.apache.commons.lang3.StringUtils;
import org.hl7.fhir.exceptions.DefinitionException; import org.hl7.fhir.exceptions.DefinitionException;
public class Property { public class Property {
@ -163,6 +164,20 @@ public class Property {
return FormatUtilities.FHIR_NS; return FormatUtilities.FHIR_NS;
} }
private boolean isElementWithOnlyExtension(final ElementDefinition ed, final List<ElementDefinition> 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) { public boolean IsLogicalAndHasPrimitiveValue(String name) {
// if (canBePrimitive!= null) // if (canBePrimitive!= null)
// return canBePrimitive; // return canBePrimitive;
@ -203,7 +218,7 @@ public class Property {
ElementDefinition ed = definition; ElementDefinition ed = definition;
StructureDefinition sd = structure; StructureDefinition sd = structure;
List<ElementDefinition> children = ProfileUtilities.getChildMap(sd, ed); List<ElementDefinition> children = ProfileUtilities.getChildMap(sd, ed);
if (children.isEmpty()) { if (children.isEmpty() || isElementWithOnlyExtension(ed, children)) {
// ok, find the right definitions // ok, find the right definitions
String t = null; String t = null;
if (ed.getType().size() == 1) if (ed.getType().size() == 1)
@ -240,7 +255,13 @@ public class Property {
} }
} }
if (!"xhtml".equals(t)) { 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) if (sd == null)
throw new DefinitionException("Unable to find type '"+t+"' for name '"+elementName+"' on property "+definition.getPath()); throw new DefinitionException("Unable to find type '"+t+"' for name '"+elementName+"' on property "+definition.getPath());
children = ProfileUtilities.getChildMap(sd, sd.getSnapshot().getElement().get(0)); children = ProfileUtilities.getChildMap(sd, sd.getSnapshot().getElement().get(0));

View File

@ -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<Property> 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<Property> 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);
}
}

View File

@ -0,0 +1,447 @@
<StructureDefinition xmlns="http://hl7.org/fhir">
<id value="Patient"/>
<meta>
<lastUpdated value="2017-07-14T08:37:31.190+02:00"/>
</meta>
<contained>
<ValueSet xmlns="http://hl7.org/fhir">
<id value="1"/>
</ValueSet>
</contained>
<contained>
<ValueSet xmlns="http://hl7.org/fhir">
<id value="2"/>
</ValueSet>
</contained>
<contained>
<ValueSet xmlns="http://hl7.org/fhir">
<id value="3"/>
</ValueSet>
</contained>
<contained>
<ValueSet xmlns="http://hl7.org/fhir">
<id value="4"/>
</ValueSet>
</contained>
<contained>
<ValueSet xmlns="http://hl7.org/fhir">
<id value="5"/>
</ValueSet>
</contained>
<url value="http://www.myServer.com/fhir/StructureDefinition/Patient"/>
<version value="1.0"/>
<name value="Custom Patient"/>
<title value="Patient"/>
<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>

View File

@ -32,235 +32,252 @@ import ca.uhn.fhir.validation.IValidatorModule;
public class FhirInstanceValidator extends BaseValidatorBridge implements 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 boolean myAnyExtensionsAllowed = true;
private BestPracticeWarningLevel myBestPracticeWarningLevel; private BestPracticeWarningLevel myBestPracticeWarningLevel;
private DocumentBuilderFactory myDocBuilderFactory; private DocumentBuilderFactory myDocBuilderFactory;
private StructureDefinition myStructureDefintion; private boolean myNoTerminologyChecks;
private IValidationSupport myValidationSupport; private StructureDefinition myStructureDefintion;
/** private IValidationSupport myValidationSupport;
* Constructor
*
* Uses {@link DefaultProfileValidationSupport} for {@link IValidationSupport validation support}
*/
public FhirInstanceValidator() {
this(new DefaultProfileValidationSupport());
}
/** /**
* Constructor which uses the given validation support * Constructor
* *
* @param theValidationSupport * Uses {@link DefaultProfileValidationSupport} for {@link IValidationSupport validation support}
* The validation support */
*/ public FhirInstanceValidator() {
public FhirInstanceValidator(IValidationSupport theValidationSupport) { this(new DefaultProfileValidationSupport());
myDocBuilderFactory = DocumentBuilderFactory.newInstance(); }
myDocBuilderFactory.setNamespaceAware(true);
myValidationSupport = theValidationSupport;
}
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(); private String determineResourceName(Document theDocument) {
for (int i = 0; i < list.getLength(); i++) { Element root = null;
if (list.item(i) instanceof Element) {
root = (Element) list.item(i);
break;
}
}
root = theDocument.getDocumentElement();
return root.getLocalName();
}
private StructureDefinition findStructureDefinitionForResourceName(final FhirContext theCtx, String resourceName) { NodeList list = theDocument.getChildNodes();
String sdName = "http://hl7.org/fhir/StructureDefinition/" + resourceName; for (int i = 0; i < list.getLength(); i++) {
StructureDefinition profile = myStructureDefintion != null ? myStructureDefintion : myValidationSupport.fetchStructureDefinition(theCtx, sdName); if (list.item(i) instanceof Element) {
return profile; root = (Element) list.item(i);
} break;
}
}
root = theDocument.getDocumentElement();
return root.getLocalName();
}
/** private StructureDefinition findStructureDefinitionForResourceName(final FhirContext theCtx, String resourceName) {
* Returns the "best practice" warning level (default is {@link BestPracticeWarningLevel#Hint}). String sdName = "http://hl7.org/fhir/StructureDefinition/" + resourceName;
* <p> StructureDefinition profile = myStructureDefintion != null ? myStructureDefintion : myValidationSupport.fetchStructureDefinition(theCtx, sdName);
* The FHIR Instance Validator has a number of checks for best practices in terms of FHIR usage. If this setting is return profile;
* 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;
}
/** /**
* Returns the {@link IValidationSupport validation support} in use by this validator. Default is an instance of * Returns the "best practice" warning level (default is {@link BestPracticeWarningLevel#Hint}).
* {@link DefaultProfileValidationSupport} if the no-arguments constructor for this object was used. * <p>
*/ * The FHIR Instance Validator has a number of checks for best practices in terms of FHIR usage. If this setting is
public IValidationSupport getValidationSupport() { * set to {@link BestPracticeWarningLevel#Error}, any resource data which does not meet these best practices will be
return myValidationSupport; * 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 * Returns the {@link IValidationSupport validation support} in use by this validator. Default is an instance of
* validator (e.g. because they have not been explicitly declared in a profile) will * {@link DefaultProfileValidationSupport} if the no-arguments constructor for this object was used.
* be validated but will not cause an error. */
*/ public IValidationSupport getValidationSupport() {
public boolean isAnyExtensionsAllowed() { return myValidationSupport;
return myAnyExtensionsAllowed; }
}
/** /**
* If set to {@literal true} (default is true) extensions which are not known to the * 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 * validator (e.g. because they have not been explicitly declared in a profile) will
* be validated but will not cause an error. * be validated but will not cause an error.
*/ */
public void setAnyExtensionsAllowed(boolean theAnyExtensionsAllowed) { public boolean isAnyExtensionsAllowed() {
myAnyExtensionsAllowed = theAnyExtensionsAllowed; return myAnyExtensionsAllowed;
} }
/** /**
* Sets the "best practice warning level". When validating, any deviations from best practices will be reported at * If set to {@literal true} (default is false) the valueSet will not be validate
* this level. */
* <p> public boolean isNoTerminologyChecks() {
* The FHIR Instance Validator has a number of checks for best practices in terms of FHIR usage. If this setting is return myNoTerminologyChecks;
* 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;
}
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 * Sets the "best practice warning level". When validating, any deviations from best practices will be reported at
* {@link DefaultProfileValidationSupport} if the no-arguments constructor for this object was used. * this level.
*/ * <p>
public void setValidationSupport(IValidationSupport theValidationSupport) { * The FHIR Instance Validator has a number of checks for best practices in terms of FHIR usage. If this setting is
myValidationSupport = theValidationSupport; * 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; public void setStructureDefintion(StructureDefinition theStructureDefintion) {
IEvaluationContext evaluationCtx = new NullEvaluationContext(); myStructureDefintion = theStructureDefintion;
try { }
v = new InstanceValidator(workerContext, evaluationCtx);
} catch (Exception e) {
throw new ConfigurationException(e);
}
v.setBestPracticeWarningLevel(getBestPracticeWarningLevel()); /**
v.setAnyExtensionsAllowed(isAnyExtensionsAllowed()); * Sets the {@link IValidationSupport validation support} in use by this validator. Default is an instance of
v.setResourceIdRule(IdStatus.OPTIONAL); * {@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) { InstanceValidator v;
Document document; IEvaluationContext evaluationCtx = new NullEvaluationContext();
try { try {
DocumentBuilder builder = myDocBuilderFactory.newDocumentBuilder(); v = new InstanceValidator(workerContext, evaluationCtx);
InputSource src = new InputSource(new StringReader(theInput)); } catch (Exception e) {
document = builder.parse(src); throw new ConfigurationException(e);
} 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);
}
String resourceName = determineResourceName(document); v.setBestPracticeWarningLevel(getBestPracticeWarningLevel());
StructureDefinition profile = findStructureDefinitionForResourceName(theCtx, resourceName); v.setAnyExtensionsAllowed(isAnyExtensionsAllowed());
if (profile != null) { v.setResourceIdRule(IdStatus.OPTIONAL);
try { v.setNoTerminologyChecks(isNoTerminologyChecks());
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);
String resourceName = json.get("resourceType").getAsString(); List<ValidationMessage> messages = new ArrayList<ValidationMessage>();
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);
}
for (int i = 0; i < messages.size(); i++) { if (theEncoding == EncodingEnum.XML) {
ValidationMessage next = messages.get(i); Document document;
if ("Binding has no source, so can't be checked".equals(next.getMessage())) { try {
messages.remove(i); DocumentBuilder builder = myDocBuilderFactory.newDocumentBuilder();
i--; InputSource src = new InputSource(new StringReader(theInput));
} document = builder.parse(src);
} } catch (Exception e2) {
return messages; 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 String resourceName = determineResourceName(document);
protected List<ValidationMessage> validate(IValidationContext<?> theCtx) { StructureDefinition profile = findStructureDefinitionForResourceName(theCtx, resourceName);
return validate(theCtx.getFhirContext(), theCtx.getResourceAsString(), theCtx.getResourceAsStringEncoding()); 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 for (int i = 0; i < messages.size(); i++) {
public TypeDetails checkFunction(Object theAppContext, String theFunctionName, List<TypeDetails> theParameters) throws PathEngineException { ValidationMessage next = messages.get(i);
return null; if ("Binding has no source, so can't be checked".equals(next.getMessage())) {
} messages.remove(i);
i--;
}
}
return messages;
}
@Override @Override
public List<Base> executeFunction(Object theAppContext, String theFunctionName, List<List<Base>> theParameters) { protected List<ValidationMessage> validate(IValidationContext<?> theCtx) {
return null; return validate(theCtx.getFhirContext(), theCtx.getResourceAsString(), theCtx.getResourceAsStringEncoding());
} }
@Override public class NullEvaluationContext implements IEvaluationContext {
public boolean log(String theArgument, List<Base> theFocus) {
return false;
}
@Override @Override
public Base resolveConstant(Object theAppContext, String theName) throws PathEngineException { public TypeDetails checkFunction(Object theAppContext, String theFunctionName, List<TypeDetails> theParameters) throws PathEngineException {
return null; return null;
} }
@Override @Override
public TypeDetails resolveConstantType(Object theAppContext, String theName) throws PathEngineException { public List<Base> executeFunction(Object theAppContext, String theFunctionName, List<List<Base>> theParameters) {
return null; return null;
} }
@Override @Override
public FunctionDetails resolveFunction(String theFunctionName) { public boolean log(String theArgument, List<Base> theFocus) {
return null; return false;
} }
@Override @Override
public Base resolveReference(Object theAppContext, String theUrl) { public Base resolveConstant(Object theAppContext, String theName) throws PathEngineException {
return null; 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;
}
}
} }

View File

@ -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();
}
}

View File

@ -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();
}
}

View File

@ -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();
}
}

View File

@ -22,7 +22,7 @@
Schematron validator now applies invariants to resources within a Bundle, not Schematron validator now applies invariants to resources within a Bundle, not
just to the outer Bundle resource itself just to the outer Bundle resource itself
</action> </action>
</release </release>
<release version="2.6" date="TBD"> <release version="2.6" date="TBD">
<action type="add"> <action type="add">
<ul> <ul>
@ -240,6 +240,11 @@
Fix an unfortunate typo in the custom structures documentation. Thanks to Fix an unfortunate typo in the custom structures documentation. Thanks to
Jason Owen for the PR! Jason Owen for the PR!
</action> </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>
<release version="2.5" date="2017-06-08"> <release version="2.5" date="2017-06-08">
<action type="fix"> <action type="fix">