Add serialization support and fix build regression

This commit is contained in:
James Agnew 2015-07-21 09:45:53 -04:00
parent e8730b6e47
commit 56a4c1b04b
18 changed files with 305 additions and 77 deletions

View File

@ -20,17 +20,31 @@ package ca.uhn.fhir.model.api;
* #L%
*/
import java.io.Externalizable;
import java.io.IOException;
import java.io.ObjectInput;
import java.io.ObjectOutput;
import org.apache.commons.lang3.builder.EqualsBuilder;
import org.apache.commons.lang3.builder.HashCodeBuilder;
import org.hl7.fhir.instance.model.api.IPrimitiveType;
import ca.uhn.fhir.parser.DataFormatException;
public abstract class BasePrimitive<T> extends BaseIdentifiableElement implements IPrimitiveDatatype<T> {
public abstract class BasePrimitive<T> extends BaseIdentifiableElement implements IPrimitiveDatatype<T>, Externalizable {
private T myCoercedValue;
private String myStringValue;
/**
* Subclasses must override to convert a "coerced" value into an encoded one.
*
* @param theValue
* Will not be null
* @return May return null if the value does not correspond to anything
*/
protected abstract String encode(T theValue);
@Override
public boolean equals(Object theObj) {
if (theObj == null) {
@ -67,6 +81,21 @@ public abstract class BasePrimitive<T> extends BaseIdentifiableElement implement
return super.isBaseEmpty() && getValue() == null;
}
/**
* Subclasses must override to convert an encoded representation of this datatype into a "coerced" one
*
* @param theValue
* Will not be null
* @return May return null if the value does not correspond to anything
*/
protected abstract T parse(String theValue);
@Override
public void readExternal(ObjectInput theIn) throws IOException, ClassNotFoundException {
String object = (String) theIn.readObject();
setValueAsString(object);
}
@Override
public IPrimitiveType<T> setValue(T theValue) throws DataFormatException {
myCoercedValue = theValue;
@ -74,15 +103,6 @@ public abstract class BasePrimitive<T> extends BaseIdentifiableElement implement
return this;
}
protected void updateStringValue() {
if (myCoercedValue == null) {
myStringValue = null;
} else {
// NB this might be null
myStringValue = encode(myCoercedValue);
}
}
@Override
public void setValueAsString(String theValue) throws DataFormatException {
if (theValue == null) {
@ -94,26 +114,22 @@ public abstract class BasePrimitive<T> extends BaseIdentifiableElement implement
myStringValue = theValue;
}
/**
* Subclasses must override to convert an encoded representation of this datatype into a "coerced" one
*
* @param theValue
* Will not be null
* @return May return null if the value does not correspond to anything
*/
protected abstract T parse(String theValue);
/**
* Subclasses must override to convert a "coerced" value into an encoded one.
*
* @param theValue
* Will not be null
* @return May return null if the value does not correspond to anything
*/
protected abstract String encode(T theValue);
@Override
public String toString() {
return getClass().getSimpleName() + "[" + getValueAsString() + "]";
}
protected void updateStringValue() {
if (myCoercedValue == null) {
myStringValue = null;
} else {
// NB this might be null
myStringValue = encode(myCoercedValue);
}
}
@Override
public void writeExternal(ObjectOutput theOut) throws IOException {
theOut.writeObject(getValueAsString());
}
}

View File

@ -22,6 +22,7 @@ package ca.uhn.fhir.model.api;
import static org.apache.commons.lang3.StringUtils.*;
import java.io.Serializable;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Date;
@ -61,7 +62,9 @@ import ca.uhn.fhir.rest.server.exceptions.InternalErrorException;
* define their own keys for storage in resource metadata if needed.
* </p>
*/
public abstract class ResourceMetadataKeyEnum<T> {
public abstract class ResourceMetadataKeyEnum<T> implements Serializable {
private static final long serialVersionUID = 1L;
/**
* If present and populated with a date/time (as an instance of {@link InstantDt}), this value is an indication that the resource is in the deleted state. This key is only used in a limited number

View File

@ -20,6 +20,8 @@ package ca.uhn.fhir.model.primitive;
* #L%
*/
import org.apache.commons.lang3.Validate;
import ca.uhn.fhir.model.api.IValueSetEnumBinder;
import ca.uhn.fhir.model.api.annotation.DatatypeDef;
@ -28,16 +30,27 @@ public class BoundCodeDt<T extends Enum<?>> extends CodeDt {
private IValueSetEnumBinder<T> myBinder;
/**
* @deprecated This constructor is provided only for serialization support. Do not call it directly!
*/
@Deprecated
public BoundCodeDt() {
// nothing
}
public BoundCodeDt(IValueSetEnumBinder<T> theBinder) {
Validate.notNull(theBinder, "theBinder must not be null");
myBinder = theBinder;
}
public BoundCodeDt(IValueSetEnumBinder<T> theBinder, T theValue) {
Validate.notNull(theBinder, "theBinder must not be null");
myBinder = theBinder;
setValueAsEnum(theValue);
}
public void setValueAsEnum(T theValue) {
Validate.notNull(myBinder, "This object does not have a binder. Constructor BoundCodeDt() should not be called!");
if (theValue==null) {
setValue(null);
} else {
@ -46,6 +59,7 @@ public class BoundCodeDt<T extends Enum<?>> extends CodeDt {
}
public T getValueAsEnum() {
Validate.notNull(myBinder, "This object does not have a binder. Constructor BoundCodeDt() should not be called!");
T retVal = myBinder.fromCodeString(getValue());
if (retVal == null) {
// TODO: throw special exception type?

View File

@ -41,6 +41,8 @@ import ca.uhn.fhir.util.XmlUtil;
@DatatypeDef(name = "xhtml")
public class XhtmlDt extends BasePrimitive<List<XMLEvent>> {
private static final long serialVersionUID = 1L;
/**
* Constructor
*/

View File

@ -1554,7 +1554,7 @@ class ParserState<T> {
return;
}
if (child.getMax() < 2 && !myParsedNonRepeatableNames.add(theChildName)) {
if ((child.getMax() == 0 || child.getMax() == 1) && !myParsedNonRepeatableNames.add(theChildName)) {
myErrorHandler.unexpectedRepeatingElement(null, theChildName);
push(new SwallowChildrenWholeState(getPreResourceState()));
return;

View File

@ -24,7 +24,9 @@ import net.sourceforge.cobertura.CoverageIgnore;
import org.hl7.fhir.instance.model.api.IBaseOperationOutcome;
import ca.uhn.fhir.context.FhirContext;
import ca.uhn.fhir.rest.server.Constants;
import ca.uhn.fhir.util.OperationOutcomeUtil;
/**
* Represents an <b>HTTP 422 Unprocessable Entity</b> response, which means that a resource was rejected by the server because it "violated applicable FHIR profiles or server business rules".
@ -56,11 +58,21 @@ public class UnprocessableEntityException extends BaseServerResponseException {
/**
* Constructor which accepts an {@link IBaseOperationOutcome} resource which will be supplied in the response
*
* @deprecated Use constructor with FhirContext argument
*/
@Deprecated
public UnprocessableEntityException(IBaseOperationOutcome theOperationOutcome) {
super(STATUS_CODE, DEFAULT_MESSAGE, theOperationOutcome);
}
/**
* Constructor which accepts an {@link IBaseOperationOutcome} resource which will be supplied in the response
*/
public UnprocessableEntityException(FhirContext theCtx, IBaseOperationOutcome theOperationOutcome) {
super(STATUS_CODE, OperationOutcomeUtil.getFirstIssueDetails(theCtx, theOperationOutcome), theOperationOutcome);
}
/**
* Constructor which accepts a String describing the issue. This string will be translated into an {@link IBaseOperationOutcome} resource which will be supplied in the response.
*/

View File

@ -1,5 +1,25 @@
package ca.uhn.fhir.validation;
/*
* #%L
* HAPI FHIR - Core Library
* %%
* Copyright (C) 2014 - 2015 University Health Network
* %%
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
* #L%
*/
import java.util.ArrayList;
import java.util.List;

View File

@ -1,5 +1,25 @@
package ca.uhn.fhir.validation;
/*
* #%L
* HAPI FHIR - Core Library
* %%
* Copyright (C) 2014 - 2015 University Health Network
* %%
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
* #L%
*/
import org.hl7.fhir.instance.model.api.IBaseResource;
import org.hl7.fhir.instance.model.api.IIdType;

View File

@ -1,5 +1,7 @@
package org.hl7.fhir.instance.model.api;
import java.io.Serializable;
/*
* #%L
* HAPI FHIR - Core Library
@ -25,7 +27,7 @@ package org.hl7.fhir.instance.model.api;
* structure of some kind. It is provided mostly to simplify convergence
* between the HL7.org structures and the HAPI ones.
*/
public interface IBase {
public interface IBase extends Serializable {
boolean isEmpty();

View File

@ -1,5 +1,25 @@
package ca.uhn.fhir.jpa.dao;
/*
* #%L
* HAPI FHIR JPA Server
* %%
* Copyright (C) 2014 - 2015 University Health Network
* %%
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
* #L%
*/
import org.hl7.fhir.instance.model.api.IBaseOperationOutcome;
import org.hl7.fhir.instance.model.api.IBaseResource;
import org.hl7.fhir.instance.model.api.IIdType;
@ -41,7 +61,7 @@ public class FhirResourceDaoQuestionnaireAnswersDstu2 extends FhirResourceDaoDst
ValidationResult result = val.validateWithResult(myRefImplCtx.newJsonParser().parseResource(getContext().newJsonParser().encodeResourceToString(qa)));
if (!result.isSuccessful()) {
IBaseOperationOutcome oo = getContext().newJsonParser().parseResource(OperationOutcome.class, myRefImplCtx.newJsonParser().encodeResourceToString(result.toOperationOutcome()));
throw new UnprocessableEntityException(oo);
throw new UnprocessableEntityException(getContext(), oo);
}
}

View File

@ -26,6 +26,8 @@ import java.util.Collection;
import java.util.HashSet;
import java.util.Set;
import org.apache.commons.lang3.Validate;
import ca.uhn.fhir.model.api.IBoundCodeableConcept;
import ca.uhn.fhir.model.api.IValueSetEnumBinder;
import ca.uhn.fhir.model.api.annotation.DatatypeDef;
@ -37,10 +39,19 @@ public class BoundCodeableConceptDt<T extends Enum<?>> extends CodeableConceptDt
private IValueSetEnumBinder<T> myBinder;
/**
* @deprecated This constructor is provided only for serialization support. Do not call it directly!
*/
@Deprecated
public BoundCodeableConceptDt() {
// nothing
}
/**
* Constructor
*/
public BoundCodeableConceptDt(IValueSetEnumBinder<T> theBinder) {
Validate.notNull(theBinder, "theBinder must not be null");
myBinder = theBinder;
}
@ -48,6 +59,7 @@ public class BoundCodeableConceptDt<T extends Enum<?>> extends CodeableConceptDt
* Constructor
*/
public BoundCodeableConceptDt(IValueSetEnumBinder<T> theBinder, T theValue) {
Validate.notNull(theBinder, "theBinder must not be null");
myBinder = theBinder;
setValueAsEnum(theValue);
}
@ -56,6 +68,7 @@ public class BoundCodeableConceptDt<T extends Enum<?>> extends CodeableConceptDt
* Constructor
*/
public BoundCodeableConceptDt(IValueSetEnumBinder<T> theBinder, Collection<T> theValues) {
Validate.notNull(theBinder, "theBinder must not be null");
myBinder = theBinder;
setValueAsEnum(theValues);
}
@ -70,6 +83,7 @@ public class BoundCodeableConceptDt<T extends Enum<?>> extends CodeableConceptDt
* The value to add, or <code>null</code>
*/
public void setValueAsEnum(Collection<T> theValues) {
Validate.notNull(myBinder, "This object does not have a binder. Constructor BoundCodeableConceptDt() should not be called!");
getCoding().clear();
if (theValues != null) {
for (T next : theValues) {
@ -88,6 +102,7 @@ public class BoundCodeableConceptDt<T extends Enum<?>> extends CodeableConceptDt
* The value to add, or <code>null</code>
*/
public void setValueAsEnum(T theValue) {
Validate.notNull(myBinder, "This object does not have a binder. Constructor BoundCodeableConceptDt() should not be called!");
getCoding().clear();
if (theValue == null) {
return;
@ -107,6 +122,7 @@ public class BoundCodeableConceptDt<T extends Enum<?>> extends CodeableConceptDt
* no codes that match the enum.
*/
public Set<T> getValueAsEnum() {
Validate.notNull(myBinder, "This object does not have a binder. Constructor BoundCodeableConceptDt() should not be called!");
Set<T> retVal = new HashSet<T>();
for (CodingDt next : getCoding()) {
if (next == null) {

View File

@ -0,0 +1,30 @@
package ca.uhn.fhir.model.dstu2;
import static org.junit.Assert.assertEquals;
import org.apache.commons.io.IOUtils;
import org.apache.commons.lang3.SerializationUtils;
import org.junit.Test;
import ca.uhn.fhir.context.FhirContext;
import ca.uhn.fhir.model.dstu2.resource.Bundle;
import ca.uhn.fhir.model.dstu2.resource.DiagnosticReport;
import ca.uhn.fhir.parser.IParser;
public class ModelSerializationTest {
private static final FhirContext ourCtx = FhirContext.forDstu2();
@Test
public void testSerialization() throws Exception {
String input = IOUtils.toString(ModelSerializationTest.class.getResourceAsStream("/diagnosticreport-examples-lab-text(72ac8493-52ac-41bd-8d5d-7258c289b5ea).xml"));
Bundle parsed = ourCtx.newXmlParser().parseResource(Bundle.class, input);
Bundle roundTripped = SerializationUtils.roundtrip(parsed);
IParser p = ourCtx.newXmlParser().setPrettyPrint(true);
assertEquals(p.encodeResourceToString(parsed), p.encodeResourceToString(roundTripped));
}
}

View File

@ -26,6 +26,7 @@ import org.hl7.fhir.instance.model.Questionnaire.GroupComponent;
import org.hl7.fhir.instance.model.Questionnaire.QuestionComponent;
import org.hl7.fhir.instance.model.QuestionnaireAnswers;
import org.hl7.fhir.instance.model.QuestionnaireAnswers.QuestionAnswerComponent;
import org.hl7.fhir.instance.model.QuestionnaireAnswers.QuestionnaireAnswersStatus;
import org.hl7.fhir.instance.model.Reference;
import org.hl7.fhir.instance.model.Resource;
import org.hl7.fhir.instance.model.StringType;
@ -44,11 +45,11 @@ import org.hl7.fhir.instance.utils.WorkerContext;
*/
public class QuestionnaireAnswersValidator extends BaseValidator {
/*
/* *****************************************************************
* Note to anyone working on this class -
*
* This class has unit tests which run within the HAPI project build. Please sync any changes here to HAPI and ensure that unit tests are run.
*/
* *****************************************************************/
private WorkerContext myWorkerCtx;
@ -102,9 +103,17 @@ public class QuestionnaireAnswersValidator extends BaseValidator {
return;
}
QuestionnaireAnswersStatus status = theAnswers.getStatus();
boolean validateRequired = false;
if (status == QuestionnaireAnswersStatus.COMPLETED || status == QuestionnaireAnswersStatus.AMENDED) {
validateRequired = true;
} else {
hint(theErrors, null, null, false, "Questionnaire has status {0} so ");
}
pathStack.removeLast();
pathStack.add("group(0)");
validateGroup(theErrors, questionnaire.getGroup(), theAnswers.getGroup(), pathStack, theAnswers);
validateGroup(theErrors, questionnaire.getGroup(), theAnswers.getGroup(), pathStack, theAnswers, validateRequired);
}
private Questionnaire getQuestionnaire(QuestionnaireAnswers theAnswers, Reference theQuestionnaireRef) {
@ -142,7 +151,7 @@ public class QuestionnaireAnswersValidator extends BaseValidator {
}
private void validateGroup(List<ValidationMessage> theErrors, GroupComponent theQuestGroup, org.hl7.fhir.instance.model.QuestionnaireAnswers.GroupComponent theAnsGroup,
LinkedList<String> thePathStack, QuestionnaireAnswers theAnswers) {
LinkedList<String> thePathStack, QuestionnaireAnswers theAnswers, boolean theValidateRequired) {
for (org.hl7.fhir.instance.model.QuestionnaireAnswers.QuestionComponent next : theAnsGroup.getQuestion()) {
rule(theErrors, IssueType.INVALID, thePathStack, isNotBlank(next.getLinkId()), "Question found with no linkId");
@ -155,7 +164,7 @@ public class QuestionnaireAnswersValidator extends BaseValidator {
for (int i = 0; i < theQuestGroup.getQuestion().size(); i++) {
QuestionComponent nextQuestion = theQuestGroup.getQuestion().get(i);
validateQuestion(theErrors, nextQuestion, theAnsGroup, thePathStack, theAnswers);
validateQuestion(theErrors, nextQuestion, theAnsGroup, thePathStack, theAnswers, theValidateRequired);
}
// Check that there are no extra answers
@ -167,12 +176,12 @@ public class QuestionnaireAnswersValidator extends BaseValidator {
thePathStack.remove();
}
validateGroupGroups(theErrors, theQuestGroup, theAnsGroup, thePathStack, theAnswers);
validateGroupGroups(theErrors, theQuestGroup, theAnsGroup, thePathStack, theAnswers, theValidateRequired);
}
private void validateQuestion(List<ValidationMessage> theErrors, QuestionComponent theQuestion, org.hl7.fhir.instance.model.QuestionnaireAnswers.GroupComponent theAnsGroup,
LinkedList<String> thePathStack, QuestionnaireAnswers theAnswers) {
LinkedList<String> thePathStack, QuestionnaireAnswers theAnswers, boolean theValidateRequired) {
String linkId = theQuestion.getLinkId();
if (!fail(theErrors, IssueType.INVALID, thePathStack, isNotBlank(linkId), "Questionnaire is invalid, question found with no link ID")) {
return;
@ -192,32 +201,32 @@ public class QuestionnaireAnswersValidator extends BaseValidator {
rule(theErrors, IssueType.BUSINESSRULE, thePathStack, !theQuestion.getRequired(), "Multiple answers repetitions found with linkId[{0}]", linkId);
}
if (answers.size() == 0) {
rule(theErrors, IssueType.BUSINESSRULE, thePathStack, !theQuestion.getRequired(), "Missing answer to required question with linkId[{0}]", linkId);
rule(theErrors, IssueType.BUSINESSRULE, thePathStack, !(theQuestion.getRequired() && theValidateRequired), "Missing answer to required question with linkId[{0}]", linkId);
return;
}
org.hl7.fhir.instance.model.QuestionnaireAnswers.QuestionComponent answerQuestion = answers.get(0);
try {
thePathStack.add("question(" + answers.indexOf(answerQuestion) + ")");
validateQuestionAnswers(theErrors, theQuestion, thePathStack, type, answerQuestion, theAnswers);
validateQuestionGroups(theErrors, theQuestion, answerQuestion, thePathStack, theAnswers);
validateQuestionAnswers(theErrors, theQuestion, thePathStack, type, answerQuestion, theAnswers, theValidateRequired);
validateQuestionGroups(theErrors, theQuestion, answerQuestion, thePathStack, theAnswers, theValidateRequired);
} finally {
thePathStack.removeLast();
}
}
private void validateQuestionGroups(List<ValidationMessage> theErrors, QuestionComponent theQuestion, org.hl7.fhir.instance.model.QuestionnaireAnswers.QuestionComponent theAnswerQuestion,
LinkedList<String> thePathSpec, QuestionnaireAnswers theAnswers) {
validateGroups(theErrors, theQuestion.getGroup(), theAnswerQuestion.getGroup(), thePathSpec, theAnswers);
LinkedList<String> thePathSpec, QuestionnaireAnswers theAnswers, boolean theValidateRequired) {
validateGroups(theErrors, theQuestion.getGroup(), theAnswerQuestion.getGroup(), thePathSpec, theAnswers, theValidateRequired);
}
private void validateGroupGroups(List<ValidationMessage> theErrors, GroupComponent theQuestGroup, org.hl7.fhir.instance.model.QuestionnaireAnswers.GroupComponent theAnsGroup,
LinkedList<String> thePathSpec, QuestionnaireAnswers theAnswers) {
validateGroups(theErrors, theQuestGroup.getGroup(), theAnsGroup.getGroup(), thePathSpec, theAnswers);
LinkedList<String> thePathSpec, QuestionnaireAnswers 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.QuestionnaireAnswers.GroupComponent> theAnswerGroups,
LinkedList<String> thePathStack, QuestionnaireAnswers theAnswers) {
LinkedList<String> thePathStack, QuestionnaireAnswers theAnswers, boolean theValidateRequired) {
Set<String> allowedGroups = new HashSet<String>();
for (GroupComponent nextQuestionGroup : theQuestionGroups) {
String linkId = nextQuestionGroup.getLinkId();
@ -225,7 +234,7 @@ public class QuestionnaireAnswersValidator extends BaseValidator {
List<org.hl7.fhir.instance.model.QuestionnaireAnswers.GroupComponent> answerGroups = findGroupByLinkId(theAnswerGroups, linkId);
if (answerGroups.isEmpty()) {
if (nextQuestionGroup.getRequired()) {
if (nextQuestionGroup.getRequired() && theValidateRequired) {
rule(theErrors, IssueType.BUSINESSRULE, thePathStack, false, "Missing required group with linkId[{0}]", linkId);
}
continue;
@ -241,7 +250,7 @@ public class QuestionnaireAnswersValidator extends BaseValidator {
for (org.hl7.fhir.instance.model.QuestionnaireAnswers.GroupComponent nextAnswerGroup : answerGroups) {
int index = theAnswerGroups.indexOf(answerGroups.get(1));
thePathStack.add("group(" + index + ")");
validateGroup(theErrors, nextQuestionGroup, nextAnswerGroup, thePathStack, theAnswers);
validateGroup(theErrors, nextQuestionGroup, nextAnswerGroup, thePathStack, theAnswers, theValidateRequired);
thePathStack.removeLast();
}
}
@ -259,13 +268,13 @@ public class QuestionnaireAnswersValidator extends BaseValidator {
}
private void validateQuestionAnswers(List<ValidationMessage> theErrors, QuestionComponent theQuestion, LinkedList<String> thePathStack, AnswerFormat type,
org.hl7.fhir.instance.model.QuestionnaireAnswers.QuestionComponent answerQuestion, QuestionnaireAnswers theAnswers) {
org.hl7.fhir.instance.model.QuestionnaireAnswers.QuestionComponent answerQuestion, QuestionnaireAnswers 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);
} else {
} else if (theValidateRequired) {
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, !(theQuestion.getRequired() && answerQuestion.getAnswer().isEmpty()), "Missing answer to required question with linkId[{0}]", linkId);

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.*;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;
@ -12,12 +13,15 @@ import java.util.List;
import org.apache.commons.io.IOUtils;
import org.hl7.fhir.instance.model.Coding;
import org.hl7.fhir.instance.model.IdType;
import org.hl7.fhir.instance.model.IntegerType;
import org.hl7.fhir.instance.model.Questionnaire;
import org.hl7.fhir.instance.model.Questionnaire.GroupComponent;
import org.hl7.fhir.instance.model.QuestionnaireAnswers;
import org.hl7.fhir.instance.model.Reference;
import org.hl7.fhir.instance.model.StringType;
import org.hl7.fhir.instance.model.ValueSet;
import org.hl7.fhir.instance.model.Questionnaire.AnswerFormat;
import org.hl7.fhir.instance.model.QuestionnaireAnswers.QuestionnaireAnswersStatus;
import org.hl7.fhir.instance.utils.WorkerContext;
import org.hl7.fhir.instance.validation.QuestionnaireAnswersValidator;
import org.hl7.fhir.instance.validation.ValidationMessage;
@ -26,37 +30,38 @@ import org.junit.Test;
import org.mockito.Mockito;
import ca.uhn.fhir.context.FhirContext;
import ca.uhn.fhir.model.primitive.IntegerDt;
import ca.uhn.fhir.rest.server.exceptions.ResourceNotFoundException;
public class QuestionnaireAnswersValidatorIntegrationTest {
private static final FhirContext ourCtx = FhirContext.forDstu2Hl7Org();
private static final org.slf4j.Logger ourLog = org.slf4j.LoggerFactory.getLogger(QuestionnaireAnswersValidatorIntegrationTest.class);
private IResourceLoader myResourceLoaderMock;
private FhirValidator myVal;
@Before
public void before() {
myResourceLoaderMock = mock(IResourceLoader.class);
FhirQuestionnaireAnswersValidator qaVal = new FhirQuestionnaireAnswersValidator();
qaVal.setResourceLoader(myResourceLoaderMock);
myVal = ourCtx.newValidator();
myVal.setValidateAgainstStandardSchema(false);
myVal.setValidateAgainstStandardSchematron(false);
myVal.registerValidatorModule(qaVal);
}
@Test
public void testAnswerWithWrongType() {
Questionnaire q = new Questionnaire();
q.getGroup().addQuestion().setLinkId("link0").setRequired(true).setType(AnswerFormat.BOOLEAN);
when(myResourceLoaderMock.load(Mockito.eq(Questionnaire.class), Mockito.eq(new IdType("http://example.com/Questionnaire/q1")))).thenReturn(q);
QuestionnaireAnswers qa = new QuestionnaireAnswers();
qa.getQuestionnaire().setReference("http://example.com/Questionnaire/q1");
qa.getGroup().addQuestion().setLinkId("link0").addAnswer().setValue(new StringType("FOO"));
@ -66,10 +71,63 @@ public class QuestionnaireAnswersValidatorIntegrationTest {
assertThat(result.getMessages().toString(), containsString("Answer to question with linkId[link0] found of type [StringType] but this is invalid for question of type [boolean]"));
}
@Test
public void testRequiredOnlyTestedForFinishedQuestionnaires() {
Questionnaire q = new Questionnaire();
q.getGroup().addQuestion().setLinkId("link0").setRequired(true).setType(AnswerFormat.BOOLEAN);
GroupComponent qg = q.getGroup().addGroup().setLinkId("link1").setRequired(true);
qg.addQuestion().setLinkId("link2").setRequired(false).setType(AnswerFormat.BOOLEAN);
when(myResourceLoaderMock.load(Mockito.eq(Questionnaire.class), Mockito.eq(new IdType("http://example.com/Questionnaire/q1")))).thenReturn(q);
// Wrong type
{
QuestionnaireAnswers qa = new QuestionnaireAnswers();
qa.getQuestionnaire().setReference("http://example.com/Questionnaire/q1");
qa.getGroup().addQuestion().setLinkId("link0").addAnswer().setValue(new IntegerType(123));
ValidationResult result = myVal.validateWithResult(qa);
ourLog.info(result.getMessages().toString());
assertThat(result.getMessages().toString(), containsString("Answer to question with linkId[link0] found of type [IntegerType] but this is invalid for question of type [boolean]"));
}
// Not populated, no status
{
QuestionnaireAnswers qa = new QuestionnaireAnswers();
qa.getQuestionnaire().setReference("http://example.com/Questionnaire/q1");
ValidationResult result = myVal.validateWithResult(qa);
ourLog.info(result.getMessages().toString());
assertThat(result.getMessages(), empty());
}
// Not populated, partial status
{
QuestionnaireAnswers qa = new QuestionnaireAnswers();
qa.getQuestionnaire().setReference("http://example.com/Questionnaire/q1");
qa.setStatus(QuestionnaireAnswersStatus.INPROGRESS);
ValidationResult result = myVal.validateWithResult(qa);
ourLog.info(result.getMessages().toString());
assertThat(result.getMessages(), empty());
}
// Not populated, finished status
{
QuestionnaireAnswers qa = new QuestionnaireAnswers();
qa.getQuestionnaire().setReference("http://example.com/Questionnaire/q1");
qa.setStatus(QuestionnaireAnswersStatus.COMPLETED);
ValidationResult result = myVal.validateWithResult(qa);
ourLog.info(result.getMessages().toString());
assertThat(result.getMessages().toString(), containsString("Missing answer to required question with linkId[link0]"));
}
}
@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/ValueSet/123"));
when(myResourceLoaderMock.load(Mockito.eq(Questionnaire.class), Mockito.eq(new IdType("http://example.com/Questionnaire/q1")))).thenReturn(q);
@ -77,45 +135,45 @@ public class QuestionnaireAnswersValidatorIntegrationTest {
ValueSet options = new ValueSet();
options.getDefine().setSystem("urn:system").addConcept().setCode("code0");
when(myResourceLoaderMock.load(Mockito.eq(ValueSet.class), Mockito.eq(new IdType("http://somevalueset/ValueSet/123")))).thenReturn(options);
QuestionnaireAnswers qa;
// Good code
qa = new QuestionnaireAnswers();
qa.getQuestionnaire().setReference(questionnaireRef);
qa.getGroup().addQuestion().setLinkId("link0").addAnswer().setValue(new Coding().setSystem("urn:system").setCode("code0"));
ValidationResult result = myVal.validateWithResult(qa);
assertEquals(result.getMessages().toString(), 0, result.getMessages().size());
// Bad code
qa = new QuestionnaireAnswers();
qa.getQuestionnaire().setReference(questionnaireRef);
qa.getGroup().addQuestion().setLinkId("link0").addAnswer().setValue(new Coding().setSystem("urn:system").setCode("code1"));
result = myVal.validateWithResult(qa);
ourLog.info(result.getMessages().toString());
assertThat(result.getMessages().toString(), containsString("myLocationString=QuestionnaireAnswers.group(0).question(0).answer(0)"));
assertThat(result.getMessages().toString(), containsString("myMessage=Question with linkId[link0] has answer with system[urn:system] and code[code1] but this is not a valid answer for ValueSet[http://somevalueset/ValueSet/123]"));
assertThat(result.getMessages().toString(),
containsString("myMessage=Question with linkId[link0] has answer with system[urn:system] and code[code1] but this is not a valid answer for ValueSet[http://somevalueset/ValueSet/123]"));
result.toOperationOutcome();
}
@Test
public void testUnknownValueSet() {
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/ValueSet/123"));
when(myResourceLoaderMock.load(Mockito.eq(Questionnaire.class), Mockito.eq(new IdType("http://example.com/Questionnaire/q1")))).thenReturn(q);
when(myResourceLoaderMock.load(Mockito.eq(ValueSet.class), Mockito.eq(new IdType("http://somevalueset/ValueSet/123")))).thenThrow(new ResourceNotFoundException("Unknown"));
QuestionnaireAnswers qa;
// Bad code
qa = new QuestionnaireAnswers();
qa.getQuestionnaire().setReference(questionnaireRef);
qa.getGroup().addQuestion().setLinkId("link0").addAnswer().setValue(new Coding().setSystem("urn:system").setCode("code1"));

View File

@ -15,6 +15,7 @@ import org.hl7.fhir.instance.model.Reference;
import org.hl7.fhir.instance.model.StringType;
import org.hl7.fhir.instance.model.ValueSet;
import org.hl7.fhir.instance.model.Questionnaire.AnswerFormat;
import org.hl7.fhir.instance.model.QuestionnaireAnswers.QuestionnaireAnswersStatus;
import org.hl7.fhir.instance.utils.WorkerContext;
import org.hl7.fhir.instance.validation.QuestionnaireAnswersValidator;
import org.hl7.fhir.instance.validation.ValidationMessage;
@ -99,6 +100,7 @@ public class QuestionnaireAnswersValidatorTest {
q.getGroup().addQuestion().setLinkId("link1").setRequired(true).setType(AnswerFormat.STRING);
QuestionnaireAnswers qa = new QuestionnaireAnswers();
qa.setStatus(QuestionnaireAnswersStatus.COMPLETED);
qa.getQuestionnaire().setReference("http://example.com/Questionnaire/q1");
qa.getGroup().addQuestion().setLinkId("link1").addAnswer().setValue(new StringType("FOO"));

View File

@ -215,11 +215,11 @@ public class TinderStructuresMojo extends AbstractMojo {
// ProfileParser pp = new ProfileParser();
// pp.parseSingleProfile(new File("../hapi-tinder-test/src/test/resources/profile/patient.xml"), "http://foo");
ValueSetGenerator vsp = new ValueSetGenerator("dstu2");
ValueSetGenerator vsp = new ValueSetGenerator("dstu");
// vsp.setResourceValueSetFiles(theResourceValueSetFiles);Directory("src/main/resources/vs/");
vsp.parse();
DatatypeGeneratorUsingSpreadsheet dtp = new DatatypeGeneratorUsingSpreadsheet("dstu2", ".");
DatatypeGeneratorUsingSpreadsheet dtp = new DatatypeGeneratorUsingSpreadsheet("dstu", ".");
dtp.parse();
dtp.markResourcesForImports();
dtp.bindValueSets(vsp);
@ -227,8 +227,8 @@ public class TinderStructuresMojo extends AbstractMojo {
String dtOutputDir = "target/generated-sources/tinder/ca/uhn/fhir/model/dev/composite";
ResourceGeneratorUsingSpreadsheet rp = new ResourceGeneratorUsingSpreadsheet("dstu2", ".");
rp.setBaseResourceNames(Arrays.asList( "composition", "list"
ResourceGeneratorUsingSpreadsheet rp = new ResourceGeneratorUsingSpreadsheet("dstu", ".");
rp.setBaseResourceNames(Arrays.asList( "patient", "list"
// //, "contract"
// "valueset", "organization", "location"
// , "observation", "conformance"

View File

@ -308,7 +308,7 @@ public class ValueSetGenerator {
public static void main(String[] args) throws FileNotFoundException, IOException {
ValueSetGenerator p = new ValueSetGenerator("dev");
ValueSetGenerator p = new ValueSetGenerator("dstu1");
p.parse();
}

View File

@ -49,6 +49,10 @@
Operations in server generated conformance statement should only
appear once per name, since the name needs to be unique.
</action>
<action>
Resources and datatypes are now serializable. This is an
experimental feature which hasn't yet been extensively tested. Please test and give us your feedback!
</action>
</release>
<release version="1.1" date="2015-07-13">
<action type="add">