Validation Derived Questionnaires

This commit is contained in:
Grahame Grieve 2023-06-17 09:41:06 +10:00
parent 99895e7a97
commit 64f84274af
7 changed files with 314 additions and 11 deletions

View File

@ -1994,4 +1994,8 @@ public class Utilities {
return txt.split("\\r?\\n|\\r"); return txt.split("\\r?\\n|\\r");
} }
public static boolean isIgnorableFile(File file) {
return Utilities.existsInList(file.getName(), ".DS_Store");
}
} }

View File

@ -884,6 +884,30 @@ public class I18nConstants {
public static final String SD_OBGLIGATION_INHERITS_PROFILE_TARGET_NOT_FOUND = "SD_OBGLIGATION_INHERITS_PROFILE_TARGET_NOT_FOUND"; public static final String SD_OBGLIGATION_INHERITS_PROFILE_TARGET_NOT_FOUND = "SD_OBGLIGATION_INHERITS_PROFILE_TARGET_NOT_FOUND";
public static final String SD_OBGLIGATION_INHERITS_PROFILE_NOT_RIGHT_TYPE = "SD_OBGLIGATION_INHERITS_PROFILE_NOT_RIGHT_TYPE"; public static final String SD_OBGLIGATION_INHERITS_PROFILE_NOT_RIGHT_TYPE = "SD_OBGLIGATION_INHERITS_PROFILE_NOT_RIGHT_TYPE";
public static final String SD_OBGLIGATION_INHERITS_PROFILE_NOT_RIGHT_BASE = "SD_OBGLIGATION_INHERITS_PROFILE_NOT_RIGHT_BASE"; public static final String SD_OBGLIGATION_INHERITS_PROFILE_NOT_RIGHT_BASE = "SD_OBGLIGATION_INHERITS_PROFILE_NOT_RIGHT_BASE";
public static final String RND_CS_CONTENT_COMPLETE = "RND_CS_CONTENT_COMPLETE";
public static final String RND_CS_CONTENT_EXAMPLE = "RND_CS_CONTENT_EXAMPLE";
public static final String RND_CS_CONTENT_FRAGMENT = "RND_CS_CONTENT_FRAGMENT";
public static final String RND_CS_CONTENT_NOTPRESENT = "RND_CS_CONTENT_NOTPRESENT";
public static final String RND_CS_CONTENT_SUPPLEMENT = "RND_CS_CONTENT_SUPPLEMENT";
public static final String QUESTIONNAIRE_Q_UNKNOWN_DERIVATION = "QUESTIONNAIRE_Q_UNKNOWN_DERIVATION";
public static final String QUESTIONNAIRE_Q_NO_DERIVATION_TYPE = "QUESTIONNAIRE_Q_NO_DERIVATION_TYPE";
public static final String QUESTIONNAIRE_Q_NO_DERIVATION_TYPE_VALUE = "QUESTIONNAIRE_Q_NO_DERIVATION_TYPE_VALUE";
public static final String QUESTIONNAIRE_Q_DERIVATION_TYPE_IGNORED = "QUESTIONNAIRE_Q_DERIVATION_TYPE_IGNORED";
public static final String QUESTIONNAIRE_Q_DERIVATION_TYPE_UNKNOWN = "QUESTIONNAIRE_Q_DERIVATION_TYPE_UNKNOWN";
public static final String QUESTIONNAIRE_Q_ITEM_NOT_DERIVED = "QUESTIONNAIRE_Q_ITEM_NOT_DERIVED";
public static final String QUESTIONNAIRE_Q_ITEM_DERIVED_NC_TYPE = "QUESTIONNAIRE_Q_ITEM_DERIVED_NC_TYPE";
public static final String QUESTIONNAIRE_Q_ITEM_DERIVED_NC_REPEATS = "QUESTIONNAIRE_Q_ITEM_DERIVED_NC_REPEATS";
public static final String QUESTIONNAIRE_Q_ITEM_DERIVED_NC_REQUIRED = "QUESTIONNAIRE_Q_ITEM_DERIVED_NC_REQUIRED";
public static final String QUESTIONNAIRE_Q_ITEM_DERIVED_NC_DEFINITION = "QUESTIONNAIRE_Q_ITEM_DERIVED_NC_DEFINITION";
public static final String QUESTIONNAIRE_Q_ITEM_DERIVED_DEFINITION = "QUESTIONNAIRE_Q_ITEM_DERIVED_DEFINITION";
public static final String QUESTIONNAIRE_Q_ITEM_DERIVED_NC_MAXLENGTH = "QUESTIONNAIRE_Q_ITEM_DERIVED_NC_MAXLENGTH";
public static final String QUESTIONNAIRE_Q_ITEM_DERIVED_MAXLENGTH = "QUESTIONNAIRE_Q_ITEM_DERIVED_MAXLENGTH";
public static final String QUESTIONNAIRE_Q_ITEM_DERIVED_NC_ANSWER_TYPE = "QUESTIONNAIRE_Q_ITEM_DERIVED_NC_ANSWER_TYPE";
public static final String QUESTIONNAIRE_Q_ITEM_DERIVED_NI_ANSWER_VS = "QUESTIONNAIRE_Q_ITEM_DERIVED_NI_ANSWER_VS";
public static final String QUESTIONNAIRE_Q_ITEM_DERIVED_ANSWER_OPTIONS = "QUESTIONNAIRE_Q_ITEM_DERIVED_ANSWER_OPTIONS";
public static final String QUESTIONNAIRE_Q_ITEM_DERIVED_ANSWER_OPTIONS_NEW = "QUESTIONNAIRE_Q_ITEM_DERIVED_ANSWER_OPTIONS_NEW";
} }

View File

@ -523,6 +523,7 @@ public class ValidationMessage implements Comparator<ValidationMessage>, Compara
private Date ruleDate; private Date ruleDate;
public static final String NO_RULE_DATE = null; public static final String NO_RULE_DATE = null;
private boolean matched; // internal use counting matching filters private boolean matched; // internal use counting matching filters
private boolean ignorableError;
/** /**
@ -850,6 +851,15 @@ public class ValidationMessage implements Comparator<ValidationMessage>, Compara
public void setMatched(boolean matched) { public void setMatched(boolean matched) {
this.matched = matched; this.matched = matched;
} }
public boolean isIgnorableError() {
return ignorableError;
}
public ValidationMessage setIgnorableError(boolean ignorableError) {
this.ignorableError = ignorableError;
return this;
}
} }

View File

@ -938,4 +938,26 @@ SD_OBGLIGATION_INHERITS_PROFILE_NO_TARGET = Unable to read a value from this ext
SD_OBGLIGATION_INHERITS_PROFILE_TARGET_NOT_FOUND = The profile ''{0}'' could not be found SD_OBGLIGATION_INHERITS_PROFILE_TARGET_NOT_FOUND = The profile ''{0}'' could not be found
SD_OBGLIGATION_INHERITS_PROFILE_NOT_RIGHT_TYPE = The profile ''{0}'' is not marked as an obligation profile SD_OBGLIGATION_INHERITS_PROFILE_NOT_RIGHT_TYPE = The profile ''{0}'' is not marked as an obligation profile
SD_OBGLIGATION_INHERITS_PROFILE_NOT_RIGHT_BASE = The profile ''{0}'' has a different base ''{1}'' from that expected ''{2}'' SD_OBGLIGATION_INHERITS_PROFILE_NOT_RIGHT_BASE = The profile ''{0}'' has a different base ''{1}'' from that expected ''{2}''
RND_CS_CONTENT_COMPLETE = This code system <param name="cs"/> defines the following code<if test="code-count != 1">s</if>:
RND_CS_CONTENT_EXAMPLE = This code system <param name="cs"/> provides some example code<if test="code-count != 1">s</if>:
RND_CS_CONTENT_FRAGMENT = This code system <param name="cs"/> provides a fragment that includes following code<if test="code-count != 1">s</if>:
RND_CS_CONTENT_NOTPRESENT = This code system <param name="cs"/> defines codes, but no codes are represented here
RND_CS_CONTENT_SUPPLEMENT = This code system <param name="cs"/> defines {0} on the following code<if test="code-count != 1">s</if>:
QUESTIONNAIRE_Q_UNKNOWN_DERIVATION = The questionnaire ''{0}'' referred to in the derivation could not be found
QUESTIONNAIRE_Q_NO_DERIVATION_TYPE = The questionnaire ''{0}'' has no derivation type specified using the ''http://hl7.org/fhir/StructureDefinition/questionnaire-derivationType'' extension, so derivation has not been checked
QUESTIONNAIRE_Q_NO_DERIVATION_TYPE_VALUE = The derivation extension has no value
QUESTIONNAIRE_Q_DERIVATION_TYPE_IGNORED = The derivation type ''{0}'' means that no derivation checking has been performed against this questionnaire
QUESTIONNAIRE_Q_DERIVATION_TYPE_UNKNOWN = The derivation type ''{0}'' is unknown, which means that no derivation checking has been performed against this questionnaire
QUESTIONNAIRE_Q_ITEM_NOT_DERIVED = No item with linkId ''{1}'' found in questionnaire ''{0}''
QUESTIONNAIRE_Q_ITEM_DERIVED_NC_TYPE = The item with linkId ''{1}'' found in questionnaire ''{0}'' has the type ''{2}'', and this cannot change to ''{3}''
QUESTIONNAIRE_Q_ITEM_DERIVED_NC_REPEATS = The item with linkId ''{1}'' found in questionnaire ''{0}'' does not repeat, so it cannot repeat here
QUESTIONNAIRE_Q_ITEM_DERIVED_NC_REQUIRED = The item with linkId ''{1}'' found in questionnaire ''{0}'' is required, so it must be required here
QUESTIONNAIRE_Q_ITEM_DERIVED_NC_DEFINITION = The item with linkId ''{1}'' found in questionnaire ''{0}'' has the definition ''{2}''. Is it intended to change this?
QUESTIONNAIRE_Q_ITEM_DERIVED_DEFINITION = The item with linkId ''{1}'' found in questionnaire ''{0}'' has the definition ''{2}'', so this should be repeated here
QUESTIONNAIRE_Q_ITEM_DERIVED_NC_MAXLENGTH = The item with linkId ''{1}'' found in questionnaire ''{0}'' has the maxLength of ''{2}'', so the max length cannot be greater than that
QUESTIONNAIRE_Q_ITEM_DERIVED_MAXLENGTH = The item with linkId ''{1}'' found in questionnaire ''{0}'' has the definition ''{2}'', so this item must also have a max length
QUESTIONNAIRE_Q_ITEM_DERIVED_NC_ANSWER_TYPE = The item with linkId ''{1}'' found in questionnaire ''{0}'' has answer{2}, while this has answer{3}. This might be valid, but the vaidator can''t check that (yet?)
QUESTIONNAIRE_Q_ITEM_DERIVED_NI_ANSWER_VS = The validator can''t check derived item value set consistency (yet?)
QUESTIONNAIRE_Q_ITEM_DERIVED_ANSWER_OPTIONS = The item with linkId ''{1}'' found in questionnaire ''{0}'' has answerOptions, so this item must have some too
QUESTIONNAIRE_Q_ITEM_DERIVED_ANSWER_OPTIONS_NEW = The item with linkId ''{1}'' found in questionnaire ''{0}'' does not have this answerOption, so it is not valid

View File

@ -72,6 +72,7 @@ import org.hl7.fhir.utilities.validation.ValidationMessage.IssueType;
import org.hl7.fhir.utilities.validation.ValidationMessage.Source; import org.hl7.fhir.utilities.validation.ValidationMessage.Source;
import org.hl7.fhir.validation.cli.utils.ValidationLevel; import org.hl7.fhir.validation.cli.utils.ValidationLevel;
import org.hl7.fhir.validation.instance.utils.IndexedElement; import org.hl7.fhir.validation.instance.utils.IndexedElement;
import org.hl7.fhir.validation.instance.utils.NodeStack;
public class BaseValidator implements IValidationContextResourceLoader { public class BaseValidator implements IValidationContextResourceLoader {
@ -297,6 +298,10 @@ public class BaseValidator implements IValidationContextResourceLoader {
return thePass; return thePass;
} }
protected boolean hint(List<ValidationMessage> errors, String ruleDate, IssueType type, NodeStack stack, boolean thePass, String msg, Object... theMessageArguments) {
return hint(errors, ruleDate, type, stack.line(), stack.col(), stack.getLiteralPath(), thePass, msg, theMessageArguments);
}
/** /**
* Test a rule and add a {@link IssueSeverity#INFORMATION} validation message if the validation fails. And mark it as a slicing hint for later recovery if appropriate * Test a rule and add a {@link IssueSeverity#INFORMATION} validation message if the validation fails. And mark it as a slicing hint for later recovery if appropriate
* *
@ -394,6 +399,14 @@ public class BaseValidator implements IValidationContextResourceLoader {
return thePass; return thePass;
} }
protected boolean rule(List<ValidationMessage> errors, String ruleDate, IssueType type, NodeStack stack, boolean thePass, String theMessage, Object... theMessageArguments) {
if (!thePass && doingErrors()) {
String message = context.formatMessage(theMessage, theMessageArguments);
addValidationMessage(errors, ruleDate, type, stack.line(), stack.col(), stack.getLiteralPath(), message, IssueSeverity.ERROR, theMessage);
}
return thePass;
}
protected boolean rulePlural(List<ValidationMessage> errors, String ruleDate, IssueType type, int line, int col, String path, boolean thePass, int num, String theMessage, Object... theMessageArguments) { protected boolean rulePlural(List<ValidationMessage> errors, String ruleDate, IssueType type, int line, int col, String path, boolean thePass, int num, String theMessage, Object... theMessageArguments) {
if (!thePass && doingErrors()) { if (!thePass && doingErrors()) {
String message = context.formatMessagePlural(num, theMessage, theMessageArguments); String message = context.formatMessagePlural(num, theMessage, theMessageArguments);
@ -538,6 +551,10 @@ public class BaseValidator implements IValidationContextResourceLoader {
} }
protected boolean warning(List<ValidationMessage> errors, String ruleDate, IssueType type, NodeStack stack, boolean thePass, String msg, Object... theMessageArguments) {
return warning(errors, ruleDate, type, stack.line(), stack.col(), stack.getLiteralPath(), thePass, msg, theMessageArguments);
}
protected boolean warningPlural(List<ValidationMessage> errors, String ruleDate, IssueType type, int line, int col, String path, boolean thePass, int num, String msg, Object... theMessageArguments) { protected boolean warningPlural(List<ValidationMessage> errors, String ruleDate, IssueType type, int line, int col, String path, boolean thePass, int num, String msg, Object... theMessageArguments) {
if (!thePass && doingWarnings()) { if (!thePass && doingWarnings()) {
String nmsg = context.formatMessagePlural(num, msg, theMessageArguments); String nmsg = context.formatMessagePlural(num, msg, theMessageArguments);

View File

@ -8,23 +8,27 @@ import java.util.HashMap;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
import org.hl7.fhir.QuestionnaireItem;
import org.hl7.fhir.exceptions.FHIRException; import org.hl7.fhir.exceptions.FHIRException;
import org.hl7.fhir.r5.context.IWorkerContext; import org.hl7.fhir.r5.context.IWorkerContext;
import org.hl7.fhir.r5.context.IWorkerContext.ValidationResult; import org.hl7.fhir.r5.context.IWorkerContext.ValidationResult;
import org.hl7.fhir.r5.elementmodel.Element; import org.hl7.fhir.r5.elementmodel.Element;
import org.hl7.fhir.r5.elementmodel.ObjectConverter; import org.hl7.fhir.r5.elementmodel.ObjectConverter;
import org.hl7.fhir.r5.model.Coding; import org.hl7.fhir.r5.model.Coding;
import org.hl7.fhir.r5.model.DataType;
import org.hl7.fhir.r5.model.DateType; import org.hl7.fhir.r5.model.DateType;
import org.hl7.fhir.r5.model.IntegerType; import org.hl7.fhir.r5.model.IntegerType;
import org.hl7.fhir.r5.model.Questionnaire; import org.hl7.fhir.r5.model.Questionnaire;
import org.hl7.fhir.r5.model.Questionnaire.QuestionnaireItemAnswerOptionComponent; import org.hl7.fhir.r5.model.Questionnaire.QuestionnaireItemAnswerOptionComponent;
import org.hl7.fhir.r5.model.Questionnaire.QuestionnaireItemComponent; import org.hl7.fhir.r5.model.Questionnaire.QuestionnaireItemComponent;
import org.hl7.fhir.r5.model.Questionnaire.QuestionnaireItemType; import org.hl7.fhir.r5.model.Questionnaire.QuestionnaireItemType;
import org.hl7.fhir.r5.model.Reference;
import org.hl7.fhir.r5.terminologies.utilities.TerminologyServiceErrorClass; import org.hl7.fhir.r5.terminologies.utilities.TerminologyServiceErrorClass;
import org.hl7.fhir.r5.model.StringType; import org.hl7.fhir.r5.model.StringType;
import org.hl7.fhir.r5.model.TimeType; import org.hl7.fhir.r5.model.TimeType;
import org.hl7.fhir.r5.model.ValueSet; import org.hl7.fhir.r5.model.ValueSet;
import org.hl7.fhir.r5.utils.FHIRPathEngine; import org.hl7.fhir.r5.utils.FHIRPathEngine;
import org.hl7.fhir.r5.utils.ToolingExtensions;
import org.hl7.fhir.r5.utils.XVerExtensionManager; import org.hl7.fhir.r5.utils.XVerExtensionManager;
import org.hl7.fhir.r5.utils.validation.ValidationContextCarrier; import org.hl7.fhir.r5.utils.validation.ValidationContextCarrier;
import org.hl7.fhir.r5.utils.validation.ValidationContextCarrier.ValidationContextResourceProxy; import org.hl7.fhir.r5.utils.validation.ValidationContextCarrier.ValidationContextResourceProxy;
@ -42,6 +46,8 @@ import org.hl7.fhir.validation.TimeTracker;
import org.hl7.fhir.validation.cli.utils.QuestionnaireMode; import org.hl7.fhir.validation.cli.utils.QuestionnaireMode;
import org.hl7.fhir.validation.instance.EnableWhenEvaluator; import org.hl7.fhir.validation.instance.EnableWhenEvaluator;
import org.hl7.fhir.validation.instance.EnableWhenEvaluator.QStack; import org.hl7.fhir.validation.instance.EnableWhenEvaluator.QStack;
import org.hl7.fhir.validation.instance.type.QuestionnaireValidator.QuestionnaireDerivation;
import org.hl7.fhir.validation.instance.type.QuestionnaireValidator.QuestionnaireDerivationMode;
import org.hl7.fhir.validation.instance.utils.NodeStack; import org.hl7.fhir.validation.instance.utils.NodeStack;
import org.hl7.fhir.validation.instance.utils.ValidatorHostContext; import org.hl7.fhir.validation.instance.utils.ValidatorHostContext;
@ -50,6 +56,29 @@ import ca.uhn.fhir.util.ObjectUtil;
public class QuestionnaireValidator extends BaseValidator { public class QuestionnaireValidator extends BaseValidator {
public enum QuestionnaireDerivationMode {
EXTENDS, COMPLIES
}
public class QuestionnaireDerivation {
private Questionnaire questionnaire;
private QuestionnaireDerivationMode mode;
protected QuestionnaireDerivation(Questionnaire questionnaire, QuestionnaireDerivationMode mode) {
super();
this.questionnaire = questionnaire;
this.mode = mode;
}
public Questionnaire getQuestionnaire() {
return questionnaire;
}
public QuestionnaireDerivationMode getMode() {
return mode;
}
}
public class ElementWithIndex { public class ElementWithIndex {
private Element element; private Element element;
@ -98,7 +127,6 @@ public class QuestionnaireValidator extends BaseValidator {
public Questionnaire q() { public Questionnaire q() {
return q; return q;
} }
} }
private EnableWhenEvaluator myEnableWhenEvaluator; private EnableWhenEvaluator myEnableWhenEvaluator;
@ -118,39 +146,75 @@ public class QuestionnaireValidator extends BaseValidator {
public boolean validateQuestionannaire(List<ValidationMessage> errors, Element element, Element element2, NodeStack stack) { public boolean validateQuestionannaire(List<ValidationMessage> errors, Element element, Element element2, NodeStack stack) {
ArrayList<Element> parents = new ArrayList<>(); ArrayList<Element> parents = new ArrayList<>();
parents.add(element); parents.add(element);
return validateQuestionannaireItem(errors, element, element, stack, parents); List<QuestionnaireDerivation> derivations = new ArrayList<>();
boolean ok = checkDerivations(errors, element, element, stack, derivations);
return validateQuestionannaireItem(errors, element, element, stack, parents, derivations) && ok;
}
private boolean checkDerivations(List<ValidationMessage> errors, Element element, Element questionnaire, NodeStack stack, List<QuestionnaireDerivation> derivations) {
boolean ok = true;
List<Element> list = new ArrayList<>();
element.getNamedChildren("derivedFrom", list);
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());
String url = e.primitiveValue();
Questionnaire q = context.fetchResource(Questionnaire.class, url);
if (warning(errors, "2023-06-15", IssueType.BUSINESSRULE, ns, q != null, I18nConstants.QUESTIONNAIRE_Q_UNKNOWN_DERIVATION, url)) {
Element ext = e.getExtension("http://hl7.org/fhir/StructureDefinition/questionnaire-derivationType");
if (warning(errors, "2023-06-15", IssueType.BUSINESSRULE, ns, ext != null, I18nConstants.QUESTIONNAIRE_Q_NO_DERIVATION_TYPE, url)) {
NodeStack next = ns.push(ext, -1, ext.getProperty().getDefinition(), ext.getProperty().getDefinition());
Element v = ext.getNamedChild("value");
if (warning(errors, "2023-06-15", IssueType.BUSINESSRULE, next, v != null, I18nConstants.QUESTIONNAIRE_Q_NO_DERIVATION_TYPE_VALUE)) {
NodeStack nv = next.push(v, -1, v.getProperty().getDefinition(), v.getProperty().getDefinition());
String s = v.getNamedChildValue("system");
String c = v.getNamedChildValue("code");
if ("http://hl7.org/fhir/questionnaire-derivationType".equals(s) && "extends".equals(c)) {
derivations.add(new QuestionnaireDerivation(q, QuestionnaireDerivationMode.EXTENDS));
} else if ("http://hl7.org/fhir/questionnaire-derivationType".equals(s) && "compliesWith".equals(c)) {
derivations.add(new QuestionnaireDerivation(q, QuestionnaireDerivationMode.COMPLIES));
} else if ("http://hl7.org/fhir/questionnaire-derivationType".equals(s) && "inspiredBy".equals(c)) {
hint(errors, "2023-06-15", IssueType.BUSINESSRULE, nv, false, I18nConstants.QUESTIONNAIRE_Q_DERIVATION_TYPE_IGNORED, s+"#"+c);
} else {
warning(errors, "2023-06-15", IssueType.BUSINESSRULE, nv, false, I18nConstants.QUESTIONNAIRE_Q_DERIVATION_TYPE_UNKNOWN, s+"#"+c);
}
}
}
}
}
return ok;
} }
private boolean validateQuestionannaireItem(List<ValidationMessage> errors, Element element, Element questionnaire, NodeStack stack, List<Element> parents) { private boolean validateQuestionannaireItem(List<ValidationMessage> errors, Element element, Element questionnaire, NodeStack stack, List<Element> parents, List<QuestionnaireDerivation> derivations) {
boolean ok = true; boolean ok = true;
List<Element> list = getItems(element); List<Element> list = getItems(element);
for (int i = 0; i < list.size(); i++) { for (int i = 0; i < list.size(); i++) {
Element e = list.get(i); Element e = list.get(i);
NodeStack ns = stack.push(e, i, e.getProperty().getDefinition(), e.getProperty().getDefinition()); NodeStack ns = stack.push(e, i, e.getProperty().getDefinition(), e.getProperty().getDefinition());
ok = validateQuestionnaireElement(errors, ns, questionnaire, e, parents) && ok; ok = validateQuestionnaireElement(errors, ns, questionnaire, e, parents, derivations) && ok;
List<Element> np = new ArrayList<Element>(); List<Element> np = new ArrayList<Element>();
np.add(e); np.add(e);
np.addAll(parents); np.addAll(parents);
ok = validateQuestionannaireItem(errors, e, questionnaire, ns, np) && ok; ok = validateQuestionannaireItem(errors, e, questionnaire, ns, np, derivations) && ok;
} }
return ok; return ok;
} }
private boolean validateQuestionnaireElement(List<ValidationMessage> errors, NodeStack ns, Element questionnaire, Element item, List<Element> parents) { private boolean validateQuestionnaireElement(List<ValidationMessage> errors, NodeStack ns, Element questionnaire, Element item, List<Element> parents, List<QuestionnaireDerivation> derivations) {
boolean ok = true; boolean ok = true;
// R4+ // R4+
if ((VersionUtilities.isR4Plus(context.getVersion())) && (item.hasChildren("enableWhen"))) { if ((VersionUtilities.isR4Plus(context.getVersion())) && (item.hasChildren("enableWhen"))) {
List<Element> ewl = item.getChildren("enableWhen"); List<Element> ewl = item.getChildren("enableWhen");
for (Element ew : ewl) { for (Element ew : ewl) {
String ql = ew.getNamedChildValue("question"); String ql = ew.getNamedChildValue("question");
if (rule(errors, NO_RULE_DATE, IssueType.BUSINESSRULE, ns.getLiteralPath(), ql != null, I18nConstants.QUESTIONNAIRE_Q_ENABLEWHEN_NOLINK)) { if (rule(errors, NO_RULE_DATE, IssueType.BUSINESSRULE, ns, ql != null, I18nConstants.QUESTIONNAIRE_Q_ENABLEWHEN_NOLINK)) {
Element tgt = getQuestionById(item, ql); Element tgt = getQuestionById(item, ql);
if (rule(errors, NO_RULE_DATE, IssueType.BUSINESSRULE, ns.getLiteralPath(), tgt == null, I18nConstants.QUESTIONNAIRE_Q_ENABLEWHEN_ISINNER)) { if (rule(errors, NO_RULE_DATE, IssueType.BUSINESSRULE, ns, tgt == null, I18nConstants.QUESTIONNAIRE_Q_ENABLEWHEN_ISINNER)) {
tgt = getQuestionById(questionnaire, ql); tgt = getQuestionById(questionnaire, ql);
if (rule(errors, NO_RULE_DATE, IssueType.BUSINESSRULE, ns.getLiteralPath(), tgt != null, I18nConstants.QUESTIONNAIRE_Q_ENABLEWHEN_NOTARGET, ql, item.getChildValue("linkId"))) { if (rule(errors, NO_RULE_DATE, IssueType.BUSINESSRULE, ns, tgt != null, I18nConstants.QUESTIONNAIRE_Q_ENABLEWHEN_NOTARGET, ql, item.getChildValue("linkId"))) {
if (rule(errors, NO_RULE_DATE, IssueType.BUSINESSRULE, ns.getLiteralPath(), tgt != item, I18nConstants.QUESTIONNAIRE_Q_ENABLEWHEN_SELF)) { if (rule(errors, NO_RULE_DATE, IssueType.BUSINESSRULE, ns, tgt != item, I18nConstants.QUESTIONNAIRE_Q_ENABLEWHEN_SELF)) {
if (!isBefore(item, tgt, parents)) { if (!isBefore(item, tgt, parents)) {
warning(errors, NO_RULE_DATE, IssueType.BUSINESSRULE, ns.getLiteralPath(), false, I18nConstants.QUESTIONNAIRE_Q_ENABLEWHEN_AFTER, ql); warning(errors, NO_RULE_DATE, IssueType.BUSINESSRULE, ns, false, I18nConstants.QUESTIONNAIRE_Q_ENABLEWHEN_AFTER, ql);
} }
} else { } else {
ok = false; ok = false;
@ -166,9 +230,163 @@ public class QuestionnaireValidator extends BaseValidator {
} }
} }
} }
for (QuestionnaireDerivation qd : derivations) {
ok = validateQuestionnaireElementDerivation(errors, ns, questionnaire, item, qd) && ok;
}
return ok; return ok;
} }
private boolean validateQuestionnaireElementDerivation(List<ValidationMessage> errors, NodeStack ns, Element questionnaire, Element item, QuestionnaireDerivation derivation) {
boolean ok = true;
String linkId = item.getNamedChildValue("linkId");
QuestionnaireItemComponent qi = derivation.questionnaire.getQuestion(linkId);
if (qi == null) {
ok = rule(errors, "2023-06-15", IssueType.NOTFOUND, ns.getLiteralPath(), derivation.mode == QuestionnaireDerivationMode.EXTENDS, I18nConstants.QUESTIONNAIRE_Q_ITEM_NOT_DERIVED, derivation.questionnaire.getUrl(), linkId) && ok;
} else {
// things to check:
// type must be the same
if (qi.hasType()) {
Element e = item.getNamedChild("type");
if (e != null) {
NodeStack ne = ns.push(e, -1, e.getProperty().getDefinition(), e.getProperty().getDefinition());
ok = rule(errors, "2023-06-15", IssueType.BUSINESSRULE, ne, qi.getType().toCode().equals(e.primitiveValue()), I18nConstants.QUESTIONNAIRE_Q_ITEM_DERIVED_NC_TYPE, derivation.questionnaire.getUrl(), linkId, qi.getType().toCode(), e.primitiveValue()) && ok;
}
}
// if it doesn't repeat, it can't start repeating
if (!qi.getRepeats()) {
Element e = item.getNamedChild("repeats");
if (e != null) {
NodeStack ne = ns.push(e, -1, e.getProperty().getDefinition(), e.getProperty().getDefinition());
ok = rule(errors, "2023-06-15", IssueType.BUSINESSRULE, ne, !"true".equals(e.primitiveValue()), I18nConstants.QUESTIONNAIRE_Q_ITEM_DERIVED_NC_REPEATS, derivation.questionnaire.getUrl(), linkId) && ok;
}
}
// if it is required, it can't become un-required
if (qi.getRequired()) {
Element e = item.getNamedChild("required");
if (e != null) {
NodeStack ne = ns.push(e, -1, e.getProperty().getDefinition(), e.getProperty().getDefinition());
ok = rule(errors, "2023-06-15", IssueType.BUSINESSRULE, ne, "true".equals(e.primitiveValue()), I18nConstants.QUESTIONNAIRE_Q_ITEM_DERIVED_NC_REQUIRED, derivation.questionnaire.getUrl(), linkId) && ok;
}
}
// if it has a definition, it shouldn't change
if (qi.hasDefinition()) {
Element e = item.getNamedChild("definition");
if (e != null) {
NodeStack ne = ns.push(e, -1, e.getProperty().getDefinition(), e.getProperty().getDefinition());
hint(errors, "2023-06-15", IssueType.BUSINESSRULE, ne, "true".equals(e.primitiveValue()), I18nConstants.QUESTIONNAIRE_Q_ITEM_DERIVED_NC_DEFINITION, derivation.questionnaire.getUrl(), linkId, qi.getDefinition());
} else {
hint(errors, "2023-06-15", IssueType.BUSINESSRULE, ns, false, I18nConstants.QUESTIONNAIRE_Q_ITEM_DERIVED_DEFINITION, derivation.questionnaire.getUrl(), linkId, qi.getDefinition());
}
}
// if it has maxLength, that can't get longer
if (qi.hasMaxLength()) {
Element e = item.getNamedChild("maxlength");
if (e != null) {
NodeStack ne = ns.push(e, -1, e.getProperty().getDefinition(), e.getProperty().getDefinition());
int ml = Utilities.parseInt(e.primitiveValue(), 0);
ok = rule(errors, "2023-06-15", IssueType.BUSINESSRULE, ne, ml <= qi.getMaxLength(), I18nConstants.QUESTIONNAIRE_Q_ITEM_DERIVED_NC_MAXLENGTH, derivation.questionnaire.getUrl(), linkId, qi.getMaxLength()) && ok;
} else {
ok = rule(errors, "2023-06-15", IssueType.BUSINESSRULE, ns, false, I18nConstants.QUESTIONNAIRE_Q_ITEM_DERIVED_MAXLENGTH, derivation.questionnaire.getUrl(), linkId, qi.getMaxLength()) & ok;
}
}
if (qi.hasAnswerOption()) {
Element e = item.getNamedChild("answerValueSet");
if (rule(errors, "2023-06-15", IssueType.BUSINESSRULE, ns, e == null, I18nConstants.QUESTIONNAIRE_Q_ITEM_DERIVED_NC_ANSWER_TYPE, derivation.questionnaire.getUrl(), linkId, "Option", "ValueSet")) {
// for each answer option here, there must be a matching answer option in the source
List<Element> list = new ArrayList<>();
item.getNamedChildren("answerOption", list);
if (rule(errors, "2023-06-15", IssueType.BUSINESSRULE, ns, !list.isEmpty(), I18nConstants.QUESTIONNAIRE_Q_ITEM_DERIVED_ANSWER_OPTIONS, derivation.questionnaire.getUrl(), linkId, qi.getAnswerOption().size())) {
for (int i = 0; i < list.size(); i++) {
Element ao = list.get(i);
NodeStack nao = ns.push(ao, i, ao.getProperty().getDefinition(), ao.getProperty().getDefinition());
Element v = ao.getNamedChild("value");
if (v != null) {
boolean aok = false;
switch (v.fhirType()) {
case "integer":
aok = findAOPrimitive(qi.getAnswerOption(), "integer", v.primitiveValue());
break;
case "date":
aok = findAOPrimitive(qi.getAnswerOption(), "date", v.primitiveValue());
break;
case "time":
aok = findAOPrimitive(qi.getAnswerOption(), "time", v.primitiveValue());
break;
case "string":
aok = findAOPrimitive(qi.getAnswerOption(), "string", v.primitiveValue());
break;
case "Coding":
aok = findAOCoding(qi.getAnswerOption(), new Coding().setSystem(v.getNamedChildValue("system")).setVersion(v.getNamedChildValue("version")).setCode(v.getNamedChildValue("code")));
break;
case "Reference":
aok = findAOReference(qi.getAnswerOption(), new Reference().setReference(v.getNamedChildValue("reference")));
break;
}
ok= rule(errors, "2023-06-15", IssueType.BUSINESSRULE, nao, aok, I18nConstants.QUESTIONNAIRE_Q_ITEM_DERIVED_ANSWER_OPTIONS_NEW, derivation.questionnaire.getUrl(), linkId) && ok;
}
}
} else {
ok = false;
}
} else {
ok = false;
}
}
if (qi.hasAnswerValueSet()) {
Element e = item.getNamedChild("answerOption");
if (rule(errors, "2023-06-15", IssueType.BUSINESSRULE, ns, e == null, I18nConstants.QUESTIONNAIRE_Q_ITEM_DERIVED_NC_ANSWER_TYPE, derivation.questionnaire.getUrl(), linkId, "ValueSet", "Option")) {
warning(errors, "2023-06-15", IssueType.BUSINESSRULE, ns, e == null, I18nConstants.QUESTIONNAIRE_Q_ITEM_DERIVED_NI_ANSWER_VS, derivation.questionnaire.getUrl(), linkId);
} else {
ok = false;;
}
}
// if it has codings, these should be repeated (can be added to)
// if it has n enableWhens, there should be at least n EnableWhens
// if it has answerCOnstraint, that snouldn't change
// if it has answerOptions, there can't be new answers
}
return ok;
}
private boolean findAOReference(List<QuestionnaireItemAnswerOptionComponent> answerOptions, Reference value) {
for (QuestionnaireItemAnswerOptionComponent ao : answerOptions) {
if (ao.hasValue() && ao.getValue() instanceof Reference) {
Reference r = ao.getValueReference();
if (r.matches(value)) {
return true;
}
}
}
return false;
}
private boolean findAOCoding(List<QuestionnaireItemAnswerOptionComponent> answerOptions, Coding value) {
for (QuestionnaireItemAnswerOptionComponent ao : answerOptions) {
if (ao.hasValue() && ao.getValue() instanceof Coding) {
Coding c = ao.getValueCoding();
if (c.matches(value)) {
return true;
}
}
}
return false;
}
private boolean findAOPrimitive(List<QuestionnaireItemAnswerOptionComponent> answerOptions, String type, String v) {
for (QuestionnaireItemAnswerOptionComponent ao : answerOptions) {
if (ao.hasValue() && ao.getValue().isPrimitive() && ao.getValue().fhirType().equals(type) && ao.getValue().primitiveValue().equals(v)) {
return true;
}
}
return false;
}
private boolean isBefore(Element item, Element tgt, List<Element> parents) { private boolean isBefore(Element item, Element tgt, List<Element> parents) {
// we work up the list, looking for tgt in the children of the parents // we work up the list, looking for tgt in the children of the parents
if (parents.contains(tgt)) { if (parents.contains(tgt)) {

View File

@ -211,5 +211,13 @@ public class NodeStack {
this.contained = contained; this.contained = contained;
} }
public int line() {
return element.line();
}
public int col() {
return element.col();
}
} }