Improve narrative templates for custom structures (#2537)
* Improve narrative templates for custom structures * Add changelog * Test fix
This commit is contained in:
parent
1bd585162d
commit
bc4266d3d2
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -32,7 +32,7 @@ public interface INarrativeTemplate {
|
|||
|
||||
Set<String> getAppliesToResourceTypes();
|
||||
|
||||
Set<Class<? extends IBase>> getAppliesToResourceClasses();
|
||||
Set<Class<? extends IBase>> getAppliesToClasses();
|
||||
|
||||
TemplateTypeEnum getTemplateType();
|
||||
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -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,24 +92,28 @@ 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) {
|
||||
List<INarrativeTemplate> retVal = getFromMap(theStyles, theElement.getClass().getName(), myClassToTemplate);
|
||||
if (retVal.isEmpty()) {
|
||||
if (theElement instanceof IBaseResource) {
|
||||
String resourceName = theFhirContext.getResourceDefinition((IBaseResource) theElement).getName();
|
||||
return getTemplateByResourceName(theFhirContext, theStyles, resourceName);
|
||||
retVal = getTemplateByResourceName(theFhirContext, theStyles, resourceName);
|
||||
} else {
|
||||
String datatypeName = theFhirContext.getElementDefinition(theElement.getClass()).getName();
|
||||
return getFromMap(theStyles, datatypeName.toUpperCase(), myStyleToDatatypeToTemplate);
|
||||
retVal = getFromMap(theStyles, datatypeName.toUpperCase(), myDatatypeToTemplate);
|
||||
}
|
||||
}
|
||||
return retVal;
|
||||
}
|
||||
|
||||
public static NarrativeTemplateManifest forManifestFileLocation(String... thePropertyFilePaths) throws IOException {
|
||||
return forManifestFileLocation(Arrays.asList(thePropertyFilePaths));
|
||||
|
@ -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);
|
||||
|
|
|
@ -980,6 +980,11 @@ public abstract class BaseParser implements IParser {
|
|||
myEncodeContext = theEncodeContext;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return myDef.getElementName();
|
||||
}
|
||||
|
||||
private void addParent(CompositeChildElement theParent, StringBuilder theB) {
|
||||
if (theParent != null) {
|
||||
if (theParent.myResDef != null) {
|
||||
|
|
|
@ -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();
|
||||
|
||||
|
|
|
@ -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>
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
|
|
|
@ -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."
|
|
@ -7,12 +7,115 @@ This process is described on the [Profiles & 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 & 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}}
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
||||
}
|
|
@ -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();
|
||||
}
|
||||
}
|
|
@ -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"));
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,6 @@
|
|||
<div>
|
||||
<h1>CustomPatient</h1>
|
||||
|
||||
<div th:narrative="${resource.favouritePizza}"></div>
|
||||
|
||||
</div>
|
|
@ -0,0 +1,10 @@
|
|||
<div>
|
||||
<h1>Favourite Pizza</h1>
|
||||
|
||||
Toppings:
|
||||
<span th:text="${resource.toppings.valueAsString}"/>
|
||||
|
||||
Size:
|
||||
<span th:text="${resource.size.value}"/>
|
||||
|
||||
</div>
|
|
@ -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
|
|
@ -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>
|
|
@ -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
|
Loading…
Reference in New Issue