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