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