Work on narrative generator

This commit is contained in:
James 2014-07-03 18:02:14 -04:00
parent c811c164b5
commit ca0929df07
31 changed files with 500 additions and 119 deletions

View File

@ -60,9 +60,11 @@ public class RuntimeChildAny extends RuntimeChildChoiceDefinition {
if (o1res && o2res) {
return theO1.getSimpleName().compareTo(theO2.getSimpleName());
} else if (o1res) {
return 1;
}else {
return -1;
} else if (o1res == false && o2res == false) {
return 0;
}else {
return 1;
}
}});

View File

@ -221,7 +221,12 @@ public class Bundle extends BaseBundle /* implements IElement */{
RuntimeResourceDefinition def = theContext.getResourceDefinition(theResource);
if (theResource.getId() != null && StringUtils.isNotBlank(theResource.getId().getValue())) {
entry.getTitle().setValue(def.getName() + " " + theResource.getId().getValue());
String title = ResourceMetadataKeyEnum.TITLE.get(theResource);
if (title != null) {
entry.getTitle().setValue(title);
} else {
entry.getTitle().setValue(def.getName() + " " + theResource.getId().getValue());
}
StringBuilder b = new StringBuilder();
b.append(theServerBase);

View File

@ -25,6 +25,8 @@ import static org.apache.commons.lang3.StringUtils.*;
import java.util.Date;
import java.util.Map;
import org.apache.commons.lang3.StringUtils;
import ca.uhn.fhir.model.primitive.IdDt;
import ca.uhn.fhir.model.primitive.InstantDt;
import ca.uhn.fhir.rest.server.exceptions.InternalErrorException;
@ -129,6 +131,26 @@ public abstract class ResourceMetadataKeyEnum<T> {
}
};
/**
* If present and populated with a string (as an instance of {@link String}),
* this value contains the title for this resource, as supplied in any bundles containing the
* resource.
* <p>
* Values for this key are of type <b>{@link String}</b>
* </p>
*/
public static final ResourceMetadataKeyEnum<String> TITLE = new ResourceMetadataKeyEnum<String>("TITLE") {
@Override
public String get(IResource theResource) {
return getStringFromMetadataOrNullIfNone(theResource.getResourceMetadata(), TITLE);
}
@Override
public void put(IResource theResource, String theObject) {
theResource.getResourceMetadata().put(TITLE, theObject);
}
};
/**
* The value for this key is the bundle entry <b>Updated</b> time. This is
@ -182,7 +204,6 @@ public abstract class ResourceMetadataKeyEnum<T> {
}
@Override
public boolean equals(Object obj) {
if (this == obj)
@ -200,6 +221,8 @@ public abstract class ResourceMetadataKeyEnum<T> {
return true;
}
public abstract T get(IResource theResource);
@Override
@ -210,6 +233,10 @@ public abstract class ResourceMetadataKeyEnum<T> {
return result;
}
private String name() {
return myValue;
}
public abstract void put(IResource theResource, T theObject);
@Override
@ -217,31 +244,27 @@ public abstract class ResourceMetadataKeyEnum<T> {
return myValue;
}
private String name() {
return myValue;
}
private static IdDt getIdFromMetadataOrNullIfNone(Map<ResourceMetadataKeyEnum<?>, Object> theResourceMetadata, ResourceMetadataKeyEnum<?> theKey) {
Object retValObj = theResourceMetadata.get(theKey);
if (retValObj == null) {
private static IdDt getIdFromMetadataOrNullIfNone(Map<ResourceMetadataKeyEnum<?>, Object> theResourceMetadata, ResourceMetadataKeyEnum<?> theKey) {
Object retValObj = theResourceMetadata.get(theKey);
if (retValObj == null) {
return null;
} else if (retValObj instanceof String) {
if (isNotBlank((String) retValObj)) {
return new IdDt((String) retValObj);
} else {
return null;
} else if (retValObj instanceof String) {
if (isNotBlank((String) retValObj)) {
return new IdDt((String) retValObj);
} else {
return null;
}
} else if (retValObj instanceof IdDt) {
if (((IdDt) retValObj).isEmpty()) {
return null;
} else {
return (IdDt) retValObj;
}
} else if (retValObj instanceof Number) {
return new IdDt(((Number)retValObj).toString());
}
throw new InternalErrorException("Found an object of type '" + retValObj.getClass().getCanonicalName() + "' in resource metadata for key " + theKey.name() + " - Expected " + IdDt.class.getCanonicalName());
} else if (retValObj instanceof IdDt) {
if (((IdDt) retValObj).isEmpty()) {
return null;
} else {
return (IdDt) retValObj;
}
} else if (retValObj instanceof Number) {
return new IdDt(((Number)retValObj).toString());
}
throw new InternalErrorException("Found an object of type '" + retValObj.getClass().getCanonicalName() + "' in resource metadata for key " + theKey.name() + " - Expected " + IdDt.class.getCanonicalName());
}
private static InstantDt getInstantFromMetadataOrNullIfNone(Map<ResourceMetadataKeyEnum<?>, Object> theResourceMetadata, ResourceMetadataKeyEnum<InstantDt> theKey) {
Object retValObj = theResourceMetadata.get(theKey);
@ -259,4 +282,18 @@ public abstract class ResourceMetadataKeyEnum<T> {
throw new InternalErrorException("Found an object of type '" + retValObj.getClass().getCanonicalName() + "' in resource metadata for key " + theKey.name() + " - Expected " + InstantDt.class.getCanonicalName());
}
private static String getStringFromMetadataOrNullIfNone(Map<ResourceMetadataKeyEnum<?>, Object> theResourceMetadata, ResourceMetadataKeyEnum<String> theKey) {
Object retValObj = theResourceMetadata.get(theKey);
if (retValObj == null) {
return null;
} else if (retValObj instanceof String) {
if (StringUtils.isBlank(((String) retValObj))) {
return null;
} else {
return (String) retValObj;
}
}
throw new InternalErrorException("Found an object of type '" + retValObj.getClass().getCanonicalName() + "' in resource metadata for key " + theKey.name() + " - Expected " + String.class.getCanonicalName());
}
}

View File

@ -408,8 +408,10 @@ public class CodingDt
public String getValueAsQueryToken() {
if (org.apache.commons.lang3.StringUtils.isNotBlank(getSystem().getValueAsString())) {
return getSystem().getValueAsString() + '|' + getCode().getValueAsString();
} else {
} else if (getSystem().getValue()==null) {
return getCode().getValueAsString();
} else {
return '|' + getCode().getValueAsString();
}
}

View File

@ -414,6 +414,8 @@ public class IdentifierDt
public String getValueAsQueryToken() {
if (org.apache.commons.lang3.StringUtils.isNotBlank(getSystem().getValueAsString())) {
return getSystem().getValueAsString() + '|' + getValue().getValueAsString();
} else if (getSystem().getValue() == null) {
return getValue().getValueAsString();
} else {
return '|' + getValue().getValueAsString();
}

View File

@ -69,17 +69,100 @@ public abstract class BaseThymeleafNarrativeGenerator implements INarrativeGener
private boolean myIgnoreMissingTemplates = true;
private TemplateEngine myProfileTemplateEngine;
private HashMap<String, String> myProfileToNarrativeName;
private HashMap<Class<?>, String> myClassToNarrativeName;
private TemplateEngine myTitleTemplateEngine;
private HashMap<String, String> myProfileToName;
private HashMap<Class<?>, String> myClassToName;
private HashMap<String, String> myNameToNarrativeTemplate;
private boolean myApplyDefaultDatatypeTemplates=true;
private boolean myApplyDefaultDatatypeTemplates = true;
private volatile boolean myInitialized;
private HashMap<String, String> myNameToTitleTemplate;
@Override
public NarrativeDt generateNarrative(IResource theResource) {
return generateNarrative(null, theResource);
}
@Override
public String generateTitle(IResource theResource) {
return generateTitle(null, theResource);
}
@Override
public String generateTitle(String theProfile, IResource theResource) {
if (!myInitialized) {
initialize();
}
String name = null;
if (StringUtils.isNotBlank(theProfile)) {
name = myProfileToName.get(theProfile);
}
if (name == null) {
name = myClassToName.get(theResource.getClass());
}
if (name == null) {
if (myIgnoreMissingTemplates) {
ourLog.debug("No title template available for profile: {}", theProfile);
return null;
} else {
throw new DataFormatException("No title template for class " + theResource.getClass().getCanonicalName());
}
}
try {
Context context = new Context();
context.setVariable("resource", theResource);
String result = myTitleTemplateEngine.process(name, context);
StringBuilder b = new StringBuilder();
boolean inTag = false;
for (int i = 0; i < result.length(); i++) {
char nextChar = result.charAt(i);
char prevChar = i > 0 ? result.charAt(i - 1) : '\n';
if (nextChar == '<') {
inTag = true;
continue;
} else if (inTag) {
if (nextChar == '>') {
inTag = false;
}
continue;
} else if (nextChar <= ' ') {
if (prevChar <= ' ' || prevChar == '>') {
continue;
} else {
b.append(' ');
}
} else {
b.append(nextChar);
}
}
while (b.length() > 0 && b.charAt(b.length()-1) == ' ') {
b.setLength(b.length() - 1);
}
result = b.toString();
if (result.startsWith("<") && result.contains(">")) {
result = result.substring(result.indexOf('>') + 1);
}
if (result.endsWith(">") && result.contains("<")) {
result = result.substring(0, result.lastIndexOf('<'));
}
return result;
} catch (Exception e) {
if (myIgnoreFailures) {
ourLog.error("Failed to generate narrative", e);
return "No title available - Error: " + e.getMessage();
} else {
throw new DataFormatException(e);
}
}
}
@Override
public NarrativeDt generateNarrative(String theProfile, IResource theResource) {
if (!myInitialized) {
@ -88,10 +171,10 @@ public abstract class BaseThymeleafNarrativeGenerator implements INarrativeGener
String name = null;
if (StringUtils.isNotBlank(theProfile)) {
name = myProfileToNarrativeName.get(theProfile);
name = myProfileToName.get(theProfile);
}
if (name == null) {
name = myClassToNarrativeName.get(theResource.getClass());
name = myClassToName.get(theResource.getClass());
}
if (name == null) {
@ -131,9 +214,10 @@ public abstract class BaseThymeleafNarrativeGenerator implements INarrativeGener
if (myInitialized) {
return;
}
myProfileToNarrativeName = new HashMap<String, String>();
myClassToNarrativeName = new HashMap<Class<?>, String>();
myProfileToName = new HashMap<String, String>();
myClassToName = new HashMap<Class<?>, String>();
myNameToNarrativeTemplate = new HashMap<String, String>();
myNameToTitleTemplate = new HashMap<String, String>();
List<String> propFileName = getPropertyFile();
@ -160,6 +244,15 @@ public abstract class BaseThymeleafNarrativeGenerator implements INarrativeGener
myProfileTemplateEngine.setDialect(dialect);
myProfileTemplateEngine.initialize();
}
{
myTitleTemplateEngine = new TemplateEngine();
TemplateResolver resolver = new TemplateResolver();
resolver.setResourceResolver(new TitleResourceResolver());
myTitleTemplateEngine.setTemplateResolver(resolver);
StandardDialect dialect = new StandardDialect();
myTitleTemplateEngine.setDialect(dialect);
myTitleTemplateEngine.initialize();
}
myInitialized = true;
}
@ -167,12 +260,9 @@ public abstract class BaseThymeleafNarrativeGenerator implements INarrativeGener
protected abstract List<String> getPropertyFile();
/**
* If set to <code>true</code> (which is the default), most whitespace will
* be trimmed from the generated narrative before it is returned.
* 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.
* 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() {
@ -180,30 +270,24 @@ public abstract class BaseThymeleafNarrativeGenerator implements INarrativeGener
}
/**
* 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.
* 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
* If set to true, will return an empty narrative block for any profiles where no template is available
*/
public boolean isIgnoreMissingTemplates() {
return myIgnoreMissingTemplates;
}
/**
* If set to <code>true</code> (which is the default), most whitespace will
* be trimmed from the generated narrative before it is returned.
* 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.
* 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) {
@ -211,18 +295,15 @@ public abstract class BaseThymeleafNarrativeGenerator implements INarrativeGener
}
/**
* 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.
* 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
* 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;
@ -245,13 +326,22 @@ public abstract class BaseThymeleafNarrativeGenerator implements INarrativeGener
String narrativePropName = name + ".narrative";
String narrativeName = file.getProperty(narrativePropName);
if (isBlank(narrativeName)) {
throw new ConfigurationException("Found property '" + nextKey + "' but no corresponding property '" + narrativePropName + "' in file " + propFileName);
String titlePropName = name + ".title";
String titleName = file.getProperty(titlePropName);
if (isBlank(narrativeName) && isBlank(titleName)) {
throw new ConfigurationException("Found property '" + nextKey + "' but no corresponding property '" + narrativePropName + "' or '" + titlePropName + "' in file " + propFileName);
}
String narrative = IOUtils.toString(loadResource(narrativeName));
myProfileToNarrativeName.put(file.getProperty(nextKey), name);
myNameToNarrativeTemplate.put(name, narrative);
myProfileToName.put(file.getProperty(nextKey), name);
if (StringUtils.isNotBlank(narrativeName)) {
String narrative = IOUtils.toString(loadResource(narrativeName));
myNameToNarrativeTemplate.put(name, narrative);
}
if (StringUtils.isNotBlank(titleName)) {
String title = IOUtils.toString(loadResource(titleName));
myNameToTitleTemplate.put(name, title);
}
} else if (nextKey.endsWith(".class")) {
@ -262,9 +352,9 @@ public abstract class BaseThymeleafNarrativeGenerator implements INarrativeGener
String className = file.getProperty(nextKey);
Class<?> dtClass;
Class<?> clazz;
try {
dtClass = Class.forName(className);
clazz = Class.forName(className);
} catch (ClassNotFoundException e) {
ourLog.warn("Unknown datatype class '{}' identified in narrative file {}", name, propFileName);
continue;
@ -272,16 +362,27 @@ public abstract class BaseThymeleafNarrativeGenerator implements INarrativeGener
String narrativePropName = name + ".narrative";
String narrativeName = file.getProperty(narrativePropName);
if (isBlank(narrativeName)) {
throw new ConfigurationException("Found property '" + nextKey + "' but no corresponding property '" + narrativePropName + "' in file " + propFileName);
String titlePropName = name + ".title";
String titleName = file.getProperty(titlePropName);
if (isBlank(narrativeName) && isBlank(titleName)) {
throw new ConfigurationException("Found property '" + nextKey + "' but no corresponding property '" + narrativePropName + "' or '" + titlePropName + "' in file " + propFileName);
}
String narrative = IOUtils.toString(loadResource(narrativeName));
myClassToNarrativeName.put(dtClass, name);
myNameToNarrativeTemplate.put(name, narrative);
myClassToName.put(clazz, name);
if (StringUtils.isNotBlank(narrativeName)) {
String narrative = IOUtils.toString(loadResource(narrativeName));
myNameToNarrativeTemplate.put(name, narrative);
}
if (StringUtils.isNotBlank(titleName)) {
String title = IOUtils.toString(loadResource(titleName));
myNameToTitleTemplate.put(name, title);
}
} else if (nextKey.endsWith(".narrative")) {
continue;
} else if (nextKey.endsWith(".title")) {
continue;
} else {
throw new ConfigurationException("Invalid property name: " + nextKey);
}
@ -379,14 +480,13 @@ public abstract class BaseThymeleafNarrativeGenerator implements INarrativeGener
final IStandardExpression expression = expressionParser.parseExpression(configuration, theArguments, attributeValue);
final Object value = expression.execute(configuration, theArguments);
theElement.removeAttribute(theAttributeName);
theElement.clearChildren();
Context context = new Context();
context.setVariable("resource", value);
String name = myClassToNarrativeName.get(value.getClass());
String name = myClassToName.get(value.getClass());
if (name == null) {
if (myIgnoreMissingTemplates) {
ourLog.debug("No narrative template available for type: {}", value.getClass());
@ -438,4 +538,21 @@ public abstract class BaseThymeleafNarrativeGenerator implements INarrativeGener
return new ReaderInputStream(new StringReader(template));
}
}
private final class TitleResourceResolver implements IResourceResolver {
@Override
public String getName() {
return getClass().getCanonicalName();
}
@Override
public InputStream getResourceAsStream(TemplateProcessingParameters theTemplateProcessingParameters, String theName) {
String template = myNameToTitleTemplate.get(theName);
if (template == null) {
ourLog.info("No narative template for resource profile: {}", theName);
return new ReaderInputStream(new StringReader(""));
}
return new ReaderInputStream(new StringReader(template));
}
}
}

View File

@ -64,4 +64,6 @@ public class DefaultThymeleafNarrativeGenerator extends BaseThymeleafNarrativeGe
return myUseHapiServerConformanceNarrative;
}
}

View File

@ -30,4 +30,8 @@ public interface INarrativeGenerator {
NarrativeDt generateNarrative(IResource theResource);
String generateTitle(IResource theResource);
String generateTitle(String theProfile, IResource theResource);
}

View File

@ -222,6 +222,7 @@ public class JsonParser extends BaseParser implements IParser {
eventWriter.writeEnd(); // entry array
eventWriter.writeEnd();
eventWriter.flush();
eventWriter.close();
}
@ -506,6 +507,7 @@ public class JsonParser extends BaseParser implements IParser {
RuntimeResourceDefinition resDef = myContext.getResourceDefinition(theResource);
encodeResourceToJsonStreamWriter(resDef, theResource, eventWriter, null,false);
eventWriter.flush();
eventWriter.close();
}
@Override
@ -536,6 +538,7 @@ public class JsonParser extends BaseParser implements IParser {
eventWriter.writeEnd();
eventWriter.flush();
eventWriter.close();
}
/**

View File

@ -417,6 +417,9 @@ class ParserState<T> {
if (myEntry.getUpdated().isEmpty() == false) {
ResourceMetadataKeyEnum.UPDATED.put(myEntry.getResource(), myEntry.getUpdated());
}
ResourceMetadataKeyEnum.TITLE.put(myEntry.getResource(), myEntry.getTitle().getValue());
if (myEntry.getCategories().isEmpty() == false) {
TagList tagList = new TagList();
for (Tag next : myEntry.getCategories()) {
@ -608,7 +611,6 @@ class ParserState<T> {
myPreResourceState = thePreResourceState;
}
@SuppressWarnings("unused")
public void attributeValue(String theName, String theValue) throws DataFormatException {
// ignore by default
}
@ -617,7 +619,6 @@ class ParserState<T> {
// ignore by default
}
@SuppressWarnings("unused")
public void enteringNewElement(String theNamespaceURI, String theLocalPart) throws DataFormatException {
// ignore by default
}
@ -657,7 +658,7 @@ class ParserState<T> {
myStack = theState;
}
public void string(@SuppressWarnings("unused") String theData) {
public void string(String theData) {
// ignore by default
}
@ -665,7 +666,7 @@ class ParserState<T> {
// allow an implementor to override
}
public void xmlEvent(@SuppressWarnings("unused") XMLEvent theNextEvent) {
public void xmlEvent(XMLEvent theNextEvent) {
// ignore
}

View File

@ -64,4 +64,10 @@ public class IdentifierListParam implements IQueryParameterOr {
}
}
public void addIdentifier(IdentifierDt theIdentifierDt) {
if (theIdentifierDt != null && theIdentifierDt.isEmpty() == false) {
getIdentifiers().add(theIdentifierDt);
}
}
}

View File

@ -160,6 +160,8 @@ public class SearchParameter extends BaseQueryParameter {
myParamType = SearchParamTypeEnum.QUANTITY;
} else if (ReferenceParam.class.isAssignableFrom(type)) {
myParamType = SearchParamTypeEnum.REFERENCE;
} else if (IdentifierListParam.class.isAssignableFrom(type)) {
myParamType = SearchParamTypeEnum.TOKEN;
} else {
throw new ConfigurationException("Unknown search parameter type: " + type);
}

View File

@ -45,6 +45,7 @@ import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.apache.commons.io.IOUtils;
import org.apache.commons.lang3.StringUtils;
import org.apache.commons.lang3.Validate;
import org.apache.commons.lang3.exception.ExceptionUtils;
@ -557,7 +558,7 @@ public class RestfulServer extends HttpServlet {
statusCode=((BaseServerResponseException) e).getStatusCode();
issue.getDetails().setValue(e.getMessage());
} else {
ourLog.warn("Failure during REST processing: {}", e.toString());
ourLog.error("Failure during REST processing: {}"+ e.toString(), e);
issue.getDetails().setValue(e.toString() + "\n\n" + ExceptionUtils.getStackTrace(e));
}
@ -779,6 +780,14 @@ public class RestfulServer extends HttpServlet {
bundle.getLinkSelf().setValue(theCompleteUrl);
for (IResource next : theResult) {
if (theContext.getNarrativeGenerator() != null) {
String title = theContext.getNarrativeGenerator().generateTitle(next);
if (StringUtils.isNotBlank(title)) {
ResourceMetadataKeyEnum.TITLE.put(next, title);
}
}
bundle.addResource(next, theContext, theServerBase);
}
@ -1019,6 +1028,7 @@ public class RestfulServer extends HttpServlet {
RestfulServer.getNewParser(theServer.getFhirContext(), theResponseEncoding, thePrettyPrint, theNarrativeMode).encodeBundleToWriter(bundle, writer);
}
} finally {
writer.flush();
writer.close();
}
}
@ -1093,6 +1103,7 @@ public class RestfulServer extends HttpServlet {
RestfulServer.getNewParser(theServer.getFhirContext(), theResponseEncoding, thePrettyPrint, theNarrativeMode).encodeResourceToWriter(theResource, writer);
}
} finally {
writer.flush();
writer.close();
}
}

View File

@ -32,6 +32,9 @@ quantity.narrative=classpath:ca/uhn/fhir/narrative/QuantityDt.html
patient.class=ca.uhn.fhir.model.dstu.resource.Patient
patient.narrative=classpath:ca/uhn/fhir/narrative/Patient.html
patient.title=classpath:ca/uhn/fhir/narrative/title/Patient.html
diagnosticreport.class=ca.uhn.fhir.model.dstu.resource.DiagnosticReport
diagnosticreport.narrative=classpath:ca/uhn/fhir/narrative/DiagnosticReport.html
diagnosticreport.title=classpath:ca/uhn/fhir/narrative/title/DiagnosticReport.html

View File

@ -0,0 +1,9 @@
<div>
<th:block th:if="${not resource.name.text.empty}" th:text="${resource.name.text.value}"/>
<th:block th:if=" ${resource.name.text.empty} and ${not resource.name.codingFirstRep.display.empty}" th:text="${resource.name.codingFirstRep.display}"/>
<th:block th:if= "${resource.name.text.empty} and ${resource.name.codingFirstRep.display.empty}" th:text="Untitled Diagnostic Report"/>
<th:block th:if="${not resource.status.empty}" th:text="' - ' + ${resource.status.value}"/>
<th:block th:text="' - ' + ${resource.result.size} + ' observations'"/>
</div>

View File

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

View File

@ -0,0 +1,41 @@
package ca.uhn.fhir.model.primitive;
import static org.junit.Assert.*;
import org.junit.Test;
import ca.uhn.fhir.model.dstu.composite.CodingDt;
public class CodingDtTest {
@Test
public void testTokenWithPipeInValue() {
CodingDt dt = new CodingDt();
dt.setValueAsQueryToken(null, "a|b|c");
assertEquals("a", dt.getSystem().getValueAsString());
assertEquals("b|c", dt.getCode().getValue());
assertEquals("a|b|c", dt.getValueAsQueryToken());
}
@Test
public void testTokenWithPipeInValueAndNoSystem() {
CodingDt dt = new CodingDt();
dt.setValueAsQueryToken(null, "|b|c");
assertEquals("", dt.getSystem().getValueAsString());
assertEquals("b|c", dt.getCode().getValue());
assertEquals("|b|c", dt.getValueAsQueryToken());
}
@Test
public void testTokenNoSystem() {
CodingDt dt = new CodingDt();
dt.setValueAsQueryToken(null, "c");
assertEquals(null, dt.getSystem().getValueAsString());
assertEquals("c", dt.getCode().getValue());
assertEquals("c", dt.getValueAsQueryToken());
}
}

View File

@ -28,4 +28,14 @@ public class IdentifierDtTest {
assertEquals("|b|c", dt.getValueAsQueryToken());
}
@Test
public void testTokenNoSystem() {
IdentifierDt dt = new IdentifierDt();
dt.setValueAsQueryToken(null, "c");
assertEquals(null, dt.getSystem().getValueAsString());
assertEquals("c", dt.getValue().getValue());
assertEquals("c", dt.getValueAsQueryToken());
}
}

View File

@ -46,9 +46,12 @@ public class DefaultThymeleafNarrativeGeneratorTest {
value.setBirthDate(new Date(), TemporalPrecisionEnum.DAY);
String output = gen.generateNarrative("http://hl7.org/fhir/profiles/Patient", value).getDiv().getValueAsString();
String output = gen.generateNarrative(value).getDiv().getValueAsString();
assertThat(output, StringContains.containsString("<div class=\"hapiHeaderText\"> joe john <b>BLOW </b></div>"));
ourLog.info(output);
String title = gen.generateTitle(value);
assertEquals("joe john BLOW (123456)", title);
ourLog.info(title);
}
@Test
@ -105,6 +108,11 @@ public class DefaultThymeleafNarrativeGeneratorTest {
ourLog.info(output);
assertThat(output, StringContains.containsString("<div class=\"hapiHeaderText\"> Some Diagnostic Report </div>"));
String title = gen.generateTitle(value);
ourLog.info(title);
assertEquals("Some Diagnostic Report - final - 2 observations", title);
// Now try it with the parser
FhirContext context = new FhirContext();

View File

@ -23,9 +23,8 @@ import ca.uhn.fhir.context.FhirContext;
import ca.uhn.fhir.model.api.Bundle;
import ca.uhn.fhir.model.api.IResource;
import ca.uhn.fhir.model.dstu.resource.Patient;
import ca.uhn.fhir.model.primitive.IdDt;
import ca.uhn.fhir.narrative.DefaultThymeleafNarrativeGenerator;
import ca.uhn.fhir.rest.annotation.OptionalParam;
import ca.uhn.fhir.rest.annotation.RequiredParam;
import ca.uhn.fhir.rest.annotation.Search;
import ca.uhn.fhir.rest.param.StringParam;
import ca.uhn.fhir.testutil.RandomServerPortProvider;
@ -36,7 +35,6 @@ import ca.uhn.fhir.testutil.RandomServerPortProvider;
public class SearchTest {
private static CloseableHttpClient ourClient;
private static final org.slf4j.Logger ourLog = org.slf4j.LoggerFactory.getLogger(SearchTest.class);
private static int ourPort;
private static Server ourServer;
private static FhirContext ourCtx = new FhirContext();
@ -46,12 +44,14 @@ public class SearchTest {
HttpGet httpGet = new HttpGet("http://localhost:" + ourPort + "/Patient?_id=aaa");
HttpResponse status = ourClient.execute(httpGet);
String responseContent = IOUtils.toString(status.getEntity().getContent());
IOUtils.closeQuietly(status.getEntity().getContent());
assertEquals(200, status.getStatusLine().getStatusCode());
Bundle bundle = ourCtx.newXmlParser().parseBundle(responseContent);
assertEquals(1, bundle.getEntries().size());
Patient p = bundle.getResources(Patient.class).get(0);
assertEquals("idaaa", p.getNameFirstRep().getFamilyAsSingleString());
assertEquals("IDAAA (identifier123)", bundle.getEntries().get(0).getTitle().getValue());
}
@AfterClass
@ -68,6 +68,8 @@ public class SearchTest {
ServletHandler proxyHandler = new ServletHandler();
RestfulServer servlet = new RestfulServer();
servlet.getFhirContext().setNarrativeGenerator(new DefaultThymeleafNarrativeGenerator());
servlet.setResourceProviders(patientProvider);
ServletHolder servletHolder = new ServletHolder(servlet);
proxyHandler.addServletWithMapping(servletHolder, "/*");
@ -92,6 +94,7 @@ public class SearchTest {
Patient patient = new Patient();
patient.setId("1");
patient.addIdentifier("system", "identifier123");
patient.addName().addFamily("id"+theParam.getValue());
retVal.add(patient);
return retVal;

View File

@ -747,6 +747,9 @@ public abstract class BaseFhirDao {
}
}
String title = ResourceMetadataKeyEnum.TITLE.get(theResource);
theEntity.setTitle(title);
}
protected ResourceTable toEntity(IResource theResource) {
@ -784,6 +787,10 @@ public abstract class BaseFhirDao {
retVal.getResourceMetadata().put(ResourceMetadataKeyEnum.PUBLISHED, theEntity.getPublished());
retVal.getResourceMetadata().put(ResourceMetadataKeyEnum.UPDATED, theEntity.getUpdated());
if (theEntity.getTitle()!=null) {
ResourceMetadataKeyEnum.TITLE.put(retVal, theEntity.getTitle());
}
if (theEntity.getDeleted()!=null) {
ResourceMetadataKeyEnum.DELETED_AT.put(retVal, new InstantDt(theEntity.getDeleted()));
}

View File

@ -431,7 +431,10 @@ public class FhirResourceDao<T extends IResource> extends BaseFhirDao implements
ArrayList<Predicate> singleCodePredicates = (new ArrayList<Predicate>());
if (StringUtils.isNotBlank(system)) {
singleCodePredicates.add(builder.equal(from.get("mySystem"), system));
} else if (system == null) {
// don't check the system
} else {
// If the system is "", we only match on null systems
singleCodePredicates.add(builder.isNull(from.get("mySystem")));
}
if (StringUtils.isNotBlank(code)) {

View File

@ -35,6 +35,17 @@ public class SearchParameterMap extends HashMap<String, List<List<IQueryParamete
}
}
public void add(String theName, IQueryParameterOr theOr) {
if (theOr == null) {
return;
}
if (!containsKey(theName)) {
put(theName, new ArrayList<List<IQueryParameterType>>());
}
get(theName).add(theOr.getValuesAsQueryTokens());
}
public void add(String theName, IQueryParameterType theParam) {
if (theParam == null) {
return;

View File

@ -17,6 +17,8 @@ import ca.uhn.fhir.model.primitive.InstantDt;
@MappedSuperclass
public abstract class BaseHasResource {
private static final int MAX_TITLE_LENGTH = 100;
@Column(name = "RES_DELETED_AT", nullable = true)
@Temporal(TemporalType.TIMESTAMP)
private Date myDeleted;
@ -33,6 +35,9 @@ public abstract class BaseHasResource {
@Lob()
private byte[] myResource;
@Column(name = "RES_TITLE", nullable = true, length = MAX_TITLE_LENGTH)
private String myTitle;
@Temporal(TemporalType.TIMESTAMP)
@Column(name = "RES_UPDATED", nullable = false)
private Date myUpdated;
@ -61,6 +66,10 @@ public abstract class BaseHasResource {
public abstract Collection<? extends BaseTag> getTags();
public String getTitle() {
return myTitle;
}
public InstantDt getUpdated() {
return new InstantDt(myUpdated);
}
@ -87,6 +96,10 @@ public abstract class BaseHasResource {
myResource = theResource;
}
public void setTitle(String theTitle) {
myTitle = theTitle;
}
public void setUpdated(Date theUpdated) {
myUpdated = theUpdated;
}

View File

@ -8,7 +8,10 @@ import org.apache.commons.lang3.StringUtils;
@Entity
@Table(name = "HFJ_SPIDX_TOKEN" /* , indexes = { @Index(name = "IDX_SP_TOKEN", columnList = "SP_SYSTEM,SP_VALUE") } */)
@org.hibernate.annotations.Table(appliesTo = "HFJ_SPIDX_TOKEN", indexes = { @org.hibernate.annotations.Index(name = "IDX_SP_TOKEN", columnNames = { "RES_TYPE", "SP_NAME", "SP_SYSTEM", "SP_VALUE" }) })
@org.hibernate.annotations.Table(appliesTo = "HFJ_SPIDX_TOKEN", indexes = {
@org.hibernate.annotations.Index(name = "IDX_SP_TOKEN", columnNames = { "RES_TYPE", "SP_NAME", "SP_SYSTEM", "SP_VALUE" }),
@org.hibernate.annotations.Index(name = "IDX_SP_TOKEN_UNQUAL", columnNames = { "RES_TYPE", "SP_NAME", "SP_VALUE" })
})
public class ResourceIndexedSearchParamToken extends BaseResourceIndexedSearchParam {
private static final long serialVersionUID = 1L;

View File

@ -252,6 +252,7 @@ public class ResourceTable extends BaseHasResource implements Serializable {
retVal.setResourceType(myResourceType);
retVal.setVersion(myVersion);
retVal.setTitle(getTitle());
retVal.setPublished(getPublished());
retVal.setUpdated(getUpdated());
retVal.setEncoding(getEncoding());

View File

@ -39,7 +39,9 @@ import ca.uhn.fhir.model.primitive.InstantDt;
import ca.uhn.fhir.model.primitive.StringDt;
import ca.uhn.fhir.rest.api.MethodOutcome;
import ca.uhn.fhir.rest.client.exceptions.FhirClientConnectionException;
import ca.uhn.fhir.rest.gclient.IQuery;
import ca.uhn.fhir.rest.param.DateRangeParam;
import ca.uhn.fhir.rest.param.IdentifierListParam;
import ca.uhn.fhir.rest.param.QualifiedDateParam;
import ca.uhn.fhir.rest.param.ReferenceParam;
import ca.uhn.fhir.rest.param.StringParam;
@ -122,6 +124,50 @@ public class FhirResourceDaoTest {
assertEquals(o1id.toUnqualifiedVersionless(), p1.getManagingOrganization().getReference().toUnqualifiedVersionless());
}
@Test
public void testSearchTokenParam() {
Patient patient = new Patient();
patient.addIdentifier("urn:system", "testSearchTokenParam001");
patient.addName().addFamily("Tester").addGiven("testSearchTokenParam1");
ourPatientDao.create(patient);
patient = new Patient();
patient.addIdentifier("urn:system", "testSearchTokenParam002");
patient.addName().addFamily("Tester").addGiven("testSearchTokenParam2");
ourPatientDao.create(patient);
{
SearchParameterMap map = new SearchParameterMap();
map.add(Patient.SP_IDENTIFIER, new IdentifierDt("urn:system", "testSearchTokenParam001"));
IBundleProvider retrieved = ourPatientDao.search(map);
assertEquals(1, retrieved.size());
}
{
SearchParameterMap map = new SearchParameterMap();
map.add(Patient.SP_IDENTIFIER, new IdentifierDt(null, "testSearchTokenParam001"));
IBundleProvider retrieved = ourPatientDao.search(map);
assertEquals(1, retrieved.size());
}
{
SearchParameterMap map = new SearchParameterMap();
IdentifierListParam listParam = new IdentifierListParam();
listParam.addIdentifier(new IdentifierDt("urn:system", "testSearchTokenParam001"));
listParam.addIdentifier(new IdentifierDt("urn:system", "testSearchTokenParam002"));
map.add(Patient.SP_IDENTIFIER, listParam);
IBundleProvider retrieved = ourPatientDao.search(map);
assertEquals(2, retrieved.size());
}
{
SearchParameterMap map = new SearchParameterMap();
IdentifierListParam listParam = new IdentifierListParam();
listParam.addIdentifier(new IdentifierDt(null, "testSearchTokenParam001"));
listParam.addIdentifier(new IdentifierDt("urn:system", "testSearchTokenParam002"));
map.add(Patient.SP_IDENTIFIER, listParam);
IBundleProvider retrieved = ourPatientDao.search(map);
assertEquals(2, retrieved.size());
}
}
@Test
public void testIdParam() {
Patient patient = new Patient();
@ -360,6 +406,7 @@ public class FhirResourceDaoTest {
Patient patient = new Patient();
patient.addIdentifier("urn:system", "001");
patient.addName().addFamily("testSearchNameParam01Fam").addGiven("testSearchNameParam01Giv");
ResourceMetadataKeyEnum.TITLE.put(patient, "P1TITLE");
id1 = ourPatientDao.create(patient).getId();
}
{
@ -374,6 +421,7 @@ public class FhirResourceDaoTest {
List<Patient> patients = toList(ourPatientDao.search(params));
assertEquals(1, patients.size());
assertEquals(id1.getIdPart(), patients.get(0).getId().getIdPart());
assertEquals("P1TITLE", ResourceMetadataKeyEnum.TITLE.get(patients.get(0)));
// Given name shouldn't return for family param
params = new HashMap<String, IQueryParameterType>();

View File

@ -1,35 +1,52 @@
<!DOCTYPE html>
<html lang="en">
<div th:fragment="banner" class="well">
<th:block th:if="${serverEntry.key} == 'home'">
<p>
This is the home for the FHIR test server operated by
<a href="http://uhn.ca">University Health Network</a>. This server
(and the testing application you are currently using to access it)
is entirely built using
<a href="https://github.com/jamesagnew/hapi-fhir">HAPI-FHIR</a>,
a 100% open-source Java implementation of the
<a href="http://hl7.org/implement/standards/fhir/">FHIR specification</a>.
</p>
<p>
Here are some things you might wish to try:
</p>
<ul>
<li>
View a
<a href="http://fhirtest.uhn.ca/search?serverId=home&amp;encoding=json&amp;pretty=true&amp;resource=Patient&amp;param.0.type=string&amp;param.0.name=_id&amp;param.0.0=&amp;resource-search-limit=">list of patients</a>
on this server.
</li>
<li>
Construct a
<a href="http://fhirtest.uhn.ca/resource?serverId=home&amp;encoding=json&amp;pretty=true&amp;resource=Patient">search query</a>
on this server.
</li>
<li>
Access a
<a href="http://fhirtest.uhn.ca/home?serverId=furore">different server</a>
(use the <b>Server</b> menu at the top of the page to see a list of public FHIR servers)
</li>
</ul>
</th:block>
<th:block th:if="${serverEntry.key} != 'home'">
<p>
You are accessing the public FHIR server
<b th:text="${baseName}"/>. This server is hosted elsewhere on the internet
but is being accessed using
</p>
</th:block>
<p>
This is the home for the FHIR test server operated by
<a href="http://uhn.ca">University Health Network</a>.
</p>
<p>
<b style="color: red;">This is not a production server!</b>
<b style="color: red;">
<span class="glyphicon glyphicon-warning-sign/>
This is not a production server!
</b>
Do not store any information here that contains personal health information
or otherwise confidential information. This server will be regularly purged
or any other confidential information. This server will be regularly purged
and reloaded with fixed test data.
</p>
<p>
Here are some things you might wish to try:
</p>
<ul>
<li>
View a
<a href="http://fhirtest.uhn.ca/search?serverId=home&amp;encoding=json&amp;pretty=true&amp;resource=Patient&amp;param.0.type=string&amp;param.0.name=_id&amp;param.0.0=&amp;resource-search-limit=">list of patients</a>
on this server.
</li>
<li>
Construct a
<a href="http://fhirtest.uhn.ca/resource?serverId=home&amp;encoding=json&amp;pretty=true&amp;resource=Patient">search query</a>
on this server.
</li>
<li>
Access a
<a href="http://fhirtest.uhn.ca/home?serverId=furore">different server</a>
(use the <b>Server</b> menu at the top of the page to see a list of public FHIR servers)
</li>
</ul>
</div>
</html>

View File

@ -11,6 +11,7 @@
<bean class="ca.uhn.fhir.to.TesterConfig">
<property name="servers">
<list>
<value>test, TEST, http://uhnvesb01d.uhn.on.ca:25180/uhn-fhir-service-1.2/</value>
<value>home , Localhost Server , http://localhost:8887/fhir/context </value>
<value>hi , Health Intersections , http://fhir.healthintersections.com.au/open</value>
<value>furore , Spark - Furore Reference Server , http://spark.furore.com/fhir</value>

View File

@ -34,7 +34,7 @@ public class ${className}ResourceProvider extends JpaResourceProvider<${classNam
#if (${param.type} == 'string' )
StringParam the${param.nameCapitalized},
#elseif (${param.type} == 'token' )
IdentifierDt the${param.nameCapitalized},
IdentifierListParam the${param.nameCapitalized},
#elseif (${param.type} == 'date' )
DateRangeParam the${param.nameCapitalized},
#elseif (${param.type} == 'quantity' )