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