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