package org.hl7.fhir.instance.validation;
import static org.apache.commons.lang3.StringUtils.isBlank;
import static org.apache.commons.lang3.StringUtils.isNotBlank;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashSet;
import java.util.Iterator;
import java.util.LinkedList;
import java.util.List;
import java.util.Set;
import org.apache.commons.lang3.StringUtils;
import org.apache.commons.lang3.Validate;
import org.hl7.fhir.instance.model.Attachment;
import org.hl7.fhir.instance.model.BooleanType;
import org.hl7.fhir.instance.model.Coding;
import org.hl7.fhir.instance.model.DateTimeType;
import org.hl7.fhir.instance.model.DateType;
import org.hl7.fhir.instance.model.DecimalType;
import org.hl7.fhir.instance.model.Extension;
import org.hl7.fhir.instance.model.InstantType;
import org.hl7.fhir.instance.model.IntegerType;
import org.hl7.fhir.instance.model.OperationOutcome.IssueSeverity;
import org.hl7.fhir.instance.model.OperationOutcome.IssueType;
import org.hl7.fhir.instance.model.Quantity;
import org.hl7.fhir.instance.model.Questionnaire;
import org.hl7.fhir.instance.model.Questionnaire.AnswerFormat;
import org.hl7.fhir.instance.model.Questionnaire.GroupComponent;
import org.hl7.fhir.instance.model.Questionnaire.QuestionComponent;
import org.hl7.fhir.instance.model.QuestionnaireResponse;
import org.hl7.fhir.instance.model.QuestionnaireResponse.QuestionAnswerComponent;
import org.hl7.fhir.instance.model.QuestionnaireResponse.QuestionnaireResponseStatus;
import org.hl7.fhir.instance.model.Reference;
import org.hl7.fhir.instance.model.Resource;
import org.hl7.fhir.instance.model.StringType;
import org.hl7.fhir.instance.model.TimeType;
import org.hl7.fhir.instance.model.Type;
import org.hl7.fhir.instance.model.UriType;
import org.hl7.fhir.instance.model.ValueSet;
import org.hl7.fhir.instance.model.ValueSet.ConceptDefinitionComponent;
import org.hl7.fhir.instance.model.ValueSet.ConceptReferenceComponent;
import org.hl7.fhir.instance.model.ValueSet.ConceptSetComponent;
import org.hl7.fhir.instance.utils.WorkerContext;
import ca.uhn.fhir.util.ElementUtil;
/**
* Validates that an instance of {@link QuestionnaireResponse} is valid against the {@link Questionnaire} that it claims to conform to.
*
* @author James Agnew
*/
public class QuestionnaireResponseValidator extends BaseValidator {
// @formatter:off
/*
* *****************************************************************
* Note to anyone working on this class -
*
* This class has unit tests which run within the HAPI project build. Please sync any changes here to HAPI and ensure that unit tests are run.
* ****************************************************************
*/
// @formatter:on
private WorkerContext myWorkerCtx;
public QuestionnaireResponseValidator(WorkerContext theWorkerCtx) {
this.myWorkerCtx = theWorkerCtx;
}
private Set<Class<? extends Type>> allowedTypes(Class<? extends Type> theClass0) {
HashSet<Class<? extends Type>> retVal = new HashSet<Class<? extends Type>>();
retVal.add(theClass0);
return Collections.unmodifiableSet(retVal);
}
private Set<Class<? extends Type>> allowedTypes(Class<? extends Type> theClass0, Class<? extends Type> theClass1) {
HashSet<Class<? extends Type>> retVal = new HashSet<Class<? extends Type>>();
retVal.add(theClass0);
retVal.add(theClass1);
return Collections.unmodifiableSet(retVal);
}
private List<org.hl7.fhir.instance.model.QuestionnaireResponse.QuestionComponent> findAnswersByLinkId(List<org.hl7.fhir.instance.model.QuestionnaireResponse.QuestionComponent> theQuestion,
String theLinkId) {
Validate.notBlank(theLinkId, "theLinkId must not be blank");
ArrayList<org.hl7.fhir.instance.model.QuestionnaireResponse.QuestionComponent> retVal = new ArrayList<QuestionnaireResponse.QuestionComponent>();
for (org.hl7.fhir.instance.model.QuestionnaireResponse.QuestionComponent next : theQuestion) {
if (theLinkId.equals(next.getLinkId())) {
retVal.add(next);
}
}
return retVal;
}
private List<org.hl7.fhir.instance.model.QuestionnaireResponse.GroupComponent> findGroupByLinkId(List<org.hl7.fhir.instance.model.QuestionnaireResponse.GroupComponent> theGroups, String theLinkId) {
ArrayList<org.hl7.fhir.instance.model.QuestionnaireResponse.GroupComponent> retVal = new ArrayList<QuestionnaireResponse.GroupComponent>();
for (org.hl7.fhir.instance.model.QuestionnaireResponse.GroupComponent next : theGroups) {
if (theLinkId == null) {
if (next.getLinkId() == null) {
retVal.add(next);
}
} else if (theLinkId.equals(next.getLinkId())) {
retVal.add(next);
}
}
return retVal;
}
// protected boolean fail(List<ValidationMessage> errors, IssueType type, List<String> pathParts, boolean thePass, String msg) {
// return test(errors, type, pathParts, thePass, msg, IssueSeverity.FATAL);
// }
public void validate(List<ValidationMessage> theErrors, QuestionnaireResponse theAnswers) {
LinkedList<String> pathStack = new LinkedList<String>();
pathStack.add("QuestionnaireResponse");
pathStack.add(QuestionnaireResponse.SP_QUESTIONNAIRE);
if (!super.fail(theErrors, IssueType.INVALID, pathStack, theAnswers.hasQuestionnaire(), "QuestionnaireResponse does not specity which questionnaire it is providing answers to")) {
return;
}
Reference questionnaireRef = theAnswers.getQuestionnaire();
Questionnaire questionnaire = getQuestionnaire(theAnswers, questionnaireRef);
if (questionnaire == null && theErrors.size() > 0 && theErrors.get(theErrors.size() - 1).getLevel() == IssueSeverity.FATAL) {
return;
}
if (!fail(theErrors, IssueType.INVALID, pathStack, questionnaire != null, "Questionnaire {0} is not found in the WorkerContext", theAnswers.getQuestionnaire().getReference())) {
return;
}
QuestionnaireResponseStatus status = theAnswers.getStatus();
boolean validateRequired = false;
if (status == QuestionnaireResponseStatus.COMPLETED || status == QuestionnaireResponseStatus.AMENDED) {
validateRequired = true;
}
pathStack.removeLast();
pathStack.add("group[0]");
validateGroup(theErrors, questionnaire.getGroup(), theAnswers.getGroup(), pathStack, theAnswers, validateRequired);
/*
* If we found any fatal errors, any other errors will be removed since the fatal error means the parsing was invalid
*/
for (ValidationMessage next : theErrors) {
if (next.getLevel() == IssueSeverity.FATAL) {
for (Iterator<ValidationMessage> iter = theErrors.iterator(); iter.hasNext();) {
if (iter.next().getLevel() != IssueSeverity.FATAL) {
iter.remove();
}
}
break;
}
}
}
private Questionnaire getQuestionnaire(QuestionnaireResponse theAnswers, Reference theQuestionnaireRef) {
Questionnaire retVal;
if (theQuestionnaireRef.getReferenceElement().isLocal()) {
retVal = (Questionnaire) theQuestionnaireRef.getResource();
if (retVal == null) {
for (Resource next : theAnswers.getContained()) {
if (theQuestionnaireRef.getReferenceElement().getValue().equals(next.getId())) {
retVal = (Questionnaire) next;
}
}
}
} else {
retVal = myWorkerCtx.getQuestionnaires().get(theQuestionnaireRef.getReferenceElement().getValue());
}
return retVal;
}
private ValueSet getValueSet(QuestionnaireResponse theAnswers, Reference theQuestionnaireRef) {
ValueSet retVal;
if (theQuestionnaireRef.getReferenceElement().isLocal()) {
retVal = (ValueSet) theQuestionnaireRef.getResource();
if (retVal == null) {
for (Resource next : theAnswers.getContained()) {
if (theQuestionnaireRef.getReferenceElement().getValue().equals(next.getId())) {
retVal = (ValueSet) next;
}
}
}
} else {
retVal = myWorkerCtx.getValueSets().get(theQuestionnaireRef.getReferenceElement().getValue());
}
return retVal;
}
private void validateGroup(List<ValidationMessage> theErrors, GroupComponent theQuestGroup, org.hl7.fhir.instance.model.QuestionnaireResponse.GroupComponent theAnsGroup,
LinkedList<String> thePathStack, QuestionnaireResponse theAnswers, boolean theValidateRequired) {
for (org.hl7.fhir.instance.model.QuestionnaireResponse.QuestionComponent next : theAnsGroup.getQuestion()) {
rule(theErrors, IssueType.INVALID, thePathStack, isNotBlank(next.getLinkId()), "Question found with no linkId");
}
Set<String> allowedQuestions = new HashSet<String>();
for (QuestionComponent nextQuestion : theQuestGroup.getQuestion()) {
allowedQuestions.add(nextQuestion.getLinkId());
}
for (int i = 0; i < theQuestGroup.getQuestion().size(); i++) {
QuestionComponent nextQuestion = theQuestGroup.getQuestion().get(i);
validateQuestion(theErrors, nextQuestion, theAnsGroup, thePathStack, theAnswers, theValidateRequired);
}
// Check that there are no extra answers
for (int i = 0; i < theAnsGroup.getQuestion().size(); i++) {
org.hl7.fhir.instance.model.QuestionnaireResponse.QuestionComponent nextQuestion = theAnsGroup.getQuestion().get(i);
thePathStack.add("question[" + i + "]");
rule(theErrors, IssueType.BUSINESSRULE, thePathStack, allowedQuestions.contains(nextQuestion.getLinkId()), "Found answer with linkId[{0}] but this ID is not allowed at this position",
nextQuestion.getLinkId());
thePathStack.remove();
}
validateGroupGroups(theErrors, theQuestGroup, theAnsGroup, thePathStack, theAnswers, theValidateRequired);
}
private void validateQuestion(List<ValidationMessage> theErrors, QuestionComponent theQuestion, org.hl7.fhir.instance.model.QuestionnaireResponse.GroupComponent theAnsGroup,
LinkedList<String> thePathStack, QuestionnaireResponse theAnswers, boolean theValidateRequired) {
QuestionComponent question = theQuestion;
String linkId = question.getLinkId();
if (!fail(theErrors, IssueType.INVALID, thePathStack, isNotBlank(linkId), "Questionnaire is invalid, question found with no link ID")) {
return;
}
AnswerFormat type = question.getType();
if (type == null) {
// Support old format/casing and new
List<Extension> extensions = question.getExtensionsByUrl("http://hl7.org/fhir/StructureDefinition/questionnaire-deReference");
if (extensions.isEmpty()) {
extensions = question.getExtensionsByUrl("http://hl7.org/fhir/StructureDefinition/questionnaire-dereference");
}
if (extensions.isEmpty() == false) {
if (extensions.size() > 1) {
warning(theErrors, IssueType.BUSINESSRULE, thePathStack, false,
"Questionnaire is invalid, element contains multiple extensions with URL 'questionnaire-dereference', maximum one may be contained in a single element");
}
return;
/*
* Hopefully we will implement this soon...
*/
// Extension ext = extensions.get(0);
// Reference ref = (Reference) ext.getValue();
// DataElement de = myWorkerCtx.getDataElements().get(ref.getReference());
// if (de.getElement().size() != 1) {
// warning(theErrors, IssueType.BUSINESSRULE, EMPTY_PATH, false, "DataElement {0} has wrong number of elements: {1}", ref.getReference(),
// de.getElement().size());
// }
// ElementDefinition element = de.getElement().get(0);
// question = toQuestion(element);
} else {
if (question.getGroup().isEmpty()) {
rule(theErrors, IssueType.INVALID, thePathStack, false, "Questionnaire is invalid, no type and no groups specified for question with link ID[{0}]", linkId);
return;
}
type = AnswerFormat.NULL;
}
}
List<org.hl7.fhir.instance.model.QuestionnaireResponse.QuestionComponent> answers = findAnswersByLinkId(theAnsGroup.getQuestion(), linkId);
if (answers.size() > 1) {
rule(theErrors, IssueType.BUSINESSRULE, thePathStack, !question.getRequired(), "Multiple answers repetitions found with linkId[{0}]", linkId);
}
if (answers.size() == 0) {
if (theValidateRequired) {
rule(theErrors, IssueType.BUSINESSRULE, thePathStack, !question.getRequired(), "Missing answer to required question with linkId[{0}]", linkId);
} else {
hint(theErrors, IssueType.BUSINESSRULE, thePathStack, !question.getRequired(), "Missing answer to required question with linkId[{0}]", linkId);
}
return;
}
org.hl7.fhir.instance.model.QuestionnaireResponse.QuestionComponent answerQuestion = answers.get(0);
try {
thePathStack.add("question[" + answers.indexOf(answerQuestion) + "]");
validateQuestionAnswers(theErrors, question, thePathStack, type, answerQuestion, theAnswers, theValidateRequired);
validateQuestionGroups(theErrors, question, answerQuestion, thePathStack, theAnswers, theValidateRequired);
} finally {
thePathStack.removeLast();
}
}
private void validateQuestionGroups(List<ValidationMessage> theErrors, QuestionComponent theQuestion, org.hl7.fhir.instance.model.QuestionnaireResponse.QuestionComponent theAnswerQuestion,
LinkedList<String> thePathSpec, QuestionnaireResponse theAnswers, boolean theValidateRequired) {
for (QuestionAnswerComponent nextAnswer : theAnswerQuestion.getAnswer()) {
validateGroups(theErrors, theQuestion.getGroup(), nextAnswer.getGroup(), thePathSpec, theAnswers, theValidateRequired);
}
}
private void validateGroupGroups(List<ValidationMessage> theErrors, GroupComponent theQuestGroup, org.hl7.fhir.instance.model.QuestionnaireResponse.GroupComponent theAnsGroup,
LinkedList<String> thePathSpec, QuestionnaireResponse theAnswers, boolean theValidateRequired) {
validateGroups(theErrors, theQuestGroup.getGroup(), theAnsGroup.getGroup(), thePathSpec, theAnswers, theValidateRequired);
}
private void validateGroups(List<ValidationMessage> theErrors, List<GroupComponent> theQuestionGroups, List<org.hl7.fhir.instance.model.QuestionnaireResponse.GroupComponent> theAnswerGroups,
LinkedList<String> thePathStack, QuestionnaireResponse theAnswers, boolean theValidateRequired) {
Set<String> linkIds = new HashSet<String>();
for (GroupComponent nextQuestionGroup : theQuestionGroups) {
String nextLinkId = StringUtils.defaultString(nextQuestionGroup.getLinkId());
if (!linkIds.add(nextLinkId)) {
if (isBlank(nextLinkId)) {
fail(theErrors, IssueType.BUSINESSRULE, thePathStack, false,
"Questionnaire in invalid, unable to validate QuestionnaireResponse: Multiple groups found at this position with blank/missing linkId", nextLinkId);
} else {
fail(theErrors, IssueType.BUSINESSRULE, thePathStack, false, "Questionnaire in invalid, unable to validate QuestionnaireResponse: Multiple groups found at this position with linkId[{0}]",
nextLinkId);
}
}
}
Set<String> allowedGroups = new HashSet<String>();
for (GroupComponent nextQuestionGroup : theQuestionGroups) {
String linkId = nextQuestionGroup.getLinkId();
allowedGroups.add(linkId);
List<org.hl7.fhir.instance.model.QuestionnaireResponse.GroupComponent> answerGroups = findGroupByLinkId(theAnswerGroups, linkId);
if (answerGroups.isEmpty()) {
if (nextQuestionGroup.getRequired()) {
if (theValidateRequired) {
rule(theErrors, IssueType.BUSINESSRULE, thePathStack, false, "Missing required group with linkId[{0}]", linkId);
} else {
hint(theErrors, IssueType.BUSINESSRULE, thePathStack, false, "Missing required group with linkId[{0}]", linkId);
}
}
continue;
}
if (answerGroups.size() > 1) {
if (nextQuestionGroup.getRepeats() == false) {
int index = theAnswerGroups.indexOf(answerGroups.get(1));
thePathStack.add("group[" + index + "]");
rule(theErrors, IssueType.BUSINESSRULE, thePathStack, false, "Multiple repetitions of group with linkId[{0}] found at this position, but this group can not repeat", linkId);
thePathStack.removeLast();
}
}
for (org.hl7.fhir.instance.model.QuestionnaireResponse.GroupComponent nextAnswerGroup : answerGroups) {
int index = theAnswerGroups.indexOf(nextAnswerGroup);
thePathStack.add("group[" + index + "]");
validateGroup(theErrors, nextQuestionGroup, nextAnswerGroup, thePathStack, theAnswers, theValidateRequired);
thePathStack.removeLast();
}
}
// Make sure there are no groups in answers that aren't in the questionnaire
int idx = -1;
for (org.hl7.fhir.instance.model.QuestionnaireResponse.GroupComponent next : theAnswerGroups) {
idx++;
if (!allowedGroups.contains(next.getLinkId())) {
thePathStack.add("group[" + idx + "]");
rule(theErrors, IssueType.BUSINESSRULE, thePathStack, false, "Group with linkId[{0}] found at this position, but this group does not exist at this position in Questionnaire",
next.getLinkId());
thePathStack.removeLast();
}
}
}
private void validateQuestionAnswers(List<ValidationMessage> theErrors, QuestionComponent theQuestion, LinkedList<String> thePathStack, AnswerFormat type,
org.hl7.fhir.instance.model.QuestionnaireResponse.QuestionComponent answerQuestion, QuestionnaireResponse theAnswers, boolean theValidateRequired) {
String linkId = theQuestion.getLinkId();
Set<Class<? extends Type>> allowedAnswerTypes = determineAllowedAnswerTypes(type);
if (allowedAnswerTypes.isEmpty()) {
for (QuestionAnswerComponent nextAnswer : answerQuestion.getAnswer()) {
rule(theErrors, IssueType.BUSINESSRULE, thePathStack, ElementUtil.isEmpty(nextAnswer.getValue()), "Question with linkId[{0}] has no answer type but an answer was provided", linkId);
}
} else {
rule(theErrors, IssueType.BUSINESSRULE, thePathStack, !(answerQuestion.getAnswer().size() > 1 && !theQuestion.getRepeats()), "Multiple answers to non repeating question with linkId[{0}]",
linkId);
if (theValidateRequired) {
rule(theErrors, IssueType.BUSINESSRULE, thePathStack, !(theQuestion.getRequired() && answerQuestion.getAnswer().isEmpty()), "Missing answer to required question with linkId[{0}]", linkId);
} else {
hint(theErrors, IssueType.BUSINESSRULE, thePathStack, !(theQuestion.getRequired() && answerQuestion.getAnswer().isEmpty()), "Missing answer to required question with linkId[{0}]", linkId);
}
}
int answerIdx = -1;
for (QuestionAnswerComponent nextAnswer : answerQuestion.getAnswer()) {
answerIdx++;
try {
thePathStack.add("answer[" + answerIdx + "]");
Type nextValue = nextAnswer.getValue();
if (nextValue == null) {
continue;
}
if (!allowedAnswerTypes.contains(nextValue.getClass())) {
rule(theErrors, IssueType.BUSINESSRULE, thePathStack, false, "Answer to question with linkId[{0}] found of type [{1}] but this is invalid for question of type [{2}]", linkId,
nextValue.getClass().getSimpleName(), type.toCode());
continue;
}
// Validate choice answers
if (type == AnswerFormat.CHOICE || type == AnswerFormat.OPENCHOICE) {
if (nextAnswer.getValue() instanceof StringType) {
StringType answer = (StringType) nextAnswer.getValue();
if (answer == null || isBlank(answer.getValueAsString())) {
rule(theErrors, IssueType.BUSINESSRULE, thePathStack, false, "Answer to question with linkId[{0}] is required but answer does not have a value", linkId);
continue;
}
} else {
Coding coding = (Coding) nextAnswer.getValue();
if (isBlank(coding.getCode())) {
rule(theErrors, IssueType.BUSINESSRULE, thePathStack, false, "Answer to question with linkId[{0}] is of type {1} but coding answer does not have a code", linkId, type.name());
continue;
}
if (isBlank(coding.getSystem())) {
rule(theErrors, IssueType.BUSINESSRULE, thePathStack, false, "Answer to question with linkId[{0}] is of type {1} but coding answer does not have a system", linkId, type.name());
continue;
}
String optionsRef = theQuestion.getOptions().getReference();
if (isNotBlank(optionsRef)) {
ValueSet valueSet = getValueSet(theAnswers, theQuestion.getOptions());
if (valueSet == null) {
rule(theErrors, IssueType.BUSINESSRULE, thePathStack, false, "Question with linkId[{0}] has options ValueSet[{1}] but this ValueSet can not be found", linkId, optionsRef);
continue;
}
boolean found = false;
if (coding.getSystem().equals(valueSet.getCodeSystem().getSystem())) {
for (ConceptDefinitionComponent next : valueSet.getCodeSystem().getConcept()) {
if (coding.getCode().equals(next.getCode())) {
found = true;
break;
}
}
}
if (!found) {
for (ConceptSetComponent nextCompose : valueSet.getCompose().getInclude()) {
if (coding.getSystem().equals(nextCompose.getSystem())) {
for (ConceptReferenceComponent next : nextCompose.getConcept()) {
if (coding.getCode().equals(next.getCode())) {
found = true;
break;
}
}
}
if (found) {
break;
}
}
}
rule(theErrors, IssueType.BUSINESSRULE, thePathStack, found, "Question with linkId[{0}] has answer with system[{1}] and code[{2}] but this is not a valid answer for ValueSet[{3}]",
linkId, coding.getSystem(), coding.getCode(), optionsRef);
}
}
}
} finally {
thePathStack.removeLast();
}
} // for answers
}
private Set<Class<? extends Type>> determineAllowedAnswerTypes(AnswerFormat type) {
Set<Class<? extends Type>> allowedAnswerTypes;
switch (type) {
case ATTACHMENT:
allowedAnswerTypes = allowedTypes(Attachment.class);
break;
case BOOLEAN:
allowedAnswerTypes = allowedTypes(BooleanType.class);
break;
case CHOICE:
allowedAnswerTypes = allowedTypes(Coding.class);
break;
case DATE:
allowedAnswerTypes = allowedTypes(DateType.class);
break;
case DATETIME:
allowedAnswerTypes = allowedTypes(DateTimeType.class);
break;
case DECIMAL:
allowedAnswerTypes = allowedTypes(DecimalType.class);
break;
case INSTANT:
allowedAnswerTypes = allowedTypes(InstantType.class);
break;
case INTEGER:
allowedAnswerTypes = allowedTypes(IntegerType.class);
break;
case OPENCHOICE:
allowedAnswerTypes = allowedTypes(Coding.class, StringType.class);
break;
case QUANTITY:
allowedAnswerTypes = allowedTypes(Quantity.class);
break;
case REFERENCE:
allowedAnswerTypes = allowedTypes(Reference.class);
break;
case STRING:
allowedAnswerTypes = allowedTypes(StringType.class);
break;
case TEXT:
allowedAnswerTypes = allowedTypes(StringType.class);
break;
case TIME:
allowedAnswerTypes = allowedTypes(TimeType.class);
break;
case URL:
allowedAnswerTypes = allowedTypes(UriType.class);
break;
case NULL:
default:
allowedAnswerTypes = Collections.emptySet();
}
return allowedAnswerTypes;
}
}