001package org.hl7.fhir.r4.utils; 002 003import java.util.ArrayList; 004import java.util.Arrays; 005import java.util.HashMap; 006import java.util.List; 007import java.util.Map; 008 009/* 010 Copyright (c) 2011+, HL7, Inc. 011 All rights reserved. 012 013 Redistribution and use in source and binary forms, with or without modification, 014 are permitted provided that the following conditions are met: 015 016 * Redistributions of source code must retain the above copyright notice, this 017 list of conditions and the following disclaimer. 018 * Redistributions in binary form must reproduce the above copyright notice, 019 this list of conditions and the following disclaimer in the documentation 020 and/or other materials provided with the distribution. 021 * Neither the name of HL7 nor the names of its contributors may be used to 022 endorse or promote products derived from this software without specific 023 prior written permission. 024 025 THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND 026 ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 027 WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. 028 IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, 029 INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT 030 NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR 031 PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, 032 WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) 033 ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE 034 POSSIBILITY OF SUCH DAMAGE. 035 036 */ 037 038import org.hl7.fhir.exceptions.FHIRException; 039import org.hl7.fhir.exceptions.PathEngineException; 040import org.hl7.fhir.r4.context.IWorkerContext; 041import org.hl7.fhir.r4.model.Base; 042import org.hl7.fhir.r4.model.ExpressionNode; 043import org.hl7.fhir.r4.model.Resource; 044import org.hl7.fhir.r4.model.Tuple; 045import org.hl7.fhir.r4.model.TypeDetails; 046import org.hl7.fhir.r4.model.ValueSet; 047import org.hl7.fhir.r4.utils.FHIRPathEngine.ExpressionNodeWithOffset; 048import org.hl7.fhir.r4.utils.FHIRPathEngine.IEvaluationContext; 049import org.hl7.fhir.r4.utils.FHIRPathUtilityClasses.FunctionDetails; 050import org.hl7.fhir.utilities.Utilities; 051 052public class LiquidEngine implements IEvaluationContext { 053 054 public interface ILiquidEngineIcludeResolver { 055 public String fetchInclude(LiquidEngine engine, String name); 056 } 057 058 private IEvaluationContext externalHostServices; 059 private FHIRPathEngine engine; 060 private ILiquidEngineIcludeResolver includeResolver; 061 062 private class LiquidEngineContext { 063 private Object externalContext; 064 private Map<String, Base> vars = new HashMap<>(); 065 066 public LiquidEngineContext(Object externalContext) { 067 super(); 068 this.externalContext = externalContext; 069 } 070 071 public LiquidEngineContext(LiquidEngineContext existing) { 072 super(); 073 externalContext = existing.externalContext; 074 vars.putAll(existing.vars); 075 } 076 } 077 078 public LiquidEngine(IWorkerContext context, IEvaluationContext hostServices) { 079 super(); 080 this.externalHostServices = hostServices; 081 engine = new FHIRPathEngine(context); 082 engine.setHostServices(this); 083 } 084 085 public ILiquidEngineIcludeResolver getIncludeResolver() { 086 return includeResolver; 087 } 088 089 public void setIncludeResolver(ILiquidEngineIcludeResolver includeResolver) { 090 this.includeResolver = includeResolver; 091 } 092 093 public LiquidDocument parse(String source, String sourceName) throws FHIRException { 094 return new LiquidParser(source).parse(sourceName); 095 } 096 097 public String evaluate(LiquidDocument document, Resource resource, Object appContext) throws FHIRException { 098 StringBuilder b = new StringBuilder(); 099 LiquidEngineContext ctxt = new LiquidEngineContext(appContext); 100 for (LiquidNode n : document.body) { 101 n.evaluate(b, resource, ctxt); 102 } 103 return b.toString(); 104 } 105 106 private abstract class LiquidNode { 107 protected void closeUp() { 108 } 109 110 public abstract void evaluate(StringBuilder b, Resource resource, LiquidEngineContext ctxt) throws FHIRException; 111 } 112 113 private class LiquidConstant extends LiquidNode { 114 private String constant; 115 private StringBuilder b = new StringBuilder(); 116 117 @Override 118 protected void closeUp() { 119 constant = b.toString(); 120 b = null; 121 } 122 123 public void addChar(char ch) { 124 b.append(ch); 125 } 126 127 @Override 128 public void evaluate(StringBuilder b, Resource resource, LiquidEngineContext ctxt) { 129 b.append(constant); 130 } 131 } 132 133 private class LiquidStatement extends LiquidNode { 134 private String statement; 135 private ExpressionNode compiled; 136 137 @Override 138 public void evaluate(StringBuilder b, Resource resource, LiquidEngineContext ctxt) throws FHIRException { 139 if (compiled == null) 140 compiled = engine.parse(statement); 141 b.append(engine.evaluateToString(ctxt, resource, resource, resource, compiled)); 142 } 143 } 144 145 private class LiquidIf extends LiquidNode { 146 private String condition; 147 private ExpressionNode compiled; 148 private List<LiquidNode> thenBody = new ArrayList<>(); 149 private List<LiquidNode> elseBody = new ArrayList<>(); 150 151 @Override 152 public void evaluate(StringBuilder b, Resource resource, LiquidEngineContext ctxt) throws FHIRException { 153 if (compiled == null) 154 compiled = engine.parse(condition); 155 boolean ok = engine.evaluateToBoolean(ctxt, resource, resource, resource, compiled); 156 List<LiquidNode> list = ok ? thenBody : elseBody; 157 for (LiquidNode n : list) { 158 n.evaluate(b, resource, ctxt); 159 } 160 } 161 } 162 163 private class LiquidLoop extends LiquidNode { 164 private String varName; 165 private String condition; 166 private ExpressionNode compiled; 167 private List<LiquidNode> body = new ArrayList<>(); 168 169 @Override 170 public void evaluate(StringBuilder b, Resource resource, LiquidEngineContext ctxt) throws FHIRException { 171 if (compiled == null) 172 compiled = engine.parse(condition); 173 List<Base> list = engine.evaluate(ctxt, resource, resource, resource, compiled); 174 LiquidEngineContext lctxt = new LiquidEngineContext(ctxt); 175 for (Base o : list) { 176 lctxt.vars.put(varName, o); 177 for (LiquidNode n : body) { 178 n.evaluate(b, resource, lctxt); 179 } 180 } 181 } 182 } 183 184 private class LiquidInclude extends LiquidNode { 185 private String page; 186 private Map<String, ExpressionNode> params = new HashMap<>(); 187 188 @Override 189 public void evaluate(StringBuilder b, Resource resource, LiquidEngineContext ctxt) throws FHIRException { 190 String src = includeResolver.fetchInclude(LiquidEngine.this, page); 191 LiquidParser parser = new LiquidParser(src); 192 LiquidDocument doc = parser.parse(page); 193 LiquidEngineContext nctxt = new LiquidEngineContext(ctxt.externalContext); 194 Tuple incl = new Tuple(); 195 nctxt.vars.put("include", incl); 196 for (String s : params.keySet()) { 197 incl.addProperty(s, engine.evaluate(ctxt, resource, resource, resource, params.get(s))); 198 } 199 for (LiquidNode n : doc.body) { 200 n.evaluate(b, resource, nctxt); 201 } 202 } 203 } 204 205 public static class LiquidDocument { 206 private List<LiquidNode> body = new ArrayList<>(); 207 208 } 209 210 private class LiquidParser { 211 212 private String source; 213 private int cursor; 214 private String name; 215 216 public LiquidParser(String source) { 217 this.source = source; 218 cursor = 0; 219 } 220 221 private char next1() { 222 if (cursor >= source.length()) 223 return 0; 224 else 225 return source.charAt(cursor); 226 } 227 228 private char next2() { 229 if (cursor >= source.length() - 1) 230 return 0; 231 else 232 return source.charAt(cursor + 1); 233 } 234 235 private char grab() { 236 cursor++; 237 return source.charAt(cursor - 1); 238 } 239 240 public LiquidDocument parse(String name) throws FHIRException { 241 this.name = name; 242 LiquidDocument doc = new LiquidDocument(); 243 parseList(doc.body, new String[0]); 244 return doc; 245 } 246 247 private String parseList(List<LiquidNode> list, String[] terminators) throws FHIRException { 248 String close = null; 249 while (cursor < source.length()) { 250 if (next1() == '{' && (next2() == '%' || next2() == '{')) { 251 if (next2() == '%') { 252 String cnt = parseTag('%'); 253 if (Utilities.existsInList(cnt, terminators)) { 254 close = cnt; 255 break; 256 } else if (cnt.startsWith("if ")) 257 list.add(parseIf(cnt)); 258 else if (cnt.startsWith("loop ")) 259 list.add(parseLoop(cnt.substring(4).trim())); 260 else if (cnt.startsWith("include ")) 261 list.add(parseInclude(cnt.substring(7).trim())); 262 else 263 throw new FHIRException( 264 "Script " + name + ": Script " + name + ": Unknown flow control statement " + cnt); 265 } else { // next2() == '{' 266 list.add(parseStatement()); 267 } 268 } else { 269 if (list.size() == 0 || !(list.get(list.size() - 1) instanceof LiquidConstant)) 270 list.add(new LiquidConstant()); 271 ((LiquidConstant) list.get(list.size() - 1)).addChar(grab()); 272 } 273 } 274 for (LiquidNode n : list) 275 n.closeUp(); 276 if (terminators.length > 0) 277 if (!Utilities.existsInList(close, terminators)) 278 throw new FHIRException( 279 "Script " + name + ": Script " + name + ": Found end of script looking for " + terminators); 280 return close; 281 } 282 283 private LiquidNode parseIf(String cnt) throws FHIRException { 284 LiquidIf res = new LiquidIf(); 285 res.condition = cnt.substring(3).trim(); 286 String term = parseList(res.thenBody, new String[] { "else", "endif" }); 287 if ("else".equals(term)) 288 term = parseList(res.elseBody, new String[] { "endif" }); 289 return res; 290 } 291 292 private LiquidNode parseInclude(String cnt) throws FHIRException { 293 int i = 1; 294 while (i < cnt.length() && !Character.isWhitespace(cnt.charAt(i))) 295 i++; 296 if (i == cnt.length() || i == 0) 297 throw new FHIRException("Script " + name + ": Error reading include: " + cnt); 298 LiquidInclude res = new LiquidInclude(); 299 res.page = cnt.substring(0, i); 300 while (i < cnt.length() && Character.isWhitespace(cnt.charAt(i))) 301 i++; 302 while (i < cnt.length()) { 303 int j = i; 304 while (i < cnt.length() && cnt.charAt(i) != '=') 305 i++; 306 if (i >= cnt.length() || j == i) 307 throw new FHIRException("Script " + name + ": Error reading include: " + cnt); 308 String n = cnt.substring(j, i); 309 if (res.params.containsKey(n)) 310 throw new FHIRException("Script " + name + ": Error reading include: " + cnt); 311 i++; 312 ExpressionNodeWithOffset t = engine.parsePartial(cnt, i); 313 i = t.getOffset(); 314 res.params.put(n, t.getNode()); 315 while (i < cnt.length() && Character.isWhitespace(cnt.charAt(i))) 316 i++; 317 } 318 return res; 319 } 320 321 private LiquidNode parseLoop(String cnt) throws FHIRException { 322 int i = 0; 323 while (!Character.isWhitespace(cnt.charAt(i))) 324 i++; 325 LiquidLoop res = new LiquidLoop(); 326 res.varName = cnt.substring(0, i); 327 while (Character.isWhitespace(cnt.charAt(i))) 328 i++; 329 int j = i; 330 while (!Character.isWhitespace(cnt.charAt(i))) 331 i++; 332 if (!"in".equals(cnt.substring(j, i))) 333 throw new FHIRException("Script " + name + ": Script " + name + ": Error reading loop: " + cnt); 334 res.condition = cnt.substring(i).trim(); 335 parseList(res.body, new String[] { "endloop" }); 336 return res; 337 } 338 339 private String parseTag(char ch) throws FHIRException { 340 grab(); 341 grab(); 342 StringBuilder b = new StringBuilder(); 343 while (cursor < source.length() && !(next1() == '%' && next2() == '}')) { 344 b.append(grab()); 345 } 346 if (!(next1() == '%' && next2() == '}')) 347 throw new FHIRException("Script " + name + ": Unterminated Liquid statement {% " + b.toString()); 348 grab(); 349 grab(); 350 return b.toString().trim(); 351 } 352 353 private LiquidStatement parseStatement() throws FHIRException { 354 grab(); 355 grab(); 356 StringBuilder b = new StringBuilder(); 357 while (cursor < source.length() && !(next1() == '}' && next2() == '}')) { 358 b.append(grab()); 359 } 360 if (!(next1() == '}' && next2() == '}')) 361 throw new FHIRException("Script " + name + ": Unterminated Liquid statement {{ " + b.toString()); 362 grab(); 363 grab(); 364 LiquidStatement res = new LiquidStatement(); 365 res.statement = b.toString().trim(); 366 return res; 367 } 368 369 } 370 371 @Override 372 public List<Base> resolveConstant(Object appContext, String name, boolean beforeContext) throws PathEngineException { 373 LiquidEngineContext ctxt = (LiquidEngineContext) appContext; 374 if (ctxt.vars.containsKey(name)) 375 return new ArrayList<>(Arrays.asList(ctxt.vars.get(name))); 376 if (externalHostServices == null) 377 return null; 378 return externalHostServices.resolveConstant(ctxt.externalContext, name, beforeContext); 379 } 380 381 @Override 382 public TypeDetails resolveConstantType(Object appContext, String name) throws PathEngineException { 383 if (externalHostServices == null) 384 return null; 385 LiquidEngineContext ctxt = (LiquidEngineContext) appContext; 386 return externalHostServices.resolveConstantType(ctxt.externalContext, name); 387 } 388 389 @Override 390 public boolean log(String argument, List<Base> focus) { 391 if (externalHostServices == null) 392 return false; 393 return externalHostServices.log(argument, focus); 394 } 395 396 @Override 397 public FunctionDetails resolveFunction(String functionName) { 398 if (externalHostServices == null) 399 return null; 400 return externalHostServices.resolveFunction(functionName); 401 } 402 403 @Override 404 public TypeDetails checkFunction(Object appContext, String functionName, List<TypeDetails> parameters) 405 throws PathEngineException { 406 if (externalHostServices == null) 407 return null; 408 LiquidEngineContext ctxt = (LiquidEngineContext) appContext; 409 return externalHostServices.checkFunction(ctxt.externalContext, functionName, parameters); 410 } 411 412 @Override 413 public List<Base> executeFunction(Object appContext, List<Base> focus, String functionName, 414 List<List<Base>> parameters) { 415 if (externalHostServices == null) 416 return null; 417 LiquidEngineContext ctxt = (LiquidEngineContext) appContext; 418 return externalHostServices.executeFunction(ctxt.externalContext, focus, functionName, parameters); 419 } 420 421 @Override 422 public Base resolveReference(Object appContext, String url, Base base) throws FHIRException { 423 if (externalHostServices == null) 424 return null; 425 LiquidEngineContext ctxt = (LiquidEngineContext) appContext; 426 return resolveReference(ctxt.externalContext, url, base); 427 } 428 429 @Override 430 public boolean conformsToProfile(Object appContext, Base item, String url) throws FHIRException { 431 if (externalHostServices == null) 432 return false; 433 LiquidEngineContext ctxt = (LiquidEngineContext) appContext; 434 return conformsToProfile(ctxt.externalContext, item, url); 435 } 436 437 @Override 438 public ValueSet resolveValueSet(Object appContext, String url) { 439 LiquidEngineContext ctxt = (LiquidEngineContext) appContext; 440 if (externalHostServices != null) 441 return externalHostServices.resolveValueSet(ctxt.externalContext, url); 442 else 443 return engine.getWorker().fetchResource(ValueSet.class, url); 444 } 445 446}