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