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