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