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