001package ca.uhn.fhir.narrative;
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.FhirContext;
024import ca.uhn.fhir.fhirpath.IFhirPath;
025import ca.uhn.fhir.fhirpath.IFhirPathEvaluationContext;
026import ca.uhn.fhir.i18n.Msg;
027import ca.uhn.fhir.narrative2.BaseNarrativeGenerator;
028import ca.uhn.fhir.narrative2.INarrativeTemplate;
029import ca.uhn.fhir.narrative2.TemplateTypeEnum;
030import ca.uhn.fhir.rest.server.exceptions.InternalErrorException;
031import com.google.common.collect.Sets;
032import org.hl7.fhir.instance.model.api.IBase;
033import org.thymeleaf.IEngineConfiguration;
034import org.thymeleaf.TemplateEngine;
035import org.thymeleaf.cache.AlwaysValidCacheEntryValidity;
036import org.thymeleaf.cache.ICacheEntryValidity;
037import org.thymeleaf.context.Context;
038import org.thymeleaf.context.IExpressionContext;
039import org.thymeleaf.context.ITemplateContext;
040import org.thymeleaf.dialect.IDialect;
041import org.thymeleaf.dialect.IExpressionObjectDialect;
042import org.thymeleaf.engine.AttributeName;
043import org.thymeleaf.expression.IExpressionObjectFactory;
044import org.thymeleaf.messageresolver.IMessageResolver;
045import org.thymeleaf.model.IProcessableElementTag;
046import org.thymeleaf.processor.IProcessor;
047import org.thymeleaf.processor.element.AbstractAttributeTagProcessor;
048import org.thymeleaf.processor.element.AbstractElementTagProcessor;
049import org.thymeleaf.processor.element.IElementTagStructureHandler;
050import org.thymeleaf.standard.StandardDialect;
051import org.thymeleaf.standard.expression.IStandardExpression;
052import org.thymeleaf.standard.expression.IStandardExpressionParser;
053import org.thymeleaf.standard.expression.StandardExpressions;
054import org.thymeleaf.templatemode.TemplateMode;
055import org.thymeleaf.templateresolver.DefaultTemplateResolver;
056import org.thymeleaf.templateresolver.ITemplateResolver;
057import org.thymeleaf.templateresource.ITemplateResource;
058import org.thymeleaf.templateresource.StringTemplateResource;
059
060import java.util.EnumSet;
061import java.util.List;
062import java.util.Map;
063import java.util.Optional;
064import java.util.Set;
065
066import static org.apache.commons.lang3.StringUtils.isNotBlank;
067
068public abstract class BaseThymeleafNarrativeGenerator extends BaseNarrativeGenerator {
069
070        public static final String FHIRPATH = "fhirpath";
071        private IMessageResolver myMessageResolver;
072        private IFhirPathEvaluationContext myFhirPathEvaluationContext;
073
074        /**
075         * Constructor
076         */
077        protected BaseThymeleafNarrativeGenerator() {
078                super();
079        }
080
081        public void setFhirPathEvaluationContext(IFhirPathEvaluationContext theFhirPathEvaluationContext) {
082                myFhirPathEvaluationContext = theFhirPathEvaluationContext;
083        }
084
085        private TemplateEngine getTemplateEngine(FhirContext theFhirContext) {
086                TemplateEngine engine = new TemplateEngine();
087                ITemplateResolver resolver = new NarrativeTemplateResolver(theFhirContext);
088                engine.setTemplateResolver(resolver);
089                if (myMessageResolver != null) {
090                        engine.setMessageResolver(myMessageResolver);
091                }
092                StandardDialect dialect = new StandardDialect() {
093                        @Override
094                        public Set<IProcessor> getProcessors(String theDialectPrefix) {
095                                Set<IProcessor> retVal = super.getProcessors(theDialectPrefix);
096                                retVal.add(new NarrativeTagProcessor(theFhirContext, theDialectPrefix));
097                                retVal.add(new NarrativeAttributeProcessor(theDialectPrefix, theFhirContext));
098                                return retVal;
099                        }
100
101                };
102                engine.setDialect(dialect);
103
104                engine.addDialect(new NarrativeGeneratorDialect(theFhirContext));
105                return engine;
106        }
107
108        @Override
109        protected String applyTemplate(FhirContext theFhirContext, INarrativeTemplate theTemplate, IBase theTargetContext) {
110
111                Context context = new Context();
112                context.setVariable("resource", theTargetContext);
113                context.setVariable("context", theTargetContext);
114                context.setVariable("fhirVersion", theFhirContext.getVersion().getVersion().name());
115
116                return getTemplateEngine(theFhirContext).process(theTemplate.getTemplateName(), context);
117        }
118
119
120        @Override
121        protected EnumSet<TemplateTypeEnum> getStyle() {
122                return EnumSet.of(TemplateTypeEnum.THYMELEAF);
123        }
124
125        private String applyTemplateWithinTag(FhirContext theFhirContext, ITemplateContext theTemplateContext, String theName, String theElement) {
126                IEngineConfiguration configuration = theTemplateContext.getConfiguration();
127                IStandardExpressionParser expressionParser = StandardExpressions.getExpressionParser(configuration);
128                final IStandardExpression expression = expressionParser.parseExpression(theTemplateContext, theElement);
129                Object elementValueObj = expression.execute(theTemplateContext);
130                final IBase elementValue = (IBase) elementValueObj;
131                if (elementValue == null) {
132                        return "";
133                }
134
135                List<INarrativeTemplate> templateOpt;
136                if (isNotBlank(theName)) {
137                        templateOpt = getManifest().getTemplateByName(theFhirContext, getStyle(), theName);
138                        if (templateOpt.isEmpty()) {
139                                throw new InternalErrorException(Msg.code(1863) + "Unknown template name: " + theName);
140                        }
141                } else {
142                        templateOpt = getManifest().getTemplateByElement(theFhirContext, getStyle(), elementValue);
143                        if (templateOpt.isEmpty()) {
144                                throw new InternalErrorException(Msg.code(1864) + "No template for type: " + elementValue.getClass());
145                        }
146                }
147
148                return applyTemplate(theFhirContext, templateOpt.get(0), elementValue);
149        }
150
151        public void setMessageResolver(IMessageResolver theMessageResolver) {
152                myMessageResolver = theMessageResolver;
153        }
154
155
156        private class NarrativeTemplateResolver extends DefaultTemplateResolver {
157                private final FhirContext myFhirContext;
158
159                private NarrativeTemplateResolver(FhirContext theFhirContext) {
160                        myFhirContext = theFhirContext;
161                }
162
163                @Override
164                protected boolean computeResolvable(IEngineConfiguration theConfiguration, String theOwnerTemplate, String theTemplate, Map<String, Object> theTemplateResolutionAttributes) {
165                        if (theOwnerTemplate == null) {
166                                return getManifest().getTemplateByName(myFhirContext, getStyle(), theTemplate).size() > 0;
167                        } else {
168                                return getManifest().getTemplateByFragmentName(myFhirContext, getStyle(), theTemplate).size() > 0;
169                        }
170                }
171
172                @Override
173                protected TemplateMode computeTemplateMode(IEngineConfiguration theConfiguration, String theOwnerTemplate, String theTemplate, Map<String, Object> theTemplateResolutionAttributes) {
174                        return TemplateMode.XML;
175                }
176
177                @Override
178                protected ITemplateResource computeTemplateResource(IEngineConfiguration theConfiguration, String theOwnerTemplate, String theTemplate, Map<String, Object> theTemplateResolutionAttributes) {
179                        if (theOwnerTemplate == null) {
180                                return getManifest()
181                                        .getTemplateByName(myFhirContext, getStyle(), theTemplate)
182                                        .stream()
183                                        .findFirst()
184                                        .map(t -> new StringTemplateResource(t.getTemplateText()))
185                                        .orElseThrow(() -> new IllegalArgumentException("Unknown template: " + theTemplate));
186                        } else {
187                                return getManifest()
188                                        .getTemplateByFragmentName(myFhirContext, getStyle(), theTemplate)
189                                        .stream()
190                                        .findFirst()
191                                        .map(t -> new StringTemplateResource(t.getTemplateText()))
192                                        .orElseThrow(() -> new IllegalArgumentException("Unknown template: " + theTemplate));
193                        }
194                }
195
196                @Override
197                protected ICacheEntryValidity computeValidity(IEngineConfiguration theConfiguration, String theOwnerTemplate, String theTemplate, Map<String, Object> theTemplateResolutionAttributes) {
198                        return AlwaysValidCacheEntryValidity.INSTANCE;
199                }
200        }
201
202        private class NarrativeTagProcessor extends AbstractElementTagProcessor {
203
204                private final FhirContext myFhirContext;
205
206                NarrativeTagProcessor(FhirContext theFhirContext, String dialectPrefix) {
207                        super(TemplateMode.XML, dialectPrefix, "narrative", true, null, true, 0);
208                        myFhirContext = theFhirContext;
209                }
210
211                @Override
212                protected void doProcess(ITemplateContext theTemplateContext, IProcessableElementTag theTag, IElementTagStructureHandler theStructureHandler) {
213                        String name = theTag.getAttributeValue("th:name");
214                        String element = theTag.getAttributeValue("th:element");
215
216                        String appliedTemplate = applyTemplateWithinTag(myFhirContext, theTemplateContext, name, element);
217                        theStructureHandler.replaceWith(appliedTemplate, false);
218                }
219        }
220
221        /**
222         * This is a thymeleaf extension that allows people to do things like
223         * <th:block th:narrative="${result}"/>
224         */
225        private class NarrativeAttributeProcessor extends AbstractAttributeTagProcessor {
226
227                private final FhirContext myFhirContext;
228
229                NarrativeAttributeProcessor(String theDialectPrefix, FhirContext theFhirContext) {
230                        super(TemplateMode.XML, theDialectPrefix, null, false, "narrative", true, 0, true);
231                        myFhirContext = theFhirContext;
232                }
233
234                @Override
235                protected void doProcess(ITemplateContext theContext, IProcessableElementTag theTag, AttributeName theAttributeName, String theAttributeValue, IElementTagStructureHandler theStructureHandler) {
236                        String text = applyTemplateWithinTag(myFhirContext, theContext, null, theAttributeValue);
237                        theStructureHandler.setBody(text, false);
238                }
239
240        }
241
242
243        private class NarrativeGeneratorDialect implements IDialect, IExpressionObjectDialect {
244
245                private final FhirContext myFhirContext;
246
247                public NarrativeGeneratorDialect(FhirContext theFhirContext) {
248                        myFhirContext = theFhirContext;
249                }
250
251                @Override
252                public String getName() {
253                        return "NarrativeGeneratorDialect";
254                }
255
256
257                @Override
258                public IExpressionObjectFactory getExpressionObjectFactory() {
259                        return new NarrativeGeneratorExpressionObjectFactory(myFhirContext);
260                }
261        }
262
263        private class NarrativeGeneratorExpressionObjectFactory implements IExpressionObjectFactory {
264
265                private final FhirContext myFhirContext;
266
267                public NarrativeGeneratorExpressionObjectFactory(FhirContext theFhirContext) {
268                        myFhirContext = theFhirContext;
269                }
270
271                @Override
272                public Set<String> getAllExpressionObjectNames() {
273                        return Sets.newHashSet(FHIRPATH);
274                }
275
276                @Override
277                public Object buildObject(IExpressionContext context, String expressionObjectName) {
278                        if (FHIRPATH.equals(expressionObjectName)) {
279                                return new NarrativeGeneratorFhirPathExpressionObject(myFhirContext);
280                        }
281                        return null;
282                }
283
284                @Override
285                public boolean isCacheable(String expressionObjectName) {
286                        return false;
287                }
288        }
289
290
291        private class NarrativeGeneratorFhirPathExpressionObject {
292
293                private final FhirContext myFhirContext;
294
295                public NarrativeGeneratorFhirPathExpressionObject(FhirContext theFhirContext) {
296                        myFhirContext = theFhirContext;
297                }
298
299                public IBase evaluateFirst(IBase theInput, String theExpression) {
300                        IFhirPath fhirPath = newFhirPath();
301                        Optional<IBase> output = fhirPath.evaluateFirst(theInput, theExpression, IBase.class);
302                        return output.orElse(null);
303                }
304
305                public List<IBase> evaluate(IBase theInput, String theExpression) {
306                        IFhirPath fhirPath = newFhirPath();
307                        return fhirPath.evaluate(theInput, theExpression, IBase.class);
308                }
309
310                private IFhirPath newFhirPath() {
311                        IFhirPath fhirPath = myFhirContext.newFhirPath();
312                        if (myFhirPathEvaluationContext != null) {
313                                fhirPath.setEvaluationContext(myFhirPathEvaluationContext);
314                        }
315                        return fhirPath;
316                }
317
318
319        }
320
321}