diff --git a/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/client/BaseHttpClientInvocation.java b/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/client/BaseHttpClientInvocation.java index 7c4a708695f..b2487cf9c3f 100644 --- a/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/client/BaseHttpClientInvocation.java +++ b/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/client/BaseHttpClientInvocation.java @@ -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); + } } } diff --git a/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/client/GenericClient.java b/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/client/GenericClient.java index 51465c6f96a..fc2dbae1c4b 100644 --- a/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/client/GenericClient.java +++ b/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/client/GenericClient.java @@ -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 diff --git a/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/client/RestfulClientFactory.java b/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/client/RestfulClientFactory.java index 1dec5c83350..83643ba70c4 100644 --- a/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/client/RestfulClientFactory.java +++ b/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/client/RestfulClientFactory.java @@ -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); } diff --git a/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/method/BaseHttpClientInvocationWithContents.java b/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/method/BaseHttpClientInvocationWithContents.java index f0bd76c56e0..19a455cd382 100644 --- a/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/method/BaseHttpClientInvocationWithContents.java +++ b/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/method/BaseHttpClientInvocationWithContents.java @@ -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) { diff --git a/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/method/HttpDeleteClientInvocation.java b/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/method/HttpDeleteClientInvocation.java index 374c0d66501..1e014f44da8 100644 --- a/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/method/HttpDeleteClientInvocation.java +++ b/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/method/HttpDeleteClientInvocation.java @@ -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; } diff --git a/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/method/HttpGetClientInvocation.java b/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/method/HttpGetClientInvocation.java index 9d2c55ee8f8..de669820a9d 100644 --- a/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/method/HttpGetClientInvocation.java +++ b/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/method/HttpGetClientInvocation.java @@ -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; } diff --git a/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/method/HttpSimpleGetClientInvocation.java b/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/method/HttpSimpleGetClientInvocation.java index 7a68a8497c4..1176fc21ffd 100644 --- a/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/method/HttpSimpleGetClientInvocation.java +++ b/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/method/HttpSimpleGetClientInvocation.java @@ -40,7 +40,7 @@ public class HttpSimpleGetClientInvocation extends BaseHttpClientInvocation { @Override public HttpRequestBase asHttpRequest(String theUrlBase, Map> theExtraParams, EncodingEnum theEncoding, Boolean thePrettyPrint) { HttpGet retVal = new HttpGet(myUrl); - super.addHeadersToRequest(retVal); + super.addHeadersToRequest(retVal, theEncoding); return retVal; } diff --git a/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/server/Constants.java b/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/server/Constants.java index ee382a07c14..ed4def1c8f8 100644 --- a/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/server/Constants.java +++ b/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/server/Constants.java @@ -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 "; diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/BaseHapiFhirResourceDao.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/BaseHapiFhirResourceDao.java index 2b1bba91465..2fa02accabe 100644 --- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/BaseHapiFhirResourceDao.java +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/BaseHapiFhirResourceDao.java @@ -625,7 +625,7 @@ public abstract class BaseHapiFhirResourceDao 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); diff --git a/hapi-fhir-structures-dstu2/src/test/java/ca/uhn/fhir/rest/client/GenericClientDstu2Test.java b/hapi-fhir-structures-dstu2/src/test/java/ca/uhn/fhir/rest/client/GenericClientDstu2Test.java index 13f609f9f31..0d01ad9f8f4 100644 --- a/hapi-fhir-structures-dstu2/src/test/java/ca/uhn/fhir/rest/client/GenericClientDstu2Test.java +++ b/hapi-fhir-structures-dstu2/src/test/java/ca/uhn/fhir/rest/client/GenericClientDstu2Test.java @@ -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 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 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() { + @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 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() { + @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 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() { + @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 { diff --git a/hapi-fhir-structures-hl7org-dstu2/src/main/java/org/hl7/fhir/instance/validation/QuestionnaireResponseValidator.java b/hapi-fhir-structures-hl7org-dstu2/src/main/java/org/hl7/fhir/instance/validation/QuestionnaireResponseValidator.java index b9e0640ae6e..f8809567901 100644 --- a/hapi-fhir-structures-hl7org-dstu2/src/main/java/org/hl7/fhir/instance/validation/QuestionnaireResponseValidator.java +++ b/hapi-fhir-structures-hl7org-dstu2/src/main/java/org/hl7/fhir/instance/validation/QuestionnaireResponseValidator.java @@ -72,8 +72,15 @@ public class QuestionnaireResponseValidator extends BaseValidator { return Collections.unmodifiableSet(retVal); } - private List findAnswersByLinkId( - List theQuestion, String theLinkId) { + private Set> allowedTypes(Class theClass0, Class theClass1) { + HashSet> retVal = new HashSet>(); + retVal.add(theClass0); + retVal.add(theClass1); + return Collections.unmodifiableSet(retVal); + } + + private List findAnswersByLinkId(List theQuestion, + String theLinkId) { Validate.notBlank(theLinkId, "theLinkId must not be blank"); ArrayList retVal = new ArrayList(); @@ -85,8 +92,7 @@ public class QuestionnaireResponseValidator extends BaseValidator { return retVal; } - private List findGroupByLinkId( - List theGroups, String theLinkId) { + private List findGroupByLinkId(List theGroups, String theLinkId) { ArrayList retVal = new ArrayList(); 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 errors, IssueType type, List pathParts, boolean thePass, String msg) { -// return test(errors, type, pathParts, thePass, msg, IssueSeverity.FATAL); -// } + // protected boolean fail(List errors, IssueType type, List pathParts, boolean thePass, String msg) { + // return test(errors, type, pathParts, thePass, msg, IssueSeverity.FATAL); + // } public void validate(List theErrors, QuestionnaireResponse theAnswers) { LinkedList pathStack = new LinkedList(); 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 theErrors, GroupComponent theQuestGroup, - org.hl7.fhir.instance.model.QuestionnaireResponse.GroupComponent theAnsGroup, LinkedList thePathStack, - QuestionnaireResponse theAnswers, boolean theValidateRequired) { + private void validateGroup(List theErrors, GroupComponent theQuestGroup, org.hl7.fhir.instance.model.QuestionnaireResponse.GroupComponent theAnsGroup, + LinkedList 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 theErrors, QuestionComponent theQuestion, - org.hl7.fhir.instance.model.QuestionnaireResponse.GroupComponent theAnsGroup, LinkedList thePathStack, - QuestionnaireResponse theAnswers, boolean theValidateRequired) { + private void validateQuestion(List theErrors, QuestionComponent theQuestion, org.hl7.fhir.instance.model.QuestionnaireResponse.GroupComponent theAnsGroup, + LinkedList 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 extensions = question - .getExtensionsByUrl("http://hl7.org/fhir/StructureDefinition/questionnaire-deReference"); + List 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 answers = findAnswersByLinkId( - theAnsGroup.getQuestion(), linkId); + List 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 theErrors, QuestionComponent theQuestion, - org.hl7.fhir.instance.model.QuestionnaireResponse.QuestionComponent theAnswerQuestion, + private void validateQuestionGroups(List theErrors, QuestionComponent theQuestion, org.hl7.fhir.instance.model.QuestionnaireResponse.QuestionComponent theAnswerQuestion, LinkedList 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 theErrors, GroupComponent theQuestGroup, - org.hl7.fhir.instance.model.QuestionnaireResponse.GroupComponent theAnsGroup, LinkedList thePathSpec, - QuestionnaireResponse theAnswers, boolean theValidateRequired) { - validateGroups(theErrors, theQuestGroup.getGroup(), theAnsGroup.getGroup(), thePathSpec, theAnswers, - theValidateRequired); + private void validateGroupGroups(List theErrors, GroupComponent theQuestGroup, org.hl7.fhir.instance.model.QuestionnaireResponse.GroupComponent theAnsGroup, + LinkedList thePathSpec, QuestionnaireResponse theAnswers, boolean theValidateRequired) { + validateGroups(theErrors, theQuestGroup.getGroup(), theAnsGroup.getGroup(), thePathSpec, theAnswers, theValidateRequired); } - private void validateGroups(List theErrors, List theQuestionGroups, - List theAnswerGroups, + private void validateGroups(List theErrors, List theQuestionGroups, List theAnswerGroups, LinkedList thePathStack, QuestionnaireResponse theAnswers, boolean theValidateRequired) { Set linkIds = new HashSet(); 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 answerGroups = findGroupByLinkId( - theAnswerGroups, linkId); + List 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 theErrors, QuestionComponent theQuestion, - LinkedList thePathStack, AnswerFormat type, - org.hl7.fhir.instance.model.QuestionnaireResponse.QuestionComponent answerQuestion, - QuestionnaireResponse theAnswers, boolean theValidateRequired) { + private void validateQuestionAnswers(List theErrors, QuestionComponent theQuestion, LinkedList thePathStack, AnswerFormat type, + org.hl7.fhir.instance.model.QuestionnaireResponse.QuestionComponent answerQuestion, QuestionnaireResponse theAnswers, boolean theValidateRequired) { String linkId = theQuestion.getLinkId(); Set> 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); diff --git a/hapi-fhir-structures-hl7org-dstu2/src/test/java/ca/uhn/fhir/validation/QuestionnaireResponseValidatorTest.java b/hapi-fhir-structures-hl7org-dstu2/src/test/java/ca/uhn/fhir/validation/QuestionnaireResponseValidatorTest.java index 3e7dcd31f79..d403e6d171c 100644 --- a/hapi-fhir-structures-hl7org-dstu2/src/test/java/ca/uhn/fhir/validation/QuestionnaireResponseValidatorTest.java +++ b/hapi-fhir-structures-hl7org-dstu2/src/test/java/ca/uhn/fhir/validation/QuestionnaireResponseValidatorTest.java @@ -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 errors = new ArrayList(); - 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 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(); - 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(); - 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(); - 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(); - 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 errors = new ArrayList(); - 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 errors = new ArrayList(); - 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 errors = new ArrayList(); - 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 errors = new ArrayList(); + 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 errors = new ArrayList(); - 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 errors = new ArrayList(); - 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 errors = new ArrayList(); - 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 errors = new ArrayList(); - myVal.validate(errors, qa); - assertEquals(errors.toString(), 0, errors.size()); - - /* - * Now change a coded value - */ - //@formatter:off - input = input.replaceAll("\n" + - " \n" + - " \n" + - " \n" + - " \n" + - " \n" + - " ", "\n" + - " \n" + - " \n" + - " \n" + - " \n" + - " \n" + - " "); - assertThat(input, containsString("GGG")); - //@formatter:on - - qa = ourCtx.newXmlParser().parseResource(QuestionnaireResponse.class, input); - errors = new ArrayList(); - myVal.validate(errors, qa); - assertEquals(errors.toString(), 10, errors.size()); - } - + QuestionnaireResponse qa; + List 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(); + 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(); + 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(); + 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(); + 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 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(); + 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(); + 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(); + 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(); + 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(); + 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(); + 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(); + 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(); + 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(); + 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(); + 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 errors = new ArrayList(); + 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 errors = new ArrayList(); + 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 errors = new ArrayList(); + 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 errors = new ArrayList(); + 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 errors = new ArrayList(); + 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 errors = new ArrayList(); + 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 errors = new ArrayList(); + myVal.validate(errors, qa); + assertEquals(errors.toString(), 0, errors.size()); + + /* + * Now change a coded value + */ + // @formatter:off + input = input.replaceAll( + "\n" + " \n" + " \n" + " \n" + + " \n" + " \n" + " ", + "\n" + " \n" + " \n" + " \n" + + " \n" + " \n" + " "); + assertThat(input, containsString("GGG")); + // @formatter:on + + qa = ourCtx.newXmlParser().parseResource(QuestionnaireResponse.class, input); + errors = new ArrayList(); + myVal.validate(errors, qa); + assertEquals(errors.toString(), 10, errors.size()); + } } diff --git a/src/changes/changes.xml b/src/changes/changes.xml index 03e8ed1806d..b3d7d8641c3 100644 --- a/src/changes/changes.xml +++ b/src/changes/changes.xml @@ -49,6 +49,17 @@ Narrative generator did not include OperationOutcome.issue.diagnostics in the generated narrative. + + 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! + + + QuestionnaireResponse validator now allows responses to questions of + type OPENCHOICE to be of type 'string' +