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}