More narrative work

This commit is contained in:
jamesagnew 2014-04-01 18:01:52 -04:00
parent 57a3a4881e
commit 81b2b1cc50
5 changed files with 131 additions and 115 deletions

View File

@ -13,6 +13,7 @@ import java.util.Properties;
import org.apache.commons.io.IOUtils; import org.apache.commons.io.IOUtils;
import org.apache.commons.io.input.ReaderInputStream; import org.apache.commons.io.input.ReaderInputStream;
import org.apache.commons.lang3.StringUtils;
import org.thymeleaf.Arguments; import org.thymeleaf.Arguments;
import org.thymeleaf.Configuration; import org.thymeleaf.Configuration;
import org.thymeleaf.TemplateEngine; import org.thymeleaf.TemplateEngine;
@ -32,6 +33,7 @@ import org.thymeleaf.standard.expression.StandardExpressions;
import org.thymeleaf.templateresolver.TemplateResolver; import org.thymeleaf.templateresolver.TemplateResolver;
import org.thymeleaf.util.DOMUtils; import org.thymeleaf.util.DOMUtils;
import ca.uhn.fhir.context.ConfigurationException;
import ca.uhn.fhir.model.api.IResource; import ca.uhn.fhir.model.api.IResource;
import ca.uhn.fhir.model.dstu.composite.NarrativeDt; import ca.uhn.fhir.model.dstu.composite.NarrativeDt;
import ca.uhn.fhir.model.dstu.valueset.NarrativeStatusEnum; import ca.uhn.fhir.model.dstu.valueset.NarrativeStatusEnum;
@ -42,56 +44,44 @@ public abstract class BaseThymeleafNarrativeGenerator implements INarrativeGener
private static final org.slf4j.Logger ourLog = org.slf4j.LoggerFactory.getLogger(BaseThymeleafNarrativeGenerator.class); private static final org.slf4j.Logger ourLog = org.slf4j.LoggerFactory.getLogger(BaseThymeleafNarrativeGenerator.class);
private boolean myCleanWhitespace = true; private boolean myCleanWhitespace = true;
private HashMap<String, String> myDatatypeClassNameToNarrativeTemplate;
private TemplateEngine myDatatypeTemplateEngine;
private boolean myIgnoreFailures = true; private boolean myIgnoreFailures = true;
private boolean myIgnoreMissingTemplates = true; private boolean myIgnoreMissingTemplates = true;
private TemplateEngine myProfileTemplateEngine; private TemplateEngine myProfileTemplateEngine;
private HashMap<String, String> myProfileToNarrativeTemplate; private HashMap<String, String> myProfileToNarrativeName;
private HashMap<Class<?>, String> myClassToNarrativeName;
private HashMap<String, String> myNameToNarrativeTemplate;
public BaseThymeleafNarrativeGenerator() throws IOException { private volatile boolean myInitialized;
myProfileToNarrativeTemplate = new HashMap<String, String>();
myDatatypeClassNameToNarrativeTemplate = new HashMap<String, String>();
String propFileName = getPropertyFile();
loadProperties(propFileName);
{
myProfileTemplateEngine = new TemplateEngine();
TemplateResolver resolver = new TemplateResolver();
resolver.setResourceResolver(new ProfileResourceResolver());
myProfileTemplateEngine.setTemplateResolver(resolver);
StandardDialect dialect = new StandardDialect();
HashSet<IProcessor> additionalProcessors = new HashSet<IProcessor>();
additionalProcessors.add(new MyProcessor());
dialect.setAdditionalProcessors(additionalProcessors);
myProfileTemplateEngine.setDialect(dialect);
myProfileTemplateEngine.initialize();
}
{
myDatatypeTemplateEngine = new TemplateEngine();
TemplateResolver resolver = new TemplateResolver();
resolver.setResourceResolver(new DatatypeResourceResolver());
myDatatypeTemplateEngine.setTemplateResolver(resolver);
myDatatypeTemplateEngine.setDialect(new StandardDialect());
myDatatypeTemplateEngine.initialize();
}
}
@Override @Override
public NarrativeDt generateNarrative(String theProfile, IResource theResource) { public NarrativeDt generateNarrative(String theProfile, IResource theResource) {
if (myIgnoreMissingTemplates && !myProfileToNarrativeTemplate.containsKey(theProfile)) { if (!myInitialized) {
initialize();
}
String name = null;
if (StringUtils.isNotBlank(theProfile)) {
name = myProfileToNarrativeName.get(theProfile);
}
if (name == null) {
name = myClassToNarrativeName.get(theResource.getClass());
}
if (name == null) {
if (myIgnoreMissingTemplates) {
ourLog.debug("No narrative template available for profile: {}", theProfile); ourLog.debug("No narrative template available for profile: {}", theProfile);
return new NarrativeDt(new XhtmlDt("<div>No narrative available</div>"), NarrativeStatusEnum.EMPTY); return new NarrativeDt(new XhtmlDt("<div>No narrative available</div>"), NarrativeStatusEnum.EMPTY);
} else {
throw new DataFormatException("No narrative template for class " + theResource.getClass().getCanonicalName());
}
} }
try { try {
Context context = new Context(); Context context = new Context();
context.setVariable("resource", theResource); context.setVariable("resource", theResource);
String result = myProfileTemplateEngine.process(theProfile, context); String result = myProfileTemplateEngine.process(name, context);
if (myCleanWhitespace) { if (myCleanWhitespace) {
ourLog.trace("Pre-whitespace cleaning: ", result); ourLog.trace("Pre-whitespace cleaning: ", result);
@ -111,15 +101,47 @@ public abstract class BaseThymeleafNarrativeGenerator implements INarrativeGener
} }
} }
private synchronized void initialize() {
if (myInitialized) {
return;
}
myProfileToNarrativeName = new HashMap<String, String>();
myClassToNarrativeName = new HashMap<Class<?>, String>();
myNameToNarrativeTemplate = new HashMap<String, String>();
String propFileName = getPropertyFile();
if (isBlank(propFileName)) {
throw new ConfigurationException("Property file name can not be null");
}
try {
loadProperties(propFileName);
} catch (IOException e) {
throw new ConfigurationException("Can not load property file " + propFileName, e);
}
{
myProfileTemplateEngine = new TemplateEngine();
TemplateResolver resolver = new TemplateResolver();
resolver.setResourceResolver(new ProfileResourceResolver());
myProfileTemplateEngine.setTemplateResolver(resolver);
StandardDialect dialect = new StandardDialect();
HashSet<IProcessor> additionalProcessors = new HashSet<IProcessor>();
additionalProcessors.add(new NarrativeAttributeProcessor());
dialect.setAdditionalProcessors(additionalProcessors);
myProfileTemplateEngine.setDialect(dialect);
myProfileTemplateEngine.initialize();
}
myInitialized = true;
}
protected abstract String getPropertyFile(); protected abstract String getPropertyFile();
/** /**
* If set to <code>true</code> (which is the default), most whitespace will * If set to <code>true</code> (which is the default), most whitespace will be trimmed from the generated narrative before it is returned.
* be trimmed from the generated narrative before it is returned.
* <p> * <p>
* Note that in order to preserve formatting, not all whitespace is trimmed. * 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.
* Repeated whitespace characters (e.g. "\n \n ") will be
* trimmed to a single space.
* </p> * </p>
*/ */
public boolean isCleanWhitespace() { public boolean isCleanWhitespace() {
@ -127,30 +149,24 @@ public abstract class BaseThymeleafNarrativeGenerator implements INarrativeGener
} }
/** /**
* If set to <code>true</code>, which is the default, if any failure occurs * If set to <code>true</code>, which is the default, if any failure occurs during narrative generation the generator will suppress any generated exceptions, and simply return a default narrative
* during narrative generation the generator will suppress any generated * indicating that no narrative is available.
* exceptions, and simply return a default narrative indicating that no
* narrative is available.
*/ */
public boolean isIgnoreFailures() { public boolean isIgnoreFailures() {
return myIgnoreFailures; return myIgnoreFailures;
} }
/** /**
* If set to true, will return an empty narrative block for any profiles * If set to true, will return an empty narrative block for any profiles where no template is available
* where no template is available
*/ */
public boolean isIgnoreMissingTemplates() { public boolean isIgnoreMissingTemplates() {
return myIgnoreMissingTemplates; return myIgnoreMissingTemplates;
} }
/** /**
* If set to <code>true</code> (which is the default), most whitespace will * If set to <code>true</code> (which is the default), most whitespace will be trimmed from the generated narrative before it is returned.
* be trimmed from the generated narrative before it is returned.
* <p> * <p>
* Note that in order to preserve formatting, not all whitespace is trimmed. * 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.
* Repeated whitespace characters (e.g. "\n \n ") will be
* trimmed to a single space.
* </p> * </p>
*/ */
public void setCleanWhitespace(boolean theCleanWhitespace) { public void setCleanWhitespace(boolean theCleanWhitespace) {
@ -158,18 +174,15 @@ public abstract class BaseThymeleafNarrativeGenerator implements INarrativeGener
} }
/** /**
* If set to <code>true</code>, which is the default, if any failure occurs * If set to <code>true</code>, which is the default, if any failure occurs during narrative generation the generator will suppress any generated exceptions, and simply return a default narrative
* during narrative generation the generator will suppress any generated * indicating that no narrative is available.
* exceptions, and simply return a default narrative indicating that no
* narrative is available.
*/ */
public void setIgnoreFailures(boolean theIgnoreFailures) { public void setIgnoreFailures(boolean theIgnoreFailures) {
myIgnoreFailures = theIgnoreFailures; myIgnoreFailures = theIgnoreFailures;
} }
/** /**
* If set to true, will return an empty narrative block for any profiles * If set to true, will return an empty narrative block for any profiles where no template is available
* where no template is available
*/ */
public void setIgnoreMissingTemplates(boolean theIgnoreMissingTemplates) { public void setIgnoreMissingTemplates(boolean theIgnoreMissingTemplates) {
myIgnoreMissingTemplates = theIgnoreMissingTemplates; myIgnoreMissingTemplates = theIgnoreMissingTemplates;
@ -188,15 +201,19 @@ public abstract class BaseThymeleafNarrativeGenerator implements INarrativeGener
continue; continue;
} }
String narrativeName = file.getProperty(name + ".narrative"); String narrativePropName = name + ".narrative";
String narrativeName = file.getProperty(narrativePropName);
if (isBlank(narrativeName)) { if (isBlank(narrativeName)) {
continue; throw new ConfigurationException("Found property '" + nextKey + "' but no corresponding property '" + narrativePropName + "' in file " + propFileName);
} }
String narrative = IOUtils.toString(loadResource(narrativeName)); String narrative = IOUtils.toString(loadResource(narrativeName));
myProfileToNarrativeTemplate.put(file.getProperty(nextKey), narrative); myProfileToNarrativeName.put(file.getProperty(nextKey), name);
} else if (nextKey.endsWith(".dtclass")) { myNameToNarrativeTemplate.put(name, narrative);
String name = nextKey.substring(0, nextKey.indexOf(".dtclass"));
} else if (nextKey.endsWith(".class")) {
String name = nextKey.substring(0, nextKey.indexOf(".class"));
if (isBlank(name)) { if (isBlank(name)) {
continue; continue;
} }
@ -211,13 +228,20 @@ public abstract class BaseThymeleafNarrativeGenerator implements INarrativeGener
continue; continue;
} }
String narrativeName = file.getProperty(name + ".dtnarrative"); String narrativePropName = name + ".narrative";
String narrativeName = file.getProperty(narrativePropName);
if (isBlank(narrativeName)) { if (isBlank(narrativeName)) {
continue; throw new ConfigurationException("Found property '" + nextKey + "' but no corresponding property '" + narrativePropName + "' in file " + propFileName);
} }
String narrative = IOUtils.toString(loadResource(narrativeName)); String narrative = IOUtils.toString(loadResource(narrativeName));
myDatatypeClassNameToNarrativeTemplate.put(dtClass.getCanonicalName(), narrative); myClassToNarrativeName.put(dtClass, name);
myNameToNarrativeTemplate.put(name, narrative);
} else if (nextKey.endsWith(".narrative")) {
continue;
} else {
throw new ConfigurationException("Invalid property name: " + nextKey);
} }
} }
@ -292,9 +316,9 @@ public abstract class BaseThymeleafNarrativeGenerator implements INarrativeGener
return b.toString(); return b.toString();
} }
public class MyProcessor extends AbstractAttrProcessor { public class NarrativeAttributeProcessor extends AbstractAttrProcessor {
protected MyProcessor() { protected NarrativeAttributeProcessor() {
super("narrative"); super("narrative");
} }
@ -316,7 +340,18 @@ public abstract class BaseThymeleafNarrativeGenerator implements INarrativeGener
Context context = new Context(); Context context = new Context();
context.setVariable("resource", value); context.setVariable("resource", value);
String result = myDatatypeTemplateEngine.process(value.getClass().getCanonicalName(), context); String name = myClassToNarrativeName.get(value.getClass());
if (name == null) {
if (myIgnoreMissingTemplates) {
ourLog.debug("No narrative template available for type: {}", value.getClass());
return ProcessorResult.ok();
} else {
throw new DataFormatException("No narrative template for class " + value.getClass());
}
}
String result = myProfileTemplateEngine.process(name, context);
Document dom = DOMUtils.getXhtmlDOMFor(new StringReader(result)); Document dom = DOMUtils.getXhtmlDOMFor(new StringReader(result));
theElement.removeAttribute(theAttributeName); theElement.removeAttribute(theAttributeName);
@ -332,22 +367,6 @@ public abstract class BaseThymeleafNarrativeGenerator implements INarrativeGener
} }
private final class DatatypeResourceResolver implements IResourceResolver {
@Override
public String getName() {
return getClass().getCanonicalName();
}
@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);
}
return new ReaderInputStream(new StringReader(template));
}
}
// public String generateString(Patient theValue) { // public String generateString(Patient theValue) {
// //
// Context context = new Context(); // Context context = new Context();
@ -368,10 +387,10 @@ public abstract class BaseThymeleafNarrativeGenerator implements INarrativeGener
} }
@Override @Override
public InputStream getResourceAsStream(TemplateProcessingParameters theTemplateProcessingParameters, String theResourceName) { public InputStream getResourceAsStream(TemplateProcessingParameters theTemplateProcessingParameters, String theName) {
String template = myProfileToNarrativeTemplate.get(theResourceName); String template = myNameToNarrativeTemplate.get(theName);
if (template == null) { if (template == null) {
ourLog.info("No narative template for resource profile: {}", theResourceName); ourLog.info("No narative template for resource profile: {}", theName);
return new ReaderInputStream(new StringReader("")); return new ReaderInputStream(new StringReader(""));
} }
return new ReaderInputStream(new StringReader(template)); return new ReaderInputStream(new StringReader(template));

View File

@ -3,35 +3,35 @@
# Primitive Datatypes # Primitive Datatypes
################################################ ################################################
string.dtclass=ca.uhn.fhir.model.primitive.StringDt string.class=ca.uhn.fhir.model.primitive.StringDt
string.dtnarrative=classpath:ca/uhn/fhir/narrative/StringDt.html string.narrative=classpath:ca/uhn/fhir/narrative/StringDt.html
datetime.dtclass=ca.uhn.fhir.model.primitive.DateTimeDt datetime.class=ca.uhn.fhir.model.primitive.DateTimeDt
datetime.dtnarrative=classpath:ca/uhn/fhir/narrative/DateTimeDt.html datetime.narrative=classpath:ca/uhn/fhir/narrative/DateTimeDt.html
# Instant uses DateTime narrative # Instant uses DateTime narrative
instant.dtclass=ca.uhn.fhir.model.primitive.InstantDt instant.class=ca.uhn.fhir.model.primitive.InstantDt
instant.dtnarrative=classpath:ca/uhn/fhir/narrative/DateTimeDt.html instant.narrative=classpath:ca/uhn/fhir/narrative/DateTimeDt.html
################################################ ################################################
# Composite Datatypes # Composite Datatypes
################################################ ################################################
address.dtclass=ca.uhn.fhir.model.dstu.composite.AddressDt address.class=ca.uhn.fhir.model.dstu.composite.AddressDt
address.dtnarrative=classpath:ca/uhn/fhir/narrative/AddressDt.html address.narrative=classpath:ca/uhn/fhir/narrative/AddressDt.html
humanname.dtclass=ca.uhn.fhir.model.dstu.composite.HumanNameDt humanname.class=ca.uhn.fhir.model.dstu.composite.HumanNameDt
humanname.dtnarrative=classpath:ca/uhn/fhir/narrative/HumanNameDt.html humanname.narrative=classpath:ca/uhn/fhir/narrative/HumanNameDt.html
quantity.dtclass=ca.uhn.fhir.model.dstu.composite.QuantityDt quantity.class=ca.uhn.fhir.model.dstu.composite.QuantityDt
quantity.dtnarrative=classpath:ca/uhn/fhir/narrative/QuantityDt.html quantity.narrative=classpath:ca/uhn/fhir/narrative/QuantityDt.html
################################################ ################################################
# Resources # Resources
################################################ ################################################
patient.profile=http://hl7.org/fhir/profiles/Patient patient.class=ca.uhn.fhir.model.dstu.resource.Patient
patient.narrative=classpath:ca/uhn/fhir/narrative/Patient.html patient.narrative=classpath:ca/uhn/fhir/narrative/Patient.html
diagnosticreport.profile=http://hl7.org/fhir/profiles/DiagnosticReport diagnosticreport.class=ca.uhn.fhir.model.dstu.resource.DiagnosticReport
diagnosticreport.narrative=classpath:ca/uhn/fhir/narrative/DiagnosticReport.html diagnosticreport.narrative=classpath:ca/uhn/fhir/narrative/DiagnosticReport.html

View File

@ -11,6 +11,8 @@ public class CustomThymeleafNarrativeGeneratorTest {
CustomThymeleafNarrativeGenerator gen = new CustomThymeleafNarrativeGenerator("src/test/resources/narrative/customnarrative.properties"); CustomThymeleafNarrativeGenerator gen = new CustomThymeleafNarrativeGenerator("src/test/resources/narrative/customnarrative.properties");
} }
} }

View File

@ -30,7 +30,7 @@ public class DefaultThymeleafNarrativeGeneratorTest {
public void before() throws IOException { public void before() throws IOException {
gen = new DefaultThymeleafNarrativeGenerator(); gen = new DefaultThymeleafNarrativeGenerator();
gen.setIgnoreFailures(false); gen.setIgnoreFailures(false);
gen.setIgnoreMissingTemplates(true); gen.setIgnoreMissingTemplates(false);
} }
@Test @Test

View File

@ -1,21 +1,16 @@
# Each resource to be defined has a pair or properties. # Each resource to be defined has a pair or properties.
# #
# The first (name.profile) defines the profile for the resource # The first (name.class) defines the class name of the
# to generate a narrative for. This profile URL string can be # resource to define a template for
# found by examining the JavaDoc for the resource in question.
# #
# The second (name.narrative) defines the path/classpath to the # The second (name.narrative) defines the path/classpath to the
# template file. # template file.
# Format is file:/path/foo.html or classpath:/com/classpath/foo.html # Format is file:/path/foo.html or classpath:/com/classpath/foo.html
# #
practitioner.profile=http://hl7.org/fhir/profiles/Practitioner practitioner.class=ca.uhn.fhir.model.dstu.resource.Practitioner
practitioner.narrative=file:src/test/resource/narrative/Practitioner.html practitioner.narrative=file:src/test/resource/narrative/Practitioner.html
# You may also override/define behaviour for datatypes, but the # You may also override/define behaviour for datatypes
# format is a bit different. humanname.class=ca.uhn.fhir.model.dstu.composite.HumanNameDt
# humanname.narrative=classpath:ca/uhn/fhir/narrative/HumanNameDt.html
# For these, the first property (name.dtclass) is the classname
# The second property (name.dtnarrative) is the
humanname.dtclass=ca.uhn.fhir.model.dstu.composite.HumanNameDt
humanname.dtnarrative=classpath:ca/uhn/fhir/narrative/HumanNameDt.html