001package ca.uhn.fhir.narrative2;
002
003/*-
004 * #%L
005 * HAPI FHIR - Core Library
006 * %%
007 * Copyright (C) 2014 - 2021 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}