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 c915fbf6dc6..49f15f1d061 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 @@ -28,6 +28,8 @@ public interface INarrativeTemplate { Set getAppliesToProfiles(); + Set getAppliesToCode(); + Set getAppliesToResourceTypes(); Set> getAppliesToClasses(); diff --git a/hapi-fhir-base/src/main/java/ca/uhn/fhir/narrative2/INarrativeTemplateManifest.java b/hapi-fhir-base/src/main/java/ca/uhn/fhir/narrative2/INarrativeTemplateManifest.java index bb6fb293a76..2541ecbbf3a 100644 --- a/hapi-fhir-base/src/main/java/ca/uhn/fhir/narrative2/INarrativeTemplateManifest.java +++ b/hapi-fhir-base/src/main/java/ca/uhn/fhir/narrative2/INarrativeTemplateManifest.java @@ -32,7 +32,8 @@ public interface INarrativeTemplateManifest { @Nonnull FhirContext theFhirContext, @Nonnull EnumSet theStyles, @Nonnull String theResourceName, - @Nonnull Collection theProfiles); + @Nonnull Collection theProfiles, + @Nonnull Collection theCodes); List getTemplateByName( @Nonnull FhirContext theFhirContext, @Nonnull EnumSet theStyles, @Nonnull String theName); 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 e3b87889f0e..7a2f1e603c5 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,6 +34,7 @@ public class NarrativeTemplate implements INarrativeTemplate { private final Set myAppliesToDataTypes = new HashSet<>(); private final Set> myAppliesToClasses = new HashSet<>(); private final Set myAppliesToFragmentNames = new HashSet<>(); + private final Set 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 getAppliesToCode() { + return Collections.unmodifiableSet(myAppliesToCode); + } + void addAppliesToProfile(String theAppliesToProfile) { myAppliesToProfiles.add(theAppliesToProfile); } + void addAppliesToCode(String theAppliesToCode) { + myAppliesToCode.add(theAppliesToCode); + } + @Override public Set getAppliesToResourceTypes() { return Collections.unmodifiableSet(myAppliesToResourceTypes); 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 8ed6d21c848..190221861d9 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 @@ -107,8 +107,9 @@ public class NarrativeTemplateManifest implements INarrativeTemplateManifest { @Nonnull FhirContext theFhirContext, @Nonnull EnumSet theStyles, @Nonnull String theResourceName, - @Nonnull Collection theProfiles) { - return getFromMap(theStyles, theResourceName.toUpperCase(), myResourceTypeToTemplate, theProfiles); + @Nonnull Collection theProfiles, + @Nonnull Collection theCodes) { + return getFromMap(theStyles, theResourceName.toUpperCase(), myResourceTypeToTemplate, theProfiles, theCodes); } @Override @@ -116,7 +117,7 @@ public class NarrativeTemplateManifest implements INarrativeTemplateManifest { @Nonnull FhirContext theFhirContext, @Nonnull EnumSet 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 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 profiles = resource.getMeta().getProfile().stream() .filter(Objects::nonNull) .map(IPrimitiveType::getValueAsString) .filter(StringUtils::isNotBlank) .collect(Collectors.toList()); - retVal = getTemplateByResourceName(theFhirContext, theStyles, resourceName, profiles); + + List 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 List getFromMap( + EnumSet theStyles, T theKey, ListMultimap theMap) { + return getFromMap(theStyles, theKey, theMap, Collections.emptyList(), Collections.emptyList()); + } + private static List getFromMap( EnumSet theStyles, T theKey, ListMultimap theMap, - Collection theProfiles) { + Collection theProfiles, + Collection 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()); } } diff --git a/hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/changelog/7_8_0/6560-narrative-template-by-code.yaml b/hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/changelog/7_8_0/6560-narrative-template-by-code.yaml new file mode 100644 index 00000000000..2f3d4dfe80b --- /dev/null +++ b/hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/changelog/7_8_0/6560-narrative-template-by-code.yaml @@ -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)." 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 03c71852b95..6d2afa437b9 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 @@ -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 th:narrative blocks in resource templates. See the [example above](#creating-your-own-templates). diff --git a/hapi-fhir-structures-dstu3/src/test/java/ca/uhn/fhir/narrative2/NarrativeTemplateManifestTest.java b/hapi-fhir-structures-dstu3/src/test/java/ca/uhn/fhir/narrative2/NarrativeTemplateManifestTest.java index ef2f4ad850c..fc7796f6a60 100644 --- a/hapi-fhir-structures-dstu3/src/test/java/ca/uhn/fhir/narrative2/NarrativeTemplateManifestTest.java +++ b/hapi-fhir-structures-dstu3/src/test/java/ca/uhn/fhir/narrative2/NarrativeTemplateManifestTest.java @@ -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 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 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 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 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 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 template = manifest.getTemplateByElement( + ourCtx, + EnumSet.of(TemplateTypeEnum.THYMELEAF), + bundle); + + assertThat(template).hasSize(theTemplateListCount); + for(int i=0; i 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 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(); + } + } diff --git a/hapi-fhir-structures-dstu3/src/test/resources/manifest/manifest-template4.html b/hapi-fhir-structures-dstu3/src/test/resources/manifest/manifest-template4.html new file mode 100644 index 00000000000..59185596e24 --- /dev/null +++ b/hapi-fhir-structures-dstu3/src/test/resources/manifest/manifest-template4.html @@ -0,0 +1,3 @@ + +template4 + diff --git a/hapi-fhir-structures-dstu3/src/test/resources/manifest/manifest-template5.html b/hapi-fhir-structures-dstu3/src/test/resources/manifest/manifest-template5.html new file mode 100644 index 00000000000..db3b2b62705 --- /dev/null +++ b/hapi-fhir-structures-dstu3/src/test/resources/manifest/manifest-template5.html @@ -0,0 +1,3 @@ + +template5 + diff --git a/hapi-fhir-structures-dstu3/src/test/resources/manifest/manifest-template6.html b/hapi-fhir-structures-dstu3/src/test/resources/manifest/manifest-template6.html new file mode 100644 index 00000000000..6859ae16c15 --- /dev/null +++ b/hapi-fhir-structures-dstu3/src/test/resources/manifest/manifest-template6.html @@ -0,0 +1,3 @@ + +template6 + diff --git a/hapi-fhir-structures-dstu3/src/test/resources/manifest/manifest-test.properties b/hapi-fhir-structures-dstu3/src/test/resources/manifest/manifest-test.properties index 1188e074f61..5ece63124e9 100644 --- a/hapi-fhir-structures-dstu3/src/test/resources/manifest/manifest-test.properties +++ b/hapi-fhir-structures-dstu3/src/test/resources/manifest/manifest-test.properties @@ -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