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