Credit for #686 and forward port the fix to R4 validator
This commit is contained in:
parent
96543c3992
commit
e326a7b0cd
|
@ -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));
|
||||||
|
|
|
@ -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);
|
||||||
|
}
|
||||||
|
}
|
|
@ -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>
|
||||||
|
|
|
@ -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;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -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();
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -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();
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -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();
|
||||||
|
}
|
||||||
|
}
|
|
@ -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">
|
||||||
|
|
Loading…
Reference in New Issue