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