Clean up the validation framework and integrate the QuestionnaireAnswers

validator
This commit is contained in:
James Agnew 2015-07-10 16:05:40 -04:00
parent f2a725ae53
commit c22aa14d29
32 changed files with 5638 additions and 614 deletions

View File

@ -2,22 +2,26 @@ package example;
import java.io.File; import java.io.File;
import java.io.FileReader; import java.io.FileReader;
import java.util.List;
import org.apache.commons.io.IOUtils; import org.apache.commons.io.IOUtils;
import org.apache.commons.io.filefilter.WildcardFileFilter; import org.apache.commons.io.filefilter.WildcardFileFilter;
import org.hl7.fhir.instance.model.api.IBaseResource; import org.hl7.fhir.instance.model.api.IBaseResource;
import ca.uhn.fhir.context.FhirContext; import ca.uhn.fhir.context.FhirContext;
import ca.uhn.fhir.model.dstu2.resource.OperationOutcome;
import ca.uhn.fhir.model.dstu2.resource.Patient; import ca.uhn.fhir.model.dstu2.resource.Patient;
import ca.uhn.fhir.model.dstu2.valueset.ContactPointSystemEnum; import ca.uhn.fhir.model.dstu2.valueset.ContactPointSystemEnum;
import ca.uhn.fhir.parser.StrictErrorHandler; import ca.uhn.fhir.parser.StrictErrorHandler;
import ca.uhn.fhir.rest.client.IGenericClient; import ca.uhn.fhir.rest.client.IGenericClient;
import ca.uhn.fhir.rest.server.RestfulServer; import ca.uhn.fhir.rest.server.RestfulServer;
import ca.uhn.fhir.validation.FhirValidator; import ca.uhn.fhir.validation.FhirValidator;
import ca.uhn.fhir.validation.SingleValidationMessage;
import ca.uhn.fhir.validation.ValidationResult; import ca.uhn.fhir.validation.ValidationResult;
public class ValidatorExamples { public class ValidatorExamples {
@SuppressWarnings("unused")
public void enableValidation() { public void enableValidation() {
// START SNIPPET: enableValidation // START SNIPPET: enableValidation
FhirContext ctx = FhirContext.forDstu2(); FhirContext ctx = FhirContext.forDstu2();
@ -55,13 +59,22 @@ public class ValidatorExamples {
} else { } else {
// We failed validation! // We failed validation!
System.out.println("Validation failed"); System.out.println("Validation failed");
// The result contains an OperationOutcome outlining the failures
String results = ctx.newXmlParser().setPrettyPrint(true).encodeResourceToString(result.getOperationOutcome());
System.out.println(results);
} }
// The result contains a list of "messages"
List<SingleValidationMessage> messages = result.getMessages();
for (SingleValidationMessage next : messages) {
System.out.println("Message:");
System.out.println(" * Location: " + next.getLocationString());
System.out.println(" * Severity: " + next.getSeverity());
System.out.println(" * Message : " + next.getMessage());
}
// You can also convert the results into an OperationOutcome resource
OperationOutcome oo = (OperationOutcome) result.toOperationOutcome();
String results = ctx.newXmlParser().setPrettyPrint(true).encodeResourceToString(oo);
System.out.println(results);
// END SNIPPET: basicValidation // END SNIPPET: basicValidation
} }

View File

@ -149,7 +149,7 @@ public class FhirValidator {
next.validateBundle(ctx); next.validateBundle(ctx);
} }
IBaseOperationOutcome oo = ctx.getOperationOutcome(); IBaseOperationOutcome oo = ctx.toResult().toOperationOutcome();
if (oo != null && OperationOutcomeUtil.hasIssues(myContext, oo)) { if (oo != null && OperationOutcomeUtil.hasIssues(myContext, oo)) {
throw new ValidationFailureException(myContext, oo); throw new ValidationFailureException(myContext, oo);
} }
@ -169,15 +169,15 @@ public class FhirValidator {
public void validate(IResource theResource) throws ValidationFailureException { public void validate(IResource theResource) throws ValidationFailureException {
ValidationResult validationResult = validateWithResult(theResource); ValidationResult validationResult = validateWithResult(theResource);
if (!validationResult.isSuccessful()) { if (!validationResult.isSuccessful()) {
throw new ValidationFailureException(myContext, validationResult.getOperationOutcome()); throw new ValidationFailureException(myContext, validationResult.toOperationOutcome());
} }
} }
/** /**
* Validates a bundle instance returning a {@link ca.uhn.fhir.validation.ValidationResult} which contains the results. * Validates a bundle instance returning a {@link ca.uhn.fhir.validation.ValidationResult} which contains the results. This validation includes validation of all resources in the bundle.
* This validation includes validation of all resources in the bundle.
* *
* @param theBundle the bundle to validate * @param theBundle
* the bundle to validate
* @return the results of validation * @return the results of validation
* @since 0.7 * @since 0.7
*/ */
@ -190,14 +190,14 @@ public class FhirValidator {
next.validateBundle(ctx); next.validateBundle(ctx);
} }
IBaseOperationOutcome oo = ctx.getOperationOutcome(); return ctx.toResult();
return ValidationResult.valueOf(myContext, oo);
} }
/** /**
* Validates a resource instance returning a {@link ca.uhn.fhir.validation.ValidationResult} which contains the results. * Validates a resource instance returning a {@link ca.uhn.fhir.validation.ValidationResult} which contains the results.
* *
* @param theResource the resource to validate * @param theResource
* the resource to validate
* @return the results of validation * @return the results of validation
* @since 0.7 * @since 0.7
*/ */
@ -210,7 +210,6 @@ public class FhirValidator {
next.validateResource(ctx); next.validateResource(ctx);
} }
IBaseOperationOutcome oo = ctx.getOperationOutcome(); return ctx.toResult();
return ValidationResult.valueOf(myContext, oo);
} }
} }

View File

@ -1,17 +1,22 @@
package ca.uhn.fhir.validation; package ca.uhn.fhir.validation;
import org.hl7.fhir.instance.model.api.IBaseOperationOutcome;
import ca.uhn.fhir.context.FhirContext; import ca.uhn.fhir.context.FhirContext;
import ca.uhn.fhir.rest.server.EncodingEnum;
interface IValidationContext<T> { interface IValidationContext<T> {
public abstract FhirContext getFhirContext(); FhirContext getFhirContext();
public abstract IBaseOperationOutcome getOperationOutcome(); T getResource();
public abstract T getResource(); String getResourceAsString();
public abstract String getXmlEncodedResource(); EncodingEnum getResourceAsStringEncoding();
String getResourceName();
void addValidationMessage(SingleValidationMessage theMessage);
ValidationResult toResult();
} }

View File

@ -0,0 +1,51 @@
package ca.uhn.fhir.validation;
import java.util.Collections;
import java.util.HashMap;
import java.util.Map;
public enum ResultSeverityEnum {
/**
* The issue has no relation to the degree of success of the action
*/
INFORMATION("information"),
/**
* The issue is not important enough to cause the action to fail, but may cause it to be performed suboptimally or in a way that is not as desired
*/
WARNING("warning"),
/**
* The issue is sufficiently important to cause the action to fail
*/
ERROR("error"),
/**
* The issue caused the action to fail, and no further checking could be performed
*/
FATAL("fatal");
private static Map<String, ResultSeverityEnum> ourValues;
private String myCode;
private ResultSeverityEnum(String theCode) {
myCode = theCode;
}
public String getCode() {
return myCode;
}
public static ResultSeverityEnum fromCode(String theCode) {
if (ourValues == null) {
HashMap<String, ResultSeverityEnum> values = new HashMap<String, ResultSeverityEnum>();
for (ResultSeverityEnum next : values()) {
values.put(next.getCode(), next);
}
ourValues = Collections.unmodifiableMap(values);
}
return ourValues.get(theCode);
}
}

View File

@ -49,8 +49,8 @@ import ca.uhn.fhir.context.ConfigurationException;
import ca.uhn.fhir.context.FhirContext; import ca.uhn.fhir.context.FhirContext;
import ca.uhn.fhir.context.FhirVersionEnum; import ca.uhn.fhir.context.FhirVersionEnum;
import ca.uhn.fhir.model.api.Bundle; import ca.uhn.fhir.model.api.Bundle;
import ca.uhn.fhir.rest.server.EncodingEnum;
import ca.uhn.fhir.rest.server.exceptions.InternalErrorException; import ca.uhn.fhir.rest.server.exceptions.InternalErrorException;
import ca.uhn.fhir.util.OperationOutcomeUtil;
class SchemaBaseValidator implements IValidator { class SchemaBaseValidator implements IValidator {
private static final org.slf4j.Logger ourLog = org.slf4j.LoggerFactory.getLogger(SchemaBaseValidator.class); private static final org.slf4j.Logger ourLog = org.slf4j.LoggerFactory.getLogger(SchemaBaseValidator.class);
@ -83,10 +83,12 @@ class SchemaBaseValidator implements IValidator {
Validator validator = schema.newValidator(); Validator validator = schema.newValidator();
MyErrorHandler handler = new MyErrorHandler(theContext); MyErrorHandler handler = new MyErrorHandler(theContext);
validator.setErrorHandler(handler); validator.setErrorHandler(handler);
String encodedResource = theContext.getXmlEncodedResource(); String encodedResource;
if (theContext.getResourceAsStringEncoding() == EncodingEnum.XML) {
// ourLog.info(new FhirContext().newXmlParser().setPrettyPrint(true).encodeBundleToString((Bundle) encodedResource = theContext.getResourceAsString();
// theContext.getResource())); } else {
encodedResource = theContext.getFhirContext().newXmlParser().encodeResourceToString((IBaseResource) theContext.getResource());
}
validator.validate(new StreamSource(new StringReader(encodedResource))); validator.validate(new StreamSource(new StringReader(encodedResource)));
} catch (SAXException e) { } catch (SAXException e) {
@ -106,10 +108,10 @@ class SchemaBaseValidator implements IValidator {
return schema; return schema;
} }
Source baseSource = loadXml("dstu", null, theSchemaName); Source baseSource = loadXml(null, theSchemaName);
SchemaFactory schemaFactory = SchemaFactory.newInstance(XMLConstants.W3C_XML_SCHEMA_NS_URI); SchemaFactory schemaFactory = SchemaFactory.newInstance(XMLConstants.W3C_XML_SCHEMA_NS_URI);
schemaFactory.setResourceResolver(new MyResourceResolver("dstu")); schemaFactory.setResourceResolver(new MyResourceResolver());
try { try {
schema = schemaFactory.newSchema(new Source[] { baseSource }); schema = schemaFactory.newSchema(new Source[] { baseSource });
@ -121,7 +123,7 @@ class SchemaBaseValidator implements IValidator {
} }
} }
private Source loadXml(String theVersion, String theSystemId, String theSchemaName) { private Source loadXml(String theSystemId, String theSchemaName) {
String pathToBase = myCtx.getVersion().getPathToSchemaDefinitions() + '/' + theSchemaName; String pathToBase = myCtx.getVersion().getPathToSchemaDefinitions() + '/' + theSchemaName;
ourLog.debug("Going to load resource: {}", pathToBase); ourLog.debug("Going to load resource: {}", pathToBase);
InputStream baseIs = FhirValidator.class.getResourceAsStream(pathToBase); InputStream baseIs = FhirValidator.class.getResourceAsStream(pathToBase);
@ -132,17 +134,6 @@ class SchemaBaseValidator implements IValidator {
InputStreamReader baseReader = new InputStreamReader(baseIs, Charset.forName("UTF-8")); InputStreamReader baseReader = new InputStreamReader(baseIs, Charset.forName("UTF-8"));
Source baseSource = new StreamSource(baseReader, theSystemId); Source baseSource = new StreamSource(baseReader, theSystemId);
// String schema;
// try {
// schema = IOUtils.toString(baseIs, Charset.forName("UTF-8"));
// } catch (IOException e) {
// throw new InternalErrorException(e);
// }
//
// ourLog.info("Schema is:\n{}", schema);
//
// Source baseSource = new StreamSource(new StringReader(schema), theSystemId);
// Source baseSource = new StreamSource(baseIs, theSystemId);
return baseSource; return baseSource;
} }
@ -168,34 +159,34 @@ class SchemaBaseValidator implements IValidator {
myContext = theContext; myContext = theContext;
} }
private void addIssue(SAXParseException theException, String theSeverity) { private void addIssue(SAXParseException theException, ResultSeverityEnum theSeverity) {
String details = theException.getLocalizedMessage(); SingleValidationMessage message = new SingleValidationMessage();
String location = "Line[" + theException.getLineNumber() + "] Col[" + theException.getColumnNumber() + "]"; message.setLocationRow(theException.getLineNumber());
OperationOutcomeUtil.addIssue(myContext.getFhirContext(), myContext.getOperationOutcome(), theSeverity, details, location); message.setLocationCol(theException.getColumnNumber());
message.setMessage(theException.getLocalizedMessage());
message.setSeverity(theSeverity);
myContext.addValidationMessage(message);
} }
@Override @Override
public void error(SAXParseException theException) throws SAXException { public void error(SAXParseException theException) {
addIssue(theException, "error"); addIssue(theException, ResultSeverityEnum.ERROR);
} }
@Override @Override
public void fatalError(SAXParseException theException) throws SAXException { public void fatalError(SAXParseException theException) {
addIssue(theException, "fatal"); addIssue(theException, ResultSeverityEnum.FATAL);
} }
@Override @Override
public void warning(SAXParseException theException) throws SAXException { public void warning(SAXParseException theException) {
addIssue(theException, "warning"); addIssue(theException, ResultSeverityEnum.WARNING);
} }
} }
private final class MyResourceResolver implements LSResourceResolver { private final class MyResourceResolver implements LSResourceResolver {
private String myVersion; private MyResourceResolver() {
private MyResourceResolver(String theVersion) {
myVersion = theVersion;
} }
@Override @Override

View File

@ -35,7 +35,6 @@ import ca.uhn.fhir.context.FhirContext;
import ca.uhn.fhir.model.api.Bundle; import ca.uhn.fhir.model.api.Bundle;
import ca.uhn.fhir.model.api.BundleEntry; import ca.uhn.fhir.model.api.BundleEntry;
import ca.uhn.fhir.rest.server.exceptions.InternalErrorException; import ca.uhn.fhir.rest.server.exceptions.InternalErrorException;
import ca.uhn.fhir.util.OperationOutcomeUtil;
import com.phloc.commons.error.IResourceError; import com.phloc.commons.error.IResourceError;
import com.phloc.commons.error.IResourceErrorGroup; import com.phloc.commons.error.IResourceErrorGroup;
@ -56,7 +55,7 @@ public class SchematronBaseValidator implements IValidator {
public void validateResource(IValidationContext<IBaseResource> theCtx) { public void validateResource(IValidationContext<IBaseResource> theCtx) {
ISchematronResource sch = getSchematron(theCtx); ISchematronResource sch = getSchematron(theCtx);
StreamSource source = new StreamSource(new StringReader(theCtx.getXmlEncodedResource())); StreamSource source = new StreamSource(new StringReader(theCtx.getResourceAsString()));
SchematronOutputType results = SchematronHelper.applySchematron(sch, source); SchematronOutputType results = SchematronHelper.applySchematron(sch, source);
if (results == null) { if (results == null) {
@ -70,16 +69,16 @@ public class SchematronBaseValidator implements IValidator {
} }
for (IResourceError next : errors.getAllErrors().getAllResourceErrors()) { for (IResourceError next : errors.getAllErrors().getAllResourceErrors()) {
String severity; ResultSeverityEnum severity;
switch (next.getErrorLevel()) { switch (next.getErrorLevel()) {
case ERROR: case ERROR:
severity = ("error"); severity = ResultSeverityEnum.ERROR;
break; break;
case FATAL_ERROR: case FATAL_ERROR:
severity = ("fatal"); severity = ResultSeverityEnum.FATAL;
break; break;
case WARN: case WARN:
severity = ("warning"); severity = ResultSeverityEnum.WARNING;
break; break;
case INFO: case INFO:
case SUCCESS: case SUCCESS:
@ -88,7 +87,14 @@ public class SchematronBaseValidator implements IValidator {
} }
String details = next.getAsString(Locale.getDefault()); String details = next.getAsString(Locale.getDefault());
OperationOutcomeUtil.addIssue(myCtx, theCtx.getOperationOutcome(), severity, details);
SingleValidationMessage message = new SingleValidationMessage();
message.setMessage(details);
message.setLocationRow(next.getLocation().getLineNumber());
message.setLocationCol(next.getLocation().getColumnNumber());
message.setLocationString(next.getLocation().getAsString());
message.setSeverity(severity);
theCtx.addValidationMessage(message);
} }
} }
@ -97,10 +103,10 @@ public class SchematronBaseValidator implements IValidator {
Class<? extends IBaseResource> resource = theCtx.getResource().getClass(); Class<? extends IBaseResource> resource = theCtx.getResource().getClass();
Class<? extends IBaseResource> baseResourceClass = theCtx.getFhirContext().getResourceDefinition(resource).getBaseDefinition().getImplementingClass(); Class<? extends IBaseResource> baseResourceClass = theCtx.getFhirContext().getResourceDefinition(resource).getBaseDefinition().getImplementingClass();
return getSchematronAndCache(theCtx, "dstu", baseResourceClass); return getSchematronAndCache(theCtx, baseResourceClass);
} }
private ISchematronResource getSchematronAndCache(IValidationContext<IBaseResource> theCtx, String theVersion, Class<? extends IBaseResource> theClass) { private ISchematronResource getSchematronAndCache(IValidationContext<IBaseResource> theCtx, Class<? extends IBaseResource> theClass) {
synchronized (myClassToSchematron) { synchronized (myClassToSchematron) {
ISchematronResource retVal = myClassToSchematron.get(theClass); ISchematronResource retVal = myClassToSchematron.get(theClass);
if (retVal != null) { if (retVal != null) {

View File

@ -0,0 +1,105 @@
package ca.uhn.fhir.validation;
import org.apache.commons.lang3.builder.EqualsBuilder;
import org.apache.commons.lang3.builder.HashCodeBuilder;
import org.apache.commons.lang3.builder.ToStringBuilder;
import org.apache.commons.lang3.builder.ToStringStyle;
public class SingleValidationMessage {
private Integer myLocationCol;
private Integer myLocationRow;
private String myLocationString;
private String myMessage;
private ResultSeverityEnum mySeverity;
@Override
public boolean equals(Object obj) {
if (this == obj) {
return true;
}
if (obj == null) {
return false;
}
if (!(obj instanceof SingleValidationMessage)) {
return false;
}
SingleValidationMessage other = (SingleValidationMessage) obj;
EqualsBuilder b = new EqualsBuilder();
b.append(myLocationCol, other.myLocationCol);
b.append(myLocationRow, other.myLocationRow);
b.append(myLocationString, other.myLocationString);
b.append(myMessage, other.myMessage);
b.append(mySeverity, other.mySeverity);
return b.isEquals();
}
public Integer getLocationCol() {
return myLocationCol;
}
public Integer getLocationRow() {
return myLocationRow;
}
public String getLocationString() {
return myLocationString;
}
public String getMessage() {
return myMessage;
}
public ResultSeverityEnum getSeverity() {
return mySeverity;
}
@Override
public int hashCode() {
HashCodeBuilder b = new HashCodeBuilder();
b.append(myLocationCol);
b.append(myLocationCol);
b.append(myLocationString);
b.append(myMessage);
b.append(mySeverity);
return b.toHashCode();
}
public void setLocationCol(Integer theLocationCol) {
myLocationCol = theLocationCol;
}
public void setLocationRow(Integer theLocationRow) {
myLocationRow = theLocationRow;
}
public void setLocationString(String theLocationString) {
myLocationString = theLocationString;
}
public void setMessage(String theMessage) {
myMessage = theMessage;
}
public void setSeverity(ResultSeverityEnum theSeverity) {
mySeverity = theSeverity;
}
@Override
public String toString() {
ToStringBuilder b = new ToStringBuilder(this, ToStringStyle.SHORT_PREFIX_STYLE);
if (myLocationCol != null || myLocationRow != null) {
b.append("myLocationCol", myLocationCol);
b.append("myLocationRow", myLocationRow);
}
if (myLocationString != null) {
b.append("myLocationString", myLocationString);
}
b.append("myMessage", myMessage);
if (mySeverity != null) {
b.append("mySeverity", mySeverity.getCode());
}
return b.toString();
}
}

View File

@ -20,97 +20,152 @@ package ca.uhn.fhir.validation;
* #L% * #L%
*/ */
import org.hl7.fhir.instance.model.api.IBaseOperationOutcome; import java.util.ArrayList;
import java.util.List;
import org.hl7.fhir.instance.model.api.IBaseResource; import org.hl7.fhir.instance.model.api.IBaseResource;
import org.thymeleaf.util.Validate;
import ca.uhn.fhir.context.FhirContext; import ca.uhn.fhir.context.FhirContext;
import ca.uhn.fhir.model.api.Bundle; import ca.uhn.fhir.model.api.Bundle;
import ca.uhn.fhir.rest.server.exceptions.InternalErrorException; import ca.uhn.fhir.model.api.IResource;
import ca.uhn.fhir.rest.server.EncodingEnum;
class ValidationContext<T> implements IValidationContext<T> { class ValidationContext<T> implements IValidationContext<T> {
private static final org.slf4j.Logger ourLog = org.slf4j.LoggerFactory.getLogger(ValidationContext.class);
private final IEncoder myEncoder; private final IEncoder myEncoder;
private final FhirContext myFhirContext; private final FhirContext myFhirContext;
private IBaseOperationOutcome myOperationOutcome; private List<SingleValidationMessage> myMessages = new ArrayList<SingleValidationMessage>();
private final T myResource; private final T myResource;
private String myXmlEncodedResource; private String myResourceAsString;
private final EncodingEnum myResourceAsStringEncoding;
private final String myResourceName;
private ValidationContext(FhirContext theContext, T theResource, IEncoder theEncoder) { private ValidationContext(FhirContext theContext, T theResource, String theResourceName, IEncoder theEncoder) {
myFhirContext = theContext; myFhirContext = theContext;
myResource = theResource; myResource = theResource;
myEncoder = theEncoder; myEncoder = theEncoder;
myResourceName = theResourceName;
if (theEncoder != null) {
myResourceAsStringEncoding = theEncoder.getEncoding();
} else {
myResourceAsStringEncoding = null;
}
}
@Override
public void addValidationMessage(SingleValidationMessage theMessage) {
Validate.notNull(theMessage, "theMessage must not be null");
myMessages.add(theMessage);
} }
/* (non-Javadoc)
* @see ca.uhn.fhir.validation.IValidationContext#getFhirContext()
*/
@Override @Override
public FhirContext getFhirContext() { public FhirContext getFhirContext() {
return myFhirContext; return myFhirContext;
} }
/* (non-Javadoc)
* @see ca.uhn.fhir.validation.IValidationContext#getOperationOutcome()
*/
@Override
public IBaseOperationOutcome getOperationOutcome() {
if (myOperationOutcome == null) {
try {
myOperationOutcome = (IBaseOperationOutcome) myFhirContext.getResourceDefinition("OperationOutcome").getImplementingClass().newInstance();
} catch (Exception e1) {
ourLog.error("Failed to instantiate OperationOutcome resource instance", e1);
throw new InternalErrorException("Failed to instantiate OperationOutcome resource instance", e1);
}
}
return myOperationOutcome;
}
/* (non-Javadoc)
* @see ca.uhn.fhir.validation.IValidationContext#getResource()
*/
@Override @Override
public T getResource() { public T getResource() {
return myResource; return myResource;
} }
/* (non-Javadoc)
* @see ca.uhn.fhir.validation.IValidationContext#getXmlEncodedResource()
*/
@Override @Override
public String getXmlEncodedResource() { public String getResourceAsString() {
if (myXmlEncodedResource == null) { if (myResourceAsString == null) {
myXmlEncodedResource = myEncoder.encode(); myResourceAsString = myEncoder.encode();
} }
return myXmlEncodedResource; return myResourceAsString;
}
@Override
public EncodingEnum getResourceAsStringEncoding() {
return myResourceAsStringEncoding;
}
@Override
public String getResourceName() {
return myResourceName;
}
@Override
public ValidationResult toResult() {
return new ValidationResult(myFhirContext, myMessages);
} }
public static IValidationContext<Bundle> forBundle(final FhirContext theContext, final Bundle theBundle) { public static IValidationContext<Bundle> forBundle(final FhirContext theContext, final Bundle theBundle) {
return new ValidationContext<Bundle>(theContext, theBundle, new IEncoder() { String resourceName = "Bundle";
return new ValidationContext<Bundle>(theContext, theBundle, resourceName, new IEncoder() {
@Override @Override
public String encode() { public String encode() {
return theContext.newXmlParser().encodeBundleToString(theBundle); return theContext.newXmlParser().encodeBundleToString(theBundle);
} }
@Override
public EncodingEnum getEncoding() {
return EncodingEnum.XML;
}
}); });
} }
public static <T extends IBaseResource> IValidationContext<T> forResource(final FhirContext theContext, final T theResource) { public static <T extends IBaseResource> IValidationContext<T> forResource(final FhirContext theContext, final T theResource) {
return new ValidationContext<T>(theContext, theResource, new IEncoder() { String resourceName = theContext.getResourceDefinition(theResource).getName();
return new ValidationContext<T>(theContext, theResource, resourceName, new IEncoder() {
@Override @Override
public String encode() { public String encode() {
return theContext.newXmlParser().encodeResourceToString(theResource); return theContext.newXmlParser().encodeResourceToString(theResource);
} }
@Override
public EncodingEnum getEncoding() {
return EncodingEnum.XML;
}
}); });
} }
public static IValidationContext<IBaseResource> newChild(IValidationContext<Bundle> theContext, IBaseResource theResource) { public static IValidationContext<IBaseResource> newChild(final IValidationContext<Bundle> theContext, final IResource theResource) {
ValidationContext<IBaseResource> retVal = (ValidationContext<IBaseResource>) forResource(theContext.getFhirContext(), theResource); return new IValidationContext<IBaseResource>() {
retVal.myOperationOutcome = theContext.getOperationOutcome();
return retVal; @Override
public void addValidationMessage(SingleValidationMessage theMessage) {
theContext.addValidationMessage(theMessage);
}
@Override
public FhirContext getFhirContext() {
return theContext.getFhirContext();
}
@Override
public IBaseResource getResource() {
return theResource;
}
@Override
public String getResourceAsString() {
return theContext.getFhirContext().newXmlParser().encodeResourceToString(theResource);
}
@Override
public EncodingEnum getResourceAsStringEncoding() {
return EncodingEnum.XML;
}
@Override
public String getResourceName() {
return theContext.getFhirContext().getResourceDefinition(theResource).getName();
}
@Override
public ValidationResult toResult() {
return theContext.toResult();
}
};
} }
private interface IEncoder { private interface IEncoder {
String encode(); String encode();
EncodingEnum getEncoding();
} }
} }

View File

@ -20,6 +20,11 @@ package ca.uhn.fhir.validation;
* #L% * #L%
*/ */
import static org.apache.commons.lang3.StringUtils.isNotBlank;
import java.util.Collections;
import java.util.List;
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.context.FhirContext;
@ -32,40 +37,25 @@ import ca.uhn.fhir.util.OperationOutcomeUtil;
* @since 0.7 * @since 0.7
*/ */
public class ValidationResult { public class ValidationResult {
private IBaseOperationOutcome myOperationOutcome; private final FhirContext myCtx;
private boolean myIsSuccessful; private final boolean myIsSuccessful;
private FhirContext myCtx; private final List<SingleValidationMessage> myMessages;
private ValidationResult(FhirContext theCtx, IBaseOperationOutcome theOperationOutcome, boolean isSuccessful) { public ValidationResult(FhirContext theCtx, List<SingleValidationMessage> theMessages) {
this.myCtx = theCtx; boolean successful = true;
this.myOperationOutcome = theOperationOutcome; myCtx = theCtx;
this.myIsSuccessful = isSuccessful; myMessages = theMessages;
for (SingleValidationMessage next : myMessages) {
next.getSeverity();
if (next.getSeverity() == null || next.getSeverity().ordinal() > ResultSeverityEnum.WARNING.ordinal()) {
successful = false;
}
}
myIsSuccessful = successful;
} }
public static ValidationResult valueOf(FhirContext theCtx, IBaseOperationOutcome myOperationOutcome) { public List<SingleValidationMessage> getMessages() {
boolean noIssues = !OperationOutcomeUtil.hasIssues(theCtx, myOperationOutcome); return Collections.unmodifiableList(myMessages);
return new ValidationResult(theCtx, myOperationOutcome, noIssues);
}
public IBaseOperationOutcome getOperationOutcome() {
return myOperationOutcome;
}
@Override
public String toString() {
return "ValidationResult{" + "myOperationOutcome=" + myOperationOutcome + ", isSuccessful=" + myIsSuccessful + ", description='" + toDescription() + '\'' + '}';
}
private String toDescription() {
StringBuilder b = new StringBuilder(100);
if (myOperationOutcome != null && OperationOutcomeUtil.hasIssues(myCtx, myOperationOutcome)) {
b.append(OperationOutcomeUtil.getFirstIssueDetails(myCtx, myOperationOutcome));
b.append(" - ");
b.append(OperationOutcomeUtil.getFirstIssueLocation(myCtx, myOperationOutcome));
} else {
b.append("No issues");
}
return b.toString();
} }
/** /**
@ -76,4 +66,50 @@ public class ValidationResult {
public boolean isSuccessful() { public boolean isSuccessful() {
return myIsSuccessful; return myIsSuccessful;
} }
private String toDescription() {
StringBuilder b = new StringBuilder(100);
if (myMessages.size() > 0) {
b.append(myMessages.get(0).getMessage());
b.append(" - ");
b.append(myMessages.get(0).getLocationString());
} else {
b.append("No issues");
}
return b.toString();
}
/**
* @deprecated Use {@link #toOperationOutcome()} instead since this method returns a view
*/
@Deprecated
public IBaseOperationOutcome getOperationOutcome() {
return toOperationOutcome();
}
/**
* Create an OperationOutcome resource which contains all of the messages found as a result of this validation
*/
public IBaseOperationOutcome toOperationOutcome() {
IBaseOperationOutcome oo = (IBaseOperationOutcome) myCtx.getResourceDefinition("OperationOutcome").newInstance();
for (SingleValidationMessage next : myMessages) {
String location;
if (isNotBlank(next.getLocationString())) {
location = next.getLocationString();
} else if (next.getLocationRow() != null || next.getLocationCol() != null) {
location = "Line[" + next.getLocationRow() + "] Col[" + next.getLocationCol() + "]";
} else {
location = null;
}
String severity = next.getSeverity() != null ? next.getSeverity().getCode() : null;
OperationOutcomeUtil.addIssue(myCtx, oo, severity, next.getMessage(), location);
}
return oo;
}
@Override
public String toString() {
return "ValidationResult{" + "messageCount=" + myMessages.size() + ", isSuccessful=" + myIsSuccessful + ", description='" + toDescription() + '\'' + '}';
}
} }

View File

@ -0,0 +1,18 @@
package ca.uhn.fhir.validation;
import static org.junit.Assert.*;
import org.junit.Test;
public class ResultSeverityEnumTest {
@Test
public void testOrdinals() {
assertEquals(0, ResultSeverityEnum.INFORMATION.ordinal());
assertEquals(1, ResultSeverityEnum.WARNING.ordinal());
assertEquals(2, ResultSeverityEnum.ERROR.ordinal());
assertEquals(3, ResultSeverityEnum.FATAL.ordinal());
}
}

View File

@ -51,6 +51,12 @@
<version>1.1-SNAPSHOT</version> <version>1.1-SNAPSHOT</version>
<optional>true</optional> <optional>true</optional>
</dependency> </dependency>
<dependency>
<groupId>ca.uhn.hapi.fhir</groupId>
<artifactId>hapi-fhir-structures-hl7org-dstu2</artifactId>
<version>1.1-SNAPSHOT</version>
<optional>true</optional>
</dependency>
<dependency> <dependency>
<groupId>org.slf4j</groupId> <groupId>org.slf4j</groupId>

View File

@ -0,0 +1,29 @@
package ca.uhn.fhir.jpa.provider;
/*
* #%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 ca.uhn.fhir.model.dstu2.resource.Encounter;
public class BaseJpaResourceProviderQuestionnaireAnswersDstu2 extends JpaResourceProviderDstu2<Encounter> {
// nothing yet
}

View File

@ -87,6 +87,9 @@ public class BaseJpaResourceProviderValueSetDstu2 extends JpaResourceProviderDst
} else { } else {
String filter = theFilter.getValue().toLowerCase(); String filter = theFilter.getValue().toLowerCase();
if (next.getDisplay().toLowerCase().contains(filter) || next.getCode().toLowerCase().contains(filter)) { if (next.getDisplay().toLowerCase().contains(filter) || next.getCode().toLowerCase().contains(filter)) {
if (include == null) {
include = getOrAddComposeInclude(retVal, systemToCompose, source.getDefine().getSystem());
}
include.addConcept(new ComposeIncludeConcept().setCode(next.getCode()).setDisplay(next.getDisplay())); include.addConcept(new ComposeIncludeConcept().setCode(next.getCode()).setDisplay(next.getDisplay()));
} }
} }

View File

@ -340,8 +340,7 @@ public class ResourceProviderDstu2Test extends BaseJpaTest {
} }
/* /*
* Try it with a raw socket call. The Apache client won't let us use the unescaped "|" in the URL but we want to * Try it with a raw socket call. The Apache client won't let us use the unescaped "|" in the URL but we want to make sure that works too..
* make sure that works too..
*/ */
Socket sock = new Socket(); Socket sock = new Socket();
sock.setSoTimeout(3000); sock.setSoTimeout(3000);
@ -715,7 +714,8 @@ public class ResourceProviderDstu2Test extends BaseJpaTest {
p1.addIdentifier().setValue("testSearchByIdentifierWithoutSystem01"); p1.addIdentifier().setValue("testSearchByIdentifierWithoutSystem01");
IdDt p1Id = (IdDt) ourClient.create().resource(p1).execute().getId(); IdDt p1Id = (IdDt) ourClient.create().resource(p1).execute().getId();
Bundle actual = ourClient.search().forResource(Patient.class).where(Patient.IDENTIFIER.exactly().systemAndCode(null, "testSearchByIdentifierWithoutSystem01")).encodedJson().prettyPrint().execute(); Bundle actual = ourClient.search().forResource(Patient.class).where(Patient.IDENTIFIER.exactly().systemAndCode(null, "testSearchByIdentifierWithoutSystem01")).encodedJson().prettyPrint()
.execute();
assertEquals(1, actual.size()); assertEquals(1, actual.size());
assertEquals(p1Id.getIdPart(), actual.getEntries().get(0).getResource().getId().getIdPart()); assertEquals(p1Id.getIdPart(), actual.getEntries().get(0).getResource().getId().getIdPart());
@ -1141,7 +1141,8 @@ public class ResourceProviderDstu2Test extends BaseJpaTest {
assertThat(p1Id.getValue(), containsString("Patient/testUpdateWithClientSuppliedIdWhichDoesntExistRpDstu2/_history")); assertThat(p1Id.getValue(), containsString("Patient/testUpdateWithClientSuppliedIdWhichDoesntExistRpDstu2/_history"));
Bundle actual = ourClient.search().forResource(Patient.class).where(Patient.IDENTIFIER.exactly().systemAndCode("urn:system", "testUpdateWithClientSuppliedIdWhichDoesntExistRpDstu2")).encodedJson().prettyPrint().execute(); Bundle actual = ourClient.search().forResource(Patient.class).where(Patient.IDENTIFIER.exactly().systemAndCode("urn:system", "testUpdateWithClientSuppliedIdWhichDoesntExistRpDstu2"))
.encodedJson().prettyPrint().execute();
assertEquals(1, actual.size()); assertEquals(1, actual.size());
assertEquals(p1Id.getIdPart(), actual.getEntries().get(0).getResource().getId().getIdPart()); assertEquals(p1Id.getIdPart(), actual.getEntries().get(0).getResource().getId().getIdPart());
@ -1174,54 +1175,6 @@ public class ResourceProviderDstu2Test extends BaseJpaTest {
} }
} }
// @Test
public void testValueSetExpandOperartion() throws IOException {
ValueSet upload = ourCtx.newXmlParser().parseResource(ValueSet.class, new InputStreamReader(ResourceProviderDstu2Test.class.getResourceAsStream("/extensional-case-2.xml")));
IIdType vsid = ourClient.create().resource(upload).execute().getId().toUnqualifiedVersionless();
HttpGet get = new HttpGet(ourServerBase + "/ValueSet/" + vsid.getIdPart() + "/$expand");
CloseableHttpResponse response = ourHttpClient.execute(get);
try {
String resp = IOUtils.toString(response.getEntity().getContent());
ourLog.info(resp);
assertEquals(200, response.getStatusLine().getStatusCode());
//@formatter:on
assertThat(resp, stringContainsInOrder(
"<ValueSet xmlns=\"http://hl7.org/fhir\">",
"<compose>" ,
"<include>" ,
"<system value=\"http://loinc.org\"/>" ,
"<concept>" ,
"<code value=\"11378-7\"/>" ,
"<display value=\"Systolic blood pressure at First encounter\"/>" ,
"</concept>"));
//@formatter:off
} finally {
IOUtils.closeQuietly(response.getEntity().getContent());
response.close();
}
/*
* Filter
*/
get = new HttpGet(ourServerBase + "/ValueSet/" + vsid.getIdPart() + "/$expand?filter=systolic");
response = ourHttpClient.execute(get);
try {
String resp = IOUtils.toString(response.getEntity().getContent());
ourLog.info(resp);
assertEquals(200, response.getStatusLine().getStatusCode());
assertThat(resp, stringContainsInOrder(
"<code value=\"11378-7\"/>" ,
"<display value=\"Systolic blood pressure at First encounter\"/>"));
} finally {
IOUtils.closeQuietly(response.getEntity().getContent());
response.close();
}
}
@Test @Test
public void testValidateResourceHuge() throws IOException { public void testValidateResourceHuge() throws IOException {
@ -1277,6 +1230,83 @@ public class ResourceProviderDstu2Test extends BaseJpaTest {
} }
} }
@Test
public void testValueSetExpandOperation() throws IOException {
ValueSet upload = ourCtx.newXmlParser().parseResource(ValueSet.class, new InputStreamReader(ResourceProviderDstu2Test.class.getResourceAsStream("/extensional-case-2.xml")));
IIdType vsid = ourClient.create().resource(upload).execute().getId().toUnqualifiedVersionless();
HttpGet get = new HttpGet(ourServerBase + "/ValueSet/" + vsid.getIdPart() + "/$expand");
CloseableHttpResponse response = ourHttpClient.execute(get);
try {
String resp = IOUtils.toString(response.getEntity().getContent());
ourLog.info(resp);
assertEquals(200, response.getStatusLine().getStatusCode());
// @formatter:off
assertThat(
resp,
stringContainsInOrder("<ValueSet xmlns=\"http://hl7.org/fhir\">",
"<compose>",
"<include>",
"<system value=\"http://loinc.org\"/>",
"<concept>",
"<code value=\"11378-7\"/>",
"<display value=\"Systolic blood pressure at First encounter\"/>",
"</concept>",
"<concept>",
"<code value=\"8450-9\"/>",
"<display value=\"Systolic blood pressure--expiration\"/>",
"</concept>"
));
//@formatter:on
} finally {
IOUtils.closeQuietly(response.getEntity().getContent());
response.close();
}
/*
* Filter with display name
*/
get = new HttpGet(ourServerBase + "/ValueSet/" + vsid.getIdPart() + "/$expand?filter=systolic");
response = ourHttpClient.execute(get);
try {
String resp = IOUtils.toString(response.getEntity().getContent());
ourLog.info(resp);
assertEquals(200, response.getStatusLine().getStatusCode());
//@formatter:off
assertThat(resp, stringContainsInOrder(
"<code value=\"11378-7\"/>",
"<display value=\"Systolic blood pressure at First encounter\"/>"));
//@formatter:on
} finally {
IOUtils.closeQuietly(response.getEntity().getContent());
response.close();
}
/*
* Filter with code
*/
get = new HttpGet(ourServerBase + "/ValueSet/" + vsid.getIdPart() + "/$expand?filter=11378");
response = ourHttpClient.execute(get);
try {
String resp = IOUtils.toString(response.getEntity().getContent());
ourLog.info(resp);
assertEquals(200, response.getStatusLine().getStatusCode());
//@formatter:off
assertThat(resp, stringContainsInOrder(
"<code value=\"11378-7\"/>",
"<display value=\"Systolic blood pressure at First encounter\"/>"
));
//@formatter:on
} finally {
IOUtils.closeQuietly(response.getEntity().getContent());
response.close();
}
}
private List<IdDt> toIdListUnqualifiedVersionless(Bundle found) { private List<IdDt> toIdListUnqualifiedVersionless(Bundle found) {
List<IdDt> list = new ArrayList<IdDt>(); List<IdDt> list = new ArrayList<IdDt>();
for (BundleEntry next : found.getEntries()) { for (BundleEntry next : found.getEntries()) {

View File

@ -19,6 +19,13 @@
<description value="an enumeration of codes defined by LOINC" /> <description value="an enumeration of codes defined by LOINC" />
<status value="draft" /> <status value="draft" />
<experimental value="true" /> <experimental value="true" />
<define>
<system value="http://loinc.org" />
<concept>
<code value="8450-9" />
<display value="Systolic blood pressure--expiration" />
</concept>
</define>
<compose> <compose>
<include> <include>
<system value="http://loinc.org" /> <system value="http://loinc.org" />
@ -38,10 +45,6 @@
<code value="8495-4" /> <code value="8495-4" />
<display value="Systolic blood pressure 24 hour minimum" /> <display value="Systolic blood pressure 24 hour minimum" />
</concept> </concept>
<concept>
<code value="8450-9" />
<display value="Systolic blood pressure--expiration" />
</concept>
<concept> <concept>
<code value="8451-7" /> <code value="8451-7" />
<display value="Systolic blood pressure--inspiration" /> <display value="Systolic blood pressure--inspiration" />

View File

@ -84,10 +84,10 @@ public class ResourceValidatorTest {
assertEquals("2001-03-31", new SimpleDateFormat("yyyy-MM-dd").format(p.getBirthDate().getValue())); assertEquals("2001-03-31", new SimpleDateFormat("yyyy-MM-dd").format(p.getBirthDate().getValue()));
ValidationResult result = ourCtx.newValidator().validateWithResult(p); ValidationResult result = ourCtx.newValidator().validateWithResult(p);
String resultString = ourCtx.newXmlParser().setPrettyPrint(true).encodeResourceToString(result.getOperationOutcome()); String resultString = ourCtx.newXmlParser().setPrettyPrint(true).encodeResourceToString(result.toOperationOutcome());
ourLog.info(resultString); ourLog.info(resultString);
assertEquals(2, ((OperationOutcome)result.getOperationOutcome()).getIssue().size()); assertEquals(2, ((OperationOutcome)result.toOperationOutcome()).getIssue().size());
assertThat(resultString, StringContains.containsString("cvc-datatype-valid.1.2.3")); assertThat(resultString, StringContains.containsString("cvc-datatype-valid.1.2.3"));
} }
@ -129,7 +129,7 @@ public class ResourceValidatorTest {
p.getTelecomFirstRep().setValue("123-4567"); p.getTelecomFirstRep().setValue("123-4567");
validationResult = val.validateWithResult(p); validationResult = val.validateWithResult(p);
assertFalse(validationResult.isSuccessful()); assertFalse(validationResult.isSuccessful());
OperationOutcome operationOutcome = (OperationOutcome) validationResult.getOperationOutcome(); OperationOutcome operationOutcome = (OperationOutcome) validationResult.toOperationOutcome();
ourLog.info(ourCtx.newXmlParser().setPrettyPrint(true).encodeResourceToString(operationOutcome)); ourLog.info(ourCtx.newXmlParser().setPrettyPrint(true).encodeResourceToString(operationOutcome));
assertEquals(1, operationOutcome.getIssue().size()); assertEquals(1, operationOutcome.getIssue().size());
assertThat(operationOutcome.getIssueFirstRep().getDetails().getValue(), containsString("Inv-2:")); assertThat(operationOutcome.getIssueFirstRep().getDetails().getValue(), containsString("Inv-2:"));
@ -147,7 +147,7 @@ public class ResourceValidatorTest {
FhirValidator val = createFhirValidator(); FhirValidator val = createFhirValidator();
ValidationResult result = val.validateWithResult(b); ValidationResult result = val.validateWithResult(b);
OperationOutcome operationOutcome = (OperationOutcome) result.getOperationOutcome(); OperationOutcome operationOutcome = (OperationOutcome) result.toOperationOutcome();
ourLog.info(ourCtx.newXmlParser().setPrettyPrint(true).encodeResourceToString(operationOutcome)); ourLog.info(ourCtx.newXmlParser().setPrettyPrint(true).encodeResourceToString(operationOutcome));
@ -170,7 +170,7 @@ public class ResourceValidatorTest {
p.getTelecomFirstRep().setValue("123-4567"); p.getTelecomFirstRep().setValue("123-4567");
validationResult = val.validateWithResult(b); validationResult = val.validateWithResult(b);
assertFalse(validationResult.isSuccessful()); assertFalse(validationResult.isSuccessful());
OperationOutcome operationOutcome = (OperationOutcome) validationResult.getOperationOutcome(); OperationOutcome operationOutcome = (OperationOutcome) validationResult.toOperationOutcome();
ourLog.info(ourCtx.newXmlParser().setPrettyPrint(true).encodeResourceToString(operationOutcome)); ourLog.info(ourCtx.newXmlParser().setPrettyPrint(true).encodeResourceToString(operationOutcome));
assertEquals(1, operationOutcome.getIssue().size()); assertEquals(1, operationOutcome.getIssue().size());
assertThat(operationOutcome.getIssueFirstRep().getDetails().getValue(), containsString("Inv-2:")); assertThat(operationOutcome.getIssueFirstRep().getDetails().getValue(), containsString("Inv-2:"));

View File

@ -1,64 +0,0 @@
package ca.uhn.fhir.validation;
import ca.uhn.fhir.context.FhirContext;
import ca.uhn.fhir.model.base.resource.BaseOperationOutcome.BaseIssue;
import ca.uhn.fhir.model.dstu.resource.OperationOutcome;
import org.junit.Test;
import java.util.List;
import java.util.UUID;
import static org.hamcrest.Matchers.containsString;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertThat;
import static org.junit.Assert.assertTrue;
public class ValidationResultDstu1Test {
private static final FhirContext ourCtx = FhirContext.forDstu1();
@Test
public void isSuccessful_FalseForIssues() {
OperationOutcome operationOutcome = new OperationOutcome();
OperationOutcome.Issue issue = operationOutcome.addIssue();
String errorMessage = "There was a validation problem";
issue.setDetails(errorMessage);
ValidationResult result = ValidationResult.valueOf(ourCtx, operationOutcome);
assertFalse(result.isSuccessful());
List<? extends BaseIssue> issues = ((OperationOutcome) result.getOperationOutcome()).getIssue();
assertEquals(1, issues.size());
assertEquals(errorMessage, issues.get(0).getDetailsElement().getValue());
assertThat("ValidationResult#toString should contain the issue description", result.toString(), containsString(errorMessage));
}
@Test
public void isSuccessful_IsTrueForNoIssues() {
OperationOutcome operationOutcome = new OperationOutcome();
// make sure a non-null ID doesn't cause the validation result to be a fail
operationOutcome.setId(UUID.randomUUID().toString());
ValidationResult result = ValidationResult.valueOf(ourCtx, operationOutcome);
assertTrue(result.isSuccessful());
}
@Test
public void isSuccessful_IsTrueForNullOperationOutcome() {
ValidationResult result = ValidationResult.valueOf(ourCtx, null);
assertTrue(result.isSuccessful());
}
/*
* Test for https://github.com/jamesagnew/hapi-fhir/issues/51
*/
@Test
public void toString_ShouldNotCauseResultToBecomeFailure() {
OperationOutcome operationOutcome = new OperationOutcome();
ValidationResult result = ValidationResult.valueOf(ourCtx, operationOutcome);
assertEquals(true, result.isSuccessful());
// need to call toString to make sure any unwanted side effects are generated
result.toString();
assertEquals(true, result.isSuccessful());
}
}

View File

@ -34,6 +34,7 @@ import ca.uhn.fhir.model.api.IResource;
import ca.uhn.fhir.model.dstu2.resource.Bundle; import ca.uhn.fhir.model.dstu2.resource.Bundle;
import ca.uhn.fhir.model.dstu2.resource.Parameters; import ca.uhn.fhir.model.dstu2.resource.Parameters;
import ca.uhn.fhir.model.dstu2.resource.Patient; import ca.uhn.fhir.model.dstu2.resource.Patient;
import ca.uhn.fhir.model.primitive.DateTimeDt;
import ca.uhn.fhir.model.primitive.IdDt; import ca.uhn.fhir.model.primitive.IdDt;
import ca.uhn.fhir.model.primitive.IntegerDt; import ca.uhn.fhir.model.primitive.IntegerDt;
import ca.uhn.fhir.model.primitive.StringDt; import ca.uhn.fhir.model.primitive.StringDt;
@ -68,6 +69,14 @@ public class OperationServerTest {
ourLastMethod = ""; ourLastMethod = "";
} }
public static void main(String[] theValue) {
Parameters p = new Parameters();
p.addParameter().setName("start").setValue(new DateTimeDt("2001-01-02"));
p.addParameter().setName("end").setValue(new DateTimeDt("2015-07-10"));
String inParamsStr = FhirContext.forDstu2().newXmlParser().encodeResourceToString(p);
ourLog.info(inParamsStr.replace("\"", "\\\""));
}
@Test @Test
public void testOperationOnType() throws Exception { public void testOperationOnType() throws Exception {
Parameters p = new Parameters(); Parameters p = new Parameters();

View File

@ -1,64 +0,0 @@
package ca.uhn.fhir.validation;
import static org.hamcrest.Matchers.containsString;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertThat;
import static org.junit.Assert.assertTrue;
import java.util.List;
import java.util.UUID;
import org.junit.Test;
import ca.uhn.fhir.context.FhirContext;
import ca.uhn.fhir.model.base.resource.BaseOperationOutcome.BaseIssue;
import ca.uhn.fhir.model.dstu2.resource.OperationOutcome;
public class ValidationResultDstu2Test {
private static final FhirContext ourCtx = FhirContext.forDstu2();
@Test
public void isSuccessful_IsTrueForNullOperationOutcome() {
ValidationResult result = ValidationResult.valueOf(ourCtx, null);
assertTrue(result.isSuccessful());
}
@Test
public void isSuccessful_IsTrueForNoIssues() {
OperationOutcome operationOutcome = new OperationOutcome();
// make sure a non-null ID doesn't cause the validation result to be a fail
operationOutcome.setId(UUID.randomUUID().toString());
ValidationResult result = ValidationResult.valueOf(ourCtx, operationOutcome);
assertTrue(result.isSuccessful());
}
@Test
public void isSuccessful_FalseForIssues() {
OperationOutcome operationOutcome = new OperationOutcome();
OperationOutcome.Issue issue = operationOutcome.addIssue();
String errorMessage = "There was a validation problem";
issue.setDetails(errorMessage);
ValidationResult result = ValidationResult.valueOf(ourCtx, operationOutcome);
assertFalse(result.isSuccessful());
List<? extends BaseIssue> issues = ((OperationOutcome)result.getOperationOutcome()).getIssue();
assertEquals(1, issues.size());
assertEquals(errorMessage, issues.get(0).getDetailsElement().getValue());
assertThat("ValidationResult#toString should contain the issue description", result.toString(), containsString(errorMessage));
}
/*
Test for https://github.com/jamesagnew/hapi-fhir/issues/51
*/
@Test
public void toString_ShouldNotCauseResultToBecomeFailure() {
OperationOutcome operationOutcome = new OperationOutcome();
ValidationResult result = ValidationResult.valueOf(ourCtx, operationOutcome);
assertEquals(true, result.isSuccessful());
// need to call toString to make sure any unwanted side effects are generated
@SuppressWarnings("UnusedDeclaration") String unused = result.toString();
assertEquals(true, result.isSuccessful());
}
}

View File

@ -23,24 +23,22 @@ import com.google.gson.JsonObject;
import ca.uhn.fhir.context.ConfigurationException; import ca.uhn.fhir.context.ConfigurationException;
import ca.uhn.fhir.context.FhirContext; import ca.uhn.fhir.context.FhirContext;
import ca.uhn.fhir.model.api.Bundle;
import ca.uhn.fhir.rest.server.EncodingEnum; import ca.uhn.fhir.rest.server.EncodingEnum;
import ca.uhn.fhir.rest.server.exceptions.InternalErrorException; import ca.uhn.fhir.rest.server.exceptions.InternalErrorException;
public class StructureDefinitionValidator { public class FhirInstanceValidator implements IValidator {
private static final org.slf4j.Logger ourLog = org.slf4j.LoggerFactory.getLogger(StructureDefinitionValidator.class); private static final org.slf4j.Logger ourLog = org.slf4j.LoggerFactory.getLogger(FhirInstanceValidator.class);
private FhirContext myCtx;
private DocumentBuilderFactory myDocBuilderFactory; private DocumentBuilderFactory myDocBuilderFactory;
StructureDefinitionValidator(FhirContext theContext) { FhirInstanceValidator() {
myCtx = theContext;
myDocBuilderFactory = DocumentBuilderFactory.newInstance(); myDocBuilderFactory = DocumentBuilderFactory.newInstance();
myDocBuilderFactory.setNamespaceAware(true); myDocBuilderFactory.setNamespaceAware(true);
} }
public List<ValidationMessage> validate(String theInput, EncodingEnum theEncoding, Class<? extends IBaseResource> theResourceType) { List<ValidationMessage> validate(FhirContext theCtx, String theInput, EncodingEnum theEncoding, String theResourceName) {
WorkerContext workerContext = new WorkerContext(); WorkerContext workerContext = new WorkerContext();
org.hl7.fhir.instance.validation.InstanceValidator v; org.hl7.fhir.instance.validation.InstanceValidator v;
try { try {
@ -49,14 +47,14 @@ public class StructureDefinitionValidator {
throw new ConfigurationException(e); throw new ConfigurationException(e);
} }
String profileCpName = "/org/hl7/fhir/instance/model/profile/" + myCtx.getResourceDefinition(theResourceType).getName().toLowerCase() + ".profile.xml"; String profileCpName = "/org/hl7/fhir/instance/model/profile/" + theResourceName.toLowerCase() + ".profile.xml";
String profileText; String profileText;
try { try {
profileText = IOUtils.toString(StructureDefinitionValidator.class.getResourceAsStream(profileCpName), "UTF-8"); profileText = IOUtils.toString(FhirInstanceValidator.class.getResourceAsStream(profileCpName), "UTF-8");
} catch (IOException e1) { } catch (IOException e1) {
throw new ConfigurationException("Failed to load profile from classpath: " + profileCpName, e1); throw new ConfigurationException("Failed to load profile from classpath: " + profileCpName, e1);
} }
StructureDefinition profile = myCtx.newXmlParser().parseResource(StructureDefinition.class, profileText); StructureDefinition profile = theCtx.newXmlParser().parseResource(StructureDefinition.class, profileText);
if (theEncoding == EncodingEnum.XML) { if (theEncoding == EncodingEnum.XML) {
Document document; Document document;
@ -91,4 +89,16 @@ public class StructureDefinitionValidator {
} }
@Override
public void validateResource(IValidationContext<IBaseResource> theCtx) {
String resourceName = theCtx.getResourceName();
String resourceBody = theCtx.getResourceAsString();
validate(theCtx.getFhirContext(), resourceBody, theCtx.getResourceAsStringEncoding(), resourceName);
}
@Override
public void validateBundle(IValidationContext<Bundle> theContext) {
}
} }

View File

@ -624,7 +624,7 @@ public class ValueSet extends DomainResource {
/** /**
* If this code is not for use as a real concept. * If this code is not for use as a real concept.
*/ */
@Child(name = "abstract_", type = {BooleanType.class}, order=2, min=0, max=1) @Child(name = "abstract", type = {BooleanType.class}, order=2, min=0, max=1)
@Description(shortDefinition="If this code is not for use as a real concept", formalDefinition="If this code is not for use as a real concept." ) @Description(shortDefinition="If this code is not for use as a real concept", formalDefinition="If this code is not for use as a real concept." )
protected BooleanType abstract_; protected BooleanType abstract_;
@ -1210,7 +1210,7 @@ public class ValueSet extends DomainResource {
/** /**
* Includes the contents of the referenced value set as a part of the contents of this value set. This is an absolute URI that is a reference to ValueSet.uri. * Includes the contents of the referenced value set as a part of the contents of this value set. This is an absolute URI that is a reference to ValueSet.uri.
*/ */
@Child(name = "import_", type = {UriType.class}, order=1, min=0, max=Child.MAX_UNLIMITED) @Child(name = "import", type = {UriType.class}, order=1, min=0, max=Child.MAX_UNLIMITED)
@Description(shortDefinition="Import the contents of another value set", formalDefinition="Includes the contents of the referenced value set as a part of the contents of this value set. This is an absolute URI that is a reference to ValueSet.uri." ) @Description(shortDefinition="Import the contents of another value set", formalDefinition="Includes the contents of the referenced value set as a part of the contents of this value set. This is an absolute URI that is a reference to ValueSet.uri." )
protected List<UriType> import_; protected List<UriType> import_;
@ -2757,7 +2757,7 @@ public class ValueSet extends DomainResource {
/** /**
* If true, this entry is included in the expansion for navigational purposes, and the user cannot select the code directly as a proper value. * If true, this entry is included in the expansion for navigational purposes, and the user cannot select the code directly as a proper value.
*/ */
@Child(name = "abstract_", type = {BooleanType.class}, order=2, min=0, max=1) @Child(name = "abstract", type = {BooleanType.class}, order=2, min=0, max=1)
@Description(shortDefinition="If user cannot select this entry", formalDefinition="If true, this entry is included in the expansion for navigational purposes, and the user cannot select the code directly as a proper value." ) @Description(shortDefinition="If user cannot select this entry", formalDefinition="If true, this entry is included in the expansion for navigational purposes, and the user cannot select the code directly as a proper value." )
protected BooleanType abstract_; protected BooleanType abstract_;

View File

@ -17,6 +17,7 @@ import org.hl7.fhir.instance.model.Conformance;
import org.hl7.fhir.instance.model.ElementDefinition.TypeRefComponent; import org.hl7.fhir.instance.model.ElementDefinition.TypeRefComponent;
import org.hl7.fhir.instance.model.OperationOutcome; import org.hl7.fhir.instance.model.OperationOutcome;
import org.hl7.fhir.instance.model.Parameters; import org.hl7.fhir.instance.model.Parameters;
import org.hl7.fhir.instance.model.Questionnaire;
import org.hl7.fhir.instance.model.Resource; import org.hl7.fhir.instance.model.Resource;
import org.hl7.fhir.instance.model.StructureDefinition; import org.hl7.fhir.instance.model.StructureDefinition;
import org.hl7.fhir.instance.model.ValueSet; import org.hl7.fhir.instance.model.ValueSet;
@ -57,14 +58,13 @@ public class WorkerContext implements NameResolver {
private Map<String, StructureDefinition> extensionDefinitions = new HashMap<String, StructureDefinition>(); private Map<String, StructureDefinition> extensionDefinitions = new HashMap<String, StructureDefinition>();
private String version; private String version;
private List<String> resourceNames = new ArrayList<String>(); private List<String> resourceNames = new ArrayList<String>();
private Map<String, Questionnaire> questionnaires = new HashMap<String, Questionnaire>();
public WorkerContext() { public WorkerContext() {
super(); super();
} }
public WorkerContext(ITerminologyServices conceptLocator, IFHIRClient client, Map<String, ValueSet> codeSystems, public WorkerContext(ITerminologyServices conceptLocator, IFHIRClient client, Map<String, ValueSet> codeSystems, Map<String, ValueSet> valueSets, Map<String, ConceptMap> maps, Map<String, StructureDefinition> profiles) {
Map<String, ValueSet> valueSets, Map<String, ConceptMap> maps, Map<String, StructureDefinition> profiles) {
super(); super();
if (conceptLocator != null) if (conceptLocator != null)
this.terminologyServices = conceptLocator; this.terminologyServices = conceptLocator;
@ -87,6 +87,7 @@ public class WorkerContext implements NameResolver {
public boolean hasClient() { public boolean hasClient() {
return !(client == null || client instanceof NullClient); return !(client == null || client instanceof NullClient);
} }
public IFHIRClient getClient() { public IFHIRClient getClient() {
return client; return client;
} }
@ -111,6 +112,10 @@ public class WorkerContext implements NameResolver {
return extensionDefinitions; return extensionDefinitions;
} }
public Map<String, Questionnaire> getQuestionnaires() {
return questionnaires;
}
public WorkerContext setTerminologyServices(ITerminologyServices terminologyServices) { public WorkerContext setTerminologyServices(ITerminologyServices terminologyServices) {
this.terminologyServices = terminologyServices; this.terminologyServices = terminologyServices;
return this; return this;
@ -124,9 +129,6 @@ public class WorkerContext implements NameResolver {
return res; return res;
} }
public void seeExtensionDefinition(String base, StructureDefinition ed) throws Exception { public void seeExtensionDefinition(String base, StructureDefinition ed) throws Exception {
if (extensionDefinitions.get(ed.getUrl()) != null) if (extensionDefinitions.get(ed.getUrl()) != null)
throw new Exception("duplicate extension definition: " + ed.getUrl()); throw new Exception("duplicate extension definition: " + ed.getUrl());
@ -135,6 +137,11 @@ public class WorkerContext implements NameResolver {
extensionDefinitions.put(ed.getUrl(), ed); extensionDefinitions.put(ed.getUrl(), ed);
} }
public void seeQuestionnaire(String base, Questionnaire theQuestionnaire) throws Exception {
questionnaires.put(theQuestionnaire.getId(), theQuestionnaire);
questionnaires.put(base + "/Questionnaire/" + theQuestionnaire.getId(), theQuestionnaire);
}
public void seeValueSet(String base, ValueSet vs) { public void seeValueSet(String base, ValueSet vs) {
valueSets.put(vs.getId(), vs); valueSets.put(vs.getId(), vs);
valueSets.put(base + "/ValueSet/" + vs.getId(), vs); valueSets.put(base + "/ValueSet/" + vs.getId(), vs);
@ -408,5 +415,3 @@ public class WorkerContext implements NameResolver {
} }
} }

View File

@ -29,8 +29,10 @@ POSSIBILITY OF SUCH DAMAGE.
*/ */
import java.text.MessageFormat;
import java.util.List; import java.util.List;
import org.apache.commons.lang3.StringUtils;
import org.hl7.fhir.instance.model.OperationOutcome.IssueSeverity; import org.hl7.fhir.instance.model.OperationOutcome.IssueSeverity;
import org.hl7.fhir.instance.model.valuesets.IssueType; import org.hl7.fhir.instance.model.valuesets.IssueType;
import org.hl7.fhir.instance.validation.ValidationMessage.Source; import org.hl7.fhir.instance.validation.ValidationMessage.Source;
@ -39,73 +41,180 @@ public class BaseValidator {
protected Source source; protected Source source;
protected boolean fail(List<ValidationMessage> errors, IssueType type, int line, int col, String path, boolean b, String msg) { /**
if (!b) * Test a rule and add a {@link IssueSeverity#FATAL} validation message if the validation fails
*
* @param thePass
* Set this parameter to <code>false</code> if the validation does not pass
* @return Returns <code>thePass</code> (in other words, returns <code>true</code> if the rule did not fail validation)
*/
protected boolean fail(List<ValidationMessage> errors, IssueType type, int line, int col, String path, boolean thePass, String msg) {
if (!thePass) {
errors.add(new ValidationMessage(source, type, line, col, path, msg, IssueSeverity.FATAL)); errors.add(new ValidationMessage(source, type, line, col, path, msg, IssueSeverity.FATAL));
return b; }
return thePass;
} }
/**
protected boolean rule(List<ValidationMessage> errors, IssueType type, int line, int col, String path, boolean b, String msg) { * Test a rule and add a {@link IssueSeverity#FATAL} validation message if the validation fails
if (!b) *
errors.add(new ValidationMessage(source, type, line, col, path, msg, IssueSeverity.ERROR)); * @param thePass
return b; * Set this parameter to <code>false</code> if the validation does not pass
} * @return Returns <code>thePass</code> (in other words, returns <code>true</code> if the rule did not fail validation)
*/
protected boolean hint(List<ValidationMessage> errors, IssueType type, int line, int col, String path, boolean b, String msg) { protected boolean fail(List<ValidationMessage> errors, IssueType type, List<String> pathParts, boolean thePass, String msg) {
if (!b) if (!thePass) {
errors.add(new ValidationMessage(source, type, line, col, path, msg, IssueSeverity.INFORMATION)); String path = StringUtils.join(pathParts, '.');
return b;
}
protected boolean warning(List<ValidationMessage> errors, IssueType type, int line, int col, String path, boolean b, String msg) {
if (!b)
errors.add(new ValidationMessage(source, type, line, col, path, msg, IssueSeverity.WARNING));
return b;
}
protected boolean fail(List<ValidationMessage> errors, IssueType type, String path, boolean b, String msg) {
if (!b)
errors.add(new ValidationMessage(source, type, -1, -1, path, msg, IssueSeverity.FATAL)); errors.add(new ValidationMessage(source, type, -1, -1, path, msg, IssueSeverity.FATAL));
return b; }
return thePass;
}
/**
* Test a rule and add a {@link IssueSeverity#FATAL} validation message if the validation fails
*
* @param thePass
* Set this parameter to <code>false</code> if the validation does not pass
* @return Returns <code>thePass</code> (in other words, returns <code>true</code> if the rule did not fail validation)
*/
protected boolean fail(List<ValidationMessage> errors, IssueType type, List<String> pathParts, boolean thePass, String theMessage, Object... theMessageArguments) {
if (!thePass) {
String path = StringUtils.join(pathParts, '.');
errors.add(new ValidationMessage(source, type, -1, -1, path, formatMessage(theMessage, theMessageArguments), IssueSeverity.FATAL));
}
return thePass;
}
/**
* Test a rule and add a {@link IssueSeverity#FATAL} validation message if the validation fails
*
* @param thePass
* Set this parameter to <code>false</code> if the validation does not pass
* @return Returns <code>thePass</code> (in other words, returns <code>true</code> if the rule did not fail validation)
*/
protected boolean fail(List<ValidationMessage> errors, IssueType type, String path, boolean thePass, String msg) {
if (!thePass) {
errors.add(new ValidationMessage(source, type, -1, -1, path, msg, IssueSeverity.FATAL));
}
return thePass;
} }
protected boolean rule(List<ValidationMessage> errors, IssueType type, String path, boolean b, String msg) { private String formatMessage(String theMessage, Object... theMessageArguments) {
if (!b) String message;
errors.add(new ValidationMessage(source, type, -1, -1, path, msg, IssueSeverity.ERROR)); if (theMessageArguments != null && theMessageArguments.length > 0) {
return b; message = MessageFormat.format(theMessage, theMessageArguments);
} else {
message = theMessage;
} }
return message;
protected boolean rule(List<ValidationMessage> errors, IssueType type, String path, boolean b, String msg, String html) {
if (!b)
errors.add(new ValidationMessage(source, type, -1, -1, path, msg, html, IssueSeverity.ERROR));
return b;
}
protected boolean hint(List<ValidationMessage> errors, IssueType type, String path, boolean b, String msg) {
if (!b)
errors.add(new ValidationMessage(source, type, -1, -1, path, msg, IssueSeverity.INFORMATION));
return b;
}
protected boolean warning(List<ValidationMessage> errors, IssueType type, String path, boolean b, String msg) {
if (!b)
errors.add(new ValidationMessage(source, type, -1, -1, path, msg, IssueSeverity.WARNING));
return b;
}
protected boolean warning(List<ValidationMessage> errors, IssueType type, String path, boolean b, String msg, String html) {
if (!b)
errors.add(new ValidationMessage(source, type, -1, -1, path, msg, html, IssueSeverity.WARNING));
return b;
} }
protected boolean grammarWord(String w) { protected boolean grammarWord(String w) {
return w.equals("and") || w.equals("or") || w.equals("a") || w.equals("the") || w.equals("for") || w.equals("this") || w.equals("that") || w.equals("of"); return w.equals("and") || w.equals("or") || w.equals("a") || w.equals("the") || w.equals("for") || w.equals("this") || w.equals("that") || w.equals("of");
} }
/**
* Test a rule and add a {@link IssueSeverity#INFORMATION} validation message if the validation fails
*
* @param thePass
* Set this parameter to <code>false</code> if the validation does not pass
* @return Returns <code>thePass</code> (in other words, returns <code>true</code> if the rule did not fail validation)
*/
protected boolean hint(List<ValidationMessage> errors, IssueType type, int line, int col, String path, boolean thePass, String msg) {
if (!thePass) {
errors.add(new ValidationMessage(source, type, line, col, path, msg, IssueSeverity.INFORMATION));
}
return thePass;
}
/**
* Test a rule and add a {@link IssueSeverity#INFORMATION} validation message if the validation fails
*
* @param thePass
* Set this parameter to <code>false</code> if the validation does not pass
* @return Returns <code>thePass</code> (in other words, returns <code>true</code> if the rule did not fail validation)
*/
protected boolean hint(List<ValidationMessage> errors, IssueType type, String path, boolean thePass, String msg) {
if (!thePass) {
errors.add(new ValidationMessage(source, type, -1, -1, path, msg, IssueSeverity.INFORMATION));
}
return thePass;
}
/**
* Test a rule and add a {@link IssueSeverity#ERROR} validation message if the validation fails
*
* @param thePass
* Set this parameter to <code>false</code> if the validation does not pass
* @return Returns <code>thePass</code> (in other words, returns <code>true</code> if the rule did not fail validation)
*/
protected boolean rule(List<ValidationMessage> errors, IssueType type, int line, int col, String path, boolean thePass, String msg) {
if (!thePass) {
errors.add(new ValidationMessage(source, type, line, col, path, msg, IssueSeverity.ERROR));
}
return thePass;
}
/**
* Test a rule and add a {@link IssueSeverity#ERROR} validation message if the validation fails
*
* @param thePass
* Set this parameter to <code>false</code> if the validation does not pass
* @return Returns <code>thePass</code> (in other words, returns <code>true</code> if the rule did not fail validation)
*/
protected boolean rule(List<ValidationMessage> errors, IssueType type, List<String> pathParts, boolean thePass, String msg) {
if (!thePass) {
String path = StringUtils.join(pathParts, '.');
errors.add(new ValidationMessage(source, type, -1, -1, path, msg, IssueSeverity.ERROR));
}
return thePass;
}
/**
* Test a rule and add a {@link IssueSeverity#ERROR} validation message if the validation fails
*
* @param thePass
* Set this parameter to <code>false</code> if the validation does not pass
* @return Returns <code>thePass</code> (in other words, returns <code>true</code> if the rule did not fail validation)
*/
protected boolean rule(List<ValidationMessage> errors, IssueType type, List<String> pathParts, boolean thePass, String theMessage, Object... theMessageArguments) {
if (!thePass) {
String path = StringUtils.join(pathParts, '.');
String message = formatMessage(theMessage, theMessageArguments);
errors.add(new ValidationMessage(source, type, -1, -1, path, message, IssueSeverity.ERROR));
}
return thePass;
}
/**
* Test a rule and add a {@link IssueSeverity#ERROR} validation message if the validation fails
*
* @param thePass
* Set this parameter to <code>false</code> if the validation does not pass
* @return Returns <code>thePass</code> (in other words, returns <code>true</code> if the rule did not fail validation)
*/
protected boolean rule(List<ValidationMessage> errors, IssueType type, String path, boolean thePass, String msg) {
if (!thePass) {
errors.add(new ValidationMessage(source, type, -1, -1, path, msg, IssueSeverity.ERROR));
}
return thePass;
}
/**
* Test a rule and add a {@link IssueSeverity#ERROR} validation message if the validation fails
*
* @param thePass
* Set this parameter to <code>false</code> if the validation does not pass
* @return Returns <code>thePass</code> (in other words, returns <code>true</code> if the rule did not fail validation)
*/
protected boolean rule(List<ValidationMessage> errors, IssueType type, String path, boolean thePass, String msg, String html) {
if (!thePass) {
errors.add(new ValidationMessage(source, type, -1, -1, path, msg, html, IssueSeverity.ERROR));
}
return thePass;
}
protected String splitByCamelCase(String s) { protected String splitByCamelCase(String s) {
StringBuilder b = new StringBuilder(); StringBuilder b = new StringBuilder();
for (int i = 0; i < s.length(); i++) { for (int i = 0; i < s.length(); i++) {
@ -127,5 +236,47 @@ public class BaseValidator {
return b.toString(); return b.toString();
} }
/**
* Test a rule and add a {@link IssueSeverity#WARNING} validation message if the validation fails
*
* @param thePass
* Set this parameter to <code>false</code> if the validation does not pass
* @return Returns <code>thePass</code> (in other words, returns <code>true</code> if the rule did not fail validation)
*/
protected boolean warning(List<ValidationMessage> errors, IssueType type, int line, int col, String path, boolean thePass, String msg) {
if (!thePass) {
errors.add(new ValidationMessage(source, type, line, col, path, msg, IssueSeverity.WARNING));
}
return thePass;
}
/**
* Test a rule and add a {@link IssueSeverity#WARNING} validation message if the validation fails
*
* @param thePass
* Set this parameter to <code>false</code> if the validation does not pass
* @return Returns <code>thePass</code> (in other words, returns <code>true</code> if the rule did not fail validation)
*/
protected boolean warning(List<ValidationMessage> errors, IssueType type, String path, boolean thePass, String msg) {
if (!thePass) {
errors.add(new ValidationMessage(source, type, -1, -1, path, msg, IssueSeverity.WARNING));
}
return thePass;
}
/**
* Test a rule and add a {@link IssueSeverity#WARNING} validation message if the validation fails
*
* @param thePass
* Set this parameter to <code>false</code> if the validation does not pass
* @return Returns <code>thePass</code> (in other words, returns <code>true</code> if the rule did not fail validation)
*/
protected boolean warning(List<ValidationMessage> errors, IssueType type, String path, boolean thePass, String msg, String html) {
if (!thePass) {
errors.add(new ValidationMessage(source, type, -1, -1, path, msg, html, IssueSeverity.WARNING));
}
return thePass;
}
} }

View File

@ -842,7 +842,7 @@ public class InstanceValidator extends BaseValidator implements IResourceValidat
// the instance validator had no issues against the base resource profile // the instance validator had no issues against the base resource profile
private void start(List<ValidationMessage> errors, WrapperElement element, StructureDefinition profile, NodeStack stack) throws Exception { private void start(List<ValidationMessage> errors, WrapperElement element, StructureDefinition profile, NodeStack stack) throws Exception {
// profile is valid, and matches the resource name // profile is valid, and matches the resource name
if (rule(errors, IssueType.STRUCTURE, element.line(), element.col(), stack.getLiteralPath(), profile.hasSnapshot(), "StructureDefinition has no snapshort - validation is against the snapshot, so it must be provided")) { if (rule(errors, IssueType.STRUCTURE, element.line(), element.col(), stack.getLiteralPath(), profile.hasSnapshot(), "StructureDefinition has no snapshot - validation is against the snapshot, so it must be provided")) {
validateElement(errors, profile, profile.getSnapshot().getElement().get(0), null, null, element, element.getName(), stack); validateElement(errors, profile, profile.getSnapshot().getElement().get(0), null, null, element, element.getName(), stack);
checkDeclaredProfiles(errors, element, stack); checkDeclaredProfiles(errors, element, stack);
@ -878,7 +878,7 @@ public class InstanceValidator extends BaseValidator implements IResourceValidat
if (rule(errors, IssueType.INVALID, element.line(), element.col(), p, !Utilities.noString(ref), "StructureDefinition reference invalid")) { if (rule(errors, IssueType.INVALID, element.line(), element.col(), p, !Utilities.noString(ref), "StructureDefinition reference invalid")) {
StructureDefinition pr = context.getProfiles().get(ref); StructureDefinition pr = context.getProfiles().get(ref);
if (warning(errors, IssueType.INVALID, element.line(), element.col(), p, pr != null, "StructureDefinition reference could not be resolved")) { if (warning(errors, IssueType.INVALID, element.line(), element.col(), p, pr != null, "StructureDefinition reference could not be resolved")) {
if (rule(errors, IssueType.STRUCTURE, element.line(), element.col(), p, pr.hasSnapshot(), "StructureDefinition has no snapshort - validation is against the snapshot, so it must be provided")) { if (rule(errors, IssueType.STRUCTURE, element.line(), element.col(), p, pr.hasSnapshot(), "StructureDefinition has no snapshot - validation is against the snapshot, so it must be provided")) {
validateElement(errors, pr, pr.getSnapshot().getElement().get(0), null, null, element, element.getName(), stack); validateElement(errors, pr, pr.getSnapshot().getElement().get(0), null, null, element, element.getName(), stack);
} }
} }

View File

@ -0,0 +1,389 @@
package org.hl7.fhir.instance.validation;
import static org.apache.commons.lang3.StringUtils.isBlank;
import static org.apache.commons.lang3.StringUtils.isNotBlank;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashSet;
import java.util.LinkedList;
import java.util.List;
import java.util.Set;
import org.apache.commons.lang3.Validate;
import org.hl7.fhir.instance.model.Attachment;
import org.hl7.fhir.instance.model.BooleanType;
import org.hl7.fhir.instance.model.Coding;
import org.hl7.fhir.instance.model.DateTimeType;
import org.hl7.fhir.instance.model.DateType;
import org.hl7.fhir.instance.model.DecimalType;
import org.hl7.fhir.instance.model.InstantType;
import org.hl7.fhir.instance.model.IntegerType;
import org.hl7.fhir.instance.model.Quantity;
import org.hl7.fhir.instance.model.Questionnaire;
import org.hl7.fhir.instance.model.Questionnaire.AnswerFormat;
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.Reference;
import org.hl7.fhir.instance.model.Resource;
import org.hl7.fhir.instance.model.StringType;
import org.hl7.fhir.instance.model.TimeType;
import org.hl7.fhir.instance.model.Type;
import org.hl7.fhir.instance.model.UriType;
import org.hl7.fhir.instance.model.ValueSet;
import org.hl7.fhir.instance.model.ValueSet.ConceptDefinitionComponent;
import org.hl7.fhir.instance.model.valuesets.IssueType;
import org.hl7.fhir.instance.utils.WorkerContext;
/**
* Validates that an instance of {@link QuestionnaireAnswers} is valid against the {@link Questionnaire} that it claims to conform to.
*
* @author James Agnew
*/
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;
public QuestionnaireAnswersValidator(WorkerContext theWorkerCtx) {
this.myWorkerCtx = theWorkerCtx;
}
private Set<Class<? extends Type>> allowedTypes(Class<? extends Type> theClass0) {
HashSet<Class<? extends Type>> retVal = new HashSet<Class<? extends Type>>();
retVal.add(theClass0);
return Collections.unmodifiableSet(retVal);
}
private List<org.hl7.fhir.instance.model.QuestionnaireAnswers.QuestionComponent> findAnswersByLinkId(List<org.hl7.fhir.instance.model.QuestionnaireAnswers.QuestionComponent> theQuestion,
String theLinkId) {
Validate.notBlank(theLinkId, "theLinkId must not be blank");
ArrayList<org.hl7.fhir.instance.model.QuestionnaireAnswers.QuestionComponent> retVal = new ArrayList<QuestionnaireAnswers.QuestionComponent>();
for (org.hl7.fhir.instance.model.QuestionnaireAnswers.QuestionComponent next : theQuestion) {
if (theLinkId.equals(next.getLinkId())) {
retVal.add(next);
}
}
return retVal;
}
private List<org.hl7.fhir.instance.model.QuestionnaireAnswers.GroupComponent> findGroupByLinkId(List<org.hl7.fhir.instance.model.QuestionnaireAnswers.GroupComponent> theGroups, String theLinkId) {
Validate.notBlank(theLinkId, "theLinkId must not be blank");
ArrayList<org.hl7.fhir.instance.model.QuestionnaireAnswers.GroupComponent> retVal = new ArrayList<QuestionnaireAnswers.GroupComponent>();
for (org.hl7.fhir.instance.model.QuestionnaireAnswers.GroupComponent next : theGroups) {
if (theLinkId.equals(next.getLinkId())) {
retVal.add(next);
}
}
return retVal;
}
public void validate(List<ValidationMessage> theErrors, QuestionnaireAnswers theAnswers) {
LinkedList<String> pathStack = new LinkedList<String>();
pathStack.add("QuestionnaireAnswers");
pathStack.add(QuestionnaireAnswers.SP_QUESTIONNAIRE);
if (!fail(theErrors, IssueType.INVALID, pathStack, theAnswers.hasQuestionnaire(), "QuestionnaireAnswers does not specity which questionnaire it is providing answers to")) {
return;
}
Reference questionnaireRef = theAnswers.getQuestionnaire();
Questionnaire questionnaire = getQuestionnaire(theAnswers, questionnaireRef);
if (!fail(theErrors, IssueType.INVALID, pathStack, questionnaire != null, "Questionnaire {0} is not found in the WorkerContext", theAnswers.getQuestionnaire().getReference())) {
return;
}
pathStack.removeLast();
pathStack.add("group(0)");
validateGroup(theErrors, questionnaire.getGroup(), theAnswers.getGroup(), pathStack, theAnswers);
}
private Questionnaire getQuestionnaire(QuestionnaireAnswers theAnswers, Reference theQuestionnaireRef) {
Questionnaire retVal;
if (theQuestionnaireRef.getReferenceElement().isLocal()) {
retVal = (Questionnaire) theQuestionnaireRef.getResource();
if (retVal == null) {
for (Resource next : theAnswers.getContained()) {
if (theQuestionnaireRef.getReferenceElement().getValue().equals(next.getId())) {
retVal = (Questionnaire) next;
}
}
}
} else {
retVal = myWorkerCtx.getQuestionnaires().get(theQuestionnaireRef.getReferenceElement().getValue());
}
return retVal;
}
private ValueSet getValueSet(QuestionnaireAnswers theAnswers, Reference theQuestionnaireRef) {
ValueSet retVal;
if (theQuestionnaireRef.getReferenceElement().isLocal()) {
retVal = (ValueSet) theQuestionnaireRef.getResource();
if (retVal == null) {
for (Resource next : theAnswers.getContained()) {
if (theQuestionnaireRef.getReferenceElement().getValue().equals(next.getId())) {
retVal = (ValueSet) next;
}
}
}
} else {
retVal = myWorkerCtx.getValueSets().get(theQuestionnaireRef.getReferenceElement().getValue());
}
return retVal;
}
private void validateGroup(List<ValidationMessage> theErrors, GroupComponent theQuestGroup, org.hl7.fhir.instance.model.QuestionnaireAnswers.GroupComponent theAnsGroup,
LinkedList<String> thePathStack, QuestionnaireAnswers theAnswers) {
for (org.hl7.fhir.instance.model.QuestionnaireAnswers.QuestionComponent next : theAnsGroup.getQuestion()) {
rule(theErrors, IssueType.INVALID, thePathStack, isNotBlank(next.getLinkId()), "Question found with no linkId");
}
Set<String> allowedQuestions = new HashSet<String>();
for (QuestionComponent nextQuestion : theQuestGroup.getQuestion()) {
allowedQuestions.add(nextQuestion.getLinkId());
}
for (int i = 0; i < theQuestGroup.getQuestion().size(); i++) {
QuestionComponent nextQuestion = theQuestGroup.getQuestion().get(i);
validateQuestion(theErrors, nextQuestion, theAnsGroup, thePathStack, theAnswers);
}
// Check that there are no extra answers
for (int i = 0; i < theAnsGroup.getQuestion().size(); i++) {
org.hl7.fhir.instance.model.QuestionnaireAnswers.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());
thePathStack.remove();
}
validateGroupGroups(theErrors, theQuestGroup, theAnsGroup, thePathStack, theAnswers);
}
private void validateQuestion(List<ValidationMessage> theErrors, QuestionComponent theQuestion, org.hl7.fhir.instance.model.QuestionnaireAnswers.GroupComponent theAnsGroup,
LinkedList<String> thePathStack, QuestionnaireAnswers theAnswers) {
String linkId = theQuestion.getLinkId();
if (!fail(theErrors, IssueType.INVALID, thePathStack, isNotBlank(linkId), "Questionnaire is invalid, question found with no link ID")) {
return;
}
AnswerFormat type = theQuestion.getType();
if (type == null) {
if (theQuestion.getGroup().isEmpty()) {
rule(theErrors, IssueType.INVALID, thePathStack, false, "Questionnaire in invalid, no type and no groups specified for question with link ID[{0}]", linkId);
return;
}
type = AnswerFormat.NULL;
}
List<org.hl7.fhir.instance.model.QuestionnaireAnswers.QuestionComponent> answers = findAnswersByLinkId(theAnsGroup.getQuestion(), linkId);
if (answers.size() > 1) {
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);
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);
} 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);
}
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);
}
private void validateGroups(List<ValidationMessage> theErrors, List<GroupComponent> theQuestionGroups, List<org.hl7.fhir.instance.model.QuestionnaireAnswers.GroupComponent> theAnswerGroups,
LinkedList<String> thePathStack, QuestionnaireAnswers theAnswers) {
Set<String> allowedGroups = new HashSet<String>();
for (GroupComponent nextQuestionGroup : theQuestionGroups) {
String linkId = nextQuestionGroup.getLinkId();
allowedGroups.add(linkId);
List<org.hl7.fhir.instance.model.QuestionnaireAnswers.GroupComponent> answerGroups = findGroupByLinkId(theAnswerGroups, linkId);
if (answerGroups.isEmpty()) {
if (nextQuestionGroup.getRequired()) {
rule(theErrors, IssueType.BUSINESSRULE, thePathStack, false, "Missing required group with linkId[{0}]", linkId);
}
continue;
}
if (answerGroups.size() > 1) {
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);
thePathStack.removeLast();
}
}
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);
thePathStack.removeLast();
}
}
// Make sure there are no groups in answers that aren't in the questionnaire
int idx = -1;
for (org.hl7.fhir.instance.model.QuestionnaireAnswers.GroupComponent next : theAnswerGroups) {
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", next.getLinkId());
thePathStack.removeLast();
}
}
}
private void validateQuestionAnswers(List<ValidationMessage> theErrors, QuestionComponent theQuestion, LinkedList<String> thePathStack, AnswerFormat type,
org.hl7.fhir.instance.model.QuestionnaireAnswers.QuestionComponent answerQuestion, QuestionnaireAnswers theAnswers) {
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 {
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);
}
int answerIdx = -1;
for (QuestionAnswerComponent nextAnswer : answerQuestion.getAnswer()) {
answerIdx++;
try {
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());
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());
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);
continue;
}
boolean found = false;
if (coding.getSystem().equals(valueSet.getDefine().getSystem())) {
for (ConceptDefinitionComponent next : valueSet.getDefine().getConcept()) {
if (coding.getCode().equals(next.getCode())) {
found = true;
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);
}
}
} finally {
thePathStack.removeLast();
}
} // for answers
}
private Set<Class<? extends Type>> determineAllowedAnswerTypes(AnswerFormat type) {
Set<Class<? extends Type>> allowedAnswerTypes;
switch (type) {
case ATTACHMENT:
allowedAnswerTypes = allowedTypes(Attachment.class);
break;
case BOOLEAN:
allowedAnswerTypes = allowedTypes(BooleanType.class);
break;
case CHOICE:
allowedAnswerTypes = allowedTypes(Coding.class);
break;
case DATE:
allowedAnswerTypes = allowedTypes(DateType.class);
break;
case DATETIME:
allowedAnswerTypes = allowedTypes(DateTimeType.class);
break;
case DECIMAL:
allowedAnswerTypes = allowedTypes(DecimalType.class);
break;
case INSTANT:
allowedAnswerTypes = allowedTypes(InstantType.class);
break;
case INTEGER:
allowedAnswerTypes = allowedTypes(IntegerType.class);
break;
case OPENCHOICE:
allowedAnswerTypes = allowedTypes(Coding.class);
break;
case QUANTITY:
allowedAnswerTypes = allowedTypes(Quantity.class);
break;
case REFERENCE:
allowedAnswerTypes = allowedTypes(Reference.class);
break;
case STRING:
allowedAnswerTypes = allowedTypes(StringType.class);
break;
case TEXT:
allowedAnswerTypes = allowedTypes(StringType.class);
break;
case TIME:
allowedAnswerTypes = allowedTypes(TimeType.class);
break;
case URL:
allowedAnswerTypes = allowedTypes(UriType.class);
break;
case NULL:
default:
allowedAnswerTypes = Collections.emptySet();
}
return allowedAnswerTypes;
}
}

View File

@ -29,6 +29,8 @@ POSSIBILITY OF SUCH DAMAGE.
*/ */
import org.apache.commons.lang3.builder.ToStringBuilder;
import org.apache.commons.lang3.builder.ToStringStyle;
import org.hl7.fhir.instance.model.CodeableConcept; import org.hl7.fhir.instance.model.CodeableConcept;
import org.hl7.fhir.instance.model.OperationOutcome; import org.hl7.fhir.instance.model.OperationOutcome;
import org.hl7.fhir.instance.model.OperationOutcome.IssueSeverity; import org.hl7.fhir.instance.model.OperationOutcome.IssueSeverity;
@ -184,5 +186,21 @@ public class ValidationMessage
return html == null ? Utilities.escapeXml(message) : html; return html == null ? Utilities.escapeXml(message) : html;
} }
/**
* Returns a representation of this ValidationMessage suitable for logging. The values of
* most of the internal fields are included, so this may not be suitable for display to
* an end user.
*/
@Override
public String toString() {
ToStringBuilder b = new ToStringBuilder(this, ToStringStyle.SHORT_PREFIX_STYLE);
b.append("level", level);
b.append("type", type);
b.append("location", location);
b.append("message", message);
return b.build();
}
} }

View File

@ -3,15 +3,18 @@ package ca.uhn.fhir.model;
import static org.junit.Assert.*; import static org.junit.Assert.*;
import java.io.IOException; import java.io.IOException;
import java.util.HashSet;
import java.util.Map.Entry; import java.util.Map.Entry;
import java.util.Properties; import java.util.Properties;
import org.hl7.fhir.instance.model.api.IBase;
import org.hl7.fhir.instance.model.api.IBaseResource; import org.hl7.fhir.instance.model.api.IBaseResource;
import org.junit.Test; import org.junit.Test;
import ca.uhn.fhir.context.BaseRuntimeChildDefinition; import ca.uhn.fhir.context.BaseRuntimeChildDefinition;
import ca.uhn.fhir.context.BaseRuntimeElementCompositeDefinition; import ca.uhn.fhir.context.BaseRuntimeElementCompositeDefinition;
import ca.uhn.fhir.context.FhirContext; import ca.uhn.fhir.context.FhirContext;
import ca.uhn.fhir.context.RuntimeChildResourceBlockDefinition;
import ca.uhn.fhir.context.RuntimeResourceDefinition; import ca.uhn.fhir.context.RuntimeResourceDefinition;
public class InstantiationTest { public class InstantiationTest {
@ -28,16 +31,27 @@ public class InstantiationTest {
Class<? extends IBaseResource> clazz = (Class<? extends IBaseResource>) Class.forName(next.getValue().toString()); Class<? extends IBaseResource> clazz = (Class<? extends IBaseResource>) Class.forName(next.getValue().toString());
RuntimeResourceDefinition res = ctx.getResourceDefinition(clazz); RuntimeResourceDefinition res = ctx.getResourceDefinition(clazz);
scanChildren(clazz, res); scanChildren(new HashSet<Class<?>>(), clazz, res);
} }
} }
} }
private void scanChildren(Class<? extends IBaseResource> theClazz, BaseRuntimeElementCompositeDefinition<?> theRes) { private void scanChildren(HashSet<Class<?>> theHashSet, Class<? extends IBase> theClazz, BaseRuntimeElementCompositeDefinition<?> theRes) {
for (BaseRuntimeChildDefinition next : theRes.getChildren()) { for (BaseRuntimeChildDefinition next : theRes.getChildren()) {
if (next.getElementName().contains("_")) { if (next.getElementName().contains("_")) {
fail("Element name " + next.getElementName() + " in type " + theClazz + " contains illegal '_'"); fail("Element name " + next.getElementName() + " in type " + theClazz + " contains illegal '_'");
} }
if (next instanceof RuntimeChildResourceBlockDefinition) {
RuntimeChildResourceBlockDefinition nextBlock = (RuntimeChildResourceBlockDefinition) next;
for (String nextName : nextBlock.getValidChildNames()) {
BaseRuntimeElementCompositeDefinition<?> elementDef = nextBlock.getChildByName(nextName);
if (theHashSet.add(elementDef.getImplementingClass())) {
scanChildren(theHashSet, elementDef.getImplementingClass(), elementDef);
}
}
}
} }
} }

View File

@ -1,18 +1,18 @@
package ca.uhn.fhir.validation; package ca.uhn.fhir.validation;
import static org.hamcrest.Matchers.*; import static org.hamcrest.Matchers.stringContainsInOrder;
import static org.junit.Assert.*; import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertThat;
import java.util.List; import java.util.List;
import org.hl7.fhir.instance.model.Patient;
import org.hl7.fhir.instance.validation.ValidationMessage; import org.hl7.fhir.instance.validation.ValidationMessage;
import org.junit.Test; import org.junit.Test;
import ca.uhn.fhir.context.FhirContext; import ca.uhn.fhir.context.FhirContext;
import ca.uhn.fhir.rest.server.EncodingEnum; import ca.uhn.fhir.rest.server.EncodingEnum;
public class StructureDefinitionValidatorTest { public class FhirInstanceValidatorTest {
private static FhirContext ourCtx = FhirContext.forDstu2Hl7Org(); private static FhirContext ourCtx = FhirContext.forDstu2Hl7Org();
@ -23,8 +23,8 @@ public class StructureDefinitionValidatorTest {
+ "\"id\":\"123\"" + "\"id\":\"123\""
+ "}"; + "}";
StructureDefinitionValidator val = new StructureDefinitionValidator(ourCtx); FhirInstanceValidator val = new FhirInstanceValidator();
List<ValidationMessage> output = val.validate(input, EncodingEnum.JSON, Patient.class); List<ValidationMessage> output = val.validate(ourCtx, input, EncodingEnum.JSON, "Patient");
assertEquals(output.toString(), 0, output.size()); assertEquals(output.toString(), 0, output.size());
} }
@ -37,8 +37,8 @@ public class StructureDefinitionValidatorTest {
+ "}"; + "}";
StructureDefinitionValidator val = new StructureDefinitionValidator(ourCtx); FhirInstanceValidator val = new FhirInstanceValidator();
List<ValidationMessage> output = val.validate(input, EncodingEnum.JSON, Patient.class); List<ValidationMessage> output = val.validate(ourCtx, input, EncodingEnum.JSON, "Patient");
assertEquals(output.toString(), 1, output.size()); assertEquals(output.toString(), 1, output.size());
assertThat(output.get(0).toXML(), stringContainsInOrder("/foo", "Element is unknown")); assertThat(output.get(0).toXML(), stringContainsInOrder("/foo", "Element is unknown"));
} }
@ -49,8 +49,8 @@ public class StructureDefinitionValidatorTest {
+ "<id value=\"123\"/>" + "<id value=\"123\"/>"
+ "</Patient>"; + "</Patient>";
StructureDefinitionValidator val = new StructureDefinitionValidator(ourCtx); FhirInstanceValidator val = new FhirInstanceValidator();
List<ValidationMessage> output = val.validate(input, EncodingEnum.XML, Patient.class); List<ValidationMessage> output = val.validate(ourCtx, input, EncodingEnum.XML, "Patient");
assertEquals(output.toString(), 0, output.size()); assertEquals(output.toString(), 0, output.size());
} }
@ -62,8 +62,8 @@ public class StructureDefinitionValidatorTest {
+ "<foo value=\"222\"/>" + "<foo value=\"222\"/>"
+ "</Patient>"; + "</Patient>";
StructureDefinitionValidator val = new StructureDefinitionValidator(ourCtx); FhirInstanceValidator val = new FhirInstanceValidator();
List<ValidationMessage> output = val.validate(input, EncodingEnum.XML, Patient.class); List<ValidationMessage> output = val.validate(ourCtx, input, EncodingEnum.XML, "Patient");
assertEquals(output.toString(), 1, output.size()); assertEquals(output.toString(), 1, output.size());
assertThat(output.get(0).toXML(), stringContainsInOrder("/f:Patient/f:foo", "Element is unknown")); assertThat(output.get(0).toXML(), stringContainsInOrder("/f:Patient/f:foo", "Element is unknown"));
} }

View File

@ -0,0 +1,185 @@
package ca.uhn.fhir.validation;
import static org.hamcrest.Matchers.containsString;
import static org.junit.Assert.*;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
import org.apache.commons.io.IOUtils;
import org.hl7.fhir.instance.model.Coding;
import org.hl7.fhir.instance.model.Questionnaire;
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.utils.WorkerContext;
import org.hl7.fhir.instance.validation.QuestionnaireAnswersValidator;
import org.hl7.fhir.instance.validation.ValidationMessage;
import org.junit.Before;
import org.junit.Test;
import ca.uhn.fhir.context.FhirContext;
public class QuestionnaireAnswersValidatorTest {
private static final FhirContext ourCtx = FhirContext.forDstu2Hl7Org();
private static final org.slf4j.Logger ourLog = org.slf4j.LoggerFactory.getLogger(QuestionnaireAnswersValidatorTest.class);
private QuestionnaireAnswersValidator myVal;
private WorkerContext myWorkerCtx;
@Before
public void before() {
myWorkerCtx = new WorkerContext();
myVal = new QuestionnaireAnswersValidator(myWorkerCtx);
}
@Test
public void testAnswerWithWrongType() {
Questionnaire q = new Questionnaire();
q.getGroup().addQuestion().setLinkId("link0").setRequired(true).setType(AnswerFormat.BOOLEAN);
QuestionnaireAnswers qa = new QuestionnaireAnswers();
qa.getQuestionnaire().setReference("http://example.com/Questionnaire/q1");
qa.getGroup().addQuestion().setLinkId("link0").addAnswer().setValue(new StringType("FOO"));
myWorkerCtx.getQuestionnaires().put(qa.getQuestionnaire().getReference(), q);
List<ValidationMessage> errors = new ArrayList<ValidationMessage>();
myVal.validate(errors, qa);
ourLog.info(errors.toString());
assertThat(errors.toString(), containsString("Answer to question with linkId[link0] found of type [StringType] but this is invalid for question of type [boolean]"));
}
@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.getDefine().setSystem("urn:system").addConcept().setCode("code0");
myWorkerCtx.getValueSets().put("http://somevalueset", options);
QuestionnaireAnswers qa;
List<ValidationMessage> errors;
// Good code
qa = new QuestionnaireAnswers();
qa.getQuestionnaire().setReference(questionnaireRef);
qa.getGroup().addQuestion().setLinkId("link0").addAnswer().setValue(new Coding().setSystem("urn:system").setCode("code0"));
errors = new ArrayList<ValidationMessage>();
myVal.validate(errors, qa);
assertEquals(errors.toString(), 0, errors.size());
// Bad code
qa = new QuestionnaireAnswers();
qa.getQuestionnaire().setReference(questionnaireRef);
qa.getGroup().addQuestion().setLinkId("link0").addAnswer().setValue(new Coding().setSystem("urn:system").setCode("code1"));
errors = new ArrayList<ValidationMessage>();
myVal.validate(errors, qa);
ourLog.info(errors.toString());
assertThat(errors.toString(), containsString("location=QuestionnaireAnswers.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]"));
}
@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);
QuestionnaireAnswers qa = new QuestionnaireAnswers();
qa.getQuestionnaire().setReference("http://example.com/Questionnaire/q1");
qa.getGroup().addQuestion().setLinkId("link1").addAnswer().setValue(new StringType("FOO"));
myWorkerCtx.getQuestionnaires().put(qa.getQuestionnaire().getReference(), q);
List<ValidationMessage> errors = new ArrayList<ValidationMessage>();
myVal.validate(errors, qa);
ourLog.info(errors.toString());
assertThat(errors.toString(), containsString("Missing answer to required question with linkId[link0]"));
}
@Test
public void testUnexpectedAnswer() {
Questionnaire q = new Questionnaire();
q.getGroup().addQuestion().setLinkId("link0").setRequired(false).setType(AnswerFormat.BOOLEAN);
QuestionnaireAnswers qa = new QuestionnaireAnswers();
qa.getQuestionnaire().setReference("http://example.com/Questionnaire/q1");
qa.getGroup().addQuestion().setLinkId("link1").addAnswer().setValue(new StringType("FOO"));
myWorkerCtx.getQuestionnaires().put(qa.getQuestionnaire().getReference(), q);
List<ValidationMessage> errors = new ArrayList<ValidationMessage>();
myVal.validate(errors, qa);
ourLog.info(errors.toString());
assertThat(errors.toString(), containsString("location=QuestionnaireAnswers.group(0).question"));
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);
QuestionnaireAnswers qa = new QuestionnaireAnswers();
qa.getQuestionnaire().setReference("http://example.com/Questionnaire/q1");
qa.getGroup().addGroup().setLinkId("link1");
myWorkerCtx.getQuestionnaires().put(qa.getQuestionnaire().getReference(), q);
List<ValidationMessage> errors = new ArrayList<ValidationMessage>();
myVal.validate(errors, qa);
ourLog.info(errors.toString());
assertThat(errors.toString(), containsString("location=QuestionnaireAnswers.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(QuestionnaireAnswersValidatorTest.class.getResourceAsStream("/questionnaireanswers-0f431c50ddbe4fff8e0dd6b7323625fc.xml"));
QuestionnaireAnswers qa = ourCtx.newXmlParser().parseResource(QuestionnaireAnswers.class, input);
ArrayList<ValidationMessage> errors = new ArrayList<ValidationMessage>();
myVal.validate(errors, qa);
assertEquals(errors.toString(), 0, errors.size());
/*
* Now change a coded value
*/
//@formatter:off
input = input.replaceAll("<answer>\n" +
" <valueCoding>\n" +
" <system value=\"f69573b8-cb63-4d31-85a4-23ac784735ab\"/>\n" +
" <code value=\"2\"/>\n" +
" <display value=\"Once/twice\"/>\n" +
" </valueCoding>\n" +
" </answer>", "<answer>\n" +
" <valueCoding>\n" +
" <system value=\"f69573b8-cb63-4d31-85a4-23ac784735ab\"/>\n" +
" <code value=\"GGG\"/>\n" +
" <display value=\"Once/twice\"/>\n" +
" </valueCoding>\n" +
" </answer>");
assertThat(input, containsString("GGG"));
//@formatter:on
qa = ourCtx.newXmlParser().parseResource(QuestionnaireAnswers.class, input);
errors = new ArrayList<ValidationMessage>();
myVal.validate(errors, qa);
assertEquals(errors.toString(), 10, errors.size());
}
}

View File

@ -5,7 +5,11 @@ import java.util.*;
import org.apache.commons.lang3.StringUtils; import org.apache.commons.lang3.StringUtils;
#if ( $version != 'dstu' && (${className} == 'Encounter' || ${className} == 'Patient' || ${className} == 'ValueSet' || ${className} == 'QuestionnaireAnswers'))
import ca.uhn.fhir.jpa.provider.BaseJpaResourceProvider${className}Dstu2;
#else
import ca.uhn.fhir.jpa.provider.JpaResourceProvider${versionCapitalized}; import ca.uhn.fhir.jpa.provider.JpaResourceProvider${versionCapitalized};
#end
import ca.uhn.fhir.jpa.dao.SearchParameterMap; import ca.uhn.fhir.jpa.dao.SearchParameterMap;
import ca.uhn.fhir.model.api.IResource; import ca.uhn.fhir.model.api.IResource;
import ca.uhn.fhir.model.api.Include; import ca.uhn.fhir.model.api.Include;
@ -20,8 +24,10 @@ import ca.uhn.fhir.model.dstu.resource.Binary;
// import ca.uhn.fhir.model.api.Bundle; // import ca.uhn.fhir.model.api.Bundle;
public class ${className}ResourceProvider extends public class ${className}ResourceProvider extends
#if ( $version != 'dstu' && (${className} == 'Patient' || ${className} == 'Encounter') ) ## We have specialized base classes for RPs that handle certain resource types. These
BaseJpaResourceProvider${className}${versionCapitalized}<${className}> ## RPs implement type specific operations
#if ( $version != 'dstu' && (${className} == 'Encounter' || ${className} == 'Patient' || ${className} == 'ValueSet' || ${className} == 'QuestionnaireAnswers'))
BaseJpaResourceProvider${className}${versionCapitalized}
#else #else
JpaResourceProvider${versionCapitalized}<${className}> JpaResourceProvider${versionCapitalized}<${className}>
#end #end

View File

@ -12,7 +12,6 @@ cp -vp $FHIRTRUNK/build/implementations/java/org.hl7.fhir.instance/src/org/hl7/f
cp -vp $FHIRTRUNK/build/implementations/java/org.hl7.fhir.instance/src/org/hl7/fhir/instance/utils/NameResolver.java hapi-fhir-structures-hl7org-dstu2/src/main/java/org/hl7/fhir/instance/utils/ cp -vp $FHIRTRUNK/build/implementations/java/org.hl7.fhir.instance/src/org/hl7/fhir/instance/utils/NameResolver.java hapi-fhir-structures-hl7org-dstu2/src/main/java/org/hl7/fhir/instance/utils/
cp -vp $FHIRTRUNK/build/implementations/java/org.hl7.fhir.instance/src/org/hl7/fhir/instance/validation/BaseValidator.java hapi-fhir-structures-hl7org-dstu2/src/main/java/org/hl7/fhir/instance/validation/ cp -vp $FHIRTRUNK/build/implementations/java/org.hl7.fhir.instance/src/org/hl7/fhir/instance/validation/BaseValidator.java hapi-fhir-structures-hl7org-dstu2/src/main/java/org/hl7/fhir/instance/validation/
cp -vp $FHIRTRUNK/build/implementations/java/org.hl7.fhir.instance/src/org/hl7/fhir/instance/validation/InstanceValidator.java hapi-fhir-structures-hl7org-dstu2/src/main/java/org/hl7/fhir/instance/validation/ cp -vp $FHIRTRUNK/build/implementations/java/org.hl7.fhir.instance/src/org/hl7/fhir/instance/validation/InstanceValidator.java hapi-fhir-structures-hl7org-dstu2/src/main/java/org/hl7/fhir/instance/validation/
cp -vp $FHIRTRUNK/build/implementations/java/org.hl7.fhir.instance/src/org/hl7/fhir/instance/validation/ProfileValidator.java hapi-fhir-structures-hl7org-dstu2/src/main/java/org/hl7/fhir/instance/validation/
cp -vp $FHIRTRUNK/build/implementations/java/org.hl7.fhir.instance/src/org/hl7/fhir/instance/validation/ValidationMessage.java hapi-fhir-structures-hl7org-dstu2/src/main/java/org/hl7/fhir/instance/validation/ValidationMessage.java cp -vp $FHIRTRUNK/build/implementations/java/org.hl7.fhir.instance/src/org/hl7/fhir/instance/validation/ValidationMessage.java hapi-fhir-structures-hl7org-dstu2/src/main/java/org/hl7/fhir/instance/validation/ValidationMessage.java
cp -vp $FHIRTRUNK/build/implementations/java/org.hl7.fhir.utilities/src/org/hl7/fhir/utilities/xhtml/HeirarchicalTableGenerator.java hapi-fhir-structures-hl7org-dstu2/src/main/java/org/hl7/fhir/utilities/xhtml/ cp -vp $FHIRTRUNK/build/implementations/java/org.hl7.fhir.utilities/src/org/hl7/fhir/utilities/xhtml/HeirarchicalTableGenerator.java hapi-fhir-structures-hl7org-dstu2/src/main/java/org/hl7/fhir/utilities/xhtml/