Credit for 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
hapi-fhir-structures-r4/src
main/java/org/hl7/fhir/r4/elementmodel
test
java/org/hl7/fhir/r4/elementmodel
resource
hapi-fhir-validation/src
src/changes

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.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<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) {
// if (canBePrimitive!= null)
// return canBePrimitive;
@ -203,7 +218,7 @@ public class Property {
ElementDefinition ed = definition;
StructureDefinition sd = structure;
List<ElementDefinition> 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));

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 {
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;
}
}
}

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
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">