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