Don't parse 1974-12-25+10:00 as this is not a valid FHIR time

This commit is contained in:
jamesagnew 2016-04-03 18:45:08 -04:00
parent 20e04a7c80
commit 45390ebc89
7 changed files with 283 additions and 235 deletions

View File

@ -87,54 +87,11 @@ public abstract class BaseDateTimeDt extends BasePrimitive<Date> {
ourFormatters = Collections.unmodifiableList(formatters);
}
private TemporalPrecisionEnum myPrecision = TemporalPrecisionEnum.SECOND;
private TimeZone myTimeZone;
private boolean myTimeZoneZulu = false;
private static final org.slf4j.Logger ourLog = org.slf4j.LoggerFactory.getLogger(BaseDateTimeDt.class);
/**
* Returns a human readable version of this date/time using the system local format.
* <p>
* <b>Note on time zones:</b> 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.
* </p>
*/
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);
}
}
/**
* 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());
}
}
private TemporalPrecisionEnum myPrecision = TemporalPrecisionEnum.SECOND;
private TimeZone myTimeZone;
private boolean myTimeZoneZulu = false;
/**
* Constructor
@ -156,6 +113,14 @@ public abstract class BaseDateTimeDt extends BasePrimitive<Date> {
}
}
/**
* Constructor
*/
public BaseDateTimeDt(Date theDate, TemporalPrecisionEnum thePrecision, TimeZone theTimeZone) {
this(theDate, thePrecision);
setTimeZone(theTimeZone);
}
/**
* Constructor
*
@ -169,14 +134,6 @@ public abstract class BaseDateTimeDt extends BasePrimitive<Date> {
}
}
/**
* Constructor
*/
public BaseDateTimeDt(Date theDate, TemporalPrecisionEnum thePrecision, TimeZone theTimeZone) {
this(theDate, thePrecision);
setTimeZone(theTimeZone);
}
private void clearTimeZone() {
myTimeZone = null;
myTimeZoneZulu = false;
@ -332,6 +289,10 @@ public abstract class BaseDateTimeDt extends BasePrimitive<Date> {
clearTimeZone();
return ((ourYearMonthDayFormat).parse(theValue));
} else if (theValue.length() >= 18) { // date and time with possible time zone
char timeSeparator = theValue.charAt(10);
if (timeSeparator != 'T') {
throw new DataFormatException("Invalid date/time string: " + theValue);
}
int dotIndex = theValue.indexOf('.', 18);
boolean hasMillis = dotIndex > -1;
@ -454,6 +415,49 @@ public abstract class BaseDateTimeDt extends BasePrimitive<Date> {
super.setValueAsString(theValue);
}
/**
* Returns a human readable version of this date/time using the system local format.
* <p>
* <b>Note on time zones:</b> 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.
* </p>
*/
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);
}
}
/**
* 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());
}
}
/**
* For unit tests only
*/

View File

@ -1,7 +1,10 @@
package ca.uhn.fhir.context;
import static org.junit.Assert.*;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.fail;
import org.junit.Ignore;
import org.junit.Test;
import ca.uhn.fhir.model.api.annotation.Compartment;
@ -76,7 +79,11 @@ public class ModelScannerDstu1Test {
}
/**
* TODO: re-enable this when Claim compartments are fixed
*/
@Test
@Ignore
public void testSearchParamWithCompartmentForNonReferenceParam() {
try {
FhirContext.forDstu1().getResourceDefinition(CompartmentForNonReferenceParam.class);

View File

@ -1,7 +1,9 @@
package ca.uhn.fhir.model.primitive;
import static org.hamcrest.Matchers.*;
import static org.junit.Assert.*;
import static org.hamcrest.Matchers.containsString;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertThat;
import static org.junit.Assert.fail;
import java.text.SimpleDateFormat;
import java.util.Calendar;
@ -25,6 +27,24 @@ public class BaseDateTimeDtDstu2Test {
myDateInstantParser = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss.SSS");
}
@Test
public void testParseInvalid() {
try {
DateTimeDt dt = new DateTimeDt();
dt.setValueAsString("1974-12-25+10:00");
fail();
} catch (ca.uhn.fhir.parser.DataFormatException e) {
assertEquals("Invalid date/time string (invalid length): 1974-12-25+10:00", e.getMessage());
}
try {
DateTimeDt dt = new DateTimeDt();
dt.setValueAsString("1974-12-25Z");
fail();
} catch (ca.uhn.fhir.parser.DataFormatException e) {
assertEquals("Invalid date/time string (invalid length): 1974-12-25Z", e.getMessage());
}
}
/**
* See HAPI #101 - https://github.com/jamesagnew/hapi-fhir/issues/101
*/

View File

@ -19,6 +19,8 @@ import org.apache.commons.lang3.Validate;
import org.apache.commons.lang3.time.DateUtils;
import org.apache.commons.lang3.time.FastDateFormat;
import ca.uhn.fhir.parser.DataFormatException;
public abstract class BaseDateTimeType extends PrimitiveType<Date> {
private static final long serialVersionUID = 1L;
@ -85,7 +87,7 @@ public abstract class BaseDateTimeType extends PrimitiveType<Date> {
/**
* Constructor
*
* @throws IllegalArgumentException
* @throws DataFormatException
* If the specified precision is not allowed for this type
*/
public BaseDateTimeType(Date theDate, TemporalPrecisionEnum thePrecision) {
@ -95,6 +97,14 @@ public abstract class BaseDateTimeType extends PrimitiveType<Date> {
}
}
/**
* Constructor
*/
public BaseDateTimeType(Date theDate, TemporalPrecisionEnum thePrecision, TimeZone theTimeZone) {
this(theDate, thePrecision);
setTimeZone(theTimeZone);
}
/**
* Constructor
*
@ -109,11 +119,47 @@ public abstract class BaseDateTimeType extends PrimitiveType<Date> {
}
/**
* Constructor
* Adds the given amount to the field specified by theField
*
* @param theField
* The field, uses constants from {@link Calendar} such as {@link Calendar#YEAR}
* @param theValue
* The number to add (or subtract for a negative number)
*/
public BaseDateTimeType(Date theDate, TemporalPrecisionEnum thePrecision, TimeZone theTimeZone) {
this(theDate, thePrecision);
setTimeZone(theTimeZone);
public void add(int theField, int theValue) {
switch (theField) {
case Calendar.YEAR:
setValue(DateUtils.addYears(getValue(), theValue), getPrecision());
break;
case Calendar.MONTH:
setValue(DateUtils.addMonths(getValue(), theValue), getPrecision());
break;
case Calendar.DATE:
setValue(DateUtils.addDays(getValue(), theValue), getPrecision());
break;
case Calendar.HOUR:
setValue(DateUtils.addHours(getValue(), theValue), getPrecision());
break;
case Calendar.MINUTE:
setValue(DateUtils.addMinutes(getValue(), theValue), getPrecision());
break;
case Calendar.SECOND:
setValue(DateUtils.addSeconds(getValue(), theValue), getPrecision());
break;
case Calendar.MILLISECOND:
setValue(DateUtils.addMilliseconds(getValue(), theValue), getPrecision());
break;
default:
throw new DataFormatException("Unknown field constant: " + theField);
}
}
public boolean after(DateTimeType theDateTimeType) {
return getValue().after(theDateTimeType.getValue());
}
public boolean before(DateTimeType theDateTimeType) {
return getValue().before(theDateTimeType.getValue());
}
private void clearTimeZone() {
@ -183,6 +229,13 @@ public abstract class BaseDateTimeType extends PrimitiveType<Date> {
return myPrecision;
}
/**
* Returns the time in millis as represented by this Date/Time
*/
public long getTime() {
return getValue().getTime();
}
/**
* Returns the TimeZone associated with this dateTime's value. May return <code>null</code> if no timezone was
* supplied.
@ -277,9 +330,14 @@ public abstract class BaseDateTimeType extends PrimitiveType<Date> {
clearTimeZone();
return ((ourYearMonthDayFormat).parse(theValue));
} else if (theValue.length() >= 16) { // date and time with possible time zone
char timeSeparator = theValue.charAt(10);
if (timeSeparator != 'T') {
throw new DataFormatException("Invalid date/time string: " + theValue);
}
int firstColonIndex = theValue.indexOf(':');
if (firstColonIndex == -1) {
throw new IllegalArgumentException("Invalid date/time string: " + theValue);
throw new DataFormatException("Invalid date/time string: " + theValue);
}
boolean hasSeconds = theValue.length() > firstColonIndex+3 ? theValue.charAt(firstColonIndex+3) == ':' : false;
@ -306,7 +364,7 @@ public abstract class BaseDateTimeType extends PrimitiveType<Date> {
retVal = ourYearMonthDayTimeMilliFormat.parse(theValue);
}
} catch (ParseException p2) {
throw new IllegalArgumentException("Invalid data/time string (" + p2.getMessage() + "): " + theValue);
throw new DataFormatException("Invalid data/time string (" + p2.getMessage() + "): " + theValue);
}
setTimeZone(theValue, hasMillis);
setPrecision(TemporalPrecisionEnum.MILLI);
@ -320,7 +378,7 @@ public abstract class BaseDateTimeType extends PrimitiveType<Date> {
retVal = ourYearMonthDayTimeFormat.parse(theValue);
}
} catch (ParseException p2) {
throw new IllegalArgumentException("Invalid data/time string (" + p2.getMessage() + "): " + theValue);
throw new DataFormatException("Invalid data/time string (" + p2.getMessage() + "): " + theValue);
}
setTimeZone(theValue, hasMillis);
@ -335,7 +393,7 @@ public abstract class BaseDateTimeType extends PrimitiveType<Date> {
retVal = ourYearMonthDayTimeMinsFormat.parse(theValue);
}
} catch (ParseException p2) {
throw new IllegalArgumentException("Invalid data/time string (" + p2.getMessage() + "): " + theValue, p2);
throw new DataFormatException("Invalid data/time string (" + p2.getMessage() + "): " + theValue, p2);
}
setTimeZone(theValue, hasMillis);
@ -344,10 +402,26 @@ public abstract class BaseDateTimeType extends PrimitiveType<Date> {
return retVal;
} else {
throw new IllegalArgumentException("Invalid date/time string (invalid length): " + theValue);
throw new DataFormatException("Invalid date/time string (invalid length): " + theValue);
}
} catch (ParseException e) {
throw new IllegalArgumentException("Invalid date string (" + e.getMessage() + "): " + theValue);
throw new DataFormatException("Invalid date string (" + e.getMessage() + "): " + theValue);
}
}
/**
* Sets the TimeZone offset in minutes relative to GMT
*/
public void setOffsetMinutes(int theZoneOffsetMinutes) {
int offsetAbs = Math.abs(theZoneOffsetMinutes);
int mins = offsetAbs % 60;
int hours = offsetAbs / 60;
if (theZoneOffsetMinutes < 0) {
setTimeZone(TimeZone.getTimeZone("GMT-" + hours + ":" + mins));
} else {
setTimeZone(TimeZone.getTimeZone("GMT+" + hours + ":" + mins));
}
}
@ -360,9 +434,9 @@ public abstract class BaseDateTimeType extends PrimitiveType<Date> {
* <li>{@link Calendar#YEAR}
* </ul>
*
* @throws IllegalArgumentException
* @throws DataFormatException
*/
public void setPrecision(TemporalPrecisionEnum thePrecision) throws IllegalArgumentException {
public void setPrecision(TemporalPrecisionEnum thePrecision) throws DataFormatException {
if (thePrecision == null) {
throw new NullPointerException("Precision may not be null");
}
@ -420,9 +494,9 @@ public abstract class BaseDateTimeType extends PrimitiveType<Date> {
* The date value
* @param thePrecision
* The precision
* @throws IllegalArgumentException
* @throws DataFormatException
*/
public void setValue(Date theValue, TemporalPrecisionEnum thePrecision) throws IllegalArgumentException {
public void setValue(Date theValue, TemporalPrecisionEnum thePrecision) throws DataFormatException {
if (myTimeZoneZulu == false && myTimeZone == null) {
myTimeZone = TimeZone.getDefault();
}
@ -431,24 +505,61 @@ public abstract class BaseDateTimeType extends PrimitiveType<Date> {
}
@Override
public void setValueAsString(String theValue) throws IllegalArgumentException {
public void setValueAsString(String theValue) throws DataFormatException {
clearTimeZone();
super.setValueAsString(theValue);
}
protected void setValueAsV3String(String theV3String) {
if (StringUtils.isBlank(theV3String)) {
setValue(null);
} else {
StringBuilder b = new StringBuilder();
String timeZone = null;
for (int i = 0; i < theV3String.length(); i++) {
char nextChar = theV3String.charAt(i);
if (nextChar == '+' || nextChar == '-' || nextChar == 'Z') {
timeZone = (theV3String.substring(i));
break;
}
// assertEquals("2013-02-02T20:13:03-05:00", DateAndTime.parseV3("20130202201303-0500").toString());
if (i == 4 || i == 6) {
b.append('-');
} else if (i == 8) {
b.append('T');
} else if (i == 10 || i == 12) {
b.append(':');
}
b.append(nextChar);
}
if (b.length() == 16)
b.append(":00"); // schema rule, must have seconds
if (timeZone != null && b.length() > 10) {
if (timeZone.length() ==5) {
b.append(timeZone.substring(0, 3));
b.append(':');
b.append(timeZone.substring(3));
}else {
b.append(timeZone);
}
}
setValueAsString(b.toString());
}
}
/**
* For unit tests only
* Returns a view of this date/time as a Calendar object
*/
static List<FastDateFormat> getFormatters() {
return ourFormatters;
}
public boolean before(DateTimeType theDateTimeType) {
return getValue().before(theDateTimeType.getValue());
}
public boolean after(DateTimeType theDateTimeType) {
return getValue().after(theDateTimeType.getValue());
public Calendar toCalendar() {
Calendar retVal = Calendar.getInstance();
retVal.setTime(getValue());
retVal.setTimeZone(getTimeZone());
return retVal;
}
/**
@ -499,115 +610,11 @@ public abstract class BaseDateTimeType extends PrimitiveType<Date> {
}
}
/**
* Returns a view of this date/time as a Calendar object
* For unit tests only
*/
public Calendar toCalendar() {
Calendar retVal = Calendar.getInstance();
retVal.setTime(getValue());
retVal.setTimeZone(getTimeZone());
return retVal;
}
/**
* Sets the TimeZone offset in minutes relative to GMT
*/
public void setOffsetMinutes(int theZoneOffsetMinutes) {
int offsetAbs = Math.abs(theZoneOffsetMinutes);
int mins = offsetAbs % 60;
int hours = offsetAbs / 60;
if (theZoneOffsetMinutes < 0) {
setTimeZone(TimeZone.getTimeZone("GMT-" + hours + ":" + mins));
} else {
setTimeZone(TimeZone.getTimeZone("GMT+" + hours + ":" + mins));
}
}
/**
* Returns the time in millis as represented by this Date/Time
*/
public long getTime() {
return getValue().getTime();
}
/**
* Adds the given amount to the field specified by theField
*
* @param theField
* The field, uses constants from {@link Calendar} such as {@link Calendar#YEAR}
* @param theValue
* The number to add (or subtract for a negative number)
*/
public void add(int theField, int theValue) {
switch (theField) {
case Calendar.YEAR:
setValue(DateUtils.addYears(getValue(), theValue), getPrecision());
break;
case Calendar.MONTH:
setValue(DateUtils.addMonths(getValue(), theValue), getPrecision());
break;
case Calendar.DATE:
setValue(DateUtils.addDays(getValue(), theValue), getPrecision());
break;
case Calendar.HOUR:
setValue(DateUtils.addHours(getValue(), theValue), getPrecision());
break;
case Calendar.MINUTE:
setValue(DateUtils.addMinutes(getValue(), theValue), getPrecision());
break;
case Calendar.SECOND:
setValue(DateUtils.addSeconds(getValue(), theValue), getPrecision());
break;
case Calendar.MILLISECOND:
setValue(DateUtils.addMilliseconds(getValue(), theValue), getPrecision());
break;
default:
throw new IllegalArgumentException("Unknown field constant: " + theField);
}
}
protected void setValueAsV3String(String theV3String) {
if (StringUtils.isBlank(theV3String)) {
setValue(null);
} else {
StringBuilder b = new StringBuilder();
String timeZone = null;
for (int i = 0; i < theV3String.length(); i++) {
char nextChar = theV3String.charAt(i);
if (nextChar == '+' || nextChar == '-' || nextChar == 'Z') {
timeZone = (theV3String.substring(i));
break;
}
// assertEquals("2013-02-02T20:13:03-05:00", DateAndTime.parseV3("20130202201303-0500").toString());
if (i == 4 || i == 6) {
b.append('-');
} else if (i == 8) {
b.append('T');
} else if (i == 10 || i == 12) {
b.append(':');
}
b.append(nextChar);
}
if (b.length() == 16)
b.append(":00"); // schema rule, must have seconds
if (timeZone != null && b.length() > 10) {
if (timeZone.length() ==5) {
b.append(timeZone.substring(0, 3));
b.append(':');
b.append(timeZone.substring(3));
}else {
b.append(timeZone);
}
}
setValueAsString(b.toString());
}
static List<FastDateFormat> getFormatters() {
return ourFormatters;
}
}

View File

@ -3,6 +3,7 @@ package ca.uhn.fhir.model;
import static org.hamcrest.Matchers.containsString;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertThat;
import static org.junit.Assert.fail;
import java.text.SimpleDateFormat;
import java.util.Calendar;
@ -27,6 +28,39 @@ public class BaseDateTimeTypeDstu3Test {
myDateInstantParser = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss.SSS");
}
@Test
public void testMinutePrecisionEncode() throws Exception {
Calendar cal = Calendar.getInstance();
cal.setTimeZone(TimeZone.getTimeZone("Europe/Berlin"));
cal.set(1990, Calendar.JANUARY, 3, 3, 22, 11);
DateTimeType date = new DateTimeType();
date.setValue(cal.getTime(), TemporalPrecisionEnum.MINUTE);
date.setTimeZone(TimeZone.getTimeZone("EST"));
assertEquals("1990-01-02T21:22-05:00", date.getValueAsString());
date.setTimeZoneZulu(true);
assertEquals("1990-01-03T02:22Z", date.getValueAsString());
}
@Test
public void testParseInvalid() {
try {
DateTimeType dt = new DateTimeType();
dt.setValueAsString("1974-12-25+10:00");
fail();
} catch (ca.uhn.fhir.parser.DataFormatException e) {
assertEquals("Invalid date/time string: 1974-12-25+10:00", e.getMessage());
}
try {
DateTimeType dt = new DateTimeType();
dt.setValueAsString("1974-12-25Z");
fail();
} catch (ca.uhn.fhir.parser.DataFormatException e) {
assertEquals("Invalid date/time string (invalid length): 1974-12-25Z", e.getMessage());
}
}
/**
* See HAPI #101 - https://github.com/jamesagnew/hapi-fhir/issues/101
*/
@ -43,21 +77,6 @@ public class BaseDateTimeTypeDstu3Test {
assertEquals("2012-01-02", date.getValueAsString());
}
@Test
public void testMinutePrecisionEncode() throws Exception {
Calendar cal = Calendar.getInstance();
cal.setTimeZone(TimeZone.getTimeZone("Europe/Berlin"));
cal.set(1990, Calendar.JANUARY, 3, 3, 22, 11);
DateTimeType date = new DateTimeType();
date.setValue(cal.getTime(), TemporalPrecisionEnum.MINUTE);
date.setTimeZone(TimeZone.getTimeZone("EST"));
assertEquals("1990-01-02T21:22-05:00", date.getValueAsString());
date.setTimeZoneZulu(true);
assertEquals("1990-01-03T02:22Z", date.getValueAsString());
}
/**
* See HAPI #101 - https://github.com/jamesagnew/hapi-fhir/issues/101
*/

View File

@ -4,7 +4,6 @@ import static org.hamcrest.Matchers.containsString;
import static org.hamcrest.Matchers.greaterThan;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertThat;
import static org.junit.Assert.fail;
import static org.mockito.Matchers.any;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;
@ -24,14 +23,9 @@ import org.hl7.fhir.dstu3.hapi.validation.DefaultProfileValidationSupport;
import org.hl7.fhir.dstu3.hapi.validation.FhirInstanceValidator;
import org.hl7.fhir.dstu3.hapi.validation.IValidationSupport;
import org.hl7.fhir.dstu3.hapi.validation.IValidationSupport.CodeValidationResult;
<<<<<<< HEAD
import org.hl7.fhir.dstu3.hapi.validation.ValidationSupportChain;
||||||| merged common ancestors
=======
import org.hl7.fhir.dstu3.hapi.validation.ValidationSupportChain;
import org.hl7.fhir.dstu3.model.CodeSystem;
import org.hl7.fhir.dstu3.model.CodeSystem.ConceptDefinitionComponent;
>>>>>>> dstu3_structs
import org.hl7.fhir.dstu3.model.CodeType;
import org.hl7.fhir.dstu3.model.Observation;
import org.hl7.fhir.dstu3.model.Observation.ObservationStatus;
@ -39,13 +33,6 @@ import org.hl7.fhir.dstu3.model.Patient;
import org.hl7.fhir.dstu3.model.StringType;
import org.hl7.fhir.dstu3.model.StructureDefinition;
import org.hl7.fhir.dstu3.model.ValueSet;
<<<<<<< HEAD
import org.hl7.fhir.dstu3.model.ValueSet.ConceptDefinitionComponent;
||||||| merged common ancestors
import org.hl7.fhir.dstu3.model.Observation.ObservationStatus;
import org.hl7.fhir.dstu3.model.ValueSet.ConceptDefinitionComponent;
=======
>>>>>>> dstu3_structs
import org.hl7.fhir.dstu3.model.ValueSet.ConceptSetComponent;
import org.hl7.fhir.dstu3.model.ValueSet.ValueSetExpansionComponent;
import org.hl7.fhir.instance.model.api.IBaseResource;

View File

@ -367,6 +367,10 @@
performance when searching over large datasets.
Thanks to Emmanuel Duviviers for the suggestion!
</action>
<action type="fix">
DateTimeType should fail to parse 1974-12-25+10:00 as this is not
a valid time in FHIR. Thanks to Grahame Grieve for reporting!
</action>
</release>
<release version="1.4" date="2016-02-04">
<action type="add">