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.NarrativeGeneratorTemplateUtils; 029import ca.uhn.fhir.narrative2.TemplateTypeEnum; 030import ca.uhn.fhir.rest.server.exceptions.InternalErrorException; 031import com.google.common.collect.Sets; 032import org.hl7.fhir.instance.model.api.IBase; 033import org.thymeleaf.IEngineConfiguration; 034import org.thymeleaf.TemplateEngine; 035import org.thymeleaf.cache.AlwaysValidCacheEntryValidity; 036import org.thymeleaf.cache.ICacheEntryValidity; 037import org.thymeleaf.context.Context; 038import org.thymeleaf.context.IExpressionContext; 039import org.thymeleaf.context.ITemplateContext; 040import org.thymeleaf.dialect.IDialect; 041import org.thymeleaf.dialect.IExpressionObjectDialect; 042import org.thymeleaf.engine.AttributeName; 043import org.thymeleaf.expression.IExpressionObjectFactory; 044import org.thymeleaf.messageresolver.IMessageResolver; 045import org.thymeleaf.model.IProcessableElementTag; 046import org.thymeleaf.processor.IProcessor; 047import org.thymeleaf.processor.element.AbstractAttributeTagProcessor; 048import org.thymeleaf.processor.element.AbstractElementTagProcessor; 049import org.thymeleaf.processor.element.IElementTagStructureHandler; 050import org.thymeleaf.standard.StandardDialect; 051import org.thymeleaf.standard.expression.IStandardExpression; 052import org.thymeleaf.standard.expression.IStandardExpressionParser; 053import org.thymeleaf.standard.expression.StandardExpressions; 054import org.thymeleaf.templatemode.TemplateMode; 055import org.thymeleaf.templateresolver.DefaultTemplateResolver; 056import org.thymeleaf.templateresolver.ITemplateResolver; 057import org.thymeleaf.templateresource.ITemplateResource; 058import org.thymeleaf.templateresource.StringTemplateResource; 059 060import java.util.EnumSet; 061import java.util.List; 062import java.util.Map; 063import java.util.Optional; 064import java.util.Set; 065 066import static org.apache.commons.lang3.StringUtils.isNotBlank; 067 068public abstract class BaseThymeleafNarrativeGenerator extends BaseNarrativeGenerator { 069 070 public static final String FHIRPATH = "fhirpath"; 071 private IMessageResolver myMessageResolver; 072 private IFhirPathEvaluationContext myFhirPathEvaluationContext; 073 074 /** 075 * Constructor 076 */ 077 protected BaseThymeleafNarrativeGenerator() { 078 super(); 079 } 080 081 public void setFhirPathEvaluationContext(IFhirPathEvaluationContext theFhirPathEvaluationContext) { 082 myFhirPathEvaluationContext = theFhirPathEvaluationContext; 083 } 084 085 private TemplateEngine getTemplateEngine(FhirContext theFhirContext) { 086 TemplateEngine engine = new TemplateEngine(); 087 ITemplateResolver resolver = new NarrativeTemplateResolver(theFhirContext); 088 engine.setTemplateResolver(resolver); 089 if (myMessageResolver != null) { 090 engine.setMessageResolver(myMessageResolver); 091 } 092 StandardDialect dialect = new StandardDialect() { 093 @Override 094 public Set<IProcessor> getProcessors(String theDialectPrefix) { 095 Set<IProcessor> retVal = super.getProcessors(theDialectPrefix); 096 retVal.add(new NarrativeTagProcessor(theFhirContext, theDialectPrefix)); 097 retVal.add(new NarrativeAttributeProcessor(theDialectPrefix, theFhirContext)); 098 return retVal; 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("narrativeUtil", NarrativeGeneratorTemplateUtils.INSTANCE); 114 context.setVariable( 115 "fhirVersion", theFhirContext.getVersion().getVersion().name()); 116 117 return getTemplateEngine(theFhirContext).process(theTemplate.getTemplateName(), context); 118 } 119 120 @Override 121 protected EnumSet<TemplateTypeEnum> getStyle() { 122 return EnumSet.of(TemplateTypeEnum.THYMELEAF); 123 } 124 125 private String applyTemplateWithinTag( 126 FhirContext theFhirContext, ITemplateContext theTemplateContext, String theName, String theElement) { 127 IEngineConfiguration configuration = theTemplateContext.getConfiguration(); 128 IStandardExpressionParser expressionParser = StandardExpressions.getExpressionParser(configuration); 129 final IStandardExpression expression = expressionParser.parseExpression(theTemplateContext, theElement); 130 Object elementValueObj = expression.execute(theTemplateContext); 131 final IBase elementValue = (IBase) elementValueObj; 132 if (elementValue == null) { 133 return ""; 134 } 135 136 List<INarrativeTemplate> templateOpt; 137 if (isNotBlank(theName)) { 138 templateOpt = getManifest().getTemplateByName(theFhirContext, getStyle(), theName); 139 if (templateOpt.isEmpty()) { 140 throw new InternalErrorException(Msg.code(1863) + "Unknown template name: " + theName); 141 } 142 } else { 143 templateOpt = getManifest().getTemplateByElement(theFhirContext, getStyle(), elementValue); 144 if (templateOpt.isEmpty()) { 145 throw new InternalErrorException(Msg.code(1864) + "No template for type: " + elementValue.getClass()); 146 } 147 } 148 149 return applyTemplate(theFhirContext, templateOpt.get(0), elementValue); 150 } 151 152 public void setMessageResolver(IMessageResolver theMessageResolver) { 153 myMessageResolver = theMessageResolver; 154 } 155 156 private class NarrativeTemplateResolver extends DefaultTemplateResolver { 157 private final FhirContext myFhirContext; 158 159 private NarrativeTemplateResolver(FhirContext theFhirContext) { 160 myFhirContext = theFhirContext; 161 } 162 163 @Override 164 protected boolean computeResolvable( 165 IEngineConfiguration theConfiguration, 166 String theOwnerTemplate, 167 String theTemplate, 168 Map<String, Object> theTemplateResolutionAttributes) { 169 if (theOwnerTemplate == null) { 170 return getManifest() 171 .getTemplateByName(myFhirContext, getStyle(), theTemplate) 172 .size() 173 > 0; 174 } else { 175 return getManifest() 176 .getTemplateByFragmentName(myFhirContext, getStyle(), theTemplate) 177 .size() 178 > 0; 179 } 180 } 181 182 @Override 183 protected TemplateMode computeTemplateMode( 184 IEngineConfiguration theConfiguration, 185 String theOwnerTemplate, 186 String theTemplate, 187 Map<String, Object> theTemplateResolutionAttributes) { 188 return TemplateMode.XML; 189 } 190 191 @Override 192 protected ITemplateResource computeTemplateResource( 193 IEngineConfiguration theConfiguration, 194 String theOwnerTemplate, 195 String theTemplate, 196 Map<String, Object> theTemplateResolutionAttributes) { 197 if (theOwnerTemplate == null) { 198 return getManifest().getTemplateByName(myFhirContext, getStyle(), theTemplate).stream() 199 .findFirst() 200 .map(t -> new StringTemplateResource(t.getTemplateText())) 201 .orElseThrow(() -> new IllegalArgumentException("Unknown template: " + theTemplate)); 202 } else { 203 return getManifest().getTemplateByFragmentName(myFhirContext, getStyle(), theTemplate).stream() 204 .findFirst() 205 .map(t -> new StringTemplateResource(t.getTemplateText())) 206 .orElseThrow(() -> new IllegalArgumentException("Unknown template: " + theTemplate)); 207 } 208 } 209 210 @Override 211 protected ICacheEntryValidity computeValidity( 212 IEngineConfiguration theConfiguration, 213 String theOwnerTemplate, 214 String theTemplate, 215 Map<String, Object> theTemplateResolutionAttributes) { 216 return AlwaysValidCacheEntryValidity.INSTANCE; 217 } 218 } 219 220 private class NarrativeTagProcessor extends AbstractElementTagProcessor { 221 222 private final FhirContext myFhirContext; 223 224 NarrativeTagProcessor(FhirContext theFhirContext, String dialectPrefix) { 225 super(TemplateMode.XML, dialectPrefix, "narrative", true, null, true, 0); 226 myFhirContext = theFhirContext; 227 } 228 229 @Override 230 protected void doProcess( 231 ITemplateContext theTemplateContext, 232 IProcessableElementTag theTag, 233 IElementTagStructureHandler theStructureHandler) { 234 String name = theTag.getAttributeValue("th:name"); 235 String element = theTag.getAttributeValue("th:element"); 236 237 String appliedTemplate = applyTemplateWithinTag(myFhirContext, theTemplateContext, name, element); 238 theStructureHandler.replaceWith(appliedTemplate, false); 239 } 240 } 241 242 /** 243 * This is a thymeleaf extension that allows people to do things like 244 * <th:block th:narrative="${result}"/> 245 */ 246 private class NarrativeAttributeProcessor extends AbstractAttributeTagProcessor { 247 248 private final FhirContext myFhirContext; 249 250 NarrativeAttributeProcessor(String theDialectPrefix, FhirContext theFhirContext) { 251 super(TemplateMode.XML, theDialectPrefix, null, false, "narrative", true, 0, true); 252 myFhirContext = theFhirContext; 253 } 254 255 @Override 256 protected void doProcess( 257 ITemplateContext theContext, 258 IProcessableElementTag theTag, 259 AttributeName theAttributeName, 260 String theAttributeValue, 261 IElementTagStructureHandler theStructureHandler) { 262 String text = applyTemplateWithinTag(myFhirContext, theContext, null, theAttributeValue); 263 theStructureHandler.setBody(text, false); 264 } 265 } 266 267 private class NarrativeGeneratorDialect implements IDialect, IExpressionObjectDialect { 268 269 private final FhirContext myFhirContext; 270 271 public NarrativeGeneratorDialect(FhirContext theFhirContext) { 272 myFhirContext = theFhirContext; 273 } 274 275 @Override 276 public String getName() { 277 return "NarrativeGeneratorDialect"; 278 } 279 280 @Override 281 public IExpressionObjectFactory getExpressionObjectFactory() { 282 return new NarrativeGeneratorExpressionObjectFactory(myFhirContext); 283 } 284 } 285 286 private class NarrativeGeneratorExpressionObjectFactory implements IExpressionObjectFactory { 287 288 private final FhirContext myFhirContext; 289 290 public NarrativeGeneratorExpressionObjectFactory(FhirContext theFhirContext) { 291 myFhirContext = theFhirContext; 292 } 293 294 @Override 295 public Set<String> getAllExpressionObjectNames() { 296 return Sets.newHashSet(FHIRPATH); 297 } 298 299 @Override 300 public Object buildObject(IExpressionContext context, String expressionObjectName) { 301 if (FHIRPATH.equals(expressionObjectName)) { 302 return new NarrativeGeneratorFhirPathExpressionObject(myFhirContext); 303 } 304 return null; 305 } 306 307 @Override 308 public boolean isCacheable(String expressionObjectName) { 309 return false; 310 } 311 } 312 313 private class NarrativeGeneratorFhirPathExpressionObject { 314 315 private final FhirContext myFhirContext; 316 317 public NarrativeGeneratorFhirPathExpressionObject(FhirContext theFhirContext) { 318 myFhirContext = theFhirContext; 319 } 320 321 public IBase evaluateFirst(IBase theInput, String theExpression) { 322 IFhirPath fhirPath = newFhirPath(); 323 Optional<IBase> output = fhirPath.evaluateFirst(theInput, theExpression, IBase.class); 324 return output.orElse(null); 325 } 326 327 public List<IBase> evaluate(IBase theInput, String theExpression) { 328 IFhirPath fhirPath = newFhirPath(); 329 return fhirPath.evaluate(theInput, theExpression, IBase.class); 330 } 331 332 private IFhirPath newFhirPath() { 333 IFhirPath fhirPath = myFhirContext.newFhirPath(); 334 if (myFhirPathEvaluationContext != null) { 335 fhirPath.setEvaluationContext(myFhirPathEvaluationContext); 336 } 337 return fhirPath; 338 } 339 } 340}