Fix #516 - Handle STU3 invalid enum values with an appropriate exception

This commit is contained in:
James 2016-12-10 14:14:22 -05:00
parent 1fba4ff265
commit ee63bbea74
20 changed files with 524 additions and 173 deletions

View File

@ -13,69 +13,68 @@ import ca.uhn.fhir.rest.param.StringParam;
import ca.uhn.fhir.rest.server.IBundleProvider;
import ca.uhn.fhir.rest.server.IResourceProvider;
@SuppressWarnings("null")
//START SNIPPET: provider
// START SNIPPET: provider
public class PagingPatientProvider implements IResourceProvider {
/**
* Search for Patient resources matching a given family name
*/
@Search
public IBundleProvider search(@RequiredParam(name = Patient.SP_FAMILY) StringParam theFamily) {
final InstantDt searchTime = InstantDt.withCurrentTime();
/**
* Search for Patient resources matching a given family name
*/
@Search
public IBundleProvider search(@RequiredParam(name = Patient.SP_FAMILY) StringParam theFamily) {
final InstantDt searchTime = InstantDt.withCurrentTime();
/*
* First, we'll search the database for a set of database row IDs that
* match the given search criteria. That way we can keep just the
* row IDs around, and load the actual resources on demand later
* as the client pages through them.
*/
final List<Long> matchingResourceIds = null; // <-- implement this
/**
* First, we'll search the database for a set of database row IDs that
* match the given search criteria. That way we can keep just the row IDs
* around, and load the actual resources on demand later as the client
* pages through them.
*/
final List<Long> matchingResourceIds = null; // <-- implement this
/*
* Return a bundle provider which can page through the IDs and
* return the resources that go with them.
*/
return new IBundleProvider() {
/**
* Return a bundle provider which can page through the IDs and return the
* resources that go with them.
*/
return new IBundleProvider() {
@Override
public int size() {
return matchingResourceIds.size();
}
@Override
public int size() {
return matchingResourceIds.size();
}
@Override
public List<IBaseResource> getResources(int theFromIndex, int theToIndex) {
int end = Math.max(theToIndex, matchingResourceIds.size() - 1);
List<Long> idsToReturn = matchingResourceIds.subList(theFromIndex, end);
return loadResourcesByIds(idsToReturn);
}
@Override
public List<IBaseResource> getResources(int theFromIndex, int theToIndex) {
int end = Math.max(theToIndex, matchingResourceIds.size() - 1);
List<Long> idsToReturn = matchingResourceIds.subList(theFromIndex, end);
return loadResourcesByIds(idsToReturn);
}
@Override
public InstantDt getPublished() {
return searchTime;
}
@Override
public InstantDt getPublished() {
return searchTime;
}
@Override
public Integer preferredPageSize() {
// Typically this method just returns null
return null;
}
};
}
};
}
/**
* Load a list of patient resources given their IDs
*/
private List<IBaseResource> loadResourcesByIds(List<Long> theIdsToReturn) {
// .. implement this search against the database ..
return null;
}
/**
* Load a list of patient resources given their IDs
*/
private List<IBaseResource> loadResourcesByIds(List<Long> theIdsToReturn) {
// .. implement this search against the database ..
return null;
}
@Override
public Class<? extends IResource> getResourceType() {
return Patient.class;
}
@Override
public Class<? extends IResource> getResourceType() {
return Patient.class;
}
}
//END SNIPPET: provider
// END SNIPPET: provider

View File

@ -1,5 +1,8 @@
package ca.uhn.fhir.parser;
import static org.apache.commons.lang3.StringUtils.isBlank;
import static org.apache.commons.lang3.StringUtils.isNotBlank;
import ca.uhn.fhir.context.FhirContext;
import ca.uhn.fhir.parser.json.JsonLikeValue.ValueType;
@ -24,7 +27,13 @@ import ca.uhn.fhir.parser.json.JsonLikeValue.ValueType;
*/
/**
* The default error handler, which logs issues but does not abort parsing
* The default error handler, which logs issues but does not abort parsing, with only one exception:
* <p>
* The {@link #invalidValue(ca.uhn.fhir.parser.IParserErrorHandler.IParseLocation, String, String)}
* method will throw a {@link DataFormatException} by default since ignoring this type of error
* can lead to data loss (since invalid values are silently ignored). See
* {@link #setErrorOnInvalidValue(boolean)} for information on this.
* </p>
*
* @see IParser#setParserErrorHandler(IParserErrorHandler)
* @see FhirContext#setParserErrorHandler(IParserErrorHandler)
@ -32,6 +41,8 @@ import ca.uhn.fhir.parser.json.JsonLikeValue.ValueType;
public class LenientErrorHandler implements IParserErrorHandler {
private static final org.slf4j.Logger ourLog = org.slf4j.LoggerFactory.getLogger(LenientErrorHandler.class);
private static final StrictErrorHandler STRICT_ERROR_HANDLER = new StrictErrorHandler();
private boolean myErrorOnInvalidValue = true;
private boolean myLogErrors;
/**
@ -67,11 +78,30 @@ public class LenientErrorHandler implements IParserErrorHandler {
@Override
public void invalidValue(IParseLocation theLocation, String theValue, String theError) {
if (myLogErrors) {
ourLog.warn("Invalid attribute value \"{}\": {}", theValue, theError);
if (isBlank(theValue) || myErrorOnInvalidValue == false) {
if (myLogErrors) {
ourLog.warn("Invalid attribute value \"{}\": {}", theValue, theError);
}
} else {
STRICT_ERROR_HANDLER.invalidValue(theLocation, theValue, theError);
}
}
/**
* If set to <code>false</code> (default is <code>true</code>) invalid values will be logged. By
* default invalid attribute values cause this error handler to throw a {@link DataFormatException} (unlike
* other methods in this class which default to simply logging errors).
* <p>
* Note that empty values (e.g. <code>""</code>) will not lead to an error when this is set to
* <code>true</code>, only invalid values (e.g. a gender code of <code>foo</code>)
* </p>
*
* @see #setErrorOnInvalidValue(boolean)
*/
public boolean isErrorOnInvalidValue() {
return myErrorOnInvalidValue;
}
@Override
public void missingRequiredElement(IParseLocation theLocation, String theElementName) {
if (myLogErrors) {
@ -79,6 +109,22 @@ public class LenientErrorHandler implements IParserErrorHandler {
}
}
/**
* If set to <code>false</code> (default is <code>true</code>) invalid values will be logged. By
* default invalid attribute values cause this error handler to throw a {@link DataFormatException} (unlike
* other methods in this class which default to simply logging errors).
* <p>
* Note that empty values (e.g. <code>""</code>) will not lead to an error when this is set to
* <code>true</code>, only invalid values (e.g. a gender code of <code>foo</code>)
* </p>
* @return Returns a reference to <code>this</code> for easy method chaining
* @see #isErrorOnInvalidValue()
*/
public LenientErrorHandler setErrorOnInvalidValue(boolean theErrorOnInvalidValue) {
myErrorOnInvalidValue = theErrorOnInvalidValue;
return this;
}
@Override
public void unexpectedRepeatingElement(IParseLocation theLocation, String theElementName) {
if (myLogErrors) {

View File

@ -2311,6 +2311,8 @@ class ParserState<T> {
} else {
try {
myInstance.setValueAsString(theValue);
} catch (DataFormatException e) {
myErrorHandler.invalidValue(null, theValue, e.getMessage());
} catch (IllegalArgumentException e) {
myErrorHandler.invalidValue(null, theValue, e.getMessage());
}

View File

@ -25,6 +25,8 @@ import org.hl7.fhir.instance.model.api.IBaseResource;
import ca.uhn.fhir.context.FhirContext;
import ca.uhn.fhir.model.api.Bundle;
import ca.uhn.fhir.model.api.IResource;
import ca.uhn.fhir.parser.IParser;
import ca.uhn.fhir.parser.LenientErrorHandler;
import ca.uhn.fhir.rest.method.MethodUtil;
import ca.uhn.fhir.rest.server.EncodingEnum;
import ca.uhn.fhir.rest.server.exceptions.InvalidRequestException;
@ -146,7 +148,11 @@ public class ValidationContext<T> extends BaseValidationContext<T> implements IV
@Override
public IBaseResource getResource() {
if (myParsed == null) {
myParsed = getResourceAsStringEncoding().newParser(getFhirContext()).parseResource(getResourceAsString());
IParser parser = getResourceAsStringEncoding().newParser(getFhirContext());
LenientErrorHandler errorHandler = new LenientErrorHandler();
errorHandler.setErrorOnInvalidValue(false);
parser.setParserErrorHandler(errorHandler);
myParsed = parser.parseResource(getResourceAsString());
}
return myParsed;
}

View File

@ -11,7 +11,13 @@ import static org.junit.Assert.assertThat;
import static org.junit.Assert.fail;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import org.apache.commons.io.IOUtils;
import org.apache.http.client.methods.CloseableHttpResponse;
import org.apache.http.client.methods.HttpPost;
import org.apache.http.entity.ContentType;
import org.apache.http.entity.StringEntity;
import org.hl7.fhir.dstu3.model.BooleanType;
import org.hl7.fhir.dstu3.model.CodeSystem;
import org.hl7.fhir.dstu3.model.CodeType;
@ -34,6 +40,7 @@ import ca.uhn.fhir.jpa.entity.ResourceTable;
import ca.uhn.fhir.jpa.entity.TermCodeSystemVersion;
import ca.uhn.fhir.jpa.entity.TermConcept;
import ca.uhn.fhir.jpa.entity.TermConceptParentChildLink.RelationshipTypeEnum;
import ca.uhn.fhir.rest.server.Constants;
import ca.uhn.fhir.rest.server.exceptions.InvalidRequestException;
import ca.uhn.fhir.util.TestUtil;
@ -54,6 +61,30 @@ public class ResourceProviderDstu3ValueSetTest extends BaseResourceProviderDstu3
myExtensionalVsId = myValueSetDao.create(upload, mySrd).getId().toUnqualifiedVersionless();
}
/**
* #516
*/
@Test
public void testInvalidFilter() throws Exception {
String string = IOUtils.toString(getClass().getResourceAsStream("/bug_516_invalid_expansion.json"), StandardCharsets.UTF_8);
HttpPost post = new HttpPost(ourServerBase+"/ValueSet/%24expand");
post.setEntity(new StringEntity(string, ContentType.parse(Constants.CT_FHIR_JSON_NEW)));
CloseableHttpResponse resp = ourHttpClient.execute(post);
try {
String respString = IOUtils.toString(resp.getEntity().getContent(), StandardCharsets.UTF_8);
ourLog.info(respString);
ourLog.info(resp.toString());
assertEquals(400, resp.getStatusLine().getStatusCode());
assertThat(respString, containsString("Unknown FilterOperator code 'n'"));
}finally {
IOUtils.closeQuietly(resp);
}
}
private CodeSystem createExternalCs() {
CodeSystem codeSystem = new CodeSystem();

View File

@ -0,0 +1,47 @@
{
"resourceType": "ValueSet",
"url": "http://sample/ValueSet/education-levels",
"version": "1",
"name": "Education Levels",
"status": "draft",
"compose": {
"include": [
{
"filter": [
{
"property": "n",
"op": "n",
"value": "365460000"
}
],
"system": "http://snomed.info/sct"
}
],
"exclude": [
{
"concept": [
{
"code": "224298008"
},
{
"code": "365460000"
},
{
"code": "473462005"
},
{
"code": "424587006"
}
],
"system": "http://snomed.info/sct"
}
]
},
"description": "A\nselection of Education Levels",
"text": {
"status": "generated",
"div": "<div\nxmlns=\"http://www.w3.org/1999/xhtml\"><h2>Education\nLevels</h2><tt>http://csiro.au/ValueSet/education-levels</tt><p>A selection of\nEducation Levels</p></div>"
},
"experimental": true,
"date": "2016-07-26"
}

View File

@ -1019,14 +1019,12 @@
<content type="text/xml">
<Patient xmlns="http://hl7.org/fhir">
<identifier>
<system value=""/>
<value value="NEED"/>
<assigner>
<display value="FHIR"/>
</assigner>
</identifier>
<identifier>
<system value=""/>
<value value="E3289"/>
<assigner>
<display value="EPIC"/>

View File

@ -27,6 +27,7 @@ import ca.uhn.fhir.model.api.IResource;
import ca.uhn.fhir.model.dstu2.composite.MetaDt;
import ca.uhn.fhir.model.dstu2.resource.Bundle;
import ca.uhn.fhir.narrative.DefaultThymeleafNarrativeGenerator;
import ca.uhn.fhir.parser.StrictErrorHandler;
import ca.uhn.fhir.rest.server.ETagSupportEnum;
import ca.uhn.fhir.rest.server.EncodingEnum;
import ca.uhn.fhir.rest.server.IResourceProvider;
@ -49,10 +50,13 @@ public class JpaServerDemo extends RestfulServer {
* We want to support FHIR DSTU2 format. This means that the server
* will use the DSTU2 bundle format and other DSTU2 encoding changes.
*
* If you want to use DSTU1 instead, change the following line, and change the 2 occurrences of dstu2 in web.xml to dstu1
* If you want to use DSTU1 instead, change the following line, and
* change the 2 occurrences of dstu2 in web.xml to dstu1
*/
FhirVersionEnum fhirVersion = FhirVersionEnum.DSTU2;
setFhirContext(new FhirContext(fhirVersion));
FhirContext context = new FhirContext(fhirVersion);
setFhirContext(context);
// Get the spring context from the web container (it's declared in web.xml)
myAppCtx = ContextLoaderListener.getCurrentWebApplicationContext();

View File

@ -20,6 +20,8 @@ import ca.uhn.fhir.model.dstu.resource.Patient;
import ca.uhn.fhir.model.dstu.valueset.ContactSystemEnum;
import ca.uhn.fhir.model.primitive.DateTimeDt;
import ca.uhn.fhir.parser.DataFormatException;
import ca.uhn.fhir.parser.IParser;
import ca.uhn.fhir.parser.StrictErrorHandler;
import ca.uhn.fhir.util.TestUtil;
public class ResourceValidatorTest {
@ -72,23 +74,34 @@ public class ResourceValidatorTest {
/**
* See issue #50
*/
@Test(expected=DataFormatException.class)
@Test()
public void testOutOfBoundsDate() {
Patient p = new Patient();
p.setBirthDate(new DateTimeDt("2000-12-31"));
// Put in an invalid date
String encoded = ourCtx.newXmlParser().setPrettyPrint(true).encodeResourceToString(p).replace("2000-12-31", "2000-15-31");
IParser parser = ourCtx.newXmlParser();
parser.setParserErrorHandler(new StrictErrorHandler());
String encoded = parser.setPrettyPrint(true).encodeResourceToString(p).replace("2000-12-31", "2000-15-31");
ourLog.info(encoded);
assertThat(encoded, StringContains.containsString("2000-15-31"));
ValidationResult result = ourCtx.newValidator().validateWithResult(encoded);
String resultString = ourCtx.newXmlParser().setPrettyPrint(true).encodeResourceToString(result.toOperationOutcome());
FhirValidator validator = ourCtx.newValidator();
ValidationResult result = validator.validateWithResult(encoded);
String resultString = parser.setPrettyPrint(true).encodeResourceToString(result.toOperationOutcome());
ourLog.info(resultString);
assertEquals(2, ((OperationOutcome)result.toOperationOutcome()).getIssue().size());
assertThat(resultString, StringContains.containsString("cvc-datatype-valid.1.2.3"));
try {
parser.parseResource(encoded);
fail();
} catch (DataFormatException e) {
assertEquals("DataFormatException at [[row,col {unknown-source}]: [2,4]]: Invalid attribute value \"2000-15-31\": Invalid date/time format: \"2000-15-31\"", e.getMessage());
}
}
@Test

View File

@ -170,7 +170,10 @@ public class JsonParserDstu2Test {
@Test
public void testParseEmptyValue() {
String input = "{\"resourceType\":\"QuestionnaireResponse\",\"id\":\"123\",\"authored\":\"\",\"group\":{\"linkId\":\"\"}}";
QuestionnaireResponse qr = ourCtx.newJsonParser().parseResource(QuestionnaireResponse.class, input);
IParser parser = ourCtx.newJsonParser();
parser.setParserErrorHandler(new LenientErrorHandler().setErrorOnInvalidValue(false));
QuestionnaireResponse qr = parser.parseResource(QuestionnaireResponse.class, input);
assertEquals("QuestionnaireResponse/123", qr.getIdElement().getValue());
assertEquals(null, qr.getAuthored());

View File

@ -2524,7 +2524,10 @@ public class XmlParserDstu2Test {
"</Patient>";
//@formatter:on
ourCtx.newXmlParser().parseResource(resource);
IParser parser = ourCtx.newXmlParser();
parser.setParserErrorHandler(new StrictErrorHandler());
parser.parseResource(resource);
}
@Test

View File

@ -40,10 +40,12 @@ import ca.uhn.fhir.model.dstu2.valueset.ContactPointSystemEnum;
import ca.uhn.fhir.model.dstu2.valueset.NarrativeStatusEnum;
import ca.uhn.fhir.model.dstu2.valueset.UnitsOfTimeEnum;
import ca.uhn.fhir.model.primitive.DateDt;
import ca.uhn.fhir.model.primitive.DateTimeDt;
import ca.uhn.fhir.model.primitive.InstantDt;
import ca.uhn.fhir.model.primitive.StringDt;
import ca.uhn.fhir.parser.DataFormatException;
import ca.uhn.fhir.parser.IParser;
import ca.uhn.fhir.parser.StrictErrorHandler;
import ca.uhn.fhir.parser.XmlParserDstu2Test.TestPatientFor327;
import ca.uhn.fhir.util.TestUtil;
import ca.uhn.fhir.validation.schematron.SchematronBaseValidator;
@ -71,26 +73,33 @@ public class ResourceValidatorDstu2Test {
/**
* See issue #50
*/
@Test(expected=DataFormatException.class)
@Test()
public void testOutOfBoundsDate() {
Patient p = new Patient();
p.setBirthDate(new DateDt("2000-15-31"));
p.setBirthDate(new DateDt("2000-12-31"));
String encoded = ourCtx.newXmlParser().setPrettyPrint(true).encodeResourceToString(p);
// Put in an invalid date
IParser parser = ourCtx.newXmlParser();
parser.setParserErrorHandler(new StrictErrorHandler());
String encoded = parser.setPrettyPrint(true).encodeResourceToString(p).replace("2000-12-31", "2000-15-31");
ourLog.info(encoded);
assertThat(encoded, StringContains.containsString("2000-15-31"));
p = ourCtx.newXmlParser().parseResource(Patient.class, encoded);
assertEquals("2000-15-31", p.getBirthDateElement().getValueAsString());
assertEquals("2001-03-31", new SimpleDateFormat("yyyy-MM-dd").format(p.getBirthDate()));
ValidationResult result = ourCtx.newValidator().validateWithResult(p);
String resultString = ourCtx.newXmlParser().setPrettyPrint(true).encodeResourceToString(result.toOperationOutcome());
ValidationResult result = ourCtx.newValidator().validateWithResult(encoded);
String resultString = parser.setPrettyPrint(true).encodeResourceToString(result.toOperationOutcome());
ourLog.info(resultString);
assertEquals(2, ((OperationOutcome) result.toOperationOutcome()).getIssue().size());
assertThat(resultString, StringContains.containsString("2000-15-31"));
assertEquals(2, ((OperationOutcome)result.toOperationOutcome()).getIssue().size());
assertThat(resultString, StringContains.containsString("cvc-pattern-valid"));
try {
parser.parseResource(encoded);
fail();
} catch (DataFormatException e) {
assertEquals("DataFormatException at [[row,col {unknown-source}]: [2,4]]: Invalid attribute value \"2000-15-31\": Invalid date/time format: \"2000-15-31\"", e.getMessage());
}
}
@SuppressWarnings("deprecation")

View File

@ -1,6 +1,7 @@
package org.hl7.fhir.dstu3.model;
import static org.apache.commons.lang3.StringUtils.isBlank;
import static org.hamcrest.Matchers.emptyOrNullString;
import java.util.Calendar;
import java.util.Date;
@ -334,7 +335,7 @@ public abstract class BaseDateTimeType extends PrimitiveType<Date> {
myFractionalSeconds = "";
}
setPrecision(precision);
myPrecision = precision;
return cal.getTime();
}
@ -372,8 +373,8 @@ public abstract class BaseDateTimeType extends PrimitiveType<Date> {
if (isBlank(theValue)) {
throwBadDateFormat(theWholeValue);
} else if (theValue.charAt(0) == 'Z') {
clearTimeZone();
setTimeZoneZulu(true);
myTimeZone = null;
myTimeZoneZulu = true;
} else if (theValue.length() != 6) {
throwBadDateFormat(theWholeValue, "Timezone offset must be in the form \"Z\", \"-HH:mm\", or \"+HH:mm\"");
} else if (theValue.charAt(3) != ':' || !(theValue.charAt(0) == '+' || theValue.charAt(0) == '-')) {
@ -381,8 +382,8 @@ public abstract class BaseDateTimeType extends PrimitiveType<Date> {
} else {
parseInt(theWholeValue, theValue.substring(1, 3), 0, 23);
parseInt(theWholeValue, theValue.substring(4, 6), 0, 59);
clearTimeZone();
setTimeZone(TimeZone.getTimeZone("GMT" + theValue));
myTimeZoneZulu = false;
myTimeZone = TimeZone.getTimeZone("GMT" + theValue);
}
return this;
@ -390,12 +391,14 @@ public abstract class BaseDateTimeType extends PrimitiveType<Date> {
public BaseDateTimeType setTimeZone(TimeZone theTimeZone) {
myTimeZone = theTimeZone;
myTimeZoneZulu = false;
updateStringValue();
return this;
}
public BaseDateTimeType setTimeZoneZulu(boolean theTimeZoneZulu) {
myTimeZoneZulu = theTimeZoneZulu;
myTimeZone = null;
updateStringValue();
return this;
}

View File

@ -70,13 +70,13 @@ public abstract class PrimitiveType<T> extends Type implements IPrimitiveType<T>
}
public void fromStringValue(String theValue) {
myStringValue = theValue;
if (theValue == null) {
myCoercedValue = null;
} else {
// NB this might be null
myCoercedValue = parse(theValue);
}
myStringValue = theValue;
}
public T getValue() {

View File

@ -1,5 +1,7 @@
package ca.uhn.fhir.parser;
import static org.junit.Assert.*;
import org.junit.Test;
import ca.uhn.fhir.parser.json.JsonLikeValue.ValueType;
@ -46,7 +48,14 @@ public class ErrorHandlerTest {
new LenientErrorHandler().containedResourceWithNoId(null);
new LenientErrorHandler().unknownReference(null, null);
new LenientErrorHandler().incorrectJsonType(null, null, ValueType.ARRAY, ValueType.SCALAR);
new LenientErrorHandler().invalidValue(null, null, null);
new LenientErrorHandler().setErrorOnInvalidValue(false).invalidValue(null, "FOO", "");
new LenientErrorHandler().invalidValue(null, null, "");
try {
new LenientErrorHandler().invalidValue(null, "FOO", "");
fail();
} catch (DataFormatException e) {
// good, this one method defaults to causing an error
}
}
@Test(expected = DataFormatException.class)

View File

@ -13,35 +13,78 @@ import static org.junit.Assert.assertSame;
import static org.junit.Assert.assertThat;
import static org.junit.Assert.assertTrue;
import static org.junit.Assert.fail;
import static org.mockito.Matchers.eq;
import static org.mockito.Matchers.isNull;
import static org.mockito.Mockito.atLeastOnce;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.ObjectInputStream;
import java.math.BigDecimal;
import java.nio.charset.StandardCharsets;
import java.util.*;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Date;
import java.util.HashSet;
import java.util.List;
import java.util.UUID;
import org.apache.commons.io.IOUtils;
import org.hamcrest.Matchers;
import org.hamcrest.core.StringContains;
import org.hl7.fhir.dstu3.model.*;
import org.hl7.fhir.dstu3.model.Address.AddressUse;
import org.hl7.fhir.dstu3.model.Address.AddressUseEnumFactory;
import org.hl7.fhir.dstu3.model.Attachment;
import org.hl7.fhir.dstu3.model.AuditEvent;
import org.hl7.fhir.dstu3.model.Basic;
import org.hl7.fhir.dstu3.model.Binary;
import org.hl7.fhir.dstu3.model.Bundle;
import org.hl7.fhir.dstu3.model.Bundle.BundleEntryComponent;
import org.hl7.fhir.dstu3.model.Bundle.BundleType;
import org.hl7.fhir.dstu3.model.Claim;
import org.hl7.fhir.dstu3.model.Coding;
import org.hl7.fhir.dstu3.model.Communication;
import org.hl7.fhir.dstu3.model.Condition;
import org.hl7.fhir.dstu3.model.Condition.ConditionVerificationStatus;
import org.hl7.fhir.dstu3.model.Conformance;
import org.hl7.fhir.dstu3.model.Conformance.UnknownContentCode;
import org.hl7.fhir.dstu3.model.Coverage;
import org.hl7.fhir.dstu3.model.DateTimeType;
import org.hl7.fhir.dstu3.model.DateType;
import org.hl7.fhir.dstu3.model.DecimalType;
import org.hl7.fhir.dstu3.model.DiagnosticReport;
import org.hl7.fhir.dstu3.model.EnumFactory;
import org.hl7.fhir.dstu3.model.Enumeration;
import org.hl7.fhir.dstu3.model.Enumerations.AdministrativeGender;
import org.hl7.fhir.dstu3.model.ExplanationOfBenefit;
import org.hl7.fhir.dstu3.model.Extension;
import org.hl7.fhir.dstu3.model.HumanName;
import org.hl7.fhir.dstu3.model.IdType;
import org.hl7.fhir.dstu3.model.Identifier.IdentifierUse;
import org.hl7.fhir.dstu3.model.Linkage;
import org.hl7.fhir.dstu3.model.Medication;
import org.hl7.fhir.dstu3.model.MedicationRequest;
import org.hl7.fhir.dstu3.model.Observation;
import org.hl7.fhir.dstu3.model.Observation.ObservationStatus;
import org.hl7.fhir.instance.model.api.IBaseResource;
import org.hl7.fhir.dstu3.model.Parameters;
import org.hl7.fhir.dstu3.model.Patient;
import org.hl7.fhir.dstu3.model.PrimitiveType;
import org.hl7.fhir.dstu3.model.Quantity;
import org.hl7.fhir.dstu3.model.QuestionnaireResponse;
import org.hl7.fhir.dstu3.model.Reference;
import org.hl7.fhir.dstu3.model.RelatedPerson;
import org.hl7.fhir.dstu3.model.SampledData;
import org.hl7.fhir.dstu3.model.SimpleQuantity;
import org.hl7.fhir.dstu3.model.StringType;
import org.hl7.fhir.dstu3.model.UriType;
import org.hl7.fhir.dstu3.model.ValueSet;
import org.hl7.fhir.utilities.xhtml.XhtmlNode;
import org.junit.*;
import org.junit.After;
import org.junit.AfterClass;
import org.junit.Assert;
import org.junit.Ignore;
import org.junit.Test;
import org.mockito.ArgumentCaptor;
import org.mockito.Mockito;
@ -49,7 +92,6 @@ import com.google.common.collect.Sets;
import ca.uhn.fhir.context.FhirContext;
import ca.uhn.fhir.narrative.DefaultThymeleafNarrativeGenerator;
import ca.uhn.fhir.parser.DataFormatException;
import ca.uhn.fhir.parser.IParserErrorHandler.IParseLocation;
import ca.uhn.fhir.parser.PatientWithExtendedContactDstu3.CustomContactComponent;
import ca.uhn.fhir.parser.XmlParserDstu3Test.TestPatientFor327;
@ -93,7 +135,7 @@ public class JsonParserDstu3Test {
IParser p = ourCtx.newJsonParser().setPrettyPrint(true);
String encoded = p.encodeResourceToString(resource);
ourLog.info(encoded);
assertEquals(3, countMatches(encoded, "resourceType"));
}
@ -122,15 +164,18 @@ public class JsonParserDstu3Test {
*/
@Test
public void testParseEmptyValue() {
String input = "{\"resourceType\":\"QuestionnaireResponse\",\"id\":\"123\",\"authored\":\"\",\"item\":[{\"item\":[{\"linkId\":\"\"}]}]}";
QuestionnaireResponse qr = ourCtx.newJsonParser().parseResource(QuestionnaireResponse.class, input);
String input = "{\"resourceType\":\"QuestionnaireResponse\",\"id\":\"123\",\"authored\":\"\",\"group\":{\"linkId\":\"\"}}";
IParser parser = ourCtx.newJsonParser();
parser.setParserErrorHandler(new LenientErrorHandler().setErrorOnInvalidValue(false));
QuestionnaireResponse qr = parser.parseResource(QuestionnaireResponse.class, input);
assertEquals("QuestionnaireResponse/123", qr.getIdElement().getValue());
assertEquals(null, qr.getAuthored());
assertEquals(null, qr.getAuthoredElement().getValue());
assertEquals(null, qr.getAuthoredElement().getValueAsString());
assertEquals(null, qr.getItemFirstRep().getItemFirstRep().getLinkId());
assertEquals(null, qr.getItemFirstRep().getItemFirstRep().getLinkIdElement().getValue());
assertEquals(null, qr.getItemFirstRep().getLinkId());
assertEquals(null, qr.getItemFirstRep().getLinkIdElement().getValue());
}
/**
@ -1194,17 +1239,77 @@ public class JsonParserDstu3Test {
assertEquals("{\"resourceType\":\"Observation\",\"valueQuantity\":{\"value\":0.0000000000000001}}", str);
}
/**
* #516
*/
@Test(expected=DataFormatException.class)
public void testInvalidEnumValue() {
String res = "{ \"resourceType\": \"ValueSet\", \"url\": \"http://sample/ValueSet/education-levels\", \"version\": \"1\", \"name\": \"Education Levels\", \"status\": \"draft\", \"compose\": { \"include\": [ { \"filter\": [ { \"property\": \"n\", \"op\": \"n\", \"value\": \"365460000\" } ], \"system\": \"http://snomed.info/sct\" } ], \"exclude\": [ { \"concept\": [ { \"code\": \"224298008\" }, { \"code\": \"365460000\" }, { \"code\": \"473462005\" }, { \"code\": \"424587006\" } ], \"system\": \"http://snomed.info/sct\" } ] }, \"description\": \"A selection of Education Levels\", \"text\": { \"status\": \"generated\", \"div\": \"<div xmlns=\\\"http://www.w3.org/1999/xhtml\\\"><h2>Education Levels</h2><tt>http://csiro.au/ValueSet/education-levels</tt><p>A selection of Education Levels</p></div>\" }, \"experimental\": true, \"date\": \"2016-07-26\" }";
IParser parser = ourCtx.newJsonParser();
parser.setParserErrorHandler(new StrictErrorHandler());
ValueSet parsed = parser.parseResource(ValueSet.class, res);
fail("DataFormat Invalid attribute exception should be thrown");
}
/**
* #516
*/
@Test(expected = DataFormatException.class)
public void testInvalidEnumValue() {
String res = "{ \"resourceType\": \"ValueSet\", \"url\": \"http://sample/ValueSet/education-levels\", \"version\": \"1\", \"name\": \"Education Levels\", \"status\": \"draft\", \"compose\": { \"include\": [ { \"filter\": [ { \"property\": \"n\", \"op\": \"n\", \"value\": \"365460000\" } ], \"system\": \"http://snomed.info/sct\" } ], \"exclude\": [ { \"concept\": [ { \"code\": \"224298008\" }, { \"code\": \"365460000\" }, { \"code\": \"473462005\" }, { \"code\": \"424587006\" } ], \"system\": \"http://snomed.info/sct\" } ] }, \"description\": \"A selection of Education Levels\", \"text\": { \"status\": \"generated\", \"div\": \"<div xmlns=\\\"http://www.w3.org/1999/xhtml\\\"><h2>Education Levels</h2><tt>http://csiro.au/ValueSet/education-levels</tt><p>A selection of Education Levels</p></div>\" }, \"experimental\": true, \"date\": \"2016-07-26\" }";
IParser parser = ourCtx.newJsonParser();
parser.setParserErrorHandler(new StrictErrorHandler());
ValueSet parsed = parser.parseResource(ValueSet.class, res);
fail("DataFormat Invalid attribute exception should be thrown");
}
@Test
public void testInvalidEnumValueBlank() {
IParserErrorHandler errorHandler = mock(IParserErrorHandler.class);
String res = "{ \"resourceType\": \"Patient\", \"gender\": \"\" }";
IParser parser = ourCtx.newJsonParser();
parser.setParserErrorHandler(errorHandler);
Patient parsed = parser.parseResource(Patient.class, res);
assertEquals(null, parsed.getGenderElement().getValue());
assertEquals(null, parsed.getGenderElement().getValueAsString());
ArgumentCaptor<String> msgCaptor = ArgumentCaptor.forClass(String.class);
verify(errorHandler, times(1)).invalidValue(isNull(IParseLocation.class), eq(""), msgCaptor.capture());
assertEquals("Attribute values must not be empty (\"\")", msgCaptor.getValue());
String encoded = ourCtx.newJsonParser().encodeResourceToString(parsed);
assertEquals("{\"resourceType\":\"Patient\"}", encoded);
}
@Test
public void testInvalidEnumValueInvalid() {
IParserErrorHandler errorHandler = mock(IParserErrorHandler.class);
String res = "{ \"resourceType\": \"Patient\", \"gender\": \"foo\" }";
IParser parser = ourCtx.newJsonParser();
parser.setParserErrorHandler(errorHandler);
Patient parsed = parser.parseResource(Patient.class, res);
assertEquals(null, parsed.getGenderElement().getValue());
assertEquals("foo", parsed.getGenderElement().getValueAsString());
ArgumentCaptor<String> msgCaptor = ArgumentCaptor.forClass(String.class);
verify(errorHandler, times(1)).invalidValue(isNull(IParseLocation.class), eq("foo"), msgCaptor.capture());
assertEquals("Unknown AdministrativeGender code 'foo'", msgCaptor.getValue());
String encoded = ourCtx.newJsonParser().encodeResourceToString(parsed);
assertEquals("{\"resourceType\":\"Patient\",\"gender\":\"foo\"}", encoded);
}
@Test
public void testInvalidDateTimeValueInvalid() throws Exception {
IParserErrorHandler errorHandler = mock(IParserErrorHandler.class);
String res = "{ \"resourceType\": \"Observation\", \"valueDateTime\": \"foo\" }";
IParser parser = ourCtx.newJsonParser();
parser.setParserErrorHandler(errorHandler);
Observation parsed = parser.parseResource(Observation.class, res);
assertEquals(null, parsed.getValueDateTimeType().getValue());
assertEquals("foo", parsed.getValueDateTimeType().getValueAsString());
ArgumentCaptor<String> msgCaptor = ArgumentCaptor.forClass(String.class);
verify(errorHandler, times(1)).invalidValue(isNull(IParseLocation.class), eq("foo"), msgCaptor.capture());
assertEquals("Invalid date/time format: \"foo\"", msgCaptor.getValue());
String encoded = ourCtx.newJsonParser().encodeResourceToString(parsed);
assertEquals("{\"resourceType\":\"Observation\",\"valueDateTime\":\"foo\"}", encoded);
}
/**
* #65

View File

@ -2855,7 +2855,7 @@ public class XmlParserDstu3Test {
/**
* See #366
*/
@Test(expected = DataFormatException.class)
@Test()
public void testParseInvalidBoolean() {
//@formatter:off
String resource = "<Patient xmlns=\"http://hl7.org/fhir\">\n" +
@ -2863,7 +2863,19 @@ public class XmlParserDstu3Test {
"</Patient>";
//@formatter:on
ourCtx.newXmlParser().parseResource(resource);
IParser p = ourCtx.newXmlParser();
try {
p.parseResource(resource);
fail();
} catch (DataFormatException e) {
assertEquals("DataFormatException at [[row,col {unknown-source}]: [2,4]]: Invalid attribute value \"1\": Invalid boolean string: '1'", e.getMessage());
}
LenientErrorHandler errorHandler = new LenientErrorHandler();
assertEquals(true, errorHandler.isErrorOnInvalidValue());
errorHandler.setErrorOnInvalidValue(false);
p.setParserErrorHandler(errorHandler);
}
@Test

View File

@ -3,8 +3,10 @@ package org.hl7.fhir.dstu3.hapi.validation;
import static org.hamcrest.Matchers.containsString;
import static org.hamcrest.Matchers.not;
import static org.hamcrest.Matchers.stringContainsInOrder;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertThat;
import static org.junit.Assert.assertTrue;
import static org.junit.Assert.fail;
import java.io.IOException;
import java.util.ArrayList;
@ -12,14 +14,24 @@ import java.util.Date;
import java.util.List;
import org.apache.commons.io.IOUtils;
import org.hl7.fhir.dstu3.model.*;
import org.hamcrest.core.StringContains;
import org.hl7.fhir.dstu3.model.CodeableConcept;
import org.hl7.fhir.dstu3.model.Coding;
import org.hl7.fhir.dstu3.model.Condition;
import org.hl7.fhir.dstu3.model.Condition.ConditionVerificationStatus;
import org.hl7.fhir.dstu3.model.DateType;
import org.hl7.fhir.dstu3.model.Narrative.NarrativeStatus;
import org.hl7.fhir.dstu3.model.OperationOutcome;
import org.hl7.fhir.dstu3.model.Patient;
import org.hl7.fhir.dstu3.model.Reference;
import org.hl7.fhir.dstu3.model.StringType;
import org.junit.AfterClass;
import org.junit.Test;
import ca.uhn.fhir.context.FhirContext;
import ca.uhn.fhir.parser.DataFormatException;
import ca.uhn.fhir.parser.IParser;
import ca.uhn.fhir.parser.StrictErrorHandler;
import ca.uhn.fhir.parser.XmlParserDstu3Test;
import ca.uhn.fhir.util.TestUtil;
import ca.uhn.fhir.validation.FhirValidator;
@ -37,6 +49,39 @@ public class ResourceValidatorDstu3Test {
TestUtil.clearAllStaticFieldsForUnitTest();
}
/**
* See issue #50
*/
@Test()
public void testOutOfBoundsDate() {
Patient p = new Patient();
p.setBirthDateElement(new DateType("2000-12-31"));
// Put in an invalid date
IParser parser = ourCtx.newXmlParser();
parser.setParserErrorHandler(new StrictErrorHandler());
String encoded = parser.setPrettyPrint(true).encodeResourceToString(p).replace("2000-12-31", "2000-15-31");
ourLog.info(encoded);
assertThat(encoded, StringContains.containsString("2000-15-31"));
ValidationResult result = ourCtx.newValidator().validateWithResult(encoded);
String resultString = parser.setPrettyPrint(true).encodeResourceToString(result.toOperationOutcome());
ourLog.info(resultString);
assertEquals(2, ((OperationOutcome)result.toOperationOutcome()).getIssue().size());
assertThat(resultString, StringContains.containsString("cvc-pattern-valid"));
try {
parser.parseResource(encoded);
fail();
} catch (DataFormatException e) {
assertEquals("DataFormatException at [[row,col {unknown-source}]: [2,4]]: Invalid attribute value \"2000-15-31\": Invalid date/time format: \"2000-15-31\"", e.getMessage());
}
}
/**
* Make sure that the elements that appear in all resources (meta, language, extension, etc) all appear in the correct order
*/

View File

@ -99,6 +99,21 @@
not by throwing an exception. Thanks to
Michael Lawley for the pull request!
</action>
<action type="add" issue="516">
When parsing DSTU3 resources with enumerated
types that contain invalid values, the parser will now
invoke the parserErrorHandler. For example, when parsing
<![CDATA[
<code>{"resourceType":"Patient", "gender":"foo"}</code>
]]>
the previous behaviour was to throw an InvalidArgumentException.
Now, the parserErrorHandler is invoked. In addition, thw
LenientErrorHandler has been modified so that this one case
will result in a DataFormatException. This has the effect
that servers which receive an invalid enum velue will return
an HTTP 400 instead of an HTTP 500. Thanks to Jim
Steel for reporting!
</action>
</release>
<release version="2.1" date="2016-11-11">
<action type="add">

View File

@ -14,12 +14,6 @@
sections below.
</p>
<ul>
<li>
<b>Resource Validation</b>
is validation of the raw or parsed resource against
the official FHIR validation rules (e.g. Schema/Schematron/Profile/StructureDefinition/ValueSet)
as well as against custom profiles which have been developed.
</li>
<li>
<b>Parser Validation</b>
is validation at runtime during the parsing
@ -31,10 +25,77 @@
that no data is being lost during parsing, but is less comprehensive than resource validation
against raw text data.
</li>
<li>
<b>Resource Validation</b>
is validation of the raw or parsed resource against
the official FHIR validation rules (e.g. Schema/Schematron/Profile/StructureDefinition/ValueSet)
as well as against custom profiles which have been developed.
</li>
</ul>
</section>
<!-- Parser Validation -->
<section name="Parser Validation">
<p>
Parser validation is controlled by calling
<code>setParserErrorHandler(IParserErrorHandler)</code>
on
either the FhirContext or on individual parser instances. This method
takes an
<code>IParserErrorHandler</code>
, which is a callback that
will be invoked any time a parse issue is detected.
</p>
<p>
There are two implementations of
<code>IParserErrorHandler</code>
worth
mentioning. You can also supply your own implementation if you want.
</p>
<ul>
<li>
<a href="./apidocs/ca/uhn/fhir/parser/LenientErrorHandler.html">LenientErrorHandler</a>
logs any errors but does not abort parsing. By default this handler is used, and it
logs errors at "warning" level. It can also be configured to silently
ignore issues.
</li>
<li>
<a href="./apidocs/ca/uhn/fhir/parser/StrictErrorHandler.html">StrictErrorHandler</a>
throws a
<code>DataFormatException</code>
if any errors are detected.
</li>
</ul>
<p>
The following example shows how to configure a parser to use strict validation.
</p>
<macro name="snippet">
<param name="id" value="parserValidation" />
<param name="file" value="examples/src/main/java/example/ValidatorExamples.java" />
</macro>
<p>
You can also configure the error handler at the FhirContext level, which is useful
for clients.
</p>
<macro name="snippet">
<param name="id" value="clientValidation" />
<param name="file" value="examples/src/main/java/example/ValidatorExamples.java" />
</macro>
<p>
FhirContext level validators can also be useful on servers.
</p>
<macro name="snippet">
<param name="id" value="serverValidation" />
<param name="file" value="examples/src/main/java/example/ValidatorExamples.java" />
</macro>
</section>
<!-- RESOURCE VALIDATION -->
<section name="Resource Validation">
@ -223,66 +284,6 @@
</section>
<section name="Parser Validation">
<p>
Parser validation is controlled by calling
<code>setParserErrorHandler(IParserErrorHandler)</code>
on
either the FhirContext or on individual parser instances. This method
takes an
<code>IParserErrorHandler</code>
, which is a callback that
will be invoked any time a parse issue is detected.
</p>
<p>
There are two implementations of
<code>IParserErrorHandler</code>
worth
mentioning. You can also supply your own implementation if you want.
</p>
<ul>
<li>
<a href="./apidocs/ca/uhn/fhir/parser/LenientErrorHandler.html">LenientErrorHandler</a>
logs any errors but does not abort parsing. By default this handler is used, and it
logs errors at "warning" level. It can also be configured to silently
ignore issues.
</li>
<li>
<a href="./apidocs/ca/uhn/fhir/parser/StrictErrorHandler.html">StrictErrorHandler</a>
throws a
<code>DataFormatException</code>
if any errors are detected.
</li>
</ul>
<p>
The following example shows how to configure a parser to use strict validation.
</p>
<macro name="snippet">
<param name="id" value="parserValidation" />
<param name="file" value="examples/src/main/java/example/ValidatorExamples.java" />
</macro>
<p>
You can also configure the error handler at the FhirContext level, which is useful
for clients.
</p>
<macro name="snippet">
<param name="id" value="clientValidation" />
<param name="file" value="examples/src/main/java/example/ValidatorExamples.java" />
</macro>
<p>
FhirContext level validators can also be useful on servers.
</p>
<macro name="snippet">
<param name="id" value="serverValidation" />
<param name="file" value="examples/src/main/java/example/ValidatorExamples.java" />
</macro>
</section>
</body>
</document>