mirror of
https://github.com/hapifhir/hapi-fhir.git
synced 2025-02-11 15:34:56 +00:00
changes to support retrieving narrative template by code using meta.tag (#6562)
* changes to support retrieving narrative template by code using meta.tag * variable name change * updated * fixed code review comments * Update hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/docs/model/narrative_generation.md Co-authored-by: JasonRoberts-smile <85363818+JasonRoberts-smile@users.noreply.github.com> * beefed up tests * fixed link...hopefully * fix link --------- Co-authored-by: JasonRoberts-smile <85363818+JasonRoberts-smile@users.noreply.github.com>
This commit is contained in:
parent
803e8ca4f5
commit
ac1d5f7840
@ -28,6 +28,8 @@ public interface INarrativeTemplate {
|
||||
|
||||
Set<String> getAppliesToProfiles();
|
||||
|
||||
Set<String> getAppliesToCode();
|
||||
|
||||
Set<String> getAppliesToResourceTypes();
|
||||
|
||||
Set<Class<? extends IBase>> getAppliesToClasses();
|
||||
|
@ -32,7 +32,8 @@ public interface INarrativeTemplateManifest {
|
||||
@Nonnull FhirContext theFhirContext,
|
||||
@Nonnull EnumSet<TemplateTypeEnum> theStyles,
|
||||
@Nonnull String theResourceName,
|
||||
@Nonnull Collection<String> theProfiles);
|
||||
@Nonnull Collection<String> theProfiles,
|
||||
@Nonnull Collection<String> theCodes);
|
||||
|
||||
List<INarrativeTemplate> getTemplateByName(
|
||||
@Nonnull FhirContext theFhirContext, @Nonnull EnumSet<TemplateTypeEnum> theStyles, @Nonnull String theName);
|
||||
|
@ -34,6 +34,7 @@ public class NarrativeTemplate implements INarrativeTemplate {
|
||||
private final Set<String> myAppliesToDataTypes = new HashSet<>();
|
||||
private final Set<Class<? extends IBase>> myAppliesToClasses = new HashSet<>();
|
||||
private final Set<String> myAppliesToFragmentNames = new HashSet<>();
|
||||
private final Set<String> myAppliesToCode = new HashSet<>();
|
||||
private String myTemplateFileName;
|
||||
private TemplateTypeEnum myTemplateType = TemplateTypeEnum.THYMELEAF;
|
||||
private String myContextPath;
|
||||
@ -81,10 +82,19 @@ public class NarrativeTemplate implements INarrativeTemplate {
|
||||
return Collections.unmodifiableSet(myAppliesToProfiles);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Set<String> getAppliesToCode() {
|
||||
return Collections.unmodifiableSet(myAppliesToCode);
|
||||
}
|
||||
|
||||
void addAppliesToProfile(String theAppliesToProfile) {
|
||||
myAppliesToProfiles.add(theAppliesToProfile);
|
||||
}
|
||||
|
||||
void addAppliesToCode(String theAppliesToCode) {
|
||||
myAppliesToCode.add(theAppliesToCode);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Set<String> getAppliesToResourceTypes() {
|
||||
return Collections.unmodifiableSet(myAppliesToResourceTypes);
|
||||
|
@ -107,8 +107,9 @@ public class NarrativeTemplateManifest implements INarrativeTemplateManifest {
|
||||
@Nonnull FhirContext theFhirContext,
|
||||
@Nonnull EnumSet<TemplateTypeEnum> theStyles,
|
||||
@Nonnull String theResourceName,
|
||||
@Nonnull Collection<String> theProfiles) {
|
||||
return getFromMap(theStyles, theResourceName.toUpperCase(), myResourceTypeToTemplate, theProfiles);
|
||||
@Nonnull Collection<String> theProfiles,
|
||||
@Nonnull Collection<String> theCodes) {
|
||||
return getFromMap(theStyles, theResourceName.toUpperCase(), myResourceTypeToTemplate, theProfiles, theCodes);
|
||||
}
|
||||
|
||||
@Override
|
||||
@ -116,7 +117,7 @@ public class NarrativeTemplateManifest implements INarrativeTemplateManifest {
|
||||
@Nonnull FhirContext theFhirContext,
|
||||
@Nonnull EnumSet<TemplateTypeEnum> theStyles,
|
||||
@Nonnull String theName) {
|
||||
return getFromMap(theStyles, theName, myNameToTemplate, Collections.emptyList());
|
||||
return getFromMap(theStyles, theName, myNameToTemplate);
|
||||
}
|
||||
|
||||
@Override
|
||||
@ -124,7 +125,7 @@ public class NarrativeTemplateManifest implements INarrativeTemplateManifest {
|
||||
@Nonnull FhirContext theFhirContext,
|
||||
@Nonnull EnumSet<TemplateTypeEnum> theStyles,
|
||||
@Nonnull String theFragmentName) {
|
||||
return getFromMap(theStyles, theFragmentName, myFragmentNameToTemplate, Collections.emptyList());
|
||||
return getFromMap(theStyles, theFragmentName, myFragmentNameToTemplate);
|
||||
}
|
||||
|
||||
@SuppressWarnings("PatternVariableCanBeUsed")
|
||||
@ -138,22 +139,30 @@ public class NarrativeTemplateManifest implements INarrativeTemplateManifest {
|
||||
if (theElement instanceof IBaseResource) {
|
||||
IBaseResource resource = (IBaseResource) theElement;
|
||||
String resourceName = theFhirContext.getResourceDefinition(resource).getName();
|
||||
|
||||
List<String> profiles = resource.getMeta().getProfile().stream()
|
||||
.filter(Objects::nonNull)
|
||||
.map(IPrimitiveType::getValueAsString)
|
||||
.filter(StringUtils::isNotBlank)
|
||||
.collect(Collectors.toList());
|
||||
retVal = getTemplateByResourceName(theFhirContext, theStyles, resourceName, profiles);
|
||||
|
||||
List<String> codes = resource.getMeta().getTag().stream()
|
||||
.filter(Objects::nonNull)
|
||||
.filter(f -> StringUtils.isNotBlank(f.getSystem()) && StringUtils.isNotBlank(f.getCode()))
|
||||
.map(t -> t.getSystem() + "|" + t.getCode())
|
||||
.collect(Collectors.toList());
|
||||
|
||||
retVal = getTemplateByResourceName(theFhirContext, theStyles, resourceName, profiles, codes);
|
||||
}
|
||||
|
||||
if (retVal.isEmpty()) {
|
||||
retVal = getFromMap(theStyles, theElement.getClass().getName(), myClassToTemplate, Collections.emptyList());
|
||||
retVal = getFromMap(theStyles, theElement.getClass().getName(), myClassToTemplate);
|
||||
}
|
||||
|
||||
if (retVal.isEmpty()) {
|
||||
String datatypeName =
|
||||
theFhirContext.getElementDefinition(theElement.getClass()).getName();
|
||||
retVal = getFromMap(theStyles, datatypeName.toUpperCase(), myDatatypeToTemplate, Collections.emptyList());
|
||||
retVal = getFromMap(theStyles, datatypeName.toUpperCase(), myDatatypeToTemplate);
|
||||
}
|
||||
return retVal;
|
||||
}
|
||||
@ -222,6 +231,11 @@ public class NarrativeTemplateManifest implements INarrativeTemplateManifest {
|
||||
if (isNotBlank(profile)) {
|
||||
nextTemplate.addAppliesToProfile(profile);
|
||||
}
|
||||
} else if (nextKey.endsWith(".tag")) {
|
||||
String tag = file.getProperty(nextKey);
|
||||
if (isNotBlank(tag)) {
|
||||
nextTemplate.addAppliesToCode(tag);
|
||||
}
|
||||
} else if (nextKey.endsWith(".resourceType")) {
|
||||
String resourceType = file.getProperty(nextKey);
|
||||
parseValuesAndAddToMap(resourceType, nextTemplate::addAppliesToResourceType);
|
||||
@ -282,15 +296,22 @@ public class NarrativeTemplateManifest implements INarrativeTemplateManifest {
|
||||
}
|
||||
}
|
||||
|
||||
private static <T> List<INarrativeTemplate> getFromMap(
|
||||
EnumSet<TemplateTypeEnum> theStyles, T theKey, ListMultimap<T, NarrativeTemplate> theMap) {
|
||||
return getFromMap(theStyles, theKey, theMap, Collections.emptyList(), Collections.emptyList());
|
||||
}
|
||||
|
||||
private static <T> List<INarrativeTemplate> getFromMap(
|
||||
EnumSet<TemplateTypeEnum> theStyles,
|
||||
T theKey,
|
||||
ListMultimap<T, NarrativeTemplate> theMap,
|
||||
Collection<String> theProfiles) {
|
||||
Collection<String> theProfiles,
|
||||
Collection<String> theCodes) {
|
||||
return theMap.get(theKey).stream()
|
||||
.filter(t -> theStyles.contains(t.getTemplateType()))
|
||||
.filter(t -> theProfiles.isEmpty()
|
||||
|| t.getAppliesToProfiles().stream().anyMatch(theProfiles::contains))
|
||||
.filter(t -> theCodes.isEmpty() || t.getAppliesToCode().stream().anyMatch(theCodes::contains))
|
||||
.collect(Collectors.toList());
|
||||
}
|
||||
}
|
||||
|
@ -0,0 +1,6 @@
|
||||
---
|
||||
type: add
|
||||
issue: 6560
|
||||
title: Added the ability to retrieve narrative generation templates from a code system and code that is
|
||||
defined in the input resource's `meta.tag` property. For more information, see
|
||||
[documentation](/hapi-fhir/docs/model/narrative_generation.html)."
|
@ -77,6 +77,11 @@ observation.narrative=classpath:com/example/narrative/Observation.html
|
||||
# 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 can also assign a template based on tag ID (Resource.meta.tag). Coding
|
||||
# must be represented as a code system and code delimited by a pipe character.
|
||||
allergyIntolerance.tag=http://loinc.org|48765-2
|
||||
allergyIntolerance.narrative=classpath:com/example/narrative/AllergyIntolerance.html
|
||||
```
|
||||
|
||||
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 above](#creating-your-own-templates).
|
||||
|
@ -1,16 +1,24 @@
|
||||
package ca.uhn.fhir.narrative2;
|
||||
|
||||
import ca.uhn.fhir.context.FhirContext;
|
||||
import ca.uhn.fhir.util.BundleBuilder;
|
||||
import ca.uhn.fhir.util.ClasspathUtil;
|
||||
import com.google.common.collect.Lists;
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import org.hl7.fhir.dstu3.model.Coding;
|
||||
import org.hl7.fhir.dstu3.model.StringType;
|
||||
import org.hl7.fhir.instance.model.api.IBaseBundle;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.params.ParameterizedTest;
|
||||
import org.junit.jupiter.params.provider.Arguments;
|
||||
import org.junit.jupiter.params.provider.MethodSource;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.Collections;
|
||||
import java.util.EnumSet;
|
||||
import java.util.List;
|
||||
import java.util.stream.Stream;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
|
||||
@ -19,45 +27,48 @@ public class NarrativeTemplateManifestTest {
|
||||
private static final FhirContext ourCtx = FhirContext.forDstu3Cached();
|
||||
|
||||
@Test
|
||||
public void getTemplateByResourceName_NoProfile() throws IOException {
|
||||
public void getTemplateByResourceName_NoProfile() {
|
||||
INarrativeTemplateManifest manifest = NarrativeTemplateManifest.forManifestFileLocation("classpath:manifest/manifest-test.properties");
|
||||
List<INarrativeTemplate> template = manifest.getTemplateByResourceName(
|
||||
ourCtx,
|
||||
EnumSet.of(TemplateTypeEnum.THYMELEAF),
|
||||
"Bundle",
|
||||
Collections.emptyList());
|
||||
Collections.emptyList(),
|
||||
Collections.emptyList());
|
||||
ourLog.info("Templates: {}", template);
|
||||
assertThat(template).hasSize(3);
|
||||
assertThat(template).hasSize(6);
|
||||
assertThat(template.get(0).getTemplateText()).contains("template3");
|
||||
assertThat(template.get(1).getTemplateText()).contains("template2");
|
||||
assertThat(template.get(2).getTemplateText()).contains("template1");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void getTemplateByResourceName_ByProfile_ExactMatch() throws IOException {
|
||||
public void getTemplateByResourceName_ByProfile_ExactMatch() {
|
||||
INarrativeTemplateManifest manifest = NarrativeTemplateManifest.forManifestFileLocation("classpath:manifest/manifest-test.properties");
|
||||
List<INarrativeTemplate> template = manifest.getTemplateByResourceName(
|
||||
ourCtx,
|
||||
EnumSet.of(TemplateTypeEnum.THYMELEAF),
|
||||
"Bundle",
|
||||
Lists.newArrayList("http://profile1"));
|
||||
Lists.newArrayList("http://profile1"),
|
||||
Collections.emptyList());
|
||||
assertThat(template).hasSize(1);
|
||||
assertThat(template.get(0).getTemplateText()).contains("template1");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void getTemplateByResourceName_ByProfile_NoMatch() throws IOException {
|
||||
public void getTemplateByResourceName_ByProfile_NoMatch() {
|
||||
INarrativeTemplateManifest manifest = NarrativeTemplateManifest.forManifestFileLocation("classpath:manifest/manifest-test.properties");
|
||||
List<INarrativeTemplate> template = manifest.getTemplateByResourceName(
|
||||
ourCtx,
|
||||
EnumSet.of(TemplateTypeEnum.THYMELEAF),
|
||||
"Bundle",
|
||||
Lists.newArrayList("http://profile99"));
|
||||
Lists.newArrayList("http://profile99"),
|
||||
Collections.emptyList());
|
||||
assertThat(template).isEmpty();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void getTemplateByResourceName_WithFallback_ByProfile_ExactMatch() throws IOException {
|
||||
public void getTemplateByResourceName_WithFallback_ByProfile_ExactMatch() {
|
||||
INarrativeTemplateManifest manifest = NarrativeTemplateManifest.forManifestFileLocation(
|
||||
"classpath:manifest/manifest2-test.properties",
|
||||
"classpath:manifest/manifest-test.properties"
|
||||
@ -66,15 +77,15 @@ public class NarrativeTemplateManifestTest {
|
||||
ourCtx,
|
||||
EnumSet.of(TemplateTypeEnum.THYMELEAF),
|
||||
"Bundle",
|
||||
Lists.newArrayList("http://profile1"));
|
||||
Lists.newArrayList("http://profile1"),
|
||||
Collections.emptyList());
|
||||
assertThat(template).hasSize(2);
|
||||
assertThat(template.get(0).getTemplateText()).contains("template2-1");
|
||||
assertThat(template.get(1).getTemplateText()).contains("template1");
|
||||
}
|
||||
|
||||
|
||||
@Test
|
||||
public void getTemplateByFragment() throws IOException {
|
||||
public void getTemplateByFragment() {
|
||||
INarrativeTemplateManifest manifest = NarrativeTemplateManifest.forManifestFileContents(
|
||||
ClasspathUtil.loadResource("classpath:manifest/fragment-test.properties")
|
||||
);
|
||||
@ -86,4 +97,99 @@ public class NarrativeTemplateManifestTest {
|
||||
assertThat(template.get(0).getTemplateText()).contains("template1");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void getTemplateByElement_MatchOnLastCode() {
|
||||
BundleBuilder bundleBuilder = new BundleBuilder(FhirContext.forDstu3Cached());
|
||||
IBaseBundle bundle = bundleBuilder.getBundle();
|
||||
|
||||
bundle.getMeta().addTag().setSystem("http://loinc.org").setCode("12345");
|
||||
bundle.getMeta().addTag().setSystem("http://loinc.org").setCode("67890");
|
||||
bundle.getMeta().addTag().setSystem("http://loinc.org").setCode("8716-3");
|
||||
|
||||
INarrativeTemplateManifest manifest = NarrativeTemplateManifest.forManifestFileLocation(
|
||||
"classpath:manifest/manifest-test.properties");
|
||||
List<INarrativeTemplate> template = manifest.getTemplateByElement(
|
||||
ourCtx,
|
||||
EnumSet.of(TemplateTypeEnum.THYMELEAF),
|
||||
bundle);
|
||||
|
||||
assertThat(template).hasSize(1);
|
||||
assertThat(template.get(0).getTemplateText()).contains("template6");
|
||||
}
|
||||
|
||||
@ParameterizedTest
|
||||
@MethodSource("getInvalidCodeSystemAndCode")
|
||||
public void getTemplateByElement_InvalidCode_GetsIgnored(String theCodeSystem, String theCode) {
|
||||
final BundleBuilder bundleBuilder = new BundleBuilder(FhirContext.forDstu3Cached());
|
||||
bundleBuilder.setMetaField("tag", new Coding(theCodeSystem, theCode, ""));
|
||||
|
||||
INarrativeTemplateManifest manifest = NarrativeTemplateManifest.forManifestFileLocation(
|
||||
"classpath:manifest/manifest-test.properties");
|
||||
List<INarrativeTemplate> template = manifest.getTemplateByElement(
|
||||
ourCtx,
|
||||
EnumSet.of(TemplateTypeEnum.THYMELEAF),
|
||||
bundleBuilder.getBundle());
|
||||
|
||||
// should return 6 profiles since invalid codes will be filtered
|
||||
assertThat(template).hasSize(6);
|
||||
}
|
||||
|
||||
@ParameterizedTest
|
||||
@MethodSource("getTemplateByElementValues")
|
||||
public void getTemplateByElement(int theTemplateListCount, String theSystem,
|
||||
String theCode, String theProfile, String theContents) {
|
||||
IBaseBundle bundle = buildBundle(theProfile, theSystem, theCode);
|
||||
|
||||
INarrativeTemplateManifest manifest = NarrativeTemplateManifest.forManifestFileLocation(
|
||||
"classpath:manifest/manifest-test.properties");
|
||||
List<INarrativeTemplate> template = manifest.getTemplateByElement(
|
||||
ourCtx,
|
||||
EnumSet.of(TemplateTypeEnum.THYMELEAF),
|
||||
bundle);
|
||||
|
||||
assertThat(template).hasSize(theTemplateListCount);
|
||||
for(int i=0; i<theTemplateListCount; i++) {
|
||||
assertThat(template.get(i).getTemplateText()).contains(theContents);
|
||||
}
|
||||
}
|
||||
|
||||
private static Stream<Arguments> getInvalidCodeSystemAndCode() {
|
||||
return Stream.of(
|
||||
Arguments.of("http://loinc.org", null),
|
||||
Arguments.of(null, "46240-8"),
|
||||
Arguments.of("", "46240-8"),
|
||||
Arguments.of("http://loinc.org", ""),
|
||||
Arguments.of(null, ""),
|
||||
Arguments.of("", null),
|
||||
Arguments.of("", ""),
|
||||
Arguments.of(null, null)
|
||||
);
|
||||
}
|
||||
|
||||
private static Stream<Arguments> getTemplateByElementValues() {
|
||||
return Stream.of(
|
||||
Arguments.of(2, "http://loinc.org", "46240-8", null, "template"),
|
||||
Arguments.of(1, "http://loinc.org", "46240-8", "http://profile5", "template5"),
|
||||
Arguments.of(0, "http://loinc.org", "INVALID", null, null),
|
||||
Arguments.of(1, null, null, "http://profile1", "template1"),
|
||||
Arguments.of(1, null, null, "http://profile2", "template2"),
|
||||
Arguments.of(0, null, null, "http://INVALID", null),
|
||||
Arguments.of(1, "http://loinc.org", "8716-3", "http://profile6", "template6"),
|
||||
Arguments.of(6, null, null, null, "template")
|
||||
);
|
||||
}
|
||||
|
||||
private static IBaseBundle buildBundle(String theProfile, String theCodeSystem, String theCode) {
|
||||
final BundleBuilder bundleBuilder = new BundleBuilder(FhirContext.forDstu3Cached());
|
||||
|
||||
if (StringUtils.isNotEmpty(theProfile)) {
|
||||
bundleBuilder.setMetaField("profile", new StringType().setValue(theProfile));
|
||||
}
|
||||
if (StringUtils.isNotEmpty(theCodeSystem) && StringUtils.isNotEmpty(theCode)) {
|
||||
bundleBuilder.setMetaField("tag",
|
||||
new Coding(theCodeSystem, theCode, ""));
|
||||
}
|
||||
return bundleBuilder.getBundle();
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -0,0 +1,3 @@
|
||||
<html>
|
||||
template4
|
||||
</html>
|
@ -0,0 +1,3 @@
|
||||
<html>
|
||||
template5
|
||||
</html>
|
@ -0,0 +1,3 @@
|
||||
<html>
|
||||
template6
|
||||
</html>
|
@ -12,3 +12,20 @@ ips-profile2.profile=http://profile2
|
||||
# No profile
|
||||
ips-profile3.resourceType=Bundle
|
||||
ips-profile3.narrative=classpath:manifest/manifest-template3.html
|
||||
|
||||
# With loinc code
|
||||
ips-profile4.resourceType=Bundle
|
||||
ips-profile4.narrative=classpath:manifest/manifest-template4.html
|
||||
ips-profile4.tag=http://loinc.org|46240-8
|
||||
|
||||
# With loinc code and profile
|
||||
ips-profile5.resourceType=Bundle
|
||||
ips-profile5.narrative=classpath:manifest/manifest-template5.html
|
||||
ips-profile5.tag=http://loinc.org|46240-8
|
||||
ips-profile5.profile=http://profile5
|
||||
|
||||
# With loinc code and profile
|
||||
ips-profile6.resourceType=Bundle
|
||||
ips-profile6.narrative=classpath:manifest/manifest-template6.html
|
||||
ips-profile6.tag=http://loinc.org|8716-3
|
||||
ips-profile6.profile=http://profile6
|
||||
|
Loading…
x
Reference in New Issue
Block a user