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