From bc4266d3d2588df7e104e86cdfb30b985cf1cfb9 Mon Sep 17 00:00:00 2001 From: James Agnew Date: Mon, 12 Apr 2021 06:00:31 -0400 Subject: [PATCH] Improve narrative templates for custom structures (#2537) * Improve narrative templates for custom structures * Add changelog * Test fix --- ...BaseRuntimeElementCompositeDefinition.java | 6 + .../java/ca/uhn/fhir/context/FhirContext.java | 6 +- .../fhir/model/api/annotation/Extension.java | 4 +- .../fhir/narrative2/INarrativeTemplate.java | 2 +- .../fhir/narrative2/NarrativeTemplate.java | 11 +- .../narrative2/NarrativeTemplateManifest.java | 98 ++++++++++------ .../java/ca/uhn/fhir/parser/BaseParser.java | 7 +- .../java/ca/uhn/fhir/parser/JsonParser.java | 3 +- .../uhn/fhir/narrative/OperationOutcome.html | 9 +- ...-improve_narrative_for_custom_structs.yaml | 5 + .../hapi/fhir/docs/model/custom_structures.md | 109 +++++++++++++++++- .../fhir/docs/model/narrative_generation.md | 28 +++-- .../docs/model/profiles_and_extensions.md | 103 +---------------- .../ca/uhn/fhir/narrative/CustomPatient.java | 29 +++++ ...stomThymeleafNarrativeGeneratorR4Test.java | 99 ++++++++++++++++ ...aultThymeleafNarrativeGeneratorR4Test.java | 23 ++-- .../narrative/FavouritePizzaExtension.java | 45 ++++++++ .../customtypes_CustomPatientR4.html | 6 + ...customtypes_FavouritePizzaExtensionR4.html | 10 ++ .../narrative/customtypes_r4.properties | 15 +++ .../standardtypes_PractitionerR4.html | 24 ++++ .../narrative/standardtypes_r4.properties | 19 +++ 22 files changed, 483 insertions(+), 178 deletions(-) create mode 100644 hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/changelog/5_4_0/2537-improve_narrative_for_custom_structs.yaml create mode 100644 hapi-fhir-structures-r4/src/test/java/ca/uhn/fhir/narrative/CustomPatient.java create mode 100644 hapi-fhir-structures-r4/src/test/java/ca/uhn/fhir/narrative/CustomThymeleafNarrativeGeneratorR4Test.java create mode 100644 hapi-fhir-structures-r4/src/test/java/ca/uhn/fhir/narrative/FavouritePizzaExtension.java create mode 100644 hapi-fhir-structures-r4/src/test/resources/narrative/customtypes_CustomPatientR4.html create mode 100644 hapi-fhir-structures-r4/src/test/resources/narrative/customtypes_FavouritePizzaExtensionR4.html create mode 100644 hapi-fhir-structures-r4/src/test/resources/narrative/customtypes_r4.properties create mode 100644 hapi-fhir-structures-r4/src/test/resources/narrative/standardtypes_PractitionerR4.html create mode 100644 hapi-fhir-structures-r4/src/test/resources/narrative/standardtypes_r4.properties diff --git a/hapi-fhir-base/src/main/java/ca/uhn/fhir/context/BaseRuntimeElementCompositeDefinition.java b/hapi-fhir-base/src/main/java/ca/uhn/fhir/context/BaseRuntimeElementCompositeDefinition.java index 3a1d213f34b..c593c04387f 100644 --- a/hapi-fhir-base/src/main/java/ca/uhn/fhir/context/BaseRuntimeElementCompositeDefinition.java +++ b/hapi-fhir-base/src/main/java/ca/uhn/fhir/context/BaseRuntimeElementCompositeDefinition.java @@ -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 ext public boolean isFirstFieldInNewClass() { return myFirstFieldInNewClass; } + + @Override + public String toString() { + return myField.getName(); + } } } diff --git a/hapi-fhir-base/src/main/java/ca/uhn/fhir/context/FhirContext.java b/hapi-fhir-base/src/main/java/ca/uhn/fhir/context/FhirContext.java index 60aebe8b2c4..6a4a7d01695 100644 --- a/hapi-fhir-base/src/main/java/ca/uhn/fhir/context/FhirContext.java +++ b/hapi-fhir-base/src/main/java/ca/uhn/fhir/context/FhirContext.java @@ -902,21 +902,21 @@ public class FhirContext { } private BaseRuntimeElementDefinition scanDatatype(final Class theResourceType) { - ArrayList> resourceTypes = new ArrayList>(); + ArrayList> resourceTypes = new ArrayList<>(); resourceTypes.add(theResourceType); Map, BaseRuntimeElementDefinition> defs = scanResourceTypes(resourceTypes); return defs.get(theResourceType); } private RuntimeResourceDefinition scanResourceType(final Class theResourceType) { - ArrayList> resourceTypes = new ArrayList>(); + ArrayList> resourceTypes = new ArrayList<>(); resourceTypes.add(theResourceType); Map, BaseRuntimeElementDefinition> defs = scanResourceTypes(resourceTypes); return (RuntimeResourceDefinition) defs.get(theResourceType); } private synchronized Map, BaseRuntimeElementDefinition> scanResourceTypes(final Collection> theResourceTypes) { - List> typesToScan = new ArrayList>(); + List> typesToScan = new ArrayList<>(); if (theResourceTypes != null) { typesToScan.addAll(theResourceTypes); } diff --git a/hapi-fhir-base/src/main/java/ca/uhn/fhir/model/api/annotation/Extension.java b/hapi-fhir-base/src/main/java/ca/uhn/fhir/model/api/annotation/Extension.java index 13e554bce5e..af91f6109a9 100644 --- a/hapi-fhir-base/src/main/java/ca/uhn/fhir/model/api/annotation/Extension.java +++ b/hapi-fhir-base/src/main/java/ca/uhn/fhir/model/api/annotation/Extension.java @@ -49,13 +49,13 @@ public @interface Extension { * by regional authorities or jurisdictional governments) *

*/ - boolean definedLocally(); + boolean definedLocally() default true; /** * Returns true if this extension is a modifier extension */ - boolean isModifier(); + boolean isModifier() default false; /** * The URL associated with this extension diff --git a/hapi-fhir-base/src/main/java/ca/uhn/fhir/narrative2/INarrativeTemplate.java b/hapi-fhir-base/src/main/java/ca/uhn/fhir/narrative2/INarrativeTemplate.java index a384de8a1b0..9794cd06032 100644 --- a/hapi-fhir-base/src/main/java/ca/uhn/fhir/narrative2/INarrativeTemplate.java +++ b/hapi-fhir-base/src/main/java/ca/uhn/fhir/narrative2/INarrativeTemplate.java @@ -32,7 +32,7 @@ public interface INarrativeTemplate { Set getAppliesToResourceTypes(); - Set> getAppliesToResourceClasses(); + Set> getAppliesToClasses(); TemplateTypeEnum getTemplateType(); diff --git a/hapi-fhir-base/src/main/java/ca/uhn/fhir/narrative2/NarrativeTemplate.java b/hapi-fhir-base/src/main/java/ca/uhn/fhir/narrative2/NarrativeTemplate.java index e38b79bea74..f6367b580c7 100644 --- a/hapi-fhir-base/src/main/java/ca/uhn/fhir/narrative2/NarrativeTemplate.java +++ b/hapi-fhir-base/src/main/java/ca/uhn/fhir/narrative2/NarrativeTemplate.java @@ -34,7 +34,7 @@ public class NarrativeTemplate implements INarrativeTemplate { private Set myAppliesToProfiles = new HashSet<>(); private Set myAppliesToResourceTypes = new HashSet<>(); private Set myAppliesToDataTypes = new HashSet<>(); - private Set> myAppliesToResourceClasses = new HashSet<>(); + private Set> 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> getAppliesToResourceClasses() { - return Collections.unmodifiableSet(myAppliesToResourceClasses); + public Set> getAppliesToClasses() { + return Collections.unmodifiableSet(myAppliesToClasses); } - void addAppliesToResourceClass(Class theAppliesToResourceClass) { - myAppliesToResourceClasses.add(theAppliesToResourceClass); + void addAppliesToClass(Class theAppliesToClass) { + myAppliesToClasses.add(theAppliesToClass); } @Override @@ -118,4 +118,5 @@ public class NarrativeTemplate implements INarrativeTemplate { void addAppliesToDatatype(String theDataType) { myAppliesToDataTypes.add(theDataType); } + } diff --git a/hapi-fhir-base/src/main/java/ca/uhn/fhir/narrative2/NarrativeTemplateManifest.java b/hapi-fhir-base/src/main/java/ca/uhn/fhir/narrative2/NarrativeTemplateManifest.java index b795ec80aa7..8ef69632c32 100644 --- a/hapi-fhir-base/src/main/java/ca/uhn/fhir/narrative2/NarrativeTemplateManifest.java +++ b/hapi-fhir-base/src/main/java/ca/uhn/fhir/narrative2/NarrativeTemplateManifest.java @@ -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> myStyleToResourceTypeToTemplate; - private final Map> myStyleToDatatypeToTemplate; - private final Map> myStyleToNameToTemplate; + private final Map> myResourceTypeToTemplate; + private final Map> myDatatypeToTemplate; + private final Map> myNameToTemplate; + private final Map> myClassToTemplate; private final int myTemplateCount; private NarrativeTemplateManifest(Collection theTemplates) { Map> resourceTypeToTemplate = new HashMap<>(); Map> datatypeToTemplate = new HashMap<>(); Map> nameToTemplate = new HashMap<>(); + Map> 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 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 getTemplateByResourceName(FhirContext theFhirContext, EnumSet theStyles, String theResourceName) { - return getFromMap(theStyles, theResourceName.toUpperCase(), myStyleToResourceTypeToTemplate); + return getFromMap(theStyles, theResourceName.toUpperCase(), myResourceTypeToTemplate); } @Override public List getTemplateByName(FhirContext theFhirContext, EnumSet theStyles, String theName) { - return getFromMap(theStyles, theName, myStyleToNameToTemplate); + return getFromMap(theStyles, theName, myNameToTemplate); } @Override public List getTemplateByElement(FhirContext theFhirContext, EnumSet 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 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) 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 List getFromMap(EnumSet theStyles, T theKey, Map> 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 Map> makeImmutable(Map> theStyleToResourceTypeToTemplate) { diff --git a/hapi-fhir-base/src/main/java/ca/uhn/fhir/parser/BaseParser.java b/hapi-fhir-base/src/main/java/ca/uhn/fhir/parser/BaseParser.java index 70259777a36..20976d44fdd 100644 --- a/hapi-fhir-base/src/main/java/ca/uhn/fhir/parser/BaseParser.java +++ b/hapi-fhir-base/src/main/java/ca/uhn/fhir/parser/BaseParser.java @@ -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()); diff --git a/hapi-fhir-base/src/main/java/ca/uhn/fhir/parser/JsonParser.java b/hapi-fhir-base/src/main/java/ca/uhn/fhir/parser/JsonParser.java index 0a7c5df81d6..a57587e24e8 100644 --- a/hapi-fhir-base/src/main/java/ca/uhn/fhir/parser/JsonParser.java +++ b/hapi-fhir-base/src/main/java/ca/uhn/fhir/parser/JsonParser.java @@ -379,7 +379,8 @@ public class JsonParser extends BaseParser implements IJsonLikeParser { } boolean haveWrittenExtensions = false; - for (CompositeChildElement nextChildElem : super.compositeChildIterator(theElement, theContainedResource, theParent, theEncodeContext)) { + Iterable compositeChildElements = super.compositeChildIterator(theElement, theContainedResource, theParent, theEncodeContext); + for (CompositeChildElement nextChildElem : compositeChildElements) { BaseRuntimeChildDefinition nextChild = nextChildElem.getDef(); diff --git a/hapi-fhir-base/src/main/resources/ca/uhn/fhir/narrative/OperationOutcome.html b/hapi-fhir-base/src/main/resources/ca/uhn/fhir/narrative/OperationOutcome.html index d38bf4cec78..a84249a298f 100644 --- a/hapi-fhir-base/src/main/resources/ca/uhn/fhir/narrative/OperationOutcome.html +++ b/hapi-fhir-base/src/main/resources/ca/uhn/fhir/narrative/OperationOutcome.html @@ -11,14 +11,7 @@ - - -

-					
-					
-						

-					
-				
+				

 			
 		
 	
diff --git a/hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/changelog/5_4_0/2537-improve_narrative_for_custom_structs.yaml b/hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/changelog/5_4_0/2537-improve_narrative_for_custom_structs.yaml
new file mode 100644
index 00000000000..a55ad8a2a58
--- /dev/null
+++ b/hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/changelog/5_4_0/2537-improve_narrative_for_custom_structs.yaml
@@ -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."
diff --git a/hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/docs/model/custom_structures.md b/hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/docs/model/custom_structures.md
index 5f2633433c1..4fcf4e9855a 100644
--- a/hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/docs/model/custom_structures.md
+++ b/hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/docs/model/custom_structures.md
@@ -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.
 
 

-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 Profiles & Extensions.

- + +# 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 + + + + + + + + + + + + + + + + + +``` + +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 + + + + + + + + + + + +``` + # 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}} diff --git a/hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/docs/model/narrative_generation.md b/hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/docs/model/narrative_generation.md index bdabbff8fc6..36fa258c477 100644 --- a/hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/docs/model/narrative_generation.md +++ b/hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/docs/model/narrative_generation.md @@ -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 th:narrative 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 th:narrative 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]]> +# 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. diff --git a/hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/docs/model/profiles_and_extensions.md b/hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/docs/model/profiles_and_extensions.md index f0e956f4218..aa8fbd78830 100644 --- a/hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/docs/model/profiles_and_extensions.md +++ b/hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/docs/model/profiles_and_extensions.md @@ -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 - - - - - - - - - - - - - - - - - -``` - -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 - - - - - - - - - - - -``` +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. diff --git a/hapi-fhir-structures-r4/src/test/java/ca/uhn/fhir/narrative/CustomPatient.java b/hapi-fhir-structures-r4/src/test/java/ca/uhn/fhir/narrative/CustomPatient.java new file mode 100644 index 00000000000..81f54b917c9 --- /dev/null +++ b/hapi-fhir-structures-r4/src/test/java/ca/uhn/fhir/narrative/CustomPatient.java @@ -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); + } + +} diff --git a/hapi-fhir-structures-r4/src/test/java/ca/uhn/fhir/narrative/CustomThymeleafNarrativeGeneratorR4Test.java b/hapi-fhir-structures-r4/src/test/java/ca/uhn/fhir/narrative/CustomThymeleafNarrativeGeneratorR4Test.java new file mode 100644 index 00000000000..6b355c7332e --- /dev/null +++ b/hapi-fhir-structures-r4/src/test/java/ca/uhn/fhir/narrative/CustomThymeleafNarrativeGeneratorR4Test.java @@ -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("

Name

given FAM1

Address

line1
line2
")); + + } + + @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 = "

CustomPatient

Favourite Pizza

Toppings: Mushrooms, Onions Size: 14
"; + assertEquals(expected, actual); + + } + + @AfterAll + public static void afterClassClearContext() { + TestUtil.clearAllStaticFieldsForUnitTest(); + } +} diff --git a/hapi-fhir-structures-r4/src/test/java/ca/uhn/fhir/narrative/DefaultThymeleafNarrativeGeneratorR4Test.java b/hapi-fhir-structures-r4/src/test/java/ca/uhn/fhir/narrative/DefaultThymeleafNarrativeGeneratorR4Test.java index e06495865f6..433b448a448 100644 --- a/hapi-fhir-structures-r4/src/test/java/ca/uhn/fhir/narrative/DefaultThymeleafNarrativeGeneratorR4Test.java +++ b/hapi-fhir-structures-r4/src/test/java/ca/uhn/fhir/narrative/DefaultThymeleafNarrativeGeneratorR4Test.java @@ -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("
joe john BLOW
")); @@ -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 { ""; //@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")); diff --git a/hapi-fhir-structures-r4/src/test/java/ca/uhn/fhir/narrative/FavouritePizzaExtension.java b/hapi-fhir-structures-r4/src/test/java/ca/uhn/fhir/narrative/FavouritePizzaExtension.java new file mode 100644 index 00000000000..e0422b1f400 --- /dev/null +++ b/hapi-fhir-structures-r4/src/test/java/ca/uhn/fhir/narrative/FavouritePizzaExtension.java @@ -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; + } +} diff --git a/hapi-fhir-structures-r4/src/test/resources/narrative/customtypes_CustomPatientR4.html b/hapi-fhir-structures-r4/src/test/resources/narrative/customtypes_CustomPatientR4.html new file mode 100644 index 00000000000..de58b90390c --- /dev/null +++ b/hapi-fhir-structures-r4/src/test/resources/narrative/customtypes_CustomPatientR4.html @@ -0,0 +1,6 @@ +
+

CustomPatient

+ +
+ +
diff --git a/hapi-fhir-structures-r4/src/test/resources/narrative/customtypes_FavouritePizzaExtensionR4.html b/hapi-fhir-structures-r4/src/test/resources/narrative/customtypes_FavouritePizzaExtensionR4.html new file mode 100644 index 00000000000..45a4ccaa596 --- /dev/null +++ b/hapi-fhir-structures-r4/src/test/resources/narrative/customtypes_FavouritePizzaExtensionR4.html @@ -0,0 +1,10 @@ +
+

Favourite Pizza

+ + Toppings: + + + Size: + + +
diff --git a/hapi-fhir-structures-r4/src/test/resources/narrative/customtypes_r4.properties b/hapi-fhir-structures-r4/src/test/resources/narrative/customtypes_r4.properties new file mode 100644 index 00000000000..9195f847039 --- /dev/null +++ b/hapi-fhir-structures-r4/src/test/resources/narrative/customtypes_r4.properties @@ -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 diff --git a/hapi-fhir-structures-r4/src/test/resources/narrative/standardtypes_PractitionerR4.html b/hapi-fhir-structures-r4/src/test/resources/narrative/standardtypes_PractitionerR4.html new file mode 100644 index 00000000000..dc0bcd9a62b --- /dev/null +++ b/hapi-fhir-structures-r4/src/test/resources/narrative/standardtypes_PractitionerR4.html @@ -0,0 +1,24 @@ +
+ +
+ + +

Name

+
+ +

Address

+
+ + +
diff --git a/hapi-fhir-structures-r4/src/test/resources/narrative/standardtypes_r4.properties b/hapi-fhir-structures-r4/src/test/resources/narrative/standardtypes_r4.properties new file mode 100644 index 00000000000..35dc55b6996 --- /dev/null +++ b/hapi-fhir-structures-r4/src/test/resources/narrative/standardtypes_r4.properties @@ -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