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.NarrativeGeneratorTemplateUtils;
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                engine.setDialect(dialect);
102
103                engine.addDialect(new NarrativeGeneratorDialect(theFhirContext));
104                return engine;
105        }
106
107        @Override
108        protected String applyTemplate(FhirContext theFhirContext, INarrativeTemplate theTemplate, IBase theTargetContext) {
109
110                Context context = new Context();
111                context.setVariable("resource", theTargetContext);
112                context.setVariable("context", theTargetContext);
113                context.setVariable("narrativeUtil", NarrativeGeneratorTemplateUtils.INSTANCE);
114                context.setVariable(
115                                "fhirVersion", theFhirContext.getVersion().getVersion().name());
116
117                return getTemplateEngine(theFhirContext).process(theTemplate.getTemplateName(), context);
118        }
119
120        @Override
121        protected EnumSet<TemplateTypeEnum> getStyle() {
122                return EnumSet.of(TemplateTypeEnum.THYMELEAF);
123        }
124
125        private String applyTemplateWithinTag(
126                        FhirContext theFhirContext, ITemplateContext theTemplateContext, String theName, String theElement) {
127                IEngineConfiguration configuration = theTemplateContext.getConfiguration();
128                IStandardExpressionParser expressionParser = StandardExpressions.getExpressionParser(configuration);
129                final IStandardExpression expression = expressionParser.parseExpression(theTemplateContext, theElement);
130                Object elementValueObj = expression.execute(theTemplateContext);
131                final IBase elementValue = (IBase) elementValueObj;
132                if (elementValue == null) {
133                        return "";
134                }
135
136                List<INarrativeTemplate> templateOpt;
137                if (isNotBlank(theName)) {
138                        templateOpt = getManifest().getTemplateByName(theFhirContext, getStyle(), theName);
139                        if (templateOpt.isEmpty()) {
140                                throw new InternalErrorException(Msg.code(1863) + "Unknown template name: " + theName);
141                        }
142                } else {
143                        templateOpt = getManifest().getTemplateByElement(theFhirContext, getStyle(), elementValue);
144                        if (templateOpt.isEmpty()) {
145                                throw new InternalErrorException(Msg.code(1864) + "No template for type: " + elementValue.getClass());
146                        }
147                }
148
149                return applyTemplate(theFhirContext, templateOpt.get(0), elementValue);
150        }
151
152        public void setMessageResolver(IMessageResolver theMessageResolver) {
153                myMessageResolver = theMessageResolver;
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(
165                                IEngineConfiguration theConfiguration,
166                                String theOwnerTemplate,
167                                String theTemplate,
168                                Map<String, Object> theTemplateResolutionAttributes) {
169                        if (theOwnerTemplate == null) {
170                                return getManifest()
171                                                                .getTemplateByName(myFhirContext, getStyle(), theTemplate)
172                                                                .size()
173                                                > 0;
174                        } else {
175                                return getManifest()
176                                                                .getTemplateByFragmentName(myFhirContext, getStyle(), theTemplate)
177                                                                .size()
178                                                > 0;
179                        }
180                }
181
182                @Override
183                protected TemplateMode computeTemplateMode(
184                                IEngineConfiguration theConfiguration,
185                                String theOwnerTemplate,
186                                String theTemplate,
187                                Map<String, Object> theTemplateResolutionAttributes) {
188                        return TemplateMode.XML;
189                }
190
191                @Override
192                protected ITemplateResource computeTemplateResource(
193                                IEngineConfiguration theConfiguration,
194                                String theOwnerTemplate,
195                                String theTemplate,
196                                Map<String, Object> theTemplateResolutionAttributes) {
197                        if (theOwnerTemplate == null) {
198                                return getManifest().getTemplateByName(myFhirContext, getStyle(), theTemplate).stream()
199                                                .findFirst()
200                                                .map(t -> new StringTemplateResource(t.getTemplateText()))
201                                                .orElseThrow(() -> new IllegalArgumentException("Unknown template: " + theTemplate));
202                        } else {
203                                return getManifest().getTemplateByFragmentName(myFhirContext, getStyle(), theTemplate).stream()
204                                                .findFirst()
205                                                .map(t -> new StringTemplateResource(t.getTemplateText()))
206                                                .orElseThrow(() -> new IllegalArgumentException("Unknown template: " + theTemplate));
207                        }
208                }
209
210                @Override
211                protected ICacheEntryValidity computeValidity(
212                                IEngineConfiguration theConfiguration,
213                                String theOwnerTemplate,
214                                String theTemplate,
215                                Map<String, Object> theTemplateResolutionAttributes) {
216                        return AlwaysValidCacheEntryValidity.INSTANCE;
217                }
218        }
219
220        private class NarrativeTagProcessor extends AbstractElementTagProcessor {
221
222                private final FhirContext myFhirContext;
223
224                NarrativeTagProcessor(FhirContext theFhirContext, String dialectPrefix) {
225                        super(TemplateMode.XML, dialectPrefix, "narrative", true, null, true, 0);
226                        myFhirContext = theFhirContext;
227                }
228
229                @Override
230                protected void doProcess(
231                                ITemplateContext theTemplateContext,
232                                IProcessableElementTag theTag,
233                                IElementTagStructureHandler theStructureHandler) {
234                        String name = theTag.getAttributeValue("th:name");
235                        String element = theTag.getAttributeValue("th:element");
236
237                        String appliedTemplate = applyTemplateWithinTag(myFhirContext, theTemplateContext, name, element);
238                        theStructureHandler.replaceWith(appliedTemplate, false);
239                }
240        }
241
242        /**
243         * This is a thymeleaf extension that allows people to do things like
244         * <th:block th:narrative="${result}"/>
245         */
246        private class NarrativeAttributeProcessor extends AbstractAttributeTagProcessor {
247
248                private final FhirContext myFhirContext;
249
250                NarrativeAttributeProcessor(String theDialectPrefix, FhirContext theFhirContext) {
251                        super(TemplateMode.XML, theDialectPrefix, null, false, "narrative", true, 0, true);
252                        myFhirContext = theFhirContext;
253                }
254
255                @Override
256                protected void doProcess(
257                                ITemplateContext theContext,
258                                IProcessableElementTag theTag,
259                                AttributeName theAttributeName,
260                                String theAttributeValue,
261                                IElementTagStructureHandler theStructureHandler) {
262                        String text = applyTemplateWithinTag(myFhirContext, theContext, null, theAttributeValue);
263                        theStructureHandler.setBody(text, false);
264                }
265        }
266
267        private class NarrativeGeneratorDialect implements IDialect, IExpressionObjectDialect {
268
269                private final FhirContext myFhirContext;
270
271                public NarrativeGeneratorDialect(FhirContext theFhirContext) {
272                        myFhirContext = theFhirContext;
273                }
274
275                @Override
276                public String getName() {
277                        return "NarrativeGeneratorDialect";
278                }
279
280                @Override
281                public IExpressionObjectFactory getExpressionObjectFactory() {
282                        return new NarrativeGeneratorExpressionObjectFactory(myFhirContext);
283                }
284        }
285
286        private class NarrativeGeneratorExpressionObjectFactory implements IExpressionObjectFactory {
287
288                private final FhirContext myFhirContext;
289
290                public NarrativeGeneratorExpressionObjectFactory(FhirContext theFhirContext) {
291                        myFhirContext = theFhirContext;
292                }
293
294                @Override
295                public Set<String> getAllExpressionObjectNames() {
296                        return Sets.newHashSet(FHIRPATH);
297                }
298
299                @Override
300                public Object buildObject(IExpressionContext context, String expressionObjectName) {
301                        if (FHIRPATH.equals(expressionObjectName)) {
302                                return new NarrativeGeneratorFhirPathExpressionObject(myFhirContext);
303                        }
304                        return null;
305                }
306
307                @Override
308                public boolean isCacheable(String expressionObjectName) {
309                        return false;
310                }
311        }
312
313        private class NarrativeGeneratorFhirPathExpressionObject {
314
315                private final FhirContext myFhirContext;
316
317                public NarrativeGeneratorFhirPathExpressionObject(FhirContext theFhirContext) {
318                        myFhirContext = theFhirContext;
319                }
320
321                public IBase evaluateFirst(IBase theInput, String theExpression) {
322                        IFhirPath fhirPath = newFhirPath();
323                        Optional<IBase> output = fhirPath.evaluateFirst(theInput, theExpression, IBase.class);
324                        return output.orElse(null);
325                }
326
327                public List<IBase> evaluate(IBase theInput, String theExpression) {
328                        IFhirPath fhirPath = newFhirPath();
329                        return fhirPath.evaluate(theInput, theExpression, IBase.class);
330                }
331
332                private IFhirPath newFhirPath() {
333                        IFhirPath fhirPath = myFhirContext.newFhirPath();
334                        if (myFhirPathEvaluationContext != null) {
335                                fhirPath.setEvaluationContext(myFhirPathEvaluationContext);
336                        }
337                        return fhirPath;
338                }
339        }
340}