001/*
002 * #%L
003 * HAPI FHIR - Core Library
004 * %%
005 * Copyright (C) 2014 - 2023 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                };
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("fhirVersion", theFhirContext.getVersion().getVersion().name());
114
115                return getTemplateEngine(theFhirContext).process(theTemplate.getTemplateName(), context);
116        }
117
118
119        @Override
120        protected EnumSet<TemplateTypeEnum> getStyle() {
121                return EnumSet.of(TemplateTypeEnum.THYMELEAF);
122        }
123
124        private String applyTemplateWithinTag(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
155        private class NarrativeTemplateResolver extends DefaultTemplateResolver {
156                private final FhirContext myFhirContext;
157
158                private NarrativeTemplateResolver(FhirContext theFhirContext) {
159                        myFhirContext = theFhirContext;
160                }
161
162                @Override
163                protected boolean computeResolvable(IEngineConfiguration theConfiguration, String theOwnerTemplate, String theTemplate, Map<String, Object> theTemplateResolutionAttributes) {
164                        if (theOwnerTemplate == null) {
165                                return getManifest().getTemplateByName(myFhirContext, getStyle(), theTemplate).size() > 0;
166                        } else {
167                                return getManifest().getTemplateByFragmentName(myFhirContext, getStyle(), theTemplate).size() > 0;
168                        }
169                }
170
171                @Override
172                protected TemplateMode computeTemplateMode(IEngineConfiguration theConfiguration, String theOwnerTemplate, String theTemplate, Map<String, Object> theTemplateResolutionAttributes) {
173                        return TemplateMode.XML;
174                }
175
176                @Override
177                protected ITemplateResource computeTemplateResource(IEngineConfiguration theConfiguration, String theOwnerTemplate, String theTemplate, Map<String, Object> theTemplateResolutionAttributes) {
178                        if (theOwnerTemplate == null) {
179                                return getManifest()
180                                        .getTemplateByName(myFhirContext, getStyle(), theTemplate)
181                                        .stream()
182                                        .findFirst()
183                                        .map(t -> new StringTemplateResource(t.getTemplateText()))
184                                        .orElseThrow(() -> new IllegalArgumentException("Unknown template: " + theTemplate));
185                        } else {
186                                return getManifest()
187                                        .getTemplateByFragmentName(myFhirContext, getStyle(), theTemplate)
188                                        .stream()
189                                        .findFirst()
190                                        .map(t -> new StringTemplateResource(t.getTemplateText()))
191                                        .orElseThrow(() -> new IllegalArgumentException("Unknown template: " + theTemplate));
192                        }
193                }
194
195                @Override
196                protected ICacheEntryValidity computeValidity(IEngineConfiguration theConfiguration, String theOwnerTemplate, String theTemplate, Map<String, Object> theTemplateResolutionAttributes) {
197                        return AlwaysValidCacheEntryValidity.INSTANCE;
198                }
199        }
200
201        private class NarrativeTagProcessor extends AbstractElementTagProcessor {
202
203                private final FhirContext myFhirContext;
204
205                NarrativeTagProcessor(FhirContext theFhirContext, String dialectPrefix) {
206                        super(TemplateMode.XML, dialectPrefix, "narrative", true, null, true, 0);
207                        myFhirContext = theFhirContext;
208                }
209
210                @Override
211                protected void doProcess(ITemplateContext theTemplateContext, IProcessableElementTag theTag, IElementTagStructureHandler theStructureHandler) {
212                        String name = theTag.getAttributeValue("th:name");
213                        String element = theTag.getAttributeValue("th:element");
214
215                        String appliedTemplate = applyTemplateWithinTag(myFhirContext, theTemplateContext, name, element);
216                        theStructureHandler.replaceWith(appliedTemplate, false);
217                }
218        }
219
220        /**
221         * This is a thymeleaf extension that allows people to do things like
222         * <th:block th:narrative="${result}"/>
223         */
224        private class NarrativeAttributeProcessor extends AbstractAttributeTagProcessor {
225
226                private final FhirContext myFhirContext;
227
228                NarrativeAttributeProcessor(String theDialectPrefix, FhirContext theFhirContext) {
229                        super(TemplateMode.XML, theDialectPrefix, null, false, "narrative", true, 0, true);
230                        myFhirContext = theFhirContext;
231                }
232
233                @Override
234                protected void doProcess(ITemplateContext theContext, IProcessableElementTag theTag, AttributeName theAttributeName, String theAttributeValue, IElementTagStructureHandler theStructureHandler) {
235                        String text = applyTemplateWithinTag(myFhirContext, theContext, null, theAttributeValue);
236                        theStructureHandler.setBody(text, false);
237                }
238
239        }
240
241
242        private class NarrativeGeneratorDialect implements IDialect, IExpressionObjectDialect {
243
244                private final FhirContext myFhirContext;
245
246                public NarrativeGeneratorDialect(FhirContext theFhirContext) {
247                        myFhirContext = theFhirContext;
248                }
249
250                @Override
251                public String getName() {
252                        return "NarrativeGeneratorDialect";
253                }
254
255
256                @Override
257                public IExpressionObjectFactory getExpressionObjectFactory() {
258                        return new NarrativeGeneratorExpressionObjectFactory(myFhirContext);
259                }
260        }
261
262        private class NarrativeGeneratorExpressionObjectFactory implements IExpressionObjectFactory {
263
264                private final FhirContext myFhirContext;
265
266                public NarrativeGeneratorExpressionObjectFactory(FhirContext theFhirContext) {
267                        myFhirContext = theFhirContext;
268                }
269
270                @Override
271                public Set<String> getAllExpressionObjectNames() {
272                        return Sets.newHashSet(FHIRPATH);
273                }
274
275                @Override
276                public Object buildObject(IExpressionContext context, String expressionObjectName) {
277                        if (FHIRPATH.equals(expressionObjectName)) {
278                                return new NarrativeGeneratorFhirPathExpressionObject(myFhirContext);
279                        }
280                        return null;
281                }
282
283                @Override
284                public boolean isCacheable(String expressionObjectName) {
285                        return false;
286                }
287        }
288
289
290        private class NarrativeGeneratorFhirPathExpressionObject {
291
292                private final FhirContext myFhirContext;
293
294                public NarrativeGeneratorFhirPathExpressionObject(FhirContext theFhirContext) {
295                        myFhirContext = theFhirContext;
296                }
297
298                public IBase evaluateFirst(IBase theInput, String theExpression) {
299                        IFhirPath fhirPath = newFhirPath();
300                        Optional<IBase> output = fhirPath.evaluateFirst(theInput, theExpression, IBase.class);
301                        return output.orElse(null);
302                }
303
304                public List<IBase> evaluate(IBase theInput, String theExpression) {
305                        IFhirPath fhirPath = newFhirPath();
306                        return fhirPath.evaluate(theInput, theExpression, IBase.class);
307                }
308
309                private IFhirPath newFhirPath() {
310                        IFhirPath fhirPath = myFhirContext.newFhirPath();
311                        if (myFhirPathEvaluationContext != null) {
312                                fhirPath.setEvaluationContext(myFhirPathEvaluationContext);
313                        }
314                        return fhirPath;
315                }
316
317
318        }
319
320}