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