001/*- 002 * #%L 003 * HAPI FHIR - Core Library 004 * %% 005 * Copyright (C) 2014 - 2024 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 jakarta.annotation.Nullable; 032import org.hl7.fhir.instance.model.api.IBase; 033import org.hl7.fhir.instance.model.api.IBaseResource; 034import org.hl7.fhir.instance.model.api.INarrative; 035 036import java.util.Collections; 037import java.util.EnumSet; 038import java.util.List; 039import java.util.Set; 040import java.util.stream.Collectors; 041 042import static org.apache.commons.lang3.StringUtils.defaultIfEmpty; 043import static org.apache.commons.lang3.StringUtils.isNotBlank; 044 045public abstract class BaseNarrativeGenerator implements INarrativeGenerator { 046 047 @Override 048 public boolean populateResourceNarrative(FhirContext theFhirContext, IBaseResource theResource) { 049 INarrativeTemplate template = selectTemplate(theFhirContext, theResource); 050 if (template != null) { 051 applyTemplate(theFhirContext, template, theResource); 052 return true; 053 } 054 055 return false; 056 } 057 058 @Nullable 059 private INarrativeTemplate selectTemplate(FhirContext theFhirContext, IBaseResource theResource) { 060 List<INarrativeTemplate> templates = getTemplateForElement(theFhirContext, theResource); 061 INarrativeTemplate template = null; 062 if (templates.isEmpty()) { 063 Logs.getNarrativeGenerationTroubleshootingLog() 064 .debug("No templates match for resource of type {}", theResource.getClass()); 065 } else { 066 if (templates.size() > 1) { 067 Logs.getNarrativeGenerationTroubleshootingLog() 068 .debug( 069 "Multiple templates match for resource of type {} - Picking first from: {}", 070 theResource.getClass(), 071 templates); 072 } 073 template = templates.get(0); 074 Logs.getNarrativeGenerationTroubleshootingLog().debug("Selected template: {}", template); 075 } 076 return template; 077 } 078 079 @Override 080 public String generateResourceNarrative(FhirContext theFhirContext, IBaseResource theResource) { 081 INarrativeTemplate template = selectTemplate(theFhirContext, theResource); 082 if (template != null) { 083 String narrative = applyTemplate(theFhirContext, template, (IBase) theResource); 084 return cleanWhitespace(narrative); 085 } 086 087 return null; 088 } 089 090 protected List<INarrativeTemplate> getTemplateForElement(FhirContext theFhirContext, IBase theElement) { 091 return getManifest().getTemplateByElement(theFhirContext, getStyle(), theElement); 092 } 093 094 private boolean applyTemplate( 095 FhirContext theFhirContext, INarrativeTemplate theTemplate, IBaseResource theResource) { 096 if (templateDoesntApplyToResource(theTemplate, theResource)) { 097 return false; 098 } 099 100 boolean retVal = false; 101 String resourceName = theFhirContext.getResourceType(theResource); 102 String contextPath = defaultIfEmpty(theTemplate.getContextPath(), resourceName); 103 104 // Narrative templates define a path within the resource that they apply to. Here, we're 105 // finding anywhere in the resource that gets a narrative 106 List<IBase> targets = findElementsInResourceRequiringNarratives(theFhirContext, theResource, contextPath); 107 for (IBase nextTargetContext : targets) { 108 109 // Extract [element].text of type Narrative 110 INarrative nextTargetNarrative = getOrCreateNarrativeChildElement(theFhirContext, nextTargetContext); 111 112 // Create the actual narrative text 113 String narrative = applyTemplate(theFhirContext, theTemplate, nextTargetContext); 114 narrative = cleanWhitespace(narrative); 115 116 if (isNotBlank(narrative)) { 117 try { 118 nextTargetNarrative.setDivAsString(narrative); 119 nextTargetNarrative.setStatusAsString("generated"); 120 retVal = true; 121 } catch (Exception e) { 122 throw new InternalErrorException(Msg.code(1865) + e); 123 } 124 } 125 } 126 return retVal; 127 } 128 129 private INarrative getOrCreateNarrativeChildElement(FhirContext theFhirContext, IBase nextTargetContext) { 130 BaseRuntimeElementCompositeDefinition<?> targetElementDef = (BaseRuntimeElementCompositeDefinition<?>) 131 theFhirContext.getElementDefinition(nextTargetContext.getClass()); 132 BaseRuntimeChildDefinition targetTextChild = targetElementDef.getChildByName("text"); 133 List<IBase> existing = targetTextChild.getAccessor().getValues(nextTargetContext); 134 INarrative nextTargetNarrative; 135 if (existing.isEmpty()) { 136 nextTargetNarrative = (INarrative) 137 theFhirContext.getElementDefinition("narrative").newInstance(); 138 targetTextChild.getMutator().addValue(nextTargetContext, nextTargetNarrative); 139 } else { 140 nextTargetNarrative = (INarrative) existing.get(0); 141 } 142 return nextTargetNarrative; 143 } 144 145 private List<IBase> findElementsInResourceRequiringNarratives( 146 FhirContext theFhirContext, IBaseResource theResource, String theContextPath) { 147 if (theFhirContext.getVersion().getVersion().isOlderThan(FhirVersionEnum.DSTU3)) { 148 return Collections.singletonList(theResource); 149 } 150 IFhirPath fhirPath = theFhirContext.newFluentPath(); 151 return fhirPath.evaluate(theResource, theContextPath, IBase.class); 152 } 153 154 protected abstract String applyTemplate( 155 FhirContext theFhirContext, INarrativeTemplate theTemplate, IBase theTargetContext); 156 157 private boolean templateDoesntApplyToResource(INarrativeTemplate theTemplate, IBaseResource theResource) { 158 boolean retVal = false; 159 if (theTemplate.getAppliesToProfiles() != null 160 && !theTemplate.getAppliesToProfiles().isEmpty()) { 161 Set<String> resourceProfiles = theResource.getMeta().getProfile().stream() 162 .map(t -> t.getValueAsString()) 163 .collect(Collectors.toSet()); 164 retVal = true; 165 for (String next : theTemplate.getAppliesToProfiles()) { 166 if (resourceProfiles.contains(next)) { 167 retVal = false; 168 break; 169 } 170 } 171 } 172 return retVal; 173 } 174 175 protected abstract EnumSet<TemplateTypeEnum> getStyle(); 176 177 /** 178 * Trims the superfluous whitespace out of an HTML block 179 */ 180 public static String cleanWhitespace(String theResult) { 181 StringBuilder b = new StringBuilder(); 182 boolean inWhitespace = false; 183 boolean betweenTags = false; 184 boolean lastNonWhitespaceCharWasTagEnd = false; 185 boolean inPre = false; 186 for (int i = 0; i < theResult.length(); i++) { 187 char nextChar = theResult.charAt(i); 188 if (inPre) { 189 b.append(nextChar); 190 continue; 191 } else if (nextChar == '>') { 192 b.append(nextChar); 193 betweenTags = true; 194 lastNonWhitespaceCharWasTagEnd = true; 195 continue; 196 } else if (nextChar == '\n' || nextChar == '\r') { 197 continue; 198 } 199 200 if (betweenTags) { 201 if (Character.isWhitespace(nextChar)) { 202 inWhitespace = true; 203 } else if (nextChar == '<') { 204 if (inWhitespace && !lastNonWhitespaceCharWasTagEnd) { 205 b.append(' '); 206 } 207 b.append(nextChar); 208 inWhitespace = false; 209 betweenTags = false; 210 lastNonWhitespaceCharWasTagEnd = false; 211 if (i + 3 < theResult.length()) { 212 char char1 = Character.toLowerCase(theResult.charAt(i + 1)); 213 char char2 = Character.toLowerCase(theResult.charAt(i + 2)); 214 char char3 = Character.toLowerCase(theResult.charAt(i + 3)); 215 char char4 = 216 Character.toLowerCase((i + 4 < theResult.length()) ? theResult.charAt(i + 4) : ' '); 217 if (char1 == 'p' && char2 == 'r' && char3 == 'e') { 218 inPre = true; 219 } else if (char1 == '/' && char2 == 'p' && char3 == 'r' && char4 == 'e') { 220 inPre = false; 221 } 222 } 223 } else { 224 lastNonWhitespaceCharWasTagEnd = false; 225 if (inWhitespace) { 226 b.append(' '); 227 inWhitespace = false; 228 } 229 b.append(nextChar); 230 } 231 } else { 232 b.append(nextChar); 233 } 234 } 235 return b.toString(); 236 } 237 238 protected abstract NarrativeTemplateManifest getManifest(); 239}