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