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