001package ca.uhn.fhir.narrative2; 002 003/*- 004 * #%L 005 * HAPI FHIR - Core Library 006 * %% 007 * Copyright (C) 2014 - 2023 Smile CDR, Inc. 008 * %% 009 * Licensed under the Apache License, Version 2.0 (the "License"); 010 * you may not use this file except in compliance with the License. 011 * You may obtain a copy of the License at 012 * 013 * http://www.apache.org/licenses/LICENSE-2.0 014 * 015 * Unless required by applicable law or agreed to in writing, software 016 * distributed under the License is distributed on an "AS IS" BASIS, 017 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 018 * See the License for the specific language governing permissions and 019 * limitations under the License. 020 * #L% 021 */ 022 023import ca.uhn.fhir.context.BaseRuntimeChildDefinition; 024import ca.uhn.fhir.context.BaseRuntimeElementCompositeDefinition; 025import ca.uhn.fhir.context.FhirContext; 026import ca.uhn.fhir.context.FhirVersionEnum; 027import ca.uhn.fhir.fhirpath.IFhirPath; 028import ca.uhn.fhir.i18n.Msg; 029import ca.uhn.fhir.narrative.INarrativeGenerator; 030import ca.uhn.fhir.rest.server.exceptions.InternalErrorException; 031import ca.uhn.fhir.util.Logs; 032import ch.qos.logback.classic.spi.LogbackServiceProvider; 033import org.hl7.fhir.instance.model.api.IBase; 034import org.hl7.fhir.instance.model.api.IBaseResource; 035import org.hl7.fhir.instance.model.api.INarrative; 036import org.slf4j.Logger; 037import org.slf4j.LoggerFactory; 038 039import javax.annotation.Nullable; 040import java.util.Collections; 041import java.util.EnumSet; 042import java.util.List; 043import java.util.Set; 044import java.util.stream.Collectors; 045 046import static org.apache.commons.lang3.StringUtils.defaultIfEmpty; 047import static org.apache.commons.lang3.StringUtils.isNotBlank; 048 049public abstract class BaseNarrativeGenerator implements INarrativeGenerator { 050 051 @Override 052 public boolean populateResourceNarrative(FhirContext theFhirContext, IBaseResource theResource) { 053 INarrativeTemplate template = selectTemplate(theFhirContext, theResource); 054 if (template != null) { 055 applyTemplate(theFhirContext, template, theResource); 056 return true; 057 } 058 059 return false; 060 } 061 062 @Nullable 063 private INarrativeTemplate selectTemplate(FhirContext theFhirContext, IBaseResource theResource) { 064 List<INarrativeTemplate> templates = getTemplateForElement(theFhirContext, theResource); 065 INarrativeTemplate template = null; 066 if (templates.isEmpty()) { 067 Logs.getNarrativeGenerationTroubleshootingLog().debug("No templates match for resource of type {}", theResource.getClass()); 068 } else { 069 if (templates.size() > 1) { 070 Logs.getNarrativeGenerationTroubleshootingLog().debug("Multiple templates match for resource of type {} - Picking first from: {}", theResource.getClass(), templates); 071 } 072 template = templates.get(0); 073 Logs.getNarrativeGenerationTroubleshootingLog().debug("Selected template: {}", template); 074 } 075 return template; 076 } 077 078 @Override 079 public String generateResourceNarrative(FhirContext theFhirContext, IBaseResource theResource) { 080 INarrativeTemplate template = selectTemplate(theFhirContext, theResource); 081 if (template != null) { 082 String narrative = applyTemplate(theFhirContext, template, (IBase)theResource); 083 return cleanWhitespace(narrative); 084 } 085 086 return null; 087 } 088 089 protected List<INarrativeTemplate> getTemplateForElement(FhirContext theFhirContext, IBase theElement) { 090 return getManifest().getTemplateByElement(theFhirContext, getStyle(), theElement); 091 } 092 093 private boolean applyTemplate(FhirContext theFhirContext, INarrativeTemplate theTemplate, IBaseResource theResource) { 094 if (templateDoesntApplyToResource(theTemplate, theResource)) { 095 return false; 096 } 097 098 boolean retVal = false; 099 String resourceName = theFhirContext.getResourceType(theResource); 100 String contextPath = defaultIfEmpty(theTemplate.getContextPath(), resourceName); 101 102 // Narrative templates define a path within the resource that they apply to. Here, we're 103 // finding anywhere in the resource that gets a narrative 104 List<IBase> targets = findElementsInResourceRequiringNarratives(theFhirContext, theResource, contextPath); 105 for (IBase nextTargetContext : targets) { 106 107 // Extract [element].text of type Narrative 108 INarrative nextTargetNarrative = getOrCreateNarrativeChildElement(theFhirContext, nextTargetContext); 109 110 // Create the actual narrative text 111 String narrative = applyTemplate(theFhirContext, theTemplate, nextTargetContext); 112 narrative = cleanWhitespace(narrative); 113 114 if (isNotBlank(narrative)) { 115 try { 116 nextTargetNarrative.setDivAsString(narrative); 117 nextTargetNarrative.setStatusAsString("generated"); 118 retVal = true; 119 } catch (Exception e) { 120 throw new InternalErrorException(Msg.code(1865) + e); 121 } 122 } 123 124 } 125 return retVal; 126 } 127 128 private INarrative getOrCreateNarrativeChildElement(FhirContext theFhirContext, IBase nextTargetContext) { 129 BaseRuntimeElementCompositeDefinition<?> targetElementDef = (BaseRuntimeElementCompositeDefinition<?>) theFhirContext.getElementDefinition(nextTargetContext.getClass()); 130 BaseRuntimeChildDefinition targetTextChild = targetElementDef.getChildByName("text"); 131 List<IBase> existing = targetTextChild.getAccessor().getValues(nextTargetContext); 132 INarrative nextTargetNarrative; 133 if (existing.isEmpty()) { 134 nextTargetNarrative = (INarrative) theFhirContext.getElementDefinition("narrative").newInstance(); 135 targetTextChild.getMutator().addValue(nextTargetContext, nextTargetNarrative); 136 } else { 137 nextTargetNarrative = (INarrative) existing.get(0); 138 } 139 return nextTargetNarrative; 140 } 141 142 private List<IBase> findElementsInResourceRequiringNarratives(FhirContext theFhirContext, IBaseResource theResource, String theContextPath) { 143 if (theFhirContext.getVersion().getVersion().isOlderThan(FhirVersionEnum.DSTU3)) { 144 return Collections.singletonList(theResource); 145 } 146 IFhirPath fhirPath = theFhirContext.newFluentPath(); 147 return fhirPath.evaluate(theResource, theContextPath, IBase.class); 148 } 149 150 protected abstract String applyTemplate(FhirContext theFhirContext, INarrativeTemplate theTemplate, IBase theTargetContext); 151 152 private boolean templateDoesntApplyToResource(INarrativeTemplate theTemplate, IBaseResource theResource) { 153 boolean retVal = false; 154 if (theTemplate.getAppliesToProfiles() != null && !theTemplate.getAppliesToProfiles().isEmpty()) { 155 Set<String> resourceProfiles = theResource 156 .getMeta() 157 .getProfile() 158 .stream() 159 .map(t -> t.getValueAsString()) 160 .collect(Collectors.toSet()); 161 retVal = true; 162 for (String next : theTemplate.getAppliesToProfiles()) { 163 if (resourceProfiles.contains(next)) { 164 retVal = false; 165 break; 166 } 167 } 168 } 169 return retVal; 170 } 171 172 protected abstract EnumSet<TemplateTypeEnum> getStyle(); 173 174 /** 175 * Trims the superfluous whitespace out of an HTML block 176 */ 177 public static String cleanWhitespace(String theResult) { 178 StringBuilder b = new StringBuilder(); 179 boolean inWhitespace = false; 180 boolean betweenTags = false; 181 boolean lastNonWhitespaceCharWasTagEnd = false; 182 boolean inPre = false; 183 for (int i = 0; i < theResult.length(); i++) { 184 char nextChar = theResult.charAt(i); 185 if (inPre) { 186 b.append(nextChar); 187 continue; 188 } else if (nextChar == '>') { 189 b.append(nextChar); 190 betweenTags = true; 191 lastNonWhitespaceCharWasTagEnd = true; 192 continue; 193 } else if (nextChar == '\n' || nextChar == '\r') { 194 continue; 195 } 196 197 if (betweenTags) { 198 if (Character.isWhitespace(nextChar)) { 199 inWhitespace = true; 200 } else if (nextChar == '<') { 201 if (inWhitespace && !lastNonWhitespaceCharWasTagEnd) { 202 b.append(' '); 203 } 204 b.append(nextChar); 205 inWhitespace = false; 206 betweenTags = false; 207 lastNonWhitespaceCharWasTagEnd = false; 208 if (i + 3 < theResult.length()) { 209 char char1 = Character.toLowerCase(theResult.charAt(i + 1)); 210 char char2 = Character.toLowerCase(theResult.charAt(i + 2)); 211 char char3 = Character.toLowerCase(theResult.charAt(i + 3)); 212 char char4 = Character.toLowerCase((i + 4 < theResult.length()) ? theResult.charAt(i + 4) : ' '); 213 if (char1 == 'p' && char2 == 'r' && char3 == 'e') { 214 inPre = true; 215 } else if (char1 == '/' && char2 == 'p' && char3 == 'r' && char4 == 'e') { 216 inPre = false; 217 } 218 } 219 } else { 220 lastNonWhitespaceCharWasTagEnd = false; 221 if (inWhitespace) { 222 b.append(' '); 223 inWhitespace = false; 224 } 225 b.append(nextChar); 226 } 227 } else { 228 b.append(nextChar); 229 } 230 } 231 return b.toString(); 232 } 233 234 protected abstract NarrativeTemplateManifest getManifest(); 235 236}