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.TypeDetails;
|
||||
import org.hl7.fhir.r4.utils.ToolingExtensions;
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import org.hl7.fhir.exceptions.DefinitionException;
|
||||
|
||||
public class Property {
|
||||
|
@ -163,6 +164,20 @@ public class Property {
|
|||
return FormatUtilities.FHIR_NS;
|
||||
}
|
||||
|
||||
private boolean isElementWithOnlyExtension(final ElementDefinition ed, final List<ElementDefinition> children) {
|
||||
boolean result = false;
|
||||
if (!ed.getType().isEmpty()) {
|
||||
result = true;
|
||||
for (final ElementDefinition ele : children) {
|
||||
if (!ele.getPath().contains("extension")) {
|
||||
result = false;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
public boolean IsLogicalAndHasPrimitiveValue(String name) {
|
||||
// if (canBePrimitive!= null)
|
||||
// return canBePrimitive;
|
||||
|
@ -203,7 +218,7 @@ public class Property {
|
|||
ElementDefinition ed = definition;
|
||||
StructureDefinition sd = structure;
|
||||
List<ElementDefinition> children = ProfileUtilities.getChildMap(sd, ed);
|
||||
if (children.isEmpty()) {
|
||||
if (children.isEmpty() || isElementWithOnlyExtension(ed, children)) {
|
||||
// ok, find the right definitions
|
||||
String t = null;
|
||||
if (ed.getType().size() == 1)
|
||||
|
@ -240,7 +255,13 @@ public class Property {
|
|||
}
|
||||
}
|
||||
if (!"xhtml".equals(t)) {
|
||||
sd = context.fetchResource(StructureDefinition.class, "http://hl7.org/fhir/StructureDefinition/"+t);
|
||||
final String url;
|
||||
if (StringUtils.isNotBlank(ed.getType().get(0).getProfile())) {
|
||||
url = ed.getType().get(0).getProfile();
|
||||
} else {
|
||||
url = "http://hl7.org/fhir/StructureDefinition/" + t;
|
||||
}
|
||||
sd = context.fetchResource(StructureDefinition.class, url);
|
||||
if (sd == null)
|
||||
throw new DefinitionException("Unable to find type '"+t+"' for name '"+elementName+"' on property "+definition.getPath());
|
||||
children = ProfileUtilities.getChildMap(sd, sd.getSnapshot().getElement().get(0));
|
||||
|
|
|
@ -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>
|
||||
|
|
@ -37,7 +37,9 @@ public class FhirInstanceValidator extends BaseValidatorBridge implements IValid
|
|||
private boolean myAnyExtensionsAllowed = true;
|
||||
private BestPracticeWarningLevel myBestPracticeWarningLevel;
|
||||
private DocumentBuilderFactory myDocBuilderFactory;
|
||||
private boolean myNoTerminologyChecks;
|
||||
private StructureDefinition myStructureDefintion;
|
||||
|
||||
private IValidationSupport myValidationSupport;
|
||||
|
||||
/**
|
||||
|
@ -113,6 +115,13 @@ public class FhirInstanceValidator extends BaseValidatorBridge implements IValid
|
|||
return myAnyExtensionsAllowed;
|
||||
}
|
||||
|
||||
/**
|
||||
* If set to {@literal true} (default is false) the valueSet will not be validate
|
||||
*/
|
||||
public boolean isNoTerminologyChecks() {
|
||||
return myNoTerminologyChecks;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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
|
||||
|
@ -140,6 +149,13 @@ public class FhirInstanceValidator extends BaseValidatorBridge implements IValid
|
|||
myBestPracticeWarningLevel = theBestPracticeWarningLevel;
|
||||
}
|
||||
|
||||
/**
|
||||
* If set to {@literal true} (default is false) the valueSet will not be validate
|
||||
*/
|
||||
public void setNoTerminologyChecks(final boolean theNoTerminologyChecks) {
|
||||
myNoTerminologyChecks = theNoTerminologyChecks;
|
||||
}
|
||||
|
||||
public void setStructureDefintion(StructureDefinition theStructureDefintion) {
|
||||
myStructureDefintion = theStructureDefintion;
|
||||
}
|
||||
|
@ -166,6 +182,7 @@ public class FhirInstanceValidator extends BaseValidatorBridge implements IValid
|
|||
v.setBestPracticeWarningLevel(getBestPracticeWarningLevel());
|
||||
v.setAnyExtensionsAllowed(isAnyExtensionsAllowed());
|
||||
v.setResourceIdRule(IdStatus.OPTIONAL);
|
||||
v.setNoTerminologyChecks(isNoTerminologyChecks());
|
||||
|
||||
List<ValidationMessage> messages = new ArrayList<ValidationMessage>();
|
||||
|
||||
|
|
|
@ -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
|
||||
just to the outer Bundle resource itself
|
||||
</action>
|
||||
</release
|
||||
</release>
|
||||
<release version="2.6" date="TBD">
|
||||
<action type="add">
|
||||
<ul>
|
||||
|
@ -240,6 +240,11 @@
|
|||
Fix an unfortunate typo in the custom structures documentation. Thanks to
|
||||
Jason Owen for the PR!
|
||||
</action>
|
||||
<action type="fix" issue="686">
|
||||
Correct an issue in the validator (DSTU3/R4) where elements were not always
|
||||
correctly validated if the element contained only a profiled extension. Thanks
|
||||
to Sébastien Rivière for the pull request!
|
||||
</action>
|
||||
</release>
|
||||
<release version="2.5" date="2017-06-08">
|
||||
<action type="fix">
|
||||
|
|
Loading…
Reference in New Issue