Narrative docs

This commit is contained in:
jamesagnew 2014-03-31 17:10:05 -04:00
parent cfdf0bcf27
commit 12d8e8d19c
6 changed files with 275 additions and 28 deletions

View File

@ -215,7 +215,7 @@
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-jxr-plugin</artifactId>
<version>2.3</version>
<version>2.4</version>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>

View File

@ -38,7 +38,7 @@ import ca.uhn.fhir.model.primitive.XhtmlDt;
import ca.uhn.fhir.parser.DataFormatException;
public class DefaultThymeleafNarrativeGenerator extends BaseThymeleafNarrativeGenerator implements INarrativeGenerator {
private static final org.slf4j.Logger ourLog = org.slf4j.LoggerFactory.getLogger(DefaultThymeleafNarrativeGenerator.class);
private HashMap<String, String> myDatatypeClassNameToNarrativeTemplate;
@ -51,6 +51,22 @@ public class DefaultThymeleafNarrativeGenerator extends BaseThymeleafNarrativeGe
private TemplateEngine myProfileTemplateEngine;
private HashMap<String, String> myProfileToNarrativeTemplate;
private boolean myCleanWhitespace = true;
/**
* If set to <code>true</code> (which is the default), most whitespace will be trimmed from the generated narrative before it is returned.
* <p>
* Note that in order to preserve formatting, not all whitespace is trimmed. Repeated whitespace characters (e.g. "\n \n ") will be trimmed to a single space.
* </p>
*/
public boolean isCleanWhitespace() {
return myCleanWhitespace;
}
public void setCleanWhitespace(boolean theCleanWhitespace) {
myCleanWhitespace = theCleanWhitespace;
}
public DefaultThymeleafNarrativeGenerator() throws IOException {
myProfileToNarrativeTemplate = new HashMap<String, String>();
myDatatypeClassNameToNarrativeTemplate = new HashMap<String, String>();
@ -92,15 +108,18 @@ public class DefaultThymeleafNarrativeGenerator extends BaseThymeleafNarrativeGe
ourLog.debug("No narrative template available for profile: {}", theProfile);
return new NarrativeDt(new XhtmlDt("<div>No narrative available</div>"), NarrativeStatusEnum.EMPTY);
}
try {
Context context = new Context();
context.setVariable("resource", theResource);
String result = myProfileTemplateEngine.process(theProfile, context);
result = cleanWhitespace(result);
ourLog.info(result);
// result = result.replace("\n", "").replaceAll("\\s+", " ");//.replace("> ", ">").replace(" <", "<");
if (myCleanWhitespace) {
ourLog.trace("Pre-whitespace cleaning: ", result);
result = cleanWhitespace(result);
ourLog.trace("Post-whitespace cleaning: ", result);
}
XhtmlDt div = new XhtmlDt(result);
return new NarrativeDt(div, NarrativeStatusEnum.GENERATED);
@ -114,29 +133,43 @@ public class DefaultThymeleafNarrativeGenerator extends BaseThymeleafNarrativeGe
}
}
private String cleanWhitespace(String theResult) {
static String cleanWhitespace(String theResult) {
StringBuilder b = new StringBuilder();
boolean inWhitespace=false;
boolean inWhitespace = false;
boolean betweenTags = false;
boolean lastNonWhitespaceCharWasTagEnd = false;
for (int i = 0; i < theResult.length(); i++) {
char nextChar = theResult.charAt(i);
if (nextChar == '>') {
b.append(nextChar);
betweenTags=true;
betweenTags = true;
lastNonWhitespaceCharWasTagEnd = true;
continue;
} else if (nextChar == '\n' || nextChar == '\r') {
// if (inWhitespace) {
// b.append(' ');
// inWhitespace = false;
// }
continue;
}
if (betweenTags) {
if (Character.isWhitespace(nextChar)) {
inWhitespace = true;
} else if (nextChar == '<') {
if (inWhitespace && !lastNonWhitespaceCharWasTagEnd) {
b.append(' ');
}
inWhitespace = false;
b.append(nextChar);
inWhitespace=false;
betweenTags=false;
inWhitespace = false;
betweenTags = false;
lastNonWhitespaceCharWasTagEnd = false;
} else {
lastNonWhitespaceCharWasTagEnd = false;
if (inWhitespace) {
b.append(' ');
inWhitespace=false;
inWhitespace = false;
}
b.append(nextChar);
}
@ -156,8 +189,7 @@ public class DefaultThymeleafNarrativeGenerator extends BaseThymeleafNarrativeGe
}
/**
* If set to true, will return an empty narrative block for any
* profiles where no template is available
* If set to true, will return an empty narrative block for any profiles where no template is available
*/
public boolean isIgnoreMissingTemplates() {
return myIgnoreMissingTemplates;
@ -247,8 +279,7 @@ public class DefaultThymeleafNarrativeGenerator extends BaseThymeleafNarrativeGe
// }
/**
* If set to true, will return an empty narrative block for any
* profiles where no template is available
* If set to true, will return an empty narrative block for any profiles where no template is available
*/
public void setIgnoreMissingTemplates(boolean theIgnoreMissingTemplates) {
myIgnoreMissingTemplates = theIgnoreMissingTemplates;
@ -263,8 +294,8 @@ public class DefaultThymeleafNarrativeGenerator extends BaseThymeleafNarrativeGe
@Override
public InputStream getResourceAsStream(TemplateProcessingParameters theTemplateProcessingParameters, String theClassName) {
String template = myDatatypeClassNameToNarrativeTemplate.get(theClassName);
if (template==null) {
throw new NullPointerException("No narrative template exists for datatype: " +theClassName);
if (template == null) {
throw new NullPointerException("No narrative template exists for datatype: " + theClassName);
}
return new ReaderInputStream(new StringReader(template));
}
@ -310,7 +341,7 @@ public class DefaultThymeleafNarrativeGenerator extends BaseThymeleafNarrativeGe
}
private final class ProfileResourceResolver implements IResourceResolver {
private final class ProfileResourceResolver implements IResourceResolver {
@Override
public String getName() {
return getClass().getCanonicalName();

View File

@ -1,6 +1,6 @@
<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>
<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

@ -0,0 +1,49 @@
package example;
import java.io.IOException;
import java.util.List;
import ca.uhn.fhir.context.FhirContext;
import ca.uhn.fhir.model.api.ExtensionDt;
import ca.uhn.fhir.model.dstu.composite.AddressDt;
import ca.uhn.fhir.model.dstu.composite.HumanNameDt;
import ca.uhn.fhir.model.dstu.resource.Patient;
import ca.uhn.fhir.model.dstu.valueset.IdentifierUseEnum;
import ca.uhn.fhir.model.dstu.valueset.NarrativeStatusEnum;
import ca.uhn.fhir.model.primitive.DateTimeDt;
import ca.uhn.fhir.model.primitive.StringDt;
import ca.uhn.fhir.narrative.DefaultThymeleafNarrativeGenerator;
import ca.uhn.fhir.narrative.ThymeleafNarrativeGeneratorTest;
import ca.uhn.fhir.parser.DataFormatException;
public class Narrative {
public static void main(String[] args) throws DataFormatException, IOException {
//START SNIPPET: example1
Patient patient = new Patient();
patient.addIdentifier("urn:foo", "7000135");
patient.addName().addFamily("Smith").addGiven("John").addGiven("Edward");
patient.addAddress().addLine("742 Evergreen Terrace").setCity("Springfield").setState("ZZ");
FhirContext ctx = new FhirContext();
// Use the narrative generator
ctx.setNarrativeGenerator(new DefaultThymeleafNarrativeGenerator());
// Encode the output, including the narrative
String output = ctx.newJsonParser().setPrettyPrint(true).encodeResourceToString(patient);
System.out.println(output);
//END SNIPPET: example1
}
public void simple() {
//START SNIPPET: simple
Patient pat = new Patient();
pat.getText().setStatus(NarrativeStatusEnum.GENERATED);
pat.getText().setDiv("<div>This is the narrative text<br/>this is line 2</div>");
//END SNIPPET: simple
}
}

View File

@ -0,0 +1,116 @@
<?xml version="1.0" encoding="UTF-8"?>
<document xmlns="http://maven.apache.org/XDOC/2.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/XDOC/2.0 http://maven.apache.org/xsd/xdoc-2.0.xsd">
<properties>
<title>Narrative Generation - HAPI FHIR</title>
<author email="jamesagnew@users.sourceforge.net">James Agnew</author>
</properties>
<body>
<section name="Narrative Generation">
<macro name="toc">
</macro>
<p>
HAPI provides a several ways to add
<a href="http://hl7.org/implement/standards/fhir/narrative.html">Narrative Text</a>
to your encoded messages.
</p>
<p>
The simplest way is to simply place the narrative text directly in the resource
via the
<a href="./apidocs/ca/uhn/fhir/model/api/IResource.html#getText()">getText()</a>
method.
</p>
<macro name="snippet">
<param name="id" value="simple" />
<param name="file" value="src/site/example/java/example/Narrative.java" />
</macro>
</section>
<section name="Automatic Narrative Generartion">
<p>
HAPI also comes with a built-in mechanism for automatically generating
narratives based on your resources.
</p>
<p>
<b>Warning: </b> This built-in capability is a work in progress, and does not cover
every type of resource or even every attribute in any resource. You should test it
and configure it for your particular use cases.
</p>
<p>
HAPI's built-in narrative generation uses the
<a href="http://www.thymeleaf.org/">Thymeleaf</a> library
for templating narrative texts. Thymeleaf provides a simple
HTML (with extra attributes) syntax which is easy to use and
meshes well with the HAPI data model.
</p>
<subsection name="A Simple Example">
<p>
Activating HAPI's built-in narrative generator is as simple
as calling
<a href="./apidocs/ca/uhn/fhir/context/FhirContext.html#setNarrativeGenerator(ca.uhn.fhir.narrative.INarrativeGenerator)">setNarrativeGenerator</a>.
</p>
<macro name="snippet">
<param name="id" value="example1" />
<param name="file" value="src/site/example/java/example/Narrative.java" />
</macro>
<p>
...which produces the following output:
</p>
<source><![CDATA[<Patient xmlns="http://hl7.org/fhir">
<text>
<status value="generated"/>
<div xmlns="http://www.w3.org/1999/xhtml">
<div class="hapiHeaderText"> John Edward <b>SMITH </b></div>
<table class="hapiPropertyTable">
<tbody>
<tr><td>Identifier</td><td>7000135</td></tr>
<tr><td>Address</td><td><span>742 Evergreen Terrace</span><br/><span>Springfield</span> <span>ZZ</span></td></tr>
</tbody>
</table>
</div>
</text>
<!-- .... snip ..... -->
</Patient>]]></source>
</subsection>
<subsection name="Built-in Narrative Templates">
<p>
HAPI currently only comes with built-in support for
a few resource types. Our intention is that people enhance these
templates and create new ones, and share these back with us so that
we can continue to build out the library. To see the current
template library, see the source repository
<a href="http://sourceforge.net/p/hl7api/fhircode/ci/master/tree/hapi-fhir-base/src/main/resources/ca/uhn/fhir/narrative/">here</a>.
</p>
<p>
Note that these templates expect a few specific CSS definitions
to be present in your site's CSS file. See the
<a href="http://sourceforge.net/p/hl7api/fhircode/ci/master/tree/hapi-fhir-base/src/main/resources/ca/uhn/fhir/narrative/narrative.css">narrative CSS</a>
to see these.
</p>
</subsection>
</section>
</body>
</document>

View File

@ -33,6 +33,57 @@ public class ThymeleafNarrativeGeneratorTest {
gen.setIgnoreMissingTemplates(true);
}
@Test
public void testTrimWhitespace() {
//@formatter:off
String input = "<div>\n" +
" <div class=\"hapiHeaderText\">\n" +
" \n" +
" joe \n" +
" john \n" +
" <b>BLOW </b>\n" +
" \n" +
"</div>\n" +
" <table class=\"hapiPropertyTable\">\n" +
" <tbody>\n" +
" <tr>\n" +
" <td>Identifier</td>\n" +
" <td>123456</td>\n" +
" </tr>\n" +
" <tr>\n" +
" <td>Address</td>\n" +
" <td>\n" +
" \n" +
" <span>123 Fake Street</span><br />\n" +
" \n" +
" \n" +
" <span>Unit 1</span><br />\n" +
" \n" +
" <span>Toronto</span>\n" +
" <span>ON</span>\n" +
" <span>Canada</span>\n" +
" </td>\n" +
" </tr>\n" +
" <tr>\n" +
" <td>Date of birth</td>\n" +
" <td>\n" +
" <span>31 March 2014</span>\n" +
" </td>\n" +
" </tr>\n" +
" </tbody>\n" +
" </table>\n" +
"</div>";
//@formatter:on
String actual = DefaultThymeleafNarrativeGenerator.cleanWhitespace(input);
String expected = "<div><div class=\"hapiHeaderText\"> joe john <b>BLOW </b></div><table class=\"hapiPropertyTable\"><tbody><tr><td>Identifier</td><td>123456</td></tr><tr><td>Address</td><td><span>123 Fake Street</span><br /><span>Unit 1</span><br /><span>Toronto</span><span>ON</span><span>Canada</span></td></tr><tr><td>Date of birth</td><td><span>31 March 2014</span></td></tr></tbody></table></div>";
ourLog.info(actual);
assertEquals(expected, actual);
}
@Test
public void testGeneratePatient() throws DataFormatException {
Patient value = new Patient();
@ -66,10 +117,10 @@ public class ThymeleafNarrativeGeneratorTest {
@Test
public void testGenerateDiagnosticReportWithObservations() throws DataFormatException, IOException {
DiagnosticReport value = new DiagnosticReport();
value.getIssued().setValueAsString("2011-02-22T11:13:00");
value.setStatus(DiagnosticReportStatusEnum.FINAL);
value.getName().setText("Some Diagnostic Report");
{
Observation obs = new Observation();
@ -91,7 +142,7 @@ public class ThymeleafNarrativeGeneratorTest {
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 Diagnostic Report </div>"));
// Now try it with the parser
@ -99,7 +150,7 @@ public class ThymeleafNarrativeGeneratorTest {
context.setNarrativeGenerator(gen);
output = context.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 Diagnostic Report </div>"));
}