Send Accept header on client requests, and allow string responses in

QuestionnaireResponse validator for questions of type OPENCHOICE
This commit is contained in:
James Agnew 2015-09-24 13:40:07 -04:00
parent 81e7d8c071
commit 072c1ece87
13 changed files with 581 additions and 360 deletions

View File

@ -31,6 +31,7 @@ import org.apache.http.Header;
import org.apache.http.client.methods.HttpRequestBase;
import org.apache.http.message.BasicHeader;
import ca.uhn.fhir.rest.server.Constants;
import ca.uhn.fhir.rest.server.EncodingEnum;
import ca.uhn.fhir.util.VersionUtil;
@ -85,7 +86,7 @@ public abstract class BaseHttpClientInvocation {
}
}
public void addHeadersToRequest(HttpRequestBase theHttpRequest) {
public void addHeadersToRequest(HttpRequestBase theHttpRequest, EncodingEnum theEncoding) {
if (myHeaders != null) {
for (Header next : myHeaders) {
theHttpRequest.addHeader(next);
@ -96,6 +97,13 @@ public abstract class BaseHttpClientInvocation {
theHttpRequest.addHeader("Accept-Charset", "utf-8");
theHttpRequest.addHeader("Accept-Encoding", "gzip");
if (theEncoding == null) {
theHttpRequest.addHeader(Constants.HEADER_ACCEPT, Constants.HEADER_ACCEPT_VALUE_ALL);
} else if (theEncoding == EncodingEnum.JSON) {
theHttpRequest.addHeader(Constants.HEADER_ACCEPT, Constants.CT_FHIR_JSON);
} else if (theEncoding == EncodingEnum.XML) {
theHttpRequest.addHeader(Constants.HEADER_ACCEPT, Constants.CT_FHIR_XML);
}
}
}

View File

@ -916,7 +916,7 @@ public class GenericClient extends BaseClient implements IGenericClient {
public Object execute() {
ResourceResponseHandler binding = new ResourceResponseHandler(myType.getImplementingClass(), null);
HttpGetClientInvocation invocation = MethodUtil.createConformanceInvocation();
return invokeClient(myContext, binding, invocation, myLogRequestAndResponse);
return super.invoke(null, binding, invocation);
}
@Override

View File

@ -280,6 +280,7 @@ public class RestfulClientFactory implements IRestfulClientFactory {
void validateServerBase(String theServerBase, HttpClient theHttpClient, BaseClient theClient) {
GenericClient client = new GenericClient(myContext, theHttpClient, theServerBase, this);
client.setEncoding(theClient.getEncoding());
for (IClientInterceptor interceptor : theClient.getInterceptors()) {
client.registerInterceptor(interceptor);
}

View File

@ -298,7 +298,7 @@ abstract class BaseHttpClientInvocationWithContents extends BaseHttpClientInvoca
}
HttpRequestBase retVal = createRequest(url, entity);
super.addHeadersToRequest(retVal);
super.addHeadersToRequest(retVal, encoding);
addMatchHeaders(retVal, url);
if (contentType != null) {

View File

@ -62,7 +62,7 @@ public class HttpDeleteClientInvocation extends BaseHttpClientInvocation {
appendExtraParamsWithQuestionMark(theExtraParams, b, b.indexOf("?") == -1);
HttpDelete retVal = new HttpDelete(b.toString());
super.addHeadersToRequest(retVal);
super.addHeadersToRequest(retVal, theEncoding);
return retVal;
}

View File

@ -103,7 +103,7 @@ public class HttpGetClientInvocation extends BaseHttpClientInvocation {
appendExtraParamsWithQuestionMark(theExtraParams, b, first);
HttpGet retVal = new HttpGet(b.toString());
super.addHeadersToRequest(retVal);
super.addHeadersToRequest(retVal, theEncoding);
return retVal;
}

View File

@ -40,7 +40,7 @@ public class HttpSimpleGetClientInvocation extends BaseHttpClientInvocation {
@Override
public HttpRequestBase asHttpRequest(String theUrlBase, Map<String, List<String>> theExtraParams, EncodingEnum theEncoding, Boolean thePrettyPrint) {
HttpGet retVal = new HttpGet(myUrl);
super.addHeadersToRequest(retVal);
super.addHeadersToRequest(retVal, theEncoding);
return retVal;
}

View File

@ -54,6 +54,7 @@ public class Constants {
public static final String FORMAT_XML = "xml";
public static final String HEADER_ACCEPT = "Accept";
public static final String HEADER_ACCEPT_ENCODING = "Accept-Encoding";
public static final String HEADER_ACCEPT_VALUE_ALL = CT_FHIR_XML + ";q=1.0, " + CT_FHIR_XML + ";q=1.0";
public static final String HEADER_ALLOW = "Allow";
public static final String HEADER_AUTHORIZATION = "Authorization";
public static final String HEADER_AUTHORIZATION_VALPREFIX_BASIC = "Basic ";

View File

@ -625,7 +625,7 @@ public abstract class BaseHapiFhirResourceDao<T extends IResource> extends BaseH
resourceId = dt.getIdPart();
}
Long targetPid = translateForcedIdToPid(new IdDt(resourceId));
ourLog.info("Searching for resource link with target PID: {}", targetPid);
ourLog.debug("Searching for resource link with target PID: {}", targetPid);
Predicate eq = builder.equal(from.get("myTargetResourcePid"), targetPid);
codePredicates.add(eq);

View File

@ -77,6 +77,7 @@ public class GenericClientDstu2Test {
ourCtx.getRestfulClientFactory().setHttpClient(myHttpClient);
ourCtx.getRestfulClientFactory().setServerValidationMode(ServerValidationModeEnum.NEVER);
myHttpResponse = mock(HttpResponse.class, new ReturnsDeepStubs());
myResponseCount = 0;
}
private String extractBody(ArgumentCaptor<HttpUriRequest> capt, int count) throws IOException {
@ -139,6 +140,130 @@ public class GenericClientDstu2Test {
idx++;
}
@Test
public void testAcceptHeaderFetchConformance() throws Exception {
IParser p = ourCtx.newXmlParser();
Conformance conf = new Conformance();
conf.setCopyright("COPY");
final String respString = p.encodeResourceToString(conf);
ArgumentCaptor<HttpUriRequest> capt = ArgumentCaptor.forClass(HttpUriRequest.class);
when(myHttpClient.execute(capt.capture())).thenReturn(myHttpResponse);
when(myHttpResponse.getStatusLine()).thenReturn(new BasicStatusLine(new ProtocolVersion("HTTP", 1, 1), 200, "OK"));
when(myHttpResponse.getEntity().getContentType()).thenReturn(new BasicHeader("content-type", Constants.CT_FHIR_XML + "; charset=UTF-8"));
when(myHttpResponse.getEntity().getContent()).thenAnswer(new Answer<ReaderInputStream>() {
@Override
public ReaderInputStream answer(InvocationOnMock theInvocation) throws Throwable {
return new ReaderInputStream(new StringReader(respString), Charset.forName("UTF-8"));
}
});
IGenericClient client = ourCtx.newRestfulGenericClient("http://example.com/fhir");
int idx = 0;
Conformance resp = (Conformance)client.fetchConformance().ofType(Conformance.class).execute();
assertEquals("http://example.com/fhir/metadata", capt.getAllValues().get(idx).getURI().toASCIIString());
assertEquals(1, capt.getAllValues().get(idx).getHeaders("Accept").length);
assertThat(capt.getAllValues().get(idx).getHeaders("Accept")[0].getValue(), containsString(Constants.HEADER_ACCEPT_VALUE_ALL));
idx++;
resp = (Conformance)client.fetchConformance().ofType(Conformance.class).encodedJson().execute();
assertEquals("http://example.com/fhir/metadata?_format=json", capt.getAllValues().get(idx).getURI().toASCIIString());
assertEquals(1, capt.getAllValues().get(idx).getHeaders("Accept").length);
assertThat(capt.getAllValues().get(idx).getHeaders("Accept")[0].getValue(), containsString(Constants.CT_FHIR_JSON));
idx++;
resp = (Conformance)client.fetchConformance().ofType(Conformance.class).encodedXml().execute();
assertEquals("http://example.com/fhir/metadata?_format=xml", capt.getAllValues().get(idx).getURI().toASCIIString());
assertEquals(1, capt.getAllValues().get(idx).getHeaders("Accept").length);
assertThat(capt.getAllValues().get(idx).getHeaders("Accept")[0].getValue(), containsString(Constants.CT_FHIR_XML));
idx++;
}
private int myResponseCount = 0;
@Test
public void testAcceptHeaderPreflightConformancePreferJson() throws Exception {
String methodName = "testAcceptHeaderPreflightConformancePreferJson";
final IParser p = ourCtx.newXmlParser();
final Conformance conf = new Conformance();
conf.setCopyright("COPY");
final Patient patient = new Patient();
patient.addName().addFamily("FAMILY");
ArgumentCaptor<HttpUriRequest> capt = ArgumentCaptor.forClass(HttpUriRequest.class);
when(myHttpClient.execute(capt.capture())).thenReturn(myHttpResponse);
when(myHttpResponse.getStatusLine()).thenReturn(new BasicStatusLine(new ProtocolVersion("HTTP", 1, 1), 200, "OK"));
when(myHttpResponse.getEntity().getContentType()).thenReturn(new BasicHeader("content-type", Constants.CT_FHIR_XML + "; charset=UTF-8"));
when(myHttpResponse.getEntity().getContent()).thenAnswer(new Answer<ReaderInputStream>() {
@Override
public ReaderInputStream answer(InvocationOnMock theInvocation) throws Throwable {
if (myResponseCount++ == 0) {
return new ReaderInputStream(new StringReader(p.encodeResourceToString(conf)), Charset.forName("UTF-8"));
} else {
return new ReaderInputStream(new StringReader(p.encodeResourceToString(patient)), Charset.forName("UTF-8"));
}
}
});
ourCtx.getRestfulClientFactory().setServerValidationMode(ServerValidationModeEnum.ONCE);
IGenericClient client = ourCtx.newRestfulGenericClient("http://"+methodName+".example.com/fhir");
client.setEncoding(EncodingEnum.JSON);
Patient resp = client.read(Patient.class, new IdDt("123"));
assertEquals("FAMILY", resp.getName().get(0).getFamily().get(0).getValue());
assertEquals("http://"+methodName+".example.com/fhir/metadata?_format=json", capt.getAllValues().get(0).getURI().toASCIIString());
assertEquals(1, capt.getAllValues().get(0).getHeaders("Accept").length);
assertThat(capt.getAllValues().get(0).getHeaders("Accept")[0].getValue(), containsString(Constants.CT_FHIR_JSON));
assertEquals("http://"+methodName+".example.com/fhir/Patient/123?_format=json", capt.getAllValues().get(1).getURI().toASCIIString());
assertEquals(1, capt.getAllValues().get(1).getHeaders("Accept").length);
assertThat(capt.getAllValues().get(1).getHeaders("Accept")[0].getValue(), containsString(Constants.CT_FHIR_JSON));
}
@Test
public void testAcceptHeaderPreflightConformance() throws Exception {
String methodName = "testAcceptHeaderPreflightConformance";
final IParser p = ourCtx.newXmlParser();
final Conformance conf = new Conformance();
conf.setCopyright("COPY");
final Patient patient = new Patient();
patient.addName().addFamily("FAMILY");
ArgumentCaptor<HttpUriRequest> capt = ArgumentCaptor.forClass(HttpUriRequest.class);
when(myHttpClient.execute(capt.capture())).thenReturn(myHttpResponse);
when(myHttpResponse.getStatusLine()).thenReturn(new BasicStatusLine(new ProtocolVersion("HTTP", 1, 1), 200, "OK"));
when(myHttpResponse.getEntity().getContentType()).thenReturn(new BasicHeader("content-type", Constants.CT_FHIR_XML + "; charset=UTF-8"));
when(myHttpResponse.getEntity().getContent()).thenAnswer(new Answer<ReaderInputStream>() {
@Override
public ReaderInputStream answer(InvocationOnMock theInvocation) throws Throwable {
if (myResponseCount++ == 0) {
return new ReaderInputStream(new StringReader(p.encodeResourceToString(conf)), Charset.forName("UTF-8"));
} else {
return new ReaderInputStream(new StringReader(p.encodeResourceToString(patient)), Charset.forName("UTF-8"));
}
}
});
ourCtx.getRestfulClientFactory().setServerValidationMode(ServerValidationModeEnum.ONCE);
IGenericClient client = ourCtx.newRestfulGenericClient("http://"+methodName+".example.com/fhir");
Patient resp = client.read(Patient.class, new IdDt("123"));
assertEquals("FAMILY", resp.getName().get(0).getFamily().get(0).getValue());
assertEquals("http://"+methodName+".example.com/fhir/metadata", capt.getAllValues().get(0).getURI().toASCIIString());
assertEquals(1, capt.getAllValues().get(0).getHeaders("Accept").length);
assertThat(capt.getAllValues().get(0).getHeaders("Accept")[0].getValue(), containsString(Constants.HEADER_ACCEPT_VALUE_ALL));
assertEquals("http://"+methodName+".example.com/fhir/Patient/123", capt.getAllValues().get(1).getURI().toASCIIString());
assertEquals(1, capt.getAllValues().get(1).getHeaders("Accept").length);
assertThat(capt.getAllValues().get(1).getHeaders("Accept")[0].getValue(), containsString(Constants.HEADER_ACCEPT_VALUE_ALL));
}
@Test
public void testCreate() throws Exception {

View File

@ -72,8 +72,15 @@ public class QuestionnaireResponseValidator extends BaseValidator {
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) {
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>();
@ -85,8 +92,7 @@ public class QuestionnaireResponseValidator extends BaseValidator {
return retVal;
}
private List<org.hl7.fhir.instance.model.QuestionnaireResponse.GroupComponent> findGroupByLinkId(
List<org.hl7.fhir.instance.model.QuestionnaireResponse.GroupComponent> theGroups, String theLinkId) {
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) {
@ -100,28 +106,25 @@ public class QuestionnaireResponseValidator extends BaseValidator {
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);
// }
// 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")) {
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) {
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())) {
if (!fail(theErrors, IssueType.INVALID, pathStack, questionnaire != null, "Questionnaire {0} is not found in the WorkerContext", theAnswers.getQuestionnaire().getReference())) {
return;
}
@ -185,9 +188,8 @@ public class QuestionnaireResponseValidator extends BaseValidator {
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) {
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");
@ -205,11 +207,10 @@ public class QuestionnaireResponseValidator extends BaseValidator {
// 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);
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());
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();
}
@ -217,21 +218,18 @@ public class QuestionnaireResponseValidator extends BaseValidator {
}
private void validateQuestion(List<ValidationMessage> theErrors, QuestionComponent theQuestion,
org.hl7.fhir.instance.model.QuestionnaireResponse.GroupComponent theAnsGroup, LinkedList<String> thePathStack,
QuestionnaireResponse theAnswers, boolean 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")) {
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");
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");
}
@ -256,27 +254,22 @@ public class QuestionnaireResponseValidator extends BaseValidator {
// 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);
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);
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);
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);
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);
hint(theErrors, IssueType.BUSINESSRULE, thePathStack, !question.getRequired(), "Missing answer to required question with linkId[{0}]", linkId);
}
return;
}
@ -291,24 +284,19 @@ public class QuestionnaireResponseValidator extends BaseValidator {
}
}
private void validateQuestionGroups(List<ValidationMessage> theErrors, QuestionComponent theQuestion,
org.hl7.fhir.instance.model.QuestionnaireResponse.QuestionComponent theAnswerQuestion,
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);
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 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,
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) {
@ -316,11 +304,9 @@ public class QuestionnaireResponseValidator extends BaseValidator {
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);
"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}]",
fail(theErrors, IssueType.BUSINESSRULE, thePathStack, false, "Questionnaire in invalid, unable to validate QuestionnaireResponse: Multiple groups found at this position with linkId[{0}]",
nextLinkId);
}
}
@ -331,16 +317,13 @@ public class QuestionnaireResponseValidator extends BaseValidator {
String linkId = nextQuestionGroup.getLinkId();
allowedGroups.add(linkId);
List<org.hl7.fhir.instance.model.QuestionnaireResponse.GroupComponent> answerGroups = findGroupByLinkId(
theAnswerGroups, 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);
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);
hint(theErrors, IssueType.BUSINESSRULE, thePathStack, false, "Missing required group with linkId[{0}]", linkId);
}
}
continue;
@ -349,9 +332,7 @@ public class QuestionnaireResponseValidator extends BaseValidator {
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);
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();
}
}
@ -369,36 +350,27 @@ public class QuestionnaireResponseValidator extends BaseValidator {
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",
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) {
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()) {
rule(theErrors, IssueType.BUSINESSRULE, thePathStack, answerQuestion.isEmpty(),
"Question with linkId[{0}] has no answer type but an answer was provided", linkId);
rule(theErrors, IssueType.BUSINESSRULE, thePathStack, answerQuestion.isEmpty(), "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);
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);
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);
hint(theErrors, IssueType.BUSINESSRULE, thePathStack, !(theQuestion.getRequired() && answerQuestion.getAnswer().isEmpty()), "Missing answer to required question with linkId[{0}]", linkId);
}
}
@ -409,72 +381,66 @@ public class QuestionnaireResponseValidator extends BaseValidator {
thePathStack.add("answer[" + answerIdx + "]");
Type nextValue = nextAnswer.getValue();
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());
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) {
Coding coding = (Coding) nextAnswer.getValue();
if (isBlank(coding.getCode()) && isBlank(coding.getSystem()) && isBlank(coding.getSystem())) {
rule(theErrors, IssueType.BUSINESSRULE, thePathStack, false,
"Answer to question with linkId[{0}] is of type coding, but none of code, system, and display are populated",
linkId);
continue;
} else if (isBlank(coding.getCode()) && isBlank(coding.getSystem())) {
if (type != AnswerFormat.OPENCHOICE) {
rule(theErrors, IssueType.BUSINESSRULE, thePathStack, false,
"Answer to question with linkId[{0}] is of type only has a display populated (no code or system) but question does not allow {1}",
linkId, AnswerFormat.OPENCHOICE.name());
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 if (isBlank(coding.getCode()) || isBlank(coding.getSystem())) {
rule(theErrors, IssueType.BUSINESSRULE, thePathStack, false,
"Answer to question with linkId[{0}] has a coding, but this coding does not contain a code and system (both must be present, or neither is the question allows {1})",
linkId, AnswerFormat.OPENCHOICE.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);
} 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;
}
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;
}
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;
}
}
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;
}
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) {
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);
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);
}
}
}
@ -513,7 +479,7 @@ public class QuestionnaireResponseValidator extends BaseValidator {
allowedAnswerTypes = allowedTypes(IntegerType.class);
break;
case OPENCHOICE:
allowedAnswerTypes = allowedTypes(Coding.class);
allowedAnswerTypes = allowedTypes(Coding.class, StringType.class);
break;
case QUANTITY:
allowedAnswerTypes = allowedTypes(Quantity.class);

View File

@ -1,6 +1,7 @@
package ca.uhn.fhir.validation;
import static org.hamcrest.Matchers.containsString;
import static org.hamcrest.Matchers.empty;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertThat;
@ -10,6 +11,7 @@ import java.util.List;
import org.apache.commons.io.IOUtils;
import org.hl7.fhir.instance.model.Coding;
import org.hl7.fhir.instance.model.DataElement;
import org.hl7.fhir.instance.model.IntegerType;
import org.hl7.fhir.instance.model.Questionnaire;
import org.hl7.fhir.instance.model.Questionnaire.AnswerFormat;
import org.hl7.fhir.instance.model.Questionnaire.GroupComponent;
@ -27,240 +29,347 @@ import org.junit.Test;
import ca.uhn.fhir.context.FhirContext;
public class QuestionnaireResponseValidatorTest {
private static final FhirContext ourCtx = FhirContext.forDstu2Hl7Org();
private static final org.slf4j.Logger ourLog = org.slf4j.LoggerFactory.getLogger(QuestionnaireResponseValidatorTest.class);
private QuestionnaireResponseValidator myVal;
private static final FhirContext ourCtx = FhirContext.forDstu2Hl7Org();
private WorkerContext myWorkerCtx;
@Before
public void before() {
myWorkerCtx = new WorkerContext();
myVal = new QuestionnaireResponseValidator(myWorkerCtx);
}
@Test
public void testAnswerWithWrongType() {
Questionnaire q = new Questionnaire();
q.getGroup().addQuestion().setLinkId("link0").setRequired(true).setType(AnswerFormat.BOOLEAN);
QuestionnaireResponse qa = new QuestionnaireResponse();
qa.getQuestionnaire().setReference("http://example.com/Questionnaire/q1");
qa.getGroup().addQuestion().setLinkId("link0").addAnswer().setValue(new StringType("FOO"));
myWorkerCtx.getQuestionnaires().put(qa.getQuestionnaire().getReference(), q);
List<ValidationMessage> errors = new ArrayList<ValidationMessage>();
myVal.validate(errors, qa);
ourLog.info(errors.toString());
assertThat(errors.toString(), containsString("Answer to question with linkId[link0] found of type [StringType] but this is invalid for question of type [boolean]"));
}
private static final org.slf4j.Logger ourLog = org.slf4j.LoggerFactory.getLogger(QuestionnaireResponseValidatorTest.class);
private QuestionnaireResponseValidator myVal;
@Test
public void testCodedAnswer() {
String questionnaireRef = "http://example.com/Questionnaire/q1";
Questionnaire q = new Questionnaire();
q.getGroup().addQuestion().setLinkId("link0").setRequired(false).setType(AnswerFormat.CHOICE).setOptions(new Reference("http://somevalueset"));
myWorkerCtx.getQuestionnaires().put(questionnaireRef, q);
ValueSet options = new ValueSet();
options.getCodeSystem().setSystem("urn:system").addConcept().setCode("code0");
options.getCompose().addInclude().setSystem("urn:system2").addConcept().setCode("code2");
myWorkerCtx.getValueSets().put("http://somevalueset", options);
QuestionnaireResponse qa;
List<ValidationMessage> errors;
// Good code
qa = new QuestionnaireResponse();
qa.getQuestionnaire().setReference(questionnaireRef);
qa.getGroup().addQuestion().setLinkId("link0").addAnswer().setValue(new Coding().setSystem("urn:system").setCode("code0"));
errors = new ArrayList<ValidationMessage>();
myVal.validate(errors, qa);
assertEquals(errors.toString(), 0, errors.size());
private WorkerContext myWorkerCtx;
qa = new QuestionnaireResponse();
qa.getQuestionnaire().setReference(questionnaireRef);
qa.getGroup().addQuestion().setLinkId("link0").addAnswer().setValue(new Coding().setSystem("urn:system2").setCode("code2"));
errors = new ArrayList<ValidationMessage>();
myVal.validate(errors, qa);
assertEquals(errors.toString(), 0, errors.size());
@Before
public void before() {
myWorkerCtx = new WorkerContext();
myVal = new QuestionnaireResponseValidator(myWorkerCtx);
}
// Bad code
qa = new QuestionnaireResponse();
qa.getQuestionnaire().setReference(questionnaireRef);
qa.getGroup().addQuestion().setLinkId("link0").addAnswer().setValue(new Coding().setSystem("urn:system").setCode("code1"));
errors = new ArrayList<ValidationMessage>();
myVal.validate(errors, qa);
ourLog.info(errors.toString());
assertThat(errors.toString(), containsString("location=//QuestionnaireResponse/group[0]/question[0]/answer[0]"));
assertThat(errors.toString(), containsString("message=Question with linkId[link0] has answer with system[urn:system] and code[code1] but this is not a valid answer for ValueSet[http://somevalueset]"));
qa = new QuestionnaireResponse();
qa.getQuestionnaire().setReference(questionnaireRef);
qa.getGroup().addQuestion().setLinkId("link0").addAnswer().setValue(new Coding().setSystem("urn:system2").setCode("code3"));
errors = new ArrayList<ValidationMessage>();
myVal.validate(errors, qa);
ourLog.info(errors.toString());
assertThat(errors.toString(), containsString("location=//QuestionnaireResponse/group[0]/question[0]/answer[0]"));
assertThat(errors.toString(), containsString("message=Question with linkId[link0] has answer with system[urn:system2] and code[code3] but this is not a valid answer for ValueSet[http://somevalueset]"));
}
@Test
public void testExtensionDereference() throws Exception {
Questionnaire q = ourCtx.newJsonParser().parseResource(Questionnaire.class, IOUtils.toString(getClass().getResourceAsStream("/dereference-q.json")));
QuestionnaireResponse qa = ourCtx.newXmlParser().parseResource(QuestionnaireResponse.class, IOUtils.toString(getClass().getResourceAsStream("/dereference-qr.xml")));
DataElement de = ourCtx.newJsonParser().parseResource(DataElement.class, IOUtils.toString(getClass().getResourceAsStream("/dereference-de.json")));
myWorkerCtx.getQuestionnaires().put(qa.getQuestionnaire().getReference(), q);
myWorkerCtx.getDataElements().put("DataElement/4771", de);
List<ValidationMessage> errors = new ArrayList<ValidationMessage>();
myVal.validate(errors, qa);
ourLog.info(errors.toString());
assertEquals(errors.toString(), errors.size(), 0);
}
@Test
public void testAnswerWithWrongType() {
Questionnaire q = new Questionnaire();
q.getGroup().addQuestion().setLinkId("link0").setRequired(true).setType(AnswerFormat.BOOLEAN);
@Test
public void testGroupWithNoLinkIdInQuestionnaireResponse() {
Questionnaire q = new Questionnaire();
GroupComponent qGroup = q.getGroup().addGroup();
qGroup.addQuestion().setLinkId("link0").setRequired(true).setType(AnswerFormat.BOOLEAN);
QuestionnaireResponse qa = new QuestionnaireResponse();
qa.getQuestionnaire().setReference("http://example.com/Questionnaire/q1");
org.hl7.fhir.instance.model.QuestionnaireResponse.GroupComponent qaGroup = qa.getGroup().addGroup();
qaGroup.addQuestion().setLinkId("link0").addAnswer().setValue(new StringType("FOO"));
myWorkerCtx.getQuestionnaires().put(qa.getQuestionnaire().getReference(), q);
List<ValidationMessage> errors = new ArrayList<ValidationMessage>();
myVal.validate(errors, qa);
ourLog.info(errors.toString());
assertThat(errors.toString(), containsString("Answer to question with linkId[link0] found of type [StringType] but this is invalid for question of type [boolean]"));
}
QuestionnaireResponse qa = new QuestionnaireResponse();
qa.getQuestionnaire().setReference("http://example.com/Questionnaire/q1");
qa.getGroup().addQuestion().setLinkId("link0").addAnswer().setValue(new StringType("FOO"));
@Test
public void testMissingRequiredQuestion() {
Questionnaire q = new Questionnaire();
q.getGroup().addQuestion().setLinkId("link0").setRequired(true).setType(AnswerFormat.STRING);
q.getGroup().addQuestion().setLinkId("link1").setRequired(true).setType(AnswerFormat.STRING);
QuestionnaireResponse qa = new QuestionnaireResponse();
qa.setStatus(QuestionnaireResponseStatus.COMPLETED);
qa.getQuestionnaire().setReference("http://example.com/Questionnaire/q1");
qa.getGroup().addQuestion().setLinkId("link1").addAnswer().setValue(new StringType("FOO"));
myWorkerCtx.getQuestionnaires().put(qa.getQuestionnaire().getReference(), q);
List<ValidationMessage> errors = new ArrayList<ValidationMessage>();
myVal.validate(errors, qa);
ourLog.info(errors.toString());
assertThat(errors.toString(), containsString("Missing answer to required question with linkId[link0]"));
}
myWorkerCtx.getQuestionnaires().put(qa.getQuestionnaire().getReference(), q);
List<ValidationMessage> errors = new ArrayList<ValidationMessage>();
myVal.validate(errors, qa);
ourLog.info(errors.toString());
assertThat(errors.toString(), containsString("Answer to question with linkId[link0] found of type [StringType] but this is invalid for question of type [boolean]"));
}
@Test
public void testMultipleGroupsWithNoLinkIdInQuestionnaire() {
Questionnaire q = new Questionnaire();
GroupComponent qGroup = q.getGroup().addGroup();
qGroup.addQuestion().setLinkId("link0").setRequired(true).setType(AnswerFormat.BOOLEAN);
qGroup = q.getGroup().addGroup();
qGroup.addQuestion().setLinkId("link1").setRequired(true).setType(AnswerFormat.BOOLEAN);
QuestionnaireResponse qa = new QuestionnaireResponse();
qa.getQuestionnaire().setReference("http://example.com/Questionnaire/q1");
org.hl7.fhir.instance.model.QuestionnaireResponse.GroupComponent qaGroup = qa.getGroup().addGroup();
qaGroup.addQuestion().setLinkId("link0").addAnswer().setValue(new StringType("FOO"));
myWorkerCtx.getQuestionnaires().put(qa.getQuestionnaire().getReference(), q);
List<ValidationMessage> errors = new ArrayList<ValidationMessage>();
myVal.validate(errors, qa);
ourLog.info(errors.toString());
assertThat(errors.toString(), containsString("ValidationMessage[level=FATAL,type=BUSINESSRULE,location=//QuestionnaireResponse/group[0],message=Questionnaire in invalid, unable to validate QuestionnaireResponse: Multiple groups found at this position with blank/missing linkId]"));
assertEquals(1, errors.size());
}
@Test
public void testCodedAnswer() {
String questionnaireRef = "http://example.com/Questionnaire/q1";
@Test
public void testUnexpectedAnswer() {
Questionnaire q = new Questionnaire();
q.getGroup().addQuestion().setLinkId("link0").setRequired(false).setType(AnswerFormat.BOOLEAN);
QuestionnaireResponse qa = new QuestionnaireResponse();
qa.getQuestionnaire().setReference("http://example.com/Questionnaire/q1");
qa.getGroup().addQuestion().setLinkId("link1").addAnswer().setValue(new StringType("FOO"));
myWorkerCtx.getQuestionnaires().put(qa.getQuestionnaire().getReference(), q);
List<ValidationMessage> errors = new ArrayList<ValidationMessage>();
myVal.validate(errors, qa);
ourLog.info(errors.toString());
assertThat(errors.toString(), containsString("location=//QuestionnaireResponse/group[0]/question[0]"));
assertThat(errors.toString(), containsString("message=Found answer with linkId[link1] but this ID is not allowed at this position"));
}
@Test
public void testUnexpectedGroup() {
Questionnaire q = new Questionnaire();
q.getGroup().addQuestion().setLinkId("link0").setRequired(false).setType(AnswerFormat.BOOLEAN);
QuestionnaireResponse qa = new QuestionnaireResponse();
qa.getQuestionnaire().setReference("http://example.com/Questionnaire/q1");
qa.getGroup().addGroup().setLinkId("link1");
myWorkerCtx.getQuestionnaires().put(qa.getQuestionnaire().getReference(), q);
List<ValidationMessage> errors = new ArrayList<ValidationMessage>();
myVal.validate(errors, qa);
ourLog.info(errors.toString());
assertThat(errors.toString(), containsString("location=//QuestionnaireResponse/group[0]/group[0]"));
assertThat(errors.toString(), containsString("Group with linkId[link1] found at this position, but this group does not exist at this position in Questionnaire"));
}
Questionnaire q = new Questionnaire();
q.getGroup().addQuestion().setLinkId("link0").setRequired(false).setType(AnswerFormat.CHOICE).setOptions(new Reference("http://somevalueset"));
myWorkerCtx.getQuestionnaires().put(questionnaireRef, q);
// @Test
public void validateHealthConnexExample() throws Exception {
String input = IOUtils.toString(QuestionnaireResponseValidatorTest.class.getResourceAsStream("/questionnaireanswers-0f431c50ddbe4fff8e0dd6b7323625fc.xml"));
ValueSet options = new ValueSet();
options.getCodeSystem().setSystem("urn:system").addConcept().setCode("code0");
options.getCompose().addInclude().setSystem("urn:system2").addConcept().setCode("code2");
myWorkerCtx.getValueSets().put("http://somevalueset", options);
QuestionnaireResponse qa = ourCtx.newXmlParser().parseResource(QuestionnaireResponse.class, input);
ArrayList<ValidationMessage> errors = new ArrayList<ValidationMessage>();
myVal.validate(errors, qa);
assertEquals(errors.toString(), 0, errors.size());
/*
* Now change a coded value
*/
//@formatter:off
input = input.replaceAll("<answer>\n" +
" <valueCoding>\n" +
" <system value=\"f69573b8-cb63-4d31-85a4-23ac784735ab\"/>\n" +
" <code value=\"2\"/>\n" +
" <display value=\"Once/twice\"/>\n" +
" </valueCoding>\n" +
" </answer>", "<answer>\n" +
" <valueCoding>\n" +
" <system value=\"f69573b8-cb63-4d31-85a4-23ac784735ab\"/>\n" +
" <code value=\"GGG\"/>\n" +
" <display value=\"Once/twice\"/>\n" +
" </valueCoding>\n" +
" </answer>");
assertThat(input, containsString("GGG"));
//@formatter:on
qa = ourCtx.newXmlParser().parseResource(QuestionnaireResponse.class, input);
errors = new ArrayList<ValidationMessage>();
myVal.validate(errors, qa);
assertEquals(errors.toString(), 10, errors.size());
}
QuestionnaireResponse qa;
List<ValidationMessage> errors;
// Good code
qa = new QuestionnaireResponse();
qa.getQuestionnaire().setReference(questionnaireRef);
qa.getGroup().addQuestion().setLinkId("link0").addAnswer().setValue(new Coding().setSystem("urn:system").setCode("code0"));
errors = new ArrayList<ValidationMessage>();
myVal.validate(errors, qa);
assertEquals(errors.toString(), 0, errors.size());
qa = new QuestionnaireResponse();
qa.getQuestionnaire().setReference(questionnaireRef);
qa.getGroup().addQuestion().setLinkId("link0").addAnswer().setValue(new Coding().setSystem("urn:system2").setCode("code2"));
errors = new ArrayList<ValidationMessage>();
myVal.validate(errors, qa);
assertEquals(errors.toString(), 0, errors.size());
// Bad code
qa = new QuestionnaireResponse();
qa.getQuestionnaire().setReference(questionnaireRef);
qa.getGroup().addQuestion().setLinkId("link0").addAnswer().setValue(new Coding().setSystem("urn:system").setCode("code1"));
errors = new ArrayList<ValidationMessage>();
myVal.validate(errors, qa);
ourLog.info(errors.toString());
assertThat(errors.toString(), containsString("location=//QuestionnaireResponse/group[0]/question[0]/answer[0]"));
assertThat(errors.toString(),
containsString("message=Question with linkId[link0] has answer with system[urn:system] and code[code1] but this is not a valid answer for ValueSet[http://somevalueset]"));
qa = new QuestionnaireResponse();
qa.getQuestionnaire().setReference(questionnaireRef);
qa.getGroup().addQuestion().setLinkId("link0").addAnswer().setValue(new Coding().setSystem("urn:system2").setCode("code3"));
errors = new ArrayList<ValidationMessage>();
myVal.validate(errors, qa);
ourLog.info(errors.toString());
assertThat(errors.toString(), containsString("location=//QuestionnaireResponse/group[0]/question[0]/answer[0]"));
assertThat(errors.toString(),
containsString("message=Question with linkId[link0] has answer with system[urn:system2] and code[code3] but this is not a valid answer for ValueSet[http://somevalueset]"));
}
@Test
public void testOpenchoiceAnswer() {
String questionnaireRef = "http://example.com/Questionnaire/q1";
Questionnaire q = new Questionnaire();
q.getGroup().addQuestion().setLinkId("link0").setRequired(true).setType(AnswerFormat.OPENCHOICE).setOptions(new Reference("http://somevalueset"));
myWorkerCtx.getQuestionnaires().put(questionnaireRef, q);
ValueSet options = new ValueSet();
options.getCodeSystem().setSystem("urn:system").addConcept().setCode("code0");
options.getCompose().addInclude().setSystem("urn:system2").addConcept().setCode("code2");
myWorkerCtx.getValueSets().put("http://somevalueset", options);
QuestionnaireResponse qa;
List<ValidationMessage> errors;
// Good code
qa = new QuestionnaireResponse();
qa.getQuestionnaire().setReference(questionnaireRef);
qa.getGroup().addQuestion().setLinkId("link0").addAnswer().setValue(new Coding().setSystem("urn:system").setCode("code0"));
errors = new ArrayList<ValidationMessage>();
myVal.validate(errors, qa);
assertEquals(errors.toString(), 0, errors.size());
qa = new QuestionnaireResponse();
qa.getQuestionnaire().setReference(questionnaireRef);
qa.getGroup().addQuestion().setLinkId("link0").addAnswer().setValue(new Coding().setSystem("urn:system2").setCode("code2"));
errors = new ArrayList<ValidationMessage>();
myVal.validate(errors, qa);
assertEquals(errors.toString(), 0, errors.size());
// Bad code
qa = new QuestionnaireResponse();
qa.getQuestionnaire().setReference(questionnaireRef);
qa.getGroup().addQuestion().setLinkId("link0").addAnswer().setValue(new Coding().setSystem("urn:system").setCode("code1"));
errors = new ArrayList<ValidationMessage>();
myVal.validate(errors, qa);
ourLog.info(errors.toString());
assertThat(errors.toString(), containsString("location=//QuestionnaireResponse/group[0]/question[0]/answer[0]"));
assertThat(errors.toString(),
containsString("message=Question with linkId[link0] has answer with system[urn:system] and code[code1] but this is not a valid answer for ValueSet[http://somevalueset]"));
// Partial code
qa = new QuestionnaireResponse();
qa.getQuestionnaire().setReference(questionnaireRef);
qa.getGroup().addQuestion().setLinkId("link0").addAnswer().setValue(new Coding().setSystem(null).setCode("code1"));
errors = new ArrayList<ValidationMessage>();
myVal.validate(errors, qa);
ourLog.info(errors.toString());
assertThat(errors.toString(), containsString("location=//QuestionnaireResponse/group[0]/question[0]/answer[0]"));
assertThat(errors.toString(), containsString("message=Answer to question with linkId[link0] is of type OPENCHOICE but coding answer does not have a system"));
qa = new QuestionnaireResponse();
qa.getQuestionnaire().setReference(questionnaireRef);
qa.getGroup().addQuestion().setLinkId("link0").addAnswer().setValue(new Coding().setSystem("").setCode("code1"));
errors = new ArrayList<ValidationMessage>();
myVal.validate(errors, qa);
ourLog.info(errors.toString());
assertThat(errors.toString(), containsString("location=//QuestionnaireResponse/group[0]/question[0]/answer[0]"));
assertThat(errors.toString(), containsString("message=Answer to question with linkId[link0] is of type OPENCHOICE but coding answer does not have a system"));
qa = new QuestionnaireResponse();
qa.getQuestionnaire().setReference(questionnaireRef);
qa.getGroup().addQuestion().setLinkId("link0").addAnswer().setValue(new Coding().setSystem("system").setCode(null));
errors = new ArrayList<ValidationMessage>();
myVal.validate(errors, qa);
ourLog.info(errors.toString());
assertThat(errors.toString(), containsString("location=//QuestionnaireResponse/group[0]/question[0]/answer[0]"));
assertThat(errors.toString(), containsString("message=Answer to question with linkId[link0] is of type OPENCHOICE but coding answer does not have a code"));
qa = new QuestionnaireResponse();
qa.getQuestionnaire().setReference(questionnaireRef);
qa.getGroup().addQuestion().setLinkId("link0").addAnswer().setValue(new Coding().setSystem("system").setCode(null));
errors = new ArrayList<ValidationMessage>();
myVal.validate(errors, qa);
ourLog.info(errors.toString());
assertThat(errors.toString(), containsString("location=//QuestionnaireResponse/group[0]/question[0]/answer[0]"));
assertThat(errors.toString(), containsString("message=Answer to question with linkId[link0] is of type OPENCHOICE but coding answer does not have a code"));
// Wrong type
qa = new QuestionnaireResponse();
qa.getQuestionnaire().setReference(questionnaireRef);
qa.getGroup().addQuestion().setLinkId("link0").addAnswer().setValue(new IntegerType(123));
errors = new ArrayList<ValidationMessage>();
myVal.validate(errors, qa);
ourLog.info(errors.toString());
assertThat(errors.toString(), containsString("location=//QuestionnaireResponse/group[0]/question[0]/answer[0]"));
assertThat(errors.toString(), containsString("message=Answer to question with linkId[link0] found of type [IntegerType] but this is invalid for question of type [open-choice]"));
// String answer
qa = new QuestionnaireResponse();
qa.getQuestionnaire().setReference(questionnaireRef);
qa.getGroup().addQuestion().setLinkId("link0").addAnswer().setValue(new StringType("Hello"));
errors = new ArrayList<ValidationMessage>();
myVal.validate(errors, qa);
ourLog.info(errors.toString());
assertThat(errors, empty());
// Missing String answer
qa = new QuestionnaireResponse();
qa.getQuestionnaire().setReference(questionnaireRef);
qa.getGroup().addQuestion().setLinkId("link0").addAnswer().setValue(new StringType(""));
errors = new ArrayList<ValidationMessage>();
myVal.validate(errors, qa);
ourLog.info(errors.toString());
assertThat(errors.toString(), containsString("location=//QuestionnaireResponse/group[0]/question[0]/answer[0]"));
assertThat(errors.toString(), containsString("Answer to question with linkId[link0] is required but answer does not have a value"));
}
@Test
public void testExtensionDereference() throws Exception {
Questionnaire q = ourCtx.newJsonParser().parseResource(Questionnaire.class, IOUtils.toString(getClass().getResourceAsStream("/dereference-q.json")));
QuestionnaireResponse qa = ourCtx.newXmlParser().parseResource(QuestionnaireResponse.class, IOUtils.toString(getClass().getResourceAsStream("/dereference-qr.xml")));
DataElement de = ourCtx.newJsonParser().parseResource(DataElement.class, IOUtils.toString(getClass().getResourceAsStream("/dereference-de.json")));
myWorkerCtx.getQuestionnaires().put(qa.getQuestionnaire().getReference(), q);
myWorkerCtx.getDataElements().put("DataElement/4771", de);
List<ValidationMessage> errors = new ArrayList<ValidationMessage>();
myVal.validate(errors, qa);
ourLog.info(errors.toString());
assertEquals(errors.toString(), errors.size(), 0);
}
@Test
public void testGroupWithNoLinkIdInQuestionnaireResponse() {
Questionnaire q = new Questionnaire();
GroupComponent qGroup = q.getGroup().addGroup();
qGroup.addQuestion().setLinkId("link0").setRequired(true).setType(AnswerFormat.BOOLEAN);
QuestionnaireResponse qa = new QuestionnaireResponse();
qa.getQuestionnaire().setReference("http://example.com/Questionnaire/q1");
org.hl7.fhir.instance.model.QuestionnaireResponse.GroupComponent qaGroup = qa.getGroup().addGroup();
qaGroup.addQuestion().setLinkId("link0").addAnswer().setValue(new StringType("FOO"));
myWorkerCtx.getQuestionnaires().put(qa.getQuestionnaire().getReference(), q);
List<ValidationMessage> errors = new ArrayList<ValidationMessage>();
myVal.validate(errors, qa);
ourLog.info(errors.toString());
assertThat(errors.toString(), containsString("Answer to question with linkId[link0] found of type [StringType] but this is invalid for question of type [boolean]"));
}
@Test
public void testMissingRequiredQuestion() {
Questionnaire q = new Questionnaire();
q.getGroup().addQuestion().setLinkId("link0").setRequired(true).setType(AnswerFormat.STRING);
q.getGroup().addQuestion().setLinkId("link1").setRequired(true).setType(AnswerFormat.STRING);
QuestionnaireResponse qa = new QuestionnaireResponse();
qa.setStatus(QuestionnaireResponseStatus.COMPLETED);
qa.getQuestionnaire().setReference("http://example.com/Questionnaire/q1");
qa.getGroup().addQuestion().setLinkId("link1").addAnswer().setValue(new StringType("FOO"));
myWorkerCtx.getQuestionnaires().put(qa.getQuestionnaire().getReference(), q);
List<ValidationMessage> errors = new ArrayList<ValidationMessage>();
myVal.validate(errors, qa);
ourLog.info(errors.toString());
assertThat(errors.toString(), containsString("Missing answer to required question with linkId[link0]"));
}
@Test
public void testMultipleGroupsWithNoLinkIdInQuestionnaire() {
Questionnaire q = new Questionnaire();
GroupComponent qGroup = q.getGroup().addGroup();
qGroup.addQuestion().setLinkId("link0").setRequired(true).setType(AnswerFormat.BOOLEAN);
qGroup = q.getGroup().addGroup();
qGroup.addQuestion().setLinkId("link1").setRequired(true).setType(AnswerFormat.BOOLEAN);
QuestionnaireResponse qa = new QuestionnaireResponse();
qa.getQuestionnaire().setReference("http://example.com/Questionnaire/q1");
org.hl7.fhir.instance.model.QuestionnaireResponse.GroupComponent qaGroup = qa.getGroup().addGroup();
qaGroup.addQuestion().setLinkId("link0").addAnswer().setValue(new StringType("FOO"));
myWorkerCtx.getQuestionnaires().put(qa.getQuestionnaire().getReference(), q);
List<ValidationMessage> errors = new ArrayList<ValidationMessage>();
myVal.validate(errors, qa);
ourLog.info(errors.toString());
assertThat(errors.toString(), containsString(
"ValidationMessage[level=FATAL,type=BUSINESSRULE,location=//QuestionnaireResponse/group[0],message=Questionnaire in invalid, unable to validate QuestionnaireResponse: Multiple groups found at this position with blank/missing linkId]"));
assertEquals(1, errors.size());
}
@Test
public void testUnexpectedAnswer() {
Questionnaire q = new Questionnaire();
q.getGroup().addQuestion().setLinkId("link0").setRequired(false).setType(AnswerFormat.BOOLEAN);
QuestionnaireResponse qa = new QuestionnaireResponse();
qa.getQuestionnaire().setReference("http://example.com/Questionnaire/q1");
qa.getGroup().addQuestion().setLinkId("link1").addAnswer().setValue(new StringType("FOO"));
myWorkerCtx.getQuestionnaires().put(qa.getQuestionnaire().getReference(), q);
List<ValidationMessage> errors = new ArrayList<ValidationMessage>();
myVal.validate(errors, qa);
ourLog.info(errors.toString());
assertThat(errors.toString(), containsString("location=//QuestionnaireResponse/group[0]/question[0]"));
assertThat(errors.toString(), containsString("message=Found answer with linkId[link1] but this ID is not allowed at this position"));
}
@Test
public void testUnexpectedGroup() {
Questionnaire q = new Questionnaire();
q.getGroup().addQuestion().setLinkId("link0").setRequired(false).setType(AnswerFormat.BOOLEAN);
QuestionnaireResponse qa = new QuestionnaireResponse();
qa.getQuestionnaire().setReference("http://example.com/Questionnaire/q1");
qa.getGroup().addGroup().setLinkId("link1");
myWorkerCtx.getQuestionnaires().put(qa.getQuestionnaire().getReference(), q);
List<ValidationMessage> errors = new ArrayList<ValidationMessage>();
myVal.validate(errors, qa);
ourLog.info(errors.toString());
assertThat(errors.toString(), containsString("location=//QuestionnaireResponse/group[0]/group[0]"));
assertThat(errors.toString(), containsString("Group with linkId[link1] found at this position, but this group does not exist at this position in Questionnaire"));
}
// @Test
public void validateHealthConnexExample() throws Exception {
String input = IOUtils.toString(QuestionnaireResponseValidatorTest.class.getResourceAsStream("/questionnaireanswers-0f431c50ddbe4fff8e0dd6b7323625fc.xml"));
QuestionnaireResponse qa = ourCtx.newXmlParser().parseResource(QuestionnaireResponse.class, input);
ArrayList<ValidationMessage> errors = new ArrayList<ValidationMessage>();
myVal.validate(errors, qa);
assertEquals(errors.toString(), 0, errors.size());
/*
* Now change a coded value
*/
// @formatter:off
input = input.replaceAll(
"<answer>\n" + " <valueCoding>\n" + " <system value=\"f69573b8-cb63-4d31-85a4-23ac784735ab\"/>\n" + " <code value=\"2\"/>\n"
+ " <display value=\"Once/twice\"/>\n" + " </valueCoding>\n" + " </answer>",
"<answer>\n" + " <valueCoding>\n" + " <system value=\"f69573b8-cb63-4d31-85a4-23ac784735ab\"/>\n" + " <code value=\"GGG\"/>\n"
+ " <display value=\"Once/twice\"/>\n" + " </valueCoding>\n" + " </answer>");
assertThat(input, containsString("GGG"));
// @formatter:on
qa = ourCtx.newXmlParser().parseResource(QuestionnaireResponse.class, input);
errors = new ArrayList<ValidationMessage>();
myVal.validate(errors, qa);
assertEquals(errors.toString(), 10, errors.size());
}
}

View File

@ -49,6 +49,17 @@
Narrative generator did not include OperationOutcome.issue.diagnostics in the
generated narrative.
</action>
<action type="add">
Clients (generic and annotation) did not populate the Accept header on outgoing
requests. This is now populated to indicate that the client supports both XML and
JSON unless the user has explicitly requested one or the other (in which case the
appropriate type only will be send in the accept header). Thanks to
Avinash Shanbhag for reporting!
</action>
<action type="add">
QuestionnaireResponse validator now allows responses to questions of
type OPENCHOICE to be of type 'string'
</action>
</release>
<release version="1.2" date="2015-09-18">
<action type="add">