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