Narrative Generator refactoring (#1219)
* Starting work on this * Working on narrative templates * Work on new narrative system * Account for some failing tests due to refactoring of narrative mode * Another test fix * More test fixes * One more test fix * Work on searches * Address review comments
This commit is contained in:
parent
94f9ffa977
commit
48c10bddc5
|
@ -108,7 +108,7 @@ public class JpaServerDemo extends RestfulServer {
|
|||
* This server tries to dynamically generate narratives
|
||||
*/
|
||||
FhirContext ctx = getFhirContext();
|
||||
ctx.setNarrativeGenerator(new DefaultThymeleafNarrativeGenerator());
|
||||
ctx.setNarrativeGenerator(new DefaultThymeleafNarrativeGenerator(getFhirContext()));
|
||||
|
||||
/*
|
||||
* Default to JSON and pretty printing
|
||||
|
|
|
@ -108,7 +108,7 @@ public class JpaServerDemoDstu2 extends RestfulServer {
|
|||
* This server tries to dynamically generate narratives
|
||||
*/
|
||||
FhirContext ctx = getFhirContext();
|
||||
ctx.setNarrativeGenerator(new DefaultThymeleafNarrativeGenerator());
|
||||
ctx.setNarrativeGenerator(new DefaultThymeleafNarrativeGenerator(getFhirContext()));
|
||||
|
||||
/*
|
||||
* Default to JSON and pretty printing
|
||||
|
|
|
@ -75,7 +75,7 @@ public class JpaServerDemo extends RestfulServer {
|
|||
* This server tries to dynamically generate narratives
|
||||
*/
|
||||
FhirContext ctx = getFhirContext();
|
||||
ctx.setNarrativeGenerator(new DefaultThymeleafNarrativeGenerator());
|
||||
ctx.setNarrativeGenerator(new DefaultThymeleafNarrativeGenerator(getFhirContext()));
|
||||
|
||||
/*
|
||||
* Default to JSON and pretty printing
|
||||
|
|
|
@ -21,7 +21,7 @@ patient.addAddress().addLine("742 Evergreen Terrace").setCity("Springfield").set
|
|||
FhirContext ctx = FhirContext.forDstu2();
|
||||
|
||||
// Use the narrative generator
|
||||
ctx.setNarrativeGenerator(new DefaultThymeleafNarrativeGenerator());
|
||||
ctx.setNarrativeGenerator(new DefaultThymeleafNarrativeGenerator(ctx));
|
||||
|
||||
// Encode the output, including the narrative
|
||||
String output = ctx.newJsonParser().setPrettyPrint(true).encodeResourceToString(patient);
|
||||
|
|
|
@ -10,10 +10,10 @@ public class NarrativeGenerator {
|
|||
public void testGenerator() throws IOException {
|
||||
|
||||
//START SNIPPET: gen
|
||||
String propFile = "classpath:/com/foo/customnarrative.properties";
|
||||
CustomThymeleafNarrativeGenerator gen = new CustomThymeleafNarrativeGenerator(propFile);
|
||||
|
||||
FhirContext ctx = FhirContext.forDstu2();
|
||||
String propFile = "classpath:/com/foo/customnarrative.properties";
|
||||
CustomThymeleafNarrativeGenerator gen = new CustomThymeleafNarrativeGenerator(ctx, propFile);
|
||||
|
||||
ctx.setNarrativeGenerator(gen);
|
||||
//END SNIPPET: gen
|
||||
|
||||
|
|
|
@ -8,7 +8,7 @@ import java.util.HashMap;
|
|||
import java.util.Map;
|
||||
|
||||
import org.apache.commons.io.IOUtils;
|
||||
import org.hl7.fhir.dstu3.hapi.validation.DefaultProfileValidationSupport;
|
||||
import org.hl7.fhir.dstu3.hapi.ctx.DefaultProfileValidationSupport;
|
||||
import org.hl7.fhir.dstu3.hapi.validation.FhirInstanceValidator;
|
||||
import org.hl7.fhir.dstu3.hapi.validation.PrePopulatedValidationSupport;
|
||||
import org.hl7.fhir.dstu3.hapi.validation.ValidationSupportChain;
|
||||
|
|
|
@ -8,6 +8,7 @@ import javax.servlet.ServletException;
|
|||
|
||||
import org.apache.commons.io.IOUtils;
|
||||
import org.apache.commons.io.filefilter.WildcardFileFilter;
|
||||
import org.hl7.fhir.dstu3.hapi.ctx.DefaultProfileValidationSupport;
|
||||
import org.hl7.fhir.dstu3.hapi.ctx.IValidationSupport;
|
||||
import org.hl7.fhir.dstu3.hapi.validation.*;
|
||||
import org.hl7.fhir.dstu3.model.*;
|
||||
|
|
|
@ -1,8 +1,7 @@
|
|||
package example;
|
||||
|
||||
import org.hl7.fhir.dstu3.hapi.validation.DefaultProfileValidationSupport;
|
||||
import org.hl7.fhir.dstu3.hapi.ctx.DefaultProfileValidationSupport;
|
||||
import org.hl7.fhir.dstu3.hapi.validation.FhirInstanceValidator;
|
||||
import org.hl7.fhir.dstu3.hapi.ctx.IValidationSupport;
|
||||
import org.hl7.fhir.dstu3.hapi.validation.ValidationSupportChain;
|
||||
|
||||
import ca.uhn.fhir.context.FhirContext;
|
||||
|
|
|
@ -128,8 +128,9 @@ public abstract class BaseResourceReferenceDt extends BaseIdentifiableElement im
|
|||
}
|
||||
|
||||
@Override
|
||||
public void setResource(IBaseResource theResource) {
|
||||
public BaseResourceReferenceDt setResource(IBaseResource theResource) {
|
||||
myResource = theResource;
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
|
|
|
@ -19,588 +19,53 @@ package ca.uhn.fhir.narrative;
|
|||
* limitations under the License.
|
||||
* #L%
|
||||
*/
|
||||
import static org.apache.commons.lang3.StringUtils.isBlank;
|
||||
|
||||
import java.io.*;
|
||||
import java.util.*;
|
||||
|
||||
import org.apache.commons.io.IOUtils;
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import org.hl7.fhir.instance.model.api.*;
|
||||
import org.thymeleaf.IEngineConfiguration;
|
||||
import org.thymeleaf.TemplateEngine;
|
||||
import org.thymeleaf.cache.AlwaysValidCacheEntryValidity;
|
||||
import org.thymeleaf.cache.ICacheEntryValidity;
|
||||
import org.thymeleaf.context.Context;
|
||||
import org.thymeleaf.context.ITemplateContext;
|
||||
import org.thymeleaf.engine.AttributeName;
|
||||
import org.thymeleaf.messageresolver.IMessageResolver;
|
||||
import org.thymeleaf.model.IProcessableElementTag;
|
||||
import org.thymeleaf.processor.IProcessor;
|
||||
import org.thymeleaf.processor.element.AbstractAttributeTagProcessor;
|
||||
import org.thymeleaf.processor.element.IElementTagStructureHandler;
|
||||
import org.thymeleaf.standard.StandardDialect;
|
||||
import org.thymeleaf.standard.expression.*;
|
||||
import org.thymeleaf.templatemode.TemplateMode;
|
||||
import org.thymeleaf.templateresolver.DefaultTemplateResolver;
|
||||
import org.thymeleaf.templateresource.ITemplateResource;
|
||||
import org.thymeleaf.templateresource.StringTemplateResource;
|
||||
|
||||
import ca.uhn.fhir.context.ConfigurationException;
|
||||
import ca.uhn.fhir.context.FhirContext;
|
||||
import ca.uhn.fhir.model.api.IDatatype;
|
||||
import ca.uhn.fhir.parser.DataFormatException;
|
||||
import ca.uhn.fhir.rest.api.Constants;
|
||||
import ca.uhn.fhir.narrative2.NarrativeTemplateManifest;
|
||||
import ca.uhn.fhir.narrative2.ThymeleafNarrativeGenerator;
|
||||
import ca.uhn.fhir.rest.server.exceptions.InternalErrorException;
|
||||
import org.hl7.fhir.instance.model.api.IBaseResource;
|
||||
|
||||
public abstract class BaseThymeleafNarrativeGenerator implements INarrativeGenerator {
|
||||
import java.io.IOException;
|
||||
import java.util.List;
|
||||
|
||||
private static final org.slf4j.Logger ourLog = org.slf4j.LoggerFactory.getLogger(BaseThymeleafNarrativeGenerator.class);
|
||||
public abstract class BaseThymeleafNarrativeGenerator extends ThymeleafNarrativeGenerator {
|
||||
|
||||
private boolean myApplyDefaultDatatypeTemplates = true;
|
||||
|
||||
private HashMap<Class<?>, String> myClassToName;
|
||||
private boolean myCleanWhitespace = true;
|
||||
private boolean myIgnoreFailures = true;
|
||||
private boolean myIgnoreMissingTemplates = true;
|
||||
private volatile boolean myInitialized;
|
||||
private HashMap<String, String> myNameToNarrativeTemplate;
|
||||
private TemplateEngine myProfileTemplateEngine;
|
||||
|
||||
private IMessageResolver resolver;
|
||||
private boolean myInitialized;
|
||||
|
||||
/**
|
||||
* Constructor
|
||||
*/
|
||||
public BaseThymeleafNarrativeGenerator() {
|
||||
super();
|
||||
public BaseThymeleafNarrativeGenerator(FhirContext theFhirContext) {
|
||||
super(theFhirContext);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void generateNarrative(FhirContext theContext, IBaseResource theResource, INarrative theNarrative) {
|
||||
public boolean populateResourceNarrative(IBaseResource theResource) {
|
||||
if (!myInitialized) {
|
||||
initialize(theContext);
|
||||
}
|
||||
|
||||
String name = myClassToName.get(theResource.getClass());
|
||||
if (name == null) {
|
||||
name = theContext.getResourceDefinition(theResource).getName().toLowerCase();
|
||||
}
|
||||
|
||||
if (name == null || !myNameToNarrativeTemplate.containsKey(name)) {
|
||||
if (myIgnoreMissingTemplates) {
|
||||
ourLog.debug("No narrative template available for resorce: {}", name);
|
||||
return;
|
||||
}
|
||||
throw new DataFormatException("No narrative template for class " + theResource.getClass().getCanonicalName());
|
||||
}
|
||||
|
||||
try {
|
||||
Context context = new Context();
|
||||
context.setVariable("resource", theResource);
|
||||
context.setVariable("fhirVersion", theContext.getVersion().getVersion().name());
|
||||
|
||||
String result = myProfileTemplateEngine.process(name, context);
|
||||
|
||||
if (myCleanWhitespace) {
|
||||
ourLog.trace("Pre-whitespace cleaning: ", result);
|
||||
result = cleanWhitespace(result);
|
||||
ourLog.trace("Post-whitespace cleaning: ", result);
|
||||
}
|
||||
|
||||
if (isBlank(result)) {
|
||||
return;
|
||||
}
|
||||
|
||||
theNarrative.setDivAsString(result);
|
||||
theNarrative.setStatusAsString("generated");
|
||||
return;
|
||||
} catch (Exception e) {
|
||||
if (myIgnoreFailures) {
|
||||
ourLog.error("Failed to generate narrative", e);
|
||||
try {
|
||||
theNarrative.setDivAsString("<div>No narrative available - Error: " + e.getMessage() + "</div>");
|
||||
} catch (Exception e1) {
|
||||
// last resort..
|
||||
}
|
||||
theNarrative.setStatusAsString("empty");
|
||||
return;
|
||||
}
|
||||
throw new DataFormatException(e);
|
||||
initialize();
|
||||
}
|
||||
super.populateResourceNarrative(theResource);
|
||||
return false;
|
||||
}
|
||||
|
||||
protected abstract List<String> getPropertyFile();
|
||||
|
||||
private synchronized void initialize(final FhirContext theContext) {
|
||||
private synchronized void initialize() {
|
||||
if (myInitialized) {
|
||||
return;
|
||||
}
|
||||
|
||||
ourLog.info("Initializing narrative generator");
|
||||
|
||||
myClassToName = new HashMap<Class<?>, String>();
|
||||
myNameToNarrativeTemplate = new HashMap<String, String>();
|
||||
|
||||
List<String> propFileName = getPropertyFile();
|
||||
|
||||
try {
|
||||
if (myApplyDefaultDatatypeTemplates) {
|
||||
loadProperties(DefaultThymeleafNarrativeGenerator.NARRATIVES_PROPERTIES);
|
||||
}
|
||||
for (String next : propFileName) {
|
||||
loadProperties(next);
|
||||
}
|
||||
NarrativeTemplateManifest manifest = NarrativeTemplateManifest.forManifestFileLocation(getFhirContext(), propFileName);
|
||||
setManifest(manifest);
|
||||
} catch (IOException e) {
|
||||
ourLog.info("Failed to load property file " + propFileName, e);
|
||||
throw new ConfigurationException("Can not load property file " + propFileName, e);
|
||||
}
|
||||
|
||||
{
|
||||
myProfileTemplateEngine = new TemplateEngine();
|
||||
ProfileResourceResolver resolver = new ProfileResourceResolver();
|
||||
myProfileTemplateEngine.setTemplateResolver(resolver);
|
||||
StandardDialect dialect = new StandardDialect() {
|
||||
@Override
|
||||
public Set<IProcessor> getProcessors(String theDialectPrefix) {
|
||||
Set<IProcessor> retVal = super.getProcessors(theDialectPrefix);
|
||||
retVal.add(new NarrativeAttributeProcessor(theContext, theDialectPrefix));
|
||||
return retVal;
|
||||
}
|
||||
|
||||
};
|
||||
myProfileTemplateEngine.setDialect(dialect);
|
||||
if (this.resolver != null) {
|
||||
myProfileTemplateEngine.setMessageResolver(this.resolver);
|
||||
}
|
||||
throw new InternalErrorException(e);
|
||||
}
|
||||
|
||||
myInitialized = true;
|
||||
}
|
||||
|
||||
public void setMessageResolver(IMessageResolver resolver) {
|
||||
this.resolver = resolver;
|
||||
if (myProfileTemplateEngine != null && resolver != null) {
|
||||
myProfileTemplateEngine.setMessageResolver(resolver);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 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;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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 indicating that no
|
||||
* narrative is available.
|
||||
*/
|
||||
public boolean isIgnoreFailures() {
|
||||
return myIgnoreFailures;
|
||||
}
|
||||
|
||||
/**
|
||||
* If set to true, will return an empty narrative block for any profiles where no template is available
|
||||
*/
|
||||
public boolean isIgnoreMissingTemplates() {
|
||||
return myIgnoreMissingTemplates;
|
||||
}
|
||||
|
||||
private void loadProperties(String propFileName) throws IOException {
|
||||
ourLog.debug("Loading narrative properties file: {}", propFileName);
|
||||
|
||||
Properties file = new Properties();
|
||||
|
||||
InputStream resource = loadResource(propFileName);
|
||||
file.load(resource);
|
||||
for (Object nextKeyObj : file.keySet()) {
|
||||
String nextKey = (String) nextKeyObj;
|
||||
if (nextKey.endsWith(".profile")) {
|
||||
String name = nextKey.substring(0, nextKey.indexOf(".profile"));
|
||||
if (isBlank(name)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
String narrativePropName = name + ".narrative";
|
||||
String narrativeName = file.getProperty(narrativePropName);
|
||||
if (isBlank(narrativeName)) {
|
||||
//FIXME resource leak
|
||||
throw new ConfigurationException("Found property '" + nextKey + "' but no corresponding property '" + narrativePropName + "' in file " + propFileName);
|
||||
}
|
||||
|
||||
if (StringUtils.isNotBlank(narrativeName)) {
|
||||
String narrative = IOUtils.toString(loadResource(narrativeName), Constants.CHARSET_UTF8);
|
||||
myNameToNarrativeTemplate.put(name, narrative);
|
||||
}
|
||||
|
||||
} else if (nextKey.endsWith(".class")) {
|
||||
|
||||
String name = nextKey.substring(0, nextKey.indexOf(".class"));
|
||||
if (isBlank(name)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
String className = file.getProperty(nextKey);
|
||||
|
||||
Class<?> clazz;
|
||||
try {
|
||||
clazz = Class.forName(className);
|
||||
} catch (ClassNotFoundException e) {
|
||||
ourLog.debug("Unknown datatype class '{}' identified in narrative file {}", name, propFileName);
|
||||
clazz = null;
|
||||
}
|
||||
|
||||
if (clazz != null) {
|
||||
myClassToName.put(clazz, name);
|
||||
}
|
||||
|
||||
} else if (nextKey.endsWith(".narrative")) {
|
||||
String name = nextKey.substring(0, nextKey.indexOf(".narrative"));
|
||||
if (isBlank(name)) {
|
||||
continue;
|
||||
}
|
||||
String narrativePropName = name + ".narrative";
|
||||
String narrativeName = file.getProperty(narrativePropName);
|
||||
if (StringUtils.isNotBlank(narrativeName)) {
|
||||
String narrative = IOUtils.toString(loadResource(narrativeName), Constants.CHARSET_UTF8);
|
||||
myNameToNarrativeTemplate.put(name, narrative);
|
||||
}
|
||||
continue;
|
||||
} else if (nextKey.endsWith(".title")) {
|
||||
ourLog.debug("Ignoring title property as narrative generator no longer generates titles: {}", nextKey);
|
||||
} else {
|
||||
throw new ConfigurationException("Invalid property name: " + nextKey);
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
private InputStream loadResource(String name) throws IOException {
|
||||
if (name.startsWith("classpath:")) {
|
||||
String cpName = name.substring("classpath:".length());
|
||||
InputStream resource = DefaultThymeleafNarrativeGenerator.class.getResourceAsStream(cpName);
|
||||
if (resource == null) {
|
||||
resource = DefaultThymeleafNarrativeGenerator.class.getResourceAsStream("/" + cpName);
|
||||
if (resource == null) {
|
||||
throw new IOException("Can not find '" + cpName + "' on classpath");
|
||||
}
|
||||
}
|
||||
//FIXME resource leak
|
||||
return resource;
|
||||
} else if (name.startsWith("file:")) {
|
||||
File file = new File(name.substring("file:".length()));
|
||||
if (file.exists() == false) {
|
||||
throw new IOException("File not found: " + file.getAbsolutePath());
|
||||
}
|
||||
return new FileInputStream(file);
|
||||
} else {
|
||||
throw new IOException("Invalid resource name: '" + name + "' (must start with classpath: or file: )");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 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 void setCleanWhitespace(boolean theCleanWhitespace) {
|
||||
myCleanWhitespace = theCleanWhitespace;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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 indicating that no
|
||||
* narrative is available.
|
||||
*/
|
||||
public void setIgnoreFailures(boolean theIgnoreFailures) {
|
||||
myIgnoreFailures = theIgnoreFailures;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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;
|
||||
}
|
||||
|
||||
static String cleanWhitespace(String theResult) {
|
||||
StringBuilder b = new StringBuilder();
|
||||
boolean inWhitespace = false;
|
||||
boolean betweenTags = false;
|
||||
boolean lastNonWhitespaceCharWasTagEnd = false;
|
||||
boolean inPre = false;
|
||||
for (int i = 0; i < theResult.length(); i++) {
|
||||
char nextChar = theResult.charAt(i);
|
||||
if (inPre) {
|
||||
b.append(nextChar);
|
||||
continue;
|
||||
} else if (nextChar == '>') {
|
||||
b.append(nextChar);
|
||||
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;
|
||||
lastNonWhitespaceCharWasTagEnd = false;
|
||||
if (i + 3 < theResult.length()) {
|
||||
char char1 = Character.toLowerCase(theResult.charAt(i + 1));
|
||||
char char2 = Character.toLowerCase(theResult.charAt(i + 2));
|
||||
char char3 = Character.toLowerCase(theResult.charAt(i + 3));
|
||||
char char4 = Character.toLowerCase((i + 4 < theResult.length()) ? theResult.charAt(i + 4) : ' ');
|
||||
if (char1 == 'p' && char2 == 'r' && char3 == 'e') {
|
||||
inPre = true;
|
||||
} else if (char1 == '/' && char2 == 'p' && char3 == 'r' && char4 == 'e') {
|
||||
inPre = false;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
lastNonWhitespaceCharWasTagEnd = false;
|
||||
if (inWhitespace) {
|
||||
b.append(' ');
|
||||
inWhitespace = false;
|
||||
}
|
||||
b.append(nextChar);
|
||||
}
|
||||
} else {
|
||||
b.append(nextChar);
|
||||
}
|
||||
}
|
||||
return b.toString();
|
||||
}
|
||||
|
||||
public class NarrativeAttributeProcessor extends AbstractAttributeTagProcessor {
|
||||
|
||||
private FhirContext myContext;
|
||||
|
||||
protected NarrativeAttributeProcessor(FhirContext theContext, String theDialectPrefix) {
|
||||
super(TemplateMode.XML, theDialectPrefix, null, false, "narrative", true, 0, true);
|
||||
myContext = theContext;
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
@Override
|
||||
protected void doProcess(ITemplateContext theContext, IProcessableElementTag theTag, AttributeName theAttributeName, String theAttributeValue, IElementTagStructureHandler theStructureHandler) {
|
||||
IEngineConfiguration configuration = theContext.getConfiguration();
|
||||
IStandardExpressionParser expressionParser = StandardExpressions.getExpressionParser(configuration);
|
||||
|
||||
final IStandardExpression expression = expressionParser.parseExpression(theContext, theAttributeValue);
|
||||
final Object value = expression.execute(theContext);
|
||||
|
||||
if (value == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
Context context = new Context();
|
||||
context.setVariable("fhirVersion", myContext.getVersion().getVersion().name());
|
||||
context.setVariable("resource", value);
|
||||
|
||||
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 (value instanceof IBaseResource) {
|
||||
name = myContext.getResourceDefinition((Class<? extends IBaseResource>) value).getName();
|
||||
} else if (value instanceof IDatatype) {
|
||||
name = value.getClass().getSimpleName();
|
||||
name = name.substring(0, name.length() - 2);
|
||||
} else if (value instanceof IBaseDatatype) {
|
||||
name = value.getClass().getSimpleName();
|
||||
if (name.endsWith("Type")) {
|
||||
name = name.substring(0, name.length() - 4);
|
||||
}
|
||||
} else {
|
||||
throw new DataFormatException("Don't know how to determine name for type: " + value.getClass());
|
||||
}
|
||||
name = name.toLowerCase();
|
||||
if (!myNameToNarrativeTemplate.containsKey(name)) {
|
||||
name = null;
|
||||
}
|
||||
}
|
||||
|
||||
if (name == null) {
|
||||
if (myIgnoreMissingTemplates) {
|
||||
ourLog.debug("No narrative template available for type: {}", value.getClass());
|
||||
return;
|
||||
}
|
||||
throw new DataFormatException("No narrative template for class " + value.getClass());
|
||||
}
|
||||
|
||||
String result = myProfileTemplateEngine.process(name, context);
|
||||
String trim = result.trim();
|
||||
|
||||
theStructureHandler.setBody(trim, true);
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// public class NarrativeAttributeProcessor extends AbstractAttributeTagProcessor {
|
||||
//
|
||||
// private FhirContext myContext;
|
||||
//
|
||||
// protected NarrativeAttributeProcessor(FhirContext theContext) {
|
||||
// super()
|
||||
// myContext = theContext;
|
||||
// }
|
||||
//
|
||||
// @Override
|
||||
// public int getPrecedence() {
|
||||
// return 0;
|
||||
// }
|
||||
//
|
||||
// @SuppressWarnings("unchecked")
|
||||
// @Override
|
||||
// protected ProcessorResult processAttribute(Arguments theArguments, Element theElement, String theAttributeName) {
|
||||
// final String attributeValue = theElement.getAttributeValue(theAttributeName);
|
||||
//
|
||||
// final Configuration configuration = theArguments.getConfiguration();
|
||||
// final IStandardExpressionParser expressionParser = StandardExpressions.getExpressionParser(configuration);
|
||||
//
|
||||
// final IStandardExpression expression = expressionParser.parseExpression(configuration, theArguments, attributeValue);
|
||||
// final Object value = expression.execute(configuration, theArguments);
|
||||
//
|
||||
// theElement.removeAttribute(theAttributeName);
|
||||
// theElement.clearChildren();
|
||||
//
|
||||
// if (value == null) {
|
||||
// return ProcessorResult.ok();
|
||||
// }
|
||||
//
|
||||
// Context context = new Context();
|
||||
// context.setVariable("fhirVersion", myContext.getVersion().getVersion().name());
|
||||
// context.setVariable("resource", value);
|
||||
//
|
||||
// String name = null;
|
||||
// if (value != 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 (value instanceof IBaseResource) {
|
||||
// name = myContext.getResourceDefinition((Class<? extends IBaseResource>) value).getName();
|
||||
// } else if (value instanceof IDatatype) {
|
||||
// name = value.getClass().getSimpleName();
|
||||
// name = name.substring(0, name.length() - 2);
|
||||
// } else if (value instanceof IBaseDatatype) {
|
||||
// name = value.getClass().getSimpleName();
|
||||
// if (name.endsWith("Type")) {
|
||||
// name = name.substring(0, name.length() - 4);
|
||||
// }
|
||||
// } else {
|
||||
// throw new DataFormatException("Don't know how to determine name for type: " + value.getClass());
|
||||
// }
|
||||
// name = name.toLowerCase();
|
||||
// if (!myNameToNarrativeTemplate.containsKey(name)) {
|
||||
// name = null;
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// 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);
|
||||
// String trim = result.trim();
|
||||
// if (!isBlank(trim + "AAA")) {
|
||||
// Document dom = getXhtmlDOMFor(new StringReader(trim));
|
||||
//
|
||||
// Element firstChild = (Element) dom.getFirstChild();
|
||||
// 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);
|
||||
// }
|
||||
//
|
||||
// }
|
||||
//
|
||||
//
|
||||
// return ProcessorResult.ok();
|
||||
// }
|
||||
//
|
||||
// }
|
||||
|
||||
// public String generateString(Patient theValue) {
|
||||
//
|
||||
// Context context = new Context();
|
||||
// context.setVariable("resource", theValue);
|
||||
// String result =
|
||||
// myProfileTemplateEngine.process("ca/uhn/fhir/narrative/Patient.html",
|
||||
// context);
|
||||
//
|
||||
// ourLog.info("Result: {}", result);
|
||||
//
|
||||
// return result;
|
||||
// }
|
||||
|
||||
private final class ProfileResourceResolver extends DefaultTemplateResolver {
|
||||
|
||||
@Override
|
||||
protected boolean computeResolvable(IEngineConfiguration theConfiguration, String theOwnerTemplate, String theTemplate, Map<String, Object> theTemplateResolutionAttributes) {
|
||||
String template = myNameToNarrativeTemplate.get(theTemplate);
|
||||
return template != null;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected TemplateMode computeTemplateMode(IEngineConfiguration theConfiguration, String theOwnerTemplate, String theTemplate, Map<String, Object> theTemplateResolutionAttributes) {
|
||||
return TemplateMode.XML;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected ITemplateResource computeTemplateResource(IEngineConfiguration theConfiguration, String theOwnerTemplate, String theTemplate, Map<String, Object> theTemplateResolutionAttributes) {
|
||||
String template = myNameToNarrativeTemplate.get(theTemplate);
|
||||
return new StringTemplateResource(template);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected ICacheEntryValidity computeValidity(IEngineConfiguration theConfiguration, String theOwnerTemplate, String theTemplate, Map<String, Object> theTemplateResolutionAttributes) {
|
||||
return AlwaysValidCacheEntryValidity.INSTANCE;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -23,6 +23,7 @@ package ca.uhn.fhir.narrative;
|
|||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
|
||||
import ca.uhn.fhir.context.FhirContext;
|
||||
import org.apache.commons.lang3.Validate;
|
||||
|
||||
public class CustomThymeleafNarrativeGenerator extends BaseThymeleafNarrativeGenerator {
|
||||
|
@ -39,7 +40,8 @@ public class CustomThymeleafNarrativeGenerator extends BaseThymeleafNarrativeGen
|
|||
* <li>classpath:/com/package/file.properties</li>
|
||||
* </ul>
|
||||
*/
|
||||
public CustomThymeleafNarrativeGenerator(String... thePropertyFile) {
|
||||
public CustomThymeleafNarrativeGenerator(FhirContext theFhirContext, String... thePropertyFile) {
|
||||
super(theFhirContext);
|
||||
setPropertyFile(thePropertyFile);
|
||||
}
|
||||
|
||||
|
|
|
@ -20,6 +20,8 @@ package ca.uhn.fhir.narrative;
|
|||
* #L%
|
||||
*/
|
||||
|
||||
import ca.uhn.fhir.context.FhirContext;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
|
@ -30,6 +32,10 @@ public class DefaultThymeleafNarrativeGenerator extends BaseThymeleafNarrativeGe
|
|||
|
||||
private boolean myUseHapiServerConformanceNarrative;
|
||||
|
||||
public DefaultThymeleafNarrativeGenerator(FhirContext theFhirContext) {
|
||||
super(theFhirContext);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected List<String> getPropertyFile() {
|
||||
List<String> retVal = new ArrayList<String>();
|
||||
|
|
|
@ -21,12 +21,17 @@ package ca.uhn.fhir.narrative;
|
|||
*/
|
||||
|
||||
import org.hl7.fhir.instance.model.api.IBaseResource;
|
||||
import org.hl7.fhir.instance.model.api.INarrative;
|
||||
|
||||
import ca.uhn.fhir.context.FhirContext;
|
||||
|
||||
public interface INarrativeGenerator {
|
||||
|
||||
void generateNarrative(FhirContext theContext, IBaseResource theResource, INarrative theNarrative);
|
||||
/**
|
||||
* Generate any narratives for the given resource that have applicable
|
||||
* templates, and populates the appropriate field(s). This almost always means
|
||||
* the <code>Resource.text.narrative</code> field, but for some resource types
|
||||
* it can mean other fields (e.g. <code>Composition.</code>
|
||||
*
|
||||
* @return Returns <code>true</code> if a narrative was actually generated
|
||||
*/
|
||||
boolean populateResourceNarrative(IBaseResource theResource);
|
||||
|
||||
}
|
||||
|
|
|
@ -0,0 +1,221 @@
|
|||
package ca.uhn.fhir.narrative2;
|
||||
|
||||
/*-
|
||||
* #%L
|
||||
* HAPI FHIR - Core Library
|
||||
* %%
|
||||
* Copyright (C) 2014 - 2019 University Health Network
|
||||
* %%
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
* #L%
|
||||
*/
|
||||
|
||||
import ca.uhn.fhir.context.BaseRuntimeChildDefinition;
|
||||
import ca.uhn.fhir.context.BaseRuntimeElementCompositeDefinition;
|
||||
import ca.uhn.fhir.context.FhirContext;
|
||||
import ca.uhn.fhir.context.FhirVersionEnum;
|
||||
import ca.uhn.fhir.fluentpath.IFluentPath;
|
||||
import ca.uhn.fhir.narrative.INarrativeGenerator;
|
||||
import ca.uhn.fhir.rest.server.exceptions.InternalErrorException;
|
||||
import org.apache.commons.lang3.Validate;
|
||||
import org.hl7.fhir.instance.model.api.IBase;
|
||||
import org.hl7.fhir.instance.model.api.IBaseResource;
|
||||
import org.hl7.fhir.instance.model.api.INarrative;
|
||||
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
import java.util.Set;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import static org.apache.commons.lang3.StringUtils.defaultIfEmpty;
|
||||
import static org.apache.commons.lang3.StringUtils.isNotBlank;
|
||||
|
||||
public abstract class BaseNarrativeGenerator implements INarrativeGenerator {
|
||||
|
||||
private INarrativeTemplateManifest myManifest;
|
||||
private final FhirContext myFhirContext;
|
||||
|
||||
public BaseNarrativeGenerator(FhirContext theFhirContext) {
|
||||
Validate.notNull(theFhirContext, "theFhirContext must not be null");
|
||||
myFhirContext = theFhirContext;
|
||||
}
|
||||
|
||||
public INarrativeTemplateManifest getManifest() {
|
||||
return myManifest;
|
||||
}
|
||||
|
||||
public void setManifest(INarrativeTemplateManifest theManifest) {
|
||||
myManifest = theManifest;
|
||||
}
|
||||
|
||||
public FhirContext getFhirContext() {
|
||||
return myFhirContext;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean populateResourceNarrative(IBaseResource theResource) {
|
||||
Optional<INarrativeTemplate> templateOpt = getTemplateForElement(theResource);
|
||||
if (templateOpt.isPresent()) {
|
||||
return applyTemplate(templateOpt.get(), theResource);
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
private Optional<INarrativeTemplate> getTemplateForElement(IBase theElement) {
|
||||
return myManifest.getTemplateByElement(getStyle(), theElement);
|
||||
}
|
||||
|
||||
private boolean applyTemplate(INarrativeTemplate theTemplate, IBaseResource theResource) {
|
||||
if (templateDoesntApplyToResource(theTemplate, theResource)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
boolean retVal = false;
|
||||
String resourceName = myFhirContext.getResourceDefinition(theResource).getName();
|
||||
String contextPath = defaultIfEmpty(theTemplate.getContextPath(), resourceName);
|
||||
|
||||
// Narrative templates define a path within the resource that they apply to. Here, we're
|
||||
// finding anywhere in the resource that gets a narrative
|
||||
List<IBase> targets = findElementsInResourceRequiringNarratives(theResource, contextPath);
|
||||
for (IBase nextTargetContext : targets) {
|
||||
|
||||
// Extract [element].text of type Narrative
|
||||
INarrative nextTargetNarrative = getOrCreateNarrativeChildElement(nextTargetContext);
|
||||
|
||||
// Create the actual narrative text
|
||||
String narrative = applyTemplate(theTemplate, nextTargetContext);
|
||||
narrative = cleanWhitespace(narrative);
|
||||
|
||||
if (isNotBlank(narrative)) {
|
||||
try {
|
||||
nextTargetNarrative.setDivAsString(narrative);
|
||||
nextTargetNarrative.setStatusAsString("generated");
|
||||
retVal = true;
|
||||
} catch (Exception e) {
|
||||
throw new InternalErrorException(e);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
return retVal;
|
||||
}
|
||||
|
||||
private INarrative getOrCreateNarrativeChildElement(IBase nextTargetContext) {
|
||||
BaseRuntimeElementCompositeDefinition<?> targetElementDef = (BaseRuntimeElementCompositeDefinition<?>) getFhirContext().getElementDefinition(nextTargetContext.getClass());
|
||||
BaseRuntimeChildDefinition targetTextChild = targetElementDef.getChildByName("text");
|
||||
List<IBase> existing = targetTextChild.getAccessor().getValues(nextTargetContext);
|
||||
INarrative nextTargetNarrative;
|
||||
if (existing.isEmpty()) {
|
||||
nextTargetNarrative = (INarrative) getFhirContext().getElementDefinition("narrative").newInstance();
|
||||
targetTextChild.getMutator().addValue(nextTargetContext, nextTargetNarrative);
|
||||
} else {
|
||||
nextTargetNarrative = (INarrative) existing.get(0);
|
||||
}
|
||||
return nextTargetNarrative;
|
||||
}
|
||||
|
||||
private List<IBase> findElementsInResourceRequiringNarratives(IBaseResource theResource, String theContextPath) {
|
||||
if (myFhirContext.getVersion().getVersion().isOlderThan(FhirVersionEnum.DSTU3)) {
|
||||
return Collections.singletonList(theResource);
|
||||
}
|
||||
IFluentPath fhirPath = myFhirContext.newFluentPath();
|
||||
return fhirPath.evaluate(theResource, theContextPath, IBase.class);
|
||||
}
|
||||
|
||||
protected abstract String applyTemplate(INarrativeTemplate theTemplate, IBase theTargetContext);
|
||||
|
||||
private boolean templateDoesntApplyToResource(INarrativeTemplate theTemplate, IBaseResource theResource) {
|
||||
boolean retVal = false;
|
||||
if (theTemplate.getAppliesToProfiles() != null && !theTemplate.getAppliesToProfiles().isEmpty()) {
|
||||
Set<String> resourceProfiles = theResource
|
||||
.getMeta()
|
||||
.getProfile()
|
||||
.stream()
|
||||
.map(t -> t.getValueAsString())
|
||||
.collect(Collectors.toSet());
|
||||
retVal = true;
|
||||
for (String next : theTemplate.getAppliesToProfiles()) {
|
||||
if (resourceProfiles.contains(next)) {
|
||||
retVal = false;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
return retVal;
|
||||
}
|
||||
|
||||
protected abstract TemplateTypeEnum getStyle();
|
||||
|
||||
/**
|
||||
* Trims the superfluous whitespace out of an HTML block
|
||||
*/
|
||||
public static String cleanWhitespace(String theResult) {
|
||||
StringBuilder b = new StringBuilder();
|
||||
boolean inWhitespace = false;
|
||||
boolean betweenTags = false;
|
||||
boolean lastNonWhitespaceCharWasTagEnd = false;
|
||||
boolean inPre = false;
|
||||
for (int i = 0; i < theResult.length(); i++) {
|
||||
char nextChar = theResult.charAt(i);
|
||||
if (inPre) {
|
||||
b.append(nextChar);
|
||||
continue;
|
||||
} else if (nextChar == '>') {
|
||||
b.append(nextChar);
|
||||
betweenTags = true;
|
||||
lastNonWhitespaceCharWasTagEnd = true;
|
||||
continue;
|
||||
} else if (nextChar == '\n' || nextChar == '\r') {
|
||||
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;
|
||||
lastNonWhitespaceCharWasTagEnd = false;
|
||||
if (i + 3 < theResult.length()) {
|
||||
char char1 = Character.toLowerCase(theResult.charAt(i + 1));
|
||||
char char2 = Character.toLowerCase(theResult.charAt(i + 2));
|
||||
char char3 = Character.toLowerCase(theResult.charAt(i + 3));
|
||||
char char4 = Character.toLowerCase((i + 4 < theResult.length()) ? theResult.charAt(i + 4) : ' ');
|
||||
if (char1 == 'p' && char2 == 'r' && char3 == 'e') {
|
||||
inPre = true;
|
||||
} else if (char1 == '/' && char2 == 'p' && char3 == 'r' && char4 == 'e') {
|
||||
inPre = false;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
lastNonWhitespaceCharWasTagEnd = false;
|
||||
if (inWhitespace) {
|
||||
b.append(' ');
|
||||
inWhitespace = false;
|
||||
}
|
||||
b.append(nextChar);
|
||||
}
|
||||
} else {
|
||||
b.append(nextChar);
|
||||
}
|
||||
}
|
||||
return b.toString();
|
||||
}
|
||||
}
|
|
@ -0,0 +1,42 @@
|
|||
package ca.uhn.fhir.narrative2;
|
||||
|
||||
/*-
|
||||
* #%L
|
||||
* HAPI FHIR - Core Library
|
||||
* %%
|
||||
* Copyright (C) 2014 - 2019 University Health Network
|
||||
* %%
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
* #L%
|
||||
*/
|
||||
|
||||
import org.hl7.fhir.instance.model.api.IBase;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.Set;
|
||||
|
||||
public interface INarrativeTemplate {
|
||||
String getContextPath();
|
||||
|
||||
Set<String> getAppliesToProfiles();
|
||||
|
||||
Set<String> getAppliesToResourceTypes();
|
||||
|
||||
Set<Class<? extends IBase>> getAppliesToResourceClasses();
|
||||
|
||||
TemplateTypeEnum getTemplateType();
|
||||
|
||||
String getTemplateName();
|
||||
|
||||
String getTemplateText();
|
||||
}
|
|
@ -0,0 +1,33 @@
|
|||
package ca.uhn.fhir.narrative2;
|
||||
|
||||
/*-
|
||||
* #%L
|
||||
* HAPI FHIR - Core Library
|
||||
* %%
|
||||
* Copyright (C) 2014 - 2019 University Health Network
|
||||
* %%
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
* #L%
|
||||
*/
|
||||
|
||||
import org.hl7.fhir.instance.model.api.IBase;
|
||||
|
||||
import java.util.Optional;
|
||||
|
||||
public interface INarrativeTemplateManifest {
|
||||
Optional<INarrativeTemplate> getTemplateByResourceName(TemplateTypeEnum theStyle, String theResourceName);
|
||||
|
||||
Optional<INarrativeTemplate> getTemplateByName(TemplateTypeEnum theStyle, String theName);
|
||||
|
||||
Optional<INarrativeTemplate> getTemplateByElement(TemplateTypeEnum theStyle, IBase theElementValue);
|
||||
}
|
|
@ -0,0 +1,121 @@
|
|||
package ca.uhn.fhir.narrative2;
|
||||
|
||||
/*-
|
||||
* #%L
|
||||
* HAPI FHIR - Core Library
|
||||
* %%
|
||||
* Copyright (C) 2014 - 2019 University Health Network
|
||||
* %%
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
* #L%
|
||||
*/
|
||||
|
||||
import ca.uhn.fhir.rest.server.exceptions.InternalErrorException;
|
||||
import org.hl7.fhir.instance.model.api.IBase;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.Collections;
|
||||
import java.util.HashSet;
|
||||
import java.util.Set;
|
||||
|
||||
public class NarrativeTemplate implements INarrativeTemplate {
|
||||
|
||||
private String myTemplateFileName;
|
||||
private Set<String> myAppliesToProfiles = new HashSet<>();
|
||||
private Set<String> myAppliesToResourceTypes = new HashSet<>();
|
||||
private Set<String> myAppliesToDataTypes = new HashSet<>();
|
||||
private Set<Class<? extends IBase>> myAppliesToResourceClasses = new HashSet<>();
|
||||
private TemplateTypeEnum myTemplateType = TemplateTypeEnum.THYMELEAF;
|
||||
private String myContextPath;
|
||||
private String myTemplateName;
|
||||
|
||||
public Set<String> getAppliesToDataTypes() {
|
||||
return Collections.unmodifiableSet(myAppliesToDataTypes);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getContextPath() {
|
||||
return myContextPath;
|
||||
}
|
||||
|
||||
public void setContextPath(String theContextPath) {
|
||||
myContextPath = theContextPath;
|
||||
}
|
||||
|
||||
private String getTemplateFileName() {
|
||||
return myTemplateFileName;
|
||||
}
|
||||
|
||||
void setTemplateFileName(String theTemplateFileName) {
|
||||
myTemplateFileName = theTemplateFileName;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Set<String> getAppliesToProfiles() {
|
||||
return Collections.unmodifiableSet(myAppliesToProfiles);
|
||||
}
|
||||
|
||||
void addAppliesToProfile(String theAppliesToProfile) {
|
||||
myAppliesToProfiles.add(theAppliesToProfile);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Set<String> getAppliesToResourceTypes() {
|
||||
return Collections.unmodifiableSet(myAppliesToResourceTypes);
|
||||
}
|
||||
|
||||
void addAppliesToResourceType(String theAppliesToResourceType) {
|
||||
myAppliesToResourceTypes.add(theAppliesToResourceType);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Set<Class<? extends IBase>> getAppliesToResourceClasses() {
|
||||
return Collections.unmodifiableSet(myAppliesToResourceClasses);
|
||||
}
|
||||
|
||||
void addAppliesToResourceClass(Class<? extends IBase> theAppliesToResourceClass) {
|
||||
myAppliesToResourceClasses.add(theAppliesToResourceClass);
|
||||
}
|
||||
|
||||
@Override
|
||||
public TemplateTypeEnum getTemplateType() {
|
||||
return myTemplateType;
|
||||
}
|
||||
|
||||
void setTemplateType(TemplateTypeEnum theTemplateType) {
|
||||
myTemplateType = theTemplateType;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getTemplateName() {
|
||||
return myTemplateName;
|
||||
}
|
||||
|
||||
NarrativeTemplate setTemplateName(String theTemplateName) {
|
||||
myTemplateName = theTemplateName;
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getTemplateText() {
|
||||
try {
|
||||
return NarrativeTemplateManifest.loadResource(getTemplateFileName());
|
||||
} catch (IOException e) {
|
||||
throw new InternalErrorException(e);
|
||||
}
|
||||
}
|
||||
|
||||
void addAppliesToDatatype(String theDataType) {
|
||||
myAppliesToDataTypes.add(theDataType);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,239 @@
|
|||
package ca.uhn.fhir.narrative2;
|
||||
|
||||
/*-
|
||||
* #%L
|
||||
* HAPI FHIR - Core Library
|
||||
* %%
|
||||
* Copyright (C) 2014 - 2019 University Health Network
|
||||
* %%
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
* #L%
|
||||
*/
|
||||
|
||||
import ca.uhn.fhir.context.ConfigurationException;
|
||||
import ca.uhn.fhir.context.FhirContext;
|
||||
import ca.uhn.fhir.narrative.DefaultThymeleafNarrativeGenerator;
|
||||
import com.google.common.base.Charsets;
|
||||
import org.apache.commons.io.IOUtils;
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import org.apache.commons.lang3.Validate;
|
||||
import org.hl7.fhir.instance.model.api.IBase;
|
||||
import org.hl7.fhir.instance.model.api.IBaseResource;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import java.io.*;
|
||||
import java.util.*;
|
||||
|
||||
import static org.apache.commons.lang3.StringUtils.isNotBlank;
|
||||
|
||||
public class NarrativeTemplateManifest implements INarrativeTemplateManifest {
|
||||
private static final Logger ourLog = LoggerFactory.getLogger(NarrativeTemplateManifest.class);
|
||||
|
||||
private final Map<TemplateTypeEnum, Map<String, NarrativeTemplate>> myStyleToResourceTypeToTemplate;
|
||||
private final Map<TemplateTypeEnum, Map<String, NarrativeTemplate>> myStyleToDatatypeToTemplate;
|
||||
private final Map<TemplateTypeEnum, Map<String, NarrativeTemplate>> myStyleToNameToTemplate;
|
||||
private final FhirContext myCtx;
|
||||
private final int myTemplateCount;
|
||||
|
||||
private NarrativeTemplateManifest(FhirContext theFhirContext, Collection<NarrativeTemplate> theTemplates) {
|
||||
myCtx = theFhirContext;
|
||||
Map<TemplateTypeEnum, Map<String, NarrativeTemplate>> styleToResourceTypeToTemplate = new HashMap<>();
|
||||
Map<TemplateTypeEnum, Map<String, NarrativeTemplate>> styleToDatatypeToTemplate = new HashMap<>();
|
||||
Map<TemplateTypeEnum, Map<String, NarrativeTemplate>> styleToNameToTemplate = new HashMap<>();
|
||||
|
||||
for (NarrativeTemplate nextTemplate : theTemplates) {
|
||||
Map<String, NarrativeTemplate> resourceTypeToTemplate = styleToResourceTypeToTemplate.computeIfAbsent(nextTemplate.getTemplateType(), t -> new HashMap<>());
|
||||
Map<String, NarrativeTemplate> datatypeToTemplate = styleToDatatypeToTemplate.computeIfAbsent(nextTemplate.getTemplateType(), t -> new HashMap<>());
|
||||
Map<String, NarrativeTemplate> nameToTemplate = styleToNameToTemplate.computeIfAbsent(nextTemplate.getTemplateType(), t -> new HashMap<>());
|
||||
nameToTemplate.put(nextTemplate.getTemplateName(), nextTemplate);
|
||||
for (String nextResourceType : nextTemplate.getAppliesToResourceTypes()) {
|
||||
resourceTypeToTemplate.put(nextResourceType.toUpperCase(), nextTemplate);
|
||||
}
|
||||
for (String nextDataType : nextTemplate.getAppliesToDataTypes()) {
|
||||
datatypeToTemplate.put(nextDataType.toUpperCase(), nextTemplate);
|
||||
}
|
||||
}
|
||||
|
||||
myTemplateCount = theTemplates.size();
|
||||
myStyleToNameToTemplate = makeImmutable(styleToNameToTemplate);
|
||||
myStyleToResourceTypeToTemplate = makeImmutable(styleToResourceTypeToTemplate);
|
||||
myStyleToDatatypeToTemplate = makeImmutable(styleToDatatypeToTemplate);
|
||||
}
|
||||
|
||||
public int getNamedTemplateCount() {
|
||||
return myTemplateCount;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Optional<INarrativeTemplate> getTemplateByResourceName(TemplateTypeEnum theStyle, String theResourceName) {
|
||||
return getFromMap(theStyle, theResourceName.toUpperCase(), myStyleToResourceTypeToTemplate);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Optional<INarrativeTemplate> getTemplateByName(TemplateTypeEnum theStyle, String theName) {
|
||||
return getFromMap(theStyle, theName, myStyleToNameToTemplate);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Optional<INarrativeTemplate> getTemplateByElement(TemplateTypeEnum theStyle, IBase theElement) {
|
||||
if (theElement instanceof IBaseResource) {
|
||||
String resourceName = myCtx.getResourceDefinition((IBaseResource) theElement).getName();
|
||||
return getTemplateByResourceName(theStyle, resourceName);
|
||||
} else {
|
||||
String datatypeName = myCtx.getElementDefinition(theElement.getClass()).getName();
|
||||
return getFromMap(theStyle, datatypeName.toUpperCase(), myStyleToDatatypeToTemplate);
|
||||
}
|
||||
}
|
||||
|
||||
public static NarrativeTemplateManifest forManifestFileLocation(FhirContext theFhirContext, String... thePropertyFilePaths) throws IOException {
|
||||
return forManifestFileLocation(theFhirContext, Arrays.asList(thePropertyFilePaths));
|
||||
}
|
||||
|
||||
public static NarrativeTemplateManifest forManifestFileLocation(FhirContext theFhirContext, Collection<String> thePropertyFilePaths) throws IOException {
|
||||
ourLog.debug("Loading narrative properties file(s): {}", thePropertyFilePaths);
|
||||
|
||||
List<String> manifestFileContents = new ArrayList<>(thePropertyFilePaths.size());
|
||||
for (String next : thePropertyFilePaths) {
|
||||
String resource = loadResource(next);
|
||||
manifestFileContents.add(resource);
|
||||
}
|
||||
|
||||
return forManifestFileContents(theFhirContext, manifestFileContents);
|
||||
}
|
||||
|
||||
public static NarrativeTemplateManifest forManifestFileContents(FhirContext theFhirContext, String... theResources) throws IOException {
|
||||
return forManifestFileContents(theFhirContext, Arrays.asList(theResources));
|
||||
}
|
||||
|
||||
public static NarrativeTemplateManifest forManifestFileContents(FhirContext theFhirContext, Collection<String> theResources) throws IOException {
|
||||
List<NarrativeTemplate> templates = new ArrayList<>();
|
||||
for (String next : theResources) {
|
||||
templates.addAll(loadProperties(next));
|
||||
}
|
||||
return new NarrativeTemplateManifest(theFhirContext, templates);
|
||||
}
|
||||
|
||||
private static Collection<NarrativeTemplate> loadProperties(String theManifestText) throws IOException {
|
||||
Map<String, NarrativeTemplate> nameToTemplate = new HashMap<>();
|
||||
|
||||
Properties file = new Properties();
|
||||
|
||||
file.load(new StringReader(theManifestText));
|
||||
for (Object nextKeyObj : file.keySet()) {
|
||||
String nextKey = (String) nextKeyObj;
|
||||
Validate.isTrue(StringUtils.countMatches(nextKey, ".") == 1, "Invalid narrative property file key: %s", nextKey);
|
||||
String name = nextKey.substring(0, nextKey.indexOf('.'));
|
||||
Validate.notBlank(name, "Invalid narrative property file key: %s", nextKey);
|
||||
|
||||
NarrativeTemplate nextTemplate = nameToTemplate.computeIfAbsent(name, t -> new NarrativeTemplate().setTemplateName(name));
|
||||
|
||||
Validate.isTrue(!nextKey.endsWith(".class"), "Narrative manifest does not support specifying templates by class name - Use \"[name].resourceType=[resourceType]\" instead");
|
||||
|
||||
if (nextKey.endsWith(".profile")) {
|
||||
String profile = file.getProperty(nextKey);
|
||||
if (isNotBlank(profile)) {
|
||||
nextTemplate.addAppliesToProfile(profile);
|
||||
}
|
||||
} else if (nextKey.endsWith(".resourceType")) {
|
||||
String resourceType = file.getProperty(nextKey);
|
||||
Arrays
|
||||
.stream(resourceType.split(","))
|
||||
.map(t -> t.trim())
|
||||
.filter(t -> isNotBlank(t))
|
||||
.forEach(t -> nextTemplate.addAppliesToResourceType(t));
|
||||
} else if (nextKey.endsWith(".dataType")) {
|
||||
String dataType = file.getProperty(nextKey);
|
||||
Arrays
|
||||
.stream(dataType.split(","))
|
||||
.map(t -> t.trim())
|
||||
.filter(t -> isNotBlank(t))
|
||||
.forEach(t -> nextTemplate.addAppliesToDatatype(t));
|
||||
} else if (nextKey.endsWith(".class")) {
|
||||
String className = file.getProperty(nextKey);
|
||||
Class<? extends IBase> clazz;
|
||||
try {
|
||||
clazz = (Class<? extends IBase>) Class.forName(className);
|
||||
} catch (ClassNotFoundException e) {
|
||||
ourLog.debug("Unknown datatype class '{}' identified in manifest", name);
|
||||
clazz = null;
|
||||
}
|
||||
if (clazz != null) {
|
||||
nextTemplate.addAppliesToResourceClass(clazz);
|
||||
}
|
||||
} else if (nextKey.endsWith(".style")) {
|
||||
String templateTypeName = file.getProperty(nextKey).toUpperCase();
|
||||
TemplateTypeEnum templateType = TemplateTypeEnum.valueOf(templateTypeName);
|
||||
nextTemplate.setTemplateType(templateType);
|
||||
} else if (nextKey.endsWith(".contextPath")) {
|
||||
String contextPath = file.getProperty(nextKey);
|
||||
nextTemplate.setContextPath(contextPath);
|
||||
} else if (nextKey.endsWith(".narrative")) {
|
||||
String narrativePropName = name + ".narrative";
|
||||
String narrativeName = file.getProperty(narrativePropName);
|
||||
if (StringUtils.isNotBlank(narrativeName)) {
|
||||
nextTemplate.setTemplateFileName(narrativeName);
|
||||
}
|
||||
} else if (nextKey.endsWith(".title")) {
|
||||
ourLog.debug("Ignoring title property as narrative generator no longer generates titles: {}", nextKey);
|
||||
} else {
|
||||
throw new ConfigurationException("Invalid property name: " + nextKey);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
return nameToTemplate.values();
|
||||
}
|
||||
|
||||
static String loadResource(String name) throws IOException {
|
||||
if (name.startsWith("classpath:")) {
|
||||
String cpName = name.substring("classpath:".length());
|
||||
try (InputStream resource = DefaultThymeleafNarrativeGenerator.class.getResourceAsStream(cpName)) {
|
||||
if (resource == null) {
|
||||
try (InputStream resource2 = DefaultThymeleafNarrativeGenerator.class.getResourceAsStream("/" + cpName)) {
|
||||
if (resource2 == null) {
|
||||
throw new IOException("Can not find '" + cpName + "' on classpath");
|
||||
}
|
||||
return IOUtils.toString(resource2, Charsets.UTF_8);
|
||||
}
|
||||
}
|
||||
return IOUtils.toString(resource, Charsets.UTF_8);
|
||||
}
|
||||
} else if (name.startsWith("file:")) {
|
||||
File file = new File(name.substring("file:".length()));
|
||||
if (file.exists() == false) {
|
||||
throw new IOException("File not found: " + file.getAbsolutePath());
|
||||
}
|
||||
try (FileInputStream inputStream = new FileInputStream(file)) {
|
||||
return IOUtils.toString(inputStream, Charsets.UTF_8);
|
||||
}
|
||||
} else {
|
||||
throw new IOException("Invalid resource name: '" + name + "' (must start with classpath: or file: )");
|
||||
}
|
||||
}
|
||||
|
||||
private static <T> Optional<INarrativeTemplate> getFromMap(TemplateTypeEnum theStyle, T theResourceName, Map<TemplateTypeEnum, Map<T, NarrativeTemplate>> theMap) {
|
||||
NarrativeTemplate retVal = null;
|
||||
Map<T, NarrativeTemplate> resourceTypeToTemplate = theMap.get(theStyle);
|
||||
if (resourceTypeToTemplate != null) {
|
||||
retVal = resourceTypeToTemplate.get(theResourceName);
|
||||
}
|
||||
return Optional.ofNullable(retVal);
|
||||
}
|
||||
|
||||
private static <T> Map<TemplateTypeEnum, Map<T, NarrativeTemplate>> makeImmutable(Map<TemplateTypeEnum, Map<T, NarrativeTemplate>> theStyleToResourceTypeToTemplate) {
|
||||
theStyleToResourceTypeToTemplate.replaceAll((key, value) -> Collections.unmodifiableMap(value));
|
||||
return Collections.unmodifiableMap(theStyleToResourceTypeToTemplate);
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,31 @@
|
|||
package ca.uhn.fhir.narrative2;
|
||||
|
||||
/*-
|
||||
* #%L
|
||||
* HAPI FHIR - Core Library
|
||||
* %%
|
||||
* Copyright (C) 2014 - 2019 University Health Network
|
||||
* %%
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
* #L%
|
||||
*/
|
||||
|
||||
import ca.uhn.fhir.narrative.INarrativeGenerator;
|
||||
import org.hl7.fhir.instance.model.api.IBaseResource;
|
||||
|
||||
public class NullNarrativeGenerator implements INarrativeGenerator {
|
||||
@Override
|
||||
public boolean populateResourceNarrative(IBaseResource theResource) {
|
||||
return false;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,28 @@
|
|||
package ca.uhn.fhir.narrative2;
|
||||
|
||||
/*-
|
||||
* #%L
|
||||
* HAPI FHIR - Core Library
|
||||
* %%
|
||||
* Copyright (C) 2014 - 2019 University Health Network
|
||||
* %%
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
* #L%
|
||||
*/
|
||||
|
||||
public enum TemplateTypeEnum {
|
||||
|
||||
THYMELEAF,
|
||||
LIQUID
|
||||
|
||||
}
|
|
@ -0,0 +1,195 @@
|
|||
package ca.uhn.fhir.narrative2;
|
||||
|
||||
/*-
|
||||
* #%L
|
||||
* HAPI FHIR - Core Library
|
||||
* %%
|
||||
* Copyright (C) 2014 - 2019 University Health Network
|
||||
* %%
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
* #L%
|
||||
*/
|
||||
|
||||
import ca.uhn.fhir.context.FhirContext;
|
||||
import ca.uhn.fhir.rest.server.exceptions.InternalErrorException;
|
||||
import org.hl7.fhir.instance.model.api.IBase;
|
||||
import org.thymeleaf.IEngineConfiguration;
|
||||
import org.thymeleaf.TemplateEngine;
|
||||
import org.thymeleaf.cache.AlwaysValidCacheEntryValidity;
|
||||
import org.thymeleaf.cache.ICacheEntryValidity;
|
||||
import org.thymeleaf.context.Context;
|
||||
import org.thymeleaf.context.ITemplateContext;
|
||||
import org.thymeleaf.engine.AttributeName;
|
||||
import org.thymeleaf.messageresolver.IMessageResolver;
|
||||
import org.thymeleaf.model.IProcessableElementTag;
|
||||
import org.thymeleaf.processor.IProcessor;
|
||||
import org.thymeleaf.processor.element.AbstractAttributeTagProcessor;
|
||||
import org.thymeleaf.processor.element.AbstractElementTagProcessor;
|
||||
import org.thymeleaf.processor.element.IElementTagStructureHandler;
|
||||
import org.thymeleaf.standard.StandardDialect;
|
||||
import org.thymeleaf.standard.expression.IStandardExpression;
|
||||
import org.thymeleaf.standard.expression.IStandardExpressionParser;
|
||||
import org.thymeleaf.standard.expression.StandardExpressions;
|
||||
import org.thymeleaf.templatemode.TemplateMode;
|
||||
import org.thymeleaf.templateresolver.DefaultTemplateResolver;
|
||||
import org.thymeleaf.templateresource.ITemplateResource;
|
||||
import org.thymeleaf.templateresource.StringTemplateResource;
|
||||
|
||||
import java.util.Map;
|
||||
import java.util.Optional;
|
||||
import java.util.Set;
|
||||
|
||||
import static org.apache.commons.lang3.StringUtils.isNotBlank;
|
||||
|
||||
public class ThymeleafNarrativeGenerator extends BaseNarrativeGenerator {
|
||||
|
||||
private IMessageResolver myMessageResolver;
|
||||
|
||||
/**
|
||||
* Constructor
|
||||
*/
|
||||
public ThymeleafNarrativeGenerator(FhirContext theFhirContext) {
|
||||
super(theFhirContext);
|
||||
}
|
||||
|
||||
private TemplateEngine getTemplateEngine() {
|
||||
TemplateEngine engine = new TemplateEngine();
|
||||
ProfileResourceResolver resolver = new ProfileResourceResolver();
|
||||
engine.setTemplateResolver(resolver);
|
||||
if (myMessageResolver != null) {
|
||||
engine.setMessageResolver(myMessageResolver);
|
||||
}
|
||||
StandardDialect dialect = new StandardDialect() {
|
||||
@Override
|
||||
public Set<IProcessor> getProcessors(String theDialectPrefix) {
|
||||
Set<IProcessor> retVal = super.getProcessors(theDialectPrefix);
|
||||
retVal.add(new NarrativeTagProcessor(theDialectPrefix));
|
||||
retVal.add(new NarrativeAttributeProcessor(theDialectPrefix));
|
||||
return retVal;
|
||||
}
|
||||
|
||||
};
|
||||
|
||||
engine.setDialect(dialect);
|
||||
return engine;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected String applyTemplate(INarrativeTemplate theTemplate, IBase theTargetContext) {
|
||||
|
||||
Context context = new Context();
|
||||
context.setVariable("resource", theTargetContext);
|
||||
context.setVariable("context", theTargetContext);
|
||||
context.setVariable("fhirVersion", getFhirContext().getVersion().getVersion().name());
|
||||
|
||||
String result = getTemplateEngine().process(theTemplate.getTemplateName(), context);
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
protected TemplateTypeEnum getStyle() {
|
||||
return TemplateTypeEnum.THYMELEAF;
|
||||
}
|
||||
|
||||
private String applyTemplateWithinTag(ITemplateContext theTemplateContext, String theName, String theElement) {
|
||||
IEngineConfiguration configuration = theTemplateContext.getConfiguration();
|
||||
IStandardExpressionParser expressionParser = StandardExpressions.getExpressionParser(configuration);
|
||||
final IStandardExpression expression = expressionParser.parseExpression(theTemplateContext, theElement);
|
||||
Object elementValueObj = expression.execute(theTemplateContext);
|
||||
final IBase elementValue = (IBase) elementValueObj;
|
||||
if (elementValue == null) {
|
||||
return "";
|
||||
}
|
||||
|
||||
Optional<INarrativeTemplate> templateOpt;
|
||||
if (isNotBlank(theName)) {
|
||||
templateOpt = getManifest().getTemplateByName(getStyle(), theName);
|
||||
if (!templateOpt.isPresent()) {
|
||||
throw new InternalErrorException("Unknown template name: " + theName);
|
||||
}
|
||||
} else {
|
||||
templateOpt = getManifest().getTemplateByElement(getStyle(), elementValue);
|
||||
if (!templateOpt.isPresent()) {
|
||||
throw new InternalErrorException("No template for type: " + elementValue.getClass());
|
||||
}
|
||||
}
|
||||
|
||||
return applyTemplate(templateOpt.get(), elementValue);
|
||||
}
|
||||
|
||||
public void setMessageResolver(IMessageResolver theMessageResolver) {
|
||||
myMessageResolver = theMessageResolver;
|
||||
}
|
||||
|
||||
|
||||
private class ProfileResourceResolver extends DefaultTemplateResolver {
|
||||
@Override
|
||||
protected boolean computeResolvable(IEngineConfiguration theConfiguration, String theOwnerTemplate, String theTemplate, Map<String, Object> theTemplateResolutionAttributes) {
|
||||
return getManifest().getTemplateByName(getStyle(), theTemplate).isPresent();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected TemplateMode computeTemplateMode(IEngineConfiguration theConfiguration, String theOwnerTemplate, String theTemplate, Map<String, Object> theTemplateResolutionAttributes) {
|
||||
return TemplateMode.XML;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected ITemplateResource computeTemplateResource(IEngineConfiguration theConfiguration, String theOwnerTemplate, String theTemplate, Map<String, Object> theTemplateResolutionAttributes) {
|
||||
return getManifest()
|
||||
.getTemplateByName(getStyle(), theTemplate)
|
||||
.map(t -> new StringTemplateResource(t.getTemplateText()))
|
||||
.orElseThrow(() -> new IllegalArgumentException("Unknown template: " + theTemplate));
|
||||
}
|
||||
|
||||
@Override
|
||||
protected ICacheEntryValidity computeValidity(IEngineConfiguration theConfiguration, String theOwnerTemplate, String theTemplate, Map<String, Object> theTemplateResolutionAttributes) {
|
||||
return AlwaysValidCacheEntryValidity.INSTANCE;
|
||||
}
|
||||
}
|
||||
|
||||
private class NarrativeTagProcessor extends AbstractElementTagProcessor {
|
||||
|
||||
public NarrativeTagProcessor(String dialectPrefix) {
|
||||
super(TemplateMode.XML, dialectPrefix, "narrative", true, null, true, 0);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void doProcess(ITemplateContext theTemplateContext, IProcessableElementTag theTag, IElementTagStructureHandler theStructureHandler) {
|
||||
String name = theTag.getAttributeValue("th:name");
|
||||
String element = theTag.getAttributeValue("th:element");
|
||||
|
||||
String appliedTemplate = applyTemplateWithinTag(theTemplateContext, name, element);
|
||||
theStructureHandler.replaceWith(appliedTemplate, false);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* This is a thymeleaf extension that allows people to do things like
|
||||
* <th:block th:narrative="${result}"/>
|
||||
*/
|
||||
private class NarrativeAttributeProcessor extends AbstractAttributeTagProcessor {
|
||||
|
||||
protected NarrativeAttributeProcessor(String theDialectPrefix) {
|
||||
super(TemplateMode.XML, theDialectPrefix, null, false, "narrative", true, 0, true);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void doProcess(ITemplateContext theContext, IProcessableElementTag theTag, AttributeName theAttributeName, String theAttributeValue, IElementTagStructureHandler theStructureHandler) {
|
||||
String text = applyTemplateWithinTag(theContext, null, theAttributeValue);
|
||||
theStructureHandler.setBody(text, false);
|
||||
}
|
||||
|
||||
}
|
||||
}
|
|
@ -360,7 +360,7 @@ public class JsonParser extends BaseParser implements IJsonLikeParser {
|
|||
narr = null;
|
||||
}
|
||||
if (narr != null && narr.isEmpty()) {
|
||||
gen.generateNarrative(myContext, theResource, narr);
|
||||
gen.populateResourceNarrative(theResource);
|
||||
if (!narr.isEmpty()) {
|
||||
RuntimeChildNarrativeDefinition child = (RuntimeChildNarrativeDefinition) nextChild;
|
||||
String childName = nextChild.getChildNameByDatatype(child.getDatatype());
|
||||
|
|
|
@ -371,7 +371,7 @@ public class XmlParser extends BaseParser /* implements IParser */ {
|
|||
}
|
||||
// FIXME potential null access on narr see line 623
|
||||
if (gen != null && narr.isEmpty()) {
|
||||
gen.generateNarrative(myContext, theResource, narr);
|
||||
gen.populateResourceNarrative(theResource);
|
||||
}
|
||||
if (narr != null && narr.isEmpty() == false) {
|
||||
RuntimeChildNarrativeDefinition child = (RuntimeChildNarrativeDefinition) nextChild;
|
||||
|
|
|
@ -25,7 +25,7 @@ public interface IBaseReference extends ICompositeType {
|
|||
|
||||
IBaseResource getResource();
|
||||
|
||||
void setResource(IBaseResource theResource);
|
||||
IBaseReference setResource(IBaseResource theResource);
|
||||
|
||||
IIdType getReferenceElement();
|
||||
|
||||
|
|
|
@ -3,6 +3,6 @@
|
|||
# Resources
|
||||
################################################
|
||||
|
||||
conformance.class=ca.uhn.fhir.model.dstu.resource.Conformance
|
||||
conformance.resourceType =Conformance
|
||||
conformance.narrative =classpath:ca/uhn/fhir/narrative/ConformanceHapiServer.html
|
||||
|
||||
|
|
|
@ -3,66 +3,62 @@
|
|||
# Primitive Datatypes
|
||||
################################################
|
||||
|
||||
code.class=ca.uhn.fhir.model.primitive.CodeDt
|
||||
code.dataType=code
|
||||
code.narrative=classpath:ca/uhn/fhir/narrative/datatype/CodeDt.html
|
||||
|
||||
datetime.class=ca.uhn.fhir.model.primitive.DateTimeDt
|
||||
datetime.dataType=dateTime, instant
|
||||
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/datatype/DateTimeDt.html
|
||||
|
||||
string.class=ca.uhn.fhir.model.primitive.StringDt
|
||||
string.dataType=string
|
||||
string.narrative=classpath:ca/uhn/fhir/narrative/datatype/StringDt.html
|
||||
|
||||
################################################
|
||||
# Composite Datatypes
|
||||
################################################
|
||||
|
||||
address.class=ca.uhn.fhir.model.dstu.composite.AddressDt
|
||||
address.dataType=Address
|
||||
address.narrative=classpath:ca/uhn/fhir/narrative/datatype/AddressDt.html
|
||||
|
||||
codeableconcept.class=ca.uhn.fhir.model.dstu.composite.CodeableConceptDt
|
||||
codeableconcept.dataType=CodeableConcept
|
||||
codeableconcept.narrative=classpath:ca/uhn/fhir/narrative/datatype/CodeableConceptDt.html
|
||||
boundcodeableconcept.narrative=classpath:ca/uhn/fhir/narrative/datatype/CodeableConceptDt.html
|
||||
|
||||
humanname.class=ca.uhn.fhir.model.dstu.composite.HumanNameDt
|
||||
humanname.dataType=HumanName
|
||||
humanname.narrative=classpath:ca/uhn/fhir/narrative/datatype/HumanNameDt.html
|
||||
|
||||
identifier.class=ca.uhn.fhir.model.dstu.composite.IdentifierDt
|
||||
identifier.dataType=Identifier
|
||||
identifier.narrative=classpath:ca/uhn/fhir/narrative/datatype/IdentifierDt.html
|
||||
|
||||
period.class=ca.uhn.fhir.model.dstu.composite.PeriodDt
|
||||
period.dataType=Period
|
||||
period.narrative=classpath:ca/uhn/fhir/narrative/datatype/PeriodDt.html
|
||||
|
||||
quantity.class=ca.uhn.fhir.model.dstu.composite.QuantityDt
|
||||
quantity.dataType=Quantity
|
||||
quantity.narrative=classpath:ca/uhn/fhir/narrative/datatype/QuantityDt.html
|
||||
|
||||
simplequantity.class=ca.uhn.fhir.model.dstu.composite.SimpleQuantityDt
|
||||
simplequantity.dataType=SimpleQuantity
|
||||
simplequantity.narrative=classpath:ca/uhn/fhir/narrative/datatype/SimpleQuantityDt.html
|
||||
|
||||
################################################
|
||||
# Resources
|
||||
################################################
|
||||
|
||||
diagnosticreport.class=ca.uhn.fhir.model.dstu.resource.DiagnosticReport
|
||||
diagnosticreport.resourceType=DiagnosticReport
|
||||
diagnosticreport.narrative=classpath:ca/uhn/fhir/narrative/DiagnosticReport.html
|
||||
|
||||
encounter.class=ca.uhn.fhir.model.dstu.resource.Encounter
|
||||
#encounter.class=ca.uhn.fhir.model.dstu.resource.Encounter
|
||||
|
||||
operationoutcome.class=ca.uhn.fhir.model.dstu.resource.OperationOutcome
|
||||
operationoutcome.resourceType=OperationOutcome
|
||||
operationoutcome.narrative=classpath:ca/uhn/fhir/narrative/OperationOutcome.html
|
||||
|
||||
organization.class=ca.uhn.fhir.model.dstu.resource.Organization
|
||||
#organization.class=ca.uhn.fhir.model.dstu.resource.Organization
|
||||
|
||||
patient.class=ca.uhn.fhir.model.dstu.resource.Patient
|
||||
patient.resourceType=Patient
|
||||
patient.narrative=classpath:ca/uhn/fhir/narrative/Patient.html
|
||||
|
||||
medicationprescription.class=ca.uhn.fhir.model.dstu.resource.MedicationPrescription
|
||||
medicationprescription.resourceType=MedicationPrescription
|
||||
medicationprescription.narrative=classpath:ca/uhn/fhir/narrative/MedicationPrescription.html
|
||||
|
||||
medication.class=ca.uhn.fhir.model.dstu.resource.Medication
|
||||
medication.resourceType=Medication
|
||||
medication.narrative=classpath:ca/uhn/fhir/narrative/Medication.html
|
||||
medication.title=classpath:ca/uhn/fhir/narrative/Medication.html
|
||||
|
||||
|
|
|
@ -44,7 +44,7 @@ import org.apache.commons.cli.Option;
|
|||
import org.apache.commons.cli.Options;
|
||||
import org.apache.commons.cli.ParseException;
|
||||
import org.apache.commons.io.FileUtils;
|
||||
import org.hl7.fhir.dstu3.hapi.validation.DefaultProfileValidationSupport;
|
||||
import org.hl7.fhir.dstu3.hapi.ctx.DefaultProfileValidationSupport;
|
||||
import org.hl7.fhir.dstu3.hapi.validation.FhirInstanceValidator;
|
||||
import org.hl7.fhir.dstu3.model.Bundle.BundleEntryComponent;
|
||||
import org.hl7.fhir.dstu3.model.Bundle.BundleType;
|
||||
|
|
|
@ -35,7 +35,7 @@ import org.apache.commons.io.IOUtils;
|
|||
import org.apache.commons.lang3.text.WordUtils;
|
||||
import org.fusesource.jansi.Ansi.Color;
|
||||
import org.hl7.fhir.dstu3.hapi.ctx.IValidationSupport;
|
||||
import org.hl7.fhir.dstu3.hapi.validation.DefaultProfileValidationSupport;
|
||||
import org.hl7.fhir.dstu3.hapi.ctx.DefaultProfileValidationSupport;
|
||||
import org.hl7.fhir.dstu3.hapi.validation.FhirInstanceValidator;
|
||||
import org.hl7.fhir.dstu3.hapi.validation.ValidationSupportChain;
|
||||
import org.hl7.fhir.dstu3.model.StructureDefinition;
|
||||
|
|
|
@ -125,7 +125,7 @@ public class JpaServerDemo extends RestfulServer {
|
|||
* This server tries to dynamically generate narratives
|
||||
*/
|
||||
FhirContext ctx = getFhirContext();
|
||||
ctx.setNarrativeGenerator(new DefaultThymeleafNarrativeGenerator());
|
||||
ctx.setNarrativeGenerator(new DefaultThymeleafNarrativeGenerator(getFhirContext()));
|
||||
|
||||
/*
|
||||
* Default to XML and pretty printing
|
||||
|
|
|
@ -91,6 +91,8 @@ public abstract class BaseHapiFhirResourceDao<T extends IBaseResource> extends B
|
|||
private String myResourceName;
|
||||
private Class<T> myResourceType;
|
||||
private String mySecondaryPrimaryKeyParamName;
|
||||
@Autowired
|
||||
private IResourceReindexingSvc myResourceReindexingSvc;
|
||||
|
||||
@Override
|
||||
public void addTag(IIdType theId, TagTypeEnum theTagType, String theScheme, String theTerm, String theLabel) {
|
||||
|
@ -694,9 +696,6 @@ public abstract class BaseHapiFhirResourceDao<T extends IBaseResource> extends B
|
|||
mySearchParamRegistry.requestRefresh();
|
||||
}
|
||||
|
||||
@Autowired
|
||||
private IResourceReindexingSvc myResourceReindexingSvc;
|
||||
|
||||
@Override
|
||||
public <MT extends IBaseMetaType> MT metaAddOperation(IIdType theResourceId, MT theMetaAdd, RequestDetails theRequestDetails) {
|
||||
// Notify interceptors
|
||||
|
@ -941,6 +940,9 @@ public abstract class BaseHapiFhirResourceDao<T extends IBaseResource> extends B
|
|||
}
|
||||
}
|
||||
|
||||
// Interceptor broadcast: RESOURCE_MAY_BE_RETURNED
|
||||
HookParams params = new HookParams().add(IBaseResource.class, retVal);
|
||||
myInterceptorBroadcaster.callHooks(Pointcut.RESOURCE_MAY_BE_RETURNED, params);
|
||||
|
||||
ourLog.debug("Processed read on {} in {}ms", theId.getValue(), w.getMillisAndRestart());
|
||||
return retVal;
|
||||
|
@ -1189,6 +1191,11 @@ public abstract class BaseHapiFhirResourceDao<T extends IBaseResource> extends B
|
|||
outcome.setId(id);
|
||||
outcome.setResource(theResource);
|
||||
outcome.setEntity(theEntity);
|
||||
|
||||
// Interceptor broadcast: RESOURCE_MAY_BE_RETURNED
|
||||
HookParams params = new HookParams().add(IBaseResource.class, theResource);
|
||||
myInterceptorBroadcaster.callHooks(Pointcut.RESOURCE_MAY_BE_RETURNED, params);
|
||||
|
||||
return outcome;
|
||||
}
|
||||
|
||||
|
|
|
@ -29,6 +29,9 @@ import ca.uhn.fhir.jpa.dao.index.IdHelperService;
|
|||
import ca.uhn.fhir.jpa.dao.r4.MatchResourceUrlService;
|
||||
import ca.uhn.fhir.jpa.entity.ResourceSearchView;
|
||||
import ca.uhn.fhir.jpa.model.entity.*;
|
||||
import ca.uhn.fhir.jpa.model.interceptor.api.HookParams;
|
||||
import ca.uhn.fhir.jpa.model.interceptor.api.IInterceptorBroadcaster;
|
||||
import ca.uhn.fhir.jpa.model.interceptor.api.Pointcut;
|
||||
import ca.uhn.fhir.jpa.model.util.StringNormalizer;
|
||||
import ca.uhn.fhir.jpa.searchparam.JpaRuntimeSearchParam;
|
||||
import ca.uhn.fhir.jpa.searchparam.MatchUrlService;
|
||||
|
@ -137,6 +140,8 @@ public class SearchBuilder implements ISearchBuilder {
|
|||
private MatchUrlService myMatchUrlService;
|
||||
@Autowired
|
||||
private IResourceIndexedCompositeStringUniqueDao myResourceIndexedCompositeStringUniqueDao;
|
||||
@Autowired
|
||||
private IInterceptorBroadcaster myInterceptorBroadcaster;
|
||||
private List<Long> myAlsoIncludePids;
|
||||
private CriteriaBuilder myBuilder;
|
||||
private BaseHapiFhirDao<?> myCallingDao;
|
||||
|
@ -1922,6 +1927,10 @@ public class SearchBuilder implements ISearchBuilder {
|
|||
}
|
||||
}
|
||||
|
||||
// Interceptor broadcast: RESOURCE_MAY_BE_RETURNED
|
||||
HookParams params = new HookParams().add(IBaseResource.class, resource);
|
||||
myInterceptorBroadcaster.callHooks(Pointcut.RESOURCE_MAY_BE_RETURNED, params);
|
||||
|
||||
theResourceListToPopulate.set(index, resource);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -23,7 +23,7 @@ package ca.uhn.fhir.jpa.validation;
|
|||
import javax.annotation.PostConstruct;
|
||||
import javax.annotation.PreDestroy;
|
||||
|
||||
import org.hl7.fhir.dstu3.hapi.validation.DefaultProfileValidationSupport;
|
||||
import org.hl7.fhir.dstu3.hapi.ctx.DefaultProfileValidationSupport;
|
||||
import org.hl7.fhir.dstu3.hapi.validation.ValidationSupportChain;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.beans.factory.annotation.Qualifier;
|
||||
|
|
|
@ -2350,6 +2350,31 @@ public class FhirResourceDaoR4SearchNoFtTest extends BaseJpaR4Test {
|
|||
}
|
||||
|
||||
|
||||
@Test
|
||||
public void testSearchWithDateRange() {
|
||||
SearchParameterMap sp = new SearchParameterMap();
|
||||
sp.setLoadSynchronous(true);
|
||||
sp.add(MedicationRequest.SP_INTENT, new TokenParam("FOO", "BAR"));
|
||||
sp.setLastUpdated(new DateRangeParam()
|
||||
.setUpperBound(new DateParam("le2019-02-22T17:50:00"))
|
||||
.setLowerBound(new DateParam("ge2019-02-22T13:50:00")));
|
||||
IBundleProvider retrieved = myMedicationRequestDao.search(sp);
|
||||
|
||||
List<String> queries = CaptureQueriesListener
|
||||
.getLastNQueries()
|
||||
.stream()
|
||||
.map(t -> t.getSql(true, true))
|
||||
.collect(Collectors.toList());
|
||||
|
||||
ourLog.info("Queries:\n {}", queries.stream().findFirst());
|
||||
|
||||
String searchQuery = queries.get(0);
|
||||
assertEquals(searchQuery, 1, StringUtils.countMatches(searchQuery.toUpperCase(), "HFJ_SPIDX_TOKEN"));
|
||||
assertEquals(searchQuery, 1, StringUtils.countMatches(searchQuery.toUpperCase(), "LEFT OUTER JOIN"));
|
||||
assertEquals(searchQuery, 2, StringUtils.countMatches(searchQuery.toUpperCase(), "AND RESOURCETA0_.RES_UPDATED"));
|
||||
}
|
||||
|
||||
|
||||
@Test
|
||||
public void testSearchTokenParam() {
|
||||
Patient patient = new Patient();
|
||||
|
|
|
@ -74,7 +74,7 @@ public abstract class BaseResourceProviderDstu2Test extends BaseJpaDstu2Test {
|
|||
|
||||
ourRestServer.setResourceProviders((List)myResourceProviders);
|
||||
|
||||
ourRestServer.getFhirContext().setNarrativeGenerator(new DefaultThymeleafNarrativeGenerator());
|
||||
ourRestServer.getFhirContext().setNarrativeGenerator(new DefaultThymeleafNarrativeGenerator(myFhirCtx));
|
||||
|
||||
ourRestServer.setPlainProviders(mySystemProvider);
|
||||
|
||||
|
|
|
@ -92,7 +92,7 @@ public abstract class BaseResourceProviderDstu3Test extends BaseJpaDstu3Test {
|
|||
|
||||
ourRestServer.setResourceProviders((List) myResourceProviders);
|
||||
|
||||
ourRestServer.getFhirContext().setNarrativeGenerator(new DefaultThymeleafNarrativeGenerator());
|
||||
ourRestServer.getFhirContext().setNarrativeGenerator(new DefaultThymeleafNarrativeGenerator(myFhirCtx));
|
||||
|
||||
myTerminologyUploaderProvider = myAppCtx.getBean(TerminologyUploaderProviderDstu3.class);
|
||||
ourRestServer.setPlainProviders(mySystemProvider, myTerminologyUploaderProvider);
|
||||
|
|
|
@ -99,7 +99,7 @@ public abstract class BaseResourceProviderR4Test extends BaseJpaR4Test {
|
|||
|
||||
ourRestServer.setResourceProviders((List) myResourceProviders);
|
||||
|
||||
ourRestServer.getFhirContext().setNarrativeGenerator(new DefaultThymeleafNarrativeGenerator());
|
||||
ourRestServer.getFhirContext().setNarrativeGenerator(new DefaultThymeleafNarrativeGenerator(myFhirCtx));
|
||||
|
||||
myTerminologyUploaderProvider = myAppCtx.getBean(TerminologyUploaderProviderR4.class);
|
||||
ourGraphQLProvider = myAppCtx.getBean("myGraphQLProvider");
|
||||
|
|
|
@ -1,33 +1,39 @@
|
|||
package ca.uhn.fhir.jpa.provider.r4;
|
||||
|
||||
import ca.uhn.fhir.jpa.dao.DaoConfig;
|
||||
import ca.uhn.fhir.jpa.model.interceptor.api.HookParams;
|
||||
import ca.uhn.fhir.jpa.model.interceptor.api.IAnonymousLambdaHook;
|
||||
import ca.uhn.fhir.jpa.model.interceptor.api.Pointcut;
|
||||
import ca.uhn.fhir.parser.StrictErrorHandler;
|
||||
import ca.uhn.fhir.rest.api.EncodingEnum;
|
||||
import ca.uhn.fhir.util.TestUtil;
|
||||
import com.google.common.base.Charsets;
|
||||
import org.apache.commons.io.IOUtils;
|
||||
import org.apache.http.client.ClientProtocolException;
|
||||
import org.apache.http.client.methods.CloseableHttpResponse;
|
||||
import org.apache.http.client.methods.HttpGet;
|
||||
|
||||
import org.hl7.fhir.instance.model.api.IBaseResource;
|
||||
import org.hl7.fhir.r4.model.*;
|
||||
import org.hl7.fhir.r4.model.codesystems.EncounterStatus;
|
||||
import org.hl7.fhir.r4.model.codesystems.ObservationStatus;
|
||||
import org.junit.After;
|
||||
import org.junit.AfterClass;
|
||||
import org.junit.Before;
|
||||
import org.junit.Test;
|
||||
import org.mockito.ArgumentCaptor;
|
||||
import org.mockito.Captor;
|
||||
import org.mockito.Mockito;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import static org.hamcrest.Matchers.hasItem;
|
||||
import static org.hamcrest.Matchers.hasItems;
|
||||
import static org.junit.Assert.assertNull;
|
||||
import static org.junit.Assert.assertThat;
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.times;
|
||||
|
||||
public class CompositionDocumentR4Test extends BaseResourceProviderR4Test {
|
||||
|
||||
|
@ -38,6 +44,8 @@ public class CompositionDocumentR4Test extends BaseResourceProviderR4Test {
|
|||
private String encId;
|
||||
private String listId;
|
||||
private String compId;
|
||||
@Captor
|
||||
private ArgumentCaptor<HookParams> myHookParamsCaptor;
|
||||
|
||||
@Before
|
||||
public void beforeDisableResultReuse() {
|
||||
|
@ -49,6 +57,7 @@ public class CompositionDocumentR4Test extends BaseResourceProviderR4Test {
|
|||
public void after() throws Exception {
|
||||
super.after();
|
||||
|
||||
myInterceptorRegistry.clearAnonymousHookForUnitTest();
|
||||
myDaoConfig.setReuseCachedSearchResultsForMillis(new DaoConfig().getReuseCachedSearchResultsForMillis());
|
||||
}
|
||||
|
||||
|
@ -77,7 +86,7 @@ public class CompositionDocumentR4Test extends BaseResourceProviderR4Test {
|
|||
ListResource listResource = new ListResource();
|
||||
|
||||
ArrayList<Observation> myObs = new ArrayList<>();
|
||||
myObsIds = new ArrayList<String>();
|
||||
myObsIds = new ArrayList<>();
|
||||
for (int i = 0; i < 5; i++) {
|
||||
Observation obs = new Observation();
|
||||
obs.getSubject().setReference(patId);
|
||||
|
@ -125,7 +134,31 @@ public class CompositionDocumentR4Test extends BaseResourceProviderR4Test {
|
|||
assertThat(actual, hasItems(myObsIds.toArray(new String[0])));
|
||||
}
|
||||
|
||||
private Bundle fetchBundle(String theUrl, EncodingEnum theEncoding) throws IOException, ClientProtocolException {
|
||||
@Test
|
||||
public void testInterceptorHookIsCalledForAllContents_RESOURCE_MAY_BE_RETURNED() throws IOException {
|
||||
|
||||
IAnonymousLambdaHook pointcut = mock(IAnonymousLambdaHook.class);
|
||||
myInterceptorRegistry.registerAnonymousHookForUnitTest(Pointcut.RESOURCE_MAY_BE_RETURNED, pointcut);
|
||||
|
||||
String theUrl = ourServerBase + "/" + compId + "/$document?_format=json";
|
||||
fetchBundle(theUrl, EncodingEnum.JSON);
|
||||
|
||||
Mockito.verify(pointcut, times(10)).invoke(myHookParamsCaptor.capture());
|
||||
|
||||
List<String> returnedClasses = myHookParamsCaptor
|
||||
.getAllValues()
|
||||
.stream()
|
||||
.map(t -> t.get(IBaseResource.class, 0))
|
||||
.map(t -> t.getClass().getSimpleName())
|
||||
.collect(Collectors.toList());
|
||||
|
||||
ourLog.info("Returned classes: {}", returnedClasses);
|
||||
|
||||
assertThat(returnedClasses, hasItem("Composition"));
|
||||
assertThat(returnedClasses, hasItem("Organization"));
|
||||
}
|
||||
|
||||
private Bundle fetchBundle(String theUrl, EncodingEnum theEncoding) throws IOException {
|
||||
Bundle bundle;
|
||||
HttpGet get = new HttpGet(theUrl);
|
||||
|
||||
|
|
|
@ -109,7 +109,7 @@ public class JpaServerDemo extends RestfulServer {
|
|||
* This server tries to dynamically generate narratives
|
||||
*/
|
||||
FhirContext ctx = getFhirContext();
|
||||
ctx.setNarrativeGenerator(new DefaultThymeleafNarrativeGenerator());
|
||||
ctx.setNarrativeGenerator(new DefaultThymeleafNarrativeGenerator(getFhirContext()));
|
||||
|
||||
/*
|
||||
* Default to JSON and pretty printing
|
||||
|
|
|
@ -108,7 +108,7 @@ public class JpaServerDemoDstu2 extends RestfulServer {
|
|||
* This server tries to dynamically generate narratives
|
||||
*/
|
||||
FhirContext ctx = getFhirContext();
|
||||
ctx.setNarrativeGenerator(new DefaultThymeleafNarrativeGenerator());
|
||||
ctx.setNarrativeGenerator(new DefaultThymeleafNarrativeGenerator(getFhirContext()));
|
||||
|
||||
/*
|
||||
* Default to JSON and pretty printing
|
||||
|
|
|
@ -29,6 +29,11 @@ import java.util.List;
|
|||
*/
|
||||
public enum Pointcut {
|
||||
|
||||
/**
|
||||
* This pointcut will be called once when a given interceptor is registered
|
||||
*/
|
||||
REGISTERED,
|
||||
|
||||
/**
|
||||
* Invoked whenever a persisted resource has been modified and is being submitted to the
|
||||
* subscription processing pipeline. This method is called before the resource is placed
|
||||
|
@ -330,7 +335,32 @@ public enum Pointcut {
|
|||
* Hooks should return <code>void</code>.
|
||||
* </p>
|
||||
*/
|
||||
OP_PRESTORAGE_RESOURCE_UPDATED("org.hl7.fhir.instance.model.api.IBaseResource", "org.hl7.fhir.instance.model.api.IBaseResource");
|
||||
OP_PRESTORAGE_RESOURCE_UPDATED("org.hl7.fhir.instance.model.api.IBaseResource", "org.hl7.fhir.instance.model.api.IBaseResource"),
|
||||
|
||||
/**
|
||||
* Invoked when a resource may be returned to the user, whether as a part of a READ,
|
||||
* a SEARCH, or even as the response to a CREATE/UPDATE, etc.
|
||||
* <p>
|
||||
* This hook is invoked when a resource has been loaded by the storage engine and
|
||||
* is being returned to the HTTP stack for response. This is not a guarantee that the
|
||||
* client will ultimately see it, since filters/headers/etc may affect what
|
||||
* is returned but if a resource is loaded it is likely to be used.
|
||||
* Note also that caching may affect whether this pointcut is invoked.
|
||||
* </p>
|
||||
* <p>
|
||||
* Hooks will have access to the contents of the resource being returned
|
||||
* and may choose to make modifications. These changes will be reflected in
|
||||
* returned resource but have no effect on storage.
|
||||
* </p>
|
||||
* Hooks may accept the following parameters:
|
||||
* <ul>
|
||||
* <li>org.hl7.fhir.instance.model.api.IBaseResource (the resource being returned)</li>
|
||||
* </ul>
|
||||
* <p>
|
||||
* Hooks should return <code>void</code>.
|
||||
* </p>
|
||||
*/
|
||||
RESOURCE_MAY_BE_RETURNED("org.hl7.fhir.instance.model.api.IBaseResource");
|
||||
|
||||
private final List<String> myParameterTypes;
|
||||
|
||||
|
|
|
@ -25,6 +25,7 @@ import ca.uhn.fhir.rest.server.exceptions.InternalErrorException;
|
|||
import com.google.common.annotations.VisibleForTesting;
|
||||
import com.google.common.collect.ArrayListMultimap;
|
||||
import com.google.common.collect.ListMultimap;
|
||||
import com.google.common.collect.Sets;
|
||||
import org.apache.commons.collections4.ListUtils;
|
||||
import org.apache.commons.lang3.Validate;
|
||||
import org.slf4j.Logger;
|
||||
|
@ -92,11 +93,26 @@ public class InterceptorService implements IInterceptorRegistry, IInterceptorBro
|
|||
Class<?> interceptorClass = theInterceptor.getClass();
|
||||
int typeOrder = determineOrder(interceptorClass);
|
||||
|
||||
if (!scanInterceptorForHookMethodsAndAddThem(theInterceptor, typeOrder)) {
|
||||
List<HookInvoker> addedInvokers = scanInterceptorForHookMethods(theInterceptor, typeOrder);
|
||||
if (addedInvokers.isEmpty()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Invoke the REGISTERED pointcut for any added hooks
|
||||
addedInvokers.stream()
|
||||
.filter(t -> t.getPointcuts().contains(Pointcut.REGISTERED))
|
||||
.forEach(t -> t.invoke(new HookParams()));
|
||||
|
||||
// Register the interceptor and its various hooks
|
||||
myInterceptors.add(theInterceptor);
|
||||
for (HookInvoker nextAddedHook : addedInvokers) {
|
||||
for (Pointcut nextPointcut : nextAddedHook.getPointcuts()) {
|
||||
if (nextPointcut.equals(Pointcut.REGISTERED)) {
|
||||
continue;
|
||||
}
|
||||
myInvokers.put(nextPointcut, nextAddedHook);
|
||||
}
|
||||
}
|
||||
|
||||
// Make sure we're always sorted according to the order declared in
|
||||
// @Order
|
||||
|
@ -110,27 +126,25 @@ public class InterceptorService implements IInterceptorRegistry, IInterceptorBro
|
|||
}
|
||||
}
|
||||
|
||||
private boolean scanInterceptorForHookMethodsAndAddThem(Object theInterceptor, int theTypeOrder) {
|
||||
boolean retVal = false;
|
||||
/**
|
||||
* @return Returns a list of any added invokers
|
||||
*/
|
||||
private List<HookInvoker> scanInterceptorForHookMethods(Object theInterceptor, int theTypeOrder) {
|
||||
ArrayList<HookInvoker> retVal = new ArrayList<>();
|
||||
for (Method nextMethod : theInterceptor.getClass().getDeclaredMethods()) {
|
||||
Hook hook = AnnotationUtils.findAnnotation(nextMethod, Hook.class);
|
||||
|
||||
if (hook != null) {
|
||||
|
||||
int methodOrder = theTypeOrder;
|
||||
Order methodOrderAnnotation = AnnotationUtils.findAnnotation(nextMethod, Order.class);
|
||||
if (methodOrderAnnotation != null) {
|
||||
methodOrder = methodOrderAnnotation.value();
|
||||
}
|
||||
|
||||
HookInvoker invoker = new HookInvoker(hook, theInterceptor, nextMethod, methodOrder);
|
||||
for (Pointcut nextPointcut : hook.value()) {
|
||||
myInvokers.put(nextPointcut, invoker);
|
||||
retVal.add(new HookInvoker(hook, theInterceptor, nextMethod, methodOrder));
|
||||
}
|
||||
}
|
||||
|
||||
retVal = true;
|
||||
}
|
||||
}
|
||||
return retVal;
|
||||
}
|
||||
|
||||
|
@ -300,12 +314,14 @@ public class InterceptorService implements IInterceptorRegistry, IInterceptorBro
|
|||
private final Method myMethod;
|
||||
private final Class<?>[] myParameterTypes;
|
||||
private final int[] myParameterIndexes;
|
||||
private final Set<Pointcut> myPointcuts;
|
||||
|
||||
/**
|
||||
* Constructor
|
||||
*/
|
||||
private HookInvoker(Hook theHook, @Nonnull Object theInterceptor, @Nonnull Method theHookMethod, int theOrder) {
|
||||
super(theInterceptor, theOrder);
|
||||
myPointcuts = Collections.unmodifiableSet(Sets.newHashSet(theHook.value()));
|
||||
myParameterTypes = theHookMethod.getParameterTypes();
|
||||
myMethod = theHookMethod;
|
||||
|
||||
|
@ -325,6 +341,10 @@ public class InterceptorService implements IInterceptorRegistry, IInterceptorBro
|
|||
}
|
||||
}
|
||||
|
||||
public Set<Pointcut> getPointcuts() {
|
||||
return myPointcuts;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return Returns true/false if the hook method returns a boolean, returns true otherwise
|
||||
*/
|
||||
|
@ -352,7 +372,7 @@ public class InterceptorService implements IInterceptorRegistry, IInterceptorBro
|
|||
if (targetException instanceof RuntimeException) {
|
||||
throw ((RuntimeException) targetException);
|
||||
} else {
|
||||
throw new InternalErrorException(targetException);
|
||||
throw new InternalErrorException("Failure invoking interceptor for pointcut(s) " + getPointcuts(), targetException);
|
||||
}
|
||||
} catch (Exception e) {
|
||||
throw new InternalErrorException(e);
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
package ca.uhn.fhir.jpa.model.interceptor.executor;
|
||||
|
||||
import ca.uhn.fhir.jpa.model.interceptor.api.*;
|
||||
import ca.uhn.fhir.rest.server.exceptions.InternalErrorException;
|
||||
import org.hl7.fhir.instance.model.api.IBaseResource;
|
||||
import org.hl7.fhir.r4.model.Patient;
|
||||
import org.junit.Before;
|
||||
|
@ -40,6 +41,21 @@ public class InterceptorServiceTest {
|
|||
@Autowired
|
||||
private MyTestInterceptorManual myInterceptorManual;
|
||||
|
||||
@Test
|
||||
public void testRegisterHookFails() {
|
||||
int initialSize = myInterceptorRegistry.getGlobalInterceptorsForUnitTest().size();
|
||||
|
||||
try {
|
||||
myInterceptorRegistry.registerInterceptor(new InterceptorThatFailsOnRegister());
|
||||
fail();
|
||||
} catch (InternalErrorException e) {
|
||||
// good
|
||||
}
|
||||
|
||||
assertEquals(initialSize, myInterceptorRegistry.getGlobalInterceptorsForUnitTest().size());
|
||||
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testGlobalInterceptorsAreFound() {
|
||||
List<Object> globalInterceptors = myInterceptorRegistry.getGlobalInterceptorsForUnitTest();
|
||||
|
@ -265,4 +281,15 @@ public class InterceptorServiceTest {
|
|||
*/
|
||||
private static class ResourceDeliveryMessage {
|
||||
}
|
||||
|
||||
@Interceptor(manualRegistration = true)
|
||||
public static class InterceptorThatFailsOnRegister {
|
||||
|
||||
@Hook(Pointcut.REGISTERED)
|
||||
public void start() throws Exception {
|
||||
throw new Exception("InterceptorThatFailsOnRegister FAILED!");
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -10,7 +10,7 @@ import ca.uhn.fhir.jpa.searchparam.registry.ISearchParamRegistry;
|
|||
import ca.uhn.fhir.util.StopWatch;
|
||||
import org.hl7.fhir.dstu3.hapi.ctx.IValidationSupport;
|
||||
import org.hl7.fhir.dstu3.hapi.validation.CachingValidationSupport;
|
||||
import org.hl7.fhir.dstu3.hapi.validation.DefaultProfileValidationSupport;
|
||||
import org.hl7.fhir.dstu3.hapi.ctx.DefaultProfileValidationSupport;
|
||||
import org.hl7.fhir.dstu3.hapi.validation.ValidationSupportChain;
|
||||
import org.hl7.fhir.dstu3.model.Patient;
|
||||
import org.junit.Test;
|
||||
|
|
|
@ -11,7 +11,7 @@ import ca.uhn.fhir.jpa.searchparam.extractor.SearchParamExtractorDstu3;
|
|||
import ca.uhn.fhir.jpa.searchparam.registry.ISearchParamRegistry;
|
||||
import ca.uhn.fhir.util.TestUtil;
|
||||
import org.hl7.fhir.dstu3.hapi.ctx.IValidationSupport;
|
||||
import org.hl7.fhir.dstu3.hapi.validation.DefaultProfileValidationSupport;
|
||||
import org.hl7.fhir.dstu3.hapi.ctx.DefaultProfileValidationSupport;
|
||||
import org.hl7.fhir.dstu3.model.Observation;
|
||||
import org.junit.AfterClass;
|
||||
import org.junit.BeforeClass;
|
||||
|
|
|
@ -26,7 +26,7 @@ import ca.uhn.fhir.jpa.searchparam.extractor.SearchParamExtractorDstu3;
|
|||
import ca.uhn.fhir.jpa.searchparam.registry.ISearchParamRegistry;
|
||||
import ca.uhn.fhir.jpa.searchparam.registry.SearchParamRegistryDstu3;
|
||||
import org.hl7.fhir.dstu3.hapi.ctx.IValidationSupport;
|
||||
import org.hl7.fhir.dstu3.hapi.validation.DefaultProfileValidationSupport;
|
||||
import org.hl7.fhir.dstu3.hapi.ctx.DefaultProfileValidationSupport;
|
||||
import org.springframework.beans.factory.annotation.Autowire;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Primary;
|
||||
|
|
|
@ -29,9 +29,12 @@ import ca.uhn.fhir.jpa.subscription.module.CanonicalSubscription;
|
|||
import ca.uhn.fhir.jpa.subscription.module.ResourceModifiedMessage;
|
||||
import ca.uhn.fhir.rest.server.exceptions.InternalErrorException;
|
||||
import org.hl7.fhir.instance.model.api.IBaseResource;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
|
||||
public class InMemorySubscriptionMatcher implements ISubscriptionMatcher {
|
||||
private static final Logger ourLog = LoggerFactory.getLogger(InMemorySubscriptionMatcher.class);
|
||||
|
||||
@Autowired
|
||||
private FhirContext myContext;
|
||||
|
@ -49,7 +52,8 @@ public class InMemorySubscriptionMatcher implements ISubscriptionMatcher {
|
|||
try {
|
||||
return match(theSubscription.getCriteriaString(), theMsg.getNewPayload(myContext));
|
||||
} catch (Exception e) {
|
||||
throw new InternalErrorException("Failure processing resource ID[" + theMsg.getId(myContext) + "] for subscription ID[" + theSubscription.getIdElementString() + "]: " + e.getMessage(), e);
|
||||
ourLog.error("Failure in in-memory matcher", e);
|
||||
throw new InternalErrorException("Failure performing memory-match for resource ID[" + theMsg.getId(myContext) + "] for subscription ID[" + theSubscription.getIdElementString() + "]: " + e.getMessage(), e);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -159,7 +159,7 @@ public class TestRestfulServer extends RestfulServer {
|
|||
* This server tries to dynamically generate narratives
|
||||
*/
|
||||
FhirContext ctx = getFhirContext();
|
||||
ctx.setNarrativeGenerator(new DefaultThymeleafNarrativeGenerator());
|
||||
ctx.setNarrativeGenerator(new DefaultThymeleafNarrativeGenerator(getFhirContext()));
|
||||
|
||||
/*
|
||||
* The resource and system providers (which actually implement the various FHIR
|
||||
|
|
|
@ -37,6 +37,7 @@ public abstract class BaseReference extends Type implements IBaseReference, ICom
|
|||
* a part of the FHIR "wire format" and is never transmitted or receieved inline, but this property
|
||||
* may be changed/accessed by parsers.
|
||||
*/
|
||||
@Override
|
||||
public IBaseResource getResource() {
|
||||
return resource;
|
||||
}
|
||||
|
@ -53,8 +54,10 @@ public abstract class BaseReference extends Type implements IBaseReference, ICom
|
|||
* a part of the FHIR "wire format" and is never transmitted or receieved inline, but this property
|
||||
* may be changed/accessed by parsers.
|
||||
*/
|
||||
public void setResource(IBaseResource theResource) {
|
||||
@Override
|
||||
public BaseReference setResource(IBaseResource theResource) {
|
||||
resource = theResource;
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
|
|
|
@ -13,6 +13,7 @@ public abstract class BaseResource extends Base implements IAnyResource, IElemen
|
|||
/**
|
||||
* @param value The logical id of the resource, as used in the url for the resoure. Once assigned, this value never changes.
|
||||
*/
|
||||
@Override
|
||||
public BaseResource setId(IIdType value) {
|
||||
if (value == null) {
|
||||
setIdElement((IdType)null);
|
||||
|
|
|
@ -129,6 +129,7 @@ public class Reference extends BaseReference implements IBaseReference, IComposi
|
|||
/**
|
||||
* @return A reference to a location at which the other resource is found. The reference may be a relative reference, in which case it is relative to the service base URL, or an absolute URL that resolves to the location where the resource is found. The reference may be version specific or not. If the reference is not to a FHIR RESTful server, then it should be assumed to be version specific. Internal fragment references (start with '#') refer to contained resources.
|
||||
*/
|
||||
@Override
|
||||
public String getReference() {
|
||||
return this.reference == null ? null : this.reference.getValue();
|
||||
}
|
||||
|
|
|
@ -103,6 +103,7 @@ public abstract class Resource extends BaseResource implements IAnyResource {
|
|||
/**
|
||||
* @param value {@link #id} (The logical id of the resource, as used in the URL for the resource. Once assigned, this value never changes.). This is the underlying object with id, value and extensions. The accessor "getId" gives direct access to the value
|
||||
*/
|
||||
@Override
|
||||
public Resource setIdElement(IdType value) {
|
||||
this.id = value;
|
||||
return this;
|
||||
|
|
|
@ -1020,7 +1020,7 @@ public class JsonParserDstu2_1Test {
|
|||
Patient p = new Patient();
|
||||
p.addName().addFamily("Smith").addGiven("John");
|
||||
|
||||
ourCtx.setNarrativeGenerator(new DefaultThymeleafNarrativeGenerator());
|
||||
ourCtx.setNarrativeGenerator(new DefaultThymeleafNarrativeGenerator(ourCtx));
|
||||
|
||||
String output = ourCtx.newJsonParser().encodeResourceToString(p);
|
||||
ourLog.info(output);
|
||||
|
|
|
@ -1695,7 +1695,7 @@ public class XmlParserDstu2_1Test {
|
|||
Patient p = new Patient();
|
||||
p.addName().addFamily("Smith").addGiven("John");
|
||||
|
||||
ourCtx.setNarrativeGenerator(new DefaultThymeleafNarrativeGenerator());
|
||||
ourCtx.setNarrativeGenerator(new DefaultThymeleafNarrativeGenerator(ourCtx));
|
||||
|
||||
String output = ourCtx.newXmlParser().encodeResourceToString(p);
|
||||
ourLog.info(output);
|
||||
|
|
|
@ -7,7 +7,6 @@ import org.junit.AfterClass;
|
|||
import org.junit.Test;
|
||||
|
||||
import ca.uhn.fhir.context.FhirContext;
|
||||
import ca.uhn.fhir.model.dstu2.composite.NarrativeDt;
|
||||
import ca.uhn.fhir.model.dstu2.resource.Practitioner;
|
||||
import ca.uhn.fhir.util.TestUtil;
|
||||
|
||||
|
@ -27,7 +26,7 @@ public class CustomThymeleafNarrativeGeneratorDstu2Test {
|
|||
public void testGenerator() {
|
||||
|
||||
// CustomThymeleafNarrativeGenerator gen = new CustomThymeleafNarrativeGenerator("file:src/test/resources/narrative/customnarrative.properties");
|
||||
CustomThymeleafNarrativeGenerator gen = new CustomThymeleafNarrativeGenerator("classpath:narrative/customnarrative_dstu2.properties");
|
||||
CustomThymeleafNarrativeGenerator gen = new CustomThymeleafNarrativeGenerator(ourCtx,"classpath:narrative/customnarrative_dstu2.properties");
|
||||
ourCtx.setNarrativeGenerator(gen);
|
||||
|
||||
Practitioner p = new Practitioner();
|
||||
|
@ -36,10 +35,9 @@ public class CustomThymeleafNarrativeGeneratorDstu2Test {
|
|||
p.addAddress().addLine("line1").addLine("line2");
|
||||
p.getName().addFamily("fam1").addGiven("given");
|
||||
|
||||
NarrativeDt narrative = new NarrativeDt();
|
||||
gen.generateNarrative(ourCtx, p, narrative);
|
||||
gen.populateResourceNarrative(p);
|
||||
|
||||
String actual = narrative.getDiv().getValueAsString();
|
||||
String actual = p.getText().getDiv().getValueAsString();
|
||||
ourLog.info(actual);
|
||||
|
||||
assertThat(actual, containsString("<h1>Name</h1><div class=\"nameElement\">given <b>FAM1 </b></div><h1>Address</h1><div><span>line1 </span><br/><span>line2 </span><br/></div></div>"));
|
||||
|
|
|
@ -17,13 +17,10 @@ import org.junit.Test;
|
|||
import ca.uhn.fhir.context.FhirContext;
|
||||
import ca.uhn.fhir.model.api.TemporalPrecisionEnum;
|
||||
import ca.uhn.fhir.model.dstu2.composite.CodeableConceptDt;
|
||||
import ca.uhn.fhir.model.dstu2.composite.NarrativeDt;
|
||||
import ca.uhn.fhir.model.dstu2.composite.PeriodDt;
|
||||
import ca.uhn.fhir.model.dstu2.composite.QuantityDt;
|
||||
import ca.uhn.fhir.model.dstu2.composite.ResourceReferenceDt;
|
||||
import ca.uhn.fhir.model.dstu2.composite.SimpleQuantityDt;
|
||||
import ca.uhn.fhir.model.dstu2.resource.DiagnosticReport;
|
||||
import ca.uhn.fhir.model.dstu2.resource.Encounter;
|
||||
import ca.uhn.fhir.model.dstu2.resource.Medication;
|
||||
import ca.uhn.fhir.model.dstu2.resource.MedicationOrder;
|
||||
import ca.uhn.fhir.model.dstu2.resource.Observation;
|
||||
|
@ -31,7 +28,6 @@ import ca.uhn.fhir.model.dstu2.resource.OperationOutcome;
|
|||
import ca.uhn.fhir.model.dstu2.resource.Parameters;
|
||||
import ca.uhn.fhir.model.dstu2.resource.Patient;
|
||||
import ca.uhn.fhir.model.dstu2.valueset.DiagnosticReportStatusEnum;
|
||||
import ca.uhn.fhir.model.dstu2.valueset.EncounterClassEnum;
|
||||
import ca.uhn.fhir.model.dstu2.valueset.MedicationOrderStatusEnum;
|
||||
import ca.uhn.fhir.model.dstu2.valueset.ObservationStatusEnum;
|
||||
import ca.uhn.fhir.model.primitive.DateTimeDt;
|
||||
|
@ -52,10 +48,8 @@ public class DefaultThymeleafNarrativeGeneratorDstu2Test {
|
|||
|
||||
@Before
|
||||
public void before() {
|
||||
myGen = new DefaultThymeleafNarrativeGenerator();
|
||||
myGen = new DefaultThymeleafNarrativeGenerator(ourCtx);
|
||||
myGen.setUseHapiServerConformanceNarrative(true);
|
||||
myGen.setIgnoreFailures(false);
|
||||
myGen.setIgnoreMissingTemplates(false);
|
||||
|
||||
ourCtx.setNarrativeGenerator(myGen);
|
||||
}
|
||||
|
@ -71,9 +65,8 @@ public class DefaultThymeleafNarrativeGeneratorDstu2Test {
|
|||
|
||||
value.setBirthDate(new Date(), TemporalPrecisionEnum.DAY);
|
||||
|
||||
NarrativeDt narrative = new NarrativeDt();
|
||||
myGen.generateNarrative(ourCtx, value, narrative);
|
||||
String output = narrative.getDiv().getValueAsString();
|
||||
myGen.populateResourceNarrative(value);
|
||||
String output = value.getText().getDiv().getValueAsString();
|
||||
ourLog.info(output);
|
||||
assertThat(output, StringContains.containsString("<div class=\"hapiHeaderText\">joe john <b>BLOW </b></div>"));
|
||||
|
||||
|
@ -81,49 +74,18 @@ public class DefaultThymeleafNarrativeGeneratorDstu2Test {
|
|||
|
||||
@Test
|
||||
public void testUnsupportedType() throws DataFormatException {
|
||||
myGen.setIgnoreMissingTemplates(true);
|
||||
|
||||
Parameters value = new Parameters();
|
||||
value.setId("123");
|
||||
|
||||
NarrativeDt narrative = new NarrativeDt();
|
||||
myGen.generateNarrative(ourCtx, value, narrative);
|
||||
String output = narrative.getDiv().getValueAsString();
|
||||
myGen.populateResourceNarrative(value);
|
||||
String output = value.getText().getDiv().getValueAsString();
|
||||
ourLog.info(output);
|
||||
assertThat(output, not(containsString("narrative")));
|
||||
|
||||
}
|
||||
|
||||
@Test(expected=DataFormatException.class)
|
||||
public void testUnsupportedTypeDontIgnore() throws DataFormatException {
|
||||
myGen.setIgnoreMissingTemplates(false);
|
||||
|
||||
Parameters value = new Parameters();
|
||||
value.setId("123");
|
||||
|
||||
NarrativeDt narrative = new NarrativeDt();
|
||||
myGen.generateNarrative(ourCtx, value, narrative);
|
||||
}
|
||||
|
||||
@Test
|
||||
@Ignore
|
||||
public void testGenerateEncounter() throws DataFormatException {
|
||||
Encounter enc = new Encounter();
|
||||
|
||||
enc.addIdentifier().setSystem("urn:visits").setValue("1234567");
|
||||
enc.setClassElement(EncounterClassEnum.AMBULATORY);
|
||||
enc.setPeriod(new PeriodDt().setStart(new DateTimeDt("2001-01-02T11:11:00")));
|
||||
|
||||
NarrativeDt narrative = new NarrativeDt();
|
||||
myGen.generateNarrative(ourCtx, enc, narrative);
|
||||
|
||||
assertEquals("", narrative.getDivAsString());
|
||||
}
|
||||
|
||||
|
||||
@Test
|
||||
public void testGenerateOperationOutcome() {
|
||||
//@formatter:off
|
||||
String parse = "<OperationOutcome xmlns=\"http://hl7.org/fhir\">\n" +
|
||||
" <issue>\n" +
|
||||
" <severity value=\"error\"/>\n" +
|
||||
|
@ -134,17 +96,11 @@ public class DefaultThymeleafNarrativeGeneratorDstu2Test {
|
|||
" <diagnostics value=\"YThis is a warning\"/>\n" +
|
||||
" </issue>\n" +
|
||||
"</OperationOutcome>";
|
||||
//@formatter:on
|
||||
|
||||
OperationOutcome oo = ourCtx.newXmlParser().parseResource(OperationOutcome.class, parse);
|
||||
|
||||
// String output = gen.generateTitle(oo);
|
||||
// ourLog.info(output);
|
||||
// assertEquals("Operation Outcome (2 issues)", output);
|
||||
|
||||
NarrativeDt narrative = new NarrativeDt();
|
||||
myGen.generateNarrative(ourCtx, oo, narrative);
|
||||
String output = narrative.getDiv().getValueAsString();
|
||||
myGen.populateResourceNarrative(oo);
|
||||
String output = oo.getText().getDiv().getValueAsString();
|
||||
|
||||
ourLog.info(output);
|
||||
|
||||
|
@ -181,9 +137,8 @@ public class DefaultThymeleafNarrativeGeneratorDstu2Test {
|
|||
value.addResult().setResource(obs);
|
||||
}
|
||||
|
||||
NarrativeDt narrative = new NarrativeDt();
|
||||
myGen.generateNarrative(ourCtx, value, narrative);
|
||||
String output = narrative.getDiv().getValueAsString();
|
||||
myGen.populateResourceNarrative(value);
|
||||
String output = value.getText().getDiv().getValueAsString();
|
||||
|
||||
ourLog.info(output);
|
||||
assertThat(output, StringContains.containsString("<div class=\"hapiHeaderText\"> Some & Diagnostic Report </div>"));
|
||||
|
@ -207,11 +162,11 @@ public class DefaultThymeleafNarrativeGeneratorDstu2Test {
|
|||
mp.setStatus(MedicationOrderStatusEnum.ACTIVE);
|
||||
mp.setDateWritten(new DateTimeDt("2014-09-01"));
|
||||
|
||||
NarrativeDt narrative = new NarrativeDt();
|
||||
myGen.generateNarrative(ourCtx, mp, narrative);
|
||||
myGen.populateResourceNarrative(mp);
|
||||
String output = mp.getText().getDiv().getValueAsString();
|
||||
|
||||
assertTrue("Expected medication name of ciprofloaxin within narrative: " + narrative.getDiv().toString(), narrative.getDiv().toString().indexOf("ciprofloaxin") > -1);
|
||||
assertTrue("Expected string status of ACTIVE within narrative: " + narrative.getDiv().toString(), narrative.getDiv().toString().indexOf("ACTIVE") > -1);
|
||||
assertTrue("Expected medication name of ciprofloaxin within narrative: " + output, output.indexOf("ciprofloaxin") > -1);
|
||||
assertTrue("Expected string status of ACTIVE within narrative: " + output, output.indexOf("ACTIVE") > -1);
|
||||
|
||||
}
|
||||
|
||||
|
@ -220,11 +175,9 @@ public class DefaultThymeleafNarrativeGeneratorDstu2Test {
|
|||
Medication med = new Medication();
|
||||
med.getCode().setText("ciproflaxin");
|
||||
|
||||
NarrativeDt narrative = new NarrativeDt();
|
||||
myGen.generateNarrative(ourCtx, med, narrative);
|
||||
|
||||
String string = narrative.getDiv().toString();
|
||||
assertThat(string, containsString("ciproflaxin"));
|
||||
myGen.populateResourceNarrative(med);
|
||||
String output = med.getText().getDiv().getValueAsString();
|
||||
assertThat(output, containsString("ciproflaxin"));
|
||||
|
||||
}
|
||||
|
||||
|
|
|
@ -71,7 +71,7 @@ public class BundleTypeInResponseTest {
|
|||
|
||||
ServletHandler proxyHandler = new ServletHandler();
|
||||
RestfulServer servlet = new RestfulServer(ourCtx);
|
||||
servlet.getFhirContext().setNarrativeGenerator(new DefaultThymeleafNarrativeGenerator());
|
||||
servlet.getFhirContext().setNarrativeGenerator(new DefaultThymeleafNarrativeGenerator(ourCtx));
|
||||
|
||||
servlet.setResourceProviders(patientProvider);
|
||||
ServletHolder servletHolder = new ServletHolder(servlet);
|
||||
|
|
|
@ -8,9 +8,12 @@
|
|||
# template file.
|
||||
# Format is file:/path/foo.html or classpath:/com/classpath/foo.html
|
||||
#
|
||||
practitioner.class=ca.uhn.fhir.model.dstu2.resource.Practitioner
|
||||
practitioner.resourceType=Practitioner
|
||||
practitioner.narrative=classpath:narrative/PractitionerDstu2.html
|
||||
|
||||
# You may also override/define behaviour for datatypes
|
||||
humanname.class=ca.uhn.fhir.model.dev.composite.HumanNameDt
|
||||
humanname.dataType=HumanName
|
||||
humanname.narrative=classpath:ca/uhn/fhir/narrative/datatype/HumanNameDt.html
|
||||
|
||||
address.dataType=Address
|
||||
address.narrative=classpath:ca/uhn/fhir/narrative/datatype/AddressDt.html
|
||||
|
|
|
@ -1,11 +1,10 @@
|
|||
package org.hl7.fhir.dstu3.hapi.validation;
|
||||
package org.hl7.fhir.dstu3.hapi.ctx;
|
||||
|
||||
import ca.uhn.fhir.context.FhirContext;
|
||||
import org.apache.commons.io.Charsets;
|
||||
import org.apache.commons.io.IOUtils;
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import org.apache.commons.lang3.Validate;
|
||||
import org.hl7.fhir.dstu3.hapi.ctx.IValidationSupport;
|
||||
import org.hl7.fhir.dstu3.model.*;
|
||||
import org.hl7.fhir.dstu3.model.Bundle.BundleEntryComponent;
|
||||
import org.hl7.fhir.dstu3.model.CodeSystem.CodeSystemContentMode;
|
|
@ -51,7 +51,7 @@ public class FhirDstu3 implements IFhirVersion {
|
|||
|
||||
@Override
|
||||
public IContextValidationSupport<?, ?, ?, ?, ?, ?> createValidationSupport() {
|
||||
String className = "org.hl7.fhir.dstu3.hapi.validation.DefaultProfileValidationSupport";
|
||||
String className = "org.hl7.fhir.dstu3.hapi.ctx.DefaultProfileValidationSupport";
|
||||
try {
|
||||
return (IContextValidationSupport<?, ?, ?, ?, ?, ?>) Class.forName(className).newInstance();
|
||||
} catch (Exception theE) {
|
||||
|
|
|
@ -37,6 +37,7 @@ public abstract class BaseReference extends Type implements IBaseReference, ICom
|
|||
* a part of the FHIR "wire format" and is never transmitted or receieved inline, but this property
|
||||
* may be changed/accessed by parsers.
|
||||
*/
|
||||
@Override
|
||||
public IBaseResource getResource() {
|
||||
return resource;
|
||||
}
|
||||
|
@ -53,8 +54,10 @@ public abstract class BaseReference extends Type implements IBaseReference, ICom
|
|||
* a part of the FHIR "wire format" and is never transmitted or receieved inline, but this property
|
||||
* may be changed/accessed by parsers.
|
||||
*/
|
||||
public void setResource(IBaseResource theResource) {
|
||||
@Override
|
||||
public BaseReference setResource(IBaseResource theResource) {
|
||||
resource = theResource;
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
|
|
|
@ -136,6 +136,7 @@ public class Reference extends BaseReference implements IBaseReference, IComposi
|
|||
/**
|
||||
* @return A reference to a location at which the other resource is found. The reference may be a relative reference, in which case it is relative to the service base URL, or an absolute URL that resolves to the location where the resource is found. The reference may be version specific or not. If the reference is not to a FHIR RESTful server, then it should be assumed to be version specific. Internal fragment references (start with '#') refer to contained resources.
|
||||
*/
|
||||
@Override
|
||||
public String getReference() {
|
||||
return this.reference == null ? null : this.reference.getValue();
|
||||
}
|
||||
|
|
|
@ -20,7 +20,6 @@ import org.hl7.fhir.dstu3.model.DiagnosticReport.DiagnosticReportStatus;
|
|||
import org.hl7.fhir.dstu3.model.Medication;
|
||||
import org.hl7.fhir.dstu3.model.MedicationRequest;
|
||||
import org.hl7.fhir.dstu3.model.MedicationRequest.MedicationRequestStatus;
|
||||
import org.hl7.fhir.dstu3.model.Narrative;
|
||||
import org.hl7.fhir.dstu3.model.Observation;
|
||||
import org.hl7.fhir.dstu3.model.Observation.ObservationStatus;
|
||||
import org.hl7.fhir.dstu3.model.OperationOutcome;
|
||||
|
@ -47,10 +46,8 @@ public class DefaultThymeleafNarrativeGeneratorDstu3Test {
|
|||
|
||||
@Before
|
||||
public void before() {
|
||||
myGen = new DefaultThymeleafNarrativeGenerator();
|
||||
myGen = new DefaultThymeleafNarrativeGenerator(ourCtx);
|
||||
myGen.setUseHapiServerConformanceNarrative(true);
|
||||
myGen.setIgnoreFailures(false);
|
||||
myGen.setIgnoreMissingTemplates(false);
|
||||
|
||||
ourCtx.setNarrativeGenerator(myGen);
|
||||
}
|
||||
|
@ -67,18 +64,15 @@ public class DefaultThymeleafNarrativeGeneratorDstu3Test {
|
|||
Patient value = new Patient();
|
||||
|
||||
value.addIdentifier().setSystem("urn:names").setValue("123456");
|
||||
value.addName().setFamily("blow").addGiven("joe").addGiven((String) null).addGiven("john");
|
||||
//@formatter:off
|
||||
value.addName().setFamily("blow").addGiven("joe").addGiven(null).addGiven("john");
|
||||
value.addAddress()
|
||||
.addLine("123 Fake Street").addLine("Unit 1")
|
||||
.setCity("Toronto").setState("ON").setCountry("Canada");
|
||||
//@formatter:on
|
||||
|
||||
value.setBirthDate(new Date());
|
||||
|
||||
Narrative narrative = new Narrative();
|
||||
myGen.generateNarrative(ourCtx, value, narrative);
|
||||
String output = narrative.getDiv().getValueAsString();
|
||||
myGen.populateResourceNarrative(value);
|
||||
String output = value.getText().getDiv().getValueAsString();
|
||||
ourLog.info(output);
|
||||
assertThat(output, StringContains.containsString("<div class=\"hapiHeaderText\">joe john <b>BLOW </b></div>"));
|
||||
|
||||
|
@ -86,9 +80,7 @@ public class DefaultThymeleafNarrativeGeneratorDstu3Test {
|
|||
|
||||
@Test
|
||||
public void testTranslations() throws DataFormatException {
|
||||
CustomThymeleafNarrativeGenerator customGen = new CustomThymeleafNarrativeGenerator("classpath:/testnarrative.properties");
|
||||
customGen.setIgnoreFailures(false);
|
||||
customGen.setIgnoreMissingTemplates(false);
|
||||
CustomThymeleafNarrativeGenerator customGen = new CustomThymeleafNarrativeGenerator(ourCtx, "classpath:/testnarrative.properties");
|
||||
|
||||
FhirContext ctx = FhirContext.forDstu3();
|
||||
ctx.setNarrativeGenerator(customGen);
|
||||
|
@ -96,7 +88,7 @@ public class DefaultThymeleafNarrativeGeneratorDstu3Test {
|
|||
Patient value = new Patient();
|
||||
|
||||
value.addIdentifier().setSystem("urn:names").setValue("123456");
|
||||
value.addName().setFamily("blow").addGiven("joe").addGiven((String) null).addGiven("john");
|
||||
value.addName().setFamily("blow").addGiven("joe").addGiven(null).addGiven("john");
|
||||
//@formatter:off
|
||||
value.addAddress()
|
||||
.addLine("123 Fake Street").addLine("Unit 1")
|
||||
|
@ -106,7 +98,6 @@ public class DefaultThymeleafNarrativeGeneratorDstu3Test {
|
|||
value.setBirthDate(new Date());
|
||||
|
||||
Transformer transformer = new Transformer() {
|
||||
|
||||
@Override
|
||||
public Object transform(Object input) {
|
||||
return "UNTRANSLATED:" + input;
|
||||
|
@ -123,9 +114,8 @@ public class DefaultThymeleafNarrativeGeneratorDstu3Test {
|
|||
}
|
||||
});
|
||||
|
||||
Narrative narrative = new Narrative();
|
||||
customGen.generateNarrative(ctx, value, narrative);
|
||||
String output = narrative.getDiv().getValueAsString();
|
||||
customGen.populateResourceNarrative(value);
|
||||
String output = value.getText().getDiv().getValueAsString();
|
||||
ourLog.info(output);
|
||||
assertThat(output, StringContains.containsString("Some beautiful proze"));
|
||||
assertThat(output, StringContains.containsString("UNTRANSLATED:other_text"));
|
||||
|
@ -140,9 +130,8 @@ public class DefaultThymeleafNarrativeGeneratorDstu3Test {
|
|||
value.addResult().setReference("Observation/2");
|
||||
value.addResult().setReference("Observation/3");
|
||||
|
||||
Narrative narrative = new Narrative();
|
||||
myGen.generateNarrative(ourCtx, value, narrative);
|
||||
String output = narrative.getDiv().getValueAsString();
|
||||
myGen.populateResourceNarrative(value);
|
||||
String output = value.getText().getDiv().getValueAsString();
|
||||
|
||||
ourLog.info(output);
|
||||
assertThat(output, StringContains.containsString(value.getCode().getTextElement().getValue()));
|
||||
|
@ -169,9 +158,8 @@ public class DefaultThymeleafNarrativeGeneratorDstu3Test {
|
|||
// ourLog.info(output);
|
||||
// assertEquals("Operation Outcome (2 issues)", output);
|
||||
|
||||
Narrative narrative = new Narrative();
|
||||
myGen.generateNarrative(ourCtx, oo, narrative);
|
||||
String output = narrative.getDiv().getValueAsString();
|
||||
myGen.populateResourceNarrative(oo);
|
||||
String output = oo.getText().getDiv().getValueAsString();
|
||||
|
||||
ourLog.info(output);
|
||||
|
||||
|
@ -209,9 +197,8 @@ public class DefaultThymeleafNarrativeGeneratorDstu3Test {
|
|||
value.addResult().setResource(obs);
|
||||
}
|
||||
|
||||
Narrative narrative = new Narrative();
|
||||
myGen.generateNarrative(ourCtx, value, narrative);
|
||||
String output = narrative.getDiv().getValueAsString();
|
||||
myGen.populateResourceNarrative(value);
|
||||
String output = value.getText().getDiv().getValueAsString();
|
||||
|
||||
ourLog.info(output);
|
||||
assertThat(output, StringContains.containsString("<div class=\"hapiHeaderText\"> Some & Diagnostic Report </div>"));
|
||||
|
@ -230,11 +217,11 @@ public class DefaultThymeleafNarrativeGeneratorDstu3Test {
|
|||
mp.setStatus(MedicationRequestStatus.ACTIVE);
|
||||
mp.setAuthoredOnElement(new DateTimeType("2014-09-01"));
|
||||
|
||||
Narrative narrative = new Narrative();
|
||||
myGen.generateNarrative(ourCtx, mp, narrative);
|
||||
myGen.populateResourceNarrative(mp);
|
||||
String output = mp.getText().getDiv().getValueAsString();
|
||||
|
||||
assertTrue("Expected medication name of ciprofloaxin within narrative: " + narrative.getDiv().toString(), narrative.getDiv().toString().indexOf("ciprofloaxin") > -1);
|
||||
assertTrue("Expected string status of ACTIVE within narrative: " + narrative.getDiv().toString(), narrative.getDiv().toString().indexOf("ACTIVE") > -1);
|
||||
assertTrue("Expected medication name of ciprofloaxin within narrative: "+output, output.contains("ciprofloaxin"));
|
||||
assertTrue("Expected string status of ACTIVE within narrative: " +output, output.contains("ACTIVE"));
|
||||
|
||||
}
|
||||
|
||||
|
@ -243,11 +230,10 @@ public class DefaultThymeleafNarrativeGeneratorDstu3Test {
|
|||
Medication med = new Medication();
|
||||
med.getCode().setText("ciproflaxin");
|
||||
|
||||
Narrative narrative = new Narrative();
|
||||
myGen.generateNarrative(ourCtx, med, narrative);
|
||||
myGen.populateResourceNarrative(med);
|
||||
|
||||
String string = narrative.getDiv().getValueAsString();
|
||||
assertThat(string, containsString("ciproflaxin"));
|
||||
String output = med.getText().getDiv().getValueAsString();
|
||||
assertThat(output, containsString("ciproflaxin"));
|
||||
|
||||
}
|
||||
|
||||
|
|
|
@ -0,0 +1,85 @@
|
|||
package ca.uhn.fhir.narrative2;
|
||||
|
||||
import ca.uhn.fhir.context.FhirContext;
|
||||
import org.hamcrest.Matchers;
|
||||
import org.hl7.fhir.dstu3.model.*;
|
||||
import org.junit.Test;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import java.io.IOException;
|
||||
|
||||
import static org.hamcrest.CoreMatchers.containsString;
|
||||
import static org.junit.Assert.assertEquals;
|
||||
import static org.junit.Assert.assertThat;
|
||||
|
||||
public class ThymeleafNarrativeGeneratorTest {
|
||||
|
||||
private static final Logger ourLog = LoggerFactory.getLogger(ThymeleafNarrativeGeneratorTest.class);
|
||||
private FhirContext myCtx = FhirContext.forDstu3();
|
||||
|
||||
@Test
|
||||
public void testGenerateCompositionWithContextPath() throws IOException {
|
||||
DiagnosticReport dr1 = new DiagnosticReport();
|
||||
dr1.setStatus(DiagnosticReport.DiagnosticReportStatus.FINAL);
|
||||
dr1.setIssuedElement(new InstantType("2019-01-01T12:12:12-05:00"));
|
||||
dr1.getCode().getCodingFirstRep().setDisplay("Complete Blood Count");
|
||||
|
||||
Observation obs1 = new Observation();
|
||||
obs1.getCode().getCodingFirstRep().setDisplay("Hemoglobin [Mass/volume] in Blood");
|
||||
obs1.setValue(new Quantity(null, 176, "http://unitsofmeasure.org", "g/L", "g/L"));
|
||||
obs1.getReferenceRangeFirstRep().getLow().setValue(135).setSystem("http://unitsofmeasure.org").setCode("g/L").setUnit("g/L");
|
||||
obs1.getReferenceRangeFirstRep().getHigh().setValue(180).setSystem("http://unitsofmeasure.org").setCode("g/L").setUnit("g/L");
|
||||
obs1.getReferenceRangeFirstRep().getTextElement().setValue("135 - 180");
|
||||
dr1.addResult().setResource(obs1);
|
||||
|
||||
Observation obs2 = new Observation();
|
||||
obs2.getCode().getCodingFirstRep().setDisplay("Erythrocytes [#/volume] in Blood by Automated count");
|
||||
obs2.setValue(new Quantity(null, 5.9, "http://unitsofmeasure.org", "x10*12/L", "x10*12/L"));
|
||||
obs2.getReferenceRangeFirstRep().getLow().setValue(4.2).setSystem("http://unitsofmeasure.org").setCode("x10*12/L").setUnit("x10*12/L");
|
||||
obs2.getReferenceRangeFirstRep().getHigh().setValue(6.0).setSystem("http://unitsofmeasure.org").setCode("x10*12/L").setUnit("x10*12/L");
|
||||
obs2.getReferenceRangeFirstRep().getTextElement().setValue("4.2 - 6.0");
|
||||
dr1.addResult().setResource(obs2);
|
||||
|
||||
Composition composition = new Composition();
|
||||
|
||||
Composition.SectionComponent sect = composition.addSection();
|
||||
sect.setTitle("History of Medication use Narrative");
|
||||
sect.getCode().getCodingFirstRep().setSystem("2.16.840.1.113883.6.1");
|
||||
sect.getCode().getCodingFirstRep().setCode("10160-0");
|
||||
sect.getCode().getCodingFirstRep().setDisplay("History of Medication use Narrative");
|
||||
|
||||
sect = composition.addSection();
|
||||
sect.setTitle("Relevant diagnostic tests/laboratory data Narrative");
|
||||
sect.getCode().getCodingFirstRep().setSystem("2.16.840.1.113883.6.1");
|
||||
sect.getCode().getCodingFirstRep().setCode("30954-2");
|
||||
sect.getCode().getCodingFirstRep().setDisplay("Relevant diagnostic tests/laboratory data Narrative");
|
||||
Reference ref = new Reference();
|
||||
ref.setReference("DiagnosticReport/1").setResource(dr1);
|
||||
sect.getEntry().add(ref);
|
||||
|
||||
NarrativeTemplateManifest manifest = NarrativeTemplateManifest.forManifestFileLocation(myCtx, "classpath:narrative2/narratives.properties");
|
||||
ThymeleafNarrativeGenerator gen = new ThymeleafNarrativeGenerator(myCtx);
|
||||
gen.setManifest(manifest);
|
||||
|
||||
gen.populateResourceNarrative(composition);
|
||||
|
||||
// Firt narrative should be empty
|
||||
String narrative = composition.getSection().get(0).getText().getDiv().getValueAsString();
|
||||
assertThat(narrative, Matchers.emptyOrNullString());
|
||||
|
||||
// Second narrative should have details
|
||||
narrative = composition.getSection().get(1).getText().getDiv().getValueAsString();
|
||||
ourLog.info("Narrative:\n{}", narrative);
|
||||
|
||||
assertThat(narrative, containsString("<thead><tr><td>Name</td><td>Value</td>"));
|
||||
assertThat(narrative, containsString("<td> 4.2 - 6.0 </td>"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testTemplateCount() throws IOException {
|
||||
NarrativeTemplateManifest manifest = NarrativeTemplateManifest.forManifestFileLocation(myCtx, "classpath:narrative2/narratives.properties");
|
||||
assertEquals(4, manifest.getNamedTemplateCount());
|
||||
}
|
||||
|
||||
}
|
|
@ -1241,7 +1241,7 @@ public class JsonParserDstu3Test {
|
|||
Patient p = new Patient();
|
||||
p.addName().setFamily("Smith").addGiven("John");
|
||||
|
||||
ourCtx.setNarrativeGenerator(new DefaultThymeleafNarrativeGenerator());
|
||||
ourCtx.setNarrativeGenerator(new DefaultThymeleafNarrativeGenerator(ourCtx));
|
||||
|
||||
String output = ourCtx.newJsonParser().encodeResourceToString(p);
|
||||
ourLog.info(output);
|
||||
|
|
|
@ -1943,7 +1943,7 @@ public class XmlParserDstu3Test {
|
|||
Patient p = new Patient();
|
||||
p.addName().setFamily("Smith").addGiven("John");
|
||||
|
||||
ourCtx.setNarrativeGenerator(new DefaultThymeleafNarrativeGenerator());
|
||||
ourCtx.setNarrativeGenerator(new DefaultThymeleafNarrativeGenerator(ourCtx));
|
||||
|
||||
String output = ourCtx.newXmlParser().encodeResourceToString(p);
|
||||
ourLog.info(output);
|
||||
|
|
|
@ -0,0 +1,12 @@
|
|||
<th:block xmlns:th="http://www.thymeleaf.org">
|
||||
|
||||
<!--/* Try to pick the best title from what's available */-->
|
||||
<th:block th:if="${not resource.code.textElement.empty} or ${resource.code.coding.empty}" th:text="${resource.code.textElement.value}"/>
|
||||
<th:block th:if="${not resource.code.coding.empty} and ${resource.code.textElement.empty} and ${not resource.code.coding[0].displayElement.empty}" th:text="${resource.code.coding[0].display}"/>
|
||||
<th:block th:if="${not resource.code.coding.empty} and ${resource.code.textElement.empty} and ${resource.code.coding[0].displayElement.empty}" th:text="Untitled Diagnostic Report"/>
|
||||
|
||||
<th:block th:each="element : ${resource.entry}">
|
||||
<th:narrative th:name="diagnosticreport" th:element="${element.getResource()}"/>
|
||||
</th:block>
|
||||
|
||||
</th:block>
|
|
@ -0,0 +1,61 @@
|
|||
<th:block xmlns:th="http://www.thymeleaf.org">
|
||||
|
||||
<table>
|
||||
<tbody>
|
||||
<tr th:if="${not resource.statusElement.empty}">
|
||||
<td>Status</td>
|
||||
<td th:text="${resource.statusElement.value}"></td>
|
||||
</tr>
|
||||
<tr th:if="${not resource.issuedElement.empty}">
|
||||
<td>Issued</td>
|
||||
<td><th:narrative th:element="${resource.issuedElement}"/></td>
|
||||
</tr>
|
||||
<tr th:if="${not resource.conclusionElement.empty}">
|
||||
<td>Conclusion</td>
|
||||
<td><th:narrative th:element="${resource.conclusionElement}"/></td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
<table th:if="${not #lists.isEmpty(resource.result)} and ${resource.result[0].resource != null}">
|
||||
<thead>
|
||||
<tr>
|
||||
<td>Name</td>
|
||||
<td>Value</td>
|
||||
<td>Interpretation</td>
|
||||
<td>Reference Range</td>
|
||||
<td>Status</td>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<th:block th:each="result,rowStat : ${resource.result}">
|
||||
<tr>
|
||||
<td>
|
||||
<th:block th:if="${not result.resource.code.textElement.empty}" th:text="${result.resource.code.textElement.value}"/>
|
||||
<th:block th:if="${result.resource.code.textElement.empty} and ${not #lists.isEmpty(result.resource.code.coding)} and ${not result.resource.code.coding[0].empty} and ${not result.resource.code.coding[0].displayElement.empty}" th:text="${result.resource.code.coding[0].display}"/>
|
||||
<th:block th:if="${result.resource.code.textElement.empty} and ${not #lists.isEmpty(result.resource.code.coding)} and ${not result.resource.code.coding[0].empty} and ${result.resource.code.coding[0].displayElement.empty}" th:text="'?'"/>
|
||||
</td>
|
||||
<td><th:narrative th:element="${result.resource.value}"/></td>
|
||||
<td>
|
||||
<th:block th:if="${not result.resource.interpretation.textElement.empty}" th:text="${result.resource.interpretation.text}"/>
|
||||
<th:block th:if="${result.resource.interpretation.textElement.empty} and ${not result.resource.interpretation.coding.empty} and ${not result.resource.interpretation.coding[0].displayElement.empty}" th:text="${result.resource.interpretation.coding[0].display}"/>
|
||||
</td>
|
||||
<td>
|
||||
<th:block th:if="${not result.resource.referenceRange.empty} and ${not result.resource.referenceRange[0].empty}">
|
||||
<th:narrative th:element="${result.resource.referenceRange[0].low}"/>
|
||||
-
|
||||
<th:narrative th:element="${result.resource.referenceRange[0].high}"/>
|
||||
</th:block>
|
||||
</td>
|
||||
<td th:text="${result.resource.statusElement.value}"></td>
|
||||
</tr>
|
||||
|
||||
<tr th:if="${not result.resource.commentElement.empty}">
|
||||
<td th:text="${result.resource.commentElement.value}" colspan="5"></td>
|
||||
</tr>
|
||||
|
||||
</th:block>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
</th:block>
|
|
@ -0,0 +1,3 @@
|
|||
<th:block xmlns:th="http://www.thymeleaf.org">
|
||||
[[${resource.getValueAsString().replace("T", " ")}]]
|
||||
</th:block>
|
|
@ -0,0 +1,3 @@
|
|||
<th:block xmlns:th="http://www.thymeleaf.org">
|
||||
[[${resource.value}]]
|
||||
</th:block>
|
|
@ -0,0 +1,24 @@
|
|||
|
||||
|
||||
################################################
|
||||
# Resources
|
||||
################################################
|
||||
|
||||
composition.resourceType = Composition
|
||||
composition.contextPath = Composition.section.where(code.coding.system='2.16.840.1.113883.6.1' and code.coding.code='30954-2')
|
||||
composition.style = thymeleaf
|
||||
composition.narrative = classpath:narrative2/composition.html
|
||||
|
||||
diagnosticreport.style = thymeleaf
|
||||
diagnosticreport.narrative = classpath:narrative2/diagnosticreport.html
|
||||
|
||||
|
||||
################################################
|
||||
# Datatypes
|
||||
################################################
|
||||
|
||||
datetime.dataType = instant, dateTime, date
|
||||
datetime.narrative = classpath:narrative2/dt_datetime.html
|
||||
|
||||
quantity_valueonly.dataType = Quantity, SimpleQuantity
|
||||
quantity_valueonly.narrative = classpath:narrative2/dt_quantity.html
|
|
@ -1,2 +1,2 @@
|
|||
patient.class=org.hl7.fhir.dstu3.model.Patient
|
||||
patient.resourceType=Patient
|
||||
patient.narrative=classpath:/TestPatient.html
|
||||
|
|
|
@ -49,8 +49,10 @@ public abstract class BaseReference extends Type implements IBaseReference, ICom
|
|||
* a part of the FHIR "wire format" and is never transmitted or receieved inline, but this property
|
||||
* may be changed/accessed by parsers.
|
||||
*/
|
||||
public void setResource(IBaseResource theResource) {
|
||||
@Override
|
||||
public BaseReference setResource(IBaseResource theResource) {
|
||||
resource = theResource;
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
|
|
|
@ -64,7 +64,7 @@ public class BundleTypeInResponseHl7OrgTest {
|
|||
|
||||
ServletHandler proxyHandler = new ServletHandler();
|
||||
RestfulServer servlet = new RestfulServer(ourCtx);
|
||||
servlet.getFhirContext().setNarrativeGenerator(new DefaultThymeleafNarrativeGenerator());
|
||||
servlet.getFhirContext().setNarrativeGenerator(new DefaultThymeleafNarrativeGenerator(ourCtx));
|
||||
|
||||
servlet.setResourceProviders(patientProvider);
|
||||
ServletHolder servletHolder = new ServletHolder(servlet);
|
||||
|
|
|
@ -37,6 +37,7 @@ public abstract class BaseReference extends Type implements IBaseReference, ICom
|
|||
* a part of the FHIR "wire format" and is never transmitted or receieved inline, but this property
|
||||
* may be changed/accessed by parsers.
|
||||
*/
|
||||
@Override
|
||||
public IBaseResource getResource() {
|
||||
return resource;
|
||||
}
|
||||
|
@ -53,8 +54,10 @@ public abstract class BaseReference extends Type implements IBaseReference, ICom
|
|||
* a part of the FHIR "wire format" and is never transmitted or receieved inline, but this property
|
||||
* may be changed/accessed by parsers.
|
||||
*/
|
||||
public void setResource(IBaseResource theResource) {
|
||||
@Override
|
||||
public BaseReference setResource(IBaseResource theResource) {
|
||||
resource = theResource;
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
|
|
|
@ -145,6 +145,7 @@ The type is the Canonical URL of Resource Definition that is the type this refer
|
|||
/**
|
||||
* @return A reference to a location at which the other resource is found. The reference may be a relative reference, in which case it is relative to the service base URL, or an absolute URL that resolves to the location where the resource is found. The reference may be version specific or not. If the reference is not to a FHIR RESTful server, then it should be assumed to be version specific. Internal fragment references (start with '#') refer to contained resources.
|
||||
*/
|
||||
@Override
|
||||
public String getReference() {
|
||||
return this.reference == null ? null : this.reference.getValue();
|
||||
}
|
||||
|
|
|
@ -449,7 +449,7 @@ public class DateRangeParamR4Test {
|
|||
|
||||
ServletHandler proxyHandler = new ServletHandler();
|
||||
RestfulServer servlet = new RestfulServer(ourCtx);
|
||||
servlet.getFhirContext().setNarrativeGenerator(new DefaultThymeleafNarrativeGenerator());
|
||||
servlet.getFhirContext().setNarrativeGenerator(new DefaultThymeleafNarrativeGenerator(ourCtx));
|
||||
|
||||
servlet.setResourceProviders(patientProvider);
|
||||
ServletHolder servletHolder = new ServletHolder(servlet);
|
||||
|
|
|
@ -474,7 +474,7 @@ public class SearchSearchServerDstu1Test {
|
|||
|
||||
ServletHandler proxyHandler = new ServletHandler();
|
||||
ourServlet = new RestfulServer(ourCtx);
|
||||
ourServlet.getFhirContext().setNarrativeGenerator(new DefaultThymeleafNarrativeGenerator());
|
||||
ourServlet.getFhirContext().setNarrativeGenerator(new DefaultThymeleafNarrativeGenerator(ourCtx));
|
||||
ourServlet.setPagingProvider(new FifoMemoryPagingProvider(10).setDefaultPageSize(10));
|
||||
|
||||
ourServlet.setResourceProviders(patientProvider, new DummyObservationResourceProvider());
|
||||
|
|
|
@ -17,6 +17,7 @@ import org.apache.commons.lang3.builder.HashCodeBuilder;
|
|||
import org.apache.commons.lang3.time.DateUtils;
|
||||
import org.fhir.ucum.UcumService;
|
||||
import org.hl7.fhir.convertors.VersionConvertor_30_40;
|
||||
import org.hl7.fhir.dstu3.hapi.ctx.DefaultProfileValidationSupport;
|
||||
import org.hl7.fhir.dstu3.hapi.ctx.HapiWorkerContext;
|
||||
import org.hl7.fhir.dstu3.hapi.ctx.IValidationSupport;
|
||||
import org.hl7.fhir.dstu3.model.*;
|
||||
|
|
|
@ -3,7 +3,7 @@ package ca.uhn.fhir.validation;
|
|||
import ca.uhn.fhir.context.FhirContext;
|
||||
import ca.uhn.fhir.parser.IParser;
|
||||
import ca.uhn.fhir.util.TestUtil;
|
||||
import org.hl7.fhir.dstu3.hapi.validation.DefaultProfileValidationSupport;
|
||||
import org.hl7.fhir.dstu3.hapi.ctx.DefaultProfileValidationSupport;
|
||||
import org.hl7.fhir.dstu3.hapi.validation.FhirInstanceValidator;
|
||||
import org.hl7.fhir.dstu3.model.ActivityDefinition;
|
||||
import org.hl7.fhir.dstu3.model.ConceptMap;
|
||||
|
|
|
@ -4,7 +4,7 @@ import ca.uhn.fhir.context.FhirContext;
|
|||
import ca.uhn.fhir.parser.IParser;
|
||||
import org.apache.commons.io.IOUtils;
|
||||
import org.hl7.fhir.dstu3.hapi.ctx.HapiWorkerContext;
|
||||
import org.hl7.fhir.dstu3.hapi.validation.DefaultProfileValidationSupport;
|
||||
import org.hl7.fhir.dstu3.hapi.ctx.DefaultProfileValidationSupport;
|
||||
import org.hl7.fhir.dstu3.model.ElementDefinition;
|
||||
import org.hl7.fhir.dstu3.model.StructureDefinition;
|
||||
import org.hl7.fhir.exceptions.DefinitionException;
|
||||
|
|
|
@ -2,7 +2,7 @@ package org.hl7.fhir.dstu3.hapi.validation;
|
|||
|
||||
import static org.junit.Assert.*;
|
||||
|
||||
import org.hl7.fhir.dstu3.hapi.validation.DefaultProfileValidationSupport;
|
||||
import org.hl7.fhir.dstu3.hapi.ctx.DefaultProfileValidationSupport;
|
||||
import org.junit.AfterClass;
|
||||
import org.junit.Test;
|
||||
|
||||
|
|
|
@ -23,6 +23,7 @@ import ca.uhn.fhir.validation.SingleValidationMessage;
|
|||
import ca.uhn.fhir.validation.ValidationResult;
|
||||
import com.google.common.base.Charsets;
|
||||
import org.apache.commons.io.IOUtils;
|
||||
import org.hl7.fhir.dstu3.hapi.ctx.DefaultProfileValidationSupport;
|
||||
import org.hl7.fhir.dstu3.hapi.ctx.HapiWorkerContext;
|
||||
import org.hl7.fhir.dstu3.hapi.ctx.IValidationSupport;
|
||||
import org.hl7.fhir.dstu3.hapi.ctx.IValidationSupport.CodeValidationResult;
|
||||
|
@ -59,7 +60,6 @@ import org.hl7.fhir.dstu3.model.ValueSet.ConceptSetComponent;
|
|||
import org.hl7.fhir.dstu3.model.ValueSet.ValueSetExpansionComponent;
|
||||
import org.hl7.fhir.dstu3.utils.FHIRPathEngine;
|
||||
import org.hl7.fhir.instance.model.api.IBaseResource;
|
||||
import org.hl7.fhir.utilities.xhtml.XhtmlNode;
|
||||
import org.junit.AfterClass;
|
||||
import org.junit.Before;
|
||||
import org.junit.Ignore;
|
||||
|
|
|
@ -9,6 +9,7 @@ import ca.uhn.fhir.validation.SingleValidationMessage;
|
|||
import ca.uhn.fhir.validation.ValidationResult;
|
||||
|
||||
import org.hamcrest.Matchers;
|
||||
import org.hl7.fhir.dstu3.hapi.ctx.DefaultProfileValidationSupport;
|
||||
import org.hl7.fhir.dstu3.hapi.ctx.IValidationSupport;
|
||||
import org.hl7.fhir.dstu3.hapi.ctx.IValidationSupport.CodeValidationResult;
|
||||
import org.hl7.fhir.dstu3.model.*;
|
||||
|
|
|
@ -6,6 +6,7 @@ import ca.uhn.fhir.validation.FhirValidator;
|
|||
import ca.uhn.fhir.validation.ResultSeverityEnum;
|
||||
import ca.uhn.fhir.validation.ValidationResult;
|
||||
import org.hamcrest.Matchers;
|
||||
import org.hl7.fhir.dstu3.hapi.ctx.DefaultProfileValidationSupport;
|
||||
import org.hl7.fhir.dstu3.hapi.ctx.IValidationSupport;
|
||||
import org.hl7.fhir.dstu3.model.CodeableConcept;
|
||||
import org.hl7.fhir.dstu3.model.Coding;
|
||||
|
|
|
@ -14,6 +14,7 @@ import org.apache.commons.lang3.Validate;
|
|||
import org.hamcrest.core.StringContains;
|
||||
import org.hl7.fhir.dstu3.conformance.ProfileUtilities;
|
||||
import org.hl7.fhir.dstu3.context.IWorkerContext;
|
||||
import org.hl7.fhir.dstu3.hapi.ctx.DefaultProfileValidationSupport;
|
||||
import org.hl7.fhir.dstu3.hapi.ctx.HapiWorkerContext;
|
||||
import org.hl7.fhir.dstu3.model.*;
|
||||
import org.hl7.fhir.dstu3.model.Condition.ConditionClinicalStatus;
|
||||
|
|
|
@ -7,6 +7,7 @@ import java.util.*;
|
|||
import javax.annotation.Nullable;
|
||||
|
||||
import org.hl7.fhir.dstu3.context.IWorkerContext;
|
||||
import org.hl7.fhir.dstu3.hapi.ctx.DefaultProfileValidationSupport;
|
||||
import org.hl7.fhir.dstu3.hapi.ctx.HapiWorkerContext;
|
||||
import org.hl7.fhir.dstu3.model.*;
|
||||
import org.hl7.fhir.dstu3.utils.StructureMapUtilities;
|
||||
|
|
|
@ -3,7 +3,7 @@ package org.hl7.fhir.dstu3.utils;
|
|||
import ca.uhn.fhir.context.FhirContext;
|
||||
import ca.uhn.fhir.util.TestUtil;
|
||||
import org.hl7.fhir.dstu3.hapi.ctx.HapiWorkerContext;
|
||||
import org.hl7.fhir.dstu3.hapi.validation.DefaultProfileValidationSupport;
|
||||
import org.hl7.fhir.dstu3.hapi.ctx.DefaultProfileValidationSupport;
|
||||
import org.hl7.fhir.dstu3.model.*;
|
||||
import org.hl7.fhir.exceptions.FHIRException;
|
||||
import org.junit.AfterClass;
|
||||
|
|
|
@ -38,7 +38,7 @@ public class ExampleRestfulServlet extends RestfulServer {
|
|||
* Two resource providers are defined. Each one handles a specific
|
||||
* type of resource.
|
||||
*/
|
||||
List<IResourceProvider> providers = new ArrayList<IResourceProvider>();
|
||||
List<IResourceProvider> providers = new ArrayList<>();
|
||||
providers.add(new PatientResourceProvider());
|
||||
providers.add(new OrganizationResourceProvider());
|
||||
setResourceProviders(providers);
|
||||
|
@ -48,7 +48,7 @@ public class ExampleRestfulServlet extends RestfulServer {
|
|||
* but can be useful as it causes HAPI to generate narratives for
|
||||
* resources which don't otherwise have one.
|
||||
*/
|
||||
INarrativeGenerator narrativeGen = new DefaultThymeleafNarrativeGenerator();
|
||||
INarrativeGenerator narrativeGen = new DefaultThymeleafNarrativeGenerator(getFhirContext());
|
||||
getFhirContext().setNarrativeGenerator(narrativeGen);
|
||||
|
||||
/*
|
||||
|
|
Loading…
Reference in New Issue