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