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}