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:
David Raeside 2024-12-19 09:07:28 -05:00 committed by GitHub
parent 803e8ca4f5
commit ac1d5f7840
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
11 changed files with 198 additions and 21 deletions

View File

@ -28,6 +28,8 @@ public interface INarrativeTemplate {
Set<String> getAppliesToProfiles();
Set<String> getAppliesToCode();
Set<String> getAppliesToResourceTypes();
Set<Class<? extends IBase>> getAppliesToClasses();

View File

@ -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);

View File

@ -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);

View File

@ -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());
}
}

View File

@ -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)."

View File

@ -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).

View File

@ -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();
}
}

View File

@ -0,0 +1,3 @@
<html>
template4
</html>

View File

@ -0,0 +1,3 @@
<html>
template5
</html>

View File

@ -0,0 +1,3 @@
<html>
template6
</html>

View File

@ -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