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.Tuple;
045import org.hl7.fhir.r5.model.TypeDetails;
046import org.hl7.fhir.r5.model.ValueSet;
047import org.hl7.fhir.r5.utils.FHIRPathEngine.ExpressionNodeWithOffset;
048import org.hl7.fhir.r5.utils.FHIRPathEngine.IEvaluationContext;
049import org.hl7.fhir.r5.utils.FHIRPathUtilityClasses.FunctionDetails;
050import org.hl7.fhir.utilities.Utilities;
051import org.hl7.fhir.utilities.i18n.I18nConstants;
052import org.hl7.fhir.utilities.xhtml.NodeType;
053import org.hl7.fhir.utilities.xhtml.XhtmlNode;
054
055public class LiquidEngine implements IEvaluationContext {
056
057  public interface ILiquidRenderingSupport {
058    String renderForLiquid(Object appContext, Base i) throws FHIRException;
059  }
060
061  public interface ILiquidEngineIncludeResolver {
062    public String fetchInclude(LiquidEngine engine, String name);
063  }
064
065  private IEvaluationContext externalHostServices;
066  private FHIRPathEngine engine;
067  private ILiquidEngineIncludeResolver includeResolver;
068  private ILiquidRenderingSupport renderingSupport;
069
070  private class LiquidEngineContext {
071    private Object externalContext;
072    private Map<String, Base> loopVars = new HashMap<>();
073    private Map<String, Base> globalVars = new HashMap<>();
074
075    public LiquidEngineContext(Object externalContext) {
076      super();
077      this.externalContext = externalContext;
078      globalVars = new HashMap<>();
079    }
080
081    public LiquidEngineContext(Object externalContext, LiquidEngineContext existing) {
082      super();
083      this.externalContext = externalContext;
084      loopVars.putAll(existing.loopVars);
085      globalVars = existing.globalVars;
086    }
087
088    public LiquidEngineContext(LiquidEngineContext existing) {
089      super();
090      externalContext = existing.externalContext;
091      loopVars.putAll(existing.loopVars);
092      globalVars = existing.globalVars;
093    }
094  }
095
096  public LiquidEngine(IWorkerContext context, IEvaluationContext hostServices) {
097    super();
098    this.externalHostServices = hostServices;
099    engine = new FHIRPathEngine(context);
100    engine.setHostServices(this);
101    engine.setLiquidMode(true);
102  }
103
104  public ILiquidEngineIncludeResolver getIncludeResolver() {
105    return includeResolver;
106  }
107
108  public void setIncludeResolver(ILiquidEngineIncludeResolver includeResolver) {
109    this.includeResolver = includeResolver;
110  }
111
112  public ILiquidRenderingSupport getRenderingSupport() {
113    return renderingSupport;
114  }
115
116  public void setRenderingSupport(ILiquidRenderingSupport renderingSupport) {
117    this.renderingSupport = renderingSupport;
118  }
119
120  public LiquidDocument parse(String source, String sourceName) throws FHIRException {
121    return new LiquidParser(source).parse(sourceName);
122  }
123
124  public String evaluate(LiquidDocument document, Base resource, Object appContext) throws FHIRException {
125    StringBuilder b = new StringBuilder();
126    LiquidEngineContext ctxt = new LiquidEngineContext(appContext);
127    for (LiquidNode n : document.body) {
128      n.evaluate(b, resource, ctxt);
129    }
130    return b.toString();
131  }
132  
133
134  private abstract class LiquidNode {
135    protected void closeUp() {
136    }
137
138    public abstract void evaluate(StringBuilder b, Base resource, LiquidEngineContext ctxt) throws FHIRException;
139  }
140
141  private class LiquidConstant extends LiquidNode {
142    private String constant;
143    private StringBuilder b = new StringBuilder();
144
145    @Override
146    protected void closeUp() {
147      constant = b.toString();
148      b = null;
149    }
150
151    public void addChar(char ch) {
152      b.append(ch);
153    }
154
155    @Override
156    public void evaluate(StringBuilder b, Base resource, LiquidEngineContext ctxt) {
157      b.append(constant);
158    }
159
160  }
161  
162  private enum LiquidFilter {
163    PREPEND;
164    
165    public static LiquidFilter fromCode(String code) {
166      if ("prepend".equals(code)) {
167        return PREPEND;
168      }
169      return null;
170    }
171  }
172
173  private class LiquidExpressionNode {
174    private LiquidFilter filter; // null at root
175    private ExpressionNode expression; // null for some filters
176    public LiquidExpressionNode(LiquidFilter filter, ExpressionNode expression) {
177      super();
178      this.filter = filter;
179      this.expression = expression;
180    }
181    
182  }
183  
184  private class LiquidStatement extends LiquidNode {
185    private String statement;
186    private List<LiquidExpressionNode> compiled = new ArrayList<>();
187
188    @Override
189    public void evaluate(StringBuilder b, Base resource, LiquidEngineContext ctxt) throws FHIRException {
190      if (compiled.size() == 0) {
191        FHIRLexer lexer = new FHIRLexer(statement, "liquid statement", false, true);
192        lexer.setLiquidMode(true);
193        compiled.add(new LiquidExpressionNode(null, engine.parse(lexer)));
194        while (!lexer.done()) {
195          if (lexer.getCurrent().equals("||")) {
196            lexer.next();
197            String f = lexer.getCurrent();
198            LiquidFilter filter = LiquidFilter.fromCode(f);
199            if (filter == null) {
200              lexer.error(engine.getWorker().formatMessage(I18nConstants.LIQUID_UNKNOWN_FILTER, f));
201            }
202            lexer.next();
203            if (!lexer.done() && lexer.getCurrent().equals(":")) {
204              lexer.next();
205              compiled.add(new LiquidExpressionNode(filter, engine.parse(lexer)));
206            } else {
207              compiled.add(new LiquidExpressionNode(filter, null));
208            }
209          } else {
210            lexer.error(engine.getWorker().formatMessage(I18nConstants.LIQUID_UNKNOWN_SYNTAX)); 
211          }
212        }
213      }
214      
215      String t = null;
216      for (LiquidExpressionNode i : compiled) {
217        if (i.filter == null) { // first
218          t = stmtToString(ctxt, engine.evaluate(ctxt, resource, resource, resource, i.expression));
219        } else switch (i.filter) {
220        case PREPEND:
221          t = stmtToString(ctxt, engine.evaluate(ctxt, resource, resource, resource, i.expression)) + t;
222          break;
223        }
224      }
225      b.append(t);
226    }
227
228    private String stmtToString(LiquidEngineContext ctxt, List<Base> items) {
229      StringBuilder b = new StringBuilder();
230      boolean first = true;
231      for (Base i : items) {
232        if (first) first = false; else b.append(", ");
233        String s = renderingSupport != null ? renderingSupport.renderForLiquid(ctxt.externalContext, i) : null;
234        b.append(s != null ? s : engine.convertToString(i));
235      }
236      return b.toString();
237    }
238  }
239
240  private class LiquidElsIf extends LiquidNode {
241    private String condition;
242    private ExpressionNode compiled;
243    private List<LiquidNode> body = new ArrayList<>();
244
245    @Override
246    public void evaluate(StringBuilder b, Base resource, LiquidEngineContext ctxt) throws FHIRException {
247      for (LiquidNode n : body) {
248        n.evaluate(b, resource, ctxt);
249      }
250    }
251  }
252
253  private class LiquidIf extends LiquidNode {
254    private String condition;
255    private ExpressionNode compiled;
256    private List<LiquidNode> thenBody = new ArrayList<>();
257    private List<LiquidElsIf> elseIf = new ArrayList<>();
258    private List<LiquidNode> elseBody = new ArrayList<>();
259
260    @Override
261    public void evaluate(StringBuilder b, Base resource, LiquidEngineContext ctxt) throws FHIRException {
262      if (compiled == null)
263        compiled = engine.parse(condition);
264      boolean ok = engine.evaluateToBoolean(ctxt, resource, resource, resource, compiled);
265      List<LiquidNode> list = null;
266      if (ok) {
267        list = thenBody;
268
269      } else {
270        list = elseBody;
271        for (LiquidElsIf i : elseIf) {
272          if (i.compiled == null)
273            i.compiled = engine.parse(i.condition);
274          ok = engine.evaluateToBoolean(ctxt, resource, resource, resource, i.compiled);
275          if (ok) {
276            list = i.body;
277            break;
278          }
279        }
280      }
281      for (LiquidNode n : list) {
282        n.evaluate(b, resource, ctxt);
283      }
284    }
285  }
286
287  private class LiquidContinueExecuted extends FHIRException {
288    private static final long serialVersionUID = 4748737094188943721L;
289  }
290
291  private class LiquidContinue extends LiquidNode {
292    public void evaluate(StringBuilder b, Base resource, LiquidEngineContext ctxt) throws FHIRException {
293      throw new LiquidContinueExecuted();
294    }
295  }
296
297  private class LiquidBreakExecuted extends FHIRException {
298    private static final long serialVersionUID = 6328496371172871082L;
299  }
300
301  private class LiquidBreak extends LiquidNode {
302    public void evaluate(StringBuilder b, Base resource, LiquidEngineContext ctxt) throws FHIRException {
303      throw new LiquidBreakExecuted();
304    }
305  }
306
307  private class LiquidCycle extends LiquidNode {
308    private List<String> list = new ArrayList<>();
309    private int cursor = 0;
310
311    public void evaluate(StringBuilder b, Base resource, LiquidEngineContext ctxt) throws FHIRException {
312      b.append(list.get(cursor));
313      cursor++;
314      if (cursor == list.size()) {
315        cursor = 0;
316      }
317    }
318  }
319
320  private class LiquidAssign extends LiquidNode {
321    private String varName;
322    private String expression;
323    private ExpressionNode compiled;
324    @Override
325    public void evaluate(StringBuilder b, Base resource, LiquidEngineContext ctxt) throws FHIRException {
326      if (compiled == null) {
327        boolean dbl = engine.isAllowDoubleQuotes();
328        engine.setAllowDoubleQuotes(true);
329        ExpressionNodeWithOffset po = engine.parsePartial(expression, 0);
330        compiled = po.getNode();
331        engine.setAllowDoubleQuotes(dbl);
332      }
333      List<Base> list = engine.evaluate(ctxt, resource, resource, resource, compiled);
334      if (list.isEmpty()) {
335        ctxt.globalVars.remove(varName);
336      } else if (list.size() == 1) {
337        ctxt.globalVars.put(varName, list.get(0));
338      } else {
339        throw new Error("Assign returned a list?");
340      }      
341    }    
342  }
343  
344  private class LiquidFor extends LiquidNode {
345    private String varName;
346    private String condition;
347    private ExpressionNode compiled;
348    private boolean reversed = false;
349    private int limit = -1;
350    private int offset = -1;
351    private List<LiquidNode> body = new ArrayList<>();
352    private List<LiquidNode> elseBody = new ArrayList<>();
353
354    @Override
355    public void evaluate(StringBuilder b, Base resource, LiquidEngineContext ctxt) throws FHIRException {
356      if (compiled == null) {
357        ExpressionNodeWithOffset po = engine.parsePartial(condition, 0);
358        compiled = po.getNode();
359        if (po.getOffset() < condition.length()) {
360          parseModifiers(condition.substring(po.getOffset()));
361        }
362      }
363      List<Base> list = engine.evaluate(ctxt, resource, resource, resource, compiled);
364      LiquidEngineContext lctxt = new LiquidEngineContext(ctxt);
365      if (list.isEmpty()) {
366        for (LiquidNode n : elseBody) {
367          n.evaluate(b, resource, lctxt);
368        }
369      } else {
370        if (reversed) {
371          Collections.reverse(list);
372        }
373        int i = 0;
374        for (Base o : list) {
375          if (offset >= 0 && i < offset) {
376            i++;
377            continue;
378          }
379          if (limit >= 0 && i == limit) {
380            break;
381          }          
382          if (lctxt.globalVars.containsKey(varName)) {
383            throw new FHIRException(engine.getWorker().formatMessage(I18nConstants.LIQUID_VARIABLE_ALREADY_ASSIGNED, varName));
384          }
385          lctxt.loopVars.put(varName, o);
386          boolean wantBreak = false;
387          for (LiquidNode n : body) {
388            try {
389              n.evaluate(b, resource, lctxt);
390            } catch (LiquidContinueExecuted e) {
391              break;
392            } catch (LiquidBreakExecuted e) {
393              wantBreak = true;
394              break;
395            }
396          }
397          if (wantBreak) {
398            break;
399          }
400          i++;
401        }
402      }
403    }
404
405    private void parseModifiers(String cnt) {
406      String src = cnt;
407      while (!Utilities.noString(cnt)) {
408        if (cnt.startsWith("reversed")) {
409          reversed = true;
410          cnt = cnt.substring(8);
411        } else if (cnt.startsWith("limit")) {
412          cnt = cnt.substring(5).trim();
413          if (!cnt.startsWith(":")) {
414            throw new FHIRException(engine.getWorker().formatMessage(I18nConstants.LIQUID_SYNTAX_COLON, src));
415          }
416          cnt = cnt.substring(1).trim();
417          int i = 0;
418          while (i < cnt.length() && Character.isDigit(cnt.charAt(i))) {
419            i++;
420          }
421          if (i == 0) {
422            throw new FHIRException(engine.getWorker().formatMessage(I18nConstants.LIQUID_SYNTAX_NUMBER, src));
423          }
424          limit = Integer.parseInt(cnt.substring(0, i));
425          cnt = cnt.substring(i);
426        } else if (cnt.startsWith("offset")) {
427          cnt = cnt.substring(6).trim();
428          if (!cnt.startsWith(":")) {
429            throw new FHIRException(engine.getWorker().formatMessage(I18nConstants.LIQUID_SYNTAX_COLON, src));
430          }
431          cnt = cnt.substring(1).trim();
432          int i = 0;
433          while (i < cnt.length() && Character.isDigit(cnt.charAt(i))) {
434            i++;
435          }
436          if (i == 0) {
437            throw new FHIRException(engine.getWorker().formatMessage(I18nConstants.LIQUID_SYNTAX_NUMBER, src));
438          }
439          offset = Integer.parseInt(cnt.substring(0, i));
440          cnt = cnt.substring(i);
441        } else {
442          throw new FHIRException(engine.getWorker().formatMessage(I18nConstants.LIQUID_SYNTAX_UNEXPECTED, cnt));
443        }
444      }      
445    }
446  }
447
448  private class LiquidInclude extends LiquidNode {
449    private String page;
450    private Map<String, ExpressionNode> params = new HashMap<>();
451
452    @Override
453    public void evaluate(StringBuilder b, Base resource, LiquidEngineContext ctxt) throws FHIRException {
454      String src = includeResolver.fetchInclude(LiquidEngine.this, page);
455      LiquidParser parser = new LiquidParser(src);
456      LiquidDocument doc = parser.parse(page);
457      LiquidEngineContext nctxt = new LiquidEngineContext(ctxt.externalContext, ctxt);
458      Tuple incl = new Tuple();
459      nctxt.loopVars.put("include", incl);
460      for (String s : params.keySet()) {
461        incl.addProperty(s, engine.evaluate(ctxt, resource, resource, resource, params.get(s)));
462      }
463      for (LiquidNode n : doc.body) {
464        n.evaluate(b, resource, nctxt);
465      }
466    }
467  }
468
469  public static class LiquidDocument {
470    private List<LiquidNode> body = new ArrayList<>();
471
472  }
473
474  private class LiquidParser {
475
476    private String source;
477    private int cursor;
478    private String name;
479
480    public LiquidParser(String source) {
481      this.source = source;
482      cursor = 0;
483    }
484
485    private char next1() {
486      if (cursor >= source.length())
487        return 0;
488      else
489        return source.charAt(cursor);
490    }
491
492    private char next2() {
493      if (cursor >= source.length() - 1)
494        return 0;
495      else
496        return source.charAt(cursor + 1);
497    }
498
499    private char grab() {
500      cursor++;
501      return source.charAt(cursor - 1);
502    }
503
504    public LiquidDocument parse(String name) throws FHIRException {
505      this.name = name;
506      LiquidDocument doc = new LiquidDocument();
507      parseList(doc.body, false, new String[0]);
508      return doc;
509    }
510
511    public LiquidCycle parseCycle(String cnt) {
512      LiquidCycle res = new LiquidCycle();
513      cnt = "," + cnt.substring(5).trim();
514      while (!Utilities.noString(cnt)) {
515        if (!cnt.startsWith(",")) {
516          throw new FHIRException(engine.getWorker().formatMessage(I18nConstants.LIQUID_SYNTAX_EXPECTING, name, cnt.charAt(0), ','));
517        }
518        cnt = cnt.substring(1).trim();
519        if (!cnt.startsWith("\"")) {
520          throw new FHIRException(engine.getWorker().formatMessage(I18nConstants.LIQUID_SYNTAX_EXPECTING, name, cnt.charAt(0), '"'));
521        }
522        cnt = cnt.substring(1);
523        int i = 0;
524        while (i < cnt.length() && cnt.charAt(i) != '"') {
525          i++;
526        }
527        if (i == cnt.length()) {
528          throw new FHIRException(engine.getWorker().formatMessage(I18nConstants.LIQUID_SYNTAX_UNTERMINATED, name));
529        }
530        res.list.add(cnt.substring(0, i));
531        cnt = cnt.substring(i + 1).trim();
532      }
533      return res;
534    }
535
536    private String parseList(List<LiquidNode> list, boolean inLoop, String[] terminators) throws FHIRException {
537      String close = null;
538      while (cursor < source.length()) {
539        if (next1() == '{' && (next2() == '%' || next2() == '{')) {
540          if (next2() == '%') {
541            String cnt = parseTag('%');
542            if (isTerminator(cnt, terminators)) {
543              close = cnt;
544              break;
545            } else if (cnt.startsWith("if "))
546              list.add(parseIf(cnt, inLoop));
547            else if (cnt.startsWith("loop ")) // loop is deprecated, but still
548                                              // supported
549              list.add(parseLoop(cnt.substring(4).trim()));
550            else if (cnt.startsWith("for "))
551              list.add(parseFor(cnt.substring(3).trim()));
552            else if (inLoop && cnt.equals("continue"))
553              list.add(new LiquidContinue());
554            else if (inLoop && cnt.equals("break"))
555              list.add(new LiquidBreak());
556            else if (inLoop && cnt.startsWith("cycle "))
557              list.add(parseCycle(cnt));
558            else if (cnt.startsWith("include "))
559              list.add(parseInclude(cnt.substring(7).trim()));
560            else if (cnt.startsWith("assign "))
561              list.add(parseAssign(cnt.substring(6).trim()));
562            else
563              throw new FHIRException(engine.getWorker().formatMessage(I18nConstants.LIQUID_UNKNOWN_FLOW_STMT,name, cnt));
564          } else { // next2() == '{'
565            list.add(parseStatement());
566          }
567        } else {
568          if (list.size() == 0 || !(list.get(list.size() - 1) instanceof LiquidConstant))
569            list.add(new LiquidConstant());
570          ((LiquidConstant) list.get(list.size() - 1)).addChar(grab());
571        }
572      }
573      for (LiquidNode n : list)
574        n.closeUp();
575      if (terminators.length > 0)
576        if (!isTerminator(close, terminators))
577          throw new FHIRException(engine.getWorker().formatMessage(I18nConstants.LIQUID_UNKNOWN_NOEND, name, terminators));
578      return close;
579    }
580
581    private boolean isTerminator(String cnt, String[] terminators) {
582      if (Utilities.noString(cnt)) {
583        return false;
584      }
585      for (String t : terminators) {
586        if (t.endsWith(" ")) {
587          if (cnt.startsWith(t)) {
588            return true;
589          }
590        } else {
591          if (cnt.equals(t)) {
592            return true;
593          }
594        }
595      }
596      return false;
597    }
598
599    private LiquidNode parseIf(String cnt, boolean inLoop) throws FHIRException {
600      LiquidIf res = new LiquidIf();
601      res.condition = cnt.substring(3).trim();
602      String term = parseList(res.thenBody, inLoop, new String[] { "else", "elsif ", "endif" });
603      while (term.startsWith("elsif ")) {
604        LiquidElsIf elsIf = new LiquidElsIf();
605        res.elseIf.add(elsIf);
606        elsIf.condition = term.substring(5).trim();
607        term = parseList(elsIf.body, inLoop, new String[] { "elsif ", "else", "endif" });
608      }
609      if ("else".equals(term)) {
610        term = parseList(res.elseBody, inLoop, new String[] { "endif" });
611      }
612
613      return res;
614    }
615
616    private LiquidNode parseInclude(String cnt) throws FHIRException {
617      int i = 1;
618      while (i < cnt.length() && !Character.isWhitespace(cnt.charAt(i)))
619        i++;
620      if (i == cnt.length() || i == 0)
621        throw new FHIRException(engine.getWorker().formatMessage(I18nConstants.LIQUID_SYNTAX_INCLUDE, name + ": Error reading include: " + cnt));
622      LiquidInclude res = new LiquidInclude();
623      res.page = cnt.substring(0, i);
624      while (i < cnt.length() && Character.isWhitespace(cnt.charAt(i)))
625        i++;
626      while (i < cnt.length()) {
627        int j = i;
628        while (i < cnt.length() && cnt.charAt(i) != '=')
629          i++;
630        if (i >= cnt.length() || j == i)
631          throw new FHIRException(engine.getWorker().formatMessage(I18nConstants.LIQUID_SYNTAX_INCLUDE, name,  cnt));
632        String n = cnt.substring(j, i);
633        if (res.params.containsKey(n))
634          throw new FHIRException(engine.getWorker().formatMessage(I18nConstants.LIQUID_SYNTAX_INCLUDE, name,  cnt));
635        i++;
636        ExpressionNodeWithOffset t = engine.parsePartial(cnt, i);
637        i = t.getOffset();
638        res.params.put(n, t.getNode());
639        while (i < cnt.length() && Character.isWhitespace(cnt.charAt(i)))
640          i++;
641      }
642      return res;
643    }
644
645    private LiquidNode parseLoop(String cnt) throws FHIRException {
646      int i = 0;
647      while (!Character.isWhitespace(cnt.charAt(i)))
648        i++;
649      LiquidFor res = new LiquidFor();
650      res.varName = cnt.substring(0, i);
651      if ("include".equals(res.varName)) {
652        throw new FHIRException(engine.getWorker().formatMessage(I18nConstants.LIQUID_VARIABLE_ILLEGAL, res.varName));
653      }
654      while (Character.isWhitespace(cnt.charAt(i)))
655        i++;
656      int j = i;
657      while (!Character.isWhitespace(cnt.charAt(i)))
658        i++;
659      if (!"in".equals(cnt.substring(j, i)))
660        throw new FHIRException(engine.getWorker().formatMessage(I18nConstants.LIQUID_SYNTAX_LOOP, name, cnt));
661      res.condition = cnt.substring(i).trim();
662      parseList(res.body, false, new String[] { "endloop" });
663      return res;
664    }
665
666    private LiquidNode parseFor(String cnt) throws FHIRException {
667      int i = 0;
668      while (!Character.isWhitespace(cnt.charAt(i)))
669        i++;
670      LiquidFor res = new LiquidFor();
671      res.varName = cnt.substring(0, i);
672      if ("include".equals(res.varName)) {
673        throw new FHIRException(engine.getWorker().formatMessage(I18nConstants.LIQUID_VARIABLE_ILLEGAL, res.varName));
674      }
675      while (Character.isWhitespace(cnt.charAt(i)))
676        i++;
677      int j = i;
678      while (!Character.isWhitespace(cnt.charAt(i)))
679        i++;
680      if (!"in".equals(cnt.substring(j, i)))
681        throw new FHIRException(engine.getWorker().formatMessage(I18nConstants.LIQUID_SYNTAX_LOOP, name, cnt));
682      res.condition = cnt.substring(i).trim();
683      String term = parseList(res.body, true, new String[] { "endfor", "else" });
684      if ("else".equals(term)) {
685        parseList(res.elseBody, false, new String[] { "endfor" });
686      }
687      return res;
688    }
689
690    private LiquidNode parseAssign(String cnt) throws FHIRException {
691      int i = 0;
692      while (!Character.isWhitespace(cnt.charAt(i)))
693        i++;
694      LiquidAssign res = new LiquidAssign();
695      res.varName = cnt.substring(0, i);
696      while (Character.isWhitespace(cnt.charAt(i)))
697        i++;
698      int j = i;
699      while (!Character.isWhitespace(cnt.charAt(i)))
700        i++;
701      res.expression = cnt.substring(i).trim();
702      return res;
703    }
704
705    private String parseTag(char ch) throws FHIRException {
706      grab();
707      grab();
708      StringBuilder b = new StringBuilder();
709      while (cursor < source.length() && !(next1() == '%' && next2() == '}')) {
710        b.append(grab());
711      }
712      if (!(next1() == '%' && next2() == '}'))
713        throw new FHIRException(engine.getWorker().formatMessage(I18nConstants.LIQUID_SYNTAX_NOTERM, name,  "{% " + b.toString()));
714      grab();
715      grab();
716      return b.toString().trim();
717    }
718
719    private LiquidStatement parseStatement() throws FHIRException {
720      grab();
721      grab();
722      StringBuilder b = new StringBuilder();
723      while (cursor < source.length() && !(next1() == '}' && next2() == '}')) {
724        b.append(grab());
725      }
726      if (!(next1() == '}' && next2() == '}'))
727        throw new FHIRException(engine.getWorker().formatMessage(I18nConstants.LIQUID_SYNTAX_NOTERM, name,  "{{ " + b.toString()));
728      grab();
729      grab();
730      LiquidStatement res = new LiquidStatement();
731      res.statement = b.toString().trim();
732      return res;
733    }
734
735  }
736
737  @Override
738  public List<Base> resolveConstant(Object appContext, String name, boolean beforeContext) throws PathEngineException {
739    LiquidEngineContext ctxt = (LiquidEngineContext) appContext;
740    if (ctxt.loopVars.containsKey(name))
741      return new ArrayList<Base>(Arrays.asList(ctxt.loopVars.get(name)));
742    if (ctxt.globalVars.containsKey(name))
743      return new ArrayList<Base>(Arrays.asList(ctxt.globalVars.get(name)));
744    if (externalHostServices == null)
745      return new ArrayList<Base>();
746    return externalHostServices.resolveConstant(ctxt.externalContext, name, beforeContext);
747  }
748
749  @Override
750  public TypeDetails resolveConstantType(Object appContext, String name) throws PathEngineException {
751    if (externalHostServices == null)
752      return null;
753    LiquidEngineContext ctxt = (LiquidEngineContext) appContext;
754    return externalHostServices.resolveConstantType(ctxt.externalContext, name);
755  }
756
757  @Override
758  public boolean log(String argument, List<Base> focus) {
759    if (externalHostServices == null)
760      return false;
761    return externalHostServices.log(argument, focus);
762  }
763
764  @Override
765  public FunctionDetails resolveFunction(String functionName) {
766    if (externalHostServices == null)
767      return null;
768    return externalHostServices.resolveFunction(functionName);
769  }
770
771  @Override
772  public TypeDetails checkFunction(Object appContext, String functionName, List<TypeDetails> parameters) throws PathEngineException {
773    if (externalHostServices == null)
774      return null;
775    LiquidEngineContext ctxt = (LiquidEngineContext) appContext;
776    return externalHostServices.checkFunction(ctxt.externalContext, functionName, parameters);
777  }
778
779  @Override
780  public List<Base> executeFunction(Object appContext, List<Base> focus, String functionName, List<List<Base>> parameters) {
781    if (externalHostServices == null)
782      return null;
783    LiquidEngineContext ctxt = (LiquidEngineContext) appContext;
784    return externalHostServices.executeFunction(ctxt.externalContext, focus, functionName, parameters);
785  }
786
787  @Override
788  public Base resolveReference(Object appContext, String url, Base refContext) throws FHIRException {
789    if (externalHostServices == null)
790      return null;
791    LiquidEngineContext ctxt = (LiquidEngineContext) appContext;
792    return resolveReference(ctxt.externalContext, url, refContext);
793  }
794
795  @Override
796  public boolean conformsToProfile(Object appContext, Base item, String url) throws FHIRException {
797    if (externalHostServices == null)
798      return false;
799    LiquidEngineContext ctxt = (LiquidEngineContext) appContext;
800    return conformsToProfile(ctxt.externalContext, item, url);
801  }
802
803  @Override
804  public ValueSet resolveValueSet(Object appContext, String url) {
805    LiquidEngineContext ctxt = (LiquidEngineContext) appContext;
806    if (externalHostServices != null)
807      return externalHostServices.resolveValueSet(ctxt.externalContext, url);
808    else
809      return engine.getWorker().fetchResource(ValueSet.class, url);
810  }
811
812  /**
813   * Lightweight method to replace fixed constants in resources
814   * 
815   * @param node
816   * @param vars
817   * @return
818   */
819  public boolean replaceInHtml(XhtmlNode node, Map<String, String> vars) {
820    boolean replaced = false;
821    if (node.getNodeType() == NodeType.Text || node.getNodeType() == NodeType.Comment) {
822      String cnt = node.getContent();
823      for (String n : vars.keySet()) {
824        cnt = cnt.replace(n, vars.get(n));
825      }
826      if (!cnt.equals(node.getContent())) {
827        node.setContent(cnt);
828        replaced = true;
829      }
830    } else if (node.getNodeType() == NodeType.Element || node.getNodeType() == NodeType.Document) {
831      for (XhtmlNode c : node.getChildNodes()) {
832        if (replaceInHtml(c, vars)) {
833          replaced = true;
834        }
835      }
836      for (String an : node.getAttributes().keySet()) {
837        String cnt = node.getAttributes().get(an);
838        for (String n : vars.keySet()) {
839          cnt = cnt.replace(n, vars.get(n));
840        }
841        if (!cnt.equals(node.getAttributes().get(an))) {
842          node.getAttributes().put(an, cnt);
843          replaced = true;
844        }
845      }
846    }
847    return replaced;
848  }
849
850}