001package org.hl7.fhir.r4.utils; 002 003import java.util.*; 004 005import org.hl7.fhir.exceptions.FHIRException; 006import org.hl7.fhir.exceptions.PathEngineException; 007import org.hl7.fhir.r4.context.IWorkerContext; 008import org.hl7.fhir.r4.model.Base; 009import org.hl7.fhir.r4.model.Resource; 010import org.hl7.fhir.r4.model.Tuple; 011import org.hl7.fhir.r4.model.ExpressionNode; 012import org.hl7.fhir.r4.model.Resource; 013import org.hl7.fhir.r4.model.TypeDetails; 014import org.hl7.fhir.r4.utils.FHIRPathEngine.ExpressionNodeWithOffset; 015import org.hl7.fhir.r4.utils.FHIRPathEngine.IEvaluationContext; 016import org.hl7.fhir.utilities.Utilities; 017 018public class LiquidEngine implements IEvaluationContext { 019 020 public interface ILiquidEngineIcludeResolver { 021 public String fetchInclude(LiquidEngine engine, String name); 022 } 023 024 private IEvaluationContext externalHostServices; 025 private FHIRPathEngine engine; 026 private ILiquidEngineIcludeResolver includeResolver; 027 028 private class LiquidEngineContext { 029 private Object externalContext; 030 private Map<String, Base> vars = new HashMap<>(); 031 032 public LiquidEngineContext(Object externalContext) { 033 super(); 034 this.externalContext = externalContext; 035 } 036 037 public LiquidEngineContext(LiquidEngineContext existing) { 038 super(); 039 externalContext = existing.externalContext; 040 vars.putAll(existing.vars); 041 } 042 } 043 044 public LiquidEngine(IWorkerContext context, IEvaluationContext hostServices) { 045 super(); 046 this.externalHostServices = hostServices; 047 engine = new FHIRPathEngine(context); 048 engine.setHostServices(this); 049 } 050 051 public ILiquidEngineIcludeResolver getIncludeResolver() { 052 return includeResolver; 053 } 054 055 public void setIncludeResolver(ILiquidEngineIcludeResolver includeResolver) { 056 this.includeResolver = includeResolver; 057 } 058 059 public LiquidDocument parse(String source, String sourceName) throws Exception { 060 return new LiquidParser(source).parse(sourceName); 061 } 062 063 public String evaluate(LiquidDocument document, Resource resource, Object appContext) throws FHIRException { 064 StringBuilder b = new StringBuilder(); 065 LiquidEngineContext ctxt = new LiquidEngineContext(appContext); 066 for (LiquidNode n : document.body) { 067 n.evaluate(b, resource, ctxt); 068 } 069 return b.toString(); 070 } 071 072 private abstract class LiquidNode { 073 protected void closeUp() {} 074 075 public abstract void evaluate(StringBuilder b, Resource resource, LiquidEngineContext ctxt) throws FHIRException; 076 } 077 078 private class LiquidConstant extends LiquidNode { 079 private String constant; 080 private StringBuilder b = new StringBuilder(); 081 082 @Override 083 protected void closeUp() { 084 constant = b.toString(); 085 b = null; 086 } 087 088 public void addChar(char ch) { 089 b.append(ch); 090 } 091 092 @Override 093 public void evaluate(StringBuilder b, Resource resource, LiquidEngineContext ctxt) { 094 b.append(constant); 095 } 096 } 097 098 private class LiquidStatement extends LiquidNode { 099 private String statement; 100 private ExpressionNode compiled; 101 102 @Override 103 public void evaluate(StringBuilder b, Resource resource, LiquidEngineContext ctxt) throws FHIRException { 104 if (compiled == null) 105 compiled = engine.parse(statement); 106 b.append(engine.evaluateToString(ctxt, resource, resource, compiled)); 107 } 108 } 109 110 private class LiquidIf extends LiquidNode { 111 private String condition; 112 private ExpressionNode compiled; 113 private List<LiquidNode> thenBody = new ArrayList<>(); 114 private List<LiquidNode> elseBody = new ArrayList<>(); 115 116 @Override 117 public void evaluate(StringBuilder b, Resource resource, LiquidEngineContext ctxt) throws FHIRException { 118 if (compiled == null) 119 compiled = engine.parse(condition); 120 boolean ok = engine.evaluateToBoolean(ctxt, resource, resource, compiled); 121 List<LiquidNode> list = ok ? thenBody : elseBody; 122 for (LiquidNode n : list) { 123 n.evaluate(b, resource, ctxt); 124 } 125 } 126 } 127 128 private class LiquidLoop extends LiquidNode { 129 private String varName; 130 private String condition; 131 private ExpressionNode compiled; 132 private List<LiquidNode> body = new ArrayList<>(); 133 @Override 134 public void evaluate(StringBuilder b, Resource resource, LiquidEngineContext ctxt) throws FHIRException { 135 if (compiled == null) 136 compiled = engine.parse(condition); 137 List<Base> list = engine.evaluate(ctxt, resource, resource, compiled); 138 LiquidEngineContext lctxt = new LiquidEngineContext(ctxt); 139 for (Base o : list) { 140 lctxt.vars.put(varName, o); 141 for (LiquidNode n : body) { 142 n.evaluate(b, resource, lctxt); 143 } 144 } 145 } 146 } 147 148 private class LiquidInclude extends LiquidNode { 149 private String page; 150 private Map<String, ExpressionNode> params = new HashMap<>(); 151 152 @Override 153 public void evaluate(StringBuilder b, Resource resource, LiquidEngineContext ctxt) throws FHIRException { 154 String src = includeResolver.fetchInclude(LiquidEngine.this, page); 155 LiquidParser parser = new LiquidParser(src); 156 LiquidDocument doc = parser.parse(page); 157 LiquidEngineContext nctxt = new LiquidEngineContext(ctxt.externalContext); 158 Tuple incl = new Tuple(); 159 nctxt.vars.put("include", incl); 160 for (String s : params.keySet()) { 161 incl.addProperty(s, engine.evaluate(ctxt, resource, resource, params.get(s))); 162 } 163 for (LiquidNode n : doc.body) { 164 n.evaluate(b, resource, nctxt); 165 } 166 } 167 } 168 169 public static class LiquidDocument { 170 private List<LiquidNode> body = new ArrayList<>(); 171 172 } 173 174 private class LiquidParser { 175 176 private String source; 177 private int cursor; 178 private String name; 179 180 public LiquidParser(String source) { 181 this.source = source; 182 cursor = 0; 183 } 184 185 private char next1() { 186 if (cursor >= source.length()) 187 return 0; 188 else 189 return source.charAt(cursor); 190 } 191 192 private char next2() { 193 if (cursor >= source.length()-1) 194 return 0; 195 else 196 return source.charAt(cursor+1); 197 } 198 199 private char grab() { 200 cursor++; 201 return source.charAt(cursor-1); 202 } 203 204 public LiquidDocument parse(String name) throws FHIRException { 205 this.name = name; 206 LiquidDocument doc = new LiquidDocument(); 207 parseList(doc.body, new String[0]); 208 return doc; 209 } 210 211 private String parseList(List<LiquidNode> list, String[] terminators) throws FHIRException { 212 String close = null; 213 while (cursor < source.length()) { 214 if (next1() == '{' && (next2() == '%' || next2() == '{' )) { 215 if (next2() == '%') { 216 String cnt = parseTag('%'); 217 if (Utilities.existsInList(cnt, terminators)) { 218 close = cnt; 219 break; 220 } else if (cnt.startsWith("if ")) 221 list.add(parseIf(cnt)); 222 else if (cnt.startsWith("loop ")) 223 list.add(parseLoop(cnt.substring(4).trim())); 224 else if (cnt.startsWith("include ")) 225 list.add(parseInclude(cnt.substring(7).trim())); 226 else 227 throw new FHIRException("Script "+name+": Script "+name+": Unknown flow control statement "+cnt); 228 } else { // next2() == '{' 229 list.add(parseStatement()); 230 } 231 } else { 232 if (list.size() == 0 || !(list.get(list.size()-1) instanceof LiquidConstant)) 233 list.add(new LiquidConstant()); 234 ((LiquidConstant) list.get(list.size()-1)).addChar(grab()); 235 } 236 } 237 for (LiquidNode n : list) 238 n.closeUp(); 239 if (terminators.length > 0) 240 if (!Utilities.existsInList(close, terminators)) 241 throw new FHIRException("Script "+name+": Script "+name+": Found end of script looking for "+ Arrays.asList(terminators)); 242 return close; 243 } 244 245 private LiquidNode parseIf(String cnt) throws FHIRException { 246 LiquidIf res = new LiquidIf(); 247 res.condition = cnt.substring(3).trim(); 248 String term = parseList(res.thenBody, new String[] { "else", "endif"} ); 249 if ("else".equals(term)) 250 term = parseList(res.elseBody, new String[] { "endif"} ); 251 return res; 252 } 253 254 private LiquidNode parseInclude(String cnt) throws FHIRException { 255 int i = 1; 256 while (i < cnt.length() && !Character.isWhitespace(cnt.charAt(i))) 257 i++; 258 if (i == cnt.length() || i == 0) 259 throw new FHIRException("Script "+name+": Error reading include: "+cnt); 260 LiquidInclude res = new LiquidInclude(); 261 res.page = cnt.substring(0, i); 262 while (i < cnt.length() && Character.isWhitespace(cnt.charAt(i))) 263 i++; 264 while (i < cnt.length()) { 265 int j = i; 266 while (i < cnt.length() && cnt.charAt(i) != '=') 267 i++; 268 if (i >= cnt.length() || j == i) 269 throw new FHIRException("Script "+name+": Error reading include: "+cnt); 270 String n = cnt.substring(j, i); 271 if (res.params.containsKey(n)) 272 throw new FHIRException("Script "+name+": Error reading include: "+cnt); 273 i++; 274 ExpressionNodeWithOffset t = engine.parsePartial(cnt, i); 275 i = t.getOffset(); 276 res.params.put(n, t.getNode()); 277 while (i < cnt.length() && Character.isWhitespace(cnt.charAt(i))) 278 i++; 279 } 280 return res; 281 } 282 283 284 private LiquidNode parseLoop(String cnt) throws FHIRException { 285 int i = 0; 286 while (!Character.isWhitespace(cnt.charAt(i))) 287 i++; 288 LiquidLoop res = new LiquidLoop(); 289 res.varName = cnt.substring(0, i); 290 while (Character.isWhitespace(cnt.charAt(i))) 291 i++; 292 int j = i; 293 while (!Character.isWhitespace(cnt.charAt(i))) 294 i++; 295 if (!"in".equals(cnt.substring(j, i))) 296 throw new FHIRException("Script "+name+": Script "+name+": Error reading loop: "+cnt); 297 res.condition = cnt.substring(i).trim(); 298 parseList(res.body, new String[] { "endloop"} ); 299 return res; 300 } 301 302 private String parseTag(char ch) throws FHIRException { 303 grab(); 304 grab(); 305 StringBuilder b = new StringBuilder(); 306 while (cursor < source.length() && !(next1() == '%' && next2() == '}')) { 307 b.append(grab()); 308 } 309 if (!(next1() == '%' && next2() == '}')) 310 throw new FHIRException("Script "+name+": Unterminated Liquid statement {% "+b.toString()); 311 grab(); 312 grab(); 313 return b.toString().trim(); 314 } 315 316 private LiquidStatement parseStatement() throws FHIRException { 317 grab(); 318 grab(); 319 StringBuilder b = new StringBuilder(); 320 while (cursor < source.length() && !(next1() == '}' && next2() == '}')) { 321 b.append(grab()); 322 } 323 if (!(next1() == '}' && next2() == '}')) 324 throw new FHIRException("Script "+name+": Unterminated Liquid statement {{ "+b.toString()); 325 grab(); 326 grab(); 327 LiquidStatement res = new LiquidStatement(); 328 res.statement = b.toString().trim(); 329 return res; 330 } 331 332 } 333 334 @Override 335 public Base resolveConstant(Object appContext, String name, boolean beforeContext) throws PathEngineException { 336 LiquidEngineContext ctxt = (LiquidEngineContext) appContext; 337 if (ctxt.vars.containsKey(name)) 338 return ctxt.vars.get(name); 339 if (externalHostServices == null) 340 return null; 341 return externalHostServices.resolveConstant(ctxt.externalContext, name, beforeContext); 342 } 343 344 @Override 345 public TypeDetails resolveConstantType(Object appContext, String name) throws PathEngineException { 346 if (externalHostServices == null) 347 return null; 348 LiquidEngineContext ctxt = (LiquidEngineContext) appContext; 349 return externalHostServices.resolveConstantType(ctxt.externalContext, name); 350 } 351 352 @Override 353 public boolean log(String argument, List<Base> focus) { 354 if (externalHostServices == null) 355 return false; 356 return externalHostServices.log(argument, focus); 357 } 358 359 @Override 360 public FunctionDetails resolveFunction(String functionName) { 361 if (externalHostServices == null) 362 return null; 363 return externalHostServices.resolveFunction(functionName); 364 } 365 366 @Override 367 public TypeDetails checkFunction(Object appContext, String functionName, List<TypeDetails> parameters) throws PathEngineException { 368 if (externalHostServices == null) 369 return null; 370 LiquidEngineContext ctxt = (LiquidEngineContext) appContext; 371 return externalHostServices.checkFunction(ctxt.externalContext, functionName, parameters); 372 } 373 374 @Override 375 public List<Base> executeFunction(Object appContext, String functionName, List<List<Base>> parameters) { 376 if (externalHostServices == null) 377 return null; 378 LiquidEngineContext ctxt = (LiquidEngineContext) appContext; 379 return externalHostServices.executeFunction(ctxt.externalContext, functionName, parameters); 380 } 381 382 @Override 383 public Base resolveReference(Object appContext, String url) throws FHIRException { 384 if (externalHostServices == null) 385 return null; 386 LiquidEngineContext ctxt = (LiquidEngineContext) appContext; 387 return resolveReference(ctxt.externalContext, url); 388 } 389 390 @Override 391 public boolean conformsToProfile(Object appContext, Base item, String url) throws FHIRException { 392 if (externalHostServices == null) 393 return false; 394 LiquidEngineContext ctxt = (LiquidEngineContext) appContext; 395 return conformsToProfile(ctxt.externalContext, item, url); 396 } 397 398}