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}