Narrative fixes

This commit is contained in:
James 2014-07-04 16:17:53 -04:00
parent 80c13494a8
commit ec2326b1f2
23 changed files with 213 additions and 41 deletions

View File

@ -583,6 +583,23 @@ public class Encounter extends BaseResource implements IResource {
return myType;
}
/**
* Gets the value(s) for <b>type</b> (Specific type of encounter).
* creating it if it does
* not exist. Will not return <code>null</code>.
*
* <p>
* <b>Definition:</b>
* Specific type of encounter (e.g. e-mail consultation, surgical day-care, skilled nursing, rehabilitation)
* </p>
*/
public BoundCodeableConceptDt<EncounterTypeEnum> getTypeFirstRep() {
if (getType().size()==0) {
addType();
}
return getType().get(0);
}
/**
* Sets the value(s) for <b>type</b> (Specific type of encounter)
*

View File

@ -126,7 +126,8 @@ public class StringDt extends BasePrimitive<String> implements IQueryParameterTy
*/
@Override
public boolean isEmpty() {
return super.isBaseEmpty() && StringUtils.isBlank(getValue());
boolean retVal = super.isBaseEmpty() && StringUtils.isBlank(getValue());
return retVal;
}
@Override

View File

@ -53,8 +53,10 @@ import org.thymeleaf.standard.expression.IStandardExpressionParser;
import org.thymeleaf.standard.expression.StandardExpressions;
import org.thymeleaf.templateresolver.TemplateResolver;
import org.thymeleaf.util.DOMUtils;
import org.w3c.dom.Text;
import ca.uhn.fhir.context.ConfigurationException;
import ca.uhn.fhir.context.FhirContext;
import ca.uhn.fhir.model.api.IResource;
import ca.uhn.fhir.model.dstu.composite.NarrativeDt;
import ca.uhn.fhir.model.dstu.valueset.NarrativeStatusEnum;
@ -94,6 +96,8 @@ public abstract class BaseThymeleafNarrativeGenerator implements INarrativeGener
initialize();
}
ourLog.trace("Generating resource title {}", theResource);
String name = null;
if (StringUtils.isNotBlank(theProfile)) {
name = myProfileToName.get(theProfile);
@ -102,6 +106,8 @@ public abstract class BaseThymeleafNarrativeGenerator implements INarrativeGener
name = myClassToName.get(theResource.getClass());
}
ourLog.trace("Template name is {}", name);
if (name == null) {
if (myIgnoreMissingTemplates) {
ourLog.debug("No title template available for profile: {}", theProfile);
@ -116,6 +122,9 @@ public abstract class BaseThymeleafNarrativeGenerator implements INarrativeGener
context.setVariable("resource", theResource);
String result = myTitleTemplateEngine.process(name, context);
ourLog.trace("Produced {}", result);
StringBuilder b = new StringBuilder();
boolean inTag = false;
for (int i = 0; i < result.length(); i++) {
@ -152,6 +161,8 @@ public abstract class BaseThymeleafNarrativeGenerator implements INarrativeGener
result = result.substring(0, result.lastIndexOf('<'));
}
result = result.replace("&gt;", ">").replace("&lt;", "<").replace("&amp;", "&");
return result;
} catch (Exception e) {
if (myIgnoreFailures) {
@ -214,6 +225,9 @@ public abstract class BaseThymeleafNarrativeGenerator implements INarrativeGener
if (myInitialized) {
return;
}
ourLog.info("Initializing narrative generator");
myProfileToName = new HashMap<String, String>();
myClassToName = new HashMap<Class<?>, String>();
myNameToNarrativeTemplate = new HashMap<String, String>();
@ -250,6 +264,9 @@ public abstract class BaseThymeleafNarrativeGenerator implements INarrativeGener
resolver.setResourceResolver(new TitleResourceResolver());
myTitleTemplateEngine.setTemplateResolver(resolver);
StandardDialect dialect = new StandardDialect();
HashSet<IProcessor> additionalProcessors = new HashSet<IProcessor>();
additionalProcessors.add(new NarrativeAttributeProcessor());
dialect.setAdditionalProcessors(additionalProcessors);
myTitleTemplateEngine.setDialect(dialect);
myTitleTemplateEngine.initialize();
}
@ -486,7 +503,13 @@ public abstract class BaseThymeleafNarrativeGenerator implements INarrativeGener
Context context = new Context();
context.setVariable("resource", value);
String name = myClassToName.get(value.getClass());
String name = null;
Class<? extends Object> nextClass = value.getClass();
do {
name = myClassToName.get(nextClass);
nextClass=nextClass.getSuperclass();
} while (name == null && nextClass.equals(Object.class)==false);
if (name == null) {
if (myIgnoreMissingTemplates) {
ourLog.debug("No narrative template available for type: {}", value.getClass());
@ -497,10 +520,18 @@ public abstract class BaseThymeleafNarrativeGenerator implements INarrativeGener
}
String result = myProfileTemplateEngine.process(name, context);
Document dom = DOMUtils.getXhtmlDOMFor(new StringReader(result));
String trim = result.trim();
Document dom = DOMUtils.getXhtmlDOMFor(new StringReader(trim));
Element firstChild = (Element) dom.getFirstChild();
for (Node next : firstChild.getChildren()) {
for (int i = 0; i < firstChild.getChildren().size(); i++) {
Node next = firstChild.getChildren().get(i);
if (i == 0 && firstChild.getChildren().size() == 1) {
if (next instanceof org.thymeleaf.dom.Text) {
org.thymeleaf.dom.Text nextText = (org.thymeleaf.dom.Text) next;
nextText.setContent(nextText.getContent().trim());
}
}
theElement.addChild(next);
}

View File

@ -223,7 +223,6 @@ public class JsonParser extends BaseParser implements IParser {
eventWriter.writeEnd();
eventWriter.flush();
eventWriter.close();
}
private void encodeChildElementToStreamWriter(RuntimeResourceDefinition theResDef, IResource theResource, JsonGenerator theWriter, IElement theValue, BaseRuntimeElementDefinition<?> theChildDef, String theChildName) throws IOException {
@ -507,7 +506,6 @@ public class JsonParser extends BaseParser implements IParser {
RuntimeResourceDefinition resDef = myContext.getResourceDefinition(theResource);
encodeResourceToJsonStreamWriter(resDef, theResource, eventWriter, null,false);
eventWriter.flush();
eventWriter.close();
}
@Override
@ -538,7 +536,6 @@ public class JsonParser extends BaseParser implements IParser {
eventWriter.writeEnd();
eventWriter.flush();
eventWriter.close();
}
/**

View File

@ -783,9 +783,12 @@ public class RestfulServer extends HttpServlet {
if (theContext.getNarrativeGenerator() != null) {
String title = theContext.getNarrativeGenerator().generateTitle(next);
ourLog.info("Narrative generator created title: {}", title);
if (StringUtils.isNotBlank(title)) {
ResourceMetadataKeyEnum.TITLE.put(next, title);
}
} else {
ourLog.info("No narrative generator specified");
}
bundle.addResource(next, theContext, theServerBase);

View File

@ -1,6 +0,0 @@
<div>
<th:block th:each="prefix : ${resource.prefix}" th:text="${prefix.value} + ' '">Dr</th:block>
<th:block th:each="givenName : ${resource.given}" th:text="${givenName.value} + ' '">John</th:block>
<b th:each="familyName : ${resource.family}" th:text="${#strings.toUpperCase(familyName.value)} + ' '">SMITH</b>
<th:block th:each="suffix : ${resource.suffix}" th:text="${suffix.value} + ' '">Jr</th:block>
</div>

View File

@ -16,7 +16,7 @@ a browser.
<tbody>
<tr th:if="${not resource.identifierFirstRep.empty}">
<td>Identifier</td>
<td th:text="${resource.identifierFirstRep.value.value}">8708660</td>
<td th:narrative="${resource.identifierFirstRep}">8708660</td>
</tr>
<tr th:if="${not resource.addressFirstRep.empty}">
<td>Address</td>

View File

@ -0,0 +1,3 @@
<div>
<th:block th:if="${not resource.empty}" th:text="${resource.value}"/>
</div>

View File

@ -0,0 +1,13 @@
<div>
<th:block th:if="${not resource.text.empty}" th:text="${resource.text.value}"/>
<th:block th:if="${resource.text.empty}">
<th:block th:if="${!resource.codingFirstRep.empty}">
<th:block th:if="${!resource.codingFirstRep.display.empty}" th:text="${!resource.codingFirstRep.display.value}"/>
<th:block th:if="${resource.codingFirstRep.display.empty}">
<th:block th:if="${!resource.codingFirstRep.code.empty}">
<th:block th:text="${resource.codingFirstRep.code.value}"/>
</th:block>
</th:block>
</th:block>
</th:block>
</div>

View File

@ -0,0 +1,6 @@
<div>
<th:block th:each="prefix : ${resource.prefix}" th:if="${!prefix.empty}" th:text="${prefix.value} + ' '">Dr</th:block>
<th:block th:each="givenName : ${resource.given}" th:if="${!givenName.empty}" th:text="${givenName.value} + ' '">John</th:block>
<b th:each="familyName : ${resource.family}" th:if="${!familyName.empty}" th:text="${#strings.toUpperCase(familyName.value)} + ' '">SMITH</b>
<th:block th:each="suffix : ${resource.suffix}" th:if="${!suffix.empty}" th:text="${suffix.value} + ' '">Jr</th:block>
</div>

View File

@ -0,0 +1,4 @@
<div>
<th:block th:if="${!resource.label.empty}" th:text="${resource.label.value}"/>
<th:block th:if="${resource.label.empty}" th:text="${resource.value.valueAsString}"/>
</div>

View File

@ -0,0 +1,5 @@
<div>
<th:block th:if="${!resource.start.empty} and ${!resource.end.empty}" th:text="${resource.start.value} + ' - ' + ${resource.end.value}"/>
<th:block th:if="${!resource.start.empty} and ${resource.end.empty}" th:text="${resource.start.value} + ' - ?'"/>
<th:block th:if="${resource.start.empty} and ${!resource.end.empty}" th:text="'? - ' + ${resource.end.value}"/>
</div>

View File

@ -3,38 +3,53 @@
# Primitive Datatypes
################################################
string.class=ca.uhn.fhir.model.primitive.StringDt
string.narrative=classpath:ca/uhn/fhir/narrative/StringDt.html
code.class=ca.uhn.fhir.model.primitive.CodeDt
code.narrative=classpath:ca/uhn/fhir/narrative/datatype/CodeDt.html
datetime.class=ca.uhn.fhir.model.primitive.DateTimeDt
datetime.narrative=classpath:ca/uhn/fhir/narrative/DateTimeDt.html
datetime.narrative=classpath:ca/uhn/fhir/narrative/datatype/DateTimeDt.html
# Instant uses DateTime narrative
instant.class=ca.uhn.fhir.model.primitive.InstantDt
instant.narrative=classpath:ca/uhn/fhir/narrative/DateTimeDt.html
instant.narrative=classpath:ca/uhn/fhir/narrative/datatype/DateTimeDt.html
string.class=ca.uhn.fhir.model.primitive.StringDt
string.narrative=classpath:ca/uhn/fhir/narrative/datatype/StringDt.html
################################################
# Composite Datatypes
################################################
address.class=ca.uhn.fhir.model.dstu.composite.AddressDt
address.narrative=classpath:ca/uhn/fhir/narrative/AddressDt.html
address.narrative=classpath:ca/uhn/fhir/narrative/datatype/AddressDt.html
codeableconcept.class=ca.uhn.fhir.model.dstu.composite.CodeableConceptDt
codeableconcept.narrative=classpath:ca/uhn/fhir/narrative/datatype/CodeableConceptDt.html
humanname.class=ca.uhn.fhir.model.dstu.composite.HumanNameDt
humanname.narrative=classpath:ca/uhn/fhir/narrative/HumanNameDt.html
humanname.narrative=classpath:ca/uhn/fhir/narrative/datatype/HumanNameDt.html
identifier.class=ca.uhn.fhir.model.dstu.composite.IdentifierDt
identifier.narrative=classpath:ca/uhn/fhir/narrative/datatype/IdentifierDt.html
period.class=ca.uhn.fhir.model.dstu.composite.PeriodDt
period.narrative=classpath:ca/uhn/fhir/narrative/datatype/PeriodDt.html
quantity.class=ca.uhn.fhir.model.dstu.composite.QuantityDt
quantity.narrative=classpath:ca/uhn/fhir/narrative/QuantityDt.html
quantity.narrative=classpath:ca/uhn/fhir/narrative/datatype/QuantityDt.html
################################################
# Resources
################################################
diagnosticreport.class=ca.uhn.fhir.model.dstu.resource.DiagnosticReport
diagnosticreport.narrative=classpath:ca/uhn/fhir/narrative/DiagnosticReport.html
diagnosticreport.title=classpath:ca/uhn/fhir/narrative/title/DiagnosticReport.html
encounter.class=ca.uhn.fhir.model.dstu.resource.Encounter
encounter.title=classpath:ca/uhn/fhir/narrative/title/Encounter.html
patient.class=ca.uhn.fhir.model.dstu.resource.Patient
patient.narrative=classpath:ca/uhn/fhir/narrative/Patient.html
patient.title=classpath:ca/uhn/fhir/narrative/title/Patient.html
diagnosticreport.class=ca.uhn.fhir.model.dstu.resource.DiagnosticReport
diagnosticreport.narrative=classpath:ca/uhn/fhir/narrative/DiagnosticReport.html
diagnosticreport.title=classpath:ca/uhn/fhir/narrative/title/DiagnosticReport.html

View File

@ -0,0 +1,15 @@
<div>
<th:block th:if="${!resource.identifierFirstRep.empty}" th:narrative="${resource.identifierFirstRep}" />
<th:block th:if="${not resource.status.empty}">
/ <th:block th:narrative="${resource.status}"/>
</th:block>
<th:block th:if="${not resource.typeFirstRep.empty}">
/ <th:block th:narrative="${resource.typeFirstRep}"/>
</th:block>
<th:block th:if="${not resource.classElement.empty}">
/ <th:block th:narrative="${resource.classElement}"/>
</th:block>
<th:block th:if="${not resource.period.empty}">
/ <th:block th:narrative="${resource.period}"/>
</th:block>
</div>

View File

@ -1,9 +1,6 @@
<div>
<th:block th:each="prefix : ${resource.nameFirstRep.prefix}" th:text="${prefix.value} + ' '">Dr</th:block>
<th:block th:each="givenName : ${resource.nameFirstRep.given}" th:text="${givenName.value} + ' '">John</th:block>
<th:block th:each="familyName : ${resource.nameFirstRep.family}" th:text="${#strings.toUpperCase(familyName.value)} + ' '">SMITH</th:block>
<th:block th:each="suffix : ${resource.nameFirstRep.suffix}" th:text="${suffix.value} + ' '">Jr</th:block>
<th:block th:narrative="${resource.nameFirstRep}" />
<th:block th:if="${not resource.identifierFirstRep.empty}">
(<th:block th:text="${resource.identifierFirstRep.value.value}">8708660</th:block>)
(<th:block th:narrative="${resource.identifierFirstRep}">8708660</th:block>)
</th:block>
</div>

View File

@ -10,20 +10,27 @@ import org.junit.Before;
import org.junit.Test;
import ca.uhn.fhir.context.FhirContext;
import ca.uhn.fhir.model.api.IResource;
import ca.uhn.fhir.model.api.TemporalPrecisionEnum;
import ca.uhn.fhir.model.dstu.composite.NarrativeDt;
import ca.uhn.fhir.model.dstu.composite.PeriodDt;
import ca.uhn.fhir.model.dstu.composite.QuantityDt;
import ca.uhn.fhir.model.dstu.composite.ResourceReferenceDt;
import ca.uhn.fhir.model.dstu.resource.Conformance;
import ca.uhn.fhir.model.dstu.resource.DiagnosticReport;
import ca.uhn.fhir.model.dstu.resource.Encounter;
import ca.uhn.fhir.model.dstu.resource.Observation;
import ca.uhn.fhir.model.dstu.resource.Patient;
import ca.uhn.fhir.model.dstu.valueset.DiagnosticReportStatusEnum;
import ca.uhn.fhir.model.dstu.valueset.EncounterClassEnum;
import ca.uhn.fhir.model.dstu.valueset.EncounterTypeEnum;
import ca.uhn.fhir.model.dstu.valueset.ObservationStatusEnum;
import ca.uhn.fhir.model.primitive.DateTimeDt;
import ca.uhn.fhir.model.primitive.StringDt;
import ca.uhn.fhir.parser.DataFormatException;
public class DefaultThymeleafNarrativeGeneratorTest {
private FhirContext myCtx;
private static final org.slf4j.Logger ourLog = org.slf4j.LoggerFactory.getLogger(DefaultThymeleafNarrativeGeneratorTest.class);
private DefaultThymeleafNarrativeGenerator gen;
@ -33,14 +40,20 @@ public class DefaultThymeleafNarrativeGeneratorTest {
gen.setUseHapiServerConformanceNarrative(true);
gen.setIgnoreFailures(false);
gen.setIgnoreMissingTemplates(false);
myCtx=new FhirContext();
myCtx.setNarrativeGenerator(gen);
}
@Test
public void testGeneratePatient() throws DataFormatException {
Patient value = new Patient();
value.addIdentifier().setSystem("urn:names").setValue("123456");
value.addName().addFamily("blow").addGiven("joe").addGiven("john");
value.addName().addFamily("blow").addGiven("joe").addGiven(null).addGiven("john");
value.getAddressFirstRep().addLine("123 Fake Street").addLine("Unit 1");
value.getAddressFirstRep().setCity("Toronto").setState("ON").setCountry("Canada");
@ -52,11 +65,33 @@ public class DefaultThymeleafNarrativeGeneratorTest {
String title = gen.generateTitle(value);
assertEquals("joe john BLOW (123456)", title);
ourLog.info(title);
value.getIdentifierFirstRep().setLabel("FOO MRN 123");
title = gen.generateTitle(value);
assertEquals("joe john BLOW (FOO MRN 123)", title);
ourLog.info(title);
}
@Test
public void testGenerateEncounter() throws DataFormatException {
Encounter enc = new Encounter();
enc.addIdentifier("urn:visits", "1234567");
enc.setClassElement(EncounterClassEnum.AMBULATORY);
enc.setPeriod(new PeriodDt().setStart(new DateTimeDt("2001-01-02T11:11:00")));
enc.setType(EncounterTypeEnum.ANNUAL_DIABETES_MELLITUS_SCREENING);
String title = gen.generateTitle(enc);
assertEquals("1234567 / ADMS / ambulatory / Tue Jan 02 11:11:00 EST 2001 - ?", title);
ourLog.info(title);
}
@Test
public void testGenerateServerConformance() throws DataFormatException {
Conformance value = new FhirContext().newXmlParser().parseResource(Conformance.class, new InputStreamReader(getClass().getResourceAsStream("/server-conformance-statement.xml")));
Conformance value = myCtx.newXmlParser().parseResource(Conformance.class, new InputStreamReader(getClass().getResourceAsStream("/server-conformance-statement.xml")));
String output = gen.generateNarrative(value).getDiv().getValueAsString();
@ -76,6 +111,7 @@ public class DefaultThymeleafNarrativeGeneratorTest {
String output = gen.generateNarrative("http://hl7.org/fhir/profiles/DiagnosticReport", value).getDiv().getValueAsString();
ourLog.info(output);
assertThat(output,StringContains.containsString(value.getName().getText().getValue()));
}
@Test
@ -85,7 +121,7 @@ public class DefaultThymeleafNarrativeGeneratorTest {
value.getIssued().setValueAsString("2011-02-22T11:13:00");
value.setStatus(DiagnosticReportStatusEnum.FINAL);
value.getName().setText("Some Diagnostic Report");
value.getName().setText("Some & Diagnostic Report");
{
Observation obs = new Observation();
obs.getName().addCoding().setCode("1938HB").setDisplay("Hemoglobin");
@ -106,7 +142,7 @@ public class DefaultThymeleafNarrativeGeneratorTest {
String output = generateNarrative.getDiv().getValueAsString();
ourLog.info(output);
assertThat(output, StringContains.containsString("<div class=\"hapiHeaderText\"> Some Diagnostic Report </div>"));
assertThat(output, StringContains.containsString("<div class=\"hapiHeaderText\"> Some &amp; Diagnostic Report </div>"));
String title = gen.generateTitle(value);
ourLog.info(title);
@ -115,11 +151,9 @@ public class DefaultThymeleafNarrativeGeneratorTest {
// Now try it with the parser
FhirContext context = new FhirContext();
context.setNarrativeGenerator(gen);
output = context.newXmlParser().setPrettyPrint(true).encodeResourceToString(value);
output = myCtx.newXmlParser().setPrettyPrint(true).encodeResourceToString(value);
ourLog.info(output);
assertThat(output, StringContains.containsString("<div class=\"hapiHeaderText\"> Some Diagnostic Report </div>"));
assertThat(output, StringContains.containsString("<div class=\"hapiHeaderText\"> Some &amp; Diagnostic Report </div>"));
}

View File

@ -9,8 +9,24 @@
</encoder>
</appender>
<root>
<appender-ref ref="STDOUT" />
<appender name="FILE" class="ch.qos.logback.core.rolling.RollingFileAppender">
<filter class="ch.qos.logback.classic.filter.ThresholdFilter">
<level>INFO</level>
</filter>
<file>${fhir.logdir}/logFile.log</file>
<rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
<fileNamePattern>${fhir.logdir}/logFile.%d{yyyy-MM-dd}.log.gz</fileNamePattern>
<maxHistory>30</maxHistory>
</rollingPolicy>
<encoder>
<!-- [%file:%line] -->
<pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{36} %msg%n</pattern>
</encoder>
</appender>
<root level="DEBUG">
<appender-ref ref="FILE" />
</root>
</configuration>

View File

@ -16,7 +16,7 @@
</div>
<br /> <label class="navBarButtonLabel">Pretty</label>
<div class="btn-group top-buffer" data-toggle="buttons">
<div class="btn-group top-buffer" data-toggle="buttons" id="prettyBtnGroup">
<label class="btn btn-sm btn-default active"> <input
type="radio" name="pretty" id="pretty-default" value="" />(default)
</label> <label class="btn btn-sm btn-default"> <input
@ -28,6 +28,11 @@
<script type="text/javascript">
$( document ).ready(function() {
// Encoding buttons are wider, so set the shorter group to the same width
// so that they wrap at the same time if the page is narrow
$('#prettyBtnGroup').width($('#encodingBtnGroup').width());
<th:block th:switch="${encoding}">
<th:block th:case="'xml'">
$('#encode-xml').trigger("click");

View File

@ -151,6 +151,22 @@
return retVal;
}
/**
* Gets the first repetition for <b>${child.elementName}</b> (${child.shortName}),
* creating it if it does not already exist.
*
* <p>
* <b>Definition:</b>
* ${child.definition}
* </p>
*/
public ${child.boundDatatype}<${child.bindingClass}> get${child.methodName}FirstRep() {
if (get${child.methodName}().size() == 0) {
add${child.methodName}();
}
return get${child.methodName}().get(0);
}
/**
* Add a value for <b>${child.elementName}</b> (${child.shortName})
*