From 2f380704de013d146785f28df4ce9106aac4d2a5 Mon Sep 17 00:00:00 2001 From: James Agnew Date: Fri, 18 Oct 2019 10:47:20 -0400 Subject: [PATCH] Fix precision and validator issues --- .../fhir/dstu2/model/BaseDateTimeType.java | 69 +++++++------------ .../dstu2016may/model/BaseDateTimeType.java | 27 +------- .../fhir/dstu3/model/BaseDateTimeType.java | 27 +------- .../hl7/fhir/r4/model/BaseDateTimeType.java | 67 +++++++----------- .../hl7/fhir/r5/elementmodel/JsonParser.java | 7 +- .../hl7/fhir/r5/model/BaseDateTimeType.java | 27 +------- .../fhir/r5/model/BaseDateTimeTypeTest.java | 15 ++++ .../org/hl7/fhir/utilities/DateTimeUtil.java | 44 ++++++++++++ .../org/hl7/fhir/utilities/Utilities.java | 5 ++ .../validation/tests/ValidationTestSuite.java | 2 +- .../validation-examples/manifest.json | 6 ++ .../observation-with-trailing-dot.json | 28 ++++++++ 12 files changed, 160 insertions(+), 164 deletions(-) create mode 100644 org.hl7.fhir.utilities/src/main/java/org/hl7/fhir/utilities/DateTimeUtil.java create mode 100644 org.hl7.fhir.validation/src/test/resources/validation-examples/observation-with-trailing-dot.json diff --git a/org.hl7.fhir.dstu2/src/main/java/org/hl7/fhir/dstu2/model/BaseDateTimeType.java b/org.hl7.fhir.dstu2/src/main/java/org/hl7/fhir/dstu2/model/BaseDateTimeType.java index 95e0777d5..20537690c 100644 --- a/org.hl7.fhir.dstu2/src/main/java/org/hl7/fhir/dstu2/model/BaseDateTimeType.java +++ b/org.hl7.fhir.dstu2/src/main/java/org/hl7/fhir/dstu2/model/BaseDateTimeType.java @@ -25,6 +25,7 @@ import org.apache.commons.lang3.StringUtils; import org.apache.commons.lang3.Validate; import org.apache.commons.lang3.time.DateUtils; import org.apache.commons.lang3.time.FastDateFormat; +import org.hl7.fhir.utilities.DateTimeUtil; import java.text.ParseException; import java.util.*; @@ -472,53 +473,29 @@ public abstract class BaseDateTimeType extends PrimitiveType { return getValue().after(theDateTimeType.getValue()); } - /** - * Returns a human readable version of this date/time using the system local format. - *

- * Note on time zones: This method renders the value using the time zone - * that is contained within the value. For example, if this date object contains the - * value "2012-01-05T12:00:00-08:00", the human display will be rendered as "12:00:00" - * even if the application is being executed on a system in a different time zone. If - * this behaviour is not what you want, use {@link #toHumanDisplayLocalTimezone()} - * instead. - *

- */ - public String toHumanDisplay() { - TimeZone tz = getTimeZone(); - Calendar value = tz != null ? Calendar.getInstance(tz) : Calendar.getInstance(); - value.setTime(getValue()); + /** + * Returns a human readable version of this date/time using the system local format. + *

+ * Note on time zones: This method renders the value using the time zone that is contained within the value. + * For example, if this date object contains the value "2012-01-05T12:00:00-08:00", + * the human display will be rendered as "12:00:00" even if the application is being executed on a system in a + * different time zone. If this behaviour is not what you want, use + * {@link #toHumanDisplayLocalTimezone()} instead. + *

+ */ + public String toHumanDisplay() { + return DateTimeUtil.toHumanDisplay(getTimeZone(), getPrecision(), getValue(), getValueAsString()); + } - switch (getPrecision()) { - case YEAR: - case MONTH: - case DAY: - return ourHumanDateFormat.format(value); - case MILLI: - case SECOND: - default: - return ourHumanDateTimeFormat.format(value); - } - } - - /** - * Returns a human readable version of this date/time using the system local format, - * converted to the local timezone if neccesary. - * - * @see #toHumanDisplay() for a method which does not convert the time to the local - * timezone before rendering it. - */ - public String toHumanDisplayLocalTimezone() { - switch (getPrecision()) { - case YEAR: - case MONTH: - case DAY: - return ourHumanDateFormat.format(getValue()); - case MILLI: - case SECOND: - default: - return ourHumanDateTimeFormat.format(getValue()); - } - } + /** + * Returns a human readable version of this date/time using the system local format, converted to the local timezone + * if neccesary. + * + * @see #toHumanDisplay() for a method which does not convert the time to the local timezone before rendering it. + */ + public String toHumanDisplayLocalTimezone() { + return DateTimeUtil.toHumanDisplayLocalTimezone(getPrecision(), getValue(), getValueAsString()); + } /** diff --git a/org.hl7.fhir.dstu2016may/src/main/java/org/hl7/fhir/dstu2016may/model/BaseDateTimeType.java b/org.hl7.fhir.dstu2016may/src/main/java/org/hl7/fhir/dstu2016may/model/BaseDateTimeType.java index 46feca76f..d3029abcc 100644 --- a/org.hl7.fhir.dstu2016may/src/main/java/org/hl7/fhir/dstu2016may/model/BaseDateTimeType.java +++ b/org.hl7.fhir.dstu2016may/src/main/java/org/hl7/fhir/dstu2016may/model/BaseDateTimeType.java @@ -34,6 +34,7 @@ import org.apache.commons.lang3.time.DateUtils; import org.apache.commons.lang3.time.FastDateFormat; import ca.uhn.fhir.parser.DataFormatException; +import org.hl7.fhir.utilities.DateTimeUtil; public abstract class BaseDateTimeType extends PrimitiveType { @@ -488,20 +489,7 @@ public abstract class BaseDateTimeType extends PrimitiveType { *

*/ public String toHumanDisplay() { - TimeZone tz = getTimeZone(); - Calendar value = tz != null ? Calendar.getInstance(tz) : Calendar.getInstance(); - value.setTime(getValue()); - - switch (getPrecision()) { - case YEAR: - case MONTH: - case DAY: - return ourHumanDateFormat.format(value); - case MILLI: - case SECOND: - default: - return ourHumanDateTimeFormat.format(value); - } + return DateTimeUtil.toHumanDisplay(getTimeZone(), getPrecision(), getValue(), getValueAsString()); } /** @@ -511,16 +499,7 @@ public abstract class BaseDateTimeType extends PrimitiveType { * @see #toHumanDisplay() for a method which does not convert the time to the local timezone before rendering it. */ public String toHumanDisplayLocalTimezone() { - switch (getPrecision()) { - case YEAR: - case MONTH: - case DAY: - return ourHumanDateFormat.format(getValue()); - case MILLI: - case SECOND: - default: - return ourHumanDateTimeFormat.format(getValue()); - } + return DateTimeUtil.toHumanDisplayLocalTimezone(getPrecision(), getValue(), getValueAsString()); } private void validateCharAtIndexIs(String theValue, int theIndex, char theChar) { diff --git a/org.hl7.fhir.dstu3/src/main/java/org/hl7/fhir/dstu3/model/BaseDateTimeType.java b/org.hl7.fhir.dstu3/src/main/java/org/hl7/fhir/dstu3/model/BaseDateTimeType.java index 2e23ce8c8..eda2edd0a 100644 --- a/org.hl7.fhir.dstu3/src/main/java/org/hl7/fhir/dstu3/model/BaseDateTimeType.java +++ b/org.hl7.fhir.dstu3/src/main/java/org/hl7/fhir/dstu3/model/BaseDateTimeType.java @@ -34,6 +34,7 @@ import org.apache.commons.lang3.time.FastDateFormat; import ca.uhn.fhir.model.api.TemporalPrecisionEnum; import ca.uhn.fhir.parser.DataFormatException; +import org.hl7.fhir.utilities.DateTimeUtil; public abstract class BaseDateTimeType extends PrimitiveType { @@ -781,20 +782,7 @@ public abstract class BaseDateTimeType extends PrimitiveType { *

*/ public String toHumanDisplay() { - TimeZone tz = getTimeZone(); - Calendar value = tz != null ? Calendar.getInstance(tz) : Calendar.getInstance(); - value.setTime(getValue()); - - switch (getPrecision()) { - case YEAR: - case MONTH: - case DAY: - return ourHumanDateFormat.format(value); - case MILLI: - case SECOND: - default: - return ourHumanDateTimeFormat.format(value); - } + return DateTimeUtil.toHumanDisplay(getTimeZone(), getPrecision(), getValue(), getValueAsString()); } /** @@ -804,16 +792,7 @@ public abstract class BaseDateTimeType extends PrimitiveType { * @see #toHumanDisplay() for a method which does not convert the time to the local timezone before rendering it. */ public String toHumanDisplayLocalTimezone() { - switch (getPrecision()) { - case YEAR: - case MONTH: - case DAY: - return ourHumanDateFormat.format(getValue()); - case MILLI: - case SECOND: - default: - return ourHumanDateTimeFormat.format(getValue()); - } + return DateTimeUtil.toHumanDisplayLocalTimezone(getPrecision(), getValue(), getValueAsString()); } private void validateBeforeOrAfter(DateTimeType theDateTimeType) { diff --git a/org.hl7.fhir.r4/src/main/java/org/hl7/fhir/r4/model/BaseDateTimeType.java b/org.hl7.fhir.r4/src/main/java/org/hl7/fhir/r4/model/BaseDateTimeType.java index 715ac7720..b71c2b13c 100644 --- a/org.hl7.fhir.r4/src/main/java/org/hl7/fhir/r4/model/BaseDateTimeType.java +++ b/org.hl7.fhir.r4/src/main/java/org/hl7/fhir/r4/model/BaseDateTimeType.java @@ -34,6 +34,7 @@ import org.apache.commons.lang3.time.DateUtils; import org.apache.commons.lang3.time.FastDateFormat; import ca.uhn.fhir.parser.DataFormatException; +import org.hl7.fhir.utilities.DateTimeUtil; public abstract class BaseDateTimeType extends PrimitiveType { @@ -771,51 +772,29 @@ public abstract class BaseDateTimeType extends PrimitiveType { return retVal; } - /** - * Returns a human readable version of this date/time using the system local format. - *

- * Note on time zones: This method renders the value using the time zone that is contained within the value. - * For example, if this date object contains the value "2012-01-05T12:00:00-08:00", - * the human display will be rendered as "12:00:00" even if the application is being executed on a system in a - * different time zone. If this behaviour is not what you want, use - * {@link #toHumanDisplayLocalTimezone()} instead. - *

- */ - public String toHumanDisplay() { - TimeZone tz = getTimeZone(); - Calendar value = tz != null ? Calendar.getInstance(tz) : Calendar.getInstance(); - value.setTime(getValue()); + /** + * Returns a human readable version of this date/time using the system local format. + *

+ * Note on time zones: This method renders the value using the time zone that is contained within the value. + * For example, if this date object contains the value "2012-01-05T12:00:00-08:00", + * the human display will be rendered as "12:00:00" even if the application is being executed on a system in a + * different time zone. If this behaviour is not what you want, use + * {@link #toHumanDisplayLocalTimezone()} instead. + *

+ */ + public String toHumanDisplay() { + return DateTimeUtil.toHumanDisplay(getTimeZone(), getPrecision(), getValue(), getValueAsString()); + } - switch (getPrecision()) { - case YEAR: - case MONTH: - case DAY: - return ourHumanDateFormat.format(value); - case MILLI: - case SECOND: - default: - return ourHumanDateTimeFormat.format(value); - } - } - - /** - * Returns a human readable version of this date/time using the system local format, converted to the local timezone - * if neccesary. - * - * @see #toHumanDisplay() for a method which does not convert the time to the local timezone before rendering it. - */ - public String toHumanDisplayLocalTimezone() { - switch (getPrecision()) { - case YEAR: - case MONTH: - case DAY: - return ourHumanDateFormat.format(getValue()); - case MILLI: - case SECOND: - default: - return ourHumanDateTimeFormat.format(getValue()); - } - } + /** + * Returns a human readable version of this date/time using the system local format, converted to the local timezone + * if neccesary. + * + * @see #toHumanDisplay() for a method which does not convert the time to the local timezone before rendering it. + */ + public String toHumanDisplayLocalTimezone() { + return DateTimeUtil.toHumanDisplayLocalTimezone(getPrecision(), getValue(), getValueAsString()); + } private void validateBeforeOrAfter(DateTimeType theDateTimeType) { if (getValue() == null) { diff --git a/org.hl7.fhir.r5/src/main/java/org/hl7/fhir/r5/elementmodel/JsonParser.java b/org.hl7.fhir.r5/src/main/java/org/hl7/fhir/r5/elementmodel/JsonParser.java index 4fb4b1433..32215c6f4 100644 --- a/org.hl7.fhir.r5/src/main/java/org/hl7/fhir/r5/elementmodel/JsonParser.java +++ b/org.hl7.fhir.r5/src/main/java/org/hl7/fhir/r5/elementmodel/JsonParser.java @@ -273,7 +273,12 @@ public class JsonParser extends ParserBase { context.getChildren().add(n); if (main != null) { JsonPrimitive p = (JsonPrimitive) main; - n.setValue(p.getAsString()); + if (p.isNumber() && p.getAsNumber() instanceof JsonTrackingParser.PresentedBigDecimal) { + String rawValue = ((JsonTrackingParser.PresentedBigDecimal) p.getAsNumber()).getPresentation(); + n.setValue(rawValue); + } else { + n.setValue(p.getAsString()); + } if (!n.getProperty().isChoice() && n.getType().equals("xhtml")) { try { n.setXhtml(new XhtmlParser().setValidatorMode(policy == ValidationPolicy.EVERYTHING).parse(n.getValue(), null).getDocumentElement()); diff --git a/org.hl7.fhir.r5/src/main/java/org/hl7/fhir/r5/model/BaseDateTimeType.java b/org.hl7.fhir.r5/src/main/java/org/hl7/fhir/r5/model/BaseDateTimeType.java index ff691cf83..25698ace9 100644 --- a/org.hl7.fhir.r5/src/main/java/org/hl7/fhir/r5/model/BaseDateTimeType.java +++ b/org.hl7.fhir.r5/src/main/java/org/hl7/fhir/r5/model/BaseDateTimeType.java @@ -26,6 +26,7 @@ import org.apache.commons.lang3.StringUtils; import org.apache.commons.lang3.Validate; import org.apache.commons.lang3.time.DateUtils; import org.apache.commons.lang3.time.FastDateFormat; +import org.hl7.fhir.utilities.DateTimeUtil; import java.util.Calendar; import java.util.Date; @@ -781,20 +782,7 @@ public abstract class BaseDateTimeType extends PrimitiveType { *

*/ public String toHumanDisplay() { - TimeZone tz = getTimeZone(); - Calendar value = tz != null ? Calendar.getInstance(tz) : Calendar.getInstance(); - value.setTime(getValue()); - - switch (getPrecision()) { - case YEAR: - case MONTH: - case DAY: - return ourHumanDateFormat.format(value); - case MILLI: - case SECOND: - default: - return ourHumanDateTimeFormat.format(value); - } + return DateTimeUtil.toHumanDisplay(getTimeZone(), getPrecision(), getValue(), getValueAsString()); } /** @@ -804,16 +792,7 @@ public abstract class BaseDateTimeType extends PrimitiveType { * @see #toHumanDisplay() for a method which does not convert the time to the local timezone before rendering it. */ public String toHumanDisplayLocalTimezone() { - switch (getPrecision()) { - case YEAR: - case MONTH: - case DAY: - return ourHumanDateFormat.format(getValue()); - case MILLI: - case SECOND: - default: - return ourHumanDateTimeFormat.format(getValue()); - } + return DateTimeUtil.toHumanDisplayLocalTimezone(getPrecision(), getValue(), getValueAsString()); } private void validateBeforeOrAfter(DateTimeType theDateTimeType) { diff --git a/org.hl7.fhir.r5/src/test/java/org/hl7/fhir/r5/model/BaseDateTimeTypeTest.java b/org.hl7.fhir.r5/src/test/java/org/hl7/fhir/r5/model/BaseDateTimeTypeTest.java index 6fc50b080..9b24998d0 100644 --- a/org.hl7.fhir.r5/src/test/java/org/hl7/fhir/r5/model/BaseDateTimeTypeTest.java +++ b/org.hl7.fhir.r5/src/test/java/org/hl7/fhir/r5/model/BaseDateTimeTypeTest.java @@ -78,6 +78,21 @@ public class BaseDateTimeTypeTest { assertFalse(compareDateTimes("2016-12-02T13:00:00Z", "2016-12-02T10:00:00")); // no timezone, might be the same time } + @Test + public void testToHumanDisplayForDateOnlyPrecisions() { + assertEquals("2019-01-02", new DateTimeType("2019-01-02").toHumanDisplay()); + assertEquals("2019-01", new DateTimeType("2019-01").toHumanDisplay()); + assertEquals("2019", new DateTimeType("2019").toHumanDisplay()); + } + + @Test + public void testToHumanDisplayLocalTimezoneForDateOnlyPrecisions() { + assertEquals("2019-01-02", new DateTimeType("2019-01-02").toHumanDisplayLocalTimezone()); + assertEquals("2019-01", new DateTimeType("2019-01").toHumanDisplayLocalTimezone()); + assertEquals("2019", new DateTimeType("2019").toHumanDisplayLocalTimezone()); + } + + private Boolean compareDateTimes(String theLeft, String theRight) { System.out.println("Compare "+theLeft+" to "+theRight); DateTimeType leftDt = new DateTimeType(theLeft); diff --git a/org.hl7.fhir.utilities/src/main/java/org/hl7/fhir/utilities/DateTimeUtil.java b/org.hl7.fhir.utilities/src/main/java/org/hl7/fhir/utilities/DateTimeUtil.java new file mode 100644 index 000000000..fbc513049 --- /dev/null +++ b/org.hl7.fhir.utilities/src/main/java/org/hl7/fhir/utilities/DateTimeUtil.java @@ -0,0 +1,44 @@ +package org.hl7.fhir.utilities; + +import ca.uhn.fhir.model.api.TemporalPrecisionEnum; +import org.apache.commons.lang3.time.FastDateFormat; + +import java.util.Calendar; +import java.util.Date; +import java.util.TimeZone; + +public class DateTimeUtil { + private static final FastDateFormat ourHumanDateFormat = FastDateFormat.getDateInstance(FastDateFormat.MEDIUM); + private static final FastDateFormat ourHumanDateTimeFormat = FastDateFormat.getDateTimeInstance(FastDateFormat.MEDIUM, FastDateFormat.MEDIUM); + + + public static String toHumanDisplay(TimeZone theTimeZone, TemporalPrecisionEnum thePrecision, Date theValue, String theValueAsString) { + Calendar value = theTimeZone != null ? Calendar.getInstance(theTimeZone) : Calendar.getInstance(); + value.setTime(theValue); + + switch (thePrecision) { + case YEAR: + case MONTH: + case DAY: + return theValueAsString; + case MILLI: + case SECOND: + default: + return ourHumanDateTimeFormat.format(value); + } + + } + + public static String toHumanDisplayLocalTimezone(TemporalPrecisionEnum thePrecision, Date theValue, String theValueAsString) { + switch (thePrecision) { + case YEAR: + case MONTH: + case DAY: + return theValueAsString; + case MILLI: + case SECOND: + default: + return ourHumanDateTimeFormat.format(theValue); + } + } +} diff --git a/org.hl7.fhir.utilities/src/main/java/org/hl7/fhir/utilities/Utilities.java b/org.hl7.fhir.utilities/src/main/java/org/hl7/fhir/utilities/Utilities.java index 3c5ee8019..019eced4d 100644 --- a/org.hl7.fhir.utilities/src/main/java/org/hl7/fhir/utilities/Utilities.java +++ b/org.hl7.fhir.utilities/src/main/java/org/hl7/fhir/utilities/Utilities.java @@ -179,6 +179,11 @@ public class Utilities { if (value.startsWith("+0") && !"+0".equals(value) && !value.startsWith("+0.")) return DecimalStatus.SYNTAX; } + + // check for trailing dot + if (value.endsWith(".")) { + return DecimalStatus.SYNTAX; + } boolean havePeriod = false; boolean haveExponent = false; diff --git a/org.hl7.fhir.validation/src/test/java/org/hl7/fhir/validation/tests/ValidationTestSuite.java b/org.hl7.fhir.validation/src/test/java/org/hl7/fhir/validation/tests/ValidationTestSuite.java index db6d0d100..732eff94c 100644 --- a/org.hl7.fhir.validation/src/test/java/org/hl7/fhir/validation/tests/ValidationTestSuite.java +++ b/org.hl7.fhir.validation/src/test/java/org/hl7/fhir/validation/tests/ValidationTestSuite.java @@ -67,7 +67,7 @@ import com.google.gson.JsonObject; public class ValidationTestSuite implements IEvaluationContext, IValidatorResourceFetcher { @Parameters(name = "{index}: id {0}") - public static Iterable data() throws ParserConfigurationException, SAXException, IOException { + public static Iterable data() throws IOException { Map examples = new HashMap(); JsonObject json = (JsonObject) new com.google.gson.JsonParser().parse(TextFile.fileToString(TestUtilities.resourceNameToFile("validation-examples", "manifest.json"))); diff --git a/org.hl7.fhir.validation/src/test/resources/validation-examples/manifest.json b/org.hl7.fhir.validation/src/test/resources/validation-examples/manifest.json index ffe981436..2eafd7a77 100644 --- a/org.hl7.fhir.validation/src/test/resources/validation-examples/manifest.json +++ b/org.hl7.fhir.validation/src/test/resources/validation-examples/manifest.json @@ -148,6 +148,12 @@ "/List/text/div (line 7, col4) Error parsing XHTML: Malformed XHTML: Found a DocType declaration, and these are not allowed (XXE security vulnerability protection) " ] }, + "observation-with-trailing-dot.json": { + "errorCount": 1, + "errors": [ + "ERROR: Observation.referenceRange[0].high.value: The value '925.' is not a valid decimal" + ] + }, "synthea.json": { "version": "4.0", "errorCount": 2, diff --git a/org.hl7.fhir.validation/src/test/resources/validation-examples/observation-with-trailing-dot.json b/org.hl7.fhir.validation/src/test/resources/validation-examples/observation-with-trailing-dot.json new file mode 100644 index 000000000..eede19316 --- /dev/null +++ b/org.hl7.fhir.validation/src/test/resources/validation-examples/observation-with-trailing-dot.json @@ -0,0 +1,28 @@ +{ + "resourceType": "Observation", + "status": "final", + "subject": { + "reference": "Patient/123" + }, + "code": { + "coding": [ + { + "system": "http://foo", + "code": "123" + } + ] + }, + "referenceRange": [ + { + "low": { + "value": 210.0, + "unit": "pg/mL" + }, + "high": { + "value": 925., + "unit": "pg/mL" + }, + "text": "The 'high' value above has an invalid number with a trailing dot" + } + ] +} \ No newline at end of file