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