Improve narrative templates for custom structures (#2537)

* Improve narrative templates for custom structures

* Add changelog

* Test fix
This commit is contained in:
James Agnew 2021-04-12 06:00:31 -04:00 committed by GitHub
parent 1bd585162d
commit bc4266d3d2
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
22 changed files with 483 additions and 178 deletions

View File

@ -38,6 +38,7 @@ import java.util.Set;
import java.util.TreeMap;
import java.util.TreeSet;
import org.apache.commons.lang3.builder.ToStringBuilder;
import org.hl7.fhir.instance.model.api.IAnyResource;
import org.hl7.fhir.instance.model.api.IBase;
import org.hl7.fhir.instance.model.api.IBaseBackboneElement;
@ -601,6 +602,11 @@ public abstract class BaseRuntimeElementCompositeDefinition<T extends IBase> ext
public boolean isFirstFieldInNewClass() {
return myFirstFieldInNewClass;
}
@Override
public String toString() {
return myField.getName();
}
}
}

View File

@ -902,21 +902,21 @@ public class FhirContext {
}
private BaseRuntimeElementDefinition<?> scanDatatype(final Class<? extends IElement> theResourceType) {
ArrayList<Class<? extends IElement>> resourceTypes = new ArrayList<Class<? extends IElement>>();
ArrayList<Class<? extends IElement>> resourceTypes = new ArrayList<>();
resourceTypes.add(theResourceType);
Map<Class<? extends IBase>, BaseRuntimeElementDefinition<?>> defs = scanResourceTypes(resourceTypes);
return defs.get(theResourceType);
}
private RuntimeResourceDefinition scanResourceType(final Class<? extends IBaseResource> theResourceType) {
ArrayList<Class<? extends IElement>> resourceTypes = new ArrayList<Class<? extends IElement>>();
ArrayList<Class<? extends IElement>> resourceTypes = new ArrayList<>();
resourceTypes.add(theResourceType);
Map<Class<? extends IBase>, BaseRuntimeElementDefinition<?>> defs = scanResourceTypes(resourceTypes);
return (RuntimeResourceDefinition) defs.get(theResourceType);
}
private synchronized Map<Class<? extends IBase>, BaseRuntimeElementDefinition<?>> scanResourceTypes(final Collection<Class<? extends IElement>> theResourceTypes) {
List<Class<? extends IBase>> typesToScan = new ArrayList<Class<? extends IBase>>();
List<Class<? extends IBase>> typesToScan = new ArrayList<>();
if (theResourceTypes != null) {
typesToScan.addAll(theResourceTypes);
}

View File

@ -49,13 +49,13 @@ public @interface Extension {
* by regional authorities or jurisdictional governments)
* </p>
*/
boolean definedLocally();
boolean definedLocally() default true;
/**
* Returns <code>true</code> if this extension is a <a
* href="http://www.hl7.org/implement/standards/fhir/extensibility.html#modifierExtension">modifier extension</a>
*/
boolean isModifier();
boolean isModifier() default false;
/**
* The URL associated with this extension

View File

@ -32,7 +32,7 @@ public interface INarrativeTemplate {
Set<String> getAppliesToResourceTypes();
Set<Class<? extends IBase>> getAppliesToResourceClasses();
Set<Class<? extends IBase>> getAppliesToClasses();
TemplateTypeEnum getTemplateType();

View File

@ -34,7 +34,7 @@ public class NarrativeTemplate implements INarrativeTemplate {
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 Set<Class<? extends IBase>> myAppliesToClasses = new HashSet<>();
private TemplateTypeEnum myTemplateType = TemplateTypeEnum.THYMELEAF;
private String myContextPath;
private String myTemplateName;
@ -79,12 +79,12 @@ public class NarrativeTemplate implements INarrativeTemplate {
}
@Override
public Set<Class<? extends IBase>> getAppliesToResourceClasses() {
return Collections.unmodifiableSet(myAppliesToResourceClasses);
public Set<Class<? extends IBase>> getAppliesToClasses() {
return Collections.unmodifiableSet(myAppliesToClasses);
}
void addAppliesToResourceClass(Class<? extends IBase> theAppliesToResourceClass) {
myAppliesToResourceClasses.add(theAppliesToResourceClass);
void addAppliesToClass(Class<? extends IBase> theAppliesToClass) {
myAppliesToClasses.add(theAppliesToClass);
}
@Override
@ -118,4 +118,5 @@ public class NarrativeTemplate implements INarrativeTemplate {
void addAppliesToDatatype(String theDataType) {
myAppliesToDataTypes.add(theDataType);
}
}

View File

@ -23,6 +23,7 @@ package ca.uhn.fhir.narrative2;
import ca.uhn.fhir.context.ConfigurationException;
import ca.uhn.fhir.context.FhirContext;
import ca.uhn.fhir.narrative.DefaultThymeleafNarrativeGenerator;
import ca.uhn.fhir.rest.server.exceptions.InternalErrorException;
import com.google.common.base.Charsets;
import org.apache.commons.io.IOUtils;
import org.apache.commons.lang3.StringUtils;
@ -32,8 +33,20 @@ import org.hl7.fhir.instance.model.api.IBaseResource;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.*;
import java.util.*;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.StringReader;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.EnumSet;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Properties;
import java.util.stream.Collectors;
import static org.apache.commons.lang3.StringUtils.isNotBlank;
@ -41,15 +54,17 @@ 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<String, List<NarrativeTemplate>> myStyleToResourceTypeToTemplate;
private final Map<String, List<NarrativeTemplate>> myStyleToDatatypeToTemplate;
private final Map<String, List<NarrativeTemplate>> myStyleToNameToTemplate;
private final Map<String, List<NarrativeTemplate>> myResourceTypeToTemplate;
private final Map<String, List<NarrativeTemplate>> myDatatypeToTemplate;
private final Map<String, List<NarrativeTemplate>> myNameToTemplate;
private final Map<String, List<NarrativeTemplate>> myClassToTemplate;
private final int myTemplateCount;
private NarrativeTemplateManifest(Collection<NarrativeTemplate> theTemplates) {
Map<String, List<NarrativeTemplate>> resourceTypeToTemplate = new HashMap<>();
Map<String, List<NarrativeTemplate>> datatypeToTemplate = new HashMap<>();
Map<String, List<NarrativeTemplate>> nameToTemplate = new HashMap<>();
Map<String, List<NarrativeTemplate>> classToTemplate = new HashMap<>();
for (NarrativeTemplate nextTemplate : theTemplates) {
nameToTemplate.computeIfAbsent(nextTemplate.getTemplateName(), t -> new ArrayList<>()).add(nextTemplate);
@ -59,12 +74,16 @@ public class NarrativeTemplateManifest implements INarrativeTemplateManifest {
for (String nextDataType : nextTemplate.getAppliesToDataTypes()) {
datatypeToTemplate.computeIfAbsent(nextDataType.toUpperCase(), t -> new ArrayList<>()).add(nextTemplate);
}
for (Class<? extends IBase> nextAppliesToClass : nextTemplate.getAppliesToClasses()) {
classToTemplate.computeIfAbsent(nextAppliesToClass.getName(), t -> new ArrayList<>()).add(nextTemplate);
}
}
myTemplateCount = theTemplates.size();
myStyleToNameToTemplate = makeImmutable(nameToTemplate);
myStyleToResourceTypeToTemplate = makeImmutable(resourceTypeToTemplate);
myStyleToDatatypeToTemplate = makeImmutable(datatypeToTemplate);
myClassToTemplate = makeImmutable(classToTemplate);
myNameToTemplate = makeImmutable(nameToTemplate);
myResourceTypeToTemplate = makeImmutable(resourceTypeToTemplate);
myDatatypeToTemplate = makeImmutable(datatypeToTemplate);
}
public int getNamedTemplateCount() {
@ -73,23 +92,27 @@ public class NarrativeTemplateManifest implements INarrativeTemplateManifest {
@Override
public List<INarrativeTemplate> getTemplateByResourceName(FhirContext theFhirContext, EnumSet<TemplateTypeEnum> theStyles, String theResourceName) {
return getFromMap(theStyles, theResourceName.toUpperCase(), myStyleToResourceTypeToTemplate);
return getFromMap(theStyles, theResourceName.toUpperCase(), myResourceTypeToTemplate);
}
@Override
public List<INarrativeTemplate> getTemplateByName(FhirContext theFhirContext, EnumSet<TemplateTypeEnum> theStyles, String theName) {
return getFromMap(theStyles, theName, myStyleToNameToTemplate);
return getFromMap(theStyles, theName, myNameToTemplate);
}
@Override
public List<INarrativeTemplate> getTemplateByElement(FhirContext theFhirContext, EnumSet<TemplateTypeEnum> theStyles, IBase theElement) {
if (theElement instanceof IBaseResource) {
String resourceName = theFhirContext.getResourceDefinition((IBaseResource) theElement).getName();
return getTemplateByResourceName(theFhirContext, theStyles, resourceName);
} else {
String datatypeName = theFhirContext.getElementDefinition(theElement.getClass()).getName();
return getFromMap(theStyles, datatypeName.toUpperCase(), myStyleToDatatypeToTemplate);
List<INarrativeTemplate> retVal = getFromMap(theStyles, theElement.getClass().getName(), myClassToTemplate);
if (retVal.isEmpty()) {
if (theElement instanceof IBaseResource) {
String resourceName = theFhirContext.getResourceDefinition((IBaseResource) theElement).getName();
retVal = getTemplateByResourceName(theFhirContext, theStyles, resourceName);
} else {
String datatypeName = theFhirContext.getElementDefinition(theElement.getClass()).getName();
retVal = getFromMap(theStyles, datatypeName.toUpperCase(), myDatatypeToTemplate);
}
}
return retVal;
}
public static NarrativeTemplateManifest forManifestFileLocation(String... thePropertyFilePaths) throws IOException {
@ -134,9 +157,16 @@ public class NarrativeTemplateManifest implements INarrativeTemplateManifest {
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")) {
if (nextKey.endsWith(".class")) {
String className = file.getProperty(nextKey);
if (isNotBlank(className)) {
try {
nextTemplate.addAppliesToClass((Class<? extends IBase>) Class.forName(className));
} catch (ClassNotFoundException theE) {
throw new InternalErrorException("Could not find class " + className + " declared in narative manifest");
}
}
} else if (nextKey.endsWith(".profile")) {
String profile = file.getProperty(nextKey);
if (isNotBlank(profile)) {
nextTemplate.addAppliesToProfile(profile);
@ -144,17 +174,17 @@ public class NarrativeTemplateManifest implements INarrativeTemplateManifest {
} 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));
.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));
.stream(dataType.split(","))
.map(t -> t.trim())
.filter(t -> isNotBlank(t))
.forEach(t -> nextTemplate.addAppliesToDatatype(t));
} else if (nextKey.endsWith(".style")) {
String templateTypeName = file.getProperty(nextKey).toUpperCase();
TemplateTypeEnum templateType = TemplateTypeEnum.valueOf(templateTypeName);
@ -171,9 +201,9 @@ public class NarrativeTemplateManifest implements INarrativeTemplateManifest {
} 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
+ " - the key must end in one of the expected extensions "
+ "'.profile', '.resourceType', '.dataType', '.style', '.contextPath', '.narrative', '.title'");
throw new ConfigurationException("Invalid property name: " + nextKey
+ " - the key must end in one of the expected extensions "
+ "'.profile', '.resourceType', '.dataType', '.style', '.contextPath', '.narrative', '.title'");
}
}
@ -210,10 +240,10 @@ public class NarrativeTemplateManifest implements INarrativeTemplateManifest {
private static <T> List<INarrativeTemplate> getFromMap(EnumSet<TemplateTypeEnum> theStyles, T theKey, Map<T, List<NarrativeTemplate>> theMap) {
return theMap
.getOrDefault(theKey, Collections.emptyList())
.stream()
.filter(t->theStyles.contains(t.getTemplateType()))
.collect(Collectors.toList());
.getOrDefault(theKey, Collections.emptyList())
.stream()
.filter(t -> theStyles.contains(t.getTemplateType()))
.collect(Collectors.toList());
}
private static <T> Map<T, List<NarrativeTemplate>> makeImmutable(Map<T, List<NarrativeTemplate>> theStyleToResourceTypeToTemplate) {

View File

@ -980,7 +980,12 @@ public abstract class BaseParser implements IParser {
myEncodeContext = theEncodeContext;
}
private void addParent(CompositeChildElement theParent, StringBuilder theB) {
@Override
public String toString() {
return myDef.getElementName();
}
private void addParent(CompositeChildElement theParent, StringBuilder theB) {
if (theParent != null) {
if (theParent.myResDef != null) {
theB.append(theParent.myResDef.getName());

View File

@ -379,7 +379,8 @@ public class JsonParser extends BaseParser implements IJsonLikeParser {
}
boolean haveWrittenExtensions = false;
for (CompositeChildElement nextChildElem : super.compositeChildIterator(theElement, theContainedResource, theParent, theEncodeContext)) {
Iterable<CompositeChildElement> compositeChildElements = super.compositeChildIterator(theElement, theContainedResource, theParent, theEncodeContext);
for (CompositeChildElement nextChildElem : compositeChildElements) {
BaseRuntimeChildDefinition nextChild = nextChildElem.getDef();

View File

@ -11,14 +11,7 @@
<tr th:each="issue : ${resource.issue}">
<td th:text="${issue.severityElement.value}" style="font-weight: bold;"></td>
<td th:text="${issue.location}"></td>
<th:block th:switch="${fhirVersion}">
<th:block th:case="'DSTU1'">
<td><pre th:text="${issue.details}"/></td>
</th:block>
<th:block th:case="*">
<td><pre th:text="${issue.diagnostics}"/></td>
</th:block>
</th:block>
<td><pre th:text="${issue.diagnostics}"/></td>
</tr>
</table>
</div>

View File

@ -0,0 +1,5 @@
---
type: add
issue: 2537
title: "It is now possible t create narrative generator templates that apply to any
custom strucures including custom extension structures."

View File

@ -7,12 +7,115 @@ This process is described on the [Profiles &amp; Extensions](./profiles_and_exte
There are situations however when you might want to create an entirely custom resource type. This feature should be used only if there is no other option, since it means you are creating a resource type that will not be interoperable with other FHIR implementations.
<p class="doc_info_bubble">
This is an advanced features and isn't needed for most uses of HAPI-FHIR. Feel free to skip this page.
This is an advanced features and isn't needed for most uses of HAPI FHIR. Feel free to skip this page. For a simpler way of interacting with resource extensions, see <a href="./profiles_and_extensions.html">Profiles &amp; Extensions</a>.
</p>
# Extending FHIR Resource Classes
The most elegant way of adding extensions to a resource is through the use of custom fields. The following example shows a custom type which extends the FHIR Patient resource definition through two extensions.
```java
{{snippet:classpath:/ca/uhn/hapi/fhir/docs/MyPatient.java|patientDef}}
```
Using this custom type is as simple as instantiating the type and working with the new fields.
```java
{{snippet:classpath:/ca/uhn/hapi/fhir/docs/MyPatientUse.java|patientUse}}
```
This example produces the following output:
```xml
<Patient xmlns="http://hl7.org/fhir">
<modifierExtension url="http://example.com/dontuse#importantDates">
<valueDateTime value="2010-01-02"/>
</modifierExtension>
<modifierExtension url="http://example.com/dontuse#importantDates">
<valueDateTime value="2014-01-26T11:11:11"/>
</modifierExtension>
<extension url="http://example.com/dontuse#petname">
<valueString value="Fido"/>
</extension>
<name>
<family value="Smith"/>
<given value="John"/>
<given value="Quincy"/>
<suffix value="Jr"/>
</name>
</Patient>
```
Parsing messages using your new custom type is equally simple. These types can also be used as method return types in clients and servers.
```java
{{snippet:classpath:/ca/uhn/hapi/fhir/docs/MyPatientUse.java|patientParse}}
```
# Using Custom Types in a Client
If you are using a client and wish to use a specific custom structure, you may simply use the custom structure as you would a build in HAPI type.
```java
{{snippet:classpath:/ca/uhn/hapi/fhir/docs/ExtensionsDstu3.java|customTypeClientSimple}}
```
You may also explicitly use custom types in searches and other operations which return resources.
```java
{{snippet:classpath:/ca/uhn/hapi/fhir/docs/ExtensionsDstu3.java|customTypeClientSearch}}
```
You can also explicitly declare a preferred response resource custom type. This is useful for some operations that do not otherwise declare their resource types in the method signature.
```java
{{snippet:classpath:/ca/uhn/hapi/fhir/docs/ExtensionsDstu3.java|customTypeClientSearch2}}
```
## Using Multiple Custom Types in a Client
Sometimes you may not know in advance exactly which type you will be receiving. For example, there are Patient resources which conform to several different profiles on a server and you aren't sure which profile you will get back for a specific read, you can declare the "primary" type for a given profile.
This is declared at the FhirContext level, and will apply to any clients created from this context (including clients created before the default was set).
```java
{{snippet:classpath:/ca/uhn/hapi/fhir/docs/ExtensionsDstu3.java|customTypeClientDeclared}}
```
# Using Custom Types in a Server
If you are using a client and wish to use a specific custom structure, you may simply use the custom structure as you would a build in HAPI type.
```java
{{snippet:classpath:/ca/uhn/hapi/fhir/docs/ExtensionsDstu3.java|customTypeClientSimple}}
```
# Custom Composite Extension Classes
The following example shows a resource containing a composite extension.
```java
{{snippet:classpath:/ca/uhn/hapi/fhir/docs/customtype/CustomCompositeExtension.java|resource}}
```
This could be used to create a resource such as the following:
```xml
<Patient xmlns="http://hl7.org/fhir">
<id value="123"/>
<extension url="http://acme.org/fooParent">
<extension url="http://acme.org/fooChildA">
<valueString value="ValueA"/>
</extension>
<extension url="http://acme.org/fooChildB">
<valueString value="ValueB"/>
</extension>
</extension>
</Patient>
```
# Custom Resource Structure
The following example shows a custom resource structure class:
The following example shows a custom resource structure class creating an entirely new resource type as opposed to simply extending an existing one. Note that this is allowable in FHIR, but is **highly discouraged** as they are by definition not good for interoperability.
```java
{{snippet:classpath:/ca/uhn/hapi/fhir/docs/customtype/CustomResource.java|resource}}

View File

@ -63,22 +63,32 @@ Then create a properties file which describes your templates. In this properties
The first (name.class) defines the class name of the resource to define a template for. The second (name.narrative) defines the path/classpath to the template file. The format of this path is `file:/path/foo.html` or `classpath:/com/classpath/foo.html`.
```properties
# Two property lines in the file per template
# Two property lines in the file per template. There are several forms you
# can use. This first form assigns a template type to a resource by
# resource name
practitioner.resourceType=Practitioner
practitioner.narrative=file:src/test/resources/narrative/Practitioner.html
practitioner.narrative=classpath:com/example/narrative/Practitioner.html
observation.class=ca.uhn.fhir.model.dstu.resource.Observation
observation.narrative=file:src/test/resources/narrative/Observation.html
# This second form assigns a template by class name. This can be used for
# HAPI FHIR built-in structures, or for custom structures as well.
observation.class=org.hl7.fhir.r4.model.Observation
observation.narrative=classpath:com/example/narrative/Observation.html
# etc...
# You can also assign a template based on profile ID (Resource.meta.profile)
vitalsigns.profile=http://hl7.org/fhir/StructureDefinition/vitalsigns
vitalsigns.narrative=classpath:com/example/narrative/Observation_Vitals.html
```
You may also override/define behaviour for datatypes. These datatype narrative definitions will be used as content within <code>th:narrative</code> blocks in resource templates. See the example resource template above for an example.
You may also override/define behaviour for datatypes and other structures. These datatype narrative definitions will be used as content within <code>th:narrative</code> blocks in resource templates. See the example resource template above for an example.
```properties
# datatypes use the same format as resources
humanname.resourceType=HumanNameDt
humanname.narrative=classpath:ca/uhn/fhir/narrative/HumanNameDt.html]]></source>
# You can create a template based on a type name
quantity.dataType=Quantity
quantity.narrative=classpath:com/example/narrative/Quantity.html
# Or by class name, which can be useful for custom datatypes and structures
custom_extension.class=com.example.model.MyCustomExtension
custom_extension.narrative=classpath:com/example/narrative/CustomExtension.html
```
Finally, use the [CustomThymeleafNarrativeGenerator](/hapi-fhir/apidocs/hapi-fhir-base/ca/uhn/fhir/narrative/CustomThymeleafNarrativeGenerator.html) and provide it to the FhirContext.

View File

@ -70,105 +70,8 @@ HAPI provides a few ways of accessing extension values in resources which are re
{{snippet:classpath:/ca/uhn/hapi/fhir/docs/ExtensionsDstu3.java|parseExtension}}
```
# Custom Resource Types
# Custom Resource Structures
The most elegant way of adding extensions to a resource is through the use of custom fields. The following example shows a custom type which extends the FHIR Patient resource definition through two extensions.
All of the examples on this page show how to work with the existing data model classes.
```java
{{snippet:classpath:/ca/uhn/hapi/fhir/docs/MyPatient.java|patientDef}}
```
Using this custom type is as simple as instantiating the type and working with the new fields.
```java
{{snippet:classpath:/ca/uhn/hapi/fhir/docs/MyPatientUse.java|patientUse}}
```
This example produces the following output:
```xml
<Patient xmlns="http://hl7.org/fhir">
<modifierExtension url="http://example.com/dontuse#importantDates">
<valueDateTime value="2010-01-02"/>
</modifierExtension>
<modifierExtension url="http://example.com/dontuse#importantDates">
<valueDateTime value="2014-01-26T11:11:11"/>
</modifierExtension>
<extension url="http://example.com/dontuse#petname">
<valueString value="Fido"/>
</extension>
<name>
<family value="Smith"/>
<given value="John"/>
<given value="Quincy"/>
<suffix value="Jr"/>
</name>
</Patient>
```
Parsing messages using your new custom type is equally simple. These types can also be used as method return types in clients and servers.
```java
{{snippet:classpath:/ca/uhn/hapi/fhir/docs/MyPatientUse.java|patientParse}}
```
## Using Custom Types in a Client
If you are using a client and wish to use a specific custom structure, you may simply use the custom structure as you would a build in HAPI type.
```java
{{snippet:classpath:/ca/uhn/hapi/fhir/docs/ExtensionsDstu3.java|customTypeClientSimple}}
```
You may also explicitly use custom types in searches and other operations which return resources.
```java
{{snippet:classpath:/ca/uhn/hapi/fhir/docs/ExtensionsDstu3.java|customTypeClientSearch}}
```
You can also explicitly declare a preferred response resource custom type. This is useful for some operations that do not otherwise declare their resource types in the method signature.
```java
{{snippet:classpath:/ca/uhn/hapi/fhir/docs/ExtensionsDstu3.java|customTypeClientSearch2}}
```
## Using Multiple Custom Types in a Client
Sometimes you may not know in advance exactly which type you will be receiving. For example, there are Patient resources which conform to several different profiles on a server and you aren't sure which profile you will get back for a specific read, you can declare the "primary" type for a given profile.
This is declared at the FhirContext level, and will apply to any clients created from this context (including clients created before the default was set).
```java
{{snippet:classpath:/ca/uhn/hapi/fhir/docs/ExtensionsDstu3.java|customTypeClientDeclared}}
```
## Using Custom Types in a Server
If you are using a client and wish to use a specific custom structure, you may simply use the custom structure as you would a build in HAPI type.
```java
{{snippet:classpath:/ca/uhn/hapi/fhir/docs/ExtensionsDstu3.java|customTypeClientSimple}}
```
## Custom Type Examples: Composite Extensions
The following example shows a resource containing a composite extension.
```java
{{snippet:classpath:/ca/uhn/hapi/fhir/docs/customtype/CustomCompositeExtension.java|resource}}
```
This could be used to create a resource such as the following:
```xml
<Patient xmlns="http://hl7.org/fhir">
<id value="123"/>
<extension url="http://acme.org/fooParent">
<extension url="http://acme.org/fooChildA">
<valueString value="ValueA"/>
</extension>
<extension url="http://acme.org/fooChildB">
<valueString value="ValueB"/>
</extension>
</extension>
</Patient>
```
This is a great way to work with extensions, and most HAPI FHIR applications use the techniques described on this page. However, there is a more advanced technique available as well, involving the creation of custom Java classes that extend the built-in classes to add statically bound extensions (as oppoed to the dynamically bound ones shown on this page). See [Custom Structures](./custom_structures.html) for more information.

View File

@ -0,0 +1,29 @@
package ca.uhn.fhir.narrative;
import ca.uhn.fhir.model.api.annotation.Child;
import ca.uhn.fhir.model.api.annotation.Extension;
import ca.uhn.fhir.model.api.annotation.ResourceDef;
import ca.uhn.fhir.util.ElementUtil;
import org.hl7.fhir.r4.model.Patient;
@ResourceDef(profile = "http://custom_patient")
public class CustomPatient extends Patient {
@Child(name = "favouritePizzaExtension")
@Extension(url = "http://example.com/favourite_pizza")
private FavouritePizzaExtension myFavouritePizza;
public FavouritePizzaExtension getFavouritePizza() {
return myFavouritePizza;
}
public void setFavouritePizza(FavouritePizzaExtension theFavouritePizza) {
myFavouritePizza = theFavouritePizza;
}
@Override
public boolean isEmpty() {
return super.isEmpty() && ElementUtil.isEmpty(myFavouritePizza);
}
}

View File

@ -0,0 +1,99 @@
package ca.uhn.fhir.narrative;
import ca.uhn.fhir.context.FhirContext;
import ca.uhn.fhir.util.TestUtil;
import org.hl7.fhir.r4.model.Practitioner;
import org.hl7.fhir.r4.model.Quantity;
import org.hl7.fhir.r4.model.StringType;
import org.junit.jupiter.api.AfterAll;
import org.junit.jupiter.api.Test;
import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.Matchers.containsString;
import static org.junit.jupiter.api.Assertions.assertEquals;
public class CustomThymeleafNarrativeGeneratorR4Test {
private static final org.slf4j.Logger ourLog = org.slf4j.LoggerFactory.getLogger(CustomThymeleafNarrativeGeneratorR4Test.class);
/** Don't use cached here since we modify the context */
private FhirContext myCtx = FhirContext.forR4();
/**
* Implement narrative for standard type
*/
@Test
public void testStandardType() {
CustomThymeleafNarrativeGenerator gen = new CustomThymeleafNarrativeGenerator("classpath:narrative/standardtypes_r4.properties");
myCtx.setNarrativeGenerator(gen);
Practitioner p = new Practitioner();
p.addIdentifier().setSystem("sys").setValue("val1");
p.addIdentifier().setSystem("sys").setValue("val2");
p.addAddress().addLine("line1").addLine("line2");
p.addName().setFamily("fam1").addGiven("given");
gen.populateResourceNarrative(myCtx, p);
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>"));
}
@Test
public void testCustomType() {
CustomPatient patient = new CustomPatient();
patient.setActive(true);
FavouritePizzaExtension parentExtension = new FavouritePizzaExtension();
parentExtension.setToppings(new StringType("Mushrooms, Onions"));
parentExtension.setSize(new Quantity(null, 14, "http://unitsofmeasure", "[in_i]", "Inches"));
patient.setFavouritePizza(parentExtension);
String output = myCtx.newJsonParser().setPrettyPrint(true).encodeResourceToString(patient);
ourLog.info("Encoded: {}", output);
String expectedEncoding = "{\n" +
" \"resourceType\": \"Patient\",\n" +
" \"meta\": {\n" +
" \"profile\": [ \"http://custom_patient\" ]\n" +
" },\n" +
" \"extension\": [ {\n" +
" \"url\": \"http://example.com/favourite_pizza\",\n" +
" \"extension\": [ {\n" +
" \"url\": \"toppings\",\n" +
" \"valueString\": \"Mushrooms, Onions\"\n" +
" }, {\n" +
" \"url\": \"size\",\n" +
" \"valueQuantity\": {\n" +
" \"value\": 14,\n" +
" \"unit\": \"Inches\",\n" +
" \"system\": \"http://unitsofmeasure\",\n" +
" \"code\": \"[in_i]\"\n" +
" }\n" +
" } ]\n" +
" } ],\n" +
" \"active\": true\n" +
"}";
assertEquals(expectedEncoding, output);
CustomThymeleafNarrativeGenerator gen = new CustomThymeleafNarrativeGenerator("classpath:narrative/customtypes_r4.properties");
myCtx.setNarrativeGenerator(gen);
gen.populateResourceNarrative(myCtx, patient);
String actual = patient.getText().getDiv().getValueAsString();
ourLog.info(actual);
String expected = "<div xmlns=\"http://www.w3.org/1999/xhtml\"><h1>CustomPatient</h1><div><div><h1>Favourite Pizza</h1> Toppings: <span>Mushrooms, Onions</span> Size: <span>14</span></div></div></div>";
assertEquals(expected, actual);
}
@AfterAll
public static void afterClassClearContext() {
TestUtil.clearAllStaticFieldsForUnitTest();
}
}

View File

@ -1,6 +1,7 @@
package ca.uhn.fhir.narrative;
import ca.uhn.fhir.context.FhirContext;
import ca.uhn.fhir.context.FhirVersionEnum;
import ca.uhn.fhir.parser.DataFormatException;
import ca.uhn.fhir.util.TestUtil;
import org.hamcrest.core.StringContains;
@ -21,7 +22,7 @@ import static org.junit.jupiter.api.Assertions.assertTrue;
public class DefaultThymeleafNarrativeGeneratorR4Test {
private static final org.slf4j.Logger ourLog = org.slf4j.LoggerFactory.getLogger(DefaultThymeleafNarrativeGeneratorR4Test.class);
private static FhirContext ourCtx = FhirContext.forR4();
private FhirContext myCtx = FhirContext.forCached(FhirVersionEnum.R4);
private DefaultThymeleafNarrativeGenerator myGen;
@BeforeEach
@ -29,7 +30,7 @@ public class DefaultThymeleafNarrativeGeneratorR4Test {
myGen = new DefaultThymeleafNarrativeGenerator();
myGen.setUseHapiServerConformanceNarrative(true);
ourCtx.setNarrativeGenerator(myGen);
myCtx.setNarrativeGenerator(myGen);
}
@Test
@ -44,7 +45,7 @@ public class DefaultThymeleafNarrativeGeneratorR4Test {
value.setBirthDate(new Date());
myGen.populateResourceNarrative(ourCtx, value);
myGen.populateResourceNarrative(myCtx, value);
String output = value.getText().getDiv().getValueAsString();
ourLog.info(output);
assertThat(output, StringContains.containsString("<div class=\"hapiHeaderText\">joe john <b>BLOW </b></div>"));
@ -60,7 +61,7 @@ public class DefaultThymeleafNarrativeGeneratorR4Test {
value.addResult().setReference("Observation/2");
value.addResult().setReference("Observation/3");
myGen.populateResourceNarrative(ourCtx, value);
myGen.populateResourceNarrative(myCtx, value);
String output = value.getText().getDiv().getValueAsString();
ourLog.info(output);
@ -82,13 +83,13 @@ public class DefaultThymeleafNarrativeGeneratorR4Test {
"</OperationOutcome>";
//@formatter:on
OperationOutcome oo = ourCtx.newXmlParser().parseResource(OperationOutcome.class, parse);
OperationOutcome oo = myCtx.newXmlParser().parseResource(OperationOutcome.class, parse);
// String output = gen.generateTitle(oo);
// ourLog.info(output);
// assertEquals("Operation Outcome (2 issues)", output);
myGen.populateResourceNarrative(ourCtx, oo);
myGen.populateResourceNarrative(myCtx, oo);
String output = oo.getText().getDiv().getValueAsString();
ourLog.info(output);
@ -126,7 +127,7 @@ public class DefaultThymeleafNarrativeGeneratorR4Test {
value.addResult().setResource(obs);
}
myGen.populateResourceNarrative(ourCtx, value);
myGen.populateResourceNarrative(myCtx, value);
String output = value.getText().getDiv().getValueAsString();
ourLog.info(output);
@ -189,8 +190,8 @@ public class DefaultThymeleafNarrativeGeneratorR4Test {
" }";
DiagnosticReport value = ourCtx.newJsonParser().parseResource(DiagnosticReport.class, input);
myGen.populateResourceNarrative(ourCtx, value);
DiagnosticReport value = myCtx.newJsonParser().parseResource(DiagnosticReport.class, input);
myGen.populateResourceNarrative(myCtx, value);
String output = value.getText().getDiv().getValueAsString();
ourLog.info(output);
@ -210,7 +211,7 @@ public class DefaultThymeleafNarrativeGeneratorR4Test {
mp.setStatus(MedicationRequestStatus.ACTIVE);
mp.setAuthoredOnElement(new DateTimeType("2014-09-01"));
myGen.populateResourceNarrative(ourCtx, mp);
myGen.populateResourceNarrative(myCtx, mp);
String output = mp.getText().getDiv().getValueAsString();
assertTrue(output.contains("ciprofloaxin"), "Expected medication name of ciprofloaxin within narrative: " + output);
@ -223,7 +224,7 @@ public class DefaultThymeleafNarrativeGeneratorR4Test {
Medication med = new Medication();
med.getCode().setText("ciproflaxin");
myGen.populateResourceNarrative(ourCtx, med);
myGen.populateResourceNarrative(myCtx, med);
String output = med.getText().getDiv().getValueAsString();
assertThat(output, containsString("ciproflaxin"));

View File

@ -0,0 +1,45 @@
package ca.uhn.fhir.narrative;
import ca.uhn.fhir.model.api.annotation.Block;
import ca.uhn.fhir.model.api.annotation.Child;
import ca.uhn.fhir.util.ElementUtil;
import org.hl7.fhir.r4.model.BackboneElement;
import org.hl7.fhir.r4.model.Quantity;
import org.hl7.fhir.r4.model.StringType;
@Block
public class FavouritePizzaExtension extends BackboneElement {
@Child(name = "childBazExtension")
@ca.uhn.fhir.model.api.annotation.Extension(url = "toppings")
private StringType myToppings;
@Child(name = "childBarExtension")
@ca.uhn.fhir.model.api.annotation.Extension(url = "size")
private Quantity mySize;
@Override
public BackboneElement copy() {
return null;
}
@Override
public boolean isEmpty() {
return super.isEmpty() && ElementUtil.isEmpty(myToppings, mySize);
}
public StringType getToppings() {
return myToppings;
}
public void setToppings(StringType theToppings) {
myToppings = theToppings;
}
public Quantity getSize() {
return mySize;
}
public void setSize(Quantity theSize) {
mySize = theSize;
}
}

View File

@ -0,0 +1,6 @@
<div>
<h1>CustomPatient</h1>
<div th:narrative="${resource.favouritePizza}"></div>
</div>

View File

@ -0,0 +1,10 @@
<div>
<h1>Favourite Pizza</h1>
Toppings:
<span th:text="${resource.toppings.valueAsString}"/>
Size:
<span th:text="${resource.size.value}"/>
</div>

View File

@ -0,0 +1,15 @@
# Each resource to be defined has a pair or properties.
#
# The first (name.class) defines the class name of the
# resource to define a template for
#
# The second (name.narrative) defines the path/classpath to the
# template file.
# Format is file:/path/foo.html or classpath:/com/classpath/foo.html
#
custompatient.class=ca.uhn.fhir.narrative.CustomPatient
custompatient.narrative=classpath:narrative/customtypes_CustomPatientR4.html
favourite_pizza.class=ca.uhn.fhir.narrative.FavouritePizzaExtension
favourite_pizza.narrative=classpath:narrative/customtypes_FavouritePizzaExtensionR4.html

View File

@ -0,0 +1,24 @@
<div>
<!--
Normal Thymeleaf tags apply. Here, we loop through each of the
identifiers that the practitioner has, and create a DIV tag for
each one, with the text "Identifier: [value]"
-->
<div th:each="identifier : ${resource.identifier}" th:text="'Identifier: ' + ${identifier.value}"></div>
<!--
HAPI also defines a custom tag attribute, th:narrative="value",
which is used to render a datatype into more HTML. In the example
below, the value of ${resource.name} (which is the name property
of the Practitioner resource, a HumanName datatype instance)
is rendered, and its contents placed in a DIV tag. That DIV tag is
then given a .nameElement CSS style.
-->
<h1>Name</h1>
<div th:narrative="${resource.nameFirstRep}" class="nameElement"></div>
<h1>Address</h1>
<div th:narrative="${resource.addressFirstRep}"></div>
</div>

View File

@ -0,0 +1,19 @@
# Each resource to be defined has a pair or properties.
#
# The first (name.class) defines the class name of the
# resource to define a template for
#
# The second (name.narrative) defines the path/classpath to the
# template file.
# Format is file:/path/foo.html or classpath:/com/classpath/foo.html
#
practitioner.resourceType=Practitioner
practitioner.narrative=classpath:narrative/standardtypes_PractitionerR4.html
# You may also override/define behaviour for datatypes
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