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