
001package org.hl7.fhir.r5.utils; 002 003import java.io.IOException; 004import java.util.ArrayList; 005import java.util.Arrays; 006import java.util.Collections; 007import java.util.HashMap; 008import java.util.List; 009import java.util.Map; 010 011/* 012 Copyright (c) 2011+, HL7, Inc. 013 All rights reserved. 014 015 Redistribution and use in source and binary forms, with or without modification, 016 are permitted provided that the following conditions are met: 017 018 * Redistributions of source code must retain the above copyright notice, this 019 list of conditions and the following disclaimer. 020 * Redistributions in binary form must reproduce the above copyright notice, 021 this list of conditions and the following disclaimer in the documentation 022 and/or other materials provided with the distribution. 023 * Neither the name of HL7 nor the names of its contributors may be used to 024 endorse or promote products derived from this software without specific 025 prior written permission. 026 027 THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND 028 ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 029 WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. 030 IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, 031 INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT 032 NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR 033 PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, 034 WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) 035 ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE 036 POSSIBILITY OF SUCH DAMAGE. 037 038 */ 039 040import org.hl7.fhir.exceptions.FHIRException; 041import org.hl7.fhir.exceptions.FHIRFormatError; 042import org.hl7.fhir.exceptions.PathEngineException; 043import org.hl7.fhir.r5.context.IWorkerContext; 044import org.hl7.fhir.r5.elementmodel.Element; 045import org.hl7.fhir.r5.model.Base; 046import org.hl7.fhir.r5.model.ExpressionNode; 047import org.hl7.fhir.r5.model.Resource; 048import org.hl7.fhir.r5.model.Tuple; 049import org.hl7.fhir.r5.model.TypeDetails; 050import org.hl7.fhir.r5.model.ValueSet; 051import org.hl7.fhir.r5.renderers.utils.BaseWrappers.BaseWrapper; 052import org.hl7.fhir.r5.renderers.utils.BaseWrappers.ResourceWrapper; 053import org.hl7.fhir.r5.utils.FHIRPathEngine.ExpressionNodeWithOffset; 054import org.hl7.fhir.r5.utils.FHIRPathEngine.IEvaluationContext; 055import org.hl7.fhir.r5.utils.LiquidEngine.ILiquidRenderingSupport; 056import org.hl7.fhir.utilities.Utilities; 057import org.hl7.fhir.utilities.xhtml.NodeType; 058import org.hl7.fhir.utilities.xhtml.XhtmlNode; 059 060public class LiquidEngine implements IEvaluationContext { 061 062 public interface ILiquidRenderingSupport { 063 String renderForLiquid(Object appContext, Base i) throws FHIRException; 064 } 065 066 public interface ILiquidEngineIncludeResolver { 067 public String fetchInclude(LiquidEngine engine, String name); 068 } 069 070 private IEvaluationContext externalHostServices; 071 private FHIRPathEngine engine; 072 private ILiquidEngineIncludeResolver includeResolver; 073 private ILiquidRenderingSupport renderingSupport; 074 075 private class LiquidEngineContext { 076 private Object externalContext; 077 private Map<String, Base> vars = new HashMap<>(); 078 079 public LiquidEngineContext(Object externalContext) { 080 super(); 081 this.externalContext = externalContext; 082 } 083 084 public LiquidEngineContext(LiquidEngineContext existing) { 085 super(); 086 externalContext = existing.externalContext; 087 vars.putAll(existing.vars); 088 } 089 } 090 091 public LiquidEngine(IWorkerContext context, IEvaluationContext hostServices) { 092 super(); 093 this.externalHostServices = hostServices; 094 engine = new FHIRPathEngine(context); 095 engine.setHostServices(this); 096 engine.setLiquidMode(true); 097 } 098 099 public ILiquidEngineIncludeResolver getIncludeResolver() { 100 return includeResolver; 101 } 102 103 public void setIncludeResolver(ILiquidEngineIncludeResolver includeResolver) { 104 this.includeResolver = includeResolver; 105 } 106 107 public ILiquidRenderingSupport getRenderingSupport() { 108 return renderingSupport; 109 } 110 111 public void setRenderingSupport(ILiquidRenderingSupport renderingSupport) { 112 this.renderingSupport = renderingSupport; 113 } 114 115 public LiquidDocument parse(String source, String sourceName) throws FHIRException { 116 return new LiquidParser(source).parse(sourceName); 117 } 118 119 public String evaluate(LiquidDocument document, Base resource, Object appContext) throws FHIRException { 120 StringBuilder b = new StringBuilder(); 121 LiquidEngineContext ctxt = new LiquidEngineContext(appContext); 122 for (LiquidNode n : document.body) { 123 n.evaluate(b, resource, ctxt); 124 } 125 return b.toString(); 126 } 127 128 129 private abstract class LiquidNode { 130 protected void closeUp() { 131 } 132 133 public abstract void evaluate(StringBuilder b, Base resource, LiquidEngineContext ctxt) throws FHIRException; 134 } 135 136 private class LiquidConstant extends LiquidNode { 137 private String constant; 138 private StringBuilder b = new StringBuilder(); 139 140 @Override 141 protected void closeUp() { 142 constant = b.toString(); 143 b = null; 144 } 145 146 public void addChar(char ch) { 147 b.append(ch); 148 } 149 150 @Override 151 public void evaluate(StringBuilder b, Base resource, LiquidEngineContext ctxt) { 152 b.append(constant); 153 } 154 155 } 156 157 private enum LiquidFilter { 158 PREPEND; 159 160 public static LiquidFilter fromCode(String code) { 161 if ("prepend".equals(code)) { 162 return PREPEND; 163 } 164 return null; 165 } 166 } 167 168 private class LiquidExpressionNode { 169 private LiquidFilter filter; // null at root 170 private ExpressionNode expression; // null for some filters 171 public LiquidExpressionNode(LiquidFilter filter, ExpressionNode expression) { 172 super(); 173 this.filter = filter; 174 this.expression = expression; 175 } 176 177 } 178 179 private class LiquidStatement extends LiquidNode { 180 private String statement; 181 private List<LiquidExpressionNode> compiled = new ArrayList<>(); 182 183 @Override 184 public void evaluate(StringBuilder b, Base resource, LiquidEngineContext ctxt) throws FHIRException { 185 if (compiled.size() == 0) { 186 FHIRLexer lexer = new FHIRLexer(statement, "liquid statement"); 187 lexer.setLiquidMode(true); 188 compiled.add(new LiquidExpressionNode(null, engine.parse(lexer))); 189 while (!lexer.done()) { 190 if (lexer.getCurrent().equals("||")) { 191 lexer.next(); 192 String f = lexer.getCurrent(); 193 LiquidFilter filter = LiquidFilter.fromCode(f); 194 if (filter == null) { 195 lexer.error("Unknown Liquid filter '"+f+"'"); 196 } 197 lexer.next(); 198 if (!lexer.done() && lexer.getCurrent().equals(":")) { 199 lexer.next(); 200 compiled.add(new LiquidExpressionNode(filter, engine.parse(lexer))); 201 } else { 202 compiled.add(new LiquidExpressionNode(filter, null)); 203 } 204 } else { 205 lexer.error("Unexpected syntax parsing liquid statement"); 206 } 207 } 208 } 209 210 String t = null; 211 for (LiquidExpressionNode i : compiled) { 212 if (i.filter == null) { // first 213 t = stmtToString(ctxt, engine.evaluate(ctxt, resource, resource, resource, i.expression)); 214 } else switch (i.filter) { 215 case PREPEND: 216 t = stmtToString(ctxt, engine.evaluate(ctxt, resource, resource, resource, i.expression)) + t; 217 break; 218 } 219 } 220 b.append(t); 221 } 222 223 private String stmtToString(LiquidEngineContext ctxt, List<Base> items) { 224 StringBuilder b = new StringBuilder(); 225 boolean first = true; 226 for (Base i : items) { 227 if (first) first = false; else b.append(", "); 228 String s = renderingSupport != null ? renderingSupport.renderForLiquid(ctxt.externalContext, i) : null; 229 b.append(s != null ? s : engine.convertToString(i)); 230 } 231 return b.toString(); 232 } 233 } 234 235 private class LiquidElsIf extends LiquidNode { 236 private String condition; 237 private ExpressionNode compiled; 238 private List<LiquidNode> body = new ArrayList<>(); 239 240 @Override 241 public void evaluate(StringBuilder b, Base resource, LiquidEngineContext ctxt) throws FHIRException { 242 for (LiquidNode n : body) { 243 n.evaluate(b, resource, ctxt); 244 } 245 } 246 } 247 248 private class LiquidIf extends LiquidNode { 249 private String condition; 250 private ExpressionNode compiled; 251 private List<LiquidNode> thenBody = new ArrayList<>(); 252 private List<LiquidElsIf> elseIf = new ArrayList<>(); 253 private List<LiquidNode> elseBody = new ArrayList<>(); 254 255 @Override 256 public void evaluate(StringBuilder b, Base resource, LiquidEngineContext ctxt) throws FHIRException { 257 if (compiled == null) 258 compiled = engine.parse(condition); 259 boolean ok = engine.evaluateToBoolean(ctxt, resource, resource, resource, compiled); 260 List<LiquidNode> list = null; 261 if (ok) { 262 list = thenBody; 263 264 } else { 265 list = elseBody; 266 for (LiquidElsIf i : elseIf) { 267 if (i.compiled == null) 268 i.compiled = engine.parse(i.condition); 269 ok = engine.evaluateToBoolean(ctxt, resource, resource, resource, i.compiled); 270 if (ok) { 271 list = i.body; 272 break; 273 } 274 } 275 } 276 for (LiquidNode n : list) { 277 n.evaluate(b, resource, ctxt); 278 } 279 } 280 } 281 282 private class LiquidContinueExecuted extends FHIRException { 283 private static final long serialVersionUID = 4748737094188943721L; 284 } 285 286 private class LiquidContinue extends LiquidNode { 287 public void evaluate(StringBuilder b, Base resource, LiquidEngineContext ctxt) throws FHIRException { 288 throw new LiquidContinueExecuted(); 289 } 290 } 291 292 private class LiquidBreakExecuted extends FHIRException { 293 private static final long serialVersionUID = 6328496371172871082L; 294 } 295 296 private class LiquidBreak extends LiquidNode { 297 public void evaluate(StringBuilder b, Base resource, LiquidEngineContext ctxt) throws FHIRException { 298 throw new LiquidBreakExecuted(); 299 } 300 } 301 302 private class LiquidCycle extends LiquidNode { 303 private List<String> list = new ArrayList<>(); 304 private int cursor = 0; 305 306 public void evaluate(StringBuilder b, Base resource, LiquidEngineContext ctxt) throws FHIRException { 307 b.append(list.get(cursor)); 308 cursor++; 309 if (cursor == list.size()) { 310 cursor = 0; 311 } 312 } 313 } 314 315 private class LiquidFor extends LiquidNode { 316 private String varName; 317 private String condition; 318 private ExpressionNode compiled; 319 private boolean reversed = false; 320 private int limit = -1; 321 private int offset = -1; 322 private List<LiquidNode> body = new ArrayList<>(); 323 private List<LiquidNode> elseBody = new ArrayList<>(); 324 325 @Override 326 public void evaluate(StringBuilder b, Base resource, LiquidEngineContext ctxt) throws FHIRException { 327 if (compiled == null) { 328 ExpressionNodeWithOffset po = engine.parsePartial(condition, 0); 329 compiled = po.getNode(); 330 if (po.getOffset() < condition.length()) { 331 parseModifiers(condition.substring(po.getOffset())); 332 } 333 } 334 List<Base> list = engine.evaluate(ctxt, resource, resource, resource, compiled); 335 LiquidEngineContext lctxt = new LiquidEngineContext(ctxt); 336 if (list.isEmpty()) { 337 for (LiquidNode n : elseBody) { 338 n.evaluate(b, resource, lctxt); 339 } 340 } else { 341 if (reversed) { 342 Collections.reverse(list); 343 } 344 int i = 0; 345 for (Base o : list) { 346 if (offset >= 0 && i < offset) { 347 i++; 348 continue; 349 } 350 if (limit >= 0 && i == limit) { 351 break; 352 } 353 lctxt.vars.put(varName, o); 354 boolean wantBreak = false; 355 for (LiquidNode n : body) { 356 try { 357 n.evaluate(b, resource, lctxt); 358 } catch (LiquidContinueExecuted e) { 359 break; 360 } catch (LiquidBreakExecuted e) { 361 wantBreak = true; 362 break; 363 } 364 } 365 if (wantBreak) { 366 break; 367 } 368 i++; 369 } 370 } 371 } 372 373 private void parseModifiers(String cnt) { 374 String src = cnt; 375 while (!Utilities.noString(cnt)) { 376 if (cnt.startsWith("reversed")) { 377 reversed = true; 378 cnt = cnt.substring(8); 379 } else if (cnt.startsWith("limit")) { 380 cnt = cnt.substring(5).trim(); 381 if (!cnt.startsWith(":")) { 382 throw new FHIRException("Exception evaluating "+src+": limit is not followed by ':'"); 383 } 384 cnt = cnt.substring(1).trim(); 385 int i = 0; 386 while (i < cnt.length() && Character.isDigit(cnt.charAt(i))) { 387 i++; 388 } 389 if (i == 0) { 390 throw new FHIRException("Exception evaluating "+src+": limit is not followed by a number"); 391 } 392 limit = Integer.parseInt(cnt.substring(0, i)); 393 cnt = cnt.substring(i); 394 } else if (cnt.startsWith("offset")) { 395 cnt = cnt.substring(6).trim(); 396 if (!cnt.startsWith(":")) { 397 throw new FHIRException("Exception evaluating "+src+": limit is not followed by ':'"); 398 } 399 cnt = cnt.substring(1).trim(); 400 int i = 0; 401 while (i < cnt.length() && Character.isDigit(cnt.charAt(i))) { 402 i++; 403 } 404 if (i == 0) { 405 throw new FHIRException("Exception evaluating "+src+": limit is not followed by a number"); 406 } 407 offset = Integer.parseInt(cnt.substring(0, i)); 408 cnt = cnt.substring(i); 409 } else { 410 throw new FHIRException("Exception evaluating "+src+": unexpected content at "+cnt); 411 } 412 } 413 } 414 } 415 416 private class LiquidInclude extends LiquidNode { 417 private String page; 418 private Map<String, ExpressionNode> params = new HashMap<>(); 419 420 @Override 421 public void evaluate(StringBuilder b, Base resource, LiquidEngineContext ctxt) throws FHIRException { 422 String src = includeResolver.fetchInclude(LiquidEngine.this, page); 423 LiquidParser parser = new LiquidParser(src); 424 LiquidDocument doc = parser.parse(page); 425 LiquidEngineContext nctxt = new LiquidEngineContext(ctxt.externalContext); 426 Tuple incl = new Tuple(); 427 nctxt.vars.put("include", incl); 428 for (String s : params.keySet()) { 429 incl.addProperty(s, engine.evaluate(ctxt, resource, resource, resource, params.get(s))); 430 } 431 for (LiquidNode n : doc.body) { 432 n.evaluate(b, resource, nctxt); 433 } 434 } 435 } 436 437 public static class LiquidDocument { 438 private List<LiquidNode> body = new ArrayList<>(); 439 440 } 441 442 private class LiquidParser { 443 444 private String source; 445 private int cursor; 446 private String name; 447 448 public LiquidParser(String source) { 449 this.source = source; 450 cursor = 0; 451 } 452 453 private char next1() { 454 if (cursor >= source.length()) 455 return 0; 456 else 457 return source.charAt(cursor); 458 } 459 460 private char next2() { 461 if (cursor >= source.length() - 1) 462 return 0; 463 else 464 return source.charAt(cursor + 1); 465 } 466 467 private char grab() { 468 cursor++; 469 return source.charAt(cursor - 1); 470 } 471 472 public LiquidDocument parse(String name) throws FHIRException { 473 this.name = name; 474 LiquidDocument doc = new LiquidDocument(); 475 parseList(doc.body, false, new String[0]); 476 return doc; 477 } 478 479 public LiquidCycle parseCycle(String cnt) { 480 LiquidCycle res = new LiquidCycle(); 481 cnt = "," + cnt.substring(5).trim(); 482 while (!Utilities.noString(cnt)) { 483 if (!cnt.startsWith(",")) { 484 throw new FHIRException("Script " + name + ": Script " + name + ": Found " + cnt.charAt(0) + " expecting ',' parsing cycle"); 485 } 486 cnt = cnt.substring(1).trim(); 487 if (!cnt.startsWith("\"")) { 488 throw new FHIRException("Script " + name + ": Script " + name + ": Found " + cnt.charAt(0) + " expecting '\"' parsing cycle"); 489 } 490 cnt = cnt.substring(1); 491 int i = 0; 492 while (i < cnt.length() && cnt.charAt(i) != '"') { 493 i++; 494 } 495 if (i == cnt.length()) { 496 throw new FHIRException("Script " + name + ": Script " + name + ": Found unterminated string parsing cycle"); 497 } 498 res.list.add(cnt.substring(0, i)); 499 cnt = cnt.substring(i + 1).trim(); 500 } 501 return res; 502 } 503 504 private String parseList(List<LiquidNode> list, boolean inLoop, String[] terminators) throws FHIRException { 505 String close = null; 506 while (cursor < source.length()) { 507 if (next1() == '{' && (next2() == '%' || next2() == '{')) { 508 if (next2() == '%') { 509 String cnt = parseTag('%'); 510 if (isTerminator(cnt, terminators)) { 511 close = cnt; 512 break; 513 } else if (cnt.startsWith("if ")) 514 list.add(parseIf(cnt, inLoop)); 515 else if (cnt.startsWith("loop ")) // loop is deprecated, but still 516 // supported 517 list.add(parseLoop(cnt.substring(4).trim())); 518 else if (cnt.startsWith("for ")) 519 list.add(parseFor(cnt.substring(3).trim())); 520 else if (inLoop && cnt.equals("continue")) 521 list.add(new LiquidContinue()); 522 else if (inLoop && cnt.equals("break")) 523 list.add(new LiquidBreak()); 524 else if (inLoop && cnt.startsWith("cycle ")) 525 list.add(parseCycle(cnt)); 526 else if (cnt.startsWith("include ")) 527 list.add(parseInclude(cnt.substring(7).trim())); 528 else 529 throw new FHIRException("Script " + name + ": Script " + name + ": Unknown flow control statement " + cnt); 530 } else { // next2() == '{' 531 list.add(parseStatement()); 532 } 533 } else { 534 if (list.size() == 0 || !(list.get(list.size() - 1) instanceof LiquidConstant)) 535 list.add(new LiquidConstant()); 536 ((LiquidConstant) list.get(list.size() - 1)).addChar(grab()); 537 } 538 } 539 for (LiquidNode n : list) 540 n.closeUp(); 541 if (terminators.length > 0) 542 if (!isTerminator(close, terminators)) 543 throw new FHIRException("Script " + name + ": Script " + name + ": Found end of script looking for " + terminators); 544 return close; 545 } 546 547 private boolean isTerminator(String cnt, String[] terminators) { 548 if (Utilities.noString(cnt)) { 549 return false; 550 } 551 for (String t : terminators) { 552 if (t.endsWith(" ")) { 553 if (cnt.startsWith(t)) { 554 return true; 555 } 556 } else { 557 if (cnt.equals(t)) { 558 return true; 559 } 560 } 561 } 562 return false; 563 } 564 565 private LiquidNode parseIf(String cnt, boolean inLoop) throws FHIRException { 566 LiquidIf res = new LiquidIf(); 567 res.condition = cnt.substring(3).trim(); 568 String term = parseList(res.thenBody, inLoop, new String[] { "else", "elsif ", "endif" }); 569 while (term.startsWith("elsif ")) { 570 LiquidElsIf elsIf = new LiquidElsIf(); 571 res.elseIf.add(elsIf); 572 elsIf.condition = term.substring(5).trim(); 573 term = parseList(elsIf.body, inLoop, new String[] { "elsif ", "else", "endif" }); 574 } 575 if ("else".equals(term)) { 576 term = parseList(res.elseBody, inLoop, new String[] { "endif" }); 577 } 578 579 return res; 580 } 581 582 private LiquidNode parseInclude(String cnt) throws FHIRException { 583 int i = 1; 584 while (i < cnt.length() && !Character.isWhitespace(cnt.charAt(i))) 585 i++; 586 if (i == cnt.length() || i == 0) 587 throw new FHIRException("Script " + name + ": Error reading include: " + cnt); 588 LiquidInclude res = new LiquidInclude(); 589 res.page = cnt.substring(0, i); 590 while (i < cnt.length() && Character.isWhitespace(cnt.charAt(i))) 591 i++; 592 while (i < cnt.length()) { 593 int j = i; 594 while (i < cnt.length() && cnt.charAt(i) != '=') 595 i++; 596 if (i >= cnt.length() || j == i) 597 throw new FHIRException("Script " + name + ": Error reading include: " + cnt); 598 String n = cnt.substring(j, i); 599 if (res.params.containsKey(n)) 600 throw new FHIRException("Script " + name + ": Error reading include: " + cnt); 601 i++; 602 ExpressionNodeWithOffset t = engine.parsePartial(cnt, i); 603 i = t.getOffset(); 604 res.params.put(n, t.getNode()); 605 while (i < cnt.length() && Character.isWhitespace(cnt.charAt(i))) 606 i++; 607 } 608 return res; 609 } 610 611 private LiquidNode parseLoop(String cnt) throws FHIRException { 612 int i = 0; 613 while (!Character.isWhitespace(cnt.charAt(i))) 614 i++; 615 LiquidFor res = new LiquidFor(); 616 res.varName = cnt.substring(0, i); 617 while (Character.isWhitespace(cnt.charAt(i))) 618 i++; 619 int j = i; 620 while (!Character.isWhitespace(cnt.charAt(i))) 621 i++; 622 if (!"in".equals(cnt.substring(j, i))) 623 throw new FHIRException("Script " + name + ": Script " + name + ": Error reading loop: " + cnt); 624 res.condition = cnt.substring(i).trim(); 625 parseList(res.body, false, new String[] { "endloop" }); 626 return res; 627 } 628 629 private LiquidNode parseFor(String cnt) throws FHIRException { 630 int i = 0; 631 while (!Character.isWhitespace(cnt.charAt(i))) 632 i++; 633 LiquidFor res = new LiquidFor(); 634 res.varName = cnt.substring(0, i); 635 while (Character.isWhitespace(cnt.charAt(i))) 636 i++; 637 int j = i; 638 while (!Character.isWhitespace(cnt.charAt(i))) 639 i++; 640 if (!"in".equals(cnt.substring(j, i))) 641 throw new FHIRException("Script " + name + ": Script " + name + ": Error reading loop: " + cnt); 642 res.condition = cnt.substring(i).trim(); 643 String term = parseList(res.body, true, new String[] { "endfor", "else" }); 644 if ("else".equals(term)) { 645 parseList(res.elseBody, false, new String[] { "endfor" }); 646 } 647 return res; 648 } 649 650 651 private String parseTag(char ch) throws FHIRException { 652 grab(); 653 grab(); 654 StringBuilder b = new StringBuilder(); 655 while (cursor < source.length() && !(next1() == '%' && next2() == '}')) { 656 b.append(grab()); 657 } 658 if (!(next1() == '%' && next2() == '}')) 659 throw new FHIRException("Script " + name + ": Unterminated Liquid statement {% " + b.toString()); 660 grab(); 661 grab(); 662 return b.toString().trim(); 663 } 664 665 private LiquidStatement parseStatement() throws FHIRException { 666 grab(); 667 grab(); 668 StringBuilder b = new StringBuilder(); 669 while (cursor < source.length() && !(next1() == '}' && next2() == '}')) { 670 b.append(grab()); 671 } 672 if (!(next1() == '}' && next2() == '}')) 673 throw new FHIRException("Script " + name + ": Unterminated Liquid statement {{ " + b.toString()); 674 grab(); 675 grab(); 676 LiquidStatement res = new LiquidStatement(); 677 res.statement = b.toString().trim(); 678 return res; 679 } 680 681 } 682 683 @Override 684 public List<Base> resolveConstant(Object appContext, String name, boolean beforeContext) throws PathEngineException { 685 LiquidEngineContext ctxt = (LiquidEngineContext) appContext; 686 if (ctxt.vars.containsKey(name)) 687 return new ArrayList<Base>(Arrays.asList(ctxt.vars.get(name))); 688 if (externalHostServices == null) 689 return new ArrayList<Base>(); 690 return externalHostServices.resolveConstant(ctxt.externalContext, name, beforeContext); 691 } 692 693 @Override 694 public TypeDetails resolveConstantType(Object appContext, String name) throws PathEngineException { 695 if (externalHostServices == null) 696 return null; 697 LiquidEngineContext ctxt = (LiquidEngineContext) appContext; 698 return externalHostServices.resolveConstantType(ctxt.externalContext, name); 699 } 700 701 @Override 702 public boolean log(String argument, List<Base> focus) { 703 if (externalHostServices == null) 704 return false; 705 return externalHostServices.log(argument, focus); 706 } 707 708 @Override 709 public FunctionDetails resolveFunction(String functionName) { 710 if (externalHostServices == null) 711 return null; 712 return externalHostServices.resolveFunction(functionName); 713 } 714 715 @Override 716 public TypeDetails checkFunction(Object appContext, String functionName, List<TypeDetails> parameters) throws PathEngineException { 717 if (externalHostServices == null) 718 return null; 719 LiquidEngineContext ctxt = (LiquidEngineContext) appContext; 720 return externalHostServices.checkFunction(ctxt.externalContext, functionName, parameters); 721 } 722 723 @Override 724 public List<Base> executeFunction(Object appContext, List<Base> focus, String functionName, List<List<Base>> parameters) { 725 if (externalHostServices == null) 726 return null; 727 LiquidEngineContext ctxt = (LiquidEngineContext) appContext; 728 return externalHostServices.executeFunction(ctxt.externalContext, focus, functionName, parameters); 729 } 730 731 @Override 732 public Base resolveReference(Object appContext, String url, Base refContext) throws FHIRException { 733 if (externalHostServices == null) 734 return null; 735 LiquidEngineContext ctxt = (LiquidEngineContext) appContext; 736 return resolveReference(ctxt.externalContext, url, refContext); 737 } 738 739 @Override 740 public boolean conformsToProfile(Object appContext, Base item, String url) throws FHIRException { 741 if (externalHostServices == null) 742 return false; 743 LiquidEngineContext ctxt = (LiquidEngineContext) appContext; 744 return conformsToProfile(ctxt.externalContext, item, url); 745 } 746 747 @Override 748 public ValueSet resolveValueSet(Object appContext, String url) { 749 LiquidEngineContext ctxt = (LiquidEngineContext) appContext; 750 if (externalHostServices != null) 751 return externalHostServices.resolveValueSet(ctxt.externalContext, url); 752 else 753 return engine.getWorker().fetchResource(ValueSet.class, url); 754 } 755 756 /** 757 * Lightweight method to replace fixed constants in resources 758 * 759 * @param node 760 * @param vars 761 * @return 762 */ 763 public boolean replaceInHtml(XhtmlNode node, Map<String, String> vars) { 764 boolean replaced = false; 765 if (node.getNodeType() == NodeType.Text || node.getNodeType() == NodeType.Comment) { 766 String cnt = node.getContent(); 767 for (String n : vars.keySet()) { 768 cnt = cnt.replace(n, vars.get(n)); 769 } 770 if (!cnt.equals(node.getContent())) { 771 node.setContent(cnt); 772 replaced = true; 773 } 774 } else if (node.getNodeType() == NodeType.Element || node.getNodeType() == NodeType.Document) { 775 for (XhtmlNode c : node.getChildNodes()) { 776 if (replaceInHtml(c, vars)) { 777 replaced = true; 778 } 779 } 780 for (String an : node.getAttributes().keySet()) { 781 String cnt = node.getAttributes().get(an); 782 for (String n : vars.keySet()) { 783 cnt = cnt.replace(n, vars.get(n)); 784 } 785 if (!cnt.equals(node.getAttributes().get(an))) { 786 node.getAttributes().put(an, cnt); 787 replaced = true; 788 } 789 } 790 } 791 return replaced; 792 } 793 794}