Fix validation issue with open-choice questions in R4 questionnaires

This commit is contained in:
Grahame Grieve 2024-10-14 09:02:52 +08:00
parent 56965820ea
commit 3b8b2a94c3
5 changed files with 109 additions and 18 deletions

View File

@ -0,0 +1,37 @@
package org.hl7.fhir.convertors.conv40_50;
import static org.junit.jupiter.api.Assertions.assertTrue;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import org.hl7.fhir.convertors.factory.VersionConvertorFactory_40_50;
import org.hl7.fhir.utilities.TextFile;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
public class Questionnaire40_50Test {
@Test
@DisplayName("Test r5 -> r4 Questionnaire conversion.")
public void testR5_R4() throws IOException {
InputStream r4_input = this.getClass().getResourceAsStream("/q_open_40.json");
String source = TextFile.streamToString(r4_input);
System.out.println(source);
org.hl7.fhir.r4.model.Questionnaire r4_actual = (org.hl7.fhir.r4.model.Questionnaire) new org.hl7.fhir.r4.formats.JsonParser().parse(source);
org.hl7.fhir.r5.model.Resource r5_conv = VersionConvertorFactory_40_50.convertResource(r4_actual);
org.hl7.fhir.r5.formats.JsonParser r5_parser = new org.hl7.fhir.r5.formats.JsonParser();
ByteArrayOutputStream stream = new ByteArrayOutputStream();
r5_parser.compose(stream, r5_conv);
org.hl7.fhir.r5.model.Resource r5_streamed = (org.hl7.fhir.r5.model.Questionnaire) new org.hl7.fhir.r5.formats.JsonParser().parse(new ByteArrayInputStream(stream.toByteArray()));
org.hl7.fhir.r4.model.Resource r4_conv = VersionConvertorFactory_40_50.convertResource(r5_streamed);
assertTrue(r4_actual.equalsDeep(r4_conv), "should be the same");
}
}

View File

@ -0,0 +1,53 @@
{"resourceType": "Questionnaire",
"id": "ed364266b937bb3bd73082b1",
"item": [
{
"extension": [
{
"url": "http://hl7.org/fhir/StructureDefinition/questionnaire-itemControl",
"valueCodeableConcept": {
"coding": [
{
"code": "editableDropdown"
}
]
}
}
],
"id": "specimen-source",
"answerOption": [
{
"valueCoding": {
"code": "U",
"display": "Urine"
}
},
{
"valueCoding": {
"code": "B",
"display": "Blood"
}
},
{
"valueCoding": {
"code": "S",
"display": "Saliva"
}
}
],
"code": [
{
"code": "specimen-source"
}
],
"linkId": "specimen-source",
"text": "Source of specimen",
"type": "open-choice"
}
],
"name": "Test Open Choice question",
"status": "active",
"subjectType": [
"Patient"
]
}

View File

@ -514,6 +514,7 @@ public class I18nConstants {
public static final String QUESTIONNAIRE_QR_ITEM_ONLYONEI = "Questionnaire_QR_Item_OnlyOneI";
public static final String QUESTIONNAIRE_QR_ITEM_ORDER = "Questionnaire_QR_Item_Order";
public static final String QUESTIONNAIRE_QR_ITEM_STRINGNOOPTIONS = "Questionnaire_QR_Item_StringNoOptions";
public static final String QUESTIONNAIRE_QR_ITEM_STRING_IN_CODING = "QUESTIONNAIRE_QR_ITEM_STRING_IN_CODING";
public static final String QUESTIONNAIRE_QR_ITEM_TEXT = "Questionnaire_QR_Item_Text";
public static final String QUESTIONNAIRE_QR_ITEM_TIMENOOPTIONS = "Questionnaire_QR_Item_TimeNoOptions";
public static final String QUESTIONNAIRE_QR_ITEM_WRONGTYPE = "Questionnaire_QR_Item_WrongType";

View File

@ -524,6 +524,7 @@ Questionnaire_QR_Item_NoOptionsCoding = Option list has no option values of type
Questionnaire_QR_Item_NoOptionsDate = Option list has no option values of type date
Questionnaire_QR_Item_NoOptionsInteger = Option list has no option values of type integer
Questionnaire_QR_Item_NoOptionsString = Option list has no option values of type string
QUESTIONNAIRE_QR_ITEM_STRING_IN_CODING = The string value ''{0}'' matches an entry in the list of valid Codings, so this is probably an error
Questionnaire_QR_Item_NoOptionsTime = Option list has no option values of type time
Questionnaire_QR_Item_NoString = The string {0} is not a valid option
Questionnaire_QR_Item_NoTime = The time {0} is not a valid option

View File

@ -16,6 +16,7 @@ import org.hl7.fhir.r5.model.Coding;
import org.hl7.fhir.r5.model.DateType;
import org.hl7.fhir.r5.model.IntegerType;
import org.hl7.fhir.r5.model.Questionnaire;
import org.hl7.fhir.r5.model.Questionnaire.QuestionnaireAnswerConstraint;
import org.hl7.fhir.r5.model.Questionnaire.QuestionnaireItemAnswerOptionComponent;
import org.hl7.fhir.r5.model.Questionnaire.QuestionnaireItemComponent;
import org.hl7.fhir.r5.model.Questionnaire.QuestionnaireItemType;
@ -812,7 +813,7 @@ public class QuestionnaireValidator extends BaseValidator {
}
private boolean checkOption(List<ValidationMessage> errors, Element answer, NodeStack stack, QuestionnaireWithContext qSrc, QuestionnaireItemComponent qItem, String type) {
return checkOption(errors, answer, stack, qSrc, qItem, type, false);
return checkOption(errors, answer, stack, qSrc, qItem, type, qItem.getAnswerConstraint() == QuestionnaireAnswerConstraint.OPTIONSORSTRING);
}
private boolean checkOption(List<ValidationMessage> errors, Element answer, NodeStack stack, QuestionnaireWithContext qSrc, QuestionnaireItemComponent qItem, String type, boolean openChoice) {
@ -928,31 +929,29 @@ public class QuestionnaireValidator extends BaseValidator {
Element v = answer.getNamedChild("valueString", false);
NodeStack ns = stack.push(v, -1, null, null);
if (qItem.getAnswerOption().size() > 0) {
List<StringType> list = new ArrayList<StringType>();
boolean found = false;
boolean empty = true;
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 (components.getValue() != null && components.hasValueStringType()) {
empty = false;
found = found || components.getValue().primitiveValue().equals((v.primitiveValue()));
}
}
if (!openChoice) {
if (list.isEmpty()) {
if (empty) {
ok = rule(errors, NO_RULE_DATE, IssueType.STRUCTURE, v.line(), v.col(), stack.getLiteralPath(), false, I18nConstants.QUESTIONNAIRE_QR_ITEM_NOOPTIONSSTRING) && ok;
} else {
boolean found = false;
for (StringType item : list) {
if (item.getValue().equals((v.primitiveValue()))) {
found = true;
break;
}
}
if (!found) {
ok = rule(errors, NO_RULE_DATE, IssueType.STRUCTURE, v.line(), v.col(), stack.getLiteralPath(), found, I18nConstants.QUESTIONNAIRE_QR_ITEM_NOSTRING, v.primitiveValue()) && ok;
ok = rule(errors, NO_RULE_DATE, IssueType.STRUCTURE, v.line(), v.col(), stack.getLiteralPath(), found, I18nConstants.QUESTIONNAIRE_QR_ITEM_NOSTRING, v.primitiveValue()) && ok;
}
} else {
found = false;
for (QuestionnaireItemAnswerOptionComponent components : qItem.getAnswerOption()) {
if (components.getValue() != null && components.hasValueCoding()) {
Coding c = components.getValueCoding();
found = found || (c.hasDisplay() && c.getDisplay().equalsIgnoreCase(v.primitiveValue())) || (c.hasCode() && c.getCode().equalsIgnoreCase(v.primitiveValue()));
}
}
ok = warning(errors, NO_RULE_DATE, IssueType.STRUCTURE, v.line(), v.col(), stack.getLiteralPath(), !found, I18nConstants.QUESTIONNAIRE_QR_ITEM_STRING_IN_CODING, v.primitiveValue()) && ok;
}
} else {
hint(errors, NO_RULE_DATE, IssueType.STRUCTURE, v.line(), v.col(), stack.getLiteralPath(), false, I18nConstants.QUESTIONNAIRE_QR_ITEM_STRINGNOOPTIONS);