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