update rendering to get timezones right, and add tests for that

This commit is contained in:
Grahame Grieve 2021-11-22 07:41:52 +11:00
parent 412e4931fb
commit bf5c621f43
6 changed files with 207 additions and 20 deletions

View File

@ -3,4 +3,4 @@ Validator:
Other changes:
* improvements to data types rendering based on new test cases (URLs, Money, Markdown)
* add locale to rendering context
* add locale to rendering context, and fix up timezone related rendering based on locale

View File

@ -3,9 +3,17 @@ package org.hl7.fhir.r5.renderers;
import java.io.IOException;
import java.io.UnsupportedEncodingException;
import java.math.BigDecimal;
import java.text.DateFormat;
import java.text.NumberFormat;
import java.text.SimpleDateFormat;
import java.time.LocalDate;
import java.time.ZoneId;
import java.time.ZonedDateTime;
import java.time.format.DateTimeFormatter;
import java.time.format.FormatStyle;
import java.util.Currency;
import java.util.List;
import java.util.TimeZone;
import org.hl7.fhir.exceptions.DefinitionException;
import org.hl7.fhir.exceptions.FHIRException;
@ -38,7 +46,6 @@ import org.hl7.fhir.r5.model.HumanName;
import org.hl7.fhir.r5.model.HumanName.NameUse;
import org.hl7.fhir.r5.model.IdType;
import org.hl7.fhir.r5.model.Identifier;
import org.hl7.fhir.r5.model.InstantType;
import org.hl7.fhir.r5.model.MarkdownType;
import org.hl7.fhir.r5.model.Money;
import org.hl7.fhir.r5.model.Period;
@ -70,6 +77,9 @@ import org.hl7.fhir.utilities.validation.ValidationOptions;
import org.hl7.fhir.utilities.xhtml.NodeType;
import org.hl7.fhir.utilities.xhtml.XhtmlNode;
import org.hl7.fhir.utilities.xhtml.XhtmlParser;
import ca.uhn.fhir.model.api.TemporalPrecisionEnum;
import org.hl7.fhir.utilities.xhtml.HierarchicalTableGenerator;
import org.hl7.fhir.utilities.xhtml.HierarchicalTableGenerator.Piece;
@ -379,6 +389,8 @@ public class DataRenderer extends Renderer {
return displayTiming((Timing) type);
} else if (type instanceof SampledData) {
return displaySampledData((SampledData) type);
} else if (type.isDateTime()) {
return displayDateTime((BaseDateTimeType) type);
} else if (type.isPrimitive()) {
return type.primitiveValue();
} else {
@ -386,6 +398,53 @@ public class DataRenderer extends Renderer {
}
}
private String displayDateTime(BaseDateTimeType type) {
if (!type.hasPrimitiveValue()) {
return "";
}
// relevant inputs in rendering context:
// timeZone, dateTimeFormat, locale, mode
// timezone - application specified timezone to use.
// null = default to the time of the date/time itself
// dateTimeFormat - application specified format for date times
// null = default to ... depends on mode
// mode - if rendering mode is technical, format defaults to XML format
// locale - otherwise, format defaults to default for the Locale
if (isOnlyDate(type.getPrecision())) {
DateTimeFormatter fmt = context.getDateFormat();
if (fmt == null) {
if (context.isTechnicalMode()) {
fmt = DateTimeFormatter.ISO_DATE;
} else {
fmt = DateTimeFormatter.ofLocalizedDate(FormatStyle.MEDIUM);
}
}
LocalDate date = LocalDate.of(type.getYear(), type.getMonth()+1, type.getDay());
return fmt.format(date);
}
DateTimeFormatter fmt = context.getDateTimeFormat();
if (fmt == null) {
if (context.isTechnicalMode()) {
fmt = DateTimeFormatter.ISO_OFFSET_DATE_TIME;
} else {
fmt = DateTimeFormatter.ofLocalizedDateTime(FormatStyle.MEDIUM);
}
}
ZonedDateTime zdt = ZonedDateTime.parse(type.primitiveValue());
ZoneId zone = context.getTimeZoneId();
if (zone != null) {
zdt = zdt.withZoneSameInstant(zone);
}
return fmt.format(zdt);
}
private boolean isOnlyDate(TemporalPrecisionEnum temporalPrecisionEnum) {
return temporalPrecisionEnum == TemporalPrecisionEnum.YEAR || temporalPrecisionEnum == TemporalPrecisionEnum.MONTH || temporalPrecisionEnum == TemporalPrecisionEnum.DAY;
}
public String display(BaseWrapper type) {
return "to do";
}
@ -414,8 +473,8 @@ public class DataRenderer extends Renderer {
}
public void render(XhtmlNode x, DataType type) throws FHIRFormatError, DefinitionException, IOException {
if (type instanceof DateTimeType) {
renderDateTime(x, (DateTimeType) type);
if (type instanceof BaseDateTimeType) {
x.tx(displayDateTime((BaseDateTimeType) type));
} else if (type instanceof UriType) {
renderUri(x, (UriType) type);
} else if (type instanceof Annotation) {
@ -448,12 +507,8 @@ public class DataRenderer extends Renderer {
renderSampledData(x, (SampledData) type);
} else if (type instanceof Reference) {
renderReference(x, (Reference) type);
} else if (type instanceof InstantType) {
x.tx(((InstantType) type).toHumanDisplay());
} else if (type instanceof MarkdownType) {
addMarkdown(x, ((MarkdownType) type).asStringValue());
} else if (type instanceof BaseDateTimeType) {
x.tx(((BaseDateTimeType) type).toHumanDisplay());
} else if (type.isPrimitive()) {
x.tx(type.primitiveValue());
} else {
@ -473,14 +528,14 @@ public class DataRenderer extends Renderer {
public void renderDateTime(XhtmlNode x, Base e) {
if (e.hasPrimitiveValue()) {
x.addText(((DateTimeType) e).toHumanDisplay());
x.addText(displayDateTime((DateTimeType) e));
}
}
public void renderDateTime(XhtmlNode x, String s) {
if (s != null) {
DateTimeType dt = new DateTimeType(s);
x.addText(dt.toHumanDisplay());
x.addText(displayDateTime(dt));
}
}
@ -1112,16 +1167,16 @@ public class DataRenderer extends Renderer {
x.tx(" "+q.getLow().getUnit());
}
public static String displayPeriod(Period p) {
String s = !p.hasStart() ? "(?)" : p.getStartElement().toHumanDisplay();
public String displayPeriod(Period p) {
String s = !p.hasStart() ? "(?)" : displayDateTime(p.getStartElement());
s = s + " --> ";
return s + (!p.hasEnd() ? "(ongoing)" : p.getEndElement().toHumanDisplay());
return s + (!p.hasEnd() ? "(ongoing)" : displayDateTime(p.getEndElement()));
}
public void renderPeriod(XhtmlNode x, Period p) {
x.addText(!p.hasStart() ? "??" : p.getStartElement().toHumanDisplay());
x.addText(!p.hasStart() ? "??" : displayDateTime(p.getStartElement()));
x.tx(" --> ");
x.addText(!p.hasEnd() ? "(ongoing)" : p.getEndElement().toHumanDisplay());
x.addText(!p.hasEnd() ? "(ongoing)" : displayDateTime(p.getEndElement()));
}
public void renderDataRequirement(XhtmlNode x, DataRequirement dr) throws FHIRFormatError, DefinitionException, IOException {
@ -1229,7 +1284,7 @@ public class DataRenderer extends Renderer {
CommaSeparatedStringBuilder c = new CommaSeparatedStringBuilder();
for (DateTimeType p : s.getEvent()) {
if (p.hasValue()) {
c.append(p.toHumanDisplay());
c.append(displayDateTime(p));
} else if (!renderExpression(c, p)) {
c.append("??");
}
@ -1240,7 +1295,7 @@ public class DataRenderer extends Renderer {
if (s.hasRepeat()) {
TimingRepeatComponent rep = s.getRepeat();
if (rep.hasBoundsPeriod() && rep.getBoundsPeriod().hasStart())
b.append("Starting "+rep.getBoundsPeriod().getStartElement().toHumanDisplay());
b.append("Starting "+displayDateTime(rep.getBoundsPeriod().getStartElement()));
if (rep.hasCount())
b.append("Count "+Integer.toString(rep.getCount())+" times");
if (rep.hasDuration())
@ -1272,7 +1327,7 @@ public class DataRenderer extends Renderer {
b.append("Do "+st);
}
if (rep.hasBoundsPeriod() && rep.getBoundsPeriod().hasEnd())
b.append("Until "+rep.getBoundsPeriod().getEndElement().toHumanDisplay());
b.append("Until "+displayDateTime(rep.getBoundsPeriod().getEndElement()));
}
return b.toString();
}

View File

@ -1,9 +1,14 @@
package org.hl7.fhir.r5.renderers.utils;
import java.io.IOException;
import java.text.DateFormat;
import java.time.ZoneId;
import java.time.format.DateTimeFormatter;
import java.time.format.FormatStyle;
import java.util.ArrayList;
import java.util.List;
import java.util.Locale;
import java.util.TimeZone;
import org.hl7.fhir.exceptions.FHIRException;
import org.hl7.fhir.exceptions.FHIRFormatError;
@ -119,6 +124,10 @@ public class RenderingContext {
private FhirPublication targetVersion;
private Locale locale;
private ZoneId timeZoneId;
private DateTimeFormatter dateTimeFormat;
private DateTimeFormatter dateFormat;
/**
*
* @param context - access to all related resources that might be needed
@ -439,4 +448,63 @@ public class RenderingContext {
this.locale = locale;
}
/**
* if the timezone is null, the rendering will default to the source timezone
* in the resource
*
* Note that if you're working server side, the FHIR project recommends the use
* of the Date header so that clients know what timezone the server defaults to,
*
* There is no standard way for the server to know what the client timezone is.
* In the case where the client timezone is unknown, the timezone should be null
*
* @return the specified timezone to render in
*/
public ZoneId getTimeZoneId() {
return timeZoneId;
}
public void setTimeZoneId(ZoneId timeZoneId) {
this.timeZoneId = timeZoneId;
}
/**
* In the absence of a specified format, the renderers will default to
* the FormatStyle.MEDIUM for the current locale.
*
* @return the format to use
*/
public DateTimeFormatter getDateTimeFormat() {
return this.dateTimeFormat;
}
public void setDateTimeFormat(DateTimeFormatter dateTimeFormat) {
this.dateTimeFormat = dateTimeFormat;
}
/**
* In the absence of a specified format, the renderers will default to
* the FormatStyle.MEDIUM for the current locale.
*
* @return the format to use
*/
public DateTimeFormatter getDateFormat() {
return this.dateFormat;
}
public void setDateFormat(DateTimeFormatter dateFormat) {
this.dateFormat = dateFormat;
}
public ResourceRendererMode getMode() {
return mode;
}
public void setMode(ResourceRendererMode mode) {
this.mode = mode;
}
}

View File

@ -1,8 +1,13 @@
package org.hl7.fhir.r5.test;
import java.io.BufferedReader;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.FileReader;
import java.io.IOException;
import java.time.ZoneId;
import java.time.format.DateTimeFormatter;
import java.util.ArrayList;
import java.util.List;
import java.util.stream.Stream;
@ -76,12 +81,14 @@ public class NarrativeGenerationTests {
private String id;
private boolean header;
private boolean meta;
private boolean technical;
public TestDetails(Element test) {
super();
id = test.getAttribute("id");
header = "true".equals(test.getAttribute("header"));
meta = "true".equals(test.getAttribute("meta"));
technical = "technical".equals(test.getAttribute("mode"));
}
public String getId() {
@ -132,6 +139,15 @@ public class NarrativeGenerationTests {
rc.setDefinitionsTarget("test.html");
rc.setTerminologyServiceOptions(TerminologyServiceOptions.defaults());
rc.setParser(new TestTypeParser());
// getting timezones correct (well, at least consistent, so tests pass on any computer)
rc.setLocale(new java.util.Locale("en", "AU"));
rc.setTimeZoneId(ZoneId.of("Australia/Sydney"));
rc.setDateTimeFormat(null);
rc.setDateFormat(null);
rc.setMode(test.technical ? ResourceRendererMode.TECHNICAL : ResourceRendererMode.END_USER);
Resource source;
if (TestingUtilities.findTestResource("r5", "narrative", test.getId() + ".json")) {
source = (Resource) new JsonParser().parse(TestingUtilities.loadTestResourceStream("r5", "narrative", test.getId() + ".json"));

View File

@ -4,22 +4,34 @@ import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.time.ZoneId;
import java.time.format.DateTimeFormatter;
import java.time.format.FormatStyle;
import java.util.Locale;
import java.util.TimeZone;
import org.hl7.fhir.exceptions.DefinitionException;
import org.hl7.fhir.exceptions.FHIRException;
import org.hl7.fhir.exceptions.FHIRFormatError;
import org.hl7.fhir.r5.formats.XmlParser;
import org.hl7.fhir.r5.model.DateTimeType;
import org.hl7.fhir.r5.model.DomainResource;
import org.hl7.fhir.r5.renderers.DataRenderer;
import org.hl7.fhir.r5.renderers.RendererFactory;
import org.hl7.fhir.r5.renderers.utils.RenderingContext;
import org.hl7.fhir.r5.renderers.utils.RenderingContext.ResourceRendererMode;
import org.hl7.fhir.r5.test.utils.TestingUtilities;
import org.hl7.fhir.r5.utils.EOperationOutcome;
import org.hl7.fhir.utilities.xhtml.NodeType;
import org.hl7.fhir.utilities.xhtml.XhtmlComposer;
import org.hl7.fhir.utilities.xhtml.XhtmlNode;
import org.junit.Assert;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.Test;
import org.xmlpull.v1.XmlPullParserException;
public class NarrativeGeneratorTests {
private static RenderingContext rc;
@BeforeAll
@ -40,4 +52,40 @@ public class NarrativeGeneratorTests {
new XmlParser().compose(s, r, true);
s.close();
}
private void checkDateTimeRendering(String src, String lang, String country, ZoneId tz, FormatStyle fmt, ResourceRendererMode mode, String expected) throws FHIRFormatError, DefinitionException, IOException {
rc.setLocale(new java.util.Locale(lang, country));
rc.setTimeZoneId(tz);
if (fmt == null) {
rc.setDateTimeFormat(null);
rc.setDateFormat(null);
} else {
rc.setDateTimeFormat(DateTimeFormatter.ofLocalizedDateTime(fmt));
rc.setDateFormat(DateTimeFormatter.ofLocalizedDate(fmt));
}
rc.setMode(mode);
DateTimeType dt = new DateTimeType(src);
String actual = new DataRenderer(rc).display(dt);
Assert.assertEquals(expected, actual);
XhtmlNode node = new XhtmlNode(NodeType.Element, "p");
new DataRenderer(rc).render(node, dt);
actual = new XhtmlComposer(true, false).compose(node);
Assert.assertEquals("<p>"+expected+"</p>", actual);
}
@Test
public void testDateTimeRendering() throws FHIRFormatError, DefinitionException, IOException {
checkDateTimeRendering("2021-11-19T14:13:12Z", "en", "AU", ZoneId.of("UTC"), null, ResourceRendererMode.TECHNICAL, "2021-11-19T14:13:12Z");
checkDateTimeRendering("2021-11-19T14:13:12Z", "en", "AU", ZoneId.of("Australia/Sydney"), null, ResourceRendererMode.TECHNICAL, "2021-11-20T01:13:12+11:00");
//todo: how to change this to get localised time as well?
checkDateTimeRendering("2021-11-19T14:13:12Z", "en", "AU", ZoneId.of("UTC"), FormatStyle.MEDIUM, ResourceRendererMode.TECHNICAL, "19 Nov. 2021, 2:13:12 pm");
checkDateTimeRendering("2021-11-19T14:13:12Z", "en", "AU", ZoneId.of("UTC"), null, ResourceRendererMode.END_USER, "19 Nov. 2021, 2:13:12 pm");
checkDateTimeRendering("2021-11-19", "en", "AU", ZoneId.of("UTC"), null, ResourceRendererMode.END_USER, "19 Nov. 2021");
}
}

View File

@ -19,7 +19,7 @@
<properties>
<hapi_fhir_version>5.1.0</hapi_fhir_version>
<validator_test_case_version>1.1.76</validator_test_case_version>
<validator_test_case_version>1.1.77</validator_test_case_version>
<junit_jupiter_version>5.7.1</junit_jupiter_version>
<junit_platform_launcher_version>1.7.1</junit_platform_launcher_version>
<maven_surefire_version>3.0.0-M5</maven_surefire_version>