Merge remote-tracking branch 'upstream/master'

This commit is contained in:
Bill Denton 2016-12-10 12:46:18 -08:00
commit c544dd4053
22 changed files with 589 additions and 192 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

@ -2309,7 +2309,13 @@ class ParserState<T> {
if ("".equals(theValue)) {
myErrorHandler.invalidValue(null, theValue, "Attribute values must not be empty (\"\")");
} else {
myInstance.setValueAsString(theValue);
try {
myInstance.setValueAsString(theValue);
} catch (DataFormatException e) {
myErrorHandler.invalidValue(null, theValue, e.getMessage());
} catch (IllegalArgumentException e) {
myErrorHandler.invalidValue(null, theValue, e.getMessage());
}
}
} else if ("id".equals(theName)) {
if (myInstance instanceof IIdentifiableElement) {

View File

@ -77,6 +77,15 @@ public class RestfulServerUtils {
private static final HashSet<String> TEXT_ENCODE_ELEMENTS = new HashSet<String>(Arrays.asList("Bundle", "*.text", "*.(mandatory)"));
private static String actualDstu3FhirVersion = FhirVersionEnum.DSTU3.getFhirVersionString();
static {
try {
Class c = Class.forName("org.hl7.fhir.dstu3.model.Constants");
actualDstu3FhirVersion = (String) c.getDeclaredField("VERSION").get(null);
} catch (Exception e) {
}
}
public static void configureResponseParser(RequestDetails theRequestDetails, IParser parser) {
// Pretty print
boolean prettyPrint = RestfulServerUtils.prettyPrintResponse(theRequestDetails.getServer(), theRequestDetails);
@ -757,12 +766,16 @@ public class RestfulServerUtils {
myEncoding = theEncoding;
if (theContentType != null) {
if (theContentType.equals(EncodingEnum.JSON_PLAIN_STRING) || theContentType.equals(EncodingEnum.XML_PLAIN_STRING)) {
myNonLegacy = !theCtx.getVersion().getVersion().isOlderThan(FhirVersionEnum.DSTU3);
FhirVersionEnum ctxtEnum = theCtx.getVersion().getVersion();
myNonLegacy = ctxtEnum.isNewerThan(FhirVersionEnum.DSTU3)
|| (ctxtEnum.isEquivalentTo(FhirVersionEnum.DSTU3) && !"1.4.0".equals(actualDstu3FhirVersion));
} else {
myNonLegacy = EncodingEnum.isNonLegacy(theContentType);
}
} else {
if (theCtx.getVersion().getVersion().isOlderThan(FhirVersionEnum.DSTU3)) {
FhirVersionEnum ctxtEnum = theCtx.getVersion().getVersion();
if (ctxtEnum.isOlderThan(FhirVersionEnum.DSTU3)
|| (ctxtEnum.isEquivalentTo(FhirVersionEnum.DSTU3) && "1.4.0".equals(actualDstu3FhirVersion))) {
myNonLegacy = null;
} else {
myNonLegacy = Boolean.TRUE;

View File

@ -79,27 +79,7 @@ public class TestUtil {
}
/*
* Set some system properties randomly after each test.. this is kind of hackish,
* but it helps us make sure we don't have any tests that depend on a particular
* environment
*/
Locale[] availableLocales = { Locale.CANADA, Locale.GERMANY, Locale.TAIWAN };
Locale.setDefault(availableLocales[(int) (Math.random() * availableLocales.length)]);
ourLog.info("Tests are running in locale: " + Locale.getDefault().getDisplayName());
if (Math.random() < 0.5) {
ourLog.info("Tests are using WINDOWS line endings and ISO-8851-1");
System.setProperty("file.encoding", "ISO-8859-1");
System.setProperty("line.separator", "\r\n");
} else {
ourLog.info("Tests are using UNIX line endings and UTF-8");
System.setProperty("file.encoding", "UTF-8");
System.setProperty("line.separator", "\n");
}
String availableTimeZones[] = { "GMT+08:00", "GMT-05:00", "GMT+00:00", "GMT+03:30" };
String timeZone = availableTimeZones[(int) (Math.random() * availableTimeZones.length)];
TimeZone.setDefault(TimeZone.getTimeZone(timeZone));
ourLog.info("Tests are using time zone: {}", TimeZone.getDefault().getID());
randomizeLocale();
/*
* If we're running a CI build, set all loggers to TRACE level to ensure coverage
@ -116,4 +96,28 @@ public class TestUtil {
}
}
/**
* Set some system properties randomly after each test.. this is kind of hackish,
* but it helps us make sure we don't have any tests that depend on a particular
* environment
*/
public static void randomizeLocale() {
Locale[] availableLocales = { Locale.CANADA, Locale.GERMANY, Locale.TAIWAN };
Locale.setDefault(availableLocales[(int) (Math.random() * availableLocales.length)]);
ourLog.info("Tests are running in locale: " + Locale.getDefault().getDisplayName());
if (Math.random() < 0.5) {
ourLog.info("Tests are using WINDOWS line endings and ISO-8851-1");
System.setProperty("file.encoding", "ISO-8859-1");
System.setProperty("line.separator", "\r\n");
} else {
ourLog.info("Tests are using UNIX line endings and UTF-8");
System.setProperty("file.encoding", "UTF-8");
System.setProperty("line.separator", "\n");
}
String availableTimeZones[] = { "GMT+08:00", "GMT-05:00", "GMT+00:00", "GMT+03:30" };
String timeZone = availableTimeZones[(int) (Math.random() * availableTimeZones.length)];
TimeZone.setDefault(TimeZone.getTimeZone(timeZone));
ourLog.info("Tests are using time zone: {}", TimeZone.getDefault().getID());
}
}

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;
@ -92,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"));
}
@ -121,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());
}
/**
@ -1193,6 +1239,78 @@ 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");
}
@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

@ -93,15 +93,42 @@
to using Thymeleaf 3. Thanks to GitHub user @gsureshkumar
for reporting!
</action>
<action type="add" issue="525">
When parsing invalid enum values in STU3,
report errors through the parserErrorHandler,
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>
<action type="add">
Enhancements to the tinder-plugin's generic template features
of the "generate-multi-files" and "generate-single-file"
Maven goals as well as the Ant hapi-tinder task. Velocity
templates now have access to the full tinder data model
for resources and data types. These "generic" tasks can
now use Velocimacro library files outside the
tinder plugin JAR and can specify any of the Velocity
configuration properties such as 'macro.provide.scope.control'
of the <![CDATA[<i>generate-multi-files</i> and <i>generate-single-file</i>
Maven goals as well as the Ant <i>hapi-tinder</i> task.
<ul>
<li>Provides the full Tinder data model by adding composites, valuesets, and profiles to resourcesw.</li>
<li>Supports generating files for resources, composites, valuesets, and profiles</li>
<li>Supports Velocimacro files outside the tinder-plugin JAR</li>
<li>Provides filename prefix as well as suffix properties</li>
<li>Can specify any of the Velocity configuration parameters such as
<i>macro.provide.scope.control</i> which allows safe macro recursion</li>
<li>Templates can now drill down into the referenced children for a ResourceBlockCopy</li>
<li>Normalization of properties across all three generic tasks</li>
</ul>
]]>
</action>
</release>
<release version="2.1" date="2016-11-11">

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>