mirror of
https://github.com/hapifhir/org.hl7.fhir.core.git
synced 2025-02-07 13:28:12 +00:00
further modularise validator and add Measure Validation
This commit is contained in:
parent
95c2f99e7b
commit
ab581fdf86
@ -125,6 +125,9 @@ public class ObjectConverter {
|
||||
}
|
||||
|
||||
public static CodeableConcept readAsCodeableConcept(Element element) {
|
||||
if (element == null) {
|
||||
return null;
|
||||
}
|
||||
CodeableConcept cc = new CodeableConcept();
|
||||
List<Element> list = new ArrayList<Element>();
|
||||
element.getNamedChildren("coding", list);
|
||||
|
@ -342,6 +342,15 @@ public class CodeableConcept extends DataType implements ICompositeType {
|
||||
super();
|
||||
addCoding(code);
|
||||
}
|
||||
|
||||
public boolean matches(CodeableConcept other) {
|
||||
for (Coding c : other.getCoding()) {
|
||||
if (hasCoding(c.getSystem(), c.getCode())) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
@ -164,6 +164,7 @@ public class I18nConstants {
|
||||
public final static String TERMINOLOGY_TX_SYSTEM_INVALID = "Terminology_TX_System_Invalid";
|
||||
public final static String TERMINOLOGY_TX_SYSTEM_NOTKNOWN = "Terminology_TX_System_NotKnown";
|
||||
public final static String TERMINOLOGY_TX_SYSTEM_RELATIVE = "Terminology_TX_System_Relative";
|
||||
public final static String TERMINOLOGY_TX_SYSTEM_NO_CODE = "TERMINOLOGY_TX_SYSTEM_NO_CODE";
|
||||
public final static String TERMINOLOGY_TX_SYSTEM_UNKNOWN = "Terminology_TX_System_Unknown";
|
||||
public final static String TERMINOLOGY_TX_SYSTEM_VALUESET = "Terminology_TX_System_ValueSet";
|
||||
public final static String TERMINOLOGY_TX_SYSTEM_VALUESET2 = "Terminology_TX_System_ValueSet2";
|
||||
@ -437,5 +438,28 @@ public class I18nConstants {
|
||||
public final static String XML_ATTR_VALUE_INVALID = "xml_attr_value_invalid";
|
||||
public final static String XML_ENCODING_INVALID = "xml_encoding_invalid";
|
||||
public final static String XML_STATED_ENCODING_INVALID = "xml_stated_encoding_invalid";
|
||||
|
||||
}
|
||||
public static final String MEASURE_MR_GRP_NO_CODE = "MEASURE_MR_GRP_NO_CODE";
|
||||
public static final String MEASURE_MR_GRP_UNK_CODE = "MEASURE_MR_GRP_UNK_CODE";
|
||||
public static final String MEASURE_MR_GRP_DUPL_CODE = "MEASURE_MR_GRP_DUPL_CODE";
|
||||
public static final String MEASURE_MR_GRP_MISSING_BY_CODE = "MEASURE_MR_GRP_MISSING_BY_CODE";
|
||||
public static final String MEASURE_MR_GRP_POP_NO_CODE = "MEASURE_MR_GRP_POP_NO_CODE";
|
||||
public static final String MEASURE_MR_GRP_POP_UNK_CODE = "MEASURE_MR_GRP_POP_UNK_CODE";
|
||||
public static final String MEASURE_MR_GRP_POP_DUPL_CODE = "MEASURE_MR_GRP_POP_DUPL_CODE";
|
||||
public static final String MEASURE_MR_GRP_POP_MISSING_BY_CODE = "MEASURE_MR_GRP_POP_MISSING_BY_CODE";
|
||||
public static final String MEASURE_MR_GRP_NO_USABLE_CODE = "MEASURE_MR_GRP_NO_USABLE_CODE";
|
||||
public static final String MEASURE_MR_GRP_NO_WRONG_CODE = "MEASURE_MR_GRP_NO_WRONG_CODE";
|
||||
public static final String DUPLICATE_ID = "DUPLICATE_ID";
|
||||
public static final String MEASURE_MR_GRP_POP_COUNT_MISMATCH = "MEASURE_MR_GRP_POP_COUNT_MISMATCH";
|
||||
public static final String MEASURE_MR_GRP_POP_NO_SUBJECTS = "MEASURE_MR_GRP_POP_NO_SUBJECTS";
|
||||
public static final String MEASURE_MR_GRP_POP_NO_COUNT = "MEASURE_MR_GRP_POP_NO_COUNT";
|
||||
public static final String MEASURE_M_GROUP_CODE = "MEASURE_M_GROUP_CODE";
|
||||
public static final String MEASURE_M_GROUP_POP_NO_CODE = "MEASURE_M_GROUP_POP_NO_CODE";
|
||||
public static final String MEASURE_M_GROUP_STRATA_NO_CODE = "MEASURE_M_GROUP_STRATA_NO_CODE";
|
||||
public static final String MEASURE_M_GROUP_STRATA_COMP_NO_CODE = "MEASURE_M_GROUP_STRATA_COMP_NO_CODE";
|
||||
public static final String MEASURE_M_NO_GROUPS = "MEASURE_M_NO_GROUPS";
|
||||
public static final String MEASURE_M_GROUP_POP = "MEASURE_M_GROUP_POP";
|
||||
public static final String MEASURE_M_GROUP_STRATA = "MEASURE_M_GROUP_STRATA";
|
||||
public static final String MEASURE_M_LIB_UNKNOWN = "MEASURE_M_LIB_UNKNOWN";
|
||||
public static final String TYPE_SPECIFIC_CHECKS_CANONICAL_ABSOLUTE = "TYPE_SPECIFIC_CHECKS_CANONICAL_ABSOLUTE";
|
||||
|
||||
}
|
@ -436,4 +436,28 @@ documentmsg = (document)
|
||||
xml_attr_value_invalid = The XML Attribute {0} has an illegal character
|
||||
xml_encoding_invalid = The XML encoding is invalid (must be UTF-8)
|
||||
xml_stated_encoding_invalid = The XML encoding stated in the header is invalid (must be "UTF-8" if stated)
|
||||
XHTML_URL_INVALID = The URL {0} is not valid
|
||||
XHTML_URL_INVALID = The URL {0} is not valid
|
||||
MEASURE_MR_GRP_NO_CODE = Group should have a code that matches the group definition in the measure
|
||||
MEASURE_MR_GRP_UNK_CODE = The code for this group has no match in the measure definition
|
||||
MEASURE_MR_GRP_DUPL_CODE = The code for this group is duplicated with another group
|
||||
MEASURE_MR_GRP_MISSING_BY_CODE = The MeasureReport does not include a group for the group {0}
|
||||
MEASURE_MR_GRP_NO_USABLE_CODE = None of the codes provided are usable for comparison - need both system and code on at least one code
|
||||
MEASURE_MR_GRP_NO_WRONG_CODE = The code provided ({0}) does not match the code specified in the measure report ({1})
|
||||
DUPLICATE_ID = Duplicate id value ''{0}''
|
||||
TERMINOLOGY_TX_SYSTEM_NO_CODE = A code with no system has no defined meaning. A system should be provided
|
||||
MEASURE_MR_GRP_POP_NO_CODE = Group should have a code that matches the group population definition in the measure
|
||||
MEASURE_MR_GRP_POP_UNK_CODE = The code for this group population has no match in the measure definition
|
||||
MEASURE_MR_GRP_POP_DUPL_CODE = The code for this group population is duplicated with another group
|
||||
MEASURE_MR_GRP_POP_MISSING_BY_CODE = The MeasureReport does not include a population group for the population group {0}
|
||||
MEASURE_MR_GRP_POP_COUNT_MISMATCH = Mismatch between count {0} and number of subjects {1}
|
||||
MEASURE_MR_GRP_POP_NO_SUBJECTS = Reports where type is not ''subject-list'' don't have subjects listed
|
||||
MEASURE_MR_GRP_POP_NO_COUNT = Count should be present for reports where type is not ''subject-list''
|
||||
MEASURE_M_NO_GROUPS = A measure should contain at least one group
|
||||
MEASURE_M_GROUP_CODE = Groups should have codes when there is more than one group
|
||||
MEASURE_M_GROUP_POP = Measure Groups should have at least one population
|
||||
MEASURE_M_GROUP_STRATA = Measure Groups should have at least one stratifier
|
||||
MEASURE_M_GROUP_POP_NO_CODE = A measure group population should have a code when there is more than one population
|
||||
MEASURE_M_GROUP_STRATA_NO_CODE = A measure group stratifier should have a code when there is more than one population
|
||||
MEASURE_M_GROUP_STRATA_COMP_NO_CODE = A measure group stratifier component should have a code when there is more than one population
|
||||
MEASURE_M_LIB_UNKNOWN = The Library {0} could not be resolved, so expression validation may not be correct
|
||||
TYPE_SPECIFIC_CHECKS_CANONICAL_ABSOLUTE = Canonical URLs must be absolute URLs if they are not fragment references ({0})
|
@ -1,5 +1,7 @@
|
||||
package org.hl7.fhir.validation;
|
||||
|
||||
import static org.apache.commons.lang3.StringUtils.isBlank;
|
||||
|
||||
/*-
|
||||
* #%L
|
||||
* org.hl7.fhir.validation
|
||||
@ -52,6 +54,11 @@ POSSIBILITY OF SUCH DAMAGE.
|
||||
import java.util.List;
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import org.hl7.fhir.r5.context.IWorkerContext;
|
||||
import org.hl7.fhir.r5.model.DomainResource;
|
||||
import org.hl7.fhir.r5.model.Resource;
|
||||
import org.hl7.fhir.r5.model.ValueSet;
|
||||
import org.hl7.fhir.r5.terminologies.ValueSetUtilities;
|
||||
import org.hl7.fhir.utilities.Utilities;
|
||||
import org.hl7.fhir.utilities.validation.ValidationMessage;
|
||||
import org.hl7.fhir.utilities.validation.ValidationMessage.IssueSeverity;
|
||||
import org.hl7.fhir.utilities.validation.ValidationMessage.IssueType;
|
||||
@ -61,6 +68,7 @@ public class BaseValidator {
|
||||
|
||||
protected Source source;
|
||||
protected IWorkerContext context;
|
||||
protected TimeTracker timeTracker = new TimeTracker();
|
||||
|
||||
|
||||
public BaseValidator(IWorkerContext context){
|
||||
@ -545,4 +553,58 @@ public class BaseValidator {
|
||||
}
|
||||
return thePass;
|
||||
}
|
||||
|
||||
|
||||
protected ValueSet resolveBindingReference(DomainResource ctxt, String reference, String uri) {
|
||||
if (reference != null) {
|
||||
if (reference.startsWith("#")) {
|
||||
for (Resource c : ctxt.getContained()) {
|
||||
if (c.getId().equals(reference.substring(1)) && (c instanceof ValueSet))
|
||||
return (ValueSet) c;
|
||||
}
|
||||
return null;
|
||||
} else {
|
||||
long t = System.nanoTime();
|
||||
ValueSet fr = context.fetchResource(ValueSet.class, reference);
|
||||
if (fr == null) {
|
||||
if (!Utilities.isAbsoluteUrl(reference)) {
|
||||
reference = resolve(uri, reference);
|
||||
fr = context.fetchResource(ValueSet.class, reference);
|
||||
}
|
||||
}
|
||||
if (fr == null)
|
||||
fr = ValueSetUtilities.generateImplicitValueSet(reference);
|
||||
timeTracker.tx(t, System.nanoTime());
|
||||
return fr;
|
||||
}
|
||||
} else
|
||||
return null;
|
||||
}
|
||||
|
||||
|
||||
private String resolve(String uri, String ref) {
|
||||
if (isBlank(uri)) {
|
||||
return ref;
|
||||
}
|
||||
String[] up = uri.split("\\/");
|
||||
String[] rp = ref.split("\\/");
|
||||
if (context.getResourceNames().contains(up[up.length - 2]) && context.getResourceNames().contains(rp[0])) {
|
||||
StringBuilder b = new StringBuilder();
|
||||
for (int i = 0; i < up.length - 2; i++) {
|
||||
b.append(up[i]);
|
||||
b.append("/");
|
||||
}
|
||||
b.append(ref);
|
||||
return b.toString();
|
||||
} else
|
||||
return ref;
|
||||
}
|
||||
|
||||
protected String describeReference(String reference) {
|
||||
if (reference == null)
|
||||
return "null";
|
||||
return reference;
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
@ -0,0 +1,58 @@
|
||||
package org.hl7.fhir.validation;
|
||||
|
||||
public class TimeTracker {
|
||||
private long overall = 0;
|
||||
private long txTime = 0;
|
||||
private long sdTime = 0;
|
||||
private long loadTime = 0;
|
||||
private long fpeTime = 0;
|
||||
|
||||
|
||||
|
||||
public long getOverall() {
|
||||
return overall;
|
||||
}
|
||||
public long getTxTime() {
|
||||
return txTime;
|
||||
}
|
||||
public long getSdTime() {
|
||||
return sdTime;
|
||||
}
|
||||
public long getLoadTime() {
|
||||
return loadTime;
|
||||
}
|
||||
public long getFpeTime() {
|
||||
return fpeTime;
|
||||
}
|
||||
|
||||
public void load(long t, long nanoTime) {
|
||||
// TODO Auto-generated method stub
|
||||
|
||||
}
|
||||
public void overall(long t, long nanoTime) {
|
||||
// TODO Auto-generated method stub
|
||||
|
||||
}
|
||||
public void tx(long t, long nanoTime) {
|
||||
// TODO Auto-generated method stub
|
||||
|
||||
}
|
||||
public void sd(long t, long nanoTime) {
|
||||
// TODO Auto-generated method stub
|
||||
|
||||
}
|
||||
public void fpe(long t, long nanoTime) {
|
||||
// TODO Auto-generated method stub
|
||||
fpeTime = fpeTime + (System.nanoTime() - t);
|
||||
|
||||
}
|
||||
public void reset() {
|
||||
overall = 0;
|
||||
txTime = 0;
|
||||
sdTime = 0;
|
||||
loadTime = 0;
|
||||
fpeTime = 0;
|
||||
|
||||
}
|
||||
|
||||
}
|
@ -94,58 +94,6 @@ public class EnableWhenEvaluator {
|
||||
}
|
||||
}
|
||||
|
||||
public static class MeasurePair {
|
||||
private MeasureReportGroupComponent g;
|
||||
private Element a;
|
||||
|
||||
public MeasurePair(MeasureReportGroupComponent g, Element a) {
|
||||
super();
|
||||
this.g = g;
|
||||
this.a = a;
|
||||
}
|
||||
|
||||
public MeasureReportGroupComponent getGroup() {
|
||||
return g;
|
||||
}
|
||||
|
||||
public Element getA() {
|
||||
return a;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
public static class MStack extends ArrayList<MeasurePair> {
|
||||
|
||||
private static final long serialVersionUID = 1L;
|
||||
private Measure m;
|
||||
private Element a;
|
||||
|
||||
public MStack(Measure m, Element a) {
|
||||
super();
|
||||
this.m = m;
|
||||
this.a = a;
|
||||
}
|
||||
|
||||
|
||||
public Measure getM() {
|
||||
return m;
|
||||
}
|
||||
|
||||
|
||||
public Element getA() {
|
||||
return a;
|
||||
}
|
||||
|
||||
|
||||
public MStack push(MeasureReportGroupComponent g, Element a) {
|
||||
MStack self = new MStack(this.m, this.a);
|
||||
self.addAll(this);
|
||||
self.add(new MeasurePair(g, a));
|
||||
return self;
|
||||
}
|
||||
}
|
||||
|
||||
public static class EnableWhenResult {
|
||||
private final boolean enabled;
|
||||
private final QuestionnaireItemEnableWhenComponent enableWhenCondition;
|
||||
|
File diff suppressed because it is too large
Load Diff
@ -0,0 +1,359 @@
|
||||
package org.hl7.fhir.validation.instance.type;
|
||||
|
||||
import static org.apache.commons.lang3.StringUtils.isNotBlank;
|
||||
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
import org.hl7.fhir.convertors.VersionConvertor_30_50;
|
||||
import org.hl7.fhir.convertors.VersionConvertor_40_50;
|
||||
import org.hl7.fhir.exceptions.FHIRException;
|
||||
import org.hl7.fhir.r5.context.IWorkerContext;
|
||||
import org.hl7.fhir.r5.elementmodel.Element;
|
||||
import org.hl7.fhir.r5.elementmodel.JsonParser;
|
||||
import org.hl7.fhir.r5.elementmodel.ObjectConverter;
|
||||
import org.hl7.fhir.r5.formats.IParser.OutputStyle;
|
||||
import org.hl7.fhir.r5.model.CodeableConcept;
|
||||
import org.hl7.fhir.r5.model.Coding;
|
||||
import org.hl7.fhir.r5.model.FhirPublication;
|
||||
import org.hl7.fhir.r5.model.Library;
|
||||
import org.hl7.fhir.r5.model.Measure;
|
||||
import org.hl7.fhir.r5.model.Resource;
|
||||
import org.hl7.fhir.r5.model.Measure.MeasureGroupComponent;
|
||||
import org.hl7.fhir.r5.model.Measure.MeasureGroupPopulationComponent;
|
||||
import org.hl7.fhir.r5.model.Measure.MeasureGroupStratifierComponent;
|
||||
import org.hl7.fhir.r5.utils.NarrativeGenerator;
|
||||
import org.hl7.fhir.utilities.Utilities;
|
||||
import org.hl7.fhir.utilities.i18n.I18nConstants;
|
||||
import org.hl7.fhir.utilities.validation.ValidationMessage;
|
||||
import org.hl7.fhir.utilities.validation.ValidationMessage.IssueType;
|
||||
import org.hl7.fhir.utilities.validation.ValidationMessage.Source;
|
||||
import org.hl7.fhir.validation.instance.utils.NodeStack;
|
||||
import org.hl7.fhir.validation.BaseValidator;
|
||||
import org.hl7.fhir.validation.TimeTracker;
|
||||
import org.hl7.fhir.validation.instance.utils.MeasureContext;
|
||||
import org.hl7.fhir.validation.instance.utils.ValidatorHostContext;
|
||||
|
||||
public class MeasureValidator extends BaseValidator {
|
||||
|
||||
public MeasureValidator(IWorkerContext context, TimeTracker timeTracker) {
|
||||
super(context);
|
||||
source = Source.InstanceValidator;
|
||||
this.timeTracker = timeTracker;
|
||||
}
|
||||
|
||||
public void validateMeasure(ValidatorHostContext hostContext, List<ValidationMessage> errors, Element element, NodeStack stack) throws FHIRException {
|
||||
MeasureContext mctxt = new MeasureContext();
|
||||
List<Element> libs = element.getChildrenByName("library");
|
||||
for (Element lib : libs) {
|
||||
String ref = lib.isPrimitive() ? lib.primitiveValue() : lib.getChildValue("reference");
|
||||
if (!Utilities.noString(ref)) {
|
||||
Library l = context.fetchResource(Library.class, ref);
|
||||
if (hint(errors, IssueType.NOTFOUND, lib.line(), lib.col(), stack.getLiteralPath(), l != null, I18nConstants.MEASURE_M_LIB_UNKNOWN, ref)) {
|
||||
mctxt.seeLibrary(l);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
List<Element> groups = element.getChildrenByName("group");
|
||||
if (warning(errors, IssueType.REQUIRED, element.line(), element.col(), stack.getLiteralPath(), groups.size() > 0, I18nConstants.MEASURE_M_NO_GROUPS)) {
|
||||
int c = 0;
|
||||
for (Element group : groups) {
|
||||
NodeStack ns = stack.push(group, c, null, null);
|
||||
warning(errors, IssueType.REQUIRED, group.line(), group.col(), ns.getLiteralPath(), groups.size() ==1 || group.hasChild("code"), I18nConstants.MEASURE_M_GROUP_CODE);
|
||||
warning(errors, IssueType.REQUIRED, group.line(), group.col(), ns.getLiteralPath(), group.hasChildren("population"), I18nConstants.MEASURE_M_GROUP_POP);
|
||||
int c1 = 0;
|
||||
List<Element> pl = group.getChildrenByName("population");
|
||||
for (Element p : pl) {
|
||||
NodeStack ns2 = ns.push(p, c1, null, null);
|
||||
warning(errors, IssueType.REQUIRED, p.line(), p.col(), ns2.getLiteralPath(), pl.size() == 1 || p.hasChild("code"), I18nConstants.MEASURE_M_GROUP_POP_NO_CODE);
|
||||
c1++;
|
||||
}
|
||||
warning(errors, IssueType.REQUIRED, group.line(), group.col(), ns.getLiteralPath(), group.hasChildren("stratifier"), I18nConstants.MEASURE_M_GROUP_STRATA);
|
||||
c1 = 0;
|
||||
List<Element> stl = group.getChildrenByName("stratifier");
|
||||
for (Element st : stl) {
|
||||
NodeStack ns2 = ns.push(st, c1, null, null);
|
||||
warning(errors, IssueType.REQUIRED, st.line(), st.col(), ns2.getLiteralPath(), stl.size() == 1 || st.hasChild("code"), I18nConstants.MEASURE_M_GROUP_STRATA_NO_CODE);
|
||||
if (st.hasChild("criteria")) {
|
||||
Element crit = st.getNamedChild("criteria");
|
||||
NodeStack nsc = ns2.push(crit, -1, null, null);
|
||||
validateMeasureCriteria(hostContext, errors, mctxt, crit, nsc);
|
||||
}
|
||||
int c2 = 0;
|
||||
List<Element> cpl = group.getChildrenByName("component");
|
||||
for (Element cp : cpl) {
|
||||
NodeStack ns3 = ns2.push(cp, c2, null, null);
|
||||
warning(errors, IssueType.REQUIRED, cp.line(), cp.col(), ns3.getLiteralPath(), cpl.size() == 1 || cp.hasChild("code"), I18nConstants.MEASURE_M_GROUP_STRATA_COMP_NO_CODE);
|
||||
if (cp.hasChild("criteria")) {
|
||||
Element crit = cp.getNamedChild("criteria");
|
||||
NodeStack nsc = ns3.push(crit, -1, null, null);
|
||||
validateMeasureCriteria(hostContext, errors, mctxt, crit, nsc);
|
||||
}
|
||||
c2++;
|
||||
}
|
||||
c1++;
|
||||
}
|
||||
c++;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void validateMeasureCriteria(ValidatorHostContext hostContext, List<ValidationMessage> errors, MeasureContext mctxt, Element crit, NodeStack nsc) {
|
||||
// TODO Auto-generated method stub
|
||||
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------------------------------------------------------------------------------------
|
||||
|
||||
public void validateMeasureReport(ValidatorHostContext hostContext, List<ValidationMessage> errors, Element element, NodeStack stack) throws FHIRException {
|
||||
Element m = element.getNamedChild("measure");
|
||||
String measure = null;
|
||||
if (m != null) {
|
||||
/*
|
||||
* q.getValue() is correct for R4 content, but we'll also accept the second
|
||||
* option just in case we're validating raw STU3 content. Being lenient here
|
||||
* isn't the end of the world since if someone is actually doing the reference
|
||||
* wrong in R4 content it'll get flagged elsewhere by the validator too
|
||||
*/
|
||||
if (isNotBlank(m.getValue())) {
|
||||
measure = m.getValue();
|
||||
} else if (isNotBlank(m.getChildValue("reference"))) {
|
||||
measure = m.getChildValue("reference");
|
||||
}
|
||||
}
|
||||
if (hint(errors, IssueType.REQUIRED, element.line(), element.col(), stack.getLiteralPath(), measure != null, I18nConstants.MEASURE_MR_M_NONE)) {
|
||||
long t = System.nanoTime();
|
||||
Measure msrc = measure.startsWith("#") ? loadMeasure(element, measure.substring(1)) : context.fetchResource(Measure.class, measure);
|
||||
if (warning(errors, IssueType.REQUIRED, m.line(), m.col(), stack.getLiteralPath(), msrc != null, I18nConstants.MEASURE_MR_M_NOTFOUND, measure)) {
|
||||
boolean inComplete = !"complete".equals(element.getNamedChildValue("status"));
|
||||
validateMeasureReportGroups(hostContext, msrc, errors, element, stack, inComplete);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private Measure loadMeasure(Element resource, String id) throws FHIRException {
|
||||
try {
|
||||
for (Element contained : resource.getChildren("contained")) {
|
||||
if (contained.getIdBase().equals(id)) {
|
||||
FhirPublication v = FhirPublication.fromCode(context.getVersion());
|
||||
ByteArrayOutputStream bs = new ByteArrayOutputStream();
|
||||
new JsonParser(context).compose(contained, bs, OutputStyle.NORMAL, id);
|
||||
byte[] json = bs.toByteArray();
|
||||
switch (v) {
|
||||
case DSTU1:
|
||||
throw new FHIRException(context.formatMessage(I18nConstants.UNSUPPORTED_VERSION_R1));
|
||||
case DSTU2:
|
||||
throw new FHIRException(context.formatMessage(I18nConstants.UNSUPPORTED_VERSION_R2));
|
||||
case DSTU2016May:
|
||||
throw new FHIRException(context.formatMessage(I18nConstants.UNSUPPORTED_VERSION_R2B));
|
||||
case STU3:
|
||||
org.hl7.fhir.dstu3.model.Resource r3 = new org.hl7.fhir.dstu3.formats.JsonParser().parse(json);
|
||||
Resource r5 = VersionConvertor_30_50.convertResource(r3, false);
|
||||
if (r5 instanceof Measure)
|
||||
return (Measure) r5;
|
||||
else
|
||||
return null;
|
||||
case R4:
|
||||
org.hl7.fhir.r4.model.Resource r4 = new org.hl7.fhir.r4.formats.JsonParser().parse(json);
|
||||
r5 = VersionConvertor_40_50.convertResource(r4);
|
||||
if (r5 instanceof Measure)
|
||||
return (Measure) r5;
|
||||
else
|
||||
return null;
|
||||
case R5:
|
||||
r5 = new org.hl7.fhir.r5.formats.JsonParser().parse(json);
|
||||
if (r5 instanceof Measure)
|
||||
return (Measure) r5;
|
||||
else
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
return null;
|
||||
} catch (IOException e) {
|
||||
throw new FHIRException(e);
|
||||
}
|
||||
}
|
||||
|
||||
private void validateMeasureReportGroups(ValidatorHostContext hostContext, Measure m, List<ValidationMessage> errors, Element mr, NodeStack stack, boolean inProgress) {
|
||||
NarrativeGenerator gen = new NarrativeGenerator(null, null, context);
|
||||
List<MeasureGroupComponent> groups = new ArrayList<MeasureGroupComponent>();
|
||||
|
||||
List<Element> glist = mr.getChildrenByName("group");
|
||||
|
||||
if (glist.size() == 1 && m.getGroup().size() == 1) {
|
||||
// if there's only one group, it can be ((and usually is) anonymous)
|
||||
// but we still check that the code, if both have one, is consistent.
|
||||
Element mrg = glist.get(0);
|
||||
NodeStack ns = stack.push(mrg, 0, mrg.getProperty().getDefinition(), mrg.getProperty().getDefinition());
|
||||
if (m.getGroupFirstRep().hasCode() && mrg.hasChild("code")) {
|
||||
CodeableConcept cc = ObjectConverter.readAsCodeableConcept(mrg.getNamedChild("code"));
|
||||
if (rule(errors, IssueType.BUSINESSRULE, mrg.line(), mrg.col(), ns.getLiteralPath(), hasUseableCode(cc), I18nConstants.MEASURE_MR_GRP_NO_USABLE_CODE)) {
|
||||
rule(errors, IssueType.BUSINESSRULE, mrg.line(), mrg.col(), ns.getLiteralPath(), cc.matches(m.getGroupFirstRep().getCode()), I18nConstants.MEASURE_MR_GRP_NO_WRONG_CODE, gen.gen(cc), gen.gen(m.getGroupFirstRep().getCode()));
|
||||
}
|
||||
}
|
||||
validateMeasureReportGroup(hostContext, m.getGroupFirstRep(), errors, mr, mrg, ns, inProgress, gen);
|
||||
} else {
|
||||
int i = 0;
|
||||
for (Element mrg : glist) {
|
||||
NodeStack ns = stack.push(mrg, i, mrg.getProperty().getDefinition(), mrg.getProperty().getDefinition());
|
||||
CodeableConcept cc = ObjectConverter.readAsCodeableConcept(mrg.getNamedChild("code"));
|
||||
if (rule(errors, IssueType.BUSINESSRULE, mrg.line(), mrg.col(), ns.getLiteralPath(), cc != null, I18nConstants.MEASURE_MR_GRP_NO_CODE)) {
|
||||
MeasureGroupComponent mg = getGroupForCode(cc, m);
|
||||
if (rule(errors, IssueType.BUSINESSRULE, mrg.line(), mrg.col(), ns.getLiteralPath(), mg != null, I18nConstants.MEASURE_MR_GRP_UNK_CODE)) {
|
||||
if (rule(errors, IssueType.BUSINESSRULE, mrg.line(), mrg.col(), ns.getLiteralPath(), !groups.contains(mg), I18nConstants.MEASURE_MR_GRP_DUPL_CODE)) {
|
||||
groups.add(mg);
|
||||
validateMeasureReportGroup(hostContext, mg, errors, mr, mrg, ns, inProgress, gen);
|
||||
}
|
||||
}
|
||||
}
|
||||
i++;
|
||||
}
|
||||
for (MeasureGroupComponent mg : m.getGroup()) {
|
||||
if (!groups.contains(mg)) {
|
||||
rule(errors, IssueType.BUSINESSRULE, mr.line(), mr.col(), stack.getLiteralPath(), groups.contains(mg), I18nConstants.MEASURE_MR_GRP_MISSING_BY_CODE, gen.gen(mg.getCode()));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void validateMeasureReportGroup(ValidatorHostContext hostContext, MeasureGroupComponent mg, List<ValidationMessage> errors, Element mr, Element mrg, NodeStack ns, boolean inProgress, NarrativeGenerator gen) {
|
||||
validateMeasureReportGroupPopulations(hostContext, mg, errors, mr, mrg, ns, inProgress, gen);
|
||||
// validate the score
|
||||
validateMeasureReportGroupStratifiers(hostContext, mg, errors, mr, mrg, ns, inProgress, gen);
|
||||
// validate the stratifiers
|
||||
}
|
||||
|
||||
private void validateMeasureReportGroupPopulations(ValidatorHostContext hostContext, MeasureGroupComponent mg, List<ValidationMessage> errors, Element mr, Element mrg, NodeStack stack, boolean inProgress, NarrativeGenerator gen) {
|
||||
// there must be a population for each population defined in the measure, and no 4others.
|
||||
List<MeasureGroupPopulationComponent> pops = new ArrayList<MeasureGroupPopulationComponent>();
|
||||
List<Element> plist = mrg.getChildrenByName("population");
|
||||
|
||||
int i = 0;
|
||||
for (Element mrgp : plist) {
|
||||
NodeStack ns = stack.push(mrgp, i, mrgp.getProperty().getDefinition(), mrgp.getProperty().getDefinition());
|
||||
CodeableConcept cc = ObjectConverter.readAsCodeableConcept(mrgp.getNamedChild("code"));
|
||||
if (rule(errors, IssueType.BUSINESSRULE, mrgp.line(), mrgp.col(), ns.getLiteralPath(), cc != null, I18nConstants.MEASURE_MR_GRP_POP_NO_CODE)) {
|
||||
MeasureGroupPopulationComponent mgp = getGroupPopForCode(cc, mg);
|
||||
if (rule(errors, IssueType.BUSINESSRULE, mrg.line(), mrg.col(), ns.getLiteralPath(), mgp != null, I18nConstants.MEASURE_MR_GRP_POP_UNK_CODE)) {
|
||||
if (rule(errors, IssueType.BUSINESSRULE, mrg.line(), mrg.col(), ns.getLiteralPath(), !pops.contains(mgp), I18nConstants.MEASURE_MR_GRP_POP_DUPL_CODE)) {
|
||||
pops.add(mgp);
|
||||
validateMeasureReportGroupPopulation(hostContext, mgp, errors, mr, mrgp, ns, inProgress);
|
||||
}
|
||||
}
|
||||
}
|
||||
i++;
|
||||
}
|
||||
for (MeasureGroupPopulationComponent mgp : mg.getPopulation()) {
|
||||
if (!pops.contains(mgp)) {
|
||||
rule(errors, IssueType.BUSINESSRULE, mrg.line(), mrg.col(), stack.getLiteralPath(), pops.contains(mg), I18nConstants.MEASURE_MR_GRP_MISSING_BY_CODE, gen.gen(mgp.getCode()));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void validateMeasureReportGroupPopulation(ValidatorHostContext hostContext, MeasureGroupPopulationComponent mgp, List<ValidationMessage> errors, Element mr, Element mrgp, NodeStack ns, boolean inProgress) {
|
||||
List<Element> sr = mrgp.getChildrenByName("subjectResults");
|
||||
if ("subject-list".equals(mr.getChildValue("type"))) {
|
||||
try {
|
||||
int c = Integer.parseInt(mrgp.getChildValue("count"));
|
||||
rule(errors, IssueType.BUSINESSRULE, mrgp.line(), mrgp.col(), ns.getLiteralPath(), c == sr.size(), I18nConstants.MEASURE_MR_GRP_POP_COUNT_MISMATCH, c, sr.size());
|
||||
} catch (Exception e) {
|
||||
// nothing; that'll be because count is not valid, and that's a different error or its missing and we don't care
|
||||
}
|
||||
} else {
|
||||
rule(errors, IssueType.BUSINESSRULE, mrgp.line(), mrgp.col(), ns.getLiteralPath(), sr.size() == 0, I18nConstants.MEASURE_MR_GRP_POP_NO_SUBJECTS);
|
||||
warning(errors, IssueType.BUSINESSRULE, mrgp.line(), mrgp.col(), ns.getLiteralPath(), mrgp.hasChild("count"), I18nConstants.MEASURE_MR_GRP_POP_NO_COUNT);
|
||||
}
|
||||
}
|
||||
|
||||
private void validateMeasureReportGroupStratifiers(ValidatorHostContext hostContext, MeasureGroupComponent mg, List<ValidationMessage> errors, Element mr, Element mrg, NodeStack stack, boolean inProgress, NarrativeGenerator gen) {
|
||||
// there must be a population for each population defined in the measure, and no 4others.
|
||||
List<MeasureGroupStratifierComponent> strats = new ArrayList<>();
|
||||
List<Element> slist = mrg.getChildrenByName("stratifier");
|
||||
|
||||
int i = 0;
|
||||
for (Element mrgs : slist) {
|
||||
NodeStack ns = stack.push(mrgs, i, mrgs.getProperty().getDefinition(), mrgs.getProperty().getDefinition());
|
||||
CodeableConcept cc = ObjectConverter.readAsCodeableConcept(mrgs.getNamedChild("code"));
|
||||
if (rule(errors, IssueType.BUSINESSRULE, mrgs.line(), mrgs.col(), ns.getLiteralPath(), cc != null, I18nConstants.MEASURE_MR_GRP_POP_NO_CODE)) {
|
||||
MeasureGroupStratifierComponent mgs = getGroupStratifierForCode(cc, mg);
|
||||
if (rule(errors, IssueType.BUSINESSRULE, mrg.line(), mrg.col(), ns.getLiteralPath(), mgs != null, I18nConstants.MEASURE_MR_GRP_POP_UNK_CODE)) {
|
||||
if (rule(errors, IssueType.BUSINESSRULE, mrg.line(), mrg.col(), ns.getLiteralPath(), !strats.contains(mgs), I18nConstants.MEASURE_MR_GRP_POP_DUPL_CODE)) {
|
||||
strats.add(mgs);
|
||||
validateMeasureReportGroupStratifier(hostContext, mgs, errors, mr, mrgs, ns, inProgress);
|
||||
}
|
||||
}
|
||||
}
|
||||
i++;
|
||||
}
|
||||
for (MeasureGroupStratifierComponent mgs : mg.getStratifier()) {
|
||||
if (!strats.contains(mgs)) {
|
||||
rule(errors, IssueType.BUSINESSRULE, mrg.line(), mrg.col(), stack.getLiteralPath(), strats.contains(mg), I18nConstants.MEASURE_MR_GRP_MISSING_BY_CODE, gen.gen(mgs.getCode()));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void validateMeasureReportGroupStratifier(ValidatorHostContext hostContext, MeasureGroupStratifierComponent mgs, List<ValidationMessage> errors, Element mr, Element mrgs, NodeStack ns, boolean inProgress) {
|
||||
// TODO Auto-generated method stub
|
||||
|
||||
}
|
||||
|
||||
private MeasureGroupStratifierComponent getGroupStratifierForCode(CodeableConcept cc, MeasureGroupComponent mg) {
|
||||
for (MeasureGroupStratifierComponent t : mg.getStratifier()) {
|
||||
if (t.hasCode()) {
|
||||
for (Coding c : t.getCode().getCoding()) {
|
||||
if (cc.hasCoding(c.getSystem(), c.getCode())) {
|
||||
return t;
|
||||
}
|
||||
}
|
||||
if (!cc.hasCoding() && !t.getCode().hasCoding()) {
|
||||
if (cc.hasText() && t.getCode().hasText()) {
|
||||
if (cc.getText().equals(t.getCode().getText())) {
|
||||
return t;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private boolean hasUseableCode(CodeableConcept cc) {
|
||||
for (Coding c : cc.getCoding()) {
|
||||
if (c.hasSystem() && c.hasCode()) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
private MeasureGroupPopulationComponent getGroupPopForCode(CodeableConcept cc, MeasureGroupComponent mg) {
|
||||
for (MeasureGroupPopulationComponent t : mg.getPopulation()) {
|
||||
if (t.hasCode()) {
|
||||
for (Coding c : t.getCode().getCoding()) {
|
||||
if (cc.hasCoding(c.getSystem(), c.getCode())) {
|
||||
return t;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
private MeasureGroupComponent getGroupForCode(CodeableConcept cc, Measure m) {
|
||||
for (MeasureGroupComponent t : m.getGroup()) {
|
||||
if (t.hasCode()) {
|
||||
for (Coding c : t.getCode().getCoding()) {
|
||||
if (cc.hasCoding(c.getSystem(), c.getCode())) {
|
||||
return t;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
|
||||
}
|
@ -0,0 +1,710 @@
|
||||
package org.hl7.fhir.validation.instance.type;
|
||||
|
||||
import static org.apache.commons.lang3.StringUtils.isBlank;
|
||||
import static org.apache.commons.lang3.StringUtils.isNotBlank;
|
||||
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
import org.hl7.fhir.convertors.VersionConvertor_10_50;
|
||||
import org.hl7.fhir.convertors.VersionConvertor_14_50;
|
||||
import org.hl7.fhir.convertors.VersionConvertor_30_50;
|
||||
import org.hl7.fhir.convertors.VersionConvertor_40_50;
|
||||
import org.hl7.fhir.exceptions.FHIRException;
|
||||
import org.hl7.fhir.r5.context.IWorkerContext;
|
||||
import org.hl7.fhir.r5.context.IWorkerContext.ValidationResult;
|
||||
import org.hl7.fhir.r5.elementmodel.Element;
|
||||
import org.hl7.fhir.r5.elementmodel.JsonParser;
|
||||
import org.hl7.fhir.r5.elementmodel.ObjectConverter;
|
||||
import org.hl7.fhir.r5.formats.IParser.OutputStyle;
|
||||
import org.hl7.fhir.r5.model.Coding;
|
||||
import org.hl7.fhir.r5.model.DateType;
|
||||
import org.hl7.fhir.r5.model.FhirPublication;
|
||||
import org.hl7.fhir.r5.model.IntegerType;
|
||||
import org.hl7.fhir.r5.model.Questionnaire;
|
||||
import org.hl7.fhir.r5.model.Resource;
|
||||
import org.hl7.fhir.r5.model.StringType;
|
||||
import org.hl7.fhir.r5.model.TimeType;
|
||||
import org.hl7.fhir.r5.model.ValueSet;
|
||||
import org.hl7.fhir.r5.model.ElementDefinition.TypeRefComponent;
|
||||
import org.hl7.fhir.r5.model.Enumerations.FHIRVersion;
|
||||
import org.hl7.fhir.r5.model.Questionnaire.QuestionnaireItemAnswerOptionComponent;
|
||||
import org.hl7.fhir.r5.model.Questionnaire.QuestionnaireItemComponent;
|
||||
import org.hl7.fhir.r5.model.Questionnaire.QuestionnaireItemType;
|
||||
import org.hl7.fhir.r5.utils.FHIRPathEngine;
|
||||
import org.hl7.fhir.utilities.CommaSeparatedStringBuilder;
|
||||
import org.hl7.fhir.utilities.Utilities;
|
||||
import org.hl7.fhir.utilities.i18n.I18nConstants;
|
||||
import org.hl7.fhir.utilities.validation.ValidationMessage;
|
||||
import org.hl7.fhir.utilities.validation.ValidationOptions;
|
||||
import org.hl7.fhir.utilities.validation.ValidationMessage.IssueType;
|
||||
import org.hl7.fhir.utilities.validation.ValidationMessage.Source;
|
||||
import org.hl7.fhir.validation.BaseValidator;
|
||||
import org.hl7.fhir.validation.TimeTracker;
|
||||
import org.hl7.fhir.validation.instance.EnableWhenEvaluator;
|
||||
import org.hl7.fhir.validation.instance.EnableWhenEvaluator.QStack;
|
||||
import org.hl7.fhir.validation.instance.utils.NodeStack;
|
||||
import org.hl7.fhir.validation.instance.utils.ValidatorHostContext;
|
||||
|
||||
import ca.uhn.fhir.util.ObjectUtil;
|
||||
|
||||
public class QuestionnaireValidator extends BaseValidator {
|
||||
|
||||
private EnableWhenEvaluator myEnableWhenEvaluator;
|
||||
private FHIRPathEngine fpe;
|
||||
|
||||
public QuestionnaireValidator(IWorkerContext context, EnableWhenEvaluator myEnableWhenEvaluator, FHIRPathEngine fpe, TimeTracker timeTracker) {
|
||||
super(context);
|
||||
source = Source.InstanceValidator;
|
||||
this.myEnableWhenEvaluator = myEnableWhenEvaluator;
|
||||
this.fpe = fpe;
|
||||
this.timeTracker = timeTracker;
|
||||
}
|
||||
|
||||
public void validateQuestionannaire(List<ValidationMessage> errors, Element element, Element element2, NodeStack stack) {
|
||||
ArrayList<Element> parents = new ArrayList<>();
|
||||
parents.add(element);
|
||||
validateQuestionannaireItem(errors, element, element, stack, parents);
|
||||
}
|
||||
|
||||
private void validateQuestionannaireItem(List<ValidationMessage> errors, Element element, Element questionnaire, NodeStack stack, List<Element> parents) {
|
||||
List<Element> list = getItems(element);
|
||||
for (int i = 0; i < list.size(); i++) {
|
||||
Element e = list.get(i);
|
||||
NodeStack ns = stack.push(e, i, e.getProperty().getDefinition(), e.getProperty().getDefinition());
|
||||
validateQuestionnaireElement(errors, ns, questionnaire, e, parents);
|
||||
List<Element> np = new ArrayList<Element>();
|
||||
np.add(e);
|
||||
np.addAll(parents);
|
||||
validateQuestionannaireItem(errors, e, questionnaire, ns, np);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private void validateQuestionnaireElement(List<ValidationMessage> errors, NodeStack ns, Element questionnaire, Element item, List<Element> parents) {
|
||||
// R4+
|
||||
if ((FHIRVersion.isR4Plus(context.getVersion())) && (item.hasChildren("enableWhen"))) {
|
||||
List<Element> ewl = item.getChildren("enableWhen");
|
||||
for (Element ew : ewl) {
|
||||
String ql = ew.getNamedChildValue("question");
|
||||
if (rule(errors, IssueType.BUSINESSRULE, ns.getLiteralPath(), ql != null, I18nConstants.QUESTIONNAIRE_Q_ENABLEWHEN_NOLINK)) {
|
||||
Element tgt = getQuestionById(item, ql);
|
||||
if (rule(errors, IssueType.BUSINESSRULE, ns.getLiteralPath(), tgt == null, I18nConstants.QUESTIONNAIRE_Q_ENABLEWHEN_ISINNER)) {
|
||||
tgt = getQuestionById(questionnaire, ql);
|
||||
if (rule(errors, IssueType.BUSINESSRULE, ns.getLiteralPath(), tgt != null, I18nConstants.QUESTIONNAIRE_Q_ENABLEWHEN_NOTARGET, ql)) {
|
||||
if (rule(errors, IssueType.BUSINESSRULE, ns.getLiteralPath(), tgt != item, I18nConstants.QUESTIONNAIRE_Q_ENABLEWHEN_SELF)) {
|
||||
if (!isBefore(item, tgt, parents)) {
|
||||
warning(errors, IssueType.BUSINESSRULE, ns.getLiteralPath(), false, I18nConstants.QUESTIONNAIRE_Q_ENABLEWHEN_AFTER, ql);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private boolean isBefore(Element item, Element tgt, List<Element> parents) {
|
||||
// we work up the list, looking for tgt in the children of the parents
|
||||
if (parents.contains(tgt)) {
|
||||
// actually, if the target is a parent, that's automatically ok
|
||||
return true;
|
||||
}
|
||||
for (Element p : parents) {
|
||||
int i = findIndex(p, item);
|
||||
int t = findIndex(p, tgt);
|
||||
if (i > -1 && t > -1) {
|
||||
return i > t;
|
||||
}
|
||||
}
|
||||
return false; // unsure... shouldn't ever get to this point;
|
||||
}
|
||||
|
||||
|
||||
private int findIndex(Element parent, Element descendant) {
|
||||
for (int i = 0; i < parent.getChildren().size(); i++) {
|
||||
if (parent.getChildren().get(i) == descendant || isChild(parent.getChildren().get(i), descendant))
|
||||
return i;
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
|
||||
private boolean isChild(Element element, Element descendant) {
|
||||
for (Element e : element.getChildren()) {
|
||||
if (e == descendant)
|
||||
return true;
|
||||
if (isChild(e, descendant))
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
private Element getQuestionById(Element focus, String ql) {
|
||||
List<Element> list = getItems(focus);
|
||||
for (Element item : list) {
|
||||
String v = item.getNamedChildValue("linkId");
|
||||
if (ql.equals(v))
|
||||
return item;
|
||||
Element tgt = getQuestionById(item, ql);
|
||||
if (tgt != null)
|
||||
return tgt;
|
||||
}
|
||||
return null;
|
||||
|
||||
}
|
||||
|
||||
private List<Element> getItems(Element element) {
|
||||
List<Element> list = new ArrayList<>();
|
||||
element.getNamedChildren("item", list);
|
||||
return list;
|
||||
}
|
||||
|
||||
public void validateQuestionannaireResponse(ValidatorHostContext hostContext, List<ValidationMessage> errors, Element element, NodeStack stack) throws FHIRException {
|
||||
Element q = element.getNamedChild("questionnaire");
|
||||
String questionnaire = null;
|
||||
if (q != null) {
|
||||
/*
|
||||
* q.getValue() is correct for R4 content, but we'll also accept the second
|
||||
* option just in case we're validating raw STU3 content. Being lenient here
|
||||
* isn't the end of the world since if someone is actually doing the reference
|
||||
* wrong in R4 content it'll get flagged elsewhere by the validator too
|
||||
*/
|
||||
if (isNotBlank(q.getValue())) {
|
||||
questionnaire = q.getValue();
|
||||
} else if (isNotBlank(q.getChildValue("reference"))) {
|
||||
questionnaire = q.getChildValue("reference");
|
||||
}
|
||||
}
|
||||
if (hint(errors, IssueType.REQUIRED, element.line(), element.col(), stack.getLiteralPath(), questionnaire != null, I18nConstants.QUESTIONNAIRE_QR_Q_NONE)) {
|
||||
long t = System.nanoTime();
|
||||
Questionnaire qsrc = questionnaire.startsWith("#") ? loadQuestionnaire(element, questionnaire.substring(1)) : context.fetchResource(Questionnaire.class, questionnaire);
|
||||
if (warning(errors, IssueType.REQUIRED, q.line(), q.col(), stack.getLiteralPath(), qsrc != null, I18nConstants.QUESTIONNAIRE_QR_Q_NOTFOUND, questionnaire)) {
|
||||
boolean inProgress = "in-progress".equals(element.getNamedChildValue("status"));
|
||||
validateQuestionannaireResponseItems(hostContext, qsrc, qsrc.getItem(), errors, element, stack, inProgress, element, new QStack(qsrc, element));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private Questionnaire loadQuestionnaire(Element resource, String id) throws FHIRException {
|
||||
try {
|
||||
for (Element contained : resource.getChildren("contained")) {
|
||||
if (contained.getIdBase().equals(id)) {
|
||||
FhirPublication v = FhirPublication.fromCode(context.getVersion());
|
||||
ByteArrayOutputStream bs = new ByteArrayOutputStream();
|
||||
new JsonParser(context).compose(contained, bs, OutputStyle.NORMAL, id);
|
||||
byte[] json = bs.toByteArray();
|
||||
switch (v) {
|
||||
case DSTU1:
|
||||
throw new FHIRException(context.formatMessage(I18nConstants.UNSUPPORTED_VERSION_R1));
|
||||
case DSTU2:
|
||||
org.hl7.fhir.dstu2.model.Resource r2 = new org.hl7.fhir.dstu2.formats.JsonParser().parse(json);
|
||||
Resource r5 = VersionConvertor_10_50.convertResource(r2);
|
||||
if (r5 instanceof Questionnaire)
|
||||
return (Questionnaire) r5;
|
||||
else
|
||||
return null;
|
||||
case DSTU2016May:
|
||||
org.hl7.fhir.dstu2016may.model.Resource r2a = new org.hl7.fhir.dstu2016may.formats.JsonParser().parse(json);
|
||||
r5 = VersionConvertor_14_50.convertResource(r2a);
|
||||
if (r5 instanceof Questionnaire)
|
||||
return (Questionnaire) r5;
|
||||
else
|
||||
return null;
|
||||
case STU3:
|
||||
org.hl7.fhir.dstu3.model.Resource r3 = new org.hl7.fhir.dstu3.formats.JsonParser().parse(json);
|
||||
r5 = VersionConvertor_30_50.convertResource(r3, false);
|
||||
if (r5 instanceof Questionnaire)
|
||||
return (Questionnaire) r5;
|
||||
else
|
||||
return null;
|
||||
case R4:
|
||||
org.hl7.fhir.r4.model.Resource r4 = new org.hl7.fhir.r4.formats.JsonParser().parse(json);
|
||||
r5 = VersionConvertor_40_50.convertResource(r4);
|
||||
if (r5 instanceof Questionnaire)
|
||||
return (Questionnaire) r5;
|
||||
else
|
||||
return null;
|
||||
case R5:
|
||||
r5 = new org.hl7.fhir.r5.formats.JsonParser().parse(json);
|
||||
if (r5 instanceof Questionnaire)
|
||||
return (Questionnaire) r5;
|
||||
else
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
return null;
|
||||
} catch (IOException e) {
|
||||
throw new FHIRException(e);
|
||||
}
|
||||
}
|
||||
|
||||
private void validateQuestionnaireResponseItem(ValidatorHostContext hostContext, Questionnaire qsrc, QuestionnaireItemComponent qItem, List<ValidationMessage> errors, Element element, NodeStack stack, boolean inProgress, Element questionnaireResponseRoot, QStack qstack) {
|
||||
String text = element.getNamedChildValue("text");
|
||||
rule(errors, IssueType.INVALID, element.line(), element.col(), stack.getLiteralPath(), Utilities.noString(text) || text.equals(qItem.getText()), I18nConstants.QUESTIONNAIRE_QR_ITEM_TEXT, qItem.getLinkId());
|
||||
|
||||
List<Element> answers = new ArrayList<Element>();
|
||||
element.getNamedChildren("answer", answers);
|
||||
if (inProgress)
|
||||
warning(errors, IssueType.REQUIRED, element.line(), element.col(), stack.getLiteralPath(), isAnswerRequirementFulfilled(qItem, answers), I18nConstants.QUESTIONNAIRE_QR_ITEM_MISSING, qItem.getLinkId());
|
||||
else if (myEnableWhenEvaluator.isQuestionEnabled(hostContext, qItem, qstack, fpe)) {
|
||||
rule(errors, IssueType.REQUIRED, element.line(), element.col(), stack.getLiteralPath(), isAnswerRequirementFulfilled(qItem, answers), I18nConstants.QUESTIONNAIRE_QR_ITEM_MISSING, qItem.getLinkId());
|
||||
} else if (!answers.isEmpty()) { // items without answers should be allowed, but not items with answers to questions that are disabled
|
||||
// it appears that this is always a duplicate error - it will always already have been reported, so no need to report it again?
|
||||
// GDG 2019-07-13
|
||||
// rule(errors, IssueType.INVALID, element.line(), element.col(), stack.getLiteralPath(), !isAnswerRequirementFulfilled(qItem, answers), I18nConstants.QUESTIONNAIRE_QR_ITEM_NOTENABLED, qItem.getLinkId());
|
||||
}
|
||||
|
||||
if (answers.size() > 1)
|
||||
rule(errors, IssueType.INVALID, answers.get(1).line(), answers.get(1).col(), stack.getLiteralPath(), qItem.getRepeats(), I18nConstants.QUESTIONNAIRE_QR_ITEM_ONLYONEA);
|
||||
|
||||
for (Element answer : answers) {
|
||||
NodeStack ns = stack.push(answer, -1, null, null);
|
||||
if (qItem.getType() != null) {
|
||||
switch (qItem.getType()) {
|
||||
case GROUP:
|
||||
rule(errors, IssueType.STRUCTURE, answer.line(), answer.col(), stack.getLiteralPath(), false, I18nConstants.QUESTIONNAIRE_QR_ITEM_GROUP);
|
||||
break;
|
||||
case DISPLAY: // nothing
|
||||
break;
|
||||
case BOOLEAN:
|
||||
validateQuestionnaireResponseItemType(errors, answer, ns, "boolean");
|
||||
break;
|
||||
case DECIMAL:
|
||||
validateQuestionnaireResponseItemType(errors, answer, ns, "decimal");
|
||||
break;
|
||||
case INTEGER:
|
||||
validateQuestionnaireResponseItemType(errors, answer, ns, "integer");
|
||||
break;
|
||||
case DATE:
|
||||
validateQuestionnaireResponseItemType(errors, answer, ns, "date");
|
||||
break;
|
||||
case DATETIME:
|
||||
validateQuestionnaireResponseItemType(errors, answer, ns, "dateTime");
|
||||
break;
|
||||
case TIME:
|
||||
validateQuestionnaireResponseItemType(errors, answer, ns, "time");
|
||||
break;
|
||||
case STRING:
|
||||
validateQuestionnaireResponseItemType(errors, answer, ns, "string");
|
||||
break;
|
||||
case TEXT:
|
||||
validateQuestionnaireResponseItemType(errors, answer, ns, "text");
|
||||
break;
|
||||
case URL:
|
||||
validateQuestionnaireResponseItemType(errors, answer, ns, "uri");
|
||||
break;
|
||||
case ATTACHMENT:
|
||||
validateQuestionnaireResponseItemType(errors, answer, ns, "Attachment");
|
||||
break;
|
||||
case REFERENCE:
|
||||
validateQuestionnaireResponseItemType(errors, answer, ns, "Reference");
|
||||
break;
|
||||
case QUANTITY:
|
||||
if ("Quantity".equals(validateQuestionnaireResponseItemType(errors, answer, ns, "Quantity")))
|
||||
if (qItem.hasExtension("???"))
|
||||
validateQuestionnaireResponseItemQuantity(errors, answer, ns);
|
||||
break;
|
||||
case CHOICE:
|
||||
String itemType = validateQuestionnaireResponseItemType(errors, answer, ns, "Coding", "date", "time", "integer", "string");
|
||||
if (itemType != null) {
|
||||
if (itemType.equals("Coding")) validateAnswerCode(errors, answer, ns, qsrc, qItem, false);
|
||||
else if (itemType.equals("date")) checkOption(errors, answer, ns, qsrc, qItem, "date");
|
||||
else if (itemType.equals("time")) checkOption(errors, answer, ns, qsrc, qItem, "time");
|
||||
else if (itemType.equals("integer"))
|
||||
checkOption(errors, answer, ns, qsrc, qItem, "integer");
|
||||
else if (itemType.equals("string")) checkOption(errors, answer, ns, qsrc, qItem, "string");
|
||||
}
|
||||
break;
|
||||
case OPENCHOICE:
|
||||
itemType = validateQuestionnaireResponseItemType(errors, answer, ns, "Coding", "date", "time", "integer", "string");
|
||||
if (itemType != null) {
|
||||
if (itemType.equals("Coding")) validateAnswerCode(errors, answer, ns, qsrc, qItem, true);
|
||||
else if (itemType.equals("date")) checkOption(errors, answer, ns, qsrc, qItem, "date");
|
||||
else if (itemType.equals("time")) checkOption(errors, answer, ns, qsrc, qItem, "time");
|
||||
else if (itemType.equals("integer"))
|
||||
checkOption(errors, answer, ns, qsrc, qItem, "integer");
|
||||
else if (itemType.equals("string"))
|
||||
checkOption(errors, answer, ns, qsrc, qItem, "string", true);
|
||||
}
|
||||
break;
|
||||
// case QUESTION:
|
||||
case NULL:
|
||||
// no validation
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (qItem.getType() != QuestionnaireItemType.GROUP) {
|
||||
// if it's a group, we already have an error before getting here, so no need to hammer away on that
|
||||
validateQuestionannaireResponseItems(hostContext, qsrc, qItem.getItem(), errors, answer, stack, inProgress, questionnaireResponseRoot, qstack);
|
||||
}
|
||||
}
|
||||
if (qItem.getType() == null) {
|
||||
fail(errors, IssueType.REQUIRED, element.line(), element.col(), stack.getLiteralPath(), false, I18nConstants.QUESTIONNAIRE_QR_ITEM_NOTYPE, qItem.getLinkId());
|
||||
} else if (qItem.getType() == QuestionnaireItemType.DISPLAY) {
|
||||
List<Element> items = new ArrayList<Element>();
|
||||
element.getNamedChildren("item", items);
|
||||
rule(errors, IssueType.STRUCTURE, element.line(), element.col(), stack.getLiteralPath(), items.isEmpty(), I18nConstants.QUESTIONNAIRE_QR_ITEM_DISPLAY, qItem.getLinkId());
|
||||
} else if (qItem.getType() != QuestionnaireItemType.GROUP) {
|
||||
List<Element> items = new ArrayList<Element>();
|
||||
element.getNamedChildren("item", items);
|
||||
rule(errors, IssueType.STRUCTURE, element.line(), element.col(), stack.getLiteralPath(), items.isEmpty(), I18nConstants.QUESTIONNAIRE_QR_ITEM_GROUP_ANSWER, qItem.getLinkId());
|
||||
} else {
|
||||
validateQuestionannaireResponseItems(hostContext, qsrc, qItem.getItem(), errors, element, stack, inProgress, questionnaireResponseRoot, qstack);
|
||||
}
|
||||
}
|
||||
|
||||
private boolean isAnswerRequirementFulfilled(QuestionnaireItemComponent qItem, List<Element> answers) {
|
||||
return !answers.isEmpty() || !qItem.getRequired() || qItem.getType() == QuestionnaireItemType.GROUP;
|
||||
}
|
||||
|
||||
private void validateQuestionnaireResponseItem(ValidatorHostContext hostcontext, Questionnaire qsrc, QuestionnaireItemComponent qItem, List<ValidationMessage> errors, List<Element> elements, NodeStack stack, boolean inProgress, Element questionnaireResponseRoot, QStack qstack) {
|
||||
if (elements.size() > 1)
|
||||
rule(errors, IssueType.INVALID, elements.get(1).line(), elements.get(1).col(), stack.getLiteralPath(), qItem.getRepeats(), I18nConstants.QUESTIONNAIRE_QR_ITEM_ONLYONEI, qItem.getLinkId());
|
||||
int i = 0;
|
||||
for (Element element : elements) {
|
||||
NodeStack ns = stack.push(element, i, null, null);
|
||||
validateQuestionnaireResponseItem(hostcontext, qsrc, qItem, errors, element, ns, inProgress, questionnaireResponseRoot, qstack.push(qItem, element));
|
||||
i++;
|
||||
}
|
||||
}
|
||||
|
||||
private int getLinkIdIndex(List<QuestionnaireItemComponent> qItems, String linkId) {
|
||||
for (int i = 0; i < qItems.size(); i++) {
|
||||
if (linkId.equals(qItems.get(i).getLinkId()))
|
||||
return i;
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
|
||||
private void validateQuestionannaireResponseItems(ValidatorHostContext hostContext, Questionnaire qsrc, List<QuestionnaireItemComponent> qItems, List<ValidationMessage> errors, Element element, NodeStack stack, boolean inProgress, Element questionnaireResponseRoot, QStack qstack) {
|
||||
List<Element> items = new ArrayList<Element>();
|
||||
element.getNamedChildren("item", items);
|
||||
// now, sort into stacks
|
||||
Map<String, List<Element>> map = new HashMap<String, List<Element>>();
|
||||
int lastIndex = -1;
|
||||
for (Element item : items) {
|
||||
String linkId = item.getNamedChildValue("linkId");
|
||||
if (rule(errors, IssueType.REQUIRED, item.line(), item.col(), stack.getLiteralPath(), !Utilities.noString(linkId), I18nConstants.QUESTIONNAIRE_QR_ITEM_NOLINKID)) {
|
||||
int index = getLinkIdIndex(qItems, linkId);
|
||||
if (index == -1) {
|
||||
QuestionnaireItemComponent qItem = findQuestionnaireItem(qsrc, linkId);
|
||||
if (qItem != null) {
|
||||
rule(errors, IssueType.STRUCTURE, item.line(), item.col(), stack.getLiteralPath(), index > -1, misplacedItemError(qItem));
|
||||
NodeStack ns = stack.push(item, -1, null, null);
|
||||
validateQuestionnaireResponseItem(hostContext, qsrc, qItem, errors, item, ns, inProgress, questionnaireResponseRoot, qstack.push(qItem, item));
|
||||
} else
|
||||
rule(errors, IssueType.NOTFOUND, item.line(), item.col(), stack.getLiteralPath(), index > -1, I18nConstants.QUESTIONNAIRE_QR_ITEM_NOTFOUND, linkId);
|
||||
} else {
|
||||
rule(errors, IssueType.STRUCTURE, item.line(), item.col(), stack.getLiteralPath(), index >= lastIndex, I18nConstants.QUESTIONNAIRE_QR_ITEM_ORDER);
|
||||
lastIndex = index;
|
||||
|
||||
// If an item has a child called "linkId" but no child called "answer",
|
||||
// we'll treat it as not existing for the purposes of enableWhen validation
|
||||
if (item.hasChildren("answer") || item.hasChildren("item")) {
|
||||
List<Element> mapItem = map.computeIfAbsent(linkId, key -> new ArrayList<>());
|
||||
mapItem.add(item);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ok, now we have a list of known items, grouped by linkId. We've made an error for anything out of order
|
||||
for (QuestionnaireItemComponent qItem : qItems) {
|
||||
List<Element> mapItem = map.get(qItem.getLinkId());
|
||||
validateQuestionnaireResponseItem(hostContext, qsrc, errors, element, stack, inProgress, questionnaireResponseRoot, qItem, mapItem, qstack);
|
||||
}
|
||||
}
|
||||
|
||||
public void validateQuestionnaireResponseItem(ValidatorHostContext hostContext, Questionnaire qsrc, List<ValidationMessage> errors, Element element, NodeStack stack, boolean inProgress, Element questionnaireResponseRoot, QuestionnaireItemComponent qItem, List<Element> mapItem, QStack qstack) {
|
||||
boolean enabled = myEnableWhenEvaluator.isQuestionEnabled(hostContext, qItem, qstack, fpe);
|
||||
if (mapItem != null) {
|
||||
if (!enabled) {
|
||||
int i = 0;
|
||||
for (Element e : mapItem) {
|
||||
NodeStack ns = stack.push(e, i, e.getProperty().getDefinition(), e.getProperty().getDefinition());
|
||||
rule(errors, IssueType.INVALID, e.line(), e.col(), ns.getLiteralPath(), enabled, I18nConstants.QUESTIONNAIRE_QR_ITEM_NOTENABLED2, qItem.getLinkId());
|
||||
i++;
|
||||
}
|
||||
}
|
||||
|
||||
// Recursively validate child items
|
||||
validateQuestionnaireResponseItem(hostContext, qsrc, qItem, errors, mapItem, stack, inProgress, questionnaireResponseRoot, qstack);
|
||||
|
||||
} else {
|
||||
|
||||
// item is missing, is the question enabled?
|
||||
if (enabled && qItem.getRequired()) {
|
||||
String message = context.formatMessage(I18nConstants.QUESTIONNAIRE_QR_ITEM_MISSING, qItem.getLinkId());
|
||||
if (inProgress) {
|
||||
warning(errors, IssueType.REQUIRED, element.line(), element.col(), stack.getLiteralPath(), false, message);
|
||||
} else {
|
||||
rule(errors, IssueType.REQUIRED, element.line(), element.col(), stack.getLiteralPath(), false, message);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
private String misplacedItemError(QuestionnaireItemComponent qItem) {
|
||||
return qItem.hasLinkId() ? String.format("Structural Error: item with linkid %s is in the wrong place", qItem.getLinkId()) : "Structural Error: item is in the wrong place";
|
||||
}
|
||||
|
||||
private void validateQuestionnaireResponseItemQuantity(List<ValidationMessage> errors, Element answer, NodeStack stack) {
|
||||
|
||||
}
|
||||
|
||||
private String validateQuestionnaireResponseItemType(List<ValidationMessage> errors, Element element, NodeStack stack, String... types) {
|
||||
List<Element> values = new ArrayList<Element>();
|
||||
element.getNamedChildrenWithWildcard("value[x]", values);
|
||||
for (int i = 0; i < types.length; i++) {
|
||||
if (types[i].equals("text")) {
|
||||
types[i] = "string";
|
||||
}
|
||||
}
|
||||
if (values.size() > 0) {
|
||||
NodeStack ns = stack.push(values.get(0), -1, null, null);
|
||||
CommaSeparatedStringBuilder l = new CommaSeparatedStringBuilder();
|
||||
for (String s : types) {
|
||||
l.append(s);
|
||||
if (values.get(0).getName().equals("value" + Utilities.capitalize(s)))
|
||||
return (s);
|
||||
}
|
||||
if (types.length == 1)
|
||||
rule(errors, IssueType.STRUCTURE, values.get(0).line(), values.get(0).col(), ns.getLiteralPath(), false, I18nConstants.QUESTIONNAIRE_QR_ITEM_WRONGTYPE, types[0]);
|
||||
else
|
||||
rule(errors, IssueType.STRUCTURE, values.get(0).line(), values.get(0).col(), ns.getLiteralPath(), false, I18nConstants.QUESTIONNAIRE_QR_ITEM_WRONGTYPE2, l.toString());
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private QuestionnaireItemComponent findQuestionnaireItem(Questionnaire qSrc, String linkId) {
|
||||
return findItem(qSrc.getItem(), linkId);
|
||||
}
|
||||
|
||||
private QuestionnaireItemComponent findItem(List<QuestionnaireItemComponent> list, String linkId) {
|
||||
for (QuestionnaireItemComponent item : list) {
|
||||
if (linkId.equals(item.getLinkId()))
|
||||
return item;
|
||||
QuestionnaireItemComponent result = findItem(item.getItem(), linkId);
|
||||
if (result != null)
|
||||
return result;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private void validateAnswerCode(List<ValidationMessage> errors, Element value, NodeStack stack, Questionnaire qSrc, String ref, boolean theOpenChoice) {
|
||||
ValueSet vs = resolveBindingReference(qSrc, ref, qSrc.getUrl());
|
||||
if (warning(errors, IssueType.CODEINVALID, value.line(), value.col(), stack.getLiteralPath(), vs != null, I18nConstants.TERMINOLOGY_TX_VALUESET_NOTFOUND, describeReference(ref))) {
|
||||
try {
|
||||
Coding c = ObjectConverter.readAsCoding(value);
|
||||
if (isBlank(c.getCode()) && isBlank(c.getSystem()) && isNotBlank(c.getDisplay())) {
|
||||
if (theOpenChoice) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
long t = System.nanoTime();
|
||||
ValidationResult res = context.validateCode(new ValidationOptions(stack.getWorkingLang()), c, vs);
|
||||
timeTracker.tx(t, System.nanoTime());
|
||||
if (!res.isOk()) {
|
||||
txRule(errors, res.getTxLink(), IssueType.CODEINVALID, value.line(), value.col(), stack.getLiteralPath(), false, I18nConstants.QUESTIONNAIRE_QR_ITEM_BADOPTION, c.getSystem(), c.getCode());
|
||||
} else if (res.getSeverity() != null) {
|
||||
super.addValidationMessage(errors, IssueType.CODEINVALID, value.line(), value.col(), stack.getLiteralPath(), res.getMessage(), res.getSeverity(), Source.TerminologyEngine);
|
||||
}
|
||||
} catch (Exception e) {
|
||||
warning(errors, IssueType.CODEINVALID, value.line(), value.col(), stack.getLiteralPath(), false, I18nConstants.QUESTIONNAIRE_QR_ITEM_CODING, e.getMessage());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void validateAnswerCode(List<ValidationMessage> errors, Element answer, NodeStack stack, Questionnaire qSrc, QuestionnaireItemComponent qItem, boolean theOpenChoice) {
|
||||
Element v = answer.getNamedChild("valueCoding");
|
||||
NodeStack ns = stack.push(v, -1, null, null);
|
||||
if (qItem.getAnswerOption().size() > 0)
|
||||
checkCodingOption(errors, answer, stack, qSrc, qItem, theOpenChoice);
|
||||
// validateAnswerCode(errors, v, stack, qItem.getOption());
|
||||
else if (qItem.hasAnswerValueSet())
|
||||
validateAnswerCode(errors, v, stack, qSrc, qItem.getAnswerValueSet(), theOpenChoice);
|
||||
else
|
||||
hint(errors, IssueType.STRUCTURE, v.line(), v.col(), stack.getLiteralPath(), false, I18nConstants.QUESTIONNAIRE_QR_ITEM_NOOPTIONS);
|
||||
}
|
||||
|
||||
private void checkOption(List<ValidationMessage> errors, Element answer, NodeStack stack, Questionnaire qSrc, QuestionnaireItemComponent qItem, String type) {
|
||||
checkOption(errors, answer, stack, qSrc, qItem, type, false);
|
||||
}
|
||||
|
||||
private void checkOption(List<ValidationMessage> errors, Element answer, NodeStack stack, Questionnaire qSrc, QuestionnaireItemComponent qItem, String type, boolean openChoice) {
|
||||
if (type.equals("integer")) checkIntegerOption(errors, answer, stack, qSrc, qItem, openChoice);
|
||||
else if (type.equals("date")) checkDateOption(errors, answer, stack, qSrc, qItem, openChoice);
|
||||
else if (type.equals("time")) checkTimeOption(errors, answer, stack, qSrc, qItem, openChoice);
|
||||
else if (type.equals("string")) checkStringOption(errors, answer, stack, qSrc, qItem, openChoice);
|
||||
else if (type.equals("Coding")) checkCodingOption(errors, answer, stack, qSrc, qItem, openChoice);
|
||||
}
|
||||
|
||||
private void checkIntegerOption(List<ValidationMessage> errors, Element answer, NodeStack stack, Questionnaire qSrc, QuestionnaireItemComponent qItem, boolean openChoice) {
|
||||
Element v = answer.getNamedChild("valueInteger");
|
||||
NodeStack ns = stack.push(v, -1, null, null);
|
||||
if (qItem.getAnswerOption().size() > 0) {
|
||||
List<IntegerType> list = new ArrayList<IntegerType>();
|
||||
for (QuestionnaireItemAnswerOptionComponent components : qItem.getAnswerOption()) {
|
||||
try {
|
||||
list.add(components.getValueIntegerType());
|
||||
} catch (FHIRException e) {
|
||||
// If it's the wrong type, just keep going
|
||||
}
|
||||
}
|
||||
if (list.isEmpty() && !openChoice) {
|
||||
rule(errors, IssueType.STRUCTURE, v.line(), v.col(), stack.getLiteralPath(), false, I18nConstants.QUESTIONNAIRE_QR_ITEM_NOOPTIONSINTEGER);
|
||||
} else {
|
||||
boolean found = false;
|
||||
for (IntegerType item : list) {
|
||||
if (item.getValue() == Integer.parseInt(v.primitiveValue())) {
|
||||
found = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!found) {
|
||||
rule(errors, IssueType.STRUCTURE, v.line(), v.col(), stack.getLiteralPath(), found, I18nConstants.QUESTIONNAIRE_QR_ITEM_NOINTEGER, v.primitiveValue());
|
||||
}
|
||||
}
|
||||
} else
|
||||
hint(errors, IssueType.STRUCTURE, v.line(), v.col(), stack.getLiteralPath(), false, I18nConstants.QUESTIONNAIRE_QR_ITEM_INTNOOPTIONS);
|
||||
}
|
||||
|
||||
private void checkDateOption(List<ValidationMessage> errors, Element answer, NodeStack stack, Questionnaire qSrc, QuestionnaireItemComponent qItem, boolean openChoice) {
|
||||
Element v = answer.getNamedChild("valueDate");
|
||||
NodeStack ns = stack.push(v, -1, null, null);
|
||||
if (qItem.getAnswerOption().size() > 0) {
|
||||
List<DateType> list = new ArrayList<DateType>();
|
||||
for (QuestionnaireItemAnswerOptionComponent components : qItem.getAnswerOption()) {
|
||||
try {
|
||||
list.add(components.getValueDateType());
|
||||
} catch (FHIRException e) {
|
||||
// If it's the wrong type, just keep going
|
||||
}
|
||||
}
|
||||
if (list.isEmpty() && !openChoice) {
|
||||
rule(errors, IssueType.STRUCTURE, v.line(), v.col(), stack.getLiteralPath(), false, I18nConstants.QUESTIONNAIRE_QR_ITEM_NOOPTIONSDATE);
|
||||
} else {
|
||||
boolean found = false;
|
||||
for (DateType item : list) {
|
||||
if (item.getValue().equals(v.primitiveValue())) {
|
||||
found = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!found) {
|
||||
rule(errors, IssueType.STRUCTURE, v.line(), v.col(), stack.getLiteralPath(), found, I18nConstants.QUESTIONNAIRE_QR_ITEM_NODATE, v.primitiveValue());
|
||||
}
|
||||
}
|
||||
} else
|
||||
hint(errors, IssueType.STRUCTURE, v.line(), v.col(), stack.getLiteralPath(), false, I18nConstants.QUESTIONNAIRE_QR_ITEM_DATENOOPTIONS);
|
||||
}
|
||||
|
||||
private void checkTimeOption(List<ValidationMessage> errors, Element answer, NodeStack stack, Questionnaire qSrc, QuestionnaireItemComponent qItem, boolean openChoice) {
|
||||
Element v = answer.getNamedChild("valueTime");
|
||||
NodeStack ns = stack.push(v, -1, null, null);
|
||||
if (qItem.getAnswerOption().size() > 0) {
|
||||
List<TimeType> list = new ArrayList<TimeType>();
|
||||
for (QuestionnaireItemAnswerOptionComponent components : qItem.getAnswerOption()) {
|
||||
try {
|
||||
list.add(components.getValueTimeType());
|
||||
} catch (FHIRException e) {
|
||||
// If it's the wrong type, just keep going
|
||||
}
|
||||
}
|
||||
if (list.isEmpty() && !openChoice) {
|
||||
rule(errors, IssueType.STRUCTURE, v.line(), v.col(), stack.getLiteralPath(), false, I18nConstants.QUESTIONNAIRE_QR_ITEM_NOOPTIONSTIME);
|
||||
} else {
|
||||
boolean found = false;
|
||||
for (TimeType item : list) {
|
||||
if (item.getValue().equals(v.primitiveValue())) {
|
||||
found = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!found) {
|
||||
rule(errors, IssueType.STRUCTURE, v.line(), v.col(), stack.getLiteralPath(), found, I18nConstants.QUESTIONNAIRE_QR_ITEM_NOTIME, v.primitiveValue());
|
||||
}
|
||||
}
|
||||
} else
|
||||
hint(errors, IssueType.STRUCTURE, v.line(), v.col(), stack.getLiteralPath(), false, I18nConstants.QUESTIONNAIRE_QR_ITEM_TIMENOOPTIONS);
|
||||
}
|
||||
|
||||
private void checkStringOption(List<ValidationMessage> errors, Element answer, NodeStack stack, Questionnaire qSrc, QuestionnaireItemComponent qItem, boolean openChoice) {
|
||||
Element v = answer.getNamedChild("valueString");
|
||||
NodeStack ns = stack.push(v, -1, null, null);
|
||||
if (qItem.getAnswerOption().size() > 0) {
|
||||
List<StringType> list = new ArrayList<StringType>();
|
||||
for (QuestionnaireItemAnswerOptionComponent components : qItem.getAnswerOption()) {
|
||||
try {
|
||||
if (components.getValue() != null) {
|
||||
list.add(components.getValueStringType());
|
||||
}
|
||||
} catch (FHIRException e) {
|
||||
// If it's the wrong type, just keep going
|
||||
}
|
||||
}
|
||||
if (!openChoice) {
|
||||
if (list.isEmpty()) {
|
||||
rule(errors, IssueType.STRUCTURE, v.line(), v.col(), stack.getLiteralPath(), false, I18nConstants.QUESTIONNAIRE_QR_ITEM_NOOPTIONSSTRING);
|
||||
} else {
|
||||
boolean found = false;
|
||||
for (StringType item : list) {
|
||||
if (item.getValue().equals((v.primitiveValue()))) {
|
||||
found = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!found) {
|
||||
rule(errors, IssueType.STRUCTURE, v.line(), v.col(), stack.getLiteralPath(), found, I18nConstants.QUESTIONNAIRE_QR_ITEM_NOSTRING, v.primitiveValue());
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
hint(errors, IssueType.STRUCTURE, v.line(), v.col(), stack.getLiteralPath(), false, I18nConstants.QUESTIONNAIRE_QR_ITEM_STRINGNOOPTIONS);
|
||||
}
|
||||
}
|
||||
|
||||
private void checkCodingOption(List<ValidationMessage> errors, Element answer, NodeStack stack, Questionnaire qSrc, QuestionnaireItemComponent qItem, boolean openChoice) {
|
||||
Element v = answer.getNamedChild("valueCoding");
|
||||
String system = v.getNamedChildValue("system");
|
||||
String code = v.getNamedChildValue("code");
|
||||
NodeStack ns = stack.push(v, -1, null, null);
|
||||
if (qItem.getAnswerOption().size() > 0) {
|
||||
List<Coding> list = new ArrayList<Coding>();
|
||||
for (QuestionnaireItemAnswerOptionComponent components : qItem.getAnswerOption()) {
|
||||
try {
|
||||
if (components.getValue() != null) {
|
||||
list.add(components.getValueCoding());
|
||||
}
|
||||
} catch (FHIRException e) {
|
||||
// If it's the wrong type, just keep going
|
||||
}
|
||||
}
|
||||
if (list.isEmpty() && !openChoice) {
|
||||
rule(errors, IssueType.STRUCTURE, v.line(), v.col(), stack.getLiteralPath(), false, I18nConstants.QUESTIONNAIRE_QR_ITEM_NOOPTIONSCODING);
|
||||
} else {
|
||||
boolean found = false;
|
||||
for (Coding item : list) {
|
||||
if (ObjectUtil.equals(item.getSystem(), system) && ObjectUtil.equals(item.getCode(), code)) {
|
||||
found = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!found) {
|
||||
rule(errors, IssueType.STRUCTURE, v.line(), v.col(), stack.getLiteralPath(), found, I18nConstants.QUESTIONNAIRE_QR_ITEM_NOCODING, system, code);
|
||||
}
|
||||
}
|
||||
} else
|
||||
hint(errors, IssueType.STRUCTURE, v.line(), v.col(), stack.getLiteralPath(), false, I18nConstants.QUESTIONNAIRE_QR_ITEM_CODINGNOOPTIONS);
|
||||
}
|
||||
|
||||
|
||||
}
|
@ -0,0 +1,12 @@
|
||||
package org.hl7.fhir.validation.instance.utils;
|
||||
|
||||
import org.hl7.fhir.r5.model.Library;
|
||||
|
||||
public class MeasureContext {
|
||||
|
||||
public void seeLibrary(Library l) {
|
||||
// TODO Auto-generated method stub
|
||||
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,177 @@
|
||||
package org.hl7.fhir.validation.instance.utils;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashMap;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
|
||||
import org.hl7.fhir.r5.context.IWorkerContext;
|
||||
import org.hl7.fhir.r5.elementmodel.Element;
|
||||
import org.hl7.fhir.r5.model.ElementDefinition;
|
||||
import org.hl7.fhir.r5.model.StructureDefinition;
|
||||
import org.hl7.fhir.r5.model.StructureDefinition.StructureDefinitionKind;
|
||||
import org.hl7.fhir.utilities.Utilities;
|
||||
|
||||
public class NodeStack {
|
||||
|
||||
protected IWorkerContext context;
|
||||
private ElementDefinition definition;
|
||||
private Element element;
|
||||
private ElementDefinition extension;
|
||||
private String literalPath; // xpath format
|
||||
private List<String> logicalPaths; // dotted format, various entry points
|
||||
private NodeStack parent;
|
||||
private ElementDefinition type;
|
||||
private String workingLang;
|
||||
private Map<String, Element> ids;
|
||||
|
||||
public NodeStack(IWorkerContext context) {
|
||||
this.context = context;
|
||||
}
|
||||
|
||||
public NodeStack(IWorkerContext context, Element element, String validationLanguage) {
|
||||
this.context = context;
|
||||
ids = new HashMap<>();
|
||||
this.element = element;
|
||||
literalPath = element.getName();
|
||||
workingLang = validationLanguage;
|
||||
if (!element.getName().equals(element.fhirType())) {
|
||||
logicalPaths = new ArrayList<>();
|
||||
logicalPaths.add(element.fhirType());
|
||||
}
|
||||
}
|
||||
|
||||
public NodeStack(IWorkerContext context, Element element, String refPath, String validationLanguage) {
|
||||
this.context = context;
|
||||
ids = new HashMap<>();
|
||||
this.element = element;
|
||||
literalPath = refPath + "->" + element.getName();
|
||||
workingLang = validationLanguage;
|
||||
}
|
||||
|
||||
public String addToLiteralPath(String... path) {
|
||||
StringBuilder b = new StringBuilder();
|
||||
b.append(getLiteralPath());
|
||||
for (String p : path) {
|
||||
if (p.startsWith(":")) {
|
||||
b.append("[");
|
||||
b.append(p.substring(1));
|
||||
b.append("]");
|
||||
} else {
|
||||
b.append(".");
|
||||
b.append(p);
|
||||
}
|
||||
}
|
||||
return b.toString();
|
||||
}
|
||||
|
||||
private ElementDefinition getDefinition() {
|
||||
return definition;
|
||||
}
|
||||
|
||||
public Element getElement() {
|
||||
return element;
|
||||
}
|
||||
|
||||
public String getLiteralPath() {
|
||||
return literalPath == null ? "" : literalPath;
|
||||
}
|
||||
|
||||
public List<String> getLogicalPaths() {
|
||||
return logicalPaths == null ? new ArrayList<String>() : logicalPaths;
|
||||
}
|
||||
|
||||
private ElementDefinition getType() {
|
||||
return type;
|
||||
}
|
||||
|
||||
public NodeStack pushTarget(Element element, int count, ElementDefinition definition, ElementDefinition type) {
|
||||
return pushInternal(element, count, definition, type, "->");
|
||||
}
|
||||
|
||||
public NodeStack push(Element element, int count, ElementDefinition definition, ElementDefinition type) {
|
||||
return pushInternal(element, count, definition, type, ".");
|
||||
}
|
||||
|
||||
private NodeStack pushInternal(Element element, int count, ElementDefinition definition, ElementDefinition type, String sep) {
|
||||
NodeStack res = new NodeStack(context);
|
||||
res.ids = ids;
|
||||
res.parent = this;
|
||||
res.workingLang = this.workingLang;
|
||||
res.element = element;
|
||||
res.definition = definition;
|
||||
res.literalPath = getLiteralPath() + sep + element.getName();
|
||||
if (count > -1)
|
||||
res.literalPath = res.literalPath + "[" + Integer.toString(count) + "]";
|
||||
else if (element.getSpecial() == null && element.getProperty().isList())
|
||||
res.literalPath = res.literalPath + "[0]";
|
||||
else if (element.getProperty().isChoice()) {
|
||||
String n = res.literalPath.substring(res.literalPath.lastIndexOf(".") + 1);
|
||||
String en = element.getProperty().getName();
|
||||
en = en.substring(0, en.length() - 3);
|
||||
String t = n.substring(en.length());
|
||||
if (isPrimitiveType(Utilities.uncapitalize(t)))
|
||||
t = Utilities.uncapitalize(t);
|
||||
res.literalPath = res.literalPath.substring(0, res.literalPath.lastIndexOf(".")) + "." + en + ".ofType(" + t + ")";
|
||||
}
|
||||
res.logicalPaths = new ArrayList<String>();
|
||||
if (type != null) {
|
||||
// type will be bull if we on a stitching point of a contained resource, or if....
|
||||
res.type = type;
|
||||
String tn = res.type.getPath();
|
||||
String t = tail(definition.getPath());
|
||||
if ("Resource".equals(tn)) {
|
||||
tn = element.fhirType();
|
||||
}
|
||||
for (String lp : getLogicalPaths()) {
|
||||
res.logicalPaths.add(lp + "." + t);
|
||||
if (t.endsWith("[x]"))
|
||||
res.logicalPaths.add(lp + "." + t.substring(0, t.length() - 3) + type.getPath());
|
||||
}
|
||||
res.logicalPaths.add(tn);
|
||||
} else if (definition != null) {
|
||||
for (String lp : getLogicalPaths()) {
|
||||
res.logicalPaths.add(lp + "." + element.getName());
|
||||
}
|
||||
res.logicalPaths.add(definition.typeSummary());
|
||||
} else
|
||||
res.logicalPaths.addAll(getLogicalPaths());
|
||||
return res;
|
||||
}
|
||||
|
||||
private void setType(ElementDefinition type) {
|
||||
this.type = type;
|
||||
}
|
||||
|
||||
public NodeStack resetIds() {
|
||||
ids = new HashMap<>();
|
||||
return this;
|
||||
}
|
||||
public Map<String, Element> getIds() {
|
||||
return ids;
|
||||
}
|
||||
private String tail(String path) {
|
||||
return path.substring(path.lastIndexOf(".") + 1);
|
||||
}
|
||||
|
||||
public boolean isPrimitiveType(String code) {
|
||||
StructureDefinition sd = context.fetchTypeDefinition(code);
|
||||
return sd != null && sd.getKind() == StructureDefinitionKind.PRIMITIVETYPE;
|
||||
}
|
||||
|
||||
public String getWorkingLang() {
|
||||
return workingLang;
|
||||
}
|
||||
|
||||
public void setWorkingLang(String workingLang) {
|
||||
this.workingLang = workingLang;
|
||||
}
|
||||
|
||||
public NodeStack getParent() {
|
||||
return parent;
|
||||
}
|
||||
|
||||
|
||||
}
|
@ -9,7 +9,7 @@ public class ResolvedReference {
|
||||
private Element resource;
|
||||
private Element focus;
|
||||
private boolean external;
|
||||
private InstanceValidator.NodeStack stack;
|
||||
private NodeStack stack;
|
||||
|
||||
public ResolvedReference setResource(Element resource) {
|
||||
this.resource = resource;
|
||||
@ -34,12 +34,12 @@ public class ResolvedReference {
|
||||
return this;
|
||||
}
|
||||
|
||||
public ResolvedReference setStack(InstanceValidator.NodeStack stack) {
|
||||
public ResolvedReference setStack(NodeStack stack) {
|
||||
this.stack = stack;
|
||||
return this;
|
||||
}
|
||||
|
||||
public InstanceValidator.NodeStack getStack() {
|
||||
public NodeStack getStack() {
|
||||
return stack;
|
||||
}
|
||||
|
||||
|
@ -141,6 +141,11 @@ public class ValidationTestSuite implements IEvaluationContext, IValidatorResour
|
||||
else
|
||||
val.setValidationLanguage(null);
|
||||
val.setFetcher(this);
|
||||
if (content.has("packages")) {
|
||||
for (JsonElement e : content.getAsJsonArray("packages")) {
|
||||
vCurr.loadIg(e.getAsString(), true);
|
||||
}
|
||||
}
|
||||
if (content.has("questionnaire")) {
|
||||
String filename = content.get("questionnaire").getAsString();
|
||||
String contents = TestingUtilities.loadTestResource("validator", filename);
|
||||
@ -151,6 +156,11 @@ public class ValidationTestSuite implements IEvaluationContext, IValidatorResour
|
||||
String contents = TestingUtilities.loadTestResource("validator", filename);
|
||||
vCurr.getContext().cacheResource(loadResource(filename, contents));
|
||||
}
|
||||
if (content.has("library")) {
|
||||
String filename = content.get("library").getAsString();
|
||||
String contents = TestingUtilities.loadTestResource("validator", filename);
|
||||
vCurr.getContext().cacheResource(loadResource(filename, contents));
|
||||
}
|
||||
if (content.has("codesystems")) {
|
||||
for (JsonElement je : content.getAsJsonArray("codesystems")) {
|
||||
String filename = je.getAsString();
|
||||
@ -320,13 +330,11 @@ public class ValidationTestSuite implements IEvaluationContext, IValidatorResour
|
||||
}
|
||||
if (vm.getLevel() == IssueSeverity.WARNING) {
|
||||
wc++;
|
||||
System.out.println("warning: "+vm.getDisplay());
|
||||
System.out.println(vm.getDisplay());
|
||||
}
|
||||
if (vm.getLevel() == IssueSeverity.INFORMATION) {
|
||||
hc++;
|
||||
if (java.has("infoCount") || java.has("debug")) {
|
||||
System.out.println("hint: "+vm.getDisplay());
|
||||
}
|
||||
System.out.println(vm.getDisplay());
|
||||
}
|
||||
}
|
||||
if (!TestingUtilities.context(version).isNoTerminologyServer() || !focus.has("tx-dependent")) {
|
||||
|
Loading…
x
Reference in New Issue
Block a user