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% * #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.EqualsBuilder;
import org.apache.commons.lang3.builder.HashCodeBuilder; import org.apache.commons.lang3.builder.HashCodeBuilder;
import org.hl7.fhir.instance.model.api.IPrimitiveType; import org.hl7.fhir.instance.model.api.IPrimitiveType;
import ca.uhn.fhir.parser.DataFormatException; 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 T myCoercedValue;
private String myStringValue; 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 @Override
public boolean equals(Object theObj) { public boolean equals(Object theObj) {
if (theObj == null) { if (theObj == null) {
@ -67,6 +81,21 @@ public abstract class BasePrimitive<T> extends BaseIdentifiableElement implement
return super.isBaseEmpty() && getValue() == null; 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 @Override
public IPrimitiveType<T> setValue(T theValue) throws DataFormatException { public IPrimitiveType<T> setValue(T theValue) throws DataFormatException {
myCoercedValue = theValue; myCoercedValue = theValue;
@ -74,15 +103,6 @@ public abstract class BasePrimitive<T> extends BaseIdentifiableElement implement
return this; return this;
} }
protected void updateStringValue() {
if (myCoercedValue == null) {
myStringValue = null;
} else {
// NB this might be null
myStringValue = encode(myCoercedValue);
}
}
@Override @Override
public void setValueAsString(String theValue) throws DataFormatException { public void setValueAsString(String theValue) throws DataFormatException {
if (theValue == null) { if (theValue == null) {
@ -94,26 +114,22 @@ public abstract class BasePrimitive<T> extends BaseIdentifiableElement implement
myStringValue = theValue; 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 @Override
public String toString() { public String toString() {
return getClass().getSimpleName() + "[" + getValueAsString() + "]"; 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 static org.apache.commons.lang3.StringUtils.*;
import java.io.Serializable;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Collections; import java.util.Collections;
import java.util.Date; 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. * define their own keys for storage in resource metadata if needed.
* </p> * </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 * 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% * #L%
*/ */
import org.apache.commons.lang3.Validate;
import ca.uhn.fhir.model.api.IValueSetEnumBinder; import ca.uhn.fhir.model.api.IValueSetEnumBinder;
import ca.uhn.fhir.model.api.annotation.DatatypeDef; import ca.uhn.fhir.model.api.annotation.DatatypeDef;
@ -28,16 +30,27 @@ public class BoundCodeDt<T extends Enum<?>> extends CodeDt {
private IValueSetEnumBinder<T> myBinder; 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) { public BoundCodeDt(IValueSetEnumBinder<T> theBinder) {
Validate.notNull(theBinder, "theBinder must not be null");
myBinder = theBinder; myBinder = theBinder;
} }
public BoundCodeDt(IValueSetEnumBinder<T> theBinder, T theValue) { public BoundCodeDt(IValueSetEnumBinder<T> theBinder, T theValue) {
Validate.notNull(theBinder, "theBinder must not be null");
myBinder = theBinder; myBinder = theBinder;
setValueAsEnum(theValue); setValueAsEnum(theValue);
} }
public void setValueAsEnum(T 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) { if (theValue==null) {
setValue(null); setValue(null);
} else { } else {
@ -46,6 +59,7 @@ public class BoundCodeDt<T extends Enum<?>> extends CodeDt {
} }
public T getValueAsEnum() { public T getValueAsEnum() {
Validate.notNull(myBinder, "This object does not have a binder. Constructor BoundCodeDt() should not be called!");
T retVal = myBinder.fromCodeString(getValue()); T retVal = myBinder.fromCodeString(getValue());
if (retVal == null) { if (retVal == null) {
// TODO: throw special exception type? // TODO: throw special exception type?

View File

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

View File

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

View File

@ -24,7 +24,9 @@ import net.sourceforge.cobertura.CoverageIgnore;
import org.hl7.fhir.instance.model.api.IBaseOperationOutcome; 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.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". * 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 * 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) { public UnprocessableEntityException(IBaseOperationOutcome theOperationOutcome) {
super(STATUS_CODE, DEFAULT_MESSAGE, 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. * 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; 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.ArrayList;
import java.util.List; import java.util.List;

View File

@ -1,5 +1,25 @@
package ca.uhn.fhir.validation; 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.IBaseResource;
import org.hl7.fhir.instance.model.api.IIdType; import org.hl7.fhir.instance.model.api.IIdType;

View File

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

View File

@ -1,5 +1,25 @@
package ca.uhn.fhir.jpa.dao; 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.IBaseOperationOutcome;
import org.hl7.fhir.instance.model.api.IBaseResource; import org.hl7.fhir.instance.model.api.IBaseResource;
import org.hl7.fhir.instance.model.api.IIdType; 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))); ValidationResult result = val.validateWithResult(myRefImplCtx.newJsonParser().parseResource(getContext().newJsonParser().encodeResourceToString(qa)));
if (!result.isSuccessful()) { if (!result.isSuccessful()) {
IBaseOperationOutcome oo = getContext().newJsonParser().parseResource(OperationOutcome.class, myRefImplCtx.newJsonParser().encodeResourceToString(result.toOperationOutcome())); 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.HashSet;
import java.util.Set; import java.util.Set;
import org.apache.commons.lang3.Validate;
import ca.uhn.fhir.model.api.IBoundCodeableConcept; import ca.uhn.fhir.model.api.IBoundCodeableConcept;
import ca.uhn.fhir.model.api.IValueSetEnumBinder; import ca.uhn.fhir.model.api.IValueSetEnumBinder;
import ca.uhn.fhir.model.api.annotation.DatatypeDef; import ca.uhn.fhir.model.api.annotation.DatatypeDef;
@ -37,10 +39,19 @@ public class BoundCodeableConceptDt<T extends Enum<?>> extends CodeableConceptDt
private IValueSetEnumBinder<T> myBinder; private IValueSetEnumBinder<T> myBinder;
/**
* @deprecated This constructor is provided only for serialization support. Do not call it directly!
*/
@Deprecated
public BoundCodeableConceptDt() {
// nothing
}
/** /**
* Constructor * Constructor
*/ */
public BoundCodeableConceptDt(IValueSetEnumBinder<T> theBinder) { public BoundCodeableConceptDt(IValueSetEnumBinder<T> theBinder) {
Validate.notNull(theBinder, "theBinder must not be null");
myBinder = theBinder; myBinder = theBinder;
} }
@ -48,6 +59,7 @@ public class BoundCodeableConceptDt<T extends Enum<?>> extends CodeableConceptDt
* Constructor * Constructor
*/ */
public BoundCodeableConceptDt(IValueSetEnumBinder<T> theBinder, T theValue) { public BoundCodeableConceptDt(IValueSetEnumBinder<T> theBinder, T theValue) {
Validate.notNull(theBinder, "theBinder must not be null");
myBinder = theBinder; myBinder = theBinder;
setValueAsEnum(theValue); setValueAsEnum(theValue);
} }
@ -56,6 +68,7 @@ public class BoundCodeableConceptDt<T extends Enum<?>> extends CodeableConceptDt
* Constructor * Constructor
*/ */
public BoundCodeableConceptDt(IValueSetEnumBinder<T> theBinder, Collection<T> theValues) { public BoundCodeableConceptDt(IValueSetEnumBinder<T> theBinder, Collection<T> theValues) {
Validate.notNull(theBinder, "theBinder must not be null");
myBinder = theBinder; myBinder = theBinder;
setValueAsEnum(theValues); setValueAsEnum(theValues);
} }
@ -70,6 +83,7 @@ public class BoundCodeableConceptDt<T extends Enum<?>> extends CodeableConceptDt
* The value to add, or <code>null</code> * The value to add, or <code>null</code>
*/ */
public void setValueAsEnum(Collection<T> theValues) { public void setValueAsEnum(Collection<T> theValues) {
Validate.notNull(myBinder, "This object does not have a binder. Constructor BoundCodeableConceptDt() should not be called!");
getCoding().clear(); getCoding().clear();
if (theValues != null) { if (theValues != null) {
for (T next : theValues) { for (T next : theValues) {
@ -88,6 +102,7 @@ public class BoundCodeableConceptDt<T extends Enum<?>> extends CodeableConceptDt
* The value to add, or <code>null</code> * The value to add, or <code>null</code>
*/ */
public void setValueAsEnum(T theValue) { public void setValueAsEnum(T theValue) {
Validate.notNull(myBinder, "This object does not have a binder. Constructor BoundCodeableConceptDt() should not be called!");
getCoding().clear(); getCoding().clear();
if (theValue == null) { if (theValue == null) {
return; return;
@ -107,6 +122,7 @@ public class BoundCodeableConceptDt<T extends Enum<?>> extends CodeableConceptDt
* no codes that match the enum. * no codes that match the enum.
*/ */
public Set<T> getValueAsEnum() { 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>(); Set<T> retVal = new HashSet<T>();
for (CodingDt next : getCoding()) { for (CodingDt next : getCoding()) {
if (next == null) { 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.Questionnaire.QuestionComponent;
import org.hl7.fhir.instance.model.QuestionnaireAnswers; import org.hl7.fhir.instance.model.QuestionnaireAnswers;
import org.hl7.fhir.instance.model.QuestionnaireAnswers.QuestionAnswerComponent; 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.Reference;
import org.hl7.fhir.instance.model.Resource; import org.hl7.fhir.instance.model.Resource;
import org.hl7.fhir.instance.model.StringType; import org.hl7.fhir.instance.model.StringType;
@ -44,11 +45,11 @@ import org.hl7.fhir.instance.utils.WorkerContext;
*/ */
public class QuestionnaireAnswersValidator extends BaseValidator { public class QuestionnaireAnswersValidator extends BaseValidator {
/* /* *****************************************************************
* Note to anyone working on this class - * 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. * 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; private WorkerContext myWorkerCtx;
@ -102,9 +103,17 @@ public class QuestionnaireAnswersValidator extends BaseValidator {
return; 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.removeLast();
pathStack.add("group(0)"); 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) { 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, 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()) { for (org.hl7.fhir.instance.model.QuestionnaireAnswers.QuestionComponent next : theAnsGroup.getQuestion()) {
rule(theErrors, IssueType.INVALID, thePathStack, isNotBlank(next.getLinkId()), "Question found with no linkId"); 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++) { for (int i = 0; i < theQuestGroup.getQuestion().size(); i++) {
QuestionComponent nextQuestion = theQuestGroup.getQuestion().get(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 // Check that there are no extra answers
@ -167,12 +176,12 @@ public class QuestionnaireAnswersValidator extends BaseValidator {
thePathStack.remove(); 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, 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(); String linkId = theQuestion.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; 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); rule(theErrors, IssueType.BUSINESSRULE, thePathStack, !theQuestion.getRequired(), "Multiple answers repetitions found with linkId[{0}]", linkId);
} }
if (answers.size() == 0) { 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; return;
} }
org.hl7.fhir.instance.model.QuestionnaireAnswers.QuestionComponent answerQuestion = answers.get(0); org.hl7.fhir.instance.model.QuestionnaireAnswers.QuestionComponent answerQuestion = answers.get(0);
try { try {
thePathStack.add("question(" + answers.indexOf(answerQuestion) + ")"); thePathStack.add("question(" + answers.indexOf(answerQuestion) + ")");
validateQuestionAnswers(theErrors, theQuestion, thePathStack, type, answerQuestion, theAnswers); validateQuestionAnswers(theErrors, theQuestion, thePathStack, type, answerQuestion, theAnswers, theValidateRequired);
validateQuestionGroups(theErrors, theQuestion, answerQuestion, thePathStack, theAnswers); validateQuestionGroups(theErrors, theQuestion, answerQuestion, thePathStack, theAnswers, theValidateRequired);
} finally { } finally {
thePathStack.removeLast(); thePathStack.removeLast();
} }
} }
private void validateQuestionGroups(List<ValidationMessage> theErrors, QuestionComponent theQuestion, org.hl7.fhir.instance.model.QuestionnaireAnswers.QuestionComponent theAnswerQuestion, private void validateQuestionGroups(List<ValidationMessage> theErrors, QuestionComponent theQuestion, org.hl7.fhir.instance.model.QuestionnaireAnswers.QuestionComponent theAnswerQuestion,
LinkedList<String> thePathSpec, QuestionnaireAnswers theAnswers) { LinkedList<String> thePathSpec, QuestionnaireAnswers theAnswers, boolean theValidateRequired) {
validateGroups(theErrors, theQuestion.getGroup(), theAnswerQuestion.getGroup(), thePathSpec, theAnswers); 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, private void validateGroupGroups(List<ValidationMessage> theErrors, GroupComponent theQuestGroup, org.hl7.fhir.instance.model.QuestionnaireAnswers.GroupComponent theAnsGroup,
LinkedList<String> thePathSpec, QuestionnaireAnswers theAnswers) { LinkedList<String> thePathSpec, QuestionnaireAnswers theAnswers, boolean theValidateRequired) {
validateGroups(theErrors, theQuestGroup.getGroup(), theAnsGroup.getGroup(), thePathSpec, theAnswers); 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, 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>(); Set<String> allowedGroups = new HashSet<String>();
for (GroupComponent nextQuestionGroup : theQuestionGroups) { for (GroupComponent nextQuestionGroup : theQuestionGroups) {
String linkId = nextQuestionGroup.getLinkId(); 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); List<org.hl7.fhir.instance.model.QuestionnaireAnswers.GroupComponent> answerGroups = findGroupByLinkId(theAnswerGroups, linkId);
if (answerGroups.isEmpty()) { if (answerGroups.isEmpty()) {
if (nextQuestionGroup.getRequired()) { if (nextQuestionGroup.getRequired() && 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);
} }
continue; continue;
@ -241,7 +250,7 @@ public class QuestionnaireAnswersValidator extends BaseValidator {
for (org.hl7.fhir.instance.model.QuestionnaireAnswers.GroupComponent nextAnswerGroup : answerGroups) { for (org.hl7.fhir.instance.model.QuestionnaireAnswers.GroupComponent nextAnswerGroup : answerGroups) {
int index = theAnswerGroups.indexOf(answerGroups.get(1)); int index = theAnswerGroups.indexOf(answerGroups.get(1));
thePathStack.add("group(" + index + ")"); thePathStack.add("group(" + index + ")");
validateGroup(theErrors, nextQuestionGroup, nextAnswerGroup, thePathStack, theAnswers); validateGroup(theErrors, nextQuestionGroup, nextAnswerGroup, thePathStack, theAnswers, theValidateRequired);
thePathStack.removeLast(); thePathStack.removeLast();
} }
} }
@ -259,13 +268,13 @@ public class QuestionnaireAnswersValidator extends BaseValidator {
} }
private void validateQuestionAnswers(List<ValidationMessage> theErrors, QuestionComponent theQuestion, LinkedList<String> thePathStack, AnswerFormat type, 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(); String linkId = theQuestion.getLinkId();
Set<Class<? extends Type>> allowedAnswerTypes = determineAllowedAnswerTypes(type); Set<Class<? extends Type>> allowedAnswerTypes = determineAllowedAnswerTypes(type);
if (allowedAnswerTypes.isEmpty()) { 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 { } else if (theValidateRequired) {
rule(theErrors, IssueType.BUSINESSRULE, thePathStack, !(answerQuestion.getAnswer().size() > 1 && !theQuestion.getRepeats()), "Multiple answers to non repeating question with linkId[{0}]", rule(theErrors, IssueType.BUSINESSRULE, thePathStack, !(answerQuestion.getAnswer().size() > 1 && !theQuestion.getRepeats()), "Multiple answers to non repeating question with linkId[{0}]",
linkId); linkId);
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);

View File

@ -1,6 +1,7 @@
package ca.uhn.fhir.validation; package ca.uhn.fhir.validation;
import static org.hamcrest.Matchers.containsString; import static org.hamcrest.Matchers.containsString;
import static org.hamcrest.Matchers.empty;
import static org.junit.Assert.*; import static org.junit.Assert.*;
import static org.mockito.Mockito.mock; import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when; import static org.mockito.Mockito.when;
@ -12,12 +13,15 @@ import java.util.List;
import org.apache.commons.io.IOUtils; import org.apache.commons.io.IOUtils;
import org.hl7.fhir.instance.model.Coding; import org.hl7.fhir.instance.model.Coding;
import org.hl7.fhir.instance.model.IdType; 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;
import org.hl7.fhir.instance.model.Questionnaire.GroupComponent;
import org.hl7.fhir.instance.model.QuestionnaireAnswers; import org.hl7.fhir.instance.model.QuestionnaireAnswers;
import org.hl7.fhir.instance.model.Reference; import org.hl7.fhir.instance.model.Reference;
import org.hl7.fhir.instance.model.StringType; import org.hl7.fhir.instance.model.StringType;
import org.hl7.fhir.instance.model.ValueSet; import org.hl7.fhir.instance.model.ValueSet;
import org.hl7.fhir.instance.model.Questionnaire.AnswerFormat; 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.utils.WorkerContext;
import org.hl7.fhir.instance.validation.QuestionnaireAnswersValidator; import org.hl7.fhir.instance.validation.QuestionnaireAnswersValidator;
import org.hl7.fhir.instance.validation.ValidationMessage; import org.hl7.fhir.instance.validation.ValidationMessage;
@ -26,6 +30,7 @@ import org.junit.Test;
import org.mockito.Mockito; import org.mockito.Mockito;
import ca.uhn.fhir.context.FhirContext; import ca.uhn.fhir.context.FhirContext;
import ca.uhn.fhir.model.primitive.IntegerDt;
import ca.uhn.fhir.rest.server.exceptions.ResourceNotFoundException; import ca.uhn.fhir.rest.server.exceptions.ResourceNotFoundException;
public class QuestionnaireAnswersValidatorIntegrationTest { public class QuestionnaireAnswersValidatorIntegrationTest {
@ -66,6 +71,59 @@ 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]")); 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 @Test
public void testCodedAnswer() { public void testCodedAnswer() {
String questionnaireRef = "http://example.com/Questionnaire/q1"; String questionnaireRef = "http://example.com/Questionnaire/q1";
@ -96,12 +154,12 @@ public class QuestionnaireAnswersValidatorIntegrationTest {
result = myVal.validateWithResult(qa); result = myVal.validateWithResult(qa);
ourLog.info(result.getMessages().toString()); ourLog.info(result.getMessages().toString());
assertThat(result.getMessages().toString(), containsString("myLocationString=QuestionnaireAnswers.group(0).question(0).answer(0)")); 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(); result.toOperationOutcome();
} }
@Test @Test
public void testUnknownValueSet() { public void testUnknownValueSet() {
String questionnaireRef = "http://example.com/Questionnaire/q1"; String questionnaireRef = "http://example.com/Questionnaire/q1";

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.StringType;
import org.hl7.fhir.instance.model.ValueSet; import org.hl7.fhir.instance.model.ValueSet;
import org.hl7.fhir.instance.model.Questionnaire.AnswerFormat; 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.utils.WorkerContext;
import org.hl7.fhir.instance.validation.QuestionnaireAnswersValidator; import org.hl7.fhir.instance.validation.QuestionnaireAnswersValidator;
import org.hl7.fhir.instance.validation.ValidationMessage; import org.hl7.fhir.instance.validation.ValidationMessage;
@ -99,6 +100,7 @@ public class QuestionnaireAnswersValidatorTest {
q.getGroup().addQuestion().setLinkId("link1").setRequired(true).setType(AnswerFormat.STRING); q.getGroup().addQuestion().setLinkId("link1").setRequired(true).setType(AnswerFormat.STRING);
QuestionnaireAnswers qa = new QuestionnaireAnswers(); QuestionnaireAnswers qa = new QuestionnaireAnswers();
qa.setStatus(QuestionnaireAnswersStatus.COMPLETED);
qa.getQuestionnaire().setReference("http://example.com/Questionnaire/q1"); qa.getQuestionnaire().setReference("http://example.com/Questionnaire/q1");
qa.getGroup().addQuestion().setLinkId("link1").addAnswer().setValue(new StringType("FOO")); 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(); // ProfileParser pp = new ProfileParser();
// pp.parseSingleProfile(new File("../hapi-tinder-test/src/test/resources/profile/patient.xml"), "http://foo"); // 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.setResourceValueSetFiles(theResourceValueSetFiles);Directory("src/main/resources/vs/");
vsp.parse(); vsp.parse();
DatatypeGeneratorUsingSpreadsheet dtp = new DatatypeGeneratorUsingSpreadsheet("dstu2", "."); DatatypeGeneratorUsingSpreadsheet dtp = new DatatypeGeneratorUsingSpreadsheet("dstu", ".");
dtp.parse(); dtp.parse();
dtp.markResourcesForImports(); dtp.markResourcesForImports();
dtp.bindValueSets(vsp); dtp.bindValueSets(vsp);
@ -227,8 +227,8 @@ public class TinderStructuresMojo extends AbstractMojo {
String dtOutputDir = "target/generated-sources/tinder/ca/uhn/fhir/model/dev/composite"; String dtOutputDir = "target/generated-sources/tinder/ca/uhn/fhir/model/dev/composite";
ResourceGeneratorUsingSpreadsheet rp = new ResourceGeneratorUsingSpreadsheet("dstu2", "."); ResourceGeneratorUsingSpreadsheet rp = new ResourceGeneratorUsingSpreadsheet("dstu", ".");
rp.setBaseResourceNames(Arrays.asList( "composition", "list" rp.setBaseResourceNames(Arrays.asList( "patient", "list"
// //, "contract" // //, "contract"
// "valueset", "organization", "location" // "valueset", "organization", "location"
// , "observation", "conformance" // , "observation", "conformance"

View File

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

View File

@ -49,6 +49,10 @@
Operations in server generated conformance statement should only Operations in server generated conformance statement should only
appear once per name, since the name needs to be unique. appear once per name, since the name needs to be unique.
</action> </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>
<release version="1.1" date="2015-07-13"> <release version="1.1" date="2015-07-13">
<action type="add"> <action type="add">