update rendering to get timezones right, and add tests for that
This commit is contained in:
parent
412e4931fb
commit
bf5c621f43
|
@ -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
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
||||
|
||||
}
|
|
@ -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"));
|
||||
|
|
|
@ -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");
|
||||
}
|
||||
|
||||
|
||||
}
|
2
pom.xml
2
pom.xml
|
@ -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>
|
||||
|
|
Loading…
Reference in New Issue