001package ca.uhn.fhir.narrative2;
002
003/*-
004 * #%L
005 * HAPI FHIR - Core Library
006 * %%
007 * Copyright (C) 2014 - 2022 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.i18n.Msg;
025import ca.uhn.fhir.rest.server.exceptions.InternalErrorException;
026import org.hl7.fhir.instance.model.api.IBase;
027import org.thymeleaf.IEngineConfiguration;
028import org.thymeleaf.TemplateEngine;
029import org.thymeleaf.cache.AlwaysValidCacheEntryValidity;
030import org.thymeleaf.cache.ICacheEntryValidity;
031import org.thymeleaf.context.Context;
032import org.thymeleaf.context.ITemplateContext;
033import org.thymeleaf.engine.AttributeName;
034import org.thymeleaf.messageresolver.IMessageResolver;
035import org.thymeleaf.model.IProcessableElementTag;
036import org.thymeleaf.processor.IProcessor;
037import org.thymeleaf.processor.element.AbstractAttributeTagProcessor;
038import org.thymeleaf.processor.element.AbstractElementTagProcessor;
039import org.thymeleaf.processor.element.IElementTagStructureHandler;
040import org.thymeleaf.standard.StandardDialect;
041import org.thymeleaf.standard.expression.IStandardExpression;
042import org.thymeleaf.standard.expression.IStandardExpressionParser;
043import org.thymeleaf.standard.expression.StandardExpressions;
044import org.thymeleaf.templatemode.TemplateMode;
045import org.thymeleaf.templateresolver.DefaultTemplateResolver;
046import org.thymeleaf.templateresource.ITemplateResource;
047import org.thymeleaf.templateresource.StringTemplateResource;
048
049import java.util.EnumSet;
050import java.util.List;
051import java.util.Map;
052import java.util.Set;
053
054import static org.apache.commons.lang3.StringUtils.isNotBlank;
055
056public class ThymeleafNarrativeGenerator extends BaseNarrativeGenerator {
057
058        private IMessageResolver myMessageResolver;
059
060        /**
061         * Constructor
062         */
063        public ThymeleafNarrativeGenerator() {
064                super();
065        }
066
067        private TemplateEngine getTemplateEngine(FhirContext theFhirContext) {
068                TemplateEngine engine = new TemplateEngine();
069                ProfileResourceResolver resolver = new ProfileResourceResolver(theFhirContext);
070                engine.setTemplateResolver(resolver);
071                if (myMessageResolver != null) {
072                        engine.setMessageResolver(myMessageResolver);
073                }
074                StandardDialect dialect = new StandardDialect() {
075                        @Override
076                        public Set<IProcessor> getProcessors(String theDialectPrefix) {
077                                Set<IProcessor> retVal = super.getProcessors(theDialectPrefix);
078                                retVal.add(new NarrativeTagProcessor(theFhirContext, theDialectPrefix));
079                                retVal.add(new NarrativeAttributeProcessor(theDialectPrefix, theFhirContext));
080                                return retVal;
081                        }
082
083                };
084
085                engine.setDialect(dialect);
086                return engine;
087        }
088
089        @Override
090        protected String applyTemplate(FhirContext theFhirContext, INarrativeTemplate theTemplate, IBase theTargetContext) {
091
092                Context context = new Context();
093                context.setVariable("resource", theTargetContext);
094                context.setVariable("context", theTargetContext);
095                context.setVariable("fhirVersion", theFhirContext.getVersion().getVersion().name());
096
097                return getTemplateEngine(theFhirContext).process(theTemplate.getTemplateName(), context);
098        }
099
100
101        @Override
102        protected EnumSet<TemplateTypeEnum> getStyle() {
103                return EnumSet.of(TemplateTypeEnum.THYMELEAF);
104        }
105
106        private String applyTemplateWithinTag(FhirContext theFhirContext, ITemplateContext theTemplateContext, String theName, String theElement) {
107                IEngineConfiguration configuration = theTemplateContext.getConfiguration();
108                IStandardExpressionParser expressionParser = StandardExpressions.getExpressionParser(configuration);
109                final IStandardExpression expression = expressionParser.parseExpression(theTemplateContext, theElement);
110                Object elementValueObj = expression.execute(theTemplateContext);
111                final IBase elementValue = (IBase) elementValueObj;
112                if (elementValue == null) {
113                        return "";
114                }
115
116                List<INarrativeTemplate> templateOpt;
117                if (isNotBlank(theName)) {
118                        templateOpt = getManifest().getTemplateByName(theFhirContext, getStyle(), theName);
119                        if (templateOpt.isEmpty()) {
120                                throw new InternalErrorException(Msg.code(1863) + "Unknown template name: " + theName);
121                        }
122                } else {
123                        templateOpt = getManifest().getTemplateByElement(theFhirContext, getStyle(), elementValue);
124                        if (templateOpt.isEmpty()) {
125                                throw new InternalErrorException(Msg.code(1864) + "No template for type: " + elementValue.getClass());
126                        }
127                }
128
129                return applyTemplate(theFhirContext, templateOpt.get(0), elementValue);
130        }
131
132        public void setMessageResolver(IMessageResolver theMessageResolver) {
133                myMessageResolver = theMessageResolver;
134        }
135
136
137        private class ProfileResourceResolver extends DefaultTemplateResolver {
138                private final FhirContext myFhirContext;
139
140                private ProfileResourceResolver(FhirContext theFhirContext) {
141                        myFhirContext = theFhirContext;
142                }
143
144                @Override
145                protected boolean computeResolvable(IEngineConfiguration theConfiguration, String theOwnerTemplate, String theTemplate, Map<String, Object> theTemplateResolutionAttributes) {
146                        return getManifest().getTemplateByName(myFhirContext, getStyle(), theTemplate).size() > 0;
147                }
148
149                @Override
150                protected TemplateMode computeTemplateMode(IEngineConfiguration theConfiguration, String theOwnerTemplate, String theTemplate, Map<String, Object> theTemplateResolutionAttributes) {
151                        return TemplateMode.XML;
152                }
153
154                @Override
155                protected ITemplateResource computeTemplateResource(IEngineConfiguration theConfiguration, String theOwnerTemplate, String theTemplate, Map<String, Object> theTemplateResolutionAttributes) {
156                        return getManifest()
157                                .getTemplateByName(myFhirContext, getStyle(), theTemplate)
158                                .stream()
159                                .findFirst()
160                                .map(t -> new StringTemplateResource(t.getTemplateText()))
161                                .orElseThrow(() -> new IllegalArgumentException("Unknown template: " + theTemplate));
162                }
163
164                @Override
165                protected ICacheEntryValidity computeValidity(IEngineConfiguration theConfiguration, String theOwnerTemplate, String theTemplate, Map<String, Object> theTemplateResolutionAttributes) {
166                        return AlwaysValidCacheEntryValidity.INSTANCE;
167                }
168        }
169
170        private class NarrativeTagProcessor extends AbstractElementTagProcessor {
171
172                private final FhirContext myFhirContext;
173
174                NarrativeTagProcessor(FhirContext theFhirContext, String dialectPrefix) {
175                        super(TemplateMode.XML, dialectPrefix, "narrative", true, null, true, 0);
176                        myFhirContext = theFhirContext;
177                }
178
179                @Override
180                protected void doProcess(ITemplateContext theTemplateContext, IProcessableElementTag theTag, IElementTagStructureHandler theStructureHandler) {
181                        String name = theTag.getAttributeValue("th:name");
182                        String element = theTag.getAttributeValue("th:element");
183
184                        String appliedTemplate = applyTemplateWithinTag(myFhirContext, theTemplateContext, name, element);
185                        theStructureHandler.replaceWith(appliedTemplate, false);
186                }
187        }
188
189        /**
190         * This is a thymeleaf extension that allows people to do things like
191         * <th:block th:narrative="${result}"/>
192         */
193        private class NarrativeAttributeProcessor extends AbstractAttributeTagProcessor {
194
195                private final FhirContext myFhirContext;
196
197                NarrativeAttributeProcessor(String theDialectPrefix, FhirContext theFhirContext) {
198                        super(TemplateMode.XML, theDialectPrefix, null, false, "narrative", true, 0, true);
199                        myFhirContext = theFhirContext;
200                }
201
202                @Override
203                protected void doProcess(ITemplateContext theContext, IProcessableElementTag theTag, AttributeName theAttributeName, String theAttributeValue, IElementTagStructureHandler theStructureHandler) {
204                        String text = applyTemplateWithinTag(myFhirContext, theContext, null, theAttributeValue);
205                        theStructureHandler.setBody(text, false);
206                }
207
208        }
209}