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}