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